@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.
@@ -28,121 +28,139 @@ class ScreenImpl {
28
28
  _roomCode = "";
29
29
  _isReady = false;
30
30
  _isDestroyed = false;
31
+ _initTimeoutId = null;
31
32
  eventHandlers = /* @__PURE__ */ new Map();
32
33
  registeredTransportHandlers = [];
33
34
  boundMessageHandler = null;
34
- // Maps user-facing handler transport wrappedHandler for proper cleanup in on()/off()
35
+ // Maps user-facing handler -> transport wrappedHandler for proper cleanup in on()/off()
35
36
  handlerToTransport = /* @__PURE__ */ new Map();
36
- // Tracks event names registered via config.listeners so off(event) without handler won't remove them
37
- _configListenerEvents = /* @__PURE__ */ new Set();
37
+ // Pending handlers registered via on() before transport is ready
38
+ _pendingHandlers = [];
39
+ // Lifecycle callback arrays
40
+ _onAllReadyCallbacks = /* @__PURE__ */ new Set();
41
+ _onControllerJoinCallbacks = /* @__PURE__ */ new Set();
42
+ _onControllerLeaveCallbacks = /* @__PURE__ */ new Set();
43
+ _onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
44
+ _onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
45
+ _onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
46
+ _onRateLimitedCallbacks = /* @__PURE__ */ new Set();
47
+ _onErrorCallbacks = /* @__PURE__ */ new Set();
48
+ // Whether all-ready has fired
49
+ _allReadyFired = false;
50
+ // Ready promise
51
+ _readyResolve;
52
+ _readyReject;
53
+ ready;
38
54
  constructor(config = {}) {
39
55
  this.config = config;
40
56
  this.logger = new logger.DebugLogger(config.debug, "[SmoreScreen]");
41
- if (config.listeners) {
42
- for (const event of Object.keys(config.listeners)) {
43
- events.validateEventName(event);
44
- }
45
- }
57
+ this.ready = new Promise((resolve, reject) => {
58
+ this._readyResolve = resolve;
59
+ this._readyReject = reject;
60
+ });
61
+ this.startInitialization();
46
62
  }
47
63
  // ---------------------------------------------------------------------------
48
- // Initialization (called by factory)
64
+ // Initialization (called in constructor)
49
65
  // ---------------------------------------------------------------------------
50
- async initialize() {
66
+ startInitialization() {
51
67
  this.logger.lifecycle("Initializing screen...");
52
68
  const parentOrigin = this.config.parentOrigin ?? "*";
53
69
  const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
54
- return new Promise((resolve, reject) => {
55
- const timeoutId = setTimeout(() => {
56
- this.cleanup();
57
- const error = new errors.SmoreSDKError(
58
- "TIMEOUT",
59
- `Screen initialization timed out after ${timeout}ms. Make sure the parent frame sends _bridge:init. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Screen instance to retry (this instance has been cleaned up).`,
60
- { details: { timeout } }
61
- );
62
- this.handleError(error);
63
- reject(error);
64
- }, timeout);
65
- this.boundMessageHandler = (e) => {
66
- if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
67
- const msg = e.data;
68
- if (!protocol.isBridgeMessage(msg)) return;
69
- if (msg.type === "_bridge:init") {
70
- clearTimeout(timeoutId);
71
- const initPayload = msg.payload;
72
- try {
73
- protocol.validateInitPayload(initPayload);
74
- } catch (err) {
75
- const error = new errors.SmoreSDKError(
76
- "INIT_FAILED",
77
- `Invalid _bridge:init payload: ${err instanceof Error ? err.message : String(err)}`,
78
- { details: { payload: initPayload } }
79
- );
80
- this.logger.warn("_bridge:init validation failed", error);
81
- this.handleError(error);
82
- reject(error);
83
- return;
84
- }
85
- const initData = initPayload;
86
- if (initData.side !== "host") {
87
- const error = new errors.SmoreSDKError(
88
- "INIT_FAILED",
89
- `Received init for wrong side: ${initData.side}. Expected "host".`,
90
- { details: { side: initData.side } }
91
- );
92
- this.handleError(error);
93
- reject(error);
94
- return;
95
- }
96
- this.transport = new PostMessageTransport.PostMessageTransport(parentOrigin);
97
- this._roomCode = initData.roomCode;
98
- this._controllers = this.mapControllersFromInit(initData.players);
99
- if (this._controllers.length === 0) {
100
- this.logger.warn("Screen initialized with zero controllers");
101
- }
102
- this.setupEventHandlers();
103
- this._isReady = true;
104
- this.logger.lifecycle("Screen ready", {
105
- roomCode: this._roomCode,
106
- controllers: this._controllers.length
107
- });
108
- const autoReady = config.getGlobalConfig().autoReady ?? true;
109
- if (autoReady) {
110
- this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
111
- this.signalReady();
112
- }
113
- resolve();
114
- } else if (msg.type === "_bridge:update") {
115
- if (!this._isReady) {
116
- this.logger.debug("Ignoring _bridge:update before init completes");
117
- return;
118
- }
119
- const updateData = msg.payload;
120
- if (updateData.players && Array.isArray(updateData.players)) {
121
- const oldControllers = this._controllers;
122
- const newControllers = this.mapControllersFromInit(updateData.players);
123
- this._controllers = newControllers;
124
- for (const nc of newControllers) {
125
- if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
126
- this.logger.lifecycle("Controller joined (via update)", { playerIndex: nc.playerIndex });
127
- this.config.onControllerJoin?.(nc.playerIndex, nc);
128
- }
70
+ this._initTimeoutId = setTimeout(() => {
71
+ this.cleanup();
72
+ const error = new errors.SmoreSDKError(
73
+ "TIMEOUT",
74
+ `Screen initialization timed out after ${timeout}ms. Make sure the parent frame sends _bridge:init. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Screen instance to retry (this instance has been cleaned up).`,
75
+ { details: { timeout } }
76
+ );
77
+ this.handleError(error);
78
+ this._readyReject(error);
79
+ }, timeout);
80
+ this.boundMessageHandler = (e) => {
81
+ if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
82
+ const msg = e.data;
83
+ if (!protocol.isBridgeMessage(msg)) return;
84
+ if (msg.type === "_bridge:init") {
85
+ clearTimeout(this._initTimeoutId);
86
+ const initPayload = msg.payload;
87
+ try {
88
+ protocol.validateInitPayload(initPayload);
89
+ } catch (err) {
90
+ const error = new errors.SmoreSDKError(
91
+ "INIT_FAILED",
92
+ `Invalid _bridge:init payload: ${err instanceof Error ? err.message : String(err)}`,
93
+ { details: { payload: initPayload } }
94
+ );
95
+ this.logger.warn("_bridge:init validation failed", error);
96
+ this.handleError(error);
97
+ this._readyReject(error);
98
+ return;
99
+ }
100
+ const initData = initPayload;
101
+ if (initData.side !== "host") {
102
+ const error = new errors.SmoreSDKError(
103
+ "INIT_FAILED",
104
+ `Received init for wrong side: ${initData.side}. Expected "host".`,
105
+ { details: { side: initData.side } }
106
+ );
107
+ this.handleError(error);
108
+ this._readyReject(error);
109
+ return;
110
+ }
111
+ this.transport = new PostMessageTransport.PostMessageTransport(parentOrigin);
112
+ this._roomCode = initData.roomCode;
113
+ this._controllers = this.mapControllersFromInit(initData.players);
114
+ if (this._controllers.length === 0) {
115
+ this.logger.warn("Screen initialized with zero controllers");
116
+ }
117
+ this.setupEventHandlers();
118
+ for (const { event, handler } of this._pendingHandlers) {
119
+ this.setupUserEventHandler(event, handler);
120
+ }
121
+ this._pendingHandlers = [];
122
+ this._isReady = true;
123
+ this.logger.lifecycle("Screen ready", {
124
+ roomCode: this._roomCode,
125
+ controllers: this._controllers.length
126
+ });
127
+ const autoReady = config.getGlobalConfig().autoReady ?? true;
128
+ if (autoReady) {
129
+ this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
130
+ this.signalReady();
131
+ }
132
+ this._readyResolve();
133
+ } else if (msg.type === "_bridge:update") {
134
+ if (!this._isReady) {
135
+ this.logger.debug("Ignoring _bridge:update before init completes");
136
+ return;
137
+ }
138
+ const updateData = msg.payload;
139
+ if (updateData.players && Array.isArray(updateData.players)) {
140
+ const oldControllers = this._controllers;
141
+ const newControllers = this.mapControllersFromInit(updateData.players);
142
+ this._controllers = newControllers;
143
+ for (const nc of newControllers) {
144
+ if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
145
+ this.logger.lifecycle("Controller joined (via update)", { playerIndex: nc.playerIndex });
146
+ this._onControllerJoinCallbacks.forEach((cb) => cb(nc.playerIndex, nc));
129
147
  }
130
- for (const oc of oldControllers) {
131
- if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
132
- this.logger.lifecycle("Controller left (via update)", { playerIndex: oc.playerIndex });
133
- this.config.onControllerLeave?.(oc.playerIndex);
134
- }
148
+ }
149
+ for (const oc of oldControllers) {
150
+ if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
151
+ this.logger.lifecycle("Controller left (via update)", { playerIndex: oc.playerIndex });
152
+ this._onControllerLeaveCallbacks.forEach((cb) => cb(oc.playerIndex));
135
153
  }
136
154
  }
137
- this.logger.lifecycle("Room updated", {
138
- controllers: this._controllers.length
139
- });
140
155
  }
141
- };
142
- window.addEventListener("message", this.boundMessageHandler);
143
- window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
144
- this.logger.lifecycle("Sent _bridge:ready to parent");
145
- });
156
+ this.logger.lifecycle("Room updated", {
157
+ controllers: this._controllers.length
158
+ });
159
+ }
160
+ };
161
+ window.addEventListener("message", this.boundMessageHandler);
162
+ window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
163
+ this.logger.lifecycle("Sent _bridge:ready to parent");
146
164
  }
147
165
  mapControllersFromInit(players) {
148
166
  return players.map((p, index) => ({
@@ -169,7 +187,7 @@ class ScreenImpl {
169
187
  if (this._controllers.some((c) => c.playerIndex === controllerInfo.playerIndex)) return;
170
188
  this._controllers = [...this._controllers, controllerInfo];
171
189
  this.logger.lifecycle("Controller joined", { playerIndex: controllerInfo.playerIndex });
172
- this.config.onControllerJoin?.(controllerInfo.playerIndex, controllerInfo);
190
+ this._onControllerJoinCallbacks.forEach((cb) => cb(controllerInfo.playerIndex, controllerInfo));
173
191
  }
174
192
  });
175
193
  this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_LEFT, (data) => {
@@ -179,7 +197,7 @@ class ScreenImpl {
179
197
  if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
180
198
  this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
181
199
  this.logger.lifecycle("Controller left", { playerIndex });
182
- this.config.onControllerLeave?.(playerIndex);
200
+ this._onControllerLeaveCallbacks.forEach((cb) => cb(playerIndex));
183
201
  }
184
202
  });
185
203
  this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_DISCONNECTED, (data) => {
@@ -190,7 +208,7 @@ class ScreenImpl {
190
208
  (c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
191
209
  );
192
210
  this.logger.lifecycle("Controller disconnected", { playerIndex });
193
- this.config.onControllerDisconnect?.(playerIndex);
211
+ this._onControllerDisconnectCallbacks.forEach((cb) => cb(playerIndex));
194
212
  }
195
213
  });
196
214
  this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_RECONNECTED, (data) => {
@@ -207,38 +225,33 @@ class ScreenImpl {
207
225
  (c) => c.playerIndex === controllerInfo.playerIndex ? controllerInfo : c
208
226
  );
209
227
  this.logger.lifecycle("Controller reconnected", { playerIndex: controllerInfo.playerIndex });
210
- this.config.onControllerReconnect?.(controllerInfo.playerIndex, controllerInfo);
228
+ this._onControllerReconnectCallbacks.forEach((cb) => cb(controllerInfo.playerIndex, controllerInfo));
211
229
  }
212
230
  });
