@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,11 +1,129 @@
1
- import { s as DailySystem } from "./daily.js";
2
- import { n as getUiStack, w as InputSystem } from "./input.js";
1
+ import { S as avatar_d_exports } from "./avatar.js";
2
+ import { M as SavedOpened, S as SavedEntry, s as DailySystem, w as SavedListOptions, y as GamePayload } from "./daily.js";
3
+ import { O as InputSystem, n as getUiStack } from "./input.js";
3
4
  import { o as multiplayerForwarder } from "./mp.js";
4
5
  import { f as raceForwarder } from "./race.js";
5
6
  import { n as roomsForwarder } from "./rooms.js";
6
- import { Q as socialForwarder, X as notifyForwarder, Z as presenceForwarder } from "./social.js";
7
- import { Board, Board as Board$1, ErrorCode, GameInfo, GameLeaderboards, GameRecords, HubVersion, Inventory, Json, Json as Json$1, LeaderboardEntry, Metrics, Metrics as Metrics$1, PublicEndpoints, RejectReason, SessionUser, SortDir, SortDir as SortDir$1, Stats, Stats as Stats$1 } from "@diffenderfer-games/hub-contract";
7
+ import { $ as AvatarRef, Q as AvatarChange, at as notifyForwarder, et as DesignerResult, ot as presenceForwarder, st as socialForwarder, tt as OpenDesignerOptions } from "./social.js";
8
+ import { AvatarLook } from "@diffenderfer-games/hub-contract/avatar";
9
+ import { Board, Board as Board$1, CreateTurnGame, ErrorCode, GameInfo, GameLeaderboards, GameRecords, HubVersion, Inventory, Inventory as Inventory$1, Json, Json as Json$1, LeaderboardEntry, Metrics, Metrics as Metrics$1, MyAvatar, OpenTurnGame, OpenTurnGame as OpenTurnGame$1, PublicEndpoints, RejectReason, ResumeSlot, ResumeSummary, SavedCounts, SessionUser, SortDir, SortDir as SortDir$1, Stats, Stats as Stats$1, TurnChatLine, TurnChatLine as TurnChatLine$1, TurnDeadline, TurnDeadline as TurnDeadline$1, TurnGameView, TurnGameView as TurnGameView$1, TurnSummary, TurnTournament } from "@diffenderfer-games/hub-contract";
8
10
  import { CallReturn, ClientData } from "@ayepi/core/client";
11
+ //#region src/core/page-account.d.ts
12
+ /** The signed-in player as the page heard it. */
13
+ interface PageUser {
14
+ readonly id: number;
15
+ /** Their look code when the source carried it, else `null`. */
16
+ readonly look: string | null;
17
+ }
18
+ /** Where the news came from: an account event, or a socket hello (a time to catch up on what was missed). */
19
+ type AccountSource = 'account' | 'session';
20
+ /** Hears every report of who is signed in. */
21
+ type AccountListener = (user: PageUser | null, source: AccountSource) => void;
22
+ /** The page's account, as last heard. */
23
+ declare class PageAccount {
24
+ /** `undefined` until something said; `null` when signed out. */
25
+ private heard;
26
+ private readonly listeners;
27
+ private isListening;
28
+ constructor();
29
+ /** The signed-in player, `null` when signed out, `undefined` before anything said. */
30
+ get user(): PageUser | null | undefined;
31
+ /** Record who is signed in (this client's own sign-in or sign-out) and tell the listeners. */
32
+ note(user: PageUser | null, source?: AccountSource): void;
33
+ /** Hear every report; returns the unsubscribe function. */
34
+ on(listener: AccountListener): () => void;
35
+ /** Listen to the window events (at once in a page; made before a window existed, on first use). */
36
+ private listen;
37
+ private onEvent;
38
+ }
39
+ //#endregion
40
+ //#region src/core/avatar/avatar-ref.d.ts
41
+ /** Anything that carries a player's look: a seat, a `PlayerLite`, a racer, a ticket user. */
42
+ interface CarriesLook {
43
+ readonly look?: string;
44
+ readonly userId?: number;
45
+ readonly id?: number;
46
+ readonly uid?: number;
47
+ }
48
+ //#endregion
49
+ //#region src/core/avatar/hub-avatar.d.ts
50
+ /** What `hub.avatar` needs from its client. */
51
+ interface AvatarHost {
52
+ /** The signed-in user's id, or `null` (`hub.userId()`). */
53
+ readonly currentUserId: () => number | null;
54
+ /** Hear every report of who is signed in (the page's account). */
55
+ readonly onAccount: (listener: AccountListener) => () => void;
56
+ /** `GET /avatar/me`; rejects when offline. */
57
+ readonly fetchMine: () => Promise<MyAvatar>;
58
+ /** `POST /avatar/batch`. */
59
+ readonly fetchCodes: (ids: readonly number[]) => Promise<Record<string, string>>;
60
+ /** Queue a save of your look (sent now when online, replayed later when not). */
61
+ readonly queueSave: (code: string, savedAt: number) => Promise<void>;
62
+ }
63
+ /** The renderer module `load()` resolves with. */
64
+ type AvatarModule = typeof avatar_d_exports;
65
+ type ChangeListener = (change: AvatarChange) => void;
66
+ /** `hub.avatar`. */
67
+ declare class HubAvatar {
68
+ private readonly host;
69
+ private readonly cache;
70
+ private readonly listeners;
71
+ private pending;
72
+ private flushQueued;
73
+ private detachRuntime;
74
+ /** The account the last report named. */
75
+ private lastUserId;
76
+ /** The code of your look the page was last given (`mine`, `mineSync`, `change`). */
77
+ private mineShown;
78
+ /** Players this page asked about or was handed (re-checked on each socket hello). */
79
+ private readonly seen;
80
+ constructor(host: AvatarHost);
81
+ /**
82
+ * Your look (your default if you never designed one). Works offline from
83
+ * the device cache. Before your account exists (a new guest's is made just
84
+ * after the game starts) it resolves at once with a default look, and
85
+ * `change` brings yours when the account is there.
86
+ */
87
+ mine(): Promise<AvatarRef>;
88
+ /** Your cached look, or your default; `null` only while nobody is signed in. */
89
+ mineSync(): AvatarRef | null;
90
+ /** Any player's look: from the cache, else batched with other requests in the same tick. */
91
+ of(userId: number): Promise<AvatarRef>;
92
+ /** Many players' looks at once. */
93
+ many(userIds: readonly number[]): Promise<Map<number, AvatarRef>>;
94
+ /** The look a payload already carries (a `RoomSeat`, `PlayerLite`, racer or ticket user). No request. */
95
+ from(carrier: CarriesLook): AvatarRef;
96
+ /** A seeded default for bots, previews and local pass-and-play seats: `bot('local:2')`. */
97
+ bot(seed: string | number): AvatarRef;
98
+ /** Save your look (designer code paths; queued offline, latest wins). */
99
+ save(look: AvatarLook): Promise<AvatarRef>;
100
+ /**
101
+ * Hear about look changes (yours, friends', your room's, and yours when
102
+ * your account appears). Returns the unsubscribe function. Wait for this
103
+ * rather than polling `mine()`.
104
+ */
105
+ on(_event: 'change', listener: ChangeListener): () => void;
106
+ /** Open "Make your avatar" over the game; resolves when it closes. */
107
+ openDesigner(options?: OpenDesignerOptions): Promise<DesignerResult>;
108
+ /** Loads the renderer and its art; resolves when rendering is synchronous. */
109
+ load(): Promise<AvatarModule>;
110
+ /** Remember the ref the page was given for your look. */
111
+ private showMine;
112
+ /** Cache your look; `change` fires when it differs from the one the page last had. */
113
+ private noteMine;
114
+ /** Someone said who is signed in: fetch your look when the account is new or its look isn't the cached one. */
115
+ private onAccount;
116
+ private refreshMine;
117
+ /** The socket (re)connected: a change sent while it was down never arrived, so ask again. */
118
+ private recheckSeen;
119
+ private emit;
120
+ private attachRuntime;
121
+ private scheduleFlush;
122
+ private flush;
123
+ /** Players' codes, in batches; offline or failing, the missing ones are left out (they get their default). */
124
+ private fetchCodes;
125
+ }
126
+ //#endregion
9
127
  //#region src/core/storage/key-value.d.ts
10
128
  /**
11
129
  * The string store behind the offline cache and queues: `localStorage` when
@@ -25,10 +143,6 @@ interface KeyValueStore {
25
143
  keys(): string[];
26
144
  }
27
145
  //#endregion
28
- //#region src/core/game-data.d.ts
29
- /** Anything a game may store as JSON: an object (interfaces included), an array, text, a number, a boolean or `null`. */
30
- type GamePayload = object | string | number | boolean | null;
31
- //#endregion
32
146
  //#region src/core/rest/endpoint-types.d.ts
33
147
  /** The name of a public REST endpoint (`getCurrentUser`, `writeSave`, ...). */
34
148
  type EndpointName = keyof PublicEndpoints;
