@smoregg/sdk 1.3.0 → 2.0.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.
@@ -16,22 +16,38 @@ class ControllerImpl {
16
16
  _myIndex = -1;
17
17
  _isReady = false;
18
18
  _isDestroyed = false;
19
+ _initTimeoutId = null;
19
20
  boundMessageHandler = null;
20
21
  registeredHandlers = [];
21
22
  eventListeners = /* @__PURE__ */ new Map();
22
23
  // Maps user-facing handler -> transport wrappedHandler for proper cleanup in on()/off()
23
24
  handlerToTransport = /* @__PURE__ */ new Map();
24
25
  _controllers = [];
25
- // Tracks event names registered via config.listeners so off(event) without handler won't remove them
26
- _configListenerEvents = /* @__PURE__ */ new Set();
26
+ // Pending handlers registered via on() before transport is ready
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();
37
+ // Whether all-ready has fired
38
+ _allReadyFired = false;
39
+ // Ready promise
40
+ _readyResolve;
41
+ _readyReject;
42
+ ready;
27
43
  constructor(config = {}) {
28
44
  this.config = config;
29
45
  this.logger = new logger.DebugLogger(config.debug, "[SmoreController]");
30
- if (config.listeners) {
31
- for (const event of Object.keys(config.listeners)) {
32
- events.validateEventName(event);
33
- }
34
- }
46
+ this.ready = new Promise((resolve, reject) => {
47
+ this._readyResolve = resolve;
48
+ this._readyReject = reject;
49
+ });
50
+ this.startInitialization();
35
51
  }
36
52
  // ---------------------------------------------------------------------------
37
53
  // Properties (readonly)
@@ -67,38 +83,36 @@ class ControllerImpl {
67
83
  // ---------------------------------------------------------------------------
68
84
  // Initialization
69
85
  // ---------------------------------------------------------------------------
70
- async initialize() {
86
+ startInitialization() {
71
87
  const parentOrigin = this.config.parentOrigin ?? "*";
72
88
  const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
73
89
  this.logger.lifecycle("Initializing controller...", { parentOrigin, timeout });
74
- return new Promise((resolve, reject) => {
75
- const timeoutId = setTimeout(() => {
76
- this.cleanup();
77
- const error = new errors.SmoreSDKError(
78
- "TIMEOUT",
79
- `Controller initialization timed out after ${timeout}ms. Make sure the parent window sends _bridge:init message. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Controller instance to retry (this instance has been cleaned up).`,
80
- { details: { timeout } }
81
- );
82
- this.handleError(error);
83
- reject(error);
84
- }, timeout);
85
- this.boundMessageHandler = (e) => {
86
- if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
87
- const msg = e.data;
88
- if (!protocol.isBridgeMessage(msg)) return;
89
- if (msg.type === "_bridge:init") {
90
- clearTimeout(timeoutId);
91
- this.handleInit(msg, parentOrigin, resolve, reject);
92
- } else if (msg.type === "_bridge:update") {
93
- this.handleUpdate(msg);
94
- }
95
- };
96
- window.addEventListener("message", this.boundMessageHandler);
97
- this.logger.lifecycle("Sending _bridge:ready to parent");
98
- window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
99
- });
90
+ this._initTimeoutId = setTimeout(() => {
91
+ this.cleanup();
92
+ const error = new errors.SmoreSDKError(
93
+ "TIMEOUT",
94
+ `Controller initialization timed out after ${timeout}ms. Make sure the parent window sends _bridge:init message. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Controller instance to retry (this instance has been cleaned up).`,
95
+ { details: { timeout } }
96
+ );
97
+ this.handleError(error);
98
+ this._readyReject(error);
99
+ }, timeout);
100
+ this.boundMessageHandler = (e) => {
101
+ if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
102
+ const msg = e.data;
103
+ if (!protocol.isBridgeMessage(msg)) return;
104
+ if (msg.type === "_bridge:init") {
105
+ clearTimeout(this._initTimeoutId);
106
+ this.handleInit(msg, parentOrigin);
107
+ } else if (msg.type === "_bridge:update") {
108
+ this.handleUpdate(msg);
109
+ }
110
+ };
111
+ window.addEventListener("message", this.boundMessageHandler);
112
+ this.logger.lifecycle("Sending _bridge:ready to parent");
113
+ window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
100
114
  }
101
- handleInit(msg, parentOrigin, resolve, reject) {
115
+ handleInit(msg, parentOrigin) {
102
116
  const initPayload = msg.payload;
103
117
  this.logger.debug("Received _bridge:init", initPayload);
104
118
  try {
@@ -111,7 +125,7 @@ class ControllerImpl {
111
125
  );
112
126
  this.logger.warn("_bridge:init validation failed", error);
113
127
  this.handleError(error);
114
- reject(error);
128
+ this._readyReject(error);
115
129
  return;
116
130
  }
117
131
  const initData = initPayload;
@@ -122,7 +136,7 @@ class ControllerImpl {
122
136
  { details: { side: initData.side } }
123
137
  );
124
138
  this.handleError(error);
125
- reject(error);
139
+ this._readyReject(error);
126
140
  return;
127
141
  }
128
142
  if (initData.myIndex === void 0) {
@@ -132,7 +146,7 @@ class ControllerImpl {
132
146
  { details: initData }
133
147
  );
134
148
  this.handleError(error);
135
- reject(error);
149
+ this._readyReject(error);
136
150
  return;
137
151
  }
138
152
  this.transport = new PostMessageTransport.PostMessageTransport(parentOrigin);
@@ -146,6 +160,10 @@ class ControllerImpl {
146
160
  appearance: p.appearance ?? p.character
147
161
  }));
148
162
  this.setupEventHandlers();
163
+ for (const { event, handler } of this._pendingHandlers) {
164
+ this.setupUserEventHandler(event, handler);
165
+ }
166
+ this._pendingHandlers = [];
149
167
  this._isReady = true;
150
168
  this.logger.lifecycle("Controller ready", {
151
169
  roomCode: this._roomCode,
@@ -156,7 +174,7 @@ class ControllerImpl {
156
174
  this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
157
175
  this.signalReady();
158
176
  }
159
- resolve();
177
+ this._readyResolve();
160
178
  }
161
179
  handleUpdate(msg) {
162
180
  if (!this._isReady) {
@@ -176,12 +194,12 @@ class ControllerImpl {
176
194
  const oldControllers = this._controllers;
177
195
  for (const nc of newControllers) {
178
196
  if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
179
- this.config.onControllerJoin?.(nc.playerIndex, nc);
197
+ this._onControllerJoinCallbacks.forEach((cb) => cb(nc.playerIndex, nc));
180
198
  }
181
199
  }
182
200
  for (const oc of oldControllers) {
183
201
  if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
184
- this.config.onControllerLeave?.(oc.playerIndex);
202
+ this._onControllerLeaveCallbacks.forEach((cb) => cb(oc.playerIndex));
185
203
  }
186
204
  }
187
205
  for (const nc of newControllers) {
@@ -189,11 +207,11 @@ class ControllerImpl {
189
207
  if (oc) {
190
208
  if (oc.connected && !nc.connected) {
191
209
  this.logger.debug("Player disconnected (via update)", { playerIndex: nc.playerIndex });
192
- this.config.onControllerDisconnect?.(nc.playerIndex);
210
+ this._onControllerDisconnectCallbacks.forEach((cb) => cb(nc.playerIndex));
193
211
  }
194
212
  if (!oc.connected && nc.connected) {
195
213
  this.logger.debug("Player reconnected (via update)", { playerIndex: nc.playerIndex });
196
- this.config.onControllerReconnect?.(nc.playerIndex, nc);
214
+ this._onControllerReconnectCallbacks.forEach((cb) => cb(nc.playerIndex, nc));
197
215
  }
198
216
  }
199
217
  }
@@ -220,7 +238,7 @@ class ControllerImpl {
220
238
  };
221
239
  this._controllers = [...this._controllers, controllerInfo];
222
240
  this.logger.debug("Player joined", { playerIndex });
223
- this.config.onControllerJoin?.(playerIndex, controllerInfo);
241
+ this._onControllerJoinCallbacks.forEach((cb) => cb(playerIndex, controllerInfo));
224
242
  }
225
243
  });
226
244
  this.registerHandler(events.SMORE_EVENTS.PLAYER_LEFT, (raw) => {
@@ -230,7 +248,7 @@ class ControllerImpl {
230
248
  if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
231
249
  this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
232
250
  this.logger.debug("Player left", { playerIndex });
233
- this.config.onControllerLeave?.(playerIndex);
251
+ this._onControllerLeaveCallbacks.forEach((cb) => cb(playerIndex));
234
252
  }
235
253
  });
236
254
  this.registerHandler(events.SMORE_EVENTS.PLAYER_DISCONNECTED, (raw) => {
@@ -242,7 +260,7 @@ class ControllerImpl {
242
260
  (c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
243
261
  );
244
262
  this.logger.debug("Player disconnected", { playerIndex });
245
- this.config.onControllerDisconnect?.(playerIndex);
263
+ this._onControllerDisconnectCallbacks.forEach((cb) => cb(playerIndex));
246
264
  }
247
265
  });
248
266
  this.registerHandler(events.SMORE_EVENTS.PLAYER_RECONNECTED, (raw) => {
@@ -264,56 +282,63 @@ class ControllerImpl {
264
282
  (c) => c.playerIndex === playerIndex ? controllerInfo : c
265
283
  );
266
284
  this.logger.debug("Player reconnected", { playerIndex });
267
- this.config.onControllerReconnect?.(playerIndex, controllerInfo);
285
+ this._onControllerReconnectCallbacks.forEach((cb) => cb(playerIndex, controllerInfo));
268
286
  }
269
287
  });
270
288
  this.registerHandler(events.SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (raw) => {
271
289
  const payload = raw;
272
290
  const playerData = payload?.player;
273
291
  if (playerData && typeof playerData.playerIndex === "number") {
292
+ const pi = playerData.playerIndex;
274
293
  const appearance = playerData.character ?? null;
275
294
  this._controllers = this._controllers.map(
276
- (c) => c.playerIndex === playerData.playerIndex ? { ...c, appearance } : c
295
+ (c) => c.playerIndex === pi ? { ...c, appearance } : c
277
296
  );
278
- this.logger.debug("Player character updated", { playerIndex: playerData.playerIndex });
279
- this.config.onCharacterUpdated?.(playerData.playerIndex, appearance ?? null);
297
+ this.logger.debug("Player character updated", { playerIndex: pi });
298
+ this._onCharacterUpdatedCallbacks.forEach((cb) => cb(pi, appearance ?? null));
280
299
  }
281
300
  });
282
301
  this.registerHandler(events.SMORE_EVENTS.RATE_LIMITED, (raw) => {
283
302
  const data = raw;
284
303
  const event = data?.event ?? "unknown";
285
304
  this.logger.warn(`Rate limited: ${event}`);
286
- this.config.onRateLimited?.(event);
305
+ this._onRateLimitedCallbacks.forEach((cb) => cb(event));
287
306
  });
288
307
  this.registerHandler(events.SMORE_EVENTS.ALL_READY, () => {
289
308
  this.logger.lifecycle("All participants ready");
290
- this.config.onReady?.();
309
+ this._allReadyFired = true;
310
+ this._onAllReadyCallbacks.forEach((cb) => cb());
291
311
  });
292
- if (this.config.onHostDisconnect) {
293
- this.logger.warn("onHostDisconnect is reserved for future use and currently non-functional");
294
- }
295
- if (this.config.onHostReconnect) {
296
- this.logger.warn("onHostReconnect is reserved for future use and currently non-functional");
297
- }
298
- if (this.config.listeners) {
299
- for (const [event, handler] of Object.entries(this.config.listeners)) {
300
- if (!handler) continue;
301
- this._configListenerEvents.add(event);
302
- this.registerHandler(event, (data) => {
303
- this.logReceive(event, data);
304
- try {
305
- handler(data);
306
- } catch (err) {
307
- this.handleError(
308
- new errors.SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
309
- cause: err instanceof Error ? err : void 0,
310
- details: { event }
311
- })
312
- );
313
- }
314
- });
312
+ }
313
+ /**
314
+ * Sets up a user event handler for controller events.
315
+ * Used for registering pending handlers after transport becomes available.
316
+ */
317
+ setupUserEventHandler(event, handler) {
318
+ const transportHandler = (data) => {
319
+ this.logReceive(event, data);
320
+ try {
321
+ handler(data);
322
+ } catch (err) {
323
+ this.handleError(
324
+ new errors.SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
325
+ cause: err instanceof Error ? err : void 0,
326
+ details: { event }
327
+ })
328
+ );
315
329
  }
