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

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 (154) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -21
  3. package/README.md +206 -139
  4. package/dist/auto.d.ts +6 -6
  5. package/dist/auto.js +2 -2
  6. package/dist/doctor/cli.mjs +62 -25
  7. package/dist/doctor/cli.mjs.map +1 -1
  8. package/dist/index.d.ts +6 -6
  9. package/dist/index.js +2 -2
  10. package/dist/mp.d.ts +3 -3
  11. package/dist/race.d.ts +1 -1
  12. package/dist/rooms.d.ts +3 -3
  13. package/dist/shared/bridge.js +9 -3
  14. package/dist/shared/bridge.js.map +1 -1
  15. package/dist/shared/client-version.gen.js +1 -1
  16. package/dist/shared/daily.d.ts +10 -4
  17. package/dist/shared/daily.d.ts.map +1 -1
  18. package/dist/shared/daily.js +12 -2
  19. package/dist/shared/daily.js.map +1 -1
  20. package/dist/shared/globals.js +8 -3
  21. package/dist/shared/globals.js.map +1 -1
  22. package/dist/shared/index.d.ts +314 -35
  23. package/dist/shared/index.d.ts.map +1 -1
  24. package/dist/shared/input.js +7 -2
  25. package/dist/shared/input.js.map +1 -1
  26. package/dist/shared/latest-state.js +1 -1
  27. package/dist/shared/mp.js +10 -2
  28. package/dist/shared/mp.js.map +1 -1
  29. package/dist/shared/multiplayer-types.d.ts +15 -2
  30. package/dist/shared/multiplayer-types.d.ts.map +1 -1
  31. package/dist/shared/race-types.d.ts +1 -1
  32. package/dist/shared/race.js +1 -1
  33. package/dist/shared/room-types.d.ts +28 -3
  34. package/dist/shared/room-types.d.ts.map +1 -1
  35. package/dist/shared/rooms.d.ts.map +1 -1
  36. package/dist/shared/rooms.js +10 -2
  37. package/dist/shared/rooms.js.map +1 -1
  38. package/dist/shared/runtime-opener.js +1 -1
  39. package/dist/shared/social.d.ts +23 -8
  40. package/dist/shared/social.d.ts.map +1 -1
  41. package/dist/shared/social.js +6 -1
  42. package/dist/shared/social.js.map +1 -1
  43. package/dist/shared/src.js +1588 -388
  44. package/dist/shared/src.js.map +1 -1
  45. package/dist/shared/wire-types.d.ts +6 -2
  46. package/dist/shared/wire-types.d.ts.map +1 -1
  47. package/dist/social.d.ts +5 -5
  48. package/dist/social.js +1 -1
  49. package/package.json +89 -89
  50. package/src/auto.ts +39 -39
  51. package/src/core/connection.ts +394 -299
  52. package/src/core/errors.ts +147 -147
  53. package/src/core/game-data.ts +40 -38
  54. package/src/core/hub-economy.ts +216 -179
  55. package/src/core/hub-progress.ts +202 -205
  56. package/src/core/hub.ts +133 -131
  57. package/src/core/menu-control.ts +61 -61
  58. package/src/core/offline/events.ts +50 -50
  59. package/src/core/offline/favorites.ts +21 -21
  60. package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
  61. package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
  62. package/src/core/offline/hub-kinds/game-fields.ts +2 -0
  63. package/src/core/offline/hub-kinds/index.ts +35 -0
  64. package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
  65. package/src/core/offline/hub-kinds/progress-kinds.ts +148 -0
  66. package/src/core/offline/kind-adapters.ts +48 -0
  67. package/src/core/offline/local-inventory.ts +64 -0
  68. package/src/core/offline/local-store.ts +259 -261
  69. package/src/core/offline/offline-api.ts +65 -0
  70. package/src/core/offline/outbox-entry.ts +59 -0
  71. package/src/core/offline/outbox-kinds.ts +78 -0
  72. package/src/core/offline/outbox-merge.ts +39 -0
  73. package/src/core/offline/outbox-replayer.ts +141 -0
  74. package/src/core/offline/outbox-store.ts +85 -0
  75. package/src/core/offline/outbox.ts +232 -0
  76. package/src/core/offline/queue.ts +126 -171
  77. package/src/core/offline/replay-lease.ts +87 -0
  78. package/src/core/offline/replay-policy.ts +52 -0
  79. package/src/core/page.ts +74 -74
  80. package/src/core/rest/endpoint-types.ts +22 -22
  81. package/src/core/rest/rest-client.ts +163 -152
  82. package/src/core/rest/routes.gen.ts +108 -87
  83. package/src/core/storage/json-guards.ts +80 -74
  84. package/src/core/storage/key-value.ts +66 -66
  85. package/src/core/ticket.ts +43 -43
  86. package/src/core/types.ts +134 -134
  87. package/src/core/version/client-version.gen.ts +1 -1
  88. package/src/core/version/hub-version.ts +62 -62
  89. package/src/core/version/semver.ts +97 -97
  90. package/src/daily/daily-system.ts +168 -161
  91. package/src/daily/page-daily.ts +36 -36
  92. package/src/daily/types.ts +68 -67
  93. package/src/daily.ts +10 -10
  94. package/src/doctor/checks.ts +86 -76
  95. package/src/doctor/cli.ts +98 -106
  96. package/src/doctor/hub-version-fetch.ts +43 -0
  97. package/src/doctor/tsconfig.json +12 -12
  98. package/src/globals.ts +115 -110
  99. package/src/index.ts +55 -50
  100. package/src/input/device-state.ts +146 -146
  101. package/src/input/edges.ts +55 -55
  102. package/src/input/gamepad.ts +61 -61
  103. package/src/input/input-events.ts +32 -32
  104. package/src/input/input-registry.ts +171 -164
  105. package/src/input/input-system.ts +393 -393
  106. package/src/input/legacy-controls.ts +64 -64
  107. package/src/input/nav-state.ts +191 -191
  108. package/src/input/navigator.ts +171 -171
  109. package/src/input/on-screen-controls.ts +101 -101
  110. package/src/input/page-control.ts +43 -43
  111. package/src/input/page-input.ts +193 -193
  112. package/src/input/player-remaps.ts +77 -77
  113. package/src/input/source-values.ts +116 -116
  114. package/src/input/spatial.ts +47 -47
  115. package/src/input/types.ts +331 -331
  116. package/src/input/zero-config.ts +177 -177
  117. package/src/input.ts +22 -22
  118. package/src/mp.ts +10 -10
  119. package/src/overlay/controls-panel.ts +163 -163
  120. package/src/overlay/dom.ts +46 -46
  121. package/src/overlay/layout-clusters.ts +162 -162
  122. package/src/overlay/layout-geometry.ts +136 -136
  123. package/src/overlay/layout.ts +203 -203
  124. package/src/overlay/listener-scope.ts +23 -23
  125. package/src/overlay/on-screen-keyboard.ts +137 -137
  126. package/src/overlay/overlay.ts +208 -208
  127. package/src/overlay/styles.ts +9 -9
  128. package/src/overlay/virtual-controls.ts +262 -262
  129. package/src/race.ts +10 -10
  130. package/src/realtime/bridge.ts +126 -121
  131. package/src/realtime/cross-bundle.ts +29 -29
  132. package/src/realtime/fallback-notice.ts +48 -48
  133. package/src/realtime/hub-link.ts +65 -65
  134. package/src/realtime/latest-state.ts +33 -33
  135. package/src/realtime/multiplayer-forwarder.ts +40 -28
  136. package/src/realtime/multiplayer-types.ts +66 -52
  137. package/src/realtime/page-runtime.ts +60 -60
  138. package/src/realtime/race-forwarder.ts +59 -59
  139. package/src/realtime/race-types.ts +132 -132
  140. package/src/realtime/room-types.ts +151 -125
  141. package/src/realtime/rooms-forwarder.ts +91 -79
  142. package/src/realtime/runtime-api.ts +60 -49
  143. package/src/realtime/runtime-opener.ts +61 -61
  144. package/src/realtime/social-forwarder.ts +77 -72
  145. package/src/realtime/social-types.ts +86 -78
  146. package/src/realtime/types.ts +53 -53
  147. package/src/realtime/wire-types.ts +79 -72
  148. package/src/rooms.ts +10 -10
  149. package/src/social.ts +18 -18
  150. package/src/types.ts +13 -13
  151. package/src/uistack/page-stack.ts +45 -45
  152. package/src/uistack/play-suspension.ts +82 -82
  153. package/src/uistack/types.ts +76 -76
  154. 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
+ });