@crustjs/store 0.0.2 → 0.0.4

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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @crustjs/store
2
2
 
3
- Minimal, DX-first, type-safe config persistence for CLI apps.
3
+ DX-first, typed persistence for CLI apps with clear config/data/state/cache separation.
4
4
 
5
- `@crustjs/store` gives your CLI a production-ready config store with near-zero setup. Declare your fields, get full type inference, and let the store handle path resolution, file IO, defaults, and error mapping.
5
+ `@crustjs/store` gives your CLI production-ready local persistence with near-zero setup. Provide a `fields` definition, pick a storage intent, and get a typed store with read/write/update/patch/reset — no manual type annotations needed.
6
6
 
7
7
  ## Install
8
8
 
@@ -28,154 +28,222 @@ const store = createStore({
28
28
  dirPath: configDir("my-cli"),
29
29
  fields: {
30
30
  theme: { type: "string", default: "light" },
31
+ fontSize: { type: "number", default: 14 },
31
32
  verbose: { type: "boolean", default: false },
32
- token: { type: "string" }, // optional — no default
33
33
  },
34
34
  });
35
35
 
36
- // Read config (returns defaults when no persisted file exists)
37
- const config = await store.read();
38
- // → { theme: "light", verbose: false, token: undefined }
36
+ // Read state (returns defaults when no persisted file exists)
37
+ const state = await store.read();
38
+ // → { theme: "light", fontSize: 14, verbose: false }
39
39
 
40
- // Write a full config object
41
- await store.write({ theme: "dark", verbose: true, token: "abc123" });
40
+ // Write a full state object
41
+ await store.write({ theme: "dark", fontSize: 16, verbose: true });
42
42
 
43
- // Update a single field
43
+ // Update with a function
44
44
  await store.update((current) => ({ ...current, verbose: false }));
45
45
 
46
+ // Patch only specific keys
47
+ await store.patch({ theme: "solarized" });
48
+
46
49
  // Reset to defaults (removes persisted file)
47
50
  await store.reset();
48
51
  ```
49
52
 
50
- No explicit generics needed — types are inferred from your field definitions.
53
+ No explicit generics needed — types are inferred from your `fields` definition.
54
+
55
+ ## Storage Intent: Choosing a Directory
56
+
57
+ `@crustjs/store` provides four path helpers that resolve platform-standard directories for different storage intents. Use the one that matches what you're persisting:
58
+
59
+ | Helper | Intent | Example Use Case |
60
+ | ----------- | -------------------------- | --------------------------------------- |
61
+ | `configDir` | User preferences & config | Theme, editor settings, API keys |
62
+ | `dataDir` | Important app data | Local databases, downloaded resources |
63
+ | `stateDir` | Runtime state | Window positions, scroll offsets, undo |
64
+ | `cacheDir` | Regenerable cached data | HTTP caches, compiled assets |
65
+
66
+ ```ts
67
+ import { createStore, configDir, dataDir, stateDir, cacheDir } from "@crustjs/store";
68
+
69
+ // User preferences → ~/.config/my-cli/config.json
70
+ const config = createStore({
71
+ dirPath: configDir("my-cli"),
72
+ fields: {
73
+ theme: { type: "string", default: "light" },
74
+ verbose: { type: "boolean", default: false },
75
+ },
76
+ });
77
+
78
+ // App data → ~/.local/share/my-cli/config.json
79
+ const data = createStore({
80
+ dirPath: dataDir("my-cli"),
81
+ fields: {
82
+ bookmarks: { type: "array", default: [] },
83
+ },
84
+ });
85
+
86
+ // Runtime state → ~/.local/state/my-cli/config.json
87
+ const state = createStore({
88
+ dirPath: stateDir("my-cli"),
89
+ fields: {
90
+ lastOpened: { type: "string", default: "" },
91
+ scrollY: { type: "number", default: 0 },
92
+ },
93
+ });
94
+
95
+ // Cache → ~/.cache/my-cli/config.json
96
+ const cache = createStore({
97
+ dirPath: cacheDir("my-cli"),
98
+ fields: {
99
+ etag: { type: "string", default: "" },
100
+ payload: { type: "string", default: "" },
101
+ },
102
+ });
103
+ ```
104
+
105
+ ### Platform Paths
106
+
107
+ All four helpers follow XDG conventions on Linux and macOS, and Windows-native conventions on Windows:
108
+
109
+ | Helper | Linux / macOS | Windows | Env Override |
110
+ | ----------- | ----------------------------------- | ------------------------------------------- | -------------------- |
111
+ | `configDir` | `~/.config/<app>` | `%APPDATA%\<app>` | `$XDG_CONFIG_HOME` |
112
+ | `dataDir` | `~/.local/share/<app>` | `%LOCALAPPDATA%\<app>\Data` | `$XDG_DATA_HOME` |
113
+ | `stateDir` | `~/.local/state/<app>` | `%LOCALAPPDATA%\<app>\State` | `$XDG_STATE_HOME` |
114
+ | `cacheDir` | `~/.cache/<app>` | `%LOCALAPPDATA%\<app>\Cache` | `$XDG_CACHE_HOME` |
115
+
116
+ macOS uses XDG conventions (same as Linux) for a unified Unix mental model.
51
117
 
52
118
  ## API
53
119
 
54
120
  ### `createStore(options)`
55
121
 
56
- Creates a typed async config store backed by a local JSON file. The file path is resolved once at creation time.
122
+ Creates a typed async store backed by a local JSON file. The file path is resolved once at creation time.
57
123
 
58
124
  ```ts
