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