Skip to content

Instantly share code, notes, and snippets.

@endel
Last active August 22, 2021 13:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save endel/3a7097c1bfc7090e7d8f34d0d5194094 to your computer and use it in GitHub Desktop.
Save endel/3a7097c1bfc7090e7d8f34d0d5194094 to your computer and use it in GitHub Desktop.
New Schema Callback API Proposal

Hi everybody! Regarding the possible new schema callback API, so far this is what I could came up so far, feedback highly appreciated! Do let me know if you feel I need to clarify anything!

I understand this may be long and hard to grasp, I can provide a side-by-side comparison between the new API and previous one so you can clearly see the difference.

The new API would allow to create all the callbacks at once, after the connection with the room has been established (even on deep structures) - rather than binding callbacks as Schema instances that are created on-demand on the client-side.

Structures:

class Point extends Schema {
    @type("number") x: number = 0;
    @type("number") y: number = 0;
}

class Item extends Schema {
    @type("string") name: string;
    @type("number") price: string;
    @type({ map: "string" }) attributes = new MapSchema<string>();
}

class Player extends Schema {
    @type('string') name: string;
    @type('number') level: number;
    @type(Point) position = new Point();
    @type({ map: Item }) items = new MapSchema<Item>();
}

class Ball extends Schema {
    @type(Point) position: Point = new Point();
}

class State extends Schema {
    @type("number") state: MatchState;
    @type({ map: Player }) players = new MapSchema<Player>();
    @type(Ball) ball = new Ball();
}

A new "snapshots" property would be used to register callbacks on incoming state patches from the server:

let snapshots: Snapshots<State>;

// user would consume this from `room.snapshots`
room.snapshots.on() // ...

Usage examples

snapshots.on("ball").on("position").on("x", (position, value, previousValue) => { });
snapshots.on("ball").on("position").on("y", (position, value, previousValue) => { });
snapshots.on("ball").on("position").onChange((position, changes) => {
    changes.forEach((change) => {
        if (change.field === "x") {
        } else if (change.field === "y") {
        }
    })
});
snapshots.on("ball").onChange((ball, changes) => {
    // ideally this would trigger if any property inside "ball" has changed (after previous registered callback)
});

snapshots.on("players").onAdd((player, sessionId) => { });
snapshots.on("players").onRemove((player, sessionId) => { });
snapshots.on("players").onChange((player, sessionId) => {
    // ideally called after any property inside "player" has changed.
    // ideally, also, no overhead in case .onChange was not registered.
});

snapshots.on("players").children().on("level", (player, value, previousValue) => {
    // this is triggered whenever "level" property is changed inside any "Player" instance.
});

snapshots.on("players").children().on("items").onAdd((item, key) => { });

Complex example, using deep structures:

snapshots.on("players").children().on("items").children().on("attributes").onAdd((attribute, key) => {
    // when listening to very deep structures, their parent instances
    // could be retrieved via .current(Class), or .currentAt(propertyName)
    const player = snapshots.current(Player);
    const player = snapshots.currentAt<Player>("players"); // dynamic alternative (when client does not have a concrete "Player" implementation)

    const item = snapshots.current(Item);
    const item = snapshots.currentAt<Item>("items"); // dynamic alternative (when client does not have a concrete "Player" implementation)
});

snapshots.on("players").children().onChange((player, changes) => {
    // whenever any change ocurred on a "Player" instance.
});

Removing callbacks

// examples removing a bound calback
const onAddPlayers = snapshots.on("players").onAdd((player, key) => {
    snapshots.off(onAddPlayers)
});

const ballPositionX = snapshots.on("ball").on("position").on("x", (position, value, previousValue) => {
    snapshots.off(ballPositionX);
});

The functionality of this API hasn't been implemented yet, only its feasibility using the TypeScript typing system has been confirmed, as you can see on the video.

@CookedApps
Copy link

I like the new callbacks, even though I don't yet see a big benefit/difference from it. Maybe you could add the side-by-side comparison to clarify. And what video? 😁

@endel
Copy link
Author

endel commented Aug 22, 2021

Hi @CookedApps! Forgot to upload the video, just did it as GIF here: https://gist.github.com/endel/3a7097c1bfc7090e7d8f34d0d5194094#file-video-gif

The major difference is when attaching callbacks to deep/nested structures, it wouldn't be required to register .onAdd to attach callbacks on children structures https://docs.colyseus.io/colyseus/state/schema/#onadd-instance-key

(Gonna update here with a side-by-side comparison soon!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment