dsh-config-manager 0.1.73 → 0.1.76
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/client.d.ts +8 -4
- package/lib/client.js +814 -682
- package/lib/core/rollback-ledger.d.ts +29 -3
- package/lib/core/rollback-ledger.js +125 -7
- package/lib/core/rollback-point.d.ts +221 -3
- package/lib/core/rollback-point.js +790 -153
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/sync/messages.d.ts +1 -0
- package/lib/sync/messages.js +3 -0
- package/lib/sync/sync-engine.js +56 -7
- package/lib/ui/i18n.d.ts +1 -1
- package/lib/ui/i18n.js +2 -2
- package/lib/utils/bundle-scan.d.ts +5 -3
- package/lib/utils/bundle-scan.js +5 -3
- package/lib/utils/line-diff.d.ts +7 -1
- package/lib/utils/line-diff.js +8 -2
- package/package.json +1 -1
- package/src/client/chat/ChatEntryView.tsx +38 -9
- package/src/client/chat/RollbackFileIcon.tsx +7 -45
- package/src/client/chat/chat-entry-view.test.ts +164 -1
- package/src/client/chat/chat-entry-view.ts +127 -10
- package/src/client/chat/chat-node-registration.ts +40 -6
- package/src/client/common/DiffTable.tsx +12 -23
- package/src/client/common/host-primitives.ts +78 -0
- package/src/client/config-manager.module.css +33 -12
- package/src/client/environment/maintenance/DiskUsageCard.tsx +20 -3
- package/src/client/index.ts +7 -1
- package/src/client/library/RestorePlanView.tsx +0 -1
- package/src/client/locales.ts +0 -2
- package/src/client/recovery/RollbackDiffPane.tsx +13 -6
- package/src/client/recovery/recovery-locales.ts +2 -4
- package/src/client/recovery/rollback-file-changes.test.ts +37 -1
- package/src/client/sync/PushPreviewCard.tsx +57 -0
- package/src/client/sync/SyncPanel.tsx +2 -37
- package/src/client/sync/SyncSettingsView.test.ts +3 -0
- package/src/client/sync/sync-push-preview.test.ts +14 -0
- package/src/client/sync/sync-view.ts +9 -1
- package/src/core/rollback-ledger.ts +123 -7
- package/src/core/rollback-point.test.ts +592 -17
- package/src/core/rollback-point.ts +861 -155
- package/src/index.ts +1 -1
- package/src/sync/messages.ts +3 -0
- package/src/sync/sync-engine.ts +55 -7
- package/src/sync/sync-preview-push.test.ts +158 -0
- package/src/ui/i18n.ts +2 -2
- package/src/utils/bundle-scan.ts +5 -3
- package/src/utils/line-diff.test.ts +12 -0
- package/src/utils/line-diff.ts +9 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RollbackPointKind, RollbackPointRecord, RollbackPointSummary, RollbackSegmentSummary, RollbackStateKind } from './rollback-point.ts';
|
|
1
|
+
import type { RollbackPointKind, RollbackPointRecord, RollbackPointSummary, RollbackSegmentSummary, RollbackStateKind, RollbackWorkspacePayload } from './rollback-point.ts';
|
|
2
2
|
/** 清单格式版本(与**记录**的 schemaVersion 无关:后者是记录内容的版本,恒为 2) */
|
|
3
3
|
export declare const ROLLBACK_LEDGER_SCHEMA_VERSION = 3;
|
|
4
4
|
/** 单条记录目录名 */
|
|
@@ -54,6 +54,12 @@ export interface RollbackLedgerManifestEntry {
|
|
|
54
54
|
payload: string | null;
|
|
55
55
|
/** 载荷文件字节数(**同一哈希只算一次**,预算与统计用) */
|
|
56
56
|
payloadBytes: number;
|
|
57
|
+
/**
|
|
58
|
+
* 这份点**解析所需的全部载荷哈希**(自身在前、基准依次在后)。
|
|
59
|
+
* 增量载荷依赖基准 ⇒ 回收(prunePayloads)必须按链保活,否则链一断这个点就读不出来了。
|
|
60
|
+
* 老清单没有该字段 ⇒ 视为 `[payload]`(老载荷都是全量、无依赖)。
|
|
61
|
+
*/
|
|
62
|
+
payloadChain?: string[];
|
|
57
63
|
};
|
|
58
64
|
session: RollbackLedgerTrackHeader & {
|
|
59
65
|
/** 会话字节副本的对象仓键;null = 没有副本(回收对象时用,不必读记录文件) */
|
|
@@ -176,14 +182,34 @@ export declare class RollbackLedger {
|
|
|
176
182
|
/** 原子写清单(小文件;条目由调用方决定) */
|
|
177
183
|
writeManifest(entries: readonly RollbackLedgerManifestEntry[]): Promise<void>;
|
|
178
184
|
/** 把完整记录拆成「清单条目 + 记录文件内容 + 载荷文件内容」(**不做 IO**,纯函数) */
|
|
179
|
-
prepareRecord(record: RollbackPointRecord
|
|
185
|
+
prepareRecord(record: RollbackPointRecord, reuse?: {
|
|
186
|
+
hash: string;
|
|
187
|
+
bytes: number;
|
|
188
|
+
chain?: string[];
|
|
189
|
+
}): RollbackLedgerPrepared;
|
|
180
190
|
/**
|
|
181
191
|
* 落盘:载荷(内容寻址;同一哈希已存在则**零写入**)+ 记录文件。
|
|
182
192
|
* 先载荷后记录:中途失败只留一份没人引用的载荷(垃圾,不是坏数据),下次 prune 回收。
|
|
183
193
|
*/
|
|
184
194
|
writePrepared(prepared: RollbackLedgerPrepared): Promise<void>;
|
|
185
195
|
/** 读校验过的载荷(哈希不符 / 形状不符 -> error,绝不返回半个) */
|
|
186
|
-
|
|
196
|
+
/**
|
|
197
|
+
* **原始**读取一份载荷(形状校验 + 内容寻址自证)。
|
|
198
|
+
* `files` 可能只是「相对基准的增量子集」(`payload.delta` 非空)—— **只给解析器用**,
|
|
199
|
+
* 消费者一律走 {@link resolvePayload} 拿物化后的完整清单。
|
|
200
|
+
*/
|
|
201
|
+
private readStoredPayload;
|
|
202
|
+
/**
|
|
203
|
+
* 解析一份载荷:增量视图**物化**成完整 `files`(含 `object` 字段补回),全量视图原样返回。
|
|
204
|
+
*
|
|
205
|
+
* 任何一环不成立(基准缺失 / 哈希不符 / 形状不符 / 链成环)⇒ **如实失败**:绝不返回半个清单 ——
|
|
206
|
+
* 半份清单会让「留点后新建」的判据把绝大多数文件当成新建的,回退就会删掉它们。
|
|
207
|
+
*/
|
|
208
|
+
resolvePayload(hash: string): Promise<{
|
|
209
|
+
payload?: RollbackWorkspacePayload;
|
|
210
|
+
error?: string;
|
|
211
|
+
}>;
|
|
212
|
+
private materializePayload;
|
|
187
213
|
/** 读一条**完整**记录(引用解析对调用方透明);任何一步不成立就如实失败 */
|
|
188
214
|
readRecord(id: string): Promise<{
|
|
189
215
|
record?: RollbackPointRecord;
|
|
@@ -490,6 +490,7 @@ export function normalizeManifestEntry(value) {
|
|
|
490
490
|
return null;
|
|
491
491
|
const rawRecordBytes = value['recordBytes'];
|
|
492
492
|
const rawPayloadBytes = isRecord(workspaceRaw) ? workspaceRaw['payloadBytes'] : 0;
|
|
493
|
+
const rawPayloadChain = isRecord(workspaceRaw) ? workspaceRaw['payloadChain'] : null;
|
|
493
494
|
return {
|
|
494
495
|
id,
|
|
495
496
|
kind,
|
|
@@ -507,6 +508,10 @@ export function normalizeManifestEntry(value) {
|
|
|
507
508
|
...workspace,
|
|
508
509
|
payload: typeof wsPayload === 'string' ? wsPayload : null,
|
|
509
510
|
payloadBytes: typeof rawPayloadBytes === 'number' && Number.isFinite(rawPayloadBytes) && rawPayloadBytes >= 0 ? rawPayloadBytes : 0,
|
|
511
|
+
// 只保留形状合法的哈希;读不回来的链由解析器如实失败(绝不猜)
|
|
512
|
+
...(Array.isArray(rawPayloadChain)
|
|
513
|
+
? { payloadChain: rawPayloadChain.filter((h) => typeof h === 'string' && OBJECT_KEY_RE.test(h)) }
|
|
514
|
+
: {}),
|
|
510
515
|
},
|
|
511
516
|
session: { ...session, object: typeof sessionObject === 'string' ? sessionObject : null },
|
|
512
517
|
segments: [first, second],
|
|
@@ -532,6 +537,26 @@ function normalizeWorkspacePayload(value) {
|
|
|
532
537
|
return null;
|
|
533
538
|
return value;
|
|
534
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* **存盘**形状校验:完整清单(`files` 是数组)或增量视图(`delta` 是对象、`files` 故意留 null)。
|
|
542
|
+
* 与 {@link normalizeWorkspacePayload}(消费侧,要求物化后的完整形状)分开:增量载荷**绝不会**被当成完整清单读出去。
|
|
543
|
+
*/
|
|
544
|
+
function normalizeStoredPayload(value) {
|
|
545
|
+
const base = normalizeWorkspacePayload(value);
|
|
546
|
+
if (base !== null)
|
|
547
|
+
return base;
|
|
548
|
+
if (!isRecord(value))
|
|
549
|
+
return null;
|
|
550
|
+
const delta = value['delta'];
|
|
551
|
+
if (!isRecord(delta))
|
|
552
|
+
return null;
|
|
553
|
+
if (typeof delta['base'] !== 'string')
|
|
554
|
+
return null;
|
|
555
|
+
if (!Array.isArray(delta['added']) || !Array.isArray(delta['removed']))
|
|
556
|
+
return null;
|
|
557
|
+
const clone = { ...value, files: [] };
|
|
558
|
+
return normalizeWorkspacePayload(clone);
|
|
559
|
+
}
|
|
535
560
|
function normalizeSessionPayload(value) {
|
|
536
561
|
if (!isRecord(value))
|
|
537
562
|
return null;
|
|
@@ -743,7 +768,7 @@ export class RollbackLedger {
|
|
|
743
768
|
}
|
|
744
769
|
/* -------------------------------------------------------- 单条记录 */
|
|
745
770
|
/** 把完整记录拆成「清单条目 + 记录文件内容 + 载荷文件内容」(**不做 IO**,纯函数) */
|
|
746
|
-
prepareRecord(record) {
|
|
771
|
+
prepareRecord(record, reuse) {
|
|
747
772
|
const summary = this.summarize(record);
|
|
748
773
|
const workspaceTrack = record.workspace;
|
|
749
774
|
const sessionTrack = record.session;
|
|
@@ -751,7 +776,12 @@ export class RollbackLedger {
|
|
|
751
776
|
let payloadJson = null;
|
|
752
777
|
let payloadHash = null;
|
|
753
778
|
let payloadBytes = 0;
|
|
754
|
-
if (payload !== null) {
|
|
779
|
+
if (payload !== null && reuse !== undefined) {
|
|
780
|
+
// **复用**:这份工作区态与基准点逐条相同 ⇒ 直接引用它已经落盘的载荷(零额外字节,链不增长)
|
|
781
|
+
payloadHash = reuse.hash;
|
|
782
|
+
payloadBytes = reuse.bytes;
|
|
783
|
+
}
|
|
784
|
+
else if (payload !== null) {
|
|
755
785
|
payloadJson = JSON.stringify(payload);
|
|
756
786
|
payloadHash = sha256Hex(payloadJson);
|
|
757
787
|
payloadBytes = byteLength(payloadJson);
|
|
@@ -773,6 +803,14 @@ export class RollbackLedger {
|
|
|
773
803
|
workspace: { ...trackHeader(workspaceTrack), payload: payloadHash },
|
|
774
804
|
session: { ...trackHeader(sessionTrack), payload: sessionTrack.payload },
|
|
775
805
|
});
|
|
806
|
+
// 增量视图 ⇒ 清单条目带上「解析所需的全部载荷哈希」(自身 + 基准链),回收按链保活
|
|
807
|
+
const payloadChain = payloadHash === null
|
|
808
|
+
? undefined
|
|
809
|
+
: reuse !== undefined
|
|
810
|
+
? (reuse.chain !== undefined && reuse.chain.length > 0 ? [...reuse.chain] : [payloadHash])
|
|
811
|
+
: payload?.delta === undefined
|
|
812
|
+
? [payloadHash]
|
|
813
|
+
: [payloadHash, ...payload.delta.baseChain];
|
|
776
814
|
const entry = {
|
|
777
815
|
id: record.id,
|
|
778
816
|
kind: record.kind,
|
|
@@ -786,7 +824,7 @@ export class RollbackLedger {
|
|
|
786
824
|
turnSeq: record.turnSeq,
|
|
787
825
|
boundarySeq: record.boundarySeq,
|
|
788
826
|
recordBytes: byteLength(recordJson),
|
|
789
|
-
workspace: { ...trackHeader(workspaceTrack), payload: payloadHash, payloadBytes },
|
|
827
|
+
workspace: { ...trackHeader(workspaceTrack), payload: payloadHash, payloadBytes, ...(payloadChain === undefined ? {} : { payloadChain }) },
|
|
790
828
|
session: { ...trackHeader(sessionTrack), object: sessionTrack.payload?.object ?? null },
|
|
791
829
|
segments: summary.segments,
|
|
792
830
|
};
|
|
@@ -813,7 +851,12 @@ export class RollbackLedger {
|
|
|
813
851
|
await this.write(recordFile, Buffer.from(prepared.recordJson, 'utf8'));
|
|
814
852
|
}
|
|
815
853
|
/** 读校验过的载荷(哈希不符 / 形状不符 -> error,绝不返回半个) */
|
|
816
|
-
|
|
854
|
+
/**
|
|
855
|
+
* **原始**读取一份载荷(形状校验 + 内容寻址自证)。
|
|
856
|
+
* `files` 可能只是「相对基准的增量子集」(`payload.delta` 非空)—— **只给解析器用**,
|
|
857
|
+
* 消费者一律走 {@link resolvePayload} 拿物化后的完整清单。
|
|
858
|
+
*/
|
|
859
|
+
async readStoredPayload(hash) {
|
|
817
860
|
const target = this.payloadPath(hash);
|
|
818
861
|
if (target === null)
|
|
819
862
|
return { error: 'record-incomplete: payload ref invalid' };
|
|
@@ -834,11 +877,64 @@ export class RollbackLedger {
|
|
|
834
877
|
catch (error) {
|
|
835
878
|
return { error: 'record-incomplete: payload unreadable (' + errorDetail(error) + ')' };
|
|
836
879
|
}
|
|
837
|
-
const payload =
|
|
880
|
+
const payload = normalizeStoredPayload(parsed);
|
|
838
881
|
if (payload === null)
|
|
839
882
|
return { error: 'record-incomplete: payload shape invalid' };
|
|
840
883
|
return { payload };
|
|
841
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* 解析一份载荷:增量视图**物化**成完整 `files`(含 `object` 字段补回),全量视图原样返回。
|
|
887
|
+
*
|
|
888
|
+
* 任何一环不成立(基准缺失 / 哈希不符 / 形状不符 / 链成环)⇒ **如实失败**:绝不返回半个清单 ——
|
|
889
|
+
* 半份清单会让「留点后新建」的判据把绝大多数文件当成新建的,回退就会删掉它们。
|
|
890
|
+
*/
|
|
891
|
+
async resolvePayload(hash) {
|
|
892
|
+
// **不缓存**:载荷被删/被改必须立刻看得见(缓存会把「存储已经坏了」蒙过去),
|
|
893
|
+
// 也守住「一次只驻留一份载荷」的内存护栏(issue #89 的 OOM 现场)。
|
|
894
|
+
return this.materializePayload(hash, new Set([hash]));
|
|
895
|
+
}
|
|
896
|
+
async materializePayload(hash, seen) {
|
|
897
|
+
const stored = await this.readStoredPayload(hash);
|
|
898
|
+
if (stored.payload === undefined)
|
|
899
|
+
return { error: stored.error ?? 'record-incomplete: payload unreadable' };
|
|
900
|
+
const payload = stored.payload;
|
|
901
|
+
const delta = payload.delta;
|
|
902
|
+
if (delta === undefined)
|
|
903
|
+
return { payload: withObjectRefs(payload) };
|
|
904
|
+
if (typeof delta.base !== 'string' || !OBJECT_KEY_RE.test(delta.base)) {
|
|
905
|
+
return { error: 'record-incomplete: delta base ref invalid' };
|
|
906
|
+
}
|
|
907
|
+
if (seen.has(delta.base))
|
|
908
|
+
return { error: 'record-incomplete: delta chain cycle' };
|
|
909
|
+
const next = new Set(seen);
|
|
910
|
+
next.add(delta.base);
|
|
911
|
+
const base = await this.materializePayload(delta.base, next);
|
|
912
|
+
if (base.payload === undefined) {
|
|
913
|
+
return { error: 'record-incomplete: delta base unresolved (' + String(base.error ?? '') + ')' };
|
|
914
|
+
}
|
|
915
|
+
const removed = new Set();
|
|
916
|
+
for (const p of Array.isArray(delta.removed) ? delta.removed : [])
|
|
917
|
+
if (typeof p === 'string')
|
|
918
|
+
removed.add(p);
|
|
919
|
+
const added = withObjectRefs({ ...payload, files: delta.added ?? [], delta: undefined }).files;
|
|
920
|
+
const addedPaths = new Set();
|
|
921
|
+
for (const file of added)
|
|
922
|
+
if (typeof file?.path === 'string')
|
|
923
|
+
addedPaths.add(file.path);
|
|
924
|
+
const files = [];
|
|
925
|
+
for (const file of base.payload.files) {
|
|
926
|
+
if (typeof file?.path !== 'string')
|
|
927
|
+
continue;
|
|
928
|
+
if (removed.has(file.path) || addedPaths.has(file.path))
|
|
929
|
+
continue;
|
|
930
|
+
files.push(file);
|
|
931
|
+
}
|
|
932
|
+
for (const file of added)
|
|
933
|
+
files.push(file);
|
|
934
|
+
// 与全量捕获同序(`payload.files.sort` 用的就是这个比较器)
|
|
935
|
+
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
936
|
+
return { payload: { ...payload, files, delta: undefined } };
|
|
937
|
+
}
|
|
842
938
|
/** 读一条**完整**记录(引用解析对调用方透明);任何一步不成立就如实失败 */
|
|
843
939
|
async readRecord(id) {
|
|
844
940
|
const target = this.recordPath(id);
|
|
@@ -870,7 +966,7 @@ export class RollbackLedger {
|
|
|
870
966
|
if (typeof payloadRef !== 'string' || !OBJECT_KEY_RE.test(payloadRef)) {
|
|
871
967
|
return { error: 'record-incomplete: payload ref invalid' };
|
|
872
968
|
}
|
|
873
|
-
const resolved = await this.
|
|
969
|
+
const resolved = await this.resolvePayload(payloadRef);
|
|
874
970
|
if (resolved.payload === undefined)
|
|
875
971
|
return { error: resolved.error ?? 'record-incomplete: payload unreadable' };
|
|
876
972
|
payload = resolved.payload;
|
|
@@ -935,6 +1031,13 @@ export class RollbackLedger {
|
|
|
935
1031
|
async prunePayloads(entries) {
|
|
936
1032
|
const referenced = new Set();
|
|
937
1033
|
for (const entry of entries) {
|
|
1034
|
+
// 增量载荷依赖基准 ⇒ **按链保活**(否则淘汰/删点会把基准删掉,那个点就永远读不回来了)
|
|
1035
|
+
const chain = entry.workspace.payloadChain;
|
|
1036
|
+
if (Array.isArray(chain) && chain.length > 0) {
|
|
1037
|
+
for (const hash of chain)
|
|
1038
|
+
referenced.add(hash);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
938
1041
|
if (entry.workspace.payload !== null)
|
|
939
1042
|
referenced.add(entry.workspace.payload);
|
|
940
1043
|
}
|
|
@@ -986,7 +1089,7 @@ export class RollbackLedger {
|
|
|
986
1089
|
for (const entry of entries) {
|
|
987
1090
|
const hash = entry.workspace.payload;
|
|
988
1091
|
if (hash !== null) {
|
|
989
|
-
const resolved = await this.
|
|
1092
|
+
const resolved = await this.resolvePayload(hash);
|
|
990
1093
|
if (resolved.payload === undefined) {
|
|
991
1094
|
return { removed: 0, bytes: 0, referencedPaths, aborted: resolved.error ?? 'payload unreadable' };
|
|
992
1095
|
}
|
|
@@ -1272,6 +1375,21 @@ function trackHeader(track) {
|
|
|
1272
1375
|
reasonDetail: track.reasonDetail,
|
|
1273
1376
|
};
|
|
1274
1377
|
}
|
|
1378
|
+
/**
|
|
1379
|
+
* 补回**落盘时省掉的** `object` 字段:内容寻址的名字就是内容哈希 ⇒ `object ≡ sha256`。
|
|
1380
|
+
* 缺 `sha256` 的畸形条目保持原样(消费侧的防御判据照旧生效)。
|
|
1381
|
+
*/
|
|
1382
|
+
function withObjectRefs(payload) {
|
|
1383
|
+
for (const file of payload.files) {
|
|
1384
|
+
if (file === null || typeof file !== 'object')
|
|
1385
|
+
continue;
|
|
1386
|
+
if (typeof file.object === 'string')
|
|
1387
|
+
continue;
|
|
1388
|
+
if (typeof file.sha256 === 'string')
|
|
1389
|
+
file.object = file.sha256;
|
|
1390
|
+
}
|
|
1391
|
+
return payload;
|
|
1392
|
+
}
|
|
1275
1393
|
function errorDetail(error) {
|
|
1276
1394
|
if (error instanceof Error) {
|
|
1277
1395
|
const code = error.code;
|
|
@@ -101,6 +101,43 @@ export declare const ROLLBACK_LEFTOVER_LIST_LIMIT = 20;
|
|
|
101
101
|
* `truncated` 置真 ⇒ 删除集自动 fail-closed(证明名单不完整就一条都不删)。
|
|
102
102
|
*/
|
|
103
103
|
export declare const ROLLBACK_DEFAULT_CAPTURE_BUDGET_MS = 30000;
|
|
104
|
+
/**
|
|
105
|
+
* 自适应预算区间(见 RollbackEngineOptions.captureBudgetMs):上一次留点被截断 ⇒ 下次 ×2,
|
|
106
|
+
* 上一次只用掉不到 1/4 ⇒ 下次 ÷2。区间外不再调整(避免无限拉长 / 缩到不可用)。
|
|
107
|
+
*/
|
|
108
|
+
export declare const ROLLBACK_MIN_CAPTURE_BUDGET_MS = 10000;
|
|
109
|
+
export declare const ROLLBACK_MAX_CAPTURE_BUDGET_MS = 120000;
|
|
110
|
+
/**
|
|
111
|
+
* 捕获期「读内容 + 写对象仓」的并发上限。
|
|
112
|
+
*
|
|
113
|
+
* 为什么必须并发:真机实测(34k 文件 / 512 MiB)一次全量捕获 175 s,其中 **148 s 花在
|
|
114
|
+
* 31,702 次对象写**(一次 4.7 ms:temp + fsync + rename),而走查本身只要 0.66 s。
|
|
115
|
+
* 对象写入是纯 I/O,串行做等于把预算全花在等待上 ⇒ 覆盖永远只有一小半。
|
|
116
|
+
* 并发上限与**在飞字节闸门**共同限流(在飞内容上界 = `captureInFlightBytes`)。
|
|
117
|
+
*/
|
|
118
|
+
export declare const ROLLBACK_DEFAULT_CAPTURE_CONCURRENCY = 16;
|
|
119
|
+
/**
|
|
120
|
+
* 捕获期**读入内存、尚未落对象仓**的内容字节上限(真正的内存闸门)。
|
|
121
|
+
*
|
|
122
|
+
* 为什么必须独立于 `maxFileBytes`(0.1.74 真机闪退事故,2026-10-16):
|
|
123
|
+
* `maxFileBytes` 的缺省是**总量预算** `maxBytes`(512 MiB)。并发捕获把闸门接到它上面,
|
|
124
|
+
* 于是「同时在飞的内容」可以到 ~512 MiB —— 串行实现的上界是「一份文件」,并发实现的
|
|
125
|
+
* 上界是「闸门限额」,两者差着两三个数量级。真机表现:发出消息后 1–2 s(= `turn/start`
|
|
126
|
+
* 开始留点)DSH Desktop 宿主被系统终止 ⇒ 桌面闪退(无 JS 栈、插件零错误、
|
|
127
|
+
* 退出码是「被外部终止」形状)。这里取一个小常量当闸门;要放宽只能显式传
|
|
128
|
+
* `captureInFlightBytes`,且被 `maxBytes` 夹住。
|
|
129
|
+
*/
|
|
130
|
+
export declare const ROLLBACK_DEFAULT_CAPTURE_INFLIGHT_BYTES: number;
|
|
131
|
+
/** 捕获阶段①的 stat 批大小:判定仍严格按顺序,只是 stat 并发取(真机 34k 次 stat 从 5 s → <1 s) */
|
|
132
|
+
export declare const ROLLBACK_CAPTURE_STAT_BATCH = 128;
|
|
133
|
+
/**
|
|
134
|
+
* 捕获期**候选名单**上限(不是入点上限):必须显著大于 maxFiles,否则「缺口优先」排序作用不到
|
|
135
|
+
* 那些排在遍历顺序尾部、上一回合没覆盖到的文件(它们连候选都进不了 ⇒ 覆盖率永远不收敛)。
|
|
136
|
+
* 走查本来就会把每条路径记进 walked(路径索引的来源),这里多留的只是候选数组本身。
|
|
137
|
+
*/
|
|
138
|
+
export declare const ROLLBACK_CAPTURE_LIST_LIMIT = 200000;
|
|
139
|
+
/** 走查中断时逐条记下的「没进去的子树」上限;超出即放弃精确范围,退回全局 fail-closed */
|
|
140
|
+
export declare const ROLLBACK_UNWALKED_DIRS_LIMIT = 200;
|
|
104
141
|
/**
|
|
105
142
|
* 「删除回退点之后新建的路径」的**整套上限**(用户 2026-10-11 定案 A)。
|
|
106
143
|
* 超过就整套拒绝(fail-closed),绝不删一半 —— 半套删除既没有可解释的状态,也无法整体回滚。
|
|
@@ -132,6 +169,14 @@ export declare const ROLLBACK_PREVIEW_DIFF_TOTAL_WORK = 32000000;
|
|
|
132
169
|
export declare const ROLLBACK_FILE_DIFF_LIMITS: {
|
|
133
170
|
readonly maxFileBytes: number;
|
|
134
171
|
readonly context: 3;
|
|
172
|
+
/**
|
|
173
|
+
* Myers 编辑距离上限(2026-10-15 用户:不该动辄出现「已按整块替换展示」)。
|
|
174
|
+
* 实测(本仓 rollback-point.ts 的 4983 行 × 2492 行版本对):D 在 3000~4000 就能拿到
|
|
175
|
+
* 最小编辑脚本 —— 耗时 ≤70 ms;代价是回溯的 V 快照 O(D²),RSS 峰值 +~100 MB。
|
|
176
|
+
* 缺省那档 400 会让几千行里的普通改动一律退化成「整块替换」,那正是用户看到的那条提示;
|
|
177
|
+
* 单文件是**按需点开**的一屏,这点内存换「真差异」划算。再大就换算法,不加预算。
|
|
178
|
+
*/
|
|
179
|
+
readonly maxD: 4000;
|
|
135
180
|
};
|
|
136
181
|
/** 恢复顺序(工作区在前;工作区失败即中止后面的会话段) */
|
|
137
182
|
export declare const ROLLBACK_REWIND_ORDER: readonly ['workspace', 'session'];
|
|
@@ -208,7 +253,8 @@ export interface RollbackSkippedEntry {
|
|
|
208
253
|
/**
|
|
209
254
|
* link(符号链接/junction,不跟随)/ missing / not-a-file / self-store /
|
|
210
255
|
* dep-dir(运行时依赖目录,整棵忽略)/ ignored(用户忽略规则命中)/ too-large(超单文件上限)/
|
|
211
|
-
* budget-bytes(加它会超总量预算)/ budget-files
|
|
256
|
+
* budget-bytes(加它会超总量预算)/ budget-files(文件数预算用尽)/
|
|
257
|
+
* asar-container(含 `.asar` 段的路径:宿主会把它当容器解析、畸形夹具能带走宿主,因此绝不碰)
|
|
212
258
|
*/
|
|
213
259
|
reason: string;
|
|
214
260
|
}
|
|
@@ -230,7 +276,28 @@ export interface RollbackPathIndex {
|
|
|
230
276
|
export interface RollbackWorkspacePayload {
|
|
231
277
|
/** 显式路径分块:恢复只在这些分块内覆盖 */
|
|
232
278
|
chunks: string[];
|
|
279
|
+
/**
|
|
280
|
+
* 记录在案的**文件条目数**。**必须自带**(不能再用 `files.length`):增量视图下 `files` 只是
|
|
281
|
+
* 「相对基准新增/改写的子集」,而摘要(界面上的条数 / 可恢复判定)要的是全量条数。
|
|
282
|
+
*/
|
|
283
|
+
fileCount?: number;
|
|
233
284
|
files: RollbackWorkspaceFile[];
|
|
285
|
+
/**
|
|
286
|
+
* **存盘专用**:这份载荷是「相对某个基准载荷的增量」(见 rollback-ledger 的 resolvePayload)。
|
|
287
|
+
* 只在写盘那一刻存在 —— 任何读路径(readRecord / 预览 / 回退 / 对象回收)拿到的都是**物化后**
|
|
288
|
+
* 的完整 `files`(`delta` 已被抹掉)。**绝不把带 delta 的对象交给消费者**:它看起来像一份
|
|
289
|
+
* 「只有这几条记录」的完整清单,拿去规划删除会删掉其它全部文件。
|
|
290
|
+
*/
|
|
291
|
+
delta?: {
|
|
292
|
+
/** 基准载荷的内容寻址哈希(必须在 prune/回收时一并保活) */
|
|
293
|
+
base: string;
|
|
294
|
+
/** 基准自己的依赖链(深→浅,含基准),保活用 */
|
|
295
|
+
baseChain: string[];
|
|
296
|
+
/** 相对基准**不再存在**的路径(逐字,与捕获产出的绝对路径同形) */
|
|
297
|
+
removed: string[];
|
|
298
|
+
/** 相对基准**新增 / 改写**的条目(`files` 在增量视图下为 null) */
|
|
299
|
+
added?: RollbackWorkspaceFile[];
|
|
300
|
+
};
|
|
234
301
|
skipped: RollbackSkippedEntry[];
|
|
235
302
|
/**
|
|
236
303
|
* 整棵跳过的「运行时依赖目录」(忽略规则 = 全仓唯一来源 EXCLUDED_DEP_DIR_NAMES,当前 node_modules)。
|
|
@@ -269,6 +336,12 @@ export interface RollbackWorkspacePayload {
|
|
|
269
336
|
* 老台账(本字段诞生前写下的点)一律没有它:按「名单完整」处理(与旧行为一致)。
|
|
270
337
|
*/
|
|
271
338
|
walkIncomplete?: boolean;
|
|
339
|
+
/**
|
|
340
|
+
* 走查被中断时**栈里还没进去的子树**(精确到目录)。有它 ⇒ 拒删只覆盖这些子树
|
|
341
|
+
* (分子树 fail-closed),而不是把整个工作区的删除全关掉;缺它(老台账 / 栈太深被截断)
|
|
342
|
+
* ⇒ 退回全局拒删。与 walkIncomplete 一起看:后者是「安全判据」,本字段是「精确范围」。
|
|
343
|
+
*/
|
|
344
|
+
unwalkedDirs?: string[];
|
|
272
345
|
/** 命中文件数 / 字节上限(内容未全部进来,已如实留痕) */
|
|
273
346
|
truncated: boolean;
|
|
274
347
|
bytes: number;
|
|
@@ -781,8 +854,25 @@ export interface RollbackEngineOptions {
|
|
|
781
854
|
/**
|
|
782
855
|
* 工作区捕获的挂钟时间预算(毫秒,缺省 ROLLBACK_DEFAULT_CAPTURE_BUDGET_MS)。
|
|
783
856
|
* 超时留**部分点**,不拒绝留点;见该常量的说明。
|
|
857
|
+
*
|
|
858
|
+
* 运行期**自适应**:被截断 ⇒ 下次翻倍;用得很少 ⇒ 下次减半(区间见
|
|
859
|
+
* ROLLBACK_MIN/MAX_CAPTURE_BUDGET_MS)。回退期走查的预算见 rewindBudgetMs。
|
|
784
860
|
*/
|
|
785
861
|
captureBudgetMs?: number;
|
|
862
|
+
/** 自适应区间下界(缺省 ROLLBACK_MIN_CAPTURE_BUDGET_MS;测试可注入更小值) */
|
|
863
|
+
captureBudgetMinMs?: number;
|
|
864
|
+
/** 自适应区间上界(缺省 ROLLBACK_MAX_CAPTURE_BUDGET_MS;测试可注入更小值) */
|
|
865
|
+
captureBudgetMaxMs?: number;
|
|
866
|
+
/** 回退期走查的时间预算(缺省 = captureBudgetMs 初值;**不参与自适应**) */
|
|
867
|
+
rewindBudgetMs?: number;
|
|
868
|
+
/** 捕获期读 + 写并发上限(缺省 ROLLBACK_DEFAULT_CAPTURE_CONCURRENCY,1..64) */
|
|
869
|
+
captureConcurrency?: number;
|
|
870
|
+
/**
|
|
871
|
+
* 捕获期**在飞内容字节**上限(缺省 ROLLBACK_DEFAULT_CAPTURE_INFLIGHT_BYTES = 32 MiB,
|
|
872
|
+
* 有效范围 1..maxBytes)。与 `maxFileBytes`(单文件「要不要读」的判据)是两件事:
|
|
873
|
+
* **这个才是并发的内存闸门**。
|
|
874
|
+
*/
|
|
875
|
+
captureInFlightBytes?: number;
|
|
786
876
|
guardPolicy?: RollbackGuardPolicy;
|
|
787
877
|
/** 存储栈探针(缺省 = 真实磁盘探测);注入失败探针可测「缺失时仍可挂载」 */
|
|
788
878
|
storage?: RollbackStorageProbe;
|
|
@@ -809,6 +899,18 @@ export interface RollbackEngineOptions {
|
|
|
809
899
|
/** best-effort 日志(清理失败等;绝不抛给调用方) */
|
|
810
900
|
log?: (line: string) => void;
|
|
811
901
|
}
|
|
902
|
+
/**
|
|
903
|
+
* 路径里是否存在以 `.asar` 结尾的段(**捕获 / 走查期一律不碰这类路径**)。
|
|
904
|
+
*
|
|
905
|
+
* 为什么(真机事故,0.1.76):Electron 宿主对任何含 `.asar` 段的路径**启用了 asar 虚拟化** ——
|
|
906
|
+
* 即使那只是个普通文件,`fs.stat`/`readdir`/`read` 也会被接管去当容器解析。工作区里只要躺着一枚
|
|
907
|
+
* **畸形** `.asar`(测试夹具、被打断的打包残留),宿主进程就会被原生层带走:crashpad
|
|
908
|
+
* `not connected`、退出码 4294930435 ⇒ DSH Desktop 闪退(无 JS 栈、插件零错误)。
|
|
909
|
+
* 实测(本仓真机捕获、同一份输入):`.tmp` 下 19 枚 `.asar` 夹具在场 → Electron 进程死;
|
|
910
|
+
* 全部改名后 → 同一趟捕获 9 s 跑完、退出码 0。
|
|
911
|
+
* `.asar.unpacked` 是**真目录**(段名不以 `.asar` 结尾)⇒ 不拦。
|
|
912
|
+
*/
|
|
913
|
+
export declare function hasAsarSegment(target: string): boolean;
|
|
812
914
|
/** child 是否等于 parent 或位于其下(win32 折叠大小写;段边界判定) */
|
|
813
915
|
export declare function isInsidePath(child: string, parent: string): boolean;
|
|
814
916
|
/**
|
|
@@ -854,6 +956,11 @@ export declare function resolveTurnBoundary(events: readonly RollbackEventLike[]
|
|
|
854
956
|
code: 'no-boundary';
|
|
855
957
|
detail: string;
|
|
856
958
|
};
|
|
959
|
+
/**
|
|
960
|
+
* 记录在案的文件条目数:优先用载荷自带的 `fileCount`(增量视图下 `files` 可能只是子集),
|
|
961
|
+
* 老载荷没有该字段才回落到 `files.length`。
|
|
962
|
+
*/
|
|
963
|
+
export declare function workspaceFileCount(payload: RollbackWorkspacePayload | null): number;
|
|
857
964
|
export declare function summarizeRollbackPoint(record: RollbackPointRecord): RollbackPointSummary;
|
|
858
965
|
/** 浅层形状校验:台账被篡改 / 半写时不把它当记录用(宁可当「读不到」)。 */
|
|
859
966
|
export declare function isRollbackPointRecord(value: unknown): value is RollbackPointRecord;
|
|
@@ -870,6 +977,7 @@ export declare class RollbackPointEngine {
|
|
|
870
977
|
private readonly maxFiles;
|
|
871
978
|
private readonly maxBytes;
|
|
872
979
|
private readonly maxFileBytes;
|
|
980
|
+
private readonly captureInFlightBytes;
|
|
873
981
|
private readonly maxSkippedEntries;
|
|
874
982
|
private readonly sessionMaxBytes;
|
|
875
983
|
private readonly sessionQuiescentMs;
|
|
@@ -899,14 +1007,41 @@ export declare class RollbackPointEngine {
|
|
|
899
1007
|
private forkLinksAt;
|
|
900
1008
|
private forkLinksComplete;
|
|
901
1009
|
/** 会话日志头缓存(mtime + size 失效;只服务 fork 链扫描) */
|
|
1010
|
+
/**
|
|
1011
|
+
* **本进程**最近一次成功留点的工作区摘要(清单增量化的基准)。
|
|
1012
|
+
*
|
|
1013
|
+
* 为什么是内存而不是读盘:一次留点只准读清单(L-08 / issue #89)—— 读上一份 12 MB 载荷去算差异
|
|
1014
|
+
* 正是那条被消灭的路。内存量 ≈ 每文件一个 `sha:bytes:chunk` 串(3.4 万文件约几 MB),
|
|
1015
|
+
* 与已有的身份缓存(`pathCache`)同量级;进程重启后第一次留点落全量锚点,随后继续链。
|
|
1016
|
+
*/
|
|
1017
|
+
private deltaBase;
|
|
902
1018
|
private readonly forkHeaderCache;
|
|
903
1019
|
/** 继承判据用的台账快照(TTL 缓存;只缓存**清单条目**,绝不缓存完整记录) */
|
|
904
1020
|
private ledgerSnapshotCache;
|
|
905
1021
|
/** 台账存储层(v3:清单 + 单条记录 + 内容寻址载荷;issue #89) */
|
|
906
1022
|
private readonly ledger;
|
|
907
1023
|
private readonly ledgerBytesBudget;
|
|
908
|
-
/**
|
|
909
|
-
|
|
1024
|
+
/**
|
|
1025
|
+
* 工作区捕获的挂钟时间预算(毫秒;见 ROLLBACK_DEFAULT_CAPTURE_BUDGET_MS)。**可变**:
|
|
1026
|
+
* 每次留点后按「是否被截断 / 用掉多少」自适应(见 ROLLBACK_MIN/MAX_CAPTURE_BUDGET_MS)。
|
|
1027
|
+
*/
|
|
1028
|
+
private captureBudgetMs;
|
|
1029
|
+
/** 自适应区间下界 / 上界(构造期定死,运行期只调 captureBudgetMs) */
|
|
1030
|
+
private readonly captureBudgetMinMs;
|
|
1031
|
+
private readonly captureBudgetMaxMs;
|
|
1032
|
+
/** 回退期走查的时间预算(与捕获预算分开:自适应不该顺手拉长回退期的走查) */
|
|
1033
|
+
private readonly rewindBudgetMs;
|
|
1034
|
+
/** 捕获期读 + 写并发上限 */
|
|
1035
|
+
private readonly captureConcurrency;
|
|
1036
|
+
/** 对象仓目录已确认存在(省掉每个对象一次 mkdir) */
|
|
1037
|
+
private objectsDirReady;
|
|
1038
|
+
/**
|
|
1039
|
+
* 本回合对象名单快照(一次 readdir 顶掉「每个候选一次 fs.access」;真机 34k 次 access ≈ 3.7 s)。
|
|
1040
|
+
* null = 读不到名单(首次还没建目录/异常)⇒ 回落逐条 objectExists。
|
|
1041
|
+
* 只保证「写进去的立刻可见」:捕获期间被**并发删除**的对象可能仍被判为存在 —— 那一路由
|
|
1042
|
+
* 读取侧(readObject + 哈希校验)响亮失败,不会静默给出错误的 before 内容。
|
|
1043
|
+
*/
|
|
1044
|
+
private captureObjectNames;
|
|
910
1045
|
/**
|
|
911
1046
|
* 启动期一次性初始化(流式迁移 v2 -> v3 + 孤儿 tmp 清理)的惰性单飞。
|
|
912
1047
|
* 为什么惰取而不是构造时同步跑:构造发生在每个请求路径上,同步扫盘/迁移会把请求卡死;
|
|
@@ -945,6 +1080,19 @@ export declare class RollbackPointEngine {
|
|
|
945
1080
|
storageStatus(): Promise<RollbackStorageStatus>;
|
|
946
1081
|
/** 台账可观测快照(清单条数 + 磁盘字节 + 预算 + 本进程淘汰计数) */
|
|
947
1082
|
private ledgerStatus;
|
|
1083
|
+
/** 对象仓目录只 mkdir 一次(每个对象各 mkdir 一次是纯白花的 syscall) */
|
|
1084
|
+
private ensureObjectsDir;
|
|
1085
|
+
/**
|
|
1086
|
+
* 写内容寻址对象(对象文件名 = 内容 SHA-256)。
|
|
1087
|
+
*
|
|
1088
|
+
* **为什么这里不用 atomicWriteFile**:那是给「可写配置文件」用的强原子写(同目录 tmp → fsync →
|
|
1089
|
+
* rename),真机实测一次约 4.7 ms —— 34k 文件的工作区一次全量捕获 148 s 全花在它上面(走查本身
|
|
1090
|
+
* 只要 0.66 s)。对象仓的正确性要求不同:文件名即内容哈希 ⇒
|
|
1091
|
+
* · 用 'wx' **独占创建最终名**本身已是原子的(不覆盖已有对象;并发同写只有一方成功);
|
|
1092
|
+
* · 半截 / 被篡改的内容在读取侧必然过不了哈希校验(readObject + sha256Hex)⇒ 按「读不出」
|
|
1093
|
+
* 处理并 fail-closed,绝不会冒充有效前像;
|
|
1094
|
+
* · 不做 fsync:回滚点是尽力而为的本地快照,不为掉电持久性付出每文件一次 fsync 的代价。
|
|
1095
|
+
*/
|
|
948
1096
|
private writeObjectWithHash;
|
|
949
1097
|
/** 对象仓里是否真有这个 blob(缓存命中时校验;任何异常一律按「没有」处理) */
|
|
950
1098
|
private objectExists;
|
|
@@ -963,10 +1111,40 @@ export declare class RollbackPointEngine {
|
|
|
963
1111
|
/**
|
|
964
1112
|
* 列出一个显式分块内的候选文件(不跟随符号链接 / junction;跳过与读不到都留痕)。
|
|
965
1113
|
* 返回 null = 该分块本身不可用(不存在 / 是链接 / 非常规文件),细节已写进 sink。
|
|
1114
|
+
*
|
|
1115
|
+
* `truncated` 与 `walkAborted` 是**两件事**:前者只说「本次列表不完整」(文件数上限 / 提交上限),
|
|
1116
|
+
* 后者说「走查被预算或取消打断,栈里还有没进去的子树」—— 只有后者才意味着那些路径
|
|
1117
|
+
* **一条都没进任何名单**,调用方必须据此让删除集 fail-closed。
|
|
966
1118
|
*/
|
|
967
1119
|
private listChunkFiles;
|
|
968
1120
|
private captureWorkspace;
|
|
969
1121
|
/** 用户忽略规则是否命中该路径(只认「相对分块根」的斜杠路径) */
|
|
1122
|
+
/**
|
|
1123
|
+
* 处理一个分块的候选文件(**两阶段**)。
|
|
1124
|
+
*
|
|
1125
|
+
* ① **顺序判定**(与旧的串行实现逐条一致,保证预算 / 计数 / 字节上限完全确定):
|
|
1126
|
+
* 文件数上限、时间预算、自身数据区、stat、单文件上限、总量上限、身份缓存 + 对象存在性
|
|
1127
|
+
* → 得出「当场入点(缓存命中)」还是「要真读」,该跳过的**当场**按原有 reason 逐条留痕。
|
|
1128
|
+
* stat 按**有序批**并发取(判定仍严格按顺序),并用「一次 readdir 的对象名单快照」代替
|
|
1129
|
+
* 每个文件一次 fs.access —— 这两处是稳态成本的大头(真机实测各占 ~5 s / ~3.7 s)。
|
|
1130
|
+
* ② **并发读 + 哈希 + 落对象仓**:这是捕获耗时的**唯一大头**(真机 148 s / 31,702 个对象,
|
|
1131
|
+
* 96% 的时间),用 captureConcurrency 个 worker 并发跑;并用**在飞字节闸门**保证
|
|
1132
|
+
* 「同时在飞的内容字节 ≤ captureInFlightBytes」。**不是** maxFileBytes —— 它缺省 =
|
|
1133
|
+
* 总量预算(512 MiB),拿它当闸门等于允许把整个点的预算同时驻留(0.1.74 闪退根因)。
|
|
1134
|
+
*
|
|
1135
|
+
* 返回本分块**真正入点**的文件数(调用方据此推进 maxFiles 预算)。
|
|
1136
|
+
* 没判到 / 没读完的条目一律逐条进 skipped(budget-time),绝不静默丢弃。
|
|
1137
|
+
*/
|
|
1138
|
+
private captureChunkCandidates;
|
|
1139
|
+
/**
|
|
1140
|
+
* 缺口优先排序:身份缓存里**没有**的路径排前面。
|
|
1141
|
+
*
|
|
1142
|
+
* 为什么:预算固定时按目录顺序走查,同一个「已覆盖且便宜」的前缀每回合都会吃掉全部预算,
|
|
1143
|
+
* 尾部永远进不来(真机:34k 文件每次只覆盖 1/5,且五次都是同一批 outputs/ 前缀)。
|
|
1144
|
+
* 未覆盖的排前面 ⇒ 每个点都在补上一次的缺口,覆盖率随回合**单调收敛**到 100%。
|
|
1145
|
+
* 组内保持走查顺序(稳定),因此同一输入下的结果仍然确定。
|
|
1146
|
+
*/
|
|
1147
|
+
private orderByCoverage;
|
|
970
1148
|
private ignoredByRules;
|
|
971
1149
|
/** 读文件(可注入 seam;只用于捕获期读内容这一步) */
|
|
972
1150
|
private readFileBytes;
|
|
@@ -978,6 +1156,13 @@ export declare class RollbackPointEngine {
|
|
|
978
1156
|
* 用毫秒换算同样不行:会把「同一毫秒内的两次写」当成同一个身份。
|
|
979
1157
|
* 读不到身份(文件消失 / 权限 / 平台不支持 bigint stat)→ null ⇒ 回落全量真读。
|
|
980
1158
|
*/
|
|
1159
|
+
/**
|
|
1160
|
+
* 一次 bigint stat 拿到阶段①需要的**全部**东西(是否文件 / 大小 / mtime / 身份)。
|
|
1161
|
+
* 旧实现「fs.stat + pathIdentityOf」每文件 stat 两次,真机 34k 文件白花约 5 s。
|
|
1162
|
+
*/
|
|
1163
|
+
private fileStatOf;
|
|
1164
|
+
/** 对象名单快照(每个捕获回合一次 readdir);读不到则返回 null ⇒ 调用方回落逐条判断 */
|
|
1165
|
+
private objectNamesSnapshot;
|
|
981
1166
|
private pathIdentityOf;
|
|
982
1167
|
private pathCachePath;
|
|
983
1168
|
/**
|
|
@@ -986,6 +1171,20 @@ export declare class RollbackPointEngine {
|
|
|
986
1171
|
*/
|
|
987
1172
|
private loadPathCache;
|
|
988
1173
|
/** 缓存命中:身份键存在、且路径逐字相同(身份被复用也绝不认)。 */
|
|
1174
|
+
/**
|
|
1175
|
+
* 「这条记录的当前内容**仍然是它**吗」的身份快路径 —— **预览与执行共用同一条判据**
|
|
1176
|
+
* (否则「预览说会改的 = 实际会改的」这条不变式会被打破)。
|
|
1177
|
+
*
|
|
1178
|
+
* 判据:路径身份(dev:ino:size:mtimeNs:ctimeNs:mode)命中**身份缓存**,且缓存里的 sha/bytes 与记录一致。
|
|
1179
|
+
* 身份未变 ⇒ 内容未变:缓存里的 sha 就是**这份内容**在留点那一次真读出来的哈希(身份含 size/mtimeNs/
|
|
1180
|
+
* ctimeNs/ino/dev/mode,改内容必动 mtime/ctime,Windows 上 user 代码改不了 ctime)。
|
|
1181
|
+
* 任何一环拿不到(读不到身份 / 缓存没有该身份 / sha 或 bytes 不同)⇒ false,调用方回落「真读 + 哈希」。
|
|
1182
|
+
*
|
|
1183
|
+
* 为什么必须有它:大仓库一次预览/回退要判 3 万+ 条目,逐个真读等于把整个工作区读两遍
|
|
1184
|
+
* (真机 34k 文件 / 609 MiB:预览 14.2 s 里 13 s 全在这,回退 60 s+ 同理)。**记录的指纹被篡改**
|
|
1185
|
+
* 时本判据必然不成立(缓存的 sha 是真算出来的)⇒ 仍走真读 + 哈希的旧路径,护栏不退化。
|
|
1186
|
+
*/
|
|
1187
|
+
private unchangedByIdentity;
|
|
989
1188
|
private lookupCachedFile;
|
|
990
1189
|
private rememberCachedFile;
|
|
991
1190
|
/** 缓存落盘(best-effort:写失败只意味着下次全量读,绝不影响留点成败) */
|
|
@@ -1060,6 +1259,20 @@ export declare class RollbackPointEngine {
|
|
|
1060
1259
|
* 诚实的边界:检测与写入之间仍有极小窗口,无跨进程锁时无法彻底消除 —— 这里保证的是
|
|
1061
1260
|
* 「不静默覆盖 + 尽量合并」,不是原子性。
|
|
1062
1261
|
*/
|
|
1262
|
+
/**
|
|
1263
|
+
* 把工作区载荷改写成**增量视图**(相对最近一个同分块的点)——清单增量化,省的是磁盘,不是语义。
|
|
1264
|
+
*
|
|
1265
|
+
* 判据与安全边界(逐条):
|
|
1266
|
+
* · 基准 = 清单里最近一个 `workspace.payload` 非空、**分块完全相同**的点(只试最近几个,代价有界);
|
|
1267
|
+
* · 增量 = 「相对基准新增 / 改写的条目」+「基准里不再存在的路径」,条目**逐字比较**(同形绝对路径);
|
|
1268
|
+
* · 只有增量明显更小(< 全量的 60%)才用;否则原样全量落盘;
|
|
1269
|
+
* · 读路径由 ledger 物化(`resolvePayload`)⇒ 预览 / 回退 / 回收看到的永远是完整清单;
|
|
1270
|
+
* · 基准读不出来 / 分块对不上 / 任何异常 ⇒ 原样全量(绝不因为省字节而让点不可用);
|
|
1271
|
+
* · 基准载荷的保活由 ledger 的 `payloadChain` 负责(淘汰时不会把基准删掉)。
|
|
1272
|
+
*/
|
|
1273
|
+
private storagePlanFor;
|
|
1274
|
+
/** 记下这份点作为下一次留点的增量基准(内存摘要,见 {@link deltaBase}) */
|
|
1275
|
+
private rememberDeltaBase;
|
|
1063
1276
|
private appendRecord;
|
|
1064
1277
|
/** `appendRecord` 的单次尝试;`conflict: true` = 这期间清单被别的实例改过,调用方应当重试。 */
|
|
1065
1278
|
private appendRecordOnce;
|
|
@@ -1116,6 +1329,11 @@ export declare class RollbackPointEngine {
|
|
|
1116
1329
|
error?: string;
|
|
1117
1330
|
storage: RollbackStorageStatus;
|
|
1118
1331
|
}>;
|
|
1332
|
+
/**
|
|
1333
|
+
* 预览的**单条只读判定**(阶段 A,可并发跑):一次 bigint lstat + 身份快路径。
|
|
1334
|
+
* `unchanged = true` 表示「已证明与记录一致」(身份命中身份缓存且 sha/bytes 相同)⇒ 调用方直接跳过。
|
|
1335
|
+
*/
|
|
1336
|
+
private previewScan;
|
|
1119
1337
|
private trackWarnings;
|
|
1120
1338
|
/**
|
|
1121
1339
|
* 「证明为留点之后新建」的完整规划 —— 预览(`preview`)与执行(`restoreWorkspace`)**共用这一份**。
|