nano-git 0.3.1 → 0.3.3

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 +1 -1
  2. package/dist/core/types.d.mts +1 -1
  3. package/dist/objects/tree.d.mts +4 -0
  4. package/dist/objects/tree.mjs +53 -2
  5. package/dist/repository/tree/tree-patch.mjs +4 -4
  6. package/dist/repository/tree/tree-walk.mjs +1 -1
  7. package/dist/repository/tree/tree-writer.mjs +1 -1
  8. package/dist/workdir/change-index-plan.mjs +2 -2
  9. package/dist/workdir/change-index.mjs +6 -6
  10. package/dist/workdir/core.d.mts +13 -44
  11. package/dist/workdir/directory-view.mjs +2 -2
  12. package/dist/workdir/dirty-dir-plan.mjs +3 -3
  13. package/dist/workdir/file-backend.d.mts +34 -12
  14. package/dist/workdir/file-backend.mjs +67 -86
  15. package/dist/workdir/file.d.mts +2 -2
  16. package/dist/workdir/file.mjs +2 -2
  17. package/dist/workdir/ids.d.mts +1 -1
  18. package/dist/workdir/ids.mjs +3 -3
  19. package/dist/workdir/memory-backend.mjs +4 -39
  20. package/dist/workdir/memory.d.mts +2 -3
  21. package/dist/workdir/memory.mjs +2 -3
  22. package/dist/workdir/nodes.d.mts +7 -7
  23. package/dist/workdir/nodes.mjs +3 -3
  24. package/dist/workdir/origin.mjs +2 -2
  25. package/dist/workdir/overlay.d.mts +3 -3
  26. package/dist/workdir/sqlite-backend.d.mts +31 -14
  27. package/dist/workdir/sqlite-backend.mjs +113 -118
  28. package/dist/workdir/sqlite.d.mts +2 -2
  29. package/dist/workdir/sqlite.mjs +2 -2
  30. package/dist/workdir/state-store.d.mts +4 -4
  31. package/dist/workdir/{session-internal.mjs → workdir-path.mjs} +10 -10
  32. package/dist/workdir/{session-transaction.mjs → workdir-transaction.mjs} +10 -10
  33. package/dist/workdir/workdir.d.mts +30 -0
  34. package/dist/workdir/{session.mjs → workdir.mjs} +17 -17
  35. package/dist/workdir/write-tree.mjs +5 -5
  36. package/package.json +1 -1
  37. package/dist/workdir/memory-backend.d.mts +0 -17
  38. package/dist/workdir/session-id.mjs +0 -18
  39. package/dist/workdir/session.d.mts +0 -30
@@ -1,90 +1,85 @@
1
1
  import { sha1 } from "../core/types.mjs";
2
2
  import { createRootDirectoryNode } from "./nodes.mjs";
3
- import { createVirtualWorkdirSessionId } from "./session-id.mjs";
4
- import { openVirtualWorkdirSession } from "./session.mjs";
3
+ import { openVirtualWorkdir } from "./workdir.mjs";
5
4
  import { Database } from "bun:sqlite";
6
5
  //#region src/workdir/sqlite-backend.ts
7
6
  /**
8
7
  * Virtual Workdir SQLite backend
9
8
  */
10
- const WORKDIR_SQLITE_SCHEMA_VERSION = 5;
9
+ const WORKDIR_SQLITE_SCHEMA_VERSION = 6;
11
10
  /**
12
- * 创建基于 SQLite Virtual Workdir backend
11
+ * 打开基于 SQLite 的持久化 VirtualWorkdir
13
12
  *
14
13
  * @example
15
14
  * ```ts
16
- * using backend = createSqliteVirtualWorkdirBackend(":memory:");
17
- * const sessionId = backend.createSession({ baseTree: tree });
18
- * const session = backend.openSession(repo.objects, sessionId);
19
- * expect(session.baseTree).toBe(tree);
15
+ * using workdir = openSqliteVirtualWorkdir(repo.objects, ":memory:", "demo", {
16
+ * baseTree: tree,
17
+ * create: true,
18
+ * });
19
+ * expect(workdir.baseTree).toBe(tree);
20
20
  * ```
21
21
  */
