@smoregg/sdk 1.2.0 → 2.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.
Files changed (179) hide show
  1. package/dist/cjs/config.cjs.map +1 -1
  2. package/dist/cjs/controller.cjs +215 -145
  3. package/dist/cjs/controller.cjs.map +1 -1
  4. package/dist/cjs/screen.cjs +220 -178
  5. package/dist/cjs/screen.cjs.map +1 -1
  6. package/dist/cjs/testing.cjs +160 -151
  7. package/dist/cjs/testing.cjs.map +1 -1
  8. package/dist/esm/config.js.map +1 -1
  9. package/dist/esm/controller.js +216 -146
  10. package/dist/esm/controller.js.map +1 -1
  11. package/dist/esm/screen.js +221 -179
  12. package/dist/esm/screen.js.map +1 -1
  13. package/dist/esm/testing.js +160 -151
  14. package/dist/esm/testing.js.map +1 -1
  15. package/dist/types/config.d.ts +1 -2
  16. package/dist/types/config.d.ts.map +1 -1
  17. package/dist/types/controller.d.ts +22 -43
  18. package/dist/types/controller.d.ts.map +1 -1
  19. package/dist/types/index.d.ts +14 -14
  20. package/dist/types/index.d.ts.map +1 -1
  21. package/dist/types/screen.d.ts +26 -37
  22. package/dist/types/screen.d.ts.map +1 -1
  23. package/dist/types/testing.d.ts +16 -0
  24. package/dist/types/testing.d.ts.map +1 -1
  25. package/dist/types/types.d.ts +244 -338
  26. package/dist/types/types.d.ts.map +1 -1
  27. package/dist/umd/smore-sdk.umd.js +595 -474
  28. package/dist/umd/smore-sdk.umd.js.map +1 -1
  29. package/dist/umd/smore-sdk.umd.min.js +1 -1
  30. package/dist/umd/smore-sdk.umd.min.js.map +1 -1
  31. package/package.json +1 -1
  32. package/dist/cjs/SmoreHost.cjs +0 -306
  33. package/dist/cjs/SmoreHost.cjs.map +0 -1
  34. package/dist/cjs/SmorePlayer.cjs +0 -229
  35. package/dist/cjs/SmorePlayer.cjs.map +0 -1
  36. package/dist/cjs/components/DirectionPad.cjs +0 -68
  37. package/dist/cjs/components/DirectionPad.cjs.map +0 -1
  38. package/dist/cjs/components/DirectionPad.module.css.cjs +0 -12
  39. package/dist/cjs/components/DirectionPad.module.css.cjs.map +0 -1
  40. package/dist/cjs/components/HoldButton.cjs +0 -57
  41. package/dist/cjs/components/HoldButton.cjs.map +0 -1
  42. package/dist/cjs/components/HoldButton.module.css.cjs +0 -12
  43. package/dist/cjs/components/HoldButton.module.css.cjs.map +0 -1
  44. package/dist/cjs/components/IframeGameBridge.cjs +0 -115
  45. package/dist/cjs/components/IframeGameBridge.cjs.map +0 -1
  46. package/dist/cjs/components/SwipeArea.cjs +0 -58
  47. package/dist/cjs/components/SwipeArea.cjs.map +0 -1
  48. package/dist/cjs/components/SwipeArea.module.css.cjs +0 -12
  49. package/dist/cjs/components/SwipeArea.module.css.cjs.map +0 -1
  50. package/dist/cjs/components/TapButton.cjs +0 -58
  51. package/dist/cjs/components/TapButton.cjs.map +0 -1
  52. package/dist/cjs/components/TapButton.module.css.cjs +0 -12
  53. package/dist/cjs/components/TapButton.module.css.cjs.map +0 -1
  54. package/dist/cjs/context/RoomProvider.cjs +0 -118
  55. package/dist/cjs/context/RoomProvider.cjs.map +0 -1
  56. package/dist/cjs/hooks/useExternalGames.cjs +0 -49
  57. package/dist/cjs/hooks/useExternalGames.cjs.map +0 -1
  58. package/dist/cjs/hooks/useGameHost.cjs +0 -206
  59. package/dist/cjs/hooks/useGameHost.cjs.map +0 -1
  60. package/dist/cjs/hooks/useGamePlayer.cjs +0 -134
  61. package/dist/cjs/hooks/useGamePlayer.cjs.map +0 -1
  62. package/dist/cjs/iframe/index.cjs +0 -260
  63. package/dist/cjs/iframe/index.cjs.map +0 -1
  64. package/dist/cjs/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.cjs +0 -33
  65. package/dist/cjs/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.cjs.map +0 -1
  66. package/dist/cjs/server/index.cjs +0 -45
  67. package/dist/cjs/server/index.cjs.map +0 -1
  68. package/dist/cjs/transport/DirectTransport.cjs +0 -23
  69. package/dist/cjs/transport/DirectTransport.cjs.map +0 -1
  70. package/dist/cjs/utils/connectionMonitor.cjs +0 -77
  71. package/dist/cjs/utils/connectionMonitor.cjs.map +0 -1
  72. package/dist/cjs/utils/preloadAssets.cjs +0 -66
  73. package/dist/cjs/utils/preloadAssets.cjs.map +0 -1
  74. package/dist/cjs/utils/serverTime.cjs +0 -43
  75. package/dist/cjs/utils/serverTime.cjs.map +0 -1
  76. package/dist/esm/SmoreHost.js +0 -304
  77. package/dist/esm/SmoreHost.js.map +0 -1
  78. package/dist/esm/SmorePlayer.js +0 -227
  79. package/dist/esm/SmorePlayer.js.map +0 -1
  80. package/dist/esm/components/DirectionPad.js +0 -66
  81. package/dist/esm/components/DirectionPad.js.map +0 -1
  82. package/dist/esm/components/DirectionPad.module.css.js +0 -8
  83. package/dist/esm/components/DirectionPad.module.css.js.map +0 -1
  84. package/dist/esm/components/HoldButton.js +0 -55
  85. package/dist/esm/components/HoldButton.js.map +0 -1
  86. package/dist/esm/components/HoldButton.module.css.js +0 -8
  87. package/dist/esm/components/HoldButton.module.css.js.map +0 -1
  88. package/dist/esm/components/IframeGameBridge.js +0 -113
  89. package/dist/esm/components/IframeGameBridge.js.map +0 -1
  90. package/dist/esm/components/SwipeArea.js +0 -56
  91. package/dist/esm/components/SwipeArea.js.map +0 -1
  92. package/dist/esm/components/SwipeArea.module.css.js +0 -8
  93. package/dist/esm/components/SwipeArea.module.css.js.map +0 -1
  94. package/dist/esm/components/TapButton.js +0 -56
  95. package/dist/esm/components/TapButton.js.map +0 -1
  96. package/dist/esm/components/TapButton.module.css.js +0 -8
  97. package/dist/esm/components/TapButton.module.css.js.map +0 -1
  98. package/dist/esm/context/RoomProvider.js +0 -109
  99. package/dist/esm/context/RoomProvider.js.map +0 -1
  100. package/dist/esm/hooks/useExternalGames.js +0 -47
  101. package/dist/esm/hooks/useExternalGames.js.map +0 -1
  102. package/dist/esm/hooks/useGameHost.js +0 -204
  103. package/dist/esm/hooks/useGameHost.js.map +0 -1
  104. package/dist/esm/hooks/useGamePlayer.js +0 -132
  105. package/dist/esm/hooks/useGamePlayer.js.map +0 -1
  106. package/dist/esm/iframe/index.js +0 -257
  107. package/dist/esm/iframe/index.js.map +0 -1
  108. package/dist/esm/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.js +0 -29
  109. package/dist/esm/node_modules/.pnpm/style-inject@0.3.0/node_modules/style-inject/dist/style-inject.es.js.map +0 -1
  110. package/dist/esm/server/index.js +0 -43
  111. package/dist/esm/server/index.js.map +0 -1
  112. package/dist/esm/transport/DirectTransport.js +0 -21
  113. package/dist/esm/transport/DirectTransport.js.map +0 -1
  114. package/dist/esm/utils/connectionMonitor.js +0 -75
  115. package/dist/esm/utils/connectionMonitor.js.map +0 -1
  116. package/dist/esm/utils/preloadAssets.js +0 -63
  117. package/dist/esm/utils/preloadAssets.js.map +0 -1
  118. package/dist/esm/utils/serverTime.js +0 -41
  119. package/dist/esm/utils/serverTime.js.map +0 -1
  120. package/dist/types/SmoreHost.d.ts +0 -187
  121. package/dist/types/SmoreHost.d.ts.map +0 -1
  122. package/dist/types/SmorePlayer.d.ts +0 -146
  123. package/dist/types/SmorePlayer.d.ts.map +0 -1
  124. package/dist/types/components/DirectionPad.d.ts +0 -21
  125. package/dist/types/components/DirectionPad.d.ts.map +0 -1
  126. package/dist/types/components/HoldButton.d.ts +0 -22
  127. package/dist/types/components/HoldButton.d.ts.map +0 -1
  128. package/dist/types/components/IframeGameBridge.d.ts +0 -38
  129. package/dist/types/components/IframeGameBridge.d.ts.map +0 -1
  130. package/dist/types/components/SwipeArea.d.ts +0 -19
  131. package/dist/types/components/SwipeArea.d.ts.map +0 -1
  132. package/dist/types/components/TapButton.d.ts +0 -19
  133. package/dist/types/components/TapButton.d.ts.map +0 -1
  134. package/dist/types/components/index.d.ts +0 -6
  135. package/dist/types/components/index.d.ts.map +0 -1
  136. package/dist/types/context/RoomProvider.d.ts +0 -69
  137. package/dist/types/context/RoomProvider.d.ts.map +0 -1
  138. package/dist/types/context/index.d.ts +0 -3
  139. package/dist/types/context/index.d.ts.map +0 -1
  140. package/dist/types/dev/DevSimulator.d.ts +0 -31
  141. package/dist/types/dev/DevSimulator.d.ts.map +0 -1
  142. package/dist/types/dev/index.d.ts +0 -2
  143. package/dist/types/dev/index.d.ts.map +0 -1
  144. package/dist/types/hooks/index.d.ts +0 -7
  145. package/dist/types/hooks/index.d.ts.map +0 -1
  146. package/dist/types/hooks/useExternalGames.d.ts +0 -32
  147. package/dist/types/hooks/useExternalGames.d.ts.map +0 -1
  148. package/dist/types/hooks/useGameHost.d.ts +0 -67
  149. package/dist/types/hooks/useGameHost.d.ts.map +0 -1
  150. package/dist/types/hooks/useGamePlayer.d.ts +0 -55
  151. package/dist/types/hooks/useGamePlayer.d.ts.map +0 -1
  152. package/dist/types/iframe/IframeRoomProvider.d.ts +0 -31
  153. package/dist/types/iframe/IframeRoomProvider.d.ts.map +0 -1
  154. package/dist/types/iframe/index.d.ts +0 -18
  155. package/dist/types/iframe/index.d.ts.map +0 -1
  156. package/dist/types/iframe/vanilla-entry.d.ts +0 -7
  157. package/dist/types/iframe/vanilla-entry.d.ts.map +0 -1
  158. package/dist/types/iframe/vanilla.d.ts +0 -49
  159. package/dist/types/iframe/vanilla.d.ts.map +0 -1
  160. package/dist/types/server/createGameRelay.d.ts +0 -26
  161. package/dist/types/server/createGameRelay.d.ts.map +0 -1
  162. package/dist/types/server/index.d.ts +0 -3
  163. package/dist/types/server/index.d.ts.map +0 -1
  164. package/dist/types/utils/connectionMonitor.d.ts +0 -57
  165. package/dist/types/utils/connectionMonitor.d.ts.map +0 -1
  166. package/dist/types/utils/index.d.ts +0 -7
  167. package/dist/types/utils/index.d.ts.map +0 -1
  168. package/dist/types/utils/preloadAssets.d.ts +0 -29
  169. package/dist/types/utils/preloadAssets.d.ts.map +0 -1
  170. package/dist/types/utils/serverTime.d.ts +0 -28
  171. package/dist/types/utils/serverTime.d.ts.map +0 -1
  172. package/dist/umd/smore-sdk-iframe.umd.js +0 -266
  173. package/dist/umd/smore-sdk-iframe.umd.js.map +0 -1
  174. package/dist/umd/smore-sdk-iframe.umd.min.js +0 -2
  175. package/dist/umd/smore-sdk-iframe.umd.min.js.map +0 -1
  176. package/dist/umd/smore-sdk-vanilla.umd.js +0 -1275
  177. package/dist/umd/smore-sdk-vanilla.umd.js.map +0 -1
  178. package/dist/umd/smore-sdk-vanilla.umd.min.js +0 -2
  179. package/dist/umd/smore-sdk-vanilla.umd.min.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"config.cjs","sources":["../../src/config.ts"],"sourcesContent":["/**\n * Global SDK configuration.\n * Settings here apply to both Screen and Controller unless overridden per-instance.\n */\n\nexport interface SmoreGlobalConfig {\n /**\n * Automatically signal ready after initialization.\n * When true (default), the SDK calls signalReady() 3 seconds after init.\n * Set to false if your game needs to load resources before signaling ready.\n * Applies to both Screen and Controller.\n * Can be overridden per-instance via ScreenConfig.autoReady / ControllerConfig.autoReady.\n * @default true\n */\n autoReady?: boolean;\n}\n\nlet globalConfig: SmoreGlobalConfig = {};\n\n/**\n * Set global SDK configuration.\n * Call this before createScreen() / createController().\n *\n * @example\n * ```ts\n * import { configure, createScreen, createController } from '@smoregg/sdk';\n *\n * // Set once, applies to both screen and controller\n * configure({ autoReady: false });\n *\n * const screen = await createScreen({ ... });\n * const controller = await createController({ ... });\n * ```\n */\nexport function configure(config: SmoreGlobalConfig): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get current global config. Used internally by Screen and Controller.\n */\nexport function getGlobalConfig(): Readonly<SmoreGlobalConfig> {\n return globalConfig;\n}\n"],"names":[],"mappings":";;AAiBA,IAAI,eAAkC,EAAC;AAiBhC,SAAS,UAAU,MAAA,EAAiC;AACzD,EAAA,YAAA,GAAe,EAAE,GAAG,YAAA,EAAc,GAAG,MAAA,EAAO;AAC9C;AAKO,SAAS,eAAA,GAA+C;AAC7D,EAAA,OAAO,YAAA;AACT;;;;;"}
