loro-repo 0.5.0 → 0.5.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.
@@ -0,0 +1,164 @@
1
+ const require_chunk = require('../chunk.cjs');
2
+ let __loro_dev_flock = require("@loro-dev/flock");
3
+ __loro_dev_flock = require_chunk.__toESM(__loro_dev_flock);
4
+ let loro_crdt = require("loro-crdt");
5
+ loro_crdt = require_chunk.__toESM(loro_crdt);
6
+ let node_fs = require("node:fs");
7
+ node_fs = require_chunk.__toESM(node_fs);
8
+ let node_path = require("node:path");
9
+ node_path = require_chunk.__toESM(node_path);
10
+ let node_crypto = require("node:crypto");
11
+ node_crypto = require_chunk.__toESM(node_crypto);
12
+
13
+ //#region src/storage/filesystem.ts
14
+ const textDecoder = new TextDecoder();
15
+ var FileSystemStorageAdaptor = class {
16
+ baseDir;
17
+ docsDir;
18
+ assetsDir;
19
+ metaPath;
20
+ initPromise;
21
+ updateCounter = 0;
22
+ constructor(options = {}) {
23
+ this.baseDir = node_path.resolve(options.baseDir ?? node_path.join(process.cwd(), ".loro-repo"));
24
+ this.docsDir = node_path.join(this.baseDir, options.docsDirName ?? "docs");
25
+ this.assetsDir = node_path.join(this.baseDir, options.assetsDirName ?? "assets");
26
+ this.metaPath = node_path.join(this.baseDir, options.metaFileName ?? "meta.json");
27
+ this.initPromise = this.ensureLayout();
28
+ }
29
+ async save(payload) {
30
+ await this.initPromise;
31
+ switch (payload.type) {
32
+ case "doc-snapshot":
33
+ await this.writeDocSnapshot(payload.docId, payload.snapshot);
34
+ return;
35
+ case "doc-update":
36
+ await this.enqueueDocUpdate(payload.docId, payload.update);
37
+ return;
38
+ case "asset":
39
+ await this.writeAsset(payload.assetId, payload.data);
40
+ return;
41
+ case "meta":
42
+ await writeFileAtomic(this.metaPath, payload.update);
43
+ return;
44
+ default: throw new Error(`Unsupported payload type: ${payload.type}`);
45
+ }
46
+ }
47
+ async deleteAsset(assetId) {
48
+ await this.initPromise;
49
+ await removeIfExists(this.assetPath(assetId));
50
+ }
51
+ async loadDoc(docId) {
52
+ await this.initPromise;
53
+ const snapshotBytes = await readFileIfExists(this.docSnapshotPath(docId));
54
+ const updateDir = this.docUpdatesDir(docId);
55
+ const updateFiles = await listFiles(updateDir);
56
+ if (!snapshotBytes && updateFiles.length === 0) return;
57
+ const doc = snapshotBytes ? loro_crdt.LoroDoc.fromSnapshot(snapshotBytes) : new loro_crdt.LoroDoc();
58
+ if (updateFiles.length === 0) return doc;
59
+ const updatePaths = updateFiles.map((file) => node_path.join(updateDir, file));
60
+ for (const updatePath of updatePaths) {
61
+ const update = await readFileIfExists(updatePath);
62
+ if (!update) continue;
63
+ doc.import(update);
64
+ }
65
+ await Promise.all(updatePaths.map((filePath) => removeIfExists(filePath)));
66
+ const consolidated = doc.export({ mode: "snapshot" });
67
+ await this.writeDocSnapshot(docId, consolidated);
68
+ return doc;
69
+ }
70
+ async loadMeta() {
71
+ await this.initPromise;
72
+ const bytes = await readFileIfExists(this.metaPath);
73
+ if (!bytes) return void 0;
74
+ try {
75
+ const bundle = JSON.parse(textDecoder.decode(bytes));
76
+ const flock = new __loro_dev_flock.Flock();
77
+ flock.importJson(bundle);
78
+ return flock;
79
+ } catch (error) {
80
+ throw new Error("Failed to hydrate metadata snapshot", { cause: error });
81
+ }
82
+ }
83
+ async loadAsset(assetId) {
84
+ await this.initPromise;
85
+ return readFileIfExists(this.assetPath(assetId));
86
+ }
87
+ async ensureLayout() {
88
+ await Promise.all([
89
+ ensureDir(this.baseDir),
90
+ ensureDir(this.docsDir),
91
+ ensureDir(this.assetsDir)
92
+ ]);
93
+ }
94
+ async writeDocSnapshot(docId, snapshot) {
95
+ await ensureDir(this.docDir(docId));
96
+ await writeFileAtomic(this.docSnapshotPath(docId), snapshot);
97
+ }
98
+ async enqueueDocUpdate(docId, update) {
99
+ const dir = this.docUpdatesDir(docId);
100
+ await ensureDir(dir);
101
+ const counter = this.updateCounter = (this.updateCounter + 1) % 1e6;
102
+ const fileName = `${Date.now().toString().padStart(13, "0")}-${counter.toString().padStart(6, "0")}.bin`;
103
+ await writeFileAtomic(node_path.join(dir, fileName), update);
104
+ }
105
+ async writeAsset(assetId, data) {
106
+ const filePath = this.assetPath(assetId);
107
+ await ensureDir(node_path.dirname(filePath));
108
+ await writeFileAtomic(filePath, data);
109
+ }
110
+ docDir(docId) {
111
+ return node_path.join(this.docsDir, encodeComponent(docId));
112
+ }
113
+ docSnapshotPath(docId) {
114
+ return node_path.join(this.docDir(docId), "snapshot.bin");
115
+ }
116
+ docUpdatesDir(docId) {
117
+ return node_path.join(this.docDir(docId), "updates");
118
+ }
119
+ assetPath(assetId) {
120
+ return node_path.join(this.assetsDir, encodeComponent(assetId));
121
+ }
122
+ };
123
+ function encodeComponent(value) {
124
+ return Buffer.from(value, "utf8").toString("base64url");
125
+ }
126
+ async function ensureDir(dir) {
127
+ await node_fs.promises.mkdir(dir, { recursive: true });
128
+ }
129
+ async function readFileIfExists(filePath) {
130
+ try {
131
+ const data = await node_fs.promises.readFile(filePath);
132
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
133
+ } catch (error) {
134
+ if (error.code === "ENOENT") return;
135
+ throw error;
136
+ }
137
+ }
138
+ async function removeIfExists(filePath) {
139
+ try {
140
+ await node_fs.promises.rm(filePath);
141
+ } catch (error) {
142
+ if (error.code === "ENOENT") return;
143
+ throw error;
144
+ }
145
+ }
146
+ async function listFiles(dir) {
147
+ try {
148
+ return (await node_fs.promises.readdir(dir)).sort();
149
+ } catch (error) {
150
+ if (error.code === "ENOENT") return [];
151
+ throw error;
152
+ }
153
+ }
154
+ async function writeFileAtomic(targetPath, data) {
155
+ const dir = node_path.dirname(targetPath);
156
+ await ensureDir(dir);
157
+ const tempPath = node_path.join(dir, `.tmp-${(0, node_crypto.randomUUID)()}`);
158
+ await node_fs.promises.writeFile(tempPath, data);
159
+ await node_fs.promises.rename(tempPath, targetPath);
160
+ }
161
+
162
+ //#endregion
163
+ exports.FileSystemStorageAdaptor = FileSystemStorageAdaptor;
164
+ //# sourceMappingURL=filesystem.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filesystem.cjs","names":["path","LoroDoc","Flock","fs"],"sources":["../../src/storage/filesystem.ts"],"sourcesContent":["import { promises as fs } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport { Flock, type ExportBundle } from \"@loro-dev/flock\";\nimport { LoroDoc } from \"loro-crdt\";\n\nimport type { AssetId, StorageAdapter, StorageSavePayload } from \"../types\";\n\nconst textDecoder = new TextDecoder();\n\nexport interface FileSystemStorageAdaptorOptions {\n /**\n * Base directory where metadata, document snapshots, and assets will be stored.\n * Defaults to `.loro-repo` inside the current working directory.\n */\n readonly baseDir?: string;\n /**\n * Subdirectory dedicated to document persistence. Defaults to `docs`.\n */\n readonly docsDirName?: string;\n /**\n * Subdirectory dedicated to asset blobs. Defaults to `assets`.\n */\n readonly assetsDirName?: string;\n /**\n * File name for the metadata snapshot bundle. Defaults to `meta.json`.\n */\n readonly metaFileName?: string;\n}\n\nexport class FileSystemStorageAdaptor implements StorageAdapter {\n private readonly baseDir: string;\n private readonly docsDir: string;\n private readonly assetsDir: string;\n private readonly metaPath: string;\n private readonly initPromise: Promise<void>;\n private updateCounter = 0;\n\n constructor(options: FileSystemStorageAdaptorOptions = {}) {\n this.baseDir = path.resolve(\n options.baseDir ?? path.join(process.cwd(), \".loro-repo\"),\n );\n this.docsDir = path.join(this.baseDir, options.docsDirName ?? \"docs\");\n this.assetsDir = path.join(this.baseDir, options.assetsDirName ?? \"assets\");\n this.metaPath = path.join(this.baseDir, options.metaFileName ?? \"meta.json\");\n this.initPromise = this.ensureLayout();\n }\n\n async save(payload: StorageSavePayload): Promise<void> {\n await this.initPromise;\n switch (payload.type) {\n case \"doc-snapshot\":\n await this.writeDocSnapshot(payload.docId, payload.snapshot);\n return;\n case \"doc-update\":\n await this.enqueueDocUpdate(payload.docId, payload.update);\n return;\n case \"asset\":\n await this.writeAsset(payload.assetId, payload.data);\n return;\n case \"meta\":\n await writeFileAtomic(this.metaPath, payload.update);\n return;\n default:\n throw new Error(`Unsupported payload type: ${(payload as { type: string }).type}`);\n }\n }\n\n async deleteAsset(assetId: AssetId): Promise<void> {\n await this.initPromise;\n const filePath = this.assetPath(assetId);\n await removeIfExists(filePath);\n }\n\n async loadDoc(docId: string): Promise<LoroDoc | undefined> {\n await this.initPromise;\n const snapshotPath = this.docSnapshotPath(docId);\n const snapshotBytes = await readFileIfExists(snapshotPath);\n const updateDir = this.docUpdatesDir(docId);\n const updateFiles = await listFiles(updateDir);\n\n if (!snapshotBytes && updateFiles.length === 0) {\n return undefined;\n }\n\n const doc = snapshotBytes\n ? LoroDoc.fromSnapshot(snapshotBytes)\n : new LoroDoc();\n\n if (updateFiles.length === 0) {\n return doc;\n }\n\n const updatePaths = updateFiles.map((file) => path.join(updateDir, file));\n for (const updatePath of updatePaths) {\n const update = await readFileIfExists(updatePath);\n if (!update) continue;\n doc.import(update);\n }\n\n await Promise.all(updatePaths.map((filePath) => removeIfExists(filePath)));\n\n const consolidated = doc.export({ mode: \"snapshot\" });\n await this.writeDocSnapshot(docId, consolidated);\n return doc;\n }\n\n async loadMeta(): Promise<Flock | undefined> {\n await this.initPromise;\n const bytes = await readFileIfExists(this.metaPath);\n if (!bytes) return undefined;\n try {\n const bundle = JSON.parse(textDecoder.decode(bytes)) as ExportBundle;\n const flock = new Flock();\n flock.importJson(bundle);\n return flock;\n } catch (error) {\n throw new Error(\"Failed to hydrate metadata snapshot\", { cause: error });\n }\n }\n\n async loadAsset(assetId: AssetId): Promise<Uint8Array | undefined> {\n await this.initPromise;\n return readFileIfExists(this.assetPath(assetId));\n }\n\n private async ensureLayout(): Promise<void> {\n await Promise.all([\n ensureDir(this.baseDir),\n ensureDir(this.docsDir),\n ensureDir(this.assetsDir),\n ]);\n }\n\n private async writeDocSnapshot(\n docId: string,\n snapshot: Uint8Array,\n ): Promise<void> {\n const targetDir = this.docDir(docId);\n await ensureDir(targetDir);\n await writeFileAtomic(this.docSnapshotPath(docId), snapshot);\n }\n\n private async enqueueDocUpdate(\n docId: string,\n update: Uint8Array,\n ): Promise<void> {\n const dir = this.docUpdatesDir(docId);\n await ensureDir(dir);\n const counter = (this.updateCounter = (this.updateCounter + 1) % 1_000_000);\n const timestamp = Date.now().toString().padStart(13, \"0\");\n const fileName = `${timestamp}-${counter.toString().padStart(6, \"0\")}.bin`;\n const filePath = path.join(dir, fileName);\n await writeFileAtomic(filePath, update);\n }\n\n private async writeAsset(assetId: AssetId, data: Uint8Array): Promise<void> {\n const filePath = this.assetPath(assetId);\n await ensureDir(path.dirname(filePath));\n await writeFileAtomic(filePath, data);\n }\n\n private docDir(docId: string): string {\n return path.join(this.docsDir, encodeComponent(docId));\n }\n\n private docSnapshotPath(docId: string): string {\n return path.join(this.docDir(docId), \"snapshot.bin\");\n }\n\n private docUpdatesDir(docId: string): string {\n return path.join(this.docDir(docId), \"updates\");\n }\n\n private assetPath(assetId: AssetId): string {\n return path.join(this.assetsDir, encodeComponent(assetId));\n }\n}\n\nfunction encodeComponent(value: string): string {\n return Buffer.from(value, \"utf8\").toString(\"base64url\");\n}\n\nasync function ensureDir(dir: string): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n}\n\nasync function readFileIfExists(filePath: string): Promise<Uint8Array | undefined> {\n try {\n const data = await fs.readFile(filePath);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return undefined;\n }\n throw error;\n }\n}\n\nasync function removeIfExists(filePath: string): Promise<void> {\n try {\n await fs.rm(filePath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return;\n }\n throw error;\n }\n}\n\nasync function listFiles(dir: string): Promise<string[]> {\n try {\n const entries = await fs.readdir(dir);\n return entries.sort();\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return [];\n }\n throw error;\n }\n}\n\nasync function writeFileAtomic(\n targetPath: string,\n data: Uint8Array,\n): Promise<void> {\n const dir = path.dirname(targetPath);\n await ensureDir(dir);\n const tempPath = path.join(dir, `.tmp-${randomUUID()}`);\n await fs.writeFile(tempPath, data);\n await fs.rename(tempPath, targetPath);\n}\n"],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import { C as StorageAdapter, r as AssetId, w as StorageSavePayload } from "../types.cjs";
2
+ import { Flock } from "@loro-dev/flock";
3
+ import { LoroDoc } from "loro-crdt";
4
+
5
+ //#region src/storage/filesystem.d.ts
6
+ interface FileSystemStorageAdaptorOptions {
7
+ /**
8
+ * Base directory where metadata, document snapshots, and assets will be stored.
9
+ * Defaults to `.loro-repo` inside the current working directory.
10
+ */
11
+ readonly baseDir?: string;
12
+ /**
13
+ * Subdirectory dedicated to document persistence. Defaults to `docs`.
14
+ */
15
+ readonly docsDirName?: string;
16
+ /**
17
+ * Subdirectory dedicated to asset blobs. Defaults to `assets`.
18
+ */
19
+ readonly assetsDirName?: string;
20
+ /**
21
+ * File name for the metadata snapshot bundle. Defaults to `meta.json`.
22
+ */
23
+ readonly metaFileName?: string;
24
+ }
25
+ declare class FileSystemStorageAdaptor implements StorageAdapter {
26
+ private readonly baseDir;
27
+ private readonly docsDir;
28
+ private readonly assetsDir;
29
+ private readonly metaPath;
30
+ private readonly initPromise;
31
+ private updateCounter;
32
+ constructor(options?: FileSystemStorageAdaptorOptions);
33
+ save(payload: StorageSavePayload): Promise<void>;
34
+ deleteAsset(assetId: AssetId): Promise<void>;
35
+ loadDoc(docId: string): Promise<LoroDoc | undefined>;
36
+ loadMeta(): Promise<Flock | undefined>;
37
+ loadAsset(assetId: AssetId): Promise<Uint8Array | undefined>;
38
+ private ensureLayout;
39
+ private writeDocSnapshot;
40
+ private enqueueDocUpdate;
41
+ private writeAsset;
42
+ private docDir;
43
+ private docSnapshotPath;
44
+ private docUpdatesDir;
45
+ private assetPath;
46
+ }
47
+ //#endregion
48
+ export { FileSystemStorageAdaptor, FileSystemStorageAdaptorOptions };
49
+ //# sourceMappingURL=filesystem.d.cts.map
@@ -0,0 +1,49 @@
1
+ import { C as StorageAdapter, r as AssetId, w as StorageSavePayload } from "../types.js";
2
+ import { Flock } from "@loro-dev/flock";
3
+ import { LoroDoc } from "loro-crdt";
4
+
5
+ //#region src/storage/filesystem.d.ts
6
+ interface FileSystemStorageAdaptorOptions {
7
+ /**
8
+ * Base directory where metadata, document snapshots, and assets will be stored.
9
+ * Defaults to `.loro-repo` inside the current working directory.
10
+ */
11
+ readonly baseDir?: string;
12
+ /**
13
+ * Subdirectory dedicated to document persistence. Defaults to `docs`.
14
+ */
15
+ readonly docsDirName?: string;
16
+ /**
17
+ * Subdirectory dedicated to asset blobs. Defaults to `assets`.
18
+ */
19
+ readonly assetsDirName?: string;
20
+ /**
21
+ * File name for the metadata snapshot bundle. Defaults to `meta.json`.
22
+ */
23
+ readonly metaFileName?: string;
24
+ }
25
+ declare class FileSystemStorageAdaptor implements StorageAdapter {
26
+ private readonly baseDir;
27
+ private readonly docsDir;
28
+ private readonly assetsDir;
29
+ private readonly metaPath;
30
+ private readonly initPromise;
31
+ private updateCounter;
32
+ constructor(options?: FileSystemStorageAdaptorOptions);
33
+ save(payload: StorageSavePayload): Promise<void>;
34
+ deleteAsset(assetId: AssetId): Promise<void>;
35
+ loadDoc(docId: string): Promise<LoroDoc | undefined>;
36
+ loadMeta(): Promise<Flock | undefined>;
37
+ loadAsset(assetId: AssetId): Promise<Uint8Array | undefined>;
38
+ private ensureLayout;
39
+ private writeDocSnapshot;
40
+ private enqueueDocUpdate;
41
+ private writeAsset;
42
+ private docDir;
43
+ private docSnapshotPath;
44
+ private docUpdatesDir;
45
+ private assetPath;
46
+ }
47
+ //#endregion
48
+ export { FileSystemStorageAdaptor, FileSystemStorageAdaptorOptions };
49
+ //# sourceMappingURL=filesystem.d.ts.map
@@ -0,0 +1,158 @@
1
+ import { Flock } from "@loro-dev/flock";
2
+ import { LoroDoc } from "loro-crdt";
3
+ import { promises } from "node:fs";
4
+ import * as path from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+
7
+ //#region src/storage/filesystem.ts
8
+ const textDecoder = new TextDecoder();
9
+ var FileSystemStorageAdaptor = class {
10
+ baseDir;
11
+ docsDir;
12
+ assetsDir;
13
+ metaPath;
14
+ initPromise;
15
+ updateCounter = 0;
16
+ constructor(options = {}) {
17
+ this.baseDir = path.resolve(options.baseDir ?? path.join(process.cwd(), ".loro-repo"));
18
+ this.docsDir = path.join(this.baseDir, options.docsDirName ?? "docs");
19
+ this.assetsDir = path.join(this.baseDir, options.assetsDirName ?? "assets");
20
+ this.metaPath = path.join(this.baseDir, options.metaFileName ?? "meta.json");
21
+ this.initPromise = this.ensureLayout();
22
+ }
23
+ async save(payload) {
24
+ await this.initPromise;
25
+ switch (payload.type) {
26
+ case "doc-snapshot":
27
+ await this.writeDocSnapshot(payload.docId, payload.snapshot);
28
+ return;
29
+ case "doc-update":
30
+ await this.enqueueDocUpdate(payload.docId, payload.update);
31
+ return;
32
+ case "asset":
33
+ await this.writeAsset(payload.assetId, payload.data);
34
+ return;
35
+ case "meta":
36
+ await writeFileAtomic(this.metaPath, payload.update);
37
+ return;
38
+ default: throw new Error(`Unsupported payload type: ${payload.type}`);
39
+ }
40
+ }
41
+ async deleteAsset(assetId) {
42
+ await this.initPromise;
43
+ await removeIfExists(this.assetPath(assetId));
44
+ }
45
+ async loadDoc(docId) {
46
+ await this.initPromise;
47
+ const snapshotBytes = await readFileIfExists(this.docSnapshotPath(docId));
48
+ const updateDir = this.docUpdatesDir(docId);
49
+ const updateFiles = await listFiles(updateDir);
50
+ if (!snapshotBytes && updateFiles.length === 0) return;
51
+ const doc = snapshotBytes ? LoroDoc.fromSnapshot(snapshotBytes) : new LoroDoc();
52
+ if (updateFiles.length === 0) return doc;
53
+ const updatePaths = updateFiles.map((file) => path.join(updateDir, file));
54
+ for (const updatePath of updatePaths) {
55
+ const update = await readFileIfExists(updatePath);
56
+ if (!update) continue;
57
+ doc.import(update);
58
+ }
59
+ await Promise.all(updatePaths.map((filePath) => removeIfExists(filePath)));
60
+ const consolidated = doc.export({ mode: "snapshot" });
61
+ await this.writeDocSnapshot(docId, consolidated);
62
+ return doc;
63
+ }
64
+ async loadMeta() {
65
+ await this.initPromise;
66
+ const bytes = await readFileIfExists(this.metaPath);
67
+ if (!bytes) return void 0;
68
+ try {
69
+ const bundle = JSON.parse(textDecoder.decode(bytes));
70
+ const flock = new Flock();
71
+ flock.importJson(bundle);
72
+ return flock;
73
+ } catch (error) {
74
+ throw new Error("Failed to hydrate metadata snapshot", { cause: error });
75
+ }
76
+ }
77
+ async loadAsset(assetId) {
78
+ await this.initPromise;
79
+ return readFileIfExists(this.assetPath(assetId));
80
+ }
81
+ async ensureLayout() {
82
+ await Promise.all([
83
+ ensureDir(this.baseDir),
84
+ ensureDir(this.docsDir),
85
+ ensureDir(this.assetsDir)
86
+ ]);
87
+ }
88
+ async writeDocSnapshot(docId, snapshot) {
89
+ await ensureDir(this.docDir(docId));
90
+ await writeFileAtomic(this.docSnapshotPath(docId), snapshot);
91
+ }
92
+ async enqueueDocUpdate(docId, update) {
93
+ const dir = this.docUpdatesDir(docId);
94
+ await ensureDir(dir);
95
+ const counter = this.updateCounter = (this.updateCounter + 1) % 1e6;
96
+ const fileName = `${Date.now().toString().padStart(13, "0")}-${counter.toString().padStart(6, "0")}.bin`;
97
+ await writeFileAtomic(path.join(dir, fileName), update);
98
+ }
99
+ async writeAsset(assetId, data) {
100
+ const filePath = this.assetPath(assetId);
101
+ await ensureDir(path.dirname(filePath));
102
+ await writeFileAtomic(filePath, data);
103
+ }
104
+ docDir(docId) {
105
+ return path.join(this.docsDir, encodeComponent(docId));
106
+ }
107
+ docSnapshotPath(docId) {
108
+ return path.join(this.docDir(docId), "snapshot.bin");
109
+ }
110
+ docUpdatesDir(docId) {
111
+ return path.join(this.docDir(docId), "updates");
112
+ }
113
+ assetPath(assetId) {
114
+ return path.join(this.assetsDir, encodeComponent(assetId));
115
+ }
116
+ };
117
+ function encodeComponent(value) {
118
+ return Buffer.from(value, "utf8").toString("base64url");
119
+ }
120
+ async function ensureDir(dir) {
121
+ await promises.mkdir(dir, { recursive: true });
122
+ }
123
+ async function readFileIfExists(filePath) {
124
+ try {
125
+ const data = await promises.readFile(filePath);
126
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
127
+ } catch (error) {
128
+ if (error.code === "ENOENT") return;
129
+ throw error;
130
+ }
131
+ }
132
+ async function removeIfExists(filePath) {
133
+ try {
134
+ await promises.rm(filePath);
135
+ } catch (error) {
136
+ if (error.code === "ENOENT") return;
137
+ throw error;
138
+ }
139
+ }
140
+ async function listFiles(dir) {
141
+ try {
142
+ return (await promises.readdir(dir)).sort();
143
+ } catch (error) {
144
+ if (error.code === "ENOENT") return [];
145
+ throw error;
146
+ }
147
+ }
148
+ async function writeFileAtomic(targetPath, data) {
149
+ const dir = path.dirname(targetPath);
150
+ await ensureDir(dir);
151
+ const tempPath = path.join(dir, `.tmp-${randomUUID()}`);
152
+ await promises.writeFile(tempPath, data);
153
+ await promises.rename(tempPath, targetPath);
154
+ }
155
+
156
+ //#endregion
157
+ export { FileSystemStorageAdaptor };
158
+ //# sourceMappingURL=filesystem.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filesystem.js","names":["fs"],"sources":["../../src/storage/filesystem.ts"],"sourcesContent":["import { promises as fs } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport { Flock, type ExportBundle } from \"@loro-dev/flock\";\nimport { LoroDoc } from \"loro-crdt\";\n\nimport type { AssetId, StorageAdapter, StorageSavePayload } from \"../types\";\n\nconst textDecoder = new TextDecoder();\n\nexport interface FileSystemStorageAdaptorOptions {\n /**\n * Base directory where metadata, document snapshots, and assets will be stored.\n * Defaults to `.loro-repo` inside the current working directory.\n */\n readonly baseDir?: string;\n /**\n * Subdirectory dedicated to document persistence. Defaults to `docs`.\n */\n readonly docsDirName?: string;\n /**\n * Subdirectory dedicated to asset blobs. Defaults to `assets`.\n */\n readonly assetsDirName?: string;\n /**\n * File name for the metadata snapshot bundle. Defaults to `meta.json`.\n */\n readonly metaFileName?: string;\n}\n\nexport class FileSystemStorageAdaptor implements StorageAdapter {\n private readonly baseDir: string;\n private readonly docsDir: string;\n private readonly assetsDir: string;\n private readonly metaPath: string;\n private readonly initPromise: Promise<void>;\n private updateCounter = 0;\n\n constructor(options: FileSystemStorageAdaptorOptions = {}) {\n this.baseDir = path.resolve(\n options.baseDir ?? path.join(process.cwd(), \".loro-repo\"),\n );\n this.docsDir = path.join(this.baseDir, options.docsDirName ?? \"docs\");\n this.assetsDir = path.join(this.baseDir, options.assetsDirName ?? \"assets\");\n this.metaPath = path.join(this.baseDir, options.metaFileName ?? \"meta.json\");\n this.initPromise = this.ensureLayout();\n }\n\n async save(payload: StorageSavePayload): Promise<void> {\n await this.initPromise;\n switch (payload.type) {\n case \"doc-snapshot\":\n await this.writeDocSnapshot(payload.docId, payload.snapshot);\n return;\n case \"doc-update\":\n await this.enqueueDocUpdate(payload.docId, payload.update);\n return;\n case \"asset\":\n await this.writeAsset(payload.assetId, payload.data);\n return;\n case \"meta\":\n await writeFileAtomic(this.metaPath, payload.update);\n return;\n default:\n throw new Error(`Unsupported payload type: ${(payload as { type: string }).type}`);\n }\n }\n\n async deleteAsset(assetId: AssetId): Promise<void> {\n await this.initPromise;\n const filePath = this.assetPath(assetId);\n await removeIfExists(filePath);\n }\n\n async loadDoc(docId: string): Promise<LoroDoc | undefined> {\n await this.initPromise;\n const snapshotPath = this.docSnapshotPath(docId);\n const snapshotBytes = await readFileIfExists(snapshotPath);\n const updateDir = this.docUpdatesDir(docId);\n const updateFiles = await listFiles(updateDir);\n\n if (!snapshotBytes && updateFiles.length === 0) {\n return undefined;\n }\n\n const doc = snapshotBytes\n ? LoroDoc.fromSnapshot(snapshotBytes)\n : new LoroDoc();\n\n if (updateFiles.length === 0) {\n return doc;\n }\n\n const updatePaths = updateFiles.map((file) => path.join(updateDir, file));\n for (const updatePath of updatePaths) {\n const update = await readFileIfExists(updatePath);\n if (!update) continue;\n doc.import(update);\n }\n\n await Promise.all(updatePaths.map((filePath) => removeIfExists(filePath)));\n\n const consolidated = doc.export({ mode: \"snapshot\" });\n await this.writeDocSnapshot(docId, consolidated);\n return doc;\n }\n\n async loadMeta(): Promise<Flock | undefined> {\n await this.initPromise;\n const bytes = await readFileIfExists(this.metaPath);\n if (!bytes) return undefined;\n try {\n const bundle = JSON.parse(textDecoder.decode(bytes)) as ExportBundle;\n const flock = new Flock();\n flock.importJson(bundle);\n return flock;\n } catch (error) {\n throw new Error(\"Failed to hydrate metadata snapshot\", { cause: error });\n }\n }\n\n async loadAsset(assetId: AssetId): Promise<Uint8Array | undefined> {\n await this.initPromise;\n return readFileIfExists(this.assetPath(assetId));\n }\n\n private async ensureLayout(): Promise<void> {\n await Promise.all([\n ensureDir(this.baseDir),\n ensureDir(this.docsDir),\n ensureDir(this.assetsDir),\n ]);\n }\n\n private async writeDocSnapshot(\n docId: string,\n snapshot: Uint8Array,\n ): Promise<void> {\n const targetDir = this.docDir(docId);\n await ensureDir(targetDir);\n await writeFileAtomic(this.docSnapshotPath(docId), snapshot);\n }\n\n private async enqueueDocUpdate(\n docId: string,\n update: Uint8Array,\n ): Promise<void> {\n const dir = this.docUpdatesDir(docId);\n await ensureDir(dir);\n const counter = (this.updateCounter = (this.updateCounter + 1) % 1_000_000);\n const timestamp = Date.now().toString().padStart(13, \"0\");\n const fileName = `${timestamp}-${counter.toString().padStart(6, \"0\")}.bin`;\n const filePath = path.join(dir, fileName);\n await writeFileAtomic(filePath, update);\n }\n\n private async writeAsset(assetId: AssetId, data: Uint8Array): Promise<void> {\n const filePath = this.assetPath(assetId);\n await ensureDir(path.dirname(filePath));\n await writeFileAtomic(filePath, data);\n }\n\n private docDir(docId: string): string {\n return path.join(this.docsDir, encodeComponent(docId));\n }\n\n private docSnapshotPath(docId: string): string {\n return path.join(this.docDir(docId), \"snapshot.bin\");\n }\n\n private docUpdatesDir(docId: string): string {\n return path.join(this.docDir(docId), \"updates\");\n }\n\n private assetPath(assetId: AssetId): string {\n return path.join(this.assetsDir, encodeComponent(assetId));\n }\n}\n\nfunction encodeComponent(value: string): string {\n return Buffer.from(value, \"utf8\").toString(\"base64url\");\n}\n\nasync function ensureDir(dir: string): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n}\n\nasync function readFileIfExists(filePath: string): Promise<Uint8Array | undefined> {\n try {\n const data = await fs.readFile(filePath);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return undefined;\n }\n throw error;\n }\n}\n\nasync function removeIfExists(filePath: string): Promise<void> {\n try {\n await fs.rm(filePath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return;\n }\n throw error;\n }\n}\n\nasync function listFiles(dir: string): Promise<string[]> {\n try {\n const entries = await fs.readdir(dir);\n return entries.sort();\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return [];\n }\n throw error;\n }\n}\n\nasync function writeFileAtomic(\n targetPath: string,\n data: Uint8Array,\n): Promise<void> {\n const dir = path.dirname(targetPath);\n await ensureDir(dir);\n const tempPath = path.join(dir, `.tmp-${randomUUID()}`);\n await fs.writeFile(tempPath, data);\n await fs.rename(tempPath, targetPath);\n}\n"],"mappings":"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"}