nano-git 0.3.0 → 0.3.2

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.
Files changed (39) hide show
  1. package/README.md +6 -0
  2. package/dist/workdir/change-index-plan.mjs +76 -0
  3. package/dist/workdir/change-index.d.mts +21 -0
  4. package/dist/workdir/change-index.mjs +413 -0
  5. package/dist/workdir/core.d.mts +64 -63
  6. package/dist/workdir/directory-view.mjs +197 -0
  7. package/dist/workdir/dirty-dir-plan.mjs +73 -0
  8. package/dist/workdir/dirty-dir.d.mts +28 -0
  9. package/dist/workdir/dirty-dir.mjs +32 -0
  10. package/dist/workdir/file-backend.d.mts +34 -12
  11. package/dist/workdir/file-backend.mjs +161 -94
  12. package/dist/workdir/file.d.mts +2 -2
  13. package/dist/workdir/file.mjs +2 -2
  14. package/dist/workdir/ids.d.mts +1 -1
  15. package/dist/workdir/ids.mjs +3 -3
  16. package/dist/workdir/memory-backend.mjs +34 -50
  17. package/dist/workdir/memory.d.mts +2 -3
  18. package/dist/workdir/memory.mjs +2 -3
  19. package/dist/workdir/nodes.d.mts +7 -7
  20. package/dist/workdir/nodes.mjs +3 -9
  21. package/dist/workdir/overlay.d.mts +3 -3
  22. package/dist/workdir/sqlite-backend.d.mts +31 -14
  23. package/dist/workdir/sqlite-backend.mjs +238 -142
  24. package/dist/workdir/sqlite.d.mts +2 -2
  25. package/dist/workdir/sqlite.mjs +2 -2
  26. package/dist/workdir/state-store.d.mts +23 -12
  27. package/dist/workdir/workdir-path.mjs +348 -0
  28. package/dist/workdir/workdir-transaction.mjs +115 -0
  29. package/dist/workdir/workdir.d.mts +30 -0
  30. package/dist/workdir/workdir.mjs +280 -0
  31. package/dist/workdir/write-tree.mjs +108 -44
  32. package/package.json +2 -1
  33. package/dist/workdir/change-log.d.mts +0 -25
  34. package/dist/workdir/change-log.mjs +0 -69
  35. package/dist/workdir/memory-backend.d.mts +0 -17
  36. package/dist/workdir/session-id.mjs +0 -18
  37. package/dist/workdir/session-internal.mjs +0 -141
  38. package/dist/workdir/session.d.mts +0 -30
  39. package/dist/workdir/session.mjs +0 -407
@@ -1,82 +1,71 @@
1
+ import { sha1 } from "../core/types.mjs";
1
2
  import { createRootDirectoryNode } from "./nodes.mjs";
2
- import { createVirtualWorkdirSessionId } from "./session-id.mjs";
3
- import { openVirtualWorkdirSession } from "./session.mjs";
3
+ import { openVirtualWorkdir } from "./workdir.mjs";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  //#region src/workdir/file-backend.ts
7
7
  /**
8
- * Virtual Workdir 文件系统 backend
8
+ * Virtual Workdir 文件系统持久化实现
9
9
  */
10
- const FILE_WORKDIR_MANIFEST_VERSION = 1;
10
+ const FILE_WORKDIR_MANIFEST_VERSION = 5;
11
11
  const FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX = ".txn-snapshot";
12
12
  /**
13
- * 创建基于文件系统目录的 Virtual Workdir backend
13
+ * 打开基于目录持久化的 VirtualWorkdir
14
14
  *
15
15
  * @example
16
16
  * ```ts
17
- * const backend = createFileVirtualWorkdirBackend("/tmp/workdirs");
18
- * const sessionId = backend.createSession({ baseTree: tree });
19
- * const session = backend.openSession(repo.objects, sessionId);
20
- * expect(session.baseTree).toBe(tree);
17
+ * const workdir = openFileVirtualWorkdir(repo.objects, "/tmp/workdir", {
18
+ * baseTree: tree,
19
+ * create: true,
20
+ * });
21
+ * expect(workdir.baseTree).toBe(tree);
21
22
  * ```
22
23
  */
