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