@moyodb/sdk 1.0.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +111 -28
  2. package/dist/assets/browser-worker-DBt51gsS.js +4 -0
  3. package/dist/assets/browser-worker-DBt51gsS.js.map +1 -0
  4. package/dist/assets/shared-worker-NdniRuEL.js +2 -0
  5. package/dist/assets/shared-worker-NdniRuEL.js.map +1 -0
  6. package/dist/browser-worker.d.ts +2 -0
  7. package/dist/browser-worker.d.ts.map +1 -0
  8. package/dist/change-events.d.ts +1 -1
  9. package/dist/chunks/indexing-CKtbpaZn.js +1657 -0
  10. package/dist/chunks/indexing-CKtbpaZn.js.map +1 -0
  11. package/dist/codec.d.ts +1 -1
  12. package/dist/compression.d.ts +16 -2
  13. package/dist/compression.d.ts.map +1 -1
  14. package/dist/demo.d.ts +2 -1
  15. package/dist/demo.d.ts.map +1 -1
  16. package/dist/engine/README.md +89 -16
  17. package/dist/engine/moyodb_engine.d.ts +34 -10
  18. package/dist/engine/moyodb_engine.js +238 -51
  19. package/dist/engine/moyodb_engine_bg.wasm +0 -0
  20. package/dist/engine/moyodb_engine_bg.wasm.d.ts +17 -10
  21. package/dist/engine/package.json +2 -2
  22. package/dist/errors.d.ts +3 -0
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/index.d.ts +11 -5
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/indexing.d.ts +1 -1
  27. package/dist/internal.d.ts +6 -0
  28. package/dist/internal.d.ts.map +1 -1
  29. package/dist/moyodb-sdk.js +2362 -640
  30. package/dist/moyodb-sdk.js.map +1 -1
  31. package/dist/node-engine.js +2838 -0
  32. package/dist/node-engine.js.map +1 -0
  33. package/dist/node-storage.d.mts +16 -0
  34. package/dist/node-storage.mjs +669 -0
  35. package/dist/node-worker-client.d.ts +20 -0
  36. package/dist/node-worker-client.d.ts.map +1 -0
  37. package/dist/node-worker.mjs +119 -0
  38. package/dist/node.d.ts +19 -0
  39. package/dist/node.d.ts.map +1 -0
  40. package/dist/node.js +3437 -0
  41. package/dist/node.js.map +1 -0
  42. package/dist/records.d.ts +75 -0
  43. package/dist/records.d.ts.map +1 -0
  44. package/dist/registry.d.ts +12 -4
  45. package/dist/registry.d.ts.map +1 -1
  46. package/dist/shared-worker-client.d.ts +28 -0
  47. package/dist/shared-worker-client.d.ts.map +1 -0
  48. package/dist/shared-worker-protocol.d.ts +16 -0
  49. package/dist/shared-worker-protocol.d.ts.map +1 -0
  50. package/dist/shared-worker.d.ts +33 -0
  51. package/dist/shared-worker.d.ts.map +1 -0
  52. package/dist/snappy.d.ts +3 -0
  53. package/dist/snappy.d.ts.map +1 -0
  54. package/dist/sql-parser.d.ts +9 -0
  55. package/dist/sql-parser.d.ts.map +1 -0
  56. package/dist/sql-types.d.ts +82 -0
  57. package/dist/sql-types.d.ts.map +1 -0
  58. package/dist/sql.d.ts +32 -0
  59. package/dist/sql.d.ts.map +1 -0
  60. package/dist/subscriptions.d.ts +2 -2
  61. package/dist/subscriptions.d.ts.map +1 -1
  62. package/dist/types.d.ts +36 -3
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/worker-api.d.ts +19 -3
  65. package/dist/worker-api.d.ts.map +1 -1
  66. package/dist/worker-client.d.ts +30 -6
  67. package/dist/worker-client.d.ts.map +1 -1
  68. package/dist/worker-protocol.d.ts +19 -4
  69. package/dist/worker-protocol.d.ts.map +1 -1
  70. package/dist/worker-server.d.ts +10 -2
  71. package/dist/worker-server.d.ts.map +1 -1
  72. package/dist/worker.d.ts +292 -1
  73. package/dist/worker.d.ts.map +1 -1
  74. package/package.json +13 -6
  75. package/dist/assets/worker-KDc8Khl_.js +0 -4
  76. package/dist/assets/worker-KDc8Khl_.js.map +0 -1
  77. package/dist/engine/snippets/moyodb-engine-cff0a0e2441a7a8f/js/opfs_shim.js +0 -513
  78. /package/dist/engine/snippets/{moyodb-engine-8ebe6af0cc2e81b7 → moyodb-engine-54a406e33b0fb849}/js/opfs_shim.js +0 -0
