@joystyk/storage 0.0.0 → 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 (56) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +166 -2
  3. package/dist/backend.d.ts +64 -0
  4. package/dist/backend.js +35 -0
  5. package/dist/backend.js.map +1 -0
  6. package/dist/browser/changes.d.ts +17 -0
  7. package/dist/browser/changes.js +96 -0
  8. package/dist/browser/changes.js.map +1 -0
  9. package/dist/browser/index.d.ts +18 -0
  10. package/dist/browser/index.js +44 -0
  11. package/dist/browser/index.js.map +1 -0
  12. package/dist/browser/indexeddb.d.ts +40 -0
  13. package/dist/browser/indexeddb.js +145 -0
  14. package/dist/browser/indexeddb.js.map +1 -0
  15. package/dist/browser/persist.d.ts +24 -0
  16. package/dist/browser/persist.js +42 -0
  17. package/dist/browser/persist.js.map +1 -0
  18. package/dist/changes.d.ts +26 -0
  19. package/dist/changes.js +50 -0
  20. package/dist/changes.js.map +1 -0
  21. package/dist/envelope.d.ts +4 -0
  22. package/dist/envelope.js +109 -0
  23. package/dist/envelope.js.map +1 -0
  24. package/dist/game-data.d.ts +17 -0
  25. package/dist/game-data.js +54 -0
  26. package/dist/game-data.js.map +1 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.js +14 -0
  29. package/dist/keys.d.ts +14 -0
  30. package/dist/keys.js +29 -0
  31. package/dist/keys.js.map +1 -0
  32. package/dist/memory.d.ts +23 -0
  33. package/dist/memory.js +65 -0
  34. package/dist/memory.js.map +1 -0
  35. package/dist/preferences.d.ts +61 -0
  36. package/dist/preferences.js +93 -0
  37. package/dist/preferences.js.map +1 -0
  38. package/dist/record.d.ts +117 -0
  39. package/dist/record.js +177 -0
  40. package/dist/record.js.map +1 -0
  41. package/dist/saves.d.ts +46 -0
  42. package/dist/saves.js +100 -0
  43. package/dist/saves.js.map +1 -0
  44. package/dist/stats.d.ts +34 -0
  45. package/dist/stats.js +78 -0
  46. package/dist/stats.js.map +1 -0
  47. package/dist/storage.d.ts +45 -0
  48. package/dist/storage.js +43 -0
  49. package/dist/storage.js.map +1 -0
  50. package/dist/streaks.d.ts +23 -0
  51. package/dist/streaks.js +41 -0
  52. package/dist/streaks.js.map +1 -0
  53. package/dist/validate.d.ts +14 -0
  54. package/dist/validate.js +24 -0
  55. package/dist/validate.js.map +1 -0
  56. package/package.json +45 -11
