Skip to content

Add seekdb-js: JavaScript bindings for libseekdb - #54

Open
dengfuping wants to merge 5 commits into
oceanbase:mainfrom
dengfuping:feat/js-bindings
Open

dengfuping wants to merge 5 commits into
oceanbase:mainfrom
dengfuping:feat/js-bindings

Conversation

@dengfuping

@dengfuping dengfuping commented Aug 21, 2026

Copy link
Copy Markdown

Summary

Adds js/, a JavaScript binding package (seekdb-js) for the seekdb C client
library, modeled on the Python bindings in python/. The N-API addon links
libseekdb dynamically and ships the seekdb server binary inside the
package, matching the Python wheel deployment layout.

The same addon runs under Node.js and Bun; Deno 2 loads it through npm
packages (nodeModulesDir: "auto" plus --allow-ffi).

What's included

  • js/ package: seekdb-js@1.4.0.dev1, N-API v8, node-gyp build (POSIX
    only: Linux x86_64/aarch64, macOS arm64; no Windows)
  • Native layer (src/): SeekdbInstance shared-instance table,
    Connection, Cursor, async AsyncWorker wrappers, type/value conversion,
    SeekdbError normalization
  • JS layer (lib/): Promise-first API (open/close/connect/execute/ fetchOne/fetchAll), module default instance, TypeScript definitions
  • Tests (test/): node:test smoke tests covering create/insert/hybrid
    search, transactions, and error codes
  • CI (pr-ci.yml): new node-test job on Node LTS 18/20/22, reusing the
    seekdb binary from prepare-seekdb; clang-format globs for js/src/*
  • Docs: top-level README section and js/README.md

API notes

  • Async-only: every blocking call returns a Promise and runs off the event
    loop; only cursor(), closed, dbDir are synchronous.
  • Value mapping mirrors Python: NULL→null, INT64/UINT64→number
    (bigint beyond Number.MAX_SAFE_INTEGER), DECIMAL→string,
    DATE/DATETIME/TIMESTAMP→string, VARCHAR→string.

Validation

  • Local: cd js && npm run build, then node --test test/ on Node 18, 20,
    and 22 — 4/4 tests pass with a clean exit (no crash)
  • PR CI: all 8 checks pass — Node.js binding build/test (18/20/22), Build and
    test, ASan/UBSan, Format check, Prepare seekdb binary, license/cla
  • clang-format (v14) clean on js/src/*.{cpp,hpp}

Two runtime issues surfaced by the first CI run were fixed in this PR:

  • SIGSEGV at process exit on Node 18/20: module-static Napi::FunctionReferences
    were destroyed after V8 teardown; fixed by resetting them from an
    Env::AddCleanupHook in each Init.
  • ENOTEMPTY teardown failure on Linux CI: the spawned seekdb server was still
    flushing its log when the test after hook removed the temp db dir; fixed by
    retrying the removal.

Not run

  • npm publish — release pipeline for prebuilt binaries is a follow-up; this PR
    delivers the local source-install path only
  • Windows support — excluded by design, matching the Python bindings

Modeled on the Python bindings in python/: a node-addon-api addon that
links libseekdb dynamically and ships the seekdb server binary, exposed
as the seekdb-js npm package (async-only, Promise-first API).

- js/: package skeleton (seekdb-js@1.4.0.dev1, N-API v8), binding.gyp
  (POSIX only), native layer (instance/connection/cursor/types), JS layer
  with TypeScript definitions, node:test smoke tests, README
- CI: node-test job (Node LTS 18/20/22) reusing the seekdb binary from
  prepare-seekdb; format-check globs for js/src/*
- README: document JS bindings build, usage, platform support (no Windows)
- Format js/src/*.{cpp,hpp} with clang-format 14 (repo .clang-format)
- node-test job: add setup-python step; the shared Configure step
  requires Python >= 3.11 (python/CMakeLists.txt find_package)
… env teardown

Module-static Napi::FunctionReference members (SeekdbInstance/Connection/Cursor
constructors) were destroyed after V8 had already shut down, dereferencing a
dead v8impl::Reference (EXC_BAD_ACCESS in v8impl::Reference::~Reference). Only
Node 22's teardown ordering hid the crash; Node 18/20 segfaulted after all
tests passed. Register an env cleanup hook in each Init that resets the static
reference, so the static destructor is a no-op.
On Linux CI the seekdb server process spawned by libseekdb shuts down
asynchronously after the last instance.close(), and may still be writing
to <dbDir>/log when the after() hook runs fs.rmSync, producing
ENOTEMPTY. Retry for up to 2s so the runner no longer fails Node 20
(which in turn cancelled the Node 18 matrix leg even though its tests
all passed).
@dengfuping dengfuping changed the title Add JavaScript bindings for libseekdb (node-addon-api) Add seekdb-js: JavaScript bindings for libseekdb Aug 21, 2026
@hnwyllmm
hnwyllmm requested a balanced review from Copilot August 25, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds JavaScript bindings for libseekdb, including an N-API native layer, Promise-based JavaScript API, tests, packaging metadata, CI, and documentation.

Changes:

  • Implements native instance, connection, cursor, error, and value conversion layers.
  • Adds JavaScript/TypeScript APIs and Node.js integration tests.
  • Adds node-gyp packaging, CI coverage, and usage documentation.

Reviewed changes

Copilot reviewed 14 out of 16 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
README.md Documents JavaScript bindings and usage.
js/.gitignore Ignores generated JavaScript build artifacts.
js/README.md Provides package build and API documentation.
js/binding.gyp Configures native addon compilation and linking.
js/package.json Defines package metadata, dependencies, and scripts.
js/package-lock.json Locks npm dependencies.
js/lib/index.js Implements the public Promise-based JavaScript API.
js/lib/seekdb.d.ts Defines TypeScript interfaces.
js/src/addon.cpp Initializes exports and error translation.
js/src/connection.cpp Implements connections and transactions.
js/src/cursor.cpp Implements query execution and fetching.
js/src/instance.cpp Implements shared seekdb instance lifecycle.
js/src/internal.hpp Declares native binding state and wrappers.
js/src/types.cpp Converts seekdb values into JavaScript values.
js/test/seekdb.test.js Tests queries, types, transactions, and errors.
.github/workflows/pr-ci.yml Adds Node.js build and test jobs.
Files not reviewed (1)
  • js/package-lock.json: Generated file
Suppressed comments (3)

js/src/connection.cpp:278

  • This disconnects immediately even when live cursors retain ConnectionState. A SeekdbResult stores the connection's borrowed MYSQL * (lib/src/seekdb.c:1067), while disconnect frees it (lib/src/seekdb.c:950-958); fetching that cursor to EOF later dereferences the freed pointer in seekdb_result_next. Invalidate/free all cursors before disconnecting, or defer disconnect until cursor references are released.
    auto deferred = Napi::Promise::Deferred::New(env);
    auto *worker = new DisconnectWorker(env, deferred, std::move(to_close));
    worker->Queue();

js/lib/index.js:150

  • Opening a different directory clears and replaces an active module default, contrary to the documented “first open becomes the module default” behavior and the Python binding. This silently redirects subsequent module-level connect() calls. Return the newly opened instance, but retain an existing active default.
  defaultInstance = null;
  return binding.open(dbDir).then((instance) => {
    defaultInstance = new SeekdbInstance(instance);

js/package.json:26

  • The declared Node 16 support conflicts with the locked toolchain: node-addon-api@8.9.2 declares Node ^18 || ^20 || >=21, and node-gyp@11.5.0 requires ^18.17.0 || >=20.5.0. Node 16 is also absent from CI. Either use dependencies that support and test Node 16, or raise this engine requirement and both READMEs to the actual minimum.
  "engines": {
    "node": ">=16"

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread js/src/connection.cpp
Comment on lines +150 to +153
void Execute() override
{
if (conn_)
conn_->reset();
Comment thread js/src/instance.cpp
Comment on lines +202 to +205
void OnOK() override
{
if (erase_ && instance_)
ReleaseInstance(db_dir_, instance_.get());
Comment thread js/lib/index.js
Comment on lines +124 to +126
close() {
return this._native.close().catch(rethrow);
}
Comment thread js/package.json
Comment on lines +13 to +17
"files": [
"lib/",
"src/",
"binding.gyp",
"README.md"
Comment on lines +71 to +73
'js/src/*.cc' \
'js/src/*.cpp' \
'js/src/*.h'
Comment thread README.md
Comment on lines +408 to +412
```js
const seekdb = require('seekdb-js');

// Async API (Promise-based)
const instance = await seekdb.open('./seekdb.db');
Comment thread js/README.md
Comment on lines +52 to +56
```js
const seekdb = require('seekdb-js');

// All blocking operations are Promise-based and run off the event loop
const instance = await seekdb.open('./seekdb.db');
Comment thread js/src/cursor.cpp
Comment on lines +35 to +40
if (!result_)
return false;
if (seekdb_result_next(result_) != SEEKDB_SUCCESS)
return false;
int64_t ncol = 0;
SDB_CHECK(seekdb_result_column_count(result_, &ncol));
Comment thread js/src/instance.cpp
{
Napi::Object obj = SeekdbInstance::constructor.New({});
SeekdbInstance *inst = Napi::ObjectWrap<SeekdbInstance>::Unwrap(obj);
inst->Adopt(db_dir_, instance_);
@hnwyllmm

Copy link
Copy Markdown
Member

Thanks for adding the JavaScript bindings. I reviewed the native lifecycle, async API, packaging, and CI paths. I found three issues that I think should block merging:

  1. Connection teardown races with in-flight operations and live cursors

    DisconnectWorker::Execute() immediately calls ConnectionState::reset(), which disconnects and frees the underlying MYSQL *. However, cursors and already queued query/transaction workers still retain the same ConnectionState and may continue to access raw().

    This reproducer aborts the Node process:

    const query = cursor.execute('select sleep(1)');
    const closing = connection.close();
    await Promise.allSettled([query, closing]);

    Observed result:

    free(): invalid pointer
    Aborted (core dumped)
    

    The same connection can also be used concurrently by multiple cursors, or by a query and commit()/rollback(), because serialization currently exists only per cursor. Please add connection-level serialization/operation tracking and defer seekdb_disconnect() until all in-flight workers and cursor results have finished.

  2. The module-static constructor references are unsafe with worker_threads

    SeekdbInstance::constructor, Connection::constructor, and Cursor::constructor are process-global statics, but each Node environment overwrites them and registers a cleanup hook that resets the same global reference.

    I reproduced this sequence:

    • load the addon in the main thread;
    • load it in a Worker and let the Worker exit;
    • call open() again in the main thread.

    The final open() segfaults because the Worker's cleanup hook reset the constructor reference used by the main environment. These references should be stored per napi_env/addon instance data, and CI should include a Worker load/exit/reuse test.

  3. The npm archive cannot be built or run outside this repository

    npm pack --dry-run includes only the JS/C++ sources, declarations, README, binding.gyp, and package.json. It does not include:

    • lib/include/seekdb.h;
    • libseekdb;
    • the seekdb server binary;
    • a prebuilt seekdb.node.

    At the same time, binding.gyp references ../lib/include, which is outside the packed package. Therefore a published package cannot compile even if the user supplies SEEKDB_LIB_DIR, and it cannot run with the deployment layout described in the README. If this PR intentionally supports repository-local development only, the package/documentation claims should be narrowed; otherwise the package needs a complete prebuild/runtime packaging flow plus an install-from-tarball test.

Additional issues:

  • Native synchronous exceptions are not consistently normalized. For example, connection.begin() after connection.close() throws synchronously before .catch(rethrow) is attached, and the error is not an instance of the exported JS SeekdbError.
  • The clang-format workflow omits js/src/*.hpp, including internal.hpp.
  • The JS documentation says execute() returns affected rows, but the implementation returns result-set row count, so INSERT/UPDATE return 0.
  • The existing comments about Node 16 dependency incompatibility, replacing the module default when another directory is opened, and treating fetch errors as EOF also look valid.

The two crash reproductions were run in isolated Node processes. No repository files were changed during this review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants