@smoregg/sdk 2.0.0 → 2.1.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.
Files changed (70) hide show
  1. package/dist/cjs/controller.cjs +193 -115
  2. package/dist/cjs/controller.cjs.map +1 -1
  3. package/dist/cjs/errors.cjs +1 -0
  4. package/dist/cjs/errors.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +19 -2
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/index.cjs +2 -7
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/screen.cjs +185 -130
  10. package/dist/cjs/screen.cjs.map +1 -1
  11. package/dist/cjs/shared.cjs +34 -0
  12. package/dist/cjs/shared.cjs.map +1 -0
  13. package/dist/cjs/testing.cjs +125 -74
  14. package/dist/cjs/testing.cjs.map +1 -1
  15. package/dist/cjs/transport/PostMessageTransport.cjs +12 -0
  16. package/dist/cjs/transport/PostMessageTransport.cjs.map +1 -1
  17. package/dist/cjs/transport/protocol.cjs +2 -0
  18. package/dist/cjs/transport/protocol.cjs.map +1 -1
  19. package/dist/cjs/types.cjs +16 -0
  20. package/dist/cjs/types.cjs.map +1 -0
  21. package/dist/esm/controller.js +195 -117
  22. package/dist/esm/controller.js.map +1 -1
  23. package/dist/esm/errors.js +1 -0
  24. package/dist/esm/errors.js.map +1 -1
  25. package/dist/esm/events.js +18 -3
  26. package/dist/esm/events.js.map +1 -1
  27. package/dist/esm/index.js +1 -3
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/screen.js +187 -132
  30. package/dist/esm/screen.js.map +1 -1
  31. package/dist/esm/shared.js +30 -0
  32. package/dist/esm/shared.js.map +1 -0
  33. package/dist/esm/testing.js +125 -74
  34. package/dist/esm/testing.js.map +1 -1
  35. package/dist/esm/transport/PostMessageTransport.js +12 -0
  36. package/dist/esm/transport/PostMessageTransport.js.map +1 -1
  37. package/dist/esm/transport/protocol.js +2 -1
  38. package/dist/esm/transport/protocol.js.map +1 -1
  39. package/dist/esm/types.js +14 -0
  40. package/dist/esm/types.js.map +1 -0
  41. package/dist/types/controller.d.ts +1 -1
  42. package/dist/types/controller.d.ts.map +1 -1
  43. package/dist/types/errors.d.ts.map +1 -1
  44. package/dist/types/events.d.ts +10 -1
  45. package/dist/types/events.d.ts.map +1 -1
  46. package/dist/types/index.d.ts +4 -8
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/screen.d.ts +3 -3
  49. package/dist/types/screen.d.ts.map +1 -1
  50. package/dist/types/shared.d.ts +21 -0
  51. package/dist/types/shared.d.ts.map +1 -0
  52. package/dist/types/testing.d.ts +63 -4
  53. package/dist/types/testing.d.ts.map +1 -1
  54. package/dist/types/transport/PostMessageTransport.d.ts +1 -0
  55. package/dist/types/transport/PostMessageTransport.d.ts.map +1 -1
  56. package/dist/types/transport/protocol.d.ts +4 -0
  57. package/dist/types/transport/protocol.d.ts.map +1 -1
  58. package/dist/types/types.d.ts +215 -347
  59. package/dist/types/types.d.ts.map +1 -1
  60. package/dist/umd/smore-sdk.umd.js +442 -787
  61. package/dist/umd/smore-sdk.umd.js.map +1 -1
  62. package/dist/umd/smore-sdk.umd.min.js +1 -1
  63. package/dist/umd/smore-sdk.umd.min.js.map +1 -1
  64. package/package.json +7 -1
  65. package/dist/cjs/config.cjs +0 -13
  66. package/dist/cjs/config.cjs.map +0 -1
  67. package/dist/esm/config.js +0 -10
  68. package/dist/esm/config.js.map +0 -1
  69. package/dist/types/config.d.ts +0 -35
  70. package/dist/types/config.d.ts.map +0 -1
@@ -1,9 +1,9 @@
1
1
  import { PostMessageTransport } from './transport/PostMessageTransport.js';
