@lmzhen/dsh-evolution-state-json 0.17.1 → 0.18.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/lib/index.js +72 -58
- package/lib/types/index.d.ts +40 -1
- package/package.json +8 -8
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { evolutionHome, makeSerialQueue, transactIo, transactTaskGuard } from "@lmzhen/dsh-evolution-core";
|
|
2
|
+
import { evolutionHome, isAbsent, isPresent, isUnknown, makeSerialQueue, probeText, transactIo, transactTaskGuard } from "@lmzhen/dsh-evolution-core";
|
|
3
3
|
import { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_TABLE, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_SESSION_CAP, REVIEW_STATE_TABLE, assertCloneable, canClaimPending, canResolvePending, cloneRecord, recordIssue, releasedStatus, selectPendingOverflow, selectSessionOverflow } from "@lmzhen/dsh-evolution-state-storage";
|
|
4
4
|
import { basename, dirname, isAbsolute, join } from "node:path";
|
|
5
5
|
//#region lib/types/index.js
|
|
@@ -68,11 +68,16 @@ const QUARANTINE_ERROR_NAME = "EvolutionStateCorruptFile";
|
|
|
68
68
|
* DIFFERENT payload gets a stamped sibling so the earlier rescue copy is never
|
|
69
69
|
* overwritten. The node backend's 7-day `.corrupt` sweep bounds the set. */
|
|
70
70
|
async function quarantineTarget(io, base, content) {
|
|
71
|
-
|
|
71
|
+
const baseProbe = await probeText(io(), base);
|
|
72
|
+
if (isUnknown(baseProbe)) return {
|
|
73
|
+
dest: `${base}.${Date.now()}`,
|
|
74
|
+
needsWrite: true
|
|
75
|
+
};
|
|
76
|
+
if (isAbsent(baseProbe)) return {
|
|
72
77
|
dest: base,
|
|
73
78
|
needsWrite: true
|
|
74
79
|
};
|
|
75
|
-
if (
|
|
80
|
+
if (baseProbe.value === content) return {
|
|
76
81
|
dest: base,
|
|
77
82
|
needsWrite: false
|
|
78
83
|
};
|
|
@@ -83,7 +88,8 @@ async function quarantineTarget(io, base, content) {
|
|
|
83
88
|
if (!name.startsWith(stem)) continue;
|
|
84
89
|
if (!/^\d+$/.test(name.slice(stem.length))) continue;
|
|
85
90
|
const sibling = join(dir, name);
|
|
86
|
-
|
|
91
|
+
const siblingProbe = await probeText(io(), sibling);
|
|
92
|
+
if (isPresent(siblingProbe) && siblingProbe.value === content) return {
|
|
87
93
|
dest: sibling,
|
|
88
94
|
needsWrite: false
|
|
89
95
|
};
|
|
@@ -94,16 +100,16 @@ async function quarantineTarget(io, base, content) {
|
|
|
94
100
|
needsWrite: true
|
|
95
101
|
};
|
|
96
102
|
}
|
|
97
|
-
async function quarantine(io, root, file, raw, reason) {
|
|
103
|
+
async function quarantine(io, root, file, raw, reason, memory) {
|
|
98
104
|
const { dest, needsWrite } = await quarantineTarget(io, `${join(root, file)}.corrupt`, raw);
|
|
99
105
|
let preservedNote = `; original preserved at ${dest} — inspect and fix it, then retry.`;
|
|
100
106
|
if (needsWrite) try {
|
|
101
107
|
await io().writeText(dest, raw);
|
|
102
|
-
|
|
108
|
+
memory.written.delete(file);
|
|
103
109
|
} catch (writeError) {
|
|
104
110
|
preservedNote = `; the quarantine copy at ${dest} FAILED to write (${writeError instanceof Error ? writeError.message : String(writeError)}) — the original file is left in place.`;
|
|
105
111
|
}
|
|
106
|
-
else
|
|
112
|
+
else memory.written.delete(file);
|
|
107
113
|
throw Object.assign(/* @__PURE__ */ new Error(`evolution state file "${file}" is not valid JSON (${reason})${preservedNote}`), { name: QUARANTINE_ERROR_NAME });
|
|
108
114
|
}
|
|
109
115
|
/** S-05: the V8-16 record-shape gate existed as two verbatim ~15-line
|
|
@@ -132,22 +138,14 @@ function gateScan(file, parsed) {
|
|
|
132
138
|
if (gate === void 0 || !isPlainRecord(parsed)) return [];
|
|
133
139
|
return Object.entries(parsed).filter(([, record]) => !isPlainRecord(record) || !gate(record));
|
|
134
140
|
}
|
|
135
|
-
/**
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
* any write (0.3.28, V4-08). The legacy `pending.json` merge stays inside the
|
|
144
|
-
* task via `readJson` where relevant.
|
|
145
|
-
* @internal Exported only for this package's own tests — not public API
|
|
146
|
-
* surface (audit v10 S-03); other packages must go through the provider seam.
|
|
147
|
-
*/
|
|
148
|
-
const recordGateWarned = /* @__PURE__ */ new Set();
|
|
149
|
-
const corruptWritten = /* @__PURE__ */ new Map();
|
|
150
|
-
const corruptWriteWarned = /* @__PURE__ */ new Set();
|
|
141
|
+
/** One provider instance's quarantine memory. */
|
|
142
|
+
function newQuarantineMemory() {
|
|
143
|
+
return {
|
|
144
|
+
warned: /* @__PURE__ */ new Set(),
|
|
145
|
+
written: /* @__PURE__ */ new Map(),
|
|
146
|
+
writeWarned: /* @__PURE__ */ new Set()
|
|
147
|
+
};
|
|
148
|
+
}
|
|
151
149
|
/** P3 (v15): name carried by the two fail-loud write gates in `jsonTransact`
|
|
152
150
|
* (task-return shape, write-back field gate) so best-effort wrappers —
|
|
153
151
|
* `retireLegacyOnce` is the one that matters — rethrow instead of deferring. */
|
|
@@ -157,26 +155,26 @@ function writeGateError(message) {
|
|
|
157
155
|
error.name = WRITE_GATE_ERROR_NAME;
|
|
158
156
|
return error;
|
|
159
157
|
}
|
|
160
|
-
function reportGateViolation(ctx, file, failing) {
|
|
161
|
-
if (
|
|
162
|
-
|
|
158
|
+
function reportGateViolation(ctx, file, failing, memory) {
|
|
159
|
+
if (memory.warned.has(file)) return;
|
|
160
|
+
memory.warned.add(file);
|
|
163
161
|
ctx.logger.warn(`evolution-state-json: ${failing.length} record(s) in "${file}" failed the record schema gate and were quarantined to "${file}.corrupt": ${failing.map(([id]) => id).join(", ")}`);
|
|
164
162
|
}
|
|
165
|
-
async function ensureCorruptCopy(ctx, io, root, file, bad) {
|
|
163
|
+
async function ensureCorruptCopy(ctx, io, root, file, bad, memory) {
|
|
166
164
|
const base = `${join(root, file)}.corrupt`;
|
|
167
165
|
const corruptKey = JSON.stringify(Object.entries(bad).map(([id, record]) => ({
|
|
168
166
|
id,
|
|
169
167
|
fields: typeof record === "object" && record !== null ? Object.entries(record).map(([field, value]) => `${field}:${Array.isArray(value) ? "array" : typeof value}`).sort() : [typeof record]
|
|
170
168
|
})).sort((a, b) => a.id.localeCompare(b.id)));
|
|
171
|
-
if (
|
|
169
|
+
if (memory.written.get(file) === corruptKey && await io().exists(base)) return true;
|
|
172
170
|
const payload = JSON.stringify(bad, null, 2);
|
|
173
171
|
const { dest, needsWrite } = await quarantineTarget(io, base, payload);
|
|
174
172
|
const wrote = needsWrite ? await io().writeText(dest, payload).then(() => true).catch(() => false) : true;
|
|
175
173
|
if (wrote) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
} else if (!
|
|
179
|
-
|
|
174
|
+
memory.written.set(file, corruptKey);
|
|
175
|
+
memory.writeWarned.delete(file);
|
|
176
|
+
} else if (!memory.writeWarned.has(file)) {
|
|
177
|
+
memory.writeWarned.add(file);
|
|
180
178
|
ctx.logger.warn(`evolution-state-json: could not write quarantine copy "${dest}" for ${Object.keys(bad).length} failed record(s) — the main file keeps them; the copy is retried on the next access`);
|
|
181
179
|
}
|
|
182
180
|
return wrote;
|
|
@@ -188,26 +186,26 @@ async function jsonTransact(ctx, io, root, file, task, options) {
|
|
|
188
186
|
try {
|
|
189
187
|
parsed = JSON.parse(current);
|
|
190
188
|
} catch (error) {
|
|
191
|
-
return await quarantine(io, root, file, current, error instanceof Error ? error.message : String(error));
|
|
189
|
+
return await quarantine(io, root, file, current, error instanceof Error ? error.message : String(error), options.memory);
|
|
192
190
|
}
|
|
193
|
-
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}
|
|
191
|
+
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}`, options.memory);
|
|
194
192
|
const malformed = firstNonRecordValue(parsed);
|
|
195
|
-
if (malformed !== null) return await quarantine(io, root, file, current, malformed);
|
|
193
|
+
if (malformed !== null) return await quarantine(io, root, file, current, malformed, options.memory);
|
|
196
194
|
const failing = gateScan(file, parsed);
|
|
197
195
|
if (failing.length > 0) {
|
|
198
196
|
const bad = {};
|
|
199
197
|
const good = {};
|
|
200
198
|
for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
|
|
201
199
|
else good[id] = record;
|
|
202
|
-
reportGateViolation(ctx, file, failing);
|
|
203
|
-
if (await ensureCorruptCopy(ctx, io, root, file, bad)) {
|
|
200
|
+
reportGateViolation(ctx, file, failing, options.memory);
|
|
201
|
+
if (await ensureCorruptCopy(ctx, io, root, file, bad, options.memory)) {
|
|
204
202
|
parsed = good;
|
|
205
|
-
options
|
|
203
|
+
options.onGateDrop?.(file, failing.map(([id]) => id));
|
|
206
204
|
} else {
|
|
207
|
-
options
|
|
205
|
+
options.onGateDrop?.(file, []);
|
|
208
206
|
ctx.logger.warn(`evolution-state-json: keeping ${failing.length} malformed record(s) in ${file} — the quarantine copy could not be written, so rewriting the file without them would destroy the only copy`);
|
|
209
207
|
}
|
|
210
|
-
} else options
|
|
208
|
+
} else options.onGateDrop?.(file, []);
|
|
211
209
|
}
|
|
212
210
|
const next = await task(parsed);
|
|
213
211
|
if (next !== null && RECORD_MAP_FILES.has(file) && !isPlainRecord(next)) throw writeGateError(`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.`);
|
|
@@ -223,6 +221,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
223
221
|
if (rawConfig.root !== void 0 && rawConfig.root.trim() !== "" && !isAbsolute(rawConfig.root)) ctx.logger.warn(`evolution-state-json: config.root "${rawConfig.root}" is a relative path and resolves against the process CWD ("${root}") — pass an absolute path to make the store location launch-independent`);
|
|
224
222
|
const io = () => ctx.evolutionIo.provider();
|
|
225
223
|
const pathOf = (file) => join(root, file);
|
|
224
|
+
const quarantineMemory = newQuarantineMemory();
|
|
226
225
|
const gateDroppedCurrent = /* @__PURE__ */ new Set();
|
|
227
226
|
const noteGateDrop = (file, ids) => {
|
|
228
227
|
if (file !== PENDING_STATE_FILE) return;
|
|
@@ -242,7 +241,16 @@ function apply(ctx, rawConfig = {}) {
|
|
|
242
241
|
* @param task - the read-modify-write body, exactly as jsonTransact takes it.
|
|
243
242
|
* @returns jsonTransact's own result.
|
|
244
243
|
*/
|
|
245
|
-
const pendingTransact = (task
|
|
244
|
+
const pendingTransact = (task, what = "the pending table") => {
|
|
245
|
+
const guard = transactTaskGuard(`pending table write for ${what}`);
|
|
246
|
+
const pendingWrite = {
|
|
247
|
+
onGateDrop: noteGateDrop,
|
|
248
|
+
memory: quarantineMemory
|
|
249
|
+
};
|
|
250
|
+
return jsonTransact(ctx, io, root, PENDING_STATE_FILE, guard.wrap(task), pendingWrite).then(() => {
|
|
251
|
+
guard.assertInvoked();
|
|
252
|
+
});
|
|
253
|
+
};
|
|
246
254
|
let warnedPendingCapacity = false;
|
|
247
255
|
let warnedPendingArgsBytes = false;
|
|
248
256
|
async function readJson(file) {
|
|
@@ -252,19 +260,19 @@ function apply(ctx, rawConfig = {}) {
|
|
|
252
260
|
try {
|
|
253
261
|
parsed = JSON.parse(raw);
|
|
254
262
|
} catch (error) {
|
|
255
|
-
return await quarantine(io, root, file, raw, error instanceof Error ? error.message : String(error));
|
|
263
|
+
return await quarantine(io, root, file, raw, error instanceof Error ? error.message : String(error), quarantineMemory);
|
|
256
264
|
}
|
|
257
|
-
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}
|
|
265
|
+
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}`, quarantineMemory);
|
|
258
266
|
const malformed = firstNonRecordValue(parsed);
|
|
259
|
-
if (malformed !== null) return await quarantine(io, root, file, raw, malformed);
|
|
267
|
+
if (malformed !== null) return await quarantine(io, root, file, raw, malformed, quarantineMemory);
|
|
260
268
|
const failing = gateScan(file, parsed);
|
|
261
269
|
if (failing.length > 0) {
|
|
262
270
|
const bad = {};
|
|
263
271
|
const good = {};
|
|
264
272
|
for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
|
|
265
273
|
else good[id] = record;
|
|
266
|
-
reportGateViolation(ctx, file, failing);
|
|
267
|
-
await ensureCorruptCopy(ctx, io, root, file, bad);
|
|
274
|
+
reportGateViolation(ctx, file, failing, quarantineMemory);
|
|
275
|
+
await ensureCorruptCopy(ctx, io, root, file, bad, quarantineMemory);
|
|
268
276
|
if (file === PENDING_STATE_FILE) {
|
|
269
277
|
gateDroppedCurrent.clear();
|
|
270
278
|
for (const id of Object.keys(bad)) gateDroppedCurrent.add(id);
|
|
@@ -362,6 +370,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
362
370
|
async function retireLegacyOnce(legacy) {
|
|
363
371
|
if (legacyMigrated) return {};
|
|
364
372
|
const keyedLegacy = keyPendingById(legacy);
|
|
373
|
+
let committed = null;
|
|
365
374
|
try {
|
|
366
375
|
const retired = {};
|
|
367
376
|
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (fresh) => {
|
|
@@ -374,15 +383,20 @@ function apply(ctx, rawConfig = {}) {
|
|
|
374
383
|
merged[id] = record;
|
|
375
384
|
retired[id] = record;
|
|
376
385
|
}
|
|
386
|
+
committed = merged;
|
|
377
387
|
return merged;
|
|
378
|
-
}, {
|
|
388
|
+
}, {
|
|
389
|
+
onGateDrop: noteGateDrop,
|
|
390
|
+
memory: quarantineMemory
|
|
391
|
+
});
|
|
379
392
|
await io().rename(pathOf(PENDING_LEGACY_FILE), `${pathOf(PENDING_LEGACY_FILE)}.migrated`);
|
|
380
393
|
legacyMigrated = true;
|
|
381
394
|
return retired;
|
|
382
395
|
} catch (error) {
|
|
383
396
|
if (error instanceof Error && (error.name === QUARANTINE_ERROR_NAME || error.name === WRITE_GATE_ERROR_NAME)) throw error;
|
|
384
397
|
ctx.logger.warn(`evolution-state-json: legacy pending retirement deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
385
|
-
return {};
|
|
398
|
+
if (committed === null) return {};
|
|
399
|
+
return committed;
|
|
386
400
|
}
|
|
387
401
|
}
|
|
388
402
|
async function loadPendingMap() {
|
|
@@ -449,9 +463,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
449
463
|
* revive a ghost twin. A dedupe no-op counts as success — the records are
|
|
450
464
|
* already in the archive. */
|
|
451
465
|
async function appendArchive(records) {
|
|
466
|
+
const guard = transactTaskGuard(`pending archive (${PENDING_ARCHIVE_FILE})`);
|
|
452
467
|
let landed = true;
|
|
453
468
|
try {
|
|
454
|
-
await transactIo(io(), pathOf(PENDING_ARCHIVE_FILE), async (current) => {
|
|
469
|
+
await transactIo(io(), pathOf(PENDING_ARCHIVE_FILE), guard.wrap(async (current) => {
|
|
455
470
|
let archive = [];
|
|
456
471
|
if (current !== null) {
|
|
457
472
|
let parsed;
|
|
@@ -488,7 +503,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
488
503
|
return JSON.stringify((fresh.length > 0 ? fresh : next).slice(-5e3));
|
|
489
504
|
}
|
|
490
505
|
return JSON.stringify(next);
|
|
491
|
-
});
|
|
506
|
+
}));
|
|
507
|
+
if (!guard.invoked()) landed = false;
|
|
492
508
|
return landed;
|
|
493
509
|
} catch (error) {
|
|
494
510
|
ctx.logger.warn(`evolution-state-json: archive append deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -549,7 +565,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
549
565
|
const pruned = {};
|
|
550
566
|
for (const [id, row] of Object.entries(stamped)) if (!evict.has(id)) pruned[id] = row;
|
|
551
567
|
return pruned;
|
|
552
|
-
}));
|
|
568
|
+
}), { memory: quarantineMemory });
|
|
553
569
|
guard.assertInvoked();
|
|
554
570
|
});
|
|
555
571
|
},
|
|
@@ -564,7 +580,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
564
580
|
await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, guard.wrap((current) => ({
|
|
565
581
|
...current ?? {},
|
|
566
582
|
[CURATOR_STATE_KEY]: record
|
|
567
|
-
})));
|
|
583
|
+
})), { memory: quarantineMemory });
|
|
568
584
|
guard.assertInvoked();
|
|
569
585
|
});
|
|
570
586
|
},
|
|
@@ -579,7 +595,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
579
595
|
...current ?? {},
|
|
580
596
|
[CURATOR_STATE_KEY]: next
|
|
581
597
|
};
|
|
582
|
-
}));
|
|
598
|
+
}), { memory: quarantineMemory });
|
|
583
599
|
guard.assertInvoked();
|
|
584
600
|
});
|
|
585
601
|
},
|
|
@@ -591,16 +607,14 @@ function apply(ctx, rawConfig = {}) {
|
|
|
591
607
|
},
|
|
592
608
|
async savePending(record) {
|
|
593
609
|
await mutate(async () => {
|
|
594
|
-
|
|
595
|
-
await pendingTransact(guard.wrap(async (current) => {
|
|
610
|
+
await pendingTransact(async (current) => {
|
|
596
611
|
const map = {
|
|
597
612
|
...await mergedWithFilteredLegacy(legacyMigrated ? null : await readLegacyPending(), current ?? {}),
|
|
598
613
|
[record.id]: record
|
|
599
614
|
};
|
|
600
615
|
warnPendingGrowth(map, "save");
|
|
601
616
|
return map;
|
|
602
|
-
}));
|
|
603
|
-
guard.assertInvoked();
|
|
617
|
+
}, `record "${record.id}" (${PENDING_STATE_FILE})`);
|
|
604
618
|
});
|
|
605
619
|
},
|
|
606
620
|
async claimPending(id, claimId) {
|
|
@@ -692,4 +706,4 @@ function apply(ctx, rawConfig = {}) {
|
|
|
692
706
|
ctx.effect(() => ctx.evolutionStateStorage.registerProvider(provider), "evolution-state-json.provider");
|
|
693
707
|
}
|
|
694
708
|
//#endregion
|
|
695
|
-
export { Config, apply, inject, jsonTransact, name };
|
|
709
|
+
export { Config, apply, inject, jsonTransact, name, newQuarantineMemory };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -14,8 +14,47 @@ export interface Config {
|
|
|
14
14
|
root?: string;
|
|
15
15
|
}
|
|
16
16
|
export declare const Config: z<Config>;
|
|
17
|
-
|
|
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 = ENSURE-ABSENT (the file is deleted
|
|
22
|
+
* when it exists — the parity of the transact seam, V6-32). For a record-map
|
|
23
|
+
* file the return must be null or a plain object map of records, and an
|
|
24
|
+
* array/scalar would be persisted as a corrupt map — so it fails loud before
|
|
25
|
+
* any write (0.3.28, V4-08). The legacy `pending.json` merge stays inside the
|
|
26
|
+
* task via `readJson` where relevant.
|
|
27
|
+
* @internal Exported only for this package's own tests — not public API
|
|
28
|
+
* surface (audit v10 S-03); other packages must go through the provider seam.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The quarantine bookkeeping ONE provider instance owns (N12/O-6, S4.4).
|
|
32
|
+
*
|
|
33
|
+
* These three used to be module-scope ("`jsonTransact` is a module function that cannot reach the
|
|
34
|
+
* apply-scoped closure"), which made warn/rewrite dedup span instances AND roots keyed by file NAME
|
|
35
|
+
* only: two providers on different roots deduped each other's quarantine writes. Threading the memory
|
|
36
|
+
* through the one module function that needs it removes that limit — the dedup is now exactly "this
|
|
37
|
+
* instance, this file", which is what the three fields always described.
|
|
38
|
+
*/
|
|
39
|
+
export interface QuarantineMemory {
|
|
40
|
+
/** V10-04 (P2-19): the record-gate warn fires once per file per instance — a permanently bad
|
|
41
|
+
* record would otherwise warn on every turn's read. */
|
|
42
|
+
readonly warned: Set<string>;
|
|
43
|
+
/** P2-24 (v11): the last `.corrupt` rewrite key per file (sorted failing id set) — skip
|
|
44
|
+
* rewriting when nothing changed, so a permanently bad record never re-atomic-writes the copy on
|
|
45
|
+
* every read. */
|
|
46
|
+
readonly written: Map<string, string>;
|
|
47
|
+
/** P3-12 (v14): the "could not write the quarantine copy" warn fires once per file; the key is set
|
|
48
|
+
* ONLY after a successful write (a failed write is retried, not marked done). */
|
|
49
|
+
readonly writeWarned: Set<string>;
|
|
50
|
+
}
|
|
51
|
+
/** One provider instance's quarantine memory. */
|
|
52
|
+
export declare function newQuarantineMemory(): QuarantineMemory;
|
|
53
|
+
export declare function jsonTransact<T>(ctx: Context, io: () => EvolutionIoLike, root: string, file: string, task: (current: T | null) => T | null | Promise<T | null>, options: {
|
|
18
54
|
onGateDrop?: (file: string, ids: string[]) => void;
|
|
55
|
+
/** N12/O-6 (S4.4): the calling INSTANCE's quarantine memory — one per provider, never module
|
|
56
|
+
* scope (that keyed the dedup by bare file name across every root in the process). */
|
|
57
|
+
memory: QuarantineMemory;
|
|
19
58
|
}): Promise<void>;
|
|
20
59
|
export declare function apply(ctx: Context, rawConfig?: Config): void;
|
|
21
60
|
//# 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.
|
|
4
|
+
"version": "0.18.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -26,17 +26,17 @@
|
|
|
26
26
|
],
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
30
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
29
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
30
|
+
"@lmzhen/dsh-evolution-core": "^0.18.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
34
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
35
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.
|
|
34
|
+
"@lmzhen/dsh-evolution-io": "^0.18.0",
|
|
35
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.18.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@lmzhen/dsh-evolution-io": "^0.
|
|
39
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.
|
|
40
|
-
"@lmzhen/dsh-evolution-io-node": "^0.
|
|
38
|
+
"@lmzhen/dsh-evolution-io": "^0.18.0",
|
|
39
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.18.0",
|
|
40
|
+
"@lmzhen/dsh-evolution-io-node": "^0.18.0"
|
|
41
41
|
}
|
|
42
42
|
}
|