hyperdb-wasm 0.0.27136-dev.wasm.gd997ff1c → 0.0.27136-dev.wasm.ged3c25a2

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/dist/api.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface HyperDBClient {
12
12
  startQuery(connection: number, sql: string): Promise<WasmResult>;
13
13
  mount(source: MountSource, path: string, mode: MountAccessMode): Promise<WasmResult>;
14
14
  unmount(path: string): Promise<WasmResult>;
15
+ refreshMount(path: string): Promise<WasmResult>;
15
16
  pollQuery(query: number): Promise<WasmResult>;
16
17
  cancelQuery(query: number): Promise<WasmResult>;
17
18
  finishQuery(query: number): Promise<WasmResult>;
@@ -28,6 +29,7 @@ export interface MountOptions {
28
29
  }
29
30
  export interface MountedResource {
30
31
  readonly path: string;
32
+ refresh(): Promise<void>;
31
33
  close(): Promise<void>;
32
34
  }
33
35
  export declare function validateMountPath(path: unknown): asserts path is string;
@@ -97,7 +99,7 @@ export declare class HyperDB {
97
99
  static create(options?: BrowserCreateOptions): Promise<HyperDB>;
98
100
  ready(): Promise<void>;
99
101
  connect(options?: HyperDBConnectOptions): Promise<HyperDBConnection>;
100
- protected mountSource(source: MountSource, path: string, mode: MountAccessMode, cleanup?: () => void): Promise<MountedResource>;
102
+ protected mountSource(source: MountSource, path: string, mode: MountAccessMode): Promise<MountedResource>;
101
103
  terminate(): Promise<void>;
102
104
  private ensureRunning;
103
105
  protected runDatabaseOperation<Result>(operation: () => Promise<Result>): Promise<Result>;
package/dist/browser.js CHANGED
@@ -118,6 +118,9 @@ var init_client = __esm({
118
118
  unmount(path) {
119
119
  return this.request({ id: this.nextId++, type: "unmount", path });
120
120
  }
121
+ refreshMount(path) {
122
+ return this.request({ id: this.nextId++, type: "refreshMount", path });
123
+ }
121
124
  pollQuery(query) {
122
125
  return this.request({ id: this.nextId++, type: "pollQuery", query });
123
126
  }
@@ -498,17 +501,21 @@ var EngineQueryOperation = class {
498
501
  }
499
502
  };
500
503
  var MountedResourceImpl = class {
501
- constructor(path, unmount, cleanup) {
504
+ constructor(path, unmount, refreshMount) {
502
505
  this.path = path;
503
506
  this.unmount = unmount;
504
- this.cleanup = cleanup;
507
+ this.refreshMount = refreshMount;
505
508
  }
506
509
  path;
507
510
  unmount;
508
- cleanup;
511
+ refreshMount;
509
512
  closed = false;
510
513
  closing;
511
- cleaned = false;
514
+ async refresh() {
515
+ if (this.closed) throw new Error("mounted resource is closed");
516
+ if (this.closing) throw new Error("mounted resource is closing");
517
+ await this.refreshMount(this.path);
518
+ }
512
519
  async close() {
513
520
  if (this.closed) return;
514
521
  if (!this.closing) {
@@ -519,19 +526,13 @@ var MountedResourceImpl = class {
519
526
  this.closing = void 0;
520
527
  throw error;
521
528
  }
522
- this.release();
529
+ this.closed = true;
523
530
  })();
524
531
  }
525
532
  await this.closing;
526
533
  }
527
534
  releaseAfterShutdown() {
528
- this.release();
529
- }
530
- release() {
531
535
  this.closed = true;
532
- if (this.cleaned) return;
533
- this.cleaned = true;
534
- this.cleanup();
535
536
  }
536
537
  };
537
538
  var HyperDB = class _HyperDB {
@@ -571,13 +572,19 @@ var HyperDB = class _HyperDB {
571
572
  this.connections.add(result);
572
573
  return result;
573
574
  }
574
- mountSource(source, path, mode, cleanup = () => void 0) {
575
+ mountSource(source, path, mode) {
575
576
  return this.runDatabaseOperation(async () => {
576
577
  const result = await this.client.mount(source, path, mode);
577
578
  expectOk(result, "mount");
578
579
  const mountedPath = new TextDecoder("utf-8", { fatal: true }).decode(result.payload);
579
580
  if (mountedPath !== path) throw new Error("mount returned an invalid path");
580
- const resource = new MountedResourceImpl(mountedPath, (mounted) => this.unmount(mounted), cleanup);
581
+ const resource = new MountedResourceImpl(
582
+ mountedPath,
583
+ (mounted) => this.unmount(mounted),
584
+ (path2) => this.runDatabaseOperation(async () => {
585
+ expectOk(await this.client.refreshMount(path2), "refresh mount");
586
+ })
587
+ );
581
588
  this.mountedResources.add(resource);
582
589
  return resource;
583
590
  });
@@ -845,13 +852,7 @@ var HyperDB2 = class _HyperDB extends HyperDB {
845
852
  throw new Error("Mounted file name must be a basename without path separators");
846
853
  }
847
854
  if (!Number.isSafeInteger(source.size) || source.size < 0) throw new Error("Mounted file size must be a nonnegative safe integer");
848
- const url = URL.createObjectURL(source);
849
- try {
850
- return await this.mountSource({ kind: "browser-file", name: source.name, url, size: source.size }, path, "read", () => URL.revokeObjectURL(url));
851
- } catch (error) {
852
- URL.revokeObjectURL(url);
853
- throw error;
854
- }
855
+ return this.mountSource({ kind: "browser-file", file: source }, path, "read");
855
856
  }
856
857
  const handle = source;
857
858
  const mode = options.mode ?? "read";
@@ -8,22 +8,27 @@ export declare class BrowserExternalDirectoryBackend implements ExternalDirector
8
8
  private readonly paths;
9
9
  private readonly files;
10
10
  private readonly writers;
11
+ registerFile(file: unknown): {
12
+ readonly locator: string;
13
+ readonly size: number;
14
+ };
11
15
  register(handle: unknown, mode: 'read' | 'readwrite'): {
12
16
  readonly name: string;
13
17
  readonly locator: string;
14
18
  };
15
19
  registered(path: string, locator: string): void;
20
+ releaseGeneration(locator: string, generation: string): void;
16
21
  removed(pathOrLocator: string): void;
17
22
  list(locator: string): Promise<readonly string[]>;
18
23
  listEntries(locator: string, path: string): Promise<readonly BrowserDirectoryEntry[]>;
19
- lookup(locator: string, path: string): Promise<{
24
+ lookup(locator: string, path: string, generation: string): Promise<{
20
25
  readonly kind: 'directory';
21
26
  } | {
22
27
  readonly kind: 'file';
23
28
  readonly locator: string;
24
29
  readonly size: number;
25
30
  } | undefined>;
26
- open(locator: string, path: string): Promise<{
31
+ open(locator: string, path: string, generation: string): Promise<{
27
32
  readonly locator: string;
28
33
  readonly size: number;
29
34
  }>;
@@ -61,6 +61,7 @@
61
61
  registerExternalFile;
62
62
  registerExternalDirectory;
63
63
  removeExternalRegistration;
64
+ refreshExternalDirectory;
64
65
  projectExternalFile;
65
66
  projectExternalDirectory;
66
67
  startQuery;
@@ -71,6 +72,7 @@
71
72
  resultPayloadLength;
72
73
  free;
73
74
  mountedPaths = /* @__PURE__ */ new Set();
75
+ mountedDirectoryPaths = /* @__PURE__ */ new Set();
74
76
  constructor(module) {
75
77
  this.module = module;
76
78
  this.initialize = module.cwrap("hyperdb_wasm_initialize", "pointer", ["string", "string", "number"]);
@@ -80,6 +82,7 @@
80
82
  this.registerExternalFile = module.cwrap("hyperdb_wasm_register_external_file", "pointer", ["string", "string", "number", "number"]);
81
83
  this.registerExternalDirectory = module.cwrap("hyperdb_wasm_register_external_directory", "pointer", ["string", "string", "number", "number"]);
82
84
  this.removeExternalRegistration = module.cwrap("hyperdb_wasm_remove_external_registration", "pointer", ["string", "number"]);
85
+ this.refreshExternalDirectory = module.cwrap("hyperdb_wasm_refresh_external_directory", "pointer", ["string", "number"]);
83
86
  this.projectExternalFile = module.cwrap("hyperdb_wasm_project_external_file", "pointer", ["string", "string", "number", "number"]);
84
87
  this.projectExternalDirectory = module.cwrap("hyperdb_wasm_project_external_directory", "pointer", ["string", "string", "number"]);
85
88
  this.startQuery = module.cwrap("hyperdb_wasm_query_start", "pointer", ["number", "string", "number", "number"]);
@@ -99,20 +102,32 @@
99
102
  if (registration.kind === "directory") return this.mountDirectory(request, request.source.path);
100
103
  return this.mountFile(request, registration.locator, registration.size, this.module.externalFileBackend);
101
104
  }
102
- if (request.type === "mount" && request.source.kind === "browser-file" && !this.module.externalFileBackendAvailable) {
103
- return { response: { id: request.id, type: "response", result: { state: "error", error: "Blob external files are supported only by the browser client" } }, transfer: [] };
104
- }
105
105
  if (request.type === "mount" && request.source.kind === "browser-directory") {
106
106
  return this.mountDirectory(request, request.source.handle);
107
107
  }
108
108
  if (request.type === "mount" && request.source.kind === "browser-file") {
109
- return this.mountFile(request, request.source.url, safeInteger(request.source.size, "external file size"));
109
+ validateCString(request.path, "external registration path");
110
+ const backend = this.module.externalDirectoryBackend;
111
+ if (!backend?.registerFile) {
112
+ return { response: { id: request.id, type: "response", result: { state: "error", error: "Browser external files are supported only by the browser client" } }, transfer: [] };
113
+ }
114
+ const file = backend.registerFile(request.source.file);
115
+ return this.mountFile(request, file.locator, file.size, backend);
116
+ }
117
+ if (request.type === "refreshMount") {
118
+ validateCString(request.path, "external registration path");
119
+ if (!this.module.externalDirectoryBackend) return { response: { id: request.id, type: "response", result: { state: "error", error: "External directories are not supported by this client" } }, transfer: [] };
120
+ if (!this.mountedDirectoryPaths.has(request.path)) {
121
+ const error = this.mountedPaths.has(request.path) ? "Only directory mounts can be refreshed" : "External directory registration was not found";
122
+ return { response: { id: request.id, type: "response", result: { state: "error", error } }, transfer: [] };
123
+ }
110
124
  }
111
125
  if (request.type === "unmount" && (this.module.externalFileBackend || this.module.externalDirectoryBackend)) {
112
126
  validateCString(request.path, "external registration path");
113
127
  const pointer2 = safeInteger(this.removeExternalRegistration(request.path, request.id), "result pointer");
114
128
  const result2 = this.decodeResult(pointer2);
115
129
  if (result2.state === "ok" || result2.state === "error" && result2.error === "External registration was not found" && this.mountedPaths.has(request.path)) {
130
+ this.mountedDirectoryPaths.delete(request.path);
116
131
  const cleanup = async () => {
117
132
  await Promise.all([
118
133
  this.module.externalFileBackend?.removed(request.path),
@@ -124,12 +139,14 @@
124
139
  }
125
140
  return { response: { id: request.id, type: "response", result: result2 }, transfer: [] };
126
141
  }
127
- if (request.type === "unmount" && !this.module.externalFileBackendAvailable) {
142
+ if (request.type === "unmount") {
128
143
  return { response: { id: request.id, type: "response", result: { state: "error", error: "External files are not supported by this client" } }, transfer: [] };
129
144
  }
130
145
  const pointer = safeInteger(this.invoke(request), "result pointer");
131
146
  const result = this.decodeResult(pointer);
132
147
  if (request.type === "shutdown" && result.state === "ok") {
148
+ this.mountedPaths.clear();
149
+ this.mountedDirectoryPaths.clear();
133
150
  const completion = completionFor([
134
151
  this.module.externalFileBackend?.shutdown(),
135
152
  this.module.externalDirectoryBackend?.shutdown()
@@ -164,6 +181,8 @@
164
181
  case "unmount":
165
182
  validateCString(request.path, "external registration path");
166
183
  return this.removeExternalRegistration(request.path, request.id);
184
+ case "refreshMount":
185
+ return this.refreshExternalDirectory(request.path, request.id);
167
186
  case "startQuery":
168
187
  validateCStringContents(request.sql, "SQL");
169
188
  return this.startQuery(request.connection, request.sql, Number(request.sqlTestMode ?? false), request.id);
@@ -184,13 +203,13 @@
184
203
  try {
185
204
  const result = this.decodeResult(safeInteger(this.registerExternalFile(request.path, locator, size, request.id), "result pointer"));
186
205
  if (result.state !== "ok") {
187
- backend?.removed(locator);
188
- return { response: { id: request.id, type: "response", result }, transfer: [] };
206
+ const completion = completionFor([backend.removed(locator)]);
207
+ return { response: { id: request.id, type: "response", result }, transfer: [], ...completion && { completion } };
189
208
  }
190
209
  path = new TextDecoder("utf-8", { fatal: true }).decode(result.payload);
191
210
  const projection = this.decodeResult(safeInteger(this.projectExternalFile(path, locator, size, request.id), "result pointer"));
192
211
  if (projection.state !== "ok") throw new Error(resultError(projection));
193
- backend?.registered(path, locator);
212
+ backend.registered(path, locator);
194
213
  registered = true;
195
214
  this.mountedPaths.add(path);
196
215
  return { response: { id: request.id, type: "response", result }, transfer: [] };
@@ -205,12 +224,15 @@
205
224
  }
206
225
  if (rollbackError !== void 0) {
207
226
  if (path !== void 0) {
208
- backend?.registered(path, locator);
227
+ backend.registered(path, locator);
209
228
  this.mountedPaths.add(path);
210
229
  }
211
230
  throw new Error(`mount failed and rollback also failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, { cause: error });
212
231
  }
213
- backend?.removed(registered && path !== void 0 ? path : locator);
232
+ const completion = completionFor([backend.removed(registered && path !== void 0 ? path : locator)]);
233
+ if (completion) {
234
+ return { response: { id: request.id, type: "response", result: { state: "error", error: error instanceof Error ? error.message : String(error) } }, transfer: [], completion };
235
+ }
214
236
  throw error;
215
237
  }
216
238
  }
@@ -234,12 +256,14 @@
234
256
  if (projection.state !== "ok") throw new Error(resultError(projection));
235
257
  backend.registered(path, registration.locator);
236
258
  this.mountedPaths.add(path);
259
+ this.mountedDirectoryPaths.add(path);
237
260
  } catch (error) {
238
261
  const cleanupPointer = safeInteger(this.removeExternalRegistration(path, request.id), "result pointer");
239
262
  const cleanup = this.decodeResult(cleanupPointer);
240
263
  if (cleanup.state !== "ok") {
241
264
  backend.registered(path, registration.locator);
242
265
  this.mountedPaths.add(path);
266
+ this.mountedDirectoryPaths.add(path);
243
267
  preserveBackend = true;
244
268
  throw new Error(`mount failed and rollback also failed: ${resultError(cleanup)}`, { cause: error });
245
269
  }
@@ -333,7 +357,7 @@
333
357
  if (!hasKeys(value, ["id", "type", "source", "path", "mode"]) || typeof value.path !== "string" || !value.path.startsWith("/mnt/external/") || new TextEncoder().encode(value.path.slice("/mnt/external/".length)).byteLength === 0 || new TextEncoder().encode(value.path.slice("/mnt/external/".length)).byteLength > 200 || value.path.slice("/mnt/external/".length).split("/").some((component) => !component || component === "." || component === ".." || !/^[A-Za-z0-9._-]+$/.test(component)) || value.mode !== "read" && value.mode !== "readwrite" || !isRecord(value.source)) return false;
334
358
  switch (value.source.kind) {
335
359
  case "browser-file":
336
- return value.mode === "read" && hasKeys(value.source, ["kind", "name", "url", "size"]) && typeof value.source.name === "string" && typeof value.source.url === "string" && typeof value.source.size === "number" && Number.isSafeInteger(value.source.size) && value.source.size >= 0;
360
+ return value.mode === "read" && hasKeys(value.source, ["kind", "file"]) && typeof File !== "undefined" && value.source.file instanceof File;
337
361
  case "browser-directory":
338
362
  return hasKeys(value.source, ["kind", "handle"]) && isRecord(value.source.handle) && value.source.handle.kind === "directory";
339
363
  case "node-path":
@@ -343,6 +367,7 @@
343
367
  }
344
368
  }
345
369
  case "unmount":
370
+ case "refreshMount":
346
371
  return hasKeys(value, ["id", "type", "path"]) && typeof value.path === "string" && !!value.path && !value.path.includes("\0");
347
372
  case "pollQuery":
348
373
  case "cancelQuery":
@@ -440,9 +465,14 @@
440
465
  };
441
466
  module.hyperdbWasmFetchCancel = (id) => fetches.get(id)?.abort();
442
467
  }
443
- function installExternalDirectoryRuntime(module) {
468
+ function installExternalDirectoryRuntime(module, transport) {
444
469
  const backend = module.externalDirectoryBackend;
445
470
  if (!backend) return;
471
+ module.hyperdbWasmExternalDirectoryReleaseGeneration = (locator, generation) => {
472
+ void Promise.resolve().then(() => backend.releaseGeneration(locator, generation)).catch(() => {
473
+ transport.postMessage({ type: "log", text: "External directory generation cleanup failed; mount removal will retry" });
474
+ });
475
+ };
446
476
  const listComplete = module._hyperdb_wasm_external_directory_list_complete;
447
477
  const openComplete = module._hyperdb_wasm_external_directory_open_complete;
448
478
  if (listComplete) {
@@ -455,8 +485,8 @@
455
485
  };
456
486
  }
457
487
  if (openComplete) {
458
- module.hyperdbWasmExternalDirectoryOpenStart = (id, locator, path) => {
459
- void backend.open(locator, path).then((file) => {
488
+ module.hyperdbWasmExternalDirectoryOpenStart = (id, locator, path, generation) => {
489
+ void backend.open(locator, path, generation).then((file) => {
460
490
  openComplete(id, safePointer(module.stringToNewUTF8(file.locator), "external file locator pointer"), file.size);
461
491
  }).catch((error) => openComplete(id, 0, isNotFoundError(error) ? -2 : -1));
462
492
  };
@@ -545,8 +575,8 @@
545
575
  complete(id, safePointer(module.stringToNewUTF8(JSON.stringify(encoded)), "filesystem listing pointer"), 0);
546
576
  }).catch(() => complete(id, 0, -1));
547
577
  };
548
- module.hyperdbWasmFilesystemLookupStart = (id, locator, path) => {
549
- const lookup = module.externalDirectoryBackend?.lookup?.(locator, path);
578
+ module.hyperdbWasmFilesystemLookupStart = (id, locator, path, generation) => {
579
+ const lookup = module.externalDirectoryBackend?.lookup?.(locator, path, generation);
550
580
  if (!lookup) {
551
581
  complete(id, 0, -1);
552
582
  return;
@@ -558,13 +588,7 @@
558
588
  }).catch(() => complete(id, 0, -1));
559
589
  };
560
590
  module.hyperdbWasmFilesystemReadStart = (id, locator, offset, length, expectedSize, pointerValue) => {
561
- const read = module.externalFileBackend?.read(locator, offset, length, expectedSize) ?? module.externalDirectoryBackend?.read(locator, offset, length, expectedSize) ?? (locator.startsWith("blob:") ? fetch(locator, { headers: { Range: `bytes=${offset}-${offset + length - 1}` } }).then(async (response) => {
562
- const contentRange = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(response.headers.get("content-range") ?? "");
563
- if (response.status !== 206 || !contentRange || contentRange[1] !== String(offset) || contentRange[2] !== String(offset + length - 1) || contentRange[3] !== String(expectedSize)) {
564
- throw new Error("Blob URL returned an invalid byte range");
565
- }
566
- return new Uint8Array(await response.arrayBuffer());
567
- }) : void 0);
591
+ const read = module.externalFileBackend?.read(locator, offset, length, expectedSize) ?? module.externalDirectoryBackend?.read(locator, offset, length, expectedSize);
568
592
  if (!read) {
569
593
  complete(id, 0, -1);
570
594
  return;
@@ -611,7 +635,7 @@
611
635
  configureFilesystem(module);
612
636
  installHttpRuntime(module, transport);
613
637
  installHostFileRuntime(module);
614
- installExternalDirectoryRuntime(module);
638
+ installExternalDirectoryRuntime(module, transport);
615
639
  installFilesystemProjectionRuntime(module);
616
640
  bindings = new HyperDBBindings(module);
617
641
  transport.postMessage({ type: "ready" });
@@ -660,27 +684,51 @@
660
684
  paths = /* @__PURE__ */ new Map();
661
685
  files = /* @__PURE__ */ new Map();
662
686
  writers = /* @__PURE__ */ new Map();
687
+ registerFile(file) {
688
+ if (typeof file !== "object" || file === null || !("size" in file) || typeof file.size !== "number" || !("slice" in file) || typeof file.slice !== "function") {
689
+ throw new Error("External file is invalid");
690
+ }
691
+ nonnegativeSafeInteger(file.size, "External file size");
692
+ const locator = `${fileLocatorPrefix}${opaqueId()}`;
693
+ this.files.set(locator, file);
694
+ return { locator, size: file.size };
695
+ }
663
696
  register(handle, mode) {
664
697
  const directoryHandle = asDirectoryHandle(handle);
665
698
  validateSegment(directoryHandle.name);
666
699
  const locator = `${directoryLocatorPrefix}${opaqueId()}`;
667
- this.directories.set(locator, { handle: directoryHandle, mode, files: /* @__PURE__ */ new Map(), locators: /* @__PURE__ */ new Set() });
700
+ this.directories.set(locator, { handle: directoryHandle, mode, generations: /* @__PURE__ */ new Map(), locators: /* @__PURE__ */ new Set() });
668
701
  return { name: directoryHandle.name, locator };
669
702
  }
670
703
  registered(path, locator) {
671
- if (!this.directories.has(locator)) throw new Error("External directory registration is unavailable");
704
+ if (!this.directories.has(locator) && !this.files.has(locator)) throw new Error("External registration is unavailable");
672
705
  this.paths.set(path, locator);
673
706
  }
707
+ releaseGeneration(locator, generation) {
708
+ const directory = this.directories.get(locator);
709
+ const state = directory?.generations.get(generation);
710
+ if (!directory || !state) return;
711
+ directory.generations.delete(generation);
712
+ for (const fileLocator of state.locators) {
713
+ this.files.delete(fileLocator);
714
+ directory.locators.delete(fileLocator);
715
+ }
716
+ }
674
717
  removed(pathOrLocator) {
675
718
  const locator = this.paths.get(pathOrLocator) ?? pathOrLocator;
676
719
  const directory = this.directories.get(locator);
677
- if (!directory) return;
678
- for (const fileLocator of directory.locators) this.files.delete(fileLocator);
679
- for (const [writer, state] of this.writers) {
680
- if (state.locator === locator) void this.abort(writer).catch(() => void 0);
720
+ if (directory) {
721
+ for (const fileLocator of directory.locators) this.files.delete(fileLocator);
722
+ for (const [writer, state] of this.writers) {
723
+ if (state.locator === locator) void this.abort(writer).catch(() => void 0);
724
+ }
725
+ this.directories.delete(locator);
726
+ } else {
727
+ this.files.delete(locator);
728
+ }
729
+ for (const [path, registeredLocator] of this.paths) {
730
+ if (registeredLocator === locator) this.paths.delete(path);
681
731
  }
682
- this.directories.delete(locator);
683
- this.paths.delete(pathOrLocator);
684
732
  }
685
733
  async list(locator) {
686
734
  const directory = this.getDirectory(locator);
@@ -709,7 +757,7 @@
709
757
  entries.sort((left, right) => left.name.localeCompare(right.name));
710
758
  return entries;
711
759
  }
712
- async lookup(locator, path) {
760
+ async lookup(locator, path, generation) {
713
761
  const directory = this.getDirectory(locator);
714
762
  const segments = relativeSegments(path);
715
763
  const parent = await this.openDirectory(directory, segments.slice(0, -1));
@@ -719,14 +767,19 @@
719
767
  validateSegment(child.name);
720
768
  if (child.name !== name) continue;
721
769
  if (child.kind === "directory") return { kind: "directory" };
722
- if (child.kind === "file") return { kind: "file", ...await this.open(locator, path) };
770
+ if (child.kind === "file") return { kind: "file", ...await this.open(locator, path, generation) };
723
771
  throw new Error("External directory contains an unsupported entry");
724
772
  }
725
773
  return void 0;
726
774
  }
727
- async open(locator, path) {
775
+ async open(locator, path, generation) {
728
776
  const directory = this.getDirectory(locator);
729
- const cached = directory.files.get(path);
777
+ let state = directory.generations.get(generation);
778
+ if (!state) {
779
+ state = { files: /* @__PURE__ */ new Map(), locators: /* @__PURE__ */ new Set() };
780
+ directory.generations.set(generation, state);
781
+ }
782
+ const cached = state.files.get(path);
730
783
  if (cached) {
731
784
  const file = await cached;
732
785
  return { locator: file.locator, size: file.file.size };
@@ -744,25 +797,24 @@
744
797
  const fileHandle = await parent.getFileHandle(name);
745
798
  if (fileHandle.kind !== "file" || fileHandle.name !== name) throw new Error("External directory returned an unexpected file handle");
746
799
  const file = await fileHandle.getFile();
747
- if (!Number.isSafeInteger(file.size) || file.size < 0) throw new Error("External file size must be a nonnegative safe integer");
748
- const fileLocator = `${fileLocatorPrefix}${opaqueId()}`;
749
- this.files.set(fileLocator, file);
800
+ const { locator: fileLocator } = this.registerFile(file);
750
801
  directory.locators.add(fileLocator);
802
+ state.locators.add(fileLocator);
751
803
  return { locator: fileLocator, file };
752
804
  })();
753
- directory.files.set(path, snapshot);
805
+ state.files.set(path, snapshot);
754
806
  try {
755
807
  const file = await snapshot;
756
808
  return { locator: file.locator, size: file.file.size };
757
809
  } catch (error) {
758
- directory.files.delete(path);
810
+ if (state.files.get(path) === snapshot) state.files.delete(path);
759
811
  throw error;
760
812
  }
761
813
  }
762
814
  read(locator, offset, length, expectedSize) {
763
815
  if (!locator.startsWith(fileLocatorPrefix)) return void 0;
764
816
  const file = this.files.get(locator);
765
- if (!file) return Promise.reject(new Error("External directory file registration does not exist"));
817
+ if (!file) return Promise.reject(new Error("External file registration does not exist"));
766
818
  const end = offset + length;
767
819
  if (expectedSize !== file.size || !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0 || !Number.isSafeInteger(end) || end > file.size) {
768
820
  return Promise.reject(new Error("External file byte range is invalid"));
@@ -847,7 +899,7 @@
847
899
  await this.cleanupWriter(state).catch(() => void 0);
848
900
  throw failure;
849
901
  }
850
- this.getDirectory(state.locator).files.delete(state.path);
902
+ for (const generation of this.getDirectory(state.locator).generations.values()) generation.files.delete(state.path);
851
903
  }, true);
852
904
  return operation.finally(() => this.writers.delete(writer));
853
905
  }
@@ -889,7 +941,7 @@
889
941
  return state;
890
942
  }
891
943
  async cleanupWriter(state) {
892
- this.directories.get(state.locator)?.files.delete(state.path);
944
+ for (const generation of this.directories.get(state.locator)?.generations.values() ?? []) generation.files.delete(state.path);
893
945
  try {
894
946
  await state.stream?.abort();
895
947
  } finally {
@@ -968,7 +1020,6 @@
968
1020
  };
969
1021
  self.Module = module;
970
1022
  configureWorker(module, transport, (runtime) => {
971
- runtime.externalFileBackendAvailable = true;
972
1023
  runtime.externalDirectoryBackend = new BrowserExternalDirectoryBackend();
973
1024
  if (!runtime.OPFS) throw new Error("OPFS backend is unavailable");
974
1025
  runtime.FS.mkdirTree("/mnt/fs");
package/dist/client.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare class HyperDBEngineClient {
19
19
  startQuery(connection: number, sql: string, sqlTestMode?: boolean): Promise<WasmResult>;
20
20
  mount(source: MountSource, path: string, mode: MountAccessMode): Promise<WasmResult>;
21
21
  unmount(path: string): Promise<WasmResult>;
22
+ refreshMount(path: string): Promise<WasmResult>;
22
23
  pollQuery(query: number): Promise<WasmResult>;
23
24
  cancelQuery(query: number): Promise<WasmResult>;
24
25
  finishQuery(query: number): Promise<WasmResult>;