330
+ };
331
+ if (this.transport) {
332
+ this.transport.on(event, transportHandler);
333
+ this.registeredHandlers.push({ event, handler: transportHandler });
334
+ this.handlerToTransport.set(handler, { event, transportHandler });
316
335
  }
336
+ let listeners = this.eventListeners.get(event);
337
+ if (!listeners) {
338
+ listeners = /* @__PURE__ */ new Set();
339
+ this.eventListeners.set(event, listeners);
340
+ }
341
+ listeners.add(handler);
317
342
  }
318
343
  registerHandler(event, handler) {
319
344
  if (!this.transport) return;
@@ -321,13 +346,67 @@ class ControllerImpl {
321
346
  this.registeredHandlers.push({ event, handler });
322
347
  }
323
348
  // ---------------------------------------------------------------------------
349
+ // Lifecycle Methods
350
+ // ---------------------------------------------------------------------------
351
+ onAllReady(callback) {
352
+ if (this._allReadyFired) {
353
+ callback();
354
+ }
355
+ this._onAllReadyCallbacks.add(callback);
356
+ return () => {
357
+ this._onAllReadyCallbacks.delete(callback);
358
+ };
359
+ }
360
+ onControllerJoin(callback) {
361
+ this._onControllerJoinCallbacks.add(callback);
362
+ return () => {
363
+ this._onControllerJoinCallbacks.delete(callback);
364
+ };
365
+ }
366
+ onControllerLeave(callback) {
367
+ this._onControllerLeaveCallbacks.add(callback);
368
+ return () => {
369
+ this._onControllerLeaveCallbacks.delete(callback);
370
+ };
371
+ }
372
+ onControllerDisconnect(callback) {
373
+ this._onControllerDisconnectCallbacks.add(callback);
374
+ return () => {
375
+ this._onControllerDisconnectCallbacks.delete(callback);
376
+ };
377
+ }
378
+ onControllerReconnect(callback) {
379
+ this._onControllerReconnectCallbacks.add(callback);
380
+ return () => {
381
+ this._onControllerReconnectCallbacks.delete(callback);
382
+ };
383
+ }
384
+ 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
+ };
395
+ }
396
+ onError(callback) {
397
+ this._onErrorCallbacks.add(callback);
398
+ return () => {
399
+ this._onErrorCallbacks.delete(callback);
400
+ };
401
+ }
402
+ // ---------------------------------------------------------------------------
324
403
  // Communication Methods
