@couch-kit/display 0.2.3 → 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,74 @@
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
+
38
+ ## 0.3.0
39
+
40
+ ### Minor Changes
41
+
42
+ - [#158](https://github.com/faluciano/react-native-couch-kit/pull/158) [`509ea7c`](https://github.com/faluciano/react-native-couch-kit/commit/509ea7c02aa6f2e56ebf01e781d6de74f0ded021) Thanks [@faluciano](https://github.com/faluciano)! - Let the relay assign room codes
43
+
44
+ `RelayDisplayHost`'s `roomId` is now optional. Omit it and the relay mints an
45
+ unused six-character code, reported through the new `onRoomCode` callback and
46
+ the `roomCode` getter.
47
+
48
+ A display could never check its own code for collisions — only the relay knows
49
+ which codes are live — so a self-chosen code could land on a game already in
50
+ progress, and did so only after it was on screen. Minted codes are drawn from
51
+ the CSPRNG over a 32-character alphabet without `O`/`0` or `I`/`1`, giving about
52
+ 1.07 billion codes.
53
+
54
+ Existing callers that pass `roomId` keep their current behaviour, including
55
+ `ROOM_EXISTS` when the code is taken. New callers should expect the code to
56
+ arrive one round trip after connecting rather than being known up front:
57
+
58
+ ```ts
59
+ const [roomCode, setRoomCode] = useState<string | null>(null);
60
+ new RelayDisplayHost({ url, onRoomCode: setRoomCode, reducer, initialState });
61
+ ```
62
+
63
+ Relays need a matching update to mint: both bundled implementations
64
+ (`services/relay`, `services/relay-worker`) support it. A display that omits
65
+ `roomId` against an older relay gets `MALFORMED`.
66
+
67
+ ### Patch Changes
68
+
69
+ - Updated dependencies [[`509ea7c`](https://github.com/faluciano/react-native-couch-kit/commit/509ea7c02aa6f2e56ebf01e781d6de74f0ded021)]:
70
+ - @couch-kit/client@0.13.0
71
+
3
72
  ## 0.2.3
4
73
 
5
74
  ### Patch Changes
package/dist/index.js CHANGED
@@ -12,17 +12,19 @@ import {
12
12
  class RelayDisplayHost {
13
13
  runtime;
14
14
  ws;
15
- roomId;
15
+ assignedRoomId;
16
+ onRoomCode;
16
17
  peers = new Set;
17
18
  constructor(options) {
18
- const { url, roomId, ...runtimeConfig } = options;
19
- this.roomId = roomId;
19
+ const { url, roomId, onRoomCode, ...runtimeConfig } = options;
20
+ this.assignedRoomId = roomId ?? null;
21
+ this.onRoomCode = onRoomCode;
20
22
  this.runtime = new GameHostRuntime(runtimeConfig);
21
23
  this.ws = new WebSocket(relayRoomUrl(url, roomId));
22
24
  this.ws.onopen = () => {
23
25
  this.ws.send(JSON.stringify({
24
26
  type: RelayMessageTypes.CREATE_ROOM,
25
- roomId: this.roomId
27
+ roomId: this.assignedRoomId ?? undefined
26
28
  }));
27
29
  };
28
30
  this.ws.onmessage = (event) => {
@@ -37,10 +39,14 @@ class RelayDisplayHost {
37
39
  this.ws.onerror = (event) => this.runtime.handleError(event instanceof Error ? event : new Error("Relay socket error"));
38
40
  const transport = {
39
41
  send: (connectionId, message) => this.sendEnvelope(message, connectionId),
40
- broadcast: (message) => this.sendEnvelope(message)
42
+ broadcast: (message) => this.sendEnvelope(message),
43
+ sendMany: (entries) => this.sendMultiEnvelope(entries)
41
44
  };
42
45
  this.runtime.setTransport(transport);
43
46
  }
47
+ get roomCode() {
48
+ return this.assignedRoomId;
49
+ }
44
50
  getState = () => this.runtime.getState();
45
51
  subscribe = (listener) => this.runtime.subscribe(listener);
46
52
  dispatch = (action) => this.runtime.dispatch(action);
@@ -49,10 +55,28 @@ class RelayDisplayHost {
49
55
  this.runtime.stop();
50
56
  this.ws.close();
51
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
+ }
52
76
  sendEnvelope(message, to) {
53
77
  const envelope = {
54
78
  type: RelayMessageTypes.DATA,
55
- roomId: this.roomId,
79
+ roomId: this.assignedRoomId ?? undefined,
56
80
  data: JSON.stringify(message)
57
81
  };
58
82
  if (to !== undefined)
@@ -83,6 +107,10 @@ class RelayDisplayHost {
83
107
  this.runtime.handleMessage(msg.from, parsed).catch((err) => this.runtime.handleError(err instanceof Error ? err : new Error(String(err))));
84
108
  break;
85
109
  }
110
+ case RelayMessageTypes.ROOM_CREATED:
111
+ this.assignedRoomId = msg.roomId;
112
+ this.onRoomCode?.(msg.roomId);
113
+ break;
86
114
  case RelayMessageTypes.ERROR:
87
115
  this.runtime.handleError(new Error(msg.message));
88
116
  break;
@@ -10,8 +10,23 @@ import type { IGameState, IAction } from "@couch-kit/core";
10
10
  export interface RelayDisplayHostOptions<S extends IGameState, A extends IAction> extends GameHostRuntimeConfig<S, A> {
11
11
  /** WebSocket URL of the shared relay server. */
12
12
  url: string;
13
- /** Room code phones will use to reach this display. */
14
- roomId: string;
13
+ /**
14
+ * Room code phones will use to reach this display.
15
+ *
16
+ * Omit it — the normal case — and the relay allocates one, reporting it via
17
+ * {@link RelayDisplayHostOptions.onRoomCode} and {@link RelayDisplayHost.roomCode}.
18
+ * Only the relay can tell whether a code is already in use, so a code chosen
19
+ * here may be rejected as `ROOM_EXISTS`; supply one only when something
20
+ * outside the relay already fixed it.
21
+ */
22
+ roomId?: string;
23
+ /**
24
+ * Called once the room exists and its code is known.
25
+ *
26
+ * A minted code is not available synchronously, so a display renders a
27
+ * placeholder until this fires — roughly a round trip to the relay.
28
+ */
29
+ onRoomCode?: (roomCode: string) => void;
15
30
  }
16
31
  /**
17
32
  * Browser **display host** for the cross-network relay transport.
@@ -33,10 +48,18 @@ export interface RelayDisplayHostOptions<S extends IGameState, A extends IAction
33
48
  export declare class RelayDisplayHost<S extends IGameState, A extends IAction> {
34
49
  private readonly runtime;
35
50
  private readonly ws;
36
- private readonly roomId;
51
+ /** Null until the relay confirms the room, when the code is relay-assigned. */
52
+ private assignedRoomId;
53
+ private readonly onRoomCode?;
37
54
  /** Connected phone connection ids (relay peer ids). */
38
55
  private readonly peers;
39
56
  constructor(options: RelayDisplayHostOptions<S, A>);
57
+ /**
58
+ * The room code phones join with, or `null` before the relay has assigned
59
+ * one. See {@link RelayDisplayHostOptions.onRoomCode} to be told when it
60
+ * arrives.
61
+ */
62
+ get roomCode(): string | null;
40
63
  /** Current authoritative game state. */
41
64
  getState: () => S;
42
65
  /** Subscribe to state changes (for `useSyncExternalStore` or manual render). */
@@ -45,6 +68,21 @@ export declare class RelayDisplayHost<S extends IGameState, A extends IAction> {
45
68
  dispatch: (action: A) => void;
46
69
  /** Tear down the runtime and relay socket. */
47
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;
48
86
  private sendEnvelope;
49
87
  private handleRelayMessage;
50
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,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;CAChB;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,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,uDAAuD;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAE3C,YAAY,OAAO,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,EAoCjD;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;IAUpB,OAAO,CAAC,kBAAkB;CAqC3B"}
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.2.3",
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.12.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,12 +20,29 @@ 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
- /** Room code phones will use to reach this display. */
27
- roomId: string;
29
+ /**
30
+ * Room code phones will use to reach this display.
31
+ *
32
+ * Omit it — the normal case — and the relay allocates one, reporting it via
33
+ * {@link RelayDisplayHostOptions.onRoomCode} and {@link RelayDisplayHost.roomCode}.
34
+ * Only the relay can tell whether a code is already in use, so a code chosen
35
+ * here may be rejected as `ROOM_EXISTS`; supply one only when something
36
+ * outside the relay already fixed it.
37
+ */
38
+ roomId?: string;
39
+ /**
40
+ * Called once the room exists and its code is known.
41
+ *
42
+ * A minted code is not available synchronously, so a display renders a
43
+ * placeholder until this fires — roughly a round trip to the relay.
44
+ */
45
+ onRoomCode?: (roomCode: string) => void;
28
46
  }
29
47
 
30
48
  /**
@@ -47,21 +65,27 @@ export interface RelayDisplayHostOptions<S extends IGameState, A extends IAction
47
65
  export class RelayDisplayHost<S extends IGameState, A extends IAction> {
48
66
  private readonly runtime: GameHostRuntime<S, A>;
49
67
  private readonly ws: WebSocket;
50
- private readonly roomId: string;
68
+ /** Null until the relay confirms the room, when the code is relay-assigned. */
69
+ private assignedRoomId: string | null;
70
+ private readonly onRoomCode?: (roomCode: string) => void;
51
71
  /** Connected phone connection ids (relay peer ids). */
52
72
  private readonly peers = new Set<string>();
53
73
 
54
74
  constructor(options: RelayDisplayHostOptions<S, A>) {
55
- const { url, roomId, ...runtimeConfig } = options;
56
- this.roomId = roomId;
75
+ const { url, roomId, onRoomCode, ...runtimeConfig } = options;
76
+ this.assignedRoomId = roomId ?? null;
77
+ this.onRoomCode = onRoomCode;
57
78
  this.runtime = new GameHostRuntime<S, A>(runtimeConfig);
58
79
  this.ws = new WebSocket(relayRoomUrl(url, roomId));
59
80
 
60
81
  this.ws.onopen = () => {
82
+ // No roomId asks the relay to allocate one. Sending the field as
83
+ // undefined omits it from the JSON, which is what the relay reads as
84
+ // "you pick".
61
85
  this.ws.send(
62
86
  JSON.stringify({
63
87
  type: RelayMessageTypes.CREATE_ROOM,
64
- roomId: this.roomId,
88
+ roomId: this.assignedRoomId ?? undefined,
65
89
  }),
66
90
  );
67
91
  };
@@ -82,13 +106,22 @@ export class RelayDisplayHost<S extends IGameState, A extends IAction> {
82
106
  );
83
107
 
84
108
  const transport: GameRuntimeTransport = {
85
- send: (connectionId, message) =>
86
- this.sendEnvelope(message, connectionId),
109
+ send: (connectionId, message) => this.sendEnvelope(message, connectionId),
87
110
  broadcast: (message) => this.sendEnvelope(message),
111
+ sendMany: (entries) => this.sendMultiEnvelope(entries),
88
112
  };
89
113
  this.runtime.setTransport(transport);
90
114
  }
91
115
 
116
+ /**
117
+ * The room code phones join with, or `null` before the relay has assigned
118
+ * one. See {@link RelayDisplayHostOptions.onRoomCode} to be told when it
119
+ * arrives.
120
+ */
121
+ get roomCode(): string | null {
122
+ return this.assignedRoomId;
123
+ }
124
+
92
125
  /** Current authoritative game state. */
93
126
  getState = (): S => this.runtime.getState();
94
127
 
@@ -106,10 +139,49 @@ export class RelayDisplayHost<S extends IGameState, A extends IAction> {
106
139
  this.ws.close();
107
140
  }
108
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
+
109
178
  private sendEnvelope(message: HostMessage, to?: string): void {
110
179
  const envelope: Record<string, unknown> = {
111
180
  type: RelayMessageTypes.DATA,
112
- roomId: this.roomId,
181
+ // The relay routes by the sender's membership, not this field, so it is
182
+ // only ever informational — and nothing is sent before a peer joins,
183
+ // which cannot happen until the room exists.
184
+ roomId: this.assignedRoomId ?? undefined,
113
185
  data: JSON.stringify(message),
114
186
  };
115
187
  if (to !== undefined) envelope.to = to;
@@ -147,10 +219,16 @@ export class RelayDisplayHost<S extends IGameState, A extends IAction> {
147
219
  );
148
220
  break;
149
221
  }
222
+ case RelayMessageTypes.ROOM_CREATED:
223
+ // Carries the code when the relay chose it, and confirms the code when
224
+ // the caller supplied one.
225
+ this.assignedRoomId = msg.roomId;
226
+ this.onRoomCode?.(msg.roomId);
227
+ break;
150
228
  case RelayMessageTypes.ERROR:
151
229
  this.runtime.handleError(new Error(msg.message));
152
230
  break;
153
- // ROOM_CREATED / ROOM_JOINED are acknowledgements; no action needed.
231
+ // ROOM_JOINED is an acknowledgement; no action needed.
154
232
  }
155
233
  }
156
234
  }