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