59
125
  const store = createStore(options);
60
126
  ```
61
127
 
62
- **Returns:** A `Store<TConfig>` with `read()`, `write()`, `update()`, and `reset()` methods.
128
+ **Returns:** A `Store<T>` with `read()`, `write()`, `update()`, `patch()`, and `reset()` methods.
63
129
 
64
130
  **Throws:** `CrustStoreError` with `PATH` code if `dirPath` or `name` is invalid.
65
131
 
66
132
  #### Options
67
133
 
68
- | Option | Type | Required | Description |
69
- | --------- | ---------- | -------- | ------------------------------------------------------------------ |
70
- | `dirPath` | `string` | Yes | Absolute directory path where the JSON file is stored. |
71
- | `name` | `string` | No | Store name used as filename (default `"config"` → `config.json`). |
72
- | `fields` | `FieldsDef`| Yes | Field definitions that declare the config schema. |
134
+ | Option | Type | Required | Description |
135
+ | -------------- | -------------------------- | -------- | ------------------------------------------------------------------ |
136
+ | `dirPath` | `string` | Yes | Absolute directory path where the JSON file is stored. |
137
+ | `name` | `string` | No | Store name used as filename (default `"config"` → `config.json`). |
138
+ | `fields` | `FieldsDef` | Yes | Field definitions defining the store's data shape, types, defaults, and optional validation. |
139
+ | `pruneUnknown` | `boolean` | No | Drop unknown persisted keys on read (default `true`). |
73
140
 
74
- ### `configDir(appName, env?)`
141
+ ### `store.read()`
75
142
 
76
- Resolves the platform-standard config directory for the given app name.
143
+ Reads the persisted state file. Missing keys are filled from field `defaults`.
77
144
 
78
145
  ```ts
79
- import { configDir } from "@crustjs/store";
80
-
81
- const dir = configDir("my-cli");
82
- // → "/home/user/.config/my-cli" (Linux)
83
- // → "/Users/user/Library/Application Support/my-cli" (macOS)
84
- // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
146
+ const state = await store.read();
85
147
  ```
86
148
 
87
- Use it with `dirPath`:
149
+ Always returns a value — never `undefined`. Does not write merged defaults back to disk.
150
+
151
+ ### `store.write(state)`
152
+
153
+ Atomically persists a full state object. The entire previous state is replaced.
88
154
 
89
155
  ```ts
