Refactor store

This commit is contained in:
Andras Schmelczer 2019-09-06 20:44:29 +02:00
commit 9933f4f9ff
15 changed files with 207 additions and 286 deletions

View file

@ -1,4 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { InnerNode, InnerNodeState } from './store/inner-node';
import { Root } from './store/root';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -6,5 +8,26 @@ import { Component } from '@angular/core';
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent { 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 { ToggleComponent } from './components/shared/toggle/toggle.component';
import { TasksComponent } from './components/pages/page/tower/tasks/tasks.component'; import { TasksComponent } from './components/pages/page/tower/tasks/tasks.component';
import { ColorPipe } from './pipes/color.pipe'; import { ColorPipe } from './pipes/color.pipe';
import { Root } from './store/root';
import { InnerNode, InnerNodeState } from './store/inner-node';
@NgModule({ @NgModule({
declarations: [ declarations: [

View file

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

View file

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

View file

@ -1,30 +1,21 @@
import { Serializable } from './serializable'; import { Serializable } from './serializable';
import { IBlock } from '../interfaces/persistance/block'; import { IBlock } from '../interfaces/persistance/block';
import { Node } from '../store/node'; import { Node } from '../store/node';
import { InnerNodeState } from '../store/inner-node';
export class Block extends Serializable implements IBlock { export interface BlockState extends IBlock, InnerNodeState {}
constructor(parent: Node, props: IBlock) {
super(parent, props, 'Block');
this.onAfterClone();
}
protected onAfterClone(): void { export class Block extends Serializable implements IBlock, BlockState {
if (this.created.constructor.name !== 'Date') { readonly created: Date;
this.created = new Date(this.created); readonly isDone: boolean;
}
// TODO: remove.
if (this.isDone === null || this.isDone === undefined) {
this.isDone = false;
}
}
changeProperties(values: Partial<IBlock>) {
this.changeKeys(values);
}
created: Date;
isDone: boolean;
readonly description: string; readonly description: string;
readonly tag: 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 { Serializable } from './serializable';
import { IPage } from '../interfaces/persistance/page'; import { IPage } from '../interfaces/persistance/page';
import { Tower } from './tower'; import { Tower } from './tower';
import { Node } from '../store/node'; import { InnerNodeState } from '../store/inner-node';
export class Page extends Serializable implements IPage { export interface PageState extends InnerNodeState, IPage {}
constructor(parent: Node, props: IPage) {
super(parent, props, 'Page');
}
export class Page extends Serializable implements IPage, PageState {
readonly name: string; readonly name: string;
get towers(): Array<Tower> {
return this.children as Array<Tower>;
}
readonly userData: { readonly userData: {
hideCreateTowerButton: boolean; 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) { setHideCreateTowerButton(value: boolean) {
this.changeKey({ this.changeKeys<PageState>({
propertyName: 'userData', userData: {
value: {
...this.userData, ...this.userData,
hideCreateTowerButton: value hideCreateTowerButton: value
} }
@ -41,9 +43,8 @@ export class Page extends Serializable implements IPage {
towers.splice(previousIndex, 1); towers.splice(previousIndex, 1);
towers.splice(currentIndex, 0, tower); towers.splice(currentIndex, 0, tower);
this.changeValue({ this.changeKeys<PageState>({
oldValue: this.towers, children: towers
newValue: towers
}); });
} }
@ -53,17 +54,18 @@ export class Page extends Serializable implements IPage {
hue = Math.random() * 360; hue = Math.random() * 360;
} while (30 <= hue && hue <= 200); } while (30 <= hue && hue <= 200);
new Tower(this, { this.addChildren([
name, new Tower({
blocks: [], name,
baseColor: { h: hue, s: 100, l: 50 } blocks: [],
}); baseColor: { h: hue, s: 100, l: 50 }
})
]);
} }
removeTower(tower: Tower) { removeTower(tower: Tower) {
this.changeValue({ this.changeKeys<PageState>({
oldValue: this.towers, towers: this.towers.filter(t => t !== tower)
newValue: this.towers.filter(t => t !== tower)
}); });
} }
} }

View file

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

View file

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

View file

@ -13,10 +13,6 @@ import { Observable } from 'rxjs/internal/Observable';
providedIn: 'root' providedIn: 'root'
}) })
export class DataService extends Root<Page> { export class DataService extends Root<Page> {
get pages(): Array<Page> {
return this.children;
}
private readonly _safeChildren: BehaviorSubject<Array<Page>> = new BehaviorSubject(null); private readonly _safeChildren: BehaviorSubject<Array<Page>> = new BehaviorSubject(null);
readonly safeChildren$: Observable<Array<Page>> = this._safeChildren.asObservable(); readonly safeChildren$: Observable<Array<Page>> = this._safeChildren.asObservable();
@ -25,6 +21,30 @@ export class DataService extends Root<Page> {
this.init().catch(); 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() { private async init() {
const pages = await this.storeService.load(); const pages = await this.storeService.load();
Serializable.childrenMap = { Serializable.childrenMap = {
@ -44,43 +64,15 @@ export class DataService extends Root<Page> {
childrenType: null childrenType: null
} }
}; };
this.children$.subscribe(value => {
this.log();
});
for (let page of pages) { this.addChildren(pages.map(p => new Page(p)));
new Page(this, page);
}
setTimeout(() => {
this.children$.subscribe(value => {
this.log();
});
}, 0);
this.children$.subscribe(value => { this.children$.subscribe(value => {
this._safeChildren.next(value); this._safeChildren.next(value);
this.save(0); 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() { constructor() {
const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY); 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) { 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 { export interface InnerNodeState extends NodeState {
readonly children: Array<InnerNode> = []; dummy: any;
protected parent: Node; }
export class InnerNode extends Node implements InnerNodeState {
readonly dummy = 3;
parent: Node;
private nextVersion: this = null; private nextVersion: this = null;
readonly children: Array<InnerNode> = [];
constructor() {
super();
}
get latestVersion(): this { get latestVersion(): this {
let version; let version;
@ -13,40 +22,34 @@ export abstract class InnerNode extends Node {
return version; return version;
} }
mutatedUpdate() { addChildren(children: Array<InnerNode>) {
this.parent.mutatedUpdate(); super.addChildren.call(this.latestVersion, children);
} }
map(map: (a: this) => void) { replaceChild(update: { oldValue: InnerNode; newValue: InnerNode }) {
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 }) {
super.replaceChild.call(this.latestVersion, update); super.replaceChild.call(this.latestVersion, update);
} }
protected abstract cloneWithMap(map: (a: this) => void): this; changeKeys<T extends NodeState>(props: Partial<T>): this {
protected abstract cloneWithChangedKeys(props: { [propertyName: string]: any }): this;
private update(cloneMethod: (self: this) => this): this {
if (this.nextVersion !== null) { if (this.nextVersion !== null) {
this.latestVersion.update(cloneMethod); this.latestVersion.changeKeys(props);
} }
const clone = cloneMethod(this); const clone = this.cloneWithChangedKeys(props);
if (clone === this) {
return this; 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; child.parent = clone;
} }
@ -58,4 +61,28 @@ export abstract class InnerNode extends Node {
this.nextVersion = clone; this.nextVersion = clone;
return 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 { Unique } from './unique';
import { InnerNode } from './inner-node';
export abstract class Node extends Unique { export interface NodeState {
readonly children: Array<InnerNode>; children: Array<InnerNode>;
// TODO: fix types. }
protected abstract changeKeys(props: any): this;
abstract mutatedUpdate(): void;
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() { protected initiate() {
super.initiate(); super.initiate();
this.copyCount++; ++this.copyCount;
} }
addChild({ child }: { child: InnerNode }) { addChildren(children: Array<InnerNode>) {
this.changeKeys({ this.changeKeys<NodeState>({
children: [...this.children, child] children: [...this.children, ...children]
}); });
} }
@ -25,7 +27,7 @@ export abstract class Node extends Unique {
return; return;
} }
this.changeKeys({ this.changeKeys<NodeState>({
children: this.children.map(c => (c === oldValue ? newValue : c)) children: this.children.map(c => (c === oldValue ? newValue : c))
}); });
} }
@ -33,7 +35,7 @@ export abstract class Node extends Unique {
protected _log(indent = ''): string { protected _log(indent = ''): string {
const basicInfo = `${indent} - ${this.constructor.name}, #${this.id}`; const basicInfo = `${indent} - ${this.constructor.name}, #${this.id}`;
let response = `${basicInfo}${' '.repeat(25 - basicInfo.length)}siblings: ${this.copyCount}\n`; 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 + ' ')}`; response += `${c._log(indent + ' ')}`;
} }
return response; return response;

View file

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

View file

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