From 5cd2c7985c4ff8227fbeb486d886451debfd7cc6 Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sat, 18 Jul 2026 20:40:26 +0800 Subject: [PATCH 1/4] feat: add napi-rs Node.js bindings Add the @holt/node package with Buffer/Uint8Array Tree APIs, bigint versions, scans, WAL reopen coverage, and napi-rs generated loader/types. Keep native artifacts out of git and document local/release builds. --- Cargo.lock | 204 +++ Cargo.toml | 2 +- packages/holt-node/.gitignore | 2 + packages/holt-node/Cargo.toml | 19 + packages/holt-node/README.md | 34 + packages/holt-node/build.rs | 3 + packages/holt-node/index.d.ts | 61 + packages/holt-node/index.js | 590 +++++++ packages/holt-node/package-lock.json | 1893 +++++++++++++++++++++++ packages/holt-node/package.json | 31 + packages/holt-node/src/lib.rs | 273 ++++ packages/holt-node/test/basic.test.mjs | 35 + packages/holt-node/test/reopen.test.mjs | 20 + 13 files changed, 3166 insertions(+), 1 deletion(-) create mode 100644 packages/holt-node/.gitignore create mode 100644 packages/holt-node/Cargo.toml create mode 100644 packages/holt-node/README.md create mode 100644 packages/holt-node/build.rs create mode 100644 packages/holt-node/index.d.ts create mode 100644 packages/holt-node/index.js create mode 100644 packages/holt-node/package-lock.json create mode 100644 packages/holt-node/package.json create mode 100644 packages/holt-node/src/lib.rs create mode 100644 packages/holt-node/test/basic.test.mjs create mode 100644 packages/holt-node/test/reopen.test.mjs diff --git a/Cargo.lock b/Cargo.lock index d5d816a..f5ff340 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,15 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -89,6 +98,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "ctor" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a394189d59f9befacce833f337f7b1eca5e9a91221bcdd4d28e0114d96e597b3" + [[package]] name = "dashmap" version = "6.2.1" @@ -143,6 +158,94 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + [[package]] name = "generator" version = "0.8.9" @@ -236,6 +339,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "holt-node" +version = "0.1.0" +dependencies = [ + "holt", + "napi", + "napi-build", + "napi-derive", +] + [[package]] name = "id-arena" version = "2.3.0" @@ -289,6 +402,16 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -338,6 +461,69 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "napi" +version = "3.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6826e5ddc15589b2d68c8ad5321c18e85d40488e93e32962f362e572669bccf6" +dependencies = [ + "bitflags", + "ctor", + "futures", + "napi-build", + "napi-sys", + "nohash-hasher", + "rustc-hash", +] + +[[package]] +name = "napi-build" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" + +[[package]] +name = "napi-derive" +version = "3.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fe526e81c105d3640516fcde83909dd1afe757c0d7a15af58830b5bc0fb9a1" +dependencies = [ + "convert_case", + "ctor", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514281397bcddd9ea9a876c7a21a57bff2374237a000ca9a64ea0211ec1993e2" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "3.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e43cf2eb0bd1bf95a43c07c076ebd2da5d1e015a71c3d201faeffffcc0ecac" +dependencies = [ + "libloading", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -534,6 +720,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustix" version = "1.1.4" @@ -640,6 +832,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.2" @@ -740,6 +938,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/Cargo.toml b/Cargo.toml index 0531512..afd9664 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ targets = [ ] [workspace] -members = [".", "crates/holt-ffi"] +members = [".", "crates/holt-ffi", "packages/holt-node"] exclude = ["benches", "fuzz", "tools/soak", "verified"] [dependencies] diff --git a/packages/holt-node/.gitignore b/packages/holt-node/.gitignore new file mode 100644 index 0000000..958d99b --- /dev/null +++ b/packages/holt-node/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.node diff --git a/packages/holt-node/Cargo.toml b/packages/holt-node/Cargo.toml new file mode 100644 index 0000000..b3429c5 --- /dev/null +++ b/packages/holt-node/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "holt-node" +version = "0.1.0" +edition = "2021" +rust-version = "1.82" +description = "Node.js bindings for the holt metadata engine." +license = "MIT" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +holt = { path = "../..", default-features = false } +napi = { version = "3", default-features = false, features = ["napi9"] } +napi-derive = "3" + +[build-dependencies] +napi-build = "2" diff --git a/packages/holt-node/README.md b/packages/holt-node/README.md new file mode 100644 index 0000000..ca8dc11 --- /dev/null +++ b/packages/holt-node/README.md @@ -0,0 +1,34 @@ +# @holt/node + +Node.js bindings for the [Holt](https://github.com/EnderRomantice/holt) +path-shaped metadata engine. + +The package is a Node-API native addon built directly on the Rust `holt` +crate. Keys and values are `Buffer`/`Uint8Array` instances; scans return +`kind`, `path`, `value`, and `version` fields. `version` is a JavaScript +`bigint` in the generated typings. + +This first package exposes the core `Tree` API. It is currently Unix-only, +matching Holt's file-store support. + +```ts +import { Tree } from "@holt/node"; + +const tree = Tree.openMemory(); +tree.put(Buffer.from("bucket/a"), Buffer.from("metadata")); +console.log(tree.get(Buffer.from("bucket/a"))?.toString()); +console.log(tree.scanKeys(Buffer.from("bucket/"))); +tree.close(); +``` + +Build the native artifact locally with: + +```sh +npm install +npm run build +``` + +The repository intentionally does not commit platform-specific `.node` +artifacts. A release pipeline should build and publish one napi-rs platform +package per supported Unix target, then attach them to the main package as +optional dependencies. diff --git a/packages/holt-node/build.rs b/packages/holt-node/build.rs new file mode 100644 index 0000000..0f1b010 --- /dev/null +++ b/packages/holt-node/build.rs @@ -0,0 +1,3 @@ +fn main() { + napi_build::setup(); +} diff --git a/packages/holt-node/index.d.ts b/packages/holt-node/index.d.ts new file mode 100644 index 0000000..40a5791 --- /dev/null +++ b/packages/holt-node/index.d.ts @@ -0,0 +1,61 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ +/** A Node.js handle to one Holt tree. */ +export declare class Tree { + /** Open a file-backed tree. */ + static open(path: string, options?: TreeOptions | undefined | null): Tree + /** Open a volatile in-memory tree. */ + static openMemory(): Tree + /** Explicitly release the native tree handle. This is idempotent. */ + close(): void + /** Return the value stored at `key`, or null when the key is absent. */ + get(key: Uint8Array): Buffer | null + /** Return the value and conditional-write version for `key`. */ + getRecord(key: Uint8Array): Record | null + /** Insert or replace a value. */ + put(key: Uint8Array, value: Uint8Array): void + /** Delete a key and return whether a live record existed. */ + delete(key: Uint8Array): boolean + /** Compare the current version and replace the value if it matches. */ + compareAndPut(key: Uint8Array, version: bigint, value: Uint8Array): boolean + /** Flush dirty frames and the WAL to the backing store. */ + checkpoint(): void + /** Scan keys under an optional prefix. */ + scanKeys(prefix?: Uint8Array | undefined | null, options?: ScanOptions | undefined | null): Array + /** Scan records under an optional prefix. */ + scanRecords(prefix?: Uint8Array | undefined | null, options?: ScanOptions | undefined | null): Array +} + +/** A live value and its conditional-write version. */ +export interface Record { + /** Value bytes stored at the key. */ + value: Buffer + /** Version token for compare-and-put. */ + version: bigint +} + +/** One emitted scan entry. */ +export interface ScanEntry { + /** `key` for a live record or `common_prefix` for a rollup. */ + kind: string + /** Key or common-prefix bytes. */ + path: Buffer + /** Value bytes; absent for key-only scans and common-prefix entries. */ + value?: Buffer + /** Conditional-write version. Zero for common-prefix entries. */ + version: bigint +} + +/** Options shared by key and record scans. */ +export interface ScanOptions { + /** Exclusive lower-bound key. */ + startAfter?: Uint8Array + /** Optional delimiter byte used for common-prefix rollups. */ + delimiter?: number +} + +/** Options for opening a file-backed tree. */ +export interface TreeOptions { + /** Force the WAL to sync before acknowledging each write. */ + walSync?: boolean +} diff --git a/packages/holt-node/index.js b/packages/holt-node/index.js new file mode 100644 index 0000000..84af9a4 --- /dev/null +++ b/packages/holt-node/index.js @@ -0,0 +1,590 @@ +// prettier-ignore +/* eslint-disable */ +// @ts-nocheck +/* auto-generated by NAPI-RS */ + +const { readFileSync } = require('fs') +let nativeBinding = null +const loadErrors = [] + +const isMusl = () => { + let musl = false + if (process.platform === 'linux') { + musl = isMuslFromFilesystem() + if (musl === null) { + musl = isMuslFromReport() + } + if (musl === null) { + musl = isMuslFromChildProcess() + } + } + return musl +} + +const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') + +const isMuslFromFilesystem = () => { + try { + return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') + } catch { + return null + } +} + +const isMuslFromReport = () => { + let report = null + if (process.report && typeof process.report.getReport === 'function') { + process.report.excludeNetwork = true + report = process.report.getReport() + } + if (!report) { + return null + } + if (report.header && report.header.glibcVersionRuntime) { + return false + } + if (Array.isArray(report.sharedObjects)) { + if (report.sharedObjects.some(isFileMusl)) { + return true + } + } + return false +} + +const isMuslFromChildProcess = () => { + try { + return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') + } catch (e) { + // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false + return false + } +} + +function requireNative() { + if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) { + try { + return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH); + } catch (err) { + loadErrors.push(err) + } + } else if (process.platform === 'android') { + if (process.arch === 'arm64') { + try { + return require('./index.android-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-android-arm64') + const bindingPackageVersion = require('@holt/node-android-arm64/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./index.android-arm-eabi.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-android-arm-eabi') + const bindingPackageVersion = require('@holt/node-android-arm-eabi/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) + } + } else if (process.platform === 'win32') { + if (process.arch === 'x64') { + if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) { + try { + return require('./index.win32-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-win32-x64-gnu') + const bindingPackageVersion = require('@holt/node-win32-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./index.win32-x64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-win32-x64-msvc') + const bindingPackageVersion = require('@holt/node-win32-x64-msvc/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ia32') { + try { + return require('./index.win32-ia32-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-win32-ia32-msvc') + const bindingPackageVersion = require('@holt/node-win32-ia32-msvc/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./index.win32-arm64-msvc.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-win32-arm64-msvc') + const bindingPackageVersion = require('@holt/node-win32-arm64-msvc/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) + } + } else if (process.platform === 'darwin') { + try { + return require('./index.darwin-universal.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-darwin-universal') + const bindingPackageVersion = require('@holt/node-darwin-universal/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + if (process.arch === 'x64') { + try { + return require('./index.darwin-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-darwin-x64') + const bindingPackageVersion = require('@holt/node-darwin-x64/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./index.darwin-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-darwin-arm64') + const bindingPackageVersion = require('@holt/node-darwin-arm64/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) + } + } else if (process.platform === 'freebsd') { + if (process.arch === 'x64') { + try { + return require('./index.freebsd-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-freebsd-x64') + const bindingPackageVersion = require('@holt/node-freebsd-x64/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm64') { + try { + return require('./index.freebsd-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-freebsd-arm64') + const bindingPackageVersion = require('@holt/node-freebsd-arm64/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) + } + } else if (process.platform === 'linux') { + if (process.arch === 'x64') { + if (isMusl()) { + try { + return require('./index.linux-x64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-x64-musl') + const bindingPackageVersion = require('@holt/node-linux-x64-musl/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./index.linux-x64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-x64-gnu') + const bindingPackageVersion = require('@holt/node-linux-x64-gnu/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm64') { + if (isMusl()) { + try { + return require('./index.linux-arm64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-arm64-musl') + const bindingPackageVersion = require('@holt/node-linux-arm64-musl/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./index.linux-arm64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-arm64-gnu') + const bindingPackageVersion = require('@holt/node-linux-arm64-gnu/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'arm') { + if (isMusl()) { + try { + return require('./index.linux-arm-musleabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-arm-musleabihf') + const bindingPackageVersion = require('@holt/node-linux-arm-musleabihf/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./index.linux-arm-gnueabihf.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-arm-gnueabihf') + const bindingPackageVersion = require('@holt/node-linux-arm-gnueabihf/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'loong64') { + if (isMusl()) { + try { + return require('./index.linux-loong64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-loong64-musl') + const bindingPackageVersion = require('@holt/node-linux-loong64-musl/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./index.linux-loong64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-loong64-gnu') + const bindingPackageVersion = require('@holt/node-linux-loong64-gnu/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'riscv64') { + if (isMusl()) { + try { + return require('./index.linux-riscv64-musl.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-riscv64-musl') + const bindingPackageVersion = require('@holt/node-linux-riscv64-musl/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + try { + return require('./index.linux-riscv64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-riscv64-gnu') + const bindingPackageVersion = require('@holt/node-linux-riscv64-gnu/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } + } else if (process.arch === 'ppc64') { + try { + return require('./index.linux-ppc64-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-ppc64-gnu') + const bindingPackageVersion = require('@holt/node-linux-ppc64-gnu/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 's390x') { + try { + return require('./index.linux-s390x-gnu.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-linux-s390x-gnu') + const bindingPackageVersion = require('@holt/node-linux-s390x-gnu/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) + } + } else if (process.platform === 'openharmony') { + if (process.arch === 'arm64') { + try { + return require('./index.openharmony-arm64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-openharmony-arm64') + const bindingPackageVersion = require('@holt/node-openharmony-arm64/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'x64') { + try { + return require('./index.openharmony-x64.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-openharmony-x64') + const bindingPackageVersion = require('@holt/node-openharmony-x64/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else if (process.arch === 'arm') { + try { + return require('./index.openharmony-arm.node') + } catch (e) { + loadErrors.push(e) + } + try { + const binding = require('@holt/node-openharmony-arm') + const bindingPackageVersion = require('@holt/node-openharmony-arm/package.json').version + if (bindingPackageVersion !== '0.1.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.1.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + return binding + } catch (e) { + loadErrors.push(e) + } + } else { + loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`)) + } + } else { + loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) + } +} + +nativeBinding = requireNative() + +// NAPI_RS_FORCE_WASI is a tri-state flag: +// unset / any other value → native binding preferred, WASI is only a fallback +// 'true' → force WASI fallback even if native loaded +// 'error' → force WASI and throw if no WASI binding is found +// Treating any non-empty string as truthy (the historical behavior) meant +// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered +// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file. +const forceWasi = + process.env.NAPI_RS_FORCE_WASI === 'true' || process.env.NAPI_RS_FORCE_WASI === 'error' + +if (!nativeBinding || forceWasi) { + let wasiBinding = null + let wasiBindingError = null + try { + wasiBinding = require('./index.wasi.cjs') + nativeBinding = wasiBinding + } catch (err) { + if (forceWasi) { + wasiBindingError = err + } + } + if (!nativeBinding || forceWasi) { + try { + wasiBinding = require('@holt/node-wasm32-wasi') + nativeBinding = wasiBinding + } catch (err) { + if (forceWasi) { + if (!wasiBindingError) { + wasiBindingError = err + } else { + wasiBindingError.cause = err + } + loadErrors.push(err) + } + } + } + if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) { + const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error') + error.cause = wasiBindingError + throw error + } +} + +if (!nativeBinding) { + if (loadErrors.length > 0) { + const error = new Error( + `Cannot find native binding. ` + + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', + ) + // assign instead of the `new Error(message, { cause })` options form, + // which Node < 16.9 silently ignores + error.cause = loadErrors.reduce((err, cur) => { + cur.cause = err + return cur + }) + throw error + } + throw new Error(`Failed to load native binding`) +} + +module.exports = nativeBinding +module.exports.Tree = nativeBinding.Tree diff --git a/packages/holt-node/package-lock.json b/packages/holt-node/package-lock.json new file mode 100644 index 0000000..cb688e2 --- /dev/null +++ b/packages/holt-node/package-lock.json @@ -0,0 +1,1893 @@ +{ + "name": "@holt/node", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@holt/node", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@napi-rs/cli": "^3.5.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", + "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", + "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.1", + "@inquirer/confirm": "^6.1.1", + "@inquirer/editor": "^5.2.2", + "@inquirer/expand": "^5.1.1", + "@inquirer/input": "^5.1.2", + "@inquirer/number": "^4.1.1", + "@inquirer/password": "^5.1.1", + "@inquirer/rawlist": "^5.3.1", + "@inquirer/search": "^4.2.1", + "@inquirer/select": "^5.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@napi-rs/cli": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-3.7.3.tgz", + "integrity": "sha512-iu5BOoYjYVixp5jwE7JniHvg72XuKWXUfXteu+6Gt/XY4/mslsS+Qbipleg1+3CAUGHkWc+ebaMJj7Pc93BXSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/prompts": "^8.5.2", + "@napi-rs/cross-toolchain": "^1.0.3", + "@napi-rs/wasm-tools": "^1.0.1", + "@octokit/rest": "^22.0.1", + "clipanion": "^4.0.0-rc.4", + "colorette": "^2.0.20", + "emnapi": "^1.11.1", + "es-toolkit": "^1.47.0", + "js-yaml": "^4.2.0", + "obug": "^2.1.2", + "semver": "^7.8.2", + "typanion": "^3.14.0" + }, + "bin": { + "napi": "dist/cli.js", + "napi-raw": "cli.mjs" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/runtime": "^1.7.1" + }, + "peerDependenciesMeta": { + "@emnapi/runtime": { + "optional": true + } + } + }, + "node_modules/@napi-rs/cross-toolchain": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@napi-rs/cross-toolchain/-/cross-toolchain-1.0.3.tgz", + "integrity": "sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg==", + "dev": true, + "license": "MIT", + "workspaces": [ + ".", + "arm64/*", + "x64/*" + ], + "dependencies": { + "@napi-rs/lzma": "^1.4.5", + "@napi-rs/tar": "^1.1.0", + "debug": "^4.4.1" + }, + "peerDependencies": { + "@napi-rs/cross-toolchain-arm64-target-aarch64": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-armv7": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-ppc64le": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-s390x": "^1.0.3", + "@napi-rs/cross-toolchain-arm64-target-x86_64": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-aarch64": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-armv7": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-ppc64le": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-s390x": "^1.0.3", + "@napi-rs/cross-toolchain-x64-target-x86_64": "^1.0.3" + }, + "peerDependenciesMeta": { + "@napi-rs/cross-toolchain-arm64-target-aarch64": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-armv7": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-ppc64le": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-s390x": { + "optional": true + }, + "@napi-rs/cross-toolchain-arm64-target-x86_64": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-aarch64": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-armv7": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-ppc64le": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-s390x": { + "optional": true + }, + "@napi-rs/cross-toolchain-x64-target-x86_64": { + "optional": true + } + } + }, + "node_modules/@napi-rs/lzma": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma/-/lzma-1.5.1.tgz", + "integrity": "sha512-sgOZ89+y8cDbY+3WbzR8CtIhCuFRWotZ9/2PjPVDJHz6np5KFTAev0DrwiyTJTgFsCRDhfGlbmhMgyhHbWdZ6g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.20 || ^24.12 || >=25" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/lzma-android-arm-eabi": "1.5.1", + "@napi-rs/lzma-android-arm64": "1.5.1", + "@napi-rs/lzma-darwin-arm64": "1.5.1", + "@napi-rs/lzma-darwin-x64": "1.5.1", + "@napi-rs/lzma-freebsd-x64": "1.5.1", + "@napi-rs/lzma-linux-arm-gnueabihf": "1.5.1", + "@napi-rs/lzma-linux-arm64-gnu": "1.5.1", + "@napi-rs/lzma-linux-arm64-musl": "1.5.1", + "@napi-rs/lzma-linux-ppc64-gnu": "1.5.1", + "@napi-rs/lzma-linux-riscv64-gnu": "1.5.1", + "@napi-rs/lzma-linux-s390x-gnu": "1.5.1", + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@napi-rs/lzma-linux-x64-musl": "1.5.1", + "@napi-rs/lzma-wasm32-wasi": "1.5.1", + "@napi-rs/lzma-win32-arm64-msvc": "1.5.1", + "@napi-rs/lzma-win32-ia32-msvc": "1.5.1", + "@napi-rs/lzma-win32-x64-msvc": "1.5.1" + } + }, + "node_modules/@napi-rs/lzma-android-arm-eabi": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm-eabi/-/lzma-android-arm-eabi-1.5.1.tgz", + "integrity": "sha512-sahBe4ko2Z69NPTddaX6ZgbQZu9SDoITxw1S3dWl1gAGynZG34qHHCT8UaUMFxf3h3zMhCJjEzz4basaBxiTuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-android-arm64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-android-arm64/-/lzma-android-arm64-1.5.1.tgz", + "integrity": "sha512-7tkQAJJuBHxAxiEBNFgSTpvrtGpbwZYYJUSOmGEK3OfbdbNeoT2rdBxpM/gY1s+itEVbtOSlpaRPPG19MnwOzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-darwin-arm64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-arm64/-/lzma-darwin-arm64-1.5.1.tgz", + "integrity": "sha512-XWX8gtF+GHGk3nH3Wm3QUZNcxw9QHsFVZz3MzVLhWWHhceede1J4/vD+3dj3E1iKB9G6mualaZxOoD08R3E+7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-darwin-x64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-darwin-x64/-/lzma-darwin-x64-1.5.1.tgz", + "integrity": "sha512-CfsqUpMTI1z8enrA/b+GcHM6YDI8D0kqCiqPYEnst4rbOABQ9KZ92ybTTNnlnZ7A017WoMZKUEWc36KXDwi0xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-freebsd-x64": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-freebsd-x64/-/lzma-freebsd-x64-1.5.1.tgz", + "integrity": "sha512-bTyNfg90FXIgE61U7l14aMmVOqRQ6AyP5JMT3jmCStaZI18apLNPdzZ8i7yqxZfKvRMVfPjE2brXIw27c+RRgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-arm-gnueabihf": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm-gnueabihf/-/lzma-linux-arm-gnueabihf-1.5.1.tgz", + "integrity": "sha512-vNE+D8nrw+eOkBsdKCsmDhowDV3pIMKXEhedvXfbgrWbrO7GlZJH+RXL+X+RYLxGwi8Ym61ZMt15sIOnNmh9Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-arm64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-gnu/-/lzma-linux-arm64-gnu-1.5.1.tgz", + "integrity": "sha512-csUem4WgoKGTprv/pOPm9UIWbb+hrfUwYXefpTHPAEGVFLl5behEFabisJ7FtihCa3yG2Efcl+yw25rlhhrIYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-arm64-musl": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-arm64-musl/-/lzma-linux-arm64-musl-1.5.1.tgz", + "integrity": "sha512-kB/xhlVN1eLvVmDJSKZEjp5Gg2xDYexNrB5jwpSMbOkeGS6N9AasByPBg5VqCpMYC+zZi7DM458DRhtWYhqXTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-ppc64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-ppc64-gnu/-/lzma-linux-ppc64-gnu-1.5.1.tgz", + "integrity": "sha512-s28RW0W1yBWQc1nbPdF7tp14koqslY3ZWLVI8uaanX292Dc6ezd4NPVwxEoCNBVON/oD7BmUbWGtyFvmm7dQ5A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-riscv64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-riscv64-gnu/-/lzma-linux-riscv64-gnu-1.5.1.tgz", + "integrity": "sha512-+lGNwYlIN14YPMTNvYtIJJqHFevDTd6Juw/1NmXbWx/iRd/LLrjhlM/yluMX6pxs6NkOGsuuEXJJrbbEUS59OQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-s390x-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-s390x-gnu/-/lzma-linux-s390x-gnu-1.5.1.tgz", + "integrity": "sha512-PB44FFWWFrLeQowhcep1hPD1YcLqKlnnY60RMU74qrxTlr4YGEyzeMItJqh2uivBfv9kQScOF/B0J9+Vab/oyw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-musl": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-musl/-/lzma-linux-x64-musl-1.5.1.tgz", + "integrity": "sha512-I3nsYrWtrW9JpeCr+mkJIVDt0HY3m6qVUBs5vTtoIvJQxwqf1PBXSy5IS7T53ksQFH2kd2UX8rLxJ7B4WISpZg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-wasm32-wasi": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-wasm32-wasi/-/lzma-wasm32-wasi-1.5.1.tgz", + "integrity": "sha512-gy3wwPBa6+XEyA4fUzq6CClrXA1ajXjuVf5zbnHytJRgoHznj+mvpU3+co2fxXwqTCmIpn6KrzqH5bRDztBPhA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-win32-arm64-msvc": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-arm64-msvc/-/lzma-win32-arm64-msvc-1.5.1.tgz", + "integrity": "sha512-dK+huOsHiyH6oJjij+cnjqFCakk2HgWmpI12Xm4pLUyPphe4ebYoJBgehaNAxprmjFqBQ7nL95YPVz9BHyqmPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-win32-ia32-msvc": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-ia32-msvc/-/lzma-win32-ia32-msvc-1.5.1.tgz", + "integrity": "sha512-dGE8L+0EQ+GyU9ap9InqB/t/PmPG/bLj918q7OsJ29FuTdn8fK4OX3U4IQZhylHIA+/dQ/SXJk5n4yfah2XVvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/lzma-win32-x64-msvc": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-win32-x64-msvc/-/lzma-win32-x64-msvc-1.5.1.tgz", + "integrity": "sha512-EKW4t/iqdCT/xnd5t9oXLvVER/PMNAWXKqUAl3fgvUcOILeZIIht77/dVnfFcc9htA/DCBXC/6YQWdW+LusjFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/tar": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar/-/tar-1.1.1.tgz", + "integrity": "sha512-p6q2HhUc5vwH1CNwfOcrhLoxfgn8ust8Sqlfx+sA4VzAcp1cMbvbkl99tZZlDqOjCHgQNSiTfk/yWPjl/D42qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/tar-android-arm-eabi": "1.1.1", + "@napi-rs/tar-android-arm64": "1.1.1", + "@napi-rs/tar-darwin-arm64": "1.1.1", + "@napi-rs/tar-darwin-x64": "1.1.1", + "@napi-rs/tar-freebsd-x64": "1.1.1", + "@napi-rs/tar-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/tar-linux-arm64-gnu": "1.1.1", + "@napi-rs/tar-linux-arm64-musl": "1.1.1", + "@napi-rs/tar-linux-ppc64-gnu": "1.1.1", + "@napi-rs/tar-linux-s390x-gnu": "1.1.1", + "@napi-rs/tar-linux-x64-gnu": "1.1.1", + "@napi-rs/tar-linux-x64-musl": "1.1.1", + "@napi-rs/tar-wasm32-wasi": "1.1.1", + "@napi-rs/tar-win32-arm64-msvc": "1.1.1", + "@napi-rs/tar-win32-ia32-msvc": "1.1.1", + "@napi-rs/tar-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/tar-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-android-arm-eabi/-/tar-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-cAhnA10cSusAUbcE9HtjQY/tZ9BH/0w2sKtRcQc94TzIlnm7QSr1htJSd/PPrbWNPtrv1orXb2CkrHlVlbnlHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-android-arm64/-/tar-android-arm64-1.1.1.tgz", + "integrity": "sha512-EslUWHCDBY/g5abTPBiHLsMaML4GagV0TXLm5WL9hAjx/DDtlxz9fegMb77RJ+f7nFLOIsUxF/3QWFvgOT0sMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-darwin-arm64/-/tar-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-+A42/6ES5G9CQ35BOwzwA+WBjLID28r2jNPgc0dteD2hhClIhng0mva7D2ujUlXBNmgNOsr1LHn3stA4uTf4NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-darwin-x64/-/tar-darwin-x64-1.1.1.tgz", + "integrity": "sha512-RYtE8w1dkEvj8hSJCDV5Jw0Rz2i13fsM7u893zv5O9n/4Ad5GNsw/f4RQ7/0YGSFaenkVxqPFrjmEvUHlKzsrg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-freebsd-x64/-/tar-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-rEepBvCJUwcuvUYkY83e8aot8RsR5Jcnal4PsG3tbWGKW1yAvcXhyMXf0fN6ZGpVRZFnB+FJqDyBxvsCPEXKhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm-gnueabihf/-/tar-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-an1bJdfyhI5FpZYyTQ20mrqwR+a676i8GkaYc4Uy12dH/a7TJIfrK6Qa2Gm46arZvxUvx56qxoRKXbpOjUPvwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm64-gnu/-/tar-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-w++Vtx36T2yHTKws7GVnmHHcUT1ybB59xLWSh9A8bwEpJVG4dG7Qub9mFe5cpcbfrJ+XP2mKKxC3oUJSunK3iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-arm64-musl/-/tar-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-Rh6UFhNtj3i4deJHOBINFIeRL0072mgbeyuK5rl1HokKnNoMKx8qKIZNEzBTTqpogMfDHWGvzyTQdnVxes5dpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-ppc64-gnu/-/tar-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-Cp+AxFbv9zcyAXtnzQi0OzmgDnQgy2w9D4Ubr+iwzMtVgJcztzcEoCcCrN1k2ATdEB01LX2Vb49IaocGOZhC9Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-s390x-gnu/-/tar-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-ZyscC3SYKTBWyDRYjLOKAd5TyJ7q0KACRdQ8bWrb3rgrra1CCIJD66CsGTH6Dh0AVSdfLwZ8MfIIXU6+14BMjQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-x64-gnu/-/tar-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-LlIv+zg4fiOQge9LQX/ieBdRWE2fhVDjCTHxnunZkbugNmdhdelxWf1RpZb/6ZujWpNF4LPu4N/MW7ygg2oYAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-linux-x64-musl/-/tar-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-gZBeoKLjanOVj55qk4EMu13P2i9M0SuINmlGQkOxm1niIJofexzddHUYtqO5o/5QqtyL8lADmAcZplLILMLhHA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-wasm32-wasi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-wasm32-wasi/-/tar-wasm32-wasi-1.1.1.tgz", + "integrity": "sha512-rwtQ1Mdt/ft6g6I54fJzbUeLspl4yTwj6I3UJ6mitKnrN42soJkcDrdh3Y/FGvlpqZTad2YMQ96fGJl3EtAm2Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@napi-rs/tar-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-arm64-msvc/-/tar-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-30PVp1AehRpfwxmv5wI4cg0yj3WmWBsZ+1QnLGnvEELu7Eu/+dhNU0nrmhI7VfPgLwSRK2eg9DQTB3tP7Wv9bA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-ia32-msvc/-/tar-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-aI3/rmz+izUChiSeaPxcasAOxhf3FpJNuIHMXlxS/vpW+HIxUsSDR5+XV61PEG5DL4L/75iENVUxmSGM5l2yaw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/tar-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/tar-win32-x64-msvc/-/tar-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-yJsB2IsrODQVLKbm2Fg1nHiVRbEj49mSPbj4x7JPZWJI0jGVPjohE2Sif0FBbx8OxsVoUODvS0BwksZZ8jl/OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@napi-rs/wasm-tools": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools/-/wasm-tools-1.0.1.tgz", + "integrity": "sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/wasm-tools-android-arm-eabi": "1.0.1", + "@napi-rs/wasm-tools-android-arm64": "1.0.1", + "@napi-rs/wasm-tools-darwin-arm64": "1.0.1", + "@napi-rs/wasm-tools-darwin-x64": "1.0.1", + "@napi-rs/wasm-tools-freebsd-x64": "1.0.1", + "@napi-rs/wasm-tools-linux-arm64-gnu": "1.0.1", + "@napi-rs/wasm-tools-linux-arm64-musl": "1.0.1", + "@napi-rs/wasm-tools-linux-x64-gnu": "1.0.1", + "@napi-rs/wasm-tools-linux-x64-musl": "1.0.1", + "@napi-rs/wasm-tools-wasm32-wasi": "1.0.1", + "@napi-rs/wasm-tools-win32-arm64-msvc": "1.0.1", + "@napi-rs/wasm-tools-win32-ia32-msvc": "1.0.1", + "@napi-rs/wasm-tools-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/wasm-tools-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-android-arm-eabi/-/wasm-tools-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-android-arm64/-/wasm-tools-android-arm64-1.0.1.tgz", + "integrity": "sha512-WDR7S+aRLV6LtBJAg5fmjKkTZIdrEnnQxgdsb7Cf8pYiMWBHLU+LC49OUVppQ2YSPY0+GeYm9yuZWW3kLjJ7Bg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-darwin-arm64/-/wasm-tools-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-qWTI+EEkiN0oIn/N2gQo7+TVYil+AJ20jjuzD2vATS6uIjVz+Updeqmszi7zq7rdFTLp6Ea3/z4kDKIfZwmR9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-darwin-x64/-/wasm-tools-darwin-x64-1.0.1.tgz", + "integrity": "sha512-bA6hubqtHROR5UI3tToAF/c6TDmaAgF0SWgo4rADHtQ4wdn0JeogvOk50gs2TYVhKPE2ZD2+qqt7oBKB+sxW3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-freebsd-x64/-/wasm-tools-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-90+KLBkD9hZEjPQW1MDfwSt5J1L46EUKacpCZWyRuL6iIEO5CgWU0V/JnEgFsDOGyyYtiTvHc5bUdUTWd4I9Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-arm64-gnu/-/wasm-tools-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-rG0QlS65x9K/u3HrKafDf8cFKj5wV2JHGfl8abWgKew0GVPyp6vfsDweOwHbWAjcHtp2LHi6JHoW80/MTHm52Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-arm64-musl/-/wasm-tools-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-jAasbIvjZXCgX0TCuEFQr+4D6Lla/3AAVx2LmDuMjgG4xoIXzjKWl7c4chuaD+TI+prWT0X6LJcdzFT+ROKGHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-x64-gnu/-/wasm-tools-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-Plgk5rPqqK2nocBGajkMVbGm010Z7dnUgq0wtnYRZbzWWxwWcXfZMPa8EYxrK4eE8SzpI7VlZP1tdVsdjgGwMw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-linux-x64-musl/-/wasm-tools-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-GW7AzGuWxtQkyHknHWYFdR0CHmW6is8rG2Rf4V6GNmMpmwtXt/ItWYWtBe4zqJWycMNazpfZKSw/BpT7/MVCXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-wasm32-wasi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-wasm32-wasi/-/wasm-tools-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-/nQVSTrqSsn7YdAc2R7Ips/tnw5SPUcl3D7QrXCNGPqjbatIspnaexvaOYNyKMU6xPu+pc0BTnKVmqhlJJCPLA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@napi-rs/wasm-tools-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-arm64-msvc/-/wasm-tools-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-PFi7oJIBu5w7Qzh3dwFea3sHRO3pojMsaEnUIy22QvsW+UJfNQwJCryVrpoUt8m4QyZXI+saEq/0r4GwdoHYFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-ia32-msvc/-/wasm-tools-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-gXkuYzxQsgkj05Zaq+KQTkHIN83dFAwMcTKa2aQcpYPRImFm2AQzEyLtpXmyCWzJ0F9ZYAOmbSyrNew8/us6bw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-tools-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-tools-win32-x64-msvc/-/wasm-tools-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.11", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.11.tgz", + "integrity": "sha512-+s7HUxjfFqOMS9VlIwDffq0MikjSAK0gSpG73W+meAvVAvX4MBrHYTK5Bj3Uot55qFT4gzUtfzE4mGWY4Br8/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "content-type": "^2.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clipanion": { + "version": "4.0.0-rc.4", + "resolved": "https://registry.npmjs.org/clipanion/-/clipanion-4.0.0-rc.4.tgz", + "integrity": "sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q==", + "dev": true, + "license": "MIT", + "workspaces": [ + "website" + ], + "dependencies": { + "typanion": "^3.8.0" + }, + "peerDependencies": { + "typanion": "*" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/emnapi": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/emnapi/-/emnapi-1.11.2.tgz", + "integrity": "sha512-iMt/XQc69fFn2EvcU6tm14HmXKwyy0lnABugsQlqp6xFuZIUuO+ONVSg2mz+MTVF8WbC+bic65AvRXdoldALKg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "node-addon-api": ">= 6.1.0" + }, + "peerDependenciesMeta": { + "node-addon-api": { + "optional": true + } + } + }, + "node_modules/es-toolkit": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.49.0.tgz", + "integrity": "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-with-bigint": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.10.tgz", + "integrity": "sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typanion": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/typanion/-/typanion-3.14.0.tgz", + "integrity": "sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug==", + "dev": true, + "license": "MIT", + "workspaces": [ + "website" + ] + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/packages/holt-node/package.json b/packages/holt-node/package.json new file mode 100644 index 0000000..c956be8 --- /dev/null +++ b/packages/holt-node/package.json @@ -0,0 +1,31 @@ +{ + "name": "@holt/node", + "version": "0.1.0", + "description": "Node.js bindings for the Holt metadata engine", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/EnderRomantice/holt.git", + "directory": "packages/holt-node" + }, + "main": "index.js", + "types": "index.d.ts", + "engines": { + "node": ">=18" + }, + "files": [ + "index.js", + "index.d.ts", + "*.node", + "npm" + ], + "scripts": { + "build": "napi build --platform --release", + "build:debug": "napi build --platform", + "test": "node --test test/*.test.mjs" + }, + "devDependencies": { + "@napi-rs/cli": "^3.5.0" + }, + "optionalDependencies": {} +} diff --git a/packages/holt-node/src/lib.rs b/packages/holt-node/src/lib.rs new file mode 100644 index 0000000..bd342e9 --- /dev/null +++ b/packages/holt-node/src/lib.rs @@ -0,0 +1,273 @@ +#![deny(unsafe_op_in_unsafe_fn)] + +use holt::{KeyRangeEntry, RangeEntry, Tree as CoreTree, TreeBuilder}; +use napi::bindgen_prelude::{BigInt, Buffer, Error, Result, Status, Uint8Array}; +use napi_derive::napi; + +fn js_error(error: holt::Error) -> Error { + Error::new(Status::GenericFailure, error.to_string()) +} + +fn delimiter(value: Option) -> Result> { + value + .map(|value| { + u8::try_from(value).map_err(|_| { + Error::new( + Status::InvalidArg, + "delimiter must be an integer between 0 and 255", + ) + }) + }) + .transpose() +} + +fn into_buffer(bytes: Vec) -> Buffer { + Buffer::from(bytes) +} + +fn into_bigint(value: u64) -> BigInt { + BigInt::from(value) +} + +fn from_bigint(value: BigInt) -> Result { + let (negative, value, lossless) = value.get_u64(); + if negative || !lossless { + return Err(Error::new( + Status::InvalidArg, + "version must be a non-negative bigint that fits in uint64", + )); + } + Ok(value) +} + +/// Options for opening a file-backed tree. +#[napi(object)] +pub struct TreeOptions { + /// Force the WAL to sync before acknowledging each write. + pub wal_sync: Option, +} + +/// Options shared by key and record scans. +#[napi(object)] +pub struct ScanOptions { + /// Exclusive lower-bound key. + pub start_after: Option, + /// Optional delimiter byte used for common-prefix rollups. + pub delimiter: Option, +} + +/// A live value and its conditional-write version. +#[napi(object)] +pub struct Record { + /// Value bytes stored at the key. + pub value: Buffer, + /// Version token for compare-and-put. + pub version: BigInt, +} + +/// One emitted scan entry. +#[napi(object)] +pub struct ScanEntry { + /// `key` for a live record or `common_prefix` for a rollup. + pub kind: String, + /// Key or common-prefix bytes. + pub path: Buffer, + /// Value bytes; absent for key-only scans and common-prefix entries. + pub value: Option, + /// Conditional-write version. Zero for common-prefix entries. + pub version: BigInt, +} + +/// A Node.js handle to one Holt tree. +#[napi] +pub struct Tree { + inner: Option, +} + +#[napi] +impl Tree { + /// Open a file-backed tree. + #[napi(factory)] + pub fn open(path: String, options: Option) -> Result { + let wal_sync = options + .and_then(|options| options.wal_sync) + .unwrap_or(false); + TreeBuilder::new(path) + .durability(holt::Durability::Wal { sync: wal_sync }) + .open() + .map(|inner| Self { inner: Some(inner) }) + .map_err(js_error) + } + + /// Open a volatile in-memory tree. + #[napi(factory)] + pub fn open_memory() -> Result { + TreeBuilder::new("holt-node-memory") + .memory() + .open() + .map(|inner| Self { inner: Some(inner) }) + .map_err(js_error) + } + + /// Explicitly release the native tree handle. This is idempotent. + #[napi] + pub fn close(&mut self) { + self.inner = None; + } + + /// Return the value stored at `key`, or null when the key is absent. + #[napi] + pub fn get(&self, key: Uint8Array) -> Result> { + self.core()? + .get(key.as_ref()) + .map(|value| value.map(into_buffer)) + .map_err(js_error) + } + + /// Return the value and conditional-write version for `key`. + #[napi(js_name = "getRecord")] + pub fn get_record(&self, key: Uint8Array) -> Result> { + self.core()? + .get_record(key.as_ref()) + .map(|record| { + record.map(|record| Record { + value: into_buffer(record.value), + version: into_bigint(record.version.as_u64()), + }) + }) + .map_err(js_error) + } + + /// Insert or replace a value. + #[napi] + pub fn put(&self, key: Uint8Array, value: Uint8Array) -> Result<()> { + self.core()? + .put(key.as_ref(), value.as_ref()) + .map_err(js_error) + } + + /// Delete a key and return whether a live record existed. + #[napi] + pub fn delete(&self, key: Uint8Array) -> Result { + self.core()?.delete(key.as_ref()).map_err(js_error) + } + + /// Compare the current version and replace the value if it matches. + #[napi(js_name = "compareAndPut")] + pub fn compare_and_put( + &self, + key: Uint8Array, + version: BigInt, + value: Uint8Array, + ) -> Result { + self.core()? + .compare_and_put( + key.as_ref(), + holt::RecordVersion::from_raw(from_bigint(version)?), + value.as_ref(), + ) + .map_err(js_error) + } + + /// Flush dirty frames and the WAL to the backing store. + #[napi] + pub fn checkpoint(&self) -> Result<()> { + self.core()?.checkpoint().map_err(js_error) + } + + /// Scan keys under an optional prefix. + #[napi(js_name = "scanKeys")] + pub fn scan_keys( + &self, + prefix: Option, + options: Option, + ) -> Result> { + let options = options.unwrap_or(ScanOptions { + start_after: None, + delimiter: None, + }); + let delimiter = delimiter(options.delimiter)?; + let prefix = prefix.map(|prefix| prefix.to_vec()).unwrap_or_default(); + let mut builder = self.core()?.scan_keys(prefix.as_ref()); + if let Some(start_after) = options.start_after { + builder = builder.start_after(start_after.as_ref()); + } + if let Some(delimiter) = delimiter { + builder = builder.delimiter(delimiter); + } + builder + .into_iter() + .map(|entry| match entry.map_err(js_error)? { + KeyRangeEntry::Key { key, version } => Ok(ScanEntry { + kind: "key".to_owned(), + path: into_buffer(key), + value: None, + version: into_bigint(version.as_u64()), + }), + KeyRangeEntry::CommonPrefix(path) => Ok(ScanEntry { + kind: "common_prefix".to_owned(), + path: into_buffer(path), + value: None, + version: into_bigint(0), + }), + _ => Err(Error::new( + Status::GenericFailure, + "Holt returned an unsupported key scan entry", + )), + }) + .collect() + } + + /// Scan records under an optional prefix. + #[napi(js_name = "scanRecords")] + pub fn scan_records( + &self, + prefix: Option, + options: Option, + ) -> Result> { + let options = options.unwrap_or(ScanOptions { + start_after: None, + delimiter: None, + }); + let delimiter = delimiter(options.delimiter)?; + let prefix = prefix.map(|prefix| prefix.to_vec()).unwrap_or_default(); + let mut builder = self.core()?.scan(prefix.as_ref()); + if let Some(start_after) = options.start_after { + builder = builder.start_after(start_after.as_ref()); + } + if let Some(delimiter) = delimiter { + builder = builder.delimiter(delimiter); + } + builder + .into_iter() + .map(|entry| match entry.map_err(js_error)? { + RangeEntry::Key { + key, + value, + version, + } => Ok(ScanEntry { + kind: "key".to_owned(), + path: into_buffer(key), + value: Some(into_buffer(value)), + version: into_bigint(version.as_u64()), + }), + RangeEntry::CommonPrefix(path) => Ok(ScanEntry { + kind: "common_prefix".to_owned(), + path: into_buffer(path), + value: None, + version: into_bigint(0), + }), + _ => Err(Error::new( + Status::GenericFailure, + "Holt returned an unsupported record scan entry", + )), + }) + .collect() + } + + fn core(&self) -> Result<&CoreTree> { + self.inner + .as_ref() + .ok_or_else(|| Error::new(Status::GenericFailure, "Holt tree is closed")) + } +} diff --git a/packages/holt-node/test/basic.test.mjs b/packages/holt-node/test/basic.test.mjs new file mode 100644 index 0000000..f67ca34 --- /dev/null +++ b/packages/holt-node/test/basic.test.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Tree } from "../index.js"; + +test("memory CRUD and conditional update", () => { + const tree = Tree.openMemory(); + const key = Buffer.from("bucket/a.jpg"); + tree.put(key, Buffer.from("old")); + assert.deepEqual(tree.get(key), Buffer.from("old")); + + const record = tree.getRecord(key); + assert.equal(typeof record.version, "bigint"); + assert.equal(tree.compareAndPut(key, record.version, Buffer.from("new")), true); + assert.deepEqual(tree.get(key), Buffer.from("new")); + assert.equal(tree.delete(key), true); + assert.equal(tree.get(key), null); + tree.close(); + tree.close(); +}); + +test("prefix and delimiter scans", () => { + const tree = Tree.openMemory(); + for (const key of ["bucket/a/1", "bucket/a/2", "bucket/b/1"]) { + tree.put(Buffer.from(key), Buffer.from(key)); + } + const entries = tree.scanKeys(Buffer.from("bucket/"), { delimiter: 47 }); + assert.deepEqual( + entries.map((entry) => [entry.kind, entry.path.toString()]), + [ + ["common_prefix", "bucket/a/"], + ["common_prefix", "bucket/b/"], + ], + ); + tree.close(); +}); diff --git a/packages/holt-node/test/reopen.test.mjs b/packages/holt-node/test/reopen.test.mjs new file mode 100644 index 0000000..2cd10ed --- /dev/null +++ b/packages/holt-node/test/reopen.test.mjs @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { Tree } from "../index.js"; + +test("file-backed tree reopens after checkpoint", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "holt-node-")); + const key = Buffer.from("objects/1"); + const first = Tree.open(dir, { walSync: true }); + first.put(key, Buffer.from("metadata")); + first.checkpoint(); + first.close(); + + const second = Tree.open(dir, { walSync: true }); + assert.deepEqual(second.get(key), Buffer.from("metadata")); + second.close(); + await fs.rm(dir, { recursive: true, force: true }); +}); From 32f65e80b95af9f3f5435d85339ba8e8716acc5e Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 19 Jul 2026 15:41:24 +0800 Subject: [PATCH 2/4] refactor: move Node bindings under crates --- Cargo.toml | 2 +- {packages => crates}/holt-node/.gitignore | 0 {packages => crates}/holt-node/Cargo.toml | 0 {packages => crates}/holt-node/README.md | 0 {packages => crates}/holt-node/build.rs | 0 {packages => crates}/holt-node/index.d.ts | 0 {packages => crates}/holt-node/index.js | 0 {packages => crates}/holt-node/package-lock.json | 0 {packages => crates}/holt-node/package.json | 2 +- {packages => crates}/holt-node/src/lib.rs | 0 {packages => crates}/holt-node/test/basic.test.mjs | 0 {packages => crates}/holt-node/test/reopen.test.mjs | 0 12 files changed, 2 insertions(+), 2 deletions(-) rename {packages => crates}/holt-node/.gitignore (100%) rename {packages => crates}/holt-node/Cargo.toml (100%) rename {packages => crates}/holt-node/README.md (100%) rename {packages => crates}/holt-node/build.rs (100%) rename {packages => crates}/holt-node/index.d.ts (100%) rename {packages => crates}/holt-node/index.js (100%) rename {packages => crates}/holt-node/package-lock.json (100%) rename {packages => crates}/holt-node/package.json (94%) rename {packages => crates}/holt-node/src/lib.rs (100%) rename {packages => crates}/holt-node/test/basic.test.mjs (100%) rename {packages => crates}/holt-node/test/reopen.test.mjs (100%) diff --git a/Cargo.toml b/Cargo.toml index afd9664..79b12e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ targets = [ ] [workspace] -members = [".", "crates/holt-ffi", "packages/holt-node"] +members = [".", "crates/holt-ffi", "crates/holt-node"] exclude = ["benches", "fuzz", "tools/soak", "verified"] [dependencies] diff --git a/packages/holt-node/.gitignore b/crates/holt-node/.gitignore similarity index 100% rename from packages/holt-node/.gitignore rename to crates/holt-node/.gitignore diff --git a/packages/holt-node/Cargo.toml b/crates/holt-node/Cargo.toml similarity index 100% rename from packages/holt-node/Cargo.toml rename to crates/holt-node/Cargo.toml diff --git a/packages/holt-node/README.md b/crates/holt-node/README.md similarity index 100% rename from packages/holt-node/README.md rename to crates/holt-node/README.md diff --git a/packages/holt-node/build.rs b/crates/holt-node/build.rs similarity index 100% rename from packages/holt-node/build.rs rename to crates/holt-node/build.rs diff --git a/packages/holt-node/index.d.ts b/crates/holt-node/index.d.ts similarity index 100% rename from packages/holt-node/index.d.ts rename to crates/holt-node/index.d.ts diff --git a/packages/holt-node/index.js b/crates/holt-node/index.js similarity index 100% rename from packages/holt-node/index.js rename to crates/holt-node/index.js diff --git a/packages/holt-node/package-lock.json b/crates/holt-node/package-lock.json similarity index 100% rename from packages/holt-node/package-lock.json rename to crates/holt-node/package-lock.json diff --git a/packages/holt-node/package.json b/crates/holt-node/package.json similarity index 94% rename from packages/holt-node/package.json rename to crates/holt-node/package.json index c956be8..b14388a 100644 --- a/packages/holt-node/package.json +++ b/crates/holt-node/package.json @@ -6,7 +6,7 @@ "repository": { "type": "git", "url": "https://github.com/EnderRomantice/holt.git", - "directory": "packages/holt-node" + "directory": "crates/holt-node" }, "main": "index.js", "types": "index.d.ts", diff --git a/packages/holt-node/src/lib.rs b/crates/holt-node/src/lib.rs similarity index 100% rename from packages/holt-node/src/lib.rs rename to crates/holt-node/src/lib.rs diff --git a/packages/holt-node/test/basic.test.mjs b/crates/holt-node/test/basic.test.mjs similarity index 100% rename from packages/holt-node/test/basic.test.mjs rename to crates/holt-node/test/basic.test.mjs diff --git a/packages/holt-node/test/reopen.test.mjs b/crates/holt-node/test/reopen.test.mjs similarity index 100% rename from packages/holt-node/test/reopen.test.mjs rename to crates/holt-node/test/reopen.test.mjs From 4139c4baf2a861c6550809dc487543da42a57a6e Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Sun, 19 Jul 2026 15:46:47 +0800 Subject: [PATCH 3/4] feat(node): expose multi-tree database API Add Database bindings for named-tree create/open/drop/list operations and shared checkpointing, with in-memory isolation, file reopen, and Next.js integration coverage. --- crates/holt-node/README.md | 23 ++++++- crates/holt-node/index.d.ts | 27 +++++++- crates/holt-node/index.js | 1 + crates/holt-node/src/lib.rs | 98 ++++++++++++++++++++++++++- crates/holt-node/test/basic.test.mjs | 28 +++++++- crates/holt-node/test/reopen.test.mjs | 27 +++++++- 6 files changed, 197 insertions(+), 7 deletions(-) diff --git a/crates/holt-node/README.md b/crates/holt-node/README.md index ca8dc11..844c75c 100644 --- a/crates/holt-node/README.md +++ b/crates/holt-node/README.md @@ -8,8 +8,8 @@ crate. Keys and values are `Buffer`/`Uint8Array` instances; scans return `kind`, `path`, `value`, and `version` fields. `version` is a JavaScript `bigint` in the generated typings. -This first package exposes the core `Tree` API. It is currently Unix-only, -matching Holt's file-store support. +The package exposes the core `Tree` API plus `Database`, Holt's multi-tree +handle. It is currently Unix-only, matching Holt's file-store support. ```ts import { Tree } from "@holt/node"; @@ -21,6 +21,25 @@ console.log(tree.scanKeys(Buffer.from("bucket/"))); tree.close(); ``` +Multiple named trees can share one database, WAL, and checkpoint boundary: + +```ts +import { Database } from "@holt/node"; + +const db = Database.open("/var/lib/app/holt", { walSync: true }); +const objects = db.openOrCreateTree("objects"); +const sessions = db.openOrCreateTree("sessions"); + +objects.put(Buffer.from("bucket/a"), Buffer.from("object metadata")); +sessions.put(Buffer.from("session/1"), Buffer.from("session metadata")); + +console.log(db.listTrees()); +db.checkpoint(); +objects.close(); +sessions.close(); +db.close(); +``` + Build the native artifact locally with: ```sh diff --git a/crates/holt-node/index.d.ts b/crates/holt-node/index.d.ts index 40a5791..ad85e47 100644 --- a/crates/holt-node/index.d.ts +++ b/crates/holt-node/index.d.ts @@ -1,5 +1,30 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ +/** A Node.js handle to a Holt multi-tree database. */ +export declare class Database { + /** Open a file-backed multi-tree database. */ + static open(path: string, options?: TreeOptions | undefined | null): Database + /** Open a volatile in-memory multi-tree database. */ + static openMemory(): Database + /** + * Explicitly release the database handle. Existing Tree handles remain + * usable until they are closed or their named tree is dropped. + */ + close(): void + /** Create a new named tree. */ + createTree(name: string): Tree + /** Open an existing named tree. */ + openTree(name: string): Tree + /** Open a named tree, creating it when it does not exist. */ + openOrCreateTree(name: string): Tree + /** List all live named trees. */ + listTrees(): Array + /** Drop a named tree and fence existing handles to it. */ + dropTree(name: string): void + /** Flush every named tree and the shared WAL to the backing store. */ + checkpoint(): void +} + /** A Node.js handle to one Holt tree. */ export declare class Tree { /** Open a file-backed tree. */ @@ -54,7 +79,7 @@ export interface ScanOptions { delimiter?: number } -/** Options for opening a file-backed tree. */ +/** Options for opening a file-backed tree or database. */ export interface TreeOptions { /** Force the WAL to sync before acknowledging each write. */ walSync?: boolean diff --git a/crates/holt-node/index.js b/crates/holt-node/index.js index 84af9a4..022f4d0 100644 --- a/crates/holt-node/index.js +++ b/crates/holt-node/index.js @@ -587,4 +587,5 @@ if (!nativeBinding) { } module.exports = nativeBinding +module.exports.Database = nativeBinding.Database module.exports.Tree = nativeBinding.Tree diff --git a/crates/holt-node/src/lib.rs b/crates/holt-node/src/lib.rs index bd342e9..35f8b83 100644 --- a/crates/holt-node/src/lib.rs +++ b/crates/holt-node/src/lib.rs @@ -1,6 +1,8 @@ #![deny(unsafe_op_in_unsafe_fn)] -use holt::{KeyRangeEntry, RangeEntry, Tree as CoreTree, TreeBuilder}; +use holt::{ + KeyRangeEntry, RangeEntry, Tree as CoreTree, TreeBuilder, TreeConfig, DB as CoreDatabase, +}; use napi::bindgen_prelude::{BigInt, Buffer, Error, Result, Status, Uint8Array}; use napi_derive::napi; @@ -40,7 +42,7 @@ fn from_bigint(value: BigInt) -> Result { Ok(value) } -/// Options for opening a file-backed tree. +/// Options for opening a file-backed tree or database. #[napi(object)] pub struct TreeOptions { /// Force the WAL to sync before acknowledging each write. @@ -78,6 +80,94 @@ pub struct ScanEntry { pub version: BigInt, } +/// A Node.js handle to a Holt multi-tree database. +#[napi] +pub struct Database { + inner: Option, +} + +#[napi] +impl Database { + /// Open a file-backed multi-tree database. + #[napi(factory)] + pub fn open(path: String, options: Option) -> Result { + let wal_sync = options + .and_then(|options| options.wal_sync) + .unwrap_or(false); + let mut config = TreeConfig::new(path); + config.durability = holt::Durability::Wal { sync: wal_sync }; + CoreDatabase::open(config) + .map(|inner| Self { inner: Some(inner) }) + .map_err(js_error) + } + + /// Open a volatile in-memory multi-tree database. + #[napi(factory)] + pub fn open_memory() -> Result { + CoreDatabase::open(TreeConfig::memory()) + .map(|inner| Self { inner: Some(inner) }) + .map_err(js_error) + } + + /// Explicitly release the database handle. Existing Tree handles remain + /// usable until they are closed or their named tree is dropped. + #[napi] + pub fn close(&mut self) { + self.inner = None; + } + + /// Create a new named tree. + #[napi(js_name = "createTree")] + pub fn create_tree(&self, name: String) -> Result { + self.core()? + .create_tree(&name) + .map(Tree::from_core) + .map_err(js_error) + } + + /// Open an existing named tree. + #[napi(js_name = "openTree")] + pub fn open_tree(&self, name: String) -> Result { + self.core()? + .open_tree(&name) + .map(Tree::from_core) + .map_err(js_error) + } + + /// Open a named tree, creating it when it does not exist. + #[napi(js_name = "openOrCreateTree")] + pub fn open_or_create_tree(&self, name: String) -> Result { + self.core()? + .open_or_create_tree(&name) + .map(Tree::from_core) + .map_err(js_error) + } + + /// List all live named trees. + #[napi(js_name = "listTrees")] + pub fn list_trees(&self) -> Result> { + self.core()?.list_trees().map_err(js_error) + } + + /// Drop a named tree and fence existing handles to it. + #[napi(js_name = "dropTree")] + pub fn drop_tree(&self, name: String) -> Result<()> { + self.core()?.drop_tree(&name).map_err(js_error) + } + + /// Flush every named tree and the shared WAL to the backing store. + #[napi] + pub fn checkpoint(&self) -> Result<()> { + self.core()?.checkpoint().map_err(js_error) + } + + fn core(&self) -> Result<&CoreDatabase> { + self.inner + .as_ref() + .ok_or_else(|| Error::new(Status::GenericFailure, "Holt database is closed")) + } +} + /// A Node.js handle to one Holt tree. #[napi] pub struct Tree { @@ -86,6 +176,10 @@ pub struct Tree { #[napi] impl Tree { + fn from_core(inner: CoreTree) -> Self { + Self { inner: Some(inner) } + } + /// Open a file-backed tree. #[napi(factory)] pub fn open(path: String, options: Option) -> Result { diff --git a/crates/holt-node/test/basic.test.mjs b/crates/holt-node/test/basic.test.mjs index f67ca34..fd67dee 100644 --- a/crates/holt-node/test/basic.test.mjs +++ b/crates/holt-node/test/basic.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { Tree } from "../index.js"; +import { Database, Tree } from "../index.js"; test("memory CRUD and conditional update", () => { const tree = Tree.openMemory(); @@ -33,3 +33,29 @@ test("prefix and delimiter scans", () => { ); tree.close(); }); + +test("database creates and isolates named trees", () => { + const db = Database.openMemory(); + const objects = db.createTree("objects"); + const sessions = db.openOrCreateTree("sessions"); + const key = Buffer.from("same-key"); + + objects.put(key, Buffer.from("object-value")); + sessions.put(key, Buffer.from("session-value")); + assert.deepEqual(objects.get(key), Buffer.from("object-value")); + assert.deepEqual(sessions.get(key), Buffer.from("session-value")); + assert.deepEqual(db.listTrees().sort(), ["objects", "sessions"]); + + const secondObjectsHandle = db.openTree("objects"); + assert.deepEqual(secondObjectsHandle.get(key), Buffer.from("object-value")); + secondObjectsHandle.close(); + + db.dropTree("sessions"); + assert.deepEqual(db.listTrees(), ["objects"]); + assert.throws(() => sessions.get(key), /dropped/i); + + sessions.close(); + objects.close(); + db.close(); + db.close(); +}); diff --git a/crates/holt-node/test/reopen.test.mjs b/crates/holt-node/test/reopen.test.mjs index 2cd10ed..1004c45 100644 --- a/crates/holt-node/test/reopen.test.mjs +++ b/crates/holt-node/test/reopen.test.mjs @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; -import { Tree } from "../index.js"; +import { Database, Tree } from "../index.js"; test("file-backed tree reopens after checkpoint", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "holt-node-")); @@ -18,3 +18,28 @@ test("file-backed tree reopens after checkpoint", async () => { second.close(); await fs.rm(dir, { recursive: true, force: true }); }); + +test("file-backed database reopens named trees", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "holt-node-db-")); + const key = Buffer.from("objects/1"); + const first = Database.open(dir, { walSync: true }); + const objects = first.createTree("objects"); + const metadata = first.createTree("metadata"); + objects.put(key, Buffer.from("object-value")); + metadata.put(key, Buffer.from("metadata-value")); + first.checkpoint(); + objects.close(); + metadata.close(); + first.close(); + + const second = Database.open(dir, { walSync: true }); + assert.deepEqual(second.listTrees().sort(), ["metadata", "objects"]); + const reopenedObjects = second.openTree("objects"); + const reopenedMetadata = second.openTree("metadata"); + assert.deepEqual(reopenedObjects.get(key), Buffer.from("object-value")); + assert.deepEqual(reopenedMetadata.get(key), Buffer.from("metadata-value")); + reopenedObjects.close(); + reopenedMetadata.close(); + second.close(); + await fs.rm(dir, { recursive: true, force: true }); +}); From 133dd6558a8b061eb7daebcb01fefc633391186a Mon Sep 17 00:00:00 2001 From: EnderRomantice Date: Mon, 20 Jul 2026 04:40:39 +0800 Subject: [PATCH 4/4] fix(node): move storage operations off event loop Expose Promise-based Tree and Database APIs backed by native worker tasks, including async open/replay, CRUD, scans, checkpoints, and close. Update generated types, documentation, and Node/Next.js coverage. --- Cargo.lock | 10 + crates/holt-node/Cargo.toml | 2 +- crates/holt-node/README.md | 34 +- crates/holt-node/index.d.ts | 88 +++-- crates/holt-node/src/lib.rs | 513 +++++++++++++++++--------- crates/holt-node/test/basic.test.mjs | 104 ++++-- crates/holt-node/test/reopen.test.mjs | 56 +-- 7 files changed, 512 insertions(+), 295 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5ff340..9c8113e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,7 @@ dependencies = [ "napi-sys", "nohash-hasher", "rustc-hash", + "tokio", ] [[package]] @@ -877,6 +878,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tokio" +version = "1.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +dependencies = [ + "pin-project-lite", +] + [[package]] name = "tracing" version = "0.1.44" diff --git a/crates/holt-node/Cargo.toml b/crates/holt-node/Cargo.toml index b3429c5..4976d7f 100644 --- a/crates/holt-node/Cargo.toml +++ b/crates/holt-node/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["cdylib"] [dependencies] holt = { path = "../..", default-features = false } -napi = { version = "3", default-features = false, features = ["napi9"] } +napi = { version = "3", default-features = false, features = ["napi9", "tokio_rt"] } napi-derive = "3" [build-dependencies] diff --git a/crates/holt-node/README.md b/crates/holt-node/README.md index 844c75c..018b0c6 100644 --- a/crates/holt-node/README.md +++ b/crates/holt-node/README.md @@ -14,11 +14,11 @@ handle. It is currently Unix-only, matching Holt's file-store support. ```ts import { Tree } from "@holt/node"; -const tree = Tree.openMemory(); -tree.put(Buffer.from("bucket/a"), Buffer.from("metadata")); -console.log(tree.get(Buffer.from("bucket/a"))?.toString()); -console.log(tree.scanKeys(Buffer.from("bucket/"))); -tree.close(); +const tree = await Tree.openMemory(); +await tree.put(Buffer.from("bucket/a"), Buffer.from("metadata")); +console.log((await tree.get(Buffer.from("bucket/a")))?.toString()); +console.log(await tree.scanKeys(Buffer.from("bucket/"))); +await tree.close(); ``` Multiple named trees can share one database, WAL, and checkpoint boundary: @@ -26,20 +26,24 @@ Multiple named trees can share one database, WAL, and checkpoint boundary: ```ts import { Database } from "@holt/node"; -const db = Database.open("/var/lib/app/holt", { walSync: true }); -const objects = db.openOrCreateTree("objects"); -const sessions = db.openOrCreateTree("sessions"); +const db = await Database.open("/var/lib/app/holt", { walSync: true }); +const objects = await db.openOrCreateTree("objects"); +const sessions = await db.openOrCreateTree("sessions"); -objects.put(Buffer.from("bucket/a"), Buffer.from("object metadata")); -sessions.put(Buffer.from("session/1"), Buffer.from("session metadata")); +await objects.put(Buffer.from("bucket/a"), Buffer.from("object metadata")); +await sessions.put(Buffer.from("session/1"), Buffer.from("session metadata")); -console.log(db.listTrees()); -db.checkpoint(); -objects.close(); -sessions.close(); -db.close(); +console.log(await db.listTrees()); +await db.checkpoint(); +await objects.close(); +await sessions.close(); +await db.close(); ``` +All storage operations return Promises and execute on native worker threads, +so file I/O, WAL sync, replay, checkpoints, and scans do not block the Node.js +event loop. + Build the native artifact locally with: ```sh diff --git a/crates/holt-node/index.d.ts b/crates/holt-node/index.d.ts index ad85e47..cec90d0 100644 --- a/crates/holt-node/index.d.ts +++ b/crates/holt-node/index.d.ts @@ -2,53 +2,59 @@ /* eslint-disable */ /** A Node.js handle to a Holt multi-tree database. */ export declare class Database { - /** Open a file-backed multi-tree database. */ - static open(path: string, options?: TreeOptions | undefined | null): Database - /** Open a volatile in-memory multi-tree database. */ - static openMemory(): Database /** - * Explicitly release the database handle. Existing Tree handles remain - * usable until they are closed or their named tree is dropped. + * Open a file-backed multi-tree database without blocking the Node.js + * event loop. */ - close(): void - /** Create a new named tree. */ - createTree(name: string): Tree - /** Open an existing named tree. */ - openTree(name: string): Tree - /** Open a named tree, creating it when it does not exist. */ - openOrCreateTree(name: string): Tree - /** List all live named trees. */ - listTrees(): Array - /** Drop a named tree and fence existing handles to it. */ - dropTree(name: string): void - /** Flush every named tree and the shared WAL to the backing store. */ - checkpoint(): void + static open(path: string, options?: TreeOptions | undefined | null): Promise + /** + * Open a volatile in-memory multi-tree database without blocking the + * Node.js event loop. + */ + static openMemory(): Promise + /** + * Explicitly release the database handle on a worker thread. Existing + * Tree handles remain usable until they are closed or dropped. + */ + close(): Promise + /** Create a new named tree on a worker thread. */ + createTree(name: string): Promise + /** Open an existing named tree on a worker thread. */ + openTree(name: string): Promise + /** Open a named tree on a worker thread, creating it when absent. */ + openOrCreateTree(name: string): Promise + /** List all live named trees on a worker thread. */ + listTrees(): Promise> + /** Drop a named tree and fence existing handles on a worker thread. */ + dropTree(name: string): Promise + /** Flush every named tree and the shared WAL on a worker thread. */ + checkpoint(): Promise } /** A Node.js handle to one Holt tree. */ export declare class Tree { - /** Open a file-backed tree. */ - static open(path: string, options?: TreeOptions | undefined | null): Tree - /** Open a volatile in-memory tree. */ - static openMemory(): Tree - /** Explicitly release the native tree handle. This is idempotent. */ - close(): void - /** Return the value stored at `key`, or null when the key is absent. */ - get(key: Uint8Array): Buffer | null - /** Return the value and conditional-write version for `key`. */ - getRecord(key: Uint8Array): Record | null - /** Insert or replace a value. */ - put(key: Uint8Array, value: Uint8Array): void - /** Delete a key and return whether a live record existed. */ - delete(key: Uint8Array): boolean - /** Compare the current version and replace the value if it matches. */ - compareAndPut(key: Uint8Array, version: bigint, value: Uint8Array): boolean - /** Flush dirty frames and the WAL to the backing store. */ - checkpoint(): void - /** Scan keys under an optional prefix. */ - scanKeys(prefix?: Uint8Array | undefined | null, options?: ScanOptions | undefined | null): Array - /** Scan records under an optional prefix. */ - scanRecords(prefix?: Uint8Array | undefined | null, options?: ScanOptions | undefined | null): Array + /** Open a file-backed tree without blocking the Node.js event loop. */ + static open(path: string, options?: TreeOptions | undefined | null): Promise + /** Open a volatile in-memory tree without blocking the Node.js event loop. */ + static openMemory(): Promise + /** Explicitly release the native tree handle on a worker thread. */ + close(): Promise + /** Return the value at `key` from a worker thread. */ + get(key: Uint8Array): Promise + /** Return the value and conditional-write version from a worker thread. */ + getRecord(key: Uint8Array): Promise + /** Insert or replace a value on a worker thread. */ + put(key: Uint8Array, value: Uint8Array): Promise + /** Delete a key on a worker thread and return whether it existed. */ + delete(key: Uint8Array): Promise + /** Compare the current version and replace the value on a worker thread. */ + compareAndPut(key: Uint8Array, version: bigint, value: Uint8Array): Promise + /** Flush dirty frames and the WAL on a worker thread. */ + checkpoint(): Promise + /** Scan keys on a worker thread. */ + scanKeys(prefix?: Uint8Array | undefined | null, options?: ScanOptions | undefined | null): Promise> + /** Scan records on a worker thread. */ + scanRecords(prefix?: Uint8Array | undefined | null, options?: ScanOptions | undefined | null): Promise> } /** A live value and its conditional-write version. */ diff --git a/crates/holt-node/src/lib.rs b/crates/holt-node/src/lib.rs index 35f8b83..5ff8ec5 100644 --- a/crates/holt-node/src/lib.rs +++ b/crates/holt-node/src/lib.rs @@ -3,13 +3,119 @@ use holt::{ KeyRangeEntry, RangeEntry, Tree as CoreTree, TreeBuilder, TreeConfig, DB as CoreDatabase, }; -use napi::bindgen_prelude::{BigInt, Buffer, Error, Result, Status, Uint8Array}; +use napi::bindgen_prelude::{ + spawn_blocking, AsyncTask, BigInt, Buffer, Error, Result, Status, ToNapiValue, TypeName, + Uint8Array, +}; +use napi::{Env, Task}; use napi_derive::napi; fn js_error(error: holt::Error) -> Error { Error::new(Status::GenericFailure, error.to_string()) } +fn worker_join_error(error: napi::tokio::task::JoinError) -> Error { + Error::new( + Status::GenericFailure, + format!("Holt worker task failed: {error}"), + ) +} + +async fn spawn_holt(operation: F) -> Result +where + T: Send + 'static, + F: FnOnce() -> holt::Result + Send + 'static, +{ + spawn_blocking(operation) + .await + .map_err(worker_join_error)? + .map_err(js_error) +} + +/// A libuv worker-pool task used by the generated Node-API Promise wrappers. +pub struct WorkerTask { + operation: Option Result + Send>>, + resolver: Option Result + Send>>, +} + +impl WorkerTask { + fn new( + operation: impl FnOnce() -> Result + Send + 'static, + resolver: impl FnOnce(T) -> Result + Send + 'static, + ) -> Self { + Self { + operation: Some(Box::new(operation)), + resolver: Some(Box::new(resolver)), + } + } +} + +impl Task for WorkerTask +where + T: Send + 'static, + J: ToNapiValue + TypeName, +{ + type Output = T; + type JsValue = J; + + fn compute(&mut self) -> Result { + self.operation.take().ok_or_else(|| { + Error::new( + Status::GenericFailure, + "Holt worker operation was already consumed", + ) + })?() + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + self.resolver.take().ok_or_else(|| { + Error::new( + Status::GenericFailure, + "Holt worker resolver was already consumed", + ) + })?(output) + } +} + +/// Promise task resolving to no JavaScript value. +pub type VoidTask = AsyncTask>; +/// Promise task resolving to a named Tree handle. +pub type TreeHandleTask = AsyncTask>; +/// Promise task resolving to a list of tree names. +pub type StringListTask = AsyncTask, Vec>>; +/// Promise task resolving to an optional value buffer. +pub type OptionalBufferTask = AsyncTask>, Option>>; +/// Promise task resolving to an optional record. +pub type OptionalRecordTask = AsyncTask, Option>>; +/// Promise task resolving to a boolean. +pub type BoolTask = AsyncTask>; +/// Promise task resolving to key-only scan entries. +pub type KeyScanTask = AsyncTask, Vec>>; +/// Promise task resolving to record scan entries. +pub type RecordScanTask = AsyncTask, Vec>>; + +fn worker_task( + operation: impl FnOnce() -> Result + Send + 'static, + resolver: impl FnOnce(T) -> Result + Send + 'static, +) -> AsyncTask> +where + T: Send + 'static, + J: ToNapiValue + TypeName, +{ + AsyncTask::new(WorkerTask::new(operation, resolver)) +} + +fn holt_task( + operation: impl FnOnce() -> holt::Result + Send + 'static, + resolver: impl FnOnce(T) -> Result + Send + 'static, +) -> AsyncTask> +where + T: Send + 'static, + J: ToNapiValue + TypeName, +{ + worker_task(move || operation().map_err(js_error), resolver) +} + fn delimiter(value: Option) -> Result> { value .map(|value| { @@ -80,6 +186,52 @@ pub struct ScanEntry { pub version: BigInt, } +fn key_scan_entry(entry: KeyRangeEntry) -> Result { + match entry { + KeyRangeEntry::Key { key, version } => Ok(ScanEntry { + kind: "key".to_owned(), + path: into_buffer(key), + value: None, + version: into_bigint(version.as_u64()), + }), + KeyRangeEntry::CommonPrefix(path) => Ok(ScanEntry { + kind: "common_prefix".to_owned(), + path: into_buffer(path), + value: None, + version: into_bigint(0), + }), + _ => Err(Error::new( + Status::GenericFailure, + "Holt returned an unsupported key scan entry", + )), + } +} + +fn record_scan_entry(entry: RangeEntry) -> Result { + match entry { + RangeEntry::Key { + key, + value, + version, + } => Ok(ScanEntry { + kind: "key".to_owned(), + path: into_buffer(key), + value: Some(into_buffer(value)), + version: into_bigint(version.as_u64()), + }), + RangeEntry::CommonPrefix(path) => Ok(ScanEntry { + kind: "common_prefix".to_owned(), + path: into_buffer(path), + value: None, + version: into_bigint(0), + }), + _ => Err(Error::new( + Status::GenericFailure, + "Holt returned an unsupported record scan entry", + )), + } +} + /// A Node.js handle to a Holt multi-tree database. #[napi] pub struct Database { @@ -88,82 +240,101 @@ pub struct Database { #[napi] impl Database { - /// Open a file-backed multi-tree database. + fn from_core(inner: CoreDatabase) -> Self { + Self { inner: Some(inner) } + } + + /// Open a file-backed multi-tree database without blocking the Node.js + /// event loop. #[napi(factory)] - pub fn open(path: String, options: Option) -> Result { + pub async fn open(path: String, options: Option) -> Result { let wal_sync = options .and_then(|options| options.wal_sync) .unwrap_or(false); let mut config = TreeConfig::new(path); config.durability = holt::Durability::Wal { sync: wal_sync }; - CoreDatabase::open(config) - .map(|inner| Self { inner: Some(inner) }) - .map_err(js_error) + spawn_holt(move || CoreDatabase::open(config)) + .await + .map(Self::from_core) } - /// Open a volatile in-memory multi-tree database. + /// Open a volatile in-memory multi-tree database without blocking the + /// Node.js event loop. #[napi(factory)] - pub fn open_memory() -> Result { - CoreDatabase::open(TreeConfig::memory()) - .map(|inner| Self { inner: Some(inner) }) - .map_err(js_error) + pub async fn open_memory() -> Result { + spawn_holt(|| CoreDatabase::open(TreeConfig::memory())) + .await + .map(Self::from_core) } - /// Explicitly release the database handle. Existing Tree handles remain - /// usable until they are closed or their named tree is dropped. - #[napi] - pub fn close(&mut self) { - self.inner = None; + /// Explicitly release the database handle on a worker thread. Existing + /// Tree handles remain usable until they are closed or dropped. + #[napi(ts_return_type = "Promise")] + pub fn close(&mut self) -> VoidTask { + let inner = self.inner.take(); + worker_task( + move || { + drop(inner); + Ok(()) + }, + Ok, + ) } - /// Create a new named tree. - #[napi(js_name = "createTree")] - pub fn create_tree(&self, name: String) -> Result { - self.core()? - .create_tree(&name) - .map(Tree::from_core) - .map_err(js_error) + /// Create a new named tree on a worker thread. + #[napi(js_name = "createTree", ts_return_type = "Promise")] + pub fn create_tree(&self, name: String) -> Result { + let database = self.clone_core()?; + Ok(holt_task( + move || database.create_tree(&name), + |tree| Ok(Tree::from_core(tree)), + )) } - /// Open an existing named tree. - #[napi(js_name = "openTree")] - pub fn open_tree(&self, name: String) -> Result { - self.core()? - .open_tree(&name) - .map(Tree::from_core) - .map_err(js_error) + /// Open an existing named tree on a worker thread. + #[napi(js_name = "openTree", ts_return_type = "Promise")] + pub fn open_tree(&self, name: String) -> Result { + let database = self.clone_core()?; + Ok(holt_task( + move || database.open_tree(&name), + |tree| Ok(Tree::from_core(tree)), + )) } - /// Open a named tree, creating it when it does not exist. - #[napi(js_name = "openOrCreateTree")] - pub fn open_or_create_tree(&self, name: String) -> Result { - self.core()? - .open_or_create_tree(&name) - .map(Tree::from_core) - .map_err(js_error) + /// Open a named tree on a worker thread, creating it when absent. + #[napi(js_name = "openOrCreateTree", ts_return_type = "Promise")] + pub fn open_or_create_tree(&self, name: String) -> Result { + let database = self.clone_core()?; + Ok(holt_task( + move || database.open_or_create_tree(&name), + |tree| Ok(Tree::from_core(tree)), + )) } - /// List all live named trees. - #[napi(js_name = "listTrees")] - pub fn list_trees(&self) -> Result> { - self.core()?.list_trees().map_err(js_error) + /// List all live named trees on a worker thread. + #[napi(js_name = "listTrees", ts_return_type = "Promise>")] + pub fn list_trees(&self) -> Result { + let database = self.clone_core()?; + Ok(holt_task(move || database.list_trees(), Ok)) } - /// Drop a named tree and fence existing handles to it. - #[napi(js_name = "dropTree")] - pub fn drop_tree(&self, name: String) -> Result<()> { - self.core()?.drop_tree(&name).map_err(js_error) + /// Drop a named tree and fence existing handles on a worker thread. + #[napi(js_name = "dropTree", ts_return_type = "Promise")] + pub fn drop_tree(&self, name: String) -> Result { + let database = self.clone_core()?; + Ok(holt_task(move || database.drop_tree(&name), Ok)) } - /// Flush every named tree and the shared WAL to the backing store. - #[napi] - pub fn checkpoint(&self) -> Result<()> { - self.core()?.checkpoint().map_err(js_error) + /// Flush every named tree and the shared WAL on a worker thread. + #[napi(ts_return_type = "Promise")] + pub fn checkpoint(&self) -> Result { + let database = self.clone_core()?; + Ok(holt_task(move || database.checkpoint(), Ok)) } - fn core(&self) -> Result<&CoreDatabase> { + fn clone_core(&self) -> Result { self.inner - .as_ref() + .clone() .ok_or_else(|| Error::new(Status::GenericFailure, "Holt database is closed")) } } @@ -180,188 +351,174 @@ impl Tree { Self { inner: Some(inner) } } - /// Open a file-backed tree. + /// Open a file-backed tree without blocking the Node.js event loop. #[napi(factory)] - pub fn open(path: String, options: Option) -> Result { + pub async fn open(path: String, options: Option) -> Result { let wal_sync = options .and_then(|options| options.wal_sync) .unwrap_or(false); - TreeBuilder::new(path) - .durability(holt::Durability::Wal { sync: wal_sync }) - .open() - .map(|inner| Self { inner: Some(inner) }) - .map_err(js_error) + spawn_holt(move || { + TreeBuilder::new(path) + .durability(holt::Durability::Wal { sync: wal_sync }) + .open() + }) + .await + .map(Self::from_core) } - /// Open a volatile in-memory tree. + /// Open a volatile in-memory tree without blocking the Node.js event loop. #[napi(factory)] - pub fn open_memory() -> Result { - TreeBuilder::new("holt-node-memory") - .memory() - .open() - .map(|inner| Self { inner: Some(inner) }) - .map_err(js_error) + pub async fn open_memory() -> Result { + spawn_holt(|| TreeBuilder::new("holt-node-memory").memory().open()) + .await + .map(Self::from_core) } - /// Explicitly release the native tree handle. This is idempotent. - #[napi] - pub fn close(&mut self) { - self.inner = None; + /// Explicitly release the native tree handle on a worker thread. + #[napi(ts_return_type = "Promise")] + pub fn close(&mut self) -> VoidTask { + let inner = self.inner.take(); + worker_task( + move || { + drop(inner); + Ok(()) + }, + Ok, + ) } - /// Return the value stored at `key`, or null when the key is absent. - #[napi] - pub fn get(&self, key: Uint8Array) -> Result> { - self.core()? - .get(key.as_ref()) - .map(|value| value.map(into_buffer)) - .map_err(js_error) + /// Return the value at `key` from a worker thread. + #[napi(ts_return_type = "Promise")] + pub fn get(&self, key: Uint8Array) -> Result { + let tree = self.clone_core()?; + let key = key.to_vec(); + Ok(holt_task( + move || tree.get(&key), + |value| Ok(value.map(into_buffer)), + )) } - /// Return the value and conditional-write version for `key`. - #[napi(js_name = "getRecord")] - pub fn get_record(&self, key: Uint8Array) -> Result> { - self.core()? - .get_record(key.as_ref()) - .map(|record| { - record.map(|record| Record { + /// Return the value and conditional-write version from a worker thread. + #[napi(js_name = "getRecord", ts_return_type = "Promise")] + pub fn get_record(&self, key: Uint8Array) -> Result { + let tree = self.clone_core()?; + let key = key.to_vec(); + Ok(holt_task( + move || tree.get_record(&key), + |record| { + Ok(record.map(|record| Record { value: into_buffer(record.value), version: into_bigint(record.version.as_u64()), - }) - }) - .map_err(js_error) + })) + }, + )) } - /// Insert or replace a value. - #[napi] - pub fn put(&self, key: Uint8Array, value: Uint8Array) -> Result<()> { - self.core()? - .put(key.as_ref(), value.as_ref()) - .map_err(js_error) + /// Insert or replace a value on a worker thread. + #[napi(ts_return_type = "Promise")] + pub fn put(&self, key: Uint8Array, value: Uint8Array) -> Result { + let tree = self.clone_core()?; + let key = key.to_vec(); + let value = value.to_vec(); + Ok(holt_task(move || tree.put(&key, &value), Ok)) } - /// Delete a key and return whether a live record existed. - #[napi] - pub fn delete(&self, key: Uint8Array) -> Result { - self.core()?.delete(key.as_ref()).map_err(js_error) + /// Delete a key on a worker thread and return whether it existed. + #[napi(ts_return_type = "Promise")] + pub fn delete(&self, key: Uint8Array) -> Result { + let tree = self.clone_core()?; + let key = key.to_vec(); + Ok(holt_task(move || tree.delete(&key), Ok)) } - /// Compare the current version and replace the value if it matches. - #[napi(js_name = "compareAndPut")] + /// Compare the current version and replace the value on a worker thread. + #[napi(js_name = "compareAndPut", ts_return_type = "Promise")] pub fn compare_and_put( &self, key: Uint8Array, version: BigInt, value: Uint8Array, - ) -> Result { - self.core()? - .compare_and_put( - key.as_ref(), - holt::RecordVersion::from_raw(from_bigint(version)?), - value.as_ref(), - ) - .map_err(js_error) + ) -> Result { + let tree = self.clone_core()?; + let key = key.to_vec(); + let version = holt::RecordVersion::from_raw(from_bigint(version)?); + let value = value.to_vec(); + Ok(holt_task( + move || tree.compare_and_put(&key, version, &value), + Ok, + )) } - /// Flush dirty frames and the WAL to the backing store. - #[napi] - pub fn checkpoint(&self) -> Result<()> { - self.core()?.checkpoint().map_err(js_error) + /// Flush dirty frames and the WAL on a worker thread. + #[napi(ts_return_type = "Promise")] + pub fn checkpoint(&self) -> Result { + let tree = self.clone_core()?; + Ok(holt_task(move || tree.checkpoint(), Ok)) } - /// Scan keys under an optional prefix. - #[napi(js_name = "scanKeys")] + /// Scan keys on a worker thread. + #[napi(js_name = "scanKeys", ts_return_type = "Promise>")] pub fn scan_keys( &self, prefix: Option, options: Option, - ) -> Result> { + ) -> Result { + let tree = self.clone_core()?; let options = options.unwrap_or(ScanOptions { start_after: None, delimiter: None, }); let delimiter = delimiter(options.delimiter)?; let prefix = prefix.map(|prefix| prefix.to_vec()).unwrap_or_default(); - let mut builder = self.core()?.scan_keys(prefix.as_ref()); - if let Some(start_after) = options.start_after { - builder = builder.start_after(start_after.as_ref()); - } - if let Some(delimiter) = delimiter { - builder = builder.delimiter(delimiter); - } - builder - .into_iter() - .map(|entry| match entry.map_err(js_error)? { - KeyRangeEntry::Key { key, version } => Ok(ScanEntry { - kind: "key".to_owned(), - path: into_buffer(key), - value: None, - version: into_bigint(version.as_u64()), - }), - KeyRangeEntry::CommonPrefix(path) => Ok(ScanEntry { - kind: "common_prefix".to_owned(), - path: into_buffer(path), - value: None, - version: into_bigint(0), - }), - _ => Err(Error::new( - Status::GenericFailure, - "Holt returned an unsupported key scan entry", - )), - }) - .collect() + let start_after = options.start_after.map(|key| key.to_vec()); + Ok(holt_task( + move || { + let mut builder = tree.scan_keys(&prefix); + if let Some(start_after) = &start_after { + builder = builder.start_after(start_after); + } + if let Some(delimiter) = delimiter { + builder = builder.delimiter(delimiter); + } + builder.into_iter().collect() + }, + |entries: Vec| entries.into_iter().map(key_scan_entry).collect(), + )) } - /// Scan records under an optional prefix. - #[napi(js_name = "scanRecords")] + /// Scan records on a worker thread. + #[napi(js_name = "scanRecords", ts_return_type = "Promise>")] pub fn scan_records( &self, prefix: Option, options: Option, - ) -> Result> { + ) -> Result { + let tree = self.clone_core()?; let options = options.unwrap_or(ScanOptions { start_after: None, delimiter: None, }); let delimiter = delimiter(options.delimiter)?; let prefix = prefix.map(|prefix| prefix.to_vec()).unwrap_or_default(); - let mut builder = self.core()?.scan(prefix.as_ref()); - if let Some(start_after) = options.start_after { - builder = builder.start_after(start_after.as_ref()); - } - if let Some(delimiter) = delimiter { - builder = builder.delimiter(delimiter); - } - builder - .into_iter() - .map(|entry| match entry.map_err(js_error)? { - RangeEntry::Key { - key, - value, - version, - } => Ok(ScanEntry { - kind: "key".to_owned(), - path: into_buffer(key), - value: Some(into_buffer(value)), - version: into_bigint(version.as_u64()), - }), - RangeEntry::CommonPrefix(path) => Ok(ScanEntry { - kind: "common_prefix".to_owned(), - path: into_buffer(path), - value: None, - version: into_bigint(0), - }), - _ => Err(Error::new( - Status::GenericFailure, - "Holt returned an unsupported record scan entry", - )), - }) - .collect() + let start_after = options.start_after.map(|key| key.to_vec()); + Ok(holt_task( + move || { + let mut builder = tree.scan(&prefix); + if let Some(start_after) = &start_after { + builder = builder.start_after(start_after); + } + if let Some(delimiter) = delimiter { + builder = builder.delimiter(delimiter); + } + builder.into_iter().collect() + }, + |entries: Vec| entries.into_iter().map(record_scan_entry).collect(), + )) } - fn core(&self) -> Result<&CoreTree> { + fn clone_core(&self) -> Result { self.inner - .as_ref() + .clone() .ok_or_else(|| Error::new(Status::GenericFailure, "Holt tree is closed")) } } diff --git a/crates/holt-node/test/basic.test.mjs b/crates/holt-node/test/basic.test.mjs index fd67dee..682f897 100644 --- a/crates/holt-node/test/basic.test.mjs +++ b/crates/holt-node/test/basic.test.mjs @@ -2,28 +2,59 @@ import assert from "node:assert/strict"; import test from "node:test"; import { Database, Tree } from "../index.js"; -test("memory CRUD and conditional update", () => { - const tree = Tree.openMemory(); +test("storage operations expose native Promises", async () => { + const open = Tree.openMemory(); + assert(open instanceof Promise); + const tree = await open; + + const put = tree.put(Buffer.from("promise/key"), Buffer.from("value")); + assert(put instanceof Promise); + await put; + + const get = tree.get(Buffer.from("promise/key")); + assert(get instanceof Promise); + await get; + + const scan = tree.scanKeys(Buffer.from("promise/")); + assert(scan instanceof Promise); + await scan; + + const checkpoint = tree.checkpoint(); + assert(checkpoint instanceof Promise); + await checkpoint; + + const close = tree.close(); + assert(close instanceof Promise); + await close; +}); + +test("memory CRUD and conditional update", async () => { + const tree = await Tree.openMemory(); const key = Buffer.from("bucket/a.jpg"); - tree.put(key, Buffer.from("old")); - assert.deepEqual(tree.get(key), Buffer.from("old")); + await tree.put(key, Buffer.from("old")); + assert.deepEqual(await tree.get(key), Buffer.from("old")); - const record = tree.getRecord(key); + const record = await tree.getRecord(key); assert.equal(typeof record.version, "bigint"); - assert.equal(tree.compareAndPut(key, record.version, Buffer.from("new")), true); - assert.deepEqual(tree.get(key), Buffer.from("new")); - assert.equal(tree.delete(key), true); - assert.equal(tree.get(key), null); - tree.close(); - tree.close(); + assert.equal( + await tree.compareAndPut(key, record.version, Buffer.from("new")), + true, + ); + assert.deepEqual(await tree.get(key), Buffer.from("new")); + assert.equal(await tree.delete(key), true); + assert.equal(await tree.get(key), null); + await tree.close(); + await tree.close(); }); -test("prefix and delimiter scans", () => { - const tree = Tree.openMemory(); +test("prefix and delimiter scans", async () => { + const tree = await Tree.openMemory(); for (const key of ["bucket/a/1", "bucket/a/2", "bucket/b/1"]) { - tree.put(Buffer.from(key), Buffer.from(key)); + await tree.put(Buffer.from(key), Buffer.from(key)); } - const entries = tree.scanKeys(Buffer.from("bucket/"), { delimiter: 47 }); + const entries = await tree.scanKeys(Buffer.from("bucket/"), { + delimiter: 47, + }); assert.deepEqual( entries.map((entry) => [entry.kind, entry.path.toString()]), [ @@ -31,31 +62,34 @@ test("prefix and delimiter scans", () => { ["common_prefix", "bucket/b/"], ], ); - tree.close(); + await tree.close(); }); -test("database creates and isolates named trees", () => { - const db = Database.openMemory(); - const objects = db.createTree("objects"); - const sessions = db.openOrCreateTree("sessions"); +test("database creates and isolates named trees", async () => { + const db = await Database.openMemory(); + const objects = await db.createTree("objects"); + const sessions = await db.openOrCreateTree("sessions"); const key = Buffer.from("same-key"); - objects.put(key, Buffer.from("object-value")); - sessions.put(key, Buffer.from("session-value")); - assert.deepEqual(objects.get(key), Buffer.from("object-value")); - assert.deepEqual(sessions.get(key), Buffer.from("session-value")); - assert.deepEqual(db.listTrees().sort(), ["objects", "sessions"]); + await objects.put(key, Buffer.from("object-value")); + await sessions.put(key, Buffer.from("session-value")); + assert.deepEqual(await objects.get(key), Buffer.from("object-value")); + assert.deepEqual(await sessions.get(key), Buffer.from("session-value")); + assert.deepEqual((await db.listTrees()).sort(), ["objects", "sessions"]); - const secondObjectsHandle = db.openTree("objects"); - assert.deepEqual(secondObjectsHandle.get(key), Buffer.from("object-value")); - secondObjectsHandle.close(); + const secondObjectsHandle = await db.openTree("objects"); + assert.deepEqual( + await secondObjectsHandle.get(key), + Buffer.from("object-value"), + ); + await secondObjectsHandle.close(); - db.dropTree("sessions"); - assert.deepEqual(db.listTrees(), ["objects"]); - assert.throws(() => sessions.get(key), /dropped/i); + await db.dropTree("sessions"); + assert.deepEqual(await db.listTrees(), ["objects"]); + await assert.rejects(sessions.get(key), /dropped/i); - sessions.close(); - objects.close(); - db.close(); - db.close(); + await sessions.close(); + await objects.close(); + await db.close(); + await db.close(); }); diff --git a/crates/holt-node/test/reopen.test.mjs b/crates/holt-node/test/reopen.test.mjs index 1004c45..bc15bad 100644 --- a/crates/holt-node/test/reopen.test.mjs +++ b/crates/holt-node/test/reopen.test.mjs @@ -8,38 +8,44 @@ import { Database, Tree } from "../index.js"; test("file-backed tree reopens after checkpoint", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "holt-node-")); const key = Buffer.from("objects/1"); - const first = Tree.open(dir, { walSync: true }); - first.put(key, Buffer.from("metadata")); - first.checkpoint(); - first.close(); + const first = await Tree.open(dir, { walSync: true }); + await first.put(key, Buffer.from("metadata")); + await first.checkpoint(); + await first.close(); - const second = Tree.open(dir, { walSync: true }); - assert.deepEqual(second.get(key), Buffer.from("metadata")); - second.close(); + const second = await Tree.open(dir, { walSync: true }); + assert.deepEqual(await second.get(key), Buffer.from("metadata")); + await second.close(); await fs.rm(dir, { recursive: true, force: true }); }); test("file-backed database reopens named trees", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "holt-node-db-")); const key = Buffer.from("objects/1"); - const first = Database.open(dir, { walSync: true }); - const objects = first.createTree("objects"); - const metadata = first.createTree("metadata"); - objects.put(key, Buffer.from("object-value")); - metadata.put(key, Buffer.from("metadata-value")); - first.checkpoint(); - objects.close(); - metadata.close(); - first.close(); + const first = await Database.open(dir, { walSync: true }); + const objects = await first.createTree("objects"); + const metadata = await first.createTree("metadata"); + await objects.put(key, Buffer.from("object-value")); + await metadata.put(key, Buffer.from("metadata-value")); + await first.checkpoint(); + await objects.close(); + await metadata.close(); + await first.close(); - const second = Database.open(dir, { walSync: true }); - assert.deepEqual(second.listTrees().sort(), ["metadata", "objects"]); - const reopenedObjects = second.openTree("objects"); - const reopenedMetadata = second.openTree("metadata"); - assert.deepEqual(reopenedObjects.get(key), Buffer.from("object-value")); - assert.deepEqual(reopenedMetadata.get(key), Buffer.from("metadata-value")); - reopenedObjects.close(); - reopenedMetadata.close(); - second.close(); + const second = await Database.open(dir, { walSync: true }); + assert.deepEqual((await second.listTrees()).sort(), ["metadata", "objects"]); + const reopenedObjects = await second.openTree("objects"); + const reopenedMetadata = await second.openTree("metadata"); + assert.deepEqual( + await reopenedObjects.get(key), + Buffer.from("object-value"), + ); + assert.deepEqual( + await reopenedMetadata.get(key), + Buffer.from("metadata-value"), + ); + await reopenedObjects.close(); + await reopenedMetadata.close(); + await second.close(); await fs.rm(dir, { recursive: true, force: true }); });