1
+ {"version":3,"file":"config.cjs","sources":["../../src/config.ts"],"sourcesContent":["/**\n * Global SDK configuration.\n * Settings here apply to both Screen and Controller unless overridden per-instance.\n */\n\nexport interface SmoreGlobalConfig {\n /**\n * Automatically signal ready after initialization.\n * When true (default), the SDK calls signalReady() automatically after init completes.\n * Set to false if your game needs to load resources before signaling ready.\n * Applies to both Screen and Controller.\n * @default true\n */\n autoReady?: boolean;\n}\n\nlet globalConfig: SmoreGlobalConfig = {};\n\n/**\n * Set global SDK configuration.\n * Call this before createScreen() / createController().\n *\n * @example\n * ```ts\n * import { configure, createScreen, createController } from '@smoregg/sdk';\n *\n * // Set once, applies to both screen and controller\n * configure({ autoReady: false });\n *\n * const screen = await createScreen({ ... });\n * const controller = await createController({ ... });\n * ```\n */\nexport function configure(config: SmoreGlobalConfig): void {\n globalConfig = { ...globalConfig, ...config };\n}\n\n/**\n * Get current global config. Used internally by Screen and Controller.\n */\nexport function getGlobalConfig(): Readonly<SmoreGlobalConfig> {\n return globalConfig;\n}\n"],"names":[],"mappings":";;AAgBA,IAAI,eAAkC,EAAC;AAiBhC,SAAS,UAAU,MAAA,EAAiC;AACzD,EAAA,YAAA,GAAe,EAAE,GAAG,YAAA,EAAc,GAAG,MAAA,EAAO;AAC9C;AAKO,SAAS,eAAA,GAA+C;AAC7D,EAAA,OAAO,YAAA;AACT;;;;;"}
@@ -16,22 +16,38 @@ class ControllerImpl {
16
16
  _myIndex = -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
- // Tracks event names registered via config.listeners so off(event) without handler won't remove them
26
- _configListenerEvents = /* @__PURE__ */ new Set();
26
+ // Pending handlers registered via on() before transport is ready
27
+ _pendingHandlers = [];
28
+ // Lifecycle callback arrays
29
+ _onAllReadyCallbacks = /* @__PURE__ */ new Set();
30
+ _onControllerJoinCallbacks = /* @__PURE__ */ new Set();
31
+ _onControllerLeaveCallbacks = /* @__PURE__ */ new Set();
32
+ _onControllerDisconnectCallbacks = /* @__PURE__ */ new Set();
33
+ _onControllerReconnectCallbacks = /* @__PURE__ */ new Set();
34
+ _onCharacterUpdatedCallbacks = /* @__PURE__ */ new Set();
35
+ _onRateLimitedCallbacks = /* @__PURE__ */ new Set();
36
+ _onErrorCallbacks = /* @__PURE__ */ new Set();
37
+ // Whether all-ready has fired
38
+ _allReadyFired = false;
39
+ // Ready promise
40
+ _readyResolve;
41
+ _readyReject;
42
+ ready;
27
43
  constructor(config = {}) {
28
44
  this.config = config;
29
45
  this.logger = new logger.DebugLogger(config.debug, "[SmoreController]");
30
- if (config.listeners) {
31
- for (const event of Object.keys(config.listeners)) {
32
- events.validateEventName(event);
33
- }
34
- }
46
+ this.ready = new Promise((resolve, reject) => {
47
+ this._readyResolve = resolve;
48
+ this._readyReject = reject;
49
+ });
50
+ this.startInitialization();
35
51
  }
36
52
  // ---------------------------------------------------------------------------
37
53
  // Properties (readonly)
@@ -67,38 +83,36 @@ class ControllerImpl {
67
83
  // ---------------------------------------------------------------------------
68
84
  // Initialization
69
85
  // ---------------------------------------------------------------------------
70
- async initialize() {
86
+ startInitialization() {
71
87
  const parentOrigin = this.config.parentOrigin ?? "*";
72
88
  const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
73
89
  this.logger.lifecycle("Initializing controller...", { parentOrigin, timeout });
74
- return new Promise((resolve, reject) => {
75
- const timeoutId = setTimeout(() => {
76
- this.cleanup();
77
- const error = new errors.SmoreSDKError(
78
- "TIMEOUT",
79
- `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).`,
80
- { details: { timeout } }
81
- );
82
- this.handleError(error);
83
- reject(error);
84
- }, timeout);
85
- this.boundMessageHandler = (e) => {
86
- if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
87
- const msg = e.data;
88
- if (!protocol.isBridgeMessage(msg)) return;
89
- if (msg.type === "_bridge:init") {
90
- clearTimeout(timeoutId);
91
- this.handleInit(msg, parentOrigin, resolve, reject);
92
- } else if (msg.type === "_bridge:update") {
93
- this.handleUpdate(msg);
94
- }
95
- };
96
- window.addEventListener("message", this.boundMessageHandler);
97
- this.logger.lifecycle("Sending _bridge:ready to parent");
98
- window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
99
- });
90
+ this._initTimeoutId = setTimeout(() => {
91
+ this.cleanup();
92
+ const error = new errors.SmoreSDKError(
93
+ "TIMEOUT",
94
+ `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).`,
95
+ { details: { timeout } }
96
+ );
97
+ this.handleError(error);
98
+ this._readyReject(error);
99
+ }, timeout);
100
+ this.boundMessageHandler = (e) => {
101
+ if (parentOrigin !== "*" && e.origin !== parentOrigin) return;
102
+ const msg = e.data;
103
+ if (!protocol.isBridgeMessage(msg)) return;
104
+ if (msg.type === "_bridge:init") {
105
+ clearTimeout(this._initTimeoutId);
106
+ this.handleInit(msg, parentOrigin);
107
+ } else if (msg.type === "_bridge:update") {
108
+ this.handleUpdate(msg);
109
+ }
110
+ };
111
+ window.addEventListener("message", this.boundMessageHandler);
112
+ this.logger.lifecycle("Sending _bridge:ready to parent");
113
+ window.parent.postMessage({ type: "_bridge:ready" }, parentOrigin);
100
114
  }
101
- handleInit(msg, parentOrigin, resolve, reject) {
115
+ handleInit(msg, parentOrigin) {
102
116
  const initPayload = msg.payload;
103
117
  this.logger.debug("Received _bridge:init", initPayload);
104
118
  try {
@@ -111,7 +125,7 @@ class ControllerImpl {
111
125
  );
112
126
  this.logger.warn("_bridge:init validation failed", error);
113
127
  this.handleError(error);
114
- reject(error);
128
+ this._readyReject(error);
115
129
  return;
116
130
  }
117
131
  const initData = initPayload;
@@ -122,7 +136,7 @@ class ControllerImpl {
122
136
  { details: { side: initData.side } }
123
137
  );
124
138
  this.handleError(error);
125
- reject(error);
139
+ this._readyReject(error);
126
140
  return;
127
141
  }
128
142
  if (initData.myIndex === void 0) {
@@ -132,7 +146,7 @@ class ControllerImpl {
132
146
  { details: initData }
133
147
  );
134
148
  this.handleError(error);
135
- reject(error);
149
+ this._readyReject(error);
136
150
  return;
137
151
  }
138
152
  this.transport = new PostMessageTransport.PostMessageTransport(parentOrigin);
@@ -146,18 +160,21 @@ class ControllerImpl {
146
160
  appearance: p.appearance ?? p.character
147
161
  }));
148
162
  this.setupEventHandlers();
163
+ for (const { event, handler } of this._pendingHandlers) {
164
+ this.setupUserEventHandler(event, handler);
165
+ }
166
+ this._pendingHandlers = [];
149
167
  this._isReady = true;
150
168
  this.logger.lifecycle("Controller ready", {
151
169
  roomCode: this._roomCode,
152
170
  myIndex: this._myIndex
153
171
  });
154
- this.config.onReady?.();
155
- const autoReady = this.config.autoReady ?? config.getGlobalConfig().autoReady ?? true;
172
+ const autoReady = config.getGlobalConfig().autoReady ?? true;
156
173
  if (autoReady) {
157
174
  this.logger.lifecycle("Auto-signaling ready (autoReady enabled)");
158
175
  this.signalReady();
159
176
  }
160
- resolve();
177
+ this._readyResolve();
161
178
  }
162
179
  handleUpdate(msg) {
163
180
  if (!this._isReady) {
@@ -177,12 +194,12 @@ class ControllerImpl {
177
194
  const oldControllers = this._controllers;
178
195
  for (const nc of newControllers) {
179
196
  if (!oldControllers.some((oc) => oc.playerIndex === nc.playerIndex)) {
180
- this.config.onControllerJoin?.(nc.playerIndex, nc);
197
+ this._onControllerJoinCallbacks.forEach((cb) => cb(nc.playerIndex, nc));
181
198
  }
182
199
  }
183
200
  for (const oc of oldControllers) {
184
201
  if (!newControllers.some((nc) => nc.playerIndex === oc.playerIndex)) {
185
- this.config.onControllerLeave?.(oc.playerIndex);
202
+ this._onControllerLeaveCallbacks.forEach((cb) => cb(oc.playerIndex));
186
203
  }
187
204
  }
188
205
  for (const nc of newControllers) {
@@ -190,11 +207,11 @@ class ControllerImpl {
190
207
  if (oc) {
191
208
  if (oc.connected && !nc.connected) {
192
209
  this.logger.debug("Player disconnected (via update)", { playerIndex: nc.playerIndex });
193
- this.config.onControllerDisconnect?.(nc.playerIndex);
210
+ this._onControllerDisconnectCallbacks.forEach((cb) => cb(nc.playerIndex));
194
211
  }
195
212
  if (!oc.connected && nc.connected) {
196
213
  this.logger.debug("Player reconnected (via update)", { playerIndex: nc.playerIndex });
197
- this.config.onControllerReconnect?.(nc.playerIndex, nc);
214
+ this._onControllerReconnectCallbacks.forEach((cb) => cb(nc.playerIndex, nc));
198
215
  }
199
216
  }
200
217
  }
@@ -221,7 +238,7 @@ class ControllerImpl {
221
238
  };
222
239
  this._controllers = [...this._controllers, controllerInfo];
223
240
  this.logger.debug("Player joined", { playerIndex });
224
- this.config.onControllerJoin?.(playerIndex, controllerInfo);
241
+ this._onControllerJoinCallbacks.forEach((cb) => cb(playerIndex, controllerInfo));
225
242
  }
226
243
  });
227
244
  this.registerHandler(events.SMORE_EVENTS.PLAYER_LEFT, (raw) => {
@@ -231,7 +248,7 @@ class ControllerImpl {
231
248
  if (!this._controllers.some((c) => c.playerIndex === playerIndex)) return;
232
249
  this._controllers = this._controllers.filter((c) => c.playerIndex !== playerIndex);
233
250
  this.logger.debug("Player left", { playerIndex });
234
- this.config.onControllerLeave?.(playerIndex);
251
+ this._onControllerLeaveCallbacks.forEach((cb) => cb(playerIndex));
235
252
  }
236
253
  });
237
254
  this.registerHandler(events.SMORE_EVENTS.PLAYER_DISCONNECTED, (raw) => {
@@ -243,7 +260,7 @@ class ControllerImpl {
243
260
  (c) => c.playerIndex === playerIndex ? { ...c, connected: false } : c
244
261
  );
245
262
  this.logger.debug("Player disconnected", { playerIndex });
246
- this.config.onControllerDisconnect?.(playerIndex);
263
+ this._onControllerDisconnectCallbacks.forEach((cb) => cb(playerIndex));
247
264
  }
248
265
  });
249
266
  this.registerHandler(events.SMORE_EVENTS.PLAYER_RECONNECTED, (raw) => {
@@ -265,56 +282,63 @@ class ControllerImpl {
265
282
  (c) => c.playerIndex === playerIndex ? controllerInfo : c
266
283
  );
267
284
  this.logger.debug("Player reconnected", { playerIndex });
268
- this.config.onControllerReconnect?.(playerIndex, controllerInfo);
285
+ this._onControllerReconnectCallbacks.forEach((cb) => cb(playerIndex, controllerInfo));
269
286
  }
270
287
  });
