@smoregg/sdk 0.6.2 → 1.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/README.md +29 -38
- package/dist/cjs/controller.cjs +312 -144
- package/dist/cjs/controller.cjs.map +1 -1
- package/dist/cjs/errors.cjs +36 -0
- package/dist/cjs/errors.cjs.map +1 -0
- package/dist/cjs/events.cjs +43 -19
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/index.cjs +3 -8
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/logger.cjs +75 -0
- package/dist/cjs/logger.cjs.map +1 -0
- package/dist/cjs/screen.cjs +315 -215
- package/dist/cjs/screen.cjs.map +1 -1
- package/dist/cjs/testing.cjs +297 -22
- package/dist/cjs/testing.cjs.map +1 -1
- package/dist/cjs/transport/DirectTransport.cjs.map +1 -1
- package/dist/cjs/transport/PostMessageTransport.cjs +11 -6
- package/dist/cjs/transport/PostMessageTransport.cjs.map +1 -1
- package/dist/cjs/transport/protocol.cjs +25 -5
- package/dist/cjs/transport/protocol.cjs.map +1 -1
- package/dist/esm/controller.js +305 -136
- package/dist/esm/controller.js.map +1 -1
- package/dist/esm/errors.js +34 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/events.js +41 -18
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.js +3 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger.js +73 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/screen.js +303 -202
- package/dist/esm/screen.js.map +1 -1
- package/dist/esm/testing.js +297 -22
- package/dist/esm/testing.js.map +1 -1
- package/dist/esm/transport/DirectTransport.js.map +1 -1
- package/dist/esm/transport/PostMessageTransport.js +12 -7
- package/dist/esm/transport/PostMessageTransport.js.map +1 -1
- package/dist/esm/transport/protocol.js +23 -4
- package/dist/esm/transport/protocol.js.map +1 -1
- package/dist/types/controller.d.ts +1 -14
- package/dist/types/controller.d.ts.map +1 -1
- package/dist/types/errors.d.ts +45 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/events.d.ts +54 -12
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/logger.d.ts +35 -0
- package/dist/types/logger.d.ts.map +1 -0
- package/dist/types/screen.d.ts +1 -14
- package/dist/types/screen.d.ts.map +1 -1
- package/dist/types/testing.d.ts +0 -1
- package/dist/types/testing.d.ts.map +1 -1
- package/dist/types/transport/DirectTransport.d.ts +2 -1
- package/dist/types/transport/DirectTransport.d.ts.map +1 -1
- package/dist/types/transport/PostMessageTransport.d.ts +17 -2
- package/dist/types/transport/PostMessageTransport.d.ts.map +1 -1
- package/dist/types/transport/index.d.ts +2 -2
- package/dist/types/transport/index.d.ts.map +1 -1
- package/dist/types/transport/protocol.d.ts +71 -23
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/transport/types.d.ts +24 -2
- package/dist/types/transport/types.d.ts.map +1 -1
- package/dist/types/types.d.ts +373 -215
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +1011 -349
- 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 +8 -13
package/dist/esm/controller.js
CHANGED
|
@@ -1,103 +1,29 @@
|
|
|
1
1
|
import { PostMessageTransport } from './transport/PostMessageTransport.js';
|
|
2
|
-
import {
|
|
2
|
+
import { isBridgeMessage, validateInitPayload } from './transport/protocol.js';
|
|
3
|
+
import { SmoreSDKError } from './errors.js';
|
|
4
|
+
import { validateEventName, SMORE_EVENTS } from './events.js';
|
|
5
|
+
import { DebugLogger } from './logger.js';
|
|
3
6
|
|
|
4
|
-
const SYSTEM_PREFIX = "smore:";
|
|
5
|
-
const SYSTEM_EVENTS = {
|
|
6
|
-
PLAYER_JOIN: `${SYSTEM_PREFIX}player-join`,
|
|
7
|
-
PLAYER_LEAVE: `${SYSTEM_PREFIX}player-leave`
|
|
8
|
-
};
|
|
9
7
|
const DEFAULT_TIMEOUT = 1e4;
|
|
10
|
-
class SmoreSDKError extends Error {
|
|
11
|
-
code;
|
|
12
|
-
cause;
|
|
13
|
-
details;
|
|
14
|
-
constructor(code, message, options) {
|
|
15
|
-
super(message);
|
|
16
|
-
this.name = "SmoreSDKError";
|
|
17
|
-
this.code = code;
|
|
18
|
-
this.cause = options?.cause;
|
|
19
|
-
this.details = options?.details;
|
|
20
|
-
const ErrorWithCapture = Error;
|
|
21
|
-
if (typeof ErrorWithCapture.captureStackTrace === "function") {
|
|
22
|
-
ErrorWithCapture.captureStackTrace(this, SmoreSDKError);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
toSmoreError() {
|
|
26
|
-
return {
|
|
27
|
-
code: this.code,
|
|
28
|
-
message: this.message,
|
|
29
|
-
cause: this.cause,
|
|
30
|
-
details: this.details
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
const EVENT_NAME_REGEX = /^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
|
|
35
|
-
function validateEventName(event) {
|
|
36
|
-
if (!event || typeof event !== "string") {
|
|
37
|
-
throw new SmoreSDKError("INVALID_EVENT", "Event name must be a non-empty string");
|
|
38
|
-
}
|
|
39
|
-
if (!EVENT_NAME_REGEX.test(event)) {
|
|
40
|
-
throw new SmoreSDKError(
|
|
41
|
-
"INVALID_EVENT",
|
|
42
|
-
`Invalid event name "${event}". Event names must:
|
|
43
|
-
- Start with a letter (a-z, A-Z)
|
|
44
|
-
- Only contain letters, numbers, hyphens (-), and underscores (_)
|
|
45
|
-
- End with a letter or number`,
|
|
46
|
-
{ details: { event } }
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function createLogger(options) {
|
|
51
|
-
const enabled = typeof options === "boolean" ? options : options?.enabled ?? false;
|
|
52
|
-
const level = (typeof options === "object" ? options.level : void 0) ?? "debug";
|
|
53
|
-
const prefix = (typeof options === "object" ? options.prefix : void 0) ?? "[SmoreController]";
|
|
54
|
-
const customLogger = typeof options === "object" ? options.logger : void 0;
|
|
55
|
-
const levelPriority = {
|
|
56
|
-
debug: 0,
|
|
57
|
-
info: 1,
|
|
58
|
-
warn: 2,
|
|
59
|
-
error: 3
|
|
60
|
-
};
|
|
61
|
-
const shouldLog = (msgLevel) => {
|
|
62
|
-
if (!enabled) return false;
|
|
63
|
-
return levelPriority[msgLevel] >= levelPriority[level];
|
|
64
|
-
};
|
|
65
|
-
const log = (msgLevel, message, data) => {
|
|
66
|
-
if (!shouldLog(msgLevel)) return;
|
|
67
|
-
if (customLogger) {
|
|
68
|
-
customLogger(msgLevel, message, data);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
const fullMessage = `${prefix} ${message}`;
|
|
72
|
-
const consoleFn = console[msgLevel] ?? console.log;
|
|
73
|
-
if (data !== void 0) {
|
|
74
|
-
consoleFn(fullMessage, data);
|
|
75
|
-
} else {
|
|
76
|
-
consoleFn(fullMessage);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
return {
|
|
80
|
-
debug: (msg, data) => log("debug", msg, data),
|
|
81
|
-
info: (msg, data) => log("info", msg, data),
|
|
82
|
-
warn: (msg, data) => log("warn", msg, data),
|
|
83
|
-
error: (msg, data) => log("error", msg, data)
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
8
|
class ControllerImpl {
|
|
87
9
|
transport = null;
|
|
88
10
|
config;
|
|
89
11
|
logger;
|
|
90
12
|
_roomCode = "";
|
|
91
13
|
_myIndex = -1;
|
|
92
|
-
_isLeader = false;
|
|
93
14
|
_isReady = false;
|
|
94
15
|
_isDestroyed = false;
|
|
95
16
|
boundMessageHandler = null;
|
|
96
17
|
registeredHandlers = [];
|
|
97
18
|
eventListeners = /* @__PURE__ */ new Map();
|
|
19
|
+
// Maps user-facing handler -> transport wrappedHandler for proper cleanup in on()/off()
|
|
20
|
+
handlerToTransport = /* @__PURE__ */ new Map();
|
|
21
|
+
_controllers = [];
|
|
22
|
+
// Tracks event names registered via config.listeners so off(event) without handler won't remove them
|
|
23
|
+
_configListenerEvents = /* @__PURE__ */ new Set();
|
|
98
24
|
constructor(config = {}) {
|
|
99
25
|
this.config = config;
|
|
100
|
-
this.logger =
|
|
26
|
+
this.logger = new DebugLogger(config.debug, "[SmoreController]");
|
|
101
27
|
if (config.listeners) {
|
|
102
28
|
for (const event of Object.keys(config.listeners)) {
|
|
103
29
|
validateEventName(event);
|
|
@@ -110,9 +36,6 @@ class ControllerImpl {
|
|
|
110
36
|
get myIndex() {
|
|
111
37
|
return this._myIndex;
|
|
112
38
|
}
|
|
113
|
-
get isLeader() {
|
|
114
|
-
return this._isLeader;
|
|
115
|
-
}
|
|
116
39
|
get roomCode() {
|
|
117
40
|
return this._roomCode;
|
|
118
41
|
}
|
|
@@ -122,19 +45,35 @@ class ControllerImpl {
|
|
|
122
45
|
get isDestroyed() {
|
|
123
46
|
return this._isDestroyed;
|
|
124
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Read-only list of all known controllers (players) in the room.
|
|
50
|
+
* Returns full ControllerInfo including playerIndex, nickname, connected status, and appearance.
|
|
51
|
+
*
|
|
52
|
+
* Returns a new shallow copy on every access. Cache the result if accessing
|
|
53
|
+
* repeatedly in the same frame/tick.
|
|
54
|
+
*/
|
|
55
|
+
get controllers() {
|
|
56
|
+
return [...this._controllers];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Returns the number of currently connected players.
|
|
60
|
+
*/
|
|
61
|
+
getControllerCount() {
|
|
62
|
+
return this._controllers.filter((c) => c.connected).length;
|
|
63
|
+
}
|
|
125
64
|
// ---------------------------------------------------------------------------
|
|
126
65
|
// Initialization
|
|
127
66
|
// ---------------------------------------------------------------------------
|
|
128
67
|
async initialize() {
|
|
129
68
|
const parentOrigin = this.config.parentOrigin ?? "*";
|
|
130
69
|
const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
|
|
131
|
-
this.logger.
|
|
70
|
+
this.logger.lifecycle("Initializing controller...", { parentOrigin, timeout });
|
|
132
71
|
return new Promise((resolve, reject) => {
|
|
133
72
|
const timeoutId = setTimeout(() => {
|
|
134
73
|
this.cleanup();
|
|
135
74
|
const error = new SmoreSDKError(
|
|
136
75
|
"TIMEOUT",
|
|
137
|
-
`Controller initialization timed out after ${timeout}ms. Make sure the parent window sends
|
|
76
|
+
`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).`,
|
|
138
77
|
{ details: { timeout } }
|
|
139
78
|
);
|
|
140
79
|
this.handleError(error);
|
|
@@ -143,22 +82,36 @@ class ControllerImpl {
|
|
|
143
82
|
this.boundMessageHandler = (e) => {
|
|
144
83
|
if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
|
|
145
84
|
const msg = e.data;
|
|
146
|
-
if (!
|
|
147
|
-
if (msg.type === "
|
|
85
|
+
if (!isBridgeMessage(msg)) return;
|
|
86
|
+
if (msg.type === "_bridge:init") {
|
|
148
87
|
clearTimeout(timeoutId);
|
|
149
88
|
this.handleInit(msg, parentOrigin, resolve, reject);
|
|
150
|
-
} else if (msg.type === "
|
|
89
|
+
} else if (msg.type === "_bridge:update") {
|
|
151
90
|
this.handleUpdate(msg);
|
|
152
91
|
}
|
|
153
92
|
};
|
|
154
93
|
window.addEventListener("message", this.boundMessageHandler);
|
|
155
|
-
this.logger.
|
|
156
|
-
window.parent.postMessage({ type: "
|
|
94
|
+
this.logger.lifecycle("Sending _bridge:ready to parent");
|
|
95
|
+
window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
|
|
157
96
|
});
|
|
158
97
|
}
|
|
159
98
|
handleInit(msg, parentOrigin, resolve, reject) {
|
|
160
|
-
const
|
|
161
|
-
this.logger.debug("Received
|
|
99
|
+
const initPayload = msg.payload;
|
|
100
|
+
this.logger.debug("Received _bridge:init", initPayload);
|
|
101
|
+
try {
|
|
102
|
+
validateInitPayload(initPayload);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const error = new SmoreSDKError(
|
|
105
|
+
"INIT_FAILED",
|
|
106
|
+
`Invalid _bridge:init payload: ${err instanceof Error ? err.message : String(err)}`,
|
|
107
|
+
{ details: { payload: initPayload } }
|
|
108
|
+
);
|
|
109
|
+
this.logger.warn("_bridge:init validation failed", error);
|
|
110
|
+
this.handleError(error);
|
|
111
|
+
reject(error);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const initData = initPayload;
|
|
162
115
|
if (initData.side !== "player") {
|
|
163
116
|
const error = new SmoreSDKError(
|
|
164
117
|
"INIT_FAILED",
|
|
@@ -182,49 +135,179 @@ class ControllerImpl {
|
|
|
182
135
|
this.transport = new PostMessageTransport(parentOrigin);
|
|
183
136
|
this._roomCode = initData.roomCode;
|
|
184
137
|
this._myIndex = initData.myIndex;
|
|
185
|
-
|
|
138
|
+
const initPlayers = initData.players;
|
|
139
|
+
this._controllers = initPlayers.filter((p) => typeof p.playerIndex === "number").map((p) => ({
|
|
140
|
+
playerIndex: p.playerIndex,
|
|
141
|
+
nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
|
|
142
|
+
connected: p.connected !== false,
|
|
143
|
+
appearance: p.appearance ?? p.character
|
|
144
|
+
}));
|
|
186
145
|
this.setupEventHandlers();
|
|
187
146
|
this._isReady = true;
|
|
188
|
-
this.logger.
|
|
147
|
+
this.logger.lifecycle("Controller ready", {
|
|
189
148
|
roomCode: this._roomCode,
|
|
190
|
-
myIndex: this._myIndex
|
|
191
|
-
isLeader: this._isLeader
|
|
149
|
+
myIndex: this._myIndex
|
|
192
150
|
});
|
|
193
151
|
this.config.onReady?.();
|
|
152
|
+
if (this.config.autoReady !== false) {
|
|
153
|
+
this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
|
|
154
|
+
this.signalReady();
|
|
155
|
+
}
|
|
194
156
|
resolve();
|
|
195
157
|
}
|
|
196
158
|
handleUpdate(msg) {
|
|
159
|
+
if (!this._isReady) {
|
|
160
|
+
this.logger.debug("Ignoring _bridge:update before init completes");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
197
163
|
const updateData = msg.payload;
|
|
198
|
-
this.logger.debug("Received
|
|
164
|
+
this.logger.debug("Received _bridge:update", updateData);
|
|
165
|
+
if (updateData.players && Array.isArray(updateData.players)) {
|
|
166
|
+
const players = updateData.players;
|
|
167
|
+
const newControllers = players.filter((p) => typeof p.playerIndex === "number").map((p) => ({
|
|
168
|
+
playerIndex: p.playerIndex,
|
|
169
|
+
nickname: p.nickname || p.name || `Player ${p.playerIndex + 1}`,
|
|
170
|
+
connected: p.connected !== false,
|
|
171
|
+
appearance: p.appearance ?? p.character
|
|
172
|
+
}));
|
|
173
|
+
const oldControllers = this._controllers;
|
|
174
|
+
for (const nc of newControllers) {
|
|
175
|
+
if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
|
|
176
|
+
this.config.onControllerJoin?.(nc.playerIndex, nc);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const oc of oldControllers) {
|
|
180
|
+
if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
|
|
181
|
+
this.config.onControllerLeave?.(oc.playerIndex);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
for (const nc of newControllers) {
|
|
185
|
+
const oc = oldControllers.find((c) => c.playerIndex === nc.playerIndex);
|
|
186
|
+
if (oc) {
|
|
187
|
+
if (oc.connected && !nc.connected) {
|
|
188
|
+
this.logger.debug("Player disconnected (via update)", { playerIndex: nc.playerIndex });
|
|
189
|
+
this.config.onControllerDisconnect?.(nc.playerIndex);
|
|
190
|
+
}
|
|
191
|
+
if (!oc.connected && nc.connected) {
|
|
192
|
+
this.logger.debug("Player reconnected (via update)", { playerIndex: nc.playerIndex });
|
|
193
|
+
this.config.onControllerReconnect?.(nc.playerIndex, nc);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this._controllers = newControllers;
|
|
198
|
+
}
|
|
199
199
|
}
|
|
200
200
|
setupEventHandlers() {
|
|
201
201
|
if (!this.transport) return;
|
|
202
|
-
this.registerHandler(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
202
|
+
this.registerHandler(SMORE_EVENTS.PLAYER_JOINED, (raw) => {
|
|
203
|
+
const data = raw;
|
|
204
|
+
const playerInfo = data.player;
|
|
205
|
+
const playerIndex = playerInfo?.playerIndex ?? data.playerIndex;
|
|
206
|
+
if (playerIndex !== void 0) {
|
|
207
|
+
if (this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
208
|
+
const controllerInfo = playerInfo ? {
|
|
209
|
+
playerIndex,
|
|
210
|
+
nickname: playerInfo.nickname || playerInfo.name || `Player ${playerIndex + 1}`,
|
|
211
|
+
connected: playerInfo.connected !== false,
|
|
212
|
+
appearance: playerInfo.appearance ?? playerInfo.character
|
|
213
|
+
} : {
|
|
214
|
+
playerIndex,
|
|
215
|
+
nickname: `Player ${playerIndex + 1}`,
|
|
216
|
+
connected: true
|
|
217
|
+
};
|
|
218
|
+
this._controllers = [...this._controllers, controllerInfo];
|
|
219
|
+
this.logger.debug("Player joined", { playerIndex });
|
|
220
|
+
this.config.onControllerJoin?.(playerIndex, controllerInfo);
|
|
210
221
|
}
|
|
211
|
-
);
|
|
212
|
-
this.registerHandler(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (playerIndex
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
});
|
|
223
|
+
this.registerHandler(SMORE_EVENTS.PLAYER_LEFT, (raw) => {
|
|
224
|
+
const data = raw;
|
|
225
|
+
const playerIndex = data.player?.playerIndex ?? data.playerIndex;
|
|
226
|
+
if (playerIndex !== void 0) {
|
|
227
|
+
if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
|
|
228
|
+
this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
|
|
229
|
+
this.logger.debug("Player left", { playerIndex });
|
|
230
|
+
this.config.onControllerLeave?.(playerIndex);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
this.registerHandler(SMORE_EVENTS.PLAYER_DISCONNECTED, (raw) => {
|
|
234
|
+
const data = raw;
|
|
235
|
+
const playerData = data.player;
|
|
236
|
+
const playerIndex = playerData?.playerIndex ?? data.playerIndex;
|
|
237
|
+
if (playerIndex !== void 0) {
|
|
238
|
+
this._controllers = this._controllers.map(
|
|
239
|
+
(c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
|
|
240
|
+
);
|
|
241
|
+
this.logger.debug("Player disconnected", { playerIndex });
|
|
242
|
+
this.config.onControllerDisconnect?.(playerIndex);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
this.registerHandler(SMORE_EVENTS.PLAYER_RECONNECTED, (raw) => {
|
|
246
|
+
const data = raw;
|
|
247
|
+
const playerData = data.player;
|
|
248
|
+
const playerIndex = playerData?.playerIndex ?? data.playerIndex;
|
|
249
|
+
if (playerIndex !== void 0) {
|
|
250
|
+
const controllerInfo = playerData ? {
|
|
251
|
+
playerIndex,
|
|
252
|
+
nickname: playerData.nickname || playerData.name || `Player ${playerIndex + 1}`,
|
|
253
|
+
connected: true,
|
|
254
|
+
appearance: playerData.appearance ?? playerData.character
|
|
255
|
+
} : {
|
|
256
|
+
playerIndex,
|
|
257
|
+
nickname: `Player ${playerIndex + 1}`,
|
|
258
|
+
connected: true
|
|
259
|
+
};
|
|
260
|
+
this._controllers = this._controllers.map(
|
|
261
|
+
(c) => c.playerIndex === playerIndex ? controllerInfo : c
|
|
262
|
+
);
|
|
263
|
+
this.logger.debug("Player reconnected", { playerIndex });
|
|
264
|
+
this.config.onControllerReconnect?.(playerIndex, controllerInfo);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
this.registerHandler(SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (raw) => {
|
|
268
|
+
const payload = raw;
|
|
269
|
+
const playerData = payload?.player;
|
|
270
|
+
if (playerData && typeof playerData.playerIndex === "number") {
|
|
271
|
+
const appearance = playerData.character ?? null;
|
|
272
|
+
this._controllers = this._controllers.map(
|
|
273
|
+
(c) => c.playerIndex === playerData.playerIndex ? { ...c, appearance } : c
|
|
274
|
+
);
|
|
275
|
+
this.logger.debug("Player character updated", { playerIndex: playerData.playerIndex });
|
|
276
|
+
this.config.onCharacterUpdated?.(playerData.playerIndex, appearance ?? null);
|
|
220
277
|
}
|
|
221
|
-
);
|
|
278
|
+
});
|
|
279
|
+
this.registerHandler(SMORE_EVENTS.RATE_LIMITED, (raw) => {
|
|
280
|
+
const data = raw;
|
|
281
|
+
const event = data?.event ?? "unknown";
|
|
282
|
+
this.logger.warn(`Rate limited: ${event}`);
|
|
283
|
+
this.config.onRateLimited?.(event);
|
|
284
|
+
});
|
|
285
|
+
this.registerHandler(SMORE_EVENTS.ALL_READY, () => {
|
|
286
|
+
this.logger.lifecycle("All participants ready");
|
|
287
|
+
this.config.onAllReady?.();
|
|
288
|
+
});
|
|
289
|
+
if (this.config.onHostDisconnect) {
|
|
290
|
+
this.logger.warn("onHostDisconnect is reserved for future use and currently non-functional");
|
|
291
|
+
}
|
|
292
|
+
if (this.config.onHostReconnect) {
|
|
293
|
+
this.logger.warn("onHostReconnect is reserved for future use and currently non-functional");
|
|
294
|
+
}
|
|
222
295
|
if (this.config.listeners) {
|
|
223
296
|
for (const [event, handler] of Object.entries(this.config.listeners)) {
|
|
224
297
|
if (!handler) continue;
|
|
298
|
+
this._configListenerEvents.add(event);
|
|
225
299
|
this.registerHandler(event, (data) => {
|
|
226
300
|
this.logReceive(event, data);
|
|
227
|
-
|
|
301
|
+
try {
|
|
302
|
+
handler(data);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
this.handleError(
|
|
305
|
+
new SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
|
|
306
|
+
cause: err instanceof Error ? err : void 0,
|
|
307
|
+
details: { event }
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
}
|
|
228
311
|
});
|
|
229
312
|
}
|
|
230
313
|
}
|
|
@@ -237,9 +320,24 @@ class ControllerImpl {
|
|
|
237
320
|
// ---------------------------------------------------------------------------
|
|
238
321
|
// Communication Methods
|
|
239
322
|
// ---------------------------------------------------------------------------
|
|
323
|
+
/**
|
|
324
|
+
* Send an event to the Screen. Controller-to-Controller direct communication
|
|
325
|
+
* is not supported; all messages must go through the Screen.
|
|
326
|
+
*
|
|
327
|
+
* Data is sent to the Screen only (not to other controllers). For Screen→Controller communication,
|
|
328
|
+
* Screen uses broadcast() or sendToController().
|
|
329
|
+
*
|
|
330
|
+
* @note Fire-and-forget sends (no callback) will silently fail if rate-limited.
|
|
331
|
+
* Use the onError callback or smore:rate-limited event to detect rate limiting.
|
|
332
|
+
*/
|
|
240
333
|
send(event, data) {
|
|
241
334
|
this.ensureReady("send");
|
|
242
335
|
validateEventName(event);
|
|
336
|
+
if (typeof data !== "object" || data === null) {
|
|
337
|
+
this.logger.warn(
|
|
338
|
+
'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).'
|
|
339
|
+
);
|
|
340
|
+
}
|
|
243
341
|
this.logSend(event, data);
|
|
244
342
|
this.transport.emit(event, data);
|
|
245
343
|
}
|
|
@@ -249,9 +347,36 @@ class ControllerImpl {
|
|
|
249
347
|
this.logSend(event, data);
|
|
250
348
|
this.transport.emit(event, data);
|
|
251
349
|
}
|
|
350
|
+
signalReady() {
|
|
351
|
+
this.ensureReady("signalReady");
|
|
352
|
+
this.logSend(SMORE_EVENTS.GAME_READY, {});
|
|
353
|
+
this.transport.emit(SMORE_EVENTS.GAME_READY, {});
|
|
354
|
+
}
|
|
252
355
|
// ---------------------------------------------------------------------------
|
|
253
356
|
// Event Subscription
|
|
254
357
|
// ---------------------------------------------------------------------------
|
|
358
|
+
/**
|
|
359
|
+
* Register a handler for custom events.
|
|
360
|
+
*
|
|
361
|
+
* When receiving events from Screen's `broadcast()`:
|
|
362
|
+
* handler receives `(data)` — no playerIndex included.
|
|
363
|
+
*
|
|
364
|
+
* When receiving events from Screen's `sendToController()`:
|
|
365
|
+
* handler receives `(data)` — targeted to this specific controller.
|
|
366
|
+
*
|
|
367
|
+
* @note Unlike Screen's `on()` which receives `(playerIndex, data)`,
|
|
368
|
+
* Controller's `on()` receives only `(data)` since there's only one player per controller.
|
|
369
|
+
*
|
|
370
|
+
* Controller's on() handler signature: (data) => void
|
|
371
|
+
* Unlike Screen's (playerIndex, data) => void, Controller doesn't receive playerIndex
|
|
372
|
+
* because Controller only receives events from Screen, not from other controllers.
|
|
373
|
+
* The sender is always the Screen, so playerIndex is not applicable.
|
|
374
|
+
*
|
|
375
|
+
* **Important:** If called before the Controller is ready (i.e., before `await createController()`
|
|
376
|
+
* resolves or before the `onReady` callback fires), the handler is stored locally but
|
|
377
|
+
* will NOT receive events until the transport is initialized. Always call `on()` after
|
|
378
|
+
* initialization completes, or use `config.listeners` for handlers needed from the start.
|
|
379
|
+
*/
|
|
255
380
|
on(event, handler) {
|
|
256
381
|
validateEventName(event);
|
|
257
382
|
let listeners = this.eventListeners.get(event);
|
|
@@ -262,11 +387,21 @@ class ControllerImpl {
|
|
|
262
387
|
listeners.add(handler);
|
|
263
388
|
const transportHandler = (data) => {
|
|
264
389
|
this.logReceive(event, data);
|
|
265
|
-
|
|
390
|
+
try {
|
|
391
|
+
handler(data);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
this.handleError(
|
|
394
|
+
new SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
|
|
395
|
+
cause: err instanceof Error ? err : void 0,
|
|
396
|
+
details: { event }
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
}
|
|
266
400
|
};
|
|
267
401
|
if (this.transport) {
|
|
268
402
|
this.transport.on(event, transportHandler);
|
|
269
403
|
this.registeredHandlers.push({ event, handler: transportHandler });
|
|
404
|
+
this.handlerToTransport.set(handler, { event, transportHandler });
|
|
270
405
|
}
|
|
271
406
|
return () => {
|
|
272
407
|
listeners?.delete(handler);
|
|
@@ -277,8 +412,25 @@ class ControllerImpl {
|
|
|
277
412
|
this.registeredHandlers = this.registeredHandlers.filter(
|
|
278
413
|
(h) => h.handler !== transportHandler
|
|
279
414
|
);
|
|
415
|
+
this.handlerToTransport.delete(handler);
|
|
280
416
|
};
|
|
281
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Add a one-time listener that auto-removes after first call.
|
|
420
|
+
*
|
|
421
|
+
* @note The handler is internally wrapped, so it cannot be removed via
|
|
422
|
+
* `off(event, originalHandler)`. Use the returned unsubscribe function instead.
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```ts
|
|
426
|
+
* const unsubscribe = controller.once('game-start', (data) => {
|
|
427
|
+
* console.log('Game started!', data);
|
|
428
|
+
* });
|
|
429
|
+
*
|
|
430
|
+
* // To cancel before it fires:
|
|
431
|
+
* unsubscribe();
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
282
434
|
once(event, handler) {
|
|
283
435
|
const unsubscribe = this.on(event, ((data) => {
|
|
284
436
|
unsubscribe();
|
|
@@ -288,15 +440,38 @@ class ControllerImpl {
|
|
|
288
440
|
}
|
|
289
441
|
off(event, handler) {
|
|
290
442
|
if (!handler) {
|
|
291
|
-
this.
|
|
292
|
-
|
|
293
|
-
|
|
443
|
+
if (this._configListenerEvents.has(event)) {
|
|
444
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
445
|
+
if (val.event === event) {
|
|
446
|
+
this.transport?.off(event, val.transportHandler);
|
|
447
|
+
this.registeredHandlers = this.registeredHandlers.filter(
|
|
448
|
+
(h) => h.handler !== val.transportHandler
|
|
449
|
+
);
|
|
450
|
+
this.handlerToTransport.delete(key);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
this.eventListeners.delete(event);
|
|
455
|
+
this.transport?.off(event);
|
|
456
|
+
this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
|
|
457
|
+
for (const [key, val] of this.handlerToTransport) {
|
|
458
|
+
if (val.event === event) this.handlerToTransport.delete(key);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
294
461
|
} else {
|
|
295
462
|
const listeners = this.eventListeners.get(event);
|
|
296
463
|
listeners?.delete(handler);
|
|
297
464
|
if (listeners?.size === 0) {
|
|
298
465
|
this.eventListeners.delete(event);
|
|
299
466
|
}
|
|
467
|
+
const entry = this.handlerToTransport.get(handler);
|
|
468
|
+
if (entry) {
|
|
469
|
+
this.transport?.off(event, entry.transportHandler);
|
|
470
|
+
this.registeredHandlers = this.registeredHandlers.filter(
|
|
471
|
+
(h) => h.handler !== entry.transportHandler
|
|
472
|
+
);
|
|
473
|
+
this.handlerToTransport.delete(handler);
|
|
474
|
+
}
|
|
300
475
|
}
|
|
301
476
|
}
|
|
302
477
|
// ---------------------------------------------------------------------------
|
|
@@ -304,7 +479,7 @@ class ControllerImpl {
|
|
|
304
479
|
// ---------------------------------------------------------------------------
|
|
305
480
|
destroy() {
|
|
306
481
|
if (this._isDestroyed) return;
|
|
307
|
-
this.logger.
|
|
482
|
+
this.logger.lifecycle("Destroying controller");
|
|
308
483
|
this.cleanup();
|
|
309
484
|
this._isDestroyed = true;
|
|
310
485
|
}
|
|
@@ -315,6 +490,7 @@ class ControllerImpl {
|
|
|
315
490
|
}
|
|
316
491
|
this.registeredHandlers = [];
|
|
317
492
|
this.eventListeners.clear();
|
|
493
|
+
this.handlerToTransport.clear();
|
|
318
494
|
if (this.transport) {
|
|
319
495
|
this.transport.destroy();
|
|
320
496
|
this.transport = null;
|
|
@@ -344,6 +520,7 @@ class ControllerImpl {
|
|
|
344
520
|
}
|
|
345
521
|
}
|
|
346
522
|
handleError(error) {
|
|
523
|
+
this.logger.warn(`Error in handler: ${error.message}`);
|
|
347
524
|
if (this.config.onError) {
|
|
348
525
|
this.config.onError(error.toSmoreError());
|
|
349
526
|
} else {
|
|
@@ -351,18 +528,10 @@ class ControllerImpl {
|
|
|
351
528
|
}
|
|
352
529
|
}
|
|
353
530
|
logSend(event, data) {
|
|
354
|
-
|
|
355
|
-
const shouldLog = typeof options === "object" ? options.logSend ?? true : Boolean(options);
|
|
356
|
-
if (shouldLog) {
|
|
357
|
-
this.logger.debug(`\u2192 SEND [${event}]`, data);
|
|
358
|
-
}
|
|
531
|
+
this.logger.send(event, data);
|
|
359
532
|
}
|
|
360
533
|
logReceive(event, data) {
|
|
361
|
-
|
|
362
|
-
const shouldLog = typeof options === "object" ? options.logReceive ?? true : Boolean(options);
|
|
363
|
-
if (shouldLog) {
|
|
364
|
-
this.logger.debug(`\u2190 RECV [${event}]`, data);
|
|
365
|
-
}
|
|
534
|
+
this.logger.receive(event, data);
|
|
366
535
|
}
|
|
367
536
|
}
|
|
368
537
|
function createController(config) {
|
|
@@ -372,5 +541,5 @@ function createController(config) {
|
|
|
372
541
|
return promise;
|
|
373
542
|
}
|
|
374
543
|
|
|
375
|
-
export {
|
|
544
|
+
export { createController };
|
|
376
545
|
//# sourceMappingURL=controller.js.map
|