nano-git 0.2.1 → 0.2.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.
@@ -0,0 +1,25 @@
1
+ //#region src/workdir/change-log.d.ts
2
+ /** 内部变更操作(含 revert,公开 API 暂不暴露 revert) */
3
+ type InternalChangeRecord = {
4
+ readonly op: "add";
5
+ readonly path: string;
6
+ } | {
7
+ readonly op: "modify";
8
+ readonly path: string;
9
+ } | {
10
+ readonly op: "delete";
11
+ readonly path: string;
12
+ } | {
13
+ readonly op: "rename";
14
+ readonly from: string;
15
+ readonly to: string;
16
+ } | {
17
+ readonly op: "copy";
18
+ readonly from: string;
19
+ readonly to: string;
20
+ } | {
21
+ readonly op: "revert";
22
+ readonly path: string;
23
+ };
24
+ //#endregion
25
+ export { InternalChangeRecord };
@@ -22,15 +22,21 @@ function createVirtualChangeLog() {
22
22
  return records.slice();
23
23
  },
24
24
  toVirtualChanges() {
25
- const out = [];
26
- for (const record of records) {
27
- const mapped = mapInternalToVirtual(record);
28
- if (mapped !== null) out.push(mapped);
29
- }
30
- return out;
25
+ return mapInternalChangesToVirtualChanges(records);
31
26
  }
32
27
  };
33
28
  }