271
288
  this.registerHandler(events.SMORE_EVENTS.PLAYER_CHARACTER_UPDATED, (raw) => {
272
289
  const payload = raw;
273
290
  const playerData = payload?.player;
274
291
  if (playerData && typeof playerData.playerIndex === "number") {
292
+ const pi = playerData.playerIndex;
275
293
  const appearance = playerData.character ?? null;
276
294
  this._controllers = this._controllers.map(
277
- (c) => c.playerIndex === playerData.playerIndex ? { ...c, appearance } : c
295
+ (c) => c.playerIndex === pi ? { ...c, appearance } : c
278
296
  );
279
- this.logger.debug("Player character updated", { playerIndex: playerData.playerIndex });
280
- this.config.onCharacterUpdated?.(playerData.playerIndex, appearance ?? null);
297
+ this.logger.debug("Player character updated", { playerIndex: pi });
298
+ this._onCharacterUpdatedCallbacks.forEach((cb) => cb(pi, appearance ?? null));
281
299
  }
282
300
  });
283
301
  this.registerHandler(events.SMORE_EVENTS.RATE_LIMITED, (raw) => {
284
302
  const data = raw;
285
303
  const event = data?.event ?? "unknown";
286
304
  this.logger.warn(`Rate limited: ${event}`);
287
- this.config.onRateLimited?.(event);
305
+ this._onRateLimitedCallbacks.forEach((cb) => cb(event));
288
306
  });
