@optimystic/db-p2p-storage-fs 1.14.0 → 1.16.0

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 (37) hide show
  1. package/README.md +142 -33
  2. package/dist/src/file-batch-backend.d.ts +69 -0
  3. package/dist/src/file-batch-backend.d.ts.map +1 -0
  4. package/dist/src/file-batch-backend.js +400 -0
  5. package/dist/src/file-batch-backend.js.map +1 -0
  6. package/dist/src/file-kv-store.d.ts +37 -3
  7. package/dist/src/file-kv-store.d.ts.map +1 -1
  8. package/dist/src/file-kv-store.js +100 -34
  9. package/dist/src/file-kv-store.js.map +1 -1
  10. package/dist/src/file-storage.d.ts +20 -58
  11. package/dist/src/file-storage.d.ts.map +1 -1
  12. package/dist/src/file-storage.js +16 -430
  13. package/dist/src/file-storage.js.map +1 -1
  14. package/dist/src/file-write-log.d.ts +361 -0
  15. package/dist/src/file-write-log.d.ts.map +1 -0
  16. package/dist/src/file-write-log.js +848 -0
  17. package/dist/src/file-write-log.js.map +1 -0
  18. package/dist/src/index.d.ts +3 -0
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/index.js +3 -0
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/store-lock.d.ts +42 -0
  23. package/dist/src/store-lock.d.ts.map +1 -0
  24. package/dist/src/store-lock.js +116 -0
  25. package/dist/src/store-lock.js.map +1 -0
  26. package/package.json +3 -3
  27. package/src/file-batch-backend.ts +430 -0
  28. package/src/file-kv-store.ts +112 -32
  29. package/src/file-storage.ts +22 -468
  30. package/src/file-write-log.ts +906 -0
  31. package/src/index.ts +3 -0
  32. package/src/store-lock.ts +123 -0
  33. package/dist/src/atomic-write.d.ts +0 -22
  34. package/dist/src/atomic-write.d.ts.map +0 -1
  35. package/dist/src/atomic-write.js +0 -90
  36. package/dist/src/atomic-write.js.map +0 -1
  37. package/src/atomic-write.ts +0 -89
