@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.
- package/README.md +142 -33
- package/dist/src/file-batch-backend.d.ts +69 -0
- package/dist/src/file-batch-backend.d.ts.map +1 -0
- package/dist/src/file-batch-backend.js +400 -0
- package/dist/src/file-batch-backend.js.map +1 -0
- package/dist/src/file-kv-store.d.ts +37 -3
- package/dist/src/file-kv-store.d.ts.map +1 -1
- package/dist/src/file-kv-store.js +100 -34
- package/dist/src/file-kv-store.js.map +1 -1
- package/dist/src/file-storage.d.ts +20 -58
- package/dist/src/file-storage.d.ts.map +1 -1
- package/dist/src/file-storage.js +16 -430
- package/dist/src/file-storage.js.map +1 -1
- package/dist/src/file-write-log.d.ts +361 -0
- package/dist/src/file-write-log.d.ts.map +1 -0
- package/dist/src/file-write-log.js +848 -0
- package/dist/src/file-write-log.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/store-lock.d.ts +42 -0
- package/dist/src/store-lock.d.ts.map +1 -0
- package/dist/src/store-lock.js +116 -0
- package/dist/src/store-lock.js.map +1 -0
- package/package.json +3 -3
- package/src/file-batch-backend.ts +430 -0
- package/src/file-kv-store.ts +112 -32
- package/src/file-storage.ts +22 -468
- package/src/file-write-log.ts +906 -0
- package/src/index.ts +3 -0
- package/src/store-lock.ts +123 -0
- package/dist/src/atomic-write.d.ts +0 -22
- package/dist/src/atomic-write.d.ts.map +0 -1
- package/dist/src/atomic-write.js +0 -90
- package/dist/src/atomic-write.js.map +0 -1
- 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":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAA4I,MAAM,oBAAoB,CAAC;AAClM,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,gBAAgB,EAAgD,MAAM,qBAAqB,CAAC;AAEjK,MAAM,GAAG,GAAG,YAAY,CAAC,cAAc,CAAC,CAAC;AAEzC,kGAAkG;AAClG,qGAAqG;AACrG,uCAAuC;AACvC,IAAI,eAAwC,CAAC;AAC7C,MAAM,OAAO,GAAG,GAAgB,EAAE,CAAC,eAAe,KAAK,IAAI,WAAW,EAAE,CAAC;AAEzE,0EAA0E;AAC1E,qEAAqE;AACrE,SAAS,yBAAyB,CAAC,QAAkB;IACpD,OAAO,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC;AAED,SAAS,wBAAwB,CAAC,QAAgB;IACjD,OAAO,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAa,CAAC;AAClD,CAAC;AAED,8EAA8E;AAC9E,2EAA2E;AAC3E,iFAAiF;AACjF,gFAAgF;AAChF,gFAAgF;AAChF,qFAAqF;AACrF,oFAAoF;AACpF,mFAAmF;AACnF,sFAAsF;AACtF,sFAAsF;AACtF,SAAS,eAAe,CAAC,KAAiB;IACzC,IAAI,CAAC;QACJ,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,GAAG,YAAY,WAAW;YAAE,OAAO,KAAK,CAAC;QAC7C,MAAM,GAAG,CAAC;IACX,CAAC;AACF,CAAC;AAID,MAAM,kBAAkB,GAAsC;IAC7D,OAAO,EAAE,MAAM;IACf,YAAY,EAAE,SAAS;IACvB,YAAY,EAAE,QAAQ;CACtB,CAAC;AAEF,MAAM,aAAa,GAAG,WAAW,CAAC;AAClC,MAAM,aAAa,GAAG,MAAM,CAAC;AAC7B,MAAM,UAAU,GAAG,QAAQ,CAAC;AAE5B;;;;;;;;GAQG;AACH,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC;AAEzH;;;;;GAKG;AACH,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC,kBAAkB,CAAC,CAAC;AAE5E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,gBAAgB;IASmB;IAR/C,2JAA2J;IAClJ,UAAU,GAAoB,YAAY,CAAC;IAE3C,QAAQ,CAAS;IACT,QAAQ,CAAgB;IACjC,GAAG,CAA2B;IAEtC,8GAA8G;IAC9G,YAAY,QAAgB,EAAmB,UAA+B,EAAE;QAAjC,YAAO,GAAP,OAAO,CAA0B;QAC/E,yBAAyB,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,CAAC,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAED,aAAa;QACZ,OAAO,IAAI,CAAC,QAAQ,CAAC;IACtB,CAAC;IAED,gBAAgB;IAEhB,KAAK,CAAC,GAAG,CAAC,GAAa;QACtB,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,QAAQ,GAAG,CAAC,KAAK,EAAE,CAAC;YACnB,KAAK,UAAU;gBACd,OAAO,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;YACrE,2FAA2F;YAC3F,2FAA2F;YAC3F,gBAAgB;YAChB,qFAAqF;YACrF,2FAA2F;YAC3F,4FAA4F;YAC5F,0FAA0F;YAC1F,wFAAwF;YACxF,yFAAyF;YACzF,8BAA8B;YAC9B,KAAK,WAAW;gBACf,OAAO,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;YAC/E,6FAA6F;YAC7F,yFAAyF;YACzF,2FAA2F;YAC3F,qDAAqD;YACrD,KAAK,QAAQ;gBACZ,OAAO,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;YAC3E,KAAK,SAAS,CAAC;YACf,KAAK,cAAc,CAAC;YACpB,KAAK,cAAc;gBAClB,OAAO,IAAI,CAAC,qBAAqB,CAChC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,EAC3D,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAClE,CAAC;QACJ,CAAC;IACF,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,OAAgB,EAAE,EAAU,EAAE,EAAU;QAC5D,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,yFAAyF;QACzF,iGAAiG;QACjG,MAAM,OAAO,GAA2B,EAAE,CAAC;QAC3C,KAAK,IAAI,GAAG,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC;YACrC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;YACnF,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACzB,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;YAC5B,CAAC;QACF,CAAC;QACD,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,OAAgB;QAC1C,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAEnF,oFAAoF;QACpF,gGAAgG;QAChG,6FAA6F;QAC7F,sDAAsD;QACtD,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACzD,IAAK,GAA6B,EAAE,IAAI,KAAK,QAAQ;gBAAE,OAAO,EAAc,CAAC;YAC7E,GAAG,CAAC,iDAAiD,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;YACrE,MAAM,GAAG,CAAC;QACX,CAAC,CAAC,CAAC;QACH,MAAM,GAAG,GAAe,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAAE,SAAS;YACtC,MAAM,QAAQ,GAAG,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7D,0EAA0E;YAC1E,4EAA4E;YAC5E,4EAA4E;YAC5E,+EAA+E;YAC/E,gFAAgF;YAChF,4EAA4E;YAC5E,6EAA6E;YAC7E,8EAA8E;YAC9E,+EAA+E;YAC/E,iFAAiF;YACjF,iFAAiF;YACjF,wDAAwD;YACxD,IAAI,CAAC,iCAAiC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACvD,6EAA6E;gBAC7E,sFAAsF;gBACtF,4EAA4E;gBAC5E,GAAG,CAAC,qEAAqE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;gBAC1F,SAAS;YACV,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpB,CAAC;QACD,OAAO,GAAG,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,CAAC,YAAY;QAClB,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,8FAA8F;QAC9F,8FAA8F;QAC9F,wFAAwF;QACxF,8FAA8F;QAC9F,8FAA8F;QAC9F,2FAA2F;QAC3F,6BAA6B;QAC7B,EAAE;QACF,6FAA6F;QAC7F,4FAA4F;QAC5F,8EAA8E;QAC9E,2FAA2F;QAC3F,+FAA+F;QAC/F,4FAA4F;QAC5F,qFAAqF;QACrF,4FAA4F;QAC5F,EAAE;QACF,2FAA2F;QAC3F,6FAA6F;QAC7F,gEAAgE;QAChE,+FAA+F;QAC/F,uFAAuF;QACvF,4EAA4E;QAC5E,KAAK,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC;IAEO,KAAK,CAAC,CAAC,oBAAoB,CAAC,QAA2B;QAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,QAAQ,CAAC,CAAC;QAClD,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACvE,IAAI,OAAO;YAAE,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAY,CAAC;QACjD,KAAK,MAAM,IAAI,IAAI,MAAM,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;YACxD,IAAI,OAAO,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,SAAS;YACpD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,SAAS;YACpE,KAAK,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,GAAG,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;QACvD,CAAC;IACF,CAAC;IAEO,KAAK,CAAC,mBAAmB,CAAC,GAAW;QAC5C,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aAC5D,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACd,IAAK,GAA6B,EAAE,IAAI,KAAK,QAAQ;gBAAE,OAAO,EAAE,CAAC;YACjE,GAAG,CAAC,yCAAyC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YACzD,MAAM,GAAG,CAAC;QACX,CAAC,CAAC,CAAC;QACJ,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9E,CAAC;IAEO,eAAe,CAAC,GAAW;QAClC,OAAO,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;aAC7C,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC;aAChB,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACd,IAAK,GAA6B,EAAE,IAAI,KAAK,QAAQ;gBAAE,OAAO,KAAK,CAAC;YACpE,GAAG,CAAC,wCAAwC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YACxD,MAAM,GAAG,CAAC;QACX,CAAC,CAAC,CAAC;IACL,CAAC;IAED,yGAAyG;IACzG,KAAK,CAAC,oBAAoB;QACzB,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED,wBAAwB;IAExB,qHAAqH;IACrH,MAAM,CAAC,OAA8B,EAAE,OAA0B;QAChE,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC1F,CAAC;IAED,wGAAwG;IACxG,IAAI;QACH,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,KAAK;QACV,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC;QACxB,IAAI,CAAC,GAAG,GAAG,SAAS,CAAC;QACrB,MAAM,MAAM,EAAE,OAAO,EAAE,CAAC;IACzB,CAAC;IAED,kBAAkB;IAEV,MAAM;QACb,OAAO,IAAI,CAAC,GAAG,KAAK,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACvE,CAAC;IAEO,IAAI;QACX,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED;;;;;;;;;;OAUG;IACK,YAAY,CAAC,KAAiB;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;QAC5F,MAAM,OAAO,GAAoB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QACtE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC;QACtB,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,GAAG,CAAC,KAAK,KAAK,cAAc,EAAE,CAAC;YAC7D,MAAM,GAAG,GAAG,IAAI,CAAC,wBAAwB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;YACvF,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,KAAK,QAAQ;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QACnG,CAAC;QACD,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,+EAA+E;IAEvE,YAAY,CAAC,GAAa;QACjC,QAAQ,GAAG,CAAC,KAAK,EAAE,CAAC;YACnB,KAAK,UAAU;gBACd,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,aAAa,EAAE,CAAC;YAC1C,KAAK,WAAW;gBACf,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,aAAa,IAAI,GAAG,CAAC,GAAG,OAAO,CAAC;YAC1D,KAAK,QAAQ;gBACZ,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,UAAU,IAAI,GAAG,CAAC,GAAG,OAAO,CAAC;YACvD,KAAK,SAAS,CAAC;YACf,KAAK,cAAc,CAAC;YACpB,KAAK,cAAc;gBAClB,OAAO,IAAI,CAAC,wBAAwB,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7E,CAAC;IACF,CAAC;IAED,iGAAiG;IACjG,qFAAqF;IACrF,iFAAiF;IACzE,wBAAwB,CAAC,KAAwB,EAAE,OAAgB,EAAE,QAAkB,EAAE,OAAO,GAAG,IAAI;QAC9G,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC1E,OAAO,GAAG,OAAO,IAAI,kBAAkB,CAAC,KAAK,CAAC,IAAI,QAAQ,OAAO,CAAC;IACnE,CAAC;IAEO,SAAS,CAAC,OAAgB;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;IAEO,YAAY,CAAC,OAAgB;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC;IAC1D,CAAC;IAEO,YAAY,CAAC,OAAgB,EAAE,GAAW;QACjD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,aAAa,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;IACzE,CAAC;IAEO,SAAS,CAAC,OAAgB,EAAE,GAAW;QAC9C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,CAAC;IACtE,CAAC;IAEO,gBAAgB,CAAC,KAAwB,EAAE,OAAgB,EAAE,QAAkB,EAAE,OAAO,GAAG,IAAI;QACtG,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IACnG,CAAC;IAED,mCAAmC;IAEnC,iFAAiF;IACjF,kFAAkF;IAClF,qEAAqE;IACrE,EAAE;IACF,0EAA0E;IAC1E,4EAA4E;IAC5E,4EAA4E;IAC5E,0EAA0E;IAC1E,EAAE;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,2EAA2E;IAC3E,sDAAsD;IAC9C,KAAK,CAAC,qBAAqB,CAAC,WAAmB,EAAE,OAAe;QACvE,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;QAC5D,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,GAAG,CAAC;QAClC,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,KAAK,WAAW;YAAE,OAAO,SAAS,CAAC;QAC9E,OAAO,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC;aACzB,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;aAC3D,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC1B,CAAC;IAED,2EAA2E;IAC3E,4FAA4F;IAC5F,gGAAgG;IAChG,2FAA2F;IAC3F,wEAAwE;IAChE,KAAK,CAAC,iBAAiB,CAAC,QAAgB,EAAE,SAAkB;QACnE,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC1C,IAAI,SAAS,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1C,GAAG,CAAC,4DAA4D,EAAE,QAAQ,CAAC,CAAC;YAC5E,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAAC,GAAW;QAC1C,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aAC5D,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACd,IAAK,GAA6B,EAAE,IAAI,KAAK,QAAQ;gBAAE,OAAO,EAAE,CAAC;YACjE,GAAG,CAAC,8CAA8C,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YAC9D,OAAO,EAAE,CAAC;QACX,CAAC,CAAC,CAAC;QAEJ,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAC7C,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACzB,KAAK,IAAI,MAAM,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YAClD,CAAC;iBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC3B,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;qBACnC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC;qBACnB,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;oBACd,IAAK,GAA6B,EAAE,IAAI,KAAK,QAAQ;wBAAE,OAAO,CAAC,CAAC;oBAChE,GAAG,CAAC,2CAA2C,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;oBACjE,OAAO,CAAC,CAAC;gBACV,CAAC,CAAC,CAAC;gBACJ,KAAK,IAAI,IAAI,CAAC;YACf,CAAC;QACF,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC;CACD"}
|
|
@@ -1,13 +1,47 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
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
|
-
|
|
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;
|
|
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 {
|
|
4
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
83
|
+
await this.group.close();
|
|
26
84
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
85
|
+
finally {
|
|
86
|
+
const shared = this.log;
|
|
87
|
+
this.log = undefined;
|
|
88
|
+
await shared?.release();
|
|
30
89
|
}
|
|
31
90
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// enumerated as a block.
|
|
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
|
}
|