@couch-kit/runtime 0.2.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,38 @@
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
+
3
36
  ## 0.2.0
4
37
 
5
38
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -565,20 +565,34 @@ class GameHostRuntime {
565
565
  return project ? project(this.state, playerId) : this.state;
566
566
  }
567
567
  broadcastState = () => {
568
- if (!this.transport)
568
+ const transport = this.transport;
569
+ if (!transport)
569
570
  return;
570
571
  const actions = this.actionQueue;
571
572
  this.actionQueue = [];
572
573
  this.stateDirty = false;
573
574
  if (!this.config.project) {
574
- this.transport.broadcast(createStateUpdateMessage(this.state, actions));
575
+ transport.broadcast(createStateUpdateMessage(this.state, actions));
575
576
  return;
576
577
  }
578
+ const entries = [];
577
579
  for (const connectionId of this.joinedConnections) {
578
580
  const playerId = this.sessionManager.getPlayerIdForSocket(connectionId);
579
581
  if (!playerId)
580
582
  continue;
581
- this.send(connectionId, createStateUpdateMessage(this.viewFor(playerId), actions));
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);
582
596
  }
583
597
  };
584
598
  send(connectionId, message) {
package/dist/index.js CHANGED
@@ -520,20 +520,34 @@ class GameHostRuntime {
520
520
  return project ? project(this.state, playerId) : this.state;
521
521
  }
522
522
  broadcastState = () => {
523
- if (!this.transport)
523
+ const transport = this.transport;
524
+ if (!transport)
524
525
  return;
525
526
  const actions = this.actionQueue;
526
527
  this.actionQueue = [];
527
528
  this.stateDirty = false;
528
529
  if (!this.config.project) {
529
- this.transport.broadcast(createStateUpdateMessage(this.state, actions));
530
+ transport.broadcast(createStateUpdateMessage(this.state, actions));
530
531
  return;
531
532
  }
533
+ const entries = [];
532
534
  for (const connectionId of this.joinedConnections) {
533
535
  const playerId = this.sessionManager.getPlayerIdForSocket(connectionId);
534
536
  if (!playerId)
535
537
  continue;
536
- this.send(connectionId, createStateUpdateMessage(this.viewFor(playerId), actions));
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);
537
551
  }
538
552
  };
539
553
  send(connectionId, message) {
package/lib/runtime.d.ts CHANGED
@@ -1,8 +1,24 @@
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> {
@@ -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":"AAAA,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,UAAU,EAEhB,MAAM,iBAAiB,CAAC;AAczB,kFAAkF;AAClF,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,8EAA8E;AAC9E,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IACvD,SAAS,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IACtC;;;;;;;;;OASG;IACH,QAAQ,CAAC,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAAC;CACvD;AAED,4EAA4E;AAC5E,MAAM,WAAW,qBAAqB,CACpC,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,OAAO;IAEjB,YAAY,EAAE,CAAC,CAAC;IAChB,OAAO,EAAE,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3B;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAClD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sDAAsD;IACtD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED;;;GAGG;AACH,qBAAa,eAAe,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IAClE,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwC;IAChE,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA2B;IACvD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA8B;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;IACvD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,WAAW,CAAiB;IACpC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,wBAAwB,CAAK;IAErC,YACE,MAAM,EAAE,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC,EACnC,SAAS,GAAE,oBAAoB,GAAG,IAAW,EAa9C;IAED,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,QAAO,CAAC,CAAe;IAExC,6CAA6C;IAC7C,QAAQ,CAAC,SAAS,aAAc,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CAKvD;IAEF,0EAA0E;IAC1E,YAAY,CAAC,SAAS,EAAE,oBAAoB,GAAG,IAAI,GAAG,IAAI,CAKzD;IAED,2EAA2E;IAC3E,YAAY,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAKtD;IAED,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,WAAY,CAAC,KAAG,IAAI,CAEnC;IAEF,8EAA8E;IAC9E,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAG3C;IAED,kEAAkE;IAC5D,aAAa,CACjB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,OAAO,GAClB,OAAO,CAAC,IAAI,CAAC,CAqLf;IAED,4EAA4E;IAC5E,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CA+B3C;IAED,wEAAwE;IACxE,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAG9B;IAED,oEAAoE;IACpE,IAAI,IAAI,IAAI,CAQX;IAED,OAAO,CAAC,WAAW;IAcnB;;;OAGG;IACH,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,QAAQ,CAAC,cAAc,CAmC7B;IAEF,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,uBAAuB;IAgB/B,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,GAAG;CAKZ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@couch-kit/runtime",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": true
@@ -56,7 +56,7 @@
56
56
  "clean": "rm -rf dist lib"
57
57
  },
58
58
  "dependencies": {
59
- "@couch-kit/core": "0.9.3"
59
+ "@couch-kit/core": "0.10.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "typescript": "^7.0.0"
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. */
@@ -404,27 +421,39 @@ export class GameHostRuntime<S extends IGameState, A extends IAction> {
404
421
  }
405
422
 
406
423
  private readonly broadcastState = (): void => {
407
- if (!this.transport) return;
424
+ const transport = this.transport;
425
+ if (!transport) return;
408
426
 
409
427
  const actions = this.actionQueue;
410
428
  this.actionQueue = [];
411
429
  this.stateDirty = false;
412
430
 
413
431
  if (!this.config.project) {
414
- this.transport.broadcast(createStateUpdateMessage(this.state, actions));
432
+ transport.broadcast(createStateUpdateMessage(this.state, actions));
415
433
  return;
416
434
  }
417
435
 
418
436
  // Projected games get one message per connection rather than a broadcast:
419
- // a shared frame cannot carry different views. Costs N sends instead of 1,
420
- // which is bounded by players-per-room.
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[] = [];
421
441
  for (const connectionId of this.joinedConnections) {
422
442
  const playerId = this.sessionManager.getPlayerIdForSocket(connectionId);
423
443
  if (!playerId) continue;
424
- this.send(
444
+ entries.push({
425
445
  connectionId,
426
- createStateUpdateMessage(this.viewFor(playerId), actions),
427
- );
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);
428
457
  }
429
458
  };
430
459