@crustjs/store 0.3.0 → 0.4.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.
- package/README.md +1 -1
- package/dist/index.d.ts +15 -11
- package/dist/index.js +25 -27
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -101,7 +101,7 @@ interface FieldDefBase<V> {
|
|
|
101
101
|
* persistence and is re-validated once to catch read-unstable
|
|
102
102
|
* transforms. Outputs outside the declared type are rejected with
|
|
103
103
|
* `VALIDATION`. On `read`, the transformed value is discarded —
|
|
104
|
-
*
|
|
104
|
+
* core fields return their defaulted, type-coerced input value.
|
|
105
105
|
* - Throwing an error — the value is rejected; the error message is
|
|
106
106
|
* captured as a validation issue with the field name as `path`.
|
|
107
107
|
*
|
|
@@ -315,11 +315,11 @@ interface CreateStoreOptions<F extends FieldsDef> {
|
|
|
315
315
|
access?: StoreAccess;
|
|
316
316
|
}
|
|
317
317
|
/**
|
|
318
|
-
* Receives the current config
|
|
319
|
-
*
|
|
320
|
-
*
|
|
318
|
+
* Receives the validated current config, including defaults, core coercion,
|
|
319
|
+
* and schema output, and returns an updated config. Invalid current state
|
|
320
|
+
* rejects before this callback runs; use `write` or `patch` to initialize or repair it.
|
|
321
321
|
*
|
|
322
|
-
* Used by {@link Store.update}, which validates the result before persistence.
|
|
322
|
+
* Used by {@link Store.update}, which also validates the result before persistence.
|
|
323
323
|
*
|
|
324
324
|
* @example
|
|
325
325
|
* ```ts
|
|
@@ -360,7 +360,8 @@ type StoreUpdater<TConfig> = (current: TConfig) => NoInfer<TConfig>;
|
|
|
360
360
|
*/
|
|
361
361
|
interface Store<TConfig> {
|
|
362
362
|
/**
|
|
363
|
-
* Reads the persisted config, applying field defaults for missing keys
|
|
363
|
+
* Reads the persisted config, applying field defaults for missing keys and
|
|
364
|
+
* returning successful schema output (including transforms) without writing.
|
|
364
365
|
*
|
|
365
366
|
* Always returns a value — fields with defaults are guaranteed present,
|
|
366
367
|
* fields without defaults may be `undefined`.
|
|
@@ -387,8 +388,9 @@ interface Store<TConfig> {
|
|
|
387
388
|
*/
|
|
388
389
|
write(config: NoInfer<TConfig>): Promise<TConfig>;
|
|
389
390
|
/**
|
|
390
|
-
* Reads current config
|
|
391
|
-
*
|
|
391
|
+
* Reads and validates current config before calling the updater. Applies
|
|
392
|
+
* the updater, validates the result, and persists. Invalid current state
|
|
393
|
+
* rejects before the callback runs; use `write` or `patch` to initialize or repair.
|
|
392
394
|
*
|
|
393
395
|
* The final file replacement is atomic, but the read-modify-write sequence
|
|
394
396
|
* is not serialized, even within one process. Await mutations sequentially
|
|
@@ -405,9 +407,11 @@ interface Store<TConfig> {
|
|
|
405
407
|
* Applies a partial update to the current config and persists.
|
|
406
408
|
*
|
|
407
409
|
* Only the provided keys are updated; everything else is preserved.
|
|
408
|
-
*
|
|
409
|
-
* applied but has not undergone full field validation. The
|
|
410
|
-
* validated before persistence.
|
|
410
|
+
* Unlike {@link Store.update}, the current config has core defaults and coercion
|
|
411
|
+
* applied but has not undergone full field validation, allowing repairs. The
|
|
412
|
+
* merged result is validated before persistence. Explicit `undefined` is rejected
|
|
413
|
+
* for defaulted core fields; optional core fields and schema-owned optionality
|
|
414
|
+
* remain supported. Await mutations sequentially to avoid lost updates.
|
|
411
415
|
*
|
|
412
416
|
* @param partial - A partial subset of the config to merge in.
|
|
413
417
|
* @returns The persisted config, including schema transformations.
|
package/dist/index.js
CHANGED
|
@@ -602,7 +602,13 @@ function createStore(options) {
|
|
|
602
602
|
const issues = [];
|
|
603
603
|
for (const [key, def] of Object.entries(fields)) {
|
|
604
604
|
const value = mutableState[key];
|
|
605
|
-
if (value === void 0 && def.schema === void 0)
|
|
605
|
+
if (value === void 0 && def.schema === void 0) {
|
|
606
|
+
if (def.default !== void 0) issues.push({
|
|
607
|
+
message: expectedTypeMessage(def),
|
|
608
|
+
path: key
|
|
609
|
+
});
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
606
612
|
if (value !== void 0 && def.schema === void 0 && !matchesDeclaredType(def, value)) {
|
|
607
613
|
issues.push({
|
|
608
614
|
message: expectedTypeMessage(def),
|
|
@@ -627,7 +633,7 @@ function createStore(options) {
|
|
|
627
633
|
if (isFieldValueResult(result)) {
|
|
628
634
|
const transformed = result.value;
|
|
629
635
|
if (operation === "read") {
|
|
630
|
-
if (
|
|
636
|
+
if (def.schema !== void 0) mutableState[key] = transformed;
|
|
631
637
|
continue;
|
|
632
638
|
}
|
|
633
639
|
if (def.schema === void 0 && !matchesDeclaredType(def, transformed)) {
|
|
@@ -662,8 +668,11 @@ function createStore(options) {
|
|
|
662
668
|
}
|
|
663
669
|
if (operation !== "read") for (const [key, value] of Object.entries(mutableState)) {
|
|
664
670
|
if (value === void 0) continue;
|
|
665
|
-
|
|
666
|
-
|
|
671
|
+
try {
|
|
672
|
+
if (isDeepStrictEqual(JSON.parse(JSON.stringify(value)), value)) continue;
|
|
673
|
+
} catch {}
|
|
674
|
+
issues.push({
|
|
675
|
+
message: "value does not survive JSON serialization (cycles, NaN, Infinity, -0, sparse arrays, or undefined properties)",
|
|
667
676
|
path: key
|
|
668
677
|
});
|
|
669
678
|
}
|
|
@@ -672,45 +681,34 @@ function createStore(options) {
|
|
|
672
681
|
issues
|
|
673
682
|
});
|
|
674
683
|
}
|
|
675
|
-
async function readRaw(
|
|
684
|
+
async function readRaw() {
|
|
676
685
|
const persisted = await readJson(filePath);
|
|
677
686
|
const persistedObject = persisted !== void 0 && isJsonObject(persisted) ? persisted : void 0;
|
|
678
687
|
if (persisted !== void 0 && persistedObject === void 0) throw new CrustStoreError("PARSE", `Expected a JSON object in config file: ${filePath}`, { path: filePath });
|
|
679
|
-
|
|
680
|
-
if (materializeSchemaDefaults) for (const [key, def] of Object.entries(fields)) {
|
|
681
|
-
if (merged[key] !== void 0 || def.schema === void 0) continue;
|
|
682
|
-
try {
|
|
683
|
-
const result = await validators.get(key)?.(void 0);
|
|
684
|
-
if (result !== void 0 && isFieldValueResult(result)) merged[key] = result.value;
|
|
685
|
-
} catch {}
|
|
686
|
-
}
|
|
687
|
-
return normalizeStateTypes(merged);
|
|
688
|
+
return normalizeStateTypes(applyFieldDefaults(persistedObject, fields, shouldPrune));
|
|
688
689
|
}
|
|
689
690
|
async function read() {
|
|
690
691
|
const document = await readRaw();
|
|
691
692
|
await runFieldValidators(document, "read");
|
|
692
693
|
return document;
|
|
693
694
|
}
|
|
694
|
-
async function
|
|
695
|
-
const normalized = normalizeStateTypes(
|
|
696
|
-
await runFieldValidators(normalized,
|
|
695
|
+
async function persist(document, operation) {
|
|
696
|
+
const normalized = normalizeStateTypes(document);
|
|
697
|
+
await runFieldValidators(normalized, operation);
|
|
697
698
|
await writeJson(filePath, normalized, writeOptions);
|
|
698
699
|
return normalized;
|
|
699
700
|
}
|
|
701
|
+
async function write(config) {
|
|
702
|
+
return persist({ ...config }, "write");
|
|
703
|
+
}
|
|
700
704
|
async function update(updater) {
|
|
701
|
-
|
|
702
|
-
await runFieldValidators(normalized, "update");
|
|
703
|
-
await writeJson(filePath, normalized, writeOptions);
|
|
704
|
-
return normalized;
|
|
705
|
+
return persist({ ...updater(await read()) }, "update");
|
|
705
706
|
}
|
|
706
707
|
async function patch(partial) {
|
|
707
|
-
|
|
708
|
-
...await readRaw(
|
|
708
|
+
return persist({
|
|
709
|
+
...await readRaw(),
|
|
709
710
|
...partial
|
|
710
|
-
});
|
|
711
|
-
await runFieldValidators(normalized, "patch");
|
|
712
|
-
await writeJson(filePath, normalized, writeOptions);
|
|
713
|
-
return normalized;
|
|
711
|
+
}, "patch");
|
|
714
712
|
}
|
|
715
713
|
async function reset() {
|
|
716
714
|
await deleteJson(filePath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crustjs/store",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "DX-first, typed persistence for CLI apps with config/data/state/cache separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
|
-
"bun": ">=1.
|
|
65
|
+
"bun": ">=1.4.0",
|
|
66
66
|
"node": ">=22",
|
|
67
67
|
"deno": ">=2.8"
|
|
68
68
|
}
|