Line data Source code
1 : import type { Logger } from '../logger/logger.js';
2 : import { stringifyError } from '../logger/logger.js';
3 : import type { EmbeddedDatabaseOptions, SetupProgress } from '../database/embedded_database_provider.js';
4 :
5 : import { HyperDB, HyperDBEngine, type HyperDBClient, type HyperDBSettings } from './hyperdb_wasm.js';
6 : import {
7 : EXPERIMENTAL_WASM_BACKEND_SETTING,
8 : HYPERDB_WASM_ENGINE_SETTINGS,
9 : isUnknownHyperDBSettingError,
10 : } from './hyperdb_settings.js';
11 : import { cleanupStaleHyperDBOPFSInstances, createHyperDBOPFSInstance } from './hyperdb_opfs_instance.js';
12 : import { getHyperDBWebSupportError } from './hyperdb_web_support.js';
13 :
14 : declare global {
15 : var __DASHQL_TEST_HYPERDB_CLIENT__: ((
16 : onSetupProgress?: (progress: SetupProgress) => void,
17 : ) => Promise<HyperDBClient>) | undefined;
18 : var __DASHQL_TEST_HYPERDB_CLIENT_PROMISE__: Promise<HyperDBClient> | undefined;
19 : var __DASHQL_TEST_HYPERDB_CLIENT_CREATIONS__: number | undefined;
20 : var __DASHQL_TEST_HYPERDB_UNLOAD_INSTALLED__: boolean | undefined;
21 : var __DASHQL_TEST_HYPERDB_FACTORY__: ((
22 : context: string,
23 : logger: Logger,
24 : onSetupProgress?: (progress: SetupProgress) => void,
25 : ) => Promise<HyperDB>) | undefined;
26 : }
27 :
28 : // eslint-disable-next-line import/no-unresolved -- package asset resolved by Vite
29 : import engineUrl from '@dashql/hyperdb-wasm-js?url';
30 : // eslint-disable-next-line import/no-unresolved -- package asset resolved by Vite
31 : import workerUrl from '@dashql/hyperdb-wasm-worker?url';
32 : // eslint-disable-next-line import/no-unresolved -- package asset resolved by Vite
33 : import workerBridgeUrl from './hyperdb_worker_bridge.js?url&no-inline';
34 : // eslint-disable-next-line import/no-unresolved -- package asset resolved by Bazel/Vite alias
35 : import wasmUrl from '@dashql/hyperdb-wasm?url';
36 :
37 1 : const HYPERDB_ENGINE_URL = new URL(engineUrl as string, import.meta.url);
38 1 : const HYPERDB_WORKER_URL = new URL(workerUrl as string, import.meta.url);
39 1 : const HYPERDB_WASM_URL = new URL(wasmUrl as string, import.meta.url);
40 :
41 1 : function createEngineScript(): { url: string; revoke: () => void } {
42 1 : const source = `self.HYPERDB_WASM_MODULE=self.Module??{};self.HYPERDB_WASM_MODULE.locateFile=(path,prefix)=>path==='hyperdb-wasm.wasm'?${JSON.stringify(HYPERDB_WASM_URL.href)}:prefix+path;importScripts(${JSON.stringify(HYPERDB_ENGINE_URL.href)});`;
43 1 : const url = URL.createObjectURL(new Blob([source], { type: 'application/javascript' }));
44 1 : return { url, revoke: () => URL.revokeObjectURL(url) };
45 1 : }
46 :
47 : /// The worker options of a browser engine client.
48 : /// The engine script is an object URL that must be revoked once the engine terminated.
49 1 : export function createWebHyperDBClientOptions(): { workerUrl: URL; engineUrl: string; wasmUrl: URL; revoke: () => void } {
50 1 : const supportError = getHyperDBWebSupportError();
51 1 : if (supportError != null) throw new Error(supportError);
52 1 : const engineScript = createEngineScript();
53 1 : const workerUrl = new URL(workerBridgeUrl as string, import.meta.url);
54 1 : workerUrl.searchParams.set('workerUrl', HYPERDB_WORKER_URL.href);
55 1 : return { workerUrl, engineUrl: engineScript.url, wasmUrl: HYPERDB_WASM_URL, revoke: engineScript.revoke };
56 1 : }
57 :
58 : /// Start a HyperDB engine in a browser worker.
59 1 : async function createWebHyperDBEngine(
60 1 : settings: HyperDBSettings,
61 1 : onSetupProgress?: (progress: SetupProgress) => void,
62 1 : ): Promise<HyperDBEngine> {
63 1 : const { revoke, ...options } = createWebHyperDBClientOptions();
64 1 : let engine: HyperDBEngine;
65 1 : try {
66 1 : engine = await HyperDBEngine.create({ ...options, onSetupProgress, settings });
67 1 : } catch (error) {
68 1 : revoke();
69 1 : throw error;
70 1 : }
71 1 : const terminate = engine.terminate.bind(engine);
72 1 : engine.terminate = async () => {
73 1 : try {
74 1 : await terminate();
75 1 : } finally {
76 1 : revoke();
77 1 : }
78 1 : };
79 1 : return engine;
80 1 : }
81 :
82 : /// Start a HyperDB engine in a browser worker, on the experimental wasm backend if requested.
83 : /// A build of hyperdb-wasm without the backend rejects its setting, the engine then starts on the default backend.
84 1 : async function createWebHyperDBEngineWithOptions(
85 1 : context: string,
86 1 : logger: Logger,
87 1 : settings: HyperDBSettings,
88 1 : options: EmbeddedDatabaseOptions,
89 1 : onSetupProgress?: (progress: SetupProgress) => void,
90 1 : ): Promise<HyperDBEngine> {
91 1 : if (!options.experimentalWasmBackend) {
92 1 : return await createWebHyperDBEngine(settings, onSetupProgress);
93 : }
94 1 : try {
95 1 : const engine = await createWebHyperDBEngine({ ...settings, [EXPERIMENTAL_WASM_BACKEND_SETTING]: true }, onSetupProgress);
96 1 : logger.info('Started HyperDB on the experimental wasm backend', { context }, 'hyperdb');
97 1 : return engine;
98 1 : } catch (error) {
99 1 : if (!isUnknownHyperDBSettingError(error, EXPERIMENTAL_WASM_BACKEND_SETTING)) throw error;
100 1 : logger.warn('HyperDB has no experimental wasm backend, starting the default backend', {
101 1 : context,
102 1 : error: stringifyError(error),
103 1 : }, 'hyperdb');
104 1 : return await createWebHyperDBEngine(settings, onSetupProgress);
105 1 : }
106 1 : }
107 :
108 1 : export async function setupWebHyperDB(
109 1 : context: string,
110 1 : logger: Logger,
111 1 : onSetupProgress?: (progress: SetupProgress) => void,
112 1 : options: EmbeddedDatabaseOptions = {},
113 1 : ): Promise<HyperDB> {
114 1 : if (globalThis.__DASHQL_TEST_HYPERDB_FACTORY__) {
115 1 : return await globalThis.__DASHQL_TEST_HYPERDB_FACTORY__(context, logger, onSetupProgress);
116 : }
117 1 : const initStart = performance.now();
118 1 : try {
119 1 : logger.info('Creating HyperDB WASM engine', { context }, 'hyperdb');
120 1 : await cleanupStaleHyperDBOPFSInstances();
121 1 : const instance = await createHyperDBOPFSInstance();
122 1 : let engine: HyperDBEngine | null = null;
123 1 : try {
124 1 : engine = await createWebHyperDBEngineWithOptions(
125 1 : context,
126 1 : logger,
127 1 : { ...HYPERDB_WASM_ENGINE_SETTINGS, log_file_path: instance.logDirectoryPath },
128 1 : options,
129 1 : onSetupProgress,
130 1 : );
131 1 : const database = await HyperDB.create(engine, {
132 1 : computeDatabasePath: instance.computeDatabasePath,
133 1 : defaultDatabasePath: instance.userDatabasePath,
134 1 : snippetsDatabasePath: instance.snippetsDatabasePath,
135 1 : });
136 1 : const terminate = database.terminate.bind(database);
137 1 : database.terminate = async () => {
138 1 : await terminate();
139 1 : await instance.dispose();
140 1 : };
141 :
142 1 : logger.info('Instantiated HyperDB WASM', {
143 1 : context,
144 1 : duration: Math.floor(performance.now() - initStart).toString(),
145 1 : }, 'hyperdb');
146 1 : return database;
147 1 : } catch (error) {
148 1 : try {
149 1 : await engine?.terminate();
150 1 : await instance.dispose();
151 1 : } catch (cleanupError) {
152 1 : logger.warn('Cleaning up failed HyperDB setup failed', {
153 1 : context,
154 1 : error: stringifyError(cleanupError),
155 1 : }, 'hyperdb');
156 1 : }
157 1 : throw error;
158 1 : }
159 1 : } catch (error) {
160 1 : logger.error('Instantiating HyperDB WASM failed', {
161 1 : context,
162 1 : error: stringifyError(error),
163 1 : duration: Math.floor(performance.now() - initStart).toString(),
164 1 : }, 'hyperdb');
165 1 : throw error;
166 1 : }
167 1 : }
|