@avidian/duckdb-bun 1.0.0 → 1.0.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
@@ -47,6 +47,7 @@ Every release is a stable version, so a range such as `^1.0.0` takes every compa
47
47
  | `@avidian/duckdb-bun` | DuckDB | `@duckdb/node-api` it matches |
48
48
  | --- | --- | --- |
49
49
  | 1.0.0 | 2.0.0 | 2.0.0-r.1 |
50
+ | 1.0.1 | 2.0.0 | 2.0.0-r.1 |
50
51
 
51
52
  At run time, `DUCKDB_VERSION` holds the bundled DuckDB version, and `diagnostics().versions` holds all of them:
52
53
 
@@ -186,6 +187,7 @@ Include the report when you open an issue.
186
187
  It runs every blocking call (open, connect, query, prepare, execute, fetch) on a native thread, so the JS thread never waits for a query.
187
188
  It installs the crash handler.
188
189
  - Every native pointer is owned by a handle object, freed by a `FinalizationRegistry` or by an explicit close, and never while a native call is using it.
190
+ - A Worker that ends, including through `terminate()` in the middle of a query, interrupts its queries and frees its databases, connections and other handles; the main thread's databases keep working ([ADR 0008](docs/adr/0008-a-worker-closes-its-context-in-an-n-api-cleanup-hook.md)).
189
191
 
190
192
  The design decisions are recorded in [`docs/adr/`](docs/adr/).
191
193
 
@@ -217,10 +219,10 @@ Each CI run uploads its full tables as the `bench-<platform>` artifact.
217
219
 
218
220
  ## Known issues
219
221
 
220
- - Terminating a Bun `Worker` with `worker.terminate()` while that Worker runs a query crashes the process with SIGSEGV.
221
- Bun runs no exit handler in a terminated Worker, so the binding never learns that the Worker's JavaScript is gone, and the finished query calls back into it.
222
- The official `@duckdb/node-api` crashes on the same sequence under Bun 1.4.4, on the first termination.
223
- Until a fix ships, let a Worker's queries settle, or close its connections, before terminating it.
222
+ - In a `bun build --compile` binary, each Worker that loads the package copies libduckdb and the shim into a new temporary folder and loads its own copy.
223
+ Starting many Workers leaves those folders behind, and the main thread's `diagnostics()` does not count the Workers' native work.
224
+ - A Worker terminated in the few microseconds between a query's native thread starting and DuckDB beginning that query does not interrupt it.
225
+ The query runs to its end before the Worker's handles are freed; nothing crashes or leaks.
224
226
  - One process can load the package in at most 4094 Workers over its whole life, because each JS thread takes a context id that is never reused (`native/ABI.md`, `dbs_init`).
225
227
  A Worker past that limit throws when it loads the package.
226
228
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avidian/duckdb-bun",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Bun-native, drop-in reimplementation of @duckdb/node-api via bun:ffi over the libduckdb C API. Embeds libduckdb so `bun build --compile` yields a self-contained binary.",
5
5
  "author": "John Michael Manlupig",
6
6
  "license": "MIT",
@@ -49,6 +49,7 @@
49
49
  "soak": "bun run scripts/soak.ts",
50
50
  "test:crash": "bun test tests/heavy/crash-report.test.ts",
51
51
  "test:issue-3060": "bun test tests/heavy/issue-3060.test.ts",
52
+ "test:worker-terminate": "bun test tests/heavy/worker-terminate.test.ts",
52
53
  "test:unit": "bun test --path-ignore-patterns 'tests/official/**' --path-ignore-patterns 'tests/heavy/**' --path-ignore-patterns 'tests/compile/**' --path-ignore-patterns tests/parity.test.ts --path-ignore-patterns 'tests/parity/**' --path-ignore-patterns 'tests/fuzz/**'",
53
54
  "coverage": "bun run scripts/coverage.ts",
54
55
  "check-version": "bun run scripts/check-version.ts",
@@ -71,9 +72,9 @@
71
72
  "analytics"
72
73
  ],