213
231
  this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (data) => {
214
232
  const payload = data;
215
233
  const playerData = payload?.player;
216
234
  if (playerData && typeof playerData.playerIndex === "number") {
235
+ const pi = playerData.playerIndex;
217
236
  const appearance = playerData.character ?? null;
218
237
  this._controllers = this._controllers.map(
219
- (c) => c.playerIndex === playerData.playerIndex ? { ...c, appearance } : c
238
+ (c) => c.playerIndex === pi ? { ...c, appearance } : c
220
239
  );
221
- this.logger.lifecycle("Player character updated", { playerIndex: playerData.playerIndex });
222
- this.config.onCharacterUpdated?.(playerData.playerIndex, appearance ?? null);
240
+ this.logger.lifecycle("Player character updated", { playerIndex: pi });
241
+ this._onCharacterUpdatedCallbacks.forEach((cb) => cb(pi, appearance ?? null));
223
242
  }
224
243
  });
225
244
  this.registerTransportHandler(events.SMORE_EVENTS.RATE_LIMITED, (data) => {
226
245
  const payload = data;
227
246
  const event = payload?.event ?? "unknown";
228
247
  this.logger.warn(`Rate limited: ${event}`);
229
- this.config.onRateLimited?.(event);
248
+ this._onRateLimitedCallbacks.forEach((cb) => cb(event));
230
249
  });