22
- function createSqliteVirtualWorkdirBackend(dbPath, options = {}) {
22
+ function openSqliteVirtualWorkdir(source, dbPath, workdirKey, options) {
23
23
  const db = new Database(dbPath);
24
- let disposed = false;
25
24
  if (options.walMode !== false) db.run("PRAGMA journal_mode = WAL");
26
25
  ensureSchema(db);
27
- return {
28
- kind: "sqlite",
29
- createSession(options) {
30
- assertBackendAvailable(disposed);
31
- const sessionId = createVirtualWorkdirSessionId();
32
- createSqliteVirtualWorkdirStateStore(db, sessionId).reset(options.baseTree);
33
- return sessionId;
34
- },
35
- openSession(source, sessionId) {
36
- assertBackendAvailable(disposed);
37
- if (!hasSession(db, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
38
- validateSessionIntegrity(db, sessionId);
39
- return openVirtualWorkdirSession(source, createSqliteVirtualWorkdirStateStore(db, sessionId));
40
- },
41
- deleteSession(sessionId) {
42
- assertBackendAvailable(disposed);
43
- if (!hasSession(db, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
44
- deleteSessionRows(db, sessionId);
45
- },
46
- listSessions() {
47
- assertBackendAvailable(disposed);
48
- return db.query("SELECT session_id FROM workdir_sessions ORDER BY session_id").all().map((row) => row.session_id).filter((sessionId) => {
49
- try {
50
- validateSessionIntegrity(db, sessionId);
51
- return true;
52
- } catch {
53
- return false;
54
- }
55
- });
56
- },
57
- [Symbol.dispose]() {
58
- if (disposed) return;
59
- disposed = true;
26
+ const store = createSqliteVirtualWorkdirStateStore(db, workdirKey);
27
+ if (!hasWorkdir(db, workdirKey)) {
28
+ if (options.create !== true) {
60
29
  db.close();
30
+ throw new Error(`Virtual workdir not found: ${workdirKey}`);
61
31
  }
62
- };
32
+ store.reset(options.baseTree);
33
+ }
34
+ validateWorkdirIntegrity(db, workdirKey);
35
+ let disposed = false;
36
+ const workdir = openVirtualWorkdir(source, store);
37
+ return Object.assign(workdir, { [Symbol.dispose]() {
38
+ if (disposed) return;
39
+ disposed = true;
40
+ db.close();
41
+ } });
63
42
  }
64
- function assertBackendAvailable(disposed) {
65
- if (disposed) throw new Error("SQLite virtual workdir backend is disposed");
43
+ /**
44
+ * 删除指定 key 上的 SQLite VirtualWorkdir
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * deleteSqliteVirtualWorkdir("/tmp/workdir.sqlite", "demo");
49
+ * ```
50
+ */
51
+ function deleteSqliteVirtualWorkdir(dbPath, workdirKey, options = {}) {
52
+ const db = new Database(dbPath);
53
+ try {
54
+ if (options.walMode !== false) db.run("PRAGMA journal_mode = WAL");
55
+ ensureSchema(db);
56
+ if (!hasWorkdir(db, workdirKey)) throw new Error(`Virtual workdir not found: ${workdirKey}`);
57
+ deleteWorkdirRows(db, workdirKey);
58
+ } finally {
59
+ db.close();
60
+ }
66
61
  }
67
62
  /**
68
- * 创建单个 session SQLite 状态存储
63
+ * 创建单个 SQLite VirtualWorkdir 的状态存储
69
64
  *
70
65
  * @example
71
66
  * ```ts
72
- * const store = createSqliteVirtualWorkdirStateStore(db, sessionId);
67
+ * const store = createSqliteVirtualWorkdirStateStore(db, "demo");
73
68
  * expect(store.kind).toBe("sqlite");
74
69
  * ```
75
70
  */
76
- function createSqliteVirtualWorkdirStateStore(db, sessionId) {
71
+ function createSqliteVirtualWorkdirStateStore(db, workdirKey) {
77
72
  const transactImpl = db.transaction((fn) => fn());
78
- const readBaseTreeStmt = db.query("SELECT base_tree FROM workdir_sessions WHERE session_id = ?");
79
- const upsertSessionStmt = db.query("INSERT INTO workdir_sessions (session_id, base_tree) VALUES (?, ?) ON CONFLICT(session_id) DO UPDATE SET base_tree = excluded.base_tree");
73
+ const readBaseTreeStmt = db.query("SELECT base_tree FROM workdirs WHERE workdir_key = ?");
74
+ const upsertWorkdirStmt = db.query("INSERT INTO workdirs (workdir_key, base_tree) VALUES (?, ?) ON CONFLICT(workdir_key) DO UPDATE SET base_tree = excluded.base_tree");
80
75
  const getNodeStmt = db.query(`SELECT node_id, origin_kind, origin_hash, origin_mode, state_kind, state_mode, content, target, directory_overlay
81
76
  FROM workdir_nodes
82
- WHERE session_id = ? AND node_id = ?`);
77
+ WHERE workdir_key = ? AND node_id = ?`);
83
78
  const setNodeStmt = db.query(`INSERT INTO workdir_nodes (
84
- session_id, node_id, origin_kind, origin_hash, origin_mode,
79
+ workdir_key, node_id, origin_kind, origin_hash, origin_mode,
85
80
  state_kind, state_mode, content, target, directory_overlay
86
81
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
87
- ON CONFLICT(session_id, node_id) DO UPDATE SET
82
+ ON CONFLICT(workdir_key, node_id) DO UPDATE SET
88
83
  origin_kind = excluded.origin_kind,
89
84
  origin_hash = excluded.origin_hash,
90
85
  origin_mode = excluded.origin_mode,
@@ -93,20 +88,20 @@ function createSqliteVirtualWorkdirStateStore(db, sessionId) {
93
88
  content = excluded.content,
94
89
  target = excluded.target,
95
90
  directory_overlay = excluded.directory_overlay`);
96
- const deleteNodeStmt = db.query("DELETE FROM workdir_nodes WHERE session_id = ? AND node_id = ?");
97
- const clearNodesStmt = db.query("DELETE FROM workdir_nodes WHERE session_id = ?");
91
+ const deleteNodeStmt = db.query("DELETE FROM workdir_nodes WHERE workdir_key = ? AND node_id = ?");
92
+ const clearNodesStmt = db.query("DELETE FROM workdir_nodes WHERE workdir_key = ?");
98
93
  const listChangesStmt = db.query(`SELECT path, previous_kind, previous_mode, previous_hash, current_kind, current_mode, current_hash, source_kind, source_path
99
94
  FROM workdir_changes
100
- WHERE session_id = ?
95
+ WHERE workdir_key = ?
101
96
  ORDER BY path`);
102
97
  const getChangeStmt = db.query(`SELECT path, previous_kind, previous_mode, previous_hash, current_kind, current_mode, current_hash, source_kind, source_path
103
98
  FROM workdir_changes
104
- WHERE session_id = ? AND path = ?`);
99
+ WHERE workdir_key = ? AND path = ?`);
105
100
  const upsertChangeStmt = db.query(`INSERT INTO workdir_changes (
106
- session_id, path, previous_kind, previous_mode, previous_hash,
101
+ workdir_key, path, previous_kind, previous_mode, previous_hash,
107
102
  current_kind, current_mode, current_hash, source_kind, source_path
108
103
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
109
- ON CONFLICT(session_id, path) DO UPDATE SET
104
+ ON CONFLICT(workdir_key, path) DO UPDATE SET
110
105
  previous_kind = excluded.previous_kind,
111
106
  previous_mode = excluded.previous_mode,
112
107
  previous_hash = excluded.previous_hash,
@@ -115,35 +110,35 @@ function createSqliteVirtualWorkdirStateStore(db, sessionId) {
115
110
  current_hash = excluded.current_hash,
116
111
  source_kind = excluded.source_kind,
117
112
  source_path = excluded.source_path`);
118
- const deleteChangeStmt = db.query("DELETE FROM workdir_changes WHERE session_id = ? AND path = ?");
119
- const clearChangesStmt = db.query("DELETE FROM workdir_changes WHERE session_id = ?");
113
+ const deleteChangeStmt = db.query("DELETE FROM workdir_changes WHERE workdir_key = ? AND path = ?");
114
+ const clearChangesStmt = db.query("DELETE FROM workdir_changes WHERE workdir_key = ?");
120
115
  const listDirtyDirsStmt = db.query(`SELECT path, is_dirty, dirty_entry_count, dirty_descendant_count, affected_names, current_tree_hash, hash_state
121
116
  FROM workdir_dirty_dirs
122
- WHERE session_id = ?
117
+ WHERE workdir_key = ?
123
118
  ORDER BY path`);
124
119
  const getDirtyDirStmt = db.query(`SELECT path, is_dirty, dirty_entry_count, dirty_descendant_count, affected_names, current_tree_hash, hash_state
125
120
  FROM workdir_dirty_dirs
126
- WHERE session_id = ? AND path = ?`);
121
+ WHERE workdir_key = ? AND path = ?`);
127
122
  const upsertDirtyDirStmt = db.query(`INSERT INTO workdir_dirty_dirs (
128
- session_id, path, is_dirty, dirty_entry_count, dirty_descendant_count,
123
+ workdir_key, path, is_dirty, dirty_entry_count, dirty_descendant_count,
129
124
  affected_names, current_tree_hash, hash_state
130
125
  )
131
126
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
132
- ON CONFLICT(session_id, path) DO UPDATE SET
127
+ ON CONFLICT(workdir_key, path) DO UPDATE SET
133
128
  is_dirty = excluded.is_dirty,
134
129
  dirty_entry_count = excluded.dirty_entry_count,
135
130
  dirty_descendant_count = excluded.dirty_descendant_count,
136
131
  affected_names = excluded.affected_names,
137
132
  current_tree_hash = excluded.current_tree_hash,
138
133
  hash_state = excluded.hash_state`);
139
- const deleteDirtyDirStmt = db.query("DELETE FROM workdir_dirty_dirs WHERE session_id = ? AND path = ?");
140
- const clearDirtyDirsStmt = db.query("DELETE FROM workdir_dirty_dirs WHERE session_id = ?");
134
+ const deleteDirtyDirStmt = db.query("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ? AND path = ?");
135
+ const clearDirtyDirsStmt = db.query("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ?");
141
136
  const resetTx = db.transaction((baseTree) => {
142
- upsertSessionStmt.run(sessionId, baseTree);
143
- clearNodesStmt.run(sessionId);
144
- clearChangesStmt.run(sessionId);
145
- clearDirtyDirsStmt.run(sessionId);
146
- writeNode(setNodeStmt, sessionId, createRootDirectoryNode(baseTree));
137
+ upsertWorkdirStmt.run(workdirKey, baseTree);
138
+ clearNodesStmt.run(workdirKey);
139
+ clearChangesStmt.run(workdirKey);
140
+ clearDirtyDirsStmt.run(workdirKey);
141
+ writeNode(setNodeStmt, workdirKey, createRootDirectoryNode(baseTree));
147
142
  });
148
143
  return {
149
144
  kind: "sqlite",
@@ -151,49 +146,49 @@ function createSqliteVirtualWorkdirStateStore(db, sessionId) {
151
146
  return transactImpl(fn);
152
147
  },
153
148
  readBaseTree() {
154
- const row = readBaseTreeStmt.get(sessionId);
155
- if (row === null) throw new Error(`Virtual workdir session not found: ${sessionId}`);
149
+ const row = readBaseTreeStmt.get(workdirKey);
150
+ if (row === null) throw new Error(`Virtual workdir not found: ${workdirKey}`);
156
151
  return readBaseTreeValue(row.base_tree);
157
152
  },
158
153
  writeBaseTree(baseTree) {
159
- upsertSessionStmt.run(sessionId, baseTree);
154
+ upsertWorkdirStmt.run(workdirKey, baseTree);
160
155
  },
161
156
  getNode(id) {
162
- const row = getNodeStmt.get(sessionId, id);
157
+ const row = getNodeStmt.get(workdirKey, id);
163
158
  if (row === null) return null;
164
159
  return readNode(row);
165
160
  },
166
161
  setNode(node) {
167
- writeNode(setNodeStmt, sessionId, node);
162
+ writeNode(setNodeStmt, workdirKey, node);
168
163
  },
169
164
  deleteNode(id) {
170
- deleteNodeStmt.run(sessionId, id);
165
+ deleteNodeStmt.run(workdirKey, id);
171
166
  },
172
167
  listChangeRecords() {
173
- return listChangesStmt.all(sessionId).map(readChangeRecord);
168
+ return listChangesStmt.all(workdirKey).map(readChangeRecord);
174
169
  },
175
170
  getChangeRecord(path) {
176
- const row = getChangeStmt.get(sessionId, path);
171
+ const row = getChangeStmt.get(workdirKey, path);
177
172
  return row === null ? null : readChangeRecord(row);
178
173
  },
179
174
  setChangeRecord(record) {
180
- upsertChangeStmt.run(sessionId, record.path, record.previous?.kind ?? null, record.previous?.mode ?? null, record.previous?.hash ?? null, record.current?.kind ?? null, record.current?.mode ?? null, record.current?.hash ?? null, record.source?.kind ?? null, record.source?.path ?? null);
175
+ upsertChangeStmt.run(workdirKey, record.path, record.previous?.kind ?? null, record.previous?.mode ?? null, record.previous?.hash ?? null, record.current?.kind ?? null, record.current?.mode ?? null, record.current?.hash ?? null, record.source?.kind ?? null, record.source?.path ?? null);
181
176
  },
182
177
  deleteChangeRecord(path) {
183
- deleteChangeStmt.run(sessionId, path);
178
+ deleteChangeStmt.run(workdirKey, path);
184
179
  },
185
180
  listDirtyDirSummaries() {
186
- return listDirtyDirsStmt.all(sessionId).map(readDirtyDirSummary);
181
+ return listDirtyDirsStmt.all(workdirKey).map(readDirtyDirSummary);
187
182
  },
188
183
  getDirtyDirSummary(path) {
189
- const row = getDirtyDirStmt.get(sessionId, path);
184
+ const row = getDirtyDirStmt.get(workdirKey, path);
190
185
  return row === null ? null : readDirtyDirSummary(row);
191
186
  },
192
187
  setDirtyDirSummary(summary) {
193
- upsertDirtyDirStmt.run(sessionId, summary.path, summary.isDirty ? 1 : 0, summary.dirtyEntryCount, summary.dirtyDescendantCount, JSON.stringify(summary.affectedNames), summary.currentTreeHash, summary.hashState);
188
+ upsertDirtyDirStmt.run(workdirKey, summary.path, summary.isDirty ? 1 : 0, summary.dirtyEntryCount, summary.dirtyDescendantCount, JSON.stringify(summary.affectedNames), summary.currentTreeHash, summary.hashState);
194
189
  },
195
190
  deleteDirtyDirSummary(path) {
196
- deleteDirtyDirStmt.run(sessionId, path);
191
+ deleteDirtyDirStmt.run(workdirKey, path);
197
192
  },
198
193
  reset(baseTree) {
199
194
  resetTx(baseTree);
@@ -204,14 +199,14 @@ function ensureSchema(db) {
204
199
  const currentVersion = readSchemaVersion(db);
205
200
  if (currentVersion !== 0 && currentVersion !== WORKDIR_SQLITE_SCHEMA_VERSION) throw new Error(`Unsupported virtual workdir SQLite schema version: expected ${WORKDIR_SQLITE_SCHEMA_VERSION}, got ${currentVersion}`);
206
201
  db.run(`
207
- CREATE TABLE IF NOT EXISTS workdir_sessions (
208
- session_id TEXT PRIMARY KEY,
202
+ CREATE TABLE IF NOT EXISTS workdirs (
203
+ workdir_key TEXT PRIMARY KEY,
209
204
  base_tree TEXT NOT NULL
210
205
  )
211
206
  `);
212
207
  db.run(`
213
208
  CREATE TABLE IF NOT EXISTS workdir_nodes (
214
- session_id TEXT NOT NULL,
209
+ workdir_key TEXT NOT NULL,
215
210
  node_id TEXT NOT NULL,
216
211
  origin_kind TEXT NOT NULL,
217
212
  origin_hash TEXT,
@@ -221,12 +216,12 @@ function ensureSchema(db) {
221
216
  content BLOB,
222
217
  target BLOB,
223
218
  directory_overlay TEXT,
224
- PRIMARY KEY (session_id, node_id)
219
+ PRIMARY KEY (workdir_key, node_id)
225
220
  )
226
221
  `);
227
222
  db.run(`
228
223
  CREATE TABLE IF NOT EXISTS workdir_changes (
229
- session_id TEXT NOT NULL,
224
+ workdir_key TEXT NOT NULL,
230
225
  path TEXT NOT NULL,
231
226
  previous_kind TEXT,
232
227
  previous_mode TEXT,
@@ -236,12 +231,12 @@ function ensureSchema(db) {
236
231
  current_hash TEXT,
237
232
  source_kind TEXT,
238
233
  source_path TEXT,
239
- PRIMARY KEY (session_id, path)
234
+ PRIMARY KEY (workdir_key, path)
240
235
  )
241
236
  `);
242
237
  db.run(`
243
238
  CREATE TABLE IF NOT EXISTS workdir_dirty_dirs (
244
- session_id TEXT NOT NULL,
239
+ workdir_key TEXT NOT NULL,
245
240
  path TEXT NOT NULL,
246
241
  is_dirty INTEGER NOT NULL,
247
242
  dirty_entry_count INTEGER NOT NULL,
@@ -249,20 +244,20 @@ function ensureSchema(db) {
249
244
  affected_names TEXT NOT NULL,
250
245
  current_tree_hash TEXT,
251
246
  hash_state TEXT NOT NULL,
252
- PRIMARY KEY (session_id, path)
247
+ PRIMARY KEY (workdir_key, path)
253
248
  )
254
249
  `);
255
250
  writeSchemaVersion(db, WORKDIR_SQLITE_SCHEMA_VERSION);
256
251
  }
257
- function hasSession(db, sessionId) {
258
- return db.query("SELECT 1 FROM workdir_sessions WHERE session_id = ?").get(sessionId) !== null;
252
+ function hasWorkdir(db, workdirKey) {
253
+ return db.query("SELECT 1 FROM workdirs WHERE workdir_key = ?").get(workdirKey) !== null;
259
254
  }
260
- function deleteSessionRows(db, sessionId) {
255
+ function deleteWorkdirRows(db, workdirKey) {
261
256
  db.transaction(() => {
262
- db.query("DELETE FROM workdir_dirty_dirs WHERE session_id = ?").run(sessionId);
263
- db.query("DELETE FROM workdir_changes WHERE session_id = ?").run(sessionId);
264
- db.query("DELETE FROM workdir_nodes WHERE session_id = ?").run(sessionId);
265
- db.query("DELETE FROM workdir_sessions WHERE session_id = ?").run(sessionId);
257
+ db.query("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ?").run(workdirKey);
258
+ db.query("DELETE FROM workdir_changes WHERE workdir_key = ?").run(workdirKey);
259
+ db.query("DELETE FROM workdir_nodes WHERE workdir_key = ?").run(workdirKey);
260
+ db.query("DELETE FROM workdirs WHERE workdir_key = ?").run(workdirKey);
266
261
  })();
267
262
  }
268
263
  function readSchemaVersion(db) {
@@ -271,33 +266,33 @@ function readSchemaVersion(db) {
271
266
  function writeSchemaVersion(db, version) {
272
267
  db.run(`PRAGMA user_version = ${version}`);
273
268
  }
274
- function validateSessionIntegrity(db, sessionId) {
275
- const sessionRow = db.query("SELECT base_tree FROM workdir_sessions WHERE session_id = ?").get(sessionId);
276
- if (sessionRow === null) throw new Error(`Virtual workdir session not found: ${sessionId}`);
277
- readBaseTreeValue(sessionRow.base_tree);
269
+ function validateWorkdirIntegrity(db, workdirKey) {
270
+ const workdirRow = db.query("SELECT base_tree FROM workdirs WHERE workdir_key = ?").get(workdirKey);
271
+ if (workdirRow === null) throw new Error(`Virtual workdir not found: ${workdirKey}`);
272
+ readBaseTreeValue(workdirRow.base_tree);
278
273
  const rootRow = db.query(`SELECT node_id, origin_kind, origin_hash, origin_mode, state_kind, state_mode, content, target, directory_overlay
279
274
  FROM workdir_nodes
280
- WHERE session_id = ? AND node_id = ?`).get(sessionId, "root");
281
- if (rootRow === null) throw new Error(`Virtual workdir session is corrupted: missing root node for ${sessionId}`);
282
- if (readNode(rootRow).state.kind !== "directory") throw new Error(`Virtual workdir session is corrupted: root node is not a directory for ${sessionId}`);
275
+ WHERE workdir_key = ? AND node_id = ?`).get(workdirKey, "root");
276
+ if (rootRow === null) throw new Error(`Virtual workdir is corrupted: missing root node for ${workdirKey}`);
277
+ if (readNode(rootRow).state.kind !== "directory") throw new Error(`Virtual workdir is corrupted: root node is not a directory for ${workdirKey}`);
283
278
  const allNodesStmt = db.query(`SELECT node_id, origin_kind, origin_hash, origin_mode, state_kind, state_mode, content, target, directory_overlay
284
279
  FROM workdir_nodes
285
- WHERE session_id = ?`);
286
- for (const row of allNodesStmt.all(sessionId)) readNode(row);
280
+ WHERE workdir_key = ?`);
281
+ for (const row of allNodesStmt.all(workdirKey)) readNode(row);
287
282
  }
288
- function writeNode(stmt, sessionId, node) {
283
+ function writeNode(stmt, workdirKey, node) {
289
284
  if (node.state.kind === "directory") {
290
- stmt.run(sessionId, node.id, node.origin.kind, node.origin.kind === "none" ? null : node.origin.hash, node.origin.kind === "repo-blob" ? node.origin.mode : null, "directory", null, null, null, JSON.stringify({
285
+ stmt.run(workdirKey, node.id, node.origin.kind, node.origin.kind === "none" ? null : node.origin.hash, node.origin.kind === "repo-blob" ? node.origin.mode : null, "directory", null, null, null, JSON.stringify({
291
286
  addedEntries: Array.from(node.state.overlay.addedEntries.entries()),
292
287
  deletedNames: Array.from(node.state.overlay.deletedNames.values())
293
288
  }));
294
289
  return;
295
290
  }
296
291
  if (node.state.kind === "file") {
297
- stmt.run(sessionId, node.id, node.origin.kind, node.origin.kind === "none" ? null : node.origin.hash, node.origin.kind === "repo-blob" ? node.origin.mode : null, "file", node.state.mode, node.state.content ?? null, null, null);
292
+ stmt.run(workdirKey, node.id, node.origin.kind, node.origin.kind === "none" ? null : node.origin.hash, node.origin.kind === "repo-blob" ? node.origin.mode : null, "file", node.state.mode, node.state.content ?? null, null, null);
298
293
  return;
299
294
  }
300
- stmt.run(sessionId, node.id, node.origin.kind, node.origin.kind === "none" ? null : node.origin.hash, node.origin.kind === "repo-blob" ? node.origin.mode : null, "symlink", "120000", null, node.state.target ?? null, null);
295
+ stmt.run(workdirKey, node.id, node.origin.kind, node.origin.kind === "none" ? null : node.origin.hash, node.origin.kind === "repo-blob" ? node.origin.mode : null, "symlink", "120000", null, node.state.target ?? null, null);
301
296
  }
302
297
  function readNode(row) {
303
298
  const origin = readNodeOrigin(row);
@@ -445,11 +440,11 @@ function readBlobColumn(raw, column) {
445
440
  return Buffer.from(raw);
446
441
  }
447
442
  function readBaseTreeValue(raw) {
448
- if (typeof raw !== "string") throw new Error("Invalid SQLite workdir session base_tree");
443
+ if (typeof raw !== "string") throw new Error("Invalid SQLite workdir base_tree");
449
444
  try {
450
445
  return sha1(raw);
451
446
  } catch {
452
- throw new Error("Invalid SQLite workdir session base_tree");
447
+ throw new Error("Invalid SQLite workdir base_tree");
453
448
  }
454
449
  }
455
450
  function isDirectoryOverlayPayload(value) {
@@ -461,4 +456,4 @@ function isDirectoryOverlayPayload(value) {
461
456
  return hasValidAddedEntries && hasValidDeletedNames;
462
457
  }
463
458
  //#endregion
464
- export { createSqliteVirtualWorkdirBackend };
459
+ export { deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir };
@@ -1,2 +1,2 @@
1
- import { CreateSqliteVirtualWorkdirBackendOptions, SqliteVirtualWorkdirBackend, createSqliteVirtualWorkdirBackend } from "./sqlite-backend.mjs";
2
- export { type CreateSqliteVirtualWorkdirBackendOptions, type SqliteVirtualWorkdirBackend, createSqliteVirtualWorkdirBackend };
1
+ import { OpenSqliteVirtualWorkdirOptions, SqliteVirtualWorkdir, deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir } from "./sqlite-backend.mjs";
2
+ export { type OpenSqliteVirtualWorkdirOptions, type SqliteVirtualWorkdir, deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir };
@@ -1,2 +1,2 @@
1
- import { createSqliteVirtualWorkdirBackend } from "./sqlite-backend.mjs";
2
- export { createSqliteVirtualWorkdirBackend };
1
+ import { deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir } from "./sqlite-backend.mjs";
2
+ export { deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir };
@@ -2,7 +2,7 @@ import { SHA1 } from "../core/types.mjs";
2
2
  import { NodeId } from "./ids.mjs";
3
3
  import { NormalizedChangeRecord } from "./change-index.mjs";
4
4
  import { DirtyDirSummary } from "./dirty-dir.mjs";
5
- import { SessionNode } from "./nodes.mjs";
5
+ import { WorkdirNode } from "./nodes.mjs";
6
6
 
7
7
  //#region src/workdir/state-store.d.ts
8
8
  /**
@@ -14,7 +14,7 @@ interface VirtualWorkdirStateStore {
14
14
  /**
15
15
  * 在单次提交边界内执行状态变更
16
16
  *
17
- * 用于把一次 session 写操作封装为单个内部事务。
17
+ * 用于把一次 workdir 写操作封装为单个内部事务。
18
18
  * 若回调抛错,store 应尽力恢复到调用前状态。
19
19
  */
20
20
  transact<T>(fn: () => T): T;
@@ -23,9 +23,9 @@ interface VirtualWorkdirStateStore {
23
23
  /** 覆盖当前基线 tree */
24
24
  writeBaseTree(baseTree: SHA1): void;
25
25
  /** 读取节点,不存在时返回 null */
26
- getNode(id: NodeId): SessionNode | null;
26
+ getNode(id: NodeId): WorkdirNode | null;
27
27
  /** 写入或覆盖节点 */
28
- setNode(node: SessionNode): void;
28
+ setNode(node: WorkdirNode): void;
29
29
  /** 删除节点 */
30
30
  deleteNode(id: NodeId): void;
31
31
  /** 列出全部规范化变更记录 */
@@ -1,13 +1,13 @@
1
1
  import { VirtualNotDirectoryError, VirtualNotFileError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError } from "../core/errors.mjs";
2
2
  import { VIRTUAL_ROOT_NODE_ID, originBackedNodeId } from "./ids.mjs";
3
- import { mergeDirectoryChildren } from "./overlay.mjs";
4
3
  import { readRepoTree, treeEntryToNodeOrigin } from "./origin.mjs";
4
+ import { mergeDirectoryChildren } from "./overlay.mjs";
5
5
  import { assertValidVirtualPath, baseName, joinPath, parentPath, splitPathSegments } from "./path.mjs";
6
- //#region src/workdir/session-internal.ts
6
+ //#region src/workdir/workdir-path.ts
7
7
  /**
8
- * Virtual Workdir session 内部共享逻辑
8
+ * Virtual Workdir 路径解析与共享读视图
9
9
  *
10
- * 供 session.ts 与 write-tree.ts 复用:
10
+ * 供 workdir.ts 与 write-tree.ts 复用:
11
11
  * - 路径解析(resolvePath / resolveWriteTarget*)
12
12
  * - 目录子项展开(listDirectoryChildren / getDirectoryChildrenView)
13
13
  * - origin 节点懒注册(ensureNodeFromTreeEntry)
@@ -28,7 +28,7 @@ function joinChildPath(dirPath, name) {
28
28
  }
29
29
  function getRootNode(state) {
30
30
  const root = state.getNode(VIRTUAL_ROOT_NODE_ID);
31
- if (root === null) throw new Error("Virtual workdir session is missing root node");
31
+ if (root === null) throw new Error("Virtual workdir is missing root node");
32
32
  return root;
33
33
  }
34
34
  /**
@@ -265,14 +265,14 @@ function resolvePathByParentLookup(source, state, path) {
265
265
  };
266
266
  }
267
267
  /**
268
- * 确保 origin 条目对应的 session 节点已懒注册
268
+ * 确保 origin 条目对应的 workdir 节点已懒注册
269
269
  */
270
270
  function ensureNodeFromTreeEntry(state, entry) {
271
271
  const id = originBackedNodeId(entry.hash);
272
272
  if (state.getNode(id) === null) {
273
273
  const origin = treeEntryToNodeOrigin(entry);
274
274
  let nodeState;
275
- if (entry.mode === "40000") nodeState = {
275
+ if (entry.mode === "040000") nodeState = {
276
276
  kind: "directory",
277
277
  overlay: {
278
278
  addedEntries: /* @__PURE__ */ new Map(),
@@ -321,12 +321,12 @@ function buildAddedModes(state, dirNode) {
321
321
  for (const name of dirNode.state.overlay.addedEntries.keys()) {
322
322
  const nodeId = dirNode.state.overlay.addedEntries.get(name);
323
323
  const node = state.getNode(nodeId);
324
- if (node !== null) addedModes.set(name, sessionNodeMode(node));
324
+ if (node !== null) addedModes.set(name, workdirNodeMode(node));
325
325
  }
326
326
  return addedModes;
327
327
  }
328
- function sessionNodeMode(node) {
329
- if (node.state.kind === "directory") return "40000";
328
+ function workdirNodeMode(node) {
329
+ if (node.state.kind === "directory") return "040000";
330
330
  if (node.state.kind === "symlink") return "120000";
331
331
  return node.state.mode;
332
332
  }
@@ -1,14 +1,14 @@
1
1
  import { createNodeId } from "./ids.mjs";
2
- import { overlayBindEntry } from "./overlay.mjs";
3
- import { cloneSessionNodeForCopy } from "./nodes.mjs";
4
2
  import { readRepoBlobContent } from "./origin.mjs";
5
- import { listDirectoryChildren } from "./session-internal.mjs";
3
+ import { overlayBindEntry } from "./overlay.mjs";
4
+ import { listDirectoryChildren } from "./workdir-path.mjs";
6
5
  import { observeListedDirectoryChild } from "./directory-view.mjs";
7
- //#region src/workdir/session-transaction.ts
6
+ import { cloneWorkdirNodeForCopy } from "./nodes.mjs";
7
+ //#region src/workdir/workdir-transaction.ts
8
8
  /**
9
- * Virtual Workdir 事务包装与会话辅助函数
9
+ * Virtual Workdir 写事务与节点辅助函数
10
10
  *
11
- * 从 session.ts 提取,降低编排层的复杂度:
11
+ * 从 workdir.ts 提取,降低编排层复杂度:
12
12
  * - 写操作事务生命周期管理
13
13
  * - 父目录 overlay 更新
14
14
  * - 节点状态统计(stat)
@@ -17,7 +17,7 @@ import { observeListedDirectoryChild } from "./directory-view.mjs";
17
17
  /**
18
18
  * 在 state store 事务边界内执行写操作。
19
19
  *
20
- * @param state - session 内部状态存储
20
+ * @param state - workdir 内部状态存储
21
21
  * @param onBeforeCommit - 提交前回调(在事务 callback 内执行);可为 null
22
22
  * @param onCommitted - 提交后回调(在事务 callback 外执行)
23
23
  * @param fn - 实际写入逻辑
@@ -46,7 +46,7 @@ function updateParentOverlay(state, parentId, newOverlay) {
46
46
  });
47
47
  }
48
48
  /**
49
- * 获取节点统计信息(用于 session.stat() 实现)
49
+ * 获取节点统计信息(用于 workdir.stat() 实现)
50
50
  */
51
51
  function statNode(source, node, path) {
52
52
  if (node.state.kind === "directory") return statDirectoryNode(node);
@@ -79,7 +79,7 @@ function statNode(source, node, path) {
79
79
  function statDirectoryNode(node) {
80
80
  return {
81
81
  kind: "tree",
82
- mode: "40000",
82
+ mode: "040000",
83
83
  size: 0,
84
84
  hash: node.origin.kind === "repo-tree" ? node.origin.hash : null
85
85
  };
@@ -91,7 +91,7 @@ function statDirectoryNode(node) {
91
91
  */
92
92
  function cloneNodeGraphForCopy(source, state, node, path) {
93
93
  const newNodeId = createNodeId();
94
- const cloned = cloneSessionNodeForCopy(node, newNodeId);
94
+ const cloned = cloneWorkdirNodeForCopy(node, newNodeId);
95
95
  state.setNode(cloned);
96
96
  if (node.state.kind !== "directory" || cloned.state.kind !== "directory") return newNodeId;
97
97
  let overlay = cloned.state.overlay;
@@ -0,0 +1,30 @@
1
+ import { ObjectDatabase } from "../core/types/odb.mjs";
2
+ import { CreateVirtualWorkdirOptions, VirtualWorkdir } from "./core.mjs";
3
+ import { VirtualWorkdirStateStore } from "./state-store.mjs";
4
+
5
+ //#region src/workdir/workdir.d.ts
6
+ /**
7
+ * 基于 ObjectDatabase 创建 VirtualWorkdir
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const repo = createMemoryRepository();
12
+ * const tree = repo.createTree([]);
13
+ * const workdir = createVirtualWorkdir(repo.objects, { baseTree: tree });
14
+ * expect(workdir.readdir()).toEqual([]);
15
+ * ```
16
+ */
17
+ declare function createVirtualWorkdir(source: ObjectDatabase, options: CreateVirtualWorkdirOptions): VirtualWorkdir;
18
+ /**
19
+ * 基于已有状态存储打开 VirtualWorkdir
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const store = createVirtualWorkdirMemoryStateStore(tree);
24
+ * const workdir = openVirtualWorkdir(repo.objects, store);
25
+ * expect(workdir.baseTree).toBe(tree);
26
+ * ```
27
+ */
28
+ declare function openVirtualWorkdir(source: ObjectDatabase, state: VirtualWorkdirStateStore): VirtualWorkdir;
29
+ //#endregion
30
+ export { createVirtualWorkdir, openVirtualWorkdir };