90
- const store = createStore({
91
- dirPath: configDir("my-cli"),
92
- fields: { /* ... */ },
93
- });
156
+ await store.write({ theme: "dark", fontSize: 14, verbose: true });
94
157
  ```
95
158
 
96
- ### Field Definitions
159
+ Calls per-field `validate` functions (if provided) before writing. Parent directories are created if missing.
160
+
161
+ ### `store.update(updater)`
162
+
163
+ Reads the current effective state, applies the updater function, and atomically persists the result.
97
164
 
98
- Each field in the `fields` record is a `FieldDef` with a `type` discriminant:
165
+ ```ts
166
+ await store.update((current) => ({
167
+ ...current,
168
+ theme: "dark",
169
+ }));
170
+ ```
99
171
 
100
- | Property | Type | Required | Description |
101
- | ------------- | --------------------------------------- | -------- | --------------------------------------------------- |
102
- | `type` | `"string" \| "number" \| "boolean"` | Yes | The value type. |
103
- | `default` | Matches `type` | No | Default value when the field is not persisted. |
104
- | `array` | `true` | No | Collect values into an array. |
105
- | `description` | `string` | No | Human-readable description for tooling. |
172
+ When no persisted file exists, the updater receives field `defaults` as the current value. Calls per-field `validate` functions before writing.
106
173
 
107
- Type inference rules:
174
+ ### `store.patch(partial)`
108
175
 
109
- - **Has `default`** → field is guaranteed present (e.g., `string`)
110
- - **No `default`** → field is optional (e.g., `string | undefined`)
111
- - **`array: true`** → wraps the type in an array (e.g., `string[]`)
176
+ Applies a shallow partial update to the current state and persists. Only the provided keys are updated; everything else is preserved.
112
177
 
113
178
  ```ts
114
- const store = createStore({
115
- dirPath: configDir("my-cli"),
116
- fields: {
117
- theme: { type: "string", default: "light" }, // → string
118
- verbose: { type: "boolean", default: false }, // → boolean
119
- retries: { type: "number", default: 3 }, // → number
120
- token: { type: "string" }, // → string | undefined
121
- tags: { type: "string", array: true, default: [] },// → string[]
122
- ids: { type: "number", array: true }, // → number[] | undefined
123
- },
124
- });
179
+ await store.patch({ theme: "solarized" });
125
180
  ```
126
181
 
127
- ### `store.read()`
182
+ Calls per-field `validate` functions before writing.
183
+
184
+ ### `store.reset()`
128
185
 
129
- Reads the persisted config file. Missing fields are filled from their `default` values. Fields without defaults are `undefined` when not persisted.
186
+ Removes the persisted state file. After reset, `read()` returns `defaults`.
130
187
 
131
188
  ```ts
132
- const config = await store.read();
189
+ await store.reset();
133
190
  ```
134
191
 
135
- Always returns a value — never `undefined`.
192
+ Reset is idempotent — calling it when no file exists is a no-op.
193
+
194
+ ### Path Helpers
136
195
 
137
- ### `store.write(config)`
196
+ #### `configDir(appName, env?)`
138
197
 
139
- Atomically persists a full config object. The entire previous config is replaced.
198
+ Resolves the platform-standard config directory for the given app name.
140
199
 
141
200
  ```ts
142
- await store.write({ theme: "dark", verbose: true, token: "abc123" });
201
+ import { configDir } from "@crustjs/store";
202
+
203
+ const dir = configDir("my-cli");
204
+ // → "/home/user/.config/my-cli" (Linux / macOS)
205
+ // → "C:\\Users\\user\\AppData\\Roaming\\my-cli" (Windows)
143
206
  ```
144
207
 
145
- ### `store.update(updater)`
208
+ #### `dataDir(appName, env?)`
146
209
 
147
- Reads the current effective config, applies the updater function, and atomically persists the result.
210
+ Resolves the platform-standard data directory.
148
211
 
149
212
  ```ts
150
- await store.update((current) => ({
151
- ...current,
152
- theme: "dark",
153
- }));
213
+ import { dataDir } from "@crustjs/store";
214
+
215
+ const dir = dataDir("my-cli");
216
+ // → "/home/user/.local/share/my-cli" (Linux / macOS)
217
+ // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Data" (Windows)
154
218
  ```
155
219
 
156
- When no persisted file exists, the updater receives field defaults as the current value.
220
+ #### `stateDir(appName, env?)`
157
221
 
158
- ### `store.reset()`
159
-
160
- Removes the persisted config file. After reset, `read()` returns field defaults.
222
+ Resolves the platform-standard state directory.
161
223
 
162
224
  ```ts
