hyperdb-wasm 0.0.27136-dev.wasm.gcf673716 → 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
@@ -99,7 +99,7 @@ export declare class HyperDB {
99
99
  static create(options?: BrowserCreateOptions): Promise<HyperDB>;
100
100
  ready(): Promise<void>;
101
101
  connect(options?: HyperDBConnectOptions): Promise<HyperDBConnection>;
102
- protected mountSource(source: MountSource, path: string, mode: MountAccessMode, cleanup?: () => void): Promise<MountedResource>;
102
+ protected mountSource(source: MountSource, path: string, mode: MountAccessMode): Promise<MountedResource>;
103
103
  terminate(): Promise<void>;
104
104
  private ensureRunning;
105
105
  protected runDatabaseOperation<Result>(operation: () => Promise<Result>): Promise<Result>;
package/dist/browser.js CHANGED
@@ -501,19 +501,16 @@ var EngineQueryOperation = class {
501
501
  }
502
502
  };
503
503
  var MountedResourceImpl = class {
504
- constructor(path, unmount, refreshMount, cleanup) {
504
+ constructor(path, unmount, refreshMount) {
505
505
  this.path = path;
506
506
  this.unmount = unmount;
507
507
  this.refreshMount = refreshMount;
508
- this.cleanup = cleanup;
509
508
  }
510
509
  path;
511
510
  unmount;
512
511
  refreshMount;
513
- cleanup;
514
512
  closed = false;
515
513
  closing;
516
- cleaned = false;
517
514
  async refresh() {
518
515
  if (this.closed) throw new Error("mounted resource is closed");
519
516
  if (this.closing) throw new Error("mounted resource is closing");
@@ -529,19 +526,13 @@ var MountedResourceImpl = class {
529
526
  this.closing = void 0;
530
527
  throw error;
531
528
  }
532
- this.release();
529
+ this.closed = true;
533
530
  })();
534
531
  }
535
532
  await this.closing;
536
533
  }
537
534
  releaseAfterShutdown() {
538
- this.release();
539
- }
540
- release() {
541
535
  this.closed = true;
542
- if (this.cleaned) return;
543
- this.cleaned = true;
544
- this.cleanup();
545
536
  }
546
537
  };
547
538
  var HyperDB = class _HyperDB {
@@ -581,7 +572,7 @@ var HyperDB = class _HyperDB {
581
572
  this.connections.add(result);
582
573
  return result;
583
574
  }
584
- mountSource(source, path, mode, cleanup = () => void 0) {
575
+ mountSource(source, path, mode) {
585
576
  return this.runDatabaseOperation(async () => {
586
577
  const result = await this.client.mount(source, path, mode);
587
578
  expectOk(result, "mount");
@@ -592,8 +583,7 @@ var HyperDB = class _HyperDB {
592
583
  (mounted) => this.unmount(mounted),
593
584
  (path2) => this.runDatabaseOperation(async () => {
594
585
  expectOk(await this.client.refreshMount(path2), "refresh mount");
595
- }),
596
- cleanup
586
+ })
597
587
  );
598
588
  this.mountedResources.add(resource);
599
589
  return resource;
@@ -862,13 +852,7 @@ var HyperDB2 = class _HyperDB extends HyperDB {
862
852
  throw new Error("Mounted file name must be a basename without path separators");
863
853
  }
864
854
  if (!Number.isSafeInteger(source.size) || source.size < 0) throw new Error("Mounted file size must be a nonnegative safe integer");
865
- const url = URL.createObjectURL(source);
866
- try {
867
- return await this.mountSource({ kind: "browser-file", name: source.name, url, size: source.size }, path, "read", () => URL.revokeObjectURL(url));
868
- } catch (error) {
869
- URL.revokeObjectURL(url);
870
- throw error;
871
- }
855
+ return this.mountSource({ kind: "browser-file", file: source }, path, "read");
872
856
  }
873
857
  const handle = source;
874
858
  const mode = options.mode ?? "read";
@@ -8,23 +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;
16
- refresh(path: string): void;
20
+ releaseGeneration(locator: string, generation: string): void;
17
21
  removed(pathOrLocator: string): void;
18
22
  list(locator: string): Promise<readonly string[]>;
19
23
  listEntries(locator: string, path: string): Promise<readonly BrowserDirectoryEntry[]>;
20
- lookup(locator: string, path: string): Promise<{
24
+ lookup(locator: string, path: string, generation: string): Promise<{
21
25
  readonly kind: 'directory';
22
26
  } | {
23
27
  readonly kind: 'file';
24
28
  readonly locator: string;
25
29
  readonly size: number;
26
30
  } | undefined>;
27
- open(locator: string, path: string): Promise<{
31
+ open(locator: string, path: string, generation: string): Promise<{
28
32
  readonly locator: string;
29
33
  readonly size: number;
30
34
  }>;
@@ -102,24 +102,25 @@
102
102
  if (registration.kind === "directory") return this.mountDirectory(request, request.source.path);
103
103
  return this.mountFile(request, registration.locator, registration.size, this.module.externalFileBackend);
104
104
  }
105
- if (request.type === "mount" && request.source.kind === "browser-file" && !this.module.externalFileBackendAvailable) {
106
- return { response: { id: request.id, type: "response", result: { state: "error", error: "Blob external files are supported only by the browser client" } }, transfer: [] };
107
- }
108
105
  if (request.type === "mount" && request.source.kind === "browser-directory") {
109
106
  return this.mountDirectory(request, request.source.handle);
110
107
  }
111
108
  if (request.type === "mount" && request.source.kind === "browser-file") {
112
- 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);
113
116
  }
114
117
  if (request.type === "refreshMount") {
115
118
  validateCString(request.path, "external registration path");
116
- const backend = this.module.externalDirectoryBackend;
117
- if (!backend) return { response: { id: request.id, type: "response", result: { state: "error", error: "External directories are not supported by this client" } }, transfer: [] };
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: [] };
118
120
  if (!this.mountedDirectoryPaths.has(request.path)) {
119
121
  const error = this.mountedPaths.has(request.path) ? "Only directory mounts can be refreshed" : "External directory registration was not found";
120
122
  return { response: { id: request.id, type: "response", result: { state: "error", error } }, transfer: [] };
121
123
  }
122
- backend.refresh(request.path);
123
124
  }
124
125
  if (request.type === "unmount" && (this.module.externalFileBackend || this.module.externalDirectoryBackend)) {
125
126
  validateCString(request.path, "external registration path");
@@ -138,7 +139,7 @@
138
139
  }
139
140
  return { response: { id: request.id, type: "response", result: result2 }, transfer: [] };
140
141
  }
