@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
package/src/core/hub.ts CHANGED
@@ -1,131 +1,133 @@
1
- /**
2
- * The `Hub` client: every REST method (in `hub-progress.ts` and
3
- * `hub-economy.ts`), the hub's version, and the page-wide namespaces as lazy
4
- * getters (`hub.input`, `hub.daily`, `hub.social`, `hub.mp`, `hub.rooms`,
5
- * `hub.race`, ...), so v1 code such as `hub.input.define(...)` works unchanged.
6
- */
7
- import type { HubVersion } from '@diffenderfer-games/hub-contract';
8
- import { getDaily, type DailyHost, type DailySystem } from '../daily';
9
- import { getInput, getUiStack, type InputSystem } from '../input';
10
- import { mp } from '../mp';
11
- import { race } from '../race';
12
- import { rooms } from '../rooms';
13
- import { linkRealtimeToHub, notify, presence, social } from '../social';
14
- import { OfflineError, toHubError } from './errors';
15
- import { HubEconomy } from './hub-economy';
16
- import { hubMenu, type HubMenuControl } from './menu-control';
17
- import { emitWindowEvent } from './page';
18
- import { fetchGameServerTicket } from './ticket';
19
- import { clientSupport, readHubVersion, type ClientSupport } from './version/hub-version';
20
- import { HUB_CLIENT_VERSION } from './version/client-version.gen';
21
- import type { HubOptions } from './types';
22
-
23
- /** The hub client. Use the ready-made {@link hub}, or `createHub()` for another slug or API base. */
24
- export class Hub extends HubEconomy {
25
- /** This client's package version (sent as `x-hub-client`). */
26
- readonly clientVersion: string = HUB_CLIENT_VERSION;
27
-
28
- /**
29
- * The hub's version and the client versions it supports (`GET /_api/version`),
30
- * read once per page. `null` for a hub from before versioning.
31
- */
32
- hubVersion(): Promise<HubVersion | null> {
33
- return readHubVersion(this.rest);
34
- }
35
-
36
- /** How the hub regards this client: `supported`, `deprecated`, `unsupported`, or `unknown` (an older hub). */
37
- async clientSupport(): Promise<ClientSupport> {
38
- return clientSupport(await this.hubVersion());
39
- }
40
-
41
- /**
42
- * The input system (keyboard, touch and gamepad): `hub.input.define({ ... })`,
43
- * `hub.input.value('jump')`. Page-wide; remaps are saved locally and to the
44
- * reserved `__controls` save slot.
45
- */
46
- get input(): InputSystem {
47
- return getInput({
48
- slug: this.slug,
49
- saveToCloud: (overrides) => {
50
- void this.putSave('__controls', overrides).catch(() => undefined);
51
- },
52
- });
53
- }
54
-
55
- /** Hub UI covering the game (menu, dialogs, chat): open/close events and `autoPause`. Page-wide. */
56
- get overlay(): ReturnType<typeof getUiStack> {
57
- return getUiStack();
58
- }
59
-
60
- /** Show or hide the injected hub menu button (page-wide). */
61
- get menu(): HubMenuControl {
62
- return hubMenu;
63
- }
64
-
65
- /**
66
- * Daily challenges: call `hub.daily.define({ play })` once at load and
67
- * `hub.daily.complete({ ... })` on finish. Page-wide, shared with the
68
- * injected menu, which can start a clicked day on the same instance.
69
- */
70
- get daily(): DailySystem {
71
- return getDaily(this.dailyHost());
72
- }
73
-
74
- /** Tell friends what the player is doing in this game. */
75
- get presence(): typeof presence {
76
- return presence;
77
- }
78
-
79
- /** Friends, players and the hub's social dialogs. */
80
- get social(): typeof social {
81
- return social;
82
- }
83
-
84
- /** Invites, launches, the leave guard and own-server tickets. */
85
- get mp(): typeof mp {
86
- return mp;
87
- }
88
-
89
- /** Hub relay rooms, for games without a server. */
90
- get rooms(): typeof rooms {
91
- return rooms;
92
- }
93
-
94
- /** Toast behaviour during intense play. */
95
- get notify(): typeof notify {
96
- return notify;
97
- }
98
-
99
- /** Races: a single-player game becomes multiplayer by racing. */
100
- get race(): typeof race {
101
- return race;
102
- }
103
-
104
- /** What the daily system needs from this client: its REST calls and window events. */
105
- private dailyHost(): DailyHost {
106
- return {
107
- slug: this.slug,
108
- status: () => this.rest.call('getGameDailyChallenges', this.gameFields()),
109
- all: () => this.rest.call('listDailyChallengesForAllGames'),
110
- start: (day) => this.rest.call('startDailyChallenge', { ...this.gameFields(), day }),
111
- complete: (day, token, meta) => this.rest.call('completeDailyChallenge', { ...this.gameFields(), day, token, meta }),
112
- emit: (eventName, detail) => {
113
- emitWindowEvent(eventName, detail);
114
- },
115
- };
116
- }
117
- }
118
-
119
- linkRealtimeToHub({
120
- toHubError,
121
- createOfflineError: (message) => new OfflineError(message),
122
- getGameServerTicket: fetchGameServerTicket,
123
- });
124
-
125
- /** A ready-to-use client, with the slug and API base detected from the page. */
126
- export const hub: Hub = new Hub();
127
-
128
- /** Create a client for another slug or API base, or with other offline behaviour. */
129
- export function createHub(options: HubOptions = {}): Hub {
130
- return new Hub(options);
131
- }
1
+ /**
2
+ * The `Hub` client: every REST method (in `hub-progress.ts` and
3
+ * `hub-economy.ts`), the hub's version, and the page-wide namespaces as lazy
4
+ * getters (`hub.input`, `hub.daily`, `hub.social`, `hub.mp`, `hub.rooms`,
5
+ * `hub.race`, ...), so v1 code such as `hub.input.define(...)` works unchanged.
6
+ */
7
+ import type { HubVersion } from '@diffenderfer-games/hub-contract';
8
+ import { getDaily, type DailyCompleteResponse, type DailyHost, type DailySystem } from '../daily';
9
+ import { getInput, getUiStack, type InputSystem } from '../input';
10
+ import { mp } from '../mp';
11
+ import { race } from '../race';
12
+ import { rooms } from '../rooms';
13
+ import { linkRealtimeToHub, notify, presence, social } from '../social';
14
+ import { OfflineError, toHubError } from './errors';
15
+ import { HubEconomy } from './hub-economy';
16
+ import { DAILY_KIND } from './offline/hub-kinds';
17
+ import { readDailyCompletion } from './offline/hub-kinds/daily-kind';
18
+ import { hubMenu, type HubMenuControl } from './menu-control';
19
+ import { emitWindowEvent } from './page';
20
+ import { fetchGameServerTicket } from './ticket';
21
+ import { clientSupport, readHubVersion, type ClientSupport } from './version/hub-version';
22
+ import { HUB_CLIENT_VERSION } from './version/client-version.gen';
23
+ import type { HubOptions } from './types';
24
+
25
+ /** The hub client. Use the ready-made {@link hub}, or `createHub()` for another slug or API base. */
26
+ export class Hub extends HubEconomy {
27
+ /** This client's package version (sent as `x-hub-client`). */
28
+ readonly clientVersion: string = HUB_CLIENT_VERSION;
29
+
30
+ /**
31
+ * The hub's version and the client versions it supports (`GET /_api/version`),
32
+ * read once per page. `null` for a hub from before versioning.
33
+ */
34
+ hubVersion(): Promise<HubVersion | null> {
35
+ return readHubVersion(this.rest);
36
+ }
37
+
38
+ /** How the hub regards this client: `supported`, `deprecated`, `unsupported`, or `unknown` (an older hub). */
39
+ async clientSupport(): Promise<ClientSupport> {
40
+ return clientSupport(await this.hubVersion());
41
+ }
42
+
43
+ /**
44
+ * The input system (keyboard, touch and gamepad): `hub.input.define({ ... })`,
45
+ * `hub.input.value('jump')`. Page-wide; remaps are saved locally and to the
46
+ * reserved `__controls` save slot.
47
+ */
48
+ get input(): InputSystem {
49
+ return getInput({
50
+ slug: this.slug,
51
+ saveToCloud: (overrides) => {
52
+ void this.putSave('__controls', overrides).catch(() => undefined);
53
+ },
54
+ });
55
+ }
56
+
57
+ /** Hub UI covering the game (menu, dialogs, chat): open/close events and `autoPause`. Page-wide. */
58
+ get overlay(): ReturnType<typeof getUiStack> {
59
+ return getUiStack();
60
+ }
61
+
62
+ /** Show or hide the injected hub menu button (page-wide). */
63
+ get menu(): HubMenuControl {
64
+ return hubMenu;
65
+ }
66
+
67
+ /**
68
+ * Daily challenges: call `hub.daily.define({ play })` once at load and
69
+ * `hub.daily.complete({ ... })` on finish. Page-wide, shared with the
70
+ * injected menu, which can start a clicked day on the same instance.
71
+ */
72
+ get daily(): DailySystem {
73
+ return getDaily(this.dailyHost());
74
+ }
75
+
76
+ /** Tell friends what the player is doing in this game. */
77
+ get presence(): typeof presence {
78
+ return presence;
79
+ }
80
+
81
+ /** Friends, players and the hub's social dialogs. */
82
+ get social(): typeof social {
83
+ return social;
84
+ }
85
+
86
+ /** Invites, launches, the leave guard and own-server tickets. */
87
+ get mp(): typeof mp {
88
+ return mp;
89
+ }
90
+
91
+ /** Hub relay rooms, for games without a server. */
92
+ get rooms(): typeof rooms {
93
+ return rooms;
94
+ }
95
+
96
+ /** Toast behaviour during intense play. */
97
+ get notify(): typeof notify {
98
+ return notify;
99
+ }
100
+
101
+ /** Races: a single-player game becomes multiplayer by racing. */
102
+ get race(): typeof race {
103
+ return race;
104
+ }
105
+
106
+ /** What the daily system needs from this client: its REST calls and window events. */
107
+ private dailyHost(): DailyHost {
108
+ return {
109
+ slug: this.slug,
110
+ status: () => this.rest.call('getGameDailyChallenges', this.gameFields()),
111
+ all: () => this.rest.call('listDailyChallengesForAllGames'),
112
+ start: (day) => this.rest.call('startDailyChallenge', { ...this.gameFields(), day }),
113
+ complete: (day, token, meta) => this.mutateOffline<DailyCompleteResponse | null>(DAILY_KIND, { day, token, meta }, () => null, (result) => readDailyCompletion(result)),
114
+ emit: (eventName, detail) => {
115
+ emitWindowEvent(eventName, detail);
116
+ },
117
+ };
118
+ }
119
+ }
120
+
121
+ linkRealtimeToHub({
122
+ toHubError,
123
+ createOfflineError: (message) => new OfflineError(message),
124
+ getGameServerTicket: fetchGameServerTicket,
125
+ });
126
+
127
+ /** A ready-to-use client, with the slug and API base detected from the page. */
128
+ export const hub: Hub = new Hub();
129
+
130
+ /** Create a client for another slug or API base, or with other offline behaviour. */
131
+ export function createHub(options: HubOptions = {}): Hub {
132
+ return new Hub(options);
133
+ }
@@ -1,61 +1,61 @@
1
- /**
2
- * `hub.menu`: show or hide the injected hub menu button. The menu
3
- * (`/_hub/menu.js`) may load before or after the game, and the game may
4
- * bundle its own client copy, so the shared state is a window global and a
5
- * window event rather than an object:
6
- *
7
- * - `window.__HUB_MENU_VISIBLE__` (boolean; absent means visible);
8
- * - `hub:menu-visibility` (`CustomEvent`, detail `{ visible }`).
9
- *
10
- * The menu reads the global on boot and listens for the event.
11
- */
12
- import { emitWindowEvent, isBrowserPage } from './page';
13
-
14
- declare global {
15
- interface Window {
16
- /** The last requested menu-button visibility (absent: visible). */
17
- __HUB_MENU_VISIBLE__?: boolean;
18
- }
19
- }
20
-
21
- /** Control over the injected hub menu button. */
22
- export interface HubMenuControl {
23
- /**
24
- * Show (`true`) or hide (`false`) the menu button. Hidden means faded out,
25
- * not focusable and `aria-hidden`; an open drawer stays open until closed,
26
- * and the gamepad Select button still opens it. A small corner hotspot
27
- * peeks it back for a few seconds on hover or tap, so players can always
28
- * reach it.
29
- */
30
- setVisible(isVisible: boolean): void;
31
- /** Show the menu button. */
32
- show(): void;
33
- /** Hide the menu button. */
34
- hide(): void;
35
- /** The last requested visibility (`true` unless a game hid it). */
36
- readonly visible: boolean;
37
- }
38
-
39
- /** The page's menu control (one per page; every client copy shares the window state). */
40
- export const hubMenu: HubMenuControl = {
41
- setVisible(isVisible: boolean): void {
42
- if (!isBrowserPage()) {
43
- return;
44
- }
45
- const wasVisible = window.__HUB_MENU_VISIBLE__;
46
- if (wasVisible === isVisible || (wasVisible === undefined && isVisible)) {
47
- return;
48
- }
49
- window.__HUB_MENU_VISIBLE__ = isVisible;
50
- emitWindowEvent('hub:menu-visibility', { visible: isVisible });
51
- },
52
- show(): void {
53
- hubMenu.setVisible(true);
54
- },
55
- hide(): void {
56
- hubMenu.setVisible(false);
57
- },
58
- get visible(): boolean {
59
- return !isBrowserPage() || window.__HUB_MENU_VISIBLE__ !== false;
60
- },
61
- };
1
+ /**
2
+ * `hub.menu`: show or hide the injected hub menu button. The menu
3
+ * (`/_hub/menu.js`) may load before or after the game, and the game may
4
+ * bundle its own client copy, so the shared state is a window global and a
5
+ * window event rather than an object:
6
+ *
7
+ * - `window.__HUB_MENU_VISIBLE__` (boolean; absent means visible);
8
+ * - `hub:menu-visibility` (`CustomEvent`, detail `{ visible }`).
9
+ *
10
+ * The menu reads the global on boot and listens for the event.
11
+ */
12
+ import { emitWindowEvent, isBrowserPage } from './page';
13
+
14
+ declare global {
15
+ interface Window {
16
+ /** The last requested menu-button visibility (absent: visible). */
17
+ __HUB_MENU_VISIBLE__?: boolean;
18
+ }
19
+ }
20
+
21
+ /** Control over the injected hub menu button. */
22
+ export interface HubMenuControl {
23
+ /**
24
+ * Show (`true`) or hide (`false`) the menu button. Hidden means faded out,
25
+ * not focusable and `aria-hidden`; an open drawer stays open until closed,
26
+ * and the gamepad Select button still opens it. A small corner hotspot
27
+ * peeks it back for a few seconds on hover or tap, so players can always
28
+ * reach it.
29
+ */
30
+ setVisible(isVisible: boolean): void;
31
+ /** Show the menu button. */
32
+ show(): void;
33
+ /** Hide the menu button. */
34
+ hide(): void;
35
+ /** The last requested visibility (`true` unless a game hid it). */
36
+ readonly visible: boolean;
37
+ }
38
+
39
+ /** The page's menu control (one per page; every client copy shares the window state). */
40
+ export const hubMenu: HubMenuControl = {
41
+ setVisible(isVisible: boolean): void {
42
+ if (!isBrowserPage()) {
43
+ return;
44
+ }
45
+ const wasVisible = window.__HUB_MENU_VISIBLE__;
46
+ if (wasVisible === isVisible || (wasVisible === undefined && isVisible)) {
47
+ return;
48
+ }
49
+ window.__HUB_MENU_VISIBLE__ = isVisible;
50
+ emitWindowEvent('hub:menu-visibility', { visible: isVisible });
51
+ },
52
+ show(): void {
53
+ hubMenu.setVisible(true);
54
+ },
55
+ hide(): void {
56
+ hubMenu.setVisible(false);
57
+ },
58
+ get visible(): boolean {
59
+ return !isBrowserPage() || window.__HUB_MENU_VISIBLE__ !== false;
60
+ },
61
+ };
@@ -1,50 +1,50 @@
1
- /**
2
- * Analytics events recorded while offline (`play`, `heartbeat`, custom
3
- * events), kept under `hub:<slug>:events` and sent when the hub is back.
4
- * The buffer is capped; heartbeats are dropped first, then the oldest.
5
- */
6
- import { isRecord, optionalNumber, optionalString } from '../storage/json-guards';
7
-
8
- /** The most events kept while offline. */
9
- export const EVENT_BUFFER_CAP = 100;
10
-
11
- /** One buffered event. */
12
- export interface BufferedEvent {
13
- /** The event type, as `recordGameEvent` sends it. */
14
- readonly type: string;
15
- /** When it happened (epoch ms). */
16
- readonly ts: number;
17
- }
18
-
19
- /** The well-formed events of a stored buffer. */
20
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: a buffer read back from localStorage (possibly written by a v1 client)
21
- export function readEvents(stored: unknown): BufferedEvent[] {
22
- if (!Array.isArray(stored)) {
23
- return [];
24
- }
25
- return stored.flatMap((entry): BufferedEvent[] => {
26
- if (!isRecord(entry)) {
27
- return [];
28
- }
29
- const type = optionalString(entry.type);
30
- const ts = optionalNumber(entry.ts);
31
- return type === undefined || ts === undefined ? [] : [{ type, ts }];
32
- });
33
- }
34
-
35
- /** The buffer with one more event, trimmed to {@link EVENT_BUFFER_CAP}: oldest heartbeats go first, then the oldest events. */
36
- export function appendEvent(events: readonly BufferedEvent[], incoming: BufferedEvent): BufferedEvent[] {
37
- let buffered = [...events, incoming];
38
- let excess = buffered.length - EVENT_BUFFER_CAP;
39
- if (excess <= 0) {
40
- return buffered;
41
- }
42
- buffered = buffered.filter((event) => {
43
- if (excess > 0 && event.type === 'heartbeat') {
44
- excess -= 1;
45
- return false;
46
- }
47
- return true;
48
- });
49
- return buffered.length > EVENT_BUFFER_CAP ? buffered.slice(buffered.length - EVENT_BUFFER_CAP) : buffered;
50
- }
1
+ /**
2
+ * Analytics events recorded while offline (`play`, `heartbeat`, custom
3
+ * events), kept under `hub:<slug>:events` and sent when the hub is back.
4
+ * The buffer is capped; heartbeats are dropped first, then the oldest.
5
+ */
6
+ import { isRecord, optionalNumber, optionalString } from '../storage/json-guards';
7
+
8
+ /** The most events kept while offline. */
9
+ export const EVENT_BUFFER_CAP = 100;
10
+
11
+ /** One buffered event. */
12
+ export interface BufferedEvent {
13
+ /** The event type, as `recordGameEvent` sends it. */
14
+ readonly type: string;
15
+ /** When it happened (epoch ms). */
16
+ readonly ts: number;
17
+ }
18
+
19
+ /** The well-formed events of a stored buffer. */
20
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: a buffer read back from localStorage (possibly written by a v1 client)
21
+ export function readEvents(stored: unknown): BufferedEvent[] {
22
+ if (!Array.isArray(stored)) {
23
+ return [];
24
+ }
25
+ return stored.flatMap((entry): BufferedEvent[] => {
26
+ if (!isRecord(entry)) {
27
+ return [];
28
+ }
29
+ const type = optionalString(entry.type);
30
+ const ts = optionalNumber(entry.ts);
31
+ return type === undefined || ts === undefined ? [] : [{ type, ts }];
32
+ });
33
+ }
34
+
35
+ /** The buffer with one more event, trimmed to {@link EVENT_BUFFER_CAP}: oldest heartbeats go first, then the oldest events. */
36
+ export function appendEvent(events: readonly BufferedEvent[], incoming: BufferedEvent): BufferedEvent[] {
37
+ let buffered = [...events, incoming];
38
+ let excess = buffered.length - EVENT_BUFFER_CAP;
39
+ if (excess <= 0) {
40
+ return buffered;
41
+ }
42
+ buffered = buffered.filter((event) => {
43
+ if (excess > 0 && event.type === 'heartbeat') {
44
+ excess -= 1;
45
+ return false;
46
+ }
47
+ return true;
48
+ });
49
+ return buffered.length > EVENT_BUFFER_CAP ? buffered.slice(buffered.length - EVENT_BUFFER_CAP) : buffered;
50
+ }
@@ -1,21 +1,21 @@
1
- /**
2
- * Starred games are site-wide, not per game, so they live under one global
3
- * key (`hub:favorites`, the v1 client's) rather than in a game's cache. The
4
- * list mirrors the hub's so the menu still shows it offline.
5
- */
6
- import { parseJsonText, stringsIn } from '../storage/json-guards';
7
- import { createPageStore } from '../storage/key-value';
8
-
9
- /** The key of the mirrored favorites list. */
10
- export const FAVORITES_KEY = 'hub:favorites';
11
-
12
- /** The mirrored favorite slugs (empty when none or unreadable). */
13
- export function readLocalFavorites(): string[] {
14
- const text = createPageStore().read(FAVORITES_KEY);
15
- return text === null ? [] : stringsIn(parseJsonText(text));
16
- }
17
-
18
- /** Mirror the hub's favorites list. */
19
- export function writeLocalFavorites(games: readonly string[]): void {
20
- createPageStore().write(FAVORITES_KEY, JSON.stringify(games));
21
- }
1
+ /**
2
+ * Starred games are site-wide, not per game, so they live under one global
3
+ * key (`hub:favorites`, the v1 client's) rather than in a game's cache. The
4
+ * list mirrors the hub's so the menu still shows it offline.
5
+ */
6
+ import { parseJsonText, stringsIn } from '../storage/json-guards';
7
+ import { createPageStore } from '../storage/key-value';
8
+
9
+ /** The key of the mirrored favorites list. */
10
+ export const FAVORITES_KEY = 'hub:favorites';
11
+
12
+ /** The mirrored favorite slugs (empty when none or unreadable). */
13
+ export function readLocalFavorites(): string[] {
14
+ const text = createPageStore().read(FAVORITES_KEY);
15
+ return text === null ? [] : stringsIn(parseJsonText(text));
16
+ }
17
+
18
+ /** Mirror the hub's favorites list. */
19
+ export function writeLocalFavorites(games: readonly string[]): void {
20
+ createPageStore().write(FAVORITES_KEY, JSON.stringify(games));
21
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The hub's daily kind, a composite op: complete a day with the token the
3
+ * game started it with; when that token has expired by the time the op
4
+ * replays (401), start the day again for a fresh token and complete with
5
+ * that. The completion is replayable, so a retry after a lost reply never
6
+ * records it twice. A completion sent at once (online) is reported by the
7
+ * daily system itself; one that replays later is reported here.
8
+ */
9
+ import type { Json } from '@diffenderfer-games/hub-contract';
10
+ import { HubError } from '../../errors';
11
+ import type { EndpointReply } from '../../rest/endpoint-types';
12
+ import { isJsonObject, isRecord, optionalNumber, optionalString } from '../../storage/json-guards';
13
+ import { checkedKind } from '../kind-adapters';
14
+ import type { RegisteredKind } from '../outbox-kinds';
15
+ import type { GameFieldsSource } from './game-fields';
16
+
17
+ /** One daily completion. */
18
+ export type DailyPayload = { readonly day: string; readonly token: string; readonly meta: { readonly [key: string]: Json } };
19
+
20
+ /** The kind name. */
21
+ export const DAILY_KIND = 'hub.daily';
22
+
23
+ /** What a completion answers. */
24
+ export type DailyCompletion = EndpointReply<'completeDailyChallenge'>;
25
+
26
+ function readDaily(stored: Json): DailyPayload | undefined {
27
+ if (!isRecord(stored) || !isJsonObject(stored.meta)) {
28
+ return undefined;
29
+ }
30
+ const day = optionalString(stored.day);
31
+ const token = optionalString(stored.token);
32
+ return day === undefined || token === undefined ? undefined : { day, token, meta: stored.meta };
33
+ }
34
+
35
+ /** A completion reply read back, or `undefined` when it doesn't read as one. */
36
+ export function readDailyCompletion(result: Json | undefined): DailyCompletion | undefined {
37
+ if (!isRecord(result) || result.ok !== true) {
38
+ return undefined;
39
+ }
40
+ const day = optionalString(result.day);
41
+ const count = optionalNumber(result.count);
42
+ const rank = result.rank === null ? null : optionalNumber(result.rank);
43
+ return day === undefined || count === undefined || rank === undefined ? undefined : { ok: true, day, count, rank };
44
+ }
45
+
46
+ /** The daily kind, by name; `onCompleted` hears each completion that lands from the queue. */
47
+ export function dailyKinds(game: GameFieldsSource, onCompleted: (payload: DailyPayload, completion: DailyCompletion) => void): Record<string, RegisteredKind> {
48
+ return {
49
+ [DAILY_KIND]: checkedKind(readDaily, {
50
+ run: async ({ day, token, meta }, context) => {
51
+ let completion: DailyCompletion;
52
+ try {
53
+ completion = await context.call('completeDailyChallenge', { ...game(), day, token, meta });
54
+ } catch (failure) {
55
+ if (!(failure instanceof HubError) || failure.status !== 401) {
56
+ throw failure;
57
+ }
58
+ const started = await context.call('startDailyChallenge', { ...game(), day });
59
+ completion = await context.call('completeDailyChallenge', { ...game(), day, token: started.token, meta });
60
+ }
61
+ if (context.isReplay) {
62
+ onCompleted({ day, token, meta }, completion);
63
+ }
64
+ return completion;
65
+ },
66
+ }),
67
+ };
68
+ }