@optimystic/db-p2p-storage-fs 0.14.0 → 0.16.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.
@@ -1,7 +1,10 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
+ import { KvRawStorage } from "@optimystic/db-p2p";
3
4
  import { createLogger } from './logger.js';
5
+ import { atomicWriteFile } from './atomic-write.js';
4
6
  const log = createLogger('storage:file');
7
+ const decoder = new TextDecoder();
5
8
  // Colons are illegal in Windows filenames; encode them so action ids like
6
9
  // `tx:<hash>` and `stamp:<hash>` round-trip safely on all platforms.
7
10
  function encodeActionIdForFilename(actionId) {
@@ -10,83 +13,240 @@ function encodeActionIdForFilename(actionId) {
10
13
  function decodeFilenameToActionId(filename) {
11
14
  return filename.replace(/%3A/g, ':');
12
15
  }
13
- export class FileRawStorage {
16
+ // A torn write leaves valid-prefix JSON cut off mid-token — JSON.parse throws
17
+ // SyntaxError. Used as the "corrupt content → treat as missing" guard so a
18
+ // crash-truncated file reads as absent (letting recover() make progress) instead
19
+ // of surfacing a parse error forever. Only the JSON-valued stores use this; the
20
+ // revisions store holds a bare ActionId string (not JSON) and is never guarded.
21
+ // NOTE: every guarded read parses the JSON here (validate, discard) and the kernel's
22
+ // decodeJson parses the same bytes AGAIN to build the value — 2× parse per get. The
23
+ // guard is intrinsic to the driver's "corrupt→missing" contract (the kernel can't
24
+ // express it), so it can't simply be dropped. Fine at current read volumes; if a read
25
+ // path ever shows up hot, parse once and thread the parsed value through the driver.
26
+ function isParseableJson(bytes) {
27
+ try {
28
+ JSON.parse(decoder.decode(bytes));
29
+ return true;
30
+ }
31
+ catch (err) {
32
+ if (err instanceof SyntaxError)
33
+ return false;
34
+ throw err;
35
+ }
36
+ }
37
+ /**
38
+ * Filesystem {@link RawStoreDriver}: the five logical block-storage stores mapped
39
+ * to five subdirectories under `basePath/<blockId>/`
40
+ * (`{meta.json,revs/,pend/,actions/,blocks/}`). The directory tree is a
41
+ * deliberate, human-inspectable/debuggable layout — it is NOT flattened into
42
+ * encoded-filename KV keys.
43
+ *
44
+ * `KvRawStorage` now owns all JSON serialization, so this driver reads/writes raw
45
+ * `Uint8Array` bytes and never does `JSON.stringify/parse` on values. Everything
46
+ * else fs-specific lives here: atomic (temp-file + rename) writes, the
47
+ * corrupt-content-as-missing read guard, colon-encoded action-id filenames with
48
+ * the legacy raw-colon read fallback + win32 guards, and rename-based promote.
49
+ */
50
+ export class FileStoreDriver {
14
51
  basePath;
15
52
  constructor(basePath) {
16
53
  this.basePath = basePath;
17
54
  // TODO: use https://www.npmjs.com/package/proper-lockfile to take a lock on the basePath, also introduce explicit dispose pattern
18
55
  }
56
+ // --- metadata ---
19
57
  async getMetadata(blockId) {
20
- return this.readIfExists(this.getMetadataPath(blockId));
58
+ return this.readBytesIfExists(this.getMetadataPath(blockId), true);
21
59
  }
22
- async saveMetadata(blockId, metadata) {
23
- await this.ensureAndWriteFile(this.getMetadataPath(blockId), JSON.stringify(metadata));
60
+ async putMetadata(blockId, value) {
61
+ await atomicWriteFile(this.getMetadataPath(blockId), value);
24
62
  }
63
+ // --- revisions ---
64
+ // The revisions store value is a bare ActionId string (kernel `encodeActionId`,
65
+ // NOT JSON), so it is read WITHOUT the JSON guard — any bytes are a valid string
66
+ // and `decodeActionId` never throws.
67
+ // NOTE: because there is no guard here, a *torn* revision file reads back as a wrong
68
+ // (truncated) ActionId rather than as missing — unlike the JSON stores, which torn-read
69
+ // as undefined. Atomic writes (temp+rename) make a new torn revision impossible; only a
70
+ // legacy pre-atomic-write torn rev could hit this, and recover() re-derives revisions
71
+ // from the actions store anyway. If revisions ever move to a non-atomic writer, add a
72
+ // checksum/length guard here.
25
73
  async getRevision(blockId, rev) {
26
- return this.readIfExists(this.getRevisionPath(blockId, rev));
74
+ return this.readBytesIfExists(this.getRevisionPath(blockId, rev), false);
75
+ }
76
+ async putRevision(blockId, rev, value) {
77
+ await atomicWriteFile(this.getRevisionPath(blockId, rev), value);
27
78
  }
28
- async saveRevision(blockId, rev, actionId) {
29
- await this.ensureAndWriteFile(this.getRevisionPath(blockId, rev), JSON.stringify(actionId));
79
+ async *rangeRevisions(blockId, lo, hi, reverse) {
80
+ // The fs backend has no cursor: walk the bounded [lo, hi] range rev-by-rev,
81
+ // reading each present rev. The range is caller-bounded, so this avoids
82
+ // listing an unbounded revs/ directory. Drain into an array BEFORE yielding
83
+ // (drain-before-yield contract) — matches the memory/native drivers and keeps
84
+ // the consumer's interleaved awaits from straddling any in-flight read.
85
+ const results = [];
86
+ for (let rev = lo; rev <= hi; rev++) {
87
+ const value = await this.readBytesIfExists(this.getRevisionPath(blockId, rev), false);
88
+ if (value !== undefined) {
89
+ results.push([rev, value]);
90
+ }
91
+ }
92
+ if (reverse) {
93
+ results.reverse();
94
+ }
95
+ for (const result of results) {
96
+ yield result;
97
+ }
30
98
  }
31
- async getPendingTransaction(blockId, actionId) {
32
- return this.readIfExists(this.getPendingActionPath(blockId, actionId));
99
+ // --- pending ---
100
+ async getPending(blockId, actionId) {
101
+ return this.readActionScopedBytes(this.getPendingActionPath(blockId, actionId), this.getPendingActionPath(blockId, actionId, false));
33
102
  }
34
- async savePendingTransaction(blockId, actionId, transform) {
35
- await this.ensureAndWriteFile(this.getPendingActionPath(blockId, actionId), JSON.stringify(transform));
103
+ async putPending(blockId, actionId, value) {
104
+ await atomicWriteFile(this.getPendingActionPath(blockId, actionId), value);
36
105
  }
37
- async deletePendingTransaction(blockId, actionId) {
106
+ async deletePending(blockId, actionId) {
38
107
  const pendingPath = this.getPendingActionPath(blockId, actionId);
39
108
  await fs.unlink(pendingPath)
40
109
  .catch((err) => {
41
110
  if (err?.code !== 'ENOENT')
42
- log('deletePendingTransaction unlink failed for %s/%s - %o', blockId, actionId, err);
111
+ log('deletePending unlink failed for %s/%s - %o', blockId, actionId, err);
43
112
  });
113
+ await this.unlinkRawColon(pendingPath, this.getPendingActionPath(blockId, actionId, false));
44
114
  }
45
- async *listPendingTransactions(blockId) {
115
+ async *listPendingActionIds(blockId) {
46
116
  const pendingPath = path.join(this.getBlockPath(blockId), 'pend');
47
- const files = await fs.readdir(pendingPath).catch((err) => { log('listPendingTransactions readdir failed for %s - %o', blockId, err); return []; });
117
+ // Only a genuinely-absent directory (ENOENT) maps to "no pendings". Any other error
118
+ // (EACCES, EIO, ENOTDIR, ...) must surface — swallowing it here would make
119
+ // listPendingActionIds silently report an empty directory, so pend's conflict
120
+ // detection would be skipped. Mirrors directoryByteSize's ENOENT-vs-other discrimination.
121
+ const files = await fs.readdir(pendingPath).catch((err) => {
122
+ if (err?.code === 'ENOENT')
123
+ return [];
124
+ log('listPendingActionIds readdir failed for %s - %o', blockId, err);
125
+ throw err;
126
+ });
127
+ // Drain into an array before yielding (drain-before-yield): readdir has already
128
+ // resolved the full listing, so this just decodes/filters up front.
129
+ const ids = [];
48
130
  for (const file of files) {
49
131
  if (!file.endsWith('.json'))
50
132
  continue;
51
133
  const actionId = decodeFilenameToActionId(file.slice(0, -5));
52
- // Accept legacy UUID format and consensus tx:/stamp: format. The
53
- // consensus hash is base64url-encoded SHA-256 (see db-core hashString),
54
- // so its alphabet is [A-Za-z0-9_-] — NOT lowercase hex.
55
- if (!/^(?:[\w\d]+-[\w\d]+-[\w\d]+-[\w\d]+-[\w\d]+|(?:tx|stamp):[A-Za-z0-9_-]+)$/.test(actionId))
134
+ // Accept every realistic action id: legacy UUIDs (`[0-9a-f-]`), consensus
135
+ // tx:/stamp: ids (base64url-encoded SHA-256, alphabet `[A-Za-z0-9_-]` — see
136
+ // db-core hashString, NOT lowercase hex), AND the bare-alphanumeric ids the
137
+ // cross-backend conformance suite uses (`a1`, `b1`, ...). This is deliberately
138
+ // broad-but-not-total: an id is any `[A-Za-z0-9_-]` string, optionally prefixed
139
+ // with `tx:`/`stamp:`. It is NOT a total accept — a file whose decoded name
140
+ // carries other punctuation (a dot, a space) is genuine junk in pend/ and is
141
+ // logged-and-skipped rather than surfaced as a phantom pending. The memory/db
142
+ // reference drivers key on the raw id and never see a filesystem name, so this
143
+ // filter is fs-only; it must not drop an id those backends would list, hence the
144
+ // widened class (an earlier hex-only class silently dropped real consensus ids —
145
+ // see `optimystic-filestorage-colon-actionid-windows`).
146
+ if (!/^(?:tx:|stamp:)?[A-Za-z0-9_-]+$/.test(actionId)) {
147
+ // Leave a breadcrumb rather than silently dropping: the .json + decode guard
148
+ // already excludes *.tmp orphans, so anything reaching here is an unexpected
149
+ // filename a maintainer should see.
150
+ log('listPendingActionIds skipping unrecognized action-id file %s for %s', file, blockId);
56
151
  continue;
57
- yield actionId;
152
+ }
153
+ ids.push(actionId);
154
+ }
155
+ for (const id of ids) {
156
+ yield id;
58
157
  }
59
158
  }
159
+ // --- transactions ---
60
160
  async getTransaction(blockId, actionId) {
61
- return this.readIfExists(this.getActionPath(blockId, actionId));
161
+ return this.readActionScopedBytes(this.getActionPath(blockId, actionId), this.getActionPath(blockId, actionId, false));
62
162
  }
63
- async *listRevisions(blockId, startRev, endRev) {
64
- for (let rev = startRev; startRev <= endRev ? rev <= endRev : rev >= endRev; startRev <= endRev ? ++rev : --rev) {
65
- const actionId = await this.getRevision(blockId, rev);
66
- if (actionId) {
67
- yield { actionId, rev };
68
- }
69
- }
163
+ async putTransaction(blockId, actionId, value) {
164
+ await atomicWriteFile(this.getActionPath(blockId, actionId), value);
70
165
  }
71
- async saveTransaction(blockId, actionId, transform) {
72
- await this.ensureAndWriteFile(this.getActionPath(blockId, actionId), JSON.stringify(transform));
166
+ // --- materialized ---
167
+ async getMaterialized(blockId, actionId) {
168
+ return this.readActionScopedBytes(this.getMaterializedPath(blockId, actionId), this.getMaterializedPath(blockId, actionId, false));
73
169
  }
74
- async getMaterializedBlock(blockId, actionId) {
75
- return this.readIfExists(this.getMaterializedPath(blockId, actionId));
170
+ async putMaterialized(blockId, actionId, value) {
171
+ await atomicWriteFile(this.getMaterializedPath(blockId, actionId), value);
76
172
  }
77
- async saveMaterializedBlock(blockId, actionId, block) {
78
- if (block) {
79
- await this.ensureAndWriteFile(this.getMaterializedPath(blockId, actionId), JSON.stringify(block));
80
- }
81
- else {
82
- await fs.unlink(this.getMaterializedPath(blockId, actionId))
173
+ // The kernel owns the put-or-delete branch of `saveMaterializedBlock`, so the
174
+ // driver exposes delete as a separate op.
175
+ async deleteMaterialized(blockId, actionId) {
176
+ const matPath = this.getMaterializedPath(blockId, actionId);
177
+ await fs.unlink(matPath)
178
+ .catch((err) => {
179
+ if (err?.code !== 'ENOENT')
180
+ log('deleteMaterialized unlink failed for %s/%s - %o', blockId, actionId, err);
181
+ });
182
+ await this.unlinkRawColon(matPath, this.getMaterializedPath(blockId, actionId, false));
183
+ }
184
+ // --- promote (the only cross-key atomic op) ---
185
+ async promote(blockId, actionId) {
186
+ const pendingPath = this.getPendingActionPath(blockId, actionId);
187
+ const actionPath = this.getActionPath(blockId, actionId);
188
+ await fs.mkdir(path.dirname(actionPath), { recursive: true });
189
+ // This single rename IS the atomic move — it is why fs honors the kernel's
190
+ // promote contract without a WAL. A crash leaves either the pending or the
191
+ // committed file, never both/neither. Do NOT replace with read-write-delete.
192
+ return fs.rename(pendingPath, actionPath)
193
+ .catch(err => {
194
+ if (err.code === 'ENOENT') {
195
+ throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
196
+ }
197
+ log('promote rename failed for %s/%s - %o', blockId, actionId, err);
198
+ throw err;
199
+ });
200
+ }
201
+ // --- optional passthroughs ---
202
+ async *listBlockIds() {
203
+ // The block layout is `basePath/<blockId>/{meta.json,revs/,pend/,actions/,blocks/}`
204
+ // (see getBlockPath), so the direct children of basePath are the per-block directories
205
+ // and each directory NAME is the blockId (used raw, no encoding). Filter to directories
206
+ // so a stray file can't be mistaken for a block; `*.tmp` atomic-write orphans live inside
207
+ // block subdirs, never at basePath root, so the root is clean.
208
+ //
209
+ // A directory alone is NOT sufficient to call a block "durable owned": a block that was
210
+ // only PENDED (never committed) still creates `<blockId>/pend/` — hence a root directory
211
+ // entry — via atomicWriteFile's recursive mkdir, but has no meta.json. So we gate on
212
+ // meta.json existence: `meta.json` IS this backend's metadata store, and enumerating it
213
+ // yields exactly the blocks with a committed revision / persisted replica (the same
214
+ // "owned" population the live change feed tracks, and the same one the metadata-keyed
215
+ // backends — sqlite/leveldb/indexeddb — enumerate for free). Existence (fs.access), not
216
+ // parse, matches key-existence semantics: a torn/corrupt meta.json still counts as a key,
217
+ // exactly as a corrupt value would in the other backends.
218
+ //
219
+ // ENOENT (basePath not created yet, or a dir without meta.json) maps to "not owned" —
220
+ // same discrimination as directoryByteSize. Any OTHER readdir/access error must surface:
221
+ // swallowing it would make the seed falsely report an empty store and under-protect data
222
+ // already on disk.
223
+ // NOTE: reads the whole root listing up front + one meta.json stat per block dir; if a
224
+ // store ever grows to millions of block subdirs and this becomes a startup-latency
225
+ // problem, page it (e.g. opendir cursor) — fine at current scale.
226
+ const entries = await fs.readdir(this.basePath, { withFileTypes: true })
227
+ .catch((err) => {
228
+ if (err?.code === 'ENOENT')
229
+ return [];
230
+ log('listBlockIds readdir failed for %s - %o', this.basePath, err);
231
+ throw err;
232
+ });
233
+ for (const entry of entries) {
234
+ if (!entry.isDirectory())
235
+ continue;
236
+ const blockId = entry.name;
237
+ const hasMeta = await fs.access(this.getMetadataPath(blockId))
238
+ .then(() => true)
83
239
  .catch((err) => {
84
- if (err?.code !== 'ENOENT')
85
- log('saveMaterializedBlock unlink failed for %s/%s - %o', blockId, actionId, err);
240
+ if (err?.code === 'ENOENT')
241
+ return false;
242
+ log('listBlockIds access failed for %s - %o', blockId, err);
243
+ throw err;
86
244
  });
245
+ if (hasMeta)
246
+ yield blockId;
87
247
  }
88
248
  }
89
- async getApproximateBytesUsed() {
249
+ async approximateBytesUsed() {
90
250
  return this.directoryByteSize(this.basePath);
91
251
  }
92
252
  async directoryByteSize(dir) {
@@ -117,19 +277,7 @@ export class FileRawStorage {
117
277
  }
118
278
  return total;
119
279
  }
120
- async promotePendingTransaction(blockId, actionId) {
121
- const pendingPath = this.getPendingActionPath(blockId, actionId);
122
- const actionPath = this.getActionPath(blockId, actionId);
123
- await fs.mkdir(path.dirname(actionPath), { recursive: true });
124
- return fs.rename(pendingPath, actionPath)
125
- .catch(err => {
126
- if (err.code === 'ENOENT') {
127
- throw new Error(`Pending action ${actionId} not found for block ${blockId}`);
128
- }
129
- log('promotePendingTransaction rename failed for %s/%s - %o', blockId, actionId, err);
130
- throw err;
131
- });
132
- }
280
+ // --- paths ---
133
281
  getBlockPath(blockId) {
134
282
  return path.join(this.basePath, blockId);
135
283
  }
@@ -139,27 +287,95 @@ export class FileRawStorage {
139
287
  getRevisionPath(blockId, rev) {
140
288
  return path.join(this.getBlockPath(blockId), 'revs', `${rev}.json`);
141
289
  }
142
- getPendingActionPath(blockId, actionId) {
143
- return path.join(this.getBlockPath(blockId), 'pend', `${encodeActionIdForFilename(actionId)}.json`);
290
+ // `encoded` controls colon handling: writes and canonical reads use the
291
+ // percent-encoded filename (encoded = true); the legacy raw-colon fallback
292
+ // (see readActionScopedBytes) passes encoded = false to reach pre-encode
293
+ // POSIX files like `actions/tx:<hash>.json`.
294
+ getPendingActionPath(blockId, actionId, encoded = true) {
295
+ const filename = encoded ? encodeActionIdForFilename(actionId) : actionId;
296
+ return path.join(this.getBlockPath(blockId), 'pend', `${filename}.json`);
144
297
  }
145
- getActionPath(blockId, actionId) {
146
- return path.join(this.getBlockPath(blockId), 'actions', `${encodeActionIdForFilename(actionId)}.json`);
298
+ getActionPath(blockId, actionId, encoded = true) {
299
+ const filename = encoded ? encodeActionIdForFilename(actionId) : actionId;
300
+ return path.join(this.getBlockPath(blockId), 'actions', `${filename}.json`);
147
301
  }
148
- getMaterializedPath(blockId, actionId) {
149
- return path.join(this.getBlockPath(blockId), 'blocks', `${encodeActionIdForFilename(actionId)}.json`);
302
+ getMaterializedPath(blockId, actionId, encoded = true) {
303
+ const filename = encoded ? encodeActionIdForFilename(actionId) : actionId;
304
+ return path.join(this.getBlockPath(blockId), 'blocks', `${filename}.json`);
150
305
  }
151
- async readIfExists(filePath) {
152
- return fs.readFile(filePath, 'utf-8')
153
- .then(content => JSON.parse(content))
154
- .catch(err => {
155
- if (err.code === 'ENOENT')
306
+ // Best-effort removal of a pre-encode raw-colon file after the encoded delete,
307
+ // so a deleted item cannot resurface via the read fallback in readActionScopedBytes.
308
+ // Skipped on win32 (raw-colon files cannot exist there) and when paths are identical
309
+ // (action id contains no colon — only one syscall needed). ENOENT is silently ignored.
310
+ async unlinkRawColon(encodedPath, rawPath) {
311
+ if (process.platform === 'win32' || rawPath === encodedPath)
312
+ return;
313
+ await fs.unlink(rawPath).catch((err) => {
314
+ if (err?.code !== 'ENOENT')
315
+ log('unlinkRawColon failed for %s - %o', rawPath, err);
316
+ });
317
+ }
318
+ // Reads an action-id-keyed file (JSON-valued) by its canonical (percent-encoded)
319
+ // path, falling back on a miss to the legacy raw-colon path written by pre-encode
320
+ // nodes (e.g. POSIX files literally named `actions/tx:<hash>.json`).
321
+ //
322
+ // Tradeoff: this reads legacy files in place and never renames them, so a
323
+ // store upgraded from a pre-encode node keeps mixed naming on disk. That is
324
+ // acceptable pre-1.0; a future migration sweep can normalize if desired. We
325
+ // deliberately do NOT migrate-on-read here — reads stay side-effect-free.
326
+ //
327
+ // win32 guard: a raw-colon path is not a benign miss on Windows — the colon
328
+ // is parsed as an NTFS alternate-data-stream separator and a read there can
329
+ // throw a non-ENOENT error rather than cleanly missing. Raw-colon files
330
+ // cannot have been written on win32 anyway, so we skip the fallback there
331
+ // (losing nothing) and swallow ALL fallback errors elsewhere, guaranteeing
332
+ // the fallback never surfaces a new throw to callers.
333
+ async readActionScopedBytes(encodedPath, rawPath) {
334
+ const hit = await this.readBytesIfExists(encodedPath, true);
335
+ if (hit !== undefined)
336
+ return hit;
337
+ if (process.platform === 'win32' || rawPath === encodedPath)
338
+ return undefined;
339
+ return fs.readFile(rawPath)
340
+ .then(bytes => (isParseableJson(bytes) ? bytes : undefined))
341
+ .catch(() => undefined);
342
+ }
343
+ // Reads a file's raw bytes. ENOENT → undefined. When `jsonGuard` is set, a
344
+ // present-but-corrupt file (JSON.parse fails — most likely a torn write from a
345
+ // crash before atomic writes existed) is treated as "missing" so recover() and
346
+ // normal reads make progress instead of rethrowing forever. A real I/O error
347
+ // (permissions, EIO, EISDIR, ...) still throws — it must not be masked.
348
+ async readBytesIfExists(filePath, jsonGuard) {
349
+ let bytes;
350
+ try {
351
+ bytes = await fs.readFile(filePath);
352
+ }
353
+ catch (err) {
354
+ if (err?.code === 'ENOENT')
156
355
  return undefined;
157
356
  throw err;
158
- });
357
+ }
358
+ if (jsonGuard && !isParseableJson(bytes)) {
359
+ log('readBytesIfExists: corrupt JSON at %s, treating as missing', filePath);
360
+ return undefined;
361
+ }
362
+ return bytes;
159
363
  }
160
- async ensureAndWriteFile(filePath, content) {
161
- await fs.mkdir(path.dirname(filePath), { recursive: true });
162
- await fs.writeFile(filePath, content);
364
+ }
365
+ /**
366
+ * Filesystem-backed {@link IRawStorage}, now a thin shell over the shared
367
+ * {@link KvRawStorage} kernel driven by a {@link FileStoreDriver}. The public
368
+ * name/constructor (`new FileRawStorage(basePath)`) is unchanged so existing
369
+ * imports keep resolving; the kernel supplies the `IRawStorage` surface and the
370
+ * driver supplies fs behavior.
371
+ *
372
+ * `listBlockIds`/`getApproximateBytesUsed` are re-declared here as always-present
373
+ * (the fs driver always implements them, so the kernel constructor always wires
374
+ * them) — the base declares them optional, but every fs consumer relies on them.
375
+ */
376
+ export class FileRawStorage extends KvRawStorage {
377
+ constructor(basePath) {
378
+ super(new FileStoreDriver(basePath));
163
379
  }
164
380
  }
165
381
  //# sourceMappingURL=file-storage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-storage.js","sourceRoot":"","sources":["../../src/file-storage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file-storage.js","sourceRoot":"","sources":["../../src/file-storage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optimystic/db-p2p-storage-fs",
3
- "version": "0.14.0",
3
+ "version": "0.16.2",
4
4
  "type": "module",
5
5
  "description": "Node.js filesystem storage backend for @optimystic/db-p2p",
6
6
  "main": "dist/src/index.js",
@@ -34,18 +34,22 @@
34
34
  ],
35
35
  "scripts": {
36
36
  "clean": "rimraf dist",
37
- "build": "tsc"
37
+ "build": "tsc",
38
+ "test": "node --import ./register.mjs node_modules/mocha/bin/mocha.js \"test/**/*.spec.ts\" --colors --reporter min",
39
+ "test:verbose": "node --import ./register.mjs node_modules/mocha/bin/mocha.js \"test/**/*.spec.ts\" --colors --reporter spec"
38
40
  },
39
41
  "devDependencies": {
40
42
  "@types/debug": "^4.1.12",
41
43
  "@types/mocha": "^10.0.10",
42
44
  "@types/node": "^25.1.0",
45
+ "mocha": "^11.7.5",
43
46
  "rimraf": "^6.1.2",
47
+ "ts-node": "^10.9.2",
44
48
  "typescript": "^5.9.3"
45
49
  },
46
50
  "dependencies": {
47
- "@optimystic/db-core": "^0.14.0",
48
- "@optimystic/db-p2p": "^0.14.0",
51
+ "@optimystic/db-core": "^0.16.2",
52
+ "@optimystic/db-p2p": "^0.16.2",
49
53
  "debug": "^4.4.3"
50
54
  }
51
55
  }
@@ -0,0 +1,82 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+
4
+ // Per-process monotonic counter for unique temp names. Combined with the pid it
5
+ // makes each temp path unique across concurrent writers to the same logical
6
+ // target — even two FileRawStorage/FileKVStore instances in one process, which
7
+ // do not lock (see the proper-lockfile TODO in file-storage.ts). Deterministic
8
+ // and collision-safe, unlike Math.random()/Date.now().
9
+ let tempCounter = 0;
10
+
11
+ /**
12
+ * Atomically write `content` to `filePath`.
13
+ *
14
+ * Writes to a unique `*.tmp` sibling, fsyncs the data, then renames it into
15
+ * place. `rename` over an existing file is atomic on POSIX and NTFS, so a
16
+ * concurrent reader only ever sees the complete old file or the complete new
17
+ * file — never a torn/half-written one. After the rename we best-effort fsync
18
+ * the containing directory so the rename itself survives power loss on POSIX;
19
+ * that directory fsync is unsupported on win32 and its error is swallowed.
20
+ *
21
+ * On any failure the temp file is removed (best-effort) so a crashed write does
22
+ * not leave the canonical path damaged. A crash *between* the temp write and the
23
+ * rename leaves only an inert `*.tmp` sibling — never read (reads target
24
+ * canonical paths) and skipped by `.json` directory scans.
25
+ *
26
+ * `content` is `string | Uint8Array`: `FileKVStore` writes strings, while the
27
+ * `KvRawStorage`-backed `FileStoreDriver` writes the kernel's raw value bytes.
28
+ * `FileHandle.writeFile` writes either losslessly — a `Uint8Array` is written
29
+ * byte-for-byte, so non-ASCII JSON round-trips exactly.
30
+ */
31
+ export async function atomicWriteFile(filePath: string, content: string | Uint8Array): Promise<void> {
32
+ const dir = path.dirname(filePath);
33
+ await fs.mkdir(dir, { recursive: true });
34
+
35
+ // Suffix ends in `.tmp` (not `.json`) so listPendingTransactions / FileKVStore.list,
36
+ // which filter on `.json`, never surface an in-flight temp file.
37
+ // NOTE: a crash between open and rename leaves an inert `*.tmp` orphan (never read,
38
+ // skipped by `.json` scans). No cleanup sweep exists; if a crash-looping writer ever
39
+ // accumulates many, add a startup sweep of stale `*.tmp` siblings.
40
+ const tmpPath = path.join(dir, `${path.basename(filePath)}.${process.pid}.${tempCounter++}.tmp`);
41
+
42
+ let handle: fs.FileHandle | undefined;
43
+ try {
44
+ handle = await fs.open(tmpPath, 'w');
45
+ await handle.writeFile(content);
46
+ await handle.sync();
47
+ await handle.close();
48
+ handle = undefined;
49
+ // NOTE: on win32, rename-over-existing can throw EPERM/EACCES/EBUSY if a
50
+ // concurrent reader holds the target open without FILE_SHARE_DELETE (Node
51
+ // readIfExists/get open→read→close in a tiny window, so it's rare). Modern
52
+ // libuv retries some cases; if this ever surfaces as spurious write failures
53
+ // under concurrent read+write on Windows, add a bounded retry loop here (the
54
+ // write-file-atomic package does exactly this). Conditional — the adapter is
55
+ // already last-writer-wins with no cross-process lock (proper-lockfile TODO
56
+ // in file-storage.ts), so it is not reachable under current single-writer use.
57
+ await fs.rename(tmpPath, filePath);
58
+ } catch (err) {
59
+ if (handle) await handle.close().catch(() => { /* best-effort */ });
60
+ await fs.unlink(tmpPath).catch(() => { /* best-effort: temp may not exist */ });
61
+ throw err;
62
+ }
63
+
64
+ await fsyncDir(dir);
65
+ }
66
+
67
+ /**
68
+ * Best-effort fsync of a directory so a completed rename is durable. POSIX needs
69
+ * this; win32 (and platforms that reject opening a directory for fsync) throw,
70
+ * and we ignore that rather than failing the write.
71
+ */
72
+ async function fsyncDir(dir: string): Promise<void> {
73
+ let handle: fs.FileHandle | undefined;
74
+ try {
75
+ handle = await fs.open(dir, 'r');
76
+ await handle.sync();
77
+ } catch {
78
+ // Directory fsync unsupported here (e.g. win32) — nothing to do.
79
+ } finally {
80
+ if (handle) await handle.close().catch(() => { /* best-effort */ });
81
+ }
82
+ }
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
3
  import type { IKVStore } from '@optimystic/db-p2p';
4
+ import { atomicWriteFile } from './atomic-write.js';
4
5
 
5
6
  /** Filesystem-backed IKVStore. Keys may contain `/` separators which become subdirectories. */
6
7
  export class FileKVStore implements IKVStore {
@@ -16,9 +17,7 @@ export class FileKVStore implements IKVStore {
16
17
  }
17
18
 
18
19
  async set(key: string, value: string): Promise<void> {
19
- const filePath = this.keyToPath(key);
20
- await fs.mkdir(path.dirname(filePath), { recursive: true });
21
- await fs.writeFile(filePath, value);
20
+ await atomicWriteFile(this.keyToPath(key), value);
22
21
  }
23
22
 
24
23
  async delete(key: string): Promise<void> {
@@ -57,6 +56,10 @@ export class FileKVStore implements IKVStore {
57
56
  }
58
57
 
59
58
  private keyToPath(key: string): string {
59
+ // NOTE: `/`-separated keys become nested dirs, so a key's first segment shares
60
+ // the top-level namespace with FileRawStorage's <blockId>/ dirs. Safe today
61
+ // because block ids are content hashes; if a KV key's first segment could ever
62
+ // equal a block id, give the two stores separate basePaths (see README Usage).
60
63
  return path.join(this.basePath, ...key.split('/')) + '.json';
61
64
  }
62
65
  }