@couch-kit/runtime 0.1.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # @couch-kit/runtime
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#164](https://github.com/faluciano/react-native-couch-kit/pull/164) [`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e) Thanks [@faluciano](https://github.com/faluciano)! - Send a projected state update as one relay frame instead of one per player
8
+
9
+ A game with a `project` function sends every player their own view, which meant
10
+ one WebSocket frame per player for every state change. Relays bill and
11
+ rate-limit per inbound frame, so a four-player table paid four messages for one
12
+ update and spent four of the display's 30-per-second budget.
13
+
14
+ `GameRuntimeTransport` gains an optional `sendMany(entries)`. When a transport
15
+ implements it, the runtime hands over the whole projected batch at once;
16
+ transports that do not — the LAN WebSocket path — keep receiving one `send` per
17
+ connection and are unaffected.
18
+
19
+ `RelayDisplayHost` implements it with a new `DATA_MULTI` envelope carrying a
20
+ peer-id-to-payload map, which the relay unpacks into ordinary `DATA` frames.
21
+ Phones need no update — nothing on the client side can tell a batched update
22
+ from a unicast one. If the combined frame would exceed the relay's 256KB
23
+ ceiling, the display falls back to individual frames rather than send something
24
+ the relay would drop.
25
+
26
+ Relays must be updated before displays: both bundled implementations
27
+ (`services/relay`, `services/relay-worker`) understand `DATA_MULTI`, and an
28
+ older relay answers it with `MALFORMED`. The type is host-only — a phone sending
29
+ it is rejected, so it cannot be used to reach another phone directly.
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies [[`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e)]:
34
+ - @couch-kit/core@0.10.0
35
+
36
+ ## 0.2.0
37
+
38
+ ### Minor Changes
39
+
40
+ - [#155](https://github.com/faluciano/react-native-couch-kit/pull/155) [`4f297a1`](https://github.com/faluciano/react-native-couch-kit/commit/4f297a19c442541703c3bee7bee26354ae3476a4) Thanks [@faluciano](https://github.com/faluciano)! - Hidden information can now be hidden for real: `GameHostRuntimeConfig` takes an
41
+ optional `project(state, playerId)` that narrows the authoritative state to what
42
+ one player may see.
43
+
44
+ Without it the runtime broadcasts the same state to everyone, so hiding a hand
45
+ depends on the client choosing not to look — any player could read opponents'
46
+ cards from devtools. With it, the data never reaches their device: the runtime
47
+ sends each connection its own projection, in `WELCOME`, `RECONNECTED`, and every
48
+ state update.
49
+
50
+ Games with no hidden information omit `project` and are unchanged — state is
51
+ still broadcast in one frame.
52
+
53
+ Because a projected client holds a _view_ rather than the whole state, it cannot
54
+ run the game reducer, so `ClientConfig.reducer` is now optional. Omit it and the
55
+ client renders what the host sends (no optimistic updates); a round trip is
56
+ imperceptible for turn-based games, and it is what makes the guarantee real.
57
+
3
58
  ## 0.1.0
4
59
 
5
60
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -434,7 +434,7 @@ class GameHostRuntime {
434
434
  type: import_core5.MessageTypes.RECONNECTED,
435
435
  payload: {
436
436
  playerId,
437
- state: this.state
437
+ state: this.viewFor(playerId)
438
438
  }
439
439
  });
440
440
  } else {
@@ -442,7 +442,7 @@ class GameHostRuntime {
442
442
  type: import_core5.MessageTypes.WELCOME,
443
443
  payload: {
444
444
  playerId,
445
- state: this.state,
445
+ state: this.viewFor(playerId),
446
446
  serverTime: Date.now()
447
447
  }
448
448
  });
@@ -560,13 +560,40 @@ class GameHostRuntime {
560
560
  }
561
561
  this.broadcastScheduler.schedule(this.broadcastState);
562
562
  }
563
+ viewFor(playerId) {
564
+ const project = this.config.project;
565
+ return project ? project(this.state, playerId) : this.state;
566
+ }
563
567
  broadcastState = () => {
564
- if (!this.transport)
568
+ const transport = this.transport;
569
+ if (!transport)
565
570
  return;
566
571
  const actions = this.actionQueue;
567
572
  this.actionQueue = [];
568
573
  this.stateDirty = false;
569
- this.transport.broadcast(createStateUpdateMessage(this.state, actions));
574
+ if (!this.config.project) {
575
+ transport.broadcast(createStateUpdateMessage(this.state, actions));
576
+ return;
577
+ }
578
+ const entries = [];
579
+ for (const connectionId of this.joinedConnections) {
580
+ const playerId = this.sessionManager.getPlayerIdForSocket(connectionId);
581
+ if (!playerId)
582
+ continue;
583
+ entries.push({
584
+ connectionId,
585
+ message: createStateUpdateMessage(this.viewFor(playerId), actions)
586
+ });
587
+ }
588
+ if (entries.length === 0)
589
+ return;
590
+ if (transport.sendMany) {
591
+ transport.sendMany(entries);
592
+ return;
593
+ }
594
+ for (const { connectionId, message } of entries) {
595
+ this.send(connectionId, message);
596
+ }
570
597
  };
571
598
  send(connectionId, message) {
572
599
  this.transport?.send(connectionId, message);
package/dist/index.js CHANGED
@@ -389,7 +389,7 @@ class GameHostRuntime {
389
389
  type: MessageTypes3.RECONNECTED,
390
390
  payload: {
391
391
  playerId,
392
- state: this.state
392
+ state: this.viewFor(playerId)
393
393
  }
394
394
  });
395
395
  } else {
@@ -397,7 +397,7 @@ class GameHostRuntime {
397
397
  type: MessageTypes3.WELCOME,
398
398
  payload: {
399
399
  playerId,
400
- state: this.state,
400
+ state: this.viewFor(playerId),
401
401
  serverTime: Date.now()
402
402
  }
403
403
  });
@@ -515,13 +515,40 @@ class GameHostRuntime {
515
515
  }
516
516
  this.broadcastScheduler.schedule(this.broadcastState);
517
517
  }
518
+ viewFor(playerId) {
519
+ const project = this.config.project;
520
+ return project ? project(this.state, playerId) : this.state;
521
+ }
518
522
  broadcastState = () => {
519
- if (!this.transport)
523
+ const transport = this.transport;
524
+ if (!transport)
520
525
  return;
521
526
  const actions = this.actionQueue;
522
527
  this.actionQueue = [];
523
528
  this.stateDirty = false;
524
- this.transport.broadcast(createStateUpdateMessage(this.state, actions));
529
+ if (!this.config.project) {
530
+ transport.broadcast(createStateUpdateMessage(this.state, actions));
531
+ return;
532
+ }
533
+ const entries = [];
534
+ for (const connectionId of this.joinedConnections) {
535
+ const playerId = this.sessionManager.getPlayerIdForSocket(connectionId);
536
+ if (!playerId)
537
+ continue;
538
+ entries.push({
539
+ connectionId,
540
+ message: createStateUpdateMessage(this.viewFor(playerId), actions)
541
+ });
542
+ }
543
+ if (entries.length === 0)
544
+ return;
545
+ if (transport.sendMany) {
546
+ transport.sendMany(entries);
547
+ return;
548
+ }
549
+ for (const { connectionId, message } of entries) {
550
+ this.send(connectionId, message);
551
+ }
525
552
  };
526
553
  send(connectionId, message) {
527
554
  this.transport?.send(connectionId, message);
@@ -1 +1 @@
1
- {"version":3,"file":"broadcast-scheduler.d.ts","sourceRoot":"","sources":["../src/broadcast-scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEjE,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,MAAM,MAAM,kBAAkB,GAAG,OAAO,CACtC,WAAW,EACX;IAAE,IAAI,EAAE,OAAO,YAAY,CAAC,YAAY,CAAA;CAAE,CAC3C,CAAC;AAEF,MAAM,WAAW,cAAc,CAAC,MAAM;IACpC,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1D,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAOD,MAAM,WAAW,yBAAyB,CAAC,MAAM;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACpC;AAED;;;GAGG;AACH,qBAAa,kBAAkB,CAAC,MAAM,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC;IACpE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,KAAK,CAAuB;gBAExB,OAAO,GAAE,yBAAyB,CAAC,MAAM,CAAM;IAO3D,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAQpC,MAAM,IAAI,IAAI;IAOd,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI;IAIjD,mBAAmB,IAAI,OAAO;CAG/B;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,SAAS,GAAE,MAAmB,GAC7B,kBAAkB,CAapB"}
1
+ {"version":3,"file":"broadcast-scheduler.d.ts","sourceRoot":"","sources":["../src/broadcast-scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEjE,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,MAAM,MAAM,kBAAkB,GAAG,OAAO,CACtC,WAAW,EACX;IAAE,IAAI,EAAE,OAAO,YAAY,CAAC,YAAY,CAAA;CAAE,CAC3C,CAAC;AAEF,MAAM,WAAW,cAAc,CAAC,MAAM;IACpC,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1D,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAOD,MAAM,WAAW,yBAAyB,CAAC,MAAM;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACpC;AAED;;;GAGG;AACH,qBAAa,kBAAkB,CAAC,MAAM,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC;IACpE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,KAAK,CAAuB;IAEpC,YAAY,OAAO,GAAE,yBAAyB,CAAC,MAAM,CAAM,EAK1D;IAED,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAMnC;IAED,MAAM,IAAI,IAAI,CAKb;IAED,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAEhD;IAED,mBAAmB,IAAI,OAAO,CAE7B;CACF;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,SAAS,GAAE,MAAmB,GAC7B,kBAAkB,CAapB"}
@@ -1 +1 @@
1
- {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,uCAAuC;AACvC,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;gBAE/C,OAAO,GAAE,wBAA6B;IAMlD,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe;IAiBzC,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI7B,KAAK,IAAI,IAAI;IAIb,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;CAGjD"}
1
+ {"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,uCAAuC;AACvC,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;IAE3D,YAAY,OAAO,GAAE,wBAA6B,EAIjD;IAED,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAexC;IAED,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE5B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAE/C;CACF"}
package/lib/runtime.d.ts CHANGED
@@ -1,13 +1,49 @@
1
1
  import { type GameReducer, type HostMessage, type IAction, type IGameState } from "@couch-kit/core";
2
+ /** One connection's share of a {@link GameRuntimeTransport.sendMany} delivery. */
3
+ export interface AddressedMessage {
4
+ connectionId: string;
5
+ message: HostMessage;
6
+ }
2
7
  /** Minimal message-delivery surface required by the authoritative runtime. */
3
8
  export interface GameRuntimeTransport {
4
9
  send(connectionId: string, message: HostMessage): void;
5
10
  broadcast(message: HostMessage): void;
11
+ /**
12
+ * Delivers a batch of per-connection messages, for transports that can carry
13
+ * them in one frame.
14
+ *
15
+ * Optional: the runtime falls back to a {@link GameRuntimeTransport.send} per
16
+ * entry, which is what a plain LAN WebSocket transport wants anyway. It earns
17
+ * its keep on the relay, where each frame the display sends is separately
18
+ * billed and rate-limited, so a projected state update costs one message
19
+ * rather than one per player.
20
+ */
21
+ sendMany?(entries: readonly AddressedMessage[]): void;
6
22
  }
7
23
  /** Configuration shared by every authoritative Couch Kit host transport. */
8
24
  export interface GameHostRuntimeConfig<S extends IGameState, A extends IAction> {
9
25
  initialState: S;
10
26
  reducer: GameReducer<S, A>;
27
+ /**
28
+ * Narrows the authoritative state to what one player is allowed to see.
29
+ *
30
+ * Without it, every player receives the whole state and any hiding is left to
31
+ * the client — which makes hidden information a convention rather than a
32
+ * rule. With it, the data a player must not see never reaches their device:
33
+ * the runtime sends each connection its own projection instead of one shared
34
+ * broadcast.
35
+ *
36
+ * Omit for games with no hidden information; state is broadcast as before.
37
+ *
38
+ * The projection is what clients receive, so their state type is the *view*,
39
+ * not `S`. A client cannot run the game reducer over a partial view, so
40
+ * `useGameClient` must be used without a `reducer` (no optimistic updates)
41
+ * when a projection is configured.
42
+ *
43
+ * @param state - Current authoritative state.
44
+ * @param playerId - Player the projection is for.
45
+ */
46
+ project?: (state: S, playerId: string) => unknown;
11
47
  debug?: boolean;
12
48
  /** Timeout before a disconnected player is permanently removed. */
13
49
  disconnectTimeout?: number;
@@ -59,6 +95,11 @@ export declare class GameHostRuntime<S extends IGameState, A extends IAction> {
59
95
  /** Cancels runtime timers and releases transport-specific state. */
60
96
  stop(): void;
61
97
  private applyAction;
98
+ /**
99
+ * What `playerId` is allowed to see. Identity unless a projection is
100
+ * configured, so games without hidden information are unaffected.
101
+ */
102
+ private viewFor;
62
103
  private readonly broadcastState;
63
104
  private send;
64
105
  private invokeLifecycleCallback;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@couch-kit/runtime",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": true
@@ -52,14 +52,13 @@
52
52
  "build": "bun build ./src/index.ts --outdir ./dist --target browser --external @couch-kit/core && bun build ./src/index.ts --outfile ./dist/index.cjs --target browser --format cjs --external @couch-kit/core && tsc -p tsconfig.build.json",
53
53
  "prepublishOnly": "bun run build",
54
54
  "test": "bun test",
55
- "lint": "eslint src/",
56
55
  "typecheck": "tsc --noEmit",
57
56
  "clean": "rm -rf dist lib"
58
57
  },
59
58
  "dependencies": {
60
- "@couch-kit/core": "0.9.3"
59
+ "@couch-kit/core": "0.10.0"
61
60
  },
62
61
  "devDependencies": {
63
- "typescript": "^6.0.0"
62
+ "typescript": "^7.0.0"
64
63
  }
65
64
  }
package/src/runtime.ts CHANGED
@@ -23,10 +23,27 @@ import {
23
23
  type JoinSessionPayload,
24
24
  } from "./session-manager.js";
25
25
 
26
+ /** One connection's share of a {@link GameRuntimeTransport.sendMany} delivery. */
27
+ export interface AddressedMessage {
28
+ connectionId: string;
29
+ message: HostMessage;
30
+ }
31
+
26
32
  /** Minimal message-delivery surface required by the authoritative runtime. */
27
33
  export interface GameRuntimeTransport {
28
34
  send(connectionId: string, message: HostMessage): void;
29
35
  broadcast(message: HostMessage): void;
36
+ /**
37
+ * Delivers a batch of per-connection messages, for transports that can carry
38
+ * them in one frame.
39
+ *
40
+ * Optional: the runtime falls back to a {@link GameRuntimeTransport.send} per
41
+ * entry, which is what a plain LAN WebSocket transport wants anyway. It earns
42
+ * its keep on the relay, where each frame the display sends is separately
43
+ * billed and rate-limited, so a projected state update costs one message
44
+ * rather than one per player.
45
+ */
46
+ sendMany?(entries: readonly AddressedMessage[]): void;
30
47
  }
31
48
 
32
49
  /** Configuration shared by every authoritative Couch Kit host transport. */
@@ -36,6 +53,26 @@ export interface GameHostRuntimeConfig<
36
53
  > {
37
54
  initialState: S;
38
55
  reducer: GameReducer<S, A>;
56
+ /**
57
+ * Narrows the authoritative state to what one player is allowed to see.
58
+ *
59
+ * Without it, every player receives the whole state and any hiding is left to
60
+ * the client — which makes hidden information a convention rather than a
61
+ * rule. With it, the data a player must not see never reaches their device:
62
+ * the runtime sends each connection its own projection instead of one shared
63
+ * broadcast.
64
+ *
65
+ * Omit for games with no hidden information; state is broadcast as before.
66
+ *
67
+ * The projection is what clients receive, so their state type is the *view*,
68
+ * not `S`. A client cannot run the game reducer over a partial view, so
69
+ * `useGameClient` must be used without a `reducer` (no optimistic updates)
70
+ * when a projection is configured.
71
+ *
72
+ * @param state - Current authoritative state.
73
+ * @param playerId - Player the projection is for.
74
+ */
75
+ project?: (state: S, playerId: string) => unknown;
39
76
  debug?: boolean;
40
77
  /** Timeout before a disconnected player is permanently removed. */
41
78
  disconnectTimeout?: number;
@@ -215,7 +252,7 @@ export class GameHostRuntime<S extends IGameState, A extends IAction> {
215
252
  type: MessageTypes.RECONNECTED,
216
253
  payload: {
217
254
  playerId,
218
- state: this.state,
255
+ state: this.viewFor(playerId),
219
256
  },
220
257
  });
221
258
  } else {
@@ -223,7 +260,7 @@ export class GameHostRuntime<S extends IGameState, A extends IAction> {
223
260
  type: MessageTypes.WELCOME,
224
261
  payload: {
225
262
  playerId,
226
- state: this.state,
263
+ state: this.viewFor(playerId),
227
264
  serverTime: Date.now(),
228
265
  },
229
266
  });
@@ -374,13 +411,50 @@ export class GameHostRuntime<S extends IGameState, A extends IAction> {
374
411
  this.broadcastScheduler.schedule(this.broadcastState);
375
412
  }
376
413
 
414
+ /**
415
+ * What `playerId` is allowed to see. Identity unless a projection is
416
+ * configured, so games without hidden information are unaffected.
417
+ */
418
+ private viewFor(playerId: string): unknown {
419
+ const project = this.config.project;
420
+ return project ? project(this.state, playerId) : this.state;
421
+ }
422
+
377
423
  private readonly broadcastState = (): void => {
378
- if (!this.transport) return;
424
+ const transport = this.transport;
425
+ if (!transport) return;
379
426
 
380
427
  const actions = this.actionQueue;
381
428
  this.actionQueue = [];
382
429
  this.stateDirty = false;
383
- this.transport.broadcast(createStateUpdateMessage(this.state, actions));
430
+
431
+ if (!this.config.project) {
432
+ transport.broadcast(createStateUpdateMessage(this.state, actions));
433
+ return;
434
+ }
435
+
436
+ // Projected games get one message per connection rather than a broadcast:
437
+ // a shared frame cannot carry different views. Transports that can batch
438
+ // (see GameRuntimeTransport.sendMany) still put them on the wire as a
439
+ // single frame; the rest fall back to N sends, bounded by players-per-room.
440
+ const entries: AddressedMessage[] = [];
441
+ for (const connectionId of this.joinedConnections) {
442
+ const playerId = this.sessionManager.getPlayerIdForSocket(connectionId);
443
+ if (!playerId) continue;
444
+ entries.push({
445
+ connectionId,
446
+ message: createStateUpdateMessage(this.viewFor(playerId), actions),
447
+ });
448
+ }
449
+ if (entries.length === 0) return;
450
+
451
+ if (transport.sendMany) {
452
+ transport.sendMany(entries);
453
+ return;
454
+ }
455
+ for (const { connectionId, message } of entries) {
456
+ this.send(connectionId, message);
457
+ }
384
458
  };
385
459
 
386
460
  private send(connectionId: string, message: HostMessage): void {