141
- if (request.type === "unmount" && !this.module.externalFileBackendAvailable) {
142
+ if (request.type === "unmount") {
142
143
  return { response: { id: request.id, type: "response", result: { state: "error", error: "External files are not supported by this client" } }, transfer: [] };
143
144
  }
144
145
  const pointer = safeInteger(this.invoke(request), "result pointer");
@@ -157,7 +158,6 @@
157
158
  };
158
159
  }
159
160
  const response = { id: request.id, type: "response", result };
160
- if (request.type === "unmount" && result.state === "ok") this.mountedPaths.delete(request.path);
161
161
  return { response, transfer: result.state === "chunk" ? [result.payload.buffer] : [] };
162
162
  }
163
163
  invoke(request) {
@@ -203,13 +203,13 @@
203
203
  try {
204
204
  const result = this.decodeResult(safeInteger(this.registerExternalFile(request.path, locator, size, request.id), "result pointer"));
205
205
  if (result.state !== "ok") {
206
- backend?.removed(locator);
207
- 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 } };
208
208
  }
209
209
  path = new TextDecoder("utf-8", { fatal: true }).decode(result.payload);
210
210
  const projection = this.decodeResult(safeInteger(this.projectExternalFile(path, locator, size, request.id), "result pointer"));
211
211
  if (projection.state !== "ok") throw new Error(resultError(projection));
212
- backend?.registered(path, locator);
212
+ backend.registered(path, locator);
213
213
  registered = true;
214
214
  this.mountedPaths.add(path);
215
215
  return { response: { id: request.id, type: "response", result }, transfer: [] };
@@ -224,12 +224,15 @@
224
224
  }