@@ -234,7 +348,6 @@ type StatUpdateMode = 'set' | 'incr' | 'max';
234
348
  declare class LocalStore {
235
349
  private readonly prefix;
236
350
  private readonly store;
237
- private sequence;
238
351
  /** The cache for game `slug` in `store` (default: `localStorage`, or memory without it). */
239
352
  constructor(slug: string, store?: KeyValueStore);
240
353
  private readStored;
@@ -257,6 +370,16 @@ declare class LocalStore {
257
370
  putSave(slot: string, saveData: Json, label: string | null): number;
258
371
  /** Forget a save. */
259
372
  deleteSave(slot: string): void;
373
+ /**
374
+ * The hub's `updatedAt` for `slot` as the current user last saw it (`null`:
375
+ * the slot was empty), or `undefined` when this device doesn't know.
376
+ */
377
+ cloudSave(slot: string): number | null | undefined;
378
+ /** Note the hub's copy of `slot` (just read or written) for the current user: its `updatedAt`, or `null` when empty. */
379
+ rememberCloudSave(slot: string, updatedAt: number | null): void;
380
+ /** Cache a slot as read from the hub (forgetting it when the hub has none), and note the hub's copy. */
381
+ cacheCloudSave(slot: string, save: Save | null): void;
382
+ private readCloudSaves;
260
383
  /** This game's cached stats. */
261
384
  getStats(): Stats;
262
385
  /** Apply one stat update and return the stat's new value. */
@@ -267,21 +390,24 @@ declare class LocalStore {
267
390
  getBoard(key: string): LocalBoard | null;
268
391
  /** Record a score locally, keeping the best one; returns the board and best. */
269
392
  submitScore(key: string, score: number, options: QueuedScoreOptions): LocalBoard;
270
- /** The queued writes, oldest first. */
271
- getQueue(): QueueOp[];
272
- /** Replace the queue. */
273
- setQueue(queue: readonly QueueOp[]): void;
274
- /** How many writes are queued. */
275
- queueLength(): number;
276
- /** Queue a write, collapsing it with a compatible queued one. */
277
- enqueue(input: QueueOpInput): void;
393
+ /** The v1 client's queued writes, removed from storage (the caller moves them into the outbox). */
394
+ takeLegacyQueue(): QueueOp[];
395
+ /** This game's cached inventory (empty when none is cached). */
396
+ getInventory(): Inventory;
397
+ /** Store this game's inventory and return it. */
398
+ setInventory(inventory: Inventory): Inventory;
278
399
  /** The buffered analytics events, oldest first. */
279
400
  getEvents(): BufferedEvent[];
280
401
  /** Replace the event buffer. */
281
402
  setEvents(events: readonly BufferedEvent[]): void;
282
403
  /** Buffer one event (capped; heartbeats are dropped first). */
283
404
  bufferEvent(type: string): void;
284
- /** Fan a `/me/snapshot` reply out across every game's cache keys. */
405
+ /**
406
+ * Fan a `/me/snapshot` reply out across every game's cache keys. It lists
407
+ * every save slot of every game the player has played, so each game's
408
+ * slots on the hub become known (and this game's are all empty when it
409
+ * isn't listed).
410
+ */
285
411
  hydrate(snapshot: SnapshotResponse): void;
286
412
  }
287
413
  //#endregion
@@ -296,21 +422,348 @@ interface RestClient {
296
422
  readonly apiBase: string;
297
423
  /** Call an endpoint; resolves with its reply body, rejects with a {@link HubError}. */
298
424
  call<K extends EndpointName>(name: K, ...fieldsArgument: CallArguments<K>): Promise<EndpointReply<K>>;
425
+ /**
426
+ * Call an endpoint as part of an offline operation: an endpoint marked for
427
+ * offline replay gets `opKey` in the operation header, so the hub runs it
428
+ * once however often it is sent; any other endpoint is called as usual.
429
+ */
430
+ callOnce<K extends EndpointName>(opKey: string, name: K, ...fieldsArgument: CallArguments<K>): Promise<EndpointReply<K>>;
431
+ }
432
+ //#endregion
433
+ //#region src/core/offline/outbox-entry.d.ts
434
+ /** A queued operation. */
435
+ interface OutboxEntry {
436
+ /** Unique and sortable by queue time: replay runs in `id` order. Also the base of its operation keys. */
437
+ readonly id: string;
438
+ /** Which handler performs it (`hub.save`, `hub.stat`, or a game's own kind). */
439
+ readonly kind: string;
440
+ /** The handler's input. */
441
+ readonly payload: Json;
442
+ /** When it was queued (epoch ms). */
443
+ readonly createdAt: number;
444
+ /** How many times replay has started it. A started entry never absorbs a later one. */
445
+ readonly attempts: number;
446
+ /** Not before this time (epoch ms); 0 means as soon as possible. */
447
+ readonly retryAt: number;
448
+ /** The last failure's message, when it has failed before. */
449
+ readonly lastError?: string;
450
+ }
451
+ //#endregion
452
+ //#region src/core/offline/outbox-kinds.d.ts
453
+ /** What a kind's handler gets besides its payload. */
454
+ interface ReplayContext {
455
+ /** The operation's id (stable across retries). */
456
+ readonly opId: string;
457
+ /** Which try this is, from 1. */
458
+ readonly attempt: number;
459
+ /** `false` when the op runs at once (online, nothing waiting); `true` when it runs from the queue. */
460
+ readonly isReplay: boolean;
461
+ /**
462
+ * Call a hub endpoint. Calls to endpoints marked for offline replay carry
463
+ * an operation key made from the op id and the call's position in the
464
+ * handler, so a retried op whose earlier calls already landed gets their
465
+ * remembered replies instead of running them twice. Make the same calls in
466
+ * the same order on every try.
467
+ */
468
+ call<K extends EndpointName>(name: K, ...fieldsArgument: CallArguments<K>): Promise<EndpointReply<K>>;
469
+ /** A key for `step` of this op that is the same on every try: send it to a game server to deduplicate there. */
470
+ key(step: string): string;
471
+ }
472
+ /** Performs one operation of a kind. Throw to fail it (see the docs for which failures retry). */
473
+ type OfflineHandler<Payload> = (payload: Payload, context: ReplayContext) => Promise<Json | undefined> | Json | undefined;
474
+ /** A kind, with optional rules for folding new operations into waiting ones. */
475
+ interface OfflineKindDefinition<Payload> {
476
+ /** Perform one operation. */
477
+ readonly run: OfflineHandler<Payload>;
478
+ /**
479
+ * Operations with the same merge key may fold together. Without one, every
480
+ * operation replays on its own.
481
+ */
482
+ readonly mergeKey?: (payload: Payload) => string;
483
+ /**
484
+ * Fold `incoming` into the newest waiting operation with the same merge key
485
+ * (in its place in the queue); `undefined` to queue it separately.
486
+ */
487
+ readonly merge?: (waiting: Payload, incoming: Payload) => Payload | undefined;
488
+ /** `true`: a new operation replaces every waiting one with the same merge key and goes last (a save slot's last write wins). */
489
+ readonly replaces?: boolean;
490
+ }
491
+ /** A kind as the outbox runs it: payloads are the stored JSON. */
492
+ interface RegisteredKind {
493
+ /** Perform one operation (malformed payloads are refused before this). */
494
+ readonly run: (payload: Json, context: ReplayContext) => Promise<Json | undefined> | Json | undefined;
495
+ /** Whether a stored payload is well formed for this kind. */
496
+ readonly accepts: (payload: Json) => boolean;
497
+ readonly mergeKey?: (payload: Json) => string;
498
+ readonly merge?: (waiting: Json, incoming: Json) => Json | undefined;
499
+ readonly replaces: boolean;
500
+ }
501
+ /** The registered kinds of one client. */
502
+ declare class KindRegistry {
503
+ private readonly kinds;
504
+ /** Register (or replace) a kind. */
505
+ register(kind: string, definition: RegisteredKind): void;
506
+ /** A registered kind, or `undefined`. */
507
+ get(kind: string): RegisteredKind | undefined;
508
+ }
509
+ //#endregion
510
+ //#region src/core/offline/outbox-store.d.ts
511
+ /** The most entries one game's outbox holds. */
512
+ declare const OUTBOX_MAX_ENTRIES = 500;
513
+ /** The most stored characters one game's outbox holds (about 2 MB of `localStorage`). */
514
+ declare const OUTBOX_MAX_CHARS = 1000000;
515
+ /** One game's outbox. */
516
+ declare class OutboxStore {
517
+ /** The key prefix of this game's entries. */
518
+ readonly prefix: string;
519
+ private readonly store;
520
+ /** The outbox for game `slug` in `store`. */
521
+ constructor(slug: string, store: KeyValueStore);
522
+ /** Every well-formed entry, oldest first. Malformed ones are removed. */
523
+ list(): OutboxEntry[];
524
+ /** How many entries are queued. */
525
+ count(): number;
526
+ /** One entry, or `undefined` once it is gone (replayed, or removed by another tab). */
527
+ get(id: string): OutboxEntry | undefined;
528
+ /** Whether `extra` more stored characters in one more entry still fit under the caps. */
529
+ fits(extraChars: number): boolean;
530
+ /** Store an entry (new or updated); `false` when the storage refused it (full or blocked). */
531
+ put(entry: OutboxEntry): boolean;
532
+ /** Forget an entry. */
533
+ remove(id: string): void;
534
+ /** Whether a storage key belongs to this outbox (for cross-tab `storage` events). */
535
+ owns(key: string | null): boolean;
536
+ private entryKeys;
537
+ }
538
+ //#endregion
539
+ //#region src/core/offline/outbox-replayer.d.ts
540
+ /** An op dropped during replay, with why. */
541
+ interface ReplayFailure {
542
+ /** The dropped op. */
543
+ readonly entry: OutboxEntry;
544
+ /** Why it was dropped (the hub's message, or the handler's). */
545
+ readonly error: string;
546
+ }
547
+ /** Why a replay stopped before the outbox was empty. */
548
+ type ReplayStop = 'connection' | 'backoff' | 'unknown-kind' | 'lease';
549
+ /** What one replay did. */
550
+ interface ReplayReport {
551
+ /** The ops that landed, in order. */
552
+ readonly replayed: OutboxEntry[];
553
+ /** The ops dropped. */
554
+ readonly failed: ReplayFailure[];
555
+ /** Why it stopped early, or `undefined` when the outbox is empty. */
556
+ readonly stoppedBy?: ReplayStop;
557
+ }
558
+ //#endregion
559
+ //#region src/core/offline/outbox.d.ts
560
+ /** A queued operation, as `hub.offline` reports it. */
561
+ type OfflineOp = OutboxEntry;
562
+ /** Each outbox event's detail. */
563
+ interface OfflineEventDetails {
564
+ /** An op was queued (or folded into a waiting one, which is then the `op`). */
565
+ queued: {
566
+ readonly op: OfflineOp;
567
+ };
568
+ /** An op landed; `result` is what its handler returned. */
569
+ replayed: {
570
+ readonly op: OfflineOp;
571
+ readonly result?: Json;
572
+ };
573
+ /** An op was dropped: the hub refused it, or it kept failing. */
574
+ failed: {
575
+ readonly op: OfflineOp;
576
+ readonly error: string;
577
+ };
578
+ }
579
+ /** An outbox event's name. */
580
+ type OfflineEventName = keyof OfflineEventDetails;
581
+ /** An outbox event listener. */
582
+ type OfflineListener<Name extends OfflineEventName> = (detail: OfflineEventDetails[Name]) => void;
583
+ /** The code of the error `add` throws when the outbox is full. */
584
+ declare const OUTBOX_FULL_CODE = "offline_queue_full";
585
+ /** What the outbox needs from its client. */
586
+ interface OutboxOptions {
587
+ /** The game whose outbox it is. */
588
+ readonly slug: string;
589
+ /** Where it lives (`localStorage`, or memory). */
590
+ readonly store: KeyValueStore;
591
+ /** The REST client its handlers call through. */
592
+ readonly rest: RestClient;
593
+ /** Fire a window event (the menu listens). */
594
+ readonly emit: (name: string, detail: OfflineEventDetails[OfflineEventName]) => void;
595
+ /** The hub couldn't be reached during a replay. */
596
+ readonly onUnreachable: () => void;
597
+ /** The queue length may have changed. */
598
+ readonly onChange: () => void;
599
+ /** A retry is due: replay again. */
600
+ readonly onRetryDue: () => void;
601
+ readonly now?: () => number;
602
+ }
603
+ /** One game's outbox. */
604
+ declare class Outbox {
605
+ /** The kinds this client can perform. */
606
+ readonly kinds: KindRegistry;
607
+ /** The stored entries. */
608
+ readonly entries: OutboxStore;
609
+ private readonly options;
610
+ private readonly replayer;
611
+ private readonly now;
612
+ private sequence;
613
+ private retryTimer;
614
+ private retryAt;
615
+ private readonly listeners;
616
+ /** The outbox for `options.slug`. */
617
+ constructor(options: OutboxOptions);
618
+ /** Register a kind. */
619
+ define(kind: string, definition: RegisteredKind): void;
620
+ /** A new, not yet stored, op of `kind`. */
621
+ stamp(kind: string, payload: Json): OutboxEntry;
622
+ /**
623
+ * Queue an op, folding it into a waiting one where its kind says so.
624
+ * Throws a {@link HubError} (code {@link OUTBOX_FULL_CODE}) when the
625
+ * outbox is full or the storage refuses it.
626
+ */
627
+ add(op: OutboxEntry): void;
628
+ /** Perform an op now, without queueing it (the online path); throws what its handler throws. */
629
+ runNow(op: OutboxEntry): Promise<Json | undefined>;
630
+ /** Replay what is due, in order. `isEager`: the connection just came back, so ops waiting for it go at once. */
631
+ replay(isEager?: boolean): Promise<ReplayReport>;
632
+ /** The waiting ops, oldest first. */
633
+ pending(): OfflineOp[];
634
+ /** How many ops are waiting. */
635
+ count(): number;
636
+ /** Listen for an event; returns the unsubscribe function. */
637
+ on<Name extends OfflineEventName>(name: Name, listener: OfflineListener<Name>): () => void;
638
+ private fire;
639
+ private contextFor;
640
+ private scheduleRetry;
641
+ private clearRetry;
642
+ }
643
+ //#endregion
644
+ //#region src/core/offline/offline-api.d.ts
645
+ /** What `hub.offline` drives. */
646
+ interface OfflineHost {
647
+ /** The game's outbox. */
648
+ readonly outbox: Outbox;
649
+ /** Replay now when the hub is reachable (and nothing else is replaying). */
650
+ requestSync(): void;
651
+ }
652
+ /** The game-facing outbox API. */
653
+ declare class OfflineApi {
654
+ private readonly host;
655
+ /** The API over `host`. */
656
+ constructor(host: OfflineHost);
657
+ /**
658
+ * Define what an operation of `kind` does when it replays. Define every
659
+ * kind at load, before or after enqueueing: waiting ops of an undefined
660
+ * kind hold the queue until it is defined. `hub.*` kinds are the hub's own.
661
+ */
662
+ define<Payload extends GamePayload>(kind: string, definition: OfflineHandler<Payload> | OfflineKindDefinition<Payload>): void;
663
+ /**
664
+ * Queue an operation of `kind` and replay it as soon as the hub is
665
+ * reachable (at once when online). Resolves the queued op; its outcome
666
+ * arrives as a `replayed` or `failed` event. Throws a `HubError` (code
667
+ * `offline_queue_full`) when the outbox is full.
668
+ */
669
+ enqueue(kind: string, payload: GamePayload): OfflineOp;
670
+ /** The waiting operations (the hub's and the game's), oldest first. */
671
+ pending(): OfflineOp[];
672
+ /** Listen for `queued`, `replayed` or `failed`; returns the unsubscribe function. */
673
+ on<Name extends OfflineEventName>(name: Name, listener: OfflineListener<Name>): () => void;
674
+ }
675
+ //#endregion
676
+ //#region src/core/resume/local-resume.d.ts
677
+ /** A run as this device saved it. */
678
+ interface LocalRun {
679
+ readonly status: 'saved';
680
+ readonly userId: number | null;
681
+ /** When the game saved it, epoch ms. */
682
+ readonly at: number;
683
+ readonly label: string;
684
+ readonly fraction: number | null;
685
+ readonly title: string | null;
686
+ readonly version: number;
687
+ /** The game's title, for the Resume card before the hub knows the run. */
688
+ readonly gameTitle: string;
689
+ readonly state: Json;
690
+ readonly isSynced: boolean;
691
+ }
692
+ /** A run this device cleared (kept until the hub has the clear). */
693
+ interface LocalClear {
694
+ readonly status: 'cleared';
695
+ readonly userId: number | null;
696
+ readonly at: number;
697
+ readonly isSynced: boolean;
698
+ }
699
+ /** A device copy of one game's slot. */
700
+ type LocalResume = LocalRun | LocalClear;
701
+ /** The device copies of every game's slot. */
702
+ declare class LocalResumeStore {
703
+ private readonly store;
704
+ constructor(store: KeyValueStore);
705
+ /** `slug`'s copy, or `null`. */
706
+ read(slug: string): LocalResume | null;
707
+ /** Store `slug`'s copy; answers whether it stuck (a full storage refuses a large state). */
708
+ write(slug: string, record: LocalResume): boolean;
709
+ /** Forget `slug`'s copy. */
710
+ remove(slug: string): void;
711
+ /** The hub now has `slug`'s save or clear made at `at`: mark it synced (a clear is simply forgotten). */
712
+ markSynced(slug: string, at: number): void;
713
+ /** Every game's copy, by slug. */
714
+ all(): Map<string, LocalResume>;
715
+ }
716
+ //#endregion
717
+ //#region src/core/saved/local-saved.d.ts
718
+ /** A daily challenge as this device last saw it. */
719
+ interface LocalDaily {
720
+ readonly id: string;
721
+ readonly game: string;
722
+ readonly day: string;
723
+ readonly seed: number;
724
+ /** `active` in progress; `finished` (won) or `discarded`, kept until the hub has it. */
725
+ readonly status: 'active' | 'finished' | 'discarded';
726
+ readonly userId: number | null;
727
+ /** When it last changed here, epoch ms. */
728
+ readonly at: number;
729
+ readonly label: string;
730
+ readonly fraction: number | null;
731
+ readonly title: string | null;
732
+ readonly version: number;
733
+ readonly gameTitle: string;
734
+ /** The state the game last saved, `null` before its first save. */
735
+ readonly state: Json;
736
+ readonly isSynced: boolean;
737
+ }
738
+ /** The device copies of daily challenges. */
739
+ declare class LocalDailyStore {
740
+ private readonly store;
741
+ constructor(store: KeyValueStore);
742
+ /** The copy of `game`'s `day`, or `null`. */
743
+ read(game: string, day: string): LocalDaily | null;
744
+ /** Store a copy; answers whether it stuck (a full storage refuses a large state). */
745
+ write(copy: LocalDaily): boolean;
746
+ /** Forget a copy. */
747
+ remove(game: string, day: string): void;
748
+ /** The hub has the change made at `at` (and calls the instance `id`): mark it synced; a closed copy is forgotten. */
749
+ markSynced(game: string, day: string, at: number, id?: string): void;
750
+ /** Every copy of `userId`'s, any game. */
751
+ all(userId: number | null): LocalDaily[];
299
752
  }
300
753
  //#endregion
301
754
  //#region src/core/connection.d.ts
302
- /** A queued write the hub refused during a sync: dropped, with the hub's message. */
755
+ /** A queued op the hub refused during a sync (or that kept failing): dropped, with the reason. */
303
756
  interface SyncConflict {
304
- /** The refused op. */
305
- readonly op: QueueOp;
306
- /** The hub's message. */
757
+ /** The dropped op. */
758
+ readonly op: OfflineOp;
759
+ /** The hub's (or the handler's) message. */
307
760
  readonly error?: string;
308
761
  }
309
762
  /** What one `sync()` did. */
310
763
  interface SyncResult {
311
- /** Queued writes the hub accepted. */
764
+ /** Queued ops that landed. */
312
765
  readonly flushed: number;
313
- /** Queued writes the hub refused (and that were dropped). */
766
+ /** Queued ops that were dropped. */
314
767
  readonly conflicts: SyncConflict[];
315
768
  }
316
769
  /** The game slug's path field, or a {@link HubError} when the slug is unknown. */
@@ -326,12 +779,22 @@ declare class HubConnection {
326
779
  readonly apiBase: string;
327
780
  /** Typed REST calls to {@link apiBase}. */
328
781
  protected readonly rest: RestClient;
329
- /** This game's offline cache, queue and event buffer. */
782
+ /** This game's offline cache and event buffer. */
330
783
  protected readonly store: LocalStore;
784
+ /** This game's queued operations. */
785
+ protected readonly outbox: Outbox;
786
+ /** The device copies of every game's resume slot (`hub.resume`). */
787
+ protected readonly resumeStore: LocalResumeStore;
788
+ /** The device copies of daily challenges in progress (every game's). */
789
+ protected readonly dailyStore: LocalDailyStore;
331
790
  private readonly offlineMode;
332
791
  private isReachable;
333
- private isSyncing;
792
+ private runningSync;
793
+ private followingSync;
794
+ private isFollowingEager;
795
+ private directSend;
334
796
  private lastPrefetchAt;
797
+ private offlineApi;
335
798
  private readonly statusListeners;
336
799
  /** A connection for `options` (slug and API base default to the page's). */
337
800
  constructor(options?: HubOptions);
@@ -339,13 +802,18 @@ declare class HubConnection {
339
802
  get isOffline(): boolean;
340
803
  /** `true` while the hub is reachable. */
341
804
  get isOnline(): boolean;
342
- /** Queued writes plus buffered analytics events waiting to sync. */
805
+ /**
806
+ * The offline outbox: define your own kinds of operation, queue them, and
807
+ * hear how they replay (`hub.offline.define`, `enqueue`, `pending`, `on`).
808
+ */
809
+ get offline(): OfflineApi;
810
+ /** Queued operations plus buffered analytics events waiting to sync. */
343
811
  pending(): number;
344
812
  /** Subscribe to connection and sync status changes; returns the unsubscribe function. */
345
813
  onStatus(listener: (status: HubStatus) => void): () => void;
346
814
  /** Tell `onStatus` listeners and the page (`hub:status`) the current status. */
347
815
  protected notifyStatus(): void;
348
- /** Record reachability; coming back online syncs the queue and re-warms the cache. */
816
+ /** Record reachability; coming back online replays the outbox and re-warms the cache. */
349
817
  protected setReachable(isReachableNow: boolean): void;
350
818
  /** `{ slug }` for this game's endpoints; throws when the slug is unknown. */
351
819
  protected gameFields(): GameFields;
@@ -355,23 +823,38 @@ declare class HubConnection {
355
823
  * status) always reach the caller.
356
824
  */
357
825
  protected withFallback<Reply>(networkCall: () => Promise<Reply>, offlineAnswer: () => Reply): Promise<Reply>;
358
- /** Send one queued write. */
359
- private sendOp;
360
826
  /**
361
- * Apply a write to the offline cache at once, then send it now (online
362
- * with an empty queue) or queue it behind the others. Resolves with the
363
- * local answer; a real API error on the direct path reaches the caller and
364
- * is not queued.
827
+ * Apply a write to the offline cache at once, then perform it as an op of
828
+ * `kind`: sent now when online with nothing waiting, else queued behind
829
+ * the others. Resolves with the local answer (or, sent now, with
830
+ * `replyAnswer`'s reading of the hub's reply). A real API error on the
831
+ * direct path reaches the caller and is not queued; a transport failure
832
+ * queues the op under the same id, so if the request did land, its replay
833
+ * is recognised by the hub and not applied twice.
365
834
  */
366
- protected mutateQueued<Reply>(input: QueueOpInput, localAnswer: () => Reply): Promise<Reply>;
835
+ protected mutateOffline<Reply>(kind: string, payload: Json, localAnswer: () => Reply, replyAnswer?: (result: Json | undefined) => Reply | undefined): Promise<Reply>;
367
836
  /**
368
- * Replay the queue, then the buffered events, in order. Stops at the first
369
- * transport failure (offline again: the rest stays queued); a write the hub
370
- * refuses is dropped and reported as a conflict.
837
+ * Replay the outbox, then the buffered events, in order. Stops at the
838
+ * first transport failure (offline again: the rest stays queued); an op
839
+ * the hub refuses is dropped and reported as a conflict.
371
840
  */
372
841
  sync(): Promise<SyncResult>;
842
+ /** A retry came due: try again even when the hub was last unreachable (the try is the probe), unless the browser itself is offline. */
843
+ private retrySync;
844
+ /**
845
+ * One replay at a time per client. A call during one queues a single
846
+ * follow-up replay (eager if any caller asked for that), so an op queued
847
+ * or a reconnect that happens mid-replay is never missed.
848
+ */
849
+ private replayAll;
850
+ private startReplay;
851
+ private runReplay;
373
852
  /** Send the buffered analytics events; a refused one is dropped (telemetry never blocks). */
374
853
  private flushEvents;
854
+ /** Move a v1 copy's queued writes into the outbox, in their order. */
855
+ private importLegacyQueue;
856
+ /** Follow the browser's connection, other tabs' outbox changes, and replay what an earlier page left queued. */
857
+ private listenToPage;
375
858
  /**
376
859
  * Warm the offline cache for every game the player has touched, in one
377
860
  * request, so going offline later still shows complete progress.
@@ -402,6 +885,16 @@ interface ProfileUpdate<TData extends GamePayload = Json> {
402
885
  }
403
886
  /** Accounts, profile, saves, stats and leaderboards. */
404
887
  declare class HubProgress extends HubConnection {
888
+ /** Who the page is signed in as, as the hub's events and this client's sign-ins tell it. */
889
+ protected readonly account: PageAccount;
890
+ /**
891
+ * Your user id, or `null` when nobody is signed in (also before an offline
892
+ * first visit has an account). Synchronous, and it follows the account as
893
+ * the hub changes it (a new guest's account made by the menu, sign-in,
894
+ * sign-out) with no `hub.me()` needed. Compare it with a seat's `userId`
895
+ * to find yours.
896
+ */
897
+ userId(): number | null;
405
898
  /** The current user (or `null`) and the games they have a profile in. */
406
899
  me(): Promise<MeResponse>;
407
900
  /**
@@ -425,6 +918,11 @@ declare class HubProgress extends HubConnection {
425
918
  ok: true;
426
919
  }>;
427
920
  private signInLocally;
921
+ /** Remember who the hub signed in, so the cache's notes about the hub's copies are read for the right player. */
922
+ private signedIn;
923
+ /** The cache and the page (`hub.userId()`, `hub.avatar`) learn who is signed in. */
924
+ private remember;
925
+ private signedOut;
428
926
  /** This game's profile for the current user, or `profile: null`. `TData` is the game's own data type. */
429
927
  getProfile<TData = Json>(): Promise<{
430
928
  profile: Profile<TData> | null;
@@ -437,10 +935,18 @@ declare class HubProgress extends HubConnection {
437
935
  listSaves(): Promise<{
438
936
  saves: SaveMeta[];
439
937
  }>;
440
- /** One slot's contents, or `save: null` when the slot is empty. `TData` is the game's own save type. */
938
+ /**
939
+ * One slot's contents, or `save: null` when the slot is empty. `TData` is
940
+ * the game's own save type. Offline it answers from the device's copy, but
941
+ * only when the device knows the hub's copy of the slot; otherwise (a fresh
942
+ * device or cleared browser) it rejects with a {@link HubError} whose
943
+ * `code` is `save_unknown` (`isOffline` is `true`), never a false "empty".
944
+ */
441
945
  getSave<TData = Json>(slot: string): Promise<{
442
946
  save: Save<TData> | null;
443
947
  }>;
948
+ /** The device's copy of `slot`, when it can be trusted offline (see {@link getSave}). */
949
+ private offlineSave;
444
950
  /** Create or overwrite a save slot. `saveData` is any JSON up to 64 KB. */
445
951
  putSave<TData extends GamePayload = Json>(slot: string, saveData: TData, label?: string): Promise<{
446
952
  ok: true;
@@ -509,7 +1015,7 @@ declare class HubEconomy extends HubProgress {
509
1015
  favorites(): Promise<{
510
1016
  games: string[];
511
1017
  }>;
512
- /** Star (`isOn = true`) or unstar a game. Needs a connection. */
1018
+ /** Star (`isOn = true`) or unstar a game. Offline, the local list changes at once and the hub's when it is back. */
513
1019
  setFavorite(slug: string, isOn: boolean): Promise<{
514
1020
  games: string[];
515
1021
  }>;
@@ -519,9 +1025,16 @@ declare class HubEconomy extends HubProgress {
519
1025
  getInventory(): Promise<InventorySnapshot>;
520
1026
  /** Another player's available holdings (to compose a trade). */
521
1027
  playerInventory(userId: number): Promise<InventorySnapshot>;
522
- /** Add goods to the player (gameplay rewards); answers the new holdings. */
1028
+ /**
1029
+ * Add goods to the player (gameplay rewards); answers the new holdings.
1030
+ * Offline, the cached holdings change at once and the grant replays later.
1031
+ */
523
1032
  grant(bundle: GrantBundle): Promise<EndpointReply<'grantInventoryItems'>>;
524
- /** An atomic swap with the house: remove `take`, add `give`. */
1033
+ /**
1034
+ * An atomic swap with the house: remove `take`, add `give`. Offline it is
1035
+ * checked against the cached holdings (409 when they can't cover `take`),
1036
+ * applied to them at once, and replayed later; the hub has the last word.
1037
+ */
525
1038
  exchange(take?: TradeBundle, give?: TradeBundle): Promise<EndpointReply<'exchangeInventoryItems'>>;
526
1039
  /** Sell items to the house for coins. */
527
1040
  sell(items: Record<string, number>, coins: number): Promise<EndpointReply<'exchangeInventoryItems'>>;
@@ -572,6 +1085,506 @@ interface HubMenuControl {
572
1085
  readonly visible: boolean;
573
1086
  }
574
1087
  //#endregion
1088
+ //#region src/core/offline/hub-kinds/resume-kind.d.ts
1089
+ /** Send the run saved at `savedAt`; `state` only when the device copy couldn't hold it. */
1090
+ type ResumeSavePayload = {
1091
+ readonly action: 'save';
1092
+ readonly savedAt: number;
1093
+ readonly label: string;
1094
+ readonly fraction: number | null;
1095
+ readonly title: string | null;
1096
+ readonly version: number;
1097
+ readonly state?: Json;
1098
+ };
1099
+ /** Clear the run, unless one was saved after `before`. */
1100
+ type ResumeClearPayload = {
1101
+ readonly action: 'clear';
1102
+ readonly before: number;
1103
+ };
1104
+ /** One resume op. */
1105
+ type ResumePayload = ResumeSavePayload | ResumeClearPayload;
1106
+ //#endregion
1107
+ //#region src/core/resume/resume-types.d.ts
1108
+ /** How far along a run is. */
1109
+ interface ResumeProgress {
1110
+ /** A short line the game writes, such as "Level 12" or "Round 3 of 5" (up to 60 characters). */
1111
+ readonly label: string;
1112
+ /** 0 to 1, drawn as a progress bar; leave it out when the game can't say. */
1113
+ readonly fraction?: number | null;
1114
+ }
1115
+ /** A run to save. `TState` is the game's own JSON-safe state type. */
1116
+ interface ResumeSaveInput<TState extends GamePayload> {
1117
+ /** Everything the game needs to pick the run up again (up to 512 KB as JSON). */
1118
+ readonly state: TState;
1119
+ readonly progress: ResumeProgress;
1120
+ /** An optional name for the run ("Klondike, draw 3"), shown under the game's name. */
1121
+ readonly title?: string | null;
1122
+ /** The game's state version (default 0). Bump it when `state` changes shape; see `load({ version })`. */
1123
+ readonly version?: number;
1124
+ }
1125
+ /** A saved run, read back. */
1126
+ interface ResumeRun<TState> {
1127
+ readonly state: TState;
1128
+ readonly progress: {
1129
+ readonly label: string;
1130
+ readonly fraction: number | null;
1131
+ };
1132
+ readonly title: string | null;
1133
+ readonly version: number;
1134
+ /** When the game saved it, epoch ms. */
1135
+ readonly savedAt: number;
1136
+ }
1137
+ /** Options for reading the run back. */
1138
+ interface ResumeLoadOptions {
1139
+ /**
1140
+ * The state version this code reads. A run saved with an older version is
1141
+ * dropped (cleared) and reads as `null`; a newer one (from a newer copy of
1142
+ * the game in another tab) reads as `null` but is kept.
1143
+ */
1144
+ readonly version?: number;
1145
+ }
1146
+ /** One saved run in the player's list (the home page's Resume card). */
1147
+ interface ResumeEntry {
1148
+ /** The game's slug. */
1149
+ readonly slug: string;
1150
+ readonly gameTitle: string;
1151
+ /** The game's URL path, like `/solitaire`. */
1152
+ readonly path: string;
1153
+ /** The game has a tile image (`/_tiles/<slug>`). */
1154
+ readonly hasImage: boolean;
1155
+ readonly progress: {
1156
+ readonly label: string;
1157
+ readonly fraction: number | null;
1158
+ };
1159
+ readonly title: string | null;
1160
+ /** When it was saved, epoch ms. */
1161
+ readonly savedAt: number;
1162
+ /** Saved on this device and not yet sent to the hub (offline, or the tab closed first). */
1163
+ readonly isLocalOnly: boolean;
1164
+ }
1165
+ /** Why a save was ignored: the page is in a race or an online room. (`'daily'` is no longer returned: a save during a daily goes to the day.) */
1166
+ type ResumeBlocker = 'race' | 'daily' | 'room' | 'turns';
1167
+ //#endregion
1168
+ //#region src/core/resume/hub-resume.d.ts
1169
+ /** How long saves must be quiet before the latest is sent, ms. */
1170
+ declare const RESUME_DEBOUNCE_MS = 3000;
1171
+ /** Largest state, bytes of JSON (the contract's `RESUME_MAX_STATE_BYTES`). */
1172
+ declare const RESUME_MAX_STATE_BYTES: number;
1173
+ /** What `hub.resume` needs from its client. */
1174
+ interface ResumeHost {
1175
+ /** This game's slug; `null` on the home page. */
1176
+ readonly slug: string | null;
1177
+ readonly local: LocalResumeStore;
1178
+ /** The signed-in player's id as the device knows it, or `null`. */
1179
+ currentUserId(): number | null;
1180
+ /** The game's display title. */
1181
+ gameTitle(): string;
1182
+ /** Why saving is off right now, or `null`. */
1183
+ blocker(): ResumeBlocker | null;
1184
+ /** The page's daily challenge while one is being played (saves go to the day instead of the run), else `null`. */
1185
+ daily?(): DailyRoute | null;
1186
+ /** Perform a resume op: at once when online with nothing waiting, else queued. */
1187
+ send(payload: ResumePayload): Promise<void>;
1188
+ /** This game's run on the hub, or `undefined` when the hub can't be reached. */
1189
+ fetchSlot(): Promise<ResumeSlot | null | undefined>;
1190
+ /** The player's runs on the hub, or `undefined` when it can't be reached. */
1191
+ fetchList(): Promise<ResumeSummary[] | undefined>;
1192
+ /** Clear another game's run on the hub (the home page's discard); throws offline. */
1193
+ clearOther(slug: string): Promise<void>;
1194
+ /** The run changed here (saved or cleared): `hub.saved` listeners hear it. */
1195
+ onChange?(): void;
1196
+ }
1197
+ /** Where a save goes during a daily challenge (`hub.daily`). */
1198
+ interface DailyRoute {
1199
+ save(input: ResumeSaveInput<GamePayload>): boolean;
1200
+ }
1201
+ /** `hub.resume`. */
1202
+ declare class HubResume {
1203
+ private readonly host;
1204
+ private timer;
1205
+ private waiting;
1206
+ private isListening;
1207
+ private readonly warnedBlockers;
1208
+ constructor(host: ResumeHost);
1209
+ /**
1210
+ * `true` when the page was opened from the Resume card (`?hub_resume=1`,
1211
+ * taken out of the address bar). Synchronous, so a game can restore before
1212
+ * it routes or draws its title screen.
1213
+ */
1214
+ get requested(): boolean;
1215
+ /** Why a save would be ignored right now (`race`, `daily`, `room`), or `null`. */
1216
+ get blockedBy(): ResumeBlocker | null;
1217
+ /**
1218
+ * Save the run (the latest save wins). Stored on the device at once; sent
1219
+ * after {@link RESUME_DEBOUNCE_MS} of quiet, or when the page is hidden.
1220
+ * Returns `false` (and saves nothing) in a race or an online room; during
1221
+ * a daily challenge the save goes to the day (`hub.daily.save`). Throws a
1222
+ * `HubError` 413 over 512 KB, 400 without a label.
1223
+ */
1224
+ save<TState extends GamePayload>(run: ResumeSaveInput<TState>): boolean;
1225
+ /** Send a waiting save now (the page does this itself when hidden or closed). */
1226
+ flush(): Promise<void>;
1227
+ /** Clear the run: call it when the run ends (won, lost or quit). Works offline. */
1228
+ clear(): Promise<void>;
1229
+ /** Delete the game's saved run (never the day being played). */
1230
+ private clearRun;
1231
+ /** The saved run with its progress and time, or `null`. The newer of the device's copy and the hub's wins. */
1232
+ get<TState = GamePayload>(options?: ResumeLoadOptions): Promise<ResumeRun<TState> | null>;
1233
+ /** The saved state, or `null` (see {@link get} for the version rule). */
1234
+ load<TState = GamePayload>(options?: ResumeLoadOptions): Promise<TState | null>;
1235
+ /** The player's saved runs in every resumable game, newest first (the home page's Resume card). */
1236
+ list(): Promise<ResumeEntry[]>;
1237
+ /** Discard another game's run from the home page (online only: throws a `HubError` offline). */
1238
+ discard(slug: string): Promise<void>;
1239
+ /** Say once per blocker why saves are ignored, so a game that forgot `hub.daily.cancel()` finds out. */
1240
+ private warnBlocked;
1241
+ private requireSlug;
1242
+ private schedule;
1243
+ /** Send a waiting save as soon as the page is hidden or closed. */
1244
+ private listenToPage;
1245
+ }
1246
+ //#endregion
1247
+ //#region src/core/saved/saved-list.d.ts
1248
+ /** What the device holds, for one player. */
1249
+ interface DeviceCopies {
1250
+ /** Run copies by slug (`hub.resume`). */
1251
+ readonly runs: ReadonlyMap<string, LocalResume>;
1252
+ /** Daily copies (`hub.daily`), any game. */
1253
+ readonly dailies: readonly LocalDaily[];
1254
+ }
1255
+ //#endregion
1256
+ //#region src/core/saved/hub-saved.d.ts
1257
+ /** How long changes must be quiet before `on('change')` listeners hear the new list, ms. */
1258
+ declare const SAVED_CHANGE_QUIET_MS = 300;
1259
+ /** What `hub.saved` needs from its client. */
1260
+ interface SavedHost {
1261
+ /** This game's slug; `null` on the home page. */
1262
+ readonly slug: string | null;
1263
+ /** The hub's list (one game, or all), or `undefined` when it can't be reached. */
1264
+ fetchList(game: string | null): Promise<readonly SavedEntry[] | undefined>;
1265
+ /** What this device holds for the signed-in player. */
1266
+ deviceCopies(): DeviceCopies;
1267
+ /** Discard a daily or run (works offline where it can). */
1268
+ discard(entry: SavedEntry): Promise<void>;
1269
+ /** Go to a URL (`location.assign`). */
1270
+ navigate(url: string): void;
1271
+ }
1272
+ /** A change listener: this game's games in progress. */
1273
+ type SavedChangeListener = (games: SavedEntry[]) => void;
1274
+ /** An `onOpen` handler: switch to this instance without a reload. */
1275
+ type SavedOpenHandler = (instance: SavedEntry) => void | Promise<void>;
1276
+ /** The detail of {@link SAVED_OPEN_EVENT}. */
1277
+ interface SavedOpenDetail {
1278
+ readonly entry: SavedEntry;
1279
+ }
1280
+ /** `hub.saved`. */
1281
+ declare class HubSaved {
1282
+ private readonly host;
1283
+ /** Each handler with the kinds it takes (every kind when `undefined`). */
1284
+ private readonly openHandlers;
1285
+ private readonly changeListeners;
1286
+ private isListeningForOpen;
1287
+ private isListeningForChange;
1288
+ private changeTimer;
1289
+ constructor(host: SavedHost);
1290
+ /**
1291
+ * The instance this page was opened for (`?hub_open=<id>&hub_kind=<kind>`,
1292
+ * taken out of the address bar), or `null`. Synchronous: read it before
1293
+ * routing or drawing a title screen.
1294
+ */
1295
+ get opened(): SavedOpened | null;
1296
+ /** This game's games in progress: online games waiting on the player first, then the newest. Works offline. */
1297
+ list(options?: SavedListOptions): Promise<SavedEntry[]>;
1298
+ /** Every game's games in progress (the home page and the menu). Online, plus this device's copies. */
1299
+ listAll(): Promise<SavedEntry[]>;
1300
+ /** The badge counts across every game. */
1301
+ counts(): Promise<SavedCounts>;
1302
+ /**
1303
+ * Switch to an instance of THIS game when the player picks it from the
1304
+ * hub menu, without a reload. Without a handler the page reloads with
1305
+ * `?hub_open=<id>`. With `{ kinds }` the handler takes only those kinds;
1306
+ * the others still reload the page (with `?hub_resume=1` or `?daily=` for
1307
+ * a saved run or a daily). Returns a function that removes the handler.
1308
+ */
1309
+ onOpen(handler: SavedOpenHandler, options?: SavedListOptions): () => void;
1310
+ /** Open an instance: in this game through `onOpen` (or a reload), in another game by going there. */
1311
+ open(id: string): Promise<void>;
1312
+ /** Open an entry already in hand (the hub's lists). */
1313
+ openEntry(entry: SavedEntry): void;
1314
+ /**
1315
+ * Discard a daily challenge or a saved run (the caller asks first; the
1316
+ * hub's own lists ask for themselves). Online games are left with
1317
+ * `hub.turns`, never discarded.
1318
+ */
1319
+ discard(id: string): Promise<void>;
1320
+ /** Hear when this game's list changes; returns a function that stops listening. */
1321
+ on(_eventName: 'change', listener: SavedChangeListener): () => void;
1322
+ /** Something of this game's changed (a save here, an event from the hub): tell the listeners, once per quiet spell. */
1323
+ changed(): void;
1324
+ private listenForOpen;
1325
+ private listenForChange;
1326
+ }
1327
+ //#endregion
1328
+ //#region src/core/turns/turn-game.d.ts
1329
+ /** The window event the social runtime fires for each `ev:turn`. */
1330
+ declare const TURN_EVENT = "hub:turn";
1331
+ /** The runtime's live connection (re)opened: anything sent while it was down is fetched again. */
1332
+ declare const TURN_RESYNC_EVENT = "hub:turn-resync";
1333
+ /** The window event the social runtime fires for each `ev:turn.chat`. */
1334
+ declare const TURN_CHAT_EVENT = "hub:turn-chat";
1335
+ /** How often a page without the live runtime looks for moves, ms. */
1336
+ declare const TURN_POLL_MS = 20000;
1337
+ /** How long a visible page waiting on another player's move (or in a lobby) may hear nothing before it fetches the game again, ms. */
1338
+ declare const TURN_QUIET_MS = 15000;
1339
+ /** A game's end: the winning seats, and optionally places and scores by seat. */
1340
+ type TurnOutcome = NonNullable<TurnGameView['outcome']>;
1341
+ /** One move as this seat saw it. */
1342
+ interface TurnMoveEvent<View, Ev> {
1343
+ readonly seq: number;
1344
+ readonly seat: number;
1345
+ /** The view after the move (for a replayed move, the view now). */
1346
+ readonly view: View;
1347
+ readonly events: readonly Ev[];
1348
+ }
1349
+ /** What a game's listeners hear. */
1350
+ interface TurnGameEvents<View, Ev> {
1351
+ move: TurnMoveEvent<View, Ev>;
1352
+ turn: TurnSummary;
1353
+ ended: {
1354
+ readonly outcome: TurnOutcome | null;
1355
+ readonly view: View;
1356
+ };
1357
+ chat: TurnChatLine;
1358
+ }
1359
+ /** What a game needs from its client: the REST calls, by name. */
1360
+ interface TurnGameHost {
1361
+ get(id: string): Promise<TurnGameView>;
1362
+ start(id: string): Promise<TurnGameView>;
1363
+ move(id: string, seq: number, move: Json): Promise<{
1364
+ game: TurnGameView;
1365
+ events: Json[];
1366
+ }>;
1367
+ history(id: string, since: number): Promise<{
1368
+ seq: number;
1369
+ seat: number;
1370
+ events: Json[];
1371
+ }[]>;
1372
+ leave(id: string): Promise<void>;
1373
+ nudge(id: string): Promise<void>;
1374
+ setNotify(id: string, mode: 'on' | 'off'): Promise<void>;
1375
+ rematch(id: string): Promise<TurnGameView>;
1376
+ invite(id: string, userIds: readonly number[]): Promise<TurnGameView>;
1377
+ share(id: string, share: {
1378
+ readonly link?: boolean;
1379
+ readonly public?: boolean;
1380
+ }): Promise<TurnGameView>;
1381
+ autoPlay(id: string): Promise<TurnGameView>;
1382
+ chat(id: string, send: {
1383
+ readonly text?: string;
1384
+ readonly quick?: string;
1385
+ }): Promise<TurnChatLine>;
1386
+ /** The page has a game open (saves of a run are off while it does). */
1387
+ opened(game: object): void;
1388
+ closed(game: object): void;
1389
+ /** Whether the live runtime is on the page (else the game polls). */
1390
+ isLive(): boolean;
1391
+ }
1392
+ type Listener<Payload> = (payload: Payload) => void;
1393
+ /** How `hub.turns` sets a game up. */
1394
+ interface TurnGameOptions {
1395
+ /** Whether this page wants the "your turn" buzz (`hub.turns.setYourTurnBuzz`); read at each buzz. Default on. */
1396
+ readonly isBuzzOn?: () => boolean;
1397
+ }
1398
+ /** One online game. `View`, `Move` and `Ev` (its events) are the game's own JSON types from its rules. */
1399
+ declare class TurnGame<View = GamePayload, Move extends GamePayload = GamePayload, Ev = GamePayload> {
1400
+ readonly id: string;
1401
+ private state;
1402
+ private readonly host;
1403
+ private readonly listeners;
1404
+ private readonly stopListening;
1405
+ private isClosed;
1406
+ private catchingUp;
1407
+ /** The move count this page's own move in flight will make (its live echo is not another player's move). */
1408
+ private ownSeq;
1409
+ /** When the page last heard from the hub about this game (a live event or an answer), epoch ms. */
1410
+ private heardAt;
1411
+ private readonly options;
1412
+ /** This game's own "your turn" buzz switch (`setYourTurnBuzz`). */
1413
+ private isBuzzOn;
1414
+ constructor(host: TurnGameHost, initial: TurnGameView, options?: TurnGameOptions);
1415
+ /** Whose move it is, the seats and the deadline. */
1416
+ get summary(): TurnSummary;
1417
+ /** This seat's view, from the game's rules (`null` before the game starts). */
1418
+ get view(): View;
1419
+ /** This player's seat, or `null` (they left). */
1420
+ get mySeat(): number | null;
1421
+ /** The end, once the game is over. */
1422
+ get outcome(): TurnOutcome | null;
1423
+ /** The last chat lines, oldest first. */
1424
+ get chat(): readonly TurnChatLine[];
1425
+ /** `on`: friends may type (moderated); `quick`: quick-chat and emotes only; `off`. */
1426
+ get chatMode(): TurnGameView['chatMode'];
1427
+ /** The link others can take a seat with (the creator's, while seats are open), else `null`. */
1428
+ get link(): string | null;
1429
+ /** Whether this player may move now without it being their turn (the rules' `mayAct`: a jump-in, calling or catching "UNO"): offer that action. */
1430
+ get mayActNow(): boolean;
1431
+ /** The tournament match this game is, or `null` (show it if you like: "Saturday Cup: your match"). */
1432
+ get tournament(): TurnTournament | null;
1433
+ /**
1434
+ * The "your turn" buzz for this game: a short vibration when this player
1435
+ * becomes due on a live update while the page is visible (never for the
1436
+ * next step of their own turn, a move out of turn, a game just opened or a
1437
+ * catch-up; never when the player turned it off or asks for less motion;
1438
+ * silently nothing where the device can't vibrate). On by default.
1439
+ */
1440
+ setYourTurnBuzz(isOn: boolean): void;
1441
+ /** Creator: start the game (empty seats go to the computer when the game allows). */
1442
+ start(): Promise<void>;
1443
+ /**
1444
+ * Make a move: when it is your turn (`summary.isMyTurn`), or out of turn
1445
+ * when the rules allow it (`mayActNow`). Rejects with a `HubError`: 403
1446
+ * `not_your_turn`, 409 `stale` (the game moved on, or another player's
1447
+ * move got there first: the view is refreshed first), 422 `illegal` with
1448
+ * the rules' own code in `rule`, 500 `rules_error` or 503 `rules_busy`
1449
+ * (the move was not made; the game is fetched again). The answer's `view` is the newest this
1450
+ * page has: moves that arrived live while the answer was on its way (the
1451
+ * computer's, another player's) are never rolled back.
1452
+ */
1453
+ move(move: Move): Promise<{
1454
+ seq: number;
1455
+ view: View;
1456
+ events: Ev[];
1457
+ }>;
1458
+ /** The moves after `sinceSeq`, with their events as this seat may see them ("what happened while you were away"). */
1459
+ history(sinceSeq: number): Promise<{
1460
+ seq: number;
1461
+ seat: number;
1462
+ events: Ev[];
1463
+ }[]>;
1464
+ /** Leave: the computer plays the seat (or, without bots, the others win). The player can't come back. */
1465
+ leave(): Promise<void>;
1466
+ /** Remind whoever must move (once a day). */
1467
+ nudge(): Promise<void>;
1468
+ /** This game's "your turn" alerts. */
1469
+ setNotify(mode: 'on' | 'off'): Promise<void>;
1470
+ /** A finished game: a new one with the same friends (they get an invite). */
1471
+ rematch(): Promise<TurnGame<View, Move, Ev>>;
1472
+ /** Creator, before the start: add an invite link (`link: true`, then read `link`) or list the open seats for anyone (`public`). */
1473
+ share(share: {
1474
+ readonly link?: boolean;
1475
+ readonly public?: boolean;
1476
+ }): Promise<void>;
1477
+ /** Invite friends to free seats (before the start). */
1478
+ invite(userIds: readonly number[]): Promise<void>;
1479
+ /**
1480
+ * Whether "Let the computer play for <name>" may work now: another
1481
+ * player is due, the hub's hand-over wait (`summary.handOverAt`) has
1482
+ * passed and a due player isn't connected (`TurnSeat.connected` false
1483
+ * or unknown), or the turn's `deadline` passed. `false` in a
1484
+ * tournament match, and from a hub too old to say. Re-check it on each
1485
+ * `turn` event (a player coming or going fires one) and when
1486
+ * `handOverAt` comes.
1487
+ */
1488
+ canLetComputerPlay(now?: number): boolean;
1489
+ /**
1490
+ * Let the computer play for whoever is due: a turn whose deadline passed
1491
+ * (or whose player is suspended) is played once; otherwise, when that
1492
+ * player has been due a while (2 minutes) and isn't connected at all, the
1493
+ * computer plays their seat for the rest of the game (it shows as `bot`,
1494
+ * named with who chose it) until they open the game again. Rejects with
1495
+ * 409 `deadline_not_passed` when neither (it never says whether they are
1496
+ * online), and 409 `not_supported` in a tournament match.
1497
+ */
1498
+ letComputerPlay(): Promise<void>;
1499
+ /** Say something: `{ text }` between friends (moderated), `{ quick }` (a quick-chat phrase id) with anyone. */
1500
+ say(send: {
1501
+ readonly text?: string;
1502
+ readonly quick?: string;
1503
+ }): Promise<TurnChatLine>;
1504
+ /**
1505
+ * Fetch the game again, and replay moves missed since the last one this
1506
+ * page saw. An answer older than what the page has by then (live moves
1507
+ * arrived while it was on its way) is dropped, never rolled back to, and
1508
+ * a move that arrived live is never replayed twice.
1509
+ */
1510
+ refresh(): Promise<void>;
1511
+ /** Hear moves, turn changes, the end and chat; returns a function that stops listening. */
1512
+ on<K extends keyof TurnGameEvents<View, Ev>>(eventName: K, listener: Listener<TurnGameEvents<View, Ev>[K]>): () => void;
1513
+ /** Stop live updates for this page (the game itself goes on). */
1514
+ close(): void;
1515
+ /** Take a fresh answer from the hub (one older than what the page has is dropped: no rollback); fire `turn` and `ended` when they changed. */
1516
+ private update;
1517
+ private emit;
1518
+ /** Whether the page is waiting on someone: another player's move, or a lobby. */
1519
+ private isWaitingOnOthers;
1520
+ /** A live `hub:turn` for this game: a move (or a missed run of them), or another change. */
1521
+ private onLive;
1522
+ /** Replay what was missed (one catch-up at a time). */
1523
+ private catchUp;
1524
+ private listen;
1525
+ }
1526
+ //#endregion
1527
+ //#region src/core/turns/hub-turns.d.ts
1528
+ /** The query parameter an invite link carries (`/<slug>/?hub_turn_link=<key>`); the client strips it. */
1529
+ declare const TURN_LINK_PARAM = "hub_turn_link";
1530
+ /** How to make a game. */
1531
+ interface TurnCreateOptions {
1532
+ readonly mode?: string;
1533
+ /** The game's own setup choices (at most 2 KB of JSON). */
1534
+ readonly options?: GamePayload;
1535
+ /** Friends to invite (their seats are held). */
1536
+ readonly invite?: readonly number[];
1537
+ /** Make a link others can take a seat with (`game.link`). */
1538
+ readonly link?: boolean;
1539
+ /** List the open seats in Play online for anyone to take. */
1540
+ readonly public?: boolean;
1541
+ /** Seats in all (within `game.turns.players`). */
1542
+ readonly seats?: number;
1543
+ /** Let the computer fill seats nobody took (default true, when the game has bots). */
1544
+ readonly bots?: boolean;
1545
+ /** How long a turn may wait before the others may let the computer play it (default none). */
1546
+ readonly deadline?: TurnDeadline;
1547
+ /** Start by itself once every seat is taken (the creator needn't come back). */
1548
+ readonly autoStart?: boolean;
1549
+ }
1550
+ /** What `hub.turns` needs from its client. */
1551
+ interface TurnsHost extends TurnGameHost {
1552
+ readonly slug: string | null;
1553
+ create(body: CreateTurnGame): ReturnType<TurnGameHost['get']>;
1554
+ join(id: string): ReturnType<TurnGameHost['get']>;
1555
+ joinByLink(key: string): ReturnType<TurnGameHost['get']>;
1556
+ listOpen(game: string | null): Promise<OpenTurnGame[]>;
1557
+ list(): Promise<SavedEntry[]>;
1558
+ }
1559
+ /** `hub.turns`. */
1560
+ declare class HubTurns {
1561
+ private readonly host;
1562
+ private isBuzzOn;
1563
+ private readonly gameOptions;
1564
+ constructor(host: TurnsHost);
1565
+ /**
1566
+ * This page's "your turn" buzz (a short vibration when the player becomes
1567
+ * due in an open game, see `TurnGame.setYourTurnBuzz`): pass `false` if
1568
+ * your game gives its own signal. On by default.
1569
+ */
1570
+ setYourTurnBuzz(isOn: boolean): void;
1571
+ /** The player's own "Buzz when it's my turn" switch on this device (the menu's Settings show it too). */
1572
+ get playerYourTurnBuzz(): boolean;
1573
+ set playerYourTurnBuzz(isOn: boolean);
1574
+ /** The invite link key this page was opened with (`?hub_turn_link=`), taken out of the address bar; `null` when none. Synchronous. */
1575
+ get link(): string | null;
1576
+ /** Make a game in this page's game (its lobby): the player takes the first seat. */
1577
+ create<View = GamePayload, Move extends GamePayload = GamePayload, Event = GamePayload>(options?: TurnCreateOptions): Promise<TurnGame<View, Move, Event>>;
1578
+ /** Take a seat: by the game's id (an invite, an open seat) or by an invite link's key. */
1579
+ join<View = GamePayload, Move extends GamePayload = GamePayload, Event = GamePayload>(idOrLinkKey: string): Promise<TurnGame<View, Move, Event>>;
1580
+ /** Open a game the player sits in (from `hub.saved.opened`, the Continue list, an invite). */
1581
+ open<View = GamePayload, Move extends GamePayload = GamePayload, Event = GamePayload>(id: string): Promise<TurnGame<View, Move, Event>>;
1582
+ /** This game's online games the player sits in (the same rows as `hub.saved.list({ kinds: ['turns'] })`). */
1583
+ list(): Promise<SavedEntry[]>;
1584
+ /** Open-seat games of this game anyone may join (none across a block). */
1585
+ openLobbies(): Promise<OpenTurnGame[]>;
1586
+ }
1587
+ //#endregion
575
1588
  //#region src/core/version/hub-version.d.ts
576
1589
  /** How a hub regards a client version. */
577
1590
  type ClientSupport =
@@ -589,6 +1602,13 @@ type ClientSupport =
589
1602
  declare class Hub extends HubEconomy {
590
1603
  /** This client's package version (sent as `x-hub-client`). */
591
1604
  readonly clientVersion: string;
1605
+ private resumeApi;
1606
+ private avatarApi;
1607
+ private savedApi;
1608
+ private dailySavesApi;
1609
+ private turnsApi;
1610
+ /** The online games open on this page (a run isn't saved while one is). */
1611
+ private readonly openTurnGames;
592
1612
  /**
593
1613
  * The hub's version and the client versions it supports (`GET /_api/version`),
594
1614
  * read once per page. `null` for a hub from before versioning.
@@ -624,7 +1644,41 @@ declare class Hub extends HubEconomy {
624
1644
  get notify(): typeof notifyForwarder;
625
1645
  /** Races: a single-player game becomes multiplayer by racing. */
626
1646
  get race(): typeof raceForwarder;
627
- /** What the daily system needs from this client: its REST calls and window events. */
1647
+ /**
1648
+ * Resume a saved game: `hub.resume.save({ state, progress })` during a long
1649
+ * single-player run, `clear()` when it ends, `load()` and `requested` to
1650
+ * pick it up again (docs: "Resume saved games"). Needs `game.resumable: true`.
1651
+ */
1652
+ get resume(): HubResume;
1653
+ /**
1654
+ * Players' avatars: `hub.avatar.mine()`, `of(userId)`, `from(seat)`,
1655
+ * `bot(seed)`, `on('change')`, `openDesigner()` and `load()` for the renderer
1656
+ * (docs: "Avatars"). No art or renderer until `load()`.
1657
+ */
1658
+ get avatar(): HubAvatar;
1659
+ /**
1660
+ * Every game the player has in progress, of every kind: `hub.saved.list()`
1661
+ * for this game's "Continue" list, `opened` and `onOpen` to open the one
1662
+ * the player picked (docs: "Saved games").
1663
+ */
1664
+ get saved(): HubSaved;
1665
+ /**
1666
+ * Online games played over minutes or days, run by the hub from the
1667
+ * game's rules module (`game.turns`): `hub.turns.create`, `join`, `open`
1668
+ * (docs/multiplayer.md "Turn games").
1669
+ */
1670
+ get turns(): HubTurns;
1671
+ /** What `hub.turns` needs from this client: its REST calls. */
1672
+ private turnsHost;
1673
+ /** What `hub.saved` needs from this client. */
1674
+ private savedHost;
1675
+ /** Discard a daily or run: this game's run through `hub.resume` (works offline), others on the hub. */
1676
+ private discardSaved;
1677
+ /** What `hub.resume` needs from this client. */
1678
+ private resumeHost;
1679
+ /** This game's daily saves (device copies and their sends). */
1680
+ private dailySaves;
1681
+ /** What the daily system needs from this client: its REST calls, the device copies and window events. */
628
1682
  private dailyHost;
629
1683
  }
630
1684
  /** A ready-to-use client, with the slug and API base detected from the page. */
@@ -640,12 +1694,18 @@ declare function createHub(options?: HubOptions): Hub;
640
1694
  declare const UNSUPPORTED_BY_HUB = "unsupported_by_hub";
641
1695
  /** The code the social runtime gives a call it could not send (no connection). */
642
1696
  declare const OFFLINE_CODE = "offline";
1697
+ /**
1698
+ * The code `getSave` rejects with when the hub can't be reached and this
1699
+ * device has no copy of the slot it can trust (a fresh device or cleared
1700
+ * browser): the slot may well hold a save, so it is not reported empty.
1701
+ */
1702
+ declare const SAVE_UNKNOWN_CODE = "save_unknown";
643
1703
  /**
644
1704
  * A machine-readable error code: one of the contract's public codes
645
1705
  * (`content_rejected`, `rate_limited`, `suspended`, `claim_required`, ...),
646
1706
  * {@link UNSUPPORTED_BY_HUB}, or a code a newer hub added.
647
1707
  */
648
- type HubErrorCode = ErrorCode | typeof UNSUPPORTED_BY_HUB | typeof OFFLINE_CODE | (string & Record<never, never>);
1708
+ type HubErrorCode = ErrorCode | typeof UNSUPPORTED_BY_HUB | typeof OFFLINE_CODE | typeof SAVE_UNKNOWN_CODE | (string & Record<never, never>);
649
1709
  /** The machine-readable extras the hub attaches to an error body. */
650
1710
  interface HubErrorDetails {
651
1711
  /** A stable error code, for code to branch on. */
@@ -658,6 +1718,8 @@ interface HubErrorDetails {
658
1718
  readonly retryAfterMs?: number | undefined;
659
1719
  /** When a suspension or mute ends (epoch ms). */
660
1720
  readonly until?: number | undefined;
1721
+ /** An online game's rules refused a move (`illegal`): the rules' own short code. */
1722
+ readonly rule?: string | undefined;
661
1723
  }
662
1724
  /**
663
1725
  * Thrown by every hub call that fails. `status` is the HTTP status when the
@@ -678,6 +1740,8 @@ declare class HubError extends Error {
678
1740
  readonly retryAfterMs: number | undefined;
679
1741
  /** When a suspension or mute ends (epoch ms). */
680
1742
  readonly until: number | undefined;
1743
+ /** An online game's rules refused a move (`code` is `illegal`): the rules' own short code, such as `cell_taken`. */
1744
+ readonly rule: string | undefined;
681
1745
  /** An error with a player-safe `message`, the HTTP `status` (if any) and the body's extras. */
682
1746
  constructor(message: string, status?: number, details?: HubErrorDetails);
683
1747
  /** `true` when the failure was a network or transport error, not an HTTP status. */
@@ -698,9 +1762,21 @@ declare class ContentRejectedError extends HubError {
698
1762
  constructor(message: string, status: number | undefined, details: HubErrorDetails);
699
1763
  }
700
1764
  //#endregion
1765
+ //#region src/core/turns/your-turn-buzz.d.ts
1766
+ /** The pattern: two short pulses, ms. */
1767
+ declare const YOUR_TURN_BUZZ: readonly number[];
1768
+ /** Where this device keeps the player's choice (`off` turns it off). */
1769
+ declare const YOUR_TURN_BUZZ_KEY = "dg:your-turn-buzz";
1770
+ /** Whether going from `previous` to `next` makes this player newly due (the moment to buzz). */
1771
+ declare function isNewlyDue(previous: Pick<TurnSummary, 'phase' | 'isMyTurn'>, next: Pick<TurnSummary, 'phase' | 'isMyTurn' | 'isMuted'>): boolean;
1772
+ /** The player's own switch on this device (on unless they turned it off). */
1773
+ declare function readYourTurnBuzz(): boolean;
1774
+ /** Save the player's switch on this device. */
1775
+ declare function saveYourTurnBuzz(isOn: boolean): void;
1776
+ //#endregion
701
1777
  //#region src/core/version/client-version.gen.d.ts
702
1778
  /** This client's package version, sent as `x-hub-client` on REST calls and `client` in `hello`. */
703
- declare const HUB_CLIENT_VERSION = "2.0.0-next.0";
1779
+ declare const HUB_CLIENT_VERSION = "2.0.0-next.10";
704
1780
  //#endregion
705
- export { SubmitScoreResult as $, BoardWithTop as A, Json$1 as B, CLIENT_VERSION_HEADER as C, QueuedScoreOptions as D, QueueOpInput as E, GlobalAnalytics as F, Profile as G, LeaderboardResult as H, HubOptions as I, SaveMeta as J, ProfileSummary as K, HubStatus as L, GameInfo$1 as M, GameLeaderboards as N, BufferedEvent as O, GameRecords as P, SubmitScoreOptions as Q, Inventory as R, SyncResult as S, QueueOp as T, MeResponse as U, LeaderboardEntry as V, Metrics$1 as W, SortDir$1 as X, SnapshotResponse as Y, Stats$1 as Z, TradeNoteOptions as _, HubErrorDetails as a, StatResult as b, Hub as c, ClientSupport as d, User as et, HubMenuControl as f, TradeListOptions as g, TradeId as h, HubErrorCode as i, GamePayload as it, GameAnalytics as j, Board$1 as k, createHub as l, TradeBundle as m, ContentRejectedError as n, EndpointName as nt, OfflineError as o, GrantBundle as p, Save as q, HubError as r, EndpointReply as rt, UNSUPPORTED_BY_HUB as s, HUB_CLIENT_VERSION as t, EndpointFields as tt, hub as u, AuthResponse as v, ProfilePatch as w, SyncConflict as x, ProfileUpdate as y, InventorySnapshot as z };
1781
+ export { TradeListOptions as $, AvatarModule as $t, TurnGame as A, HubOptions as At, HubResume as B, ProfileSummary as Bt, TURN_LINK_PARAM as C, Board$1 as Ct, TURN_POLL_MS as D, GameLeaderboards as Dt, TURN_EVENT as E, GameInfo$1 as Et, HubSaved as F, LeaderboardEntry as Ft, ResumeLoadOptions as G, Stats$1 as Gt, RESUME_MAX_STATE_BYTES as H, SaveMeta as Ht, SAVED_CHANGE_QUIET_MS as I, LeaderboardResult as It, ResumeSaveInput as J, User as Jt, ResumeProgress as K, SubmitScoreOptions as Kt, SavedChangeListener as L, MeResponse as Lt, TurnGameOptions as M, Inventory$1 as Mt, TurnMoveEvent as N, InventorySnapshot as Nt, TURN_QUIET_MS as O, GameRecords as Ot, TurnOutcome as P, Json$1 as Pt, TradeId as Q, AvatarHost as Qt, SavedOpenDetail as R, Metrics$1 as Rt, HubTurns as S, BufferedEvent as St, TURN_CHAT_EVENT as T, GameAnalytics as Tt, ResumeBlocker as U, SnapshotResponse as Ut, RESUME_DEBOUNCE_MS as V, Save as Vt, ResumeEntry as W, SortDir$1 as Wt, GrantBundle as X, EndpointName as Xt, HubMenuControl as Y, EndpointFields as Yt, TradeBundle as Z, EndpointReply as Zt, UNSUPPORTED_BY_HUB as _, CLIENT_VERSION_HEADER as _t, HUB_CLIENT_VERSION as a, SyncResult as at, hub as b, QueueOpInput as bt, isNewlyDue as c, OfflineEventDetails as ct, ContentRejectedError as d, OfflineOp as dt, HubAvatar as en, TradeNoteOptions as et, HubError as f, OUTBOX_MAX_CHARS as ft, SAVE_UNKNOWN_CODE as g, ReplayContext as gt, OfflineError as h, OfflineKindDefinition as ht, TurnGameView$1 as i, SyncConflict as it, TurnGameEvents as j, HubStatus as jt, TURN_RESYNC_EVENT as k, GlobalAnalytics as kt, readYourTurnBuzz as l, OfflineEventName as lt, HubErrorDetails as m, OfflineHandler as mt, TurnChatLine$1 as n, ProfileUpdate as nt, YOUR_TURN_BUZZ as o, OfflineApi as ot, HubErrorCode as p, OUTBOX_MAX_ENTRIES as pt, ResumeRun as q, SubmitScoreResult as qt, TurnDeadline$1 as r, StatResult as rt, YOUR_TURN_BUZZ_KEY as s, OUTBOX_FULL_CODE as st, OpenTurnGame$1 as t, CarriesLook as tn, AuthResponse as tt, saveYourTurnBuzz as u, OfflineListener as ut, Hub as v, ProfilePatch as vt, TurnCreateOptions as w, BoardWithTop as wt, ClientSupport as x, QueuedScoreOptions as xt, createHub as y, QueueOp as yt, SavedOpenHandler as z, Profile as zt };
706
1782
  //# sourceMappingURL=index.d.ts.map