@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/esm/controller.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 { validateEventName,
|
|
4
|
+
import { SMORE_EVENTS, validateEventName, CONTROLLER_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
|
class ControllerImpl {
|
|
@@ -11,31 +11,46 @@ class ControllerImpl {
|
|
|
11
11
|
config;
|
|
12
12
|
logger;
|
|
13
13
|
_roomCode = "";
|
|
14
|
-
|
|
14
|
+
_myPlayerIndex = -1;
|
|
15
15
|
_isReady = false;
|
|
16
16
|
_isDestroyed = false;
|
|
17
|
+
_initTimeoutId = null;
|
|
17
18
|
boundMessageHandler = null;
|
|
18
19
|
registeredHandlers = [];
|
|
19
20
|
eventListeners = /* @__PURE__ */ new Map();
|
|
20
21
|
// Maps user-facing handler -> transport wrappedHandler for proper cleanup in on()/off()
|
|
21
22
|
handlerToTransport = /* @__PURE__ */ new Map();
|
|
22
23
|
_controllers = [];
|
|
23
|
-
//
|
|
24
|
-
|
|
24
|
+
// Pending handlers registered via on() before transport is ready
|
|
25
|
+
_pendingHandlers = [];
|
|
26
|
+
// Unified lifecycle listener map (supports both onXxx() and on('$xxx') patterns)
|
|
27
|
+
_lifecycleListeners = /* @__PURE__ */ new Map();
|
|
28
|
+
// Outbound message buffer (messages sent before ready)
|
|
29
|
+
_outboundBuffer = [];
|
|
30
|
+
// Whether all-ready has fired
|
|
31
|
+
_allReadyFired = false;
|
|
32
|
+
// Self-connection awareness
|
|
33
|
+
_isConnected = false;
|
|
34
|
+
// Protocol versioning
|
|
35
|
+
_protocolVersion = PROTOCOL_VERSION;
|
|
36
|
+
// Ready promise
|
|
37
|
+
_readyResolve;
|
|
38
|
+
_readyReject;
|
|
39
|
+
ready;
|
|
25
40
|
constructor(config = {}) {
|
|
26
41
|
this.config = config;
|
|
27
42
|
this.logger = new DebugLogger(config.debug, "[SmoreController]");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
this.ready = new Promise((resolve, reject) => {
|
|
44
|
+
this._readyResolve = resolve;
|
|
45
|
+
this._readyReject = reject;
|
|
46
|
+
});
|
|
47
|
+
this.startInitialization();
|
|
33
48
|
}
|
|
34
49
|
// ---------------------------------------------------------------------------
|
|
35
50
|
// Properties (readonly)
|
|
36
51
|
// ---------------------------------------------------------------------------
|
|
37
|
-
get
|
|
38
|
-
return this.
|
|
52
|
+
get myPlayerIndex() {
|
|
53
|
+
return this._myPlayerIndex;
|
|
39
54
|
}
|
|
40
55
|
get roomCode() {
|
|
41
56
|
return this._roomCode;
|
|
@@ -46,6 +61,12 @@ class ControllerImpl {
|
|
|
46
61
|
get isDestroyed() {
|
|
47
62
|
return this._isDestroyed;
|
|
48
63
|
}
|
|
64
|
+
get isConnected() {
|
|
65
|
+
return this._isConnected;
|
|
66
|
+
}
|
|
67
|
+
get protocolVersion() {
|
|
68
|
+
return this._protocolVersion;
|
|
69
|
+
}
|
|
49
70
|
/**
|
|
50
71
|
* Read-only list of all known controllers (players) in the room.
|
|
51
72
|
* Returns full ControllerInfo including playerIndex, nickname, connected status, and appearance.
|
|
@@ -62,41 +83,42 @@ class ControllerImpl {
|
|
|
62
83
|
getControllerCount() {
|
|
63
84
|
return this._controllers.filter((c) => c.connected).length;
|
|
64
85
|
}
|
|
86
|
+
getController(playerIndex) {
|
|
87
|
+
return this._controllers.find((c) => c.playerIndex === playerIndex);
|
|
88
|
+
}
|
|
65
89
|
// ---------------------------------------------------------------------------
|
|
66
90
|
// Initialization
|
|
67
91
|
// ---------------------------------------------------------------------------
|
|
68
|
-
|
|
92
|
+
startInitialization() {
|
|
69
93
|
const parentOrigin = this.config.parentOrigin ?? "*";
|
|
70
94
|
const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
|
|
71
95
|
this.logger.lifecycle("Initializing controller...", { parentOrigin, timeout });
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
|
|
97
|
-
});
|
|
96
|
+
this._initTimeoutId = setTimeout(() => {
|
|
97
|
+
this.cleanup();
|
|
98
|
+
const error = new SmoreSDKError(
|
|
99
|
+
"TIMEOUT",
|
|
100
|
+
`Controller initialization timed out after ${timeout}ms. Make sure the parent window sends _bridge:init message. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Controller instance to retry (this instance has been cleaned up).`,
|
|
101
|
+
{ details: { timeout } }
|
|
102
|
+
);
|
|
103
|
+
this.handleError(error);
|
|
104
|
+
this._readyReject(error);
|
|
105
|
+
}, timeout);
|
|
106
|
+
this.boundMessageHandler = (e) => {
|
|
107
|
+
if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
|
|
108
|
+
const msg = e.data;
|
|
109
|
+
if (!isBridgeMessage(msg)) return;
|
|
110
|
+
if (msg.type === "_bridge:init") {
|
|
111
|
+
clearTimeout(this._initTimeoutId);
|
|
112
|
+
this.handleInit(msg, parentOrigin);
|
|
113
|
+
} else if (msg.type === "_bridge:update") {
|
|
114
|
+
this.handleUpdate(msg);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
window.addEventListener("message", this.boundMessageHandler);
|
|
118
|
+
this.logger.lifecycle("Sending _bridge:ready to parent");
|
|
119
|
+
window.parent.postMessage({ type: "_bridge:ready", protocolVersion: PROTOCOL_VERSION }, parentOrigin);
|
|
98
120
|
}
|
|
99
|
-
handleInit(msg, parentOrigin
|
|
121
|
+
handleInit(msg, parentOrigin) {
|
|
100
122
|
const initPayload = msg.payload;
|
|
101
123
|
this.logger.debug("Received _bridge:init", initPayload);
|
|
102
124
|
try {
|
|
@@ -109,7 +131,7 @@ class ControllerImpl {
|
|
|
109
131
|
);
|
|
110
132
|
this.logger.warn("_bridge:init validation failed", error);
|
|
111
133
|
this.handleError(error);
|
|
112
|
-
|
|
134
|
+
this._readyReject(error);
|
|
113
135
|
return;
|
|
114
136
|
}
|
|
115
137
|
const initData = initPayload;
|
|
@@ -120,7 +142,7 @@ class ControllerImpl {
|
|
|
120
142
|
{ details: { side: initData.side } }
|
|
121
143
|
);
|
|
122
144
|
this.handleError(error);
|
|
123
|
-
|
|
145
|
+
this._readyReject(error);
|
|
124
146
|
return;
|
|
125
147
|
}
|
|
126
148
|
if (initData.myIndex === void 0) {
|
|
@@ -130,31 +152,51 @@ class ControllerImpl {
|
|
|
130
152
|
{ details: initData }
|
|
131
153
|
);
|
|
132
154
|
this.handleError(error);
|
|
133
|
-
|
|
155
|
+
this._readyReject(error);
|
|
134
156
|
return;
|
|
135
157
|
}
|
|
136
|
-
this.transport = new PostMessageTransport(parentOrigin);
|
|
158
|
+
this.transport = this.config.transport ?? new PostMessageTransport(parentOrigin);
|
|
137
159
|
this._roomCode = initData.roomCode;
|
|
138
|
-
|
|
160
|
+
const serverProtocolVersion = initData.protocolVersion;
|
|
161
|
+
if (serverProtocolVersion !== void 0) {
|
|
162
|
+
this._protocolVersion = serverProtocolVersion;
|
|
163
|
+
if (serverProtocolVersion !== PROTOCOL_VERSION) {
|
|
164
|
+
this.logger.warn(
|
|
165
|
+
`Protocol version mismatch: SDK v${PROTOCOL_VERSION}, server v${serverProtocolVersion}. Some features may not work correctly.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this._myPlayerIndex = initData.myIndex;
|
|
139
170
|
const initPlayers = initData.players;
|
|
140
|
-
this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p) => (
|
|
141
|
-
playerIndex: p.playerIndex,
|
|
142
|
-
nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
|
|
143
|
-
connected: p.connected !== false,
|
|
144
|
-
appearance: p.appearance ?? p.character
|
|
145
|
-
}));
|
|
171
|
+
this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
|
|
146
172
|
this.setupEventHandlers();
|
|
173
|
+
for (const { event, handler } of this._pendingHandlers) {
|
|
174
|
+
this.setupUserEventHandler(event, handler);
|
|
175
|
+
}
|
|
176
|
+
this._pendingHandlers = [];
|
|
177
|
+
this._isConnected = true;
|
|
147
178
|
this._isReady = true;
|
|
179
|
+
for (const buffered of this._outboundBuffer) {
|
|
180
|
+
try {
|
|
181
|
+
switch (buffered.method) {
|
|
182
|
+
case "send":
|
|
183
|
+
this.send(buffered.args[0], buffered.args[1]);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
this.handleError(err instanceof SmoreSDKError ? err : new SmoreSDKError("UNKNOWN", "Failed to flush buffered message"));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
this._outboundBuffer = [];
|
|
148
191
|
this.logger.lifecycle("Controller ready", {
|
|
149
192
|
roomCode: this._roomCode,
|
|
150
|
-
myIndex: this.
|
|
193
|
+
myIndex: this._myPlayerIndex
|
|
151
194
|
});
|
|
152
|
-
|
|
153
|
-
if (autoReady) {
|
|
195
|
+
if (this.config.autoReady !== false) {
|
|
154
196
|
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
155
197
|
this.signalReady();
|
|
156
198
|
}
|
|
157
|
-
|
|
199
|
+
this._readyResolve();
|
|
158
200
|
}
|
|
159
201
|
handleUpdate(msg) {
|
|
160
202
|
if (!this._isReady) {
|
|
@@ -165,21 +207,16 @@ class ControllerImpl {
|
|
|
165
207
|
this.logger.debug("Received _bridge:update", updateData);
|
|
166
208
|
if (updateData.players && Array.isArray(updateData.players)) {
|
|
167
209
|
const players = updateData.players;
|
|
168
|
-
const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p) => (
|
|
169
|
-
playerIndex: p.playerIndex,
|
|
170
|
-
nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
|
|
171
|
-
connected: p.connected !== false,
|
|
172
|
-
appearance: p.appearance ?? p.character
|
|
173
|
-
}));
|
|
210
|
+
const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p, index) => mapPlayerDTO(p, index));
|
|
174
211
|
const oldControllers = this._controllers;
|
|
175
212
|
for (const nc of newControllers) {
|
|
176
213
|
if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
|
|
177
|
-
this.
|
|
214
|
+
this._emitLifecycle("$controller-join", nc.playerIndex, nc);
|
|
178
215
|
}
|
|
179
216
|
}
|
|
180
217
|
for (const oc of oldControllers) {
|
|
181
218
|
if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
|
|
182
|
-
this.
|
|
219
|
+
this._emitLifecycle("$controller-leave", oc.playerIndex);
|
|
183
220
|
}
|
|
184
221
|
}
|
|
185
222
|
for (const nc of newControllers) {
|
|
@@ -187,11 +224,11 @@ class ControllerImpl {
|
|
|
187
224
|
if (oc) {
|
|
188
225
|
if (oc.connected && !nc.connected) {
|
|
189
226
|
this.logger.debug("Player disconnected (via update)", { playerIndex: nc.playerIndex });
|
|
190
|
-
this.
|
|
227
|
+
this._emitLifecycle("$controller-disconnect", nc.playerIndex);
|
|
191
228
|
}
|
|
192
229
|
if (!oc.connected && nc.connected) {
|
|
193
230
|
this.logger.debug("Player reconnected (via update)", { playerIndex: nc.playerIndex });
|
|
194
|
-
this.
|
|
231
|
+
this._emitLifecycle("$controller-reconnect", nc.playerIndex, nc);
|
|
195
232
|
}
|
|
196
233
|
}
|
|
197
234
|
}
|
|
@@ -206,19 +243,10 @@ class ControllerImpl {
|
|
|
206
243
|
const playerIndex = playerInfo?.playerIndex ?? data.playerIndex;
|
|
207
244
|
if (playerIndex !== void 0) {
|
|
208
245
|
if (this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
209
|
-
const controllerInfo = playerInfo ? {
|
|
210
|
-
playerIndex,
|
|
211
|
-
nickname: playerInfo.nickname || playerInfo.name || `Player ${playerIndex + 1}`,
|
|
212
|
-
connected: playerInfo.connected !== false,
|
|
213
|
-
appearance: playerInfo.appearance ?? playerInfo.character
|
|
214
|
-
} : {
|
|
215
|
-
playerIndex,
|
|
216
|
-
nickname: `Player ${playerIndex + 1}`,
|
|
217
|
-
connected: true
|
|
218
|
-
};
|
|
246
|
+
const controllerInfo = playerInfo ? mapPlayerDTO(playerInfo, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
|
|
219
247
|
this._controllers = [...this._controllers, controllerInfo];
|
|
220
248
|
this.logger.debug("Player joined", { playerIndex });
|
|
221
|
-
this.
|
|
249
|
+
this._emitLifecycle("$controller-join", playerIndex, controllerInfo);
|
|
222
250
|
}
|
|
223
251
|
});
|
|
224
252
|
this.registerHandler(SMORE_EVENTS.PLAYER_LEFT, (raw) => {
|
|
@@ -228,7 +256,7 @@ class ControllerImpl {
|
|
|
228
256
|
if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
229
257
|
this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
230
258
|
this.logger.debug("Player left", { playerIndex });
|
|
231
|
-
this.
|
|
259
|
+
this._emitLifecycle("$controller-leave", playerIndex);
|
|
232
260
|
}
|
|
233
261
|
});
|
|
234
262
|
this.registerHandler(SMORE_EVENTS.PLAYER_DISCONNECTED, (raw) => {
|
|
@@ -240,7 +268,7 @@ class ControllerImpl {
|
|
|
240
268
|
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
241
269
|
);
|
|
242
270
|
this.logger.debug("Player disconnected", { playerIndex });
|
|
243
|
-
this.
|
|
271
|
+
this._emitLifecycle("$controller-disconnect", playerIndex);
|
|
244
272
|
}
|
|
245
273
|
});
|
|
246
274
|
this.registerHandler(SMORE_EVENTS.PLAYER_RECONNECTED, (raw) => {
|
|
@@ -248,70 +276,86 @@ class ControllerImpl {
|
|
|
248
276
|
const playerData = data.player;
|
|
249
277
|
const playerIndex = playerData?.playerIndex ?? data.playerIndex;
|
|
250
278
|
if (playerIndex !== void 0) {
|
|
251
|
-
const controllerInfo = playerData ? {
|
|
252
|
-
playerIndex,
|
|
253
|
-
nickname: playerData.nickname || playerData.name || `Player ${playerIndex + 1}`,
|
|
254
|
-
connected: true,
|
|
255
|
-
appearance: playerData.appearance ?? playerData.character
|
|
256
|
-
} : {
|
|
257
|
-
playerIndex,
|
|
258
|
-
nickname: `Player ${playerIndex + 1}`,
|
|
259
|
-
connected: true
|
|
260
|
-
};
|
|
279
|
+
const controllerInfo = playerData ? mapPlayerDTO(playerData, playerIndex) : mapPlayerDTO({ playerIndex, connected: true }, playerIndex);
|
|
261
280
|
this._controllers = this._controllers.map(
|
|
262
281
|
(c) => c.playerIndex === playerIndex ? controllerInfo : c
|
|
263
282
|
);
|
|
264
283
|
this.logger.debug("Player reconnected", { playerIndex });
|
|
265
|
-
this.
|
|
284
|
+
this._emitLifecycle("$controller-reconnect", playerIndex, controllerInfo);
|
|
266
285
|
}
|
|
267
286
|
});
|
|
268
287
|
this.registerHandler(SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (raw) => {
|
|
269
288
|
const payload = raw;
|
|
270
289
|
const playerData = payload?.player;
|
|
271
290
|
if (playerData && typeof playerData.playerIndex === "number") {
|
|
291
|
+
const pi = playerData.playerIndex;
|
|
272
292
|
const appearance = playerData.character ?? null;
|
|
273
293
|
this._controllers = this._controllers.map(
|
|
274
|
-
(c) => c.playerIndex ===
|
|
294
|
+
(c) => c.playerIndex === pi ? { ...c, appearance } : c
|
|
275
295
|
);
|
|
276
|
-
this.logger.debug("Player character updated", { playerIndex:
|
|
277
|
-
this.
|
|
296
|
+
this.logger.debug("Player character updated", { playerIndex: pi });
|
|
297
|
+
this._emitLifecycle("$character-updated", pi, appearance ?? null);
|
|
278
298
|
}
|
|
279
299
|
});
|
|
280
300
|
this.registerHandler(SMORE_EVENTS.RATE_LIMITED, (raw) => {
|
|
281
301
|
const data = raw;
|
|
282
|
-
const
|
|
283
|
-
this.
|
|
284
|
-
|
|
302
|
+
const eventName = data?.event ?? "unknown";
|
|
303
|
+
this.handleError(
|
|
304
|
+
new SmoreSDKError("RATE_LIMITED", `Server rate-limited event: ${eventName}`, {
|
|
305
|
+
details: { event: eventName }
|
|
306
|
+
})
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
this.registerHandler(SMORE_EVENTS.GAME_OVER, (raw) => {
|
|
310
|
+
const data = raw;
|
|
311
|
+
this.logger.lifecycle("Game over", data?.results);
|
|
312
|
+
this._emitLifecycle("$game-over", data?.results);
|
|
285
313
|
});
|
|
286
314
|
this.registerHandler(SMORE_EVENTS.ALL_READY, () => {
|
|
287
315
|
this.logger.lifecycle("All participants ready");
|
|
288
|
-
this.
|
|
316
|
+
this._allReadyFired = true;
|
|
317
|
+
this._emitLifecycle("$all-ready");
|
|
289
318
|
});
|
|
290
|
-
|
|
291
|
-
this.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
319
|
+
this.registerHandler(SMORE_EVENTS.SELF_DISCONNECTED, () => {
|
|
320
|
+
this._isConnected = false;
|
|
321
|
+
this.logger.lifecycle("Connection lost");
|
|
322
|
+
this._emitLifecycle("$connection-change", false);
|
|
323
|
+
});
|
|
324
|
+
this.registerHandler(SMORE_EVENTS.SELF_RECONNECTED, () => {
|
|
325
|
+
this._isConnected = true;
|
|
326
|
+
this.logger.lifecycle("Connection restored");
|
|
327
|
+
this._emitLifecycle("$connection-change", true);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Sets up a user event handler for controller events.
|
|
332
|
+
* Used for registering pending handlers after transport becomes available.
|
|
333
|
+
*/
|
|
334
|
+
setupUserEventHandler(event, handler) {
|
|
335
|
+
const transportHandler = (data) => {
|
|
336
|
+
this.logReceive(event, data);
|
|
337
|
+
try {
|
|
338
|
+
handler(data);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
this.handleError(
|
|
341
|
+
new SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
|
|
342
|
+
cause: err instanceof Error ? err : void 0,
|
|
343
|
+
details: { event }
|
|
344
|
+
})
|
|
345
|
+
);
|
|
313
346
|
}
|
|
347
|
+
};
|
|
348
|
+
if (this.transport) {
|
|
349
|
+
this.transport.on(event, transportHandler);
|
|
350
|
+
this.registeredHandlers.push({ event, handler: transportHandler });
|
|
351
|
+
this.handlerToTransport.set(handler, { event, transportHandler });
|
|
352
|
+
}
|
|
353
|
+
let listeners = this.eventListeners.get(event);
|
|
354
|
+
if (!listeners) {
|
|
355
|
+
listeners = /* @__PURE__ */ new Set();
|
|
356
|
+
this.eventListeners.set(event, listeners);
|
|
314
357
|
}
|
|
358
|
+
listeners.add(handler);
|
|
315
359
|
}
|
|
316
360
|
registerHandler(event, handler) {
|
|
317
361
|
if (!this.transport) return;
|
|
@@ -319,21 +363,95 @@ class ControllerImpl {
|
|
|
319
363
|
this.registeredHandlers.push({ event, handler });
|
|
320
364
|
}
|
|
321
365
|
// ---------------------------------------------------------------------------
|
|
366
|
+
// Lifecycle Listener Helpers
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
_addLifecycleListener(event, listener) {
|
|
369
|
+
let set = this._lifecycleListeners.get(event);
|
|
370
|
+
if (!set) {
|
|
371
|
+
set = /* @__PURE__ */ new Set();
|
|
372
|
+
this._lifecycleListeners.set(event, set);
|
|
373
|
+
}
|
|
374
|
+
set.add(listener);
|
|
375
|
+
return () => {
|
|
376
|
+
set.delete(listener);
|
|
377
|
+
if (set.size === 0) this._lifecycleListeners.delete(event);
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
_emitLifecycle(event, ...args) {
|
|
381
|
+
this._lifecycleListeners.get(event)?.forEach((cb) => {
|
|
382
|
+
try {
|
|
383
|
+
cb(...args);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
this.handleError(
|
|
386
|
+
new SmoreSDKError("UNKNOWN", `Error in lifecycle handler for "${event}"`, {
|
|
387
|
+
cause: err instanceof Error ? err : void 0,
|
|
388
|
+
details: { event }
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
_hasLifecycleListeners(event) {
|
|
395
|
+
const set = this._lifecycleListeners.get(event);
|
|
396
|
+
return set !== void 0 && set.size > 0;
|
|
397
|
+
}
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// Lifecycle Methods
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
onAllReady(callback) {
|
|
402
|
+
if (this._allReadyFired) {
|
|
403
|
+
callback();
|
|
404
|
+
}
|
|
405
|
+
return this._addLifecycleListener("$all-ready", callback);
|
|
406
|
+
}
|
|
407
|
+
onControllerJoin(callback) {
|
|
408
|
+
return this._addLifecycleListener("$controller-join", callback);
|
|
409
|
+
}
|
|
410
|
+
onControllerLeave(callback) {
|
|
411
|
+
return this._addLifecycleListener("$controller-leave", callback);
|
|
412
|
+
}
|
|
413
|
+
onControllerDisconnect(callback) {
|
|
414
|
+
return this._addLifecycleListener("$controller-disconnect", callback);
|
|
415
|
+
}
|
|
416
|
+
onControllerReconnect(callback) {
|
|
417
|
+
return this._addLifecycleListener("$controller-reconnect", callback);
|
|
418
|
+
}
|
|
419
|
+
onCharacterUpdated(callback) {
|
|
420
|
+
return this._addLifecycleListener("$character-updated", callback);
|
|
421
|
+
}
|
|
422
|
+
onError(callback) {
|
|
423
|
+
return this._addLifecycleListener("$error", callback);
|
|
424
|
+
}
|
|
425
|
+
onConnectionChange(callback) {
|
|
426
|
+
return this._addLifecycleListener("$connection-change", callback);
|
|
427
|
+
}
|
|
428
|
+
onGameOver(callback) {
|
|
429
|
+
return this._addLifecycleListener("$game-over", callback);
|
|
430
|
+
}
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
322
432
|
// Communication Methods
|
|
323
433
|
// ---------------------------------------------------------------------------
|
|
324
434
|
/**
|
|
325
435
|
* Send an event to the Screen. Controller-to-Controller direct communication
|
|
326
436
|
* is not supported; all messages must go through the Screen.
|
|
327
437
|
*
|
|
328
|
-
* Data is sent to the Screen only (not to other controllers). For Screen
|
|
438
|
+
* Data is sent to the Screen only (not to other controllers). For Screen->Controller communication,
|
|
329
439
|
* Screen uses broadcast() or sendToController().
|
|
330
440
|
*
|
|
331
441
|
* @note Fire-and-forget sends (no callback) will silently fail if rate-limited.
|
|
332
442
|
* Use the onError callback or smore:rate-limited event to detect rate limiting.
|
|
333
443
|
*/
|
|
334
444
|
send(event, data) {
|
|
335
|
-
this.
|
|
445
|
+
if (this._isDestroyed) {
|
|
446
|
+
throw new SmoreSDKError("DESTROYED", "Cannot call send() after destroy()");
|
|
447
|
+
}
|
|
448
|
+
if (!this._isReady || !this.transport) {
|
|
449
|
+
this._outboundBuffer.push({ method: "send", args: [event, data] });
|
|
450
|
+
this.logger.debug(`Buffered send "${event}" (controller not ready yet)`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
336
453
|
validateEventName(event);
|
|
454
|
+
validatePayloadSize(data);
|
|
337
455
|
if (typeof data !== "object" || data === null) {
|
|
338
456
|
this.logger.warn(
|
|
339
457
|
'Event data should be an object. Primitive values will be wrapped as { data: value } by the relay server. To avoid confusion, wrap explicitly: send("event", { value: 42 }) instead of send("event", 42).'
|
|
@@ -342,12 +460,6 @@ class ControllerImpl {
|
|
|
342
460
|
this.logSend(event, data);
|
|
343
461
|
this.transport.emit(event, data);
|
|
344
462
|
}
|
|
345
|
-
sendRaw(event, data) {
|
|
346
|
-
this.ensureReady("sendRaw");
|
|
347
|
-
validateEventName(event);
|
|
348
|
-
this.logSend(event, data);
|
|
349
|
-
this.transport.emit(event, data);
|
|
350
|
-
}
|
|
351
463
|
signalReady() {
|
|
352
464
|
this.ensureReady("signalReady");
|
|
353
465
|
this.logSend(SMORE_EVENTS.GAME_READY, {});
|
|
@@ -359,26 +471,26 @@ class ControllerImpl {
|
|
|
359
471
|
/**
|
|
360
472
|
* Register a handler for custom events.
|
|
361
473
|
*
|
|
474
|
+
* Can be called before the Controller is ready. Handlers registered before ready
|
|
475
|
+
* are queued and activated when the transport becomes available.
|
|
476
|
+
*
|
|
362
477
|
* When receiving events from Screen's `broadcast()`:
|
|
363
|
-
* handler receives `(data)`
|
|
478
|
+
* handler receives `(data)` -- no playerIndex included.
|
|
364
479
|
*
|
|
365
480
|
* When receiving events from Screen's `sendToController()`:
|
|
366
|
-
* handler receives `(data)`
|
|
367
|
-
*
|
|
368
|
-
* @note Unlike Screen's `on()` which receives `(playerIndex, data)`,
|
|
369
|
-
* Controller's `on()` receives only `(data)` since there's only one player per controller.
|
|
370
|
-
*
|
|
371
|
-
* Controller's on() handler signature: (data) => void
|
|
372
|
-
* Unlike Screen's (playerIndex, data) => void, Controller doesn't receive playerIndex
|
|
373
|
-
* because Controller only receives events from Screen, not from other controllers.
|
|
374
|
-
* The sender is always the Screen, so playerIndex is not applicable.
|
|
375
|
-
*
|
|
376
|
-
* **Important:** If called before the Controller is ready (i.e., before `await createController()`
|
|
377
|
-
* resolves or before the `onReady` callback fires), the handler is stored locally but
|
|
378
|
-
* will NOT receive events until the transport is initialized. Always call `on()` after
|
|
379
|
-
* initialization completes, or use `config.listeners` for handlers needed from the start.
|
|
481
|
+
* handler receives `(data)` -- targeted to this specific controller.
|
|
380
482
|
*/
|
|
381
483
|
on(event, handler) {
|
|
484
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
485
|
+
const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
|
|
486
|
+
if (!validEvents.has(event)) {
|
|
487
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}". Valid lifecycle events: ${Array.from(validEvents).join(", ")}`);
|
|
488
|
+
}
|
|
489
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
490
|
+
handler();
|
|
491
|
+
}
|
|
492
|
+
return this._addLifecycleListener(event, handler);
|
|
493
|
+
}
|
|
382
494
|
validateEventName(event);
|
|
383
495
|
let listeners = this.eventListeners.get(event);
|
|
384
496
|
if (!listeners) {
|
|
@@ -386,34 +498,42 @@ class ControllerImpl {
|
|
|
386
498
|
this.eventListeners.set(event, listeners);
|
|
387
499
|
}
|
|
388
500
|
listeners.add(handler);
|
|
389
|
-
const transportHandler = (data) => {
|
|
390
|
-
this.logReceive(event, data);
|
|
391
|
-
try {
|
|
392
|
-
handler(data);
|
|
393
|
-
} catch (err) {
|
|
394
|
-
this.handleError(
|
|
395
|
-
new SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
|
|
396
|
-
cause: err instanceof Error ? err : void 0,
|
|
397
|
-
details: { event }
|
|
398
|
-
})
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
501
|
if (this.transport) {
|
|
502
|
+
const transportHandler = (data) => {
|
|
503
|
+
this.logReceive(event, data);
|
|
504
|
+
try {
|
|
505
|
+
handler(data);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
this.handleError(
|
|
508
|
+
new SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
|
|
509
|
+
cause: err instanceof Error ? err : void 0,
|
|
510
|
+
details: { event }
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
403
515
|
this.transport.on(event, transportHandler);
|
|
404
516
|
this.registeredHandlers.push({ event, handler: transportHandler });
|
|
405
517
|
this.handlerToTransport.set(handler, { event, transportHandler });
|
|
518
|
+
} else {
|
|
519
|
+
this._pendingHandlers.push({ event, handler });
|
|
406
520
|
}
|
|
407
521
|
return () => {
|
|
408
522
|
listeners?.delete(handler);
|
|
409
523
|
if (listeners?.size === 0) {
|
|
410
524
|
this.eventListeners.delete(event);
|
|
411
525
|
}
|
|
412
|
-
this.
|
|
413
|
-
|
|
414
|
-
(h) => h.handler !== transportHandler
|
|
526
|
+
this._pendingHandlers = this._pendingHandlers.filter(
|
|
527
|
+
(p) => !(p.event === event && p.handler === handler)
|
|
415
528
|
);
|
|
416
|
-
this.handlerToTransport.
|
|
529
|
+
const entry = this.handlerToTransport.get(handler);
|
|
530
|
+
if (entry) {
|
|
531
|
+
this.transport?.off(event, entry.transportHandler);
|
|
532
|
+
this.registeredHandlers = this.registeredHandlers.filter(
|
|
533
|
+
(h) => h.handler !== entry.transportHandler
|
|
534
|
+
);
|
|
535
|
+
this.handlerToTransport.delete(handler);
|
|
536
|
+
}
|
|
417
537
|
};
|
|
418
538
|
}
|
|
419
539
|
/**
|
|
@@ -421,18 +541,25 @@ class ControllerImpl {
|
|
|
421
541
|
*
|
|
422
542
|
* @note The handler is internally wrapped, so it cannot be removed via
|
|
423
543
|
* `off(event, originalHandler)`. Use the returned unsubscribe function instead.
|
|
424
|
-
*
|
|
425
|
-
* @example
|
|
426
|
-
* ```ts
|
|
427
|
-
* const unsubscribe = controller.once('game-start', (data) => {
|
|
428
|
-
* console.log('Game started!', data);
|
|
429
|
-
* });
|
|
430
|
-
*
|
|
431
|
-
* // To cancel before it fires:
|
|
432
|
-
* unsubscribe();
|
|
433
|
-
* ```
|
|
434
544
|
*/
|
|
435
545
|
once(event, handler) {
|
|
546
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
547
|
+
const validEvents = CONTROLLER_LIFECYCLE_EVENTS;
|
|
548
|
+
if (!validEvents.has(event)) {
|
|
549
|
+
throw new SmoreSDKError("INVALID_EVENT", `Unknown lifecycle event: "${event}"`);
|
|
550
|
+
}
|
|
551
|
+
if (event === "$all-ready" && this._allReadyFired) {
|
|
552
|
+
handler();
|
|
553
|
+
return () => {
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
const wrapper = (...args) => {
|
|
557
|
+
unsub();
|
|
558
|
+
handler(...args);
|
|
559
|
+
};
|
|
560
|
+
const unsub = this._addLifecycleListener(event, wrapper);
|
|
561
|
+
return unsub;
|
|
562
|
+
}
|
|
436
563
|
const unsubscribe = this.on(event, ((data) => {
|
|
437
564
|
unsubscribe();
|
|
438
565
|
handler(data);
|
|
@@ -440,25 +567,22 @@ class ControllerImpl {
|
|
|
440
567
|
return unsubscribe;
|
|
441
568
|
}
|
|
442
569
|
off(event, handler) {
|
|
443
|
-
if (
|
|
444
|
-
if (
|
|
445
|
-
|
|
446
|
-
if (val.event === event) {
|
|
447
|
-
this.transport?.off(event, val.transportHandler);
|
|
448
|
-
this.registeredHandlers = this.registeredHandlers.filter(
|
|
449
|
-
(h) => h.handler !== val.transportHandler
|
|
450
|
-
);
|
|
451
|
-
this.handlerToTransport.delete(key);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
570
|
+
if (typeof event === "string" && event.startsWith("$")) {
|
|
571
|
+
if (!handler) {
|
|
572
|
+
this._lifecycleListeners.delete(event);
|
|
454
573
|
} else {
|
|
455
|
-
this.
|
|
456
|
-
this.transport?.off(event);
|
|
457
|
-
this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
|
|
458
|
-
for (const [key, val] of this.handlerToTransport) {
|
|
459
|
-
if (val.event === event) this.handlerToTransport.delete(key);
|
|
460
|
-
}
|
|
574
|
+
this._lifecycleListeners.get(event)?.delete(handler);
|
|
461
575
|
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (!handler) {
|
|
579
|
+
this.eventListeners.delete(event);
|
|
580
|
+
this.transport?.off(event);
|
|
581
|
+
this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
|
|
582
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
583
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
584
|
+
}
|
|
585
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
462
586
|
} else {
|
|
463
587
|
const listeners = this.eventListeners.get(event);
|
|
464
588
|
listeners?.delete(handler);
|
|
@@ -473,6 +597,24 @@ class ControllerImpl {
|
|
|
473
597
|
);
|
|
474
598
|
this.handlerToTransport.delete(handler);
|
|
475
599
|
}
|
|
600
|
+
this._pendingHandlers = this._pendingHandlers.filter(
|
|
601
|
+
(p) => !(p.event === event && p.handler === handler)
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
removeAllListeners(event) {
|
|
606
|
+
if (event) {
|
|
607
|
+
this.eventListeners.delete(event);
|
|
608
|
+
this.transport?.off(event);
|
|
609
|
+
this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
|
|
610
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
611
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
612
|
+
}
|
|
613
|
+
this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
|
|
614
|
+
} else {
|
|
615
|
+
for (const evt of [...this.eventListeners.keys()]) {
|
|
616
|
+
this.removeAllListeners(evt);
|
|
617
|
+
}
|
|
476
618
|
}
|
|
477
619
|
}
|
|
478
620
|
// ---------------------------------------------------------------------------
|
|
@@ -481,10 +623,15 @@ class ControllerImpl {
|
|
|
481
623
|
destroy() {
|
|
482
624
|
if (this._isDestroyed) return;
|
|
483
625
|
this.logger.lifecycle("Destroying controller");
|
|
484
|
-
this.cleanup();
|
|
485
626
|
this._isDestroyed = true;
|
|
627
|
+
this._isReady = false;
|
|
628
|
+
this.cleanup();
|
|
486
629
|
}
|
|
487
630
|
cleanup() {
|
|
631
|
+
if (this._initTimeoutId) {
|
|
632
|
+
clearTimeout(this._initTimeoutId);
|
|
633
|
+
this._initTimeoutId = null;
|
|
634
|
+
}
|
|
488
635
|
this._isReady = false;
|
|
489
636
|
for (const { event, handler } of this.registeredHandlers) {
|
|
490
637
|
this.transport?.off(event, handler);
|
|
@@ -492,6 +639,10 @@ class ControllerImpl {
|
|
|
492
639
|
this.registeredHandlers = [];
|
|
493
640
|
this.eventListeners.clear();
|
|
494
641
|
this.handlerToTransport.clear();
|
|
642
|
+
this._pendingHandlers = [];
|
|
643
|
+
this._lifecycleListeners.clear();
|
|
644
|
+
this._isConnected = false;
|
|
645
|
+
this._outboundBuffer = [];
|
|
495
646
|
if (this.transport) {
|
|
496
647
|
this.transport.destroy();
|
|
497
648
|
this.transport = null;
|
|
@@ -515,15 +666,16 @@ class ControllerImpl {
|
|
|
515
666
|
if (!this._isReady || !this.transport) {
|
|
516
667
|
throw new SmoreSDKError(
|
|
517
668
|
"NOT_READY",
|
|
518
|
-
`Cannot call ${method}() before controller is ready. Use await
|
|
669
|
+
`Cannot call ${method}() before controller is ready. Use await controller.ready.`,
|
|
519
670
|
{ details: { method, isReady: this._isReady } }
|
|
520
671
|
);
|
|
521
672
|
}
|
|
522
673
|
}
|
|
523
674
|
handleError(error) {
|
|
524
675
|
this.logger.warn(`Error in handler: ${error.message}`);
|
|
525
|
-
|
|
526
|
-
|
|
676
|
+
const smoreError = error.toSmoreError();
|
|
677
|
+
if (this._hasLifecycleListeners("$error")) {
|
|
678
|
+
this._emitLifecycle("$error", smoreError);
|
|
527
679
|
} else {
|
|
528
680
|
this.logger.error(error.message, error.details);
|
|
529
681
|
}
|
|
@@ -536,10 +688,7 @@ class ControllerImpl {
|
|
|
536
688
|
}
|
|
537
689
|
}
|
|
538
690
|
function createController(config) {
|
|
539
|
-
|
|
540
|
-
const promise = controller.initialize().then(() => controller);
|
|
541
|
-
promise.instance = controller;
|
|
542
|
-
return promise;
|
|
691
|
+
return new ControllerImpl(config ?? {});
|
|
543
692
|
}
|
|
544
693
|
|
|
545
694
|
export { createController };
|