163
- await store.reset();
225
+ import { stateDir } from "@crustjs/store";
226
+
227
+ const dir = stateDir("my-cli");
228
+ // → "/home/user/.local/state/my-cli" (Linux / macOS)
229
+ // → "C:\\Users\\user\\AppData\\Local\\my-cli\\State" (Windows)
164
230
  ```
165
231
 
166
- Reset is idempotent — calling it when no file exists is a no-op.
232
+ #### `cacheDir(appName, env?)`
167
233
 
168
- ## Config File Path
234
+ Resolves the platform-standard cache directory.
235
+
236
+ ```ts
237
+ import { cacheDir } from "@crustjs/store";
169
238
 
170
- `configDir()` resolves the platform-standard config directory. The `name` option defaults to `"config"`, producing `config.json`:
239
+ const dir = cacheDir("my-cli");
240
+ // → "/home/user/.cache/my-cli" (Linux / macOS)
241
+ // → "C:\\Users\\user\\AppData\\Local\\my-cli\\Cache" (Windows)
242
+ ```
171
243
 
172
- | Platform | Default Path | Env Override |
173
- | -------- | ------------------------------------------------------- | ------------------ |
174
- | Linux | `~/.config/<appName>/<name>.json` | `$XDG_CONFIG_HOME` |
175
- | macOS | `~/Library/Application Support/<appName>/<name>.json` | — |
176
- | Windows | `%APPDATA%\<appName>\<name>.json` | `%APPDATA%` |
244
+ All path helpers accept an optional `PlatformEnv` parameter for deterministic testing without mutating `process.env`.
177
245
 
178
- ### Multiple Stores
246
+ ## Multiple Stores
179
247
 
180
248
  Use the `name` option to create multiple stores under the same directory:
181
249
 
@@ -185,10 +253,11 @@ import { createStore, configDir } from "@crustjs/store";
185
253
  const dir = configDir("my-cli");
186
254
 
187
255
  // Default store → ~/.config/my-cli/config.json
188
- const configStore = createStore({
256
+ const settingsStore = createStore({
189
257
  dirPath: dir,
190
258
  fields: {
191
259
  theme: { type: "string", default: "light" },
260
+ fontSize: { type: "number", default: 14 },
192
261
  verbose: { type: "boolean", default: false },
193
262
  },
194
263
  });
@@ -198,7 +267,7 @@ const authStore = createStore({
198
267
  dirPath: dir,
199
268
  name: "auth",
200
269
  fields: {
201
- token: { type: "string" },
270
+ token: { type: "string", default: "" },
202
271
  },
203
272
  });
204
273
  ```
@@ -209,45 +278,157 @@ The `name` must not contain path separators or the `.json` extension.
209
278
 
210
279
  ## Defaults & Merge
211
280
 
212
- When a persisted file exists, `read()` applies field defaults for any missing keys:
281
+ When a persisted file exists, `read()` fills missing keys from field `defaults`:
213
282
 
214
283
  ```ts
215
284
  const store = createStore({
216
285
  dirPath: configDir("my-cli"),
217
286
  fields: {
218
287
  theme: { type: "string", default: "light" },
288
+ fontSize: { type: "number", default: 14 },
219
289
  verbose: { type: "boolean", default: false },
220
- retries: { type: "number", default: 3 },
221
290
  },
222
291
  });
223
292
 
224
- // Persisted file contains: { "theme": "dark" }
225
- const config = await store.read();
226
- // → { theme: "dark", verbose: false, retries: 3 }
293
+ // Persisted file contains: { "theme": "dark", "verbose": true }
294
+ const state = await store.read();
295
+ // → { theme: "dark", fontSize: 14, verbose: true }
227
296
  ```
228
297
 
229
- ### Merge rules
298
+ ### Merge Rules
230
299
 
