dsh-lite-memory 0.16.2 → 0.17.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 +9 -9
- package/TRIGGERS.md +14 -13
- package/anchor.js +259 -24
- package/client.js +1211 -206
- package/docs/CONVENTIONS.md +143 -0
- package/docs/RELEASE-0.17.0.md +8 -0
- package/index.js +337 -95
- package/learning.js +456 -48
- package/package.json +5 -2
- package/state.js +110 -5
- package/store.js +447 -61
- package/test/anchor.test.mjs +240 -0
- package/test/client.test.mjs +176 -6
- package/test/contract.test.mjs +9 -1
- package/test/learning.test.mjs +484 -2
- package/test/palace.test.mjs +102 -0
- package/test/runtime-context.test.mjs +186 -0
- package/test/state.test.mjs +121 -2
- package/test/store.test.mjs +454 -15
- package/view.js +60 -3
package/state.js
CHANGED
|
@@ -260,7 +260,7 @@ function gitRoot(cwd) {
|
|
|
260
260
|
return { root: '', reason: 'failed' };
|
|
261
261
|
} catch (error) {
|
|
262
262
|
const text = `${error && error.stderr ? error.stderr : ''}${error && error.message ? error.message : ''}`;
|
|
263
|
-
if (/not a git repository
|
|
263
|
+
if (/not a git repository|不是[\s\S]*git/i.test(text)) return { root: '', reason: 'not_git' };
|
|
264
264
|
return { root: '', reason: 'failed' };
|
|
265
265
|
}
|
|
266
266
|
}
|
|
@@ -366,6 +366,43 @@ export function knownProjectScopes(limit = 40) {
|
|
|
366
366
|
|
|
367
367
|
const stateFile = (scope) => path.join(scope.dir, 'state.json');
|
|
368
368
|
const prevFile = (scope) => path.join(scope.dir, 'state.prev.json');
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 损坏的 state.json 先留一份原始字节,再谈恢复:只复制、不改名、不删除,
|
|
372
|
+
* 同一份坏字节重复遇到时复用已有副本,避免每次读都堆一个新文件。
|
|
373
|
+
*/
|
|
374
|
+
function quarantineCorrupt(scope, file) {
|
|
375
|
+
let raw;
|
|
376
|
+
try {
|
|
377
|
+
raw = fs.readFileSync(file);
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
let names;
|
|
382
|
+
try {
|
|
383
|
+
names = fs.readdirSync(scope.dir);
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
for (const name of names) {
|
|
388
|
+
if (!name.startsWith('state.corrupt-') || !name.endsWith('.json')) continue;
|
|
389
|
+
const candidate = path.join(scope.dir, name);
|
|
390
|
+
try {
|
|
391
|
+
if (fs.readFileSync(candidate).equals(raw)) return candidate;
|
|
392
|
+
} catch {
|
|
393
|
+
/* 再试下一个副本 */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').replace(/\..+/, '');
|
|
397
|
+
let copy = path.join(scope.dir, `state.corrupt-${stamp}.json`);
|
|
398
|
+
if (fs.existsSync(copy)) copy = path.join(scope.dir, `state.corrupt-${stamp}-${crypto.randomBytes(3).toString('hex')}.json`);
|
|
399
|
+
try {
|
|
400
|
+
writeDurable(copy, raw);
|
|
401
|
+
} catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return copy;
|
|
405
|
+
}
|
|
369
406
|
const lockFile = (scope) => path.join(scope.dir, '.lock');
|
|
370
407
|
const migrationIntentFile = (scope) => path.join(scope.dir, 'migration.intent.json');
|
|
371
408
|
const purgeIntentFile = (scope) => path.join(scope.dir, 'purge.intent.json');
|
|
@@ -389,6 +426,40 @@ function sleepSync(ms) {
|
|
|
389
426
|
|
|
390
427
|
const lockDepth = new Map();
|
|
391
428
|
|
|
429
|
+
/** pid 还在不在:EPERM 说明进程存在但不属于当前用户,按「活着」处理,绝不回收。 */
|
|
430
|
+
function pidAlive(pid) {
|
|
431
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
432
|
+
try {
|
|
433
|
+
process.kill(pid, 0);
|
|
434
|
+
return true;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
return error.code === 'EPERM';
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 崩溃残留的锁:token 里的 pid 已经不在、且文件内容没被换过,才允许删。
|
|
442
|
+
* 活进程(含当前进程)的锁一律不动,迟到的新锁也不会被误删。
|
|
443
|
+
*/
|
|
444
|
+
function reclaimStaleLock(file) {
|
|
445
|
+
let owner;
|
|
446
|
+
try {
|
|
447
|
+
owner = fs.readFileSync(file, 'utf8').trim();
|
|
448
|
+
} catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (!owner) return false;
|
|
452
|
+
const pid = Number.parseInt(owner.split(':')[0], 10);
|
|
453
|
+
if (!Number.isInteger(pid) || pid === process.pid || pidAlive(pid)) return false;
|
|
454
|
+
try {
|
|
455
|
+
if (fs.readFileSync(file, 'utf8').trim() !== owner) return false;
|
|
456
|
+
fs.rmSync(file, { force: true });
|
|
457
|
+
return true;
|
|
458
|
+
} catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
392
463
|
/**
|
|
393
464
|
* 跨进程一把锁:.lock 用 wx 创建,写 owner token,只释放自己那把。
|
|
394
465
|
* 同进程内允许重入(读—改—写在同一个锁窗口里连续调用)。
|
|
@@ -414,6 +485,8 @@ export function withScopeLock(scope, fn) {
|
|
|
414
485
|
fd = fs.openSync(file, 'wx');
|
|
415
486
|
} catch (error) {
|
|
416
487
|
if (error.code !== 'EEXIST') throw new StateError('unavailable', `记忆库加锁失败(${error.code || 'IO'}):${file}`);
|
|
488
|
+
/** 锁主人已经不在了(崩溃/被 kill):回收后立刻重试,不把用户卡在 500ms 的 busy 上。 */
|
|
489
|
+
if (reclaimStaleLock(file)) continue;
|
|
417
490
|
if (Date.now() > deadline) {
|
|
418
491
|
throw new StateError('busy', `记忆库正忙(另一个进程在写),稍后再试;若确认没有写入进程,删掉 ${file}`);
|
|
419
492
|
}
|
|
@@ -645,7 +718,7 @@ export function cleanStateResidue(scope) {
|
|
|
645
718
|
}
|
|
646
719
|
}
|
|
647
720
|
|
|
648
|
-
function writeDurable(file, text) {
|
|
721
|
+
export function writeDurable(file, text) {
|
|
649
722
|
const dir = path.dirname(file);
|
|
650
723
|
fs.mkdirSync(dir, { recursive: true });
|
|
651
724
|
const tmp = tempOf(file);
|
|
@@ -1078,8 +1151,39 @@ export function load(scope, options = {}) {
|
|
|
1078
1151
|
try {
|
|
1079
1152
|
data = readEnvelope(current);
|
|
1080
1153
|
} catch (error) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1154
|
+
/**
|
|
1155
|
+
* 损坏的 current 不当空库覆盖:先把原始字节留成 state.corrupt-<时间戳>.json,
|
|
1156
|
+
* 再试 state.prev.json;prev 过了同一套校验才采用。两处都不可用就报出可执行的恢复路径。
|
|
1157
|
+
*/
|
|
1158
|
+
if (!error || error.code !== 'corrupt') throw error;
|
|
1159
|
+
const copy = quarantineCorrupt(scope, current);
|
|
1160
|
+
const prev = prevFile(scope);
|
|
1161
|
+
let recovered = null;
|
|
1162
|
+
try {
|
|
1163
|
+
recovered = readEnvelope(prev);
|
|
1164
|
+
} catch {
|
|
1165
|
+
recovered = null;
|
|
1166
|
+
}
|
|
1167
|
+
if (recovered) {
|
|
1168
|
+
return {
|
|
1169
|
+
state: recovered,
|
|
1170
|
+
exists: true,
|
|
1171
|
+
recovered: true,
|
|
1172
|
+
recovery: {
|
|
1173
|
+
corruptFile: copy,
|
|
1174
|
+
prevFile: prev,
|
|
1175
|
+
message: `记忆状态已损坏,已从上一版恢复(${prev});损坏副本保留在 ${copy}`,
|
|
1176
|
+
},
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
const hint = new StateError('corrupt', [
|
|
1180
|
+
`记忆状态已损坏,上一版也不可用,已停止读写:${current}`,
|
|
1181
|
+
`损坏副本已保留:${copy || '(原文件读不出来,未生成副本)'}`,
|
|
1182
|
+
`上一版路径:${prev}`,
|
|
1183
|
+
`恢复步骤:先备份 ${current} 和 ${prev};确认 ${prev} 可独立复原后把它复制成 state.json,或把损坏的 state.json 移走后重试;不要直接覆盖原文件`,
|
|
1184
|
+
].join(';'));
|
|
1185
|
+
hint.recovery = { corruptFile: copy, prevFile: prev };
|
|
1186
|
+
throw hint;
|
|
1083
1187
|
}
|
|
1084
1188
|
if (!data) {
|
|
1085
1189
|
const hasLegacy = LEGACY_MARKERS.some((name) => fs.existsSync(path.join(scope.dir, name)));
|
|
@@ -1153,7 +1257,8 @@ export function transact(scope, options, work) {
|
|
|
1153
1257
|
}].slice(-LIMITS.receipts);
|
|
1154
1258
|
}
|
|
1155
1259
|
validateState(draft);
|
|
1156
|
-
|
|
1260
|
+
/** 从 prev 恢复出来的这一轮,current 是坏的:不要把它当「上一版」写进 prev,先让好文件顶回去。 */
|
|
1261
|
+
const published = publish(scope, draft, loaded.exists && !loaded.recovered);
|
|
1157
1262
|
return { changed: true, state: draft, result: outcome.result, archived: evicted, durability: published.durability };
|
|
1158
1263
|
});
|
|
1159
1264
|
}
|