225
225
  if (rollbackError !== void 0) {
226
226
  if (path !== void 0) {
227
- backend?.registered(path, locator);
227
+ backend.registered(path, locator);
228
228
  this.mountedPaths.add(path);
229
229
  }
230
230
  throw new Error(`mount failed and rollback also failed: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, { cause: error });
231
231
  }
232
- 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
+ }
233
236
  throw error;
234
237
  }
235
238
  }
@@ -354,7 +357,7 @@
354
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;
355
358
  switch (value.source.kind) {
356
359
  case "browser-file":
357
- 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;
358
361
  case "browser-directory":
359
362
  return hasKeys(value.source, ["kind", "handle"]) && isRecord(value.source.handle) && value.source.handle.kind === "directory";
360
363
  case "node-path":
@@ -462,9 +465,14 @@
462
465
  };
463
466
  module.hyperdbWasmFetchCancel = (id) => fetches.get(id)?.abort();
464
467
  }
465
- function installExternalDirectoryRuntime(module) {
468
+ function installExternalDirectoryRuntime(module, transport) {
466
469
  const backend = module.externalDirectoryBackend;
467
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
+ };
468
476
  const listComplete = module._hyperdb_wasm_external_directory_list_complete;
469
477
  const openComplete = module._hyperdb_wasm_external_directory_open_complete;
470
478
  if (listComplete) {
@@ -477,8 +485,8 @@
477
485
  };
478
486
  }
479
487
  if (openComplete) {
480
- module.hyperdbWasmExternalDirectoryOpenStart = (id, locator, path) => {
481
- void backend.open(locator, path).then((file) => {
488
+ module.hyperdbWasmExternalDirectoryOpenStart = (id, locator, path, generation) => {
489
+ void backend.open(locator, path, generation).then((file) => {
482
490
  openComplete(id, safePointer(module.stringToNewUTF8(file.locator), "external file locator pointer"), file.size);
483
491
  }).catch((error) => openComplete(id, 0, isNotFoundError(error) ? -2 : -1));
484
492
  };
@@ -567,8 +575,8 @@
567
575
  complete(id, safePointer(module.stringToNewUTF8(JSON.stringify(encoded)), "filesystem listing pointer"), 0);
568
576
  }).catch(() => complete(id, 0, -1));
569
577
  };
570
- module.hyperdbWasmFilesystemLookupStart = (id, locator, path) => {
571
- const lookup = module.externalDirectoryBackend?.lookup?.(locator, path);
578
+ module.hyperdbWasmFilesystemLookupStart = (id, locator, path, generation) => {
579
+ const lookup = module.externalDirectoryBackend?.lookup?.(locator, path, generation);
572
580
  if (!lookup) {
573
581
  complete(id, 0, -1);
574
582
  return;
@@ -580,13 +588,7 @@
580
588
  }).catch(() => complete(id, 0, -1));
581
589
  };
582
590
  module.hyperdbWasmFilesystemReadStart = (id, locator, offset, length, expectedSize, pointerValue) => {
583
- 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) => {
584
- const contentRange = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(response.headers.get("content-range") ?? "");
585
- if (response.status !== 206 || !contentRange || contentRange[1] !== String(offset) || contentRange[2] !== String(offset + length - 1) || contentRange[3] !== String(expectedSize)) {
586
- throw new Error("Blob URL returned an invalid byte range");
587
- }
588
- return new Uint8Array(await response.arrayBuffer());
589
- }) : void 0);
591
+ const read = module.externalFileBackend?.read(locator, offset, length, expectedSize) ?? module.externalDirectoryBackend?.read(locator, offset, length, expectedSize);
590
592
  if (!read) {
591
593
  complete(id, 0, -1);
592
594
  return;
@@ -633,7 +635,7 @@
633
635
  configureFilesystem(module);
634
636
  installHttpRuntime(module, transport);
635
637
  installHostFileRuntime(module);
636
- installExternalDirectoryRuntime(module);
638
+ installExternalDirectoryRuntime(module, transport);
637
639
  installFilesystemProjectionRuntime(module);
638
640
  bindings = new HyperDBBindings(module);
639
641
  transport.postMessage({ type: "ready" });
@@ -682,33 +684,51 @@
682
684
  paths = /* @__PURE__ */ new Map();
683
685
  files = /* @__PURE__ */ new Map();
684
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
+ }
685
696
  register(handle, mode) {
686
697
  const directoryHandle = asDirectoryHandle(handle);
687
698
  validateSegment(directoryHandle.name);
688
699
  const locator = `${directoryLocatorPrefix}${opaqueId()}`;
689
- 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() });
690
701
  return { name: directoryHandle.name, locator };
691
702
  }
692
703
  registered(path, locator) {
693
- 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");
694
705
  this.paths.set(path, locator);
695
706
  }
696
- /// Clears future path lookups, retaining File locators until lease-protected mount removal
697
- refresh(path) {
698
- const locator = this.paths.get(path);
699
- if (locator === void 0) throw new Error("External directory registration does not exist");
700
- this.getDirectory(locator).files.clear();
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
+ }
701
716
  }
702
717
  removed(pathOrLocator) {
703
718
  const locator = this.paths.get(pathOrLocator) ?? pathOrLocator;
704
719
  const directory = this.directories.get(locator);
705
- if (!directory) return;
706
- for (const fileLocator of directory.locators) this.files.delete(fileLocator);
707
- for (const [writer, state] of this.writers) {
708
- 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);
709
731
  }
710
- this.directories.delete(locator);
711
- this.paths.delete(pathOrLocator);
712
732
  }
713
733
  async list(locator) {
714
734
  const directory = this.getDirectory(locator);
@@ -737,7 +757,7 @@
737
757
  entries.sort((left, right) => left.name.localeCompare(right.name));
738
758
  return entries;
739
759
  }
740
- async lookup(locator, path) {
760
+ async lookup(locator, path, generation) {
741
761
  const directory = this.getDirectory(locator);
742
762
  const segments = relativeSegments(path);
743
763
  const parent = await this.openDirectory(directory, segments.slice(0, -1));
@@ -747,14 +767,19 @@
747
767
  validateSegment(child.name);
748
768
  if (child.name !== name) continue;
749
769
  if (child.kind === "directory") return { kind: "directory" };
750
- 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) };
751
771
  throw new Error("External directory contains an unsupported entry");
752
772
  }
753
773
  return void 0;
754
774
  }
755
- async open(locator, path) {
775
+ async open(locator, path, generation) {
756
776
  const directory = this.getDirectory(locator);
757
- 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);
758
783
  if (cached) {
759
784
  const file = await cached;
760
785
  return { locator: file.locator, size: file.file.size };
@@ -772,25 +797,24 @@
772
797
  const fileHandle = await parent.getFileHandle(name);
773
798
  if (fileHandle.kind !== "file" || fileHandle.name !== name) throw new Error("External directory returned an unexpected file handle");
774
799
  const file = await fileHandle.getFile();
775
- if (!Number.isSafeInteger(file.size) || file.size < 0) throw new Error("External file size must be a nonnegative safe integer");
776
- const fileLocator = `${fileLocatorPrefix}${opaqueId()}`;
777
- this.files.set(fileLocator, file);
800
+ const { locator: fileLocator } = this.registerFile(file);
778
801
  directory.locators.add(fileLocator);
802
+ state.locators.add(fileLocator);
779
803
  return { locator: fileLocator, file };
780
804
  })();
781
- directory.files.set(path, snapshot);
805
+ state.files.set(path, snapshot);
782
806
  try {
783
807
  const file = await snapshot;
784
808
  return { locator: file.locator, size: file.file.size };
785
809
  } catch (error) {
786
- if (directory.files.get(path) === snapshot) directory.files.delete(path);
810
+ if (state.files.get(path) === snapshot) state.files.delete(path);
787
811
  throw error;
788
812
  }
789
813
  }
790
814
  read(locator, offset, length, expectedSize) {
791
815
  if (!locator.startsWith(fileLocatorPrefix)) return void 0;
792
816
  const file = this.files.get(locator);
793
- 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"));
794
818
  const end = offset + length;
795
819
  if (expectedSize !== file.size || !Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0 || !Number.isSafeInteger(end) || end > file.size) {
796
820
  return Promise.reject(new Error("External file byte range is invalid"));
@@ -875,7 +899,7 @@
875
899
  await this.cleanupWriter(state).catch(() => void 0);
876
900
  throw failure;
877
901
  }
878
- this.getDirectory(state.locator).files.delete(state.path);
902
+ for (const generation of this.getDirectory(state.locator).generations.values()) generation.files.delete(state.path);
879
903
  }, true);
880
904
  return operation.finally(() => this.writers.delete(writer));
881
905
  }
@@ -917,7 +941,7 @@
917
941
  return state;
918
942
  }
919
943
  async cleanupWriter(state) {
920
- 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);
921
945
  try {
922
946
  await state.stream?.abort();
923
947
  } finally {
@@ -996,7 +1020,6 @@
996
1020
  };
997
1021
  self.Module = module;
998
1022
  configureWorker(module, transport, (runtime) => {
999
- runtime.externalFileBackendAvailable = true;
1000
1023
  runtime.externalDirectoryBackend = new BrowserExternalDirectoryBackend();
1001
1024
  if (!runtime.OPFS) throw new Error("OPFS backend is unavailable");
1002
1025
  runtime.FS.mkdirTree("/mnt/fs");