@@ -0,0 +1,400 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ import { NODE_KV_NAMESPACES } from '@optimystic/db-p2p';
4
+ import { createLogger } from './logger.js';
5
+ import { FileWriteLog, assertFileWriteLogOptions, fileStoreIdentity, readFileIfExists } from './file-write-log.js';
6
+ const log = createLogger('storage:file');
7
+ // Built on first use rather than at module load, like every other `TextDecoder` in library source
8
+ // (see NO_MODULE_SCOPE_TEXT_DECODER in eslint.config.js). Harmless on Node; kept uniform so the lint
9
+ // rule needs no per-package exemption.
10
+ let decoderInstance;
11
+ const decoder = () => decoderInstance ??= new TextDecoder();
12
+ // Colons are illegal in Windows filenames; encode them so action ids like
13
+ // `tx:<hash>` and `stamp:<hash>` round-trip safely on all platforms.
14
+ function encodeActionIdForFilename(actionId) {
15
+ return actionId.replace(/:/g, '%3A');
16
+ }
17
+ function decodeFilenameToActionId(filename) {
18
+ return filename.replace(/%3A/g, ':');
19
+ }
20
+ // A torn write leaves valid-prefix JSON cut off mid-token — JSON.parse throws
21
+ // SyntaxError. Used as the "corrupt content → treat as missing" guard so a
22
+ // crash-truncated file reads as absent (letting recover() make progress) instead
23
+ // of surfacing a parse error forever. Only the JSON-valued stores use this; the
24
+ // revisions store holds a bare ActionId string (not JSON) and is never guarded.
25
+ // NOTE: every guarded read parses the JSON here (validate, discard) and the kernel's
26
+ // decodeJson parses the same bytes AGAIN to build the value — 2× parse per get. The
27
+ // guard is intrinsic to the backend's "corrupt→missing" contract (the kernel can't
28
+ // express it), so it can't simply be dropped. Fine at current read volumes; if a read
29
+ // path ever shows up hot, parse once and thread the parsed value through the backend.
30
+ function isParseableJson(bytes) {
31
+ try {
32
+ JSON.parse(decoder().decode(bytes));
33
+ return true;
34
+ }
35
+ catch (err) {
36
+ if (err instanceof SyntaxError)
37
+ return false;
38
+ throw err;
39
+ }
40
+ }
41
+ const ACTION_SCOPED_DIRS = {
42
+ pending: 'pend',
43
+ transactions: 'actions',
44
+ materialized: 'blocks',
45
+ };
46
+ const METADATA_FILE = 'meta.json';
47
+ const REVISIONS_DIR = 'revs';
48
+ const PROOFS_DIR = 'proofs';
49
+ /**
50
+ * The directories a block keeps its own records in, skipped when the block listing walks below a
51
+ * block directory (a block's directory can hold another block's: `.../Ledger/index/LedgerLatest`
52
+ * sits inside `.../Ledger/`).
53
+ * NOTE: a block id that extends another block's id with one of these segments (`x/revs`,
54
+ * `x/blocks/y`) lands inside block `x`'s own store directory — on write `x/revs` shares it outright,
55
+ * and the listing never yields either id. A limit of the raw-id layout; if such ids ever occur,
56
+ * encode block-id segments in the layout.
57
+ */
58
+ const BLOCK_STORE_DIRS = new Set([...Object.values(ACTION_SCOPED_DIRS), REVISIONS_DIR, PROOFS_DIR]);
59
+ /**
60
+ * The node's KV namespaces, which `FileKVStore` writes at the root when it shares this base path.
61
+ * Skipped by the listing so a KV key whose last segment is `meta` (stored as `meta.json`) is not
62
+ * read as a block, and so a startup scan does not walk `executed/`. A block id starting with one of
63
+ * these words already collides with the KV store on write (see `FileKVStore.keyToPath`).
64
+ */
65
+ const ROOT_KV_NAMESPACES = new Set(NODE_KV_NAMESPACES);
66
+ /**
67
+ * Filesystem {@link BatchBackend}: the six logical block-storage stores mapped to six paths under
68
+ * `basePath/<blockId>/` (`{meta.json,revs/,pend/,actions/,proofs/,blocks/}`), every batch committed
69
+ * through the directory's {@link FileWriteLog}. The directory tree is a deliberate,
70
+ * human-inspectable layout — it is NOT flattened into encoded-filename KV keys.
71
+ *
72
+ * `KvRawStorage` owns all JSON serialization, so this backend reads raw `Uint8Array` bytes and
73
+ * never does `JSON.stringify/parse` on values. What is fs-specific lives here: the path per
74
+ * {@link StoreKey}, the corrupt-content-as-missing read guard, colon-encoded action-id filenames
75
+ * with the legacy raw-colon read fallback and win32 guards, and the directory listings.
76
+ *
77
+ * Every operation opens the shared log first — on the first one: take the lock, replay, then
78
+ * serve — so no read ever sees a tree the log has not been replayed into. The log is acquired on
79
+ * that first operation rather than in the constructor, so an instance that is constructed and
80
+ * never used (one `withReadCache` dedupes away) holds no handle and no lock.
81
+ */
82
+ export class FileBatchBackend {
83
+ options;
84
+ /** A committed batch's record is synced to the log by the deadline its commit allowed, or before the commit resolves; replay rebuilds the tree from it. */
85
+ durability = 'power-loss';
86
+ basePath;
87
+ identity;
88
+ log;
89
+ /** `options` are the directory log's checkpoint settings, applied when this is the first store to open it. */
90
+ constructor(basePath, options = {}) {
91
+ this.options = options;
92
+ assertFileWriteLogOptions(options);
93
+ this.basePath = path.resolve(basePath);
94
+ this.identity = fileStoreIdentity(basePath);
95
+ }
96
+ storeIdentity() {
97
+ return this.identity;
98
+ }
99
+ // --- reads ---
100
+ async get(key) {
101
+ await this.open();
102
+ switch (key.store) {
103
+ case 'metadata':
104
+ return this.readBytesIfExists(this.metadataPath(key.blockId), true);
105
+ // The revisions store value is a bare ActionId string (kernel `encodeActionId`, NOT JSON),
106
+ // so it is read WITHOUT the JSON guard — any bytes are a valid string and `decodeActionId`
107
+ // never throws.
108
+ // NOTE: because there is no guard here, a *torn* revision file reads back as a wrong
109
+ // (truncated) ActionId rather than as missing — unlike the JSON stores, which torn-read as
110
+ // undefined. A tree file can only be torn by a crash during its write, and that write's log
111
+ // record is durable before the write starts, so the next open's replay rewrites it whole.
112
+ // Only a file written by a build before the log can stay torn, and recover() re-derives
113
+ // revisions from the actions store anyway. If revisions ever move outside the log, add a
114
+ // checksum/length guard here.
115
+ case 'revisions':
116
+ return this.readBytesIfExists(this.revisionPath(key.blockId, key.rev), false);
117
+ // Keyed by (blockId, rev) like the revisions store, but JSON-valued like the action stores —
118
+ // so it takes the corrupt-content-as-missing guard the revisions store does not, and the
119
+ // filename carries no colon to encode. Deliberately no delete: a proof lives and dies with
120
+ // its revision record (see RawStoreDriver.getProof).
121
+ case 'proofs':
122
+ return this.readBytesIfExists(this.proofPath(key.blockId, key.rev), true);
123
+ case 'pending':
124
+ case 'transactions':
125
+ case 'materialized':
126
+ return this.readActionScopedBytes(this.actionScopedPath(key.store, key.blockId, key.actionId), this.actionScopedPath(key.store, key.blockId, key.actionId, false));
127
+ }
128
+ }
129
+ async rangeRevisions(blockId, lo, hi) {
130
+ await this.open();
131
+ // The fs backend has no cursor: walk the bounded [lo, hi] range rev-by-rev, reading each
132
+ // present rev. The range is caller-bounded, so this avoids listing an unbounded revs/ directory.
133
+ const results = [];
134
+ for (let rev = lo; rev <= hi; rev++) {
135
+ const value = await this.readBytesIfExists(this.revisionPath(blockId, rev), false);
136
+ if (value !== undefined) {
137
+ results.push([rev, value]);
138
+ }
139
+ }
140
+ return results;
141
+ }
142
+ async listPendingActionIds(blockId) {
143
+ await this.open();
144
+ const pendingPath = path.join(this.blockPath(blockId), ACTION_SCOPED_DIRS.pending);
145
+ // Only a genuinely-absent directory (ENOENT) maps to "no pendings". Any other error
146
+ // (EACCES, EIO, ENOTDIR, ...) must surface — swallowing it here would make listPendingActionIds
147
+ // silently report an empty directory, so pend's conflict detection would be skipped. Mirrors
148
+ // directoryByteSize's ENOENT-vs-other discrimination.
149
+ const files = await fs.readdir(pendingPath).catch((err) => {
150
+ if (err?.code === 'ENOENT')
151
+ return [];
152
+ log('listPendingActionIds readdir failed for %s - %o', blockId, err);
153
+ throw err;
154
+ });
155
+ const ids = [];
156
+ for (const file of files) {
157
+ if (!file.endsWith('.json'))
158
+ continue;
159
+ const actionId = decodeFilenameToActionId(file.slice(0, -5));
160
+ // Accept every realistic action id: legacy UUIDs (`[0-9a-f-]`), consensus
161
+ // tx:/stamp: ids (base64url-encoded SHA-256, alphabet `[A-Za-z0-9_-]` — see
162
+ // db-core hashString, NOT lowercase hex), AND the bare-alphanumeric ids the
163
+ // cross-backend conformance suite uses (`a1`, `b1`, ...). This is deliberately
164
+ // broad-but-not-total: an id is any `[A-Za-z0-9_-]` string, optionally prefixed
165
+ // with `tx:`/`stamp:`. It is NOT a total accept — a file whose decoded name
166
+ // carries other punctuation (a dot, a space) is genuine junk in pend/ and is
167
+ // logged-and-skipped rather than surfaced as a phantom pending. The memory/db
168
+ // reference drivers key on the raw id and never see a filesystem name, so this
169
+ // filter is fs-only; it must not drop an id those backends would list, hence the
170
+ // widened class (an earlier hex-only class silently dropped real consensus ids —
171
+ // see `optimystic-filestorage-colon-actionid-windows`).
172
+ if (!/^(?:tx:|stamp:)?[A-Za-z0-9_-]+$/.test(actionId)) {
173
+ // Leave a breadcrumb rather than silently dropping: the .json + decode guard
174
+ // already excludes the `*.tmp` orphans an older build's atomic writer could leave, so
175
+ // anything reaching here is an unexpected filename a maintainer should see.
176
+ log('listPendingActionIds skipping unrecognized action-id file %s for %s', file, blockId);
177
+ continue;
178
+ }
179
+ ids.push(actionId);
180
+ }
181
+ return ids;
182
+ }
183
+ async *listBlockIds() {
184
+ await this.open();
185
+ // A block id is used raw as a relative path (see blockPath), so an id containing `/` lives in
186
+ // nested directories: `default/app/Ledger` is `basePath/default/app/Ledger/meta.json`. So the
187
+ // listing walks the whole tree and yields every directory holding a meta.json, its path
188
+ // relative to basePath rebuilt with `/` (never path.sep, so a win32 host yields the id it was
189
+ // given). Intermediate directories (`default/`, `default/app/`) hold no meta.json: descended,
190
+ // not listed. Files are never descended, so `write-ahead.log` and `store.lock` at the root
191
+ // are skipped by that alone.
192
+ //
193
+ // Gating on meta.json — existence (fs.access), not parse — enumerates exactly the population
194
+ // the metadata-keyed backends (sqlite/leveldb/indexeddb) get for free: every block with ANY
195
+ // durable metadata. That INCLUDES a pend-only block on the node path, because
196
+ // `BlockStorage.savePendingTransaction` seeds metadata first; a pend straight through this
197
+ // backend creates `<blockId>/pend/` without meta.json and is not listed. Do not "improve" this
198
+ // into a committed-revision filter: parsing every meta.json would turn a cheap sweep into a
199
+ // per-block read at startup (see the `listBlockIds` contract in i-raw-storage.ts). A
200
+ // torn/corrupt meta.json still counts as a key, exactly as a corrupt value would elsewhere.
201
+ //
202
+ // ENOENT (basePath not created yet, or a directory without meta.json) maps to "not there".
203
+ // Any OTHER readdir/access error surfaces: swallowing it would make the startup seed falsely
204
+ // report an empty store and under-protect data already on disk.
205
+ // NOTE: one readdir + one meta.json access per directory on the walk, yielded as it goes; if a
206
+ // store ever grows to millions of block directories and this becomes a startup-latency
207
+ // problem, page each listing (e.g. opendir cursor) — fine at current scale.
208
+ yield* this.walkBlockDirectories([]);
209
+ }
210
+ async *walkBlockDirectories(segments) {
211
+ const dir = path.join(this.basePath, ...segments);
212
+ const isBlock = segments.length > 0 && await this.hasMetadataFile(dir);
213
+ if (isBlock)
214
+ yield segments.join('/');
215
+ for (const name of await this.childDirectoryNames(dir)) {
216
+ if (isBlock && BLOCK_STORE_DIRS.has(name))
217
+ continue;
218
+ if (segments.length === 0 && ROOT_KV_NAMESPACES.has(name))
219
+ continue;
220
+ yield* this.walkBlockDirectories([...segments, name]);
221
+ }
222
+ }
223
+ async childDirectoryNames(dir) {
224
+ const entries = await fs.readdir(dir, { withFileTypes: true })
225
+ .catch((err) => {
226
+ if (err?.code === 'ENOENT')
227
+ return [];
228
+ log('listBlockIds readdir failed for %s - %o', dir, err);
229
+ throw err;
230
+ });
231
+ return entries.filter(entry => entry.isDirectory()).map(entry => entry.name);
232
+ }
233
+ hasMetadataFile(dir) {
234
+ return fs.access(path.join(dir, METADATA_FILE))
235
+ .then(() => true)
236
+ .catch((err) => {
237
+ if (err?.code === 'ENOENT')
238
+ return false;
239
+ log('listBlockIds access failed for %s - %o', dir, err);
240
+ throw err;
241
+ });
242
+ }
243
+ /** The tree's bytes, the write-ahead log and the lock included: all of it is what the store occupies. */
244
+ async approximateBytesUsed() {
245
+ await this.open();
246
+ return this.directoryByteSize(this.basePath);
247
+ }
248
+ // --- the one write ---
249
+ /** One log record, synced within `syncWithinMs` (see `FileWriteLog.commit`); the `durable` returned is the log's. */
250
+ commit(entries, options) {
251
+ return this.shared().commit(entries.flatMap(entry => this.toLogEntries(entry)), options);
252
+ }
253
+ /** Hasten the log's sync. A backend that never committed holds no log, and so has nothing to hasten. */
254
+ sync() {
255
+ return this.log?.sync() ?? Promise.resolve();
256
+ }
257
+ async close() {
258
+ const shared = this.log;
259
+ this.log = undefined;
260
+ await shared?.release();
261
+ }
262
+ // --- the log ---
263
+ shared() {
264
+ return this.log ??= FileWriteLog.acquire(this.basePath, this.options);
265
+ }
266
+ open() {
267
+ return this.shared().open();
268
+ }
269
+ /**
270
+ * A put is one entry at the key's canonical path. A delete of an action-scoped record is that
271
+ * path's entry plus, off win32 and when the id carries a colon, the pre-encode raw-colon sibling's,
272
+ * so a deleted item cannot resurface through the read fallback in readActionScopedBytes — in the
273
+ * log, so a replay removes the sibling too.
274
+ *
275
+ * NOTE: that sibling entry names a path with a colon, which only a POSIX build writes; a log
276
+ * carrying one and moved unreplayed to a win32 machine replays it as an unlink of an NTFS
277
+ * stream path, where anything but ENOENT fails the open. A store is not moved across platforms
278
+ * mid-log today; if it ever is, have the replay skip a delete it cannot spell on this platform.
279
+ */
280
+ toLogEntries(entry) {
281
+ const relative = this.relativePath(entry.key);
282
+ if (entry.value !== undefined)
283
+ return [{ kind: 'put', path: relative, value: entry.value }];
284
+ const entries = [{ kind: 'delete', path: relative }];
285
+ const { key } = entry;
286
+ if (key.store === 'pending' || key.store === 'materialized') {
287
+ const raw = this.relativeActionScopedPath(key.store, key.blockId, key.actionId, false);
288
+ if (process.platform !== 'win32' && raw !== relative)
289
+ entries.push({ kind: 'delete', path: raw });
290
+ }
291
+ return entries;
292
+ }
293
+ // --- paths: relative with forward slashes for the log, absolute for reads ---
294
+ relativePath(key) {
295
+ switch (key.store) {
296
+ case 'metadata':
297
+ return `${key.blockId}/${METADATA_FILE}`;
298
+ case 'revisions':
299
+ return `${key.blockId}/${REVISIONS_DIR}/${key.rev}.json`;
300
+ case 'proofs':
301
+ return `${key.blockId}/${PROOFS_DIR}/${key.rev}.json`;
302
+ case 'pending':
303
+ case 'transactions':
304
+ case 'materialized':
305
+ return this.relativeActionScopedPath(key.store, key.blockId, key.actionId);
306
+ }
307
+ }
308
+ // `encoded` controls colon handling: writes and canonical reads use the percent-encoded filename
309
+ // (encoded = true); the legacy raw-colon fallback (see readActionScopedBytes) passes
310
+ // encoded = false to reach pre-encode POSIX files like `actions/tx:<hash>.json`.
311
+ relativeActionScopedPath(store, blockId, actionId, encoded = true) {
312
+ const filename = encoded ? encodeActionIdForFilename(actionId) : actionId;
313
+ return `${blockId}/${ACTION_SCOPED_DIRS[store]}/${filename}.json`;
314
+ }
315
+ blockPath(blockId) {
316
+ return path.join(this.basePath, blockId);
317
+ }
318
+ metadataPath(blockId) {
319
+ return path.join(this.blockPath(blockId), METADATA_FILE);
320
+ }
321
+ revisionPath(blockId, rev) {
322
+ return path.join(this.blockPath(blockId), REVISIONS_DIR, `${rev}.json`);
323
+ }
324
+ proofPath(blockId, rev) {
325
+ return path.join(this.blockPath(blockId), PROOFS_DIR, `${rev}.json`);
326
+ }
327
+ actionScopedPath(store, blockId, actionId, encoded = true) {
328
+ return path.join(this.basePath, this.relativeActionScopedPath(store, blockId, actionId, encoded));
329
+ }
330
+ // --- reads with the fs guards ---
331
+ // Reads an action-id-keyed file (JSON-valued) by its canonical (percent-encoded)
332
+ // path, falling back on a miss to the legacy raw-colon path written by pre-encode
333
+ // nodes (e.g. POSIX files literally named `actions/tx:<hash>.json`).
334
+ //
335
+ // Tradeoff: this reads legacy files in place and never renames them, so a
336
+ // store upgraded from a pre-encode node keeps mixed naming on disk. That is
337
+ // acceptable pre-1.0; a future migration sweep can normalize if desired. We
338
+ // deliberately do NOT migrate-on-read here — reads stay side-effect-free.
339
+ //
340
+ // win32 guard: a raw-colon path is not a benign miss on Windows — the colon
341
+ // is parsed as an NTFS alternate-data-stream separator and a read there can
342
+ // throw a non-ENOENT error rather than cleanly missing. Raw-colon files
343
+ // cannot have been written on win32 anyway, so we skip the fallback there
344
+ // (losing nothing) and swallow ALL fallback errors elsewhere, guaranteeing
345
+ // the fallback never surfaces a new throw to callers.
346
+ async readActionScopedBytes(encodedPath, rawPath) {
347
+ const hit = await this.readBytesIfExists(encodedPath, true);
348
+ if (hit !== undefined)
349
+ return hit;
350
+ if (process.platform === 'win32' || rawPath === encodedPath)
351
+ return undefined;
352
+ return fs.readFile(rawPath)
353
+ .then(bytes => (isParseableJson(bytes) ? bytes : undefined))
354
+ .catch(() => undefined);
355
+ }
356
+ // Reads a file's raw bytes. ENOENT → undefined. When `jsonGuard` is set, a
357
+ // present-but-corrupt file (JSON.parse fails — a torn write of a build before the log, or a
358
+ // crash mid-write that the next open's replay has not yet rewritten) is treated as "missing" so
359
+ // recover() and normal reads make progress instead of rethrowing forever. A real I/O error
360
+ // (permissions, EIO, EISDIR, ...) still throws — it must not be masked.
361
+ async readBytesIfExists(filePath, jsonGuard) {
362
+ const bytes = await readFileIfExists(filePath);
363
+ if (bytes === undefined)
364
+ return undefined;
365
+ if (jsonGuard && !isParseableJson(bytes)) {
366
+ log('readBytesIfExists: corrupt JSON at %s, treating as missing', filePath);
367
+ return undefined;
368
+ }
369
+ return bytes;
370
+ }
371
+ async directoryByteSize(dir) {
372
+ const entries = await fs.readdir(dir, { withFileTypes: true })
373
+ .catch((err) => {
374
+ if (err?.code === 'ENOENT')
375
+ return [];
376
+ log('directoryByteSize readdir failed for %s - %o', dir, err);
377
+ return [];
378
+ });
379
+ let total = 0;
380
+ for (const entry of entries) {
381
+ const entryPath = path.join(dir, entry.name);
382
+ if (entry.isDirectory()) {
383
+ total += await this.directoryByteSize(entryPath);
384
+ }
385
+ else if (entry.isFile()) {
386
+ const size = await fs.stat(entryPath)
387
+ .then(st => st.size)
388
+ .catch((err) => {
389
+ if (err?.code === 'ENOENT')
390
+ return 0;
391
+ log('directoryByteSize stat failed for %s - %o', entryPath, err);
392
+ return 0;
393
+ });
394
+ total += size;
395
+ }
396
+ }
397
+ return total;
398
+ }
399
+ }
400
+ //# sourceMappingURL=file-batch-backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-batch-backend.js","sourceRoot":"","sources":["../../src/file-batch-backend.ts"],"names":[],"mappings":"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"}
@@ -1,13 +1,47 @@
1
- import type { IKVStore } from '@optimystic/db-p2p';
2
- /** Filesystem-backed IKVStore. Keys may contain `/` separators which become subdirectories. */
1
+ import { type FlushOptions, type IKVStore, type WriteGroupOptions } from '@optimystic/db-p2p';
2
+ import { type FileWriteLogOptions } from './file-write-log.js';
3
+ /** The write group's `idleFlushMs` and the log's checkpoint settings; `setTimer` (test-only) arms both. */
4
+ export type FileKVStoreOptions = WriteGroupOptions & FileWriteLogOptions;
5
+ /**
6
+ * Filesystem-backed {@link IKVStore}: each key is the file `<basePath>/<key>.json`, a `/` in the key
7
+ * a subdirectory. Writes go through a {@link WriteGroup} over the directory's {@link FileWriteLog} —
8
+ * the same log a `FileRawStorage` over the same directory holds, so the two share one log, one
9
+ * lock and one checkpoint. A `set` or `delete` resolves into memory, served from there to `get` and
10
+ * `list`; `flush()` appends everything written since the last flush to the log as one record, synced
11
+ * before the flush resolves or within the window the flush allows (`FileWriteLog.commit`). A write
12
+ * no flush reaches is flushed by the group's idle timer.
13
+ *
14
+ * The constructor is synchronous and holds nothing: the log is acquired, the lock taken and the log
15
+ * replayed on the first operation. `close()` flushes what is still held and releases this store's
16
+ * reference to the log.
17
+ */
3
18
  export declare class FileKVStore implements IKVStore {
4
19
  private readonly basePath;
5
- constructor(basePath: string);
20
+ private readonly options;
21
+ private readonly group;
22
+ private log;
23
+ constructor(basePath: string, options?: FileKVStoreOptions);
6
24
  get(key: string): Promise<string | undefined>;
7
25
  set(key: string, value: string): Promise<void>;
8
26
  delete(key: string): Promise<void>;
27
+ /**
28
+ * Every key under `prefix`, which names a directory: it ends in `/`, as every prefix the node
29
+ * lists by does. The tree's keys, plus those written and not yet flushed, less those deleted and
30
+ * not yet flushed.
31
+ */
9
32
  list(prefix: string): Promise<string[]>;
33
+ /** Make every write resolved before this call durable, at once or within `options.withinMs`: one log record, synced by then. The contract is `RawStoreDriver.flush`'s. */
34
+ flush(options?: FlushOptions): Promise<void>;
35
+ /** Resolves once every write covered by a flush that resolved before this call is durable; `RawStoreDriver.whenDurable`'s contract. */
36
+ whenDurable(): Promise<void>;
37
+ /** Flush what is still held, then release this store's reference to the directory's log. */
38
+ close(): Promise<void>;
39
+ private commit;
40
+ private shared;
41
+ private open;
10
42
  private listRecursive;
43
+ /** The key's file relative to the store's directory, forward slashes, as the log records it. */
44
+ private keyToRelative;
11
45
  private keyToPath;
12
46
  }
