From ca0bf943f71d8d03909ac2dd16d0db24476125cb Mon Sep 17 00:00:00 2001 From: schmelczerandras Date: Sat, 31 Aug 2019 14:24:54 +0200 Subject: [PATCH] Create immutable storage --- src/app/app.component.ts | 18 +++++++- src/app/model/base.ts | 8 +++- src/app/services/data.service.ts | 5 +-- src/app/storage/cloneable.ts | 71 ++++++++++++++++++++++++++++++++ src/app/storage/inner-node.ts | 67 ++++++++++++++++++++++++++++++ src/app/storage/node.ts | 29 +++++++++++++ src/app/storage/root.ts | 28 +++++++++++++ src/app/utils/hash.ts | 11 +++-- 8 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 src/app/storage/cloneable.ts create mode 100644 src/app/storage/inner-node.ts create mode 100644 src/app/storage/node.ts create mode 100644 src/app/storage/root.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index dddf567..70b176e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; -import { ModalService } from './services/modal.service'; +import { Cloneable } from './storage/cloneable'; +import { Root } from './storage/root'; @Component({ selector: 'app-root', @@ -8,4 +9,19 @@ import { ModalService } from './services/modal.service'; }) export class AppComponent { title = 'frontend'; + + constructor() { + const root = new Root(); + + const l1 = new Cloneable(root, 'l1'); + const r1 = new Cloneable(root, 'r1'); + const r1r1 = new Cloneable(r1, 'r1r1'); + const r1l1 = new Cloneable(r1, 'r1l1'); + + r1r1.changeName('r1r1 new'); + + r1l1.changeName('r1l1 new'); + + r1l1.map((c: Cloneable) => c.changeNameMap('bdeiwf')); + } } diff --git a/src/app/model/base.ts b/src/app/model/base.ts index c39afdc..d3c75f6 100644 --- a/src/app/model/base.ts +++ b/src/app/model/base.ts @@ -1,7 +1,11 @@ +import { Subject } from 'rxjs/internal/Subject'; + export class Base { private static propertyList: any = {}; protected subscribers: (() => void)[] = []; + subject: Subject = new Subject(); + constructor(properties: any) { const type = this.constructor.name; if (!Base.propertyList.hasOwnProperty(type)) { @@ -37,7 +41,8 @@ export class Base { [property]: this[property], ...object }), - {} + // TODO + { type: this.constructor.name } ); } @@ -46,6 +51,7 @@ export class Base { } protected update() { + this.subject.next(this); this.subscribers.map(f => f()); } } diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts index 8f333fb..e7d7356 100644 --- a/src/app/services/data.service.ts +++ b/src/app/services/data.service.ts @@ -1,4 +1,4 @@ -import { ApplicationRef, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { StoreService } from './store.service'; import { Page } from '../model/page'; @@ -22,7 +22,7 @@ export class DataService { private hasLoaded = new Promise(resolve => (this.afterLoadFinished = resolve)); private afterLoadFinished: () => void; - constructor(private storeService: StoreService, private applicationRef: ApplicationRef) { + constructor(private storeService: StoreService) { this.init(); } @@ -72,7 +72,6 @@ export class DataService { private update() { this.subscribers.map(f => f()); - this.applicationRef.tick(); } private loadActiveIndex() { diff --git a/src/app/storage/cloneable.ts b/src/app/storage/cloneable.ts new file mode 100644 index 0000000..9534c67 --- /dev/null +++ b/src/app/storage/cloneable.ts @@ -0,0 +1,71 @@ +import { InnerNode } from './inner-node'; +import { Node } from './node'; + +export class Cloneable extends InnerNode { + name; + constructor(parent: Node, name: any) { + super(parent); + this.name = name; + } + + changeNameMap = (newValue: string) => { + this.name = newValue; + }; + + changeName = (newValue: any) => { + this.changeValue({ + oldValue: this.name, + newValue + }); + }; + + 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; + } + const value = target[prop as string].value; + if (typeof value === 'function') { + return value.bind(proxy); + } + return value; + }, + set: (target, prop, value) => { + return (target[prop as string].value = value); + } + }); + map(insidesProxy); + + return Object.create(Object.getPrototypeOf(this), insidesProxy.__target__); + } + + protected cloneWithAdd({ value, propertyName }: { value: any; propertyName: string }): this { + const insides = Object.getOwnPropertyDescriptors(this); + insides[propertyName].value = value; + insides.id.value = Node.id++; + + return Object.create(Object.getPrototypeOf(this), insides); + } + + protected cloneWithModify({ oldValue, newValue }: { oldValue: any; newValue: any }): this { + const insides = Object.getOwnPropertyDescriptors(this); + insides.id.value = Node.id++; + + 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 Object.create(Object.getPrototypeOf(this), insides); + } +} diff --git a/src/app/storage/inner-node.ts b/src/app/storage/inner-node.ts new file mode 100644 index 0000000..8d2f8a7 --- /dev/null +++ b/src/app/storage/inner-node.ts @@ -0,0 +1,67 @@ +import { Node } from './node'; + +export abstract class InnerNode extends Node { + parent: Node; + nextVersion: this = null; + + protected readonly children: Array = []; + + get latestVersion(): this { + let version; + for (version = this; version.nextVersion !== null; version = version.nextVersion) { + // pass + } + return version; + } + + protected constructor(parent: Node) { + super(); + + parent.addChild({ + value: this + }); + } + + map(map: (a: this) => void) { + return this.update((self: this) => this.cloneWithMap.call(self, map)); + } + + changeKey(update: { value: any; propertyName: string }): this { + return this.update((self: this) => this.cloneWithAdd.call(self, update)); + } + + changeValue(update: { oldValue: any; newValue: any }): this { + return this.update((self: this) => this.cloneWithModify.call(self, update)); + } + + addChild(update: { value: InnerNode }) { + super.addChild.call(this.latestVersion, update); + } + + changeChild(update: { oldValue: InnerNode; newValue: InnerNode }) { + super.changeChild.call(this.latestVersion, update); + } + + protected abstract cloneWithMap(map: (a: this) => void): this; + protected abstract cloneWithAdd(update: { value: any; propertyName: string }): this; + protected abstract cloneWithModify(update: { oldValue: any; newValue: any }): this; + + private update(cloneMethod: (self: this) => this): this { + if (this.nextVersion !== null) { + this.latestVersion.update(cloneMethod); + } + + const clone = cloneMethod(this); + for (let child of clone.children) { + child.parent = clone; + } + + this.parent.changeChild({ + oldValue: this, + newValue: clone + }); + + this.nextVersion = clone; + return clone; + } +} diff --git a/src/app/storage/node.ts b/src/app/storage/node.ts new file mode 100644 index 0000000..e0dc6ff --- /dev/null +++ b/src/app/storage/node.ts @@ -0,0 +1,29 @@ +import { InnerNode } from './inner-node'; + +export abstract class Node { + public static id = 0; + protected abstract readonly children: Array; + private id = Node.id++; + + changeKey(update: { value: any; propertyName: string }) { + throw new TypeError('Not implemented!'); + } + + changeValue(update: { oldValue: any; newValue: any }) { + throw new TypeError('Not implemented!'); + } + + addChild(update: { value: InnerNode }) { + this.changeValue({ + oldValue: this.children, + newValue: [...this.children, update.value] + }); + } + + changeChild({ oldValue, newValue }: { oldValue: InnerNode; newValue: InnerNode }) { + this.changeValue({ + oldValue: this.children, + newValue: this.children.map(c => (c === oldValue ? newValue : c)) + }); + } +} diff --git a/src/app/storage/root.ts b/src/app/storage/root.ts new file mode 100644 index 0000000..88b9f57 --- /dev/null +++ b/src/app/storage/root.ts @@ -0,0 +1,28 @@ +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { Observable } from 'rxjs/internal/Observable'; +import { Node } from './node'; +import { InnerNode } from './inner-node'; + +export class Root extends Node { + private readonly _children: BehaviorSubject> = new BehaviorSubject([]); + readonly children$: Observable> = this._children.asObservable(); + + get children(): Array { + return this._children.getValue(); + } + + set children(value: Array) { + this._children.next(value); + } + + changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }) { + if (this.children === oldValue) { + this.children = newValue; + for (let child of this.children) { + child.parent = this; + } + } else { + throw new TypeError('Only children can be changed.'); + } + } +} diff --git a/src/app/utils/hash.ts b/src/app/utils/hash.ts index 049f611..ae2412b 100644 --- a/src/app/utils/hash.ts +++ b/src/app/utils/hash.ts @@ -1,8 +1,13 @@ export const hash = (text: string): number => { - // Return number between 0 and 1. + // Return number from [0, 1) if (!text) { return 0; } - const hash = Array.prototype.reduce.call(text, (hash, char) => (hash << 5) - hash + char.charCodeAt(0), 7); - return hash / (Math.pow(2, 32) - 1); + const hashValue = Array.prototype.reduce.call( + // tslint:disable-next-line:no-bitwise + text, + (value, char) => ((value << 5) - value + (char.charCodeAt(0) as number)) | 0, + 7 + ); + return hashValue / (Math.pow(2, 32) - 2) + 0.5; };