325
404
  // ---------------------------------------------------------------------------
326
405
  /**
327
406
  * Send an event to the Screen. Controller-to-Controller direct communication
328
407
  * is not supported; all messages must go through the Screen.
329
408
  *
330
- * Data is sent to the Screen only (not to other controllers). For ScreenController communication,
409
+ * Data is sent to the Screen only (not to other controllers). For Screen->Controller communication,
331
410
  * Screen uses broadcast() or sendToController().
332
411
  *
333
412
  * @note Fire-and-forget sends (no callback) will silently fail if rate-limited.
@@ -361,24 +440,14 @@ class ControllerImpl {
361
440
  /**
362
441
  * Register a handler for custom events.
363
442
  *
443
+ * Can be called before the Controller is ready. Handlers registered before ready
444
+ * are queued and activated when the transport becomes available.
445
+ *
364
446
  * When receiving events from Screen's `broadcast()`:
365
- * handler receives `(data)` no playerIndex included.
447
+ * handler receives `(data)` -- no playerIndex included.
366
448
  *
367
449
  * When receiving events from Screen's `sendToController()`:
368
- * handler receives `(data)` targeted to this specific controller.
369
- *
370
- * @note Unlike Screen's `on()` which receives `(playerIndex, data)`,
371
- * Controller's `on()` receives only `(data)` since there's only one player per controller.
372
- *
373
- * Controller's on() handler signature: (data) => void
374
- * Unlike Screen's (playerIndex, data) => void, Controller doesn't receive playerIndex
375
- * because Controller only receives events from Screen, not from other controllers.
376
- * The sender is always the Screen, so playerIndex is not applicable.
377
- *
378
- * **Important:** If called before the Controller is ready (i.e., before `await createController()`
379
- * resolves or before the `onReady` callback fires), the handler is stored locally but
380
- * will NOT receive events until the transport is initialized. Always call `on()` after
381
- * initialization completes, or use `config.listeners` for handlers needed from the start.
450
+ * handler receives `(data)` -- targeted to this specific controller.
382
451
  */
