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