@smoregg/sdk 2.0.0 → 2.2.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 +260 -113
- package/dist/cjs/controller.cjs.map +1 -1
- package/dist/cjs/errors.cjs +1 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/events.cjs +26 -3
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/index.cjs +2 -7
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/screen.cjs +244 -128
- package/dist/cjs/screen.cjs.map +1 -1
- package/dist/cjs/shared.cjs +34 -0
- package/dist/cjs/shared.cjs.map +1 -0
- package/dist/cjs/testing.cjs +181 -73
- package/dist/cjs/testing.cjs.map +1 -1
- package/dist/cjs/transport/PostMessageTransport.cjs +12 -0
- package/dist/cjs/transport/PostMessageTransport.cjs.map +1 -1
- package/dist/cjs/transport/protocol.cjs +2 -0
- package/dist/cjs/transport/protocol.cjs.map +1 -1
- package/dist/cjs/types.cjs +16 -0
- package/dist/cjs/types.cjs.map +1 -0
- package/dist/esm/controller.js +262 -115
- package/dist/esm/controller.js.map +1 -1
- package/dist/esm/errors.js +1 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/events.js +25 -4
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.js +1 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/screen.js +246 -130
- package/dist/esm/screen.js.map +1 -1
- package/dist/esm/shared.js +30 -0
- package/dist/esm/shared.js.map +1 -0
- package/dist/esm/testing.js +181 -73
- package/dist/esm/testing.js.map +1 -1
- package/dist/esm/transport/PostMessageTransport.js +12 -0
- package/dist/esm/transport/PostMessageTransport.js.map +1 -1
- package/dist/esm/transport/protocol.js +2 -1
- package/dist/esm/transport/protocol.js.map +1 -1
- package/dist/esm/types.js +14 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/controller.d.ts +1 -1
- package/dist/types/controller.d.ts.map +1 -1
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/events.d.ts +14 -1
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/screen.d.ts +3 -3
- package/dist/types/screen.d.ts.map +1 -1
- package/dist/types/shared.d.ts +21 -0
- package/dist/types/shared.d.ts.map +1 -0
- package/dist/types/testing.d.ts +65 -4
- package/dist/types/testing.d.ts.map +1 -1
- package/dist/types/transport/PostMessageTransport.d.ts +1 -0
- package/dist/types/transport/PostMessageTransport.d.ts.map +1 -1
- package/dist/types/transport/protocol.d.ts +5 -0
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/types.d.ts +254 -345
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +575 -784
- 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 +7 -1
- package/dist/cjs/config.cjs +0 -13
- package/dist/cjs/config.cjs.map +0 -1
- package/dist/esm/config.js +0 -10
- package/dist/esm/config.js.map +0 -1
- package/dist/types/config.d.ts +0 -35
- package/dist/types/config.d.ts.map +0 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
})(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
7
|
const BRIDGE_MSG_PREFIX = "_bridge:";
|
|
8
|
+
const PROTOCOL_VERSION = 1;
|
|
8
9
|
function isBridgeMessage(data) {
|
|
9
10
|
return data !== null && typeof data === "object" && "type" in data && typeof data.type === "string" && data.type.startsWith(BRIDGE_MSG_PREFIX);
|
|
10
11
|
}
|
|
@@ -29,6 +30,8 @@
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
class PostMessageTransport {
|
|
33
|
+
static ACK_TIMEOUT = 3e4;
|
|
34
|
+
// 30 seconds
|
|
32
35
|
handlers = /* @__PURE__ */ new Map();
|
|
33
36
|
ackCallbacks = /* @__PURE__ */ new Map();
|
|
34
37
|
ackCounter = 0;
|
|
@@ -47,11 +50,21 @@
|
|
|
47
50
|
const callback = args[0];
|
|
48
51
|
ackId = `ack_${++this.ackCounter}`;
|
|
49
52
|
this.ackCallbacks.set(ackId, callback);
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
if (this.ackCallbacks.has(ackId)) {
|
|
55
|
+
this.ackCallbacks.delete(ackId);
|
|
56
|
+
}
|
|
57
|
+
}, PostMessageTransport.ACK_TIMEOUT);
|
|
50
58
|
} else if (args.length >= 2 && typeof args[args.length - 1] === "function") {
|
|
51
59
|
data = args[0];
|
|
52
60
|
const callback = args[args.length - 1];
|
|
53
61
|
ackId = `ack_${++this.ackCounter}`;
|
|
54
62
|
this.ackCallbacks.set(ackId, callback);
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
if (this.ackCallbacks.has(ackId)) {
|
|
65
|
+
this.ackCallbacks.delete(ackId);
|
|
66
|
+
}
|
|
67
|
+
}, PostMessageTransport.ACK_TIMEOUT);
|
|
55
68
|
}
|
|
56
69
|
window.parent.postMessage(
|
|
57
70
|
{ type: "_bridge:emit", payload: { event, data, ackId } },
|
|
@@ -115,6 +128,7 @@
|
|
|
115
128
|
super(message, options?.cause ? { cause: options.cause } : void 0);
|
|
116
129
|
this.name = "SmoreSDKError";
|
|
117
130
|
this.code = code;
|
|
131
|
+
this.cause = options?.cause;
|
|
118
132
|
this.details = options?.details;
|
|
119
133
|
const ErrorWithCapture = Error;
|
|
120
134
|
if (typeof ErrorWithCapture.captureStackTrace === "function") {
|
|
@@ -134,8 +148,6 @@
|
|
|
134
148
|
const SMORE_EVENTS = {
|
|
135
149
|
// Game lifecycle
|
|
136
150
|
GAME_OVER: "smore:game-over",
|
|
137
|
-
RETURN_TO_LOBBY: "smore:return-to-lobby",
|
|
138
|
-
// Used internally by platform, not handled by SDK
|
|
139
151
|
// Player management
|
|
140
152
|
PLAYER_JOINED: "smore:player-joined",
|
|
141
153
|
PLAYER_LEFT: "smore:player-left",
|
|
@@ -148,9 +160,17 @@
|
|
|
148
160
|
// Game ready sync
|
|
149
161
|
GAME_READY: "smore:game-ready",
|
|
150
162
|
ALL_READY: "smore:all-ready",
|
|
163
|
+
// Connection status (self)
|
|
164
|
+
SELF_DISCONNECTED: "smore:self-disconnected",
|
|
165
|
+
SELF_RECONNECTED: "smore:self-reconnected",
|
|
151
166
|
// Send to specific player (internal use)
|
|
152
|
-
SEND_TO_PLAYER: "smore:send-to-player"
|
|
167
|
+
SEND_TO_PLAYER: "smore:send-to-player",
|
|
153
168
|
// Used internally by platform, not handled by SDK
|
|
169
|
+
// Custom state management
|
|
170
|
+
STATE_SET: "smore:set-custom-state",
|
|
171
|
+
STATE_CHANGED: "smore:custom-state-changed",
|
|
172
|
+
STATE_GET_ALL: "smore:get-custom-states",
|
|
173
|
+
STATE_ALL: "smore:custom-states"
|
|
154
174
|
};
|
|
155
175
|
new Set(
|
|
156
176
|
Object.values(SMORE_EVENTS)
|
|
@@ -176,6 +196,21 @@
|
|
|
176
196
|
);
|
|
177
197
|
}
|
|
178
198
|
}
|
|
199
|
+
const SCREEN_LIFECYCLE_EVENTS = /* @__PURE__ */ new Set([
|
|
200
|
+
"$all-ready",
|
|
201
|
+
"$controller-join",
|
|
202
|
+
"$controller-leave",
|
|
203
|
+
"$controller-disconnect",
|
|
204
|
+
"$controller-reconnect",
|
|
205
|
+
"$character-updated",
|
|
206
|
+
"$error",
|
|
207
|
+
"$connection-change"
|
|
208
|
+
]);
|
|
209
|
+
const CONTROLLER_LIFECYCLE_EVENTS = /* @__PURE__ */ new Set([
|
|
210
|
+
...SCREEN_LIFECYCLE_EVENTS,
|
|
211
|
+
"$game-over",
|
|
212
|
+
"$state-recovery"
|
|
213
|
+
]);
|
|
179
214
|
|
|
180
215
|
class DebugLogger {
|
|
181
216
|
enabled;
|
|
@@ -248,12 +283,30 @@
|
|
|
248
283
|
}
|
|
249
284
|
}
|
|
250
285
|
|
|
251
|
-
|
|
252
|
-
function
|
|
253
|
-
|
|
286
|
+
const MAX_PAYLOAD_SIZE = 65536;
|
|
287
|
+
function mapPlayerDTO(raw, fallbackIndex) {
|
|
288
|
+
return {
|
|
289
|
+
playerIndex: raw.playerIndex ?? fallbackIndex,
|
|
290
|
+
nickname: raw.nickname || raw.name || `Player ${(raw.playerIndex ?? fallbackIndex) + 1}`,
|
|
291
|
+
connected: raw.connected !== false,
|
|
292
|
+
appearance: raw.appearance ?? raw.character
|
|
293
|
+
};
|
|
254
294
|
}
|
|
255
|
-
function
|
|
256
|
-
return
|
|
295
|
+
function validatePayloadSize(data) {
|
|
296
|
+
if (data === void 0 || data === null) return;
|
|
297
|
+
try {
|
|
298
|
+
const serialized = JSON.stringify(data);
|
|
299
|
+
const byteLength = new TextEncoder().encode(serialized).byteLength;
|
|
300
|
+
if (byteLength > MAX_PAYLOAD_SIZE) {
|
|
301
|
+
throw new SmoreSDKError(
|
|
302
|
+
"PAYLOAD_TOO_LARGE",
|
|
303
|
+
`Event payload exceeds maximum size of ${MAX_PAYLOAD_SIZE} bytes (got ${byteLength} bytes). The server will silently drop this event.`,
|
|
304
|
+
{ details: { size: byteLength, limit: MAX_PAYLOAD_SIZE } }
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (err instanceof SmoreSDKError) throw err;
|
|
309
|
+
}
|
|
257
310
|
}
|
|
258
311
|
|
|
259
312
|
const DEFAULT_TIMEOUT$1 = 1e4;
|
|
@@ -285,17 +338,19 @@
|
|
|
285
338
|
handlerToTransport = /* @__PURE__ */ new Map();
|
|
286
339
|
// Pending handlers registered via on() before transport is ready
|
|
287
340
|
_pendingHandlers = [];
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
_onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
|
|
293
|
-
_onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
|
|
294
|
-
_onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
|
|
295
|
-
_onRateLimitedCallbacks = /* @__PURE__ */ new Set();
|
|
296
|
-
_onErrorCallbacks = /* @__PURE__ */ new Set();
|
|
341
|
+
// Unified lifecycle listener map (supports both onXxx() and on('$xxx') patterns)
|
|
342
|
+
_lifecycleListeners = /* @__PURE__ */ new Map();
|
|
343
|
+
// Outbound message buffer (messages sent before ready)
|
|
344
|
+
_outboundBuffer = [];
|
|
297
345
|
// Whether all-ready has fired
|
|
298
346
|
_allReadyFired = false;
|
|
347
|
+
// Self-connection awareness
|
|
348
|
+
_isConnected = false;
|
|
349
|
+
// Custom state management
|
|
350
|
+
_customStates = /* @__PURE__ */ new Map();
|
|
351
|
+
_stateChangeListeners = /* @__PURE__ */ new Set();
|
|
352
|
+
// Protocol versioning
|
|
353
|
+
_protocolVersion = PROTOCOL_VERSION;
|
|
299
354
|
// Ready promise
|
|
300
355
|
_readyResolve;
|
|
301
356
|
_readyReject;
|
|
@@ -357,8 +412,17 @@
|
|
|
357
412
|
this._readyReject(error);
|
|
358
413
|
return;
|
|
359
414
|
}
|
|
360
|
-
this.transport = new PostMessageTransport(parentOrigin);
|
|
415
|
+
this.transport = this.config.transport ?? new PostMessageTransport(parentOrigin);
|
|
361
416
|
this._roomCode = initData.roomCode;
|
|
417
|
+
const serverProtocolVersion = initData.protocolVersion;
|
|
418
|
+
if (serverProtocolVersion !== void 0) {
|
|
419
|
+
this._protocolVersion = serverProtocolVersion;
|
|
420
|
+
if (serverProtocolVersion !== PROTOCOL_VERSION) {
|
|
421
|
+
this.logger.warn(
|
|
422
|
+
`Protocol version mismatch: SDK v${PROTOCOL_VERSION}, server v${serverProtocolVersion}. Some features may not work correctly.`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
362
426
|
this._controllers = this.mapControllersFromInit(initData.players);
|
|
363
427
|
if (this._controllers.length === 0) {
|
|
364
428
|
this.logger.warn("Screen initialized with zero controllers");
|
|
@@ -368,13 +432,28 @@
|
|
|
368
432
|
this.setupUserEventHandler(event, handler);
|
|
369
433
|
}
|
|
370
434
|
this._pendingHandlers = [];
|
|
435
|
+
this._isConnected = true;
|
|
371
436
|
this._isReady = true;
|
|
437
|
+
for (const buffered of this._outboundBuffer) {
|
|
438
|
+
try {
|
|
439
|
+
switch (buffered.method) {
|
|
440
|
+
case "broadcast":
|
|
441
|
+
this.broadcast(buffered.args[0], buffered.args[1]);
|
|
442
|
+
break;
|
|
443
|
+
case "sendToController":
|
|
444
|
+
this.sendToController(buffered.args[0], buffered.args[1], buffered.args[2]);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
this.handleError(err instanceof SmoreSDKError ? err : new SmoreSDKError("UNKNOWN", "Failed to flush buffered message"));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
this._outboundBuffer = [];
|
|
372
452
|
this.logger.lifecycle("Screen ready", {
|
|
373
453
|
roomCode: this._roomCode,
|
|
374
454
|
controllers: this._controllers.length
|
|
375
455
|
});
|
|
376
|
-
|
|
377
|
-
if (autoReady) {
|
|
456
|
+
if (this.config.autoReady !== false) {
|
|
378
457
|
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
379
458
|
this.signalReady();
|
|
380
459
|
}
|
|
@@ -392,13 +471,13 @@
|
|
|
392
471
|
for (const nc of newControllers) {
|
|
393
472
|
if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
|
|
394
473
|
this.logger.lifecycle("Controller joined (via update)", { playerIndex: nc.playerIndex });
|
|
395
|
-
this.
|
|
474
|
+
this._emitLifecycle("$controller-join", nc.playerIndex, nc);
|
|
396
475
|
}
|
|
397
476
|
}
|
|
398
477
|
for (const oc of oldControllers) {
|
|
399
478
|
if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
|
|
400
479
|
this.logger.lifecycle("Controller left (via update)", { playerIndex: oc.playerIndex });
|
|
401
|
-
this.
|
|
480
|
+
this._emitLifecycle("$controller-leave", oc.playerIndex);
|
|
402
481
|
}
|
|
403
482
|
}
|
|
404
483
|
}
|
|
@@ -408,18 +487,11 @@
|
|
|
408
487
|
}
|
|
409
488
|
};
|
|
410
489
|
window.addEventListener("message", this.boundMessageHandler);
|
|
411
|
-
window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
|
|
490
|
+
window.parent.postMessage({ type: "_bridge:ready", protocolVersion: PROTOCOL_VERSION }, parentOrigin);
|
|
412
491
|
this.logger.lifecycle("Sent _bridge:ready to parent");
|
|
413
492
|
}
|
|
414
493
|
mapControllersFromInit(players) {
|
|
415
|
-
return players.map((p, index) => (
|
|
416
|
-
playerIndex: p.playerIndex ?? index,
|
|
417
|
-
// Fallback to `nickname` for defensive compatibility (server currently always sends `name`)
|
|
418
|
-
nickname: p.nickname || p.name || `Player ${index + 1}`,
|
|
419
|
-
connected: p.connected !== false,
|
|
420
|
-
// Fallback to `appearance` for defensive compatibility (server currently always sends `character`)
|
|
421
|
-
appearance: p.appearance ?? p.character
|
|
422
|
-
}));
|
|
494
|
+
return players.map((p, index) => mapPlayerDTO(p, index));
|
|
423
495
|
}
|
|
424
496
|
setupEventHandlers() {
|
|
425
497
|
if (!this.transport) return;
|
|
@@ -427,16 +499,11 @@
|
|
|
427
499
|
const payload = data;
|
|
428
500
|
const playerData = payload?.player;
|
|
429
501
|
if (playerData && typeof playerData.playerIndex === "number") {
|
|
430
|
-
const controllerInfo =
|
|
431
|
-
playerIndex: playerData.playerIndex,
|
|
432
|
-
nickname: playerData.nickname || playerData.name || `Player ${playerData.playerIndex + 1}`,
|
|
433
|
-
connected: playerData.connected !== false,
|
|
434
|
-
appearance: playerData.appearance ?? playerData.character
|
|
435
|
-
};
|
|
502
|
+
const controllerInfo = mapPlayerDTO(playerData, playerData.playerIndex);
|
|
436
503
|
if (this._controllers.some((c) => c.playerIndex === controllerInfo.playerIndex)) return;
|
|
437
504
|
this._controllers = [...this._controllers, controllerInfo];
|
|
438
505
|
this.logger.lifecycle("Controller joined", { playerIndex: controllerInfo.playerIndex });
|
|
439
|
-
this.
|
|
506
|
+
this._emitLifecycle("$controller-join", controllerInfo.playerIndex, controllerInfo);
|
|
440
507
|
}
|
|
441
508
|
});
|
|
442
509
|
this.registerTransportHandler(SMORE_EVENTS.PLAYER_LEFT, (data) => {
|
|
@@ -446,7 +513,7 @@
|
|
|
446
513
|
if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
447
514
|
this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
448
515
|
this.logger.lifecycle("Controller left", { playerIndex });
|
|
449
|
-
this.
|
|
516
|
+
this._emitLifecycle("$controller-leave", playerIndex);
|
|
450
517
|
}
|
|
451
518
|
});
|
|
452
519
|
this.registerTransportHandler(SMORE_EVENTS.PLAYER_DISCONNECTED, (data) => {
|
|
@@ -457,24 +524,19 @@
|
|
|
457
524
|
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
458
525
|
);
|
|
459
526
|
this.logger.lifecycle("Controller disconnected", { playerIndex });
|
|
460
|
-
this.
|
|
527
|
+
this._emitLifecycle("$controller-disconnect", playerIndex);
|
|
461
528
|
}
|
|
462
529
|
});
|
|
463
530
|
this.registerTransportHandler(SMORE_EVENTS.PLAYER_RECONNECTED, (data) => {
|
|
464
531
|
const payload = data;
|
|
465
532
|
const playerData = payload?.player;
|
|
466
533
|
if (playerData && typeof playerData.playerIndex === "number") {
|
|
467
|
-
const controllerInfo =
|
|
468
|
-
playerIndex: playerData.playerIndex,
|
|
469
|
-
nickname: playerData.nickname || playerData.name || `Player ${playerData.playerIndex + 1}`,
|
|
470
|
-
connected: true,
|
|
471
|
-
appearance: playerData.appearance ?? playerData.character
|
|
472
|
-
};
|
|
534
|
+
const controllerInfo = mapPlayerDTO(playerData, playerData.playerIndex);
|
|
473
535
|
this._controllers = this._controllers.map(
|
|
474
536
|
(c) => c.playerIndex === controllerInfo.playerIndex ? controllerInfo : c
|
|
475
537
|
);
|
|
476
538
|
this.logger.lifecycle("Controller reconnected", { playerIndex: controllerInfo.playerIndex });
|
|
477
|
-
this.
|
|
539
|
+
this._emitLifecycle("$controller-reconnect", controllerInfo.playerIndex, controllerInfo);
|
|
478
540
|
}
|
|
479
541
|
});
|
|
480
542
|
this.registerTransportHandler(SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (data) => {
|
|
@@ -487,19 +549,69 @@
|
|
|
487
549
|
(c) => c.playerIndex === pi ? { ...c, appearance } : c
|
|
488
550
|
);
|
|
489
551
|
this.logger.lifecycle("Player character updated", { playerIndex: pi });
|
|
490
|
-
this.
|
|
552
|
+
this._emitLifecycle("$character-updated", pi, appearance ?? null);
|
|
491
553
|
}
|
|
492
554
|
});
|
|
493
555
|
this.registerTransportHandler(SMORE_EVENTS.RATE_LIMITED, (data) => {
|
|
494
556
|
const payload = data;
|
|
495
|
-
const
|
|
496
|
-
this.
|
|
497
|
-
|
|
557
|
+
const eventName = payload?.event ?? "unknown";
|
|
558
|
+
this.handleError(
|
|
559
|
+
new SmoreSDKError("RATE_LIMITED", `Server rate-limited event: ${eventName}`, {
|
|
560
|
+
details: { event: eventName }
|
|
561
|
+
})
|
|
562
|
+
);
|
|
498
563
|
});
|
|
499
564
|
this.registerTransportHandler(SMORE_EVENTS.ALL_READY, () => {
|
|
500
565
|
this.logger.lifecycle("All participants ready");
|
|
501
566
|
this._allReadyFired = true;
|
|
502
|
-
this.
|
|
567
|
+
this._emitLifecycle("$all-ready");
|
|
568
|
+
});
|
|
569
|
+
this.registerTransportHandler(SMORE_EVENTS.SELF_DISCONNECTED, () => {
|
|
570
|
+
this._isConnected = false;
|
|
571
|
+
this.logger.lifecycle("Connection lost");
|
|
572
|
+
this._emitLifecycle("$connection-change", false);
|
|
573
|
+
});
|
|
574
|
+
this.registerTransportHandler(SMORE_EVENTS.SELF_RECONNECTED, () => {
|
|
575
|
+
this._isConnected = true;
|
|
576
|
+
this.logger.lifecycle("Connection restored");
|
|
577
|
+
this._emitLifecycle("$connection-change", true);
|
|
578
|
+
});
|
|
579
|
+
this.registerTransportHandler(SMORE_EVENTS.STATE_CHANGED, (raw) => {
|
|
580
|
+
const data = raw;
|
|
581
|
+
if (typeof data?.playerIndex === "number" && data.state) {
|
|
582
|
+
this._customStates.set(data.playerIndex, data.state);
|
|
583
|
+
this._stateChangeListeners.forEach((cb) => {
|
|
584
|
+
try {
|
|
585
|
+
cb(data.playerIndex, data.state);
|
|
586
|
+
} catch (err) {
|
|
587
|
+
this.handleError(
|
|
588
|
+
new SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
|
|
589
|
+
cause: err instanceof Error ? err : void 0
|
|
590
|
+
})
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
this.registerTransportHandler(SMORE_EVENTS.STATE_ALL, (raw) => {
|
|
597
|
+
const data = raw;
|
|
598
|
+
if (data?.states) {
|
|
599
|
+
for (const [key, value] of Object.entries(data.states)) {
|
|
600
|
+
const pi = Number(key);
|
|
601
|
+
this._customStates.set(pi, value);
|
|
602
|
+
this._stateChangeListeners.forEach((cb) => {
|
|
603
|
+
try {
|
|
604
|
+
cb(pi, value);
|
|
605
|
+
} catch (err) {
|
|
606
|
+
this.handleError(
|
|
607
|
+
new SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
|
|
608
|
+
cause: err instanceof Error ? err : void 0
|
|
609
|
+
})
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
503
615
|
});
|
|
504
616
|
}
|
|
505
617
|
/**
|
|
@@ -568,6 +680,45 @@
|
|
|
568
680
|
get isDestroyed() {
|
|
569
681
|
return this._isDestroyed;
|
|
570
682
|
}
|
|
683
|
+
get isConnected() {
|
|
684
|
+
return this._isConnected;
|
|
685
|
+
}
|
|
686
|
+
get protocolVersion() {
|
|
687
|
+
return this._protocolVersion;
|
|
688
|
+
}
|
|
689
|
+
// ---------------------------------------------------------------------------
|
|
690
|
+
// Lifecycle Listener Helpers
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
_addLifecycleListener(event, listener) {
|
|
693
|
+
let set = this._lifecycleListeners.get(event);
|
|
694
|
+
if (!set) {
|
|
695
|
+
set = /* @__PURE__ */ new Set();
|
|
696
|
+
this._lifecycleListeners.set(event, set);
|
|
697
|
+
}
|
|
698
|
+
set.add(listener);
|
|
699
|
+
return () => {
|
|
700
|
+
set.delete(listener);
|
|
701
|
+
if (set.size === 0) this._lifecycleListeners.delete(event);
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
_emitLifecycle(event, ...args) {
|
|
705
|
+
this._lifecycleListeners.get(event)?.forEach((cb) => {
|
|
706
|
+
try {
|
|
707
|
+
cb(...args);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
this.handleError(
|
|
710
|
+
new SmoreSDKError("UNKNOWN", `Error in lifecycle handler for "${event}"`, {
|
|
711
|
+
cause: err instanceof Error ? err : void 0,
|
|
712
|
+
details: { event }
|
|
713
|
+
})
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
_hasLifecycleListeners(event) {
|
|
719
|
+
const set = this._lifecycleListeners.get(event);
|
|
720
|
+
return set !== void 0 && set.size > 0;
|
|
721
|
+
}
|
|
571
722
|
// ---------------------------------------------------------------------------
|
|
572
723
|
// Lifecycle Methods
|
|
573
724
|
// ---------------------------------------------------------------------------
|
|
@@ -575,51 +726,46 @@
|
|
|
575
726
|
if (this._allReadyFired) {
|
|
576
727
|
callback();
|
|
577
728
|
}
|
|
578
|
-
this.
|
|
579
|
-
return () => {
|
|
580
|
-
this._onAllReadyCallbacks.delete(callback);
|
|
581
|
-
};
|
|
729
|
+
return this._addLifecycleListener("$all-ready", callback);
|
|
582
730
|
}
|
|
583
731
|
onControllerJoin(callback) {
|
|
584
|
-
this.
|
|
585
|
-
return () => {
|
|
586
|
-
this._onControllerJoinCallbacks.delete(callback);
|
|
587
|
-
};
|
|
732
|
+
return this._addLifecycleListener("$controller-join", callback);
|
|
588
733
|
}
|
|
589
734
|
onControllerLeave(callback) {
|
|
590
|
-
this.
|
|
591
|
-
return () => {
|
|
592
|
-
this._onControllerLeaveCallbacks.delete(callback);
|
|
593
|
-
};
|
|
735
|
+
return this._addLifecycleListener("$controller-leave", callback);
|
|
594
736
|
}
|
|
595
737
|
onControllerDisconnect(callback) {
|
|
596
|
-
this.
|
|
597
|
-
return () => {
|
|
598
|
-
this._onControllerDisconnectCallbacks.delete(callback);
|
|
599
|
-
};
|
|
738
|
+
return this._addLifecycleListener("$controller-disconnect", callback);
|
|
600
739
|
}
|
|
601
740
|
onControllerReconnect(callback) {
|
|
602
|
-
this.
|
|
603
|
-
return () => {
|
|
604
|
-
this._onControllerReconnectCallbacks.delete(callback);
|
|
605
|
-
};
|
|
741
|
+
return this._addLifecycleListener("$controller-reconnect", callback);
|
|
606
742
|
}
|
|
607
743
|
onCharacterUpdated(callback) {
|
|
608
|
-
this.
|
|
609
|
-
return () => {
|
|
610
|
-
this._onCharacterUpdatedCallbacks.delete(callback);
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
onRateLimited(callback) {
|
|
614
|
-
this._onRateLimitedCallbacks.add(callback);
|
|
615
|
-
return () => {
|
|
616
|
-
this._onRateLimitedCallbacks.delete(callback);
|
|
617
|
-
};
|
|
744
|
+
return this._addLifecycleListener("$character-updated", callback);
|
|
618
745
|
}
|
|
619
746
|
onError(callback) {
|
|
620
|
-
this.
|
|
747
|
+
return this._addLifecycleListener("$error", callback);
|
|
748
|
+
}
|
|
749
|
+
onConnectionChange(callback) {
|
|
750
|
+
return this._addLifecycleListener("$connection-change", callback);
|
|
751
|
+
}
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
// Custom State Methods
|
|
754
|
+
// ---------------------------------------------------------------------------
|
|
755
|
+
getControllerState(playerIndex) {
|
|
756
|
+
return this._customStates.get(playerIndex);
|
|
757
|
+
}
|
|
758
|
+
getAllControllerStates() {
|
|
759
|
+
const result = {};
|
|
760
|
+
for (const [key, value] of this._customStates) {
|
|
761
|
+
result[key] = value;
|
|
762
|
+
}
|
|
763
|
+
return result;
|
|
764
|
+
}
|
|
765
|
+
onCustomStateChange(listener) {
|
|
766
|
+
this._stateChangeListeners.add(listener);
|
|
621
767
|
return () => {
|
|
622
|
-
this.
|
|
768
|
+
this._stateChangeListeners.delete(listener);
|
|
623
769
|
};
|
|
624
770
|
}
|
|
625
771
|
// ---------------------------------------------------------------------------
|
|
@@ -629,7 +775,6 @@
|
|
|
629
775
|
* Send type-safe events to all controllers.
|
|
630
776
|
*
|
|
631
777
|
* Uses EventMap generic for compile-time type checking of event names and data payloads.
|
|
632
|
-
* Runtime behavior is identical to broadcastRaw - both call validateEventName at runtime.
|
|
633
778
|
*
|
|
634
779
|
* @note Data should be an object. Primitive values will be wrapped as `{ data: value }` by the relay server.
|
|
635
780
|
* @note Maximum payload size is 64KB. Data exceeding this limit will be silently dropped by the server.
|
|
@@ -638,31 +783,18 @@
|
|
|
638
783
|
*
|
|
639
784
|
* Warning: Avoid sending primitive values directly (string, number, boolean).
|
|
640
785
|
* Wrap in an object: broadcast('event', { value: 42 }) instead of broadcast('event', 42)
|
|
641
|
-
*
|
|
642
|
-
* @see broadcastRaw for bypassing TypeScript type checking (runtime behavior identical)
|
|
643
786
|
*/
|
|
644
787
|
broadcast(event, data) {
|
|
645
|
-
this.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
this.transport
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
* Bypasses EventMap generic type checks at compile time.
|
|
654
|
-
* Runtime behavior is identical to broadcast - both call validateEventName at runtime.
|
|
655
|
-
*
|
|
656
|
-
* Use this when you need dynamic event names or when working without a predefined EventMap.
|
|
657
|
-
*
|
|
658
|
-
* @note Data should be an object. Primitive values will be wrapped as `{ data: value }` by the relay server.
|
|
659
|
-
* @note Maximum payload size is 64KB. Data exceeding this limit will be silently dropped by the server.
|
|
660
|
-
*
|
|
661
|
-
* @see broadcast for type-safe version using EventMap generic
|
|
662
|
-
*/
|
|
663
|
-
broadcastRaw(event, data) {
|
|
664
|
-
this.ensureReady("broadcastRaw");
|
|
788
|
+
if (this._isDestroyed) {
|
|
789
|
+
throw new SmoreSDKError("DESTROYED", "Cannot call broadcast() after destroy()");
|
|
790
|
+
}
|
|
791
|
+
if (!this._isReady || !this.transport) {
|
|
792
|
+
this._outboundBuffer.push({ method: "broadcast", args: [event, data] });
|
|
793
|
+
this.logger.debug(`Buffered broadcast "${event}" (screen not ready yet)`);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
665
796
|
validateEventName(event);
|
|
797
|
+
validatePayloadSize(data);
|
|
666
798
|
this.logger.send(event, data);
|
|
667
799
|
this.transport.emit(event, data);
|
|
668
800
|
}
|
|
@@ -681,24 +813,17 @@
|
|
|
681
813
|
* @param data - Event data payload
|
|
682
814
|
*/
|
|
683
815
|
sendToController(playerIndex, event, data) {
|
|
684
|
-
this.
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (
|
|
688
|
-
this.
|
|
689
|
-
|
|
690
|
-
|
|
816
|
+
if (this._isDestroyed) {
|
|
817
|
+
throw new SmoreSDKError("DESTROYED", "Cannot call sendToController() after destroy()");
|
|
818
|
+
}
|
|
819
|
+
if (!this._isReady || !this.transport) {
|
|
820
|
+
this._outboundBuffer.push({ method: "sendToController", args: [playerIndex, event, data] });
|
|
821
|
+
this.logger.debug(`Buffered sendToController "${event}" -> Player ${playerIndex} (screen not ready yet)`);
|
|
822
|
+
return;
|
|
691
823
|
}
|
|
692
|
-
this.logger.send(`${event} -> Player ${playerIndex}`, data);
|
|
693
|
-
this.transport.emit(event, {
|
|
694
|
-
targetPlayerIndex: playerIndex,
|
|
695
|
-
...data && typeof data === "object" ? data : { data }
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
sendToControllerRaw(playerIndex, event, data) {
|
|
699
|
-
this.ensureReady("sendToControllerRaw");
|
|
700
824
|
validateEventName(event);
|
|
701
825
|
validatePlayerIndex(playerIndex, this._controllers);
|
|
826
|
+
validatePayloadSize(data);
|
|
702
827
|
if (data && typeof data === "object" && "targetPlayerIndex" in data) {
|
|
703
828
|
this.logger.warn(
|
|
704
829
|
`Event "${event}" data contains reserved field "targetPlayerIndex" which will be overwritten for routing.`
|
|
@@ -733,6 +858,16 @@
|
|
|
733
858
|
* are queued and activated when the transport becomes available.
|
|
734
859
|
*/
|
|
735
860
|
on(event, handler) {
|
|
861
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
862
|
+
const validEvents = SCREEN_LIFECYCLE_EVENTS;
|
|
863
|
+
if (!validEvents.has(event)) {
|
|
864
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}". Valid lifecycle events: ${Array.from(validEvents).join(", ")}`);
|
|
865
|
+
}
|
|
866
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
867
|
+
handler();
|
|
868
|
+
}
|
|
869
|
+
return this._addLifecycleListener(event, handler);
|
|
870
|
+
}
|
|
736
871
|
validateEventName(event);
|
|
737
872
|
let handlers = this.eventHandlers.get(event);
|
|
738
873
|
if (!handlers) {
|
|
@@ -795,6 +930,23 @@
|
|
|
795
930
|
* @returns Unsubscribe function to remove the handler before it fires
|
|
796
931
|
*/
|
|
797
932
|
once(event, handler) {
|
|
933
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
934
|
+
const validEvents = SCREEN_LIFECYCLE_EVENTS;
|
|
935
|
+
if (!validEvents.has(event)) {
|
|
936
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}"`);
|
|
937
|
+
}
|
|
938
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
939
|
+
handler();
|
|
940
|
+
return () => {
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const wrapper = (...args) => {
|
|
944
|
+
unsub();
|
|
945
|
+
handler(...args);
|
|
946
|
+
};
|
|
947
|
+
const unsub = this._addLifecycleListener(event, wrapper);
|
|
948
|
+
return unsub;
|
|
949
|
+
}
|
|
798
950
|
const wrappedHandler = (playerIndex, data) => {
|
|
799
951
|
unsubscribe();
|
|
800
952
|
handler(playerIndex, data);
|
|
@@ -803,6 +955,14 @@
|
|
|
803
955
|
return unsubscribe;
|
|
804
956
|
}
|
|
805
957
|
off(event, handler) {
|
|
958
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
959
|
+
if (!handler) {
|
|
960
|
+
this._lifecycleListeners.delete(event);
|
|
961
|
+
} else {
|
|
962
|
+
this._lifecycleListeners.get(event)?.delete(handler);
|
|
963
|
+
}
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
806
966
|
if (!handler) {
|
|
807
967
|
this.eventHandlers.delete(event);
|
|
808
968
|
this.transport?.off(event);
|
|
@@ -830,6 +990,21 @@
|
|
|
830
990
|
);
|
|
831
991
|
}
|
|
832
992
|
}
|
|
993
|
+
removeAllListeners(event) {
|
|
994
|
+
if (event) {
|
|
995
|
+
this.eventHandlers.delete(event);
|
|
996
|
+
this.transport?.off(event);
|
|
997
|
+
this.registeredTransportHandlers = this.registeredTransportHandlers.filter((h) => h.event !== event);
|
|
998
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
999
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
1000
|
+
}
|
|
1001
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
1002
|
+
} else {
|
|
1003
|
+
for (const evt of [...this.eventHandlers.keys()]) {
|
|
1004
|
+
this.removeAllListeners(evt);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
833
1008
|
// ---------------------------------------------------------------------------
|
|
834
1009
|
// Utilities
|
|
835
1010
|
// ---------------------------------------------------------------------------
|
|
@@ -839,9 +1014,6 @@
|
|
|
839
1014
|
getControllerCount() {
|
|
840
1015
|
return this._controllers.filter((c) => c.connected).length;
|
|
841
1016
|
}
|
|
842
|
-
hasAnyConnectedControllers() {
|
|
843
|
-
return this._controllers.some((c) => c.connected);
|
|
844
|
-
}
|
|
845
1017
|
// ---------------------------------------------------------------------------
|
|
846
1018
|
// Cleanup
|
|
847
1019
|
// ---------------------------------------------------------------------------
|
|
@@ -865,14 +1037,11 @@
|
|
|
865
1037
|
this.eventHandlers.clear();
|
|
866
1038
|
this.handlerToTransport.clear();
|
|
867
1039
|
this._pendingHandlers = [];
|
|
868
|
-
this.
|
|
869
|
-
this.
|
|
870
|
-
this.
|
|
871
|
-
this.
|
|
872
|
-
this.
|
|
873
|
-
this._onCharacterUpdatedCallbacks.clear();
|
|
874
|
-
this._onRateLimitedCallbacks.clear();
|
|
875
|
-
this._onErrorCallbacks.clear();
|
|
1040
|
+
this._lifecycleListeners.clear();
|
|
1041
|
+
this._customStates.clear();
|
|
1042
|
+
this._stateChangeListeners.clear();
|
|
1043
|
+
this._isConnected = false;
|
|
1044
|
+
this._outboundBuffer = [];
|
|
876
1045
|
if (this.transport instanceof PostMessageTransport) {
|
|
877
1046
|
this.transport.destroy();
|
|
878
1047
|
}
|
|
@@ -888,8 +1057,8 @@
|
|
|
888
1057
|
handleError(error) {
|
|
889
1058
|
this.logger.warn(`Error in handler: ${error.message}`);
|
|
890
1059
|
const smoreError = error.toSmoreError();
|
|
891
|
-
if (this.
|
|
892
|
-
this.
|
|
1060
|
+
if (this._hasLifecycleListeners("$error")) {
|
|
1061
|
+
this._emitLifecycle("$error", smoreError);
|
|
893
1062
|
} else {
|
|
894
1063
|
this.logger.error(error.message, error.details);
|
|
895
1064
|
}
|
|
@@ -921,7 +1090,7 @@
|
|
|
921
1090
|
config;
|
|
922
1091
|
logger;
|
|
923
1092
|
_roomCode = "";
|
|
924
|
-
|
|
1093
|
+
_myPlayerIndex = -1;
|
|
925
1094
|
_isReady = false;
|
|
926
1095
|
_isDestroyed = false;
|
|
927
1096
|
_initTimeoutId = null;
|
|
@@ -931,19 +1100,20 @@
|
|
|
931
1100
|
// Maps user-facing handler -> transport wrappedHandler for proper cleanup in on()/off()
|
|
932
1101
|
handlerToTransport = /* @__PURE__ */ new Map();
|
|
933
1102
|
_controllers = [];
|
|
1103
|
+
_customStates = /* @__PURE__ */ new Map();
|
|
1104
|
+
_stateChangeListeners = /* @__PURE__ */ new Set();
|
|
934
1105
|
// Pending handlers registered via on() before transport is ready
|
|
935
1106
|
_pendingHandlers = [];
|
|
936
|
-
//
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
_onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
|
|
941
|
-
_onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
|
|
942
|
-
_onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
|
|
943
|
-
_onRateLimitedCallbacks = /* @__PURE__ */ new Set();
|
|
944
|
-
_onErrorCallbacks = /* @__PURE__ */ new Set();
|
|
1107
|
+
// Unified lifecycle listener map (supports both onXxx() and on('$xxx') patterns)
|
|
1108
|
+
_lifecycleListeners = /* @__PURE__ */ new Map();
|
|
1109
|
+
// Outbound message buffer (messages sent before ready)
|
|
1110
|
+
_outboundBuffer = [];
|
|
945
1111
|
// Whether all-ready has fired
|
|
946
1112
|
_allReadyFired = false;
|
|
1113
|
+
// Self-connection awareness
|
|
1114
|
+
_isConnected = false;
|
|
1115
|
+
// Protocol versioning
|
|
1116
|
+
_protocolVersion = PROTOCOL_VERSION;
|
|
947
1117
|
// Ready promise
|
|
948
1118
|
_readyResolve;
|
|
949
1119
|
_readyReject;
|
|
@@ -960,8 +1130,8 @@
|
|
|
960
1130
|
// ---------------------------------------------------------------------------
|
|
961
1131
|
// Properties (readonly)
|
|
962
1132
|
// ---------------------------------------------------------------------------
|
|
963
|
-
get
|
|
964
|
-
return this.
|
|
1133
|
+
get myPlayerIndex() {
|
|
1134
|
+
return this._myPlayerIndex;
|
|
965
1135
|
}
|
|
966
1136
|
get roomCode() {
|
|
967
1137
|
return this._roomCode;
|
|
@@ -972,6 +1142,12 @@
|
|
|
972
1142
|
get isDestroyed() {
|
|
973
1143
|
return this._isDestroyed;
|
|
974
1144
|
}
|
|
1145
|
+
get isConnected() {
|
|
1146
|
+
return this._isConnected;
|
|
1147
|
+
}
|
|
1148
|
+
get protocolVersion() {
|
|
1149
|
+
return this._protocolVersion;
|
|
1150
|
+
}
|
|
975
1151
|
/**
|
|
976
1152
|
* Read-only list of all known controllers (players) in the room.
|
|
977
1153
|
* Returns full ControllerInfo including playerIndex, nickname, connected status, and appearance.
|
|
@@ -982,12 +1158,18 @@
|
|
|
982
1158
|
get controllers() {
|
|
983
1159
|
return [...this._controllers];
|
|
984
1160
|
}
|
|
1161
|
+
get me() {
|
|
1162
|
+
return this._controllers.find((c) => c.playerIndex === this._myPlayerIndex);
|
|
1163
|
+
}
|
|
985
1164
|
/**
|
|
986
1165
|
* Returns the number of currently connected players.
|
|
987
1166
|
*/
|
|
988
1167
|
getControllerCount() {
|
|
989
1168
|
return this._controllers.filter((c) => c.connected).length;
|
|
990
1169
|
}
|
|
1170
|
+
getController(playerIndex) {
|
|
1171
|
+
return this._controllers.find((c) => c.playerIndex === playerIndex);
|
|
1172
|
+
}
|
|
991
1173
|
// ---------------------------------------------------------------------------
|
|
992
1174
|
// Initialization
|
|
993
1175
|
// ---------------------------------------------------------------------------
|
|
@@ -1018,7 +1200,7 @@
|
|
|
1018
1200
|
};
|
|
1019
1201
|
window.addEventListener("message", this.boundMessageHandler);
|
|
1020
1202
|
this.logger.lifecycle("Sending _bridge:ready to parent");
|
|
1021
|
-
window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
|
|
1203
|
+
window.parent.postMessage({ type: "_bridge:ready", protocolVersion: PROTOCOL_VERSION }, parentOrigin);
|
|
1022
1204
|
}
|
|
1023
1205
|
handleInit(msg, parentOrigin) {
|
|
1024
1206
|
const initPayload = msg.payload;
|
|
@@ -1057,28 +1239,48 @@
|
|
|
1057
1239
|
this._readyReject(error);
|
|
1058
1240
|
return;
|
|
1059
1241
|
}
|
|
1060
|
-
this.transport = new PostMessageTransport(parentOrigin);
|
|
1242
|
+
this.transport = this.config.transport ?? new PostMessageTransport(parentOrigin);
|
|
1061
1243
|
this._roomCode = initData.roomCode;
|
|
1062
|
-
|
|
1244
|
+
const serverProtocolVersion = initData.protocolVersion;
|
|
1245
|
+
if (serverProtocolVersion !== void 0) {
|
|
1246
|
+
this._protocolVersion = serverProtocolVersion;
|
|
1247
|
+
if (serverProtocolVersion !== PROTOCOL_VERSION) {
|
|
1248
|
+
this.logger.warn(
|
|
1249
|
+
`Protocol version mismatch: SDK v${PROTOCOL_VERSION}, server v${serverProtocolVersion}. Some features may not work correctly.`
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
this._myPlayerIndex = initData.myIndex;
|
|
1063
1254
|
const initPlayers = initData.players;
|
|
1064
|
-
this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p) => (
|
|
1065
|
-
playerIndex: p.playerIndex,
|
|
1066
|
-
nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
|
|
1067
|
-
connected: p.connected !== false,
|
|
1068
|
-
appearance: p.appearance ?? p.character
|
|
1069
|
-
}));
|
|
1255
|
+
this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
|
|
1070
1256
|
this.setupEventHandlers();
|
|
1071
1257
|
for (const { event, handler } of this._pendingHandlers) {
|
|
1072
1258
|
this.setupUserEventHandler(event, handler);
|
|
1073
1259
|
}
|
|
1074
1260
|
this._pendingHandlers = [];
|
|
1261
|
+
this._isConnected = true;
|
|
1075
1262
|
this._isReady = true;
|
|
1263
|
+
if (initData.gameInProgress && this.transport) {
|
|
1264
|
+
this.logger.lifecycle("Game in progress detected, requesting state recovery");
|
|
1265
|
+
this.transport.emit(SMORE_EVENTS.STATE_GET_ALL, {});
|
|
1266
|
+
}
|
|
1267
|
+
for (const buffered of this._outboundBuffer) {
|
|
1268
|
+
try {
|
|
1269
|
+
switch (buffered.method) {
|
|
1270
|
+
case "send":
|
|
1271
|
+
this.send(buffered.args[0], buffered.args[1]);
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
} catch (err) {
|
|
1275
|
+
this.handleError(err instanceof SmoreSDKError ? err : new SmoreSDKError("UNKNOWN", "Failed to flush buffered message"));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
this._outboundBuffer = [];
|
|
1076
1279
|
this.logger.lifecycle("Controller ready", {
|
|
1077
1280
|
roomCode: this._roomCode,
|
|
1078
|
-
myIndex: this.
|
|
1281
|
+
myIndex: this._myPlayerIndex
|
|
1079
1282
|
});
|
|
1080
|
-
|
|
1081
|
-
if (autoReady) {
|
|
1283
|
+
if (this.config.autoReady !== false) {
|
|
1082
1284
|
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
1083
1285
|
this.signalReady();
|
|
1084
1286
|
}
|
|
@@ -1093,21 +1295,16 @@
|
|
|
1093
1295
|
this.logger.debug("Received _bridge:update", updateData);
|
|
1094
1296
|
if (updateData.players && Array.isArray(updateData.players)) {
|
|
1095
1297
|
const players = updateData.players;
|
|
1096
|
-
const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p) => (
|
|
1097
|
-
playerIndex: p.playerIndex,
|
|
1098
|
-
nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
|
|
1099
|
-
connected: p.connected !== false,
|
|
1100
|
-
appearance: p.appearance ?? p.character
|
|
1101
|
-
}));
|
|
1298
|
+
const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
|
|
1102
1299
|
const oldControllers = this._controllers;
|
|
1103
1300
|
for (const nc of newControllers) {
|
|
1104
1301
|
if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
|
|
1105
|
-
this.
|
|
1302
|
+
this._emitLifecycle("$controller-join", nc.playerIndex, nc);
|
|
1106
1303
|
}
|
|
1107
1304
|
}
|
|
1108
1305
|
for (const oc of oldControllers) {
|
|
1109
1306
|
if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
|
|
1110
|
-
this.
|
|
1307
|
+
this._emitLifecycle("$controller-leave", oc.playerIndex);
|
|
1111
1308
|
}
|
|
1112
1309
|
}
|
|
1113
1310
|
for (const nc of newControllers) {
|
|
@@ -1115,11 +1312,11 @@
|
|
|
1115
1312
|
if (oc) {
|
|
1116
1313
|
if (oc.connected && !nc.connected) {
|
|
1117
1314
|
this.logger.debug("Player disconnected (via update)", { playerIndex: nc.playerIndex });
|
|
1118
|
-
this.
|
|
1315
|
+
this._emitLifecycle("$controller-disconnect", nc.playerIndex);
|
|
1119
1316
|
}
|
|
1120
1317
|
if (!oc.connected && nc.connected) {
|
|
1121
1318
|
this.logger.debug("Player reconnected (via update)", { playerIndex: nc.playerIndex });
|
|
1122
|
-
this.
|
|
1319
|
+
this._emitLifecycle("$controller-reconnect", nc.playerIndex, nc);
|
|
1123
1320
|
}
|
|
1124
1321
|
}
|
|
1125
1322
|
}
|
|
@@ -1134,19 +1331,10 @@
|
|
|
1134
1331
|
const playerIndex = playerInfo?.playerIndex ?? data.playerIndex;
|
|
1135
1332
|
if (playerIndex !== void 0) {
|
|
1136
1333
|
if (this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
1137
|
-
const controllerInfo = playerInfo ? {
|
|
1138
|
-
playerIndex,
|
|
1139
|
-
nickname: playerInfo.nickname || playerInfo.name || `Player ${playerIndex + 1}`,
|
|
1140
|
-
connected: playerInfo.connected !== false,
|
|
1141
|
-
appearance: playerInfo.appearance ?? playerInfo.character
|
|
1142
|
-
} : {
|
|
1143
|
-
playerIndex,
|
|
1144
|
-
nickname: `Player ${playerIndex + 1}`,
|
|
1145
|
-
connected: true
|
|
1146
|
-
};
|
|
1334
|
+
const controllerInfo = playerInfo ? mapPlayerDTO(playerInfo, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
|
|
1147
1335
|
this._controllers = [...this._controllers, controllerInfo];
|
|
1148
1336
|
this.logger.debug("Player joined", { playerIndex });
|
|
1149
|
-
this.
|
|
1337
|
+
this._emitLifecycle("$controller-join", playerIndex, controllerInfo);
|
|
1150
1338
|
}
|
|
1151
1339
|
});
|
|
1152
1340
|
this.registerHandler(SMORE_EVENTS.PLAYER_LEFT, (raw) => {
|
|
@@ -1156,7 +1344,7 @@
|
|
|
1156
1344
|
if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
1157
1345
|
this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
1158
1346
|
this.logger.debug("Player left", { playerIndex });
|
|
1159
|
-
this.
|
|
1347
|
+
this._emitLifecycle("$controller-leave", playerIndex);
|
|
1160
1348
|
}
|
|
1161
1349
|
});
|
|
1162
1350
|
this.registerHandler(SMORE_EVENTS.PLAYER_DISCONNECTED, (raw) => {
|
|
@@ -1168,7 +1356,7 @@
|
|
|
1168
1356
|
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
1169
1357
|
);
|
|
1170
1358
|
this.logger.debug("Player disconnected", { playerIndex });
|
|
1171
|
-
this.
|
|
1359
|
+
this._emitLifecycle("$controller-disconnect", playerIndex);
|
|
1172
1360
|
}
|
|
1173
1361
|
});
|
|
1174
1362
|
this.registerHandler(SMORE_EVENTS.PLAYER_RECONNECTED, (raw) => {
|
|
@@ -1176,21 +1364,12 @@
|
|
|
1176
1364
|
const playerData = data.player;
|
|
1177
1365
|
const playerIndex = playerData?.playerIndex ?? data.playerIndex;
|
|
1178
1366
|
if (playerIndex !== void 0) {
|
|
1179
|
-
const controllerInfo = playerData ? {
|
|
1180
|
-
playerIndex,
|
|
1181
|
-
nickname: playerData.nickname || playerData.name || `Player ${playerIndex + 1}`,
|
|
1182
|
-
connected: true,
|
|
1183
|
-
appearance: playerData.appearance ?? playerData.character
|
|
1184
|
-
} : {
|
|
1185
|
-
playerIndex,
|
|
1186
|
-
nickname: `Player ${playerIndex + 1}`,
|
|
1187
|
-
connected: true
|
|
1188
|
-
};
|
|
1367
|
+
const controllerInfo = playerData ? mapPlayerDTO(playerData, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
|
|
1189
1368
|
this._controllers = this._controllers.map(
|
|
1190
1369
|
(c) => c.playerIndex === playerIndex ? controllerInfo : c
|
|
1191
1370
|
);
|
|
1192
1371
|
this.logger.debug("Player reconnected", { playerIndex });
|
|
1193
|
-
this.
|
|
1372
|
+
this._emitLifecycle("$controller-reconnect", playerIndex, controllerInfo);
|
|
1194
1373
|
}
|
|
1195
1374
|
});
|
|
1196
1375
|
this.registerHandler(SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (raw) => {
|
|
@@ -1203,19 +1382,75 @@
|
|
|
1203
1382
|
(c) => c.playerIndex === pi ? { ...c, appearance } : c
|
|
1204
1383
|
);
|
|
1205
1384
|
this.logger.debug("Player character updated", { playerIndex: pi });
|
|
1206
|
-
this.
|
|
1385
|
+
this._emitLifecycle("$character-updated", pi, appearance ?? null);
|
|
1207
1386
|
}
|
|
1208
1387
|
});
|
|
1209
1388
|
this.registerHandler(SMORE_EVENTS.RATE_LIMITED, (raw) => {
|
|
1210
1389
|
const data = raw;
|
|
1211
|
-
const
|
|
1212
|
-
this.
|
|
1213
|
-
|
|
1390
|
+
const eventName = data?.event ?? "unknown";
|
|
1391
|
+
this.handleError(
|
|
1392
|
+
new SmoreSDKError("RATE_LIMITED", `Server rate-limited event: ${eventName}`, {
|
|
1393
|
+
details: { event: eventName }
|
|
1394
|
+
})
|
|
1395
|
+
);
|
|
1396
|
+
});
|
|
1397
|
+
this.registerHandler(SMORE_EVENTS.GAME_OVER, (raw) => {
|
|
1398
|
+
const data = raw;
|
|
1399
|
+
this.logger.lifecycle("Game over", data?.results);
|
|
1400
|
+
this._emitLifecycle("$game-over", data?.results);
|
|
1214
1401
|
});
|
|
1215
1402
|
this.registerHandler(SMORE_EVENTS.ALL_READY, () => {
|
|
1216
1403
|
this.logger.lifecycle("All participants ready");
|
|
1217
1404
|
this._allReadyFired = true;
|
|
1218
|
-
this.
|
|
1405
|
+
this._emitLifecycle("$all-ready");
|
|
1406
|
+
});
|
|
1407
|
+
this.registerHandler(SMORE_EVENTS.SELF_DISCONNECTED, () => {
|
|
1408
|
+
this._isConnected = false;
|
|
1409
|
+
this.logger.lifecycle("Connection lost");
|
|
1410
|
+
this._emitLifecycle("$connection-change", false);
|
|
1411
|
+
});
|
|
1412
|
+
this.registerHandler(SMORE_EVENTS.SELF_RECONNECTED, () => {
|
|
1413
|
+
this._isConnected = true;
|
|
1414
|
+
this.logger.lifecycle("Connection restored");
|
|
1415
|
+
this._emitLifecycle("$connection-change", true);
|
|
1416
|
+
});
|
|
1417
|
+
this.registerHandler(SMORE_EVENTS.STATE_CHANGED, (raw) => {
|
|
1418
|
+
const data = raw;
|
|
1419
|
+
if (typeof data?.playerIndex === "number" && data.state) {
|
|
1420
|
+
this._customStates.set(data.playerIndex, data.state);
|
|
1421
|
+
this._stateChangeListeners.forEach((cb) => {
|
|
1422
|
+
try {
|
|
1423
|
+
cb(data.playerIndex, data.state);
|
|
1424
|
+
} catch (err) {
|
|
1425
|
+
this.handleError(
|
|
1426
|
+
new SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
|
|
1427
|
+
cause: err instanceof Error ? err : void 0
|
|
1428
|
+
})
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
this.registerHandler(SMORE_EVENTS.STATE_ALL, (raw) => {
|
|
1435
|
+
const data = raw;
|
|
1436
|
+
if (data?.states) {
|
|
1437
|
+
for (const [key, value] of Object.entries(data.states)) {
|
|
1438
|
+
const pi = Number(key);
|
|
1439
|
+
this._customStates.set(pi, value);
|
|
1440
|
+
this._stateChangeListeners.forEach((cb) => {
|
|
1441
|
+
try {
|
|
1442
|
+
cb(pi, value);
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
this.handleError(
|
|
1445
|
+
new SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
|
|
1446
|
+
cause: err instanceof Error ? err : void 0
|
|
1447
|
+
})
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
this._emitLifecycle("$state-recovery", data.states);
|
|
1453
|
+
}
|
|
1219
1454
|
});
|
|
1220
1455
|
}
|
|
1221
1456
|
/**
|
|
@@ -1254,57 +1489,89 @@
|
|
|
1254
1489
|
this.registeredHandlers.push({ event, handler });
|
|
1255
1490
|
}
|
|
1256
1491
|
// ---------------------------------------------------------------------------
|
|
1492
|
+
// Lifecycle Listener Helpers
|
|
1493
|
+
// ---------------------------------------------------------------------------
|
|
1494
|
+
_addLifecycleListener(event, listener) {
|
|
1495
|
+
let set = this._lifecycleListeners.get(event);
|
|
1496
|
+
if (!set) {
|
|
1497
|
+
set = /* @__PURE__ */ new Set();
|
|
1498
|
+
this._lifecycleListeners.set(event, set);
|
|
1499
|
+
}
|
|
1500
|
+
set.add(listener);
|
|
1501
|
+
return () => {
|
|
1502
|
+
set.delete(listener);
|
|
1503
|
+
if (set.size === 0) this._lifecycleListeners.delete(event);
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
_emitLifecycle(event, ...args) {
|
|
1507
|
+
this._lifecycleListeners.get(event)?.forEach((cb) => {
|
|
1508
|
+
try {
|
|
1509
|
+
cb(...args);
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
this.handleError(
|
|
1512
|
+
new SmoreSDKError("UNKNOWN", `Error in lifecycle handler for "${event}"`, {
|
|
1513
|
+
cause: err instanceof Error ? err : void 0,
|
|
1514
|
+
details: { event }
|
|
1515
|
+
})
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
_hasLifecycleListeners(event) {
|
|
1521
|
+
const set = this._lifecycleListeners.get(event);
|
|
1522
|
+
return set !== void 0 && set.size > 0;
|
|
1523
|
+
}
|
|
1524
|
+
// ---------------------------------------------------------------------------
|
|
1257
1525
|
// Lifecycle Methods
|
|
1258
1526
|
// ---------------------------------------------------------------------------
|
|
1259
1527
|
onAllReady(callback) {
|
|
1260
1528
|
if (this._allReadyFired) {
|
|
1261
1529
|
callback();
|
|
1262
1530
|
}
|
|
1263
|
-
this.
|
|
1264
|
-
return () => {
|
|
1265
|
-
this._onAllReadyCallbacks.delete(callback);
|
|
1266
|
-
};
|
|
1531
|
+
return this._addLifecycleListener("$all-ready", callback);
|
|
1267
1532
|
}
|
|
1268
1533
|
onControllerJoin(callback) {
|
|
1269
|
-
this.
|
|
1270
|
-
return () => {
|
|
1271
|
-
this._onControllerJoinCallbacks.delete(callback);
|
|
1272
|
-
};
|
|
1534
|
+
return this._addLifecycleListener("$controller-join", callback);
|
|
1273
1535
|
}
|
|
1274
1536
|
onControllerLeave(callback) {
|
|
1275
|
-
this.
|
|
1276
|
-
return () => {
|
|
1277
|
-
this._onControllerLeaveCallbacks.delete(callback);
|
|
1278
|
-
};
|
|
1537
|
+
return this._addLifecycleListener("$controller-leave", callback);
|
|
1279
1538
|
}
|
|
1280
1539
|
onControllerDisconnect(callback) {
|
|
1281
|
-
this.
|
|
1282
|
-
return () => {
|
|
1283
|
-
this._onControllerDisconnectCallbacks.delete(callback);
|
|
1284
|
-
};
|
|
1540
|
+
return this._addLifecycleListener("$controller-disconnect", callback);
|
|
1285
1541
|
}
|
|
1286
1542
|
onControllerReconnect(callback) {
|
|
1287
|
-
this.
|
|
1288
|
-
return () => {
|
|
1289
|
-
this._onControllerReconnectCallbacks.delete(callback);
|
|
1290
|
-
};
|
|
1543
|
+
return this._addLifecycleListener("$controller-reconnect", callback);
|
|
1291
1544
|
}
|
|
1292
1545
|
onCharacterUpdated(callback) {
|
|
1293
|
-
this.
|
|
1294
|
-
return () => {
|
|
1295
|
-
this._onCharacterUpdatedCallbacks.delete(callback);
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
onRateLimited(callback) {
|
|
1299
|
-
this._onRateLimitedCallbacks.add(callback);
|
|
1300
|
-
return () => {
|
|
1301
|
-
this._onRateLimitedCallbacks.delete(callback);
|
|
1302
|
-
};
|
|
1546
|
+
return this._addLifecycleListener("$character-updated", callback);
|
|
1303
1547
|
}
|
|
1304
1548
|
onError(callback) {
|
|
1305
|
-
this.
|
|
1549
|
+
return this._addLifecycleListener("$error", callback);
|
|
1550
|
+
}
|
|
1551
|
+
onConnectionChange(callback) {
|
|
1552
|
+
return this._addLifecycleListener("$connection-change", callback);
|
|
1553
|
+
}
|
|
1554
|
+
onGameOver(callback) {
|
|
1555
|
+
return this._addLifecycleListener("$game-over", callback);
|
|
1556
|
+
}
|
|
1557
|
+
// ---------------------------------------------------------------------------
|
|
1558
|
+
// Custom State Methods
|
|
1559
|
+
// ---------------------------------------------------------------------------
|
|
1560
|
+
setState(state) {
|
|
1561
|
+
const current = this._customStates.get(this._myPlayerIndex) ?? {};
|
|
1562
|
+
const merged = { ...current, ...state };
|
|
1563
|
+
this._customStates.set(this._myPlayerIndex, merged);
|
|
1564
|
+
if (this.transport) {
|
|
1565
|
+
this.transport.emit(SMORE_EVENTS.STATE_SET, { state });
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
getMyState() {
|
|
1569
|
+
return this._customStates.get(this._myPlayerIndex);
|
|
1570
|
+
}
|
|
1571
|
+
onCustomStateChange(listener) {
|
|
1572
|
+
this._stateChangeListeners.add(listener);
|
|
1306
1573
|
return () => {
|
|
1307
|
-
this.
|
|
1574
|
+
this._stateChangeListeners.delete(listener);
|
|
1308
1575
|
};
|
|
1309
1576
|
}
|
|
1310
1577
|
// ---------------------------------------------------------------------------
|
|
@@ -1321,8 +1588,16 @@
|
|
|
1321
1588
|
* Use the onError callback or smore:rate-limited event to detect rate limiting.
|
|
1322
1589
|
*/
|
|
1323
1590
|
send(event, data) {
|
|
1324
|
-
this.
|
|
1591
|
+
if (this._isDestroyed) {
|
|
1592
|
+
throw new SmoreSDKError("DESTROYED", "Cannot call send() after destroy()");
|
|
1593
|
+
}
|
|
1594
|
+
if (!this._isReady || !this.transport) {
|
|
1595
|
+
this._outboundBuffer.push({ method: "send", args: [event, data] });
|
|
1596
|
+
this.logger.debug(`Buffered send "${event}" (controller not ready yet)`);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1325
1599
|
validateEventName(event);
|
|
1600
|
+
validatePayloadSize(data);
|
|
1326
1601
|
if (typeof data !== "object" || data === null) {
|
|
1327
1602
|
this.logger.warn(
|
|
1328
1603
|
'Event data should be an object. Primitive values will be wrapped as { data: value } by the relay server. To avoid confusion, wrap explicitly: send("event", { value: 42 }) instead of send("event", 42).'
|
|
@@ -1331,12 +1606,6 @@
|
|
|
1331
1606
|
this.logSend(event, data);
|
|
1332
1607
|
this.transport.emit(event, data);
|
|
1333
1608
|
}
|
|
1334
|
-
sendRaw(event, data) {
|
|
1335
|
-
this.ensureReady("sendRaw");
|
|
1336
|
-
validateEventName(event);
|
|
1337
|
-
this.logSend(event, data);
|
|
1338
|
-
this.transport.emit(event, data);
|
|
1339
|
-
}
|
|
1340
1609
|
signalReady() {
|
|
1341
1610
|
this.ensureReady("signalReady");
|
|
1342
1611
|
this.logSend(SMORE_EVENTS.GAME_READY, {});
|
|
@@ -1358,6 +1627,16 @@
|
|
|
1358
1627
|
* handler receives `(data)` -- targeted to this specific controller.
|
|
1359
1628
|
*/
|
|
1360
1629
|
on(event, handler) {
|
|
1630
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
1631
|
+
const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
|
|
1632
|
+
if (!validEvents.has(event)) {
|
|
1633
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}". Valid lifecycle events: ${Array.from(validEvents).join(", ")}`);
|
|
1634
|
+
}
|
|
1635
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
1636
|
+
handler();
|
|
1637
|
+
}
|
|
1638
|
+
return this._addLifecycleListener(event, handler);
|
|
1639
|
+
}
|
|
1361
1640
|
validateEventName(event);
|
|
1362
1641
|
let listeners = this.eventListeners.get(event);
|
|
1363
1642
|
if (!listeners) {
|
|
@@ -1410,6 +1689,23 @@
|
|
|
1410
1689
|
* `off(event, originalHandler)`. Use the returned unsubscribe function instead.
|
|
1411
1690
|
*/
|
|
1412
1691
|
once(event, handler) {
|
|
1692
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
1693
|
+
const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
|
|
1694
|
+
if (!validEvents.has(event)) {
|
|
1695
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}"`);
|
|
1696
|
+
}
|
|
1697
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
1698
|
+
handler();
|
|
1699
|
+
return () => {
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
const wrapper = (...args) => {
|
|
1703
|
+
unsub();
|
|
1704
|
+
handler(...args);
|
|
1705
|
+
};
|
|
1706
|
+
const unsub = this._addLifecycleListener(event, wrapper);
|
|
1707
|
+
return unsub;
|
|
1708
|
+
}
|
|
1413
1709
|
const unsubscribe = this.on(event, ((data) => {
|
|
1414
1710
|
unsubscribe();
|
|
1415
1711
|
handler(data);
|
|
@@ -1417,6 +1713,14 @@
|
|
|
1417
1713
|
return unsubscribe;
|
|
1418
1714
|
}
|
|
1419
1715
|
off(event, handler) {
|
|
1716
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
1717
|
+
if (!handler) {
|
|
1718
|
+
this._lifecycleListeners.delete(event);
|
|
1719
|
+
} else {
|
|
1720
|
+
this._lifecycleListeners.get(event)?.delete(handler);
|
|
1721
|
+
}
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1420
1724
|
if (!handler) {
|
|
1421
1725
|
this.eventListeners.delete(event);
|
|
1422
1726
|
this.transport?.off(event);
|
|
@@ -1444,6 +1748,21 @@
|
|
|
1444
1748
|
);
|
|
1445
1749
|
}
|
|
1446
1750
|
}
|
|
1751
|
+
removeAllListeners(event) {
|
|
1752
|
+
if (event) {
|
|
1753
|
+
this.eventListeners.delete(event);
|
|
1754
|
+
this.transport?.off(event);
|
|
1755
|
+
this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
|
|
1756
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
1757
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
1758
|
+
}
|
|
1759
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
1760
|
+
} else {
|
|
1761
|
+
for (const evt of [...this.eventListeners.keys()]) {
|
|
1762
|
+
this.removeAllListeners(evt);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1447
1766
|
// ---------------------------------------------------------------------------
|
|
1448
1767
|
// Cleanup
|
|
1449
1768
|
// ---------------------------------------------------------------------------
|
|
@@ -1467,14 +1786,11 @@
|
|
|
1467
1786
|
this.eventListeners.clear();
|
|
1468
1787
|
this.handlerToTransport.clear();
|
|
1469
1788
|
this._pendingHandlers = [];
|
|
1470
|
-
this.
|
|
1471
|
-
this.
|
|
1472
|
-
this.
|
|
1473
|
-
this.
|
|
1474
|
-
this.
|
|
1475
|
-
this._onCharacterUpdatedCallbacks.clear();
|
|
1476
|
-
this._onRateLimitedCallbacks.clear();
|
|
1477
|
-
this._onErrorCallbacks.clear();
|
|
1789
|
+
this._lifecycleListeners.clear();
|
|
1790
|
+
this._customStates.clear();
|
|
1791
|
+
this._stateChangeListeners.clear();
|
|
1792
|
+
this._isConnected = false;
|
|
1793
|
+
this._outboundBuffer = [];
|
|
1478
1794
|
if (this.transport) {
|
|
1479
1795
|
this.transport.destroy();
|
|
1480
1796
|
this.transport = null;
|
|
@@ -1506,8 +1822,8 @@
|
|
|
1506
1822
|
handleError(error) {
|
|
1507
1823
|
this.logger.warn(`Error in handler: ${error.message}`);
|
|
1508
1824
|
const smoreError = error.toSmoreError();
|
|
1509
|
-
if (this.
|
|
1510
|
-
this.
|
|
1825
|
+
if (this._hasLifecycleListeners("$error")) {
|
|
1826
|
+
this._emitLifecycle("$error", smoreError);
|
|
1511
1827
|
} else {
|
|
1512
1828
|
this.logger.error(error.message, error.details);
|
|
1513
1829
|
}
|
|
@@ -1523,547 +1839,22 @@
|
|
|
1523
1839
|
return new ControllerImpl(config ?? {});
|
|
1524
1840
|
}
|
|
1525
1841
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
const _onControllerJoinCallbacks = /* @__PURE__ */ new Set();
|
|
1538
|
-
const _onControllerLeaveCallbacks = /* @__PURE__ */ new Set();
|
|
1539
|
-
const _onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
|
|
1540
|
-
const _onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
|
|
1541
|
-
const _onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
|
|
1542
|
-
const _onRateLimitedCallbacks = /* @__PURE__ */ new Set();
|
|
1543
|
-
const _onErrorCallbacks = /* @__PURE__ */ new Set();
|
|
1544
|
-
let _allReadyFired = false;
|
|
1545
|
-
let _readyResolve;
|
|
1546
|
-
const _readyPromise = new Promise((resolve) => {
|
|
1547
|
-
_readyResolve = resolve;
|
|
1548
|
-
});
|
|
1549
|
-
const broadcasts = [];
|
|
1550
|
-
const sends = [];
|
|
1551
|
-
const screen = {
|
|
1552
|
-
// === Properties ===
|
|
1553
|
-
get controllers() {
|
|
1554
|
-
return [..._controllers];
|
|
1555
|
-
},
|
|
1556
|
-
get roomCode() {
|
|
1557
|
-
return roomCode;
|
|
1558
|
-
},
|
|
1559
|
-
get isReady() {
|
|
1560
|
-
return _isReady;
|
|
1561
|
-
},
|
|
1562
|
-
get isDestroyed() {
|
|
1563
|
-
return _isDestroyed;
|
|
1564
|
-
},
|
|
1565
|
-
get ready() {
|
|
1566
|
-
return _readyPromise;
|
|
1567
|
-
},
|
|
1568
|
-
// === Lifecycle Methods ===
|
|
1569
|
-
onAllReady(callback) {
|
|
1570
|
-
if (_allReadyFired) {
|
|
1571
|
-
callback();
|
|
1572
|
-
}
|
|
1573
|
-
_onAllReadyCallbacks.add(callback);
|
|
1574
|
-
return () => {
|
|
1575
|
-
_onAllReadyCallbacks.delete(callback);
|
|
1576
|
-
};
|
|
1577
|
-
},
|
|
1578
|
-
onControllerJoin(callback) {
|
|
1579
|
-
_onControllerJoinCallbacks.add(callback);
|
|
1580
|
-
return () => {
|
|
1581
|
-
_onControllerJoinCallbacks.delete(callback);
|
|
1582
|
-
};
|
|
1583
|
-
},
|
|
1584
|
-
onControllerLeave(callback) {
|
|
1585
|
-
_onControllerLeaveCallbacks.add(callback);
|
|
1586
|
-
return () => {
|
|
1587
|
-
_onControllerLeaveCallbacks.delete(callback);
|
|
1588
|
-
};
|
|
1589
|
-
},
|
|
1590
|
-
onControllerDisconnect(callback) {
|
|
1591
|
-
_onControllerDisconnectCallbacks.add(callback);
|
|
1592
|
-
return () => {
|
|
1593
|
-
_onControllerDisconnectCallbacks.delete(callback);
|
|
1594
|
-
};
|
|
1595
|
-
},
|
|
1596
|
-
onControllerReconnect(callback) {
|
|
1597
|
-
_onControllerReconnectCallbacks.add(callback);
|
|
1598
|
-
return () => {
|
|
1599
|
-
_onControllerReconnectCallbacks.delete(callback);
|
|
1600
|
-
};
|
|
1601
|
-
},
|
|
1602
|
-
onCharacterUpdated(callback) {
|
|
1603
|
-
_onCharacterUpdatedCallbacks.add(callback);
|
|
1604
|
-
return () => {
|
|
1605
|
-
_onCharacterUpdatedCallbacks.delete(callback);
|
|
1606
|
-
};
|
|
1607
|
-
},
|
|
1608
|
-
onRateLimited(callback) {
|
|
1609
|
-
_onRateLimitedCallbacks.add(callback);
|
|
1610
|
-
return () => {
|
|
1611
|
-
_onRateLimitedCallbacks.delete(callback);
|
|
1612
|
-
};
|
|
1613
|
-
},
|
|
1614
|
-
onError(callback) {
|
|
1615
|
-
_onErrorCallbacks.add(callback);
|
|
1616
|
-
return () => {
|
|
1617
|
-
_onErrorCallbacks.delete(callback);
|
|
1618
|
-
};
|
|
1619
|
-
},
|
|
1620
|
-
// === Communication Methods ===
|
|
1621
|
-
broadcast(event, data) {
|
|
1622
|
-
if (_isDestroyed) {
|
|
1623
|
-
throw new Error("Cannot broadcast: screen is destroyed");
|
|
1624
|
-
}
|
|
1625
|
-
if (!_isReady) {
|
|
1626
|
-
throw new Error("Cannot broadcast: screen is not ready");
|
|
1627
|
-
}
|
|
1628
|
-
validateEventName(event);
|
|
1629
|
-
broadcasts.push({ event, data });
|
|
1630
|
-
},
|
|
1631
|
-
broadcastRaw(event, data) {
|
|
1632
|
-
if (_isDestroyed) {
|
|
1633
|
-
throw new Error("Cannot broadcast: screen is destroyed");
|
|
1634
|
-
}
|
|
1635
|
-
if (!_isReady) {
|
|
1636
|
-
throw new Error("Cannot broadcast: screen is not ready");
|
|
1637
|
-
}
|
|
1638
|
-
validateEventName(event);
|
|
1639
|
-
broadcasts.push({ event, data });
|
|
1640
|
-
},
|
|
1641
|
-
sendToController(playerIndex, event, data) {
|
|
1642
|
-
if (_isDestroyed) {
|
|
1643
|
-
throw new Error("Cannot send: screen is destroyed");
|
|
1644
|
-
}
|
|
1645
|
-
if (!_isReady) {
|
|
1646
|
-
throw new Error("Cannot send: screen is not ready");
|
|
1647
|
-
}
|
|
1648
|
-
validateEventName(event);
|
|
1649
|
-
if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
|
|
1650
|
-
throw new Error(`Invalid player index: ${playerIndex}`);
|
|
1651
|
-
}
|
|
1652
|
-
sends.push({ playerIndex, event, data });
|
|
1653
|
-
},
|
|
1654
|
-
sendToControllerRaw(playerIndex, event, data) {
|
|
1655
|
-
if (_isDestroyed) {
|
|
1656
|
-
throw new Error("Cannot send: screen is destroyed");
|
|
1657
|
-
}
|
|
1658
|
-
if (!_isReady) {
|
|
1659
|
-
throw new Error("Cannot send: screen is not ready");
|
|
1660
|
-
}
|
|
1661
|
-
validateEventName(event);
|
|
1662
|
-
if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
|
|
1663
|
-
throw new Error(`Invalid player index: ${playerIndex}`);
|
|
1664
|
-
}
|
|
1665
|
-
sends.push({ playerIndex, event, data });
|
|
1666
|
-
},
|
|
1667
|
-
// === Game Lifecycle ===
|
|
1668
|
-
gameOver(results) {
|
|
1669
|
-
if (_isDestroyed) {
|
|
1670
|
-
throw new Error("Cannot call gameOver: screen is destroyed");
|
|
1671
|
-
}
|
|
1672
|
-
if (!_isReady) {
|
|
1673
|
-
throw new Error("Cannot call gameOver: screen is not ready");
|
|
1674
|
-
}
|
|
1675
|
-
broadcasts.push({ event: "smore:game-over", data: { results } });
|
|
1676
|
-
},
|
|
1677
|
-
signalReady() {
|
|
1678
|
-
if (_isDestroyed) {
|
|
1679
|
-
throw new Error("Cannot call signalReady: screen is destroyed");
|
|
1680
|
-
}
|
|
1681
|
-
if (!_isReady) {
|
|
1682
|
-
throw new Error("Cannot call signalReady: screen is not ready");
|
|
1683
|
-
}
|
|
1684
|
-
},
|
|
1685
|
-
// === Event Subscription ===
|
|
1686
|
-
on(event, handler) {
|
|
1687
|
-
validateEventName(event);
|
|
1688
|
-
const eventStr = event;
|
|
1689
|
-
if (!listeners.has(eventStr)) {
|
|
1690
|
-
listeners.set(eventStr, /* @__PURE__ */ new Set());
|
|
1691
|
-
}
|
|
1692
|
-
listeners.get(eventStr).add(handler);
|
|
1693
|
-
return () => {
|
|
1694
|
-
listeners.get(eventStr)?.delete(handler);
|
|
1695
|
-
};
|
|
1696
|
-
},
|
|
1697
|
-
once(event, handler) {
|
|
1698
|
-
validateEventName(event);
|
|
1699
|
-
const wrapper = (playerIndex, data) => {
|
|
1700
|
-
handler(playerIndex, data);
|
|
1701
|
-
screen.off(event, wrapper);
|
|
1702
|
-
};
|
|
1703
|
-
return screen.on(event, wrapper);
|
|
1704
|
-
},
|
|
1705
|
-
off(event, handler) {
|
|
1706
|
-
validateEventName(event);
|
|
1707
|
-
const eventStr = event;
|
|
1708
|
-
if (!handler) {
|
|
1709
|
-
listeners.delete(eventStr);
|
|
1710
|
-
} else {
|
|
1711
|
-
listeners.get(eventStr)?.delete(handler);
|
|
1712
|
-
}
|
|
1713
|
-
},
|
|
1714
|
-
// === Utilities ===
|
|
1715
|
-
getController(playerIndex) {
|
|
1716
|
-
return _controllers.find((c) => c.playerIndex === playerIndex);
|
|
1717
|
-
},
|
|
1718
|
-
getControllerCount() {
|
|
1719
|
-
return _controllers.filter((c) => c.connected).length;
|
|
1720
|
-
},
|
|
1721
|
-
hasAnyConnectedControllers() {
|
|
1722
|
-
return _controllers.some((c) => c.connected);
|
|
1723
|
-
},
|
|
1724
|
-
// === Cleanup ===
|
|
1725
|
-
/**
|
|
1726
|
-
* Note: destroy() clears recorded broadcast/event arrays. Call getBroadcasts() before destroy() if assertions are needed.
|
|
1727
|
-
*/
|
|
1728
|
-
destroy() {
|
|
1729
|
-
_isDestroyed = true;
|
|
1730
|
-
listeners.clear();
|
|
1731
|
-
broadcasts.length = 0;
|
|
1732
|
-
sends.length = 0;
|
|
1733
|
-
},
|
|
1734
|
-
// === Mock-specific methods ===
|
|
1735
|
-
simulateEvent(playerIndex, event, data) {
|
|
1736
|
-
validateEventName(event);
|
|
1737
|
-
const eventStr = event;
|
|
1738
|
-
const handlers = listeners.get(eventStr);
|
|
1739
|
-
if (handlers) {
|
|
1740
|
-
handlers.forEach((handler) => {
|
|
1741
|
-
handler(playerIndex, data);
|
|
1742
|
-
});
|
|
1743
|
-
}
|
|
1744
|
-
},
|
|
1745
|
-
simulateControllerJoin(info) {
|
|
1746
|
-
_controllers.push(info);
|
|
1747
|
-
_onControllerJoinCallbacks.forEach((cb) => cb(info.playerIndex, info));
|
|
1748
|
-
},
|
|
1749
|
-
simulateControllerLeave(playerIndex) {
|
|
1750
|
-
_controllers = _controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
1751
|
-
_onControllerLeaveCallbacks.forEach((cb) => cb(playerIndex));
|
|
1752
|
-
},
|
|
1753
|
-
/**
|
|
1754
|
-
* Simulate a controller network disconnect (player still in room but unreachable).
|
|
1755
|
-
*/
|
|
1756
|
-
simulateControllerDisconnect(playerIndex) {
|
|
1757
|
-
const controller = _controllers.find((c) => c.playerIndex === playerIndex);
|
|
1758
|
-
if (!controller) {
|
|
1759
|
-
throw new Error(`Controller ${playerIndex} not found`);
|
|
1760
|
-
}
|
|
1761
|
-
_controllers = _controllers.map(
|
|
1762
|
-
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
1763
|
-
);
|
|
1764
|
-
_onControllerDisconnectCallbacks.forEach((cb) => cb(playerIndex));
|
|
1765
|
-
},
|
|
1766
|
-
/**
|
|
1767
|
-
* Simulate a controller network reconnect after disconnect.
|
|
1768
|
-
*/
|
|
1769
|
-
simulateControllerReconnect(playerIndex) {
|
|
1770
|
-
const controller = _controllers.find((c) => c.playerIndex === playerIndex);
|
|
1771
|
-
if (!controller) {
|
|
1772
|
-
throw new Error(`Controller ${playerIndex} not found`);
|
|
1773
|
-
}
|
|
1774
|
-
const reconnectedController = { ...controller, connected: true };
|
|
1775
|
-
_controllers = _controllers.map(
|
|
1776
|
-
(c) => c.playerIndex === playerIndex ? reconnectedController : c
|
|
1777
|
-
);
|
|
1778
|
-
_onControllerReconnectCallbacks.forEach((cb) => cb(playerIndex, reconnectedController));
|
|
1779
|
-
},
|
|
1780
|
-
simulateCharacterUpdate(playerIndex, appearance) {
|
|
1781
|
-
const controller = _controllers.find((c) => c.playerIndex === playerIndex);
|
|
1782
|
-
if (!controller) {
|
|
1783
|
-
throw new Error(`Controller ${playerIndex} not found`);
|
|
1784
|
-
}
|
|
1785
|
-
_controllers = _controllers.map(
|
|
1786
|
-
(c) => c.playerIndex === playerIndex ? { ...c, appearance } : c
|
|
1787
|
-
);
|
|
1788
|
-
_onCharacterUpdatedCallbacks.forEach((cb) => cb(playerIndex, appearance));
|
|
1789
|
-
},
|
|
1790
|
-
simulateRateLimited(event) {
|
|
1791
|
-
_onRateLimitedCallbacks.forEach((cb) => cb(event));
|
|
1792
|
-
},
|
|
1793
|
-
simulateAllReady() {
|
|
1794
|
-
_allReadyFired = true;
|
|
1795
|
-
_onAllReadyCallbacks.forEach((cb) => cb());
|
|
1796
|
-
},
|
|
1797
|
-
simulateError(error) {
|
|
1798
|
-
_onErrorCallbacks.forEach((cb) => cb(error));
|
|
1799
|
-
},
|
|
1800
|
-
getBroadcasts() {
|
|
1801
|
-
return [...broadcasts];
|
|
1802
|
-
},
|
|
1803
|
-
getSentToController(playerIndex) {
|
|
1804
|
-
return sends.filter((s) => s.playerIndex === playerIndex).map((s) => ({ event: s.event, data: s.data }));
|
|
1805
|
-
},
|
|
1806
|
-
clearRecordedEvents() {
|
|
1807
|
-
broadcasts.length = 0;
|
|
1808
|
-
sends.length = 0;
|
|
1809
|
-
},
|
|
1810
|
-
triggerReady() {
|
|
1811
|
-
_isReady = true;
|
|
1812
|
-
_readyResolve();
|
|
1813
|
-
}
|
|
1814
|
-
};
|
|
1815
|
-
if (autoReady) {
|
|
1816
|
-
setTimeout(() => screen.triggerReady(), 0);
|
|
1817
|
-
}
|
|
1818
|
-
return screen;
|
|
1819
|
-
}
|
|
1820
|
-
function createMockController(options = {}) {
|
|
1821
|
-
const {
|
|
1822
|
-
roomCode = "TEST",
|
|
1823
|
-
myIndex = 0,
|
|
1824
|
-
autoReady = true
|
|
1825
|
-
} = options;
|
|
1826
|
-
let _isReady = false;
|
|
1827
|
-
let _isDestroyed = false;
|
|
1828
|
-
let _controllers = options.controllers ?? [];
|
|
1829
|
-
const listeners = /* @__PURE__ */ new Map();
|
|
1830
|
-
const _onAllReadyCallbacks = /* @__PURE__ */ new Set();
|
|
1831
|
-
const _onControllerJoinCallbacks = /* @__PURE__ */ new Set();
|
|
1832
|
-
const _onControllerLeaveCallbacks = /* @__PURE__ */ new Set();
|
|
1833
|
-
const _onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
|
|
1834
|
-
const _onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
|
|
1835
|
-
const _onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
|
|
1836
|
-
const _onRateLimitedCallbacks = /* @__PURE__ */ new Set();
|
|
1837
|
-
const _onErrorCallbacks = /* @__PURE__ */ new Set();
|
|
1838
|
-
let _allReadyFired = false;
|
|
1839
|
-
let _readyResolve;
|
|
1840
|
-
const _readyPromise = new Promise((resolve) => {
|
|
1841
|
-
_readyResolve = resolve;
|
|
1842
|
-
});
|
|
1843
|
-
const sentEvents = [];
|
|
1844
|
-
const controller = {
|
|
1845
|
-
// === Properties ===
|
|
1846
|
-
get myIndex() {
|
|
1847
|
-
return myIndex;
|
|
1848
|
-
},
|
|
1849
|
-
get roomCode() {
|
|
1850
|
-
return roomCode;
|
|
1851
|
-
},
|
|
1852
|
-
get isReady() {
|
|
1853
|
-
return _isReady;
|
|
1854
|
-
},
|
|
1855
|
-
get isDestroyed() {
|
|
1856
|
-
return _isDestroyed;
|
|
1857
|
-
},
|
|
1858
|
-
get controllers() {
|
|
1859
|
-
return [..._controllers];
|
|
1860
|
-
},
|
|
1861
|
-
get ready() {
|
|
1862
|
-
return _readyPromise;
|
|
1863
|
-
},
|
|
1864
|
-
// === Lifecycle Methods ===
|
|
1865
|
-
onAllReady(callback) {
|
|
1866
|
-
if (_allReadyFired) {
|
|
1867
|
-
callback();
|
|
1868
|
-
}
|
|
1869
|
-
_onAllReadyCallbacks.add(callback);
|
|
1870
|
-
return () => {
|
|
1871
|
-
_onAllReadyCallbacks.delete(callback);
|
|
1872
|
-
};
|
|
1873
|
-
},
|
|
1874
|
-
onControllerJoin(callback) {
|
|
1875
|
-
_onControllerJoinCallbacks.add(callback);
|
|
1876
|
-
return () => {
|
|
1877
|
-
_onControllerJoinCallbacks.delete(callback);
|
|
1878
|
-
};
|
|
1879
|
-
},
|
|
1880
|
-
onControllerLeave(callback) {
|
|
1881
|
-
_onControllerLeaveCallbacks.add(callback);
|
|
1882
|
-
return () => {
|
|
1883
|
-
_onControllerLeaveCallbacks.delete(callback);
|
|
1884
|
-
};
|
|
1885
|
-
},
|
|
1886
|
-
onControllerDisconnect(callback) {
|
|
1887
|
-
_onControllerDisconnectCallbacks.add(callback);
|
|
1888
|
-
return () => {
|
|
1889
|
-
_onControllerDisconnectCallbacks.delete(callback);
|
|
1890
|
-
};
|
|
1891
|
-
},
|
|
1892
|
-
onControllerReconnect(callback) {
|
|
1893
|
-
_onControllerReconnectCallbacks.add(callback);
|
|
1894
|
-
return () => {
|
|
1895
|
-
_onControllerReconnectCallbacks.delete(callback);
|
|
1896
|
-
};
|
|
1897
|
-
},
|
|
1898
|
-
onCharacterUpdated(callback) {
|
|
1899
|
-
_onCharacterUpdatedCallbacks.add(callback);
|
|
1900
|
-
return () => {
|
|
1901
|
-
_onCharacterUpdatedCallbacks.delete(callback);
|
|
1902
|
-
};
|
|
1903
|
-
},
|
|
1904
|
-
onRateLimited(callback) {
|
|
1905
|
-
_onRateLimitedCallbacks.add(callback);
|
|
1906
|
-
return () => {
|
|
1907
|
-
_onRateLimitedCallbacks.delete(callback);
|
|
1908
|
-
};
|
|
1909
|
-
},
|
|
1910
|
-
onError(callback) {
|
|
1911
|
-
_onErrorCallbacks.add(callback);
|
|
1912
|
-
return () => {
|
|
1913
|
-
_onErrorCallbacks.delete(callback);
|
|
1914
|
-
};
|
|
1915
|
-
},
|
|
1916
|
-
getControllerCount() {
|
|
1917
|
-
return _controllers.filter((c) => c.connected).length;
|
|
1918
|
-
},
|
|
1919
|
-
// === Communication Methods ===
|
|
1920
|
-
send(event, data) {
|
|
1921
|
-
if (_isDestroyed) {
|
|
1922
|
-
throw new Error("Cannot send: controller is destroyed");
|
|
1923
|
-
}
|
|
1924
|
-
validateEventName(event);
|
|
1925
|
-
sentEvents.push({ event, data });
|
|
1926
|
-
},
|
|
1927
|
-
sendRaw(event, data) {
|
|
1928
|
-
if (_isDestroyed) {
|
|
1929
|
-
throw new Error("Cannot send: controller is destroyed");
|
|
1930
|
-
}
|
|
1931
|
-
validateEventName(event);
|
|
1932
|
-
sentEvents.push({ event, data });
|
|
1933
|
-
},
|
|
1934
|
-
signalReady() {
|
|
1935
|
-
if (_isDestroyed) {
|
|
1936
|
-
throw new Error("Cannot call signalReady: controller is destroyed");
|
|
1937
|
-
}
|
|
1938
|
-
if (!_isReady) {
|
|
1939
|
-
throw new Error("Cannot call signalReady: controller is not ready");
|
|
1940
|
-
}
|
|
1941
|
-
},
|
|
1942
|
-
// === Event Subscription ===
|
|
1943
|
-
on(event, handler) {
|
|
1944
|
-
validateEventName(event);
|
|
1945
|
-
const eventStr = event;
|
|
1946
|
-
if (!listeners.has(eventStr)) {
|
|
1947
|
-
listeners.set(eventStr, /* @__PURE__ */ new Set());
|
|
1948
|
-
}
|
|
1949
|
-
listeners.get(eventStr).add(handler);
|
|
1950
|
-
return () => {
|
|
1951
|
-
listeners.get(eventStr)?.delete(handler);
|
|
1952
|
-
};
|
|
1953
|
-
},
|
|
1954
|
-
once(event, handler) {
|
|
1955
|
-
validateEventName(event);
|
|
1956
|
-
const wrapper = (data) => {
|
|
1957
|
-
handler(data);
|
|
1958
|
-
controller.off(event, wrapper);
|
|
1959
|
-
};
|
|
1960
|
-
return controller.on(event, wrapper);
|
|
1961
|
-
},
|
|
1962
|
-
off(event, handler) {
|
|
1963
|
-
validateEventName(event);
|
|
1964
|
-
const eventStr = event;
|
|
1965
|
-
if (!handler) {
|
|
1966
|
-
listeners.delete(eventStr);
|
|
1967
|
-
} else {
|
|
1968
|
-
listeners.get(eventStr)?.delete(handler);
|
|
1969
|
-
}
|
|
1970
|
-
},
|
|
1971
|
-
// === Cleanup ===
|
|
1972
|
-
destroy() {
|
|
1973
|
-
_isDestroyed = true;
|
|
1974
|
-
listeners.clear();
|
|
1975
|
-
sentEvents.length = 0;
|
|
1976
|
-
},
|
|
1977
|
-
// === Mock-specific methods ===
|
|
1978
|
-
simulateEvent(event, data) {
|
|
1979
|
-
validateEventName(event);
|
|
1980
|
-
const eventStr = event;
|
|
1981
|
-
const handlers = listeners.get(eventStr);
|
|
1982
|
-
if (handlers) {
|
|
1983
|
-
handlers.forEach((handler) => {
|
|
1984
|
-
handler(data);
|
|
1985
|
-
});
|
|
1986
|
-
}
|
|
1987
|
-
},
|
|
1988
|
-
getSentEvents() {
|
|
1989
|
-
return [...sentEvents];
|
|
1990
|
-
},
|
|
1991
|
-
clearRecordedEvents() {
|
|
1992
|
-
sentEvents.length = 0;
|
|
1993
|
-
},
|
|
1994
|
-
triggerReady() {
|
|
1995
|
-
_isReady = true;
|
|
1996
|
-
_readyResolve();
|
|
1997
|
-
},
|
|
1998
|
-
/**
|
|
1999
|
-
* Simulate a new player joining the room.
|
|
2000
|
-
* Stores full ControllerInfo (nickname, appearance, etc.) for later retrieval.
|
|
2001
|
-
*/
|
|
2002
|
-
simulatePlayerJoin(playerIndex, info) {
|
|
2003
|
-
if (!_controllers.some((c) => c.playerIndex === playerIndex)) {
|
|
2004
|
-
_controllers = [..._controllers, { ...info, connected: info.connected ?? true }];
|
|
2005
|
-
}
|
|
2006
|
-
_onControllerJoinCallbacks.forEach((cb) => cb(playerIndex, info));
|
|
2007
|
-
},
|
|
2008
|
-
/**
|
|
2009
|
-
* Simulate a player leaving the room (fully removed).
|
|
2010
|
-
*/
|
|
2011
|
-
simulatePlayerLeave(playerIndex) {
|
|
2012
|
-
_controllers = _controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
2013
|
-
_onControllerLeaveCallbacks.forEach((cb) => cb(playerIndex));
|
|
2014
|
-
},
|
|
2015
|
-
/**
|
|
2016
|
-
* Simulate a player network disconnect (player still in room but unreachable).
|
|
2017
|
-
*/
|
|
2018
|
-
simulatePlayerDisconnect(playerIndex) {
|
|
2019
|
-
_controllers = _controllers.map(
|
|
2020
|
-
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
2021
|
-
);
|
|
2022
|
-
_onControllerDisconnectCallbacks.forEach((cb) => cb(playerIndex));
|
|
2023
|
-
},
|
|
2024
|
-
/**
|
|
2025
|
-
* Simulate a player network reconnect after disconnect.
|
|
2026
|
-
*/
|
|
2027
|
-
simulatePlayerReconnect(playerIndex, info) {
|
|
2028
|
-
_controllers = _controllers.map(
|
|
2029
|
-
(c) => c.playerIndex === playerIndex ? { ...info, connected: true } : c
|
|
2030
|
-
);
|
|
2031
|
-
_onControllerReconnectCallbacks.forEach((cb) => cb(playerIndex, info));
|
|
2032
|
-
},
|
|
2033
|
-
simulateCharacterUpdate(playerIndex, appearance) {
|
|
2034
|
-
const ctrl = _controllers.find((c) => c.playerIndex === playerIndex);
|
|
2035
|
-
if (!ctrl) {
|
|
2036
|
-
throw new Error(`Controller ${playerIndex} not found`);
|
|
2037
|
-
}
|
|
2038
|
-
_controllers = _controllers.map(
|
|
2039
|
-
(c) => c.playerIndex === playerIndex ? { ...c, appearance } : c
|
|
2040
|
-
);
|
|
2041
|
-
_onCharacterUpdatedCallbacks.forEach((cb) => cb(playerIndex, appearance));
|
|
2042
|
-
},
|
|
2043
|
-
simulateRateLimited(event) {
|
|
2044
|
-
_onRateLimitedCallbacks.forEach((cb) => cb(event));
|
|
2045
|
-
},
|
|
2046
|
-
simulateAllReady() {
|
|
2047
|
-
_allReadyFired = true;
|
|
2048
|
-
_onAllReadyCallbacks.forEach((cb) => cb());
|
|
2049
|
-
},
|
|
2050
|
-
simulateError(error) {
|
|
2051
|
-
_onErrorCallbacks.forEach((cb) => cb(error));
|
|
2052
|
-
}
|
|
2053
|
-
};
|
|
2054
|
-
if (autoReady) {
|
|
2055
|
-
setTimeout(() => controller.triggerReady(), 0);
|
|
2056
|
-
}
|
|
2057
|
-
return controller;
|
|
2058
|
-
}
|
|
1842
|
+
const LifecycleEvent = {
|
|
1843
|
+
ALL_READY: "$all-ready",
|
|
1844
|
+
CONTROLLER_JOIN: "$controller-join",
|
|
1845
|
+
CONTROLLER_LEAVE: "$controller-leave",
|
|
1846
|
+
CONTROLLER_DISCONNECT: "$controller-disconnect",
|
|
1847
|
+
CONTROLLER_RECONNECT: "$controller-reconnect",
|
|
1848
|
+
CHARACTER_UPDATED: "$character-updated",
|
|
1849
|
+
ERROR: "$error",
|
|
1850
|
+
GAME_OVER: "$game-over",
|
|
1851
|
+
CONNECTION_CHANGE: "$connection-change"
|
|
1852
|
+
};
|
|
2059
1853
|
|
|
1854
|
+
exports.LifecycleEvent = LifecycleEvent;
|
|
2060
1855
|
exports.SmoreSDKError = SmoreSDKError;
|
|
2061
|
-
exports.configure = configure;
|
|
2062
1856
|
exports.createController = createController;
|
|
2063
|
-
exports.createMockController = createMockController;
|
|
2064
|
-
exports.createMockScreen = createMockScreen;
|
|
2065
1857
|
exports.createScreen = createScreen;
|
|
2066
|
-
exports.validateEventName = validateEventName;
|
|
2067
1858
|
|
|
2068
1859
|
}));
|
|
2069
1860
|
//# sourceMappingURL=smore-sdk.umd.js.map
|