@surrealdb/wasm-native 3.3.4 → 4.0.0-beta.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
@@ -61,12 +61,80 @@ engine.free();
61
61
  | Export | Description |
62
62
  | --- | --- |
63
63
  | `init` | Instantiates the module; must resolve before anything else is called |
64
- | `SurrealWasmEngine` | One embedded instance: `connect`, `execute`, `notifications`, `export`, `import`, `version`, `free` |
64
+ | `SurrealWasmEngine` | One embedded instance: `connect`, `execute`, `notifications`, `export`, `import`, `version`, `close`, `persisted`, `free` |
65
65
  | `ConnectionOptions` | Capabilities, timeouts, and the namespace/database created on new storage |
66
+ | `./worker` | `connect(endpoint, options)`: starts a dedicated worker, hosts the engine in it and returns a `WorkerEngine` with the same methods, for `surrealkv://` |
66
67
  | `./module` | The `.wasm` file itself, for a caller that resolves the module source itself |
67
68
 
68
- Storage backends are selected by the endpoint: `mem://` and `indxdb://name`, the latter backed by
69
- the browser's IndexedDB.
69
+ Storage backends are selected by the endpoint: `mem://` and `surrealkv://name`, the latter backed by
70
+ SurrealKV over the browser's Origin Private File System (OPFS). The two are covered in
71
+ [Persistent storage in the browser](#persistent-storage-in-the-browser).
72
+
73
+ ### Persistent storage in the browser
74
+
75
+ `surrealkv://name` keeps the database in the origin private file system, in a database called
76
+ `name` that belongs to the origin. `mem://` keeps nothing and works on any thread.
77
+
78
+ **The module must run in a dedicated worker.** The synchronous file handles that SurrealKV needs
79
+ exist only in dedicated workers, so a `surrealkv://` database cannot be opened from the main
80
+ thread of a page, from a shared worker or from a service worker. `SurrealWasmEngine.connect`
81
+ rejects such a call before it touches storage. The `./worker` export is the bridge: `connect` starts a dedicated worker, loads this module in it
82
+ (`dist/worker.js` beside the loader), connects the engine there and returns a `WorkerEngine` whose
83
+ `execute`, `queryStream`, `notifications`, `export`, `import`, `version`, `persisted` and `close`
84
+ mirror the engine's. Byte payloads are copied or transferred and `ReadableStream`s are transferred,
85
+ so a reader that stops reading still stops the engine's producer.
86
+
87
+ ```ts
88
+ import { connect } from "@surrealdb/wasm-native/worker";
89
+
90
+ const engine = await connect("surrealkv://my-database");
91
+ // ... engine.execute(request) ...
92
+ await engine.close(); // before the application ends
93
+ ```
94
+
95
+ A caller that hosts the module in its own worker uses `SurrealWasmEngine` directly: it calls `init`
96
+ and `connect` there and carries requests, replies and notifications by message itself. The module needs no threads, no async runtime and no `SharedArrayBuffer`.
97
+
98
+ **One tab per database.** SurrealKV takes an exclusive lock on the database. A second tab (or
99
+ worker) that connects to the same name waits a few seconds for a holder that is closing and then
100
+ `connect` rejects with a message that says the database is open in another tab or worker. The engine does not retry or take the lock over: the
101
+ page decides, for example by telling the user to close the other tab, or by connecting again later.
102
+ After a reload without a `close()` the browser can take longer than the wait to release the lock,
103
+ so the same rejection can appear for a short time.
104
+
105
+ **Close the engine when the application ends.** A worker cannot wait for an asynchronous close
106
+ inside a page-unload handler. Call `await engine.close()` as early as the application knows it is
107
+ ending, for example when the page sends a message to the worker. On a `WorkerEngine`, `close()`
108
+ closes the datastore, frees the engine and ends the worker. On a `SurrealWasmEngine`, call
109
+ `await engine.close()` and then `engine.free()`. `close()` closes the SurrealKV tree and then
110
+ releases its files and the lock; it takes longer the more files the database has. A
111
+ `SurrealWasmEngine` that is freed without `close()` closes its datastore on a task of its own, so the
112
+ lock is released shortly after `free()` returns rather than before. A database that was not closed is recovered the next time it is
113
+ opened. Nothing may use the engine after `close()` resolves, other than `free()`.
114
+
115
+ **Ask for persistent storage from the page.** Unless the origin has been granted persistent
116
+ storage, the browser may evict its data when the disk fills. Only a page can ask: call
117
+ `navigator.storage.persist()` there. Inside the worker, `await SurrealWasmEngine.persisted()`
118
+ reports whether the request was granted; it resolves to `false` until then.
119
+
120
+ **Data from the browser's earlier storage moves over by itself.** At every `surrealkv://name` connect,
121
+ before the database is opened, the module copies the rows of an earlier browser database called
122
+ `name` into the origin private file system, unless a database `name` exists there already. The
123
+ earlier database is only read: it is never changed or deleted, and the application removes it with
124
+ `indexedDB.deleteDatabase(name)` once it has checked the copy. The copy is verified, and one that
125
+ was interrupted is made again at the next connect, which then opens no database until the copy
126
+ completes. A copy that fails makes `connect` reject, and the database is not opened.
127
+
128
+ Limits: Chrome is the only browser that is tested. Encryption, checkpoints and the migrators of
129
+ earlier engines are not available in the browser. `Durability` in a browser is an ordering guarantee of
130
+ the file handle's `flush()` and not a promise about a power cut or an operating-system crash. A
131
+ worker has an address space of at most 4 GiB, and the default memtable and block cache sizes
132
+ assume the 1 GiB that is assumed when the memory of the host cannot be read.
133
+
134
+ The `browser/` directory holds two pages that exercise this in a browser: `smoke.html` drives the
135
+ `./worker` bridge (persistence across close and reopen, the second-tab rejection) and `drop.html`
136
+ frees an engine without closing it and reconnects. Build, serve this directory with any static
137
+ server, and open them in Chrome; each leaves its outcome in `window.__results` or `window.__drop`.
70
138
 
71
139
  ### Where `init` reads the module from
72
140
 
@@ -110,7 +178,7 @@ tick, which a browser does not notice but which keeps a Node or Bun process from
110
178
  ## Requirements
111
179
 
112
180
  - ES modules (`import`) — CommonJS (`require`) is not supported
113
- - A browser, or any host with `WebAssembly`; `indxdb://` additionally needs IndexedDB
181
+ - A browser, or any host with `WebAssembly`; `surrealkv://` additionally needs a dedicated worker with the origin private file system
114
182
 
115
183
  ## Contributing
116
184
 
@@ -130,6 +198,17 @@ rustup target add wasm32-unknown-unknown
130
198
  cargo install wasm-opt --version 0.116.1
131
199
  ```
132
200
 
201
+ SurrealKV links zstd, which is C code, so the build needs a C compiler and an archiver that target
202
+ WebAssembly. On macOS the compiler and archiver that ship with the system do not, so use LLVM's:
203
+
204
+ ```sh
205
+ brew install llvm
206
+ export CC_wasm32_unknown_unknown="$(brew --prefix llvm)/bin/clang"
207
+ export AR_wasm32_unknown_unknown="$(brew --prefix llvm)/bin/llvm-ar"
208
+ ```
209
+
210
+ On Linux, `clang` and `llvm-ar` serve the same purpose.
211
+
133
212
  `wasm-bindgen-cli` has to match the `wasm-bindgen` version in the workspace lockfile exactly —
134
213
  the two share a schema that changes between patch releases. `bun run build` checks this before
135
214
  it does anything and prints the `cargo install` line to run, so there is no version to copy from
@@ -147,5 +226,5 @@ The Rust half is a normal member of the Cargo workspace, but it compiles to noth
147
226
  the target explicitly:
148
227
 
149
228
  ```sh
150
- cargo clippy -p surrealdb-wasm --features kv-mem,kv-indxdb --target wasm32-unknown-unknown
229
+ cargo clippy -p surrealdb-wasm --features kv-mem,kv-surrealkv --target wasm32-unknown-unknown
151
230
  ```
@@ -0,0 +1,39 @@
1
+ import type { ConnectionOptions } from "./index.js";
2
+
3
+ export type { ConnectionOptions };
4
+
5
+ export type ConnectOptions = {
6
+ /** Where the engine's options go; see the module's `ConnectionOptions`. */
7
+ options?: ConnectionOptions;
8
+ /** The worker script. Defaults to `worker.js` beside this module. */
9
+ workerUrl?: string | URL;
10
+ /** Where the worker reads the `.wasm` from. Defaults to the loader's own location. */
11
+ module?: string | URL;
12
+ };
13
+
14
+ /** One embedded engine, hosted in a dedicated worker. */
15
+ export declare class WorkerEngine {
16
+ /** Runs one RPC request. */
17
+ execute(data: Uint8Array): Promise<Uint8Array>;
18
+ /** Runs a query, answering with its frames as they are produced. */
19
+ queryStream(data: Uint8Array): Promise<ReadableStream<Uint8Array>>;
20
+ /** The live-query notifications of this connection; one stream per engine. */
21
+ notifications(): ReadableStream<Uint8Array>;
22
+ export(config?: Uint8Array): Promise<string>;
23
+ import(input: string): Promise<void>;
24
+ version(): Promise<string>;
25
+ /** Whether the browser keeps this origin's storage until the user clears it. */
26
+ persisted(): Promise<boolean>;
27
+ /**
28
+ * Closes the datastore, frees the engine and ends the worker. When this
29
+ * resolves the storage lock is released.
30
+ */
31
+ close(): Promise<void>;
32
+ [Symbol.asyncDispose](): Promise<void>;
33
+ }
34
+
35
+ /**
36
+ * Starts a dedicated worker, loads the engine in it and connects to
37
+ * `endpoint`.
38
+ */
39
+ export declare function connect(endpoint: string, options?: ConnectOptions): Promise<WorkerEngine>;
package/dist/client.js ADDED
@@ -0,0 +1,102 @@
1
+ // ts/client.ts
2
+ class WorkerEngine {
3
+ #worker;
4
+ #next = 0;
5
+ #pending = new Map;
6
+ #notifications;
7
+ #closed = false;
8
+ #closing;
9
+ constructor(worker) {
10
+ this.#worker = worker;
11
+ worker.onmessage = (event) => {
12
+ const reply = event.data;
13
+ const pending = this.#pending.get(reply.id);
14
+ if (!pending)
15
+ return;
16
+ this.#pending.delete(reply.id);
17
+ if (reply.ok)
18
+ pending.resolve(reply.value);
19
+ else
20
+ pending.reject(new Error(reply.error));
21
+ };
22
+ worker.onerror = (event) => this.#fail(new Error(event.message || "The worker failed"));
23
+ worker.onmessageerror = () => this.#fail(new Error("The worker sent an unreadable message"));
24
+ }
25
+ #fail(error) {
26
+ this.#closed = true;
27
+ this.#worker.terminate();
28
+ for (const pending of this.#pending.values())
29
+ pending.reject(error);
30
+ this.#pending.clear();
31
+ }
32
+ call(request, transfer = []) {
33
+ if (this.#closed)
34
+ return Promise.reject(new Error("The engine is closed"));
35
+ return new Promise((resolve, reject) => {
36
+ const id = ++this.#next;
37
+ this.#pending.set(id, { resolve, reject });
38
+ this.#worker.postMessage({ ...request, id }, transfer);
39
+ });
40
+ }
41
+ execute(data) {
42
+ return this.call({ op: "execute", data });
43
+ }
44
+ queryStream(data) {
45
+ return this.call({ op: "queryStream", data });
46
+ }
47
+ notifications() {
48
+ if (!this.#notifications)
49
+ throw new Error("The engine is not connected");
50
+ return this.#notifications;
51
+ }
52
+ export(config) {
53
+ return this.call({ op: "export", config });
54
+ }
55
+ import(input) {
56
+ return this.call({ op: "import", input });
57
+ }
58
+ version() {
59
+ return this.call({ op: "version" });
60
+ }
61
+ persisted() {
62
+ return this.call({ op: "persisted" });
63
+ }
64
+ close() {
65
+ this.#closing ??= this.#close();
66
+ return this.#closing;
67
+ }
68
+ async#close() {
69
+ if (this.#closed)
70
+ return;
71
+ try {
72
+ await this.call({ op: "close" });
73
+ } finally {
74
+ this.#fail(new Error("The engine is closed"));
75
+ }
76
+ }
77
+ [Symbol.asyncDispose]() {
78
+ return this.close();
79
+ }
80
+ _setNotifications(stream) {
81
+ this.#notifications = stream;
82
+ }
83
+ }
84
+ async function connect(endpoint, { options, workerUrl, module } = {}) {
85
+ const worker = new Worker(workerUrl ?? new URL("./worker.js", import.meta.url), {
86
+ type: "module"
87
+ });
88
+ const engine = new WorkerEngine(worker);
89
+ try {
90
+ await engine.call({ op: "init", module: module === undefined ? undefined : String(module) });
91
+ await engine.call({ op: "connect", endpoint, options });
92
+ engine._setNotifications(await engine.call({ op: "notifications" }));
93
+ return engine;
94
+ } catch (error) {
95
+ await engine.close().catch(() => {});
96
+ throw error;
97
+ }
98
+ }
99
+ export {
100
+ WorkerEngine,
101
+ connect
102
+ };
package/dist/index.d.ts CHANGED
@@ -33,6 +33,7 @@ export type ConnectionOptions = {
33
33
  functions?: boolean | string[] | CapabilitiesAllowDenyList;
34
34
  network_targets?: boolean | string[] | CapabilitiesAllowDenyList;
35
35
  experimental?: boolean | string[] | CapabilitiesAllowDenyList;
36
+ /** @deprecated Accepted and ignored: the engine has a single execution path. */
36
37
  planner_strategy?: "best-effort" | "compute-only" | "all-read-only";
37
38
  };
38
39
  }
@@ -71,14 +72,33 @@ export class IntoUnderlyingSource {
71
72
  * An embedded SurrealDB instance, addressed over the RPC protocol.
72
73
  *
73
74
  * wasm-bindgen generates a `free()` for this, which is what the JavaScript
74
- * side calls to close the connection; dropping the engine closes the
75
+ * side calls to dispose of the connection; dropping the engine closes the
75
76
  * datastore's notification channel, which ends any stream handed out by
76
77
  * [`Self::notifications`].
78
+ *
79
+ * Dropping an engine that [`Self::close`] has not closed also closes its
80
+ * datastore, on a task of its own, since a drop cannot wait: the storage lock
81
+ * is released once that task finishes rather than by the time `free()`
82
+ * returns. A caller that needs the lock released at a known point awaits
83
+ * `close()` first.
77
84
  */
78
85
  export class SurrealWasmEngine {
79
86
  private constructor();
80
87
  free(): void;
81
88
  [Symbol.dispose](): void;
89
+ /**
90
+ * Closes the datastore: the storage engine is closed and then the files
91
+ * it holds are released, which frees the database's lock in the origin
92
+ * private file system so that another tab or worker can open it.
93
+ *
94
+ * A worker cannot await an asynchronous close from a page-unload
95
+ * handler, so the application calls this when it knows that it is ending,
96
+ * for example on a message from the page, and as early as it can: the
97
+ * more files the database has, the longer it takes. A database that is
98
+ * not closed is recovered the next time it is opened. Nothing may use
99
+ * the engine after this resolves, other than `free()`.
100
+ */
101
+ close(): Promise<void>;
82
102
  static connect(endpoint: string, opts?: ConnectionOptions | null): Promise<SurrealWasmEngine>;
83
103
  /**
84
104
  * Every method takes `&self`. With `&mut self` the wasm-bindgen trampoline
@@ -98,6 +118,16 @@ export class SurrealWasmEngine {
98
118
  * accumulating behind it.
99
119
  */
100
120
  notifications(): ReadableStream;
121
+ /**
122
+ * Whether the browser keeps this origin's storage until the user clears
123
+ * it, which is what stops it from evicting a `surrealkv://` database when
124
+ * the disk fills.
125
+ *
126
+ * It is `false` until the page has asked with `navigator.storage.persist()`,
127
+ * which a worker cannot call. Always `false` for a build without
128
+ * `surrealkv://` storage, which keeps nothing.
129
+ */
130
+ static persisted(): Promise<boolean>;
101
131
  /**
102
132
  * Runs a query, answering with frames as the executor produces them rather
103
133
  * than with every statement's result at once.
@@ -120,17 +150,10 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
120
150
 
121
151
  export interface InitOutput {
122
152
  readonly memory: WebAssembly.Memory;
123
- readonly __wbg_surrealwasmengine_free: (a: number, b: number) => void;
124
- readonly surrealwasmengine_connect: (a: number, b: number, c: number) => number;
125
- readonly surrealwasmengine_execute: (a: number, b: number, c: number) => number;
126
- readonly surrealwasmengine_export: (a: number, b: number, c: number) => number;
127
- readonly surrealwasmengine_import: (a: number, b: number, c: number) => number;
128
- readonly surrealwasmengine_notifications: (a: number) => number;
129
- readonly surrealwasmengine_query_stream: (a: number, b: number, c: number) => number;
130
- readonly surrealwasmengine_version: (a: number) => void;
131
153
  readonly __wbg_intounderlyingbytesource_free: (a: number, b: number) => void;
132
154
  readonly __wbg_intounderlyingsink_free: (a: number, b: number) => void;
133
155
  readonly __wbg_intounderlyingsource_free: (a: number, b: number) => void;
156
+ readonly __wbg_surrealwasmengine_free: (a: number, b: number) => void;
134
157
  readonly intounderlyingbytesource_autoAllocateChunkSize: (a: number) => number;
135
158
  readonly intounderlyingbytesource_cancel: (a: number) => void;
136
159
  readonly intounderlyingbytesource_pull: (a: number, b: number) => number;
@@ -141,11 +164,28 @@ export interface InitOutput {
141
164
  readonly intounderlyingsink_write: (a: number, b: number) => number;
142
165
  readonly intounderlyingsource_cancel: (a: number) => void;
143
166
  readonly intounderlyingsource_pull: (a: number, b: number) => number;
144
- readonly __wasm_bindgen_func_elem_29890: (a: number, b: number, c: number, d: number) => void;
145
- readonly __wasm_bindgen_func_elem_4538: (a: number, b: number, c: number, d: number) => void;
146
- readonly __wasm_bindgen_func_elem_29891: (a: number, b: number, c: number, d: number) => void;
147
- readonly __wasm_bindgen_func_elem_4606: (a: number, b: number, c: number, d: number) => void;
148
- readonly __wasm_bindgen_func_elem_8031: (a: number, b: number) => void;
167
+ readonly surrealwasmengine_close: (a: number) => number;
168
+ readonly surrealwasmengine_connect: (a: number, b: number, c: number) => number;
169
+ readonly surrealwasmengine_execute: (a: number, b: number, c: number) => number;
170
+ readonly surrealwasmengine_export: (a: number, b: number, c: number) => number;
171
+ readonly surrealwasmengine_import: (a: number, b: number, c: number) => number;
172
+ readonly surrealwasmengine_notifications: (a: number) => number;
173
+ readonly surrealwasmengine_persisted: () => number;
174
+ readonly surrealwasmengine_query_stream: (a: number, b: number, c: number) => number;
175
+ readonly surrealwasmengine_version: (a: number) => void;
176
+ readonly rust_zstd_wasm_shim_calloc: (a: number, b: number) => number;
177
+ readonly rust_zstd_wasm_shim_free: (a: number) => void;
178
+ readonly rust_zstd_wasm_shim_malloc: (a: number) => number;
179
+ readonly rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number;
180
+ readonly rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number;
181
+ readonly rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number;
182
+ readonly rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number;
183
+ readonly rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
184
+ readonly __wasm_bindgen_func_elem_5798: (a: number, b: number, c: number, d: number) => void;
185
+ readonly __wasm_bindgen_func_elem_5862: (a: number, b: number, c: number, d: number) => void;
186
+ readonly __wasm_bindgen_func_elem_46027: (a: number, b: number, c: number) => void;
187
+ readonly __wasm_bindgen_func_elem_46027_22: (a: number, b: number, c: number) => void;
188
+ readonly __wasm_bindgen_func_elem_9452: (a: number, b: number) => void;
149
189
  readonly __wbindgen_export: (a: number, b: number) => number;
150
190
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
151
191
  readonly __wbindgen_export3: (a: number) => void;