@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.
- package/dist/cjs/controller.cjs +214 -143
- package/dist/cjs/controller.cjs.map +1 -1
- package/dist/cjs/screen.cjs +220 -177
- package/dist/cjs/screen.cjs.map +1 -1
- package/dist/cjs/testing.cjs +160 -139
- package/dist/cjs/testing.cjs.map +1 -1
- package/dist/esm/controller.js +215 -144
- package/dist/esm/controller.js.map +1 -1
- package/dist/esm/screen.js +221 -178
- package/dist/esm/screen.js.map +1 -1
- package/dist/esm/testing.js +160 -139
- package/dist/esm/testing.js.map +1 -1
- package/dist/types/controller.d.ts +22 -43
- package/dist/types/controller.d.ts.map +1 -1
- package/dist/types/index.d.ts +14 -14
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/screen.d.ts +26 -37
- package/dist/types/screen.d.ts.map +1 -1
- package/dist/types/testing.d.ts +16 -0
- package/dist/types/testing.d.ts.map +1 -1
- package/dist/types/types.d.ts +250 -267
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +594 -459
- package/dist/umd/smore-sdk.umd.js.map +1 -1
- package/dist/umd/smore-sdk.umd.min.js +1 -1
- package/dist/umd/smore-sdk.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/screen.cjs
CHANGED
|
@@ -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
|
|
35
|
+
// Maps user-facing handler -> transport wrappedHandler for proper cleanup in on()/off()
|
|
35
36
|
handlerToTransport = /* @__PURE__ */ new Map();
|
|
36
|
-
//
|
|
37
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
64
|
+
// Initialization (called in constructor)
|
|
49
65
|
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
this.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 ===
|
|
238
|
+
(c) => c.playerIndex === pi ? { ...c, appearance } : c
|
|
220
239
|
);
|
|
221
|
-
this.logger.lifecycle("Player character updated", { playerIndex:
|
|
222
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
416
|
-
*
|
|
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
|
-
|
|
458
|
-
|
|
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 !==
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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.
|
|
597
|
-
this.
|
|
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
|
|
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
|
-
|
|
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;
|