383
452
  on(event, handler) {
384
453
  events.validateEventName(event);
@@ -388,34 +457,42 @@ class ControllerImpl {
388
457
  this.eventListeners.set(event, listeners);
389
458
  }
390
459
  listeners.add(handler);
391
- const transportHandler = (data) => {
392
- this.logReceive(event, data);
393
- try {
394
- handler(data);
395
- } catch (err) {
396
- this.handleError(
397
- new errors.SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
398
- cause: err instanceof Error ? err : void 0,
399
- details: { event }
400
- })
401
- );
402
- }
403
- };
404
460
  if (this.transport) {
461
+ const transportHandler = (data) => {
462
+ this.logReceive(event, data);
463
+ try {
464
+ handler(data);
465
+ } catch (err) {
466
+ this.handleError(
467
+ new errors.SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
468
+ cause: err instanceof Error ? err : void 0,
469
+ details: { event }
470
+ })
471
+ );
472
+ }
473
+ };
405
474
  this.transport.on(event, transportHandler);
406
475
  this.registeredHandlers.push({ event, handler: transportHandler });
407
476
  this.handlerToTransport.set(handler, { event, transportHandler });
477
+ } else {
478
+ this._pendingHandlers.push({ event, handler });
408
479
  }
409
480
  return () => {
410
481
  listeners?.delete(handler);
411
482
  if (listeners?.size === 0) {
412
483
  this.eventListeners.delete(event);
413
484
  }
414
- this.transport?.off(event, transportHandler);
415
- this.registeredHandlers = this.registeredHandlers.filter(
416
- (h) => h.handler !== transportHandler
485
+ this._pendingHandlers = this._pendingHandlers.filter(
486
+ (p) => !(p.event === event && p.handler === handler)
417
487
  );
418
- this.handlerToTransport.delete(handler);
488
+ const entry = this.handlerToTransport.get(handler);
489
+ if (entry) {
490
+ this.transport?.off(event, entry.transportHandler);
491
+ this.registeredHandlers = this.registeredHandlers.filter(
492
+ (h) => h.handler !== entry.transportHandler
493
+ );
494
+ this.handlerToTransport.delete(handler);
495
+ }
419
496
  };
420
497
  }