13
47
  //# sourceMappingURL=file-kv-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-kv-store.d.ts","sourceRoot":"","sources":["../../src/file-kv-store.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAGnD,+FAA+F;AAC/F,qBAAa,WAAY,YAAW,QAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,MAAM;IAEvC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAS7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQlC,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAS/B,aAAa;IAkB3B,OAAO,CAAC,SAAS;CAUjB"}
1
+ {"version":3,"file":"file-kv-store.d.ts","sourceRoot":"","sources":["../../src/file-kv-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAmC,KAAK,YAAY,EAAE,KAAK,QAAQ,EAAgD,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC7K,OAAO,EAA6D,KAAK,mBAAmB,EAAsB,MAAM,qBAAqB,CAAC;AAE9I,2GAA2G;AAC3G,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG,mBAAmB,CAAC;AAQzE;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAY,YAAW,QAAQ;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,GAAG,CAA2B;gBAE1B,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB;IAWxD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQ7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC;;;;OAIG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAc7C,0KAA0K;IAC1K,KAAK,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5C,uIAAuI;IACvI,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,4FAA4F;IACtF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAU5B,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,IAAI;YAIE,aAAa;IAiB3B,gGAAgG;IAChG,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,SAAS;CAWjB"}
@@ -1,41 +1,103 @@
1
1
  import { promises as fs } from 'fs';
2
2
  import * as path from 'path';
3
- import { atomicWriteFile } from './atomic-write.js';
4
- /** Filesystem-backed IKVStore. Keys may contain `/` separators which become subdirectories. */
3
+ import { WriteGroup } from '@optimystic/db-p2p';
4
+ import { FileWriteLog, assertFileWriteLogOptions, readFileIfExists } from './file-write-log.js';
5
+ const encoder = new TextEncoder();
6
+ // Built on first use rather than at module load, like every other `TextDecoder` in library source
7
+ // (see NO_MODULE_SCOPE_TEXT_DECODER in eslint.config.js).
8
+ let decoderInstance;
9
+ const decoder = () => decoderInstance ??= new TextDecoder();
10
+ /**
11
+ * Filesystem-backed {@link IKVStore}: each key is the file `<basePath>/<key>.json`, a `/` in the key
12
+ * a subdirectory. Writes go through a {@link WriteGroup} over the directory's {@link FileWriteLog} —
13
+ * the same log a `FileRawStorage` over the same directory holds, so the two share one log, one
14
+ * lock and one checkpoint. A `set` or `delete` resolves into memory, served from there to `get` and
15
+ * `list`; `flush()` appends everything written since the last flush to the log as one record, synced
16
+ * before the flush resolves or within the window the flush allows (`FileWriteLog.commit`). A write
17
+ * no flush reaches is flushed by the group's idle timer.
18
+ *
19
+ * The constructor is synchronous and holds nothing: the log is acquired, the lock taken and the log
20
+ * replayed on the first operation. `close()` flushes what is still held and releases this store's
21
+ * reference to the log.
22
+ */
5
23
  export class FileKVStore {
6
24
  basePath;
7
- constructor(basePath) {
8
- this.basePath = basePath;
25
+ options;
26
+ group;
27
+ log;
28
+ constructor(basePath, options = {}) {
29
+ assertFileWriteLogOptions(options);
30
+ this.basePath = path.resolve(basePath);
31
+ this.options = options;
32
+ this.group = new WriteGroup(key => key, {
33
+ commit: (entries, commitOptions) => this.commit(entries, commitOptions),
34
+ // A store that never committed holds no log, and so has nothing to hasten.
35
+ sync: () => this.log?.sync() ?? Promise.resolve(),
36
+ }, options);
9
37
  }
10
38
  async get(key) {
11
- try {
12
- return await fs.readFile(this.keyToPath(key), 'utf-8');
13
- }
14
- catch (err) {
15
- if (err?.code === 'ENOENT')
16
- return undefined;
17
- throw err;
18
- }
39
+ const held = this.group.lookup(key);
40
+ if (held !== undefined)
41
+ return held === null ? undefined : decoder().decode(held);
42
+ await this.open();
43
+ const bytes = await readFileIfExists(this.keyToPath(key));
44
+ return bytes === undefined ? undefined : decoder().decode(bytes);
19
45
  }
20
46
  async set(key, value) {
21
- await atomicWriteFile(this.keyToPath(key), value);
47
+ this.group.put(key, encoder.encode(value));
22
48
  }
23
49
  async delete(key) {
50
+ this.group.delete(key);
51
+ }
52
+ /**
53
+ * Every key under `prefix`, which names a directory: it ends in `/`, as every prefix the node
54
+ * lists by does. The tree's keys, plus those written and not yet flushed, less those deleted and
55
+ * not yet flushed.
56
+ */
57
+ async list(prefix) {
58
+ // Taken before awaiting the tree: a commit resolving meanwhile drops entries from the overlay
59
+ // that the listing below may have been taken without.
60
+ const held = [...this.group.entries()].filter(([key]) => key.startsWith(prefix));
61
+ await this.open();
62
+ const keys = new Set();
63
+ await this.listRecursive(path.join(this.basePath, ...prefix.split('/').filter(Boolean)), prefix, keys);
64
+ for (const [key, value] of held) {
65
+ if (value === null)
66
+ keys.delete(key);
67
+ else
68
+ keys.add(key);
69
+ }
70
+ return [...keys];
71
+ }
72
+ /** Make every write resolved before this call durable, at once or within `options.withinMs`: one log record, synced by then. The contract is `RawStoreDriver.flush`'s. */
73
+ flush(options) {
74
+ return this.group.flush(options);
75
+ }
76
+ /** Resolves once every write covered by a flush that resolved before this call is durable; `RawStoreDriver.whenDurable`'s contract. */
77
+ whenDurable() {
78
+ return this.group.whenDurable();
79
+ }
80
+ /** Flush what is still held, then release this store's reference to the directory's log. */
81
+ async close() {
24
82
  try {
25
- await fs.unlink(this.keyToPath(key));
83
+ await this.group.close();
26
84
  }
27
- catch (err) {
28
- if (err?.code !== 'ENOENT')
29
- throw err;
85
+ finally {
86
+ const shared = this.log;
87
+ this.log = undefined;
88
+ await shared?.release();
30
89
  }
31
90
  }
32
- async list(prefix) {
33
- // prefix like "coordinator/" → scan basePath/coordinator/ directory
34
- const parts = prefix.split('/').filter(Boolean);
35
- const dirPath = path.join(this.basePath, ...parts);
36
- const results = [];
37
- await this.listRecursive(dirPath, prefix, results);
38
- return results;
91
+ commit(entries, options) {
92
+ return this.shared().commit(entries.map(({ key, value }) => value === undefined
93
+ ? { kind: 'delete', path: this.keyToRelative(key) }
94
+ : { kind: 'put', path: this.keyToRelative(key), value }), options);
95
+ }
96
+ shared() {
97
+ return this.log ??= FileWriteLog.acquire(this.basePath, this.options);
98
+ }
99
+ open() {
100
+ return this.shared().open();
39
101
  }
40
102
  async listRecursive(dirPath, prefix, results) {
41
103
  let entries;
@@ -49,22 +111,26 @@ export class FileKVStore {
49
111
  }
50
112
  for (const entry of entries) {
51
113
  if (entry.isDirectory()) {
52
- const subPrefix = prefix + entry.name + '/';
53
- await this.listRecursive(path.join(dirPath, entry.name), subPrefix, results);
114
+ await this.listRecursive(path.join(dirPath, entry.name), prefix + entry.name + '/', results);
54
115
  }
55
116
  else if (entry.isFile() && entry.name.endsWith('.json')) {
56
- results.push(prefix + entry.name.slice(0, -5));
117
+ results.add(prefix + entry.name.slice(0, -5));
57
118
  }
58
119
  }
59
120
  }
121
+ /** The key's file relative to the store's directory, forward slashes, as the log records it. */
122
+ keyToRelative(key) {
123
+ return `${key}.json`;
124
+ }
60
125
  keyToPath(key) {
61
- // NOTE: `/`-separated keys become nested dirs, so a key's first segment shares
62
- // the top-level namespace with FileRawStorage's <blockId>/ dirs. Safe today
63
- // because block ids are content hashes; if a KV key's first segment could ever
64
- // equal a block id, give the two stores separate basePaths (see README Usage).
65
- // FileRawStorage.listBlockIds also yields only root directories holding a meta.json, so a
66
- // KV directory such as the under-replication ledger's `under-replicated/` is never
67
- // enumerated as a block. Its bytes do count toward FileRawStorage.getApproximateBytesUsed.
126
+ // NOTE: `/`-separated keys become nested dirs, so a key's first segment shares the top-level
127
+ // namespace with FileRawStorage's <blockId>/ dirs. Safe while no block id's first segment
128
+ // (a block id containing `/` nests too) equals a key's first segment — the node's keys all start with a fixed word (`under-replicated`,
129
+ // `coordinator`, `participant`, `executed`) — and if one ever could, give the two stores
130
+ // separate basePaths (see README Usage). FileRawStorage.listBlockIds walks the tree but skips
131
+ // these root namespaces (db-p2p's NODE_KV_NAMESPACES — a new namespace must be added there),
132
+ // so a KV key whose last segment is `meta` is never enumerated as a block. Their bytes do
133
+ // count toward FileRawStorage.getApproximateBytesUsed.
68
134
  return path.join(this.basePath, ...key.split('/')) + '.json';
69
135
  }
70
136
  }