@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
package/dist/cjs/screen.cjs
CHANGED
|
@@ -5,7 +5,7 @@ var protocol = require('./transport/protocol.cjs');
|
|
|
5
5
|
var errors = require('./errors.cjs');
|
|
6
6
|
var events = require('./events.cjs');
|
|
7
7
|
var logger = require('./logger.cjs');
|
|
8
|
-
var
|
|
8
|
+
var shared = require('./shared.cjs');
|
|
9
9
|
|
|
10
10
|
const DEFAULT_TIMEOUT = 1e4;
|
|
11
11
|
function validatePlayerIndex(playerIndex, controllers) {
|
|
@@ -36,17 +36,19 @@ class ScreenImpl {
|
|
|
36
36
|
handlerToTransport = /* @__PURE__ */ new Map();
|
|
37
37
|
// Pending handlers registered via on() before transport is ready
|
|
38
38
|
_pendingHandlers = [];
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
_onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
|
|
44
|
-
_onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
|
|
45
|
-
_onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
|
|
46
|
-
_onRateLimitedCallbacks = /* @__PURE__ */ new Set();
|
|
47
|
-
_onErrorCallbacks = /* @__PURE__ */ new Set();
|
|
39
|
+
// Unified lifecycle listener map (supports both onXxx() and on('$xxx') patterns)
|
|
40
|
+
_lifecycleListeners = /* @__PURE__ */ new Map();
|
|
41
|
+
// Outbound message buffer (messages sent before ready)
|
|
42
|
+
_outboundBuffer = [];
|
|
48
43
|
// Whether all-ready has fired
|
|
49
44
|
_allReadyFired = false;
|
|
45
|
+
// Self-connection awareness
|
|
46
|
+
_isConnected = false;
|
|
47
|
+
// Custom state management
|
|
48
|
+
_customStates = /* @__PURE__ */ new Map();
|
|
49
|
+
_stateChangeListeners = /* @__PURE__ */ new Set();
|
|
50
|
+
// Protocol versioning
|
|
51
|
+
_protocolVersion = protocol.PROTOCOL_VERSION;
|
|
50
52
|
// Ready promise
|
|
51
53
|
_readyResolve;
|
|
52
54
|
_readyReject;
|
|
@@ -108,8 +110,17 @@ class ScreenImpl {
|
|
|
108
110
|
this._readyReject(error);
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
|
-
this.transport = new PostMessageTransport.PostMessageTransport(parentOrigin);
|
|
113
|
+
this.transport = this.config.transport ?? new PostMessageTransport.PostMessageTransport(parentOrigin);
|
|
112
114
|
this._roomCode = initData.roomCode;
|
|
115
|
+
const serverProtocolVersion = initData.protocolVersion;
|
|
116
|
+
if (serverProtocolVersion !== void 0) {
|
|
117
|
+
this._protocolVersion = serverProtocolVersion;
|
|
118
|
+
if (serverProtocolVersion !== protocol.PROTOCOL_VERSION) {
|
|
119
|
+
this.logger.warn(
|
|
120
|
+
`Protocol version mismatch: SDK v${protocol.PROTOCOL_VERSION}, server v${serverProtocolVersion}. Some features may not work correctly.`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
113
124
|
this._controllers = this.mapControllersFromInit(initData.players);
|
|
114
125
|
if (this._controllers.length === 0) {
|
|
115
126
|
this.logger.warn("Screen initialized with zero controllers");
|
|
@@ -119,13 +130,28 @@ class ScreenImpl {
|
|
|
119
130
|
this.setupUserEventHandler(event, handler);
|
|
120
131
|
}
|
|
121
132
|
this._pendingHandlers = [];
|
|
133
|
+
this._isConnected = true;
|
|
122
134
|
this._isReady = true;
|
|
135
|
+
for (const buffered of this._outboundBuffer) {
|
|
136
|
+
try {
|
|
137
|
+
switch (buffered.method) {
|
|
138
|
+
case "broadcast":
|
|
139
|
+
this.broadcast(buffered.args[0], buffered.args[1]);
|
|
140
|
+
break;
|
|
141
|
+
case "sendToController":
|
|
142
|
+
this.sendToController(buffered.args[0], buffered.args[1], buffered.args[2]);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
this.handleError(err instanceof errors.SmoreSDKError ? err : new errors.SmoreSDKError("UNKNOWN", "Failed to flush buffered message"));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this._outboundBuffer = [];
|
|
123
150
|
this.logger.lifecycle("Screen ready", {
|
|
124
151
|
roomCode: this._roomCode,
|
|
125
152
|
controllers: this._controllers.length
|
|
126
153
|
});
|
|
127
|
-
|
|
128
|
-
if (autoReady) {
|
|
154
|
+
if (this.config.autoReady !== false) {
|
|
129
155
|
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
130
156
|
this.signalReady();
|
|
131
157
|
}
|
|
@@ -143,13 +169,13 @@ class ScreenImpl {
|
|
|
143
169
|
for (const nc of newControllers) {
|
|
144
170
|
if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
|
|
145
171
|
this.logger.lifecycle("Controller joined (via update)", { playerIndex: nc.playerIndex });
|
|
146
|
-
this.
|
|
172
|
+
this._emitLifecycle("$controller-join", nc.playerIndex, nc);
|
|
147
173
|
}
|
|
148
174
|
}
|
|
149
175
|
for (const oc of oldControllers) {
|
|
150
176
|
if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
|
|
151
177
|
this.logger.lifecycle("Controller left (via update)", { playerIndex: oc.playerIndex });
|
|
152
|
-
this.
|
|
178
|
+
this._emitLifecycle("$controller-leave", oc.playerIndex);
|
|
153
179
|
}
|
|
154
180
|
}
|
|
155
181
|
}
|
|
@@ -159,18 +185,11 @@ class ScreenImpl {
|
|
|
159
185
|
}
|
|
160
186
|
};
|
|
161
187
|
window.addEventListener("message", this.boundMessageHandler);
|
|
162
|
-
window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
|
|
188
|
+
window.parent.postMessage({ type: "_bridge:ready", protocolVersion: protocol.PROTOCOL_VERSION }, parentOrigin);
|
|
163
189
|
this.logger.lifecycle("Sent _bridge:ready to parent");
|
|
164
190
|
}
|
|
165
191
|
mapControllersFromInit(players) {
|
|
166
|
-
return players.map((p, index) => (
|
|
167
|
-
playerIndex: p.playerIndex ?? index,
|
|
168
|
-
// Fallback to `nickname` for defensive compatibility (server currently always sends `name`)
|
|
169
|
-
nickname: p.nickname || p.name || `Player ${index + 1}`,
|
|
170
|
-
connected: p.connected !== false,
|
|
171
|
-
// Fallback to `appearance` for defensive compatibility (server currently always sends `character`)
|
|
172
|
-
appearance: p.appearance ?? p.character
|
|
173
|
-
}));
|
|
192
|
+
return players.map((p, index) => shared.mapPlayerDTO(p, index));
|
|
174
193
|
}
|
|
175
194
|
setupEventHandlers() {
|
|
176
195
|
if (!this.transport) return;
|
|
@@ -178,16 +197,11 @@ class ScreenImpl {
|
|
|
178
197
|
const payload = data;
|
|
179
198
|
const playerData = payload?.player;
|
|
180
199
|
if (playerData && typeof playerData.playerIndex === "number") {
|
|
181
|
-
const controllerInfo =
|
|
182
|
-
playerIndex: playerData.playerIndex,
|
|
183
|
-
nickname: playerData.nickname || playerData.name || `Player ${playerData.playerIndex + 1}`,
|
|
184
|
-
connected: playerData.connected !== false,
|
|
185
|
-
appearance: playerData.appearance ?? playerData.character
|
|
186
|
-
};
|
|
200
|
+
const controllerInfo = shared.mapPlayerDTO(playerData, playerData.playerIndex);
|
|
187
201
|
if (this._controllers.some((c) => c.playerIndex === controllerInfo.playerIndex)) return;
|
|
188
202
|
this._controllers = [...this._controllers, controllerInfo];
|
|
189
203
|
this.logger.lifecycle("Controller joined", { playerIndex: controllerInfo.playerIndex });
|
|
190
|
-
this.
|
|
204
|
+
this._emitLifecycle("$controller-join", controllerInfo.playerIndex, controllerInfo);
|
|
191
205
|
}
|
|
192
206
|
});
|
|
193
207
|
this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_LEFT, (data) => {
|
|
@@ -197,7 +211,7 @@ class ScreenImpl {
|
|
|
197
211
|
if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
198
212
|
this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
199
213
|
this.logger.lifecycle("Controller left", { playerIndex });
|
|
200
|
-
this.
|
|
214
|
+
this._emitLifecycle("$controller-leave", playerIndex);
|
|
201
215
|
}
|
|
202
216
|
});
|
|
203
217
|
this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_DISCONNECTED, (data) => {
|
|
@@ -208,24 +222,19 @@ class ScreenImpl {
|
|
|
208
222
|
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
209
223
|
);
|
|
210
224
|
this.logger.lifecycle("Controller disconnected", { playerIndex });
|
|
211
|
-
this.
|
|
225
|
+
this._emitLifecycle("$controller-disconnect", playerIndex);
|
|
212
226
|
}
|
|
213
227
|
});
|
|
214
228
|
this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_RECONNECTED, (data) => {
|
|
215
229
|
const payload = data;
|
|
216
230
|
const playerData = payload?.player;
|
|
217
231
|
if (playerData && typeof playerData.playerIndex === "number") {
|
|
218
|
-
const controllerInfo =
|
|
219
|
-
playerIndex: playerData.playerIndex,
|
|
220
|
-
nickname: playerData.nickname || playerData.name || `Player ${playerData.playerIndex + 1}`,
|
|
221
|
-
connected: true,
|
|
222
|
-
appearance: playerData.appearance ?? playerData.character
|
|
223
|
-
};
|
|
232
|
+
const controllerInfo = shared.mapPlayerDTO(playerData, playerData.playerIndex);
|
|
224
233
|
this._controllers = this._controllers.map(
|
|
225
234
|
(c) => c.playerIndex === controllerInfo.playerIndex ? controllerInfo : c
|
|
226
235
|
);
|
|
227
236
|
this.logger.lifecycle("Controller reconnected", { playerIndex: controllerInfo.playerIndex });
|
|
228
|
-
this.
|
|
237
|
+
this._emitLifecycle("$controller-reconnect", controllerInfo.playerIndex, controllerInfo);
|
|
229
238
|
}
|
|
230
239
|
});
|
|
231
240
|
this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (data) => {
|
|
@@ -238,19 +247,69 @@ class ScreenImpl {
|
|
|
238
247
|
(c) => c.playerIndex === pi ? { ...c, appearance } : c
|
|
239
248
|
);
|
|
240
249
|
this.logger.lifecycle("Player character updated", { playerIndex: pi });
|
|
241
|
-
this.
|
|
250
|
+
this._emitLifecycle("$character-updated", pi, appearance ?? null);
|
|
242
251
|
}
|
|
243
252
|
});
|
|
244
253
|
this.registerTransportHandler(events.SMORE_EVENTS.RATE_LIMITED, (data) => {
|
|
245
254
|
const payload = data;
|
|
246
|
-
const
|
|
247
|
-
this.
|
|
248
|
-
|
|
255
|
+
const eventName = payload?.event ?? "unknown";
|
|
256
|
+
this.handleError(
|
|
257
|
+
new errors.SmoreSDKError("RATE_LIMITED", `Server rate-limited event: ${eventName}`, {
|
|
258
|
+
details: { event: eventName }
|
|
259
|
+
})
|
|
260
|
+
);
|
|
249
261
|
});
|
|
250
262
|
this.registerTransportHandler(events.SMORE_EVENTS.ALL_READY, () => {
|
|
251
263
|
this.logger.lifecycle("All participants ready");
|
|
252
264
|
this._allReadyFired = true;
|
|
253
|
-
this.
|
|
265
|
+
this._emitLifecycle("$all-ready");
|
|
266
|
+
});
|
|
267
|
+
this.registerTransportHandler(events.SMORE_EVENTS.SELF_DISCONNECTED, () => {
|
|
268
|
+
this._isConnected = false;
|
|
269
|
+
this.logger.lifecycle("Connection lost");
|
|
270
|
+
this._emitLifecycle("$connection-change", false);
|
|
271
|
+
});
|
|
272
|
+
this.registerTransportHandler(events.SMORE_EVENTS.SELF_RECONNECTED, () => {
|
|
273
|
+
this._isConnected = true;
|
|
274
|
+
this.logger.lifecycle("Connection restored");
|
|
275
|
+
this._emitLifecycle("$connection-change", true);
|
|
276
|
+
});
|
|
277
|
+
this.registerTransportHandler(events.SMORE_EVENTS.STATE_CHANGED, (raw) => {
|
|
278
|
+
const data = raw;
|
|
279
|
+
if (typeof data?.playerIndex === "number" && data.state) {
|
|
280
|
+
this._customStates.set(data.playerIndex, data.state);
|
|
281
|
+
this._stateChangeListeners.forEach((cb) => {
|
|
282
|
+
try {
|
|
283
|
+
cb(data.playerIndex, data.state);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
this.handleError(
|
|
286
|
+
new errors.SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
|
|
287
|
+
cause: err instanceof Error ? err : void 0
|
|
288
|
+
})
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
this.registerTransportHandler(events.SMORE_EVENTS.STATE_ALL, (raw) => {
|
|
295
|
+
const data = raw;
|
|
296
|
+
if (data?.states) {
|
|
297
|
+
for (const [key, value] of Object.entries(data.states)) {
|
|
298
|
+
const pi = Number(key);
|
|
299
|
+
this._customStates.set(pi, value);
|
|
300
|
+
this._stateChangeListeners.forEach((cb) => {
|
|
301
|
+
try {
|
|
302
|
+
cb(pi, value);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
this.handleError(
|
|
305
|
+
new errors.SmoreSDKError("UNKNOWN", "Error in custom state change listener", {
|
|
306
|
+
cause: err instanceof Error ? err : void 0
|
|
307
|
+
})
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
254
313
|
});
|
|
255
314
|
}
|
|
256
315
|
/**
|
|
@@ -319,6 +378,45 @@ class ScreenImpl {
|
|
|
319
378
|
get isDestroyed() {
|
|
320
379
|
return this._isDestroyed;
|
|
321
380
|
}
|
|
381
|
+
get isConnected() {
|
|
382
|
+
return this._isConnected;
|
|
383
|
+
}
|
|
384
|
+
get protocolVersion() {
|
|
385
|
+
return this._protocolVersion;
|
|
386
|
+
}
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// Lifecycle Listener Helpers
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
_addLifecycleListener(event, listener) {
|
|
391
|
+
let set = this._lifecycleListeners.get(event);
|
|
392
|
+
if (!set) {
|
|
393
|
+
set = /* @__PURE__ */ new Set();
|
|
394
|
+
this._lifecycleListeners.set(event, set);
|
|
395
|
+
}
|
|
396
|
+
set.add(listener);
|
|
397
|
+
return () => {
|
|
398
|
+
set.delete(listener);
|
|
399
|
+
if (set.size === 0) this._lifecycleListeners.delete(event);
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
_emitLifecycle(event, ...args) {
|
|
403
|
+
this._lifecycleListeners.get(event)?.forEach((cb) => {
|
|
404
|
+
try {
|
|
405
|
+
cb(...args);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
this.handleError(
|
|
408
|
+
new errors.SmoreSDKError("UNKNOWN", `Error in lifecycle handler for "${event}"`, {
|
|
409
|
+
cause: err instanceof Error ? err : void 0,
|
|
410
|
+
details: { event }
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
_hasLifecycleListeners(event) {
|
|
417
|
+
const set = this._lifecycleListeners.get(event);
|
|
418
|
+
return set !== void 0 && set.size > 0;
|
|
419
|
+
}
|
|
322
420
|
// ---------------------------------------------------------------------------
|
|
323
421
|
// Lifecycle Methods
|
|
324
422
|
// ---------------------------------------------------------------------------
|
|
@@ -326,51 +424,46 @@ class ScreenImpl {
|
|
|
326
424
|
if (this._allReadyFired) {
|
|
327
425
|
callback();
|
|
328
426
|
}
|
|
329
|
-
this.
|
|
330
|
-
return () => {
|
|
331
|
-
this._onAllReadyCallbacks.delete(callback);
|
|
332
|
-
};
|
|
427
|
+
return this._addLifecycleListener("$all-ready", callback);
|
|
333
428
|
}
|
|
334
429
|
onControllerJoin(callback) {
|
|
335
|
-
this.
|
|
336
|
-
return () => {
|
|
337
|
-
this._onControllerJoinCallbacks.delete(callback);
|
|
338
|
-
};
|
|
430
|
+
return this._addLifecycleListener("$controller-join", callback);
|
|
339
431
|
}
|
|
340
432
|
onControllerLeave(callback) {
|
|
341
|
-
this.
|
|
342
|
-
return () => {
|
|
343
|
-
this._onControllerLeaveCallbacks.delete(callback);
|
|
344
|
-
};
|
|
433
|
+
return this._addLifecycleListener("$controller-leave", callback);
|
|
345
434
|
}
|
|
346
435
|
onControllerDisconnect(callback) {
|
|
347
|
-
this.
|
|
348
|
-
return () => {
|
|
349
|
-
this._onControllerDisconnectCallbacks.delete(callback);
|
|
350
|
-
};
|
|
436
|
+
return this._addLifecycleListener("$controller-disconnect", callback);
|
|
351
437
|
}
|
|
352
438
|
onControllerReconnect(callback) {
|
|
353
|
-
this.
|
|
354
|
-
return () => {
|
|
355
|
-
this._onControllerReconnectCallbacks.delete(callback);
|
|
356
|
-
};
|
|
439
|
+
return this._addLifecycleListener("$controller-reconnect", callback);
|
|
357
440
|
}
|
|
358
441
|
onCharacterUpdated(callback) {
|
|
359
|
-
this.
|
|
360
|
-
return () => {
|
|
361
|
-
this._onCharacterUpdatedCallbacks.delete(callback);
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
onRateLimited(callback) {
|
|
365
|
-
this._onRateLimitedCallbacks.add(callback);
|
|
366
|
-
return () => {
|
|
367
|
-
this._onRateLimitedCallbacks.delete(callback);
|
|
368
|
-
};
|
|
442
|
+
return this._addLifecycleListener("$character-updated", callback);
|
|
369
443
|
}
|
|
370
444
|
onError(callback) {
|
|
371
|
-
this.
|
|
445
|
+
return this._addLifecycleListener("$error", callback);
|
|
446
|
+
}
|
|
447
|
+
onConnectionChange(callback) {
|
|
448
|
+
return this._addLifecycleListener("$connection-change", callback);
|
|
449
|
+
}
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Custom State Methods
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
getControllerState(playerIndex) {
|
|
454
|
+
return this._customStates.get(playerIndex);
|
|
455
|
+
}
|
|
456
|
+
getAllControllerStates() {
|
|
457
|
+
const result = {};
|
|
458
|
+
for (const [key, value] of this._customStates) {
|
|
459
|
+
result[key] = value;
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
onCustomStateChange(listener) {
|
|
464
|
+
this._stateChangeListeners.add(listener);
|
|
372
465
|
return () => {
|
|
373
|
-
this.
|
|
466
|
+
this._stateChangeListeners.delete(listener);
|
|
374
467
|
};
|
|
375
468
|
}
|
|
376
469
|
// ---------------------------------------------------------------------------
|
|
@@ -380,7 +473,6 @@ class ScreenImpl {
|
|
|
380
473
|
* Send type-safe events to all controllers.
|
|
381
474
|
*
|
|
382
475
|
* Uses EventMap generic for compile-time type checking of event names and data payloads.
|
|
383
|
-
* Runtime behavior is identical to broadcastRaw - both call validateEventName at runtime.
|
|
384
476
|
*
|
|
385
477
|
* @note Data should be an object. Primitive values will be wrapped as `{ data: value }` by the relay server.
|
|
386
478
|
* @note Maximum payload size is 64KB. Data exceeding this limit will be silently dropped by the server.
|
|
@@ -389,31 +481,18 @@ class ScreenImpl {
|
|
|
389
481
|
*
|
|
390
482
|
* Warning: Avoid sending primitive values directly (string, number, boolean).
|
|
391
483
|
* Wrap in an object: broadcast('event', { value: 42 }) instead of broadcast('event', 42)
|
|
392
|
-
*
|
|
393
|
-
* @see broadcastRaw for bypassing TypeScript type checking (runtime behavior identical)
|
|
394
484
|
*/
|
|
395
485
|
broadcast(event, data) {
|
|
396
|
-
this.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
this.transport
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
* Bypasses EventMap generic type checks at compile time.
|
|
405
|
-
* Runtime behavior is identical to broadcast - both call validateEventName at runtime.
|
|
406
|
-
*
|
|
407
|
-
* Use this when you need dynamic event names or when working without a predefined EventMap.
|
|
408
|
-
*
|
|
409
|
-
* @note Data should be an object. Primitive values will be wrapped as `{ data: value }` by the relay server.
|
|
410
|
-
* @note Maximum payload size is 64KB. Data exceeding this limit will be silently dropped by the server.
|
|
411
|
-
*
|
|
412
|
-
* @see broadcast for type-safe version using EventMap generic
|
|
413
|
-
*/
|
|
414
|
-
broadcastRaw(event, data) {
|
|
415
|
-
this.ensureReady("broadcastRaw");
|
|
486
|
+
if (this._isDestroyed) {
|
|
487
|
+
throw new errors.SmoreSDKError("DESTROYED", "Cannot call broadcast() after destroy()");
|
|
488
|
+
}
|
|
489
|
+
if (!this._isReady || !this.transport) {
|
|
490
|
+
this._outboundBuffer.push({ method: "broadcast", args: [event, data] });
|
|
491
|
+
this.logger.debug(`Buffered broadcast "${event}" (screen not ready yet)`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
416
494
|
events.validateEventName(event);
|
|
495
|
+
shared.validatePayloadSize(data);
|
|
417
496
|
this.logger.send(event, data);
|
|
418
497
|
this.transport.emit(event, data);
|
|
419
498
|
}
|
|
@@ -432,24 +511,17 @@ class ScreenImpl {
|
|
|
432
511
|
* @param data - Event data payload
|
|
433
512
|
*/
|
|
434
513
|
sendToController(playerIndex, event, data) {
|
|
435
|
-
this.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
|
|
514
|
+
if (this._isDestroyed) {
|
|
515
|
+
throw new errors.SmoreSDKError("DESTROYED", "Cannot call sendToController() after destroy()");
|
|
516
|
+
}
|
|
517
|
+
if (!this._isReady || !this.transport) {
|
|
518
|
+
this._outboundBuffer.push({ method: "sendToController", args: [playerIndex, event, data] });
|
|
519
|
+
this.logger.debug(`Buffered sendToController "${event}" -> Player ${playerIndex} (screen not ready yet)`);
|
|
520
|
+
return;
|
|
442
521
|
}
|
|
443
|
-
this.logger.send(`${event} -> Player ${playerIndex}`, data);
|
|
444
|
-
this.transport.emit(event, {
|
|
445
|
-
targetPlayerIndex: playerIndex,
|
|
446
|
-
...data && typeof data === "object" ? data : { data }
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
sendToControllerRaw(playerIndex, event, data) {
|
|
450
|
-
this.ensureReady("sendToControllerRaw");
|
|
451
522
|
events.validateEventName(event);
|
|
452
523
|
validatePlayerIndex(playerIndex, this._controllers);
|
|
524
|
+
shared.validatePayloadSize(data);
|
|
453
525
|
if (data && typeof data === "object" && "targetPlayerIndex" in data) {
|
|
454
526
|
this.logger.warn(
|
|
455
527
|
`Event "${event}" data contains reserved field "targetPlayerIndex" which will be overwritten for routing.`
|
|
@@ -484,6 +556,16 @@ class ScreenImpl {
|
|
|
484
556
|
* are queued and activated when the transport becomes available.
|
|
485
557
|
*/
|
|
486
558
|
on(event, handler) {
|
|
559
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
560
|
+
const validEvents = events.SCREEN_LIFECYCLE_EVENTS;
|
|
561
|
+
if (!validEvents.has(event)) {
|
|
562
|
+
throw new errors.SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}". Valid lifecycle events: ${Array.from(validEvents).join(", ")}`);
|
|
563
|
+
}
|
|
564
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
565
|
+
handler();
|
|
566
|
+
}
|
|
567
|
+
return this._addLifecycleListener(event, handler);
|
|
568
|
+
}
|
|
487
569
|
events.validateEventName(event);
|
|
488
570
|
let handlers = this.eventHandlers.get(event);
|
|
489
571
|
if (!handlers) {
|
|
@@ -546,6 +628,23 @@ class ScreenImpl {
|
|
|
546
628
|
* @returns Unsubscribe function to remove the handler before it fires
|
|
547
629
|
*/
|
|
548
630
|
once(event, handler) {
|
|
631
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
632
|
+
const validEvents = events.SCREEN_LIFECYCLE_EVENTS;
|
|
633
|
+
if (!validEvents.has(event)) {
|
|
634
|
+
throw new errors.SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}"`);
|
|
635
|
+
}
|
|
636
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
637
|
+
handler();
|
|
638
|
+
return () => {
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const wrapper = (...args) => {
|
|
642
|
+
unsub();
|
|
643
|
+
handler(...args);
|
|
644
|
+
};
|
|
645
|
+
const unsub = this._addLifecycleListener(event, wrapper);
|
|
646
|
+
return unsub;
|
|
647
|
+
}
|
|
549
648
|
const wrappedHandler = (playerIndex, data) => {
|
|
550
649
|
unsubscribe();
|
|
551
650
|
handler(playerIndex, data);
|
|
@@ -554,6 +653,14 @@ class ScreenImpl {
|
|
|
554
653
|
return unsubscribe;
|
|
555
654
|
}
|
|
556
655
|
off(event, handler) {
|
|
656
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
657
|
+
if (!handler) {
|
|
658
|
+
this._lifecycleListeners.delete(event);
|
|
659
|
+
} else {
|
|
660
|
+
this._lifecycleListeners.get(event)?.delete(handler);
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
557
664
|
if (!handler) {
|
|
558
665
|
this.eventHandlers.delete(event);
|
|
559
666
|
this.transport?.off(event);
|
|
@@ -581,6 +688,21 @@ class ScreenImpl {
|
|
|
581
688
|
);
|
|
582
689
|
}
|
|
583
690
|
}
|
|
691
|
+
removeAllListeners(event) {
|
|
692
|
+
if (event) {
|
|
693
|
+
this.eventHandlers.delete(event);
|
|
694
|
+
this.transport?.off(event);
|
|
695
|
+
this.registeredTransportHandlers = this.registeredTransportHandlers.filter((h) => h.event !== event);
|
|
696
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
697
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
698
|
+
}
|
|
699
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
700
|
+
} else {
|
|
701
|
+
for (const evt of [...this.eventHandlers.keys()]) {
|
|
702
|
+
this.removeAllListeners(evt);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
584
706
|
// ---------------------------------------------------------------------------
|
|
585
707
|
// Utilities
|
|
586
708
|
// ---------------------------------------------------------------------------
|
|
@@ -590,9 +712,6 @@ class ScreenImpl {
|
|
|
590
712
|
getControllerCount() {
|
|
591
713
|
return this._controllers.filter((c) => c.connected).length;
|
|
592
714
|
}
|
|
593
|
-
hasAnyConnectedControllers() {
|
|
594
|
-
return this._controllers.some((c) => c.connected);
|
|
595
|
-
}
|
|
596
715
|
// ---------------------------------------------------------------------------
|
|
597
716
|
// Cleanup
|
|
598
717
|
// ---------------------------------------------------------------------------
|
|
@@ -616,14 +735,11 @@ class ScreenImpl {
|
|
|
616
735
|
this.eventHandlers.clear();
|
|
617
736
|
this.handlerToTransport.clear();
|
|
618
737
|
this._pendingHandlers = [];
|
|
619
|
-
this.
|
|
620
|
-
this.
|
|
621
|
-
this.
|
|
622
|
-
this.
|
|
623
|
-
this.
|
|
624
|
-
this._onCharacterUpdatedCallbacks.clear();
|
|
625
|
-
this._onRateLimitedCallbacks.clear();
|
|
626
|
-
this._onErrorCallbacks.clear();
|
|
738
|
+
this._lifecycleListeners.clear();
|
|
739
|
+
this._customStates.clear();
|
|
740
|
+
this._stateChangeListeners.clear();
|
|
741
|
+
this._isConnected = false;
|
|
742
|
+
this._outboundBuffer = [];
|
|
627
743
|
if (this.transport instanceof PostMessageTransport.PostMessageTransport) {
|
|
628
744
|
this.transport.destroy();
|
|
629
745
|
}
|
|
@@ -639,8 +755,8 @@ class ScreenImpl {
|
|
|
639
755
|
handleError(error) {
|
|
640
756
|
this.logger.warn(`Error in handler: ${error.message}`);
|
|
641
757
|
const smoreError = error.toSmoreError();
|
|
642
|
-
if (this.
|
|
643
|
-
this.
|
|
758
|
+
if (this._hasLifecycleListeners("$error")) {
|
|
759
|
+
this._emitLifecycle("$error", smoreError);
|
|
644
760
|
} else {
|
|
645
761
|
this.logger.error(error.message, error.details);
|
|
646
762
|
}
|