@reddb-io/browser 0.0.0-stage → 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/src/index.js ADDED
@@ -0,0 +1,150 @@
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'
4
+
5
+ function createClient(suppliedWorker) {
6
+ if (!globalThis.Worker) throw new BrowserError('RUNTIME_UNSUPPORTED', 'RedDB requires module Workers and WebAssembly')
7
+ if (suppliedWorker !== undefined && !(suppliedWorker instanceof Worker)) {
8
+ throw new BrowserError('INVALID_ARGUMENT', 'worker must be a dedicated Worker instance')
9
+ }
10
+ const worker = suppliedWorker ?? new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })
11
+ const pending = new Map()
12
+ let sequence = 0
13
+ let closed = false
14
+ let terminated = false
15
+
16
+ function shutdown(error = new BrowserError('CLOSED', 'This runtime handle is closed')) {
17
+ closed = true
18
+ for (const request of pending.values()) request.reject(error)
19
+ pending.clear()
20
+ if (!terminated) worker.terminate()
21
+ terminated = true
22
+ }
23
+
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
+ }
34
+ const request = pending.get(data.id)
35
+ if (!request) return
36
+ pending.delete(data.id)
37
+ if (data.error) request.reject(new BrowserError(data.error.code, data.error.message))
38
+ else request.resolve(data.result)
39
+ }
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'))
42
+
43
+ function call(method, params = {}) {
44
+ if (closed) return Promise.reject(new BrowserError('CLOSED', 'This runtime handle is closed'))
45
+ const id = ++sequence
46
+ return new Promise((resolve, reject) => {
47
+ pending.set(id, { resolve, reject })
48
+ try { worker.postMessage({ abiVersion: RUNTIME_ABI_VERSION, id, method, params }) }
49
+ catch (error) { pending.delete(id); reject(error) }
50
+ })
51
+ }
52
+
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.
61
+ let closing
62
+ return {
63
+ ready, call, shutdown,
64
+ closeFile() {
65
+ if (!closing) {
66
+ closing = closed ? Promise.resolve() : call('close').finally(() => shutdown())
67
+ closed = true
68
+ }
69
+ return closing
70
+ },
71
+ }
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
+ }
@@ -0,0 +1,89 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+
4
+ /**
5
+ * Canonical `.rdb` artifact I/O. The worker owns and closes the underlying
6
+ * exclusive access handle; freeing this wrapper never unlocks it early.
7
+ */
8
+ export class BrowserArtifact {
9
+ private constructor();
10
+ free(): void;
11
+ [Symbol.dispose](): void;
12
+ append(payloads: Array<any>): void;
13
+ checkpoint(snapshot: Uint8Array, min_wal_bytes: number): void;
14
+ static create(handle: any, name: string, created_at_unix_ms: number): BrowserArtifact;
15
+ /**
16
+ * String-valued counters preserve all u64 bits across JavaScript.
17
+ */
18
+ generation(): string;
19
+ static open(handle: any, name: string): BrowserArtifact;
20
+ readSnapshot(): Uint8Array | undefined;
21
+ readWal(): Array<any>;
22
+ }
23
+
24
+ /**
25
+ * Experimental storage-free execution, using the native scalar evaluator.
26
+ */
27
+ export function executeScalar(sql: string): any;
28
+
29
+ /**
30
+ * Contract shared by the JS worker and foreign-language browser adapters.
31
+ */
32
+ export function runtimeInfo(): any;
33
+
34
+ export function validateQuery(sql: string): void;
35
+
36
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
37
+
38
+ export interface InitOutput {
39
+ readonly memory: WebAssembly.Memory;
40
+ readonly __wbg_browserartifact_free: (a: number, b: number) => void;
41
+ readonly browserartifact_append: (a: number, b: any) => [number, number];
42
+ readonly browserartifact_checkpoint: (a: number, b: number, c: number, d: number) => [number, number];
43
+ readonly browserartifact_create: (a: any, b: number, c: number, d: number) => [number, number, number];
44
+ readonly browserartifact_generation: (a: number) => [number, number, number, number];
45
+ readonly browserartifact_open: (a: any, b: number, c: number) => [number, number, number];
46
+ readonly browserartifact_readSnapshot: (a: number) => [number, number, number, number];
47
+ readonly browserartifact_readWal: (a: number) => [number, number, number];
48
+ readonly executeScalar: (a: number, b: number) => [number, number, number];
49
+ readonly runtimeInfo: () => [number, number, number];
50
+ readonly validateQuery: (a: number, b: number) => [number, number];
51
+ readonly rust_zstd_wasm_shim_calloc: (a: number, b: number) => number;
52
+ readonly rust_zstd_wasm_shim_free: (a: number) => void;
53
+ readonly rust_zstd_wasm_shim_malloc: (a: number) => number;
54
+ readonly rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number;
55
+ readonly rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number;
56
+ readonly rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number;
57
+ readonly rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number;
58
+ readonly rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
59
+ readonly __wbindgen_malloc: (a: number, b: number) => number;
60
+ readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
61
+ readonly __wbindgen_exn_store: (a: number) => void;
62
+ readonly __externref_table_alloc: () => number;
63
+ readonly __wbindgen_externrefs: WebAssembly.Table;
64
+ readonly __externref_table_dealloc: (a: number) => void;
65
+ readonly __wbindgen_free: (a: number, b: number, c: number) => void;
66
+ readonly __wbindgen_start: () => void;
67
+ }
68
+
69
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
70
+
71
+ /**
72
+ * Instantiates the given `module`, which can either be bytes or
73
+ * a precompiled `WebAssembly.Module`.
74
+ *
75
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
76
+ *
77
+ * @returns {InitOutput}
78
+ */
79
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
80
+
81
+ /**
82
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
83
+ * for everything else, calls `WebAssembly.instantiate` directly.
84
+ *
85
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
86
+ *
87
+ * @returns {Promise<InitOutput>}
88
+ */
89
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;