289
307
  this.registerHandler(events.SMORE_EVENTS.ALL_READY, () => {
290
308
  this.logger.lifecycle("All participants ready");
291
- this.config.onAllReady?.();
309
+ this._allReadyFired = true;
310
+ this._onAllReadyCallbacks.forEach((cb) => cb());
292
311
  });
293
- if (this.config.onHostDisconnect) {
294
- this.logger.warn("onHostDisconnect is reserved for future use and currently non-functional");
295
- }
296
- if (this.config.onHostReconnect) {
297
- this.logger.warn("onHostReconnect is reserved for future use and currently non-functional");
298
- }
299
- if (this.config.listeners) {
300
- for (const [event, handler] of Object.entries(this.config.listeners)) {
301
- if (!handler) continue;
302
- this._configListenerEvents.add(event);
303
- this.registerHandler(event, (data) => {
304
- this.logReceive(event, data);
305
- try {
306
- handler(data);
307
- } catch (err) {
308
- this.handleError(
309
- new errors.SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
310
- cause: err instanceof Error ? err : void 0,
311
- details: { event }
312
- })
313
- );
314
- }
315
- });
312
+ }
313
+ /**
314
+ * Sets up a user event handler for controller events.
315
+ * Used for registering pending handlers after transport becomes available.
316
+ */
317
+ setupUserEventHandler(event, handler) {
318
+ const transportHandler = (data) => {
319
+ this.logReceive(event, data);
320
+ try {
321
+ handler(data);
322
+ } catch (err) {
323
+ this.handleError(
324
+ new errors.SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
325
+ cause: err instanceof Error ? err : void 0,
326
+ details: { event }
327
+ })
328
+ );
316
329
  }
