bonescript-compiler 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export { FullEmitter } from "./emit_full";
18
18
  export { NakamaEmitter } from "./emit_nakama";
19
19
  export { PrismaEmitter } from "./emit_prisma";
20
20
  export { SqliteEmitter } from "./emit_sqlite";
21
+ export { ColyseusEmitter } from "./emit_colyseus";
21
22
  export type { NakamaEmittedFile } from "./emit_nakama";
22
23
  export type { EmittedFile } from "./emitter";
23
24
  export { Verifier } from "./verifier";
package/src/lowering.ts CHANGED
@@ -17,6 +17,27 @@ function toSnakeCase(s: string): string {
17
17
  return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
18
18
  }
19
19
 
20
+ /**
21
+ * Extract the entity name from a `participants:` type expression.
22
+ *
23
+ * - `Player` → "Player"
24
+ * - `set<Player>` → "Player"
25
+ * - `list<Player>` → "Player"
26
+ * - `optional<Player>` → "Player"
27
+ * - anything else → null
28
+ */
29
+ function extractParticipantEntity(t: AST.TypeExprNode | null): string | null {
30
+ if (!t) return null;
31
+ if (t.kind === "EntityRefType") return t.name;
32
+ if (t.kind === "GenericType") {
33
+ for (const arg of t.typeArgs) {
34
+ const inner = extractParticipantEntity(arg);
35
+ if (inner) return inner;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+
20
41
  // ─── Deterministic ID Generation ─────────────────────────────────────────────
21
42
 
22
43
  function makeId(systemName: string, kind: string, name: string): string {
@@ -122,11 +143,30 @@ export class Lowering {
122
143
  }
123
144
 
124
145
  // Lower channels → realtime_service modules
146
+ // Lower channels — realtime_service modules.
147
+ //
148
+ // Capabilities tagged `sync: realtime` are routed into the channel that
149
+ // matches their participant entity, so each room can expose them as typed
150
+ // onMessage handlers. The matching rule: if any of the capability's
151
+ // parameters is the participant entity type (e.g. channel has
152
+ // `participants: set<Player>` and capability has `move(player: Player, ...)`),
153
+ // it gets dispatched on that room.
154
+ const realtimeCaps = capabilities.filter(c => c.sync === "realtime");
125
155
  for (const channel of channels) {
126
- modules.push(this.lowerChannel(channel));
156
+ const channelCaps = realtimeCaps.filter(c => {
157
+ const partEntityName = extractParticipantEntity(channel.participants);
158
+ if (!partEntityName) return false;
159
+ return c.params.some(p => {
160
+ if (p.type.kind === "EntityRefType") return p.type.name === partEntityName;
161
+ // Also match generic types like `set<Player>` if they happen to appear as a param
162
+ if (p.type.kind === "GenericType") {
163
+ return p.type.typeArgs.some(a => a.kind === "EntityRefType" && a.name === partEntityName);
164
+ }
165
+ return false;
166
+ });
167
+ });
168
+ modules.push(this.lowerChannel(channel, channelCaps, entities));
127
169
  }
128
-
129
- // Lower events
130
170
  for (const ev of eventDecls) {
131
171
  events.push(this.lowerEvent(ev));
132
172
  }
@@ -516,18 +556,48 @@ export class Lowering {
516
556
 
517
557
  // ─── Channel Lowering ──────────────────────────────────────────────────────
518
558
 
519
- private lowerChannel(channel: AST.ChannelDeclNode): IR.IRModule {
559
+ private lowerChannel(
560
+ channel: AST.ChannelDeclNode,
561
+ realtimeCapabilities: AST.CapabilityDeclNode[] = [],
562
+ entities: AST.EntityDeclNode[] = [],
563
+ ): IR.IRModule {
564
+ // Default lifecycle methods that every Colyseus/WebSocket channel exposes.
565
+ const baseMethods: IR.IRMethod[] = [
566
+ { name: "connect", input: [], output: "connection", preconditions: [], effects: [], emissions: [], idempotent: false, authenticated: true, timeout_ms: 5000, retry: null, pipeline: null, algorithm: null, sync: null },
567
+ { name: "subscribe", input: [{ name: "topic", type: "string", nullable: false, unique: false, indexed: false, default_value: null }], output: "subscription", preconditions: [], effects: [], emissions: [], idempotent: true, authenticated: true, timeout_ms: 5000, retry: null, pipeline: null, algorithm: null, sync: null },
568
+ { name: "publish", input: [{ name: "message", type: "json", nullable: false, unique: false, indexed: false, default_value: null }], output: "void", preconditions: [], effects: [], emissions: [], idempotent: false, authenticated: true, timeout_ms: 5000, retry: null, pipeline: null, algorithm: null, sync: null },
569
+ ];
570
+
571
+ // Realtime capabilities lower as additional interface methods. The
572
+ // Colyseus emitter will turn these into typed onMessage handlers.
573
+ const capMethods = realtimeCapabilities.map(c => this.lowerCapability(c));
574
+
575
+ const participantEntity = extractParticipantEntity(channel.participants);
576
+
577
+ // If we have an entity reference, copy its field definitions into the
578
+ // channel's config so the Colyseus emitter can build a Player schema
579
+ // without re-resolving the AST. We only forward primitive-typed `owns:`
580
+ // fields — entity refs and complex types are skipped because Colyseus
581
+ // schemas need scalar @type decorators.
582
+ const participantFields: { name: string; type: string }[] = [];
583
+ if (participantEntity) {
584
+ const entity = entities.find(e => e.name === participantEntity);
585
+ if (entity) {
586
+ for (const f of entity.owns) {
587
+ if (f.type.kind === "PrimitiveType") {
588
+ participantFields.push({ name: f.name, type: f.type.name });
589
+ }
590
+ }
591
+ }
592
+ }
593
+
520
594
  return {
521
595
  id: makeId(this.systemName, "realtime_service", channel.name),
522
596
  kind: "realtime_service",
523
597
  name: channel.name,
524
598
  interfaces: [{
525
599
  name: `I${channel.name}Channel`,
526
- methods: [
527
- { name: "connect", input: [], output: "connection", preconditions: [], effects: [], emissions: [], idempotent: false, authenticated: true, timeout_ms: 5000, retry: null, pipeline: null, algorithm: null, sync: null },
528
- { name: "subscribe", input: [{ name: "topic", type: "string", nullable: false, unique: false, indexed: false, default_value: null }], output: "subscription", preconditions: [], effects: [], emissions: [], idempotent: true, authenticated: true, timeout_ms: 5000, retry: null, pipeline: null, algorithm: null, sync: null },
529
- { name: "publish", input: [{ name: "message", type: "json", nullable: false, unique: false, indexed: false, default_value: null }], output: "void", preconditions: [], effects: [], emissions: [], idempotent: false, authenticated: true, timeout_ms: 5000, retry: null, pipeline: null, algorithm: null, sync: null },
530
- ],
600
+ methods: [...baseMethods, ...capMethods],
531
601
  }],
532
602
  models: [],
533
603
  events: [],
@@ -539,6 +609,8 @@ export class Lowering {
539
609
  ordering: channel.ordering || "fifo",
540
610
  persistence: channel.persistence || "none",
541
611
  max_size: channel.maxSize || 10000,
612
+ ...(participantEntity ? { participant_entity: participantEntity } : {}),
613
+ ...(participantFields.length > 0 ? { participant_fields: JSON.stringify(participantFields) } : {}),
542
614
  },
543
615
  };
544
616
  }