@joystyk/daily 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +36 -0
  4. package/README.md +156 -2
  5. package/dist/browser/clock.d.ts +42 -0
  6. package/dist/browser/clock.js +56 -0
  7. package/dist/browser/clock.js.map +1 -0
  8. package/dist/browser/content.d.ts +73 -0
  9. package/dist/browser/content.js +170 -0
  10. package/dist/browser/content.js.map +1 -0
  11. package/dist/browser/errors.d.ts +23 -0
  12. package/dist/browser/errors.js +20 -0
  13. package/dist/browser/errors.js.map +1 -0
  14. package/dist/browser/hash.d.ts +6 -0
  15. package/dist/browser/hash.js +16 -0
  16. package/dist/browser/hash.js.map +1 -0
  17. package/dist/browser/index.d.ts +5 -0
  18. package/dist/browser/index.js +6 -0
  19. package/dist/browser/results.d.ts +37 -0
  20. package/dist/browser/results.js +102 -0
  21. package/dist/browser/results.js.map +1 -0
  22. package/dist/dates.d.ts +20 -0
  23. package/dist/dates.js +30 -0
  24. package/dist/dates.js.map +1 -0
  25. package/dist/ids.d.ts +15 -0
  26. package/dist/ids.js +25 -0
  27. package/dist/ids.js.map +1 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +9 -0
  30. package/dist/local-time.d.ts +17 -0
  31. package/dist/local-time.js +29 -0
  32. package/dist/local-time.js.map +1 -0
  33. package/dist/release.d.ts +23 -0
  34. package/dist/release.js +29 -0
  35. package/dist/release.js.map +1 -0
  36. package/dist/schemas.d.ts +99 -0
  37. package/dist/schemas.js +121 -0
  38. package/dist/schemas.js.map +1 -0
  39. package/dist/share.d.ts +55 -0
  40. package/dist/share.js +73 -0
  41. package/dist/share.js.map +1 -0
  42. package/dist/streaks.d.ts +31 -0
  43. package/dist/streaks.js +69 -0
  44. package/dist/streaks.js.map +1 -0
  45. package/dist/word-stats.d.ts +12 -0
  46. package/dist/word-stats.js +46 -0
  47. package/dist/word-stats.js.map +1 -0
  48. package/package.json +53 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # @joystyk/daily