2
- import { isBridgeMessage, validateInitPayload } from './transport/protocol.js';
2
+ import { PROTOCOL_VERSION, isBridgeMessage, validateInitPayload } from './transport/protocol.js';
3
3
  import { SmoreSDKError } from './errors.js';
4
- import { SMORE_EVENTS, validateEventName } from './events.js';
4
+ import { SMORE_EVENTS, validateEventName, CONTROLLER_LIFECYCLE_EVENTS } from './events.js';
5
5
  import { DebugLogger } from './logger.js';
6
- import { getGlobalConfig } from './config.js';
6
+ import { mapPlayerDTO, validatePayloadSize } from './shared.js';
7
7
 
8
8
  const DEFAULT_TIMEOUT = 1e4;
9
9
  class ControllerImpl {
@@ -11,7 +11,7 @@ class ControllerImpl {
11
11
  config;
12
12
  logger;
13
13
  _roomCode = "";
14
- _myIndex = -1;
14
+ _myPlayerIndex = -1;
15
15
  _isReady = false;
16
16
  _isDestroyed = false;
17
17
  _initTimeoutId = null;
@@ -23,17 +23,16 @@ class ControllerImpl {
23
23
  _controllers = [];
24
24
  // Pending handlers registered via on() before transport is ready
25
25
  _pendingHandlers = [];
26
- // Lifecycle callback arrays
27
- _onAllReadyCallbacks = /* @__PURE__ */ new Set();
28
- _onControllerJoinCallbacks = /* @__PURE__ */ new Set();
29
- _onControllerLeaveCallbacks = /* @__PURE__ */ new Set();
30
- _onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
31
- _onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
32
- _onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
33
- _onRateLimitedCallbacks = /* @__PURE__ */ new Set();
34
- _onErrorCallbacks = /* @__PURE__ */ new Set();
26
+ // Unified lifecycle listener map (supports both onXxx() and on('$xxx') patterns)
27
+ _lifecycleListeners = /* @__PURE__ */ new Map();
28
+ // Outbound message buffer (messages sent before ready)
29
+ _outboundBuffer = [];
35
30
  // Whether all-ready has fired
36
31
  _allReadyFired = false;
32
+ // Self-connection awareness
33
+ _isConnected = false;
34
+ // Protocol versioning
35
+ _protocolVersion = PROTOCOL_VERSION;
37
36
  // Ready promise
38
37
  _readyResolve;
39
38
  _readyReject;
@@ -50,8 +49,8 @@ class ControllerImpl {
50
49
  // ---------------------------------------------------------------------------
51
50
  // Properties (readonly)
52
51
  // ---------------------------------------------------------------------------
53
- get myIndex() {
54
- return this._myIndex;
52
+ get myPlayerIndex() {
53
+ return this._myPlayerIndex;
55
54
  }
56
55
  get roomCode() {
57
56
  return this._roomCode;
@@ -62,6 +61,12 @@ class ControllerImpl {
62
61
  get isDestroyed() {
63
62
  return this._isDestroyed;
64
63
  }
64
+ get isConnected() {
65
+ return this._isConnected;
66
+ }
67
+ get protocolVersion() {
68
+ return this._protocolVersion;
69
+ }
65
70
  /**
66
71
  * Read-only list of all known controllers (players) in the room.
67
72
  * Returns full ControllerInfo including playerIndex, nickname, connected status, and appearance.
@@ -78,6 +83,9 @@ class ControllerImpl {
78
83
  getControllerCount() {
79
84
  return this._controllers.filter((c) => c.connected).length;
80
85
  }
86
+ getController(playerIndex) {
87
+ return this._controllers.find((c) => c.playerIndex === playerIndex);
88
+ }
81
89
  // ---------------------------------------------------------------------------
82
90
  // Initialization
83
91
  // ---------------------------------------------------------------------------
@@ -108,7 +116,7 @@ class ControllerImpl {
108
116
  };
109
117
  window.addEventListener("message", this.boundMessageHandler);
110
118
  this.logger.lifecycle("Sending _bridge:ready to parent");
111
- window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
119
+ window.parent.postMessage({ type: "_bridge:ready", protocolVersion: PROTOCOL_VERSION }, parentOrigin);
112
120
  }
113
121
  handleInit(msg, parentOrigin) {
114
122
  const initPayload = msg.payload;
@@ -147,28 +155,44 @@ class ControllerImpl {
147
155
  this._readyReject(error);
148
156
  return;
149
157
  }
150
- this.transport = new PostMessageTransport(parentOrigin);
158
+ this.transport = this.config.transport ?? new PostMessageTransport(parentOrigin);
151
159
  this._roomCode = initData.roomCode;
152
- this._myIndex = initData.myIndex;
160
+ const serverProtocolVersion = initData.protocolVersion;
161
+ if (serverProtocolVersion !== void 0) {
162
+ this._protocolVersion = serverProtocolVersion;
163
+ if (serverProtocolVersion !== PROTOCOL_VERSION) {
164
+ this.logger.warn(
165
+ `Protocol version mismatch: SDK v${PROTOCOL_VERSION}, server v${serverProtocolVersion}. Some features may not work correctly.`
166
+ );
167
+ }
168
+ }
169
+ this._myPlayerIndex = initData.myIndex;
153
170
  const initPlayers = initData.players;
154
- this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p) => ({
155
- playerIndex: p.playerIndex,
156
- nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
157
- connected: p.connected !== false,
158
- appearance: p.appearance ?? p.character
159
- }));
171
+ this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
160
172
  this.setupEventHandlers();
161
173
  for (const { event, handler } of this._pendingHandlers) {
162
174
  this.setupUserEventHandler(event, handler);
163
175
  }
164
176
  this._pendingHandlers = [];
177
+ this._isConnected = true;
165
178
  this._isReady = true;
179
+ for (const buffered of this._outboundBuffer) {
180
+ try {
181
+ switch (buffered.method) {
182
+ case "send":
183
+ this.send(buffered.args[0], buffered.args[1]);
184
+ break;
185
+ }
186
+ } catch (err) {
187
+ this.handleError(err instanceof SmoreSDKError ? err : new SmoreSDKError("UNKNOWN", "Failed to flush buffered message"));
188
+ }
189
+ }
190
+ this._outboundBuffer = [];
166
191
  this.logger.lifecycle("Controller ready", {
167
192
  roomCode: this._roomCode,
168
- myIndex: this._myIndex
193
+ myIndex: this._myPlayerIndex
169
194
  });
170
- const autoReady = getGlobalConfig().autoReady ?? true;
171
- if (autoReady) {
195
+ if (this.config.autoReady !== false) {
172
196
  this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
173
197
  this.signalReady();
174
198
  }
@@ -183,21 +207,16 @@ class ControllerImpl {
183
207
  this.logger.debug("Received _bridge:update", updateData);
184
208
  if (updateData.players && Array.isArray(updateData.players)) {
185
209
  const players = updateData.players;
186
- const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p) => ({
187
- playerIndex: p.playerIndex,
188
- nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
189
- connected: p.connected !== false,
190
- appearance: p.appearance ?? p.character
191
- }));
210
+ const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
192
211
  const oldControllers = this._controllers;
193
212
  for (const nc of newControllers) {
194
213
  if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
195
- this._onControllerJoinCallbacks.forEach((cb) => cb(nc.playerIndex, nc));
214
+ this._emitLifecycle("$controller-join", nc.playerIndex, nc);
196
215
  }
197
216
  }
198
217
  for (const oc of oldControllers) {
199
218
  if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
200
- this._onControllerLeaveCallbacks.forEach((cb) => cb(oc.playerIndex));
219
+ this._emitLifecycle("$controller-leave", oc.playerIndex);
201
220
  }
202
221
  }
203
222
  for (const nc of newControllers) {
@@ -205,11 +224,11 @@ class ControllerImpl {
205
224
  if (oc) {
206
225
  if (oc.connected && !nc.connected) {
207
226
  this.logger.debug("Player disconnected (via update)", { playerIndex: nc.playerIndex });
208
- this._onControllerDisconnectCallbacks.forEach((cb) => cb(nc.playerIndex));
227
+ this._emitLifecycle("$controller-disconnect", nc.playerIndex);
209
228
  }
210
229
  if (!oc.connected && nc.connected) {
211
230
  this.logger.debug("Player reconnected (via update)", { playerIndex: nc.playerIndex });
212
- this._onControllerReconnectCallbacks.forEach((cb) => cb(nc.playerIndex, nc));
231
+ this._emitLifecycle("$controller-reconnect", nc.playerIndex, nc);
213
232
  }
214
233
  }
215
234
  }
@@ -224,19 +243,10 @@ class ControllerImpl {
224
243
  const playerIndex = playerInfo?.playerIndex ?? data.playerIndex;
225
244
  if (playerIndex !== void 0) {
226
245
  if (this._controllers.some((c) => c.playerIndex === playerIndex)) return;
227
- const controllerInfo = playerInfo ? {
228
- playerIndex,
229
- nickname: playerInfo.nickname || playerInfo.name || `Player ${playerIndex + 1}`,
230
- connected: playerInfo.connected !== false,
231
- appearance: playerInfo.appearance ?? playerInfo.character
232
- } : {
233
- playerIndex,
234
- nickname: `Player ${playerIndex + 1}`,
235
- connected: true
236
- };
246
+ const controllerInfo = playerInfo ? mapPlayerDTO(playerInfo, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
237
247
  this._controllers = [...this._controllers, controllerInfo];
238
248
  this.logger.debug("Player joined", { playerIndex });
239
- this._onControllerJoinCallbacks.forEach((cb) => cb(playerIndex, controllerInfo));
249
+ this._emitLifecycle("$controller-join", playerIndex, controllerInfo);
240
250
  }
241
251
  });
242
252
  this.registerHandler(SMORE_EVENTS.PLAYER_LEFT, (raw) => {
@@ -246,7 +256,7 @@ class ControllerImpl {
246
256
  if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
247
257
  this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
248
258
  this.logger.debug("Player left", { playerIndex });
249
- this._onControllerLeaveCallbacks.forEach((cb) => cb(playerIndex));
259
+ this._emitLifecycle("$controller-leave", playerIndex);
250
260
  }
251
261
  });
252
262
  this.registerHandler(SMORE_EVENTS.PLAYER_DISCONNECTED, (raw) => {
@@ -258,7 +268,7 @@ class ControllerImpl {
258
268
  (c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
259
269
  );
260
270
  this.logger.debug("Player disconnected", { playerIndex });
261
- this._onControllerDisconnectCallbacks.forEach((cb) => cb(playerIndex));
271
+ this._emitLifecycle("$controller-disconnect", playerIndex);
262
272
  }
263
273
  });
264
274
  this.registerHandler(SMORE_EVENTS.PLAYER_RECONNECTED, (raw) => {
@@ -266,21 +276,12 @@ class ControllerImpl {
266
276
  const playerData = data.player;
267
277
  const playerIndex = playerData?.playerIndex ?? data.playerIndex;
268
278
  if (playerIndex !== void 0) {
269
- const controllerInfo = playerData ? {
270
- playerIndex,
271
- nickname: playerData.nickname || playerData.name || `Player ${playerIndex + 1}`,
272
- connected: true,
273
- appearance: playerData.appearance ?? playerData.character
274
- } : {
275
- playerIndex,
276
- nickname: `Player ${playerIndex + 1}`,
277
- connected: true
278
- };
279
+ const controllerInfo = playerData ? mapPlayerDTO(playerData, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
279
280
  this._controllers = this._controllers.map(
280
281
  (c) => c.playerIndex === playerIndex ? controllerInfo : c
281
282
  );
282
283
  this.logger.debug("Player reconnected", { playerIndex });
283
- this._onControllerReconnectCallbacks.forEach((cb) => cb(playerIndex, controllerInfo));
284
+ this._emitLifecycle("$controller-reconnect", playerIndex, controllerInfo);
284
285
  }
285
286
  });
286
287
  this.registerHandler(SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (raw) => {
@@ -293,19 +294,37 @@ class ControllerImpl {
293
294
  (c) => c.playerIndex === pi ? { ...c, appearance } : c
294
295
  );
295
296
  this.logger.debug("Player character updated", { playerIndex: pi });
296
- this._onCharacterUpdatedCallbacks.forEach((cb) => cb(pi, appearance ?? null));
297
+ this._emitLifecycle("$character-updated", pi, appearance ?? null);
297
298
  }
298
299
  });
299
300
  this.registerHandler(SMORE_EVENTS.RATE_LIMITED, (raw) => {
300
301
  const data = raw;
301
- const event = data?.event ?? "unknown";
302
- this.logger.warn(`Rate limited: ${event}`);
303
- this._onRateLimitedCallbacks.forEach((cb) => cb(event));
302
+ const eventName = data?.event ?? "unknown";
303
+ this.handleError(
304
+ new SmoreSDKError("RATE_LIMITED", `Server rate-limited event: ${eventName}`, {
305
+ details: { event: eventName }
306
+ })
307
+ );
308
+ });
309
+ this.registerHandler(SMORE_EVENTS.GAME_OVER, (raw) => {
310
+ const data = raw;
311
+ this.logger.lifecycle("Game over", data?.results);
312
+ this._emitLifecycle("$game-over", data?.results);
304
313
  });
305
314
  this.registerHandler(SMORE_EVENTS.ALL_READY, () => {
306
315
  this.logger.lifecycle("All participants ready");
307
316
  this._allReadyFired = true;
308
- this._onAllReadyCallbacks.forEach((cb) => cb());
317
+ this._emitLifecycle("$all-ready");
318
+ });
319
+ this.registerHandler(SMORE_EVENTS.SELF_DISCONNECTED, () => {
320
+ this._isConnected = false;
321
+ this.logger.lifecycle("Connection lost");
322
+ this._emitLifecycle("$connection-change", false);
323
+ });
324
+ this.registerHandler(SMORE_EVENTS.SELF_RECONNECTED, () => {
325
+ this._isConnected = true;
326
+ this.logger.lifecycle("Connection restored");
327
+ this._emitLifecycle("$connection-change", true);
309
328
  });
310
329
  }
311
330
  /**
@@ -344,58 +363,70 @@ class ControllerImpl {
344
363
  this.registeredHandlers.push({ event, handler });
345
364
  }
346
365
  // ---------------------------------------------------------------------------
366
+ // Lifecycle Listener Helpers
367
+ // ---------------------------------------------------------------------------
368
+ _addLifecycleListener(event, listener) {
369
+ let set = this._lifecycleListeners.get(event);
370
+ if (!set) {
371
+ set = /* @__PURE__ */ new Set();
372
+ this._lifecycleListeners.set(event, set);
373
+ }
374
+ set.add(listener);
375
+ return () => {
376
+ set.delete(listener);
377
+ if (set.size === 0) this._lifecycleListeners.delete(event);
378
+ };
379
+ }
380
+ _emitLifecycle(event, ...args) {
381
+ this._lifecycleListeners.get(event)?.forEach((cb) => {
382
+ try {
383
+ cb(...args);
384
+ } catch (err) {
385
+ this.handleError(
386
+ new SmoreSDKError("UNKNOWN", `Error in lifecycle handler for "${event}"`, {
387
+ cause: err instanceof Error ? err : void 0,
388
+ details: { event }
389
+ })
390
+ );
391
+ }
392
+ });
393
+ }
394
+ _hasLifecycleListeners(event) {
395
+ const set = this._lifecycleListeners.get(event);
396
+ return set !== void 0 && set.size > 0;
397
+ }
398
+ // ---------------------------------------------------------------------------
347
399
  // Lifecycle Methods
348
400
  // ---------------------------------------------------------------------------
349
401
  onAllReady(callback) {
350
402
  if (this._allReadyFired) {
351
403
  callback();
352
404
  }
353
- this._onAllReadyCallbacks.add(callback);
354
- return () => {
355
- this._onAllReadyCallbacks.delete(callback);
356
- };
405
+ return this._addLifecycleListener("$all-ready", callback);
357
406
  }
358
407
  onControllerJoin(callback) {
359
- this._onControllerJoinCallbacks.add(callback);
360
- return () => {
361
- this._onControllerJoinCallbacks.delete(callback);
362
- };
408
+ return this._addLifecycleListener("$controller-join", callback);
363
409
  }
364
410
  onControllerLeave(callback) {
365
- this._onControllerLeaveCallbacks.add(callback);
366
- return () => {
367
- this._onControllerLeaveCallbacks.delete(callback);
368
- };
411
+ return this._addLifecycleListener("$controller-leave", callback);
369
412
  }
370
413
  onControllerDisconnect(callback) {
371
- this._onControllerDisconnectCallbacks.add(callback);
372
- return () => {
373
- this._onControllerDisconnectCallbacks.delete(callback);
374
- };
414
+ return this._addLifecycleListener("$controller-disconnect", callback);
375
415
  }
376
416
  onControllerReconnect(callback) {
377
- this._onControllerReconnectCallbacks.add(callback);
378
- return () => {
379
- this._onControllerReconnectCallbacks.delete(callback);
380
- };
417
+ return this._addLifecycleListener("$controller-reconnect", callback);
381
418
  }
382
419
  onCharacterUpdated(callback) {
383
- this._onCharacterUpdatedCallbacks.add(callback);
384
- return () => {
385
- this._onCharacterUpdatedCallbacks.delete(callback);
386
- };
387
- }
388
- onRateLimited(callback) {
389
- this._onRateLimitedCallbacks.add(callback);
390
- return () => {
391
- this._onRateLimitedCallbacks.delete(callback);
392
- };
420
+ return this._addLifecycleListener("$character-updated", callback);
393
421
  }
394
422
  onError(callback) {
395
- this._onErrorCallbacks.add(callback);
396
- return () => {
397
- this._onErrorCallbacks.delete(callback);
398
- };
423
+ return this._addLifecycleListener("$error", callback);
424
+ }
425
+ onConnectionChange(callback) {
426
+ return this._addLifecycleListener("$connection-change", callback);
427
+ }
428
+ onGameOver(callback) {
429
+ return this._addLifecycleListener("$game-over", callback);
399
430
  }
400
431
  // ---------------------------------------------------------------------------
401
432
  // Communication Methods
@@ -411,8 +442,16 @@ class ControllerImpl {
411
442
  * Use the onError callback or smore:rate-limited event to detect rate limiting.
412
443
  */
413
444
  send(event, data) {
414
- this.ensureReady("send");
445
+ if (this._isDestroyed) {
446
+ throw new SmoreSDKError("DESTROYED", "Cannot call send() after destroy()");
447
+ }
448
+ if (!this._isReady || !this.transport) {
449
+ this._outboundBuffer.push({ method: "send", args: [event, data] });
450
+ this.logger.debug(`Buffered send "${event}" (controller not ready yet)`);
451
+ return;
452
+ }
415
453
  validateEventName(event);
454
+ validatePayloadSize(data);
416
455
  if (typeof data !== "object" || data === null) {
417
456
  this.logger.warn(
418
457
  'Event data should be an object. Primitive values will be wrapped as { data: value } by the relay server. To avoid confusion, wrap explicitly: send("event", { value: 42 }) instead of send("event", 42).'
@@ -421,12 +460,6 @@ class ControllerImpl {
421
460
  this.logSend(event, data);
422
461
  this.transport.emit(event, data);
423
462
  }
424
- sendRaw(event, data) {
425
- this.ensureReady("sendRaw");
426
- validateEventName(event);
427
- this.logSend(event, data);
428
- this.transport.emit(event, data);
429
- }
430
463
  signalReady() {
431
464
  this.ensureReady("signalReady");
432
465
  this.logSend(SMORE_EVENTS.GAME_READY, {});
@@ -448,6 +481,16 @@ class ControllerImpl {
448
481
  * handler receives `(data)` -- targeted to this specific controller.
449
482
  */
450
483
  on(event, handler) {
484
+ if (typeof event === "string" && event.startsWith("$")) {
485
+ const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
486
+ if (!validEvents.has(event)) {
487
+ throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}". Valid lifecycle events: ${Array.from(validEvents).join(", ")}`);
488
+ }
489
+ if (event === "$all-ready" && this._allReadyFired) {
490
+ handler();
491
+ }
492
+ return this._addLifecycleListener(event, handler);
493
+ }
451
494
  validateEventName(event);
452
495
  let listeners = this.eventListeners.get(event);
453
496
  if (!listeners) {
@@ -500,6 +543,23 @@ class ControllerImpl {
500
543
  * `off(event, originalHandler)`. Use the returned unsubscribe function instead.
501
544
  */
502
545
  once(event, handler) {
546
+ if (typeof event === "string" && event.startsWith("$")) {
547
+ const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
548
+ if (!validEvents.has(event)) {
549
+ throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}"`);
550
+ }
551
+ if (event === "$all-ready" && this._allReadyFired) {
552
+ handler();
553
+ return () => {
554
+ };
555
+ }
556
+ const wrapper = (...args) => {
557
+ unsub();
558
+ handler(...args);
559
+ };
560
+ const unsub = this._addLifecycleListener(event, wrapper);
561
+ return unsub;
562
+ }
503
563
  const unsubscribe = this.on(event, ((data) => {
504
564
  unsubscribe();
505
565
  handler(data);
@@ -507,6 +567,14 @@ class ControllerImpl {
507
567
  return unsubscribe;
508
568
  }
509
569
  off(event, handler) {
570
+ if (typeof event === "string" && event.startsWith("$")) {
571
+ if (!handler) {
572
+ this._lifecycleListeners.delete(event);
573
+ } else {
574
+ this._lifecycleListeners.get(event)?.delete(handler);
575
+ }
576
+ return;
577
+ }
510
578
  if (!handler) {
511
579
  this.eventListeners.delete(event);
512
580
  this.transport?.off(event);
@@ -534,6 +602,21 @@ class ControllerImpl {
534
602
  );
535
603
  }
536
604
  }
605
+ removeAllListeners(event) {
606
+ if (event) {
607
+ this.eventListeners.delete(event);
608
+ this.transport?.off(event);
609
+ this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
610
+ for (const [key, val] of this.handlerToTransport) {
611
+ if (val.event === event) this.handlerToTransport.delete(key);
612
+ }
613
+ this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
614
+ } else {
615
+ for (const evt of [...this.eventListeners.keys()]) {
616
+ this.removeAllListeners(evt);
617
+ }
618
+ }
619
+ }
537
620
  // ---------------------------------------------------------------------------
538
621
  // Cleanup
539
622
  // ---------------------------------------------------------------------------
@@ -557,14 +640,9 @@ class ControllerImpl {
557
640
  this.eventListeners.clear();
558
641
  this.handlerToTransport.clear();
559
642
  this._pendingHandlers = [];
560
- this._onAllReadyCallbacks.clear();
561
- this._onControllerJoinCallbacks.clear();
562
- this._onControllerLeaveCallbacks.clear();
563
- this._onControllerDisconnectCallbacks.clear();
564
- this._onControllerReconnectCallbacks.clear();
565
- this._onCharacterUpdatedCallbacks.clear();
566
- this._onRateLimitedCallbacks.clear();
567
- this._onErrorCallbacks.clear();
643
+ this._lifecycleListeners.clear();
644
+ this._isConnected = false;
645
+ this._outboundBuffer = [];
568
646
  if (this.transport) {
569
647
  this.transport.destroy();
570
648
  this.transport = null;
@@ -596,8 +674,8 @@ class ControllerImpl {
596
674
  handleError(error) {
597
675
  this.logger.warn(`Error in handler: ${error.message}`);
598
676
  const smoreError = error.toSmoreError();
599
- if (this._onErrorCallbacks.size > 0) {
600
- this._onErrorCallbacks.forEach((cb) => cb(smoreError));
677
+ if (this._hasLifecycleListeners("$error")) {
678
+ this._emitLifecycle("$error", smoreError);
601
679
  } else {
602
680
  this.logger.error(error.message, error.details);
603
681
  }