@smoregg/sdk 1.3.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 +342 -193
- 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 +336 -238
- 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 +269 -197
- 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 +344 -195
- 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 +338 -240
- 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 +269 -197
- 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 +22 -43
- 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 +15 -19
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/screen.d.ts +26 -37
- package/dist/types/screen.d.ts.map +1 -1
- package/dist/types/shared.d.ts +21 -0
- package/dist/types/shared.d.ts.map +1 -0
- package/dist/types/testing.d.ts +78 -3
- 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 +391 -540
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +742 -952
- 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) {
|
|
@@ -28,131 +28,165 @@ class ScreenImpl {
|
|
|
28
28
|
_roomCode = "";
|
|
29
29
|
_isReady = false;
|
|
30
30
|
_isDestroyed = false;
|
|
31
|
+
_initTimeoutId = null;
|
|
31
32
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
32
33
|
registeredTransportHandlers = [];
|
|
33
34
|
boundMessageHandler = null;
|
|
34
|
-
// Maps user-facing handler
|
|
35
|
+
// Maps user-facing handler -> transport wrappedHandler for proper cleanup in on()/off()
|
|
35
36
|
handlerToTransport = /* @__PURE__ */ new Map();
|
|
36
|
-
//
|
|
37
|
-
|
|
37
|
+
// Pending handlers registered via on() before transport is ready
|
|
38
|
+
_pendingHandlers = [];
|
|
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 = [];
|
|
43
|
+
// Whether all-ready has fired
|
|
44
|
+
_allReadyFired = false;
|
|
45
|
+
// Self-connection awareness
|
|
46
|
+
_isConnected = false;
|
|
47
|
+
// Protocol versioning
|
|
48
|
+
_protocolVersion = protocol.PROTOCOL_VERSION;
|
|
49
|
+
// Ready promise
|
|
50
|
+
_readyResolve;
|
|
51
|
+
_readyReject;
|
|
52
|
+
ready;
|
|
38
53
|
constructor(config = {}) {
|
|
39
54
|
this.config = config;
|
|
40
55
|
this.logger = new logger.DebugLogger(config.debug, "[SmoreScreen]");
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
this.ready = new Promise((resolve, reject) => {
|
|
57
|
+
this._readyResolve = resolve;
|
|
58
|
+
this._readyReject = reject;
|
|
59
|
+
});
|
|
60
|
+
this.startInitialization();
|
|
46
61
|
}
|
|
47
62
|
// ---------------------------------------------------------------------------
|
|
48
|
-
// Initialization (called
|
|
63
|
+
// Initialization (called in constructor)
|
|
49
64
|
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
65
|
+
startInitialization() {
|
|
51
66
|
this.logger.lifecycle("Initializing screen...");
|
|
52
67
|
const parentOrigin = this.config.parentOrigin ?? "*";
|
|
53
68
|
const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
69
|
+
this._initTimeoutId = setTimeout(() => {
|
|
70
|
+
this.cleanup();
|
|
71
|
+
const error = new errors.SmoreSDKError(
|
|
72
|
+
"TIMEOUT",
|
|
73
|
+
`Screen initialization timed out after ${timeout}ms. Make sure the parent frame sends _bridge:init. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Screen instance to retry (this instance has been cleaned up).`,
|
|
74
|
+
{ details: { timeout } }
|
|
75
|
+
);
|
|
76
|
+
this.handleError(error);
|
|
77
|
+
this._readyReject(error);
|
|
78
|
+
}, timeout);
|
|
79
|
+
this.boundMessageHandler = (e) => {
|
|
80
|
+
if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
|
|
81
|
+
const msg = e.data;
|
|
82
|
+
if (!protocol.isBridgeMessage(msg)) return;
|
|
83
|
+
if (msg.type === "_bridge:init") {
|
|
84
|
+
clearTimeout(this._initTimeoutId);
|
|
85
|
+
const initPayload = msg.payload;
|
|
86
|
+
try {
|
|
87
|
+
protocol.validateInitPayload(initPayload);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const error = new errors.SmoreSDKError(
|
|
90
|
+
"INIT_FAILED",
|
|
91
|
+
`Invalid _bridge:init payload: ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
{ details: { payload: initPayload } }
|
|
93
|
+
);
|
|
94
|
+
this.logger.warn("_bridge:init validation failed", error);
|
|
95
|
+
this.handleError(error);
|
|
96
|
+
this._readyReject(error);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const initData = initPayload;
|
|
100
|
+
if (initData.side !== "host") {
|
|
101
|
+
const error = new errors.SmoreSDKError(
|
|
102
|
+
"INIT_FAILED",
|
|
103
|
+
`Received init for wrong side: ${initData.side}. Expected "host".`,
|
|
104
|
+
{ details: { side: initData.side } }
|
|
105
|
+
);
|
|
106
|
+
this.handleError(error);
|
|
107
|
+
this._readyReject(error);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.transport = this.config.transport ?? new PostMessageTransport.PostMessageTransport(parentOrigin);
|
|
111
|
+
this._roomCode = initData.roomCode;
|
|
112
|
+
const serverProtocolVersion = initData.protocolVersion;
|
|
113
|
+
if (serverProtocolVersion !== void 0) {
|
|
114
|
+
this._protocolVersion = serverProtocolVersion;
|
|
115
|
+
if (serverProtocolVersion !== protocol.PROTOCOL_VERSION) {
|
|
116
|
+
this.logger.warn(
|
|
117
|
+
`Protocol version mismatch: SDK v${protocol.PROTOCOL_VERSION}, server v${serverProtocolVersion}. Some features may not work correctly.`
|
|
91
118
|
);
|
|
92
|
-
this.handleError(error);
|
|
93
|
-
reject(error);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
this.transport = new PostMessageTransport.PostMessageTransport(parentOrigin);
|
|
97
|
-
this._roomCode = initData.roomCode;
|
|
98
|
-
this._controllers = this.mapControllersFromInit(initData.players);
|
|
99
|
-
if (this._controllers.length === 0) {
|
|
100
|
-
this.logger.warn("Screen initialized with zero controllers");
|
|
101
|
-
}
|
|
102
|
-
this.setupEventHandlers();
|
|
103
|
-
this._isReady = true;
|
|
104
|
-
this.logger.lifecycle("Screen ready", {
|
|
105
|
-
roomCode: this._roomCode,
|
|
106
|
-
controllers: this._controllers.length
|
|
107
|
-
});
|
|
108
|
-
const autoReady = config.getGlobalConfig().autoReady ?? true;
|
|
109
|
-
if (autoReady) {
|
|
110
|
-
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
111
|
-
this.signalReady();
|
|
112
119
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
}
|
|
121
|
+
this._controllers = this.mapControllersFromInit(initData.players);
|
|
122
|
+
if (this._controllers.length === 0) {
|
|
123
|
+
this.logger.warn("Screen initialized with zero controllers");
|
|
124
|
+
}
|
|
125
|
+
this.setupEventHandlers();
|
|
126
|
+
for (const { event, handler } of this._pendingHandlers) {
|
|
127
|
+
this.setupUserEventHandler(event, handler);
|
|
128
|
+
}
|
|
129
|
+
this._pendingHandlers = [];
|
|
130
|
+
this._isConnected = true;
|
|
131
|
+
this._isReady = true;
|
|
132
|
+
for (const buffered of this._outboundBuffer) {
|
|
133
|
+
try {
|
|
134
|
+
switch (buffered.method) {
|
|
135
|
+
case "broadcast":
|
|
136
|
+
this.broadcast(buffered.args[0], buffered.args[1]);
|
|
137
|
+
break;
|
|
138
|
+
case "sendToController":
|
|
139
|
+
this.sendToController(buffered.args[0], buffered.args[1], buffered.args[2]);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
this.handleError(err instanceof errors.SmoreSDKError ? err : new errors.SmoreSDKError("UNKNOWN", "Failed to flush buffered message"));
|
|
118
144
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
}
|
|
146
|
+
this._outboundBuffer = [];
|
|
147
|
+
this.logger.lifecycle("Screen ready", {
|
|
148
|
+
roomCode: this._roomCode,
|
|
149
|
+
controllers: this._controllers.length
|
|
150
|
+
});
|
|
151
|
+
if (this.config.autoReady !== false) {
|
|
152
|
+
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
153
|
+
this.signalReady();
|
|
154
|
+
}
|
|
155
|
+
this._readyResolve();
|
|
156
|
+
} else if (msg.type === "_bridge:update") {
|
|
157
|
+
if (!this._isReady) {
|
|
158
|
+
this.logger.debug("Ignoring _bridge:update before init completes");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const updateData = msg.payload;
|
|
162
|
+
if (updateData.players && Array.isArray(updateData.players)) {
|
|
163
|
+
const oldControllers = this._controllers;
|
|
164
|
+
const newControllers = this.mapControllersFromInit(updateData.players);
|
|
165
|
+
this._controllers = newControllers;
|
|
166
|
+
for (const nc of newControllers) {
|
|
167
|
+
if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
|
|
168
|
+
this.logger.lifecycle("Controller joined (via update)", { playerIndex: nc.playerIndex });
|
|
169
|
+
this._emitLifecycle("$controller-join", nc.playerIndex, nc);
|
|
129
170
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
171
|
+
}
|
|
172
|
+
for (const oc of oldControllers) {
|
|
173
|
+
if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
|
|
174
|
+
this.logger.lifecycle("Controller left (via update)", { playerIndex: oc.playerIndex });
|
|
175
|
+
this._emitLifecycle("$controller-leave", oc.playerIndex);
|
|
135
176
|
}
|
|
136
177
|
}
|
|
137
|
-
this.logger.lifecycle("Room updated", {
|
|
138
|
-
controllers: this._controllers.length
|
|
139
|
-
});
|
|
140
178
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
179
|
+
this.logger.lifecycle("Room updated", {
|
|
180
|
+
controllers: this._controllers.length
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
window.addEventListener("message", this.boundMessageHandler);
|
|
185
|
+
window.parent.postMessage({ type: "_bridge:ready", protocolVersion: protocol.PROTOCOL_VERSION }, parentOrigin);
|
|
186
|
+
this.logger.lifecycle("Sent _bridge:ready to parent");
|
|
146
187
|
}
|
|
147
188
|
mapControllersFromInit(players) {
|
|
148
|
-
return players.map((p, index) => (
|
|
149
|
-
playerIndex: p.playerIndex ?? index,
|
|
150
|
-
// Fallback to `nickname` for defensive compatibility (server currently always sends `name`)
|
|
151
|
-
nickname: p.nickname || p.name || `Player ${index + 1}`,
|
|
152
|
-
connected: p.connected !== false,
|
|
153
|
-
// Fallback to `appearance` for defensive compatibility (server currently always sends `character`)
|
|
154
|
-
appearance: p.appearance ?? p.character
|
|
155
|
-
}));
|
|
189
|
+
return players.map((p, index) => shared.mapPlayerDTO(p, index));
|
|
156
190
|
}
|
|
157
191
|
setupEventHandlers() {
|
|
158
192
|
if (!this.transport) return;
|
|
@@ -160,16 +194,11 @@ class ScreenImpl {
|
|
|
160
194
|
const payload = data;
|
|
161
195
|
const playerData = payload?.player;
|
|
162
196
|
if (playerData && typeof playerData.playerIndex === "number") {
|
|
163
|
-
const controllerInfo =
|
|
164
|
-
playerIndex: playerData.playerIndex,
|
|
165
|
-
nickname: playerData.nickname || playerData.name || `Player ${playerData.playerIndex + 1}`,
|
|
166
|
-
connected: playerData.connected !== false,
|
|
167
|
-
appearance: playerData.appearance ?? playerData.character
|
|
168
|
-
};
|
|
197
|
+
const controllerInfo = shared.mapPlayerDTO(playerData, playerData.playerIndex);
|
|
169
198
|
if (this._controllers.some((c) => c.playerIndex === controllerInfo.playerIndex)) return;
|
|
170
199
|
this._controllers = [...this._controllers, controllerInfo];
|
|
171
200
|
this.logger.lifecycle("Controller joined", { playerIndex: controllerInfo.playerIndex });
|
|
172
|
-
this.
|
|
201
|
+
this._emitLifecycle("$controller-join", controllerInfo.playerIndex, controllerInfo);
|
|
173
202
|
}
|
|
174
203
|
});
|
|
175
204
|
this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_LEFT, (data) => {
|
|
@@ -179,7 +208,7 @@ class ScreenImpl {
|
|
|
179
208
|
if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
180
209
|
this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
181
210
|
this.logger.lifecycle("Controller left", { playerIndex });
|
|
182
|
-
this.
|
|
211
|
+
this._emitLifecycle("$controller-leave", playerIndex);
|
|
183
212
|
}
|
|
184
213
|
});
|
|
185
214
|
this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_DISCONNECTED, (data) => {
|
|
@@ -190,55 +219,58 @@ class ScreenImpl {
|
|
|
190
219
|
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
191
220
|
);
|
|
192
221
|
this.logger.lifecycle("Controller disconnected", { playerIndex });
|
|
193
|
-
this.
|
|
222
|
+
this._emitLifecycle("$controller-disconnect", playerIndex);
|
|
194
223
|
}
|
|
195
224
|
});
|
|
196
225
|
this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_RECONNECTED, (data) => {
|
|
197
226
|
const payload = data;
|
|
198
227
|
const playerData = payload?.player;
|
|
199
228
|
if (playerData && typeof playerData.playerIndex === "number") {
|
|
200
|
-
const controllerInfo =
|
|
201
|
-
playerIndex: playerData.playerIndex,
|
|
202
|
-
nickname: playerData.nickname || playerData.name || `Player ${playerData.playerIndex + 1}`,
|
|
203
|
-
connected: true,
|
|
204
|
-
appearance: playerData.appearance ?? playerData.character
|
|
205
|
-
};
|
|
229
|
+
const controllerInfo = shared.mapPlayerDTO(playerData, playerData.playerIndex);
|
|
206
230
|
this._controllers = this._controllers.map(
|
|
207
231
|
(c) => c.playerIndex === controllerInfo.playerIndex ? controllerInfo : c
|
|
208
232
|
);
|
|
209
233
|
this.logger.lifecycle("Controller reconnected", { playerIndex: controllerInfo.playerIndex });
|
|
210
|
-
this.
|
|
234
|
+
this._emitLifecycle("$controller-reconnect", controllerInfo.playerIndex, controllerInfo);
|
|
211
235
|
}
|
|
212
236
|
});
|
|
213
237
|
this.registerTransportHandler(events.SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (data) => {
|
|
214
238
|
const payload = data;
|
|
215
239
|
const playerData = payload?.player;
|
|
216
240
|
if (playerData && typeof playerData.playerIndex === "number") {
|
|
241
|
+
const pi = playerData.playerIndex;
|
|
217
242
|
const appearance = playerData.character ?? null;
|
|
218
243
|
this._controllers = this._controllers.map(
|
|
219
|
-
(c) => c.playerIndex ===
|
|
244
|
+
(c) => c.playerIndex === pi ? { ...c, appearance } : c
|
|
220
245
|
);
|
|
221
|
-
this.logger.lifecycle("Player character updated", { playerIndex:
|
|
222
|
-
this.
|
|
246
|
+
this.logger.lifecycle("Player character updated", { playerIndex: pi });
|
|
247
|
+
this._emitLifecycle("$character-updated", pi, appearance ?? null);
|
|
223
248
|
}
|
|
224
249
|
});
|
|
225
250
|
this.registerTransportHandler(events.SMORE_EVENTS.RATE_LIMITED, (data) => {
|
|
226
251
|
const payload = data;
|
|
227
|
-
const
|
|
228
|
-
this.
|
|
229
|
-
|
|
252
|
+
const eventName = payload?.event ?? "unknown";
|
|
253
|
+
this.handleError(
|
|
254
|
+
new errors.SmoreSDKError("RATE_LIMITED", `Server rate-limited event: ${eventName}`, {
|
|
255
|
+
details: { event: eventName }
|
|
256
|
+
})
|
|
257
|
+
);
|
|
230
258
|
});
|
|
231
259
|
this.registerTransportHandler(events.SMORE_EVENTS.ALL_READY, () => {
|
|
232
260
|
this.logger.lifecycle("All participants ready");
|
|
233
|
-
this.
|
|
261
|
+
this._allReadyFired = true;
|
|
262
|
+
this._emitLifecycle("$all-ready");
|
|
263
|
+
});
|
|
264
|
+
this.registerTransportHandler(events.SMORE_EVENTS.SELF_DISCONNECTED, () => {
|
|
265
|
+
this._isConnected = false;
|
|
266
|
+
this.logger.lifecycle("Connection lost");
|
|
267
|
+
this._emitLifecycle("$connection-change", false);
|
|
268
|
+
});
|
|
269
|
+
this.registerTransportHandler(events.SMORE_EVENTS.SELF_RECONNECTED, () => {
|
|
270
|
+
this._isConnected = true;
|
|
271
|
+
this.logger.lifecycle("Connection restored");
|
|
272
|
+
this._emitLifecycle("$connection-change", true);
|
|
234
273
|
});
|
|
235
|
-
if (this.config.listeners) {
|
|
236
|
-
for (const [event, handler] of Object.entries(this.config.listeners)) {
|
|
237
|
-
if (!handler) continue;
|
|
238
|
-
this._configListenerEvents.add(event);
|
|
239
|
-
this.setupUserEventHandler(event, handler);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
274
|
}
|
|
243
275
|
/**
|
|
244
276
|
* Sets up a user event handler with playerIndex extraction.
|
|
@@ -280,6 +312,7 @@ class ScreenImpl {
|
|
|
280
312
|
this.eventHandlers.set(event, handlers);
|
|
281
313
|
}
|
|
282
314
|
handlers.add(handler);
|
|
315
|
+
this.handlerToTransport.set(handler, { event, transportHandler: wrappedHandler });
|
|
283
316
|
}
|
|
284
317
|
registerTransportHandler(event, handler) {
|
|
285
318
|
if (!this.transport) return;
|
|
@@ -305,6 +338,75 @@ class ScreenImpl {
|
|
|
305
338
|
get isDestroyed() {
|
|
306
339
|
return this._isDestroyed;
|
|
307
340
|
}
|
|
341
|
+
get isConnected() {
|
|
342
|
+
return this._isConnected;
|
|
343
|
+
}
|
|
344
|
+
get protocolVersion() {
|
|
345
|
+
return this._protocolVersion;
|
|
346
|
+
}
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Lifecycle Listener Helpers
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
_addLifecycleListener(event, listener) {
|
|
351
|
+
let set = this._lifecycleListeners.get(event);
|
|
352
|
+
if (!set) {
|
|
353
|
+
set = /* @__PURE__ */ new Set();
|
|
354
|
+
this._lifecycleListeners.set(event, set);
|
|
355
|
+
}
|
|
356
|
+
set.add(listener);
|
|
357
|
+
return () => {
|
|
358
|
+
set.delete(listener);
|
|
359
|
+
if (set.size === 0) this._lifecycleListeners.delete(event);
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
_emitLifecycle(event, ...args) {
|
|
363
|
+
this._lifecycleListeners.get(event)?.forEach((cb) => {
|
|
364
|
+
try {
|
|
365
|
+
cb(...args);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
this.handleError(
|
|
368
|
+
new errors.SmoreSDKError("UNKNOWN", `Error in lifecycle handler for "${event}"`, {
|
|
369
|
+
cause: err instanceof Error ? err : void 0,
|
|
370
|
+
details: { event }
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
_hasLifecycleListeners(event) {
|
|
377
|
+
const set = this._lifecycleListeners.get(event);
|
|
378
|
+
return set !== void 0 && set.size > 0;
|
|
379
|
+
}
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
// Lifecycle Methods
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
onAllReady(callback) {
|
|
384
|
+
if (this._allReadyFired) {
|
|
385
|
+
callback();
|
|
386
|
+
}
|
|
387
|
+
return this._addLifecycleListener("$all-ready", callback);
|
|
388
|
+
}
|
|
389
|
+
onControllerJoin(callback) {
|
|
390
|
+
return this._addLifecycleListener("$controller-join", callback);
|
|
391
|
+
}
|
|
392
|
+
onControllerLeave(callback) {
|
|
393
|
+
return this._addLifecycleListener("$controller-leave", callback);
|
|
394
|
+
}
|
|
395
|
+
onControllerDisconnect(callback) {
|
|
396
|
+
return this._addLifecycleListener("$controller-disconnect", callback);
|
|
397
|
+
}
|
|
398
|
+
onControllerReconnect(callback) {
|
|
399
|
+
return this._addLifecycleListener("$controller-reconnect", callback);
|
|
400
|
+
}
|
|
401
|
+
onCharacterUpdated(callback) {
|
|
402
|
+
return this._addLifecycleListener("$character-updated", callback);
|
|
403
|
+
}
|
|
404
|
+
onError(callback) {
|
|
405
|
+
return this._addLifecycleListener("$error", callback);
|
|
406
|
+
}
|
|
407
|
+
onConnectionChange(callback) {
|
|
408
|
+
return this._addLifecycleListener("$connection-change", callback);
|
|
409
|
+
}
|
|
308
410
|
// ---------------------------------------------------------------------------
|
|
309
411
|
// Communication Methods
|
|
310
412
|
// ---------------------------------------------------------------------------
|
|
@@ -312,7 +414,6 @@ class ScreenImpl {
|
|
|
312
414
|
* Send type-safe events to all controllers.
|
|
313
415
|
*
|
|
314
416
|
* Uses EventMap generic for compile-time type checking of event names and data payloads.
|
|
315
|
-
* Runtime behavior is identical to broadcastRaw - both call validateEventName at runtime.
|
|
316
417
|
*
|
|
317
418
|
* @note Data should be an object. Primitive values will be wrapped as `{ data: value }` by the relay server.
|
|
318
419
|
* @note Maximum payload size is 64KB. Data exceeding this limit will be silently dropped by the server.
|
|
@@ -321,31 +422,18 @@ class ScreenImpl {
|
|
|
321
422
|
*
|
|
322
423
|
* Warning: Avoid sending primitive values directly (string, number, boolean).
|
|
323
424
|
* Wrap in an object: broadcast('event', { value: 42 }) instead of broadcast('event', 42)
|
|
324
|
-
*
|
|
325
|
-
* @see broadcastRaw for bypassing TypeScript type checking (runtime behavior identical)
|
|
326
425
|
*/
|
|
327
426
|
broadcast(event, data) {
|
|
328
|
-
this.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.transport
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
* Bypasses EventMap generic type checks at compile time.
|
|
337
|
-
* Runtime behavior is identical to broadcast - both call validateEventName at runtime.
|
|
338
|
-
*
|
|
339
|
-
* Use this when you need dynamic event names or when working without a predefined EventMap.
|
|
340
|
-
*
|
|
341
|
-
* @note Data should be an object. Primitive values will be wrapped as `{ data: value }` by the relay server.
|
|
342
|
-
* @note Maximum payload size is 64KB. Data exceeding this limit will be silently dropped by the server.
|
|
343
|
-
*
|
|
344
|
-
* @see broadcast for type-safe version using EventMap generic
|
|
345
|
-
*/
|
|
346
|
-
broadcastRaw(event, data) {
|
|
347
|
-
this.ensureReady("broadcastRaw");
|
|
427
|
+
if (this._isDestroyed) {
|
|
428
|
+
throw new errors.SmoreSDKError("DESTROYED", "Cannot call broadcast() after destroy()");
|
|
429
|
+
}
|
|
430
|
+
if (!this._isReady || !this.transport) {
|
|
431
|
+
this._outboundBuffer.push({ method: "broadcast", args: [event, data] });
|
|
432
|
+
this.logger.debug(`Buffered broadcast "${event}" (screen not ready yet)`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
348
435
|
events.validateEventName(event);
|
|
436
|
+
shared.validatePayloadSize(data);
|
|
349
437
|
this.logger.send(event, data);
|
|
350
438
|
this.transport.emit(event, data);
|
|
351
439
|
}
|
|
@@ -364,24 +452,17 @@ class ScreenImpl {
|
|
|
364
452
|
* @param data - Event data payload
|
|
365
453
|
*/
|
|
366
454
|
sendToController(playerIndex, event, data) {
|
|
367
|
-
this.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (
|
|
371
|
-
this.
|
|
372
|
-
|
|
373
|
-
|
|
455
|
+
if (this._isDestroyed) {
|
|
456
|
+
throw new errors.SmoreSDKError("DESTROYED", "Cannot call sendToController() after destroy()");
|
|
457
|
+
}
|
|
458
|
+
if (!this._isReady || !this.transport) {
|
|
459
|
+
this._outboundBuffer.push({ method: "sendToController", args: [playerIndex, event, data] });
|
|
460
|
+
this.logger.debug(`Buffered sendToController "${event}" -> Player ${playerIndex} (screen not ready yet)`);
|
|
461
|
+
return;
|
|
374
462
|
}
|
|
375
|
-
this.logger.send(`${event} -> Player ${playerIndex}`, data);
|
|
376
|
-
this.transport.emit(event, {
|
|
377
|
-
targetPlayerIndex: playerIndex,
|
|
378
|
-
...data && typeof data === "object" ? data : { data }
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
sendToControllerRaw(playerIndex, event, data) {
|
|
382
|
-
this.ensureReady("sendToControllerRaw");
|
|
383
463
|
events.validateEventName(event);
|
|
384
464
|
validatePlayerIndex(playerIndex, this._controllers);
|
|
465
|
+
shared.validatePayloadSize(data);
|
|
385
466
|
if (data && typeof data === "object" && "targetPlayerIndex" in data) {
|
|
386
467
|
this.logger.warn(
|
|
387
468
|
`Event "${event}" data contains reserved field "targetPlayerIndex" which will be overwritten for routing.`
|
|
@@ -412,13 +493,20 @@ class ScreenImpl {
|
|
|
412
493
|
/**
|
|
413
494
|
* Register an event handler for messages from controllers.
|
|
414
495
|
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* will NOT be registered with the transport layer. This means the handler will never
|
|
418
|
-
* fire for events received during the pre-ready window. Always call `on()` after
|
|
419
|
-
* initialization completes, or use `config.listeners` for handlers needed from the start.
|
|
496
|
+
* Can be called before the Screen is ready. Handlers registered before ready
|
|
497
|
+
* are queued and activated when the transport becomes available.
|
|
420
498
|
*/
|
|
421
499
|
on(event, handler) {
|
|
500
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
501
|
+
const validEvents = events.SCREEN_LIFECYCLE_EVENTS;
|
|
502
|
+
if (!validEvents.has(event)) {
|
|
503
|
+
throw new errors.SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}". Valid lifecycle events: ${Array.from(validEvents).join(", ")}`);
|
|
504
|
+
}
|
|
505
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
506
|
+
handler();
|
|
507
|
+
}
|
|
508
|
+
return this._addLifecycleListener(event, handler);
|
|
509
|
+
}
|
|
422
510
|
events.validateEventName(event);
|
|
423
511
|
let handlers = this.eventHandlers.get(event);
|
|
424
512
|
if (!handlers) {
|
|
@@ -426,9 +514,8 @@ class ScreenImpl {
|
|
|
426
514
|
this.eventHandlers.set(event, handlers);
|
|
427
515
|
}
|
|
428
516
|
handlers.add(handler);
|
|
429
|
-
let wrappedHandler = null;
|
|
430
517
|
if (this.transport) {
|
|
431
|
-
wrappedHandler = (data) => {
|
|
518
|
+
const wrappedHandler = (data) => {
|
|
432
519
|
this.logger.receive(event, data);
|
|
433
520
|
const payload = data;
|
|
434
521
|
const { playerIndex, ...rest } = payload;
|
|
@@ -448,16 +535,22 @@ class ScreenImpl {
|
|
|
448
535
|
};
|
|
449
536
|
this.registerTransportHandler(event, wrappedHandler);
|
|
450
537
|
this.handlerToTransport.set(handler, { event, transportHandler: wrappedHandler });
|
|
538
|
+
} else {
|
|
539
|
+
this._pendingHandlers.push({ event, handler });
|
|
451
540
|
}
|
|
452
541
|
return () => {
|
|
453
542
|
handlers?.delete(handler);
|
|
454
543
|
if (handlers?.size === 0) {
|
|
455
544
|
this.eventHandlers.delete(event);
|
|
456
545
|
}
|
|
457
|
-
|
|
458
|
-
|
|
546
|
+
this._pendingHandlers = this._pendingHandlers.filter(
|
|
547
|
+
(p) => !(p.event === event && p.handler === handler)
|
|
548
|
+
);
|
|
549
|
+
const entry = this.handlerToTransport.get(handler);
|
|
550
|
+
if (entry) {
|
|
551
|
+
this.transport?.off(event, entry.transportHandler);
|
|
459
552
|
this.registeredTransportHandlers = this.registeredTransportHandlers.filter(
|
|
460
|
-
(h) => h.handler !==
|
|
553
|
+
(h) => h.handler !== entry.transportHandler
|
|
461
554
|
);
|
|
462
555
|
this.handlerToTransport.delete(handler);
|
|
463
556
|
}
|
|
@@ -474,18 +567,25 @@ class ScreenImpl {
|
|
|
474
567
|
* @param event - Event name to listen for
|
|
475
568
|
* @param handler - Handler function to call once
|
|
476
569
|
* @returns Unsubscribe function to remove the handler before it fires
|
|
477
|
-
*
|
|
478
|
-
* @example
|
|
479
|
-
* ```ts
|
|
480
|
-
* const unsubscribe = screen.once('ready', (playerIndex, data) => {
|
|
481
|
-
* console.log('Ready event received');
|
|
482
|
-
* });
|
|
483
|
-
*
|
|
484
|
-
* // To remove before the event fires:
|
|
485
|
-
* unsubscribe();
|
|
486
|
-
* ```
|
|
487
570
|
*/
|
|
488
571
|
once(event, handler) {
|
|
572
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
573
|
+
const validEvents = events.SCREEN_LIFECYCLE_EVENTS;
|
|
574
|
+
if (!validEvents.has(event)) {
|
|
575
|
+
throw new errors.SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}"`);
|
|
576
|
+
}
|
|
577
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
578
|
+
handler();
|
|
579
|
+
return () => {
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const wrapper = (...args) => {
|
|
583
|
+
unsub();
|
|
584
|
+
handler(...args);
|
|
585
|
+
};
|
|
586
|
+
const unsub = this._addLifecycleListener(event, wrapper);
|
|
587
|
+
return unsub;
|
|
588
|
+
}
|
|
489
589
|
const wrappedHandler = (playerIndex, data) => {
|
|
490
590
|
unsubscribe();
|
|
491
591
|
handler(playerIndex, data);
|
|
@@ -494,25 +594,22 @@ class ScreenImpl {
|
|
|
494
594
|
return unsubscribe;
|
|
495
595
|
}
|
|
496
596
|
off(event, handler) {
|
|
497
|
-
if (
|
|
498
|
-
if (
|
|
499
|
-
|
|
500
|
-
if (val.event === event) {
|
|
501
|
-
this.transport?.off(event, val.transportHandler);
|
|
502
|
-
this.registeredTransportHandlers = this.registeredTransportHandlers.filter(
|
|
503
|
-
(h) => h.handler !== val.transportHandler
|
|
504
|
-
);
|
|
505
|
-
this.handlerToTransport.delete(key);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
597
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
598
|
+
if (!handler) {
|
|
599
|
+
this._lifecycleListeners.delete(event);
|
|
508
600
|
} else {
|
|
509
|
-
this.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
601
|
+
this._lifecycleListeners.get(event)?.delete(handler);
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (!handler) {
|
|
606
|
+
this.eventHandlers.delete(event);
|
|
607
|
+
this.transport?.off(event);
|
|
608
|
+
this.registeredTransportHandlers = this.registeredTransportHandlers.filter((h) => h.event !== event);
|
|
609
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
610
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
515
611
|
}
|
|
612
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
516
613
|
} else {
|
|
517
614
|
const handlers = this.eventHandlers.get(event);
|
|
518
615
|
handlers?.delete(handler);
|
|
@@ -527,6 +624,24 @@ class ScreenImpl {
|
|
|
527
624
|
);
|
|
528
625
|
this.handlerToTransport.delete(handler);
|
|
529
626
|
}
|
|
627
|
+
this._pendingHandlers = this._pendingHandlers.filter(
|
|
628
|
+
(p) => !(p.event === event && p.handler === handler)
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
removeAllListeners(event) {
|
|
633
|
+
if (event) {
|
|
634
|
+
this.eventHandlers.delete(event);
|
|
635
|
+
this.transport?.off(event);
|
|
636
|
+
this.registeredTransportHandlers = this.registeredTransportHandlers.filter((h) => h.event !== event);
|
|
637
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
638
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
639
|
+
}
|
|
640
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
641
|
+
} else {
|
|
642
|
+
for (const evt of [...this.eventHandlers.keys()]) {
|
|
643
|
+
this.removeAllListeners(evt);
|
|
644
|
+
}
|
|
530
645
|
}
|
|
531
646
|
}
|
|
532
647
|
// ---------------------------------------------------------------------------
|
|
@@ -538,28 +653,6 @@ class ScreenImpl {
|
|
|
538
653
|
getControllerCount() {
|
|
539
654
|
return this._controllers.filter((c) => c.connected).length;
|
|
540
655
|
}
|
|
541
|
-
/**
|
|
542
|
-
* Check if there is at least one connected controller.
|
|
543
|
-
* Useful for detecting when all players have disconnected
|
|
544
|
-
* (e.g., to pause the game or show a waiting screen).
|
|
545
|
-
*
|
|
546
|
-
* Use this in onControllerDisconnect callback to detect when all controllers have disconnected.
|
|
547
|
-
*
|
|
548
|
-
* @example
|
|
549
|
-
* ```ts
|
|
550
|
-
* const screen = await createScreen<MyEvents>({
|
|
551
|
-
* onControllerDisconnect: (playerIndex) => {
|
|
552
|
-
* if (!screen.hasAnyConnectedControllers()) {
|
|
553
|
-
* console.log('All controllers disconnected!');
|
|
554
|
-
* screen.broadcast('waiting-for-players', {});
|
|
555
|
-
* }
|
|
556
|
-
* },
|
|
557
|
-
* });
|
|
558
|
-
* ```
|
|
559
|
-
*/
|
|
560
|
-
hasAnyConnectedControllers() {
|
|
561
|
-
return this._controllers.some((c) => c.connected);
|
|
562
|
-
}
|
|
563
656
|
// ---------------------------------------------------------------------------
|
|
564
657
|
// Cleanup
|
|
565
658
|
// ---------------------------------------------------------------------------
|
|
@@ -572,12 +665,20 @@ class ScreenImpl {
|
|
|
572
665
|
this.logger.lifecycle("Screen destroyed");
|
|
573
666
|
}
|
|
574
667
|
cleanup() {
|
|
668
|
+
if (this._initTimeoutId) {
|
|
669
|
+
clearTimeout(this._initTimeoutId);
|
|
670
|
+
this._initTimeoutId = null;
|
|
671
|
+
}
|
|
575
672
|
for (const { event, handler } of this.registeredTransportHandlers) {
|
|
576
673
|
this.transport?.off(event, handler);
|
|
577
674
|
}
|
|
578
675
|
this.registeredTransportHandlers = [];
|
|
579
676
|
this.eventHandlers.clear();
|
|
580
677
|
this.handlerToTransport.clear();
|
|
678
|
+
this._pendingHandlers = [];
|
|
679
|
+
this._lifecycleListeners.clear();
|
|
680
|
+
this._isConnected = false;
|
|
681
|
+
this._outboundBuffer = [];
|
|
581
682
|
if (this.transport instanceof PostMessageTransport.PostMessageTransport) {
|
|
582
683
|
this.transport.destroy();
|
|
583
684
|
}
|
|
@@ -593,8 +694,8 @@ class ScreenImpl {
|
|
|
593
694
|
handleError(error) {
|
|
594
695
|
this.logger.warn(`Error in handler: ${error.message}`);
|
|
595
696
|
const smoreError = error.toSmoreError();
|
|
596
|
-
if (this.
|
|
597
|
-
this.
|
|
697
|
+
if (this._hasLifecycleListeners("$error")) {
|
|
698
|
+
this._emitLifecycle("$error", smoreError);
|
|
598
699
|
} else {
|
|
599
700
|
this.logger.error(error.message, error.details);
|
|
600
701
|
}
|
|
@@ -610,17 +711,14 @@ class ScreenImpl {
|
|
|
610
711
|
if (!this._isReady || !this.transport) {
|
|
611
712
|
throw new errors.SmoreSDKError(
|
|
612
713
|
"NOT_READY",
|
|
613
|
-
`Cannot call ${method}() before screen is ready. Use await
|
|
714
|
+
`Cannot call ${method}() before screen is ready. Use await screen.ready.`,
|
|
614
715
|
{ details: { method } }
|
|
615
716
|
);
|
|
616
717
|
}
|
|
617
718
|
}
|
|
618
719
|
}
|
|
619
720
|
function createScreen(config) {
|
|
620
|
-
|
|
621
|
-
const promise = screen.initialize().then(() => screen);
|
|
622
|
-
promise.instance = screen;
|
|
623
|
-
return promise;
|
|
721
|
+
return new ScreenImpl(config);
|
|
624
722
|
}
|
|
625
723
|
|
|
626
724
|
exports.createScreen = createScreen;
|