2
+
3
+ ## 0.2.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [bb095c7]
8
+ - Updated dependencies [bb095c7]
9
+ - @joystyk/contracts@0.2.0
10
+ - @joystyk/storage@0.2.0
11
+ - @joystyk/kit@0.2.0
12
+
13
+ ## 0.1.0
14
+
15
+ ### Minor Changes
16
+
17
+ - Prepare the first compiled library release: ESM and API declarations for every public entry point, runtime assets, and checks against installed npm tarballs. Games and applications remain private.
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+ - @joystyk/contracts@0.1.0
23
+ - @joystyk/kit@0.1.0
24
+ - @joystyk/storage@0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joystyk contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,36 @@
1
+ Third-party material in the Joystyk library release
2
+
3
+ This notice preserves third-party attribution. Original Joystyk code in the 17
4
+ platform libraries under packages/ uses the MIT license in packages/LICENSE.
5
+ That choice does not license the private applications, games or other repository
6
+ material, and does not replace the third-party terms below.
7
+
8
+ @joystyk/contracts
9
+ ------------------
10
+ Standard Schema v1 type definitions from @standard-schema/spec 1.1.0:
11
+ https://github.com/standard-schema/standard-schema
12
+ https://standardschema.dev
13
+ Copyright (c) 2024 Colin McDonnell. MIT License.
14
+ The complete license and source/change notes accompany the distributed
15
+ dist/standard-schema.d.ts definitions. Changes: individually exported declarations
16
+ and formatting; the declarations themselves are unchanged.
17
+
18
+ @joystyk/identity and @joystyk/content
19
+ -----------------------------------
20
+ Word blocklists derived from List of Dirty, Naughty, Obscene, and Otherwise Bad
21
+ Words, copyright Shutterstock, Inc. and contributors:
22
+ https://github.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words
23
+ Licensed under Creative Commons Attribution 4.0 International:
24
+ https://creativecommons.org/licenses/by/4.0/
25
+ Identity uses the English list at commit
26
+ 5faf2ba42d7b1c0977169ec3611df25a3c08eb13. Changes include reducing or dropping
27
+ phrases, dropping words with everyday meanings, substituting stems for inflections
28
+ and adding entries. Identity's README and content's lists/blocklist.txt retain
29
+ their source-specific provenance and changes.
30
+
31
+ @joystyk/content
32
+ ---------------
33
+ SCOWL-derived common, vocabulary and ranked word lists use SCOWL 2020.12.07.
34
+ Their complete source licenses and attribution accompany the package in
35
+ lists/SCOWL-COPYRIGHT. Word-list transformations and build provenance are described
36
+ in the package README. Preserve that copyright file when redistributing the lists.
package/README.md CHANGED
@@ -1,3 +1,157 @@
1
- # Temporary Holding Version
1
+ # @joystyk/daily
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Everything about daily puzzles that every daily game shares: which day it is, when the next
4
+ puzzle comes out in the player's own time, puzzle ids, results and streaks, spoiler-free
5
+ share text, and a client that downloads each day's puzzle and keeps it for offline play. No
6
+ game writes these again. The design is content-pipeline.md (Joystyk host reference)
7
+ ("Which day is today", "Delivery", "Results and streaks", "Share text") and the daily rows of
8
+ offline-and-updates.md (Joystyk host reference).
9
+
10
+ ## npm package
11
+
12
+ The first `0.1.0` release is being prepared; it has not been published. Once available:
13
+
14
+ ```sh
15
+ npm install @joystyk/daily
16
+ ```
17
+
18
+ Node consumers require Node 24.15 or later. Browser entry points require a modern
19
+ browser and should be loaded in the browser.
20
+
21
+ | Import | Format |
22
+ | --- | --- |
23
+ | `@joystyk/daily` | ES module and TypeScript declarations |
24
+ | `@joystyk/daily/browser` | ES module and TypeScript declarations |
25
+
26
+ Only the entries listed above are public API. Installed packages contain compiled ESM,
27
+ type declarations and runtime assets. Repository-relative links describe the Joystyk
28
+ workspace and are not needed to use the npm package.
29
+
30
+ ## Entry points
31
+
32
+ | Import | Runs in | Contents |
33
+ | --- | --- | --- |
34
+ | `@joystyk/daily` | Anywhere (no clock, storage, network or DOM) | Dates and release rules, local-time text, puzzle ids, `streaks`, share text, schemas |
35
+ | `@joystyk/daily/browser` | Browser | `today`, `nextReleaseLocal`, `watchDay`, the content client with its cache and prefetch, the results store |
36
+
37
+ The core imports only `@joystyk/contracts`, `@joystyk/kit` (daily ids and calendar
38
+ arithmetic) and Zod. Only the browser entry imports `@joystyk/storage`.
39
+ `tsconfig.core.json` type-checks the core without DOM types.
40
+
41
+ ## Use
42
+
43
+ ```ts
44
+ import { canStart, gridRows, shareText } from '@joystyk/daily';
45
+ import { createDailyClient, createDailyResults, nextReleaseLocal, today } from '@joystyk/daily/browser';
46
+
47
+ const date = today(); // the UTC date of the device clock
48
+ const client = createDailyClient({ baseUrl: '/daily', gameId: 'daily-word', config: engine.schemas.config, storage });
49
+ const puzzle = await client.puzzle(date); // Result: decoded config, from network or cache
50
+ if (!puzzle.ok) frame.notice(puzzle.error.message); // "This puzzle needs a connection…"
51
+
52
+ const results = createDailyResults(storage, profile.id);
53
+ const { streaks } = (await results.record(dailyResult, today())).value; // { current, best, winCurrent, winBest }
54
+
55
+ shareText({ title: 'Daily Word', date, headline: '4/6', rows: gridRows(board, legend, (i) => `Guess ${i + 1}`), url });
56
+ shareText(sameInput, 'words'); // "Copy as text": rows in plain words
57
+
58
+ const next = nextReleaseLocal(); // { time: '8:00 PM', countdown: 'in 6 hours', … }
59
+ frame.notice(`New puzzle at ${next.time} (${next.countdown})`);
60
+
61
+ await client.prefetch(date); // the PWA, at a safe point, if installed or pinned
62
+ await client.prune(date); // drop cached days before yesterday
63
+ ```
64
+
65
+ ## Days
66
+
67
+ - **One puzzle per UTC day** (host policy). **`utcDate(instant)`** is the puzzle date at an
68
+ instant, the same everywhere at once. In the browser, `today()` reads `Date.now()`. Travel
69
+ and daylight saving never change the date.
70
+ - **Release:** puzzle D comes out at 00:00 UTC on D (`releaseInstant`, `isReleased`);
71
+ `nextRelease(instant)` and `untilNextDay(instant)` give the next one. The server's index
72
+ lists dates up to tomorrow (`latestListedDate`), so installed clients can prefetch it.
73
+ - **Local time for display only.** `localTimeText(instant, { locale, timeZone })` gives the
74
+ release time as the player reads it ("8:00 PM"), and `countdownText(ms, locale)` a plain
75
+ countdown ("in 6 hours"). Both default to the runtime's locale and zone. In the browser,
76
+ `nextReleaseLocal()` puts them together with the device clock.
77
+ - **Starting and finishing:** a player starts only today's puzzle (`canStart`); a prefetched
78
+ puzzle for tomorrow stays locked until its day begins. A puzzle started is kept, and its
79
+ result counts for the puzzle's date even when it is finished after the next release
80
+ (`creditDate`).
81
+ - `addDays`, `daysBetween`, `dayNumber`, `startOfDay` and `puzzleNumber` work on
82
+ `yyyy-mm-dd` strings. The first three come from the kit.
83
+
84
+ ## Streaks
85
+
86
+ `streaks(results, today)` is pure and is the only place streaks are decided. Streaks count
87
+ UTC days:
88
+
89
+ - **Streak:** consecutive puzzle dates finished (won, lost or completed, with or without
90
+ help; any variant). **Win streak:** consecutive dates won; only games that can be lost show it.
91
+ - Today not yet played leaves a streak standing. Any missed date before it sets the current
92
+ streak to 0; the best is kept. There are no freezes or grace days.
93
+ - A skipped date counts as missed; a repeated date counts once. Results dated after `today`
94
+ (a clock set ahead) count toward the best only.
95
+
96
+ The results store keeps the results and writes a summary to the storage package's streak
97
+ records after every `record` and `streaks` call: the days-finished streak in the player's
98
+ `StreakStore`, the win streak at `<player>/<game>/win` in the same shape. A stored best is
99
+ never lowered, ready for v2 sync's "max wins".
100
+
101
+ ## Share text
102
+
103
+ ```text
104
+ Daily Word 2026-10-08 4/6 ← title, date (or "#412"), variant in brackets, headline
105
+ Solved in 1:42, no help ← summary line, when the game has one
106
+ ⬜🟨⬜⬜⬜ ← symbol rows; in the words form, "Guess 1: 1 wrong place."
107
+ https://…/games/daily-word ← the game page, with no query string or fragment
108
+ ```
109
+
110
+ The game supplies the headline, summary and rows; `gridRows` builds rows in both forms from a
111
+ grid of cell kinds and a legend. `shareText` throws on a symbol row containing any letter or
112
+ digit, a line break in a one-line field, or a URL with a query or fragment, since each could
113
+ leak an answer or carry tracking. Never put letters, clues or answers in a summary.
114
+
115
+ ## Delivery formats
116
+
117
+ The site serves `/daily/<game>/index.json` (`no-cache`) and
118
+ `/daily/<game>/<date>.<hash>.json` (immutable). Both decode through this package's schemas:
119
+
120
+ | Schema | Shape |
121
+ | --- | --- |
122
+ | `dailyIndex` | `{ schemaVersion: 1, gameId, dates: [{ date, hash, size }] }` |
123
+ | `dailyFile` / `dailyEnvelope(config)` | `{ schemaVersion: 1, puzzleId, date, contentHash, config }`; `dailyEnvelope` also decodes `config` with the game's `schemas.config` and can require a game, date and hash |
124
+ | `dailyResult` | The `DailyResult` record from content-pipeline.md, plus an optional `shareWords` (the rows in words, for *Copy as text*) |
125
+
126
+ The **content hash** is the first 12 hex digits of SHA-256 over `canonicalJson(config)`
127
+ (keys sorted by UTF-16 code unit, no whitespace). The client checks it with Web Crypto before
128
+ caching a file. The `content` package must hash the same way.
129
+
130
+ ## The content client
131
+
132
+ `puzzle(date)` revalidates the index, refuses a date the index doesn't list, uses the cached
133
+ file when its hash is still current, and otherwise downloads, checks and caches it. If the
134
+ network or server fails it falls back to any cached copy for that date; with none, it returns
135
+ `Offline` with a plain message. Every read, from the network or the cache, decodes the file
136
+ again, so a new game build with a stricter schema never trusts an old download. Requests send
137
+ no cookies, referrer or query string.
138
+
139
+ `prefetch(today)` downloads today and the next day (`prefetchDates`), where the index lists
140
+ them and the device lacks them; never further ahead (host policy). The package doesn't decide who
141
+ prefetches: the PWA calls it for installed clients and pinned games, at a safe point.
142
+
143
+ Downloads live in the storage package's `game-data` store under `~daily/<game>/<date>`;
144
+ results under `<player>/~daily/<game>/<date>/<variant or ->`. A `~` can't start a player id or
145
+ a game id, so neither meets a game's own keys.
146
+
147
+ ## Tests
148
+
149
+ - `src/*.test.ts` (Node): the UTC date across midnight, daylight saving, leap days and 1970,
150
+ with a year-long hourly sweep against `Date#toISOString`; release and listing rules at
151
+ every quarter hour of three days; local release times in several zones and locales, and
152
+ the countdown text; streak rules (with a randomized check against a day-by-day count);
153
+ share text for Daily Word and Crossword; and the schemas.
154
+ - `src/browser/*.browser.test.ts` (Chromium): the content client against a fake `fetch` over
155
+ real IndexedDB and Web Crypto (cache hits, offline, withdraw and replace, bad hashes and
156
+ configs, prefetch of the next day only, prune), the results store, `today`,
157
+ `nextReleaseLocal` and `watchDay`.
@@ -0,0 +1,42 @@
1
+ import { type LocalTimeOptions } from '../local-time.js';
2
+ export type ClockOptions = {
3
+ /** Epoch milliseconds. Defaults to `Date.now`. */
4
+ readonly now?: () => number;
5
+ };
6
+ /** Today's puzzle date, `yyyy-mm-dd`: the UTC date of the device clock. */
7
+ export declare const today: (options?: ClockOptions) => string;
8
+ /** Milliseconds until the next puzzle comes out, at the start of the next UTC day. */
9
+ export declare const timeUntilNextDay: (options?: ClockOptions) => number;
10
+ /** The next release, ready to show: "New puzzle at 8:00 PM (in 6 hours)". */
11
+ export type NextRelease = {
12
+ /** The date of the next puzzle, `yyyy-mm-dd`. */
13
+ readonly date: string;
14
+ /** When it comes out, in epoch milliseconds. */
15
+ readonly instant: number;
16
+ /** Milliseconds from now until then. */
17
+ readonly inMs: number;
18
+ /** The release time in the player's zone, such as "8:00 PM". */
19
+ readonly time: string;
20
+ /** A plain countdown, such as "in 6 hours". */
21
+ readonly countdown: string;
22
+ };
23
+ /**
24
+ * When the next puzzle comes out, in the device's time zone and locale unless `timeZone` or
25
+ * `locale` says otherwise. Call it again at safe points to refresh the countdown; it sets no
26
+ * timers of its own.
27
+ */
28
+ export declare const nextReleaseLocal: (options?: ClockOptions & LocalTimeOptions) => NextRelease;
29
+ export type DayWatcher = {
30
+ /** Compares the date with the last one seen, and reports a change. Call it at safe points. */
31
+ check(): void;
32
+ /** Stops listening. Idempotent. */
33
+ stop(): void;
34
+ };
35
+ /**
36
+ * Tells the frame when the puzzle date changes: on `visibilitychange` (a tab brought back
37
+ * after the release) and whenever the frame calls `check` at a safe point. It never
38
+ * interrupts a game; the frame decides when to offer the new day.
39
+ */
40
+ export declare const watchDay: (onChange: (date: string, previous: string) => void, options?: ClockOptions & {
41
+ readonly target?: Pick<Document, "addEventListener" | "removeEventListener">;
42
+ }) => DayWatcher;
@@ -0,0 +1,56 @@
1
+ import { untilNextDay, utcDate } from "../dates.js";
2
+ import { countdownText, localTimeText } from "../local-time.js";
3
+ import { nextRelease } from "../release.js";
4
+ //#region src/browser/clock.ts
5
+ /** Today's puzzle date, `yyyy-mm-dd`: the UTC date of the device clock. */
6
+ var today = (options = {}) => utcDate((options.now ?? Date.now)());
7
+ /** Milliseconds until the next puzzle comes out, at the start of the next UTC day. */
8
+ var timeUntilNextDay = (options = {}) => untilNextDay((options.now ?? Date.now)());
9
+ /**
10
+ * When the next puzzle comes out, in the device's time zone and locale unless `timeZone` or
11
+ * `locale` says otherwise. Call it again at safe points to refresh the countdown; it sets no
12
+ * timers of its own.
13
+ */
14
+ var nextReleaseLocal = (options = {}) => {
15
+ const now = (options.now ?? Date.now)();
16
+ const instant = nextRelease(now);
17
+ const inMs = instant - now;
18
+ return {
19
+ date: utcDate(instant),
20
+ instant,
21
+ inMs,
22
+ time: localTimeText(instant, options),
23
+ countdown: countdownText(inMs, options.locale)
24
+ };
25
+ };
26
+ /**
27
+ * Tells the frame when the puzzle date changes: on `visibilitychange` (a tab brought back
28
+ * after the release) and whenever the frame calls `check` at a safe point. It never
29
+ * interrupts a game; the frame decides when to offer the new day.
30
+ */
31
+ var watchDay = (onChange, options = {}) => {
32
+ const target = options.target ?? document;
33
+ let seen = today(options);
34
+ let stopped = false;
35
+ const check = () => {
36
+ if (stopped) return;
37
+ const date = today(options);
38
+ if (date === seen) return;
39
+ const previous = seen;
40
+ seen = date;
41
+ onChange(date, previous);
42
+ };
43
+ target.addEventListener("visibilitychange", check);
44
+ return {
45
+ check,
46
+ stop: () => {
47
+ if (stopped) return;
48
+ stopped = true;
49
+ target.removeEventListener("visibilitychange", check);
50
+ }
51
+ };
52
+ };
53
+ //#endregion
54
+ export { nextReleaseLocal, timeUntilNextDay, today, watchDay };
55
+
56
+ //# sourceMappingURL=clock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.js","names":[],"sources":["../../src/browser/clock.ts"],"sourcesContent":["// Today from the device clock, the next release in the player's own time, and a watcher that\n// notices when the puzzle date changes. The date is the UTC date (D-46); only the times shown\n// to the player are local.\n\nimport { untilNextDay, utcDate } from '../dates.ts';\nimport { countdownText, localTimeText, type LocalTimeOptions } from '../local-time.ts';\nimport { nextRelease } from '../release.ts';\n\nexport type ClockOptions = {\n /** Epoch milliseconds. Defaults to `Date.now`. */\n readonly now?: () => number;\n};\n\n/** Today's puzzle date, `yyyy-mm-dd`: the UTC date of the device clock. */\nexport const today = (options: ClockOptions = {}): string => utcDate((options.now ?? Date.now)());\n\n/** Milliseconds until the next puzzle comes out, at the start of the next UTC day. */\nexport const timeUntilNextDay = (options: ClockOptions = {}): number => untilNextDay((options.now ?? Date.now)());\n\n/** The next release, ready to show: \"New puzzle at 8:00 PM (in 6 hours)\". */\nexport type NextRelease = {\n /** The date of the next puzzle, `yyyy-mm-dd`. */\n readonly date: string;\n /** When it comes out, in epoch milliseconds. */\n readonly instant: number;\n /** Milliseconds from now until then. */\n readonly inMs: number;\n /** The release time in the player's zone, such as \"8:00 PM\". */\n readonly time: string;\n /** A plain countdown, such as \"in 6 hours\". */\n readonly countdown: string;\n};\n\n/**\n * When the next puzzle comes out, in the device's time zone and locale unless `timeZone` or\n * `locale` says otherwise. Call it again at safe points to refresh the countdown; it sets no\n * timers of its own.\n */\nexport const nextReleaseLocal = (options: ClockOptions & LocalTimeOptions = {}): NextRelease => {\n const now = (options.now ?? Date.now)();\n const instant = nextRelease(now);\n const inMs = instant - now;\n return {\n date: utcDate(instant),\n instant,\n inMs,\n time: localTimeText(instant, options),\n countdown: countdownText(inMs, options.locale),\n };\n};\n\nexport type DayWatcher = {\n /** Compares the date with the last one seen, and reports a change. Call it at safe points. */\n check(): void;\n /** Stops listening. Idempotent. */\n stop(): void;\n};\n\n/**\n * Tells the frame when the puzzle date changes: on `visibilitychange` (a tab brought back\n * after the release) and whenever the frame calls `check` at a safe point. It never\n * interrupts a game; the frame decides when to offer the new day.\n */\nexport const watchDay = (\n onChange: (date: string, previous: string) => void,\n options: ClockOptions & { readonly target?: Pick<Document, 'addEventListener' | 'removeEventListener'> } = {},\n): DayWatcher => {\n const target = options.target ?? document;\n let seen = today(options);\n let stopped = false;\n const check = (): void => {\n if (stopped) return;\n const date = today(options);\n if (date === seen) return;\n const previous = seen;\n seen = date;\n onChange(date, previous);\n };\n target.addEventListener('visibilitychange', check);\n return {\n check,\n stop: () => {\n if (stopped) return;\n stopped = true;\n target.removeEventListener('visibilitychange', check);\n },\n };\n};\n"],"mappings":";;;;;AAcA,IAAa,SAAS,UAAwB,CAAC,MAAc,SAAS,QAAQ,OAAO,KAAK,IAAA,CAAK,CAAC;;AAGhG,IAAa,oBAAoB,UAAwB,CAAC,MAAc,cAAc,QAAQ,OAAO,KAAK,IAAA,CAAK,CAAC;;;;;;AAqBhH,IAAa,oBAAoB,UAA2C,CAAC,MAAmB;CAC9F,MAAM,OAAO,QAAQ,OAAO,KAAK,IAAA,CAAK;CACtC,MAAM,UAAU,YAAY,GAAG;CAC/B,MAAM,OAAO,UAAU;CACvB,OAAO;EACL,MAAM,QAAQ,OAAO;EACrB;EACA;EACA,MAAM,cAAc,SAAS,OAAO;EACpC,WAAW,cAAc,MAAM,QAAQ,MAAM;CAC/C;AACF;;;;;;AAcA,IAAa,YACX,UACA,UAA2G,CAAC,MAC7F;CACf,MAAM,SAAS,QAAQ,UAAU;CACjC,IAAI,OAAO,MAAM,OAAO;CACxB,IAAI,UAAU;CACd,MAAM,cAAoB;EACxB,IAAI,SAAS;EACb,MAAM,OAAO,MAAM,OAAO;EAC1B,IAAI,SAAS,MAAM;EACnB,MAAM,WAAW;EACjB,OAAO;EACP,SAAS,MAAM,QAAQ;CACzB;CACA,OAAO,iBAAiB,oBAAoB,KAAK;CACjD,OAAO;EACL;EACA,YAAY;GACV,IAAI,SAAS;GACb,UAAU;GACV,OAAO,oBAAoB,oBAAoB,KAAK;EACtD;CACF;AACF"}
@@ -0,0 +1,73 @@
1
+ import { type Json, type Result, type Validator } from '@joystyk/contracts';
2
+ import { type Storage } from '@joystyk/storage';
3
+ import { type DailyEnvelope, type DailyIndex } from '../schemas.js';
4
+ import { type DailyError } from './errors.js';
5
+ /** A downloaded puzzle file, as the cache keeps it. */
6
+ export type CachedPuzzle = {
7
+ readonly hash: string;
8
+ /** The file exactly as served; decoded again on every read. */
9
+ readonly file: Json;
10
+ };
11
+ /** Version 1 of a cached puzzle file. */
12
+ export declare const CACHED_PUZZLE_RECORD: import("@joystyk/storage").RecordKind<CachedPuzzle>;
13
+ /** The cache key prefix for one game's downloaded puzzles. */
14
+ export declare const cachePrefix: (gameId: string) => string;
15
+ /** Just enough of `fetch` for the client; tests pass a fake. */
16
+ export type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
17
+ export type DailyClientOptions<Config> = {
18
+ /** Where the site serves daily content: "/daily" or "https://example.com/daily". */
19
+ readonly baseUrl: string;
20
+ readonly gameId: string;
21
+ /** The game's `schemas.config`, which every puzzle file's `config` must pass. */
22
+ readonly config: Validator<Config>;
23
+ readonly storage: Storage;
24
+ /** Defaults to the global `fetch`. */
25
+ readonly fetch?: FetchLike;
26
+ /**
27
+ * Check each downloaded file's content hash with Web Crypto before keeping it. Default true;
28
+ * skipped where `crypto.subtle` is missing (pages served over plain http).
29
+ */
30
+ readonly verifyHash?: boolean;
31
+ };
32
+ /** A decoded puzzle and where it came from. */
33
+ export type DailyPuzzle<Config> = DailyEnvelope<Config> & {
34
+ readonly source: 'network' | 'cache';
35
+ };
36
+ /** What a prefetch did with each date. */
37
+ export type PrefetchReport = {
38
+ /** Downloaded and kept now. */
39
+ readonly fetched: readonly string[];
40
+ /** Already on the device with the current hash. */
41
+ readonly cached: readonly string[];
42
+ /** Not listed in the index yet. */
43
+ readonly unavailable: readonly string[];
44
+ /** Listed, but the download or decode failed. Tried again next time. */
45
+ readonly failed: readonly string[];
46
+ /** Set when the index itself couldn't be loaded; then every date is in `failed`. */
47
+ readonly error: DailyError | null;
48
+ };
49
+ export interface DailyClient<Config> {
50
+ /** Fetches and decodes the game's index, revalidating every time. */
51
+ index(): Promise<Result<DailyIndex, DailyError>>;
52
+ /**
53
+ * The puzzle for `date`. Checks the index; uses the cached file when its hash is current,
54
+ * otherwise downloads, verifies, decodes and caches it. Offline, or when the server fails,
55
+ * it falls back to a cached file only when the index itself is unreachable. A known
56
+ * superseded or unlisted copy is purged, including for subsequent offline reads.
57
+ * Pick `date` with `canStart`; this client doesn't judge which dates a player may start.
58
+ */
59
+ puzzle(date: string): Promise<Result<DailyPuzzle<Config>, DailyError>>;
60
+ /** The cached puzzle for `date`, decoded, or null. Never touches the network. */
61
+ cached(date: string): Promise<DailyPuzzle<Config> | null>;
62
+ /**
63
+ * Downloads today's puzzle and the next day's (D-46), where the index lists them and the
64
+ * device doesn't hold them yet. The next day's puzzle stays locked until its UTC day begins
65
+ * (`canStart`). The PWA calls this at a safe point for installed clients and pinned games;
66
+ * this package doesn't decide who prefetches.
67
+ */
68
+ prefetch(today: string): Promise<PrefetchReport>;
69
+ /** Deletes cached puzzles older than `keepPast` days before `today` (default 1: keep yesterday). */
70
+ prune(today: string, keepPast?: number): Promise<void>;
71
+ }
72
+ /** Creates the content client for one daily game. */
73
+ export declare const createDailyClient: <Config>(options: DailyClientOptions<Config>) => DailyClient<Config>;
@@ -0,0 +1,170 @@
1
+ import { addDays, isIsoDate } from "../dates.js";
2
+ import { prefetchDates } from "../release.js";
3
+ import { CONTENT_HASH, dailyEnvelope, dailyIndex, decode, json } from "../schemas.js";
4
+ import { dailyError } from "./errors.js";
5
+ import { contentHashOf } from "./hash.js";
6
+ import { err, isGameId, isJson, ok } from "@joystyk/contracts";
7
+ import { defineRecordKind } from "@joystyk/storage";
8
+ import * as z from "zod/mini";
9
+ //#region src/browser/content.ts
10
+ /** Version 1 of a cached puzzle file. */
11
+ var CACHED_PUZZLE_RECORD = defineRecordKind({
12
+ store: "game-data",
13
+ version: 1,
14
+ schema: z.object({
15
+ hash: z.string().check(z.regex(CONTENT_HASH)),
16
+ file: json
17
+ }),
18
+ label: "downloaded daily puzzle"
19
+ });
20
+ /** The cache key prefix for one game's downloaded puzzles. */
21
+ var cachePrefix = (gameId) => `~daily/${gameId}/`;
22
+ var trimSlash = (url) => url.replace(/\/+$/, "");
23
+ var checkDate = (date) => {
24
+ if (!isIsoDate(date)) throw new RangeError(`Expected a "yyyy-mm-dd" date, got ${JSON.stringify(date)}.`);
25
+ };
26
+ /** Creates the content client for one daily game. */
27
+ var createDailyClient = (options) => {
28
+ const { gameId, storage } = options;
29
+ if (!isGameId(gameId)) throw new RangeError(`Expected a kebab-case game id, got ${JSON.stringify(gameId)}.`);
30
+ const base = `${trimSlash(options.baseUrl)}/${gameId}`;
31
+ const fetchJson = options.fetch ?? ((url, init) => fetch(url, init));
32
+ const verify = options.verifyHash ?? true;
33
+ const cache = storage.records(CACHED_PUZZLE_RECORD);
34
+ const keyOf = (date) => `${cachePrefix(gameId)}${date}`;
35
+ const getJson = async (url, init) => {
36
+ let response;
37
+ try {
38
+ response = await fetchJson(url, {
39
+ ...init,
40
+ credentials: "omit",
41
+ referrerPolicy: "no-referrer"
42
+ });
43
+ } catch (cause) {
44
+ return err(dailyError("Offline", String(cause)));
45
+ }
46
+ if (response.status === 404) return err(dailyError("NotPublished", `${url}: 404`));
47
+ if (!response.ok) return err(dailyError("Failed", `${url}: HTTP ${response.status}`));
48
+ try {
49
+ return ok(await response.json());
50
+ } catch (cause) {
51
+ return err(dailyError("Invalid", `${url}: ${String(cause)}`));
52
+ }
53
+ };
54
+ const index = async () => {
55
+ const url = `${base}/index.json`;
56
+ const raw = await getJson(url, { cache: "no-cache" });
57
+ if (!raw.ok) return raw;
58
+ const decoded = await decode(dailyIndex, raw.value);
59
+ if (!decoded.ok) return err(dailyError("Invalid", [url, ...decoded.error]));
60
+ if (decoded.value.gameId !== gameId) return err(dailyError("Invalid", [url, `index is for ${decoded.value.gameId}`]));
61
+ return decoded;
62
+ };
63
+ const decodeFile = async (raw, date, hash) => decode(dailyEnvelope(options.config, {
64
+ gameId,
65
+ date,
66
+ contentHash: hash
67
+ }), raw);
68
+ const cached = async (date) => {
69
+ checkDate(date);
70
+ const record = await cache.get(keyOf(date));
71
+ if (!record.ok || record.value === null) return null;
72
+ const decoded = await decodeFile(record.value.file, date, record.value.hash);
73
+ if (!decoded.ok) {
74
+ await cache.delete(keyOf(date));
75
+ return null;
76
+ }
77
+ return {
78
+ ...decoded.value,
79
+ source: "cache"
80
+ };
81
+ };
82
+ const download = async (date, hash) => {
83
+ const url = `${base}/${date}.${hash}.json`;
84
+ const raw = await getJson(url, {});
85
+ if (!raw.ok) return raw.error.code === "NotPublished" ? err(dailyError("Failed", raw.error.detail)) : raw;
86
+ const decoded = await decodeFile(raw.value, date, hash);
87
+ if (!decoded.ok || !isJson(raw.value)) return err(dailyError("Invalid", [url, ...decoded.ok ? [] : decoded.error]));
88
+ if (verify) {
89
+ const actual = await contentHashOf(raw.value.config);
90
+ if (actual !== null && actual !== hash) return err(dailyError("Invalid", [url, `content hash is ${actual}`]));
91
+ }
92
+ await cache.set(keyOf(date), {
93
+ hash,
94
+ file: raw.value
95
+ });
96
+ return ok({
97
+ ...decoded.value,
98
+ source: "network"
99
+ });
100
+ };
101
+ const load = async (date, listing, held) => {
102
+ const entry = listing.dates.find((candidate) => candidate.date === date);
103
+ if (entry === void 0) {
104
+ await cache.delete(keyOf(date));
105
+ return err(dailyError("NotPublished", `${gameId} ${date}`));
106
+ }
107
+ if (held !== null && held.contentHash === entry.hash) return ok(held);
108
+ if (held !== null) await cache.delete(keyOf(date));
109
+ return download(date, entry.hash);
110
+ };
111
+ return {
112
+ index,
113
+ cached,
114
+ puzzle: async (date) => {
115
+ checkDate(date);
116
+ const held = await cached(date);
117
+ const listing = await index();
118
+ if (!listing.ok) return held === null ? listing : ok(held);
119
+ return load(date, listing.value, held);
120
+ },
121
+ prefetch: async (today) => {
122
+ checkDate(today);
123
+ const dates = prefetchDates(today);
124
+ const report = {
125
+ fetched: [],
126
+ cached: [],
127
+ unavailable: [],
128
+ failed: []
129
+ };
130
+ const listing = await index();
131
+ if (!listing.ok) return {
132
+ ...report,
133
+ failed: [...dates],
134
+ error: listing.error
135
+ };
136
+ for (const date of dates) {
137
+ const entry = listing.value.dates.find((candidate) => candidate.date === date);
138
+ if (entry === void 0) {
139
+ await cache.delete(keyOf(date));
140
+ report.unavailable.push(date);
141
+ continue;
142
+ }
143
+ if ((await cached(date))?.contentHash === entry.hash) {
144
+ report.cached.push(date);
145
+ continue;
146
+ }
147
+ await cache.delete(keyOf(date));
148
+ ((await download(date, entry.hash)).ok ? report.fetched : report.failed).push(date);
149
+ }
150
+ return {
151
+ ...report,
152
+ error: null
153
+ };
154
+ },
155
+ prune: async (today, keepPast = 1) => {
156
+ checkDate(today);
157
+ const oldest = addDays(today, -keepPast);
158
+ const keys = await cache.keys(cachePrefix(gameId));
159
+ if (!keys.ok) return;
160
+ for (const key of keys.value) {
161
+ const date = key.slice(cachePrefix(gameId).length);
162
+ if (!isIsoDate(date) || date < oldest) await cache.delete(key);
163
+ }
164
+ }
165
+ };
166
+ };
167
+ //#endregion
168
+ export { CACHED_PUZZLE_RECORD, cachePrefix, createDailyClient };
169
+
170
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","names":[],"sources":["../../src/browser/content.ts"],"sourcesContent":["// The content client: fetch a game's index and puzzle files from the site, decode them through\n// the game's schema, and keep them on the device so a downloaded day plays offline\n// (content-pipeline.md, \"Delivery\" and \"Prefetching for offline play\").\n//\n// Downloaded puzzles are kept as raw JSON in the storage package's `game-data` store, under\n// `~daily/<game>/<date>`. The `~` can't start a player id, so these keys never meet a player's\n// own records. Every read, from the network or from the cache, decodes the file again.\n\nimport { err, isGameId, isJson, ok, type Json, type Result, type Validator } from '@joystyk/contracts';\nimport { defineRecordKind, type RecordStore, type Storage } from '@joystyk/storage';\nimport * as z from 'zod/mini';\nimport { addDays, isIsoDate } from '../dates.ts';\nimport { prefetchDates } from '../release.ts';\nimport {\n CONTENT_HASH,\n dailyEnvelope,\n dailyIndex,\n decode,\n json,\n type DailyEnvelope,\n type DailyIndex,\n} from '../schemas.ts';\nimport { dailyError, type DailyError } from './errors.ts';\nimport { contentHashOf } from './hash.ts';\n\n/** A downloaded puzzle file, as the cache keeps it. */\nexport type CachedPuzzle = {\n readonly hash: string;\n /** The file exactly as served; decoded again on every read. */\n readonly file: Json;\n};\n\n/** Version 1 of a cached puzzle file. */\nexport const CACHED_PUZZLE_RECORD = defineRecordKind<CachedPuzzle>({\n store: 'game-data',\n version: 1,\n schema: z.object({ hash: z.string().check(z.regex(CONTENT_HASH)), file: json }),\n label: 'downloaded daily puzzle',\n});\n\n/** The cache key prefix for one game's downloaded puzzles. */\nexport const cachePrefix = (gameId: string): string => `~daily/${gameId}/`;\n\n/** Just enough of `fetch` for the client; tests pass a fake. */\nexport type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;\n\nexport type DailyClientOptions<Config> = {\n /** Where the site serves daily content: \"/daily\" or \"https://example.com/daily\". */\n readonly baseUrl: string;\n readonly gameId: string;\n /** The game's `schemas.config`, which every puzzle file's `config` must pass. */\n readonly config: Validator<Config>;\n readonly storage: Storage;\n /** Defaults to the global `fetch`. */\n readonly fetch?: FetchLike;\n /**\n * Check each downloaded file's content hash with Web Crypto before keeping it. Default true;\n * skipped where `crypto.subtle` is missing (pages served over plain http).\n */\n readonly verifyHash?: boolean;\n};\n\n/** A decoded puzzle and where it came from. */\nexport type DailyPuzzle<Config> = DailyEnvelope<Config> & { readonly source: 'network' | 'cache' };\n\n/** What a prefetch did with each date. */\nexport type PrefetchReport = {\n /** Downloaded and kept now. */\n readonly fetched: readonly string[];\n /** Already on the device with the current hash. */\n readonly cached: readonly string[];\n /** Not listed in the index yet. */\n readonly unavailable: readonly string[];\n /** Listed, but the download or decode failed. Tried again next time. */\n readonly failed: readonly string[];\n /** Set when the index itself couldn't be loaded; then every date is in `failed`. */\n readonly error: DailyError | null;\n};\n\nexport interface DailyClient<Config> {\n /** Fetches and decodes the game's index, revalidating every time. */\n index(): Promise<Result<DailyIndex, DailyError>>;\n /**\n * The puzzle for `date`. Checks the index; uses the cached file when its hash is current,\n * otherwise downloads, verifies, decodes and caches it. Offline, or when the server fails,\n * it falls back to a cached file only when the index itself is unreachable. A known\n * superseded or unlisted copy is purged, including for subsequent offline reads.\n * Pick `date` with `canStart`; this client doesn't judge which dates a player may start.\n */\n puzzle(date: string): Promise<Result<DailyPuzzle<Config>, DailyError>>;\n /** The cached puzzle for `date`, decoded, or null. Never touches the network. */\n cached(date: string): Promise<DailyPuzzle<Config> | null>;\n /**\n * Downloads today's puzzle and the next day's (D-46), where the index lists them and the\n * device doesn't hold them yet. The next day's puzzle stays locked until its UTC day begins\n * (`canStart`). The PWA calls this at a safe point for installed clients and pinned games;\n * this package doesn't decide who prefetches.\n */\n prefetch(today: string): Promise<PrefetchReport>;\n /** Deletes cached puzzles older than `keepPast` days before `today` (default 1: keep yesterday). */\n prune(today: string, keepPast?: number): Promise<void>;\n}\n\nconst trimSlash = (url: string): string => url.replace(/\\/+$/, '');\n\nconst checkDate = (date: string): void => {\n if (!isIsoDate(date)) throw new RangeError(`Expected a \"yyyy-mm-dd\" date, got ${JSON.stringify(date)}.`);\n};\n\n/** Creates the content client for one daily game. */\nexport const createDailyClient = <Config>(options: DailyClientOptions<Config>): DailyClient<Config> => {\n const { gameId, storage } = options;\n if (!isGameId(gameId)) throw new RangeError(`Expected a kebab-case game id, got ${JSON.stringify(gameId)}.`);\n const base = `${trimSlash(options.baseUrl)}/${gameId}`;\n const fetchJson = options.fetch ?? ((url, init) => fetch(url, init));\n const verify = options.verifyHash ?? true;\n const cache: RecordStore<CachedPuzzle> = storage.records(CACHED_PUZZLE_RECORD);\n const keyOf = (date: string): string => `${cachePrefix(gameId)}${date}`;\n\n // No cookies, identifiers or query strings: only the path (content-pipeline.md, \"Delivery\").\n const getJson = async (url: string, init: RequestInit): Promise<Result<unknown, DailyError>> => {\n let response: Response;\n try {\n response = await fetchJson(url, { ...init, credentials: 'omit', referrerPolicy: 'no-referrer' });\n } catch (cause) {\n return err(dailyError('Offline', String(cause)));\n }\n if (response.status === 404) return err(dailyError('NotPublished', `${url}: 404`));\n if (!response.ok) return err(dailyError('Failed', `${url}: HTTP ${response.status}`));\n try {\n return ok(await response.json());\n } catch (cause) {\n return err(dailyError('Invalid', `${url}: ${String(cause)}`));\n }\n };\n\n const index = async (): Promise<Result<DailyIndex, DailyError>> => {\n const url = `${base}/index.json`;\n const raw = await getJson(url, { cache: 'no-cache' });\n if (!raw.ok) return raw;\n const decoded = await decode(dailyIndex, raw.value);\n if (!decoded.ok) return err(dailyError('Invalid', [url, ...decoded.error]));\n if (decoded.value.gameId !== gameId)\n return err(dailyError('Invalid', [url, `index is for ${decoded.value.gameId}`]));\n return decoded;\n };\n\n const decodeFile = async (raw: unknown, date: string, hash: string) =>\n decode(dailyEnvelope(options.config, { gameId, date, contentHash: hash }), raw);\n\n const cached = async (date: string): Promise<DailyPuzzle<Config> | null> => {\n checkDate(date);\n const record = await cache.get(keyOf(date));\n if (!record.ok || record.value === null) return null;\n const decoded = await decodeFile(record.value.file, date, record.value.hash);\n if (!decoded.ok) {\n // Usually a newer game build whose config schema changed; the next load downloads it again.\n await cache.delete(keyOf(date));\n return null;\n }\n return { ...decoded.value, source: 'cache' };\n };\n\n const download = async (date: string, hash: string): Promise<Result<DailyPuzzle<Config>, DailyError>> => {\n const url = `${base}/${date}.${hash}.json`;\n const raw = await getJson(url, {});\n if (!raw.ok) return raw.error.code === 'NotPublished' ? err(dailyError('Failed', raw.error.detail)) : raw;\n const decoded = await decodeFile(raw.value, date, hash);\n if (!decoded.ok || !isJson(raw.value))\n return err(dailyError('Invalid', [url, ...(decoded.ok ? [] : decoded.error)]));\n if (verify) {\n const actual = await contentHashOf((raw.value as { config: Json }).config);\n if (actual !== null && actual !== hash) return err(dailyError('Invalid', [url, `content hash is ${actual}`]));\n }\n // A failed write is reported by storage itself; the puzzle still plays from memory.\n await cache.set(keyOf(date), { hash, file: raw.value });\n return ok({ ...decoded.value, source: 'network' });\n };\n\n const load = async (\n date: string,\n listing: DailyIndex,\n held: DailyPuzzle<Config> | null,\n ): Promise<Result<DailyPuzzle<Config>, DailyError>> => {\n const entry = listing.dates.find((candidate) => candidate.date === date);\n if (entry === undefined) {\n await cache.delete(keyOf(date));\n return err(dailyError('NotPublished', `${gameId} ${date}`));\n }\n if (held !== null && held.contentHash === entry.hash) return ok(held);\n // The index revoked this version. Do not resurrect it if the new download fails.\n if (held !== null) await cache.delete(keyOf(date));\n return download(date, entry.hash);\n };\n\n return {\n index,\n cached,\n puzzle: async (date) => {\n checkDate(date);\n const held = await cached(date);\n const listing = await index();\n if (!listing.ok) return held === null ? listing : ok(held);\n return load(date, listing.value, held);\n },\n prefetch: async (today) => {\n checkDate(today);\n const dates = prefetchDates(today);\n const report = {\n fetched: [] as string[],\n cached: [] as string[],\n unavailable: [] as string[],\n failed: [] as string[],\n };\n const listing = await index();\n if (!listing.ok) return { ...report, failed: [...dates], error: listing.error };\n for (const date of dates) {\n const entry = listing.value.dates.find((candidate) => candidate.date === date);\n if (entry === undefined) {\n await cache.delete(keyOf(date));\n report.unavailable.push(date);\n continue;\n }\n if ((await cached(date))?.contentHash === entry.hash) {\n report.cached.push(date);\n continue;\n }\n await cache.delete(keyOf(date));\n const fetched = await download(date, entry.hash);\n (fetched.ok ? report.fetched : report.failed).push(date);\n }\n return { ...report, error: null };\n },\n prune: async (today, keepPast = 1) => {\n checkDate(today);\n const oldest = addDays(today, -keepPast);\n const keys = await cache.keys(cachePrefix(gameId));\n if (!keys.ok) return;\n for (const key of keys.value) {\n const date = key.slice(cachePrefix(gameId).length);\n if (!isIsoDate(date) || date < oldest) await cache.delete(key);\n }\n },\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { GameError, Json } from '@joystyk/contracts';
2
+ /** Why a daily puzzle couldn't be loaded. */
3
+ export type DailyErrorCode =
4
+ /** The network is unreachable and nothing is cached for that date. */
5
+ 'Offline'
6
+ /** The game's index doesn't list that date (not out yet, or no daily content at all). */
7
+ | 'NotPublished'
8
+ /** The server sent something that isn't a valid puzzle for that date. */
9
+ | 'Invalid'
10
+ /** Anything else, such as a server error. */
11
+ | 'Failed';
12
+ /** An expected daily failure, worded for players. Shaped like `GameError`. */
13
+ export type DailyError = GameError & {
14
+ readonly code: DailyErrorCode;
15
+ };
16
+ /** Player-facing wording for each failure (content-pipeline.md, "Prefetching for offline play"). */
17
+ export declare const DAILY_MESSAGES: {
18
+ readonly Offline: "This puzzle needs a connection. It will download next time you're online.";
19
+ readonly NotPublished: "This puzzle isn't out yet.";
20
+ readonly Invalid: "This puzzle couldn't be opened. Please try again later.";
21
+ readonly Failed: "Something went wrong while loading the puzzle. Please try again later.";
22
+ };
23
+ export declare const dailyError: (code: DailyErrorCode, detail?: Json) => DailyError;