package/dist/saves.js ADDED
@@ -0,0 +1,100 @@
1
+ import { MESSAGES, storageError } from "./backend.js";
2
+ import { gamePrefix, slotKey } from "./keys.js";
3
+ import { validate } from "./validate.js";
4
+ import { createRecordStore, defineRecordKind } from "./record.js";
5
+ import { saveEnvelope } from "./envelope.js";
6
+ import { err, ok, runMigrations } from "@joystyk/contracts";
7
+ import * as z from "zod/mini";
8
+ //#region src/saves.ts
9
+ /** Version 1 of the save record: the contracts' envelope plus the time it was written. */
10
+ var SAVE_RECORD = defineRecordKind({
11
+ store: "saves",
12
+ version: 1,
13
+ schema: z.object({
14
+ savedAt: z.number().check(z.nonnegative()),
15
+ envelope: saveEnvelope
16
+ }),
17
+ onInvalid: "archive",
18
+ label: "saved game"
19
+ });
20
+ var createSaveStore = (context, playerId) => {
21
+ const records = createRecordStore(context, SAVE_RECORD);
22
+ const read = async (gameId, slot) => {
23
+ const key = slotKey(playerId, gameId, slot);
24
+ if (!key.ok) return key;
25
+ const record = await records.get(key.value);
26
+ if (!record.ok || record.value === null) return record;
27
+ if (record.value.envelope.gameId !== gameId) {
28
+ await records.reject(key.value, [`envelope.gameId: expected "${gameId}"`]);
29
+ return ok(null);
30
+ }
31
+ return ok({
32
+ slot,
33
+ ...record.value
34
+ });
35
+ };
36
+ return {
37
+ list: async (gameId) => {
38
+ const prefix = gamePrefix(playerId, gameId);
39
+ if (!prefix.ok) return prefix;
40
+ const keys = await records.keys(prefix.value);
41
+ if (!keys.ok) return keys;
42
+ const saves = [];
43
+ for (const key of keys.value) {
44
+ const save = await read(gameId, key.slice(prefix.value.length));
45
+ if (!save.ok) return save;
46
+ if (save.value !== null) saves.push(save.value);
47
+ }
48
+ return ok(saves);
49
+ },
50
+ read,
51
+ load: async (gameId, slot, decoder) => {
52
+ const save = await read(gameId, slot);
53
+ if (!save.ok || save.value === null) return save;
54
+ const { envelope } = save.value;
55
+ const migrated = runMigrations(decoder.migrations, envelope.state, envelope.saveVersion, decoder.saveVersion);
56
+ if (!migrated.ok && migrated.error.code === "SaveFromFuture") {
57
+ context.notify({
58
+ code: "RecordFromFuture",
59
+ message: `This ${SAVE_RECORD.label} was made by a newer version of the game. Reload the page to open it.`,
60
+ store: SAVE_RECORD.store,
61
+ key: `${playerId}/${gameId}/${slot}`,
62
+ issues: [`${migrated.error.code}: ${migrated.error.message}`]
63
+ });
64
+ return ok(null);
65
+ }
66
+ const state = migrated.ok ? await validate(decoder.state, migrated.value) : err([`${migrated.error.code}: ${migrated.error.message}`]);
67
+ if (!state.ok) {
68
+ const key = slotKey(playerId, gameId, slot);
69
+ if (key.ok) await records.reject(key.value, state.error);
70
+ return ok(null);
71
+ }
72
+ return ok({
73
+ ...save.value,
74
+ envelope: {
75
+ ...envelope,
76
+ saveVersion: decoder.saveVersion,
77
+ state: state.value
78
+ }
79
+ });
80
+ },
81
+ write: async (gameId, slot, envelope) => {
82
+ const key = slotKey(playerId, gameId, slot);
83
+ if (!key.ok) return key;
84
+ if (envelope.gameId !== gameId) return err(storageError("InvalidValue", MESSAGES.InvalidValue, { gameId: envelope.gameId }));
85
+ return records.set(key.value, {
86
+ savedAt: context.now(),
87
+ envelope
88
+ });
89
+ },
90
+ delete: async (gameId, slot) => {
91
+ const key = slotKey(playerId, gameId, slot);
92
+ if (!key.ok) return key;
93
+ return records.delete(key.value);
94
+ }
95
+ };
96
+ };
97
+ //#endregion
98
+ export { SAVE_RECORD, createSaveStore };
99
+
100
+ //# sourceMappingURL=saves.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"saves.js","names":[],"sources":["../src/saves.ts"],"sourcesContent":["import {\n err,\n ok,\n runMigrations,\n type Json,\n type Migration,\n type Result,\n type SaveEnvelope,\n type Validator,\n} from '@joystyk/contracts';\nimport * as z from 'zod/mini';\nimport { MESSAGES, storageError, type StorageError } from './backend.ts';\nimport { saveEnvelope } from './envelope.ts';\nimport { gamePrefix, slotKey } from './keys.ts';\nimport { createRecordStore, defineRecordKind, type RecordContext } from './record.ts';\nimport { validate } from './validate.ts';\n\n/** One saved game in a slot. */\nexport type SavedGame = {\n readonly slot: string;\n /** Epoch milliseconds of the write. */\n readonly savedAt: number;\n readonly envelope: SaveEnvelope;\n};\n\n/** What a game supplies to bring its saved state up to date: the game module's chain and schema. */\nexport type SaveDecoder = {\n /** The game's current save version (`manifest.versions.save`). */\n readonly saveVersion: number;\n readonly migrations: readonly Migration[];\n /** The engine's `schemas.state` (or the simulation's). */\n readonly state: Validator<Json>;\n};\n\ntype SaveRecord = { readonly savedAt: number; readonly envelope: SaveEnvelope };\n\n/** Version 1 of the save record: the contracts' envelope plus the time it was written. */\nexport const SAVE_RECORD = defineRecordKind<SaveRecord>({\n store: 'saves',\n version: 1,\n schema: z.object({ savedAt: z.number().check(z.nonnegative()), envelope: saveEnvelope }),\n onInvalid: 'archive',\n label: 'saved game',\n});\n\n/** One player's save slots, by game id and slot name. */\nexport interface SaveStore {\n /** Every readable save of one game, by slot name. Unreadable ones are archived with a notice. */\n list(gameId: string): Promise<Result<readonly SavedGame[], StorageError>>;\n /**\n * The save in a slot with its envelope decoded, or null. The game's own state is not\n * migrated or checked; use `load` to resume play.\n */\n read(gameId: string, slot: string): Promise<Result<SavedGame | null, StorageError>>;\n /**\n * The save in a slot, ready to resume: its state migrated from the saved version to the\n * game's current one and validated with the game's schema. A save that can't be brought up\n * to date is archived with a notice and comes back as null. The caller still compares\n * `rulesVersion` with the game's (offline-and-updates.md, Rule 3).\n */\n load(gameId: string, slot: string, decoder: SaveDecoder): Promise<Result<SavedGame | null, StorageError>>;\n /** Writes an envelope to a slot. The envelope's `gameId` must be `gameId`. */\n write(gameId: string, slot: string, envelope: SaveEnvelope): Promise<Result<void, StorageError>>;\n delete(gameId: string, slot: string): Promise<Result<void, StorageError>>;\n}\n\nexport const createSaveStore = (context: RecordContext, playerId: string): SaveStore => {\n const records = createRecordStore(context, SAVE_RECORD);\n\n const read = async (gameId: string, slot: string): Promise<Result<SavedGame | null, StorageError>> => {\n const key = slotKey(playerId, gameId, slot);\n if (!key.ok) return key;\n const record = await records.get(key.value);\n if (!record.ok || record.value === null) return record as Result<null, StorageError>;\n if (record.value.envelope.gameId !== gameId) {\n await records.reject(key.value, [`envelope.gameId: expected \"${gameId}\"`]);\n return ok(null);\n }\n return ok({ slot, ...record.value });\n };\n\n return {\n list: async (gameId) => {\n const prefix = gamePrefix(playerId, gameId);\n if (!prefix.ok) return prefix;\n const keys = await records.keys(prefix.value);\n if (!keys.ok) return keys;\n const saves: SavedGame[] = [];\n for (const key of keys.value) {\n const save = await read(gameId, key.slice(prefix.value.length));\n if (!save.ok) return save;\n if (save.value !== null) saves.push(save.value);\n }\n return ok(saves);\n },\n read,\n load: async (gameId, slot, decoder) => {\n const save = await read(gameId, slot);\n if (!save.ok || save.value === null) return save;\n const { envelope } = save.value;\n const migrated = runMigrations(decoder.migrations, envelope.state, envelope.saveVersion, decoder.saveVersion);\n if (!migrated.ok && migrated.error.code === 'SaveFromFuture') {\n // A newer build of the game wrote it: keep it for that build rather than archive it.\n context.notify({\n code: 'RecordFromFuture',\n message: `This ${SAVE_RECORD.label} was made by a newer version of the game. Reload the page to open it.`,\n store: SAVE_RECORD.store,\n key: `${playerId}/${gameId}/${slot}`,\n issues: [`${migrated.error.code}: ${migrated.error.message}`],\n });\n return ok(null);\n }\n const state = migrated.ok\n ? await validate(decoder.state, migrated.value)\n : err([`${migrated.error.code}: ${migrated.error.message}`]);\n if (!state.ok) {\n const key = slotKey(playerId, gameId, slot);\n if (key.ok) await records.reject(key.value, state.error);\n return ok(null);\n }\n return ok({\n ...save.value,\n envelope: { ...envelope, saveVersion: decoder.saveVersion, state: state.value },\n });\n },\n write: async (gameId, slot, envelope) => {\n const key = slotKey(playerId, gameId, slot);\n if (!key.ok) return key;\n if (envelope.gameId !== gameId) {\n return err(storageError('InvalidValue', MESSAGES.InvalidValue, { gameId: envelope.gameId }));\n }\n return records.set(key.value, { savedAt: context.now(), envelope });\n },\n delete: async (gameId, slot) => {\n const key = slotKey(playerId, gameId, slot);\n if (!key.ok) return key;\n return records.delete(key.value);\n },\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { type Outcome, type Result } from '@joystyk/contracts';
2
+ import type { StorageError } from './backend.js';
3
+ import { type RecordContext } from './record.js';
4
+ /** One player's totals for one game, across every mode played on this device. */
5
+ export type GameStats = {
6
+ readonly played: number;
7
+ readonly won: number;
8
+ readonly lost: number;
9
+ readonly drawn: number;
10
+ /** Solo games that end without winning or losing, such as a solved puzzle. */
11
+ readonly completed: number;
12
+ readonly abandoned: number;
13
+ /** Active play time across all games, in milliseconds. */
14
+ readonly totalElapsedMs: number;
15
+ /** Epoch milliseconds of the last finished game, or null. */
16
+ readonly lastPlayedAt: number | null;
17
+ };
18
+ export declare const EMPTY_STATS: GameStats;
19
+ /** Version 1 of the stats record. */
20
+ export declare const STATS_RECORD: import("./record.js").RecordKind<GameStats>;
21
+ /** Adds one finished game to the totals. Pure. `at` is epoch milliseconds. */
22
+ export declare const recordOutcome: (stats: GameStats, outcome: Outcome, at: number) => GameStats;
23
+ /**
24
+ * One player's stats, by game id. Written through this interface so v2 can sync them to an
25
+ * account without changing games.
26
+ */
27
+ export interface StatsStore {
28
+ /** The game's stats, or `EMPTY_STATS` when there are none yet. */
29
+ get(gameId: string): Promise<Result<GameStats, StorageError>>;
30
+ set(gameId: string, stats: GameStats): Promise<Result<void, StorageError>>;
31
+ /** Reads, adds the outcome with `recordOutcome`, writes, and returns the new totals. */
32
+ record(gameId: string, outcome: Outcome): Promise<Result<GameStats, StorageError>>;
33
+ }
34
+ export declare const createStatsStore: (context: RecordContext, playerId: string) => StatsStore;
package/dist/stats.js ADDED
@@ -0,0 +1,78 @@
1
+ import { gameKey } from "./keys.js";
2
+ import { createRecordStore, defineRecordKind } from "./record.js";
3
+ import { ok } from "@joystyk/contracts";
4
+ import * as z from "zod/mini";
5
+ //#region src/stats.ts
6
+ var EMPTY_STATS = {
7
+ played: 0,
8
+ won: 0,
9
+ lost: 0,
10
+ drawn: 0,
11
+ completed: 0,
12
+ abandoned: 0,
13
+ totalElapsedMs: 0,
14
+ lastPlayedAt: null
15
+ };
16
+ var count = z.int().check(z.nonnegative());
17
+ /** Version 1 of the stats record. */
18
+ var STATS_RECORD = defineRecordKind({
19
+ store: "stats",
20
+ version: 1,
21
+ schema: z.object({
22
+ played: count,
23
+ won: count,
24
+ lost: count,
25
+ drawn: count,
26
+ completed: count,
27
+ abandoned: count,
28
+ totalElapsedMs: z.number().check(z.nonnegative()),
29
+ lastPlayedAt: z.nullable(z.number().check(z.nonnegative()))
30
+ }),
31
+ label: "set of game stats"
32
+ });
33
+ var COUNTERS = {
34
+ won: "won",
35
+ lost: "lost",
36
+ draw: "drawn",
37
+ completed: "completed",
38
+ abandoned: "abandoned"
39
+ };
40
+ /** Adds one finished game to the totals. Pure. `at` is epoch milliseconds. */
41
+ var recordOutcome = (stats, outcome, at) => {
42
+ const counter = COUNTERS[outcome.result];
43
+ return {
44
+ ...stats,
45
+ played: stats.played + 1,
46
+ [counter]: stats[counter] + 1,
47
+ totalElapsedMs: stats.totalElapsedMs + (outcome.elapsedMs ?? 0),
48
+ lastPlayedAt: at
49
+ };
50
+ };
51
+ var createStatsStore = (context, playerId) => {
52
+ const records = createRecordStore(context, STATS_RECORD);
53
+ const get = async (gameId) => {
54
+ const key = gameKey(playerId, gameId);
55
+ if (!key.ok) return key;
56
+ const stats = await records.get(key.value);
57
+ return stats.ok ? ok(stats.value ?? EMPTY_STATS) : stats;
58
+ };
59
+ const set = async (gameId, stats) => {
60
+ const key = gameKey(playerId, gameId);
61
+ return key.ok ? records.set(key.value, stats) : key;
62
+ };
63
+ return {
64
+ get,
65
+ set,
66
+ record: async (gameId, outcome) => {
67
+ const current = await get(gameId);
68
+ if (!current.ok) return current;
69
+ const next = recordOutcome(current.value, outcome, context.now());
70
+ const written = await set(gameId, next);
71
+ return written.ok ? ok(next) : written;
72
+ }
73
+ };
74
+ };
75
+ //#endregion
76
+ export { EMPTY_STATS, STATS_RECORD, createStatsStore, recordOutcome };
77
+
78
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.js","names":[],"sources":["../src/stats.ts"],"sourcesContent":["import { ok, type Outcome, type Result } from '@joystyk/contracts';\nimport * as z from 'zod/mini';\nimport type { StorageError } from './backend.ts';\nimport { gameKey } from './keys.ts';\nimport { createRecordStore, defineRecordKind, type RecordContext } from './record.ts';\n\n/** One player's totals for one game, across every mode played on this device. */\nexport type GameStats = {\n readonly played: number;\n readonly won: number;\n readonly lost: number;\n readonly drawn: number;\n /** Solo games that end without winning or losing, such as a solved puzzle. */\n readonly completed: number;\n readonly abandoned: number;\n /** Active play time across all games, in milliseconds. */\n readonly totalElapsedMs: number;\n /** Epoch milliseconds of the last finished game, or null. */\n readonly lastPlayedAt: number | null;\n};\n\nexport const EMPTY_STATS: GameStats = {\n played: 0,\n won: 0,\n lost: 0,\n drawn: 0,\n completed: 0,\n abandoned: 0,\n totalElapsedMs: 0,\n lastPlayedAt: null,\n};\n\nconst count = z.int().check(z.nonnegative());\n\n/** Version 1 of the stats record. */\nexport const STATS_RECORD = defineRecordKind<GameStats>({\n store: 'stats',\n version: 1,\n schema: z.object({\n played: count,\n won: count,\n lost: count,\n drawn: count,\n completed: count,\n abandoned: count,\n totalElapsedMs: z.number().check(z.nonnegative()),\n lastPlayedAt: z.nullable(z.number().check(z.nonnegative())),\n }),\n label: 'set of game stats',\n});\n\nconst COUNTERS = {\n won: 'won',\n lost: 'lost',\n draw: 'drawn',\n completed: 'completed',\n abandoned: 'abandoned',\n} as const satisfies Record<Outcome['result'], keyof GameStats>;\n\n/** Adds one finished game to the totals. Pure. `at` is epoch milliseconds. */\nexport const recordOutcome = (stats: GameStats, outcome: Outcome, at: number): GameStats => {\n const counter = COUNTERS[outcome.result];\n return {\n ...stats,\n played: stats.played + 1,\n [counter]: stats[counter] + 1,\n totalElapsedMs: stats.totalElapsedMs + (outcome.elapsedMs ?? 0),\n lastPlayedAt: at,\n };\n};\n\n/**\n * One player's stats, by game id. Written through this interface so v2 can sync them to an\n * account without changing games.\n */\nexport interface StatsStore {\n /** The game's stats, or `EMPTY_STATS` when there are none yet. */\n get(gameId: string): Promise<Result<GameStats, StorageError>>;\n set(gameId: string, stats: GameStats): Promise<Result<void, StorageError>>;\n /** Reads, adds the outcome with `recordOutcome`, writes, and returns the new totals. */\n record(gameId: string, outcome: Outcome): Promise<Result<GameStats, StorageError>>;\n}\n\nexport const createStatsStore = (context: RecordContext, playerId: string): StatsStore => {\n const records = createRecordStore(context, STATS_RECORD);\n const get = async (gameId: string): Promise<Result<GameStats, StorageError>> => {\n const key = gameKey(playerId, gameId);\n if (!key.ok) return key;\n const stats = await records.get(key.value);\n return stats.ok ? ok(stats.value ?? EMPTY_STATS) : stats;\n };\n const set = async (gameId: string, stats: GameStats): Promise<Result<void, StorageError>> => {\n const key = gameKey(playerId, gameId);\n return key.ok ? records.set(key.value, stats) : key;\n };\n return {\n get,\n set,\n record: async (gameId, outcome) => {\n const current = await get(gameId);\n if (!current.ok) return current;\n const next = recordOutcome(current.value, outcome, context.now());\n const written = await set(gameId, next);\n return written.ok ? ok(next) : written;\n },\n };\n};\n"],"mappings":";;;;;AAqBA,IAAa,cAAyB;CACpC,QAAQ;CACR,KAAK;CACL,MAAM;CACN,OAAO;CACP,WAAW;CACX,WAAW;CACX,gBAAgB;CAChB,cAAc;AAChB;AAEA,IAAM,QAAQ,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,CAAC;;AAG3C,IAAa,eAAe,iBAA4B;CACtD,OAAO;CACP,SAAS;CACT,QAAQ,EAAE,OAAO;EACf,QAAQ;EACR,KAAK;EACL,MAAM;EACN,OAAO;EACP,WAAW;EACX,WAAW;EACX,gBAAgB,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,CAAC;EAChD,cAAc,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC5D,CAAC;CACD,OAAO;AACT,CAAC;AAED,IAAM,WAAW;CACf,KAAK;CACL,MAAM;CACN,MAAM;CACN,WAAW;CACX,WAAW;AACb;;AAGA,IAAa,iBAAiB,OAAkB,SAAkB,OAA0B;CAC1F,MAAM,UAAU,SAAS,QAAQ;CACjC,OAAO;EACL,GAAG;EACH,QAAQ,MAAM,SAAS;GACtB,UAAU,MAAM,WAAW;EAC5B,gBAAgB,MAAM,kBAAkB,QAAQ,aAAa;EAC7D,cAAc;CAChB;AACF;AAcA,IAAa,oBAAoB,SAAwB,aAAiC;CACxF,MAAM,UAAU,kBAAkB,SAAS,YAAY;CACvD,MAAM,MAAM,OAAO,WAA6D;EAC9E,MAAM,MAAM,QAAQ,UAAU,MAAM;EACpC,IAAI,CAAC,IAAI,IAAI,OAAO;EACpB,MAAM,QAAQ,MAAM,QAAQ,IAAI,IAAI,KAAK;EACzC,OAAO,MAAM,KAAK,GAAG,MAAM,SAAS,WAAW,IAAI;CACrD;CACA,MAAM,MAAM,OAAO,QAAgB,UAA0D;EAC3F,MAAM,MAAM,QAAQ,UAAU,MAAM;EACpC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,OAAO,KAAK,IAAI;CAClD;CACA,OAAO;EACL;EACA;EACA,QAAQ,OAAO,QAAQ,YAAY;GACjC,MAAM,UAAU,MAAM,IAAI,MAAM;GAChC,IAAI,CAAC,QAAQ,IAAI,OAAO;GACxB,MAAM,OAAO,cAAc,QAAQ,OAAO,SAAS,QAAQ,IAAI,CAAC;GAChE,MAAM,UAAU,MAAM,IAAI,QAAQ,IAAI;GACtC,OAAO,QAAQ,KAAK,GAAG,IAAI,IAAI;EACjC;CACF;AACF"}
@@ -0,0 +1,45 @@
1
+ import type { GameStorage, Json } from '@joystyk/contracts';
2
+ import type { StorageBackend } from './backend.js';
3
+ import type { ChangeFeed } from './changes.js';
4
+ import { type PreferencesStore } from './preferences.js';
5
+ import { type RecordKind, type RecordStore, type StorageNotice } from './record.js';
6
+ import { type SaveStore } from './saves.js';
7
+ import { type StatsStore } from './stats.js';
8
+ import { type StreakStore } from './streaks.js';
9
+ export type StorageOptions = {
10
+ /** `createMemoryBackend()` in tests and on the server; `openIndexedDbBackend()` in the browser. */
11
+ readonly backend: StorageBackend;
12
+ /** Receives every notice; the frame shows them to the player. */
13
+ readonly onNotice?: (notice: StorageNotice) => void;
14
+ /** Epoch milliseconds. Defaults to `Date.now`. */
15
+ readonly now?: () => number;
16
+ /**
17
+ * Tells other tabs about changes and hears theirs, so a preference set in one tab reaches
18
+ * the rest (gp-d4v). `openBrowserStorage` provides one; without it, changes stay in this tab.
19
+ */
20
+ readonly changes?: ChangeFeed;
21
+ };
22
+ /** Everything one player keeps. Every key starts with the player id, never a device id. */
23
+ export interface PlayerStorage {
24
+ readonly playerId: string;
25
+ readonly saves: SaveStore;
26
+ readonly stats: StatsStore;
27
+ readonly streaks: StreakStore;
28
+ readonly preferences: PreferencesStore;
29
+ /** The `PlatformApi.storage` for one game. */
30
+ gameStorage(gameId: string): GameStorage;
31
+ }
32
+ /** The opened storage: typed record stores over one backend. */
33
+ export interface Storage {
34
+ /**
35
+ * One player's data. Throws a `RangeError` for an id that can't be part of a key, which
36
+ * is a bug: ids come from `@joystyk/identity`.
37
+ */
38
+ player(playerId: string): PlayerStorage;
39
+ /** A typed store for a record kind another package defines, such as identity's profile. */
40
+ records<T extends Json>(kind: RecordKind<T>): RecordStore<T>;
41
+ /** Closes the backend and the change feed. Idempotent. */
42
+ close(): void;
43
+ }
44
+ /** Opens typed, validated storage over a backend. */
45
+ export declare const openStorage: (options: StorageOptions) => Storage;
@@ -0,0 +1,43 @@
1
+ import { isKeyPart } from "./keys.js";
2
+ import { createRecordStore } from "./record.js";
3
+ import { createGameStorage } from "./game-data.js";
4
+ import { createPreferencesStore } from "./preferences.js";
5
+ import { createSaveStore } from "./saves.js";
6
+ import { createStatsStore } from "./stats.js";
7
+ import { createStreakStore } from "./streaks.js";
8
+ //#region src/storage.ts
9
+ /** Opens typed, validated storage over a backend. */
10
+ var openStorage = (options) => {
11
+ const context = {
12
+ backend: options.backend,
13
+ notify: options.onNotice ?? (() => {}),
14
+ now: options.now ?? Date.now
15
+ };
16
+ const players = /* @__PURE__ */ new Map();
17
+ return {
18
+ player: (playerId) => {
19
+ if (!isKeyPart(playerId)) throw new RangeError(`"${playerId}" is not a valid player id`);
20
+ const existing = players.get(playerId);
21
+ if (existing !== void 0) return existing;
22
+ const player = {
23
+ playerId,
24
+ saves: createSaveStore(context, playerId),
25
+ stats: createStatsStore(context, playerId),
26
+ streaks: createStreakStore(context, playerId),
27
+ preferences: createPreferencesStore(context, playerId, options.changes),
28
+ gameStorage: (gameId) => createGameStorage(context, playerId, gameId)
29
+ };
30
+ players.set(playerId, player);
31
+ return player;
32
+ },
33
+ records: (kind) => createRecordStore(context, kind),
34
+ close: () => {
35
+ options.changes?.close();
36
+ options.backend.close();
37
+ }
38
+ };
39
+ };
40
+ //#endregion
41
+ export { openStorage };
42
+
43
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","names":[],"sources":["../src/storage.ts"],"sourcesContent":["import type { GameStorage, Json } from '@joystyk/contracts';\nimport type { StorageBackend } from './backend.ts';\nimport type { ChangeFeed } from './changes.ts';\nimport { createGameStorage } from './game-data.ts';\nimport { isKeyPart } from './keys.ts';\nimport { createPreferencesStore, type PreferencesStore } from './preferences.ts';\nimport {\n createRecordStore,\n type RecordContext,\n type RecordKind,\n type RecordStore,\n type StorageNotice,\n} from './record.ts';\nimport { createSaveStore, type SaveStore } from './saves.ts';\nimport { createStatsStore, type StatsStore } from './stats.ts';\nimport { createStreakStore, type StreakStore } from './streaks.ts';\n\nexport type StorageOptions = {\n /** `createMemoryBackend()` in tests and on the server; `openIndexedDbBackend()` in the browser. */\n readonly backend: StorageBackend;\n /** Receives every notice; the frame shows them to the player. */\n readonly onNotice?: (notice: StorageNotice) => void;\n /** Epoch milliseconds. Defaults to `Date.now`. */\n readonly now?: () => number;\n /**\n * Tells other tabs about changes and hears theirs, so a preference set in one tab reaches\n * the rest (gp-d4v). `openBrowserStorage` provides one; without it, changes stay in this tab.\n */\n readonly changes?: ChangeFeed;\n};\n\n/** Everything one player keeps. Every key starts with the player id, never a device id. */\nexport interface PlayerStorage {\n readonly playerId: string;\n readonly saves: SaveStore;\n readonly stats: StatsStore;\n readonly streaks: StreakStore;\n readonly preferences: PreferencesStore;\n /** The `PlatformApi.storage` for one game. */\n gameStorage(gameId: string): GameStorage;\n}\n\n/** The opened storage: typed record stores over one backend. */\nexport interface Storage {\n /**\n * One player's data. Throws a `RangeError` for an id that can't be part of a key, which\n * is a bug: ids come from `@joystyk/identity`.\n */\n player(playerId: string): PlayerStorage;\n /** A typed store for a record kind another package defines, such as identity's profile. */\n records<T extends Json>(kind: RecordKind<T>): RecordStore<T>;\n /** Closes the backend and the change feed. Idempotent. */\n close(): void;\n}\n\n/** Opens typed, validated storage over a backend. */\nexport const openStorage = (options: StorageOptions): Storage => {\n const context: RecordContext = {\n backend: options.backend,\n notify: options.onNotice ?? (() => {}),\n now: options.now ?? Date.now,\n };\n const players = new Map<string, PlayerStorage>();\n\n return {\n player: (playerId) => {\n if (!isKeyPart(playerId)) throw new RangeError(`\"${playerId}\" is not a valid player id`);\n const existing = players.get(playerId);\n if (existing !== undefined) return existing;\n const player: PlayerStorage = {\n playerId,\n saves: createSaveStore(context, playerId),\n stats: createStatsStore(context, playerId),\n streaks: createStreakStore(context, playerId),\n preferences: createPreferencesStore(context, playerId, options.changes),\n gameStorage: (gameId) => createGameStorage(context, playerId, gameId),\n };\n players.set(playerId, player);\n return player;\n },\n records: (kind) => createRecordStore(context, kind),\n close: () => {\n options.changes?.close();\n options.backend.close();\n },\n };\n};\n"],"mappings":";;;;;;;;;AAwDA,IAAa,eAAe,YAAqC;CAC/D,MAAM,UAAyB;EAC7B,SAAS,QAAQ;EACjB,QAAQ,QAAQ,mBAAmB,CAAC;EACpC,KAAK,QAAQ,OAAO,KAAK;CAC3B;CACA,MAAM,0BAAU,IAAI,IAA2B;CAE/C,OAAO;EACL,SAAS,aAAa;GACpB,IAAI,CAAC,UAAU,QAAQ,GAAG,MAAM,IAAI,WAAW,IAAI,SAAS,2BAA2B;GACvF,MAAM,WAAW,QAAQ,IAAI,QAAQ;GACrC,IAAI,aAAa,KAAA,GAAW,OAAO;GACnC,MAAM,SAAwB;IAC5B;IACA,OAAO,gBAAgB,SAAS,QAAQ;IACxC,OAAO,iBAAiB,SAAS,QAAQ;IACzC,SAAS,kBAAkB,SAAS,QAAQ;IAC5C,aAAa,uBAAuB,SAAS,UAAU,QAAQ,OAAO;IACtE,cAAc,WAAW,kBAAkB,SAAS,UAAU,MAAM;GACtE;GACA,QAAQ,IAAI,UAAU,MAAM;GAC5B,OAAO;EACT;EACA,UAAU,SAAS,kBAAkB,SAAS,IAAI;EAClD,aAAa;GACX,QAAQ,SAAS,MAAM;GACvB,QAAQ,QAAQ,MAAM;EACxB;CACF;AACF"}
@@ -0,0 +1,23 @@
1
+ import { type Result } from '@joystyk/contracts';
2
+ import type { StorageError } from './backend.js';
3
+ import { type RecordContext } from './record.js';
4
+ /**
5
+ * One player's daily streak for one game. The `daily` package decides how a streak grows
6
+ * or breaks; this package only keeps it.
7
+ */
8
+ export type Streak = {
9
+ readonly current: number;
10
+ readonly best: number;
11
+ /** The last day played, `yyyy-mm-dd`, or null. */
12
+ readonly lastDay: string | null;
13
+ };
14
+ export declare const EMPTY_STREAK: Streak;
15
+ /** Version 1 of the streak record. */
16
+ export declare const STREAK_RECORD: import("./record.js").RecordKind<Streak>;
17
+ /** One player's daily streaks, by game id. v2 syncs them as "max wins" (offline-and-updates.md). */
18
+ export interface StreakStore {
19
+ /** The game's streak, or `EMPTY_STREAK`. */
20
+ get(gameId: string): Promise<Result<Streak, StorageError>>;
21
+ set(gameId: string, streak: Streak): Promise<Result<void, StorageError>>;
22
+ }
23
+ export declare const createStreakStore: (context: RecordContext, playerId: string) => StreakStore;
@@ -0,0 +1,41 @@
1
+ import { gameKey } from "./keys.js";
2
+ import { createRecordStore, defineRecordKind } from "./record.js";
3
+ import { ok } from "@joystyk/contracts";
4
+ import * as z from "zod/mini";
5
+ //#region src/streaks.ts
6
+ var EMPTY_STREAK = {
7
+ current: 0,
8
+ best: 0,
9
+ lastDay: null
10
+ };
11
+ var count = z.int().check(z.nonnegative());
12
+ /** Version 1 of the streak record. */
13
+ var STREAK_RECORD = defineRecordKind({
14
+ store: "streaks",
15
+ version: 1,
16
+ schema: z.object({
17
+ current: count,
18
+ best: count,
19
+ lastDay: z.nullable(z.string().check(z.regex(/^\d{4}-\d{2}-\d{2}$/)))
20
+ }).check(z.refine((streak) => streak.best >= streak.current, "best must be at least current")),
21
+ label: "daily streak"
22
+ });
23
+ var createStreakStore = (context, playerId) => {
24
+ const records = createRecordStore(context, STREAK_RECORD);
25
+ return {
26
+ get: async (gameId) => {
27
+ const key = gameKey(playerId, gameId);
28
+ if (!key.ok) return key;
29
+ const streak = await records.get(key.value);
30
+ return streak.ok ? ok(streak.value ?? EMPTY_STREAK) : streak;
31
+ },
32
+ set: async (gameId, streak) => {
33
+ const key = gameKey(playerId, gameId);
34
+ return key.ok ? records.set(key.value, streak) : key;
35
+ }
36
+ };
37
+ };
38
+ //#endregion
39
+ export { EMPTY_STREAK, STREAK_RECORD, createStreakStore };
40
+
41
+ //# sourceMappingURL=streaks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaks.js","names":[],"sources":["../src/streaks.ts"],"sourcesContent":["import { ok, type Result } from '@joystyk/contracts';\nimport * as z from 'zod/mini';\nimport type { StorageError } from './backend.ts';\nimport { gameKey } from './keys.ts';\nimport { createRecordStore, defineRecordKind, type RecordContext } from './record.ts';\n\n/**\n * One player's daily streak for one game. The `daily` package decides how a streak grows\n * or breaks; this package only keeps it.\n */\nexport type Streak = {\n readonly current: number;\n readonly best: number;\n /** The last day played, `yyyy-mm-dd`, or null. */\n readonly lastDay: string | null;\n};\n\nexport const EMPTY_STREAK: Streak = { current: 0, best: 0, lastDay: null };\n\nconst count = z.int().check(z.nonnegative());\n\n/** Version 1 of the streak record. */\nexport const STREAK_RECORD = defineRecordKind<Streak>({\n store: 'streaks',\n version: 1,\n schema: z\n .object({\n current: count,\n best: count,\n lastDay: z.nullable(z.string().check(z.regex(/^\\d{4}-\\d{2}-\\d{2}$/))),\n })\n .check(z.refine((streak) => streak.best >= streak.current, 'best must be at least current')),\n label: 'daily streak',\n});\n\n/** One player's daily streaks, by game id. v2 syncs them as \"max wins\" (offline-and-updates.md). */\nexport interface StreakStore {\n /** The game's streak, or `EMPTY_STREAK`. */\n get(gameId: string): Promise<Result<Streak, StorageError>>;\n set(gameId: string, streak: Streak): Promise<Result<void, StorageError>>;\n}\n\nexport const createStreakStore = (context: RecordContext, playerId: string): StreakStore => {\n const records = createRecordStore(context, STREAK_RECORD);\n return {\n get: async (gameId) => {\n const key = gameKey(playerId, gameId);\n if (!key.ok) return key;\n const streak = await records.get(key.value);\n return streak.ok ? ok(streak.value ?? EMPTY_STREAK) : streak;\n },\n set: async (gameId, streak) => {\n const key = gameKey(playerId, gameId);\n return key.ok ? records.set(key.value, streak) : key;\n },\n };\n};\n"],"mappings":";;;;;AAiBA,IAAa,eAAuB;CAAE,SAAS;CAAG,MAAM;CAAG,SAAS;AAAK;AAEzE,IAAM,QAAQ,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,YAAY,CAAC;;AAG3C,IAAa,gBAAgB,iBAAyB;CACpD,OAAO;CACP,SAAS;CACT,QAAQ,EACL,OAAO;EACN,SAAS;EACT,MAAM;EACN,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,qBAAqB,CAAC,CAAC;CACtE,CAAC,CAAC,CACD,MAAM,EAAE,QAAQ,WAAW,OAAO,QAAQ,OAAO,SAAS,+BAA+B,CAAC;CAC7F,OAAO;AACT,CAAC;AASD,IAAa,qBAAqB,SAAwB,aAAkC;CAC1F,MAAM,UAAU,kBAAkB,SAAS,aAAa;CACxD,OAAO;EACL,KAAK,OAAO,WAAW;GACrB,MAAM,MAAM,QAAQ,UAAU,MAAM;GACpC,IAAI,CAAC,IAAI,IAAI,OAAO;GACpB,MAAM,SAAS,MAAM,QAAQ,IAAI,IAAI,KAAK;GAC1C,OAAO,OAAO,KAAK,GAAG,OAAO,SAAS,YAAY,IAAI;EACxD;EACA,KAAK,OAAO,QAAQ,WAAW;GAC7B,MAAM,MAAM,QAAQ,UAAU,MAAM;GACpC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,OAAO,MAAM,IAAI;EACnD;CACF;AACF"}
@@ -0,0 +1,14 @@
1
+ import { type Json, type Result, type StandardSchemaV1 } from '@joystyk/contracts';
2
+ import * as z from 'zod/mini';
3
+ /** One validation problem, flattened for notices and logs: `path: message`. */
4
+ export type Issue = string;
5
+ /**
6
+ * Runs any Standard Schema (D-18) and returns its output or its issues. Works with sync and
7
+ * async validators alike.
8
+ */
9
+ export declare const validate: <Output>(schema: StandardSchemaV1<unknown, Output>, value: unknown) => Promise<Result<Output, readonly Issue[]>>;
10
+ /**
11
+ * Any JSON value, checked with the contracts' `isJson`: no `undefined`, `NaN`, `Infinity`,
12
+ * `-0`, class instances or cycles.
13
+ */
14
+ export declare const json: z.ZodMiniCustom<Json, Json>;
@@ -0,0 +1,24 @@
1
+ import { err, isJson, ok } from "@joystyk/contracts";
2
+ import * as z from "zod/mini";
3
+ //#region src/validate.ts
4
+ var describe = (issue) => {
5
+ const path = (issue.path ?? []).map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
6
+ return path === "" ? issue.message : `${path}: ${issue.message}`;
7
+ };
8
+ /**
9
+ * Runs any Standard Schema (D-18) and returns its output or its issues. Works with sync and
10
+ * async validators alike.
11
+ */
12
+ var validate = async (schema, value) => {
13
+ const result = await schema["~standard"].validate(value);
14
+ return result.issues === void 0 ? ok(result.value) : err(result.issues.map(describe));
15
+ };
16
+ /**
17
+ * Any JSON value, checked with the contracts' `isJson`: no `undefined`, `NaN`, `Infinity`,
18
+ * `-0`, class instances or cycles.
19
+ */
20
+ var json = z.custom(isJson, { error: "Expected a JSON value" });
21
+ //#endregion
22
+ export { json, validate };
23
+
24
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","names":[],"sources":["../src/validate.ts"],"sourcesContent":["import { err, isJson, ok, type Json, type Result, type StandardSchemaV1 } from '@joystyk/contracts';\nimport * as z from 'zod/mini';\n\n/** One validation problem, flattened for notices and logs: `path: message`. */\nexport type Issue = string;\n\nconst describe = (issue: StandardSchemaV1.Issue): Issue => {\n const path = (issue.path ?? [])\n .map((segment) => String(typeof segment === 'object' ? segment.key : segment))\n .join('.');\n return path === '' ? issue.message : `${path}: ${issue.message}`;\n};\n\n/**\n * Runs any Standard Schema (D-18) and returns its output or its issues. Works with sync and\n * async validators alike.\n */\nexport const validate = async <Output>(\n schema: StandardSchemaV1<unknown, Output>,\n value: unknown,\n): Promise<Result<Output, readonly Issue[]>> => {\n const result = await schema['~standard'].validate(value);\n return result.issues === undefined ? ok(result.value) : err(result.issues.map(describe));\n};\n\n/**\n * Any JSON value, checked with the contracts' `isJson`: no `undefined`, `NaN`, `Infinity`,\n * `-0`, class instances or cycles.\n */\nexport const json = z.custom<Json>(isJson, { error: 'Expected a JSON value' });\n"],"mappings":";;;AAMA,IAAM,YAAY,UAAyC;CACzD,MAAM,QAAQ,MAAM,QAAQ,CAAC,EAAA,CAC1B,KAAK,YAAY,OAAO,OAAO,YAAY,WAAW,QAAQ,MAAM,OAAO,CAAC,CAAC,CAC7E,KAAK,GAAG;CACX,OAAO,SAAS,KAAK,MAAM,UAAU,GAAG,KAAK,IAAI,MAAM;AACzD;;;;;AAMA,IAAa,WAAW,OACtB,QACA,UAC8C;CAC9C,MAAM,SAAS,MAAM,OAAO,YAAY,CAAC,SAAS,KAAK;CACvD,OAAO,OAAO,WAAW,KAAA,IAAY,GAAG,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,IAAI,QAAQ,CAAC;AACzF;;;;;AAMA,IAAa,OAAO,EAAE,OAAa,QAAQ,EAAE,OAAO,wBAAwB,CAAC"}
package/package.json CHANGED
@@ -1,20 +1,54 @@
1
1
  {
2
2
  "name": "@joystyk/storage",
3
- "version": "0.0.0",
4
- "description": "Reserved for the first Joystyk library release.",
3
+ "version": "0.2.0",
4
+ "description": "Versioned, validated player data: saves, stats, streaks and preferences, over IndexedDB or memory.",
5
+ "private": false,
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./browser": {
14
+ "types": "./dist/browser/index.d.ts",
15
+ "default": "./dist/browser/index.js"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "zod": "4.6.5",
20
+ "@joystyk/contracts": "0.2.0"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "5.9.3",
24
+ "vite": "8.3.3",
25
+ "vitest": "4.1.11"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE",
34
+ "NOTICE",
35
+ "CHANGELOG.md"
36
+ ],
37
+ "engines": {
38
+ "node": ">=24.15.0"
39
+ },
5
40
  "license": "MIT",
6
41
  "repository": {
7
42
  "type": "git",
8
43
  "url": "git+https://github.com/joystyk-games/game-libraries.git",
9
44
  "directory": "packages/storage"
10
45
  },
11
- "homepage": "https://github.com/joystyk-games/game-libraries",
12
- "files": [
13
- "README.md",
14
- "LICENSE",
15
- "NOTICE"
16
- ],
17
- "publishConfig": {
18
- "access": "public"
46
+ "homepage": "https://github.com/joystyk-games/game-libraries/tree/main/packages/storage#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/joystyk-games/game-libraries/issues"
49
+ },
50
+ "scripts": {
51
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.core.json",
52
+ "build": "node ../../scripts/build-package.ts"
19
53
  }
20
- }
54
+ }