231
- - Fields present in the persisted file use the persisted value.
232
- - Missing fields with a `default` use the default value.
233
- - Missing fields without a `default` are omitted (typed as `T | undefined`).
234
- - Extra keys in the persisted file that are not defined in `fields` are dropped.
235
- - Array defaults are shallow-copied to prevent shared mutation.
300
+ - Persisted values override defaults.
301
+ - Missing keys fall back to defaults.
302
+ - Unknown persisted keys are dropped by default (`pruneUnknown: true`). Set `pruneUnknown: false` to preserve them.
303
+ - All values are deep-cloned to prevent shared-reference mutation from defaults.
236
304
  - Falsy values (`null`, `0`, `""`, `false`) in the persisted file are preserved — only truly missing keys trigger defaults.
237
305
 
238
306
  ### Important: defaults are not auto-persisted
239
307
 
240
- Merged defaults exist only in memory. The merged result is **not** written back to disk. The persisted file remains unchanged until you explicitly call `write()` or `update()`.
308
+ Merged defaults exist only in memory. The persisted file remains unchanged until you explicitly call `write()`, `update()`, or `patch()`.
309
+
310
+ ## Validation
311
+
312
+ Add per-field validation to enforce config integrity on every read, write, update, and patch. When a `validate` function is configured on a field, validation is **strict by default** — invalid values fail loudly.
313
+
314
+ ### Using `@crustjs/validate`
315
+
316
+ The easiest way to add validation is with field adapters from `@crustjs/validate`:
317
+
318
+ ```ts
319
+ import { z } from "zod";
320
+ import { field } from "@crustjs/validate/zod";
321
+ import { createStore, configDir } from "@crustjs/store";
322
+
323
+ const store = createStore({
324
+ dirPath: configDir("my-cli"),
325
+ fields: {
326
+ theme: {
327
+ type: "string",
328
+ default: "light",
329
+ validate: field(z.enum(["light", "dark"])),
330
+ },
331
+ verbose: { type: "boolean", default: false },
332
+ },
333
+ });
334
+
335
+ // write() validates before persisting
336
+ await store.write({ theme: "neon", verbose: true });
337
+ // → throws CrustStoreError("VALIDATION")
338
+
339
+ // read() validates after applying defaults
340
+ const config = await store.read();
341
+ // → throws if persisted config is invalid
342
+ ```
343
+
344
+ `fieldSync()` is also available for synchronous schemas.
345
+
346
+ For Effect schemas, wrap with `Schema.standardSchemaV1()`:
347
+
348
+ ```ts
349
+ import * as Schema from "effect/Schema";
350
+ import { field } from "@crustjs/validate/effect";
351
+
352
+ const store = createStore({
353
+ dirPath: configDir("my-cli"),
354
+ fields: {
355
+ theme: {
356
+ type: "string",
357
+ default: "light",
358
+ validate: field(
359
+ Schema.standardSchemaV1(
360
+ Schema.Literal("light", "dark"),
361
+ ),
362
+ ),
363
+ },
364
+ },
365
+ });
366
+ ```
367
+
368
+ ### Custom validators
369
+
370
+ You can also provide a validator function directly on each field:
371
+
372
+ ```ts
373
+ const store = createStore({
374
+ dirPath: configDir("my-cli"),
375
+ fields: {
376
+ theme: {
377
+ type: "string",
378
+ default: "light",
379
+ validate(value) {
380
+ const str = value as string;
381
+ if (str !== "light" && str !== "dark") {
382
+ return {
383
+ ok: false,
384
+ issues: [{ message: 'Must be "light" or "dark"', path: "" }],
385
+ };
386
+ }
387
+ return { ok: true, value: str };
388
+ },
389
+ },
390
+ },
391
+ });
392
+ ```
393
+
394
+ The per-field `validate` function follows the `StoreValidator<T>` contract: `(value: unknown) => StoreValidatorResult<T> | Promise<StoreValidatorResult<T>>`, where `StoreValidatorResult<T>` is `{ ok: true, value: T } | { ok: false, issues: StoreValidatorIssue[] }`.
395
+
396
+ ### Validation behavior
397
+
398
+ - **Write**: Validates each field before persisting. If a validator transforms the value, the transformed result is persisted.
399
+ - **Read**: Validates each field after applying defaults. Invalid persisted config fails loudly — no silent fallback to defaults.
400
+ - **Update**: Reads raw config (no validation), applies updater, validates each field, then persists.
401
+ - **Patch**: Reads raw config, applies shallow partial merge, validates each field, then persists.
402
+ - **Reset**: No validation — just removes the persisted file.
403
+
404
+ ### Catching validation errors
405
+
406
+ ```ts
407
+ import { CrustStoreError } from "@crustjs/store";
408
+
409
+ try {
410
+ await store.read();
411
+ } catch (err) {
412
+ if (err instanceof CrustStoreError && err.is("VALIDATION")) {
413
+ // err.details is { operation: "read" | "write" | "update" | "patch", issues: StoreValidationIssue[] }
414
+ console.error(`Validation failed during ${err.details.operation}:`);
415
+ for (const issue of err.details.issues) {
416
+ console.error(` ${issue.path || "(root)"}: ${issue.message}`);
417
+ }
418
+ }
419
+ }
420
+ ```
241
421
 