330
+ };
331
+ if (this.transport) {
332
+ this.transport.on(event, transportHandler);
333
+ this.registeredHandlers.push({ event, handler: transportHandler });
334
+ this.handlerToTransport.set(handler, { event, transportHandler });
317
335
  }
336
+ let listeners = this.eventListeners.get(event);
337
+ if (!listeners) {
338
+ listeners = /* @__PURE__ */ new Set();
339
+ this.eventListeners.set(event, listeners);
340
+ }
341
+ listeners.add(handler);
318
342
  }
319
343
  registerHandler(event, handler) {
320
344
  if (!this.transport) return;
@@ -322,13 +346,67 @@ class ControllerImpl {
322
346
  this.registeredHandlers.push({ event, handler });
323
347
  }
324
348
  // ---------------------------------------------------------------------------
349
+ // Lifecycle Methods
350
+ // ---------------------------------------------------------------------------
351
+ onAllReady(callback) {
352
+ if (this._allReadyFired) {
353
+ callback();
354
+ }
355
+ this._onAllReadyCallbacks.add(callback);
356
+ return () => {
357
+ this._onAllReadyCallbacks.delete(callback);
358
+ };
359
+ }
360
+ onControllerJoin(callback) {
361
+ this._onControllerJoinCallbacks.add(callback);
362
+ return () => {
363
+ this._onControllerJoinCallbacks.delete(callback);
364
+ };
365
+ }
366
+ onControllerLeave(callback) {
367
+ this._onControllerLeaveCallbacks.add(callback);
368
+ return () => {
369
+ this._onControllerLeaveCallbacks.delete(callback);
370
+ };
371
+ }
372
+ onControllerDisconnect(callback) {
373
+ this._onControllerDisconnectCallbacks.add(callback);
374
+ return () => {
375
+ this._onControllerDisconnectCallbacks.delete(callback);
376
+ };
377
+ }
378
+ onControllerReconnect(callback) {
379
+ this._onControllerReconnectCallbacks.add(callback);
380
+ return () => {
381
+ this._onControllerReconnectCallbacks.delete(callback);
382
+ };
383
+ }
384
+ onCharacterUpdated(callback) {
385
+ this._onCharacterUpdatedCallbacks.add(callback);
386
+ return () => {
387
+ this._onCharacterUpdatedCallbacks.delete(callback);
388
+ };
389
+ }
390
+ onRateLimited(callback) {
391
+ this._onRateLimitedCallbacks.add(callback);
392
+ return () => {
393
+ this._onRateLimitedCallbacks.delete(callback);
394
+ };
395
+ }
396
+ onError(callback) {
397
+ this._onErrorCallbacks.add(callback);
398
+ return () => {
399
+ this._onErrorCallbacks.delete(callback);
400
+ };
401
+ }
402
+ // ---------------------------------------------------------------------------
325
403
  // Communication Methods
326
404
  // ---------------------------------------------------------------------------
327
405
  /**
328
406
  * Send an event to the Screen. Controller-to-Controller direct communication
329
407
  * is not supported; all messages must go through the Screen.
330
408
  *
331
- * Data is sent to the Screen only (not to other controllers). For ScreenController communication,
409
+ * Data is sent to the Screen only (not to other controllers). For Screen->Controller communication,
332
410
  * Screen uses broadcast() or sendToController().
333
411
  *
334
412
  * @note Fire-and-forget sends (no callback) will silently fail if rate-limited.
@@ -362,24 +440,14 @@ class ControllerImpl {
362
440
  /**
363
441
  * Register a handler for custom events.
364
442
  *
443
+ * Can be called before the Controller is ready. Handlers registered before ready
444
+ * are queued and activated when the transport becomes available.
445
+ *
365
446
  * When receiving events from Screen's `broadcast()`:
366
- * handler receives `(data)` no playerIndex included.
447
+ * handler receives `(data)` -- no playerIndex included.
367
448
  *
368
449
  * When receiving events from Screen's `sendToController()`:
369
- * handler receives `(data)` targeted to this specific controller.
370
- *
371
- * @note Unlike Screen's `on()` which receives `(playerIndex, data)`,
372
- * Controller's `on()` receives only `(data)` since there's only one player per controller.
373
- *
374
- * Controller's on() handler signature: (data) => void
375
- * Unlike Screen's (playerIndex, data) => void, Controller doesn't receive playerIndex
376
- * because Controller only receives events from Screen, not from other controllers.
377
- * The sender is always the Screen, so playerIndex is not applicable.
378
- *
379
- * **Important:** If called before the Controller is ready (i.e., before `await createController()`
380
- * resolves or before the `onReady` callback fires), the handler is stored locally but
381
- * will NOT receive events until the transport is initialized. Always call `on()` after
382
- * initialization completes, or use `config.listeners` for handlers needed from the start.
450
+ * handler receives `(data)` -- targeted to this specific controller.
383
451
  */
384
452
  on(event, handler) {
385
453
  events.validateEventName(event);
@@ -389,34 +457,42 @@ class ControllerImpl {
389
457
  this.eventListeners.set(event, listeners);
390
458
  }
391
459
  listeners.add(handler);
392
- const transportHandler = (data) => {
393
- this.logReceive(event, data);
394
- try {
395
- handler(data);
396
- } catch (err) {
397
- this.handleError(
398
- new errors.SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
399
- cause: err instanceof Error ? err : void 0,
400
- details: { event }
401
- })
402
- );
403
- }
404
- };
405
460
  if (this.transport) {
461
+ const transportHandler = (data) => {
462
+ this.logReceive(event, data);
463
+ try {
464
+ handler(data);
465
+ } catch (err) {
466
+ this.handleError(
467
+ new errors.SmoreSDKError("UNKNOWN", `Error in handler for event "${event}"`, {
468
+ cause: err instanceof Error ? err : void 0,
469
+ details: { event }
470
+ })
471
+ );
472
+ }
473
+ };
406
474
  this.transport.on(event, transportHandler);
407
475
  this.registeredHandlers.push({ event, handler: transportHandler });
408
476
  this.handlerToTransport.set(handler, { event, transportHandler });
477
+ } else {
478
+ this._pendingHandlers.push({ event, handler });
409
479
  }
