Replies: 3 comments 2 replies
-
|
Hi @serjek, can this be optional / add-on? We can incorporate it into the SDK itself but I wouldn't feel comfortable maintaining such complex macro systems after it gets merged |
Beta Was this translation helpful? Give feedback.
-
|
Hey @endel! Yeah, this is completely optional feature, user can annotate a class with build macro or skip it completely and manage callbacks on their own. My current implementation looks like this (simplified): final matchObservables:MatchSchemaObservables = new MatchSchemaObservables();
client.joinOrCreate(
MATCH,
new ClientOptions({ ... }),
MatchSchema,
(err, room) -> {
...
matchObservables.listen(room);
}
);
@:build(ObservableSchemaMacro.build(MatchSchema))
class MatchSchemaObservables {
private var room:Room<MatchSchema>;
private var cb:SchemaCallbacks<MatchSchema>;
private var link:CallbackLink;
public function listen(room:Room<MatchSchema>) {
this.room = room;
cb = Callbacks.get(room);
link = SchemaListenMacro.listenRef(cb, room.state);
}
public function dispose() {
link.cancel();
}
}Generated code. Note that schema is not touched, all macro output is generated in separate class that user has to create manually: package ru.smartpref.ui.colyseus.match;
class MatchSchemaDebug {
private var room : Room<MatchSchema>;
private var cb : SchemaCallbacks<MatchSchema>;
private var link : CallbackLink;
public function listen(room:Room<MatchSchema>) {
this.room = room;
cb = Callbacks.get(room);
link = SchemaListenMacro.listenRef(cb, room.state);
}
public function dispose() {
link.cancel();
}
public final sides :
tink.state.ObservableMap<String, {
var rating : tink.state.State<common.api.Types.RatingData>;
var username : tink.state.State<String>;
var id : tink.state.State<Int>;
var isOnline : tink.state.State<Bool>;
}> = new tink.state.ObservableMap([]);
public final createdBy : tink.state.State<Int> = new tink.state.State(0);
public final isStarted : tink.state.State<Bool> = new tink.state.State(false);
public function new() { }
}SchemaListenMacro unwraps to this: cb.onChange(room.state, function() {
{
function __rebuild() {
for (k => _ in this.sides) this.sides.remove(k);
for (__k => __item in ((room.state.sides : SchemaUtils.MapType<Dynamic>)))
this.sides.set(__k, {
rating : new tink.state.State(((tink.Json.parse(__item.rating) : common.api.Types.RatingData))),
username : new tink.state.State(__item.username),
id : new tink.state.State(__item.id),
isOnline : new tink.state.State(__item.isOnline)
});
};
__rebuild();
cb.onChange(room.state.sides, __rebuild);
};
this.createdBy.set(room.state.createdBy);
this.isStarted.set(room.state.isStarted);
})Note the @:keep
class MatchSchema extends Schema {
@:type("map", MatchUserSchema)
public var sides:MapSchema<MatchUserSchema> = new MapSchema();
@:type("number")
public var createdBy:Int;
@:type("boolean")
public var isStarted:Bool;
}
@:keep
class MatchUserSchema extends Schema {
@:type("string")
public var rating:Serialized<RatingData>;
@:type("string")
public var username:String = "";
@:type("number")
public var id:Int = 0;
@:type("boolean")
public var isOnline:Bool = false;
} |
Beta Was this translation helpful? Give feedback.
-
|
So, here is the PR - colyseus/colyseus-haxe#71 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Idea: Macro-Driven Observability Layer for Colyseus Haxe Client
Summary
Introduce an optional observability layer for the Colyseus Haxe client that automatically converts Colyseus
Schemaobjects into reactive, observable client models using compile-time macros.The goal is to eliminate manual callback wiring, drastically reduce complexity when working with nested schemas, and provide a declarative, reactive API on top of Colyseus’ low-level callback system.
Problem Statement
Colyseus schemas expose state changes via callbacks such as:
onChangelistenonAddonRemoveWhile flexible, this approach becomes hard to scale on the client when:
Typical client code ends up with:
Proposed Solution
A macro-driven observability layer that:
All logic is generated at compile time — no runtime reflection.
Key Features
1. Automatic Model Generation
Given a Colyseus
Schema, macros generate a corresponding observable model:State<T>ObservableArray/ObservableMapNo manual mapping or glue code required.
2. Declarative Reactivity
Consumers interact with:
Instead of manually managing:
onChangelistenonAdd/onRemove3. Macro-Generated Wiring
Macros generate deterministic, optimized wiring code:
onChangeper collectionCallback complexity is fully hidden from the user.
4. Zero Runtime Overhead
All logic is expanded at compile time.
Example (Conceptual)
Generated observable model (conceptually):
{ phase: State<String>, players: ObservableArray<{ id: State<String>, score: State<Int> }> }All Colyseus listeners are generated and wired automatically.
Advantages and Trade-offs
Pros
Cons
These trade-offs are acceptable for medium-to-large or long-lived projects.
Required Dependencies
Dependency stack:
tink_state(observable primitives)tink_json(optional,Serialized<T>support)tink_coreTarget Use Cases
Non-Goals
Open Questions
Conclusion
A macro-driven observability layer can significantly improve developer experience for Haxe Colyseus clients by shifting complexity from runtime to compile time.
It leverages Haxe’s strengths - macros, typing, and code generation - to transform Colyseus’ low-level callbacks into a high-level reactive model, without sacrificing performance.
Beta Was this translation helpful? Give feedback.
All reactions