242
422
  ## Error Handling
243
423
 
244
424
  All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a typed `code` property:
245
425
 
246
- | Code | When | Details |
247
- | ------- | ----------------------------------------------------------- | --------------------------- |
248
- | `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform | `{ path: string }` |
249
- | `PARSE` | Malformed JSON in persisted config file | `{ path: string }` |
250
- | `IO` | Filesystem read, write, or delete failure | `{ path, operation }` |
426
+ | Code | When | Details |
427
+ | ------------ | --------------------------------------------------------------- | --------------------------- |
428
+ | `PATH` | Invalid `dirPath`, invalid `name`, unsupported platform | `{ path: string }` |
429
+ | `PARSE` | Malformed JSON in persisted config file | `{ path: string }` |
430
+ | `IO` | Filesystem read, write, or delete failure | `{ path, operation }` |
431
+ | `VALIDATION` | Config fails validator on read, write, update, or patch | `{ operation, issues }` |
251
432
 
252
433
  ### Catching errors by code
253
434
 
@@ -255,12 +436,12 @@ All errors thrown by `@crustjs/store` are instances of `CrustStoreError` with a
255
436
  import { CrustStoreError } from "@crustjs/store";
256
437
 
257
438
  try {
258
- const config = await store.read();
439
+ const state = await store.read();
259
440
  } catch (err) {
260
441
  if (err instanceof CrustStoreError) {
261
442
  switch (err.code) {
262
443
  case "PARSE":
263
- console.error(`Corrupt config at ${err.details.path}`);
444
+ console.error(`Corrupt file at ${err.details.path}`);
264
445
  console.error("Delete the file and retry, or fix the JSON manually.");
265
446
  break;
266
447
  case "IO":
@@ -287,14 +468,14 @@ if (err instanceof CrustStoreError && err.is("IO")) {
287
468
  `CrustStoreError` preserves the original error as `cause` for debugging:
288
469
 
289
470
  ```ts
