@slatedb/uniffi 0.12.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 ADDED
@@ -0,0 +1,211 @@
1
+ # SlateDB Node Binding
2
+
3
+ `bindings/node` contains the official Node.js package for SlateDB.
4
+
5
+ ## Install
6
+
7
+ Package:
8
+
9
+ ```text
10
+ @slatedb/uniffi
11
+ ```
12
+
13
+ Requirements:
14
+
15
+ - Node.js 20 or newer
16
+
17
+ Install from npm:
18
+
19
+ ```bash
20
+ npm install @slatedb/uniffi
21
+ ```
22
+
23
+ ## API Model
24
+
25
+ - `ObjectStore.resolve(...)` opens an object store from a URL such as `memory:///` or `file:///...`
26
+ - `DbBuilder` opens a writable database and `DbReaderBuilder` opens a read-only reader
27
+ - keys and values are binary; pass `Buffer` or `Uint8Array`
28
+ - most database operations are async and should be awaited
29
+ - builders are single-use; `Db` and `DbReader` stay open until `shutdown()` resolves
30
+ - native-backed handles also expose `dispose()` for deterministic cleanup after `shutdown()` or when abandoning a builder
31
+
32
+ ## Quick Start
33
+
34
+ ```js
35
+ import assert from "node:assert/strict";
36
+ import { DbBuilder, ObjectStore } from "@slatedb/uniffi";
37
+
38
+ async function main() {
39
+ const store = ObjectStore.resolve("memory:///");
40
+ let db;
41
+
42
+ try {
43
+ const builder = new DbBuilder("demo-db", store);
44
+ try {
45
+ db = await builder.build();
46
+ } finally {
47
+ builder.dispose();
48
+ }
49
+
50
+ const key = Buffer.from("hello");
51
+ const value = Buffer.from("world");
52
+
53
+ await db.put(key, value);
54
+
55
+ const read = await db.get(key);
56
+ assert.deepEqual(read, value);
57
+
58
+ console.log(Buffer.from(read).toString("utf8"));
59
+ } finally {
60
+ if (db != null) {
61
+ await db.shutdown();
62
+ db.dispose();
63
+ }
64
+ store.dispose();
65
+ }
66
+ }
67
+
68
+ main().catch((error) => {
69
+ console.error(error);
70
+ process.exitCode = 1;
71
+ });
72
+ ```
73
+
74
+ Replace `memory:///` with any object store URL supported by Rust's [`object_store`](https://docs.rs/object_store/latest/object_store/fn.parse_url_opts.html) crate.
75
+
76
+ ## Metrics
77
+
78
+ The Node binding exposes both application-provided metrics recorders and the built-in
79
+ `DefaultMetricsRecorder`:
80
+
81
+ - `DbBuilder.with_metrics_recorder(...)`
82
+ - `DbReaderBuilder.with_metrics_recorder(...)`
83
+ - `DefaultMetricsRecorder.snapshot()`
84
+ - `DefaultMetricsRecorder.metrics_by_name(...)`
85
+ - `DefaultMetricsRecorder.metric_by_name_and_labels(...)`
86
+
87
+ Example:
88
+
89
+ ```js
90
+ import { DbBuilder, DefaultMetricsRecorder, ObjectStore } from "@slatedb/uniffi";
91
+
92
+ const store = ObjectStore.resolve("memory:///");
93
+ const recorder = new DefaultMetricsRecorder();
94
+ const builder = new DbBuilder("metrics-demo", store);
95
+
96
+ try {
97
+ builder.with_metrics_recorder(recorder);
98
+ const db = await builder.build();
99
+ try {
100
+ await db.put(Buffer.from("hello"), Buffer.from("world"));
101
+
102
+ const metric = recorder.metric_by_name_and_labels("slatedb.db.write_ops", []);
103
+ if (metric?.value.tag === "Counter") {
104
+ console.log(metric.value[""]);
105
+ }
106
+ } finally {
107
+ await db.shutdown();
108
+ db.dispose();
109
+ }
110
+ } finally {
111
+ builder.dispose();
112
+ recorder.dispose();
113
+ store.dispose();
114
+ }
115
+ ```
116
+
117
+ ## Local Development
118
+
119
+ The package is generated from the UniFFI `slatedb-uniffi` cdylib using [`uniffi-bindgen-node-js`](https://crates.io/crates/uniffi-bindgen-node-js).
120
+
121
+ You only need these tools when regenerating bindings, running tests from this repository, or packing the npm artifact locally:
122
+
123
+ - Node.js 20 or newer
124
+ - Rust toolchain for this repository
125
+ - `uniffi-bindgen-node-js` on `PATH`
126
+
127
+ Install the generator with:
128
+
129
+ ```bash
130
+ cargo install uniffi-bindgen-node-js --version 0.0.7
131
+ ```
132
+
133
+ Install the package dependency used by the generated bindings with:
134
+
135
+ ```bash
136
+ npm --prefix bindings/node install
137
+ ```
138
+
139
+ ### Regenerate Bindings
140
+
141
+ From the repository root:
142
+
143
+ ```bash
144
+ npm --prefix bindings/node run build
145
+ ```
146
+
147
+ This command:
148
+
149
+ 1. builds the host `slatedb-uniffi` library
150
+ 2. runs `uniffi-bindgen-node-js`
151
+ 3. copies the generated package files into `bindings/node`
152
+ 4. stages the host native library under `bindings/node/prebuilds/<target>/`
153
+
154
+ Generated API files are written into `bindings/node` and are not committed. `package.json`, `build.mjs`, and this `README.md` are maintained by hand.
155
+
156
+ ### Run Tests
157
+
158
+ From the repository root:
159
+
160
+ ```bash
161
+ npm --prefix bindings/node test
162
+ ```
163
+
164
+ The test script rebuilds the package and then runs `node --test` inside `bindings/node`.
165
+
166
+ ### Reproduce The PR CI Flow
167
+
168
+ From the repository root, this mirrors the Node validation done in `.github/workflows/pr.yaml`:
169
+
170
+ ```bash
171
+ npm --prefix bindings/node ci
172
+ npm --prefix bindings/node run build
173
+ (cd bindings/node && node --test)
174
+ git diff --exit-code -- bindings/node
175
+ rm -rf /tmp/slatedb-node-pack
176
+ mkdir -p /tmp/slatedb-node-pack
177
+ (cd bindings/node && npm pack --pack-destination /tmp/slatedb-node-pack)
178
+ TARBALL="$(find /tmp/slatedb-node-pack -maxdepth 1 -name '*.tgz' | head -n 1)"
179
+ test -n "${TARBALL}"
180
+ tar -tf "${TARBALL}" | grep -Fx 'package/index.js'
181
+ tar -tf "${TARBALL}" | grep -Fx 'package/index.d.ts'
182
+ tar -tf "${TARBALL}" | grep -Fx 'package/slatedb.js'
183
+ tar -tf "${TARBALL}" | grep -Fx 'package/slatedb.d.ts'
184
+ tar -tf "${TARBALL}" | grep -Fx 'package/slatedb-ffi.js'
185
+ tar -tf "${TARBALL}" | grep -Fx 'package/slatedb-ffi.d.ts'
186
+ tar -tf "${TARBALL}" | grep -Fx 'package/runtime/ffi-types.js'
187
+ tar -tf "${TARBALL}" | grep -Fx 'package/prebuilds/linux-x64-gnu/libslatedb_uniffi.so'
188
+ ```
189
+
190
+ ## Packaging And Runtime Notes
191
+
192
+ The published `@slatedb/uniffi` tarball contains generated JavaScript and TypeScript bindings plus bundled native libraries. Consumers install one npm package; there is no separate Rust build step or native download in normal usage.
193
+
194
+ At runtime, the package loads the native library that matches the current host from `prebuilds/<target>/`. The published package currently includes:
195
+
196
+ - `linux-x64-gnu`
197
+ - `linux-arm64-gnu`
198
+ - `darwin-x64`
199
+ - `darwin-arm64`
200
+ - `win32-x64`
201
+ - `win32-arm64`
202
+
203
+ Linux musl targets are not packaged today. Local builds stage only the host native library; release builds stage all supported targets into the published npm package.
204
+
205
+ When assembling a release package from a prebuilt native directory, run:
206
+
207
+ ```bash
208
+ npm --prefix bindings/node run build -- --prebuilt-dir <dir>
209
+ ```
210
+
211
+ The generated API files stay the same; only the packaged native libraries change between local and release builds.
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./slatedb.js";
package/index.js ADDED
@@ -0,0 +1,7 @@
1
+
2
+ import { load as loadFfi } from "./slatedb-ffi.js";
3
+
4
+ loadFfi();
5
+
6
+
7
+ export * from "./slatedb.js";
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@slatedb/uniffi",
3
+ "version": "0.12.0",
4
+ "description": "Node.js bindings for SlateDB generated from UniFFI and packaged with native libraries.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "types": "./index.d.ts",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "files": [
12
+ "README.md",
13
+ "index.js",
14
+ "index.d.ts",
15
+ "slatedb.js",
16
+ "slatedb.d.ts",
17
+ "slatedb-ffi.js",
18
+ "slatedb-ffi.d.ts",
19
+ "runtime/",
20
+ "prebuilds/"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./index.d.ts",
25
+ "import": "./index.js"
26
+ }
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/slatedb/slatedb.git",
31
+ "directory": "bindings/node"
32
+ },
33
+ "homepage": "https://slatedb.io",
34
+ "bugs": {
35
+ "url": "https://github.com/slatedb/slatedb/issues"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "build": "node ./build.mjs",
42
+ "test": "npm run build && node --test"
43
+ },
44
+ "dependencies": {
45
+ "koffi": "^2.0.0"
46
+ }
47
+ }
@@ -0,0 +1,86 @@
1
+ import type { RustCallStatusStruct } from "./ffi-types.js";
2
+ import type { RustCallOptions, UniffiRustCaller } from "./rust-call.js";
3
+
4
+ export declare const RUST_FUTURE_POLL_READY: 0;
5
+ export declare const RUST_FUTURE_POLL_WAKE: 1;
6
+
7
+ export interface AsyncCallState {
8
+ resolverHandle: bigint | null;
9
+ }
10
+
11
+ export type RustFutureContinuationCallback = (
12
+ handle: bigint | number,
13
+ pollResult: bigint | number,
14
+ ) => boolean;
15
+
16
+ export interface PollRustFutureOptions {
17
+ continuationCallback?: RustFutureContinuationCallback;
18
+ state?: AsyncCallState;
19
+ }
20
+
21
+ export interface CompleteRustFutureOptions<
22
+ Status = RustCallStatusStruct,
23
+ E extends Error = Error,
24
+ > extends RustCallOptions<E> {
25
+ rustCaller?: UniffiRustCaller<Status>;
26
+ }
27
+
28
+ export interface RustCallAsyncOptions<
29
+ Lowered,
30
+ Result = Lowered,
31
+ Status = RustCallStatusStruct,
32
+ E extends Error = Error,
33
+ > extends CompleteRustFutureOptions<Status, E> {
34
+ cancelFunc?: ((rustFuture: bigint) => void) | null;
35
+ completeFunc: (rustFuture: bigint, status: Status) => Lowered;
36
+ continuationCallback?: RustFutureContinuationCallback;
37
+ freeFunc?: ((rustFuture: bigint) => void) | null;
38
+ liftFunc?: (value: Lowered) => Result;
39
+ pollFunc: (
40
+ rustFuture: bigint,
41
+ continuationCallback: RustFutureContinuationCallback,
42
+ continuationHandle: bigint,
43
+ ) => void;
44
+ rustFutureFunc: () => bigint;
45
+ signal?: AbortSignal | null;
46
+ }
47
+
48
+ export declare function decodeRustFuturePoll(value: bigint | number): number;
49
+ export declare function createAsyncCallState(): AsyncCallState;
50
+ export declare function cleanupAsyncCallState(state: AsyncCallState): void;
51
+ export declare const rustFutureContinuationCallback: RustFutureContinuationCallback;
52
+ export declare function pollRustFuture(
53
+ rustFuture: bigint,
54
+ pollFunc: (
55
+ rustFuture: bigint,
56
+ continuationCallback: RustFutureContinuationCallback,
57
+ continuationHandle: bigint,
58
+ ) => void,
59
+ options?: PollRustFutureOptions,
60
+ ): Promise<number>;
61
+ export declare function completeRustFuture<
62
+ Lowered,
63
+ Status = RustCallStatusStruct,
64
+ E extends Error = Error,
65
+ >(
66
+ rustFuture: bigint,
67
+ completeFunc: (rustFuture: bigint, status: Status) => Lowered,
68
+ options?: CompleteRustFutureOptions<Status, E>,
69
+ ): Lowered;
70
+ export declare function cancelRustFuture(
71
+ rustFuture: bigint,
72
+ cancelFunc?: ((rustFuture: bigint) => void) | null,
73
+ ): void;
74
+ export declare function freeRustFuture(
75
+ rustFuture: bigint,
76
+ freeFunc?: ((rustFuture: bigint) => void) | null,
77
+ ): void;
78
+ export declare function rustCallAsync<
79
+ Lowered,
80
+ Result = Lowered,
81
+ Status = RustCallStatusStruct,
82
+ E extends Error = Error,
83
+ >(
84
+ options: RustCallAsyncOptions<Lowered, Result, Status, E>,
85
+ ): Promise<Result>;
86
+ export declare function rustFutureHandleCount(): number;
@@ -0,0 +1,217 @@
1
+ import { AbortError, ConverterRangeError } from "./errors.js";
2
+ import { UniffiHandleMap } from "./handle-map.js";
3
+ import { defaultRustCaller } from "./rust-call.js";
4
+
5
+ export const RUST_FUTURE_POLL_READY = 0;
6
+ export const RUST_FUTURE_POLL_WAKE = 1;
7
+ const RUST_FUTURE_KEEPALIVE_DELAY_MS = 0x7fffffff;
8
+
9
+ const MIN_I8 = -0x80;
10
+ const MAX_I8 = 0x7f;
11
+ const MAX_U8 = 0xff;
12
+ const RUST_FUTURE_RESOLVER_MAP = new UniffiHandleMap();
13
+ const RUST_FUTURE_KEEPALIVE_MAP = new Map();
14
+
15
+ function identity(value) {
16
+ return value;
17
+ }
18
+
19
+ export function decodeRustFuturePoll(value) {
20
+ const numericValue =
21
+ typeof value === "bigint"
22
+ ? Number(value)
23
+ : value;
24
+ if (!Number.isInteger(numericValue) || numericValue < MIN_I8 || numericValue > MAX_U8) {
25
+ throw new ConverterRangeError(
26
+ `RustFuturePoll must be an integer between ${MIN_I8} and ${MAX_U8}.`,
27
+ );
28
+ }
29
+ return numericValue > MAX_I8
30
+ ? numericValue - 0x100
31
+ : numericValue;
32
+ }
33
+
34
+ export function createAsyncCallState() {
35
+ return {
36
+ resolverHandle: null,
37
+ };
38
+ }
39
+
40
+ function clearRustFutureKeepAlive(handle) {
41
+ const keepAlive = RUST_FUTURE_KEEPALIVE_MAP.get(handle);
42
+ if (keepAlive === undefined) {
43
+ return;
44
+ }
45
+
46
+ RUST_FUTURE_KEEPALIVE_MAP.delete(handle);
47
+ clearTimeout(keepAlive);
48
+ }
49
+
50
+ export function cleanupAsyncCallState(state) {
51
+ if (!state || state.resolverHandle == null) {
52
+ return;
53
+ }
54
+
55
+ clearRustFutureKeepAlive(state.resolverHandle);
56
+ RUST_FUTURE_RESOLVER_MAP.remove(state.resolverHandle);
57
+ state.resolverHandle = null;
58
+ }
59
+
60
+ export const rustFutureContinuationCallback = (handle, pollResult) => {
61
+ clearRustFutureKeepAlive(handle);
62
+ const resolve = RUST_FUTURE_RESOLVER_MAP.remove(handle);
63
+ if (resolve === undefined) {
64
+ return false;
65
+ }
66
+
67
+ queueMicrotask(() => {
68
+ resolve(decodeRustFuturePoll(pollResult));
69
+ });
70
+ return true;
71
+ };
72
+
73
+ export function pollRustFuture(
74
+ rustFuture,
75
+ pollFunc,
76
+ {
77
+ continuationCallback = rustFutureContinuationCallback,
78
+ state = createAsyncCallState(),
79
+ } = {},
80
+ ) {
81
+ cleanupAsyncCallState(state);
82
+
83
+ return new Promise((resolve, reject) => {
84
+ // Node 22 can tear down the event loop while this promise is still waiting
85
+ // on a native callback. Keep a ref'ed timer alive until the callback
86
+ // settles so Rust future completions arrive before environment cleanup.
87
+ const keepAlive = setTimeout(() => {}, RUST_FUTURE_KEEPALIVE_DELAY_MS);
88
+ let resolverHandle = null;
89
+ const settle = (callback, value) => {
90
+ clearRustFutureKeepAlive(resolverHandle);
91
+ callback(value);
92
+ };
93
+ resolverHandle = RUST_FUTURE_RESOLVER_MAP.insert((pollCode) => {
94
+ state.resolverHandle = null;
95
+ settle(resolve, pollCode);
96
+ });
97
+ RUST_FUTURE_KEEPALIVE_MAP.set(resolverHandle, keepAlive);
98
+ state.resolverHandle = resolverHandle;
99
+
100
+ try {
101
+ pollFunc(rustFuture, continuationCallback, resolverHandle);
102
+ } catch (error) {
103
+ cleanupAsyncCallState(state);
104
+ settle(reject, error);
105
+ }
106
+ });
107
+ }
108
+
109
+ export function completeRustFuture(
110
+ rustFuture,
111
+ completeFunc,
112
+ {
113
+ errorHandler,
114
+ freeRustBuffer,
115
+ liftString,
116
+ rustCaller = defaultRustCaller,
117
+ } = {},
118
+ ) {
119
+ return rustCaller.makeRustCall(
120
+ (status) => completeFunc(rustFuture, status),
121
+ {
122
+ errorHandler,
123
+ freeRustBuffer,
124
+ liftString,
125
+ },
126
+ );
127
+ }
128
+
129
+ export function cancelRustFuture(rustFuture, cancelFunc = undefined) {
130
+ if (typeof cancelFunc === "function") {
131
+ cancelFunc(rustFuture);
132
+ }
133
+ }
134
+
135
+ export function freeRustFuture(rustFuture, freeFunc = undefined) {
136
+ if (typeof freeFunc === "function") {
137
+ freeFunc(rustFuture);
138
+ }
139
+ }
140
+
141
+ export async function rustCallAsync({
142
+ cancelFunc,
143
+ completeFunc,
144
+ continuationCallback = rustFutureContinuationCallback,
145
+ errorHandler,
146
+ freeFunc,
147
+ freeRustBuffer,
148
+ liftFunc = identity,
149
+ liftString,
150
+ pollFunc,
151
+ rustCaller = defaultRustCaller,
152
+ rustFutureFunc,
153
+ signal = undefined,
154
+ }) {
155
+ if (signal?.aborted) {
156
+ throw new AbortError();
157
+ }
158
+
159
+ const rustFuture = rustFutureFunc();
160
+ const state = createAsyncCallState();
161
+ const abortListener =
162
+ signal == null
163
+ ? null
164
+ : () => {
165
+ try {
166
+ cancelRustFuture(rustFuture, cancelFunc);
167
+ } catch {
168
+ // Preserve the original async failure path.
169
+ }
170
+ };
171
+
172
+ if (signal && abortListener) {
173
+ if (signal.aborted) {
174
+ abortListener();
175
+ } else {
176
+ signal.addEventListener("abort", abortListener, { once: true });
177
+ }
178
+ }
179
+
180
+ try {
181
+ while (true) {
182
+ const pollCode = await pollRustFuture(rustFuture, pollFunc, {
183
+ continuationCallback,
184
+ state,
185
+ });
186
+ if (pollCode === RUST_FUTURE_POLL_READY) {
187
+ break;
188
+ }
189
+ if (pollCode !== RUST_FUTURE_POLL_WAKE) {
190
+ throw new ConverterRangeError(
191
+ `Unexpected RustFuturePoll value ${String(pollCode)}.`,
192
+ );
193
+ }
194
+ }
195
+
196
+ const completed = completeRustFuture(rustFuture, completeFunc, {
197
+ errorHandler,
198
+ freeRustBuffer,
199
+ liftString,
200
+ rustCaller,
201
+ });
202
+ return liftFunc(completed);
203
+ } finally {
204
+ if (signal && abortListener) {
205
+ signal.removeEventListener("abort", abortListener);
206
+ }
207
+ try {
208
+ freeRustFuture(rustFuture, freeFunc);
209
+ } finally {
210
+ cleanupAsyncCallState(state);
211
+ }
212
+ }
213
+ }
214
+
215
+ export function rustFutureHandleCount() {
216
+ return RUST_FUTURE_RESOLVER_MAP.size;
217
+ }