@couch-kit/display 0.3.0 → 0.4.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,40 @@
1
1
  # @couch-kit/display
2
2
 
3
+ ## 0.4.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), [`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e)]:
34
+ - @couch-kit/runtime@0.3.0
35
+ - @couch-kit/client@0.14.0
36
+ - @couch-kit/core@0.10.0
37
+
3
38
  ## 0.3.0
4
39
 
5
40
  ### Minor Changes
package/dist/index.js CHANGED
@@ -39,7 +39,8 @@ class RelayDisplayHost {
39
39
  this.ws.onerror = (event) => this.runtime.handleError(event instanceof Error ? event : new Error("Relay socket error"));
40
40
  const transport = {
41
41
  send: (connectionId, message) => this.sendEnvelope(message, connectionId),
42
- broadcast: (message) => this.sendEnvelope(message)
42
+ broadcast: (message) => this.sendEnvelope(message),
43
+ sendMany: (entries) => this.sendMultiEnvelope(entries)
43
44
  };
44
45
  this.runtime.setTransport(transport);
45
46
  }
@@ -54,6 +55,24 @@ class RelayDisplayHost {
54
55
  this.runtime.stop();
55
56
  this.ws.close();
56
57
  }
58
+ sendMultiEnvelope(entries) {
59
+ const payloads = {};
60
+ for (const { connectionId, message } of entries) {
61
+ payloads[connectionId] = JSON.stringify(message);
62
+ }
63
+ const frame = JSON.stringify({
64
+ type: RelayMessageTypes.DATA_MULTI,
65
+ roomId: this.assignedRoomId ?? undefined,
66
+ payloads
67
+ });
68
+ if (frameByteLength(frame) > DEFAULT_MAX_MESSAGE_BYTES) {
69
+ for (const { connectionId, message } of entries) {
70
+ this.sendEnvelope(message, connectionId);
71
+ }
72
+ return;
73
+ }
74
+ this.ws.send(frame);
75
+ }
57
76
  sendEnvelope(message, to) {
58
77
  const envelope = {
59
78
  type: RelayMessageTypes.DATA,
@@ -68,6 +68,21 @@ export declare class RelayDisplayHost<S extends IGameState, A extends IAction> {
68
68
  dispatch: (action: A) => void;
69
69
  /** Tear down the runtime and relay socket. */
70
70
  stop(): void;
71
+ /**
72
+ * Sends per-connection messages as one `DATA_MULTI` frame, which the relay
73
+ * unpacks into an ordinary `DATA` frame per phone.
74
+ *
75
+ * A projected game re-sends every player's view on every state change, so on
76
+ * a four-player table this is the difference between one billed relay message
77
+ * and four — and between one and four against the relay's per-connection rate
78
+ * limit, which the display shares across all its fan-out.
79
+ *
80
+ * Falls back to individual sends if the combined frame would exceed the
81
+ * relay's message ceiling: N views in one envelope is N times the bytes, and
82
+ * a frame the relay rejects delivers nothing to anyone. Splitting costs
83
+ * messages; being dropped costs the game.
84
+ */
85
+ private sendMultiEnvelope;
71
86
  private sendEnvelope;
72
87
  private handleRelayMessage;
73
88
  }
@@ -1 +1 @@
1
- {"version":3,"file":"relay-display-host.d.ts","sourceRoot":"","sources":["../src/relay-display-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,qBAAqB,EAE3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAe,MAAM,iBAAiB,CAAC;AAOxE;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,CAC9E,SAAQ,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC;IACnC,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACnE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAY;IAC/B,+EAA+E;IAC/E,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAA6B;IACzD,uDAAuD;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAE3C,YAAY,OAAO,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,EAwCjD;IAED;;;;OAIG;IACH,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAE5B;IAED,wCAAwC;IACxC,QAAQ,QAAO,CAAC,CAA4B;IAE5C,gFAAgF;IAChF,SAAS,aAAc,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CACb;IAEnC,8CAA8C;IAC9C,QAAQ,WAAY,CAAC,KAAG,IAAI,CAAkC;IAE9D,8CAA8C;IAC9C,IAAI,IAAI,IAAI,CAIX;IAED,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,kBAAkB;CA2C3B"}
1
+ {"version":3,"file":"relay-display-host.d.ts","sourceRoot":"","sources":["../src/relay-display-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,qBAAqB,EAE3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAe,MAAM,iBAAiB,CAAC;AAOxE;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB,CACtC,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,OAAO,CACjB,SAAQ,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC;IACnC,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACnE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAY;IAC/B,+EAA+E;IAC/E,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAA6B;IACzD,uDAAuD;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAE3C,YAAY,OAAO,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,EAwCjD;IAED;;;;OAIG;IACH,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAE5B;IAED,wCAAwC;IACxC,QAAQ,QAAO,CAAC,CAA4B;IAE5C,gFAAgF;IAChF,SAAS,aAAc,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CACb;IAEnC,8CAA8C;IAC9C,QAAQ,WAAY,CAAC,KAAG,IAAI,CAAkC;IAE9D,8CAA8C;IAC9C,IAAI,IAAI,IAAI,CAIX;IAED;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,kBAAkB;CA2C3B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@couch-kit/display",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": true
@@ -51,9 +51,9 @@
51
51
  "clean": "rm -rf dist lib"
52
52
  },
53
53
  "dependencies": {
54
- "@couch-kit/client": "0.13.0",
55
- "@couch-kit/core": "0.9.3",
56
- "@couch-kit/runtime": "0.2.0"
54
+ "@couch-kit/client": "0.14.0",
55
+ "@couch-kit/core": "0.10.0",
56
+ "@couch-kit/runtime": "0.3.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "typescript": "^7.0.0"
@@ -2,6 +2,7 @@ import {
2
2
  GameHostRuntime,
3
3
  frameByteLength,
4
4
  DEFAULT_MAX_MESSAGE_BYTES,
5
+ type AddressedMessage,
5
6
  type GameHostRuntimeConfig,
6
7
  type GameRuntimeTransport,
7
8
  } from "@couch-kit/runtime";
@@ -19,8 +20,10 @@ import {
19
20
  * object used by the RN-TV host) and the shared relay coordinates; the display
20
21
  * host owns the authoritative runtime and the relay socket.
21
22
  */
22
- export interface RelayDisplayHostOptions<S extends IGameState, A extends IAction>
23
- extends GameHostRuntimeConfig<S, A> {
23
+ export interface RelayDisplayHostOptions<
24
+ S extends IGameState,
25
+ A extends IAction,
26
+ > extends GameHostRuntimeConfig<S, A> {
24
27
  /** WebSocket URL of the shared relay server. */
25
28
  url: string;
26
29
  /**
@@ -103,9 +106,9 @@ export class RelayDisplayHost<S extends IGameState, A extends IAction> {
103
106
  );
104
107
 
105
108
  const transport: GameRuntimeTransport = {
106
- send: (connectionId, message) =>
107
- this.sendEnvelope(message, connectionId),
109
+ send: (connectionId, message) => this.sendEnvelope(message, connectionId),
108
110
  broadcast: (message) => this.sendEnvelope(message),
111
+ sendMany: (entries) => this.sendMultiEnvelope(entries),
109
112
  };
110
113
  this.runtime.setTransport(transport);
111
114
  }
@@ -136,6 +139,42 @@ export class RelayDisplayHost<S extends IGameState, A extends IAction> {
136
139
  this.ws.close();
137
140
  }
138
141
 
142
+ /**
143
+ * Sends per-connection messages as one `DATA_MULTI` frame, which the relay
144
+ * unpacks into an ordinary `DATA` frame per phone.
145
+ *
146
+ * A projected game re-sends every player's view on every state change, so on
147
+ * a four-player table this is the difference between one billed relay message
148
+ * and four — and between one and four against the relay's per-connection rate
149
+ * limit, which the display shares across all its fan-out.
150
+ *
151
+ * Falls back to individual sends if the combined frame would exceed the
152
+ * relay's message ceiling: N views in one envelope is N times the bytes, and
153
+ * a frame the relay rejects delivers nothing to anyone. Splitting costs
154
+ * messages; being dropped costs the game.
155
+ */
156
+ private sendMultiEnvelope(entries: readonly AddressedMessage[]): void {
157
+ const payloads: Record<string, string> = {};
158
+ for (const { connectionId, message } of entries) {
159
+ payloads[connectionId] = JSON.stringify(message);
160
+ }
161
+
162
+ const frame = JSON.stringify({
163
+ type: RelayMessageTypes.DATA_MULTI,
164
+ roomId: this.assignedRoomId ?? undefined,
165
+ payloads,
166
+ });
167
+
168
+ if (frameByteLength(frame) > DEFAULT_MAX_MESSAGE_BYTES) {
169
+ for (const { connectionId, message } of entries) {
170
+ this.sendEnvelope(message, connectionId);
171
+ }
172
+ return;
173
+ }
174
+
175
+ this.ws.send(frame);
176
+ }
177
+
139
178
  private sendEnvelope(message: HostMessage, to?: string): void {
140
179
  const envelope: Record<string, unknown> = {
141
180
  type: RelayMessageTypes.DATA,