@reddb-io/browser 0.1.0-alpha.0 → 0.1.0-alpha.1

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/README.md CHANGED
@@ -4,7 +4,8 @@ This experimental package is the first distribution of RedDB Browser. It runs th
4
4
  canonical Rust `.rdb` artifact publication/recovery code, RQL parser and shared
5
5
  scalar evaluator in a dedicated WebAssembly worker, persisting through a real
6
6
  OPFS sync access handle.
7
- It requires a secure context, module workers, OPFS and Web Locks.
7
+ Artifact storage requires a secure context, module workers, OPFS and Web Locks.
8
+ The storage-independent runtime only needs module workers and WebAssembly.
8
9
 
9
10
  The target is a fully offline local database with a JavaScript/TypeScript API.
10
11
  The Rust/WASM engine ships with the package; local operation must not require a
@@ -40,6 +41,45 @@ try {
40
41
  Consumers receive built JS/Worker/WASM assets and TypeScript declarations; they
41
42
  need no Rust toolchain. The commands below are for engine/package contributors.
42
43
 
44
+ ## Shared runtime and language interop
45
+
46
+ `createRuntime({ worker? })` opens no file and requires neither OPFS nor Web
47
+ Locks. It exposes `info`, `validateQuery`, `executeScalar` and `close`:
48
+
49
+ ```ts
50
+ import { createRuntime } from '@reddb-io/browser'
51
+ import RedDBWorker from '@reddb-io/browser/worker?worker'
52
+ const runtime = await createRuntime({ worker: new RedDBWorker() })
53
+ try {
54
+ console.log(await runtime.info())
55
+ console.log(await runtime.executeScalar('SELECT 6 * 7 AS answer'))
56
+ } finally { await runtime.close() }
57
+ ```
58
+
59
+ The versioned Worker/WASM contract reports `abiVersion: 1`,
60
+ `valueEncoding: 'reddb-json-v1'` and capabilities. Currently `queryValidation`
61
+ and `scalarQueries` are true; `storedQueries`, `transactions` and `sync` are
62
+ false. Mismatched SDK/Worker/WASM versions fail with `VERSION_MISMATCH` before
63
+ opening storage. Cache the JS, Worker and WASM as one versioned unit.
64
+
65
+ `createInteropRuntime({ worker? })` returns a synchronous bridge object with
66
+ `invoke(method, params?)` and `close()`. `invoke` accepts `info`, `validateQuery`
67
+ and `executeScalar` (queries take `{ sql }`) and resolves JSON-safe envelopes:
68
+ `{ ok: true, result }` or `{ ok: false, error: { code, message } }`.
69
+ A void result is `null`; exact integer/decimal tags are unchanged. `close()` is
70
+ idempotent and cancels pending initialization/operations. Calls after close
71
+ return `CLOSED`. Each bridge owns a dedicated Worker.
72
+
73
+ Source adapters use this same bridge for
74
+ [Dart/Flutter Web](https://github.com/reddb-io/reddb/blob/main/drivers/dart/BROWSER.md) and
75
+ [C#/Blazor](https://github.com/reddb-io/reddb/blob/main/drivers/dotnet/src/Reddb.Browser/README.md). They are not yet
76
+ published on pub.dev/NuGet. Their real compiled browser probes exercise scalar
77
+ execution, exact values, errors and disposal online and offline. Full offline
78
+ browser restarts are verified on Chromium/Firefox; Playwright WPE persistent
79
+ profiles lack working CacheStorage, so Safari/iOS qualification remains pending. Run `pnpm --filter @reddb-io/browser build:interop` followed by
80
+ `pnpm --filter @reddb-io/browser test:interop` with Dart 3.13.7 and .NET 8.0.426.
81
+ These adapters do not expose artifact storage or qualify complete offline apps.
82
+
43
83
  ## Build and verify
44
84
 
45
85
  Install the repository-pinned Rust toolchain, its `wasm32-unknown-unknown` target,
@@ -145,7 +185,7 @@ these primitives do not promise that a browser will grant it or sync after closi
145
185
  ## Offline asset loading
146
186
 
147
187
  The SDK does not register a service worker or control application updates. A
148
- PWA should include `index.js`, `worker.js`, `wasm/reddb_wasm.js` and
188
+ PWA should include `index.js`, `protocol.js`, `worker.js`, `wasm/reddb_wasm.js` and
149
189
  `wasm/reddb_wasm_bg.wasm` from `src/` in its own versioned application cache,
150
190
  along with its application shell. Those assets must remain available together
151
191
  after installation. A locally packaged host can serve the same assets locally.
@@ -245,6 +285,40 @@ artifact operations and storage-free scalar SELECTs; stored-data dispatch,
245
285
  durable runtime transactions/WAL and application collection catalogs remain
246
286
  unfinished. No file format, wire envelope or public browser API changes here.
247
287
 
288
+ ## Shared resident collection storage
289
+
290
+ The growing/sealed segment, typed metadata, key/unique-key and physical graph
291
+ candidate indexes, and resident payload accounting now live in
292
+ `reddb-engine::storage`. Native paths re-export the same implementations. The
293
+ native segment manager retains lifecycle, consolidation, explicit snapshot and
294
+ moderation gates. Storage clocks use Unix seconds on both native and WASM hosts.
295
+ Four shared conformance cases exercise all original record-oracle entity shapes,
296
+ key-version lookup and raw mutation, bounded/cancelled scans, metadata and sealing;
297
+ the browser worker repeats them after the existing offline restart.
298
+
299
+ These are canonical resident storage operations, not durable application
300
+ transactions. WAL publication, catalog dispatch and the application SDK still
301
+ need integration before JS/TS can query and mutate persisted collections. The
302
+ production artifact/scalar API and all file formats remain unchanged.
303
+
304
+ ## Shared snapshot recovery
305
+
306
+ Native and WASM now use the same typed metadata/entity-record codecs and streaming
307
+ V1–V12 dump reader. Recovery preserves empty collection registration, record
308
+ metadata, cross-references and the optional V10+ catalog blob. The shared file
309
+ layer verifies the checksum before any restore callback and lends the payload
310
+ without copying the entire snapshot. Hosts must restore into private state and
311
+ publish it only after successful recovery.
312
+
313
+ Frozen pre-extraction native metadata bytes and entity fixtures check compatible
314
+ encoding and decoding. Shared native/WASM cases also cover truncated metadata,
315
+ invalid counts/tags/UTF-8, nesting at the RQL depth limit, checksum failures,
316
+ legacy optional tails and host callback errors. Metadata nesting above the existing
317
+ RQL JSON limit is now rejected; previously the native metadata reader was unbounded.
318
+ The native host retains collection/index restoration, ID allocation and WAL policy.
319
+ These internal recovery primitives do not yet expose stored-data queries or
320
+ application transactions through the browser SDK.
321
+
248
322
  ## Remaining work toward the approved Browser design
249
323
 
250
324
  1. Extract the remaining storage-coupled executor/runtime into the shared portable engine and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reddb-io/browser",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Experimental RedDB browser persistence and shared scalar execution; stored-data queries and sync remain pending.",
@@ -14,9 +14,11 @@
14
14
  },
15
15
  "scripts": {
16
16
  "build": "node scripts/build.mjs",
17
- "test": "node scripts/build-test.mjs && node --test test/browser.test.mjs",
17
+ "test": "node scripts/build-test.mjs && node --test test/runtime.test.mjs test/browser.test.mjs",
18
18
  "prepack": "node scripts/verify-dist.mjs",
19
- "test:distribution": "node --test test/distribution.test.mjs"
19
+ "test:distribution": "node --test test/distribution.test.mjs",
20
+ "build:interop": "node scripts/build-interop.mjs",
21
+ "test:interop": "node --test test/interop.test.mjs"
20
22
  },
21
23
  "devDependencies": {
22
24
  "playwright": "1.61.1",
@@ -3,7 +3,7 @@ import { dirname, resolve } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
 
5
5
  const directory = resolve(dirname(fileURLToPath(import.meta.url)), '..')
6
- for (const path of ['src/index.js', 'src/index.d.ts', 'src/worker.js', 'src/wasm/reddb_wasm.js', 'LICENSE']) {
6
+ for (const path of ['src/index.js', 'src/index.d.ts', 'src/protocol.js', 'src/worker.js', 'src/wasm/reddb_wasm.js', 'LICENSE']) {
7
7
  if (!readFileSync(resolve(directory, path)).length) throw new Error(`Missing browser distribution file: ${path}`)
8
8
  }
9
9
  const wasm = readFileSync(resolve(directory, 'src/wasm/reddb_wasm_bg.wasm'))
package/src/index.d.ts CHANGED
@@ -4,11 +4,46 @@ export class BrowserError extends Error {
4
4
  constructor(code: string, message: string)
5
5
  }
6
6
 
7
- export interface Artifact {
7
+ export interface RuntimeInfo {
8
+ readonly abiVersion: 1
9
+ readonly valueEncoding: 'reddb-json-v1'
10
+ readonly capabilities: {
11
+ readonly queryValidation: boolean
12
+ readonly scalarQueries: boolean
13
+ readonly storedQueries: boolean
14
+ readonly transactions: boolean
15
+ readonly sync: boolean
16
+ }
17
+ }
18
+
19
+ export interface Runtime {
20
+ /** Compiled engine capabilities; does not imply OPFS availability on this host. */
21
+ info(): Promise<RuntimeInfo>
8
22
  /** Parse with the canonical RQL front-end; does not execute the statement. */
9
23
  validateQuery(sql: string): Promise<void>
10
24
  /** Storage-free scalar SELECT only; does not read or mutate the artifact. */
11
25
  executeScalar(sql: string): Promise<{ columns: string[]; rows: unknown[][] }>
26
+ /** Terminate the storage-independent worker, rejecting pending operations. */
27
+ close(): Promise<void>
28
+ }
29
+
30
+ export type InteropResult =
31
+ | { ok: true; result: unknown }
32
+ | { ok: false; error: { code: string; message: string } }
33
+
34
+ export interface RuntimeBridge {
35
+ /** info, validateQuery or executeScalar. Failures resolve as structured envelopes. */
36
+ invoke(method: string, params?: Record<string, unknown>): Promise<InteropResult>
37
+ /** Synchronous, idempotent cancellation, including during initialization. */
38
+ close(): void
39
+ }
40
+
41
+ /** No OPFS or Web Locks required; scalar execution only. */
42
+ export function createRuntime(options?: { worker?: Worker }): Promise<Runtime>
43
+ /** JSON-safe adapter boundary; does not expose artifact mutation or stored-data queries. */
44
+ export function createInteropRuntime(options?: { worker?: Worker }): RuntimeBridge
45
+
46
+ export interface Artifact extends Runtime {
12
47
  generation(): Promise<string>
13
48
  readWal(): Promise<Uint8Array[]>
14
49
  readSnapshot(): Promise<Uint8Array | undefined>
@@ -16,6 +51,7 @@ export interface Artifact {
16
51
  append(payloads: Uint8Array[]): Promise<void>
17
52
  /** Complete native store snapshot; the caller must fold in the committed WAL. */
18
53
  checkpoint(snapshot: Uint8Array, minWalBytes?: number): Promise<void>
54
+ /** Flush/close through the owner before terminating the worker. */
19
55
  close(): Promise<void>
20
56
  }
21
57
 
package/src/index.js CHANGED
@@ -1,22 +1,9 @@
1
- /** Experimental artifact storage and storage-free scalar execution. */
2
-
3
- export class BrowserError extends Error {
4
- constructor(code, message) {
5
- super(message)
6
- this.name = 'BrowserError'
7
- this.code = code
8
- }
9
- }
10
-
11
- /** Open an exclusively owned, canonical .rdb artifact in an OPFS worker. */
12
- export async function openArtifact({ name, worker: suppliedWorker } = {}) {
13
- if (typeof name !== 'string' || !/^[a-zA-Z0-9_-][a-zA-Z0-9_.-]{0,79}$/.test(name)) {
14
- throw new BrowserError('INVALID_NAME', 'Artifact names must contain 1–80 letters, digits, dots, underscores or hyphens')
15
- }
16
- if (!globalThis.isSecureContext || !globalThis.Worker || !globalThis.navigator?.storage?.getDirectory || !globalThis.navigator?.locks) {
17
- throw new BrowserError('STORAGE_UNSUPPORTED', 'RedDB Browser requires HTTPS, module Workers, OPFS and Web Locks')
18
- }
1
+ /** Shared Rust/WASM runtime and experimental artifact storage. */
2
+ import { BrowserError, RUNTIME_ABI_VERSION, VALUE_ENCODING } from './protocol.js'
3
+ export { BrowserError } from './protocol.js'
19
4
 
5
+ function createClient(suppliedWorker) {
6
+ if (!globalThis.Worker) throw new BrowserError('RUNTIME_UNSUPPORTED', 'RedDB requires module Workers and WebAssembly')
20
7
  if (suppliedWorker !== undefined && !(suppliedWorker instanceof Worker)) {
21
8
  throw new BrowserError('INVALID_ARGUMENT', 'worker must be a dedicated Worker instance')
22
9
  }
@@ -24,56 +11,140 @@ export async function openArtifact({ name, worker: suppliedWorker } = {}) {
24
11
  const pending = new Map()
25
12
  let sequence = 0
26
13
  let closed = false
14
+ let terminated = false
27
15
 
28
- function fail(error) {
16
+ function shutdown(error = new BrowserError('CLOSED', 'This runtime handle is closed')) {
29
17
  closed = true
30
18
  for (const request of pending.values()) request.reject(error)
31
19
  pending.clear()
32
- worker.terminate()
20
+ if (!terminated) worker.terminate()
21
+ terminated = true
33
22
  }
34
23
 
35
24
  worker.onmessage = ({ data }) => {
25
+ if (terminated) return
26
+ if (data?.abiVersion !== RUNTIME_ABI_VERSION) {
27
+ shutdown(new BrowserError('VERSION_MISMATCH', 'Worker runtime ABI does not match this SDK'))
28
+ return
29
+ }
30
+ if (!Number.isSafeInteger(data.id) || data.id < 1) {
31
+ shutdown(new BrowserError('WORKER_FAILED', 'Invalid runtime response identifier'))
32
+ return
33
+ }
36
34
  const request = pending.get(data.id)
37
35
  if (!request) return
38
36
  pending.delete(data.id)
39
37
  if (data.error) request.reject(new BrowserError(data.error.code, data.error.message))
40
38
  else request.resolve(data.result)
41
39
  }
42
- worker.onerror = (event) => fail(new BrowserError('WORKER_FAILED', event.message || 'RedDB storage worker failed'))
43
- worker.onmessageerror = () => fail(new BrowserError('WORKER_FAILED', 'Unable to decode a storage worker response'))
40
+ worker.onerror = (event) => shutdown(new BrowserError('WORKER_FAILED', event.message || 'RedDB worker failed'))
41
+ worker.onmessageerror = () => shutdown(new BrowserError('WORKER_FAILED', 'Unable to decode a worker response'))
44
42
 
45
43
  function call(method, params = {}) {
46
- if (closed) return Promise.reject(new BrowserError('CLOSED', 'This artifact handle is closed'))
44
+ if (closed) return Promise.reject(new BrowserError('CLOSED', 'This runtime handle is closed'))
47
45
  const id = ++sequence
48
46
  return new Promise((resolve, reject) => {
49
47
  pending.set(id, { resolve, reject })
50
- try { worker.postMessage({ id, method, params }) }
48
+ try { worker.postMessage({ abiVersion: RUNTIME_ABI_VERSION, id, method, params }) }
51
49
  catch (error) { pending.delete(id); reject(error) }
52
50
  })
53
51
  }
54
52
 
55
- try { await call('open', { name }) }
56
- catch (error) { fail(error); throw error }
57
-
53
+ const ready = call('initialize').then(info => {
54
+ if (info?.abiVersion !== RUNTIME_ABI_VERSION || info.valueEncoding !== VALUE_ENCODING
55
+ || info.capabilities?.queryValidation !== true || info.capabilities?.scalarQueries !== true) {
56
+ throw new BrowserError('VERSION_MISMATCH', 'WASM runtime capabilities do not match this SDK')
57
+ }
58
+ return info
59
+ }).catch(error => { shutdown(error); throw error })
60
+ ready.catch(() => {}) // A bridge can be closed before its first invocation.
58
61
  let closing
59
62
  return {
60
- validateQuery: (sql) => call('validateQuery', { sql }),
61
- executeScalar: (sql) => call('executeScalar', { sql }),
62
- generation: () => call('generation'),
63
- readWal: () => call('readWal'),
64
- readSnapshot: () => call('readSnapshot'),
65
- append: (payloads) => call('append', { payloads }),
66
- checkpoint: (snapshot, minWalBytes = 0) => call('checkpoint', { snapshot, minWalBytes }),
67
- close() {
63
+ ready, call, shutdown,
64
+ closeFile() {
68
65
  if (!closing) {
69
- closing = closed ? Promise.resolve() : call('close').finally(() => {
70
- closed = true
71
- worker.terminate()
72
- })
73
- // Reject calls issued after close(), including calls in the same turn.
66
+ closing = closed ? Promise.resolve() : call('close').finally(() => shutdown())
74
67
  closed = true
75
68
  }
76
69
  return closing
77
70
  },
78
71
  }
79
72
  }
73
+
74
+ function runtimeMethods(client) {
75
+ const query = (method, sql) => typeof sql === 'string'
76
+ ? client.call(method, { sql })
77
+ : Promise.reject(new BrowserError('INVALID_ARGUMENT', 'Expected an RQL string'))
78
+ return {
79
+ info: () => client.call('info'),
80
+ validateQuery: sql => query('validateQuery', sql),
81
+ executeScalar: sql => query('executeScalar', sql),
82
+ }
83
+ }
84
+
85
+ /** Storage-independent Rust/WASM operations; does not open an OPFS file. */
86
+ export async function createRuntime({ worker } = {}) {
87
+ const client = createClient(worker)
88
+ await client.ready
89
+ return { ...runtimeMethods(client), close() { client.shutdown(); return Promise.resolve() } }
90
+ }
91
+
92
+ /** JSON-safe boundary for Dart, .NET and other JS-interop hosts. */
93
+ export function createInteropRuntime({ worker } = {}) {
94
+ let client
95
+ let ready
96
+ let closed = false
97
+ try {
98
+ client = createClient(worker)
99
+ ready = client.ready.then(() => runtimeMethods(client))
100
+ } catch (error) { ready = Promise.reject(error) }
101
+ ready.catch(() => {})
102
+ return {
103
+ async invoke(method, params = {}) {
104
+ try {
105
+ if (closed) throw new BrowserError('CLOSED', 'This runtime handle is closed')
106
+ const runtime = await ready
107
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
108
+ throw new BrowserError('INVALID_ARGUMENT', 'Expected an argument object')
109
+ }
110
+ let result
111
+ switch (method) {
112
+ case 'info': result = await runtime.info(); break
113
+ case 'validateQuery': result = await runtime.validateQuery(params.sql); break
114
+ case 'executeScalar': result = await runtime.executeScalar(params.sql); break
115
+ default: throw new BrowserError('INVALID_ARGUMENT', 'Unknown runtime operation')
116
+ }
117
+ return { ok: true, result: result ?? null }
118
+ } catch (error) {
119
+ return { ok: false, error: {
120
+ code: typeof error.code === 'string' ? error.code : 'RUNTIME_ERROR',
121
+ message: error.message || String(error),
122
+ } }
123
+ }
124
+ },
125
+ close() { closed = true; client?.shutdown() },
126
+ }
127
+ }
128
+
129
+ /** Open an exclusively owned, canonical .rdb artifact in an OPFS worker. */
130
+ export async function openArtifact({ name, worker } = {}) {
131
+ if (typeof name !== 'string' || !/^[a-zA-Z0-9_-][a-zA-Z0-9_.-]{0,79}$/.test(name)) {
132
+ throw new BrowserError('INVALID_NAME', 'Artifact names must contain 1–80 letters, digits, dots, underscores or hyphens')
133
+ }
134
+ if (!globalThis.isSecureContext || !globalThis.Worker || !globalThis.navigator?.storage?.getDirectory || !globalThis.navigator?.locks) {
135
+ throw new BrowserError('STORAGE_UNSUPPORTED', 'RedDB Browser requires HTTPS, module Workers, OPFS and Web Locks')
136
+ }
137
+ const client = createClient(worker)
138
+ try { await client.ready; await client.call('open', { name }) }
139
+ catch (error) { client.shutdown(error); throw error }
140
+ const { call } = client
141
+ return {
142
+ ...runtimeMethods(client),
143
+ generation: () => call('generation'),
144
+ readWal: () => call('readWal'),
145
+ readSnapshot: () => call('readSnapshot'),
146
+ append: (payloads) => call('append', { payloads }),
147
+ checkpoint: (snapshot, minWalBytes = 0) => call('checkpoint', { snapshot, minWalBytes }),
148
+ close: client.closeFile,
149
+ }
150
+ }
@@ -0,0 +1,10 @@
1
+ export const RUNTIME_ABI_VERSION = 1
2
+ export const VALUE_ENCODING = 'reddb-json-v1'
3
+
4
+ export class BrowserError extends Error {
5
+ constructor(code, message) {
6
+ super(message)
7
+ this.name = 'BrowserError'
8
+ this.code = code
9
+ }
10
+ }
@@ -26,6 +26,11 @@ export class BrowserArtifact {
26
26
  */
27
27
  export function executeScalar(sql: string): any;
28
28
 
29
+ /**
30
+ * Contract shared by the JS worker and foreign-language browser adapters.
31
+ */
32
+ export function runtimeInfo(): any;
33
+
29
34
  export function validateQuery(sql: string): void;
30
35
 
31
36
  export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
@@ -41,6 +46,7 @@ export interface InitOutput {
41
46
  readonly browserartifact_readSnapshot: (a: number) => [number, number, number, number];
42
47
  readonly browserartifact_readWal: (a: number) => [number, number, number];
43
48
  readonly executeScalar: (a: number, b: number) => [number, number, number];
49
+ readonly runtimeInfo: () => [number, number, number];
44
50
  readonly validateQuery: (a: number, b: number) => [number, number];
45
51
  readonly rust_zstd_wasm_shim_calloc: (a: number, b: number) => number;
46
52
  readonly rust_zstd_wasm_shim_free: (a: number) => void;
@@ -136,6 +136,18 @@ export function executeScalar(sql) {
136
136
  return takeFromExternrefTable0(ret[0]);
137
137
  }
138
138
 
139
+ /**
140
+ * Contract shared by the JS worker and foreign-language browser adapters.
141
+ * @returns {any}
142
+ */
143
+ export function runtimeInfo() {
144
+ const ret = wasm.runtimeInfo();
145
+ if (ret[2]) {
146
+ throw takeFromExternrefTable0(ret[1]);
147
+ }
148
+ return takeFromExternrefTable0(ret[0]);
149
+ }
150
+
139
151
  /**
140
152
  * @param {string} sql
141
153
  */
Binary file
@@ -10,6 +10,7 @@ export const browserartifact_open: (a: any, b: number, c: number) => [number, nu
10
10
  export const browserartifact_readSnapshot: (a: number) => [number, number, number, number];
11
11
  export const browserartifact_readWal: (a: number) => [number, number, number];
12
12
  export const executeScalar: (a: number, b: number) => [number, number, number];
13
+ export const runtimeInfo: () => [number, number, number];
13
14
  export const validateQuery: (a: number, b: number) => [number, number];
14
15
  export const rust_zstd_wasm_shim_calloc: (a: number, b: number) => number;
15
16
  export const rust_zstd_wasm_shim_free: (a: number) => void;
package/src/worker.js CHANGED
@@ -1,10 +1,26 @@
1
- import init, { BrowserArtifact, validateQuery, executeScalar } from './wasm/reddb_wasm.js'
1
+ import init, * as wasm from './wasm/reddb_wasm.js'
2
+ import { RUNTIME_ABI_VERSION, VALUE_ENCODING } from './protocol.js'
2
3
 
3
4
  let artifact
4
5
  let access
5
6
  let release
6
7
  let ownership
7
8
  let queue = Promise.resolve()
9
+ let initialized
10
+
11
+ function initialize() {
12
+ return initialized ??= (async () => {
13
+ await init()
14
+ if (typeof wasm.runtimeInfo !== 'function') throw error('VERSION_MISMATCH', 'WASM runtime metadata is missing')
15
+ let info
16
+ try { info = wasm.runtimeInfo() }
17
+ catch { throw error('VERSION_MISMATCH', 'WASM binary does not expose the expected runtime ABI') }
18
+ if (info.abiVersion !== RUNTIME_ABI_VERSION || info.valueEncoding !== VALUE_ENCODING) {
19
+ throw error('VERSION_MISMATCH', 'WASM runtime ABI does not match this worker')
20
+ }
21
+ return info
22
+ })()
23
+ }
8
24
 
9
25
  function error(code, message) {
10
26
  return Object.assign(new Error(message), { code })
@@ -19,7 +35,7 @@ function synchronous(value) {
19
35
  }
20
36
 
21
37
  async function open(name) {
22
- await init()
38
+ await initialize()
23
39
  await new Promise((resolve, reject) => {
24
40
  ownership = navigator.locks.request(`reddb-browser:${name}`, { ifAvailable: true }, async (lock) => {
25
41
  if (!lock) throw error('FILE_BUSY', 'This artifact is already open in another worker or tab')
@@ -39,8 +55,8 @@ async function open(name) {
39
55
  flush: () => synchronous(access.flush()),
40
56
  }
41
57
  artifact = fileIo.getSize() === 0
42
- ? BrowserArtifact.create(fileIo, name, Date.now())
43
- : BrowserArtifact.open(fileIo, name)
58
+ ? wasm.BrowserArtifact.create(fileIo, name, Date.now())
59
+ : wasm.BrowserArtifact.open(fileIo, name)
44
60
  const lifetime = new Promise((resolveRelease) => { release = resolveRelease })
45
61
  resolve()
46
62
  await lifetime
@@ -65,15 +81,15 @@ function bytes(value) {
65
81
  }
66
82
 
67
83
  async function execute(method, params) {
84
+ if (method === 'initialize' || method === 'info') return initialize()
68
85
  if (method === 'open') return open(params.name)
86
+ if (method === 'validateQuery' || method === 'executeScalar') {
87
+ await initialize()
88
+ if (typeof params.sql !== 'string') throw error('INVALID_ARGUMENT', 'Expected an RQL string')
89
+ return method === 'validateQuery' ? wasm.validateQuery(params.sql) : wasm.executeScalar(params.sql)
90
+ }
69
91
  if (!artifact) throw error('CLOSED', 'The artifact is not open')
70
92
  switch (method) {
71
- case 'validateQuery':
72
- if (typeof params.sql !== 'string') throw error('INVALID_ARGUMENT', 'Expected an RQL string')
73
- return validateQuery(params.sql)
74
- case 'executeScalar':
75
- if (typeof params.sql !== 'string') throw error('INVALID_ARGUMENT', 'Expected an RQL string')
76
- return executeScalar(params.sql)
77
93
  case 'generation': return artifact.generation()
78
94
  case 'readWal': return artifact.readWal()
79
95
  case 'readSnapshot': return artifact.readSnapshot()
@@ -93,14 +109,19 @@ async function execute(method, params) {
93
109
  }
94
110
  }
95
111
 
96
- self.onmessage = ({ data: { id, method, params } }) => {
112
+ self.onmessage = ({ data }) => {
113
+ const { id, method, params = {} } = data ?? {}
114
+ if (data?.abiVersion !== RUNTIME_ABI_VERSION) {
115
+ self.postMessage({ abiVersion: RUNTIME_ABI_VERSION, id, error: { code: 'VERSION_MISMATCH', message: 'SDK runtime ABI does not match this worker' } })
116
+ return
117
+ }
97
118
  // Opening is asynchronous; all following operations wait for ownership.
98
119
  queue = queue.then(async () => {
99
120
  try {
100
121
  const result = await execute(method, params)
101
- self.postMessage({ id, result })
122
+ self.postMessage({ abiVersion: RUNTIME_ABI_VERSION, id, result })
102
123
  } catch (failure) {
103
- self.postMessage({ id, error: { code: failure.code || 'ARTIFACT_ERROR', message: failure.message || String(failure) } })
124
+ self.postMessage({ abiVersion: RUNTIME_ABI_VERSION, id, error: { code: failure.code || 'ARTIFACT_ERROR', message: failure.message || String(failure) } })
104
125
  }
105
126
  })
106
127
  }