Refactor store

This commit is contained in:
Andras Schmelczer 2019-09-06 20:44:29 +02:00
parent 952a3cd34c
commit 9933f4f9ff
15 changed files with 208 additions and 287 deletions

View file

@ -1,4 +1,6 @@
import { Component } from '@angular/core';
import { InnerNode, InnerNodeState } from './store/inner-node';
import { Root } from './store/root';
@Component({
selector: 'app-root',
@ -6,5 +8,26 @@ import { Component } from '@angular/core';
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'frontend';
title = 'life';
/* tests
constructor() {
const root = new Root<InnerNode>();
root.log();
const l = new InnerNode();
const r = new InnerNode();
root.addChildren([l, r]);
root.log();
const rl = new InnerNode();
const rr = new InnerNode();
r.addChildren([rl, rr]);
root.log();
rr.changeKeys<InnerNodeState>({ dummy: 8 });
root.log();
}
*/
}

View file

@ -23,6 +23,8 @@ import { RemoveBlockComponent } from './components/modal/modals/remove-block/rem
import { ToggleComponent } from './components/shared/toggle/toggle.component';
import { TasksComponent } from './components/pages/page/tower/tasks/tasks.component';
import { ColorPipe } from './pipes/color.pipe';
import { Root } from './store/root';
import { InnerNode, InnerNodeState } from './store/inner-node';
@NgModule({
declarations: [

View file

@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core';
import { Block } from '../../../../../model/block';
import { Block, BlockState } from '../../../../../model/block';
import { ModalService } from '../../../../../services/modal.service';
import { ColoredBlock, Tower } from '../../../../../model/tower';
@ -23,7 +23,7 @@ export class BlockComponent {
isDone: this.block.isDone
});
console.log(description);
this.block.changeProperties({
this.block.changeKeys<BlockState>({
tag: selected,
description,
isDone

View file

@ -1,5 +1,5 @@
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { Block } from '../../../../../model/block';
import { Block, BlockState } from '../../../../../model/block';
import { Tower } from '../../../../../model/tower';
import { ModalService } from '../../../../../services/modal.service';
import { CancelService } from '../../../../../services/cancel.service';
@ -55,7 +55,7 @@ export class TasksComponent implements OnInit {
change.created = new Date();
}
block.changeProperties(change);
block.changeKeys<BlockState>(change);
} catch {
// pass
}

View file

@ -1,30 +1,21 @@
import { Serializable } from './serializable';
import { IBlock } from '../interfaces/persistance/block';
import { Node } from '../store/node';
import { InnerNodeState } from '../store/inner-node';
export class Block extends Serializable implements IBlock {
constructor(parent: Node, props: IBlock) {
super(parent, props, 'Block');
this.onAfterClone();
}
export interface BlockState extends IBlock, InnerNodeState {}
protected onAfterClone(): void {
if (this.created.constructor.name !== 'Date') {
this.created = new Date(this.created);
}
// TODO: remove.
if (this.isDone === null || this.isDone === undefined) {
this.isDone = false;
}
}
changeProperties(values: Partial<IBlock>) {
this.changeKeys(values);
}
created: Date;
isDone: boolean;
export class Block extends Serializable implements IBlock, BlockState {
readonly created: Date;
readonly isDone: boolean;
readonly description: string;
readonly tag: string;
constructor(props: IBlock) {
console.log('b');
if (props.created.constructor.name !== 'Date') {
props.created = new Date(props.created);
}
super(props, 'Block');
}
}

View file

@ -1,17 +1,12 @@
import { Serializable } from './serializable';
import { IPage } from '../interfaces/persistance/page';
import { Tower } from './tower';
import { Node } from '../store/node';
import { InnerNodeState } from '../store/inner-node';
export class Page extends Serializable implements IPage {
constructor(parent: Node, props: IPage) {
super(parent, props, 'Page');
}
export interface PageState extends InnerNodeState, IPage {}
export class Page extends Serializable implements IPage, PageState {
readonly name: string;
get towers(): Array<Tower> {
return this.children as Array<Tower>;
}
readonly userData: {
hideCreateTowerButton: boolean;
@ -21,10 +16,17 @@ export class Page extends Serializable implements IPage {
};
};
constructor(props: IPage) {
super(props, 'Page');
}
get towers(): Array<Tower> {
return this.children as Array<Tower>;
}
setHideCreateTowerButton(value: boolean) {
this.changeKey({
propertyName: 'userData',
value: {
this.changeKeys<PageState>({
userData: {
...this.userData,
hideCreateTowerButton: value
}
@ -41,9 +43,8 @@ export class Page extends Serializable implements IPage {
towers.splice(previousIndex, 1);
towers.splice(currentIndex, 0, tower);
this.changeValue({
oldValue: this.towers,
newValue: towers
this.changeKeys<PageState>({
children: towers
});
}
@ -53,17 +54,18 @@ export class Page extends Serializable implements IPage {
hue = Math.random() * 360;
} while (30 <= hue && hue <= 200);
new Tower(this, {
name,
blocks: [],
baseColor: { h: hue, s: 100, l: 50 }
});
this.addChildren([
new Tower({
name,
blocks: [],
baseColor: { h: hue, s: 100, l: 50 }
})
]);
}
removeTower(tower: Tower) {
this.changeValue({
oldValue: this.towers,
newValue: this.towers.filter(t => t !== tower)
this.changeKeys<PageState>({
towers: this.towers.filter(t => t !== tower)
});
}
}

View file

@ -1,10 +1,6 @@
import { Cloneable } from '../store/cloneable';
import { Node } from '../store/node';
import { InnerNode } from '../store/inner-node';
export class Serializable extends Cloneable {
protected type: string;
private static propertyList: any = {};
export class Serializable extends InnerNode {
static childrenMap: {
[type: string]: {
childrenConstructor: typeof Serializable;
@ -13,12 +9,11 @@ export class Serializable extends Cloneable {
};
};
protected onAfterClone(): void {
// pass
}
private static propertyList: any = {};
protected type: string;
protected constructor(parent: Node, properties: any, type: string) {
super(parent);
protected constructor(properties: any, type: string) {
super();
const compiledType = this.constructor.name;
if (!Serializable.propertyList.hasOwnProperty(compiledType)) {
@ -28,16 +23,15 @@ export class Serializable extends Cloneable {
if (properties.hasOwnProperty(property)) {
const propertyValue = properties[property];
// This should be ran after the original constructor has finished.
console.log(type);
if (property === Serializable.childrenMap[type].childrenListName) {
new Promise(r => r()).then(() => {
for (let child of propertyValue) {
new Serializable.childrenMap[type].childrenConstructor(
this,
child,
Serializable.childrenMap[type].childrenType
);
}
const children = propertyValue.map(
c =>
new Serializable.childrenMap[type].childrenConstructor(c, Serializable.childrenMap[type].childrenType)
);
console.log(type, 'created');
this.addChildren(children);
console.log(type, 'added');
});
} else {
this[property] = properties[property];

View file

@ -3,16 +3,16 @@ import { lighten } from '../utils/color';
import { Block } from './block';
import { Serializable } from './serializable';
import { hash } from '../utils/hash';
import { Node } from '../store/node';
import { IColor } from '../interfaces/color';
import { InnerNodeState } from '../store/inner-node';
export type ColoredBlock = Block & { color: IColor };
export class Tower extends Serializable implements ITower {
protected type = 'Tower';
export interface TowerState extends ITower, InnerNodeState {}
export class Tower extends Serializable implements ITower, TowerState {
tags: string[];
name: string;
readonly name: string;
get blocks(): Array<Block> {
return this.children as Array<Block>;
@ -22,12 +22,11 @@ export class Tower extends Serializable implements ITower {
readonly baseColor: IColor;
constructor(parent: Node, props: ITower) {
super(parent, props, 'Tower');
this.onAfterClone();
constructor(props: ITower) {
super(props, 'Tower');
}
protected onAfterClone(): void {
protected onAfterClone() {
this.blocks.sort((a, b) => {
return a.created.getTime() - b.created.getTime();
});
@ -47,15 +46,15 @@ export class Tower extends Serializable implements ITower {
}
addBlock(props: { tag: string; description: string; isDone: boolean }) {
new Block(this, {
created: new Date(),
...props
});
this.addChildren([
new Block({
created: new Date(),
...props
})
]);
}
changeName(newName: string) {
// For optimization purposes.
this.name = newName;
this.mutatedUpdate();
changeName(name: string) {
this.changeKeys<TowerState>({ name });
}
}

View file

@ -13,10 +13,6 @@ import { Observable } from 'rxjs/internal/Observable';
providedIn: 'root'
})
export class DataService extends Root<Page> {
get pages(): Array<Page> {
return this.children;
}
private readonly _safeChildren: BehaviorSubject<Array<Page>> = new BehaviorSubject(null);
readonly safeChildren$: Observable<Array<Page>> = this._safeChildren.asObservable();
@ -25,6 +21,30 @@ export class DataService extends Root<Page> {
this.init().catch();
}
get pages(): Array<Page> {
return this.children;
}
save(timeout: number) {
this.storeService.scheduleSave(this.pages, timeout);
}
addPage(name: string) {
const page = new Page({
name,
userData: {},
towers: []
});
this.addChildren([page]);
page.addTower();
}
removePage(page: Page) {
this.changeKeys<any>({
children: this.children.filter(c => c !== page)
});
}
private async init() {
const pages = await this.storeService.load();
Serializable.childrenMap = {
@ -44,43 +64,15 @@ export class DataService extends Root<Page> {
childrenType: null
}
};
this.children$.subscribe(value => {
this.log();
});
for (let page of pages) {
new Page(this, page);
}
setTimeout(() => {
this.children$.subscribe(value => {
this.log();
});
}, 0);
this.addChildren(pages.map(p => new Page(p)));
this.children$.subscribe(value => {
this._safeChildren.next(value);
this.save(0);
});
}
mutatedUpdate() {
this.save(2500);
}
save(timeout: number) {
this.storeService.scheduleSave(this.pages, timeout);
}
addPage(name: string) {
const page = new Page(this, {
name,
userData: {},
towers: []
});
page.addTower();
}
removePage(page: Page) {
this.changeValue({
oldValue: this.children,
newValue: this.children.filter(c => c !== page)
});
}
}

View file

@ -95,7 +95,7 @@ export class StoreService<T> {
constructor() {
const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY);
this.storedData = JSON.parse(localStorageData ? localStorageData : this.mockData) as T;
this.storedData = JSON.parse(false ? localStorageData : this.mockData) as T;
}
scheduleSave(data: T, timeout: number) {

View file

@ -1,102 +0,0 @@
import { InnerNode } from './inner-node';
import { Node } from './node';
export abstract class Cloneable extends InnerNode {
protected constructor(parent: Node) {
super(parent);
}
protected abstract onAfterClone(): void;
protected cloneWithMap(map: (node: this) => void): this {
const insides = Object.getOwnPropertyDescriptors(this);
const insidesProxy = new Proxy(insides, {
get: (target, prop, proxy) => {
if (prop == '__target__') {
return target;
}
if (target.hasOwnProperty(prop)) {
const value = target[prop as string].value;
if (typeof value === 'function') {
return value.bind(proxy);
}
return value;
} else if (target.prototype.hasOwnProperty(prop)) {
const value = target.prototype[prop];
if (typeof value === 'function') {
return value.bind(proxy);
}
return value;
}
},
set: (target, prop, value) => {
return (target[prop as string].value = value);
}
});
map(<any>insidesProxy);
return this.cloneFromInsides(<any>insidesProxy.__target__);
}
protected cloneWithAdd({ propertyName, value }: { value: any; propertyName: string }): this {
if (this[propertyName] === value) {
return this;
}
const insides = Object.getOwnPropertyDescriptors(this);
insides[propertyName].value = value;
return this.cloneFromInsides(insides);
}
protected cloneWithChangedKeys(props: { [propertyName: string]: any }): this {
const insides = Object.getOwnPropertyDescriptors(this);
for (let key in props) {
if (props.hasOwnProperty(key)) {
if (insides.hasOwnProperty(key)) {
insides[key].value = props[key];
} else {
// @ts-ignore
insides[key] = {
value: props[key]
};
}
}
}
return this.cloneFromInsides(insides);
}
protected cloneWithModify({ oldValue, newValue }: { oldValue: any; newValue: any }): this {
if (oldValue === newValue) {
return this;
}
const insides = Object.getOwnPropertyDescriptors(this);
let wasMatch = false;
for (let name in insides) {
if (insides.hasOwnProperty(name) && insides[name].value === oldValue) {
insides[name].value = newValue;
wasMatch = true;
}
}
if (!wasMatch) {
throw new TypeError(`Object has no property with value: ${oldValue.toString()}`);
}
return this.cloneFromInsides(insides);
}
private cloneFromInsides(insides): this {
insides.id.value = Node.id++;
insides.copyCount.value++;
Node.sumCopyCount++;
const clone = Object.create(Object.getPrototypeOf(this), insides);
clone.onAfterClone();
return clone;
}
}

View file

@ -1,9 +1,18 @@
import { Node } from './node';
import { Node, NodeState } from './node';
export abstract class InnerNode extends Node {
readonly children: Array<InnerNode> = [];
protected parent: Node;
export interface InnerNodeState extends NodeState {
dummy: any;
}
export class InnerNode extends Node implements InnerNodeState {
readonly dummy = 3;
parent: Node;
private nextVersion: this = null;
readonly children: Array<InnerNode> = [];
constructor() {
super();
}
get latestVersion(): this {
let version;
@ -13,40 +22,34 @@ export abstract class InnerNode extends Node {
return version;
}
mutatedUpdate() {
this.parent.mutatedUpdate();
addChildren(children: Array<InnerNode>) {
super.addChildren.call(this.latestVersion, children);
}
map(map: (a: this) => void) {
return this.update((self: this) => this.cloneWithMap.call(self, map));
}
changeKeys(props: { [propertyName: string]: any }): this {
return this.update((self: this) => this.cloneWithChangedKeys.call(self, props));
}
addChild(update: { child: InnerNode }) {
super.addChild.call(this.latestVersion, update);
}
changeChild(update: { oldValue: InnerNode; newValue: InnerNode }) {
replaceChild(update: { oldValue: InnerNode; newValue: InnerNode }) {
super.replaceChild.call(this.latestVersion, update);
}
protected abstract cloneWithMap(map: (a: this) => void): this;
protected abstract cloneWithChangedKeys(props: { [propertyName: string]: any }): this;
private update(cloneMethod: (self: this) => this): this {
changeKeys<T extends NodeState>(props: Partial<T>): this {
if (this.nextVersion !== null) {
this.latestVersion.update(cloneMethod);
this.latestVersion.changeKeys(props);
}
const clone = cloneMethod(this);
if (clone === this) {
return this;
const clone = this.cloneWithChangedKeys(props);
let shouldClone = false;
for (const prop in props) {
// @ts-ignore
if (props.hasOwnProperty(prop) && props[prop] !== this[prop]) {
shouldClone = true;
break;
}
}
if (!shouldClone) {
return;
}
for (let child of clone.children) {
for (const child of clone.children) {
child.parent = clone;
}
@ -58,4 +61,28 @@ export abstract class InnerNode extends Node {
this.nextVersion = clone;
return clone;
}
protected onAfterClone() {}
protected cloneWithChangedKeys<T extends NodeState>(props: Partial<T>): this {
const insides = Object.getOwnPropertyDescriptors(this);
for (const key in props) {
if (props.hasOwnProperty(key)) {
if (insides.hasOwnProperty(key)) {
insides[key].value = props[key];
} else {
// @ts-ignore
insides[key] = {
value: props[key]
};
}
}
}
const clone = Object.create(Object.getPrototypeOf(this), insides);
clone.initiate();
clone.onAfterClone();
return clone;
}
}

View file

@ -1,22 +1,24 @@
import { InnerNode } from './inner-node';
import { Unique } from './unique';
import { InnerNode } from './inner-node';
export abstract class Node extends Unique {
readonly children: Array<InnerNode>;
// TODO: fix types.
protected abstract changeKeys(props: any): this;
abstract mutatedUpdate(): void;
export interface NodeState {
children: Array<InnerNode>;
}
private copyCount = 0;
export abstract class Node extends Unique implements NodeState {
protected copyCount = 1;
abstract readonly children: Array<InnerNode>;
protected abstract changeKeys<T extends NodeState>(props: Partial<T>): this;
protected initiate() {
super.initiate();
this.copyCount++;
++this.copyCount;
}
addChild({ child }: { child: InnerNode }) {
this.changeKeys({
children: [...this.children, child]
addChildren(children: Array<InnerNode>) {
this.changeKeys<NodeState>({
children: [...this.children, ...children]
});
}
@ -25,7 +27,7 @@ export abstract class Node extends Unique {
return;
}
this.changeKeys({
this.changeKeys<NodeState>({
children: this.children.map(c => (c === oldValue ? newValue : c))
});
}
@ -33,7 +35,7 @@ export abstract class Node extends Unique {
protected _log(indent = ''): string {
const basicInfo = `${indent} - ${this.constructor.name}, #${this.id}`;
let response = `${basicInfo}${' '.repeat(25 - basicInfo.length)}siblings: ${this.copyCount}\n`;
for (let c of this.children) {
for (const c of this.children) {
response += `${c._log(indent + ' ')}`;
}
return response;

View file

@ -1,12 +1,16 @@
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { Observable } from 'rxjs/internal/Observable';
import { Node } from './node';
import { Node, NodeState } from './node';
import { InnerNode } from './inner-node';
export class Root<T extends InnerNode> extends Node {
private readonly _children: BehaviorSubject<Array<T>> = new BehaviorSubject([]);
readonly children$: Observable<Array<T>> = this._children.asObservable();
constructor() {
super();
}
get children(): Array<T> {
return this._children.getValue();
}
@ -15,27 +19,14 @@ export class Root<T extends InnerNode> extends Node {
this._children.next(value);
}
mutatedUpdate() {
// pass
}
changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }) {
if (this.children !== oldValue) {
throw new TypeError('Only children can be changed.');
}
this.children = newValue;
for (let child of this.children) {
child.parent = this;
}
}
changeKey({ propertyName, value }: { propertyName: string; value: any }) {
if (propertyName !== 'children') {
throw new TypeError('Only children can be changed.');
}
this.children = value;
for (let child of this.children) {
child.parent = this;
changeKeys<U extends NodeState>(props: Partial<U>): this {
if (props.hasOwnProperty('children')) {
// @ts-ignore
this.children = props.children;
for (const child of this.children) {
child.parent = this;
}
}
return this;
}
}

View file

@ -6,10 +6,10 @@ export abstract class Unique extends Initiable {
return Unique.nextId;
}
private _id: number;
get id(): number {
return this._id;
}
private _id: number;
protected initiate() {
this._id = Unique.nextId++;