@lmzhen/dsh-evolution-state-json 0.3.25 → 0.3.27
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 +37 -11
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -18,6 +18,16 @@ const Config = z.object({ root: z.string().default("") });
|
|
|
18
18
|
* bound; the oldest over the cap are archived (made package-private so the
|
|
19
19
|
* archive sidecar and the provider enforce one number). */
|
|
20
20
|
const PENDING_RESOLVED_CAP = 200;
|
|
21
|
+
/** 0.3.27 (V4-01): the audit sidecar (pending-state-archive.json) is bounded
|
|
22
|
+
* at this many resolved records. Past it the oldest history rotates to a
|
|
23
|
+
* `.bak` sidecar, so the file — and the full-array rewrite on every append —
|
|
24
|
+
* never grows without bound. */
|
|
25
|
+
const ARCHIVE_RESOLVED_CAP = 5e3;
|
|
26
|
+
/** 0.3.27 (V4-01): an archive entry's dedupe identity. The same audit record
|
|
27
|
+
* (id + status + resolvedAt) must never appear twice; the read-only legacy
|
|
28
|
+
* `pending.json` merge used to re-introduce an evicted record on the next
|
|
29
|
+
* resolve and archive it again, growing the sidecar without bound. */
|
|
30
|
+
const pendingArchiveKey = (record) => `${record.id}\u0000${record.status}\u0000${record.resolvedAt ?? ""}`;
|
|
21
31
|
/** 0.3.22 (F-215): a record-map state file must parse to a non-null plain
|
|
22
32
|
* object (a map of records) — valid JSON that is `null`/array/scalar is a
|
|
23
33
|
* corrupt map that used to read as "empty" and was silently overwritten by
|
|
@@ -31,7 +41,7 @@ const RECORD_MAP_FILES = new Set([
|
|
|
31
41
|
"pending.json"
|
|
32
42
|
]);
|
|
33
43
|
function apply(ctx, rawConfig) {
|
|
34
|
-
const root = rawConfig.root || evolutionHome();
|
|
44
|
+
const root = (rawConfig.root ?? "").trim() || evolutionHome();
|
|
35
45
|
const io = () => ctx.evolutionIo.provider();
|
|
36
46
|
const pathOf = (file) => join(root, file);
|
|
37
47
|
/** 0.3.17 (E-9): a malformed state file used to parse to `null` and was then
|
|
@@ -48,13 +58,14 @@ function apply(ctx, rawConfig) {
|
|
|
48
58
|
async function readJson(file) {
|
|
49
59
|
const raw = await io().readText(pathOf(file));
|
|
50
60
|
if (raw === null) return null;
|
|
61
|
+
let parsed;
|
|
51
62
|
try {
|
|
52
|
-
|
|
53
|
-
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}`);
|
|
54
|
-
return parsed;
|
|
63
|
+
parsed = JSON.parse(raw);
|
|
55
64
|
} catch (error) {
|
|
56
65
|
return await quarantine(file, raw, error instanceof Error ? error.message : String(error));
|
|
57
66
|
}
|
|
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}`);
|
|
68
|
+
return parsed;
|
|
58
69
|
}
|
|
59
70
|
/**
|
|
60
71
|
* Cross-process JSON-file RMW (v3-audit M-8): every read-modify-write state
|
|
@@ -66,11 +77,13 @@ function apply(ctx, rawConfig) {
|
|
|
66
77
|
async function jsonTransact(file, task) {
|
|
67
78
|
await transactIo(ctx.evolutionIo.provider(), pathOf(file), async (current) => {
|
|
68
79
|
let parsed = null;
|
|
69
|
-
if (current !== null)
|
|
70
|
-
|
|
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
|
+
}
|
|
71
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}`);
|
|
72
|
-
} catch (error) {
|
|
73
|
-
return await quarantine(file, current, error instanceof Error ? error.message : String(error));
|
|
74
87
|
}
|
|
75
88
|
const next = await task(parsed);
|
|
76
89
|
return next === null ? null : JSON.stringify(next, null, 2);
|
|
@@ -112,16 +125,29 @@ function apply(ctx, rawConfig) {
|
|
|
112
125
|
* (top-level array, oldest-first). This is a best-effort audit aid: a
|
|
113
126
|
* corrupt/unreadable archive is skipped and an archive write failure must
|
|
114
127
|
* NEVER fail the resolve that triggered it — the live map is already
|
|
115
|
-
* trimmed, so the audit copy is allowed to fall behind.
|
|
128
|
+
* trimmed, so the audit copy is allowed to fall behind.
|
|
129
|
+
* 0.3.27 (V4-01): dedupe by id+status+resolvedAt before appending (the
|
|
130
|
+
* read-only legacy `pending.json` re-introduces an evicted record on the
|
|
131
|
+
* next resolve) and rotate the sidecar to `.bak` past ARCHIVE_RESOLVED_CAP
|
|
132
|
+
* so neither the file nor the per-append full-array rewrite grows without
|
|
133
|
+
* bound. */
|
|
116
134
|
async function appendArchive(records) {
|
|
117
135
|
try {
|
|
118
|
-
await transactIo(io(), pathOf("pending-state-archive.json"), (current) => {
|
|
136
|
+
await transactIo(io(), pathOf("pending-state-archive.json"), async (current) => {
|
|
119
137
|
let archive = [];
|
|
120
138
|
if (current !== null) try {
|
|
121
139
|
const parsed = JSON.parse(current);
|
|
122
140
|
if (Array.isArray(parsed)) archive = parsed;
|
|
123
141
|
} catch {}
|
|
124
|
-
|
|
142
|
+
const seen = new Set(archive.map(pendingArchiveKey));
|
|
143
|
+
const fresh = records.filter((record) => !seen.has(pendingArchiveKey(record)));
|
|
144
|
+
if (fresh.length === 0) return current;
|
|
145
|
+
const next = [...archive, ...fresh];
|
|
146
|
+
if (next.length > ARCHIVE_RESOLVED_CAP) {
|
|
147
|
+
await io().writeText(pathOf("pending-state-archive.json.bak"), JSON.stringify(archive, null, 2)).catch(() => {});
|
|
148
|
+
return JSON.stringify(fresh, null, 2);
|
|
149
|
+
}
|
|
150
|
+
return JSON.stringify(next, null, 2);
|
|
125
151
|
});
|
|
126
152
|
} catch {}
|
|
127
153
|
}
|
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.27",
|
|
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.27"
|
|
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.27",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.27"
|
|
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.27",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.27"
|
|
46
46
|
}
|
|
47
47
|
}
|