290
- if (err instanceof CrustStoreError && err.is("PARSE")) {
291
- console.error("Original parse error:", err.cause);
471
+ if (err instanceof CrustStoreError && err.is("VALIDATION")) {
472
+ console.error("Validation error:", err.cause);
292
473
  }
293
474
  ```
294
475
 
295
476
  ### Malformed JSON
296
477
 
297
- `@crustjs/store` does **not** silently recover from malformed JSON. If the persisted config file contains invalid JSON, `read()` throws a `CrustStoreError` with `PARSE` code immediately — no fallback to defaults.
478
+ `@crustjs/store` does **not** silently recover from malformed JSON. If the persisted file contains invalid JSON, `read()` throws a `CrustStoreError` with `PARSE` code immediately — no fallback to defaults.
298
479
 
299
480
  ## Types
300
481
 
@@ -303,38 +484,49 @@ All types are exported for use in your application:
303
484
  ```ts
304
485
  import type {
305
486
  CreateStoreOptions,
487
+ Store,
488
+ StoreUpdater,
306
489
  FieldDef,
307
490
  FieldsDef,
308
491
  InferStoreConfig,
309
- Store,
310
- StoreUpdater,
311
- ValueType,
492
+ StoreValidator,
493
+ StoreValidatorResult,
494
+ StoreValidatorIssue,
312
495
  StoreErrorCode,
496
+ StoreValidationIssue,
497
+ ValidationErrorDetails,
498
+ PlatformEnv,
313
499
  } from "@crustjs/store";
314
500
  ```
315
501
 
316
- | Type | Description |
317
- | -------------------- | ----------------------------------------------------------------------- |
318
- | `CreateStoreOptions` | Options object for `createStore()`. |
319
- | `FieldDef` | Single field definition (discriminated by `type` and `array`). |
320
- | `FieldsDef` | Record mapping field names to `FieldDef`. |
321
- | `InferStoreConfig` | Utility type: infers the config shape from a `FieldsDef`. |
322
- | `Store` | Store instance with `read`, `write`, `update`, `reset`. |
323
- | `StoreUpdater` | Updater function type `(current: TConfig) => TConfig`. |
324
- | `ValueType` | Supported type literals: `"string" \| "number" \| "boolean"`. |
325
- | `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO"`. |
326
-
327
- ## v1 Scope & Non-Goals
328
-
329
- `@crustjs/store` v1 is intentionally minimal. The following are **not** included:
330
-
331
- - **Generic state/cache abstractions** — v1 is config-only (single typed object per store).
502
+ | Type | Description |
503
+ | ------------------------- | ----------------------------------------------------------------------- |
504
+ | `CreateStoreOptions` | Options object for `createStore()`. |
505
+ | `Store` | Store instance with `read`, `write`, `update`, `patch`, `reset`. |
506
+ | `StoreUpdater` | Updater function type `(current: T) => T`. |
507
+ | `FieldDef` | Single field definition with `type`, optional `default`, and optional `validate`. |
508
+ | `FieldsDef` | Record of field names to `FieldDef` definitions. |
509
+ | `InferStoreConfig` | Inferred store state type from a `FieldsDef` definition. |
510
+ | `StoreValidator` | Validator function contract `(value: unknown) => StoreValidatorResult`. |
511
+ | `StoreValidatorResult` | Discriminated union: `{ ok: true, value: T } \| { ok: false, issues: StoreValidatorIssue[] }`. |
512
+ | `StoreValidatorIssue` | `{ message: string, path: string }`. |
513
+ | `StoreValidationIssue` | Validation issue in error details payload. |
514
+ | `ValidationErrorDetails` | Error details for `VALIDATION` code: `{ operation, issues }`. |
515
+ | `StoreErrorCode` | Union of error codes: `"PATH" \| "PARSE" \| "IO" \| "VALIDATION"`. |
516
+ | `PlatformEnv` | Injectable platform environment for testing path helpers. |
517
+ | `CrustStoreError` | Typed error class with `code`, `details`, and `cause`. |
518
+
519
+ ## Scope & Non-Goals
520
+
521
+ `@crustjs/store` is intentionally focused. The following are **not** included:
522
+
523
+ - **Cross-process locking** — assumes single-process usage for write coordination.
332
524
  - **Built-in encryption or keychain integration.**
333
- - **Cross-process locking** — v1 assumes single-process usage for write coordination.
334
525
  - **Sync API variants** (`readSync`, `writeSync`, etc.).
335
- - **Built-in validation** — will be added later via `@crustjs/validate` integration.
526
+ - **Automatic migration of invalid persisted config** — validation fails loudly; callers handle recovery.
336
527
  - **Alternative formats** (YAML, TOML, JSON5) — JSON is the only on-disk format.
337
- - **Deep `@crustjs/core` integration** — the store is a standalone package.
528
+ - **Remote/cloud synchronization** or multi-device sync.
529
+ - **Domain-specific migration frameworks** beyond simple store-level upgrade hooks.
338
530
 
339
531
  ## License
340
532