29
+ /**
30
+ * 将内部变更记录映射为公开 VirtualChange 列表
31
+ */
32
+ function mapInternalChangesToVirtualChanges(records) {
33
+ const out = [];
34
+ for (const record of records) {
35
+ const mapped = mapInternalToVirtual(record);
36
+ if (mapped !== null) out.push(mapped);
37
+ }
38
+ return out;
39
+ }
34
40
  function mapInternalToVirtual(record) {
35
41
  switch (record.op) {
36
42
  case "add": return {
@@ -60,4 +66,4 @@ function mapInternalToVirtual(record) {
60
66
  }
61
67
  }
62
68
  //#endregion
63
- export { createVirtualChangeLog };
69
+ export { createVirtualChangeLog, mapInternalChangesToVirtualChanges };
@@ -1,4 +1,5 @@
1
1
  import { SHA1 } from "../core/types.mjs";
2
+ import { ObjectDatabase } from "../core/types/odb.mjs";
2
3
  import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
3
4
 
4
5
  //#region src/workdir/core.d.ts
@@ -65,12 +66,22 @@ interface CreateVirtualWorkdirSessionOptions {
65
66
  /** 基线 tree 的 SHA-1 哈希 */
66
67
  readonly baseTree: SHA1;
67
68
  }
69
+ /**
70
+ * Virtual Workdir session 标识
71
+ */
72
+ type VirtualWorkdirSessionId = string & {
73
+ readonly __brand: "VirtualWorkdirSessionId";
74
+ };
68
75
  /**
69
76
  * VirtualWorkdirSession(虚拟工作目录会话)
70
77
  *
71
78
  * 提供独立生命周期的可变 tree 视图,基于 `baseTree + CoW overlay` 模型。
72
79
  * 不绑定 commit,不涉及 Git index / 真实工作目录。
73
80
  *
81
+ * 当前 session 对 origin 仓库对象采用弱保证:
82
+ * 如果 base tree / origin blob 在后续被移除、损坏或不可读取,
83
+ * 相关读取、`revert()`、`writeTree()` 等操作会抛出 `VirtualOriginUnavailableError`。
84
+ *
74
85
  * @example
75
86
  * ```ts
76
87
  * import { createMemoryRepository } from "nano-git/repository/memory";
@@ -155,12 +166,23 @@ interface VirtualWorkdirSession {
155
166
  * session 内部状态存储的抽象接口。
156
167
  * memory / file / sqlite 后端通过实现此接口来提供不同的持久化策略。
157
168
  *
169
+ * `file` / `sqlite` 持久化 backend 当前都按单进程、单写者场景收口;
170
+ * 不承诺跨进程并发写安全,也不提供多写者协调协议。
171
+ *
158
172
  * 本接口在后续 Phase 中会逐步补充完整方法签名。
159
173
  * 当前仅为命名冻结与角色声明。
160
174
  */
161
175
  interface VirtualWorkdirBackend {
162
176
  /** 后端类型标识 */
163
177
  readonly kind: "memory" | "file" | "sqlite";
178
+ /** 创建新 session 并返回其标识 */
179
+ createSession(options: CreateVirtualWorkdirSessionOptions): VirtualWorkdirSessionId;
180
+ /** 打开已存在的 session */
181
+ openSession(source: ObjectDatabase, sessionId: VirtualWorkdirSessionId): VirtualWorkdirSession;
182
+ /** 删除 session */
183
+ deleteSession(sessionId: VirtualWorkdirSessionId): void;
184
+ /** 列出当前后端中可完整恢复的 session */
185
+ listSessions(): VirtualWorkdirSessionId[];
164
186
  }
165
187
  //#endregion
166
- export { CreateVirtualWorkdirSessionOptions, VirtualChange, VirtualChangeType, VirtualDirEntry, VirtualEntryKind, VirtualEntryStat, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError, VirtualWorkdirBackend, VirtualWorkdirSession };
188
+ export { CreateVirtualWorkdirSessionOptions, VirtualChange, VirtualChangeType, VirtualDirEntry, VirtualEntryKind, VirtualEntryStat, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError, VirtualWorkdirBackend, VirtualWorkdirSession, VirtualWorkdirSessionId };
@@ -0,0 +1,22 @@
1
+ import { VirtualWorkdirBackend } from "./core.mjs";
2
+
3
+ //#region src/workdir/file-backend.d.ts
4
+ /** 创建文件系统 Virtual Workdir backend 的可选参数 */
5
+ interface CreateFileVirtualWorkdirBackendOptions {
6
+ /** session 根目录名,默认 `sessions` */
7
+ readonly sessionsDirName?: string;
8
+ }
9
+ /**
10
+ * 创建基于文件系统目录的 Virtual Workdir backend
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const backend = createFileVirtualWorkdirBackend("/tmp/workdirs");
15
+ * const sessionId = backend.createSession({ baseTree: tree });
16
+ * const session = backend.openSession(repo.objects, sessionId);
17
+ * expect(session.baseTree).toBe(tree);
18
+ * ```
19
+ */
20
+ declare function createFileVirtualWorkdirBackend(rootDir: string, options?: CreateFileVirtualWorkdirBackendOptions): VirtualWorkdirBackend;
21
+ //#endregion
22
+ export { CreateFileVirtualWorkdirBackendOptions, createFileVirtualWorkdirBackend };
@@ -0,0 +1,366 @@
1
+ import { createRootDirectoryNode } from "./nodes.mjs";
2
+ import { createVirtualWorkdirSessionId } from "./session-id.mjs";
3
+ import { openVirtualWorkdirSession } from "./session.mjs";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ //#region src/workdir/file-backend.ts
7
+ /**
8
+ * Virtual Workdir 文件系统 backend
9
+ */
10
+ const FILE_WORKDIR_MANIFEST_VERSION = 1;
11
+ const FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX = ".txn-snapshot";
12
+ /**
13
+ * 创建基于文件系统目录的 Virtual Workdir backend
14
+ *
15
+ * @example
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);
21
+ * ```
22
+ */
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
+ };
57
+ }
58
+ /**
59
+ * 创建单个 session 的文件系统状态存储
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const store = createFileVirtualWorkdirStateStore("/tmp/workdirs/sessions", sessionId);
64
+ * expect(store.kind).toBe("file");
65
+ * ```
66
+ */
67
+ function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
68
+ const sessionDir = getSessionDir(sessionsRoot, sessionId);
69
+ const manifestPath = getManifestPath(sessionDir);
70
+ const contentDir = getContentDir(sessionDir);
71
+ return {
72
+ kind: "file",
73
+ transact(fn) {
74
+ const snapshotDir = `${sessionDir}${FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX}`;
75
+ rmSync(snapshotDir, {
76
+ recursive: true,
77
+ force: true
78
+ });
79
+ if (existsSync(sessionDir)) copyDirectoryRecursive(sessionDir, snapshotDir);
80
+ try {
81
+ const result = fn();
82
+ rmSync(snapshotDir, {
83
+ recursive: true,
84
+ force: true
85
+ });
86
+ return result;
87
+ } catch (error) {
88
+ rmSync(sessionDir, {
89
+ recursive: true,
90
+ force: true
91
+ });
92
+ if (existsSync(snapshotDir)) renameSync(snapshotDir, sessionDir);
93
+ throw error;
94
+ }
95
+ },
96
+ readBaseTree() {
97
+ return readManifest(manifestPath).baseTree;
98
+ },
99
+ writeBaseTree(baseTree) {
100
+ updateManifest(sessionDir, (manifest) => ({
101
+ ...manifest,
102
+ baseTree
103
+ }));
104
+ },
105
+ getNode(id) {
106
+ const record = readManifest(manifestPath).nodes[id];
107
+ if (record === void 0) return null;
108
+ return restoreNode(record, contentDir);
109
+ },
110
+ setNode(node) {
111
+ updateManifest(sessionDir, (manifest) => {
112
+ const record = persistNode(contentDir, node);
113
+ return {
114
+ ...manifest,
115
+ nodes: {
116
+ ...manifest.nodes,
117
+ [node.id]: record
118
+ }
119
+ };
120
+ });
121
+ },
122
+ deleteNode(id) {
123
+ updateManifest(sessionDir, (manifest) => {
124
+ if (manifest.nodes[id] === void 0) return manifest;
125
+ const { [id]: _deleted, ...rest } = manifest.nodes;
126
+ return {
127
+ ...manifest,
128
+ nodes: rest
129
+ };
130
+ });
131
+ },
132
+ appendChange(record) {
133
+ updateManifest(sessionDir, (manifest) => ({
134
+ ...manifest,
135
+ changes: [...manifest.changes, record]
136
+ }));
137
+ },
138
+ listChangeRecords() {
139
+ return readManifest(manifestPath).changes;
140
+ },
141
+ clearChanges() {
142
+ updateManifest(sessionDir, (manifest) => ({
143
+ ...manifest,
144
+ changes: []
145
+ }));
146
+ },
147
+ reset(baseTree) {
148
+ rmSync(sessionDir, {
149
+ recursive: true,
150
+ force: true
151
+ });
152
+ ensureSessionDirs(sessionDir, contentDir);
153
+ writeManifestAtomic(manifestPath, createManifest(baseTree, { [createRootDirectoryNode(baseTree).id]: serializeDirectoryNode(createRootDirectoryNode(baseTree)) }));
154
+ }
155
+ };
156
+ }
157
+ function hasSession(sessionsRoot, sessionId) {
158
+ return existsSync(getManifestPath(getSessionDir(sessionsRoot, sessionId)));
159
+ }
160
+ function validateSessionIntegrity(sessionsRoot, sessionId) {
161
+ const sessionDir = getSessionDir(sessionsRoot, sessionId);
162
+ const manifest = readManifest(getManifestPath(sessionDir));
163
+ 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));
167
+ }
168
+ function getSessionDir(sessionsRoot, sessionId) {
169
+ return join(sessionsRoot, encodePathToken(sessionId));
170
+ }
171
+ function getManifestPath(sessionDir) {
172
+ return join(sessionDir, "manifest.json");
173
+ }
174
+ function getContentDir(sessionDir) {
175
+ return join(sessionDir, "content");
176
+ }
177
+ function getContentPath(contentDir, payloadRef) {
178
+ return join(contentDir, `${encodePathToken(payloadRef)}.bin`);
179
+ }
180
+ function ensureSessionDirs(sessionDir, contentDir) {
181
+ mkdirSync(sessionDir, { recursive: true });
182
+ mkdirSync(contentDir, { recursive: true });
183
+ }
184
+ function copyDirectoryRecursive(sourceDir, targetDir) {
185
+ mkdirSync(targetDir, { recursive: true });
186
+ for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
187
+ const sourcePath = join(sourceDir, entry.name);
188
+ const targetPath = join(targetDir, entry.name);
189
+ if (entry.isDirectory()) {
190
+ copyDirectoryRecursive(sourcePath, targetPath);
191
+ continue;
192
+ }
193
+ writeFileSync(targetPath, readFileSync(sourcePath));
194
+ }
195
+ }
196
+ function createManifest(baseTree, nodes) {
197
+ return {
198
+ formatVersion: FILE_WORKDIR_MANIFEST_VERSION,
199
+ baseTree,
200
+ changes: [],
201
+ nodes
202
+ };
203
+ }
204
+ function updateManifest(sessionDir, updater) {
205
+ const manifestPath = getManifestPath(sessionDir);
206
+ ensureSessionDirs(sessionDir, getContentDir(sessionDir));
207
+ writeManifestAtomic(manifestPath, updater(readManifest(manifestPath)));
208
+ }
209
+ function writeManifestAtomic(path, manifest) {
210
+ writeJsonAtomic(path, manifest);
211
+ }
212
+ function readManifest(manifestPath) {
213
+ if (!existsSync(manifestPath)) throw new Error(`Virtual workdir session manifest not found: ${manifestPath}`);
214
+ const manifest = readJson(manifestPath);
215
+ validateManifest(manifest);
216
+ return manifest;
217
+ }
218
+ function validateManifest(manifest) {
219
+ 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
+ if (typeof manifest.nodes !== "object" || manifest.nodes === null || Array.isArray(manifest.nodes)) throw new Error("Invalid virtual workdir manifest nodes");
222
+ 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
+ }
224
+ function persistNode(contentDir, node) {
225
+ if (node.state.kind === "directory") return serializeDirectoryNode(node);
226
+ if (node.state.kind === "file") {
227
+ const contentRef = node.state.content === void 0 ? null : persistPayload(contentDir, node.id, node.state.content);
228
+ return {
229
+ id: node.id,
230
+ origin: node.origin,
231
+ state: {
232
+ kind: "file",
233
+ mode: node.state.mode,
234
+ contentRef
235
+ }
236
+ };
237
+ }
238
+ const targetRef = node.state.target === void 0 ? null : persistPayload(contentDir, node.id, node.state.target);
239
+ return {
240
+ id: node.id,
241
+ origin: node.origin,
242
+ state: {
243
+ kind: "symlink",
244
+ mode: "120000",
245
+ targetRef
246
+ }
247
+ };
248
+ }
249
+ function serializeDirectoryNode(node) {
250
+ if (node.state.kind !== "directory") throw new Error("serializeDirectoryNode: node is not a directory");
251
+ return {
252
+ id: node.id,
253
+ origin: node.origin,
254
+ state: {
255
+ kind: "directory",
256
+ overlay: {
257
+ addedEntries: Array.from(node.state.overlay.addedEntries.entries()),
258
+ deletedNames: Array.from(node.state.overlay.deletedNames.values())
259
+ }
260
+ }
261
+ };
262
+ }
263
+ function persistPayload(contentDir, nodeId, payload) {
264
+ mkdirSync(contentDir, { recursive: true });
265
+ const payloadRef = `${nodeId}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}`;
266
+ writeBufferAtomic(getContentPath(contentDir, payloadRef), payload);
267
+ return payloadRef;
268
+ }
269
+ function restoreNode(record, contentDir) {
270
+ const origin = restoreOrigin(record.origin);
271
+ if (record.state.kind === "directory") return {
272
+ id: record.id,
273
+ origin,
274
+ state: {
275
+ kind: "directory",
276
+ overlay: readDirectoryOverlayRecord(record.state.overlay)
277
+ }
278
+ };
279
+ const rawState = record.state;
280
+ if (rawState.kind === "file") {
281
+ const mode = rawState.mode;
282
+ if (mode !== "100644" && mode !== "100755") throw new Error(`Invalid file workdir node state mode: ${String(mode)}`);
283
+ const contentRef = rawState.contentRef;
284
+ if (contentRef !== void 0 && contentRef !== null && typeof contentRef !== "string") throw new Error("Invalid file workdir node content ref");
285
+ return {
286
+ id: record.id,
287
+ origin,
288
+ state: {
289
+ kind: "file",
290
+ mode,
291
+ content: contentRef === void 0 || contentRef === null ? void 0 : readPayload(contentDir, contentRef)
292
+ }
293
+ };
294
+ }
295
+ if (rawState.kind !== "symlink") throw new Error(`Invalid file workdir node state kind: ${String(rawState.kind)}`);
296
+ const targetRef = rawState.targetRef;
297
+ if (targetRef !== void 0 && targetRef !== null && typeof targetRef !== "string") throw new Error("Invalid file workdir node target ref");
298
+ return {
299
+ id: record.id,
300
+ origin,
301
+ state: {
302
+ kind: "symlink",
303
+ mode: "120000",
304
+ target: targetRef === void 0 || targetRef === null ? void 0 : readPayload(contentDir, targetRef)
305
+ }
306
+ };
307
+ }
308
+ function readDirectoryOverlayRecord(value) {
309
+ if (!isFileDirectoryOverlayPayload(value)) throw new Error("Invalid file workdir directory overlay payload");
310
+ return {
311
+ addedEntries: new Map(value.addedEntries.map(([name, nodeId]) => [name, nodeId])),
312
+ deletedNames: new Set(value.deletedNames)
313
+ };
314
+ }
315
+ function isFileDirectoryOverlayPayload(value) {
316
+ if (typeof value !== "object" || value === null) return false;
317
+ const maybe = value;
318
+ if (!Array.isArray(maybe.addedEntries) || !Array.isArray(maybe.deletedNames)) return false;
319
+ const hasValidAddedEntries = maybe.addedEntries.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && typeof entry[1] === "string");
320
+ const hasValidDeletedNames = maybe.deletedNames.every((name) => typeof name === "string");
321
+ return hasValidAddedEntries && hasValidDeletedNames;
322
+ }
323
+ function restoreOrigin(record) {
324
+ const origin = record;
325
+ if (origin.kind === "none") return { kind: "none" };
326
+ if (origin.kind === "repo-tree") {
327
+ if (typeof origin.hash !== "string" || origin.hash.length === 0) throw new Error("Invalid file workdir node: repo-tree origin is missing hash");
328
+ return {
329
+ kind: "repo-tree",
330
+ hash: origin.hash
331
+ };
332
+ }
333
+ if (origin.kind !== "repo-blob") throw new Error(`Invalid file workdir node origin kind: ${String(origin.kind)}`);
334
+ if (typeof origin.hash !== "string" || origin.hash.length === 0) throw new Error("Invalid file workdir node: repo-blob origin is missing hash");
335
+ if (origin.mode !== "100644" && origin.mode !== "100755" && origin.mode !== "120000") throw new Error(`Invalid file workdir node origin mode: ${String(origin.mode)}`);
336
+ return {
337
+ kind: "repo-blob",
338
+ mode: origin.mode,
339
+ hash: origin.hash
340
+ };
341
+ }
342
+ function readPayload(contentDir, payloadRef) {
343
+ const path = getContentPath(contentDir, payloadRef);
344
+ if (!existsSync(path)) throw new Error(`Virtual workdir payload not found: ${payloadRef}`);
345
+ return readFileSync(path);
346
+ }
347
+ function readJson(path) {
348
+ return JSON.parse(readFileSync(path, "utf-8"));
349
+ }
350
+ function writeJsonAtomic(path, value) {
351
+ writeBufferAtomic(path, Buffer.from(JSON.stringify(value), "utf-8"));
352
+ }
353
+ function writeBufferAtomic(path, value) {
354
+ mkdirSync(dirname(path), { recursive: true });
355
+ const tempPath = `${path}.tmp`;
356
+ writeFileSync(tempPath, value);
357
+ renameSync(tempPath, path);
358
+ }
359
+ function encodePathToken(value) {
360
+ return encodeURIComponent(value);
361
+ }
362
+ function decodePathToken(value) {
363
+ return decodeURIComponent(value);
364
+ }
365
+ //#endregion
366
+ export { createFileVirtualWorkdirBackend };
@@ -0,0 +1,2 @@
1
+ import { CreateFileVirtualWorkdirBackendOptions, createFileVirtualWorkdirBackend } from "./file-backend.mjs";
2
+ export { type CreateFileVirtualWorkdirBackendOptions, createFileVirtualWorkdirBackend };
@@ -0,0 +1,2 @@
1
+ import { createFileVirtualWorkdirBackend } from "./file-backend.mjs";
2
+ export { createFileVirtualWorkdirBackend };
@@ -0,0 +1,7 @@
1
+ //#region src/workdir/ids.d.ts
2
+ /** 会话内节点的稳定身份 */
3
+ type NodeId = string & {
4
+ readonly __brand: "NodeId";
5
+ };
6
+ //#endregion
7
+ export { NodeId };
@@ -0,0 +1,17 @@
1
+ import { VirtualWorkdirBackend } from "./core.mjs";
2
+
3
+ //#region src/workdir/memory-backend.d.ts
4
+ /**
5
+ * 创建内存版 Virtual Workdir backend
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const backend = createMemoryVirtualWorkdirBackend();
10
+ * const sessionId = backend.createSession({ baseTree: tree });
11
+ * const session = backend.openSession(repo.objects, sessionId);
12
+ * expect(session.baseTree).toBe(tree);
13
+ * ```
14
+ */
15
+ declare function createMemoryVirtualWorkdirBackend(): VirtualWorkdirBackend;
16
+ //#endregion
17
+ export { createMemoryVirtualWorkdirBackend };