@oliphaunt/wasix-ts 0.1.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/ARCHITECTURE.md +655 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/lib/archive.d.ts +21 -0
- package/lib/archive.js +336 -0
- package/lib/asset-source.d.ts +4 -0
- package/lib/asset-source.js +15 -0
- package/lib/byte-channel.d.ts +27 -0
- package/lib/byte-channel.js +170 -0
- package/lib/client-common.d.ts +6 -0
- package/lib/client-common.js +36 -0
- package/lib/client.d.ts +7 -0
- package/lib/client.js +24 -0
- package/lib/database-root.d.ts +19 -0
- package/lib/database-root.js +139 -0
- package/lib/database.d.ts +135 -0
- package/lib/database.js +1039 -0
- package/lib/descriptor-validation.d.ts +10 -0
- package/lib/descriptor-validation.js +75 -0
- package/lib/direct-client-common.d.ts +50 -0
- package/lib/direct-client-common.js +717 -0
- package/lib/direct-client.d.ts +4 -0
- package/lib/direct-client.js +13 -0
- package/lib/direct.node.d.ts +2 -0
- package/lib/direct.node.js +2 -0
- package/lib/errors.d.ts +25 -0
- package/lib/errors.js +34 -0
- package/lib/extension-descriptor.d.ts +14 -0
- package/lib/extension-descriptor.js +419 -0
- package/lib/extensions.d.ts +68 -0
- package/lib/extensions.js +769 -0
- package/lib/host/LICENSE +21 -0
- package/lib/host/index.d.mts +123 -0
- package/lib/host/index.mjs +11 -0
- package/lib/host/provenance.json +16 -0
- package/lib/host/wasmer_js_bg.wasm +0 -0
- package/lib/host/worker.mjs +11 -0
- package/lib/host-runtime.d.ts +5 -0
- package/lib/host-runtime.js +13 -0
- package/lib/icu-descriptor.d.ts +3 -0
- package/lib/icu-descriptor.js +92 -0
- package/lib/index.bun.d.ts +2 -0
- package/lib/index.bun.js +2 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.deno.d.ts +2 -0
- package/lib/index.deno.js +2 -0
- package/lib/index.js +2 -0
- package/lib/index.node.d.ts +2 -0
- package/lib/index.node.js +2 -0
- package/lib/internal-common.d.ts +12 -0
- package/lib/internal-common.js +274 -0
- package/lib/internal.d.ts +5 -0
- package/lib/internal.js +42 -0
- package/lib/internal.node.d.ts +5 -0
- package/lib/internal.node.js +8 -0
- package/lib/native-addon.d.ts +111 -0
- package/lib/native-addon.js +223 -0
- package/lib/native-server.d.ts +21 -0
- package/lib/native-server.js +109 -0
- package/lib/native-session.d.ts +60 -0
- package/lib/native-session.js +565 -0
- package/lib/node-actor.d.ts +7 -0
- package/lib/node-actor.js +10 -0
- package/lib/node-client-common.d.ts +9 -0
- package/lib/node-client-common.js +35 -0
- package/lib/node-client.d.ts +4 -0
- package/lib/node-client.js +13 -0
- package/lib/node-direct.d.ts +7 -0
- package/lib/node-direct.js +10 -0
- package/lib/node-worker-options.d.ts +5 -0
- package/lib/node-worker-options.js +65 -0
- package/lib/node-worker-port.d.ts +4 -0
- package/lib/node-worker-port.js +116 -0
- package/lib/node-worker.d.ts +1 -0
- package/lib/node-worker.js +38 -0
- package/lib/pgwire-connection.d.ts +60 -0
- package/lib/pgwire-connection.js +528 -0
- package/lib/pgwire.d.ts +3 -0
- package/lib/pgwire.js +105 -0
- package/lib/physical-archive.d.ts +29 -0
- package/lib/physical-archive.js +527 -0
- package/lib/protocol.d.ts +1 -0
- package/lib/protocol.js +1 -0
- package/lib/public.d.ts +4 -0
- package/lib/public.js +3 -0
- package/lib/query.d.ts +1 -0
- package/lib/query.js +1 -0
- package/lib/rpc.d.ts +203 -0
- package/lib/rpc.js +84 -0
- package/lib/runtime-descriptor.d.ts +3 -0
- package/lib/runtime-descriptor.js +79 -0
- package/lib/server.node.d.ts +1 -0
- package/lib/server.node.js +1 -0
- package/lib/startup-config.d.ts +2 -0
- package/lib/startup-config.js +19 -0
- package/lib/storage/bun.d.ts +6 -0
- package/lib/storage/bun.js +6 -0
- package/lib/storage/deno.d.ts +7 -0
- package/lib/storage/deno.js +7 -0
- package/lib/storage/incremental-storage.d.ts +25 -0
- package/lib/storage/incremental-storage.js +154 -0
- package/lib/storage/indexed-db-provider.d.ts +40 -0
- package/lib/storage/indexed-db-provider.js +259 -0
- package/lib/storage/indexed-db.d.ts +9 -0
- package/lib/storage/indexed-db.js +11 -0
- package/lib/storage/node.d.ts +10 -0
- package/lib/storage/node.js +13 -0
- package/lib/storage/opfs-pool.d.ts +31 -0
- package/lib/storage/opfs-pool.js +1271 -0
- package/lib/storage/opfs-provider.d.ts +4 -0
- package/lib/storage/opfs-provider.js +257 -0
- package/lib/storage/opfs.d.ts +8 -0
- package/lib/storage/opfs.js +10 -0
- package/lib/storage/restore-cleanup.d.ts +4 -0
- package/lib/storage/restore-cleanup.js +22 -0
- package/lib/storage/web-lock.d.ts +2 -0
- package/lib/storage/web-lock.js +64 -0
- package/lib/storage-provider.d.ts +47 -0
- package/lib/storage-provider.js +141 -0
- package/lib/storage-snapshot.d.ts +44 -0
- package/lib/storage-snapshot.js +274 -0
- package/lib/storage.d.ts +46 -0
- package/lib/storage.js +83 -0
- package/lib/tool-runtime.d.ts +43 -0
- package/lib/tool-runtime.js +93 -0
- package/lib/tool-worker-common.d.ts +48 -0
- package/lib/tool-worker-common.js +97 -0
- package/lib/tool-worker.d.ts +1 -0
- package/lib/tool-worker.js +10 -0
- package/lib/types.d.ts +230 -0
- package/lib/types.js +1 -0
- package/lib/wasix-runtime.d.ts +24 -0
- package/lib/wasix-runtime.js +186 -0
- package/lib/worker-client.d.ts +4 -0
- package/lib/worker-client.js +45 -0
- package/lib/worker-dispatch.d.ts +11 -0
- package/lib/worker-dispatch.js +174 -0
- package/lib/worker-entry.bun.d.ts +2 -0
- package/lib/worker-entry.bun.js +2 -0
- package/lib/worker-entry.d.ts +2 -0
- package/lib/worker-entry.deno.d.ts +2 -0
- package/lib/worker-entry.deno.js +2 -0
- package/lib/worker-entry.js +2 -0
- package/lib/worker-entry.node.d.ts +2 -0
- package/lib/worker-entry.node.js +2 -0
- package/lib/worker-node-client.d.ts +4 -0
- package/lib/worker-node-client.js +55 -0
- package/lib/worker-rpc.d.ts +36 -0
- package/lib/worker-rpc.js +422 -0
- package/lib/worker-transfer.d.ts +6 -0
- package/lib/worker-transfer.js +11 -0
- package/lib/worker.d.ts +1 -0
- package/lib/worker.js +21 -0
- package/lib/zstd.d.ts +4 -0
- package/lib/zstd.js +12 -0
- package/node_modules/@oliphaunt/js-core/README.md +7 -0
- package/node_modules/@oliphaunt/js-core/dist/commonjs/protocol.d.ts +1 -0
- package/node_modules/@oliphaunt/js-core/dist/commonjs/protocol.js +22 -0
- package/node_modules/@oliphaunt/js-core/dist/commonjs/query.d.ts +255 -0
- package/node_modules/@oliphaunt/js-core/dist/commonjs/query.js +2068 -0
- package/node_modules/@oliphaunt/js-core/dist/module/package.json +3 -0
- package/node_modules/@oliphaunt/js-core/dist/module/protocol.d.ts +1 -0
- package/node_modules/@oliphaunt/js-core/dist/module/protocol.js +19 -0
- package/node_modules/@oliphaunt/js-core/dist/module/query.d.ts +255 -0
- package/node_modules/@oliphaunt/js-core/dist/module/query.js +2039 -0
- package/node_modules/@oliphaunt/js-core/package.json +21 -0
- package/package.json +122 -0
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
import { parseJsonWithUniqueObjectKeys } from '../database-root.js';
|
|
2
|
+
import { composeWasixStorageFailure, WasixStorageError } from '../errors.js';
|
|
3
|
+
import { assertWasixPhysicalIdentity, physicalIdentityMatches, } from '../storage-provider.js';
|
|
4
|
+
import { splitStorageDeltaDeletes, VOLATILE_DATABASE_FILES, validateDirectoryEntryName, validateStoredSnapshot, } from '../storage-snapshot.js';
|
|
5
|
+
export const OPFS_POOL_ROOT = '.oliphaunt-wasix-pool-v1';
|
|
6
|
+
const STATE_FILE = 'state.json';
|
|
7
|
+
const DATA_DIRECTORY = 'data';
|
|
8
|
+
const STATE_SCHEMA = 'oliphaunt-wasix-opfs-pool-v1';
|
|
9
|
+
const PREOPENED_FILE_RESERVE = 32;
|
|
10
|
+
const MAX_PARALLEL_IO = 16;
|
|
11
|
+
const TYPE_FILE = 1;
|
|
12
|
+
const TYPE_DIRECTORY = 2;
|
|
13
|
+
const OP = {
|
|
14
|
+
metadata: 1,
|
|
15
|
+
readDirectory: 2,
|
|
16
|
+
createDirectory: 3,
|
|
17
|
+
removeDirectory: 4,
|
|
18
|
+
rename: 5,
|
|
19
|
+
removeFile: 6,
|
|
20
|
+
open: 7,
|
|
21
|
+
close: 8,
|
|
22
|
+
read: 9,
|
|
23
|
+
write: 10,
|
|
24
|
+
flush: 11,
|
|
25
|
+
truncate: 12,
|
|
26
|
+
unlink: 13,
|
|
27
|
+
fileSize: 14,
|
|
28
|
+
};
|
|
29
|
+
const RESULT = {
|
|
30
|
+
ok: 0,
|
|
31
|
+
notFound: 1,
|
|
32
|
+
exists: 2,
|
|
33
|
+
notDirectory: 3,
|
|
34
|
+
notFile: 4,
|
|
35
|
+
directoryNotEmpty: 5,
|
|
36
|
+
permission: 6,
|
|
37
|
+
invalid: 7,
|
|
38
|
+
storageFull: 8,
|
|
39
|
+
unsupported: 9,
|
|
40
|
+
timeout: 10,
|
|
41
|
+
io: 11,
|
|
42
|
+
};
|
|
43
|
+
const FLAG_WRITE = 1 << 1;
|
|
44
|
+
const FLAG_CREATE_NEW = 1 << 2;
|
|
45
|
+
const FLAG_CREATE = 1 << 3;
|
|
46
|
+
const FLAG_APPEND = 1 << 4;
|
|
47
|
+
const FLAG_TRUNCATE = 1 << 5;
|
|
48
|
+
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
49
|
+
const UTF8_ENCODER = new TextEncoder();
|
|
50
|
+
/**
|
|
51
|
+
* A same-realm synchronous OPFS filesystem for the WASIX host.
|
|
52
|
+
*
|
|
53
|
+
* Logical PostgreSQL paths live in one atomic state file while relation bytes
|
|
54
|
+
* live in flat, pre-opened backing files. That deliberate opacity removes all
|
|
55
|
+
* asynchronous OPFS namespace calls from PostgreSQL's hot syscall path.
|
|
56
|
+
*/
|
|
57
|
+
export class DirectOpfsPool {
|
|
58
|
+
state;
|
|
59
|
+
request;
|
|
60
|
+
#name;
|
|
61
|
+
#database;
|
|
62
|
+
#data;
|
|
63
|
+
#physicalIdentity;
|
|
64
|
+
#entries = new Map();
|
|
65
|
+
#children = new Map();
|
|
66
|
+
#descriptors = new Map();
|
|
67
|
+
#records = new Set();
|
|
68
|
+
#dirty = new Set();
|
|
69
|
+
#spares = [];
|
|
70
|
+
#pendingReclaims = new Set();
|
|
71
|
+
#initializationComplete;
|
|
72
|
+
#nextDescriptor = 1;
|
|
73
|
+
#namespaceDirty = false;
|
|
74
|
+
#closed = false;
|
|
75
|
+
constructor(name, database, data, physicalIdentity, state) {
|
|
76
|
+
this.#name = name;
|
|
77
|
+
this.#database = database;
|
|
78
|
+
this.#data = data;
|
|
79
|
+
this.#physicalIdentity = physicalIdentity;
|
|
80
|
+
this.state = state;
|
|
81
|
+
this.#initializationComplete = state === 'existing';
|
|
82
|
+
this.#entries.set('', { type: 'directory' });
|
|
83
|
+
this.request = (opcode, path, buffer, arg0, arg1, flags) => {
|
|
84
|
+
if (this.#closed)
|
|
85
|
+
return [RESULT.io, 0, 0, 0];
|
|
86
|
+
try {
|
|
87
|
+
return this.#dispatch(opcode, path, buffer, arg0, arg1, flags);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return [classifyError(error), 0, 0, 0];
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
static async open(name, loadClusterSeed, physicalIdentity) {
|
|
95
|
+
const selectedIdentity = assertWasixPhysicalIdentity(physicalIdentity);
|
|
96
|
+
const database = await openPooledDatabaseDirectory(name);
|
|
97
|
+
let data = await database.getDirectoryHandle(DATA_DIRECTORY, { create: true });
|
|
98
|
+
let stored = await readPoolState(database, name, selectedIdentity);
|
|
99
|
+
if (stored?.phase === 'initializing') {
|
|
100
|
+
({ data, state: stored } = await resetPoolClusterSeed(database, name, await loadClusterSeed(), selectedIdentity));
|
|
101
|
+
}
|
|
102
|
+
else if (stored === undefined) {
|
|
103
|
+
stored = await initializePoolState(database, data, name, await loadClusterSeed(), selectedIdentity);
|
|
104
|
+
}
|
|
105
|
+
const state = stored.phase === 'ready' ? 'existing' : 'new';
|
|
106
|
+
const pool = new DirectOpfsPool(name, database, data, selectedIdentity, state);
|
|
107
|
+
try {
|
|
108
|
+
await pool.#hydrate(stored);
|
|
109
|
+
await pool.#restoreSpares();
|
|
110
|
+
await pool.#removeVolatileEntries();
|
|
111
|
+
await pool.#maintainSpares();
|
|
112
|
+
return pool;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
try {
|
|
116
|
+
pool.#closeHandles(false);
|
|
117
|
+
}
|
|
118
|
+
catch (closeError) {
|
|
119
|
+
const primary = error instanceof Error ? error : new Error(describeError(error));
|
|
120
|
+
throw composeWasixStorageFailure(primary, 'OPFS handle cleanup also failed', closeError);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async sync(boundary) {
|
|
126
|
+
this.#assertOpen();
|
|
127
|
+
if (boundary === 'full' || boundary === 'close')
|
|
128
|
+
this.#flushAll();
|
|
129
|
+
else
|
|
130
|
+
this.#flushWal();
|
|
131
|
+
await this.#materializeStagedFiles();
|
|
132
|
+
const completesInitialization = !this.#initializationComplete && boundary === 'full';
|
|
133
|
+
if (this.#namespaceDirty || completesInitialization) {
|
|
134
|
+
// A namespace publication makes newly mapped bytes reachable. Reuse the
|
|
135
|
+
// complete PostgreSQL order instead of depending on map insertion order.
|
|
136
|
+
this.#flushAll();
|
|
137
|
+
await writePoolState(this.#database, serializePoolState(this.#name, this.#physicalIdentity, this.#entries, this.#initializationComplete || completesInitialization ? 'ready' : 'initializing'));
|
|
138
|
+
this.#namespaceDirty = false;
|
|
139
|
+
if (completesInitialization)
|
|
140
|
+
this.#initializationComplete = true;
|
|
141
|
+
}
|
|
142
|
+
this.#publishReclaims();
|
|
143
|
+
if (boundary !== 'close') {
|
|
144
|
+
// The boundary is already durable. Spare replenishment is only an
|
|
145
|
+
// optimization for the next synchronous file-creation burst and must
|
|
146
|
+
// never turn a successful publication into an ambiguous failure.
|
|
147
|
+
await this.#maintainSpares().catch(() => undefined);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async close(clean) {
|
|
151
|
+
if (this.#closed)
|
|
152
|
+
return;
|
|
153
|
+
let failure;
|
|
154
|
+
if (clean) {
|
|
155
|
+
try {
|
|
156
|
+
await this.sync('close');
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
failure = error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.#closed = true;
|
|
163
|
+
try {
|
|
164
|
+
this.#closeHandles(clean);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
failure = failure === undefined ? error : new AggregateError([failure, error]);
|
|
168
|
+
}
|
|
169
|
+
if (failure !== undefined)
|
|
170
|
+
throw failure;
|
|
171
|
+
}
|
|
172
|
+
#dispatch(opcode, path, buffer, arg0, arg1, flags) {
|
|
173
|
+
switch (opcode) {
|
|
174
|
+
case OP.metadata:
|
|
175
|
+
return this.#metadata(path);
|
|
176
|
+
case OP.readDirectory:
|
|
177
|
+
return this.#readDirectory(path, arg0, buffer);
|
|
178
|
+
case OP.createDirectory:
|
|
179
|
+
this.#createDirectory(path);
|
|
180
|
+
break;
|
|
181
|
+
case OP.removeDirectory:
|
|
182
|
+
this.#removeDirectory(path);
|
|
183
|
+
break;
|
|
184
|
+
case OP.rename:
|
|
185
|
+
// wasm-bindgen views the threaded host memory through SharedArrayBuffer;
|
|
186
|
+
// TextDecoder deliberately rejects shared views, so copy only the rare
|
|
187
|
+
// rename payload. Data I/O still uses the exact requested ranges and
|
|
188
|
+
// avoids the retired copied mailbox.
|
|
189
|
+
this.#rename(path, UTF8_DECODER.decode(Uint8Array.from(buffer)));
|
|
190
|
+
break;
|
|
191
|
+
case OP.removeFile:
|
|
192
|
+
this.#removeFile(path);
|
|
193
|
+
break;
|
|
194
|
+
case OP.open:
|
|
195
|
+
return this.#open(path, flags);
|
|
196
|
+
case OP.close:
|
|
197
|
+
this.#closeDescriptor(arg0);
|
|
198
|
+
break;
|
|
199
|
+
case OP.read:
|
|
200
|
+
return this.#read(arg0, arg1, buffer);
|
|
201
|
+
case OP.write:
|
|
202
|
+
return this.#write(arg0, arg1, buffer);
|
|
203
|
+
case OP.flush:
|
|
204
|
+
this.#flushRecord(this.#descriptor(arg0).record);
|
|
205
|
+
break;
|
|
206
|
+
case OP.truncate:
|
|
207
|
+
this.#truncate(arg0, arg1);
|
|
208
|
+
break;
|
|
209
|
+
case OP.unlink:
|
|
210
|
+
this.#unlinkDescriptor(arg0);
|
|
211
|
+
break;
|
|
212
|
+
case OP.fileSize:
|
|
213
|
+
return [RESULT.ok, 0, this.#recordSize(this.#descriptor(arg0).record), 0];
|
|
214
|
+
default:
|
|
215
|
+
throw new DOMException('unsupported OPFS bridge operation', 'NotSupportedError');
|
|
216
|
+
}
|
|
217
|
+
return [RESULT.ok, 0, 0, 0];
|
|
218
|
+
}
|
|
219
|
+
#metadata(path) {
|
|
220
|
+
validatePath(path, true);
|
|
221
|
+
const entry = this.#entries.get(path);
|
|
222
|
+
if (entry === undefined)
|
|
223
|
+
throw notFound();
|
|
224
|
+
return entry.type === 'directory'
|
|
225
|
+
? [RESULT.ok, 0, TYPE_DIRECTORY, 0]
|
|
226
|
+
: [RESULT.ok, 0, TYPE_FILE, this.#recordSize(entry.record)];
|
|
227
|
+
}
|
|
228
|
+
#readDirectory(path, start, output) {
|
|
229
|
+
this.#requireDirectory(path, true);
|
|
230
|
+
if (!Number.isSafeInteger(start) || start < 0)
|
|
231
|
+
throw invalid();
|
|
232
|
+
const entries = [...(this.#children.get(path)?.entries() ?? [])]
|
|
233
|
+
.map(([name, entry]) => ({
|
|
234
|
+
name,
|
|
235
|
+
type: entry.type === 'directory' ? TYPE_DIRECTORY : TYPE_FILE,
|
|
236
|
+
}))
|
|
237
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
238
|
+
if (start > entries.length)
|
|
239
|
+
throw invalid();
|
|
240
|
+
const page = encodeDirectoryPage(entries, start, output.byteLength);
|
|
241
|
+
output.set(page.bytes);
|
|
242
|
+
return [RESULT.ok, page.bytes.byteLength, page.next, Number(page.next === entries.length)];
|
|
243
|
+
}
|
|
244
|
+
#createDirectory(path) {
|
|
245
|
+
validatePath(path);
|
|
246
|
+
if (this.#entries.has(path))
|
|
247
|
+
throw new ExistsError();
|
|
248
|
+
this.#requireDirectory(parentPath(path), true);
|
|
249
|
+
this.#entries.set(path, { type: 'directory' });
|
|
250
|
+
this.#rebuildChildren();
|
|
251
|
+
this.#namespaceDirty = true;
|
|
252
|
+
}
|
|
253
|
+
#removeDirectory(path) {
|
|
254
|
+
this.#requireDirectory(path);
|
|
255
|
+
if ((this.#children.get(path)?.size ?? 0) > 0)
|
|
256
|
+
throw new DirectoryNotEmptyError();
|
|
257
|
+
this.#entries.delete(path);
|
|
258
|
+
this.#rebuildChildren();
|
|
259
|
+
this.#namespaceDirty = true;
|
|
260
|
+
}
|
|
261
|
+
#rename(from, to) {
|
|
262
|
+
validatePath(from);
|
|
263
|
+
validatePath(to);
|
|
264
|
+
if (from === to)
|
|
265
|
+
return;
|
|
266
|
+
const source = this.#entries.get(from);
|
|
267
|
+
if (source === undefined)
|
|
268
|
+
throw notFound();
|
|
269
|
+
this.#requireDirectory(parentPath(to), true);
|
|
270
|
+
if (source.type === 'directory' && (to.startsWith(`${from}/`) || from === to))
|
|
271
|
+
throw invalid();
|
|
272
|
+
const destination = this.#entries.get(to);
|
|
273
|
+
if (destination !== undefined) {
|
|
274
|
+
if (source.type !== destination.type) {
|
|
275
|
+
throw source.type === 'directory' ? notDirectory() : notFile();
|
|
276
|
+
}
|
|
277
|
+
if (destination.type === 'directory') {
|
|
278
|
+
if ((this.#children.get(to)?.size ?? 0) > 0)
|
|
279
|
+
throw new DirectoryNotEmptyError();
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
this.#detach(destination.record);
|
|
283
|
+
}
|
|
284
|
+
this.#entries.delete(to);
|
|
285
|
+
}
|
|
286
|
+
const moved = [...this.#entries.entries()]
|
|
287
|
+
.filter(([path]) => path === from || path.startsWith(`${from}/`))
|
|
288
|
+
.sort(([left], [right]) => left.length - right.length);
|
|
289
|
+
for (const [path] of moved)
|
|
290
|
+
this.#entries.delete(path);
|
|
291
|
+
for (const [path, entry] of moved) {
|
|
292
|
+
const next = path === from ? to : `${to}${path.slice(from.length)}`;
|
|
293
|
+
this.#entries.set(next, entry);
|
|
294
|
+
if (entry.type === 'file')
|
|
295
|
+
entry.record.path = next;
|
|
296
|
+
}
|
|
297
|
+
this.#rebuildChildren();
|
|
298
|
+
this.#namespaceDirty = true;
|
|
299
|
+
}
|
|
300
|
+
#removeFile(path) {
|
|
301
|
+
const entry = this.#requireFile(path);
|
|
302
|
+
this.#entries.delete(path);
|
|
303
|
+
this.#detach(entry.record);
|
|
304
|
+
this.#rebuildChildren();
|
|
305
|
+
this.#namespaceDirty = true;
|
|
306
|
+
}
|
|
307
|
+
#open(path, flags) {
|
|
308
|
+
validatePath(path);
|
|
309
|
+
const existing = this.#entries.get(path);
|
|
310
|
+
if ((flags & FLAG_CREATE_NEW) !== 0 && existing !== undefined)
|
|
311
|
+
throw new ExistsError();
|
|
312
|
+
let entry;
|
|
313
|
+
if (existing === undefined) {
|
|
314
|
+
if ((flags & (FLAG_CREATE | FLAG_CREATE_NEW)) === 0)
|
|
315
|
+
throw notFound();
|
|
316
|
+
this.#requireDirectory(parentPath(path), true);
|
|
317
|
+
const record = this.#takeSpare(path);
|
|
318
|
+
entry = { type: 'file', record };
|
|
319
|
+
this.#entries.set(path, entry);
|
|
320
|
+
this.#rebuildChildren();
|
|
321
|
+
this.#namespaceDirty = true;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
if (existing.type !== 'file')
|
|
325
|
+
throw notFile();
|
|
326
|
+
entry = existing;
|
|
327
|
+
}
|
|
328
|
+
if ((flags & FLAG_TRUNCATE) !== 0) {
|
|
329
|
+
if ((flags & (FLAG_WRITE | FLAG_APPEND)) === 0)
|
|
330
|
+
throw invalid();
|
|
331
|
+
this.#setRecordSize(entry.record, 0);
|
|
332
|
+
}
|
|
333
|
+
const descriptor = this.#nextDescriptor++;
|
|
334
|
+
entry.record.references += 1;
|
|
335
|
+
this.#descriptors.set(descriptor, {
|
|
336
|
+
record: entry.record,
|
|
337
|
+
append: (flags & FLAG_APPEND) !== 0,
|
|
338
|
+
});
|
|
339
|
+
return [RESULT.ok, 0, descriptor, this.#recordSize(entry.record)];
|
|
340
|
+
}
|
|
341
|
+
#closeDescriptor(descriptor) {
|
|
342
|
+
const record = this.#descriptor(descriptor).record;
|
|
343
|
+
this.#descriptors.delete(descriptor);
|
|
344
|
+
record.references -= 1;
|
|
345
|
+
if (record.references === 0 && record.path === undefined)
|
|
346
|
+
this.#queueReclaim(record);
|
|
347
|
+
}
|
|
348
|
+
#read(descriptor, offset, output) {
|
|
349
|
+
validateOffset(offset);
|
|
350
|
+
const record = this.#descriptor(descriptor).record;
|
|
351
|
+
const available = Math.max(0, Math.min(output.byteLength, this.#recordSize(record) - offset));
|
|
352
|
+
if (available === 0)
|
|
353
|
+
return [RESULT.ok, 0, 0, 0];
|
|
354
|
+
if (record.staged !== undefined) {
|
|
355
|
+
output.set(record.staged.subarray(offset, offset + available));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
const read = requireAccess(record).read(output.subarray(0, available), { at: offset });
|
|
359
|
+
validateProgress(read, available, 'read');
|
|
360
|
+
if (read !== available)
|
|
361
|
+
return [RESULT.ok, read, 0, 0];
|
|
362
|
+
}
|
|
363
|
+
return [RESULT.ok, available, 0, 0];
|
|
364
|
+
}
|
|
365
|
+
#write(descriptor, offset, bytes) {
|
|
366
|
+
validateOffset(offset);
|
|
367
|
+
const state = this.#descriptor(descriptor);
|
|
368
|
+
const record = state.record;
|
|
369
|
+
const writeOffset = state.append ? this.#recordSize(record) : offset;
|
|
370
|
+
this.#markDirty(record);
|
|
371
|
+
if (record.staged !== undefined) {
|
|
372
|
+
const previousSize = this.#recordSize(record);
|
|
373
|
+
const needed = writeOffset + bytes.byteLength;
|
|
374
|
+
if (!Number.isSafeInteger(needed))
|
|
375
|
+
throw invalid();
|
|
376
|
+
this.#ensureStagedCapacity(record, needed, previousSize);
|
|
377
|
+
if (writeOffset > previousSize)
|
|
378
|
+
record.staged.fill(0, previousSize, writeOffset);
|
|
379
|
+
record.staged.set(bytes, writeOffset);
|
|
380
|
+
record.stagedSize = Math.max(previousSize, needed);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const written = requireAccess(record).write(bytes, { at: writeOffset });
|
|
384
|
+
validateProgress(written, bytes.byteLength, 'write');
|
|
385
|
+
if (written !== bytes.byteLength)
|
|
386
|
+
return [RESULT.ok, 0, written, writeOffset + written];
|
|
387
|
+
}
|
|
388
|
+
return [RESULT.ok, 0, bytes.byteLength, writeOffset + bytes.byteLength];
|
|
389
|
+
}
|
|
390
|
+
#truncate(descriptor, size) {
|
|
391
|
+
this.#setRecordSize(this.#descriptor(descriptor).record, size);
|
|
392
|
+
}
|
|
393
|
+
#unlinkDescriptor(descriptor) {
|
|
394
|
+
const record = this.#descriptor(descriptor).record;
|
|
395
|
+
if (record.path === undefined)
|
|
396
|
+
return;
|
|
397
|
+
this.#entries.delete(record.path);
|
|
398
|
+
this.#detach(record);
|
|
399
|
+
this.#rebuildChildren();
|
|
400
|
+
this.#namespaceDirty = true;
|
|
401
|
+
}
|
|
402
|
+
#recordSize(record) {
|
|
403
|
+
return record.staged === undefined
|
|
404
|
+
? requireAccess(record).getSize()
|
|
405
|
+
: (record.stagedSize ?? record.staged.byteLength);
|
|
406
|
+
}
|
|
407
|
+
#setRecordSize(record, size) {
|
|
408
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
409
|
+
throw invalid();
|
|
410
|
+
this.#markDirty(record);
|
|
411
|
+
if (record.staged !== undefined) {
|
|
412
|
+
const previousSize = this.#recordSize(record);
|
|
413
|
+
this.#ensureStagedCapacity(record, size, previousSize);
|
|
414
|
+
if (size !== previousSize) {
|
|
415
|
+
record.staged.fill(0, Math.min(size, previousSize), Math.max(size, previousSize));
|
|
416
|
+
}
|
|
417
|
+
record.stagedSize = size;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
requireAccess(record).truncate(size);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
#ensureStagedCapacity(record, size, previousSize) {
|
|
424
|
+
const staged = record.staged;
|
|
425
|
+
if (staged === undefined || size <= staged.byteLength)
|
|
426
|
+
return;
|
|
427
|
+
try {
|
|
428
|
+
const grown = new Uint8Array(Math.max(size, staged.byteLength * 2, 8 * 1024));
|
|
429
|
+
grown.set(staged.subarray(0, previousSize));
|
|
430
|
+
record.staged = grown;
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
throw new DOMException(`could not stage ${size} bytes: ${describeError(error)}`, 'QuotaExceededError');
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
#markDirty(record) {
|
|
437
|
+
record.dirty = true;
|
|
438
|
+
this.#dirty.add(record);
|
|
439
|
+
}
|
|
440
|
+
#flushRecord(record) {
|
|
441
|
+
// A staged file cannot be flushed synchronously because allocating its
|
|
442
|
+
// OPFS backing is asynchronous. It remains dirty until the mandatory host
|
|
443
|
+
// boundary materializes and flushes it before publishing the namespace.
|
|
444
|
+
if (!record.dirty || record.staged !== undefined)
|
|
445
|
+
return;
|
|
446
|
+
requireAccess(record).flush();
|
|
447
|
+
record.dirty = false;
|
|
448
|
+
this.#dirty.delete(record);
|
|
449
|
+
}
|
|
450
|
+
#flushWal() {
|
|
451
|
+
for (const record of [...this.#dirty]) {
|
|
452
|
+
if (isWalRecord(record))
|
|
453
|
+
this.#flushRecord(record);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
#flushAll() {
|
|
457
|
+
for (const record of [...this.#dirty].sort(compareFlushOrder))
|
|
458
|
+
this.#flushRecord(record);
|
|
459
|
+
}
|
|
460
|
+
#descriptor(value) {
|
|
461
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
462
|
+
throw invalid();
|
|
463
|
+
const descriptor = this.#descriptors.get(value);
|
|
464
|
+
if (descriptor === undefined)
|
|
465
|
+
throw new DOMException('invalid descriptor', 'InvalidStateError');
|
|
466
|
+
return descriptor;
|
|
467
|
+
}
|
|
468
|
+
#requireDirectory(path, allowRoot = false) {
|
|
469
|
+
validatePath(path, allowRoot);
|
|
470
|
+
const entry = this.#entries.get(path);
|
|
471
|
+
if (entry === undefined)
|
|
472
|
+
throw notFound();
|
|
473
|
+
if (entry.type !== 'directory')
|
|
474
|
+
throw notDirectory();
|
|
475
|
+
return entry;
|
|
476
|
+
}
|
|
477
|
+
#requireFile(path) {
|
|
478
|
+
validatePath(path);
|
|
479
|
+
const entry = this.#entries.get(path);
|
|
480
|
+
if (entry === undefined)
|
|
481
|
+
throw notFound();
|
|
482
|
+
if (entry.type !== 'file')
|
|
483
|
+
throw notFile();
|
|
484
|
+
return entry;
|
|
485
|
+
}
|
|
486
|
+
#takeSpare(path) {
|
|
487
|
+
const record = this.#spares.pop();
|
|
488
|
+
if (record !== undefined) {
|
|
489
|
+
requireAccess(record).truncate(0);
|
|
490
|
+
record.path = path;
|
|
491
|
+
record.references = 0;
|
|
492
|
+
this.#markDirty(record);
|
|
493
|
+
return record;
|
|
494
|
+
}
|
|
495
|
+
const staged = {
|
|
496
|
+
path,
|
|
497
|
+
staged: new Uint8Array(),
|
|
498
|
+
stagedSize: 0,
|
|
499
|
+
references: 0,
|
|
500
|
+
dirty: false,
|
|
501
|
+
};
|
|
502
|
+
this.#records.add(staged);
|
|
503
|
+
return staged;
|
|
504
|
+
}
|
|
505
|
+
#detach(record) {
|
|
506
|
+
record.path = undefined;
|
|
507
|
+
if (record.references === 0)
|
|
508
|
+
this.#queueReclaim(record);
|
|
509
|
+
}
|
|
510
|
+
#queueReclaim(record) {
|
|
511
|
+
if (record.staged !== undefined) {
|
|
512
|
+
this.#dirty.delete(record);
|
|
513
|
+
this.#records.delete(record);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
this.#pendingReclaims.add(record);
|
|
517
|
+
}
|
|
518
|
+
#publishReclaims() {
|
|
519
|
+
for (const record of [...this.#pendingReclaims]) {
|
|
520
|
+
record.dirty = false;
|
|
521
|
+
this.#dirty.delete(record);
|
|
522
|
+
this.#spares.push(record);
|
|
523
|
+
this.#pendingReclaims.delete(record);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async #hydrate(state) {
|
|
527
|
+
for (const entry of state.entries) {
|
|
528
|
+
if (entry.type === 'directory')
|
|
529
|
+
this.#entries.set(entry.path, { type: 'directory' });
|
|
530
|
+
}
|
|
531
|
+
const files = state.entries.filter((entry) => entry.type === 'file');
|
|
532
|
+
await parallelMap(files, async (entry) => {
|
|
533
|
+
let file;
|
|
534
|
+
try {
|
|
535
|
+
file = (await this.#data.getFileHandle(entry.backing));
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (errorName(error) === 'NotFoundError') {
|
|
539
|
+
throw corrupt(this.#name, `is missing backing ${JSON.stringify(entry.backing)} for ${JSON.stringify(entry.path)}`, error);
|
|
540
|
+
}
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
const access = await createAccess(file);
|
|
544
|
+
const record = {
|
|
545
|
+
path: entry.path,
|
|
546
|
+
backing: entry.backing,
|
|
547
|
+
access,
|
|
548
|
+
references: 0,
|
|
549
|
+
dirty: false,
|
|
550
|
+
};
|
|
551
|
+
this.#records.add(record);
|
|
552
|
+
this.#entries.set(entry.path, { type: 'file', record });
|
|
553
|
+
});
|
|
554
|
+
this.#rebuildChildren();
|
|
555
|
+
this.#validateEssentials();
|
|
556
|
+
}
|
|
557
|
+
async #restoreSpares() {
|
|
558
|
+
const referenced = new Set([...this.#records].flatMap((record) => record.backing === undefined ? [] : [record.backing]));
|
|
559
|
+
const unused = [];
|
|
560
|
+
for await (const name of this.#data.keys()) {
|
|
561
|
+
try {
|
|
562
|
+
validateBackingName(name);
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
throw corrupt(this.#name, `contains invalid pool entry ${JSON.stringify(name)}`, error);
|
|
566
|
+
}
|
|
567
|
+
if (!referenced.has(name))
|
|
568
|
+
unused.push(name);
|
|
569
|
+
}
|
|
570
|
+
const keep = unused.sort().slice(0, PREOPENED_FILE_RESERVE);
|
|
571
|
+
await parallelMap(keep, async (backing) => {
|
|
572
|
+
const file = (await this.#data.getFileHandle(backing));
|
|
573
|
+
const access = await createAccess(file);
|
|
574
|
+
const record = { backing, access, references: 0, dirty: false };
|
|
575
|
+
this.#records.add(record);
|
|
576
|
+
this.#spares.push(record);
|
|
577
|
+
});
|
|
578
|
+
await parallelMap(unused.slice(PREOPENED_FILE_RESERVE), (backing) => this.#data.removeEntry(backing));
|
|
579
|
+
}
|
|
580
|
+
async #maintainSpares() {
|
|
581
|
+
const missing = PREOPENED_FILE_RESERVE - this.#spares.length;
|
|
582
|
+
if (missing <= 0)
|
|
583
|
+
return;
|
|
584
|
+
await parallelCreate(missing, async () => {
|
|
585
|
+
const { backing, access } = await this.#createBacking();
|
|
586
|
+
const record = { backing, access, references: 0, dirty: false };
|
|
587
|
+
this.#records.add(record);
|
|
588
|
+
this.#spares.push(record);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
async #materializeStagedFiles() {
|
|
592
|
+
const staged = [...this.#records]
|
|
593
|
+
.filter((record) => record.staged !== undefined && record.path !== undefined)
|
|
594
|
+
.sort(compareFlushOrder);
|
|
595
|
+
await parallelMap(staged, async (record) => {
|
|
596
|
+
const bytes = record.staged;
|
|
597
|
+
if (bytes === undefined)
|
|
598
|
+
throw new Error('staged OPFS record lost its bytes');
|
|
599
|
+
const allocation = await this.#createBacking();
|
|
600
|
+
try {
|
|
601
|
+
writeComplete(allocation.access, bytes.subarray(0, this.#recordSize(record)), 0);
|
|
602
|
+
allocation.access.flush();
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
throw await this.#discardBacking(allocation.backing, allocation.access, error);
|
|
606
|
+
}
|
|
607
|
+
record.backing = allocation.backing;
|
|
608
|
+
record.access = allocation.access;
|
|
609
|
+
record.staged = undefined;
|
|
610
|
+
record.stagedSize = undefined;
|
|
611
|
+
record.dirty = false;
|
|
612
|
+
this.#dirty.delete(record);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async #createBacking() {
|
|
616
|
+
const backing = newBackingName();
|
|
617
|
+
let access;
|
|
618
|
+
try {
|
|
619
|
+
const file = (await this.#data.getFileHandle(backing, { create: true }));
|
|
620
|
+
access = await createAccess(file);
|
|
621
|
+
return { backing, access };
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
throw await this.#discardBacking(backing, access, error);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
async #discardBacking(backing, access, error) {
|
|
628
|
+
let failure = error instanceof Error ? error : new Error(describeError(error));
|
|
629
|
+
try {
|
|
630
|
+
access?.close();
|
|
631
|
+
}
|
|
632
|
+
catch (cleanupError) {
|
|
633
|
+
const primaryName = failure.name;
|
|
634
|
+
failure = composeWasixStorageFailure(failure, 'backing handle cleanup also failed', cleanupError);
|
|
635
|
+
failure.name = primaryName;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
await this.#data.removeEntry(backing);
|
|
639
|
+
}
|
|
640
|
+
catch (cleanupError) {
|
|
641
|
+
if (errorName(cleanupError) !== 'NotFoundError') {
|
|
642
|
+
const primaryName = failure.name;
|
|
643
|
+
failure = composeWasixStorageFailure(failure, 'backing file cleanup also failed', cleanupError);
|
|
644
|
+
failure.name = primaryName;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return failure;
|
|
648
|
+
}
|
|
649
|
+
async #removeVolatileEntries() {
|
|
650
|
+
for (const path of VOLATILE_DATABASE_FILES) {
|
|
651
|
+
const entry = this.#entries.get(path);
|
|
652
|
+
if (entry?.type === 'file') {
|
|
653
|
+
this.#entries.delete(path);
|
|
654
|
+
this.#detach(entry.record);
|
|
655
|
+
this.#namespaceDirty = true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (this.#namespaceDirty) {
|
|
659
|
+
this.#rebuildChildren();
|
|
660
|
+
await this.sync('operation');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
#rebuildChildren() {
|
|
664
|
+
this.#children.clear();
|
|
665
|
+
for (const [path, entry] of this.#entries) {
|
|
666
|
+
if (path === '')
|
|
667
|
+
continue;
|
|
668
|
+
const parent = parentPath(path);
|
|
669
|
+
let children = this.#children.get(parent);
|
|
670
|
+
if (children === undefined) {
|
|
671
|
+
children = new Map();
|
|
672
|
+
this.#children.set(parent, children);
|
|
673
|
+
}
|
|
674
|
+
children.set(baseName(path), entry);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
#validateEssentials() {
|
|
678
|
+
const versionEntry = this.#entries.get('PG_VERSION');
|
|
679
|
+
if (versionEntry?.type !== 'file') {
|
|
680
|
+
throw corrupt(this.#name, 'is missing PG_VERSION');
|
|
681
|
+
}
|
|
682
|
+
const version = versionEntry.record;
|
|
683
|
+
const size = this.#recordSize(version);
|
|
684
|
+
if (size === 0 || size > 32)
|
|
685
|
+
throw corrupt(this.#name, 'contains an invalid PG_VERSION');
|
|
686
|
+
const bytes = new Uint8Array(size);
|
|
687
|
+
readComplete(version, bytes, 0);
|
|
688
|
+
let actual;
|
|
689
|
+
try {
|
|
690
|
+
actual = UTF8_DECODER.decode(bytes).trim();
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
throw corrupt(this.#name, 'contains a malformed PG_VERSION', error);
|
|
694
|
+
}
|
|
695
|
+
const expected = String(this.#physicalIdentity.postgresMajor);
|
|
696
|
+
if (actual !== expected) {
|
|
697
|
+
throw corrupt(this.#name, `contains PG_VERSION ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`);
|
|
698
|
+
}
|
|
699
|
+
const control = this.#entries.get('global/pg_control');
|
|
700
|
+
if (control?.type !== 'file') {
|
|
701
|
+
throw corrupt(this.#name, 'is missing global/pg_control');
|
|
702
|
+
}
|
|
703
|
+
if (this.#recordSize(control.record) === 0) {
|
|
704
|
+
throw corrupt(this.#name, 'contains an empty global/pg_control');
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
#closeHandles(flush) {
|
|
708
|
+
let failure;
|
|
709
|
+
for (const record of [...this.#records].sort(compareFlushOrder)) {
|
|
710
|
+
const access = record.access;
|
|
711
|
+
if (access === undefined)
|
|
712
|
+
continue;
|
|
713
|
+
try {
|
|
714
|
+
if (flush && record.dirty)
|
|
715
|
+
access.flush();
|
|
716
|
+
access.close();
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
failure = failure === undefined ? error : new AggregateError([failure, error]);
|
|
720
|
+
}
|
|
721
|
+
record.access = undefined;
|
|
722
|
+
}
|
|
723
|
+
if (failure !== undefined)
|
|
724
|
+
throw failure;
|
|
725
|
+
}
|
|
726
|
+
#assertOpen() {
|
|
727
|
+
if (this.#closed)
|
|
728
|
+
throw new Error(`OPFS pool ${JSON.stringify(this.#name)} is closed`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
export async function inspectPooledOpfsDatabase(database, name, physicalIdentity) {
|
|
732
|
+
const state = await readPoolState(database, name, physicalIdentity);
|
|
733
|
+
return inspectPoolState(database, name, physicalIdentity, state);
|
|
734
|
+
}
|
|
735
|
+
/** Reset only an unpublished first-open generation before portable hydration. */
|
|
736
|
+
export async function preparePooledOpfsDatabase(database, name, loadClusterSeed, physicalIdentity) {
|
|
737
|
+
let state = await readPoolState(database, name, physicalIdentity);
|
|
738
|
+
if (state?.phase === 'initializing') {
|
|
739
|
+
({ state } = await resetPoolClusterSeed(database, name, await loadClusterSeed(), assertWasixPhysicalIdentity(physicalIdentity)));
|
|
740
|
+
}
|
|
741
|
+
return inspectPoolState(database, name, physicalIdentity, state);
|
|
742
|
+
}
|
|
743
|
+
async function inspectPoolState(database, name, physicalIdentity, state) {
|
|
744
|
+
if (state === undefined)
|
|
745
|
+
return { snapshot: undefined, state: 'new' };
|
|
746
|
+
let data;
|
|
747
|
+
try {
|
|
748
|
+
data = await database.getDirectoryHandle(DATA_DIRECTORY);
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
if (errorName(error) === 'NotFoundError') {
|
|
752
|
+
throw corrupt(name, 'is missing its backing-file directory', error);
|
|
753
|
+
}
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
const entries = state.entries.filter((entry) => !VOLATILE_DATABASE_FILES.has(entry.path));
|
|
757
|
+
if (entries.length !== state.entries.length) {
|
|
758
|
+
await writePoolState(database, { ...state, entries });
|
|
759
|
+
await pruneUnreferencedData(data, entries).catch(() => undefined);
|
|
760
|
+
}
|
|
761
|
+
const directories = entries
|
|
762
|
+
.filter((entry) => entry.type === 'directory')
|
|
763
|
+
.map(({ path }) => path);
|
|
764
|
+
const storedFiles = entries.filter((entry) => entry.type === 'file');
|
|
765
|
+
const files = await parallelMap(storedFiles, async ({ path, backing }) => {
|
|
766
|
+
try {
|
|
767
|
+
return {
|
|
768
|
+
path,
|
|
769
|
+
bytes: new Uint8Array(await (await (await data.getFileHandle(backing)).getFile()).arrayBuffer()),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
if (errorName(error) === 'NotFoundError') {
|
|
774
|
+
throw corrupt(name, `is missing backing ${JSON.stringify(backing)} for ${JSON.stringify(path)}`, error);
|
|
775
|
+
}
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
return {
|
|
780
|
+
snapshot: validateStoredSnapshot({ schema: 'oliphaunt-wasix-directory-snapshot-v1', directories, files }, String(physicalIdentity.postgresMajor), {
|
|
781
|
+
label: poolLabel(name),
|
|
782
|
+
corrupt: (detail, cause) => corrupt(name, detail, cause),
|
|
783
|
+
}),
|
|
784
|
+
state: state.phase === 'ready' ? 'existing' : 'new',
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
export async function applyPooledOpfsDelta(database, name, physicalIdentity, delta) {
|
|
788
|
+
validateDeltaPaths(delta, name);
|
|
789
|
+
const data = await database.getDirectoryHandle(DATA_DIRECTORY, { create: true });
|
|
790
|
+
const existing = await readPoolState(database, name, physicalIdentity);
|
|
791
|
+
const entries = new Map(existing?.entries.map((entry) => [entry.path, entry]));
|
|
792
|
+
await pruneUnreferencedData(data, entries.values()).catch(() => undefined);
|
|
793
|
+
const deletes = splitStorageDeltaDeletes(delta);
|
|
794
|
+
for (const path of deletes.replacements)
|
|
795
|
+
removeStoredPath(entries, path);
|
|
796
|
+
for (const path of delta.directories)
|
|
797
|
+
entries.set(path, { path, type: 'directory' });
|
|
798
|
+
const files = [...delta.files].sort(comparePostgresPaths);
|
|
799
|
+
for (const { path, bytes } of files) {
|
|
800
|
+
const backing = newBackingName();
|
|
801
|
+
await writeAtomicDataFile(data, backing, bytes);
|
|
802
|
+
entries.set(path, { path, type: 'file', backing });
|
|
803
|
+
}
|
|
804
|
+
for (const path of deletes.removals)
|
|
805
|
+
removeStoredPath(entries, path);
|
|
806
|
+
validateEntryRelationships([...entries.values()], name);
|
|
807
|
+
await writePoolState(database, {
|
|
808
|
+
schema: STATE_SCHEMA,
|
|
809
|
+
name,
|
|
810
|
+
phase: 'ready',
|
|
811
|
+
physicalIdentity: assertWasixPhysicalIdentity(physicalIdentity),
|
|
812
|
+
entries: [...entries.values()].sort(compareStoredEntries),
|
|
813
|
+
});
|
|
814
|
+
await pruneUnreferencedData(data, entries.values()).catch(() => undefined);
|
|
815
|
+
}
|
|
816
|
+
export async function openPooledDatabaseDirectory(name) {
|
|
817
|
+
const root = await openPooledOpfsRoot();
|
|
818
|
+
return root.getDirectoryHandle(name, { create: true });
|
|
819
|
+
}
|
|
820
|
+
export async function openPooledOpfsRoot() {
|
|
821
|
+
const origin = await requireOpfsRoot();
|
|
822
|
+
return origin.getDirectoryHandle(OPFS_POOL_ROOT, { create: true });
|
|
823
|
+
}
|
|
824
|
+
async function initializePoolState(database, data, name, clusterSeed, physicalIdentity) {
|
|
825
|
+
const entries = [...new Set(clusterSeed.directories)].map((path) => ({
|
|
826
|
+
path,
|
|
827
|
+
type: 'directory',
|
|
828
|
+
}));
|
|
829
|
+
const files = await parallelMap(Object.entries(clusterSeed.files), async ([path, bytes]) => {
|
|
830
|
+
const backing = newBackingName();
|
|
831
|
+
await writeAtomicDataFile(data, backing, bytes);
|
|
832
|
+
return { path, type: 'file', backing };
|
|
833
|
+
});
|
|
834
|
+
entries.push(...files);
|
|
835
|
+
const state = {
|
|
836
|
+
schema: STATE_SCHEMA,
|
|
837
|
+
name,
|
|
838
|
+
phase: 'initializing',
|
|
839
|
+
physicalIdentity,
|
|
840
|
+
entries: entries.sort(compareStoredEntries),
|
|
841
|
+
};
|
|
842
|
+
await writePoolState(database, state);
|
|
843
|
+
return state;
|
|
844
|
+
}
|
|
845
|
+
async function resetPoolClusterSeed(database, name, clusterSeed, physicalIdentity) {
|
|
846
|
+
try {
|
|
847
|
+
await database.removeEntry(DATA_DIRECTORY, { recursive: true });
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
if (errorName(error) !== 'NotFoundError')
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
853
|
+
const data = await database.getDirectoryHandle(DATA_DIRECTORY, { create: true });
|
|
854
|
+
const state = await initializePoolState(database, data, name, clusterSeed, physicalIdentity);
|
|
855
|
+
return { data, state };
|
|
856
|
+
}
|
|
857
|
+
async function readPoolState(database, name, physicalIdentity) {
|
|
858
|
+
let handle;
|
|
859
|
+
try {
|
|
860
|
+
handle = await database.getFileHandle(STATE_FILE);
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
if (errorName(error) === 'NotFoundError') {
|
|
864
|
+
for await (const entry of database.keys()) {
|
|
865
|
+
if (entry !== DATA_DIRECTORY)
|
|
866
|
+
throw corrupt(name, 'contains storage without state metadata');
|
|
867
|
+
}
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
let parsed;
|
|
873
|
+
try {
|
|
874
|
+
parsed = parseJsonWithUniqueObjectKeys(await (await handle.getFile()).text());
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
throw corrupt(name, `has malformed state metadata: ${describeError(error)}`, error);
|
|
878
|
+
}
|
|
879
|
+
if (!isObject(parsed) ||
|
|
880
|
+
parsed.schema !== STATE_SCHEMA ||
|
|
881
|
+
parsed.name !== name ||
|
|
882
|
+
(parsed.phase !== 'initializing' && parsed.phase !== 'ready') ||
|
|
883
|
+
!Array.isArray(parsed.entries) ||
|
|
884
|
+
!hasExactKeys(parsed, ['entries', 'name', 'phase', 'physicalIdentity', 'schema'])) {
|
|
885
|
+
throw corrupt(name, 'has unsupported or mismatched state metadata');
|
|
886
|
+
}
|
|
887
|
+
let compatible;
|
|
888
|
+
try {
|
|
889
|
+
compatible = physicalIdentityMatches(parsed.physicalIdentity, physicalIdentity);
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
throw corrupt(name, `has malformed physical identity: ${describeError(error)}`, error);
|
|
893
|
+
}
|
|
894
|
+
if (!compatible) {
|
|
895
|
+
throw new WasixStorageError(`${poolLabel(name)} is incompatible with the selected WASIX runtime`, { code: 'incompatible', commitState: 'unchanged' });
|
|
896
|
+
}
|
|
897
|
+
const paths = new Set();
|
|
898
|
+
const backings = new Set();
|
|
899
|
+
const entries = parsed.entries.map((entry) => {
|
|
900
|
+
if (!isObject(entry) || typeof entry.path !== 'string') {
|
|
901
|
+
throw corrupt(name, 'contains a malformed state entry');
|
|
902
|
+
}
|
|
903
|
+
try {
|
|
904
|
+
validateStoredPath(entry.path);
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
throw corrupt(name, `contains invalid path ${JSON.stringify(entry.path)}`, error);
|
|
908
|
+
}
|
|
909
|
+
if (paths.has(entry.path))
|
|
910
|
+
throw corrupt(name, `repeats path ${JSON.stringify(entry.path)}`);
|
|
911
|
+
paths.add(entry.path);
|
|
912
|
+
if (entry.type === 'directory') {
|
|
913
|
+
if (!hasExactKeys(entry, ['path', 'type'])) {
|
|
914
|
+
throw corrupt(name, `contains a malformed entry for ${JSON.stringify(entry.path)}`);
|
|
915
|
+
}
|
|
916
|
+
return { path: entry.path, type: 'directory' };
|
|
917
|
+
}
|
|
918
|
+
if (entry.type !== 'file' || typeof entry.backing !== 'string') {
|
|
919
|
+
throw corrupt(name, `contains a malformed entry for ${JSON.stringify(entry.path)}`);
|
|
920
|
+
}
|
|
921
|
+
if (!hasExactKeys(entry, ['backing', 'path', 'type'])) {
|
|
922
|
+
throw corrupt(name, `contains a malformed entry for ${JSON.stringify(entry.path)}`);
|
|
923
|
+
}
|
|
924
|
+
try {
|
|
925
|
+
validateBackingName(entry.backing);
|
|
926
|
+
}
|
|
927
|
+
catch (error) {
|
|
928
|
+
throw corrupt(name, `contains invalid backing for ${JSON.stringify(entry.path)}`, error);
|
|
929
|
+
}
|
|
930
|
+
if (backings.has(entry.backing)) {
|
|
931
|
+
throw corrupt(name, `aliases backing file ${JSON.stringify(entry.backing)}`);
|
|
932
|
+
}
|
|
933
|
+
backings.add(entry.backing);
|
|
934
|
+
return { path: entry.path, type: 'file', backing: entry.backing };
|
|
935
|
+
});
|
|
936
|
+
validateEntryRelationships(entries, name);
|
|
937
|
+
return {
|
|
938
|
+
schema: STATE_SCHEMA,
|
|
939
|
+
name,
|
|
940
|
+
phase: parsed.phase,
|
|
941
|
+
physicalIdentity: parsed.physicalIdentity,
|
|
942
|
+
entries,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
async function writePoolState(database, state) {
|
|
946
|
+
const handle = await database.getFileHandle(STATE_FILE, { create: true });
|
|
947
|
+
const writable = await handle.createWritable({ keepExistingData: false });
|
|
948
|
+
try {
|
|
949
|
+
await writable.write(UTF8_ENCODER.encode(JSON.stringify(state)));
|
|
950
|
+
await writable.close();
|
|
951
|
+
}
|
|
952
|
+
catch (error) {
|
|
953
|
+
await writable.abort(error).catch(() => undefined);
|
|
954
|
+
throw error;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async function writeAtomicDataFile(data, backing, bytes) {
|
|
958
|
+
const handle = await data.getFileHandle(backing, { create: true });
|
|
959
|
+
const writable = await handle.createWritable({ keepExistingData: false });
|
|
960
|
+
try {
|
|
961
|
+
await writable.write(bytes);
|
|
962
|
+
await writable.close();
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
await writable.abort(error).catch(() => undefined);
|
|
966
|
+
throw error;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function serializePoolState(name, physicalIdentity, live, phase) {
|
|
970
|
+
const entries = [];
|
|
971
|
+
for (const [path, entry] of live) {
|
|
972
|
+
if (path === '')
|
|
973
|
+
continue;
|
|
974
|
+
if (entry.type === 'directory')
|
|
975
|
+
entries.push({ path, type: 'directory' });
|
|
976
|
+
else {
|
|
977
|
+
if (entry.record.backing === undefined)
|
|
978
|
+
throw new Error(`file ${path} is not materialized`);
|
|
979
|
+
entries.push({ path, type: 'file', backing: entry.record.backing });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
schema: STATE_SCHEMA,
|
|
984
|
+
name,
|
|
985
|
+
phase,
|
|
986
|
+
physicalIdentity,
|
|
987
|
+
entries: entries.sort(compareStoredEntries),
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function removeStoredPath(entries, path) {
|
|
991
|
+
for (const candidate of [...entries.keys()]) {
|
|
992
|
+
if (candidate === path || candidate.startsWith(`${path}/`))
|
|
993
|
+
entries.delete(candidate);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async function pruneUnreferencedData(data, entries) {
|
|
997
|
+
const referenced = new Set([...entries].flatMap((entry) => (entry.type === 'file' ? [entry.backing] : [])));
|
|
998
|
+
const removals = [];
|
|
999
|
+
for await (const backing of data.keys()) {
|
|
1000
|
+
if (!referenced.has(backing))
|
|
1001
|
+
removals.push(backing);
|
|
1002
|
+
}
|
|
1003
|
+
await parallelMap(removals, (backing) => data.removeEntry(backing));
|
|
1004
|
+
}
|
|
1005
|
+
function validateDeltaPaths(delta, name) {
|
|
1006
|
+
for (const path of [
|
|
1007
|
+
...delta.directories,
|
|
1008
|
+
...delta.files.map((file) => file.path),
|
|
1009
|
+
...delta.deleted,
|
|
1010
|
+
]) {
|
|
1011
|
+
try {
|
|
1012
|
+
validateStoredPath(path);
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
throw corrupt(name, `received invalid path ${JSON.stringify(path)}`, error);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function validateEntryRelationships(entries, name) {
|
|
1020
|
+
const byPath = new Map(entries.map((entry) => [entry.path, entry]));
|
|
1021
|
+
for (const entry of entries) {
|
|
1022
|
+
const parent = parentPath(entry.path);
|
|
1023
|
+
if (parent !== '' && byPath.get(parent)?.type !== 'directory') {
|
|
1024
|
+
throw corrupt(name, `is missing parent directory ${JSON.stringify(parent)}`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function validatePath(path, allowRoot = false) {
|
|
1029
|
+
const parts = segments(path);
|
|
1030
|
+
if (!allowRoot && parts.length === 0)
|
|
1031
|
+
throw invalid();
|
|
1032
|
+
for (const part of parts)
|
|
1033
|
+
validateDirectoryEntryName(part);
|
|
1034
|
+
}
|
|
1035
|
+
function validateStoredPath(path) {
|
|
1036
|
+
const parts = segments(path);
|
|
1037
|
+
if (parts.length === 0)
|
|
1038
|
+
throw new Error('empty stored path');
|
|
1039
|
+
for (const part of parts)
|
|
1040
|
+
validateDirectoryEntryName(part);
|
|
1041
|
+
}
|
|
1042
|
+
function segments(path) {
|
|
1043
|
+
if (path === '')
|
|
1044
|
+
return [];
|
|
1045
|
+
const parts = path.split('/');
|
|
1046
|
+
if (parts.some((part) => part.length === 0 || part === '.' || part === '..' || part.includes('\0'))) {
|
|
1047
|
+
throw invalid();
|
|
1048
|
+
}
|
|
1049
|
+
return parts;
|
|
1050
|
+
}
|
|
1051
|
+
function parentPath(path) {
|
|
1052
|
+
const index = path.lastIndexOf('/');
|
|
1053
|
+
return index < 0 ? '' : path.slice(0, index);
|
|
1054
|
+
}
|
|
1055
|
+
function baseName(path) {
|
|
1056
|
+
const index = path.lastIndexOf('/');
|
|
1057
|
+
return index < 0 ? path : path.slice(index + 1);
|
|
1058
|
+
}
|
|
1059
|
+
function newBackingName() {
|
|
1060
|
+
return `f-${crypto.randomUUID()}`;
|
|
1061
|
+
}
|
|
1062
|
+
function validateBackingName(name) {
|
|
1063
|
+
if (!/^f-[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(name)) {
|
|
1064
|
+
throw new Error(`invalid OPFS pool backing name ${JSON.stringify(name)}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
async function createAccess(file) {
|
|
1068
|
+
if (file.createSyncAccessHandle === undefined) {
|
|
1069
|
+
throw new DOMException('OPFS synchronous access is unavailable', 'NotSupportedError');
|
|
1070
|
+
}
|
|
1071
|
+
return file.createSyncAccessHandle();
|
|
1072
|
+
}
|
|
1073
|
+
function requireAccess(record) {
|
|
1074
|
+
if (record.access === undefined)
|
|
1075
|
+
throw new Error('OPFS pool record has no access handle');
|
|
1076
|
+
return record.access;
|
|
1077
|
+
}
|
|
1078
|
+
function readComplete(record, bytes, offset) {
|
|
1079
|
+
if (record.staged !== undefined) {
|
|
1080
|
+
bytes.set(record.staged.subarray(offset, offset + bytes.byteLength));
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const access = requireAccess(record);
|
|
1084
|
+
let read = 0;
|
|
1085
|
+
while (read < bytes.byteLength) {
|
|
1086
|
+
const count = access.read(bytes.subarray(read), { at: offset + read });
|
|
1087
|
+
validateProgress(count, bytes.byteLength - read, 'read');
|
|
1088
|
+
read += count;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function writeComplete(access, bytes, offset) {
|
|
1092
|
+
let written = 0;
|
|
1093
|
+
while (written < bytes.byteLength) {
|
|
1094
|
+
const count = access.write(bytes.subarray(written), { at: offset + written });
|
|
1095
|
+
validateProgress(count, bytes.byteLength - written, 'write');
|
|
1096
|
+
written += count;
|
|
1097
|
+
}
|
|
1098
|
+
access.truncate(offset + bytes.byteLength);
|
|
1099
|
+
}
|
|
1100
|
+
function isWalRecord(record) {
|
|
1101
|
+
return record.path?.startsWith('pg_wal/') === true;
|
|
1102
|
+
}
|
|
1103
|
+
function compareFlushOrder(left, right) {
|
|
1104
|
+
return (postgresRank(left.path ?? '') - postgresRank(right.path ?? '') ||
|
|
1105
|
+
(left.path ?? '').localeCompare(right.path ?? ''));
|
|
1106
|
+
}
|
|
1107
|
+
function comparePostgresPaths(left, right) {
|
|
1108
|
+
return postgresRank(left.path) - postgresRank(right.path) || left.path.localeCompare(right.path);
|
|
1109
|
+
}
|
|
1110
|
+
function postgresRank(path) {
|
|
1111
|
+
if (path.startsWith('pg_wal/'))
|
|
1112
|
+
return 0;
|
|
1113
|
+
if (path === 'global/pg_control')
|
|
1114
|
+
return 2;
|
|
1115
|
+
return 1;
|
|
1116
|
+
}
|
|
1117
|
+
function compareStoredEntries(left, right) {
|
|
1118
|
+
return left.path.localeCompare(right.path);
|
|
1119
|
+
}
|
|
1120
|
+
function encodeDirectoryPage(entries, start, capacity) {
|
|
1121
|
+
const selected = [];
|
|
1122
|
+
let length = 4;
|
|
1123
|
+
let next = start;
|
|
1124
|
+
while (next < entries.length) {
|
|
1125
|
+
const entry = entries[next];
|
|
1126
|
+
const name = UTF8_ENCODER.encode(entry.name);
|
|
1127
|
+
if (length + 5 + name.byteLength > capacity)
|
|
1128
|
+
break;
|
|
1129
|
+
selected.push({ entry, name });
|
|
1130
|
+
length += 5 + name.byteLength;
|
|
1131
|
+
next += 1;
|
|
1132
|
+
}
|
|
1133
|
+
if (next < entries.length && selected.length === 0) {
|
|
1134
|
+
throw new DOMException('directory entry exceeds the bridge capacity', 'QuotaExceededError');
|
|
1135
|
+
}
|
|
1136
|
+
const bytes = new Uint8Array(length);
|
|
1137
|
+
const view = new DataView(bytes.buffer);
|
|
1138
|
+
view.setUint32(0, selected.length, true);
|
|
1139
|
+
let offset = 4;
|
|
1140
|
+
for (const selectedEntry of selected) {
|
|
1141
|
+
bytes[offset] = selectedEntry.entry.type;
|
|
1142
|
+
view.setUint32(offset + 1, selectedEntry.name.byteLength, true);
|
|
1143
|
+
bytes.set(selectedEntry.name, offset + 5);
|
|
1144
|
+
offset += 5 + selectedEntry.name.byteLength;
|
|
1145
|
+
}
|
|
1146
|
+
return { bytes, next };
|
|
1147
|
+
}
|
|
1148
|
+
function classifyError(error) {
|
|
1149
|
+
switch (errorName(error)) {
|
|
1150
|
+
case 'NotFoundError':
|
|
1151
|
+
return RESULT.notFound;
|
|
1152
|
+
case 'TypeMismatchError':
|
|
1153
|
+
return RESULT.notFile;
|
|
1154
|
+
case 'NoModificationAllowedError':
|
|
1155
|
+
case 'NotAllowedError':
|
|
1156
|
+
case 'SecurityError':
|
|
1157
|
+
return RESULT.permission;
|
|
1158
|
+
case 'InvalidStateError':
|
|
1159
|
+
case 'DataError':
|
|
1160
|
+
case 'InvalidModificationError':
|
|
1161
|
+
return RESULT.invalid;
|
|
1162
|
+
case 'QuotaExceededError':
|
|
1163
|
+
return RESULT.storageFull;
|
|
1164
|
+
case 'NotSupportedError':
|
|
1165
|
+
return RESULT.unsupported;
|
|
1166
|
+
case 'TimeoutError':
|
|
1167
|
+
return RESULT.timeout;
|
|
1168
|
+
default:
|
|
1169
|
+
if (error instanceof ExistsError)
|
|
1170
|
+
return RESULT.exists;
|
|
1171
|
+
if (error instanceof DirectoryNotEmptyError)
|
|
1172
|
+
return RESULT.directoryNotEmpty;
|
|
1173
|
+
if (error instanceof NotDirectoryError)
|
|
1174
|
+
return RESULT.notDirectory;
|
|
1175
|
+
if (error instanceof NotFileError)
|
|
1176
|
+
return RESULT.notFile;
|
|
1177
|
+
return RESULT.io;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
class NotDirectoryError extends Error {
|
|
1181
|
+
}
|
|
1182
|
+
class NotFileError extends Error {
|
|
1183
|
+
}
|
|
1184
|
+
class DirectoryNotEmptyError extends Error {
|
|
1185
|
+
}
|
|
1186
|
+
class ExistsError extends Error {
|
|
1187
|
+
}
|
|
1188
|
+
function notDirectory() {
|
|
1189
|
+
return new NotDirectoryError();
|
|
1190
|
+
}
|
|
1191
|
+
function notFile() {
|
|
1192
|
+
return new NotFileError();
|
|
1193
|
+
}
|
|
1194
|
+
function notFound() {
|
|
1195
|
+
return new DOMException('entry not found', 'NotFoundError');
|
|
1196
|
+
}
|
|
1197
|
+
function invalid() {
|
|
1198
|
+
return new DOMException('invalid OPFS bridge input', 'DataError');
|
|
1199
|
+
}
|
|
1200
|
+
function validateProgress(transferred, available, operation) {
|
|
1201
|
+
if (!Number.isSafeInteger(transferred) ||
|
|
1202
|
+
transferred < 0 ||
|
|
1203
|
+
transferred > available ||
|
|
1204
|
+
(available > 0 && transferred === 0)) {
|
|
1205
|
+
throw new Error(`OPFS synchronous ${operation} made invalid progress`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function validateOffset(value) {
|
|
1209
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
1210
|
+
throw invalid();
|
|
1211
|
+
}
|
|
1212
|
+
async function requireOpfsRoot() {
|
|
1213
|
+
const storage = globalThis.navigator?.storage;
|
|
1214
|
+
if (storage?.getDirectory === undefined) {
|
|
1215
|
+
throw new WasixStorageError('OPFS is unavailable in this @oliphaunt/wasix-ts host', {
|
|
1216
|
+
code: 'unavailable',
|
|
1217
|
+
commitState: 'unchanged',
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
return storage.getDirectory();
|
|
1221
|
+
}
|
|
1222
|
+
async function parallelMap(values, operation) {
|
|
1223
|
+
const output = new Array(values.length);
|
|
1224
|
+
let cursor = 0;
|
|
1225
|
+
let failed = false;
|
|
1226
|
+
let failure;
|
|
1227
|
+
const worker = async () => {
|
|
1228
|
+
while (!failed && cursor < values.length) {
|
|
1229
|
+
const index = cursor++;
|
|
1230
|
+
try {
|
|
1231
|
+
output[index] = await operation(values[index]);
|
|
1232
|
+
}
|
|
1233
|
+
catch (error) {
|
|
1234
|
+
if (!failed) {
|
|
1235
|
+
failed = true;
|
|
1236
|
+
failure = error;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
await Promise.all(Array.from({ length: Math.min(MAX_PARALLEL_IO, values.length) }, worker));
|
|
1242
|
+
if (failed)
|
|
1243
|
+
throw failure;
|
|
1244
|
+
return output;
|
|
1245
|
+
}
|
|
1246
|
+
async function parallelCreate(count, operation) {
|
|
1247
|
+
return parallelMap(Array.from({ length: count }, (_, index) => index), operation);
|
|
1248
|
+
}
|
|
1249
|
+
function poolLabel(name) {
|
|
1250
|
+
return `OPFS storage ${JSON.stringify(name)}`;
|
|
1251
|
+
}
|
|
1252
|
+
function corrupt(name, detail, cause) {
|
|
1253
|
+
return new WasixStorageError(`${poolLabel(name)} ${detail}`, {
|
|
1254
|
+
code: 'corrupt',
|
|
1255
|
+
commitState: 'unchanged',
|
|
1256
|
+
...(cause === undefined ? {} : { cause }),
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
function isObject(value) {
|
|
1260
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1261
|
+
}
|
|
1262
|
+
function hasExactKeys(value, expected) {
|
|
1263
|
+
const keys = Object.keys(value).sort();
|
|
1264
|
+
return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
|
|
1265
|
+
}
|
|
1266
|
+
function errorName(error) {
|
|
1267
|
+
return typeof error === 'object' && error !== null && 'name' in error ? String(error.name) : '';
|
|
1268
|
+
}
|
|
1269
|
+
function describeError(error) {
|
|
1270
|
+
return error instanceof Error ? error.message : String(error);
|
|
1271
|
+
}
|