231
250
  this.registerTransportHandler(events.SMORE_EVENTS.ALL_READY, () => {
232
251
  this.logger.lifecycle("All participants ready");
233
- this.config.onReady?.();
252
+ this._allReadyFired = true;
253
+ this._onAllReadyCallbacks.forEach((cb) => cb());
234
254
  });
235
- if (this.config.listeners) {
236
- for (const [event, handler] of Object.entries(this.config.listeners)) {
237
- if (!handler) continue;
238
- this._configListenerEvents.add(event);
239
- this.setupUserEventHandler(event, handler);
240
- }
241
- }
242
255
  }
243
256
  /**
244
257
  * Sets up a user event handler with playerIndex extraction.
@@ -280,6 +293,7 @@ class ScreenImpl {
280
293
  this.eventHandlers.set(event, handlers);
281
294
  }
282
295
  handlers.add(handler);
296
+ this.handlerToTransport.set(handler, { event, transportHandler: wrappedHandler });
283
297
  }
284
298
  registerTransportHandler(event, handler) {
285
299
  if (!this.transport) return;
@@ -306,6 +320,60 @@ class ScreenImpl {
306
320
  return this._isDestroyed;
307
321
  }
308
322
  // ---------------------------------------------------------------------------
323
+ // Lifecycle Methods
324
+ // ---------------------------------------------------------------------------
325
+ onAllReady(callback) {
326
+ if (this._allReadyFired) {
327
+ callback();
328
+ }
329
+ this._onAllReadyCallbacks.add(callback);
330
+ return () => {
331
+ this._onAllReadyCallbacks.delete(callback);
332
+ };
333
+ }
334
+ onControllerJoin(callback) {
335
+ this._onControllerJoinCallbacks.add(callback);
336
+ return () => {
337
+ this._onControllerJoinCallbacks.delete(callback);
338
+ };
339
+ }
340
+ onControllerLeave(callback) {
341
+ this._onControllerLeaveCallbacks.add(callback);
342
+ return () => {
343
+ this._onControllerLeaveCallbacks.delete(callback);
344
+ };
345
+ }
346
+ onControllerDisconnect(callback) {
347
+ this._onControllerDisconnectCallbacks.add(callback);
348
+ return () => {
349
+ this._onControllerDisconnectCallbacks.delete(callback);
350
+ };
351
+ }
352
+ onControllerReconnect(callback) {
353
+ this._onControllerReconnectCallbacks.add(callback);
354
+ return () => {
355
+ this._onControllerReconnectCallbacks.delete(callback);
356
+ };
357
+ }
358
+ onCharacterUpdated(callback) {
359
+ this._onCharacterUpdatedCallbacks.add(callback);
360
+ return () => {
361
+ this._onCharacterUpdatedCallbacks.delete(callback);
362
+ };
363
+ }
364
+ onRateLimited(callback) {
365
+ this._onRateLimitedCallbacks.add(callback);
366
+ return () => {
367
+ this._onRateLimitedCallbacks.delete(callback);
368
+ };
369
+ }
370
+ onError(callback) {
371
+ this._onErrorCallbacks.add(callback);
372
+ return () => {
373
+ this._onErrorCallbacks.delete(callback);
374
+ };
375
+ }
376
+ // ---------------------------------------------------------------------------
309
377
  // Communication Methods
310
378
  // ---------------------------------------------------------------------------
311
379
  /**
@@ -412,11 +480,8 @@ class ScreenImpl {
412
480
  /**
413
481
  * Register an event handler for messages from controllers.
414
482
  *
415
- * **Important:** If called before the Screen is ready (i.e., before `await createScreen()`
416
- * resolves or before the `onReady` callback fires), the handler is stored locally but
417
- * will NOT be registered with the transport layer. This means the handler will never
418
- * fire for events received during the pre-ready window. Always call `on()` after
419
- * initialization completes, or use `config.listeners` for handlers needed from the start.
483
+ * Can be called before the Screen is ready. Handlers registered before ready
484
+ * are queued and activated when the transport becomes available.
420
485
  */