410
480
  return () => {
411
481
  listeners?.delete(handler);
412
482
  if (listeners?.size === 0) {
413
483
  this.eventListeners.delete(event);
414
484
  }
415
- this.transport?.off(event, transportHandler);
416
- this.registeredHandlers = this.registeredHandlers.filter(
417
- (h) => h.handler !== transportHandler
485
+ this._pendingHandlers = this._pendingHandlers.filter(
486
+ (p) => !(p.event === event && p.handler === handler)
418
487
  );
419
- this.handlerToTransport.delete(handler);
488
+ const entry = this.handlerToTransport.get(handler);
489
+ if (entry) {
490
+ this.transport?.off(event, entry.transportHandler);
491
+ this.registeredHandlers = this.registeredHandlers.filter(
492
+ (h) => h.handler !== entry.transportHandler
493
+ );
494
+ this.handlerToTransport.delete(handler);
495
+ }
420
496
  };
421
497
  }
422
498
  /**
@@ -424,16 +500,6 @@ class ControllerImpl {
424
500
  *
425
501
  * @note The handler is internally wrapped, so it cannot be removed via
426
502
  * `off(event, originalHandler)`. Use the returned unsubscribe function instead.
427
- *
428
- * @example
429
- * ```ts
430
- * const unsubscribe = controller.once('game-start', (data) => {
431
- * console.log('Game started!', data);
432
- * });
433
- *
434
- * // To cancel before it fires:
435
- * unsubscribe();
436
- * ```
437
503
  */
