@smoregg/sdk 2.0.0 → 2.1.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 +193 -115
- 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 +19 -2
- 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 +185 -130
- 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 +125 -74
- 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 +195 -117
- 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 +18 -3
- 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 +187 -132
- 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 +125 -74
- 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 +10 -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 +63 -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 +4 -0
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/types.d.ts +215 -347
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +442 -787
- 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,16 @@ 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
|
+
// Protocol versioning
|
|
46
|
+
_protocolVersion = PROTOCOL_VERSION;
|
|
48
47
|
// Ready promise
|
|
49
48
|
_readyResolve;
|
|
50
49
|
_readyReject;
|
|
@@ -106,8 +105,17 @@ class ScreenImpl {
|
|
|
106
105
|
this._readyReject(error);
|
|
107
106
|
return;
|
|
108
107
|
}
|
|
109
|
-
this.transport = new PostMessageTransport(parentOrigin);
|
|
108
|
+
this.transport = this.config.transport ?? new PostMessageTransport(parentOrigin);
|
|
110
109
|
this._roomCode = initData.roomCode;
|
|
110
|
+
const serverProtocolVersion = initData.protocolVersion;
|
|
111
|
+
if (serverProtocolVersion !== void 0) {
|
|
112
|
+
this._protocolVersion = serverProtocolVersion;
|
|
113
|
+
if (serverProtocolVersion !== PROTOCOL_VERSION) {
|
|
114
|
+
this.logger.warn(
|
|
115
|
+
`Protocol version mismatch: SDK v${PROTOCOL_VERSION}, server v${serverProtocolVersion}. Some features may not work correctly.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
111
119
|
this._controllers = this.mapControllersFromInit(initData.players);
|
|
112
120
|
if (this._controllers.length === 0) {
|
|
113
121
|
this.logger.warn("Screen initialized with zero controllers");
|
|
@@ -117,13 +125,28 @@ class ScreenImpl {
|
|
|
117
125
|
this.setupUserEventHandler(event, handler);
|
|
118
126
|
}
|
|
119
127
|
this._pendingHandlers = [];
|
|
128
|
+
this._isConnected = true;
|
|
120
129
|
this._isReady = true;
|
|
130
|
+
for (const buffered of this._outboundBuffer) {
|
|
131
|
+
try {
|
|
132
|
+
switch (buffered.method) {
|
|
133
|
+
case "broadcast":
|
|
134
|
+
this.broadcast(buffered.args[0], buffered.args[1]);
|
|
135
|
+
break;
|
|
136
|
+
case "sendToController":
|
|
137
|
+
this.sendToController(buffered.args[0], buffered.args[1], buffered.args[2]);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
this.handleError(err instanceof SmoreSDKError ? err : new SmoreSDKError("UNKNOWN", "Failed to flush buffered message"));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this._outboundBuffer = [];
|
|
121
145
|
this.logger.lifecycle("Screen ready", {
|
|
122
146
|
roomCode: this._roomCode,
|
|
123
147
|
controllers: this._controllers.length
|
|
124
148
|
});
|
|
125
|
-
|
|
126
|
-
if (autoReady) {
|
|
149
|
+
if (this.config.autoReady !== false) {
|
|
127
150
|
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
128
151
|
this.signalReady();
|
|
129
152
|
}
|
|
@@ -141,13 +164,13 @@ class ScreenImpl {
|
|
|
141
164
|
for (const nc of newControllers) {
|
|
142
165
|
if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
|
|
143
166
|
this.logger.lifecycle("Controller joined (via update)", { playerIndex: nc.playerIndex });
|
|
144
|
-
this.
|
|
167
|
+
this._emitLifecycle("$controller-join", nc.playerIndex, nc);
|
|
145
168
|
}
|
|
146
169
|
}
|
|
147
170
|
for (const oc of oldControllers) {
|
|
148
171
|
if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
|
|
149
172
|
this.logger.lifecycle("Controller left (via update)", { playerIndex: oc.playerIndex });
|
|
150
|
-
this.
|
|
173
|
+
this._emitLifecycle("$controller-leave", oc.playerIndex);
|
|
151
174
|
}
|
|
152
175
|
}
|
|
153
176
|
}
|
|
@@ -157,18 +180,11 @@ class ScreenImpl {
|
|
|
157
180
|
}
|
|
158
181
|
};
|
|
159
182
|
window.addEventListener("message", this.boundMessageHandler);
|
|
160
|
-
window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
|
|
183
|
+
window.parent.postMessage({ type: "_bridge:ready", protocolVersion: PROTOCOL_VERSION }, parentOrigin);
|
|
161
184
|
this.logger.lifecycle("Sent _bridge:ready to parent");
|
|
162
185
|
}
|
|
163
186
|
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
|
-
}));
|
|
187
|
+
return players.map((p, index) => mapPlayerDTO(p, index));
|
|
172
188
|
}
|
|
173
189
|
setupEventHandlers() {
|
|
174
190
|
if (!this.transport) return;
|
|
@@ -176,16 +192,11 @@ class ScreenImpl {
|
|
|
176
192
|
const payload = data;
|
|
177
193
|
const playerData = payload?.player;
|
|
178
194
|
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
|
-
};
|
|
195
|
+
const controllerInfo = mapPlayerDTO(playerData, playerData.playerIndex);
|
|
185
196
|
if (this._controllers.some((c) => c.playerIndex === controllerInfo.playerIndex)) return;
|
|
186
197
|
this._controllers = [...this._controllers, controllerInfo];
|
|
187
198
|
this.logger.lifecycle("Controller joined", { playerIndex: controllerInfo.playerIndex });
|
|
188
|
-
this.
|
|
199
|
+
this._emitLifecycle("$controller-join", controllerInfo.playerIndex, controllerInfo);
|
|
189
200
|
}
|
|
190
201
|
});
|
|
191
202
|
this.registerTransportHandler(SMORE_EVENTS.PLAYER_LEFT, (data) => {
|
|
@@ -195,7 +206,7 @@ class ScreenImpl {
|
|
|
195
206
|
if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
196
207
|
this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
197
208
|
this.logger.lifecycle("Controller left", { playerIndex });
|
|
198
|
-
this.
|
|
209
|
+
this._emitLifecycle("$controller-leave", playerIndex);
|
|
199
210
|
}
|
|
200
211
|
});
|
|
201
212
|
this.registerTransportHandler(SMORE_EVENTS.PLAYER_DISCONNECTED, (data) => {
|
|
@@ -206,24 +217,19 @@ class ScreenImpl {
|
|
|
206
217
|
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
207
218
|
);
|
|
208
219
|
this.logger.lifecycle("Controller disconnected", { playerIndex });
|
|
209
|
-
this.
|
|
220
|
+
this._emitLifecycle("$controller-disconnect", playerIndex);
|
|
210
221
|
}
|
|
211
222
|
});
|
|
212
223
|
this.registerTransportHandler(SMORE_EVENTS.PLAYER_RECONNECTED, (data) => {
|
|
213
224
|
const payload = data;
|
|
214
225
|
const playerData = payload?.player;
|
|
215
226
|
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
|
-
};
|
|
227
|
+
const controllerInfo = mapPlayerDTO(playerData, playerData.playerIndex);
|
|
222
228
|
this._controllers = this._controllers.map(
|
|
223
229
|
(c) => c.playerIndex === controllerInfo.playerIndex ? controllerInfo : c
|
|
224
230
|
);
|
|
225
231
|
this.logger.lifecycle("Controller reconnected", { playerIndex: controllerInfo.playerIndex });
|
|
226
|
-
this.
|
|
232
|
+
this._emitLifecycle("$controller-reconnect", controllerInfo.playerIndex, controllerInfo);
|
|
227
233
|
}
|
|
228
234
|
});
|
|
229
235
|
this.registerTransportHandler(SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (data) => {
|
|
@@ -236,19 +242,32 @@ class ScreenImpl {
|
|
|
236
242
|
(c) => c.playerIndex === pi ? { ...c, appearance } : c
|
|
237
243
|
);
|
|
238
244
|
this.logger.lifecycle("Player character updated", { playerIndex: pi });
|
|
239
|
-
this.
|
|
245
|
+
this._emitLifecycle("$character-updated", pi, appearance ?? null);
|
|
240
246
|
}
|
|
241
247
|
});
|
|
242
248
|
this.registerTransportHandler(SMORE_EVENTS.RATE_LIMITED, (data) => {
|
|
243
249
|
const payload = data;
|
|
244
|
-
const
|
|
245
|
-
this.
|
|
246
|
-
|
|
250
|
+
const eventName = payload?.event ?? "unknown";
|
|
251
|
+
this.handleError(
|
|
252
|
+
new SmoreSDKError("RATE_LIMITED", `Server rate-limited event: ${eventName}`, {
|
|
253
|
+
details: { event: eventName }
|
|
254
|
+
})
|
|
255
|
+
);
|
|
247
256
|
});
|
|
248
257
|
this.registerTransportHandler(SMORE_EVENTS.ALL_READY, () => {
|
|
249
258
|
this.logger.lifecycle("All participants ready");
|
|
250
259
|
this._allReadyFired = true;
|
|
251
|
-
this.
|
|
260
|
+
this._emitLifecycle("$all-ready");
|
|
261
|
+
});
|
|
262
|
+
this.registerTransportHandler(SMORE_EVENTS.SELF_DISCONNECTED, () => {
|
|
263
|
+
this._isConnected = false;
|
|
264
|
+
this.logger.lifecycle("Connection lost");
|
|
265
|
+
this._emitLifecycle("$connection-change", false);
|
|
266
|
+
});
|
|
267
|
+
this.registerTransportHandler(SMORE_EVENTS.SELF_RECONNECTED, () => {
|
|
268
|
+
this._isConnected = true;
|
|
269
|
+
this.logger.lifecycle("Connection restored");
|
|
270
|
+
this._emitLifecycle("$connection-change", true);
|
|
252
271
|
});
|
|
253
272
|
}
|
|
254
273
|
/**
|
|
@@ -317,6 +336,45 @@ class ScreenImpl {
|
|
|
317
336
|
get isDestroyed() {
|
|
318
337
|
return this._isDestroyed;
|
|
319
338
|
}
|
|
339
|
+
get isConnected() {
|
|
340
|
+
return this._isConnected;
|
|
341
|
+
}
|
|
342
|
+
get protocolVersion() {
|
|
343
|
+
return this._protocolVersion;
|
|
344
|
+
}
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Lifecycle Listener Helpers
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
_addLifecycleListener(event, listener) {
|
|
349
|
+
let set = this._lifecycleListeners.get(event);
|
|
350
|
+
if (!set) {
|
|
351
|
+
set = /* @__PURE__ */ new Set();
|
|
352
|
+
this._lifecycleListeners.set(event, set);
|
|
353
|
+
}
|
|
354
|
+
set.add(listener);
|
|
355
|
+
return () => {
|
|
356
|
+
set.delete(listener);
|
|
357
|
+
if (set.size === 0) this._lifecycleListeners.delete(event);
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
_emitLifecycle(event, ...args) {
|
|
361
|
+
this._lifecycleListeners.get(event)?.forEach((cb) => {
|
|
362
|
+
try {
|
|
363
|
+
cb(...args);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
this.handleError(
|
|
366
|
+
new SmoreSDKError("UNKNOWN", `Error in lifecycle handler for "${event}"`, {
|
|
367
|
+
cause: err instanceof Error ? err : void 0,
|
|
368
|
+
details: { event }
|
|
369
|
+
})
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
_hasLifecycleListeners(event) {
|
|
375
|
+
const set = this._lifecycleListeners.get(event);
|
|
376
|
+
return set !== void 0 && set.size > 0;
|
|
377
|
+
}
|
|
320
378
|
// ---------------------------------------------------------------------------
|
|
321
379
|
// Lifecycle Methods
|
|
322
380
|
// ---------------------------------------------------------------------------
|
|
@@ -324,52 +382,28 @@ class ScreenImpl {
|
|
|
324
382
|
if (this._allReadyFired) {
|
|
325
383
|
callback();
|
|
326
384
|
}
|
|
327
|
-
this.
|
|
328
|
-
return () => {
|
|
329
|
-
this._onAllReadyCallbacks.delete(callback);
|
|
330
|
-
};
|
|
385
|
+
return this._addLifecycleListener("$all-ready", callback);
|
|
331
386
|
}
|
|
332
387
|
onControllerJoin(callback) {
|
|
333
|
-
this.
|
|
334
|
-
return () => {
|
|
335
|
-
this._onControllerJoinCallbacks.delete(callback);
|
|
336
|
-
};
|
|
388
|
+
return this._addLifecycleListener("$controller-join", callback);
|
|
337
389
|
}
|
|
338
390
|
onControllerLeave(callback) {
|
|
339
|
-
this.
|
|
340
|
-
return () => {
|
|
341
|
-
this._onControllerLeaveCallbacks.delete(callback);
|
|
342
|
-
};
|
|
391
|
+
return this._addLifecycleListener("$controller-leave", callback);
|
|
343
392
|
}
|
|
344
393
|
onControllerDisconnect(callback) {
|
|
345
|
-
this.
|
|
346
|
-
return () => {
|
|
347
|
-
this._onControllerDisconnectCallbacks.delete(callback);
|
|
348
|
-
};
|
|
394
|
+
return this._addLifecycleListener("$controller-disconnect", callback);
|
|
349
395
|
}
|
|
350
396
|
onControllerReconnect(callback) {
|
|
351
|
-
this.
|
|
352
|
-
return () => {
|
|
353
|
-
this._onControllerReconnectCallbacks.delete(callback);
|
|
354
|
-
};
|
|
397
|
+
return this._addLifecycleListener("$controller-reconnect", callback);
|
|
355
398
|
}
|
|
356
399
|
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
|
-
};
|
|
400
|
+
return this._addLifecycleListener("$character-updated", callback);
|
|
367
401
|
}
|
|
368
402
|
onError(callback) {
|
|
369
|
-
this.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
403
|
+
return this._addLifecycleListener("$error", callback);
|
|
404
|
+
}
|
|
405
|
+
onConnectionChange(callback) {
|
|
406
|
+
return this._addLifecycleListener("$connection-change", callback);
|
|
373
407
|
}
|
|
374
408
|
// ---------------------------------------------------------------------------
|
|
375
409
|
// Communication Methods
|
|
@@ -378,7 +412,6 @@ class ScreenImpl {
|
|
|
378
412
|
* Send type-safe events to all controllers.
|
|
379
413
|
*
|
|
380
414
|
* 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
415
|
*
|
|
383
416
|
* @note Data should be an object. Primitive values will be wrapped as `{ data: value }` by the relay server.
|
|
384
417
|
* @note Maximum payload size is 64KB. Data exceeding this limit will be silently dropped by the server.
|
|
@@ -387,31 +420,18 @@ class ScreenImpl {
|
|
|
387
420
|
*
|
|
388
421
|
* Warning: Avoid sending primitive values directly (string, number, boolean).
|
|
389
422
|
* 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
423
|
*/
|
|
393
424
|
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");
|
|
425
|
+
if (this._isDestroyed) {
|
|
426
|
+
throw new SmoreSDKError("DESTROYED", "Cannot call broadcast() after destroy()");
|
|
427
|
+
}
|
|
428
|
+
if (!this._isReady || !this.transport) {
|
|
429
|
+
this._outboundBuffer.push({ method: "broadcast", args: [event, data] });
|
|
430
|
+
this.logger.debug(`Buffered broadcast "${event}" (screen not ready yet)`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
414
433
|
validateEventName(event);
|
|
434
|
+
validatePayloadSize(data);
|
|
415
435
|
this.logger.send(event, data);
|
|
416
436
|
this.transport.emit(event, data);
|
|
417
437
|
}
|
|
@@ -430,24 +450,17 @@ class ScreenImpl {
|
|
|
430
450
|
* @param data - Event data payload
|
|
431
451
|
*/
|
|
432
452
|
sendToController(playerIndex, event, data) {
|
|
433
|
-
this.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (
|
|
437
|
-
this.
|
|
438
|
-
|
|
439
|
-
|
|
453
|
+
if (this._isDestroyed) {
|
|
454
|
+
throw new SmoreSDKError("DESTROYED", "Cannot call sendToController() after destroy()");
|
|
455
|
+
}
|
|
456
|
+
if (!this._isReady || !this.transport) {
|
|
457
|
+
this._outboundBuffer.push({ method: "sendToController", args: [playerIndex, event, data] });
|
|
458
|
+
this.logger.debug(`Buffered sendToController "${event}" -> Player ${playerIndex} (screen not ready yet)`);
|
|
459
|
+
return;
|
|
440
460
|
}
|
|
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
461
|
validateEventName(event);
|
|
450
462
|
validatePlayerIndex(playerIndex, this._controllers);
|
|
463
|
+
validatePayloadSize(data);
|
|
451
464
|
if (data && typeof data === "object" && "targetPlayerIndex" in data) {
|
|
452
465
|
this.logger.warn(
|
|
453
466
|
`Event "${event}" data contains reserved field "targetPlayerIndex" which will be overwritten for routing.`
|
|
@@ -482,6 +495,16 @@ class ScreenImpl {
|
|
|
482
495
|
* are queued and activated when the transport becomes available.
|
|
483
496
|
*/
|
|
484
497
|
on(event, handler) {
|
|
498
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
499
|
+
const validEvents = SCREEN_LIFECYCLE_EVENTS;
|
|
500
|
+
if (!validEvents.has(event)) {
|
|
501
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}". Valid lifecycle events: ${Array.from(validEvents).join(", ")}`);
|
|
502
|
+
}
|
|
503
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
504
|
+
handler();
|
|
505
|
+
}
|
|
506
|
+
return this._addLifecycleListener(event, handler);
|
|
507
|
+
}
|
|
485
508
|
validateEventName(event);
|
|
486
509
|
let handlers = this.eventHandlers.get(event);
|
|
487
510
|
if (!handlers) {
|
|
@@ -544,6 +567,23 @@ class ScreenImpl {
|
|
|
544
567
|
* @returns Unsubscribe function to remove the handler before it fires
|
|
545
568
|
*/
|
|
546
569
|
once(event, handler) {
|
|
570
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
571
|
+
const validEvents = SCREEN_LIFECYCLE_EVENTS;
|
|
572
|
+
if (!validEvents.has(event)) {
|
|
573
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}"`);
|
|
574
|
+
}
|
|
575
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
576
|
+
handler();
|
|
577
|
+
return () => {
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
const wrapper = (...args) => {
|
|
581
|
+
unsub();
|
|
582
|
+
handler(...args);
|
|
583
|
+
};
|
|
584
|
+
const unsub = this._addLifecycleListener(event, wrapper);
|
|
585
|
+
return unsub;
|
|
586
|
+
}
|
|
547
587
|
const wrappedHandler = (playerIndex, data) => {
|
|
548
588
|
unsubscribe();
|
|
549
589
|
handler(playerIndex, data);
|
|
@@ -552,6 +592,14 @@ class ScreenImpl {
|
|
|
552
592
|
return unsubscribe;
|
|
553
593
|
}
|
|
554
594
|
off(event, handler) {
|
|
595
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
596
|
+
if (!handler) {
|
|
597
|
+
this._lifecycleListeners.delete(event);
|
|
598
|
+
} else {
|
|
599
|
+
this._lifecycleListeners.get(event)?.delete(handler);
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
555
603
|
if (!handler) {
|
|
556
604
|
this.eventHandlers.delete(event);
|
|
557
605
|
this.transport?.off(event);
|
|
@@ -579,6 +627,21 @@ class ScreenImpl {
|
|
|
579
627
|
);
|
|
580
628
|
}
|
|
581
629
|
}
|
|
630
|
+
removeAllListeners(event) {
|
|
631
|
+
if (event) {
|
|
632
|
+
this.eventHandlers.delete(event);
|
|
633
|
+
this.transport?.off(event);
|
|
634
|
+
this.registeredTransportHandlers = this.registeredTransportHandlers.filter((h) => h.event !== event);
|
|
635
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
636
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
637
|
+
}
|
|
638
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
639
|
+
} else {
|
|
640
|
+
for (const evt of [...this.eventHandlers.keys()]) {
|
|
641
|
+
this.removeAllListeners(evt);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
582
645
|
// ---------------------------------------------------------------------------
|
|
583
646
|
// Utilities
|
|
584
647
|
// ---------------------------------------------------------------------------
|
|
@@ -588,9 +651,6 @@ class ScreenImpl {
|
|
|
588
651
|
getControllerCount() {
|
|
589
652
|
return this._controllers.filter((c) => c.connected).length;
|
|
590
653
|
}
|
|
591
|
-
hasAnyConnectedControllers() {
|
|
592
|
-
return this._controllers.some((c) => c.connected);
|
|
593
|
-
}
|
|
594
654
|
// ---------------------------------------------------------------------------
|
|
595
655
|
// Cleanup
|
|
596
656
|
// ---------------------------------------------------------------------------
|
|
@@ -614,14 +674,9 @@ class ScreenImpl {
|
|
|
614
674
|
this.eventHandlers.clear();
|
|
615
675
|
this.handlerToTransport.clear();
|
|
616
676
|
this._pendingHandlers = [];
|
|
617
|
-
this.
|
|
618
|
-
this.
|
|
619
|
-
this.
|
|
620
|
-
this._onControllerDisconnectCallbacks.clear();
|
|
621
|
-
this._onControllerReconnectCallbacks.clear();
|
|
622
|
-
this._onCharacterUpdatedCallbacks.clear();
|
|
623
|
-
this._onRateLimitedCallbacks.clear();
|
|
624
|
-
this._onErrorCallbacks.clear();
|
|
677
|
+
this._lifecycleListeners.clear();
|
|
678
|
+
this._isConnected = false;
|
|
679
|
+
this._outboundBuffer = [];
|
|
625
680
|
if (this.transport instanceof PostMessageTransport) {
|
|
626
681
|
this.transport.destroy();
|
|
627
682
|
}
|
|
@@ -637,8 +692,8 @@ class ScreenImpl {
|
|
|
637
692
|
handleError(error) {
|
|
638
693
|
this.logger.warn(`Error in handler: ${error.message}`);
|
|
639
694
|
const smoreError = error.toSmoreError();
|
|
640
|
-
if (this.
|
|
641
|
-
this.
|
|
695
|
+
if (this._hasLifecycleListeners("$error")) {
|
|
696
|
+
this._emitLifecycle("$error", smoreError);
|
|
642
697
|
} else {
|
|
643
698
|
this.logger.error(error.message, error.details);
|
|
644
699
|
}
|