23
- function createFileVirtualWorkdirBackend(rootDir, options = {}) {
24
- const sessionsRoot = join(rootDir, options.sessionsDirName ?? "sessions");
25
- mkdirSync(sessionsRoot, { recursive: true });
26
- return {
27
- kind: "file",
28
- createSession(options) {
29
- const sessionId = createVirtualWorkdirSessionId();
30
- createFileVirtualWorkdirStateStore(sessionsRoot, sessionId).reset(options.baseTree);
31
- return sessionId;
32
- },
33
- openSession(source, sessionId) {
34
- if (!hasSession(sessionsRoot, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
35
- validateSessionIntegrity(sessionsRoot, sessionId);
36
- return openVirtualWorkdirSession(source, createFileVirtualWorkdirStateStore(sessionsRoot, sessionId));
37
- },
38
- deleteSession(sessionId) {
39
- if (!hasSession(sessionsRoot, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
40
- rmSync(getSessionDir(sessionsRoot, sessionId), {
41
- recursive: true,
42
- force: true
43
- });
44
- },
45
- listSessions() {
46
- if (!existsSync(sessionsRoot)) return [];
47
- return readdirSync(sessionsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => !entry.name.endsWith(FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX)).filter((entry) => existsSync(getManifestPath(join(sessionsRoot, entry.name)))).map((entry) => decodePathToken(entry.name)).filter((sessionId) => {
48
- try {
49
- validateSessionIntegrity(sessionsRoot, sessionId);
50
- return true;
51
- } catch {
52
- return false;
53
- }
54
- }).sort();
55
- }
56
- };
24
+ function openFileVirtualWorkdir(source, workdirDir, options) {
25
+ const store = createFileVirtualWorkdirStateStore(workdirDir);
26
+ if (!hasFileVirtualWorkdir(workdirDir)) {
27
+ if (options.create !== true) throw new Error(`Virtual workdir not found: ${workdirDir}`);
28
+ store.reset(options.baseTree);
29
+ }
30
+ validateFileVirtualWorkdirIntegrity(workdirDir);
31
+ return openVirtualWorkdir(source, store);
32
+ }
33
+ /**
34
+ * 删除指定目录上的持久化 VirtualWorkdir
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * deleteFileVirtualWorkdir("/tmp/workdir");
39
+ * ```
40
+ */
41
+ function deleteFileVirtualWorkdir(workdirDir) {
42
+ if (!hasFileVirtualWorkdir(workdirDir)) throw new Error(`Virtual workdir not found: ${workdirDir}`);
43
+ rmSync(workdirDir, {
44
+ recursive: true,
45
+ force: true
46
+ });
57
47
  }
58
48
  /**
59
- * 创建单个 session 的文件系统状态存储
49
+ * 创建单个文件系统 VirtualWorkdir 的状态存储
60
50
  *
61
51
  * @example
62
52
  * ```ts
63
- * const store = createFileVirtualWorkdirStateStore("/tmp/workdirs/sessions", sessionId);
53
+ * const store = createFileVirtualWorkdirStateStore("/tmp/workdir");
64
54
  * expect(store.kind).toBe("file");
65
55
  * ```
66
56
  */
67
- function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
68
- const sessionDir = getSessionDir(sessionsRoot, sessionId);
69
- const manifestPath = getManifestPath(sessionDir);
70
- const contentDir = getContentDir(sessionDir);
57
+ function createFileVirtualWorkdirStateStore(workdirDir) {
58
+ const manifestPath = getManifestPath(workdirDir);
59
+ const contentDir = getContentDir(workdirDir);
71
60
  return {
72
61
  kind: "file",
73
62
  transact(fn) {
74
- const snapshotDir = `${sessionDir}${FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX}`;
63
+ const snapshotDir = `${workdirDir}${FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX}`;
75
64
  rmSync(snapshotDir, {
76
65
  recursive: true,
77
66
  force: true
78
67
  });
79
- if (existsSync(sessionDir)) copyDirectoryRecursive(sessionDir, snapshotDir);
68
+ if (existsSync(workdirDir)) copyDirectoryRecursive(workdirDir, snapshotDir);
80
69
  try {
81
70
  const result = fn();
82
71
  rmSync(snapshotDir, {
@@ -85,11 +74,11 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
85
74
  });
86
75
  return result;
87
76
  } catch (error) {
88
- rmSync(sessionDir, {
77
+ rmSync(workdirDir, {
89
78
  recursive: true,
90
79
  force: true
91
80
  });
92
- if (existsSync(snapshotDir)) renameSync(snapshotDir, sessionDir);
81
+ if (existsSync(snapshotDir)) renameSync(snapshotDir, workdirDir);
93
82
  throw error;
94
83
  }
95
84
  },
@@ -97,7 +86,7 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
97
86
  return readManifest(manifestPath).baseTree;
98
87
  },
99
88
  writeBaseTree(baseTree) {
100
- updateManifest(sessionDir, (manifest) => ({
89
+ updateManifest(workdirDir, (manifest) => ({
101
90
  ...manifest,
102
91
  baseTree
103
92
  }));
@@ -108,7 +97,7 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
108
97
  return restoreNode(record, contentDir);
109
98
  },
110
99
  setNode(node) {
111
- updateManifest(sessionDir, (manifest) => {
100
+ updateManifest(workdirDir, (manifest) => {
112
101
  const record = persistNode(contentDir, node);
113
102
  return {
114
103
  ...manifest,
@@ -120,7 +109,7 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
120
109
  });
121
110
  },
122
111
  deleteNode(id) {
123
- updateManifest(sessionDir, (manifest) => {
112
+ updateManifest(workdirDir, (manifest) => {
124
113
  if (manifest.nodes[id] === void 0) return manifest;
125
114
  const { [id]: _deleted, ...rest } = manifest.nodes;
126
115
  return {
@@ -129,56 +118,79 @@ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
129
118
  };
130
119
  });
131
120
  },
132
- appendChange(record) {
133
- updateManifest(sessionDir, (manifest) => ({
121
+ listChangeRecords() {
122
+ return readManifest(manifestPath).changeRecords.map(restoreChangeRecord);
123
+ },
124
+ getChangeRecord(path) {
125
+ return readManifest(manifestPath).changeRecords.map(restoreChangeRecord).find((record) => record.path === path) ?? null;
126
+ },
127
+ setChangeRecord(record) {
128
+ updateManifest(workdirDir, (manifest) => {
129
+ const others = manifest.changeRecords.filter((item) => item.path !== record.path);
130
+ return {
131
+ ...manifest,
132
+ changeRecords: [...others, serializeChangeRecord(record)].sort((left, right) => left.path.localeCompare(right.path))
133
+ };
134
+ });
135
+ },
136
+ deleteChangeRecord(path) {
137
+ updateManifest(workdirDir, (manifest) => ({
134
138
  ...manifest,
135
- changes: [...manifest.changes, record]
139
+ changeRecords: manifest.changeRecords.filter((item) => item.path !== path)
136
140
  }));
137
141
  },
138
- listChangeRecords() {
139
- return readManifest(manifestPath).changes;
142
+ listDirtyDirSummaries() {
143
+ return readManifest(manifestPath).dirtyDirSummaries.map(restoreDirtyDirSummary);
144
+ },
145
+ getDirtyDirSummary(path) {
146
+ return readManifest(manifestPath).dirtyDirSummaries.map(restoreDirtyDirSummary).find((summary) => summary.path === path) ?? null;
147
+ },
148
+ setDirtyDirSummary(summary) {
149
+ updateManifest(workdirDir, (manifest) => {
150
+ const others = manifest.dirtyDirSummaries.filter((item) => item.path !== summary.path);
151
+ return {
152
+ ...manifest,
153
+ dirtyDirSummaries: [...others, serializeDirtyDirSummary(summary)].sort((left, right) => left.path.localeCompare(right.path))
154
+ };
155
+ });
140
156
  },
141
- clearChanges() {
142
- updateManifest(sessionDir, (manifest) => ({
157
+ deleteDirtyDirSummary(path) {
158
+ updateManifest(workdirDir, (manifest) => ({
143
159
  ...manifest,
144
- changes: []
160
+ dirtyDirSummaries: manifest.dirtyDirSummaries.filter((item) => item.path !== path)
145
161
  }));
146
162
  },
147
163
  reset(baseTree) {
148
- rmSync(sessionDir, {
164
+ rmSync(workdirDir, {
149
165
  recursive: true,
150
166
  force: true
151
167
  });
152
- ensureSessionDirs(sessionDir, contentDir);
168
+ ensureWorkdirDirs(workdirDir, contentDir);
153
169
  writeManifestAtomic(manifestPath, createManifest(baseTree, { [createRootDirectoryNode(baseTree).id]: serializeDirectoryNode(createRootDirectoryNode(baseTree)) }));
154
170
  }
155
171
  };
156
172
  }
157
- function hasSession(sessionsRoot, sessionId) {
158
- return existsSync(getManifestPath(getSessionDir(sessionsRoot, sessionId)));
173
+ function hasFileVirtualWorkdir(workdirDir) {
174
+ return existsSync(getManifestPath(workdirDir));
159
175
  }
160
- function validateSessionIntegrity(sessionsRoot, sessionId) {
161
- const sessionDir = getSessionDir(sessionsRoot, sessionId);
162
- const manifest = readManifest(getManifestPath(sessionDir));
176
+ function validateFileVirtualWorkdirIntegrity(workdirDir) {
177
+ const manifest = readManifest(getManifestPath(workdirDir));
163
178
  const root = manifest.nodes.root;
164
- if (root === void 0) throw new Error(`Virtual workdir session is corrupted: missing root node for ${sessionId}`);
165
- if (restoreNode(root, getContentDir(sessionDir)).state.kind !== "directory") throw new Error(`Virtual workdir session is corrupted: root node is not a directory for ${sessionId}`);
166
- for (const record of Object.values(manifest.nodes)) restoreNode(record, getContentDir(sessionDir));
179
+ if (root === void 0) throw new Error(`Virtual workdir is corrupted: missing root node for ${workdirDir}`);
180
+ if (restoreNode(root, getContentDir(workdirDir)).state.kind !== "directory") throw new Error(`Virtual workdir is corrupted: root node is not a directory for ${workdirDir}`);
181
+ for (const record of Object.values(manifest.nodes)) restoreNode(record, getContentDir(workdirDir));
167
182
  }
168
- function getSessionDir(sessionsRoot, sessionId) {
169
- return join(sessionsRoot, encodePathToken(sessionId));
183
+ function getManifestPath(workdirDir) {
184
+ return join(workdirDir, "manifest.json");
170
185
  }
171
- function getManifestPath(sessionDir) {
172
- return join(sessionDir, "manifest.json");
173
- }
174
- function getContentDir(sessionDir) {
175
- return join(sessionDir, "content");
186
+ function getContentDir(workdirDir) {
187
+ return join(workdirDir, "content");
176
188
  }
177
189
  function getContentPath(contentDir, payloadRef) {
178
190
  return join(contentDir, `${encodePathToken(payloadRef)}.bin`);
179
191
  }
180
- function ensureSessionDirs(sessionDir, contentDir) {
181
- mkdirSync(sessionDir, { recursive: true });
192
+ function ensureWorkdirDirs(workdirDir, contentDir) {
193
+ mkdirSync(workdirDir, { recursive: true });
182
194
  mkdirSync(contentDir, { recursive: true });
183
195
  }
184
196
  function copyDirectoryRecursive(sourceDir, targetDir) {
@@ -197,28 +209,30 @@ function createManifest(baseTree, nodes) {
197
209
  return {
198
210
  formatVersion: FILE_WORKDIR_MANIFEST_VERSION,
199
211
  baseTree,
200
- changes: [],
201
- nodes
212
+ nodes,
213
+ changeRecords: [],
214
+ dirtyDirSummaries: []
202
215
  };
203
216
  }
204
- function updateManifest(sessionDir, updater) {
205
- const manifestPath = getManifestPath(sessionDir);
206
- ensureSessionDirs(sessionDir, getContentDir(sessionDir));
217
+ function updateManifest(workdirDir, updater) {
218
+ const manifestPath = getManifestPath(workdirDir);
219
+ ensureWorkdirDirs(workdirDir, getContentDir(workdirDir));
207
220
  writeManifestAtomic(manifestPath, updater(readManifest(manifestPath)));
208
221
  }
209
222
  function writeManifestAtomic(path, manifest) {
210
223
  writeJsonAtomic(path, manifest);
211
224
  }
212
225
  function readManifest(manifestPath) {
213
- if (!existsSync(manifestPath)) throw new Error(`Virtual workdir session manifest not found: ${manifestPath}`);
226
+ if (!existsSync(manifestPath)) throw new Error(`Virtual workdir manifest not found: ${manifestPath}`);
214
227
  const manifest = readJson(manifestPath);
215
228
  validateManifest(manifest);
216
229
  return manifest;
217
230
  }
218
231
  function validateManifest(manifest) {
219
232
  if (typeof manifest.baseTree !== "string" || manifest.baseTree.length === 0) throw new Error("Invalid virtual workdir manifest baseTree");
220
- if (!Array.isArray(manifest.changes)) throw new Error("Invalid virtual workdir manifest changes");
221
233
  if (typeof manifest.nodes !== "object" || manifest.nodes === null || Array.isArray(manifest.nodes)) throw new Error("Invalid virtual workdir manifest nodes");
234
+ if (!Array.isArray(manifest.changeRecords)) throw new Error("Invalid virtual workdir manifest changeRecords");
235
+ if (!Array.isArray(manifest.dirtyDirSummaries)) throw new Error("Invalid virtual workdir manifest dirtyDirSummaries");
222
236
  if (manifest.formatVersion !== FILE_WORKDIR_MANIFEST_VERSION) throw new Error(`Unsupported virtual workdir file manifest version: expected ${FILE_WORKDIR_MANIFEST_VERSION}, got ${manifest.formatVersion}`);
223
237
  }
224
238
  function persistNode(contentDir, node) {
@@ -339,6 +353,62 @@ function restoreOrigin(record) {
339
353
  hash: origin.hash
340
354
  };
341
355
  }
356
+ function serializeChangeRecord(record) {
357
+ return {
358
+ path: record.path,
359
+ previous: record.previous,
360
+ current: record.current,
361
+ source: record.source
362
+ };
363
+ }
364
+ function restoreChangeRecord(record) {
365
+ return {
366
+ path: record.path,
367
+ previous: record.previous === null ? null : {
368
+ ...record.previous,
369
+ hash: record.previous.hash
370
+ },
371
+ current: record.current === null ? null : {
372
+ ...record.current,
373
+ hash: record.current.hash
374
+ },
375
+ source: record.source
376
+ };
377
+ }
378
+ function serializeDirtyDirSummary(summary) {
379
+ return {
380
+ path: summary.path,
381
+ isDirty: summary.isDirty,
382
+ dirtyEntryCount: summary.dirtyEntryCount,
383
+ dirtyDescendantCount: summary.dirtyDescendantCount,
384
+ affectedNames: [...summary.affectedNames],
385
+ currentTreeHash: summary.currentTreeHash,
386
+ hashState: summary.hashState
387
+ };
388
+ }
389
+ function restoreDirtyDirSummary(summary) {
390
+ return {
391
+ path: summary.path,
392
+ isDirty: summary.isDirty,
393
+ dirtyEntryCount: readDirtyDirCount(summary.dirtyEntryCount, "dirtyEntryCount"),
394
+ dirtyDescendantCount: readDirtyDirCount(summary.dirtyDescendantCount, "dirtyDescendantCount"),
395
+ affectedNames: readDirtyDirAffectedNames(summary.affectedNames),
396
+ currentTreeHash: summary.currentTreeHash === null ? null : sha1(summary.currentTreeHash),
397
+ hashState: readDirtyDirHashState(summary.hashState)
398
+ };
399
+ }
400
+ function readDirtyDirCount(raw, field) {
401
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) throw new Error(`Invalid file workdir dirty dir summary ${field}`);
402
+ return raw;
403
+ }
404
+ function readDirtyDirAffectedNames(raw) {
405
+ if (!Array.isArray(raw) || raw.some((item) => typeof item !== "string")) throw new Error("Invalid file workdir dirty dir summary affectedNames");
406
+ return [...raw].sort((left, right) => left.localeCompare(right));
407
+ }
408
+ function readDirtyDirHashState(raw) {
409
+ if (raw === "stale" || raw === "materialized") return raw;
410
+ throw new Error(`Invalid file workdir dirty dir summary hashState: ${String(raw)}`);
411
+ }
342
412
  function readPayload(contentDir, payloadRef) {
343
413
  const path = getContentPath(contentDir, payloadRef);
344
414
  if (!existsSync(path)) throw new Error(`Virtual workdir payload not found: ${payloadRef}`);
@@ -359,8 +429,5 @@ function writeBufferAtomic(path, value) {
359
429
  function encodePathToken(value) {
360
430
  return encodeURIComponent(value);
361
431
  }
362
- function decodePathToken(value) {
363
- return decodeURIComponent(value);
364
- }
365
432
  //#endregion
366
- export { createFileVirtualWorkdirBackend };
433
+ export { createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir };
@@ -1,2 +1,2 @@
1
- import { CreateFileVirtualWorkdirBackendOptions, createFileVirtualWorkdirBackend } from "./file-backend.mjs";
2
- export { type CreateFileVirtualWorkdirBackendOptions, createFileVirtualWorkdirBackend };
1
+ import { OpenFileVirtualWorkdirOptions, createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir } from "./file-backend.mjs";
2
+ export { type OpenFileVirtualWorkdirOptions, createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir };
@@ -1,2 +1,2 @@
1
- import { createFileVirtualWorkdirBackend } from "./file-backend.mjs";
2
- export { createFileVirtualWorkdirBackend };
1
+ import { createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir } from "./file-backend.mjs";
2
+ export { createFileVirtualWorkdirStateStore, deleteFileVirtualWorkdir, openFileVirtualWorkdir };
@@ -1,5 +1,5 @@
1
1
  //#region src/workdir/ids.d.ts
2
- /** 会话内节点的稳定身份 */
2
+ /** workdir 内节点的稳定身份 */
3
3
  type NodeId = string & {
4
4
  readonly __brand: "NodeId";
5
5
  };
@@ -1,9 +1,9 @@
1
1
  //#region src/workdir/ids.ts
2
- /** 根目录节点的固定 ID(每个 session 一致) */
2
+ /** 根目录节点的固定 ID(每个 workdir 一致) */
3
3
  const VIRTUAL_ROOT_NODE_ID = "root";
4
4
  let nextNodeCounter = 1;
5
5
  /**
6
- * 分配新的 session 节点 ID
6
+ * 分配新的 workdir 节点 ID
7
7
  */
8
8
  function createNodeId() {
9
9
  const id = `node:${nextNodeCounter}`;
@@ -11,7 +11,7 @@ function createNodeId() {
11
11
  return id;
12
12
  }
13
13
  /**
14
- * 为仓库对象哈希派生稳定的 session 节点 ID(懒加载、未 copy 前复用)
14
+ * 为仓库对象哈希派生稳定的 workdir 节点 ID(懒加载、未 copy 前复用)
15
15
  */
16
16
  function originBackedNodeId(hash) {
17
17
  return `origin:${hash}`;
@@ -1,8 +1,5 @@
1
- import { createVirtualChangeLog } from "./change-log.mjs";
2
1
  import { VIRTUAL_ROOT_NODE_ID } from "./ids.mjs";
3
2
  import { createRootDirectoryNode } from "./nodes.mjs";
4
- import { createVirtualWorkdirSessionId } from "./session-id.mjs";
5
- import { openVirtualWorkdirSession } from "./session.mjs";
6
3
  //#region src/workdir/memory-backend.ts
7
4
  /**
8
5
  * Virtual Workdir 内存状态存储
@@ -20,7 +17,8 @@ function createVirtualWorkdirMemoryStateStore(baseTree) {
20
17
  const state = {
21
18
  baseTree,
22
19
  nodes: /* @__PURE__ */ new Map(),
23
- changeLog: createVirtualChangeLog()
20
+ changeRecords: /* @__PURE__ */ new Map(),
21
+ dirtyDirSummaries: /* @__PURE__ */ new Map()
24
22
  };
25
23
  resetState(state, baseTree);
26
24
  return {
@@ -49,76 +47,62 @@ function createVirtualWorkdirMemoryStateStore(baseTree) {
49
47
  deleteNode(id) {
50
48
  state.nodes.delete(id);
51
49
  },
52
- appendChange(record) {
53
- state.changeLog.append(record);
54
- },
55
50
  listChangeRecords() {
56
- return state.changeLog.snapshot();
51
+ return Array.from(state.changeRecords.values()).sort((left, right) => left.path.localeCompare(right.path));
57
52
  },
58
- clearChanges() {
59
- state.changeLog.clear();
53
+ getChangeRecord(path) {
54
+ return state.changeRecords.get(path) ?? null;
60
55
  },
61
- reset(nextBaseTree) {
62
- resetState(state, nextBaseTree);
63
- }
64
- };
65
- }
66
- /**
67
- * 创建内存版 Virtual Workdir backend
68
- *
69
- * @example
70
- * ```ts
71
- * const backend = createMemoryVirtualWorkdirBackend();
72
- * const sessionId = backend.createSession({ baseTree: tree });
73
- * const session = backend.openSession(repo.objects, sessionId);
74
- * expect(session.baseTree).toBe(tree);
75
- * ```
76
- */
77
- function createMemoryVirtualWorkdirBackend() {
78
- const sessions = /* @__PURE__ */ new Map();
79
- return {
80
- kind: "memory",
81
- createSession(options) {
82
- const sessionId = createVirtualWorkdirSessionId();
83
- sessions.set(sessionId, createVirtualWorkdirMemoryStateStore(options.baseTree));
84
- return sessionId;
56
+ setChangeRecord(record) {
57
+ state.changeRecords.set(record.path, record);
58
+ },
59
+ deleteChangeRecord(path) {
60
+ state.changeRecords.delete(path);
85
61
  },
86
- openSession(source, sessionId) {
87
- const store = sessions.get(sessionId);
88
- if (store === void 0) throw new Error(`Virtual workdir session not found: ${sessionId}`);
89
- return openVirtualWorkdirSession(source, store);
62
+ listDirtyDirSummaries() {
63
+ return Array.from(state.dirtyDirSummaries.values()).sort((left, right) => left.path.localeCompare(right.path));
90
64
  },
91
- deleteSession(sessionId) {
92
- if (!sessions.delete(sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
65
+ getDirtyDirSummary(path) {
66
+ return state.dirtyDirSummaries.get(path) ?? null;
93
67
  },
94
- listSessions() {
95
- return Array.from(sessions.keys());
68
+ setDirtyDirSummary(summary) {
69
+ state.dirtyDirSummaries.set(summary.path, summary);
70
+ },
71
+ deleteDirtyDirSummary(path) {
72
+ state.dirtyDirSummaries.delete(path);
73
+ },
74
+ reset(nextBaseTree) {
75
+ resetState(state, nextBaseTree);
96
76
  }
97
77
  };
98
78
  }
99
79
  function resetState(state, baseTree) {
100
80
  state.baseTree = baseTree;
101
81
  state.nodes.clear();
82
+ state.changeRecords.clear();
83
+ state.dirtyDirSummaries.clear();
102
84
  state.nodes.set(VIRTUAL_ROOT_NODE_ID, createRootDirectoryNode(baseTree));
103
- state.changeLog.clear();
104
85
  }
105
86
  function snapshotState(state) {
106
87
  const nodes = /* @__PURE__ */ new Map();
107
- for (const [nodeId, node] of state.nodes) nodes.set(nodeId, cloneSessionNode(node));
88
+ for (const [nodeId, node] of state.nodes) nodes.set(nodeId, cloneWorkdirNode(node));
108
89
  return {
109
90
  baseTree: state.baseTree,
110
91
  nodes,
111
- changes: state.changeLog.snapshot()
92
+ changeRecords: new Map(state.changeRecords),
93
+ dirtyDirSummaries: new Map(state.dirtyDirSummaries)
112
94
  };
113
95
  }
114
96
  function restoreState(state, snapshot) {
115
97
  state.baseTree = snapshot.baseTree;
116
98
  state.nodes.clear();
117
- for (const [nodeId, node] of snapshot.nodes) state.nodes.set(nodeId, cloneSessionNode(node));
118
- state.changeLog.clear();
119
- for (const record of snapshot.changes) state.changeLog.append(record);
99
+ state.changeRecords.clear();
100
+ state.dirtyDirSummaries.clear();
101
+ for (const [nodeId, node] of snapshot.nodes) state.nodes.set(nodeId, cloneWorkdirNode(node));
102
+ for (const [path, record] of snapshot.changeRecords) state.changeRecords.set(path, record);
103
+ for (const [path, summary] of snapshot.dirtyDirSummaries) state.dirtyDirSummaries.set(path, summary);
120
104
  }
121
- function cloneSessionNode(node) {
105
+ function cloneWorkdirNode(node) {
122
106
  if (node.state.kind === "directory") return {
123
107
  id: node.id,
124
108
  origin: node.origin,
@@ -156,4 +140,4 @@ function cloneSessionNode(node) {
156
140
  };
157
141
  }
158
142
  //#endregion
159
- export { createMemoryVirtualWorkdirBackend, createVirtualWorkdirMemoryStateStore };
143
+ export { createVirtualWorkdirMemoryStateStore };
@@ -1,3 +1,2 @@
1
- import { createMemoryVirtualWorkdirBackend } from "./memory-backend.mjs";
2
- import { createVirtualWorkdirSession, openVirtualWorkdirSession } from "./session.mjs";
3
- export { createMemoryVirtualWorkdirBackend, createVirtualWorkdirSession, openVirtualWorkdirSession };
1
+ import { createVirtualWorkdir, openVirtualWorkdir } from "./workdir.mjs";
2
+ export { createVirtualWorkdir, openVirtualWorkdir };
@@ -1,3 +1,2 @@
1
- import { createVirtualWorkdirSession, openVirtualWorkdirSession } from "./session.mjs";
2
- import { createMemoryVirtualWorkdirBackend } from "./memory-backend.mjs";
3
- export { createMemoryVirtualWorkdirBackend, createVirtualWorkdirSession, openVirtualWorkdirSession };
1
+ import { createVirtualWorkdir, openVirtualWorkdir } from "./workdir.mjs";
2
+ export { createVirtualWorkdir, openVirtualWorkdir };
@@ -6,7 +6,7 @@ import { DirectoryOverlay } from "./overlay.mjs";
6
6
  /** Blob / 符号链接在 origin 与 state 中使用的 mode */
7
7
  type BlobObjectMode = "100644" | "100755" | "120000";
8
8
  /**
9
- * 节点来源(repo 对象或纯 session 新建)
9
+ * 节点来源(repo 对象或纯 workdir 新建)
10
10
  */
11
11
  type NodeOrigin = {
12
12
  readonly kind: "none";
@@ -21,7 +21,7 @@ type NodeOrigin = {
21
21
  /**
22
22
  * 目录节点当前状态
23
23
  *
24
- * `overlay` 表达 session 层增删改;子项 nodeId 通过 overlay 合成与懒加载解析。
24
+ * `overlay` 表达 workdir 层增删改;子项 nodeId 通过 overlay 合成与懒加载解析。
25
25
  */
26
26
  interface DirectoryNodeState {
27
27
  readonly kind: "directory";
@@ -44,14 +44,14 @@ interface SymlinkNodeState {
44
44
  readonly mode: "120000";
45
45
  readonly target?: Buffer;
46
46
  }
47
- type SessionNodeState = DirectoryNodeState | FileNodeState | SymlinkNodeState;
47
+ type WorkdirNodeState = DirectoryNodeState | FileNodeState | SymlinkNodeState;
48
48
  /**
49
- * 完整的会话节点记录
49
+ * 完整的 workdir 节点记录
50
50
  */
51
- interface SessionNode {
51
+ interface WorkdirNode {
52
52
  readonly id: NodeId;
53
53
  readonly origin: NodeOrigin;
54
- readonly state: SessionNodeState;
54
+ readonly state: WorkdirNodeState;
55
55
  }
56
56
  //#endregion
57
- export { SessionNode };
57
+ export { WorkdirNode };
@@ -2,7 +2,7 @@ import { VIRTUAL_ROOT_NODE_ID } from "./ids.mjs";
2
2
  import { cloneDirectoryOverlay, createEmptyDirectoryOverlay } from "./overlay.mjs";
3
3
  //#region src/workdir/nodes.ts
4
4
  /**
5
- * Virtual Workdir 会话节点状态模型
5
+ * Virtual Workdir 节点状态模型
6
6
  *
7
7
  * 节点身份(nodeId)与目录路径绑定分离;origin 描述 repo-backed 来源。
8
8
  */
@@ -29,12 +29,6 @@ function nodeHasRepoOrigin(node) {
29
29
  return node.origin.kind === "repo-tree" || node.origin.kind === "repo-blob";
30
30
  }
31
31
  /**
32
- * 目录 overlay 是否有 session 层修改
33
- */
34
- function isDirectoryOverlayDirty(overlay) {
35
- return overlay.addedEntries.size > 0 || overlay.deletedNames.size > 0;
36
- }
37
- /**
38
32
  * 将节点状态恢复到 origin 语义(丢弃 materialize / 目录 overlay)
39
33
  *
40
34
  * 无 repo origin 时返回原状态引用,由上层决定是否抛错。
@@ -69,7 +63,7 @@ function revertNodeState(node) {
69
63
  /**
70
64
  * 为 `copy` 创建新节点:共享 origin,目录为浅复制(子项绑定保留,但 nodeId 为新)
71
65
  */
72
- function cloneSessionNodeForCopy(source, newId) {
66
+ function cloneWorkdirNodeForCopy(source, newId) {
73
67
  const origin = source.origin;
74
68
  if (source.state.kind === "directory") return {
75
69
  id: newId,
@@ -109,4 +103,4 @@ function cloneSessionNodeForCopy(source, newId) {
109
103
  };
110
104
  }
111
105
  //#endregion
112
- export { cloneSessionNodeForCopy, createRootDirectoryNode, isDirectoryOverlayDirty, revertNodeState };
106
+ export { cloneWorkdirNodeForCopy, createRootDirectoryNode, revertNodeState };
@@ -2,12 +2,12 @@ import { NodeId } from "./ids.mjs";
2
2
 
3
3
  //#region src/workdir/overlay.d.ts
4
4
  /**
5
- * 目录 overlay 状态(挂在目录 SessionNode 上)
5
+ * 目录 overlay 状态(挂在目录 WorkdirNode 上)
6
6
  */
7
7
  interface DirectoryOverlay {
8
- /** session 新增或覆盖:条目名 -> 绑定的 nodeId */
8
+ /** workdir 新增或覆盖:条目名 -> 绑定的 nodeId */
9
9
  readonly addedEntries: Map<string, NodeId>;
10
- /** session 删除的 origin/先前条目名(tombstone) */
10
+ /** workdir 删除的 origin/先前条目名(tombstone) */
11
11
  readonly deletedNames: Set<string>;
12
12
  }
13
13
  //#endregion