@lmzhen/dsh-evolution-state-json 0.3.27 → 0.3.28
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 +47 -43
- package/lib/types/index.d.ts +12 -0
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -40,21 +40,47 @@ const RECORD_MAP_FILES = new Set([
|
|
|
40
40
|
"pending-state.json",
|
|
41
41
|
"pending.json"
|
|
42
42
|
]);
|
|
43
|
+
/** 0.3.17 (E-9): a malformed state file used to parse to `null` and was then
|
|
44
|
+
* OVERWRITTEN by the next save — every other session's review state / the
|
|
45
|
+
* whole pending table vanished silently. Fail loud instead: preserve the
|
|
46
|
+
* original bytes beside it and throw, so the operator can rescue and the
|
|
47
|
+
* corruption is never accepted as "empty". */
|
|
48
|
+
async function quarantine(io, root, file, raw, reason) {
|
|
49
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
50
|
+
const dest = `${join(root, file)}.corrupt-${stamp}-${Math.random().toString(36).slice(2, 6)}`;
|
|
51
|
+
await io().writeText(dest, raw).catch(() => {});
|
|
52
|
+
throw new Error(`evolution state file "${file}" is not valid JSON (${reason}); original preserved at ${dest} — inspect and fix it, then retry.`);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Cross-process JSON-file RMW (v3-audit M-8): every read-modify-write state
|
|
56
|
+
* mutation runs inside the IO backend's transact lock (via transactIo) so a
|
|
57
|
+
* second process sharing DSH_HOME cannot interleave its claim/resolve.
|
|
58
|
+
* `task` returns the next value (null = keep); for a record-map file the
|
|
59
|
+
* return must be null or a plain object map of records, and an array/scalar
|
|
60
|
+
* would be persisted as a corrupt map — so it fails loud before any write
|
|
61
|
+
* (0.3.28, V4-08). The legacy `pending.json` merge stays inside the task via
|
|
62
|
+
* `readJson` where relevant.
|
|
63
|
+
*/
|
|
64
|
+
async function jsonTransact(io, root, file, task) {
|
|
65
|
+
await transactIo(io(), join(root, file), async (current) => {
|
|
66
|
+
let parsed = null;
|
|
67
|
+
if (current !== null) {
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(current);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return await quarantine(io, root, file, current, error instanceof Error ? error.message : String(error));
|
|
72
|
+
}
|
|
73
|
+
if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(io, root, file, current, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
|
|
74
|
+
}
|
|
75
|
+
const next = await task(parsed);
|
|
76
|
+
if (next !== null && RECORD_MAP_FILES.has(file) && !isPlainRecord(next)) throw new Error(`evolution state file "${file}" task returned ${Array.isArray(next) ? "an array" : typeof next} (expected null or a plain JSON object map of records); not written.`);
|
|
77
|
+
return next === null ? null : JSON.stringify(next, null, 2);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
43
80
|
function apply(ctx, rawConfig) {
|
|
44
81
|
const root = (rawConfig.root ?? "").trim() || evolutionHome();
|
|
45
82
|
const io = () => ctx.evolutionIo.provider();
|
|
46
83
|
const pathOf = (file) => join(root, file);
|
|
47
|
-
/** 0.3.17 (E-9): a malformed state file used to parse to `null` and was then
|
|
48
|
-
* OVERWRITTEN by the next save — every other session's review state / the
|
|
49
|
-
* whole pending table vanished silently. Fail loud instead: preserve the
|
|
50
|
-
* original bytes beside it and throw, so the operator can rescue and the
|
|
51
|
-
* corruption is never accepted as "empty". */
|
|
52
|
-
async function quarantine(file, raw, reason) {
|
|
53
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
54
|
-
const dest = `${pathOf(file)}.corrupt-${stamp}-${Math.random().toString(36).slice(2, 6)}`;
|
|
55
|
-
await io().writeText(dest, raw).catch(() => {});
|
|
56
|
-
throw new Error(`evolution state file "${file}" is not valid JSON (${reason}); original preserved at ${dest} — inspect and fix it, then retry.`);
|
|
57
|
-
}
|
|
58
84
|
async function readJson(file) {
|
|
59
85
|
const raw = await io().readText(pathOf(file));
|
|
60
86
|
if (raw === null) return null;
|
|
@@ -62,33 +88,11 @@ function apply(ctx, rawConfig) {
|
|
|
62
88
|
try {
|
|
63
89
|
parsed = JSON.parse(raw);
|
|
64
90
|
} catch (error) {
|
|
65
|
-
return await quarantine(file, raw, error instanceof Error ? error.message : String(error));
|
|
91
|
+
return await quarantine(io, root, file, raw, error instanceof Error ? error.message : String(error));
|
|
66
92
|
}
|
|
67
|
-
if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(file, raw, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
|
|
93
|
+
if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(io, root, file, raw, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
|
|
68
94
|
return parsed;
|
|
69
95
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Cross-process JSON-file RMW (v3-audit M-8): every read-modify-write state
|
|
72
|
-
* mutation runs inside the IO backend's transact lock (via transactIo) so a
|
|
73
|
-
* second process sharing DSH_HOME cannot interleave its claim/resolve.
|
|
74
|
-
* `task` returns the next value (null = delete); the legacy `pending.json`
|
|
75
|
-
* merge stays inside the task via `readJson` where relevant.
|
|
76
|
-
*/
|
|
77
|
-
async function jsonTransact(file, task) {
|
|
78
|
-
await transactIo(ctx.evolutionIo.provider(), pathOf(file), async (current) => {
|
|
79
|
-
let parsed = null;
|
|
80
|
-
if (current !== null) {
|
|
81
|
-
try {
|
|
82
|
-
parsed = JSON.parse(current);
|
|
83
|
-
} catch (error) {
|
|
84
|
-
return await quarantine(file, current, error instanceof Error ? error.message : String(error));
|
|
85
|
-
}
|
|
86
|
-
if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(file, current, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
|
|
87
|
-
}
|
|
88
|
-
const next = await task(parsed);
|
|
89
|
-
return next === null ? null : JSON.stringify(next, null, 2);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
96
|
const mutate = makeSerialQueue();
|
|
93
97
|
async function loadPendingMap() {
|
|
94
98
|
const [current, legacy] = await Promise.all([readJson("pending-state.json"), readJson("pending.json")]);
|
|
@@ -160,7 +164,7 @@ function apply(ctx, rawConfig) {
|
|
|
160
164
|
},
|
|
161
165
|
async saveReviewState(sessionId, record) {
|
|
162
166
|
await mutate(async () => {
|
|
163
|
-
await jsonTransact("review-state.json", (current) => ({
|
|
167
|
+
await jsonTransact(io, root, "review-state.json", (current) => ({
|
|
164
168
|
...current ?? {},
|
|
165
169
|
[sessionId]: record
|
|
166
170
|
}));
|
|
@@ -173,7 +177,7 @@ function apply(ctx, rawConfig) {
|
|
|
173
177
|
},
|
|
174
178
|
async saveCuratorState(record) {
|
|
175
179
|
await mutate(async () => {
|
|
176
|
-
await jsonTransact("curator-state.json", (current) => ({
|
|
180
|
+
await jsonTransact(io, root, "curator-state.json", (current) => ({
|
|
177
181
|
...current ?? {},
|
|
178
182
|
primary: record
|
|
179
183
|
}));
|
|
@@ -181,7 +185,7 @@ function apply(ctx, rawConfig) {
|
|
|
181
185
|
},
|
|
182
186
|
async transactCuratorState(task) {
|
|
183
187
|
await mutate(async () => {
|
|
184
|
-
await jsonTransact("curator-state.json", (current) => {
|
|
188
|
+
await jsonTransact(io, root, "curator-state.json", (current) => {
|
|
185
189
|
const next = task(current?.primary ?? null);
|
|
186
190
|
if (next === null) return current;
|
|
187
191
|
return {
|
|
@@ -199,7 +203,7 @@ function apply(ctx, rawConfig) {
|
|
|
199
203
|
},
|
|
200
204
|
async savePending(record) {
|
|
201
205
|
await mutate(async () => {
|
|
202
|
-
await jsonTransact("pending-state.json", async (current) => {
|
|
206
|
+
await jsonTransact(io, root, "pending-state.json", async (current) => {
|
|
203
207
|
return {
|
|
204
208
|
...await readJson("pending.json") ?? {},
|
|
205
209
|
...current ?? {},
|
|
@@ -211,7 +215,7 @@ function apply(ctx, rawConfig) {
|
|
|
211
215
|
async claimPending(id, claimId) {
|
|
212
216
|
return await mutate(async () => {
|
|
213
217
|
const slot = { claimed: null };
|
|
214
|
-
await jsonTransact("pending-state.json", async (current) => {
|
|
218
|
+
await jsonTransact(io, root, "pending-state.json", async (current) => {
|
|
215
219
|
const map = {
|
|
216
220
|
...await readJson("pending.json") ?? {},
|
|
217
221
|
...current ?? {}
|
|
@@ -233,7 +237,7 @@ function apply(ctx, rawConfig) {
|
|
|
233
237
|
},
|
|
234
238
|
async releasePendingClaim(id, claimId) {
|
|
235
239
|
await mutate(async () => {
|
|
236
|
-
await jsonTransact("pending-state.json", async (current) => {
|
|
240
|
+
await jsonTransact(io, root, "pending-state.json", async (current) => {
|
|
237
241
|
const map = {
|
|
238
242
|
...await readJson("pending.json") ?? {},
|
|
239
243
|
...current ?? {}
|
|
@@ -254,7 +258,7 @@ function apply(ctx, rawConfig) {
|
|
|
254
258
|
applied: false
|
|
255
259
|
};
|
|
256
260
|
let evicted = [];
|
|
257
|
-
await jsonTransact("pending-state.json", async (current) => {
|
|
261
|
+
await jsonTransact(io, root, "pending-state.json", async (current) => {
|
|
258
262
|
const map = {
|
|
259
263
|
...await readJson("pending.json") ?? {},
|
|
260
264
|
...current ?? {}
|
|
@@ -289,4 +293,4 @@ function apply(ctx, rawConfig) {
|
|
|
289
293
|
ctx.effect(() => ctx.evolutionStateStorage.registerProvider(provider), "evolution-state-json.provider");
|
|
290
294
|
}
|
|
291
295
|
//#endregion
|
|
292
|
-
export { Config, apply, inject, name };
|
|
296
|
+
export { Config, apply, inject, jsonTransact, name };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -7,11 +7,23 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis';
|
|
9
9
|
import z from '@deepseek-ai/schemastery';
|
|
10
|
+
import { type EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
10
11
|
export declare const name = "evolution-state-json";
|
|
11
12
|
export declare const inject: string[];
|
|
12
13
|
export interface Config {
|
|
13
14
|
root?: string;
|
|
14
15
|
}
|
|
15
16
|
export declare const Config: z<Config>;
|
|
17
|
+
/**
|
|
18
|
+
* Cross-process JSON-file RMW (v3-audit M-8): every read-modify-write state
|
|
19
|
+
* mutation runs inside the IO backend's transact lock (via transactIo) so a
|
|
20
|
+
* second process sharing DSH_HOME cannot interleave its claim/resolve.
|
|
21
|
+
* `task` returns the next value (null = keep); for a record-map file the
|
|
22
|
+
* return must be null or a plain object map of records, and an array/scalar
|
|
23
|
+
* would be persisted as a corrupt map — so it fails loud before any write
|
|
24
|
+
* (0.3.28, V4-08). The legacy `pending.json` merge stays inside the task via
|
|
25
|
+
* `readJson` where relevant.
|
|
26
|
+
*/
|
|
27
|
+
export declare function jsonTransact<T>(io: () => EvolutionIoLike, root: string, file: string, task: (current: T | null) => T | null | Promise<T | null>): Promise<void>;
|
|
16
28
|
export declare function apply(ctx: Context, rawConfig: Config): void;
|
|
17
29
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-state-json",
|
|
3
3
|
"description": "JSON-file evolution state provider over the IO seam (community build)",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.28",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -31,17 +31,17 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
34
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
34
|
+
"@lmzhen/dsh-evolution-core": "^0.3.28"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
39
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
40
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.3.
|
|
39
|
+
"@lmzhen/dsh-evolution-io": "^0.3.28",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.28"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
44
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
45
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.3.
|
|
44
|
+
"@lmzhen/dsh-evolution-io": "^0.3.28",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.28"
|
|
46
46
|
}
|
|
47
47
|
}
|