73
74
  "optionalDependencies": {
74
- "@avidian/duckdb-bun-darwin-arm64": "1.0.0",
75
- "@avidian/duckdb-bun-linux-arm64": "1.0.0",
76
- "@avidian/duckdb-bun-linux-x64": "1.0.0"
75
+ "@avidian/duckdb-bun-darwin-arm64": "1.0.1",
76
+ "@avidian/duckdb-bun-linux-arm64": "1.0.1",
77
+ "@avidian/duckdb-bun-linux-x64": "1.0.1"
77
78
  },
78
79
  "devDependencies": {
79
80
  "@biomejs/biome": "2.6.7",
@@ -7,8 +7,9 @@
7
7
  // each call onto the JS thread, so everything here runs on the JS thread, and a
8
8
  // job's completion always runs after the code that started the job has returned.
9
9
  import { CString, JSCallback, type Pointer, read } from "bun:ffi";
10
+ import { isMainThread } from "node:worker_threads";
10
11
  import type { Handle } from "./handle.ts";
11
- import { shim } from "./native.ts";
12
+ import { shim, shimPath } from "./native.ts";
12
13
  import { jobOp, record } from "./ring.ts";
13
14
  import { type CallContext, traced } from "./trace.ts";
14
15
 
@@ -196,5 +197,17 @@ const context = shim.dbs_init(onComplete.ptr, onCall.ptr, onReleaseCallback.ptr)
196
197
  if (context < 1) throw new Error(`duckdb-bun: the native shim refused this thread (dbs_init returned ${context})`);
197
198
  contextBase = context * CONTEXT_SCALE;
198
199
  shim.dbs_set_sync_call(onCallSync.ptr);
199
- // A Worker's callbacks die with it, so the shim drops whatever arrives for it afterwards.
200
- process.on("exit", () => shim.dbs_context_close());
200
+
201
+ if (isMainThread) {
202
+ // After "exit" no more script runs, so the shim drops whatever arrives for this thread.
203
+ process.on("exit", () => shim.dbs_context_close());
204
+ } else {
205
+ // A Worker stopped by terminate() emits no "exit" and runs no script, but Bun runs
206
+ // its Node-API cleanup hooks on the Worker's thread before its VM goes away. The
207
+ // shim, loaded as an addon here, adds one that closes this context and frees the
208
+ // handles it owns (docs/adr/0008-a-worker-closes-its-context-in-an-n-api-cleanup-hook.md).
209
+ process.dlopen({ exports: {} }, shimPath);
210
+ if (!shim.dbs_context_hooked()) {
211
+ throw new Error("duckdb-bun: the native shim could not add its Node-API cleanup hook in this Worker");
212
+ }
213
+ }
@@ -1,44 +1,44 @@
1
- // One constructor per handle kind, pairing each owned pointer with the C
2
- // function that frees it, as the official binding's externals.h does.
1
+ // One constructor per handle kind. The shim frees each owned pointer with the C
2
+ // function the official binding's externals.h pairs with its kind, and records it
3
+ // in this JS thread's context, so a Worker that ends without running script
4
+ // still frees it (native/ABI.md, "Handle ownership").
3
5
  import type { Pointer } from "bun:ffi";
6
+ import "./dispatch.ts"; // Opens this JS thread's shim context before the first handle.
4
7
  import { borrowed, type Handle, type Kind, owned } from "./handle.ts";
5
- import { slot } from "./marshal.ts";
6
- import { duckdb } from "./native.ts";
8
+ import { shim } from "./native.ts";
7
9
 
8
- /** Calls a `duckdb_destroy_x(T *handle)` style function with the address of `ptr`. */
9
- function viaSlot(destroy: (address: BigUint64Array) => void): (ptr: Pointer) => void {
10
- return (ptr) => destroy(slot(ptr));
11
- }
10
+ /** Handle kind codes from native/ABI.md. */
11
+ const KIND_CODE = {
12
+ appender: 1,
13
+ catalog: 2,
14
+ "catalog entry": 3,
15
+ "client context": 4,
16
+ config: 5,
17
+ connection: 6,
18
+ "data chunk": 7,
19
+ database: 8,
20
+ "extracted statements": 9,
21
+ "instance cache": 10,
22
+ "logical type": 11,
23
+ "pending result": 12,
24
+ "prepared statement": 13,
25
+ // A `duckdb_result *` the shim allocated with duckdb_malloc.
26
+ result: 14,
27
+ "scalar function": 15,
28
+ "table function": 16,
29
+ value: 17,
30
+ vector: 18,
31
+ } as const satisfies Partial<Record<Kind, number>>;
12
32
 
13
- const DESTROY = {
14
- appender: viaSlot((a) => duckdb.duckdb_appender_destroy(a)),
15
- catalog: viaSlot((a) => duckdb.duckdb_destroy_catalog(a)),
16
- "catalog entry": viaSlot((a) => duckdb.duckdb_destroy_catalog_entry(a)),
17
- "client context": viaSlot((a) => duckdb.duckdb_destroy_client_context(a)),
18
- config: viaSlot((a) => duckdb.duckdb_destroy_config(a)),
19
- connection: viaSlot((a) => duckdb.duckdb_disconnect(a)),
20
- "data chunk": viaSlot((a) => duckdb.duckdb_destroy_data_chunk(a)),
21
- database: viaSlot((a) => duckdb.duckdb_close(a)),
22
- "extracted statements": viaSlot((a) => duckdb.duckdb_destroy_extracted(a)),
23
- "instance cache": viaSlot((a) => duckdb.duckdb_destroy_instance_cache(a)),
24
- "logical type": viaSlot((a) => duckdb.duckdb_destroy_logical_type(a)),
25
- "pending result": viaSlot((a) => duckdb.duckdb_destroy_pending(a)),
26
- "prepared statement": viaSlot((a) => duckdb.duckdb_destroy_prepare(a)),
27
- // A result is a `duckdb_result *` the shim allocated with duckdb_malloc.
28
- result: (ptr: Pointer) => {
29
- duckdb.duckdb_destroy_result(ptr);
30
- duckdb.duckdb_free(ptr);
31
- },
32
- "scalar function": viaSlot((a) => duckdb.duckdb_destroy_scalar_function(a)),
33
- "table function": viaSlot((a) => duckdb.duckdb_destroy_table_function(a)),
34
- value: viaSlot((a) => duckdb.duckdb_destroy_value(a)),
35
- vector: viaSlot((a) => duckdb.duckdb_destroy_vector(a)),
36
- } as const satisfies Partial<Record<Kind, (ptr: Pointer) => void>>;
33
+ export type OwnedKind = keyof typeof KIND_CODE;
37
34
 
38
- export type OwnedKind = keyof typeof DESTROY;
35
+ const DESTROY = Object.fromEntries(
36
+ Object.entries(KIND_CODE).map(([kind, code]) => [kind, (ptr: Pointer) => shim.dbs_destroy(code, ptr)]),
37
+ ) as Record<OwnedKind, (ptr: Pointer) => void>;
39
38
 
40
39
  /** Wraps a pointer the caller now owns; it is freed when collected or explicitly destroyed. */
41
40
  export function own<K extends OwnedKind>(kind: K, ptr: Pointer): Handle<K> {
41
+ shim.dbs_own(KIND_CODE[kind], ptr);
42
42
  return owned(kind, ptr, DESTROY[kind]);
43
43
  }
44
44
 
@@ -16,6 +16,9 @@ const shimAbi = {
16
16
  dbs_init: { args: ["ptr", "ptr", "ptr"], returns: "i32" },
17
17
  dbs_set_sync_call: { args: ["ptr"], returns: "void" },
18
18
  dbs_context_close: { args: [], returns: "void" },
19
+ dbs_context_hooked: { args: [], returns: "bool" },
20
+ dbs_own: { args: ["u32", "ptr"], returns: "void" },
21
+ dbs_destroy: { args: ["u32", "ptr"], returns: "void" },
19
22
  dbs_counters: { args: [], returns: "ptr" },
20
23
  dbs_version: { args: [], returns: "cstring" },
21
24
  dbs_capi_table: { args: ["ptr"], returns: "ptr" },