421
498
  /**
@@ -423,16 +500,6 @@ class ControllerImpl {
423
500
  *
424
501
  * @note The handler is internally wrapped, so it cannot be removed via
425
502
  * `off(event, originalHandler)`. Use the returned unsubscribe function instead.
426
- *
427
- * @example
428
- * ```ts
429
- * const unsubscribe = controller.once('game-start', (data) => {
430
- * console.log('Game started!', data);
431
- * });
432
- *
433
- * // To cancel before it fires:
434
- * unsubscribe();
435
- * ```
436
503
  */
437
504
  once(event, handler) {
438
505
  const unsubscribe = this.on(event, ((data) => {
@@ -443,24 +510,13 @@ class ControllerImpl {
443
510
  }
444
511
  off(event, handler) {
445
512
  if (!handler) {
446
- if (this._configListenerEvents.has(event)) {
447
- for (const [key, val] of this.handlerToTransport) {
448
- if (val.event === event) {
449
- this.transport?.off(event, val.transportHandler);
450
- this.registeredHandlers = this.registeredHandlers.filter(
451
- (h) => h.handler !== val.transportHandler
452
- );
453
- this.handlerToTransport.delete(key);
454
- }
455
- }
456
- } else {
457
- this.eventListeners.delete(event);
458
- this.transport?.off(event);
459
- this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
460
- for (const [key, val] of this.handlerToTransport) {
461
- if (val.event === event) this.handlerToTransport.delete(key);
462
- }
513
+ this.eventListeners.delete(event);
514
+ this.transport?.off(event);
515
+ this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
516
+ for (const [key, val] of this.handlerToTransport) {
517
+ if (val.event === event) this.handlerToTransport.delete(key);
463
518
  }
519
+ this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
464
520
  } else {
465
521
  const listeners = this.eventListeners.get(event);
466
522
  listeners?.delete(handler);
@@ -475,6 +531,9 @@ class ControllerImpl {
475
531
  );
476
532
  this.handlerToTransport.delete(handler);
477
533
  }
534
+ this._pendingHandlers = this._pendingHandlers.filter(
535
+ (p) => !(p.event === event && p.handler === handler)
536
+ );
478
537
  }
479
538
  }
480
539
  // ---------------------------------------------------------------------------
@@ -483,10 +542,15 @@ class ControllerImpl {
483
542
  destroy() {
484
543
  if (this._isDestroyed) return;
485
544
  this.logger.lifecycle("Destroying controller");
486
- this.cleanup();
487
545
  this._isDestroyed = true;
546
+ this._isReady = false;
547
+ this.cleanup();
488
548
  }
489
549
  cleanup() {
550
+ if (this._initTimeoutId) {
551
+ clearTimeout(this._initTimeoutId);
552
+ this._initTimeoutId = null;
553
+ }
490
554
  this._isReady = false;
491
555
  for (const { event, handler } of this.registeredHandlers) {
492
556
  this.transport?.off(event, handler);
@@ -494,6 +558,15 @@ class ControllerImpl {
494
558
  this.registeredHandlers = [];
495
559
  this.eventListeners.clear();
496
560
  this.handlerToTransport.clear();
561
+ 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();
497
570
  if (this.transport) {
498
571
  this.transport.destroy();
499
572
  this.transport = null;
@@ -517,15 +590,16 @@ class ControllerImpl {
517
590
  if (!this._isReady || !this.transport) {
518
591
  throw new errors.SmoreSDKError(
519
592
  "NOT_READY",
520
- `Cannot call ${method}() before controller is ready. Use await createController() or wait for onReady callback.`,
593
+ `Cannot call ${method}() before controller is ready. Use await controller.ready.`,
521
594
  { details: { method, isReady: this._isReady } }
522
595
  );
523
596
  }
524
597
  }
525
598
  handleError(error) {
526
599
  this.logger.warn(`Error in handler: ${error.message}`);
527
- if (this.config.onError) {
528
- this.config.onError(error.toSmoreError());
600
+ const smoreError = error.toSmoreError();
601
+ if (this._onErrorCallbacks.size > 0) {
602
+ this._onErrorCallbacks.forEach((cb) => cb(smoreError));
529
603
  } else {
530
604
  this.logger.error(error.message, error.details);
531
605
  }
@@ -538,10 +612,7 @@ class ControllerImpl {
538
612
  }
539
613
  }
540
614
  function createController(config) {
541
- const controller = new ControllerImpl(config ?? {});
542
- const promise = controller.initialize().then(() => controller);
543
- promise.instance = controller;
544
- return promise;
615
+ return new ControllerImpl(config ?? {});
545
616
  }
546
617
 
547
618
  exports.createController = createController;