From 8cab9d1e98068c8a27ddab39c969a6c39d0b65cc Mon Sep 17 00:00:00 2001 From: Endel Dreyer Date: Sat, 6 Jun 2020 18:38:06 -0300 Subject: [PATCH] use changetrees for map indexes #62 --- src/Schema.ts | 194 ++++++++++++++++++++------------------ src/annotations.ts | 25 ++++- src/changes/ChangeTree.ts | 25 ++++- src/types/ArraySchema.ts | 6 +- src/types/MapSchema.ts | 30 +++++- test/NextIterationTest.ts | 29 ++++-- 6 files changed, 196 insertions(+), 113 deletions(-) diff --git a/src/Schema.ts b/src/Schema.ts index f75597cd..b553a10d 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -20,7 +20,7 @@ export interface DataChange { class EncodeSchemaError extends Error {} -function assertType(value: any, type: string, klass: Schema, field: string) { +function assertType(value: any, type: string, klass: Schema, field: string | number) { let typeofTarget: string; let allowNull: boolean = false; @@ -56,13 +56,13 @@ function assertType(value: any, type: string, klass: Schema, field: string) { } } -function assertInstanceType(value: Schema, type: typeof Schema | typeof ArraySchema | typeof MapSchema, klass: Schema, field: string) { +function assertInstanceType(value: Schema, type: typeof Schema | typeof ArraySchema | typeof MapSchema, klass: Schema, field: string | number) { if (!(value instanceof type)) { throw new EncodeSchemaError(`a '${type.name}' was expected, but '${(value as any).constructor.name}' was provided in ${klass.constructor.name}#${field}`); } } -function encodePrimitiveType (type: PrimitiveType, bytes: number[], value: any, klass: Schema, field: string) { +function encodePrimitiveType (type: PrimitiveType, bytes: number[], value: any, klass: Schema, field: string | number) { assertType(value, type as string, klass, field); const encodeFunc = encode[type as string]; @@ -475,36 +475,46 @@ export abstract class Schema { useFilters: boolean = false, ) { const $root = root.$changes.root; - const changeTrees = Array.from($root.changes); + + const changeTrees = (encodeAll) + ? Array.from($root.allChanges) + : Array.from($root.changes); + + console.log("ChangeTrees =>", changeTrees); for (let i = 0, l = changeTrees.length; i < l; i++) { - const change = changeTrees[i]; - const ref = change.ref as Schema; + const changeTree = changeTrees[i]; + const ref = changeTree.ref as Schema; // const indexes = change.indexes; // root `refId` is skipped. - console.log("encode, refId =>", change.refId); - encode.uint8(bytes, SWITCH_TO_STRUCTURE); - encode.number(bytes, change.refId); + console.log("ENCODE, refId =>", changeTree.refId, { + isSchema: ref instanceof Schema, + isMap: ref instanceof MapSchema, + isArray: ref instanceof ArraySchema, + }); - // console.log("changetree:", change); + encode.uint8(bytes, SWITCH_TO_STRUCTURE); + encode.number(bytes, changeTree.refId); const changes = (encodeAll) - ? Array.from(change.allChanges) - : Array.from(change.changes.keys()); + ? Array.from(changeTree.allChanges) + : Array.from(changeTree.changes.keys()); for (let j = 0, cl = changes.length; j < cl; j++) { + console.log("CHANGES =>", changeTree.changes); const fieldIndex = changes[j]; - const operation = change.changes.get(fieldIndex); + const operation = changeTree.changes.get(fieldIndex); - const schema = ref._schema; - const fieldsByIndex = ref._fieldsByIndex; + console.log("CHANGETREE_TYPE", changeTree.childType); - const field = fieldsByIndex[fieldIndex]; + const field = (ref._fieldsByIndex && ref._fieldsByIndex[fieldIndex]) || fieldIndex; const _field = `_${field}`; - const type = schema[field]; - const value = ref[_field]; + const type = changeTree.childType || ref._schema[field]; + + // const type = changeTree.getType(fieldIndex); + const value = changeTree.getValue(fieldIndex); console.log({ field, type, value, operation }); @@ -512,7 +522,7 @@ export abstract class Schema { const beginIndex = bytes.length; // encode field index + operation - encode.uint8(bytes, fieldIndex | operation.op); + encode.number(bytes, fieldIndex | operation.op); if (operation.op === OPERATION.DELETE) { // TODO: delete from $root.cache @@ -533,8 +543,6 @@ export abstract class Schema { console.log("ENCODING ARRAY!"); const $changes: ChangeTree = value.$changes; - encode.number(bytes, fieldIndex); - // total number of items in the array encode.number(bytes, value.length); @@ -593,103 +601,105 @@ export abstract class Schema { } } else if (MapSchema.is(type)) { - console.log("ENCODING MAP!"); const $changes: ChangeTree = value.$changes; - // encode Map of type - encode.number(bytes, fieldIndex); - - // TODO: during `encodeAll`, removed entries are not going to be encoded const keys = Array.from( (encodeAll) ? Array.from($changes.allChanges) : Array.from($changes.changes.keys()) ); - encode.number(bytes, keys.length) + // // total of elements being encoded. + // encode.number(bytes, keys.length); - // const previousKeys = Object.keys(ref[_field]); // this is costly! - const previousKeys = Array.from($changes.allChanges); - const isChildSchema = typeof((type as any).map) !== "string"; + // // const previousKeys = Object.keys(ref[_field]); // this is costly! + // const previousKeys = Array.from($changes.allChanges); + // const isChildSchema = typeof((type as any).map) !== "string"; const numChanges = keys.length; - // console.log("ENCODE MAP =>", { keys, numChanges, previousKeys, isChildSchema }); + console.log("ENCODING MAP!", { keys, numChanges }); // assert MapSchema was provided assertInstanceType(ref[_field], MapSchema, ref, field); - for (let i = 0; i < numChanges; i++) { - const key = keys[i]; - const item = ref[_field].get(key); + // + // Encode refId for this instance. + // The actual instance is going to be encoded on next `changeTree` iteration. + // + encode.number(bytes, $changes.refId); - let mapItemIndex: number = undefined; + // for (let i = 0; i < numChanges; i++) { + // const key = keys[i]; + // const item = ref[_field]['getByIndex'](key); - // console.log("ENCODING MAP ITEM", { key, item }); + // let mapItemIndex: number = undefined; - if (encodeAll) { - if (item === undefined) { - // previously deleted items are skipped during `encodeAll` - continue; - } + // console.log("ENCODING MAP ITEM", { key, item }); - } else { - // encode index change - // const indexChange = $changes.getIndexChange(item); - // if (item && indexChange !== undefined) { - // encode.uint8(bytes, INDEX_CHANGE); - // encode.number(bytes, ref[_field]._indexes.get(indexChange)); - // } + // if (encodeAll) { + // if (item === undefined) { + // // previously deleted items are skipped during `encodeAll` + // continue; + // } - /** - * - Allow item replacement - * - Allow to use the index of a deleted item to encode as NIL - */ - // mapItemIndex = (!$changes.isDeleted(key) || !item) - // ? ref[_field]._indexes.get(key) - // : undefined; + // } else { + // // encode index change + // // const indexChange = $changes.getIndexChange(item); + // // if (item && indexChange !== undefined) { + // // encode.uint8(bytes, INDEX_CHANGE); + // // encode.number(bytes, ref[_field]._indexes.get(indexChange)); + // // } - // console.log({ indexChange, mapItemIndex }); - } + // /** + // * - Allow item replacement + // * - Allow to use the index of a deleted item to encode as NIL + // */ + // // mapItemIndex = (!$changes.isDeleted(key) || !item) + // // ? ref[_field]._indexes.get(key) + // // : undefined; - const isNil = (item === undefined); + // // console.log({ indexChange, mapItemIndex }); + // } - /** - * Invert NIL to prevent collision with data starting with NIL byte - */ - if (isNil) { + // const isNil = (item === undefined); - // TODO: remove item - // console.log("REMOVE KEY INDEX", { key }); - // ref[_field]._indexes.delete(key); - encode.uint8(bytes, NIL); - } + // /** + // * Invert NIL to prevent collision with data starting with NIL byte + // */ + // if (isNil) { - if (mapItemIndex !== undefined) { - encode.number(bytes, mapItemIndex); + // // TODO: remove item + // // console.log("REMOVE KEY INDEX", { key }); + // // ref[_field]._indexes.delete(key); + // encode.uint8(bytes, NIL); + // } - } else { - encode.string(bytes, key); - } + // if (mapItemIndex !== undefined) { + // encode.number(bytes, mapItemIndex); - if (item && isChildSchema) { - assertInstanceType(item, (type as any).map, ref, field); - this.tryEncodeTypeId(bytes, (type as any).map, item.constructor as typeof Schema); - (item as Schema).encode(root, encodeAll, bytes, useFilters); + // } else { + // encode.string(bytes, key); + // } - } else if (!isNil) { - encodePrimitiveType((type as any).map, bytes, item, ref, field); - } + // if (item && isChildSchema) { + // assertInstanceType(item, (type as any).map, ref, field); + // this.tryEncodeTypeId(bytes, (type as any).map, item.constructor as typeof Schema); + // (item as Schema).encode(root, encodeAll, bytes, useFilters); - } + // } else if (!isNil) { + // encodePrimitiveType((type as any).map, bytes, item, ref, field); + // } - if (!encodeAll && !useFilters) { - $changes.discard(); + // } - // TODO: track array/map indexes per client (for filtering)? + // if (!encodeAll && !useFilters) { + // $changes.discard(); - // TODO: do not iterate though all MapSchema indexes here. - ref[_field]._updateIndexes(previousKeys); - } + // // TODO: track array/map indexes per client (for filtering)? + + // // TODO: do not iterate though all MapSchema indexes here. + // ref[_field]._updateIndexes(previousKeys); + // } } else { encodePrimitiveType(type as PrimitiveType, bytes, value, ref, field) @@ -697,22 +707,18 @@ export abstract class Schema { if (useFilters) { // cache begin / end index - change.cache(fieldIndex as number, beginIndex, bytes.length) + changeTree.cache(fieldIndex as number, beginIndex, bytes.length) } } if (!encodeAll && !useFilters) { - change.discard(); + changeTree.discard(); } } return bytes; } - // encodeFiltered(client: Client, bytes?: number[]) { - // return this.encode(this, false, client, bytes); - // } - encodeAll (bytes?: number[]) { return this.encode(this, true, bytes); } @@ -1008,6 +1014,10 @@ export abstract class Schema { this.$changes.discard(); } + protected getByIndex(index: number) { + return this[this._fieldsByIndex[index]]; + } + private _encodeEndOfStructure(instance: Schema, root: Schema, bytes: number[]) { if (instance !== root) { bytes.push(SWITCH_TO_STRUCTURE); diff --git a/src/annotations.ts b/src/annotations.ts index 77a87120..2002bd20 100644 --- a/src/annotations.ts +++ b/src/annotations.ts @@ -189,13 +189,16 @@ export function type (type: DefinitionType, context: Context = globalContext): P this[fieldCached] = value; const $root = this.$changes.root; - // TODO: don't create a new ChangeTree - just update the root & parent. if (isArray) { // directly assigning an array of items as value. this.$changes.change(field); - value.$changes.setParent(this.$changes, $root); + value.$changes.setParent( + this.$changes, + $root, + constructor._schema[field], + ); // value.$changes = new ChangeTree(value, {}, this.$changes, $root); for (let i = 0; i < value.length; i++) { @@ -210,13 +213,21 @@ export function type (type: DefinitionType, context: Context = globalContext): P } else if (isMap) { // directly assigning a map - value.$changes.setParent(this.$changes, $root); + value.$changes.setParent( + this.$changes, + $root, + (constructor._schema[field] as any).map, + ); + this.$changes.change(field); (value as MapSchema).forEach((val, key) => { console.log("FLAG AS CHANGED:", key); if (val instanceof Schema) { - val.$changes.setParent(value.$changes, $root); + val.$changes.setParent( + value.$changes, + $root, + ); // val.$changes.changeAll(val); } // value.$changes.mapIndex(val, key); @@ -229,7 +240,11 @@ export function type (type: DefinitionType, context: Context = globalContext): P this.$changes.change(field); if (value) { - value.$changes.setParent(this.$changes, $root); + value.$changes.setParent( + this.$changes, + $root, + constructor._schema[field], + ); } } else { diff --git a/src/changes/ChangeTree.ts b/src/changes/ChangeTree.ts index a404ff48..c045ddb4 100644 --- a/src/changes/ChangeTree.ts +++ b/src/changes/ChangeTree.ts @@ -1,6 +1,7 @@ import { Ref, Root } from "./Root"; import { OPERATION } from "../spec"; import { Schema } from "../Schema"; +import { DefinitionType, PrimitiveType } from "../annotations"; // type FieldKey = string | number; @@ -20,6 +21,9 @@ export interface FieldCache { export class ChangeTree { refId: number; + dynamicIndexes: boolean; + childType: PrimitiveType; + changes = new Map(); allChanges = new Set(); @@ -33,11 +37,22 @@ export class ChangeTree { protected _root?: Root, ) { this.setParent(parent, _root || new Root()); + + // + // Use "dynamic indexes" for: + // MapSchema / ArraySchema / SetSchema + // + this.dynamicIndexes = !(ref instanceof Schema); } - setParent(parent: ChangeTree, root: Root) { + setParent( + parent: ChangeTree, + root: Root, + childType?: PrimitiveType, + ) { this.parent = parent; this.root = root; + this.childType = childType; // // assign same parent on child structures @@ -93,6 +108,14 @@ export class ChangeTree { this.root.dirty(this); } + getType(index: number) { + + } + + getValue(index: number) { + return this.ref['getByIndex'](index); + } + delete(fieldName: string | number) { // const fieldIndex = this.indexes[fieldName]; // const field = (typeof (fieldIndex) === "number") ? fieldIndex : fieldName; diff --git a/src/types/ArraySchema.ts b/src/types/ArraySchema.ts index f2f04cd4..51edcbc4 100644 --- a/src/types/ArraySchema.ts +++ b/src/types/ArraySchema.ts @@ -18,7 +18,11 @@ export class ArraySchema extends Array { Object.setPrototypeOf(this, Object.create(ArraySchema.prototype)); Object.defineProperties(this, { $sorting: { value: undefined, enumerable: false, writable: true }, - $changes: { value: undefined, enumerable: false, writable: true }, + $changes: { + value: new ChangeTree(this, {}), + enumerable: false, + writable: true + }, onAdd: { value: undefined, enumerable: false, writable: true }, onRemove: { value: undefined, enumerable: false, writable: true }, diff --git a/src/types/MapSchema.ts b/src/types/MapSchema.ts index e92d825f..0f40b774 100644 --- a/src/types/MapSchema.ts +++ b/src/types/MapSchema.ts @@ -1,10 +1,15 @@ import { ChangeTree } from "../changes/ChangeTree"; +import { Schema } from "../Schema"; type K = string; // TODO: allow to specify K generic on MapSchema. export class MapSchema { protected $changes: ChangeTree; + protected $items: Map = new Map(); + protected $indexes: Map = new Map(); + + protected $refId: number; static is(type: any) { return type['map'] !== undefined; @@ -21,7 +26,12 @@ export class MapSchema { } Object.defineProperties(this, { - $changes: { value: undefined, enumerable: false, writable: true }, + $changes: { + value: new ChangeTree(this, {}), + enumerable: false, + writable: true + }, + $refId: { value: 0, enumerable: false, writable: true }, onAdd: { value: undefined, enumerable: false, writable: true }, onRemove: { value: undefined, enumerable: false, writable: true }, @@ -92,12 +102,18 @@ export class MapSchema { } set(key: K, value: V) { - if (this.$changes) { - this.$changes.change(key); - } - this.$items.set(key, value); + // set "index" for reference. + const index = (value instanceof Schema) + ? value['$changes'].refId + : this.$refId++ + + this.$changes.indexes[key] = index; + this.$indexes.set(index, key); + + this.$changes.change(key); + return this; } @@ -137,6 +153,10 @@ export class MapSchema { return this.$items.size; } + protected getByIndex(index: number) { + return this.$items.get(this.$indexes.get(index)); + } + // // Decoding utilities // diff --git a/test/NextIterationTest.ts b/test/NextIterationTest.ts index 18f8e169..6a9955e0 100644 --- a/test/NextIterationTest.ts +++ b/test/NextIterationTest.ts @@ -27,26 +27,37 @@ describe("Next Iteration", () => { assert.deepEqual(decoded.arr, ['one', 'twotwo', 'three']); }); - xit("add and modify a map item", () => { + it("add and modify a map item", () => { class State extends Schema { @type({ map: "number" }) map = new Map(); } - const encoded = new State(); - encoded.map.set("one", 1); - encoded.map.set("two", 2); - encoded.map.set("three", 3); + const state = new State(); + state.map.set("one", 1); + state.map.set("two", 2); + state.map.set("three", 3); const decoded = new State(); - decoded.decode(encoded.encode()); + + let encoded = state.encode(); + console.log("ENCODED (full):", encoded); + + console.log("\n\nWILL DECODE:\n"); + decoded.decode(encoded); + assert.deepEqual(decoded.map.get("one"), 1); assert.deepEqual(decoded.map.get("two"), 2); assert.deepEqual(decoded.map.get("three"), 3); - encoded.map.set("two", 22); + state.map.set("two", 22); + + encoded = state.encode(); + console.log("ENCODED (patch):", encoded); + + console.log("\n\nWILL DECODE:\n"); + decoded.decode(encoded); - decoded.decode(encoded.encode()); assert.deepEqual(decoded.map.get("two"), 22); }); @@ -101,7 +112,7 @@ describe("Next Iteration", () => { console.log("DECODED =>", decoded.toJSON()); }); - it("should encode filtered", () => { + xit("should encode filtered", () => { class Item extends Schema { @type("number") damage: number; }