@reddb-io/browser 0.1.0-alpha.0 → 0.1.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.
- package/README.md +80 -7
- package/package.json +7 -5
- package/scripts/verify-dist.mjs +1 -1
- package/src/index.d.ts +38 -2
- package/src/index.js +111 -40
- package/src/protocol.js +10 -0
- package/src/wasm/reddb_wasm.d.ts +6 -0
- package/src/wasm/reddb_wasm.js +12 -0
- package/src/wasm/reddb_wasm_bg.wasm +0 -0
- package/src/wasm/reddb_wasm_bg.wasm.d.ts +1 -0
- package/src/worker.js +34 -13
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# RedDB Browser —
|
|
1
|
+
# RedDB Browser — persistence and query engine
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
canonical Rust `.rdb` artifact publication/recovery code, RQL parser and shared
|
|
3
|
+
The `@reddb-io/browser` package runs the canonical Rust `.rdb` artifact publication/recovery code, RQL parser and shared
|
|
5
4
|
scalar evaluator in a dedicated WebAssembly worker, persisting through a real
|
|
6
5
|
OPFS sync access handle.
|
|
7
|
-
|
|
6
|
+
Artifact storage requires a secure context, module workers, OPFS and Web Locks.
|
|
7
|
+
The storage-independent runtime only needs module workers and WebAssembly.
|
|
8
8
|
|
|
9
9
|
The target is a fully offline local database with a JavaScript/TypeScript API.
|
|
10
10
|
The Rust/WASM engine ships with the package; local operation must not require a
|
|
@@ -14,7 +14,7 @@ application cache. Sync is optional and must never gate local reads or writes.
|
|
|
14
14
|
|
|
15
15
|
**It is not a database SDK yet.** It executes storage-free scalar SELECTs;
|
|
16
16
|
queries over stored data, five-model local transactions, synchronization,
|
|
17
|
-
conflict resolution and forwarding between tabs remain pending. It is published on the `
|
|
17
|
+
conflict resolution and forwarding between tabs remain pending. It is published on the `latest` npm channel and does not replace the remote `@reddb-io/client`.
|
|
18
18
|
|
|
19
19
|
## Use from JS/TS
|
|
20
20
|
|
|
@@ -22,7 +22,7 @@ See the [application guide](https://github.com/reddb-io/reddb/blob/main/docs/cli
|
|
|
22
22
|
Vite/React/Vue/Svelte, plain JavaScript, Worker ownership and offline assets.
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
|
-
npm add @reddb-io/browser
|
|
25
|
+
npm add @reddb-io/browser
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
```ts
|
|
@@ -40,6 +40,45 @@ try {
|
|
|
40
40
|
Consumers receive built JS/Worker/WASM assets and TypeScript declarations; they
|
|
41
41
|
need no Rust toolchain. The commands below are for engine/package contributors.
|
|
42
42
|
|
|
43
|
+
## Shared runtime and language interop
|
|
44
|
+
|
|
45
|
+
`createRuntime({ worker? })` opens no file and requires neither OPFS nor Web
|
|
46
|
+
Locks. It exposes `info`, `validateQuery`, `executeScalar` and `close`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createRuntime } from '@reddb-io/browser'
|
|
50
|
+
import RedDBWorker from '@reddb-io/browser/worker?worker'
|
|
51
|
+
const runtime = await createRuntime({ worker: new RedDBWorker() })
|
|
52
|
+
try {
|
|
53
|
+
console.log(await runtime.info())
|
|
54
|
+
console.log(await runtime.executeScalar('SELECT 6 * 7 AS answer'))
|
|
55
|
+
} finally { await runtime.close() }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The versioned Worker/WASM contract reports `abiVersion: 1`,
|
|
59
|
+
`valueEncoding: 'reddb-json-v1'` and capabilities. Currently `queryValidation`
|
|
60
|
+
and `scalarQueries` are true; `storedQueries`, `transactions` and `sync` are
|
|
61
|
+
false. Mismatched SDK/Worker/WASM versions fail with `VERSION_MISMATCH` before
|
|
62
|
+
opening storage. Cache the JS, Worker and WASM as one versioned unit.
|
|
63
|
+
|
|
64
|
+
`createInteropRuntime({ worker? })` returns a synchronous bridge object with
|
|
65
|
+
`invoke(method, params?)` and `close()`. `invoke` accepts `info`, `validateQuery`
|
|
66
|
+
and `executeScalar` (queries take `{ sql }`) and resolves JSON-safe envelopes:
|
|
67
|
+
`{ ok: true, result }` or `{ ok: false, error: { code, message } }`.
|
|
68
|
+
A void result is `null`; exact integer/decimal tags are unchanged. `close()` is
|
|
69
|
+
idempotent and cancels pending initialization/operations. Calls after close
|
|
70
|
+
return `CLOSED`. Each bridge owns a dedicated Worker.
|
|
71
|
+
|
|
72
|
+
Source adapters use this same bridge for
|
|
73
|
+
[Dart/Flutter Web](https://github.com/reddb-io/reddb/blob/main/drivers/dart/BROWSER.md) and
|
|
74
|
+
[C#/Blazor](https://github.com/reddb-io/reddb/blob/main/drivers/dotnet/src/Reddb.Browser/README.md). They are not yet
|
|
75
|
+
published on pub.dev/NuGet. Their real compiled browser probes exercise scalar
|
|
76
|
+
execution, exact values, errors and disposal online and offline. Full offline
|
|
77
|
+
browser restarts are verified on Chromium/Firefox; Playwright WPE persistent
|
|
78
|
+
profiles lack working CacheStorage, so Safari/iOS qualification remains pending. Run `pnpm --filter @reddb-io/browser build:interop` followed by
|
|
79
|
+
`pnpm --filter @reddb-io/browser test:interop` with Dart 3.13.7 and .NET 8.0.426.
|
|
80
|
+
These adapters do not expose artifact storage or qualify complete offline apps.
|
|
81
|
+
|
|
43
82
|
## Build and verify
|
|
44
83
|
|
|
45
84
|
Install the repository-pinned Rust toolchain, its `wasm32-unknown-unknown` target,
|
|
@@ -145,7 +184,7 @@ these primitives do not promise that a browser will grant it or sync after closi
|
|
|
145
184
|
## Offline asset loading
|
|
146
185
|
|
|
147
186
|
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
|
|
187
|
+
PWA should include `index.js`, `protocol.js`, `worker.js`, `wasm/reddb_wasm.js` and
|
|
149
188
|
`wasm/reddb_wasm_bg.wasm` from `src/` in its own versioned application cache,
|
|
150
189
|
along with its application shell. Those assets must remain available together
|
|
151
190
|
after installation. A locally packaged host can serve the same assets locally.
|
|
@@ -245,6 +284,40 @@ artifact operations and storage-free scalar SELECTs; stored-data dispatch,
|
|
|
245
284
|
durable runtime transactions/WAL and application collection catalogs remain
|
|
246
285
|
unfinished. No file format, wire envelope or public browser API changes here.
|
|
247
286
|
|
|
287
|
+
## Shared resident collection storage
|
|
288
|
+
|
|
289
|
+
The growing/sealed segment, typed metadata, key/unique-key and physical graph
|
|
290
|
+
candidate indexes, and resident payload accounting now live in
|
|
291
|
+
`reddb-engine::storage`. Native paths re-export the same implementations. The
|
|
292
|
+
native segment manager retains lifecycle, consolidation, explicit snapshot and
|
|
293
|
+
moderation gates. Storage clocks use Unix seconds on both native and WASM hosts.
|
|
294
|
+
Four shared conformance cases exercise all original record-oracle entity shapes,
|
|
295
|
+
key-version lookup and raw mutation, bounded/cancelled scans, metadata and sealing;
|
|
296
|
+
the browser worker repeats them after the existing offline restart.
|
|
297
|
+
|
|
298
|
+
These are canonical resident storage operations, not durable application
|
|
299
|
+
transactions. WAL publication, catalog dispatch and the application SDK still
|
|
300
|
+
need integration before JS/TS can query and mutate persisted collections. The
|
|
301
|
+
production artifact/scalar API and all file formats remain unchanged.
|
|
302
|
+
|
|
303
|
+
## Shared snapshot recovery
|
|
304
|
+
|
|
305
|
+
Native and WASM now use the same typed metadata/entity-record codecs and streaming
|
|
306
|
+
V1–V12 dump reader. Recovery preserves empty collection registration, record
|
|
307
|
+
metadata, cross-references and the optional V10+ catalog blob. The shared file
|
|
308
|
+
layer verifies the checksum before any restore callback and lends the payload
|
|
309
|
+
without copying the entire snapshot. Hosts must restore into private state and
|
|
310
|
+
publish it only after successful recovery.
|
|
311
|
+
|
|
312
|
+
Frozen pre-extraction native metadata bytes and entity fixtures check compatible
|
|
313
|
+
encoding and decoding. Shared native/WASM cases also cover truncated metadata,
|
|
314
|
+
invalid counts/tags/UTF-8, nesting at the RQL depth limit, checksum failures,
|
|
315
|
+
legacy optional tails and host callback errors. Metadata nesting above the existing
|
|
316
|
+
RQL JSON limit is now rejected; previously the native metadata reader was unbounded.
|
|
317
|
+
The native host retains collection/index restoration, ID allocation and WAL policy.
|
|
318
|
+
These internal recovery primitives do not yet expose stored-data queries or
|
|
319
|
+
application transactions through the browser SDK.
|
|
320
|
+
|
|
248
321
|
## Remaining work toward the approved Browser design
|
|
249
322
|
|
|
250
323
|
1. Extract the remaining storage-coupled executor/runtime into the shared portable engine and
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/browser",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "RedDB browser persistence and shared scalar execution; stored-data queries and sync remain pending.",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./src/index.d.ts",
|
|
@@ -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",
|
|
@@ -39,6 +41,6 @@
|
|
|
39
41
|
],
|
|
40
42
|
"publishConfig": {
|
|
41
43
|
"access": "public",
|
|
42
|
-
"tag": "
|
|
44
|
+
"tag": "latest"
|
|
43
45
|
}
|
|
44
46
|
}
|
package/scripts/verify-dist.mjs
CHANGED
|
@@ -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
|
@@ -1,14 +1,49 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Persistence and scalar execution; stored-data queries and sync remain pending. */
|
|
2
2
|
export class BrowserError extends Error {
|
|
3
3
|
readonly code: string
|
|
4
4
|
constructor(code: string, message: string)
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export interface
|
|
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
|
-
/**
|
|
2
|
-
|
|
3
|
-
export
|
|
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 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
|
|
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) =>
|
|
43
|
-
worker.onmessageerror = () =>
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
+
}
|
package/src/protocol.js
ADDED
package/src/wasm/reddb_wasm.d.ts
CHANGED
|
@@ -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;
|
package/src/wasm/reddb_wasm.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
}
|