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