@diffenderfer-games/hub 2.0.0-next.0 → 2.0.0-next.10

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 (293) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/LICENSE +21 -21
  3. package/README.md +203 -139
  4. package/dist/auto.d.ts +8 -8
  5. package/dist/auto.js +3 -3
  6. package/dist/avatar-pixi.d.ts +165 -0
  7. package/dist/avatar-pixi.d.ts.map +1 -0
  8. package/dist/avatar-pixi.js +299 -0
  9. package/dist/avatar-pixi.js.map +1 -0
  10. package/dist/avatar-svg.d.ts +4 -0
  11. package/dist/avatar-svg.js +5 -0
  12. package/dist/avatar-three.d.ts +69 -0
  13. package/dist/avatar-three.d.ts.map +1 -0
  14. package/dist/avatar-three.js +49 -0
  15. package/dist/avatar-three.js.map +1 -0
  16. package/dist/avatar.d.ts +7 -0
  17. package/dist/avatar.js +500 -0
  18. package/dist/avatar.js.map +1 -0
  19. package/dist/daily.d.ts +2 -2
  20. package/dist/doctor/cli.mjs +119 -28
  21. package/dist/doctor/cli.mjs.map +1 -1
  22. package/dist/index.d.ts +8 -8
  23. package/dist/index.js +3 -3
  24. package/dist/input.d.ts +2 -2
  25. package/dist/input.js +2 -2
  26. package/dist/mp.d.ts +3 -3
  27. package/dist/race.d.ts +1 -1
  28. package/dist/rooms.d.ts +3 -3
  29. package/dist/rooms.js +1 -1
  30. package/dist/shared/animator.d.ts +104 -0
  31. package/dist/shared/animator.d.ts.map +1 -0
  32. package/dist/shared/art.js +1946 -0
  33. package/dist/shared/art.js.map +1 -0
  34. package/dist/shared/atlas.d.ts +138 -0
  35. package/dist/shared/atlas.d.ts.map +1 -0
  36. package/dist/shared/avatar.d.ts +123 -0
  37. package/dist/shared/avatar.d.ts.map +1 -0
  38. package/dist/shared/bridge.js +18 -4
  39. package/dist/shared/bridge.js.map +1 -1
  40. package/dist/shared/client-version.gen.js +1 -1
  41. package/dist/shared/daily.d.ts +145 -19
  42. package/dist/shared/daily.d.ts.map +1 -1
  43. package/dist/shared/daily.js +138 -25
  44. package/dist/shared/daily.js.map +1 -1
  45. package/dist/shared/defineProperty.js +39 -0
  46. package/dist/shared/describe.d.ts +32 -0
  47. package/dist/shared/describe.d.ts.map +1 -0
  48. package/dist/shared/describe.js +109 -0
  49. package/dist/shared/describe.js.map +1 -0
  50. package/dist/shared/globals.js +9 -42
  51. package/dist/shared/globals.js.map +1 -1
  52. package/dist/shared/index.d.ts +1122 -46
  53. package/dist/shared/index.d.ts.map +1 -1
  54. package/dist/shared/input.d.ts +74 -14
  55. package/dist/shared/input.d.ts.map +1 -1
  56. package/dist/shared/input.js +179 -25
  57. package/dist/shared/input.js.map +1 -1
  58. package/dist/shared/json-guards.js +48 -0
  59. package/dist/shared/json-guards.js.map +1 -0
  60. package/dist/shared/latest-state.js +1 -1
  61. package/dist/shared/live-canvas.js +62 -0
  62. package/dist/shared/live-canvas.js.map +1 -0
  63. package/dist/shared/mp.js +10 -2
  64. package/dist/shared/mp.js.map +1 -1
  65. package/dist/shared/multiplayer-types.d.ts +15 -2
  66. package/dist/shared/multiplayer-types.d.ts.map +1 -1
  67. package/dist/shared/overlays.js +800 -0
  68. package/dist/shared/overlays.js.map +1 -0
  69. package/dist/shared/page-stack.js +2 -1
  70. package/dist/shared/page-stack.js.map +1 -1
  71. package/dist/shared/pose-key.js +249 -0
  72. package/dist/shared/pose-key.js.map +1 -0
  73. package/dist/shared/race-types.d.ts +1 -1
  74. package/dist/shared/race.js +1 -1
  75. package/dist/shared/room-types.d.ts +28 -3
  76. package/dist/shared/room-types.d.ts.map +1 -1
  77. package/dist/shared/rooms.d.ts.map +1 -1
  78. package/dist/shared/rooms.js +23 -4
  79. package/dist/shared/rooms.js.map +1 -1
  80. package/dist/shared/runtime-opener.js +1 -1
  81. package/dist/shared/scene.d.ts +90 -0
  82. package/dist/shared/scene.d.ts.map +1 -0
  83. package/dist/shared/social.d.ts +90 -9
  84. package/dist/shared/social.d.ts.map +1 -1
  85. package/dist/shared/social.js +6 -1
  86. package/dist/shared/social.js.map +1 -1
  87. package/dist/shared/src.js +4563 -667
  88. package/dist/shared/src.js.map +1 -1
  89. package/dist/shared/svg-markup.js +2354 -0
  90. package/dist/shared/svg-markup.js.map +1 -0
  91. package/dist/shared/svg.js +27 -0
  92. package/dist/shared/svg.js.map +1 -0
  93. package/dist/shared/types.d.ts +174 -0
  94. package/dist/shared/types.d.ts.map +1 -0
  95. package/dist/shared/wire-types.d.ts +6 -2
  96. package/dist/shared/wire-types.d.ts.map +1 -1
  97. package/dist/social.d.ts +5 -5
  98. package/dist/social.js +2 -2
  99. package/dist/turns-rules.d.ts +2 -0
  100. package/dist/turns-rules.js +1 -0
  101. package/dist/turns.d.ts +61 -0
  102. package/dist/turns.d.ts.map +1 -0
  103. package/dist/turns.js +183 -0
  104. package/dist/turns.js.map +1 -0
  105. package/package.json +125 -89
  106. package/src/auto.ts +39 -39
  107. package/src/avatar/adapters/pixi-avatar.ts +73 -0
  108. package/src/avatar/adapters/pixi-crowd.ts +329 -0
  109. package/src/avatar/adapters/pixi-types.ts +84 -0
  110. package/src/avatar/adapters/three.ts +126 -0
  111. package/src/avatar/animator/animator.ts +285 -0
  112. package/src/avatar/animator/reactions.ts +44 -0
  113. package/src/avatar/animator/schedule.ts +73 -0
  114. package/src/avatar/art/beard.ts +49 -0
  115. package/src/avatar/art/glasses.ts +67 -0
  116. package/src/avatar/art/hair.ts +254 -0
  117. package/src/avatar/art/headwear.ts +75 -0
  118. package/src/avatar/art/index.ts +21 -0
  119. package/src/avatar/art/shape.ts +87 -0
  120. package/src/avatar/art-store.ts +36 -0
  121. package/src/avatar/atlas.ts +246 -0
  122. package/src/avatar/catalog-view.ts +14 -0
  123. package/src/avatar/colour.ts +109 -0
  124. package/src/avatar/describe.ts +79 -0
  125. package/src/avatar/face.ts +187 -0
  126. package/src/avatar/geom.ts +173 -0
  127. package/src/avatar/layout.ts +47 -0
  128. package/src/avatar/look-like.ts +52 -0
  129. package/src/avatar/outputs/bitmap.ts +75 -0
  130. package/src/avatar/outputs/canvas-paint.ts +177 -0
  131. package/src/avatar/outputs/canvas.ts +62 -0
  132. package/src/avatar/outputs/custom-element.ts +140 -0
  133. package/src/avatar/outputs/element.ts +58 -0
  134. package/src/avatar/outputs/live-canvas.ts +57 -0
  135. package/src/avatar/outputs/overlay-svg.ts +22 -0
  136. package/src/avatar/outputs/pose-key.ts +30 -0
  137. package/src/avatar/outputs/svg-markup.ts +113 -0
  138. package/src/avatar/outputs/svg.ts +41 -0
  139. package/src/avatar/overlays.ts +128 -0
  140. package/src/avatar/render/body.ts +116 -0
  141. package/src/avatar/render/colours.ts +81 -0
  142. package/src/avatar/render/compose.ts +320 -0
  143. package/src/avatar/render/context.ts +117 -0
  144. package/src/avatar/render/eyes.ts +78 -0
  145. package/src/avatar/render/eyewear.ts +118 -0
  146. package/src/avatar/render/frame.ts +73 -0
  147. package/src/avatar/render/head.ts +81 -0
  148. package/src/avatar/render/mouth.ts +86 -0
  149. package/src/avatar/render/props.ts +91 -0
  150. package/src/avatar/render/types.ts +93 -0
  151. package/src/avatar/render/walker-parts.ts +121 -0
  152. package/src/avatar/render/walker.ts +199 -0
  153. package/src/avatar/scene.ts +119 -0
  154. package/src/avatar-pixi.ts +20 -0
  155. package/src/avatar-svg.ts +21 -0
  156. package/src/avatar-three.ts +18 -0
  157. package/src/avatar.ts +63 -0
  158. package/src/core/avatar/avatar-cache.ts +95 -0
  159. package/src/core/avatar/avatar-ref.ts +34 -0
  160. package/src/core/avatar/avatar-types.ts +43 -0
  161. package/src/core/avatar/hub-avatar.ts +308 -0
  162. package/src/core/connection.ts +408 -299
  163. package/src/core/errors.ts +160 -147
  164. package/src/core/game-data.ts +40 -38
  165. package/src/core/hub-economy.ts +216 -179
  166. package/src/core/hub-progress.ts +269 -205
  167. package/src/core/hub.ts +352 -131
  168. package/src/core/menu-control.ts +61 -61
  169. package/src/core/offline/cloud-saves.ts +62 -0
  170. package/src/core/offline/events.ts +50 -50
  171. package/src/core/offline/favorites.ts +21 -21
  172. package/src/core/offline/hub-kinds/avatar-kind.ts +42 -0
  173. package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
  174. package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
  175. package/src/core/offline/hub-kinds/game-fields.ts +2 -0
  176. package/src/core/offline/hub-kinds/index.ts +52 -0
  177. package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
  178. package/src/core/offline/hub-kinds/progress-kinds.ts +165 -0
  179. package/src/core/offline/hub-kinds/resume-kind.ts +108 -0
  180. package/src/core/offline/hub-kinds/saved-kind.ts +93 -0
  181. package/src/core/offline/kind-adapters.ts +48 -0
  182. package/src/core/offline/local-inventory.ts +64 -0
  183. package/src/core/offline/local-store.ts +304 -261
  184. package/src/core/offline/offline-api.ts +65 -0
  185. package/src/core/offline/outbox-entry.ts +59 -0
  186. package/src/core/offline/outbox-kinds.ts +78 -0
  187. package/src/core/offline/outbox-merge.ts +39 -0
  188. package/src/core/offline/outbox-replayer.ts +141 -0
  189. package/src/core/offline/outbox-store.ts +85 -0
  190. package/src/core/offline/outbox.ts +232 -0
  191. package/src/core/offline/queue.ts +126 -171
  192. package/src/core/offline/replay-lease.ts +87 -0
  193. package/src/core/offline/replay-policy.ts +52 -0
  194. package/src/core/page-account.ts +121 -0
  195. package/src/core/page.ts +87 -74
  196. package/src/core/rest/endpoint-types.ts +22 -22
  197. package/src/core/rest/rest-client.ts +163 -152
  198. package/src/core/rest/routes.gen.ts +144 -88
  199. package/src/core/resume/hub-resume.ts +286 -0
  200. package/src/core/resume/local-resume.ts +130 -0
  201. package/src/core/resume/resume-list.ts +65 -0
  202. package/src/core/resume/resume-request.ts +52 -0
  203. package/src/core/resume/resume-types.ts +64 -0
  204. package/src/core/saved/daily-saves.ts +196 -0
  205. package/src/core/saved/daily-seed.ts +16 -0
  206. package/src/core/saved/hub-saved.ts +221 -0
  207. package/src/core/saved/local-saved.ts +122 -0
  208. package/src/core/saved/saved-list.ts +129 -0
  209. package/src/core/saved/saved-open.ts +104 -0
  210. package/src/core/saved/saved-order.ts +49 -0
  211. package/src/core/saved/saved-types.ts +24 -0
  212. package/src/core/storage/json-guards.ts +80 -74
  213. package/src/core/storage/key-value.ts +66 -66
  214. package/src/core/ticket.ts +43 -43
  215. package/src/core/turns/hub-turns.ts +161 -0
  216. package/src/core/turns/turn-game.ts +510 -0
  217. package/src/core/turns/your-turn-buzz.ts +69 -0
  218. package/src/core/types.ts +134 -134
  219. package/src/core/version/client-version.gen.ts +2 -2
  220. package/src/core/version/hub-version.ts +62 -62
  221. package/src/core/version/semver.ts +97 -97
  222. package/src/daily/daily-system.ts +276 -161
  223. package/src/daily/page-daily.ts +36 -36
  224. package/src/daily/types.ts +105 -67
  225. package/src/daily.ts +10 -10
  226. package/src/doctor/checks.ts +86 -76
  227. package/src/doctor/cli.ts +101 -106
  228. package/src/doctor/hub-version-fetch.ts +43 -0
  229. package/src/doctor/tsconfig.json +12 -12
  230. package/src/doctor/world-check.ts +61 -0
  231. package/src/globals.ts +115 -110
  232. package/src/index.ts +68 -50
  233. package/src/input/device-state.ts +146 -146
  234. package/src/input/edges.ts +55 -55
  235. package/src/input/focus-ring.ts +13 -0
  236. package/src/input/gamepad.ts +61 -61
  237. package/src/input/input-events.ts +32 -32
  238. package/src/input/input-registry.ts +171 -164
  239. package/src/input/input-system.ts +406 -393
  240. package/src/input/legacy-controls.ts +64 -64
  241. package/src/input/modality.ts +122 -0
  242. package/src/input/nav-state.ts +200 -191
  243. package/src/input/navigator.ts +182 -171
  244. package/src/input/on-screen-controls.ts +101 -101
  245. package/src/input/page-control.ts +43 -43
  246. package/src/input/page-input.ts +193 -193
  247. package/src/input/player-remaps.ts +77 -77
  248. package/src/input/source-values.ts +116 -116
  249. package/src/input/spatial.ts +47 -47
  250. package/src/input/types.ts +331 -331
  251. package/src/input/zero-config.ts +164 -177
  252. package/src/input.ts +26 -22
  253. package/src/mp.ts +10 -10
  254. package/src/overlay/controls-panel.ts +163 -163
  255. package/src/overlay/dom.ts +46 -46
  256. package/src/overlay/layout-clusters.ts +162 -162
  257. package/src/overlay/layout-geometry.ts +136 -136
  258. package/src/overlay/layout.ts +203 -203
  259. package/src/overlay/listener-scope.ts +23 -23
  260. package/src/overlay/on-screen-keyboard.ts +137 -137
  261. package/src/overlay/overlay.ts +234 -208
  262. package/src/overlay/styles.ts +13 -12
  263. package/src/overlay/virtual-controls.ts +262 -262
  264. package/src/race.ts +10 -10
  265. package/src/realtime/bridge.ts +126 -121
  266. package/src/realtime/cross-bundle.ts +29 -29
  267. package/src/realtime/fallback-notice.ts +48 -48
  268. package/src/realtime/hub-link.ts +65 -65
  269. package/src/realtime/latest-state.ts +33 -33
  270. package/src/realtime/multiplayer-forwarder.ts +40 -28
  271. package/src/realtime/multiplayer-types.ts +66 -52
  272. package/src/realtime/page-runtime.ts +60 -60
  273. package/src/realtime/race-forwarder.ts +59 -59
  274. package/src/realtime/race-types.ts +132 -132
  275. package/src/realtime/room-types.ts +151 -125
  276. package/src/realtime/rooms-forwarder.ts +104 -79
  277. package/src/realtime/runtime-api.ts +74 -49
  278. package/src/realtime/runtime-opener.ts +61 -61
  279. package/src/realtime/social-forwarder.ts +77 -72
  280. package/src/realtime/social-types.ts +86 -78
  281. package/src/realtime/types.ts +54 -53
  282. package/src/realtime/wire-types.ts +79 -72
  283. package/src/realtime/world-types.ts +20 -0
  284. package/src/rooms.ts +10 -10
  285. package/src/social.ts +19 -18
  286. package/src/turns-kit/check-rules.ts +281 -0
  287. package/src/turns-rules.ts +12 -0
  288. package/src/turns.ts +14 -0
  289. package/src/types.ts +13 -13
  290. package/src/uistack/page-stack.ts +45 -45
  291. package/src/uistack/play-suspension.ts +82 -82
  292. package/src/uistack/types.ts +76 -76
  293. package/src/uistack/ui-stack.ts +144 -144
