Skip to content

Commit

Permalink
support nested onAdd calls with 'immediate' mode #147
Browse files Browse the repository at this point in the history
  • Loading branch information
endel committed Mar 28, 2024
1 parent aad6c6c commit 4f65c67
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 88 deletions.
30 changes: 8 additions & 22 deletions src/decoder/DecodeOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
}

Expand Down
14 changes: 12 additions & 2 deletions src/decoder/ReferenceTracker.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,7 +21,7 @@ export class ReferenceTracker {
public refCounts: { [refId: number]: number; } = {};
public deletedRefs = new Set<number>();

public callbacks: {[refId: number]: {[field: string | number]: Function[]}} = {};
public callbacks: { [refId: number]: SchemaCallbacks } = {};
protected nextUniqueId: number = 0;

getNextUniqueId() {
Expand Down Expand Up @@ -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);
}
}

}
99 changes: 65 additions & 34 deletions src/decoder/strategy/StateCallbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,26 +52,29 @@ type CollectionCallback<K, V> = {
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<number>();

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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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");

Expand All @@ -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<NonFunctionPropNames<T>>) {
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -235,7 +266,7 @@ export function getStateCallbacks(decoder: Decoder) {
}

function $<T extends Ref>(instance: T): GetProxyType<T> {
return getProxy(instance.constructor[Symbol.metadata], instance) as GetProxyType<T>;
return getProxy(instance.constructor[Symbol.metadata], { instance }) as GetProxyType<T>;
}

return {
Expand Down
57 changes: 27 additions & 30 deletions src/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Card>(
// 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<Card>(
new Card().assign({ suit: "Hearts", num: 1 }),
new Card().assign({ suit: "Spaces", num: 2 }),
new Card().assign({ suit: "Diamonds", num: 3 }),
);

[$callback.$onCreate]() {
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down

0 comments on commit 4f65c67

Please sign in to comment.