@deepseek-ai/dsh-storage-json 0.1.2-alpha.4 → 0.1.2-alpha.5
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/lib/index.js +57 -21
- package/lib/types/format.d.ts +9 -7
- package/lib/types/per-record-unit.d.ts +17 -6
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -132,16 +132,18 @@ function serializeRecord(version, value) {
|
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
134
|
* Parse one per-record document, validating its version stamp. A document
|
|
135
|
-
* that is malformed or stamped with
|
|
136
|
-
* as absent — the per-record contract: one bad or stale record file
|
|
137
|
-
* brick the whole unit, and a version bump discards stale records
|
|
138
|
-
* migrating them (the whole-unit format rejects instead, because
|
|
139
|
-
* exactly one document).
|
|
135
|
+
* that is malformed or stamped with an unaccepted version is FOREIGN and
|
|
136
|
+
* reads as absent — the per-record contract: one bad or stale record file
|
|
137
|
+
* must not brick the whole unit, and a version bump discards stale records
|
|
138
|
+
* instead of migrating them (the whole-unit format rejects instead, because
|
|
139
|
+
* there is exactly one document).
|
|
140
140
|
* @param text - Raw per-record document content.
|
|
141
|
-
* @param
|
|
141
|
+
* @param versions - Accepted unit versions (the current one plus the
|
|
142
|
+
* descriptor's compatibleVersions); any other stamp discards the
|
|
143
|
+
* document.
|
|
142
144
|
* @returns the record value, or `undefined` for a foreign document.
|
|
143
145
|
*/
|
|
144
|
-
function parseRecord(text,
|
|
146
|
+
function parseRecord(text, versions) {
|
|
145
147
|
let document;
|
|
146
148
|
try {
|
|
147
149
|
document = JSON.parse(text);
|
|
@@ -150,7 +152,7 @@ function parseRecord(text, version) {
|
|
|
150
152
|
}
|
|
151
153
|
if (typeof document !== "object" || document === null) return void 0;
|
|
152
154
|
const { version: stamped, record } = document;
|
|
153
|
-
if (stamped !==
|
|
155
|
+
if (typeof stamped !== "number" || !versions.includes(stamped)) return void 0;
|
|
154
156
|
return record;
|
|
155
157
|
}
|
|
156
158
|
//#endregion
|
|
@@ -281,15 +283,18 @@ var SingleJsonUnit = class {
|
|
|
281
283
|
* memory unchanged.
|
|
282
284
|
*
|
|
283
285
|
* Per-record contract: a record document that is malformed or stamped with a
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
286
|
+
* version outside the accepted set (the descriptor's current version plus
|
|
287
|
+
* its `compatibleVersions`) reads as an absent record — one bad or stale
|
|
288
|
+
* file never bricks the whole unit, and a version bump discards stale
|
|
289
|
+
* records instead of migrating them. Record keys become path segments, so
|
|
290
|
+
* they must be path-safe (`[a-zA-Z0-9_-]+`); an unsafe key rejects at write.
|
|
288
291
|
*
|
|
289
292
|
* Legacy bootstrap: when the new tree has no document path, a legacy
|
|
290
293
|
* whole-unit file `<root>/<name>.json` (the pre-per-record layout) seeds
|
|
291
|
-
* per-record documents
|
|
292
|
-
*
|
|
294
|
+
* per-record documents, provided its stored unit version is in the accepted
|
|
295
|
+
* set — a legacy file stamped with any other version is left alone and reads
|
|
296
|
+
* as the empty unit. Any new document path, including one whose contents are
|
|
297
|
+
* unreadable or stale, suppresses the bootstrap for the whole unit. The
|
|
293
298
|
* legacy file is never changed or deleted.
|
|
294
299
|
* @module @deepseek-ai/dsh-storage-json/src/per-record-unit
|
|
295
300
|
*/
|
|
@@ -318,6 +323,7 @@ async function openPerRecordUnit(descriptor, root, onClose) {
|
|
|
318
323
|
* @returns the authoritative state reconstructed from the tree.
|
|
319
324
|
*/
|
|
320
325
|
async function loadPerRecordState(descriptor, dir) {
|
|
326
|
+
const versions = acceptedStamps(descriptor);
|
|
321
327
|
const state = {
|
|
322
328
|
version: descriptor.version,
|
|
323
329
|
global: null,
|
|
@@ -332,10 +338,10 @@ async function loadPerRecordState(descriptor, dir) {
|
|
|
332
338
|
if (!(entries === void 0 ? false : (await Promise.all(entries.map(async (entry) => {
|
|
333
339
|
if (entry.isDirectory()) {
|
|
334
340
|
const records = state.tables.get(entry.name);
|
|
335
|
-
if (records !== void 0) return loadTableRecords(records,
|
|
341
|
+
if (records !== void 0) return loadTableRecords(records, versions, join(dir, entry.name));
|
|
336
342
|
}
|
|
337
343
|
if (entry.name === "global.json" && descriptor.hasGlobal) {
|
|
338
|
-
const global = await readRecord(join(dir, entry.name),
|
|
344
|
+
const global = await readRecord(join(dir, entry.name), versions);
|
|
339
345
|
if (global !== void 0) state.global = global;
|
|
340
346
|
return true;
|
|
341
347
|
}
|
|
@@ -343,12 +349,20 @@ async function loadPerRecordState(descriptor, dir) {
|
|
|
343
349
|
}))).some(Boolean))) await bootstrapLegacyUnit(descriptor, dir, state);
|
|
344
350
|
return state;
|
|
345
351
|
}
|
|
352
|
+
/** The version stamps this unit reads as its own: current plus declared compatible versions. */
|
|
353
|
+
function acceptedStamps(descriptor) {
|
|
354
|
+
return [descriptor.version, ...descriptor.compatibleVersions ?? []];
|
|
355
|
+
}
|
|
346
356
|
/**
|
|
347
357
|
* Bootstrap an empty per-record tree from a legacy whole-unit file
|
|
348
358
|
* (`<root>/<name>.json`, the pre-per-record layout). Every declared-table
|
|
349
359
|
* record is copied into a current-version document, while the legacy file is
|
|
350
360
|
* retained unchanged. A missing, foreign (another unit's name), malformed,
|
|
351
|
-
* or non-unit legacy file is left alone
|
|
361
|
+
* or non-unit legacy file is left alone, and so is one whose stored unit
|
|
362
|
+
* version is outside the accepted set — migrating records the owner never
|
|
363
|
+
* vouched for would stamp them with the current version and turn a
|
|
364
|
+
* discardable stale cache into schema failures at the domain layer. Other
|
|
365
|
+
* read failures propagate.
|
|
352
366
|
* @param descriptor - Static identity and shape of the unit.
|
|
353
367
|
* @param dir - The per-record unit directory (`<root>/<name>`).
|
|
354
368
|
* @param state - The empty tree state; bootstrapped records are added.
|
|
@@ -369,6 +383,8 @@ async function bootstrapLegacyUnit(descriptor, dir, state) {
|
|
|
369
383
|
return;
|
|
370
384
|
}
|
|
371
385
|
if (document.unit?.name !== descriptor.name) return;
|
|
386
|
+
const stamped = document.unit.version;
|
|
387
|
+
if (typeof stamped !== "number" || !acceptedStamps(descriptor).includes(stamped)) return;
|
|
372
388
|
const tables = document.tables;
|
|
373
389
|
if (typeof tables !== "object" || tables === null) return;
|
|
374
390
|
const recordsByTable = tables;
|
|
@@ -391,23 +407,23 @@ async function bootstrapLegacyUnit(descriptor, dir, state) {
|
|
|
391
407
|
* @returns whether the directory contains any `.json` document path,
|
|
392
408
|
* independently of key safety, readability, or stored version.
|
|
393
409
|
*/
|
|
394
|
-
async function loadTableRecords(records,
|
|
410
|
+
async function loadTableRecords(records, versions, dir) {
|
|
395
411
|
const files = await readdir(dir, { withFileTypes: true });
|
|
396
412
|
const hasDocuments = files.some((file) => file.name.endsWith(".json"));
|
|
397
413
|
const loaded = await Promise.all(files.map(async (file) => {
|
|
398
414
|
if (!file.name.endsWith(".json")) return;
|
|
399
415
|
const key = file.name.slice(0, -5);
|
|
400
416
|
if (!SAFE_KEY_RE.test(key)) return;
|
|
401
|
-
const record = await readRecord(join(dir, file.name),
|
|
417
|
+
const record = await readRecord(join(dir, file.name), versions);
|
|
402
418
|
if (record !== void 0) return [key, record];
|
|
403
419
|
}));
|
|
404
420
|
for (const record of loaded) if (record !== void 0) records.set(...record);
|
|
405
421
|
return hasDocuments;
|
|
406
422
|
}
|
|
407
423
|
/** Read one record document; a foreign (unreadable or stale) one reads as absent. */
|
|
408
|
-
async function readRecord(path,
|
|
424
|
+
async function readRecord(path, versions) {
|
|
409
425
|
try {
|
|
410
|
-
return parseRecord(await readFile(path, "utf8"),
|
|
426
|
+
return parseRecord(await readFile(path, "utf8"), versions);
|
|
411
427
|
} catch {
|
|
412
428
|
return;
|
|
413
429
|
}
|
|
@@ -453,6 +469,21 @@ var PerRecordJsonUnit = class {
|
|
|
453
469
|
assertSafeKey(this.descriptor.name, key);
|
|
454
470
|
await this.tracked(rm(join(this.tableDir(table), `${key}.json`), { force: true }));
|
|
455
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Move one record's document aside as `<key>.json.bak.<YYYYMMDDHHmm>`. The
|
|
474
|
+
* moved file no longer ends in `.json`, so every later read ignores it; the
|
|
475
|
+
* bytes stay on disk for inspection. A same-minute backup of the same
|
|
476
|
+
* key overwrites the previous backup (the newer bytes are the ones worth
|
|
477
|
+
* keeping).
|
|
478
|
+
*/
|
|
479
|
+
async backupRecord(table, key) {
|
|
480
|
+
this.assertOpen();
|
|
481
|
+
assertSafeKey(this.descriptor.name, key);
|
|
482
|
+
const path = join(this.tableDir(table), `${key}.json`);
|
|
483
|
+
const moved = `${path}.bak.${backupStamp(/* @__PURE__ */ new Date())}`;
|
|
484
|
+
await this.tracked(rename(path, moved));
|
|
485
|
+
return moved;
|
|
486
|
+
}
|
|
456
487
|
/** Durably replace the global singleton. Only valid when declared. */
|
|
457
488
|
async setGlobal(value) {
|
|
458
489
|
this.assertOpen();
|
|
@@ -494,6 +525,11 @@ var PerRecordJsonUnit = class {
|
|
|
494
525
|
return write;
|
|
495
526
|
}
|
|
496
527
|
};
|
|
528
|
+
/** Local-time `YYYYMMDDHHmm` suffix for backed-up documents. */
|
|
529
|
+
function backupStamp(now) {
|
|
530
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
531
|
+
return `${String(now.getFullYear())}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}`;
|
|
532
|
+
}
|
|
497
533
|
/** Reject a record key that would be unsafe as a path segment. */
|
|
498
534
|
function assertSafeKey(unit, key) {
|
|
499
535
|
if (!SAFE_KEY_RE.test(key)) throw new Error(`unit '${unit}': per-record key '${key}' is not path-safe (must match ${SAFE_KEY_RE})`);
|
package/lib/types/format.d.ts
CHANGED
|
@@ -39,14 +39,16 @@ export declare function parse(text: string, descriptor: KvUnitDescriptor): UnitS
|
|
|
39
39
|
export declare function serializeRecord(version: number, value: unknown): string;
|
|
40
40
|
/**
|
|
41
41
|
* Parse one per-record document, validating its version stamp. A document
|
|
42
|
-
* that is malformed or stamped with
|
|
43
|
-
* as absent — the per-record contract: one bad or stale record file
|
|
44
|
-
* brick the whole unit, and a version bump discards stale records
|
|
45
|
-
* migrating them (the whole-unit format rejects instead, because
|
|
46
|
-
* exactly one document).
|
|
42
|
+
* that is malformed or stamped with an unaccepted version is FOREIGN and
|
|
43
|
+
* reads as absent — the per-record contract: one bad or stale record file
|
|
44
|
+
* must not brick the whole unit, and a version bump discards stale records
|
|
45
|
+
* instead of migrating them (the whole-unit format rejects instead, because
|
|
46
|
+
* there is exactly one document).
|
|
47
47
|
* @param text - Raw per-record document content.
|
|
48
|
-
* @param
|
|
48
|
+
* @param versions - Accepted unit versions (the current one plus the
|
|
49
|
+
* descriptor's compatibleVersions); any other stamp discards the
|
|
50
|
+
* document.
|
|
49
51
|
* @returns the record value, or `undefined` for a foreign document.
|
|
50
52
|
*/
|
|
51
|
-
export declare function parseRecord(text: string,
|
|
53
|
+
export declare function parseRecord(text: string, versions: readonly number[]): unknown;
|
|
52
54
|
//# sourceMappingURL=format.d.ts.map
|
|
@@ -10,15 +10,18 @@
|
|
|
10
10
|
* memory unchanged.
|
|
11
11
|
*
|
|
12
12
|
* Per-record contract: a record document that is malformed or stamped with a
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* version outside the accepted set (the descriptor's current version plus
|
|
14
|
+
* its `compatibleVersions`) reads as an absent record — one bad or stale
|
|
15
|
+
* file never bricks the whole unit, and a version bump discards stale
|
|
16
|
+
* records instead of migrating them. Record keys become path segments, so
|
|
17
|
+
* they must be path-safe (`[a-zA-Z0-9_-]+`); an unsafe key rejects at write.
|
|
17
18
|
*
|
|
18
19
|
* Legacy bootstrap: when the new tree has no document path, a legacy
|
|
19
20
|
* whole-unit file `<root>/<name>.json` (the pre-per-record layout) seeds
|
|
20
|
-
* per-record documents
|
|
21
|
-
*
|
|
21
|
+
* per-record documents, provided its stored unit version is in the accepted
|
|
22
|
+
* set — a legacy file stamped with any other version is left alone and reads
|
|
23
|
+
* as the empty unit. Any new document path, including one whose contents are
|
|
24
|
+
* unreadable or stale, suppresses the bootstrap for the whole unit. The
|
|
22
25
|
* legacy file is never changed or deleted.
|
|
23
26
|
* @module @deepseek-ai/dsh-storage-json/src/per-record-unit
|
|
24
27
|
*/
|
|
@@ -56,6 +59,14 @@ export declare class PerRecordJsonUnit implements KvUnit {
|
|
|
56
59
|
putRecord(table: string, key: string, value: unknown): Promise<void>;
|
|
57
60
|
/** Durably delete one record. Idempotent: a missing key is a no-op. */
|
|
58
61
|
deleteRecord(table: string, key: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Move one record's document aside as `<key>.json.bak.<YYYYMMDDHHmm>`. The
|
|
64
|
+
* moved file no longer ends in `.json`, so every later read ignores it; the
|
|
65
|
+
* bytes stay on disk for inspection. A same-minute backup of the same
|
|
66
|
+
* key overwrites the previous backup (the newer bytes are the ones worth
|
|
67
|
+
* keeping).
|
|
68
|
+
*/
|
|
69
|
+
backupRecord(table: string, key: string): Promise<string>;
|
|
59
70
|
/** Durably replace the global singleton. Only valid when declared. */
|
|
60
71
|
setGlobal(value: unknown): Promise<void>;
|
|
61
72
|
/** Drain in-flight writes and release the unit. Idempotent. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-storage-json",
|
|
3
3
|
"description": "JSON file KV storage backend for the DeepSeek Harness storage hub",
|
|
4
|
-
"version": "0.1.2-alpha.
|
|
4
|
+
"version": "0.1.2-alpha.5",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@deepseek-ai/dsh-storage": "^0.1.2-alpha.
|
|
30
|
+
"@deepseek-ai/dsh-storage": "^0.1.2-alpha.5",
|
|
31
31
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@deepseek-ai/dsh-storage": "^0.1.2-alpha.
|
|
37
|
+
"@deepseek-ai/dsh-storage": "^0.1.2-alpha.5",
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
39
39
|
}
|
|
40
40
|
}
|