From 4f65c67aba1b9a22735f549ff76b17cf333a63e2 Mon Sep 17 00:00:00 2001 From: Endel Dreyer Date: Thu, 28 Mar 2024 16:14:31 -0300 Subject: [PATCH] support nested onAdd calls with 'immediate' mode #147 --- src/decoder/DecodeOperation.ts | 30 +++----- src/decoder/ReferenceTracker.ts | 14 +++- src/decoder/strategy/StateCallbacks.ts | 99 +++++++++++++++++--------- src/v3.ts | 57 +++++++-------- 4 files changed, 112 insertions(+), 88 deletions(-) diff --git a/src/decoder/DecodeOperation.ts b/src/decoder/DecodeOperation.ts index 0c31ceec..a5252665 100644 --- a/src/decoder/DecodeOperation.ts +++ b/src/decoder/DecodeOperation.ts @@ -169,28 +169,14 @@ export const decodeSchemaOperation: DecodeOperation = function ( // add change if (previousValue !== value) { - - // const callbacks = decoder.$root.callbacks.get(ref); - // if (callbacks) { - // callbacks.changes.push({ - // ref, - // refId: decoder.currentRefId, - // op: operation, - // field: field, - // value, - // previousValue, - // }); - // } - - // allChanges.push({ - // ref, - // refId: decoder.currentRefId, - // op: operation, - // field: field, - // value, - // previousValue, - // }); - + allChanges.push({ + ref, + refId: decoder.currentRefId, + op: operation, + field: field, + value, + previousValue, + }); } } diff --git a/src/decoder/ReferenceTracker.ts b/src/decoder/ReferenceTracker.ts index a0740d08..13b49241 100644 --- a/src/decoder/ReferenceTracker.ts +++ b/src/decoder/ReferenceTracker.ts @@ -1,13 +1,15 @@ import { Metadata } from "../Metadata"; import { $changes } from "../types/symbols"; import { Ref } from "../encoder/ChangeTree"; +import { spliceOne } from "../types/utils"; import type { MapSchema } from "../types/MapSchema"; -import { DataChange } from "./DecodeOperation"; /** * Used for decoding only. */ +export type SchemaCallbacks = { [field: string | number]: Function[] }; + export class ReferenceTracker { // // Relation of refId => Schema structure @@ -19,7 +21,7 @@ export class ReferenceTracker { public refCounts: { [refId: number]: number; } = {}; public deletedRefs = new Set(); - public callbacks: {[refId: number]: {[field: string | number]: Function[]}} = {}; + public callbacks: { [refId: number]: SchemaCallbacks } = {}; protected nextUniqueId: number = 0; getNextUniqueId() { @@ -108,6 +110,14 @@ export class ReferenceTracker { this.callbacks[refId][field] = []; } this.callbacks[refId][field].push(callback); + return () => this.removeCallback(refId, field, callback); + } + + removeCallback(refId: number, field: string | number, callback: Function) { + const index = this.callbacks?.[refId]?.[field]?.indexOf(callback); + if (index !== -1) { + spliceOne(this.callbacks[refId][field], index); + } } } diff --git a/src/decoder/strategy/StateCallbacks.ts b/src/decoder/strategy/StateCallbacks.ts index 96d7b1b6..1b9c4f2c 100644 --- a/src/decoder/strategy/StateCallbacks.ts +++ b/src/decoder/strategy/StateCallbacks.ts @@ -6,6 +6,7 @@ import { DataChange } from "../DecodeOperation"; import { OPERATION } from "../../encoding/spec"; import { DefinitionType } from "../../annotations"; import { Schema } from "../../Schema"; +import type { ArraySchema } from "../../types/ArraySchema"; // // Discussion: https://github.com/colyseus/schema/issues/155 @@ -51,26 +52,29 @@ type CollectionCallback = { onRemove(callback: (item: V, index: K) => void): void; }; +type OnInstanceAvailableCallback = (callback: (ref: Ref) => void) => void; + +type CallContext = { + instance?: Ref, + onParentInstanceAvailable?: OnInstanceAvailableCallback, +} + export function getStateCallbacks(decoder: Decoder) { const $root = decoder.$root; const callbacks = $root.callbacks; decoder.triggerChanges = function (allChanges: DataChange[]) { - console.log("Trigger changes!"); const uniqueRefIds = new Set(); for (let i = 0, l = allChanges.length; i < l; i++) { const change = allChanges[i]; const refId = change.refId; const ref = change.ref; - const $callbacks = callbacks[refId] + const $callbacks = callbacks[refId]; - if (!$callbacks) { - console.log("no callbacks for", refId, ref.constructor[Symbol.metadata], ", skip..."); - continue; - } + // console.log("change =>", { refId, field: change.field }); - console.log("HAS CALLBACKS!", $callbacks); + if (!$callbacks) { continue; } // // trigger onRemove on child structure. @@ -86,7 +90,7 @@ export function getStateCallbacks(decoder: Decoder) { if (!uniqueRefIds.has(refId)) { try { // trigger onChange - ($callbacks as Schema['$callbacks'])?.[OPERATION.REPLACE]?.forEach(callback => + $callbacks?.[OPERATION.REPLACE]?.forEach(callback => callback()); } catch (e) { @@ -148,13 +152,12 @@ export function getStateCallbacks(decoder: Decoder) { }; - function getProxy(metadataOrType: Metadata | DefinitionType, instance?: Ref, onParentInstanceAvailable?: (ref: Ref) => void) { - console.log({ metadataOrType }); - + function getProxy(metadataOrType: Metadata | DefinitionType, context: CallContext) { let metadata: Metadata; let isCollection = false; - if (onParentInstanceAvailable !== undefined) { + // not root... + if (context.onParentInstanceAvailable !== undefined) { if (typeof (metadataOrType) === "object") { isCollection = (Object.keys(metadataOrType)[0] !== "ref"); @@ -166,19 +169,25 @@ export function getStateCallbacks(decoder: Decoder) { metadata = metadataOrType as Metadata; } - console.log(`->`, { metadata, isCollection }); - - if (metadataOrType && !isCollection) { + if (metadata && !isCollection) { + /** + * Schema instances + */ return new Proxy({ - listen: function listen(prop: string, callback: (value: any, previousValue: any) => void, immediate?: boolean) { - console.log("LISTEN on refId:", $root.refIds.get(instance)); - $root.addCallback( - $root.refIds.get(instance), + listen: function listen(prop: string, callback: (value: any, previousValue: any) => void, immediate: boolean = true) { + // immediate trigger + if (immediate && context.instance[prop] !== undefined) { + callback(context.instance[prop], undefined); + } + + return $root.addCallback( + $root.refIds.get(context.instance), prop, callback ); }, onChange: function onChange(callback: () => void) { + // TODO: // $root.addCallback(tree, OPERATION.REPLACE, callback); }, bindTo: function bindTo(targetObject: any, properties?: Array>) { @@ -188,16 +197,16 @@ export function getStateCallbacks(decoder: Decoder) { get(target, prop: string) { if (metadataOrType[prop]) { - // TODO: instance might not be available yet, due to pending decoding for actual reference (+refId) - // .listen("prop", () => {/* attaching more... */}); - - // if (instance) { - // callbacks.set(instance, ) - // } + const instance = context.instance?.[prop]; + const onParentInstanceAvailable: OnInstanceAvailableCallback = !instance && ((callback: (ref: Ref) => void) => { + // @ts-ignore + const dettach = $(context.instance).listen(prop, (value, previousValue) => { + dettach(); + callback(value); + }); + }) || undefined; - return getProxy(metadataOrType[prop].type, instance?.[prop], (ref) => { - - }); + return getProxy(metadataOrType[prop].type, { instance, onParentInstanceAvailable }); } else { // accessing the function @@ -208,14 +217,36 @@ export function getStateCallbacks(decoder: Decoder) { set(target, prop, value) { throw new Error("not allowed"); }, deleteProperty(target, p) { throw new Error("not allowed"); }, }); + } else { - // collection instance + const onAdd = function (ref: Ref, callback: (value, key) => void, immediate: boolean = true) { + // collection instance is set + $root.addCallback( + $root.refIds.get(ref), + OPERATION.ADD, + callback + ); + + if (immediate) { + (ref as ArraySchema).forEach((v, k) => callback(v, k)); + } + } + + /** + * Collection instances + */ return new Proxy({ - onAdd: function onAdd(callback, immediate) { - if (onParentInstanceAvailable) { - } + onAdd: function(callback: (value, key) => void, immediate: boolean = true) { + if (context.instance) { + onAdd(context.instance, callback, immediate); - // $root.addCallback([...tree], OPERATION.ADD, callback); + } else if (context.onParentInstanceAvailable) { + console.log("onAdd, instance not available yet..."); + + // collection instance not received yet + context.onParentInstanceAvailable((ref: Ref) => + onAdd(ref, callback, false)); + } }, onRemove: function onRemove(callback) { // $root.addCallback([...tree], OPERATION.DELETE, callback); @@ -235,7 +266,7 @@ export function getStateCallbacks(decoder: Decoder) { } function $(instance: T): GetProxyType { - return getProxy(instance.constructor[Symbol.metadata], instance) as GetProxyType; + return getProxy(instance.constructor[Symbol.metadata], { instance }) as GetProxyType; } return { diff --git a/src/v3.ts b/src/v3.ts index dca1e447..5c0be9d8 100644 --- a/src/v3.ts +++ b/src/v3.ts @@ -229,12 +229,12 @@ class Player extends Entity { @type(Vec3) rotation = new Vec3().assign({ x: 0, y: 0, z: 0 }); @type("string") secret: string = "private info only for this player"; - // @type([Card]) - // cards = new ArraySchema( - // new Card().assign({ suit: "Hearts", num: 1 }), - // new Card().assign({ suit: "Spaces", num: 2 }), - // new Card().assign({ suit: "Diamonds", num: 3 }), - // ); + @type([Card]) + cards = new ArraySchema( + new Card().assign({ suit: "Hearts", num: 1 }), + new Card().assign({ suit: "Spaces", num: 2 }), + new Card().assign({ suit: "Diamonds", num: 3 }), + ); [$callback.$onCreate]() { } @@ -373,31 +373,34 @@ console.log("> register callbacks..."); const s: any = {}; -// $(decoder.state).listen("teams", (teams) => { -// $(teams).onAdd((team, _) => { -// $(team).entities.onAdd((entity, entityId) => { -// $(entity).position.bindTo(1, ["x", "y", "z"]); -// }); -// }); -// }); + +console.log("> will decode..."); + +// decoder.decode(encoded); +const changes = decoder.decode(viewEncoded1); $(decoder.state).listen("str", (value, previousValue) => { console.log("'str' changed:", { value, previousValue }); }); -// $(decoder.state).teams.onAdd((team, index) => { -// console.log("Teams.onAdd =>", { team, index }); -// $(team).entities.onAdd((entity, entityId) => { -// console.log(`Teams.${index}.onAdd =>`, { entity, entityId }); +$(decoder.state).teams.onAdd((team, index) => { + console.log("Teams.onAdd =>", { team, index }); -// const frontendObj: any = {}; -// $(entity).position.bindTo(frontendObj, ["x", "y", "z"]); -// }); + $(team).entities.onAdd((entity, entityId) => { + console.log(`Entities.onAdd =>`, { teamIndex: index, entity, entityId }); -// // $(team).entities.get("one").position.listen("x", (value, previousValue) => { -// // }); + // $(entity as Player).cards.onAdd((card, cardIndex) => { + // console.log(entityId, "card added =>", { card, cardIndex }); + // }); -// }); + // const frontendObj: any = {}; + // $(entity).position.bindTo(frontendObj, ["x", "y", "z"]); + }); + + // $(team).entities.get("one").position.listen("x", (value, previousValue) => { + // }); + +}); // $(decoder.state).teams.onAdd((team, index) => { // // room.$state.bind(team, frontendTeam); @@ -412,13 +415,7 @@ $(decoder.state).listen("str", (value, previousValue) => { // $.listen("") - -console.log($); - -console.log("> will decode..."); - -// decoder.decode(encoded); -const changes = decoder.decode(viewEncoded1); +console.log("Decoded =>", decoder.state.toJSON()); // decoder.decode(viewEncoded2);