438
504
  once(event, handler) {
439
505
  const unsubscribe = this.on(event, ((data) => {
@@ -444,24 +510,13 @@ class ControllerImpl {
444
510
  }
445
511
  off(event, handler) {
446
512
  if (!handler) {
447
- if (this._configListenerEvents.has(event)) {
448
- for (const [key, val] of this.handlerToTransport) {
449
- if (val.event === event) {
450
- this.transport?.off(event, val.transportHandler);
451
- this.registeredHandlers = this.registeredHandlers.filter(
452
- (h) => h.handler !== val.transportHandler
453
- );
454
- this.handlerToTransport.delete(key);
455
- }
456
- }
457
- } else {
458
- this.eventListeners.delete(event);
459
- this.transport?.off(event);
460
- this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
461
- for (const [key, val] of this.handlerToTransport) {
462
- if (val.event === event) this.handlerToTransport.delete(key);
463
- }
513
+ this.eventListeners.delete(event);
514
+ this.transport?.off(event);
515
+ this.registeredHandlers = this.registeredHandlers.filter((h) => h.event !== event);
516
+ for (const [key, val] of this.handlerToTransport) {
517
+ if (val.event === event) this.handlerToTransport.delete(key);
464
518
  }
519
+ this._pendingHandlers = this._pendingHandlers.filter((p) => p.event !== event);
465
520
  } else {
466
521
  const listeners = this.eventListeners.get(event);
467
522
  listeners?.delete(handler);
@@ -476,6 +531,9 @@ class ControllerImpl {
476
531
  );
477
532
  this.handlerToTransport.delete(handler);
478
533
  }
534
+ this._pendingHandlers = this._pendingHandlers.filter(
535
+ (p) => !(p.event === event && p.handler === handler)
536
+ );
479
537
  }
480
538
  }
481
539
  // ---------------------------------------------------------------------------
@@ -484,10 +542,15 @@ class ControllerImpl {
484
542
  destroy() {
485
543
  if (this._isDestroyed) return;
486
544
  this.logger.lifecycle("Destroying controller");
487
- this.cleanup();
488
545
  this._isDestroyed = true;
546
+ this._isReady = false;
547
+ this.cleanup();
489
548
  }
490
549
  cleanup() {
550
+ if (this._initTimeoutId) {
551
+ clearTimeout(this._initTimeoutId);
552
+ this._initTimeoutId = null;
553
+ }
491
554
  this._isReady = false;
492
555
  for (const { event, handler } of this.registeredHandlers) {
493
556
  this.transport?.off(event, handler);
@@ -495,6 +558,15 @@ class ControllerImpl {
495
558
  this.registeredHandlers = [];
496
559
  this.eventListeners.clear();
497
560
  this.handlerToTransport.clear();
561
+ this._pendingHandlers = [];
562
+ this._onAllReadyCallbacks.clear();
563
+ this._onControllerJoinCallbacks.clear();
564
+ this._onControllerLeaveCallbacks.clear();
565
+ this._onControllerDisconnectCallbacks.clear();
566
+ this._onControllerReconnectCallbacks.clear();
567
+ this._onCharacterUpdatedCallbacks.clear();
568
+ this._onRateLimitedCallbacks.clear();
569
+ this._onErrorCallbacks.clear();
498
570
  if (this.transport) {
499
571
  this.transport.destroy();
500
572
  this.transport = null;
@@ -518,15 +590,16 @@ class ControllerImpl {
518
590
  if (!this._isReady || !this.transport) {
519
591
  throw new errors.SmoreSDKError(
520
592
  "NOT_READY",
521
- `Cannot call ${method}() before controller is ready. Use await createController() or wait for onReady callback.`,
593
+ `Cannot call ${method}() before controller is ready. Use await controller.ready.`,
522
594
  { details: { method, isReady: this._isReady } }
523
595
  );
524
596
  }
525
597
  }
526
598
  handleError(error) {
527
599
  this.logger.warn(`Error in handler: ${error.message}`);
528
- if (this.config.onError) {
529
- this.config.onError(error.toSmoreError());
600
+ const smoreError = error.toSmoreError();
601
+ if (this._onErrorCallbacks.size > 0) {
602
+ this._onErrorCallbacks.forEach((cb) => cb(smoreError));
530
603
  } else {
531
604
  this.logger.error(error.message, error.details);
532
605
  }
@@ -539,10 +612,7 @@ class ControllerImpl {
539
612
  }
540
613
  }
541
614
  function createController(config) {
542
- const controller = new ControllerImpl(config ?? {});
543
- const promise = controller.initialize().then(() => controller);
544
- promise.instance = controller;
545
- return promise;
615
+ return new ControllerImpl(config ?? {});
546
616
  }
547
617
 
548
618
  exports.createController = createController;