@@ -0,0 +1,16 @@
1
+ export const MISSING_DATABASE_MESSAGE: string;
2
+
3
+ export interface NodeStorageOptions {
4
+ lockToken?: string;
5
+ encodedDbName?: string;
6
+ createIfMissing?: boolean;
7
+ }
8
+
9
+ export interface NodeStorageHandle {
10
+ directory: string;
11
+ close(): void;
12
+ }
13
+
14
+ export function ensureStorageRoot(directory: string, createIfMissing?: boolean): string;
15
+ export function installNodeStorage(directory: string, options?: NodeStorageOptions): Promise<NodeStorageHandle>;
16
+ export function releaseNodeStorageLease(directory: string, lockToken: string): boolean;
@@ -0,0 +1,669 @@
1
+ import fs from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
4
+ import { isMainThread } from 'node:worker_threads';
5
+
6
+ const LOCK_DIRECTORY = '.moyodb.lock';
7
+ const PENDING_PREFIX = '.moyodb.pending-';
8
+ const TOKEN_PATTERN = /^[a-f\d]{8}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{4}-[a-f\d]{12}$/i;
9
+ // NativeFileBackend rejects a larger record without deleting it.
10
+ const MAX_LEASE_RECORD_BYTES = 1024;
11
+ const MAX_LEASE_PID = 0xffffffff;
12
+ export const MISSING_DATABASE_MESSAGE = 'database missing and create_if_missing=false';
13
+ let installedStorage = null;
14
+
15
+ function namedError(name, message, cause) {
16
+ const error = new Error(message, cause === undefined ? undefined : { cause });
17
+ error.name = name;
18
+ error.code = cause?.code ?? name;
19
+ return error;
20
+ }
21
+
22
+ function storageError(error) {
23
+ if (
24
+ ['NotFoundError', 'TypeMismatchError', 'SecurityError', 'DatabaseBusyError', 'StorageError'].includes(
25
+ error?.name
26
+ )
27
+ ) {
28
+ return error;
29
+ }
30
+ if (error?.code === 'ENOENT') return namedError('NotFoundError', error.message, error);
31
+ return namedError('StorageError', error?.message ?? String(error), error);
32
+ }
33
+
34
+ function validateDirectory(directory) {
35
+ if (typeof directory !== 'string' || directory.length === 0 || directory.includes('\0')) {
36
+ throw new TypeError('directory must be a nonempty filesystem path');
37
+ }
38
+ return resolve(directory);
39
+ }
40
+
41
+ function validateToken(token) {
42
+ if (typeof token !== 'string' || !TOKEN_PATTERN.test(token)) throw new TypeError('invalid storage lock token');
43
+ return token;
44
+ }
45
+
46
+ function validateName(name) {
47
+ if (
48
+ typeof name !== 'string' ||
49
+ name.length === 0 ||
50
+ name === '.' ||
51
+ name === '..' ||
52
+ name.toLowerCase() === LOCK_DIRECTORY ||
53
+ name.toLowerCase().startsWith(PENDING_PREFIX) ||
54
+ Array.from(name).some((character) => character.charCodeAt(0) < 32) ||
55
+ /[<>:"/\\|?*]/.test(name) ||
56
+ /[. ]$/.test(name) ||
57
+ /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name)
58
+ ) {
59
+ throw new TypeError('invalid storage entry name');
60
+ }
61
+ return name;
62
+ }
63
+
64
+ function entryStat(path, kind) {
65
+ const stat = fs.lstatSync(path);
66
+ if (stat.isSymbolicLink() || (stat.isFile() && stat.nlink !== 1)) {
67
+ throw namedError('SecurityError', 'storage entries must not be links');
68
+ }
69
+ if ((kind === 'directory' && !stat.isDirectory()) || (kind === 'file' && !stat.isFile())) {
70
+ throw namedError('TypeMismatchError', `storage entry is not a ${kind}`);
71
+ }
72
+ if (!stat.isDirectory() && !stat.isFile()) throw namedError('SecurityError', 'unsupported storage entry type');
73
+ return stat;
74
+ }
75
+
76
+ function syncDirectory(path) {
77
+ // Node cannot open directory descriptors on Windows. File flushes still use fsync.
78
+ if (process.platform === 'win32') return;
79
+ const fd = fs.openSync(path, fs.constants.O_RDONLY | (fs.constants.O_DIRECTORY ?? 0));
80
+ try {
81
+ fs.fsyncSync(fd);
82
+ } finally {
83
+ fs.closeSync(fd);
84
+ }
85
+ }
86
+
87
+ function readLease(directory, token) {
88
+ const lockDirectory = join(directory, LOCK_DIRECTORY);
89
+ entryStat(lockDirectory, 'directory');
90
+ const recordPath = join(lockDirectory, `${validateToken(token)}.json`);
91
+ const stat = entryStat(recordPath, 'file');
92
+ if (stat.size > MAX_LEASE_RECORD_BYTES) {
93
+ throw namedError('DatabaseBusyError', 'storage lock owner cannot be verified');
94
+ }
95
+ const owner = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
96
+ if (owner.token !== token || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || owner.pid > MAX_LEASE_PID) {
97
+ throw namedError('DatabaseBusyError', 'storage lock owner cannot be verified');
98
+ }
99
+ return { lockDirectory, recordPath, owner };
100
+ }
101
+
102
+ function removeLease(lease) {
103
+ // Remove only this token. A newer nonempty lease must survive cleanup.
104
+ fs.unlinkSync(lease.recordPath);
105
+ try {
106
+ fs.rmdirSync(lease.lockDirectory);
107
+ } catch (error) {
108
+ if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error?.code)) throw error;
109
+ }
110
+ syncDirectory(dirname(lease.lockDirectory));
111
+ }
112
+
113
+ function removeVacantLease(directory) {
114
+ const path = join(directory, LOCK_DIRECTORY);
115
+ try {
116
+ entryStat(path, 'directory');
117
+ if (fs.readdirSync(path).length !== 0) return false;
118
+ fs.rmdirSync(path);
119
+ syncDirectory(directory);
120
+ return true;
121
+ } catch (error) {
122
+ if (['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error?.code)) return false;
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ function publishLease(directory, pending) {
128
+ const lockDirectory = join(directory, LOCK_DIRECTORY);
129
+ for (let attempt = 0; attempt < 3; attempt += 1) {
130
+ let published = false;
131
+ try {
132
+ fs.renameSync(pending, lockDirectory);
133
+ published = true;
134
+ } catch (error) {
135
+ try {
136
+ fs.lstatSync(lockDirectory);
137
+ } catch {
138
+ throw storageError(error);
139
+ }
140
+ }
141
+ if (published) {
142
+ syncDirectory(directory);
143
+ return;
144
+ }
145
+ let lease;
146
+ try {
147
+ entryStat(lockDirectory, 'directory');
148
+ const records = fs.readdirSync(lockDirectory);
149
+ if (records.length === 0) {
150
+ removeVacantLease(directory);
151
+ continue;
152
+ }
153
+ if (records.length !== 1 || !records[0].endsWith('.json')) throw new Error('incomplete lock record');
154
+ lease = readLease(directory, records[0].slice(0, -5));
155
+ try {
156
+ process.kill(lease.owner.pid, 0);
157
+ } catch (failure) {
158
+ if (failure?.code === 'ESRCH') {
159
+ removeLease(lease);
160
+ continue;
161
+ }
162
+ throw failure;
163
+ }
164
+ } catch (failure) {
165
+ throw namedError('DatabaseBusyError', 'storage directory is locked; its owner cannot be released', failure);
166
+ }
167
+ throw namedError('DatabaseBusyError', `storage directory is owned by process ${lease.owner.pid}`);
168
+ }
169
+ throw namedError('DatabaseBusyError', 'storage directory changed while acquiring its lock');
170
+ }
171
+
172
+ function removePendingLease(directory, token) {
173
+ const pending = join(directory, `${PENDING_PREFIX}${process.pid}-${token}`);
174
+ try {
175
+ entryStat(pending, 'directory');
176
+ } catch (error) {
177
+ if (error?.code === 'ENOENT') return false;
178
+ throw error;
179
+ }
180
+ const entries = fs.readdirSync(pending);
181
+ if (entries.length === 1 && entries[0] === `${token}.json`) {
182
+ const recordPath = join(pending, entries[0]);
183
+ entryStat(recordPath, 'file');
184
+ fs.unlinkSync(recordPath);
185
+ } else if (entries.length !== 0) {
186
+ throw namedError('SecurityError', 'pending storage lock contains unknown entries');
187
+ }
188
+ fs.rmdirSync(pending);
189
+ return true;
190
+ }
191
+
192
+ function acquireLease(directory, token) {
193
+ const pending = join(directory, `${PENDING_PREFIX}${process.pid}-${token}`);
194
+ const recordPath = join(pending, `${token}.json`);
195
+ let fd;
196
+ let created = false;
197
+ try {
198
+ fs.mkdirSync(pending, { mode: 0o700 });
199
+ created = true;
200
+ fd = fs.openSync(recordPath, 'wx', 0o600);
201
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, token }));
202
+ fs.fsyncSync(fd);
203
+ fs.closeSync(fd);
204
+ fd = undefined;
205
+ syncDirectory(pending);
206
+ // Publish the complete owner record. Termination before rename cannot block a later owner.
207
+ publishLease(directory, pending);
208
+ } catch (error) {
209
+ const primary = storageError(error);
210
+ if (fd !== undefined) {
211
+ try {
212
+ fs.closeSync(fd);
213
+ } catch (cleanupError) {
214
+ primary.cleanupErrors = [cleanupError];
215
+ }
216
+ }
217
+ if (created) {
218
+ try {
219
+ releaseNodeStorageLease(directory, token);
220
+ } catch (cleanupError) {
221
+ primary.cleanupErrors = [...(primary.cleanupErrors ?? []), cleanupError];
222
+ }
223
+ }
224
+ throw primary;
225
+ }
226
+ }
227
+
228
+ export function releaseNodeStorageLease(directory, lockToken) {
229
+ const path = validateDirectory(directory);
230
+ validateToken(lockToken);
231
+ let lease;
232
+ let canonical;
233
+ try {
234
+ canonical = fs.realpathSync(path);
235
+ lease = readLease(canonical, lockToken);
236
+ } catch (error) {
237
+ if (error?.code === 'ENOTDIR') return false;
238
+ if (error?.code === 'ENOENT') {
239
+ if (!canonical) return false;
240
+ const removedPending = removePendingLease(canonical, lockToken);
241
+ return removeVacantLease(canonical) || removedPending;
242
+ }
243
+ throw storageError(error);
244
+ }
245
+ if (lease.owner.pid !== process.pid) return false;
246
+ try {
247
+ removeLease(lease);
248
+ removePendingLease(canonical, lockToken);
249
+ return true;
250
+ } catch (error) {
251
+ if (error?.code === 'ENOENT') return false;
252
+ throw storageError(error);
253
+ }
254
+ }
255
+
256
+ function validPosition(value, label) {
257
+ if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${label} must be a nonnegative safe integer`);
258
+ return value;
259
+ }
260
+
261
+ class AccessHandle {
262
+ #storage;
263
+ #path;
264
+ #fd;
265
+
266
+ constructor(storage, path, fd) {
267
+ this.#storage = storage;
268
+ this.#path = path;
269
+ this.#fd = fd;
270
+ }
271
+
272
+ #live() {
273
+ this.#storage.live();
274
+ if (this.#fd === null) throw namedError('InvalidStateError', 'storage access handle is closed');
275
+ return this.#fd;
276
+ }
277
+
278
+ read(buffer, { at = 0 } = {}) {
279
+ return fs.readSync(this.#live(), buffer, 0, buffer.byteLength, validPosition(at, 'read offset'));
280
+ }
281
+
282
+ write(buffer, { at = 0 } = {}) {
283
+ validPosition(at, 'write offset');
284
+ validPosition(at + buffer.byteLength, 'write end');
285
+ return fs.writeSync(this.#live(), buffer, 0, buffer.byteLength, at);
286
+ }
287
+
288
+ getSize() {
289
+ return validPosition(fs.fstatSync(this.#live()).size, 'file size');
290
+ }
291
+
292
+ truncate(size) {
293
+ fs.ftruncateSync(this.#live(), validPosition(size, 'file size'));
294
+ }
295
+
296
+ flush() {
297
+ fs.fsyncSync(this.#live());
298
+ }
299
+
300
+ close() {
301
+ if (this.#fd === null) return;
302
+ const fd = this.#fd;
303
+ this.#fd = null;
304
+ try {
305
+ fs.closeSync(fd);
306
+ } finally {
307
+ this.#storage.handles.delete(this.#path);
308
+ }
309
+ }
310
+ }
311
+
312
+ class FileHandle {
313
+ kind = 'file';
314
+
315
+ constructor(storage, path) {
316
+ this.storage = storage;
317
+ this.path = path;
318
+ this.name = basename(path);
319
+ }
320
+
321
+ async createSyncAccessHandle() {
322
+ this.storage.check(this.path, 'file');
323
+ if (this.storage.handles.has(this.path)) {
324
+ throw namedError('NoModificationAllowedError', 'storage file already has an access handle');
325
+ }
326
+ let fd;
327
+ try {
328
+ fd = fs.openSync(this.path, fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW ?? 0));
329
+ const stat = fs.fstatSync(fd);
330
+ const expected = this.storage.check(this.path, 'file');
331
+ if (!stat.isFile() || stat.nlink !== 1 || stat.dev !== expected.dev || stat.ino !== expected.ino) {
332
+ throw namedError('SecurityError', 'storage file changed while opening');
333
+ }
334
+ const handle = new AccessHandle(this.storage, this.path, fd);
335
+ this.storage.handles.set(this.path, handle);
336
+ return handle;
337
+ } catch (error) {
338
+ const primary = storageError(error);
339
+ if (fd !== undefined) {
340
+ try {
341
+ fs.closeSync(fd);
342
+ } catch (cleanupError) {
343
+ primary.cleanupErrors = [cleanupError];
344
+ }
345
+ }
346
+ throw primary;
347
+ }
348
+ }
349
+
350
+ async getFile() {
351
+ return { size: this.storage.check(this.path, 'file').size };
352
+ }
353
+ }
354
+
355
+ class DirectoryHandle {
356
+ kind = 'directory';
357
+
358
+ constructor(storage, path) {
359
+ this.storage = storage;
360
+ this.path = path;
361
+ this.name = basename(path);
362
+ }
363
+
364
+ async getDirectoryHandle(name, { create = false } = {}) {
365
+ this.storage.check(this.path, 'directory');
366
+ const path = join(this.path, validateName(name));
367
+ try {
368
+ if (create) {
369
+ try {
370
+ fs.mkdirSync(path, { mode: 0o700 });
371
+ syncDirectory(this.path);
372
+ } catch (error) {
373
+ if (error?.code !== 'EEXIST') throw error;
374
+ }
375
+ }
376
+ this.storage.check(path, 'directory');
377
+ return new DirectoryHandle(this.storage, path);
378
+ } catch (error) {
379
+ if (error?.name === 'TypeMismatchError' || error?.name === 'SecurityError') throw error;
380
+ throw storageError(error);
381
+ }
382
+ }
383
+
384
+ async getFileHandle(name, { create = false } = {}) {
385
+ this.storage.check(this.path, 'directory');
386
+ const path = join(this.path, validateName(name));
387
+ try {
388
+ if (create) {
389
+ let fd;
390
+ try {
391
+ fd = fs.openSync(path, 'wx', 0o600);
392
+ fs.fsyncSync(fd);
393
+ } catch (error) {
394
+ if (error?.code !== 'EEXIST') throw error;
395
+ } finally {
396
+ if (fd !== undefined) fs.closeSync(fd);
397
+ }
398
+ if (fd !== undefined) syncDirectory(this.path);
399
+ }
400
+ this.storage.check(path, 'file');
401
+ return new FileHandle(this.storage, path);
402
+ } catch (error) {
403
+ if (error?.name === 'TypeMismatchError' || error?.name === 'SecurityError') throw error;
404
+ throw storageError(error);
405
+ }
406
+ }
407
+
408
+ async *entries() {
409
+ this.storage.check(this.path, 'directory');
410
+ for (const name of fs.readdirSync(this.path).sort()) {
411
+ if (this.path === this.storage.directory && (name === LOCK_DIRECTORY || name.startsWith(PENDING_PREFIX)))
412
+ continue;
413
+ validateName(name);
414
+ const path = join(this.path, name);
415
+ const stat = this.storage.check(path);
416
+ yield [
417
+ name,
418
+ stat.isDirectory() ? new DirectoryHandle(this.storage, path) : new FileHandle(this.storage, path)
419
+ ];
420
+ }
421
+ }
422
+
423
+ async removeEntry(name, { recursive = false } = {}) {
424
+ this.storage.check(this.path, 'directory');
425
+ const path = join(this.path, validateName(name));
426
+ const stat = this.storage.check(path);
427
+ for (const openPath of this.storage.handles.keys()) {
428
+ if (openPath === path || openPath.startsWith(`${path}${sep}`)) {
429
+ throw namedError('NoModificationAllowedError', 'storage entry has an open access handle');
430
+ }
431
+ }
432
+ try {
433
+ if (stat.isDirectory()) {
434
+ if (recursive) this.storage.removeTree(path);
435
+ else fs.rmdirSync(path);
436
+ } else {
437
+ fs.unlinkSync(path);
438
+ }
439
+ syncDirectory(this.path);
440
+ } catch (error) {
441
+ if (error?.name === 'SecurityError') throw error;
442
+ throw storageError(error);
443
+ }
444
+ }
445
+ }
446
+
447
+ class Storage {
448
+ handles = new Map();
449
+ closed = false;
450
+
451
+ constructor(directory) {
452
+ this.directory = directory;
453
+ this.identity = entryStat(directory, 'directory');
454
+ }
455
+
456
+ live() {
457
+ if (this.closed) throw namedError('InvalidStateError', 'Node storage is closed');
458
+ }
459
+
460
+ check(path, kind) {
461
+ this.live();
462
+ const parts = relative(this.directory, path).split(sep).filter(Boolean);
463
+ if (parts.some((name) => name === '..')) throw namedError('SecurityError', 'storage path leaves its directory');
464
+ let current = this.directory;
465
+ let stat;
466
+ try {
467
+ stat = entryStat(current, 'directory');
468
+ if (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino) {
469
+ throw namedError('SecurityError', 'storage directory changed');
470
+ }
471
+ for (let index = 0; index < parts.length; index += 1) {
472
+ current = join(current, parts[index]);
473
+ stat = entryStat(current, index + 1 === parts.length ? kind : 'directory');
474
+ }
475
+ return stat;
476
+ } catch (error) {
477
+ if (error?.name === 'TypeMismatchError' || error?.name === 'SecurityError') throw error;
478
+ throw storageError(error);
479
+ }
480
+ }
481
+
482
+ removeTree(path) {
483
+ for (const name of fs.readdirSync(path)) {
484
+ const child = join(path, validateName(name));
485
+ const stat = this.check(child);
486
+ if (stat.isDirectory()) this.removeTree(child);
487
+ else fs.unlinkSync(child);
488
+ }
489
+ fs.rmdirSync(path);
490
+ }
491
+ }
492
+
493
+ class MountedDirectoryHandle {
494
+ kind = 'directory';
495
+
496
+ constructor(storage, name, childName, child, remove) {
497
+ this.storage = storage;
498
+ this.name = name;
499
+ this.childName = childName;
500
+ this.child = child;
501
+ this.remove = remove;
502
+ }
503
+
504
+ async getDirectoryHandle(name, { create = false } = {}) {
505
+ this.storage.live();
506
+ if (validateName(name) !== this.childName)
507
+ throw namedError('NotFoundError', 'directory is outside the database mount');
508
+ return this.child(create);
509
+ }
510
+
511
+ async getFileHandle(name) {
512
+ this.storage.live();
513
+ validateName(name);
514
+ if (name === this.childName) throw namedError('TypeMismatchError', 'mounted entry is a directory');
515
+ throw namedError('NotFoundError', 'file is outside the database mount');
516
+ }
517
+
518
+ async *entries() {
519
+ this.storage.live();
520
+ let child;
521
+ try {
522
+ child = this.child(false);
523
+ } catch (error) {
524
+ if (error?.name === 'NotFoundError') return;
525
+ throw error;
526
+ }
527
+ yield [this.childName, child];
528
+ }
529
+
530
+ async removeEntry(name, { recursive = false } = {}) {
531
+ this.storage.live();
532
+ if (validateName(name) !== this.childName)
533
+ throw namedError('NotFoundError', 'directory is outside the database mount');
534
+ if (!this.remove) throw namedError('NoModificationAllowedError', 'the storage mount cannot be removed');
535
+ await this.remove(recursive);
536
+ }
537
+ }
538
+
539
+ function mountedRoot(storage, encodedDbName) {
540
+ const database = new DirectoryHandle(storage, storage.directory);
541
+ database.name = encodedDbName;
542
+ let exists = true;
543
+ const stackdb = new MountedDirectoryHandle(
544
+ storage,
545
+ 'stackdb',
546
+ encodedDbName,
547
+ (create) => {
548
+ if (!exists && !create) throw namedError('NotFoundError', 'database directory does not exist');
549
+ exists = true;
550
+ return database;
551
+ },
552
+ async (recursive) => {
553
+ if (!exists) throw namedError('NotFoundError', 'database directory does not exist');
554
+ const names = [];
555
+ for await (const [name] of database.entries()) names.push(name);
556
+ if (!recursive && names.length !== 0)
557
+ throw namedError('InvalidModificationError', 'database directory is not empty');
558
+ for (const name of names) await database.removeEntry(name, { recursive });
559
+ // Keep the physical database directory and its lease until the worker stops.
560
+ exists = false;
561
+ }
562
+ );
563
+ return new MountedDirectoryHandle(storage, '', 'stackdb', () => stackdb);
564
+ }
565
+
566
+ export function ensureStorageRoot(directory, createIfMissing = true) {
567
+ const path = validateDirectory(directory);
568
+ if (!createIfMissing) {
569
+ try {
570
+ entryStat(path, 'directory');
571
+ } catch (error) {
572
+ if (error?.code === 'ENOENT') throw namedError('StorageError', MISSING_DATABASE_MESSAGE);
573
+ if (error?.name === 'TypeMismatchError' || error?.name === 'SecurityError') throw error;
574
+ throw storageError(error);
575
+ }
576
+ return path;
577
+ }
578
+ fs.mkdirSync(path, { recursive: true });
579
+ return path;
580
+ }
581
+
582
+ export async function installNodeStorage(
583
+ directory,
584
+ { lockToken = randomUUID(), encodedDbName, createIfMissing = true } = {}
585
+ ) {
586
+ if (isMainThread) throw new Error('Node storage must be installed in a dedicated worker');
587
+ if (installedStorage) throw new Error('Node storage is already installed in this worker');
588
+ const path = validateDirectory(directory);
589
+ validateToken(lockToken);
590
+ if (
591
+ encodedDbName !== undefined &&
592
+ (typeof encodedDbName !== 'string' || !/^(?:[a-f\d]{2})+$/.test(encodedDbName))
593
+ ) {
594
+ throw new TypeError('encodedDbName must be the lowercase hexadecimal UTF-8 database name');
595
+ }
596
+ if (createIfMissing === false) {
597
+ try {
598
+ entryStat(path, 'directory');
599
+ } catch (error) {
600
+ if (error?.code === 'ENOENT') throw namedError('StorageError', MISSING_DATABASE_MESSAGE);
601
+ if (error?.name === 'TypeMismatchError' || error?.name === 'SecurityError') throw error;
602
+ throw storageError(error);
603
+ }
604
+ }
605
+ let canonical;
606
+ try {
607
+ fs.mkdirSync(path, { recursive: true, mode: 0o700 });
608
+ canonical = fs.realpathSync(path);
609
+ entryStat(canonical, 'directory');
610
+ } catch (error) {
611
+ throw storageError(error);
612
+ }
613
+ acquireLease(canonical, lockToken);
614
+ const previousNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
615
+ let storage;
616
+ try {
617
+ storage = new Storage(canonical);
618
+ } catch (error) {
619
+ releaseNodeStorageLease(canonical, lockToken);
620
+ throw storageError(error);
621
+ }
622
+ let closed = false;
623
+ const close = () => {
624
+ if (closed) return;
625
+ closed = true;
626
+ let failure;
627
+ for (const handle of storage.handles.values()) {
628
+ try {
629
+ handle.close();
630
+ } catch (error) {
631
+ failure ??= error;
632
+ }
633
+ }
634
+ storage.closed = true;
635
+ try {
636
+ releaseNodeStorageLease(canonical, lockToken);
637
+ } catch (error) {
638
+ failure ??= error;
639
+ }
640
+ try {
641
+ if (previousNavigator) Object.defineProperty(globalThis, 'navigator', previousNavigator);
642
+ else delete globalThis.navigator;
643
+ } catch (error) {
644
+ failure ??= error;
645
+ }
646
+ installedStorage = null;
647
+ if (failure) throw failure;
648
+ };
649
+ try {
650
+ const root =
651
+ encodedDbName === undefined ? new DirectoryHandle(storage, canonical) : mountedRoot(storage, encodedDbName);
652
+ Object.defineProperty(globalThis, 'navigator', {
653
+ configurable: true,
654
+ value: {
655
+ storage: {
656
+ getDirectory: async () => {
657
+ storage.live();
658
+ return root;
659
+ }
660
+ }
661
+ }
662
+ });
663
+ installedStorage = storage;
664
+ return { directory: canonical, close };
665
+ } catch (error) {
666
+ close();
667
+ throw error;
668
+ }
669
+ }
@@ -0,0 +1,20 @@
1
+ import { WorkerProtocolClient } from './worker-client.js';
2
+ export interface NodeWorkerData {
3
+ directory: string;
4
+ dbName: string;
5
+ channelName: string;
6
+ lockToken: string;
7
+ ownerWaitMs: number;
8
+ encodedDbName: string;
9
+ createIfMissing: boolean;
10
+ }
11
+ export declare class NodeWorkerTransport {
12
+ #private;
13
+ readonly proxy: WorkerProtocolClient;
14
+ constructor(data: NodeWorkerData, releaseLease: () => void);
15
+ postMessage(message: unknown, transfer?: Transferable[]): void;
16
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject): void;
17
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject): void;
18
+ terminate(): Promise<number>;
19
+ }
20
+ //# sourceMappingURL=node-worker-client.d.ts.map