421
486
  on(event, handler) {
422
487
  events.validateEventName(event);
@@ -426,9 +491,8 @@ class ScreenImpl {
426
491
  this.eventHandlers.set(event, handlers);
427
492
  }
428
493
  handlers.add(handler);
429
- let wrappedHandler = null;
430
494
  if (this.transport) {
431
- wrappedHandler = (data) => {
495
+ const wrappedHandler = (data) => {
432
496
  this.logger.receive(event, data);
433
497
  const payload = data;
434
498
  const { playerIndex, ...rest } = payload;
@@ -448,16 +512,22 @@ class ScreenImpl {
448
512
  };
449
513
  this.registerTransportHandler(event, wrappedHandler);
450
514
  this.handlerToTransport.set(handler, { event, transportHandler: wrappedHandler });
515
+ } else {
516
+ this._pendingHandlers.push({ event, handler });
451
517
  }
452
518
  return () => {
453
519
  handlers?.delete(handler);
454
520
  if (handlers?.size === 0) {
455
521
  this.eventHandlers.delete(event);
456
522
  }
457
- if (wrappedHandler) {
458
- this.transport?.off(event, wrappedHandler);
523
+ this._pendingHandlers = this._pendingHandlers.filter(
524
+ (p) => !(p.event === event && p.handler === handler)
525
+ );
526
+ const entry = this.handlerToTransport.get(handler);
527
+ if (entry) {
528
+ this.transport?.off(event, entry.transportHandler);
459
529
  this.registeredTransportHandlers = this.registeredTransportHandlers.filter(
460
- (h) => h.handler !== wrappedHandler
530
+ (h) => h.handler !== entry.transportHandler
461
531
  );
462
532
  this.handlerToTransport.delete(handler);
463
533
  }
@@ -474,16 +544,6 @@ class ScreenImpl {
474
544
  * @param event - Event name to listen for
475
545
  * @param handler - Handler function to call once
476
546
  * @returns Unsubscribe function to remove the handler before it fires
477
- *
478
- * @example
479
- * ```ts
480
- * const unsubscribe = screen.once('ready', (playerIndex, data) => {
481
- * console.log('Ready event received');
482
- * });
483
- *
484
- * // To remove before the event fires:
485
- * unsubscribe();
486
- * ```
487
547
  */
488
548
  once(event, handler) {
489
549
  const wrappedHandler = (playerIndex, data) => {
@@ -495,24 +555,13 @@ class ScreenImpl {
495
555
  }
496
556
  off(event, handler) {
497
557
  if (!handler) {
498
- if (this._configListenerEvents.has(event)) {
499
- for (const [key, val] of this.handlerToTransport) {
500
- if (val.event === event) {
501
- this.transport?.off(event, val.transportHandler);
502
- this.registeredTransportHandlers = this.registeredTransportHandlers.filter(
503
- (h) => h.handler !== val.transportHandler
504
- );
505
- this.handlerToTransport.delete(key);
506
- }
507
- }
508
- } else {
509
- this.eventHandlers.delete(event);
510
- this.transport?.off(event);
511
- this.registeredTransportHandlers = this.registeredTransportHandlers.filter((h) => h.event !== event);
512
- for (const [key, val] of this.handlerToTransport) {
513
- if (val.event === event) this.handlerToTransport.delete(key);
514
- }
558
+ this.eventHandlers.delete(event);
559
+ this.transport?.off(event);
560
+ this.registeredTransportHandlers = this.registeredTransportHandlers.filter((h) => h.event !== event);
561
+ for (const [key, val] of this.handlerToTransport) {
562
+ if (val.event === event) this.handlerToTransport.delete(key);
515
563
  }
564
+ this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
516
565
  } else {
517
566
  const handlers = this.eventHandlers.get(event);
518
567
  handlers?.delete(handler);
@@ -527,6 +576,9 @@ class ScreenImpl {
527
576
  );
528
577
  this.handlerToTransport.delete(handler);
529
578
  }
579
+ this._pendingHandlers = this._pendingHandlers.filter(
580
+ (p) => !(p.event === event && p.handler === handler)
581
+ );
530
582
  }
531
583
  }
532
584
  // ---------------------------------------------------------------------------
@@ -538,25 +590,6 @@ class ScreenImpl {
538
590
  getControllerCount() {
539
591
  return this._controllers.filter((c) => c.connected).length;
540
592
  }
541
- /**
542
- * Check if there is at least one connected controller.
543
- * Useful for detecting when all players have disconnected
544
- * (e.g., to pause the game or show a waiting screen).
545
- *
546
- * Use this in onControllerDisconnect callback to detect when all controllers have disconnected.
547
- *
548
- * @example
549
- * ```ts
550
- * const screen = await createScreen<MyEvents>({
551
- * onControllerDisconnect: (playerIndex) => {
552
- * if (!screen.hasAnyConnectedControllers()) {
553
- * console.log('All controllers disconnected!');
554
- * screen.broadcast('waiting-for-players', {});
555
- * }
556
- * },
557
- * });
558
- * ```
559
- */
560
593
  hasAnyConnectedControllers() {
561
594
  return this._controllers.some((c) => c.connected);
562
595
  }
@@ -572,12 +605,25 @@ class ScreenImpl {
572
605
  this.logger.lifecycle("Screen destroyed");
573
606
  }
574
607
  cleanup() {
608
+ if (this._initTimeoutId) {
609
+ clearTimeout(this._initTimeoutId);
610
+ this._initTimeoutId = null;
611
+ }
575
612
  for (const { event, handler } of this.registeredTransportHandlers) {
576
613
  this.transport?.off(event, handler);
577
614
  }
578
615
  this.registeredTransportHandlers = [];
579
616
  this.eventHandlers.clear();
580
617
  this.handlerToTransport.clear();
618
+ this._pendingHandlers = [];
619
+ this._onAllReadyCallbacks.clear();
620
+ this._onControllerJoinCallbacks.clear();
621
+ this._onControllerLeaveCallbacks.clear();
622
+ this._onControllerDisconnectCallbacks.clear();
623
+ this._onControllerReconnectCallbacks.clear();
624
+ this._onCharacterUpdatedCallbacks.clear();
625
+ this._onRateLimitedCallbacks.clear();
626
+ this._onErrorCallbacks.clear();
581
627
  if (this.transport instanceof PostMessageTransport.PostMessageTransport) {
582
628
  this.transport.destroy();
583
629
  }
@@ -593,8 +639,8 @@ class ScreenImpl {
593
639
  handleError(error) {
594
640
  this.logger.warn(`Error in handler: ${error.message}`);
595
641
  const smoreError = error.toSmoreError();
596
- if (this.config.onError) {
597
- this.config.onError(smoreError);
642
+ if (this._onErrorCallbacks.size > 0) {
643
+ this._onErrorCallbacks.forEach((cb) => cb(smoreError));
598
644
  } else {
599
645
  this.logger.error(error.message, error.details);
600
646
  }
@@ -610,17 +656,14 @@ class ScreenImpl {
610
656
  if (!this._isReady || !this.transport) {
611
657
  throw new errors.SmoreSDKError(
612
658
  "NOT_READY",
613
- `Cannot call ${method}() before screen is ready. Use await createScreen() or onReady callback.`,
659
+ `Cannot call ${method}() before screen is ready. Use await screen.ready.`,
614
660
  { details: { method } }
615
661
  );
616
662
  }
617
663
  }
618
664
  }
619
665
  function createScreen(config) {
620
- const screen = new ScreenImpl(config);
621
- const promise = screen.initialize().then(() => screen);
622
- promise.instance = screen;
623
- return promise;
666
+ return new ScreenImpl(config);
624
667
  }
625
668
 
626
669
  exports.createScreen = createScreen;