@@ -1,121 +1,126 @@
1
- /**
2
- * The bridge from the forwarders to the social runtime, which the menu loads
3
- * after the page (and which may never load). The forwarders stay thin so a
4
- * game's bundled copy never goes stale:
5
- *
6
- * - listeners are kept here and attached whenever the runtime appears;
7
- * - "latest state" calls are remembered and replayed when it appears;
8
- * - calls that return data wait up to {@link RUNTIME_WAIT_MS}, then reject
9
- * with `OfflineError`;
10
- * - with no `window.__HUB__` (a page the hub didn't inject) waits end at once.
11
- */
12
- import { hubLink } from './hub-link';
13
- import { findRealtimeApi, isHubInjectedPage } from './page-runtime';
14
- import type { RealtimeApi } from './runtime-api';
15
- import { REALTIME_READY_EVENT } from './runtime-api';
16
-
17
- /** How long data and UI calls wait for the social runtime before giving up. */
18
- export const RUNTIME_WAIT_MS = 15_000;
19
-
20
- /** Something to run with the runtime once it exists. */
21
- type RuntimeCallback = (realtime: RealtimeApi) => void;
22
-
23
- /** Resolves the runtime's realtime API, waiting for it to load when necessary. */
24
- export class RealtimeBridge {
25
- private pendingCallbacks: RuntimeCallback[] = [];
26
-
27
- constructor() {
28
- if (typeof window !== 'undefined') {
29
- window.addEventListener(REALTIME_READY_EVENT, () => {
30
- this.runPendingCallbacks();
31
- });
32
- }
33
- }
34
-
35
- /** The runtime's API if it has loaded. */
36
- current(): RealtimeApi | null {
37
- return findRealtimeApi();
38
- }
39
-
40
- /** Run `callback` as soon as the runtime exists (now, or whenever it loads). */
41
- onReady(callback: RuntimeCallback): void {
42
- const realtime = this.current();
43
- if (realtime === null) {
44
- this.pendingCallbacks.push(callback);
45
- } else {
46
- callback(realtime);
47
- }
48
- }
49
-
50
- /** The runtime, waiting up to {@link RUNTIME_WAIT_MS}; rejects with `OfflineError`. */
51
- whenReady(): Promise<RealtimeApi> {
52
- const realtime = this.current();
53
- if (realtime !== null) {
54
- return Promise.resolve(realtime);
55
- }
56
- if (!isHubInjectedPage()) {
57
- return Promise.reject(hubLink().createOfflineError('Online features are not available on this page.'));
58
- }
59
- return new Promise((resolve, reject) => {
60
- const resolveWhenReady: RuntimeCallback = (loaded) => {
61
- clearTimeout(timer);
62
- resolve(loaded);
63
- };
64
- const timer = setTimeout(() => {
65
- this.pendingCallbacks = this.pendingCallbacks.filter((callback) => callback !== resolveWhenReady);
66
- reject(hubLink().createOfflineError('Online features did not load.'));
67
- }, RUNTIME_WAIT_MS);
68
- this.pendingCallbacks.push(resolveWhenReady);
69
- });
70
- }
71
-
72
- /** Attach a listener now or on load; the returned function detaches it. */
73
- subscribe(attach: (realtime: RealtimeApi) => () => void): () => void {
74
- let detach: (() => void) | null = null;
75
- let isCancelled = false;
76
- this.onReady((realtime) => {
77
- if (!isCancelled) {
78
- detach = attach(realtime);
79
- }
80
- });
81
- return () => {
82
- isCancelled = true;
83
- detach?.();
84
- };
85
- }
86
-
87
- /** Call the runtime (waiting for it) and re-create its errors as the core client's classes. */
88
- async call<Result>(request: (realtime: RealtimeApi) => Promise<Result>): Promise<Result> {
89
- const realtime = await this.whenReady();
90
- return rethrowAsHubError(request(realtime));
91
- }
92
-
93
- private runPendingCallbacks(): void {
94
- const realtime = this.current();
95
- if (realtime === null) {
96
- return;
97
- }
98
- const callbacks = this.pendingCallbacks;
99
- this.pendingCallbacks = [];
100
- for (const callback of callbacks) {
101
- try {
102
- callback(realtime);
103
- } catch (error) {
104
- // eslint-disable-next-line no-console -- one runtime callback failing must not stop the others; surface it
105
- console.error('[hub] social runtime callback failed', error);
106
- }
107
- }
108
- }
109
- }
110
-
111
- /** The same promise, with rejections re-created as the core client's error classes. */
112
- export async function rethrowAsHubError<Result>(pending: Promise<Result>): Promise<Result> {
113
- try {
114
- return await pending;
115
- } catch (error) {
116
- throw hubLink().toHubError(error);
117
- }
118
- }
119
-
120
- /** The page's one bridge (per client bundle). */
121
- export const pageBridge = new RealtimeBridge();
1
+ /**
2
+ * The bridge from the forwarders to the social runtime, which the menu loads
3
+ * after the page (and which may never load). The forwarders stay thin so a
4
+ * game's bundled copy never goes stale:
5
+ *
6
+ * - listeners are kept here and attached whenever the runtime appears;
7
+ * - "latest state" calls are remembered and replayed when it appears;
8
+ * - calls that return data wait up to {@link RUNTIME_WAIT_MS}, then reject
9
+ * with `OfflineError`;
10
+ * - with no `window.__HUB__` (a page the hub didn't inject) waits end at once.
11
+ */
12
+ import { hubLink } from './hub-link';
13
+ import { findRealtimeApi, isHubInjectedPage } from './page-runtime';
14
+ import type { RealtimeApi } from './runtime-api';
15
+ import { REALTIME_READY_EVENT } from './runtime-api';
16
+
17
+ /** How long data and UI calls wait for the social runtime before giving up. */
18
+ export const RUNTIME_WAIT_MS = 15_000;
19
+
20
+ /** Something to run with the runtime once it exists. */
21
+ type RuntimeCallback = (realtime: RealtimeApi) => void;
22
+
23
+ /** Resolves the runtime's realtime API, waiting for it to load when necessary. */
24
+ export class RealtimeBridge {
25
+ private pendingCallbacks: RuntimeCallback[] = [];
26
+
27
+ constructor() {
28
+ if (typeof window !== 'undefined') {
29
+ window.addEventListener(REALTIME_READY_EVENT, () => {
30
+ this.runPendingCallbacks();
31
+ });
32
+ }
33
+ }
34
+
35
+ /** The runtime's API if it has loaded. */
36
+ current(): RealtimeApi | null {
37
+ return findRealtimeApi();
38
+ }
39
+
40
+ /** Run `callback` as soon as the runtime exists (now, or whenever it loads). */
41
+ onReady(callback: RuntimeCallback): void {
42
+ const realtime = this.current();
43
+ if (realtime === null) {
44
+ this.pendingCallbacks.push(callback);
45
+ } else {
46
+ callback(realtime);
47
+ }
48
+ }
49
+
50
+ /** The runtime, waiting up to {@link RUNTIME_WAIT_MS}; rejects with `OfflineError`. */
51
+ whenReady(): Promise<RealtimeApi> {
52
+ const realtime = this.current();
53
+ if (realtime !== null) {
54
+ return Promise.resolve(realtime);
55
+ }
56
+ if (!isHubInjectedPage()) {
57
+ return Promise.reject(hubLink().createOfflineError('Online features are not available on this page.'));
58
+ }
59
+ return new Promise((resolve, reject) => {
60
+ const resolveWhenReady: RuntimeCallback = (loaded) => {
61
+ clearTimeout(timer);
62
+ resolve(loaded);
63
+ };
64
+ const timer = setTimeout(() => {
65
+ this.pendingCallbacks = this.pendingCallbacks.filter((callback) => callback !== resolveWhenReady);
66
+ reject(hubLink().createOfflineError('Online features did not load.'));
67
+ }, RUNTIME_WAIT_MS);
68
+ this.pendingCallbacks.push(resolveWhenReady);
69
+ });
70
+ }
71
+
72
+ /** Attach a listener now or on load; the returned function detaches it. */
73
+ subscribe(attach: (realtime: RealtimeApi) => () => void): () => void {
74
+ let detach: (() => void) | null = null;
75
+ let isCancelled = false;
76
+ this.onReady((realtime) => {
77
+ if (!isCancelled) {
78
+ detach = attach(realtime);
79
+ }
80
+ });
81
+ return () => {
82
+ isCancelled = true;
83
+ detach?.();
84
+ };
85
+ }
86
+
87
+ /** Call the runtime (waiting for it) and re-create its errors as the core client's classes. */
88
+ async call<Result>(request: (realtime: RealtimeApi) => Promise<Result>): Promise<Result> {
89
+ const realtime = await this.whenReady();
90
+ return rethrowAsHubError(request(realtime));
91
+ }
92
+
93
+ private runPendingCallbacks(): void {
94
+ const realtime = this.current();
95
+ if (realtime === null) {
96
+ return;
97
+ }
98
+ const callbacks = this.pendingCallbacks;
99
+ this.pendingCallbacks = [];
100
+ for (const callback of callbacks) {
101
+ try {
102
+ callback(realtime);
103
+ } catch (error) {
104
+ // eslint-disable-next-line no-console -- one runtime callback failing must not stop the others; surface it
105
+ console.error('[hub] social runtime callback failed', error);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ /** The same promise, with rejections re-created as the core client's error classes. */
112
+ export async function rethrowAsHubError<Result>(pending: Promise<Result>): Promise<Result> {
113
+ try {
114
+ return await pending;
115
+ } catch (error) {
116
+ throw hubLink().toHubError(error);
117
+ }
118
+ }
119
+
120
+ /** The rejection for a feature the page's social runtime is too old to have (version 3 additions). */
121
+ export function outdatedRuntime(feature: string): Error {
122
+ return hubLink().createOfflineError(`${feature} needs the newest hub. Reload the page to update.`);
123
+ }
124
+
125
+ /** The page's one bridge (per client bundle). */
126
+ export const pageBridge = new RealtimeBridge();
@@ -1,29 +1,29 @@
1
- /**
2
- * Checks for objects another bundle put on the page (a game's copy of the
3
- * client, the host's `/_hub/hub.js`, the social runtime, a v1 vendored copy).
4
- * Their classes are not ours, so they are recognised by shape.
5
- */
6
-
7
- /** A function of any arity, as seen from outside. */
8
- type AnyMethod = (...args: never[]) => void;
9
-
10
- /** Whether `candidate` is a non-null object. */
11
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: an object from another bundle on the page
12
- export function isObject(candidate: unknown): candidate is object {
13
- return typeof candidate === 'object' && candidate !== null;
14
- }
15
-
16
- /** Whether `candidate` is an object with a function under every one of `names`. */
17
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: an object from another bundle on the page
18
- export function hasMethods<Name extends string>(candidate: unknown, names: readonly Name[]): candidate is Record<Name, AnyMethod> {
19
- if (!isObject(candidate)) {
20
- return false;
21
- }
22
- return names.every((name) => name in candidate && typeof Reflect.get(candidate, name) === 'function');
23
- }
24
-
25
- /** The value under `key` on another bundle's object, or `undefined`. */
26
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: reads a field of another bundle's object, checked by the caller
27
- export function readField(candidate: unknown, key: string): unknown {
28
- return isObject(candidate) ? Reflect.get(candidate, key) : undefined;
29
- }
1
+ /**
2
+ * Checks for objects another bundle put on the page (a game's copy of the
3
+ * client, the host's `/_hub/hub.js`, the social runtime, a v1 vendored copy).
4
+ * Their classes are not ours, so they are recognised by shape.
5
+ */
6
+
7
+ /** A function of any arity, as seen from outside. */
8
+ type AnyMethod = (...args: never[]) => void;
9
+
10
+ /** Whether `candidate` is a non-null object. */
11
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: an object from another bundle on the page
12
+ export function isObject(candidate: unknown): candidate is object {
13
+ return typeof candidate === 'object' && candidate !== null;
14
+ }
15
+
16
+ /** Whether `candidate` is an object with a function under every one of `names`. */
17
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: an object from another bundle on the page
18
+ export function hasMethods<Name extends string>(candidate: unknown, names: readonly Name[]): candidate is Record<Name, AnyMethod> {
19
+ if (!isObject(candidate)) {
20
+ return false;
21
+ }
22
+ return names.every((name) => name in candidate && typeof Reflect.get(candidate, name) === 'function');
23
+ }
24
+
25
+ /** The value under `key` on another bundle's object, or `undefined`. */
26
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: reads a field of another bundle's object, checked by the caller
27
+ export function readField(candidate: unknown, key: string): unknown {
28
+ return isObject(candidate) ? Reflect.get(candidate, key) : undefined;
29
+ }
@@ -1,48 +1,48 @@
1
- /**
2
- * A minimal top-centre notice for when the social runtime (which owns the
3
- * real toasts) isn't available: plain DOM with inline styles in the hub
4
- * palette, one at a time.
5
- */
6
- import { pageBody } from '../overlay/dom';
7
-
8
- const NOTICE_ELEMENT_ID = 'hub-fallback-notice';
9
- const NOTICE_STYLE = 'position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;'
10
- + 'max-width:min(92vw,420px);padding:10px 14px;border-radius:10px;font:13px/1.4 "Space Mono",ui-monospace,monospace;'
11
- + 'background:#11141c;color:#e8e6df;border:1px solid #2a3142;box-shadow:0 8px 24px rgba(0,0,0,.5);pointer-events:none;';
12
-
13
- let hideTimer: ReturnType<typeof setTimeout> | null = null;
14
-
15
- /** Show `text`; it hides itself after `visibleMs` (0 = until replaced or hidden). */
16
- export function showFallbackNotice(text: string, visibleMs = 0): void {
17
- if (typeof document === 'undefined') {
18
- return;
19
- }
20
- const body = pageBody();
21
- if (body === null) {
22
- return;
23
- }
24
- let notice = document.getElementById(NOTICE_ELEMENT_ID);
25
- if (notice === null) {
26
- notice = document.createElement('div');
27
- notice.id = NOTICE_ELEMENT_ID;
28
- notice.setAttribute('role', 'status');
29
- notice.style.cssText = NOTICE_STYLE;
30
- body.appendChild(notice);
31
- }
32
- notice.textContent = text;
33
- if (hideTimer !== null) {
34
- clearTimeout(hideTimer);
35
- }
36
- hideTimer = visibleMs > 0 ? setTimeout(hideFallbackNotice, visibleMs) : null;
37
- }
38
-
39
- /** Remove the notice. */
40
- export function hideFallbackNotice(): void {
41
- if (hideTimer !== null) {
42
- clearTimeout(hideTimer);
43
- hideTimer = null;
44
- }
45
- if (typeof document !== 'undefined') {
46
- document.getElementById(NOTICE_ELEMENT_ID)?.remove();
47
- }
48
- }
1
+ /**
2
+ * A minimal top-centre notice for when the social runtime (which owns the
3
+ * real toasts) isn't available: plain DOM with inline styles in the hub
4
+ * palette, one at a time.
5
+ */
6
+ import { pageBody } from '../overlay/dom';
7
+
8
+ const NOTICE_ELEMENT_ID = 'hub-fallback-notice';
9
+ const NOTICE_STYLE = 'position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;'
10
+ + 'max-width:min(92vw,420px);padding:10px 14px;border-radius:10px;font:13px/1.4 "Space Mono",ui-monospace,monospace;'
11
+ + 'background:#11141c;color:#e8e6df;border:1px solid #2a3142;box-shadow:0 8px 24px rgba(0,0,0,.5);pointer-events:none;';
12
+
13
+ let hideTimer: ReturnType<typeof setTimeout> | null = null;
14
+
15
+ /** Show `text`; it hides itself after `visibleMs` (0 = until replaced or hidden). */
16
+ export function showFallbackNotice(text: string, visibleMs = 0): void {
17
+ if (typeof document === 'undefined') {
18
+ return;
19
+ }
20
+ const body = pageBody();
21
+ if (body === null) {
22
+ return;
23
+ }
24
+ let notice = document.getElementById(NOTICE_ELEMENT_ID);
25
+ if (notice === null) {
26
+ notice = document.createElement('div');
27
+ notice.id = NOTICE_ELEMENT_ID;
28
+ notice.setAttribute('role', 'status');
29
+ notice.style.cssText = NOTICE_STYLE;
30
+ body.appendChild(notice);
31
+ }
32
+ notice.textContent = text;
33
+ if (hideTimer !== null) {
34
+ clearTimeout(hideTimer);
35
+ }
36
+ hideTimer = visibleMs > 0 ? setTimeout(hideFallbackNotice, visibleMs) : null;
37
+ }
38
+
39
+ /** Remove the notice. */
40
+ export function hideFallbackNotice(): void {
41
+ if (hideTimer !== null) {
42
+ clearTimeout(hideTimer);
43
+ hideTimer = null;
44
+ }
45
+ if (typeof document !== 'undefined') {
46
+ document.getElementById(NOTICE_ELEMENT_ID)?.remove();
47
+ }
48
+ }
@@ -1,65 +1,65 @@
1
- /**
2
- * What the forwarders need from the core client: its error classes (so a
3
- * runtime rejection is an `instanceof HubError` in the game) and the direct
4
- * own-server ticket request. The core client links itself at load with
5
- * {@link linkRealtimeToHub}; until then plain errors with the same fields are
6
- * used, so a forwarder imported on its own still works.
7
- */
8
- import { readField } from './cross-bundle';
9
- import type { RealtimeErrorShape } from './wire-types';
10
-
11
- /** The core client's side of the link. */
12
- export interface RealtimeHubLink {
13
- /** Re-create an error from the runtime (another bundle) as this client's `HubError` family. */
14
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: a rejection from another bundle
15
- toHubError(error: unknown): Error;
16
- /** This client's `OfflineError` with a message. */
17
- createOfflineError(message: string): Error;
18
- /** A 60-second identity ticket for this game's own server (`POST /mp/ticket`), straight from the hub. */
19
- getGameServerTicket(): Promise<string>;
20
- }
21
-
22
- /** Copy the error-body fields of another bundle's error onto a plain `Error`. */
23
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: a rejection from another bundle
24
- function copyAsPlainError(error: unknown): RealtimeErrorShape {
25
- if (error instanceof Error) {
26
- return error;
27
- }
28
- const message = readField(error, 'message');
29
- const copy: RealtimeErrorShape = new Error(typeof message === 'string' ? message : String(error));
30
- for (const key of ['status', 'retryAfterMs'] as const) {
31
- const field = readField(error, key);
32
- if (typeof field === 'number') {
33
- copy[key] = field;
34
- }
35
- }
36
- for (const key of ['code', 'reason', 'hint'] as const) {
37
- const field = readField(error, key);
38
- if (typeof field === 'string') {
39
- copy[key] = field;
40
- }
41
- }
42
- return copy;
43
- }
44
-
45
- const standaloneLink: RealtimeHubLink = {
46
- toHubError: copyAsPlainError,
47
- createOfflineError(message) {
48
- const offline: RealtimeErrorShape = new Error(message);
49
- offline.code = 'offline';
50
- return offline;
51
- },
52
- getGameServerTicket: () => Promise.reject(new Error('Hub: import @diffenderfer-games/hub to request game-server tickets.')),
53
- };
54
-
55
- let currentLink: RealtimeHubLink = standaloneLink;
56
-
57
- /** Called once by the core client so forwarder errors use its classes and tickets use its REST client. */
58
- export function linkRealtimeToHub(link: RealtimeHubLink): void {
59
- currentLink = link;
60
- }
61
-
62
- /** The link in effect. */
63
- export function hubLink(): RealtimeHubLink {
64
- return currentLink;
65
- }
1
+ /**
2
+ * What the forwarders need from the core client: its error classes (so a
3
+ * runtime rejection is an `instanceof HubError` in the game) and the direct
4
+ * own-server ticket request. The core client links itself at load with
5
+ * {@link linkRealtimeToHub}; until then plain errors with the same fields are
6
+ * used, so a forwarder imported on its own still works.
7
+ */
8
+ import { readField } from './cross-bundle';
9
+ import type { RealtimeErrorShape } from './wire-types';
10
+
11
+ /** The core client's side of the link. */
12
+ export interface RealtimeHubLink {
13
+ /** Re-create an error from the runtime (another bundle) as this client's `HubError` family. */
14
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: a rejection from another bundle
15
+ toHubError(error: unknown): Error;
16
+ /** This client's `OfflineError` with a message. */
17
+ createOfflineError(message: string): Error;
18
+ /** A 60-second identity ticket for this game's own server (`POST /mp/ticket`), straight from the hub. */
19
+ getGameServerTicket(): Promise<string>;
20
+ }
21
+
22
+ /** Copy the error-body fields of another bundle's error onto a plain `Error`. */
23
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: a rejection from another bundle
24
+ function copyAsPlainError(error: unknown): RealtimeErrorShape {
25
+ if (error instanceof Error) {
26
+ return error;
27
+ }
28
+ const message = readField(error, 'message');
29
+ const copy: RealtimeErrorShape = new Error(typeof message === 'string' ? message : String(error));
30
+ for (const key of ['status', 'retryAfterMs'] as const) {
31
+ const field = readField(error, key);
32
+ if (typeof field === 'number') {
33
+ copy[key] = field;
34
+ }
35
+ }
36
+ for (const key of ['code', 'reason', 'hint'] as const) {
37
+ const field = readField(error, key);
38
+ if (typeof field === 'string') {
39
+ copy[key] = field;
40
+ }
41
+ }
42
+ return copy;
43
+ }
44
+
45
+ const standaloneLink: RealtimeHubLink = {
46
+ toHubError: copyAsPlainError,
47
+ createOfflineError(message) {
48
+ const offline: RealtimeErrorShape = new Error(message);
49
+ offline.code = 'offline';
50
+ return offline;
51
+ },
52
+ getGameServerTicket: () => Promise.reject(new Error('Hub: import @diffenderfer-games/hub to request game-server tickets.')),
53
+ };
54
+
55
+ let currentLink: RealtimeHubLink = standaloneLink;
56
+
57
+ /** Called once by the core client so forwarder errors use its classes and tickets use its REST client. */
58
+ export function linkRealtimeToHub(link: RealtimeHubLink): void {
59
+ currentLink = link;
60
+ }
61
+
62
+ /** The link in effect. */
63
+ export function hubLink(): RealtimeHubLink {
64
+ return currentLink;
65
+ }
@@ -1,33 +1,33 @@
1
- /**
2
- * The "latest state" calls (presence, the busy flag, quiet mode): only the
3
- * last value matters, so each is remembered and replayed, in that order,
4
- * when the social runtime loads.
5
- */
6
- import type { PresenceInput } from '@diffenderfer-games/hub-contract';
7
- import { pageBridge } from './bridge';
8
- import type { BusyOptions } from './multiplayer-types';
9
-
10
- /** The busy flag as last set. */
11
- interface BusyState {
12
- readonly isBusy: boolean;
13
- readonly options: BusyOptions;
14
- }
15
-
16
- /** The latest state calls, as last made on this page. */
17
- export const latestState: { presence: PresenceInput | null; busy: BusyState | null; isQuiet: boolean | null } = {
18
- presence: null,
19
- busy: null,
20
- isQuiet: null,
21
- };
22
-
23
- pageBridge.onReady((realtime) => {
24
- if (latestState.presence !== null) {
25
- realtime.presence.set(latestState.presence);
26
- }
27
- if (latestState.busy !== null) {
28
- realtime.mp.setBusy(latestState.busy.isBusy, latestState.busy.options);
29
- }
30
- if (latestState.isQuiet !== null) {
31
- realtime.notify.setQuiet(latestState.isQuiet);
32
- }
33
- });
1
+ /**
2
+ * The "latest state" calls (presence, the busy flag, quiet mode): only the
3
+ * last value matters, so each is remembered and replayed, in that order,
4
+ * when the social runtime loads.
5
+ */
6
+ import type { PresenceInput } from '@diffenderfer-games/hub-contract';
7
+ import { pageBridge } from './bridge';
8
+ import type { BusyOptions } from './multiplayer-types';
9
+
10
+ /** The busy flag as last set. */
11
+ interface BusyState {
12
+ readonly isBusy: boolean;
13
+ readonly options: BusyOptions;
14
+ }
15
+
16
+ /** The latest state calls, as last made on this page. */
17
+ export const latestState: { presence: PresenceInput | null; busy: BusyState | null; isQuiet: boolean | null } = {
18
+ presence: null,
19
+ busy: null,
20
+ isQuiet: null,
21
+ };
22
+
23
+ pageBridge.onReady((realtime) => {
24
+ if (latestState.presence !== null) {
25
+ realtime.presence.set(latestState.presence);
26
+ }
27
+ if (latestState.busy !== null) {
28
+ realtime.mp.setBusy(latestState.busy.isBusy, latestState.busy.options);
29
+ }
30
+ if (latestState.isQuiet !== null) {
31
+ realtime.notify.setQuiet(latestState.isQuiet);
32
+ }
33
+ });