@drasi/lib
v0.2.0
Published
Embed the Drasi continuous-query engine in Node.js, with dynamic plugin loading and JavaScript-defined sources and reactions
Downloads
411
Keywords
Readme
@drasi/lib
📖 Documentation: https://drasi-project.github.io/drasi-nodejs/ — installation, concepts, the full API reference, guides, and an end-to-end tutorial.
Embed the Drasi continuous-query engine directly in your
Node.js application. @drasi/lib is a native (napi-rs)
binding around Drasi's embeddable engine (drasi-lib) and its host SDK
(drasi-host-sdk), so you get:
- In-process continuous queries over a property graph, in Cypher or GQL.
- Dynamic plugin loading — discover and load Drasi source/reaction/bootstrap
plugins (self-contained
cdylib.so/.dylib/.dllfiles) at runtime, exactly likedrasi-serverdoes. - JavaScript-defined components — define a reaction as a JS callback, or a source you push changes into from your own code. No Rust required.
Status: pre-1.0 but functional and published to npm. The core engine, dynamic plugin loading with OCI plugin fetch (from
ghcr.io/drasi-project), JS sources/reactions, streaming, lifecycle management, a secret store, a persistent state store (redb), concrete TypeScript types with typed error codes, and metrics are implemented and tested. Distributed with prebuilt binaries for Windows (x64), Linux (x64/arm64), and Apple-silicon macOS (arm64). The API is still evolving ahead of 1.0 — see the Roadmap below.
Install
Once published, @drasi/lib ships prebuilt binaries, so consumers install with
no Rust toolchain:
npm install @drasi/libnpm resolves the correct native binary from a per-platform optional dependency
(@drasi/lib-<platform>) for Windows (x64), Linux (x64/arm64), and Apple-silicon
macOS (arm64). Intel macOS (x64) has no prebuilt binary and must be built from
source. See docs/releasing.md for how the binaries are
built and distributed.
Build from source
Building from source is only needed to develop @drasi/lib or to run on a platform
without a prebuilt binary. It builds a native addon whose Rust dependencies (the
Drasi engine and plugin host SDK) come from crates.io, so no sibling checkout is
required (see Dependency strategy):
npm install
npm run build # produces index.js, index.d.ts and the .node binary
npm testQuickstart
import { Drasi } from '@drasi/lib';
const drasi = await Drasi.create('my-app');
// Discover & load cdylib plugins from a directory.
await drasi.loadPlugins('./plugins');
await drasi.start();
// A source plugin produces graph changes...
await drasi.addSource('mock', 'counters', { dataType: { type: 'counter' }, intervalMs: 500 });
// ...a continuous query reacts to them...
await drasi.addQuery('big', 'MATCH (c:Counter) WHERE c.value > 3 RETURN c.value AS value', ['counters']);
// ...and you can read the live result set.
console.log(await drasi.getQueryResults('big'));Define a reaction in JavaScript
await drasi.addJsReaction('on-change', ['big'], (result) => {
// { query_id, sequence, results: [{ type: 'ADD'|'UPDATE'|'DELETE', data, before?, after? }] }
for (const diff of result.results) {
if (diff.type === 'ADD') console.log('added', diff.data);
}
});Push changes from a JavaScript source
await drasi.addJsSource('orders');
await drasi.addQuery('open', 'MATCH (o:Order) WHERE o.status = "open" RETURN o.id AS id', ['orders']);
await drasi.pushChange('orders', {
op: 'insert', // insert | update | delete
id: 'o1',
labels: ['Order'],
properties: { status: 'open', total: 42 },
});See examples/ for runnable scripts (quickstart.mjs,
js-reaction.mjs, js-source.mjs), and
examples/electron-explorer for a full Electron +
React desktop app that browses/installs plugins and builds and observes a topology
on the embedded engine.
API overview
The full, browsable API reference lives on the documentation site. The summary below is a quick reference.
Drasi.create(id, options?) → Promise<Drasi>. options.secrets seeds an
in-memory secret store ({ secrets: { DB_PASSWORD: '…' } }) that cdylib plugins
resolve ConfigValue::Secret/EnvironmentVariable references against;
options.stateStore ({ kind: 'redb', path }) enables a persistent plugin state store.
Drasi.fromConfig(config) builds and starts an engine from a declarative
object (see the API reference,
or docs/api-reference.md for the audit-oriented deep dive).
| Area | Methods |
| --- | --- |
| Plugins | loadPlugins(dir, verify?), watchPlugins(dir), pluginKinds(), sourceConfigSchema(kind), reactionConfigSchema(kind), bootstrapConfigSchema(kind), listPluginTags(repo), pullPlugin(reference, destDir, filename, options?) |
| Sources | addSource(kind, id, config, autoStart?, bootstrap?), addJsSource(id, autoStart?), pushChange(sourceId, change), updateSource, startSource, stopSource, removeSource, listSources, getSourceSchema(id), getGraphSchema() |
| Queries | addQuery(id, query, sources, language?, joins?), updateQuery, startQuery, stopQuery, removeQuery, getQueryResults(id), listQueries |
| Reactions | addReaction(kind, id, queryIds, config), addJsReaction(id, queryIds, cb), addDurableJsReaction(id, queryIds, asyncCb, options?), updateReaction, startReaction, stopReaction, removeReaction, listReactions |
| Metrics | getQueryMetrics(id), getReactionMetrics(id), getLifecycleMetrics() |
| Streaming | onAllEvents(cb), onQueryEvents(id, cb), onSourceEvents(id, cb), onReactionEvents(id, cb), onSourceLogs(id, cb), onQueryLogs(id, cb), onReactionLogs(id, cb) |
| Lifecycle | start(), stop(), close() |
language is "cypher" (default) or "gql" — any other value is rejected with a
typed UNKNOWN_QUERY_LANGUAGE error. pushChange emits nodes, or
relations when change includes startId/endId. Generated TypeScript types
are in index.d.ts. Callbacks are unref'd, so they don't keep the Node process
alive on their own.
Ordering tip
Call start() first, then add components. Components added to a running engine
auto-start individually. (Adding everything and then calling start() also works.)
Types
The generated index.d.ts is self-contained: every config/result parameter and
return, plus the callback payloads, is exposed with a concrete TypeScript type
(no bare any). Import them directly from the package.
import type { SourceChangeInput, QueryResultEvent } from '@drasi/lib';
const change: SourceChangeInput = { op: 'insert', id: 'o1', labels: ['Order'] };
const onResult = (event: QueryResultEvent) => console.log(event.results.length);Error handling
Argument/validation failures throw a stable, machine-readable code so callers
can branch on it instead of matching human-readable messages. DrasiErrorCode is
exported as a regular (non-const) enum with string values — it is safe under
isolatedModules / esbuild / swc / Vite and has a real runtime value, so both the
type and value are usable:
import { DrasiErrorCode } from '@drasi/lib';
try {
await drasi.addSource('unknown', 's', {});
} catch (err) {
if ((err as { code?: string }).code === DrasiErrorCode.UnknownSourceKind) {
// handle the unregistered-kind case
}
}Because napi-rs can only attach a custom code on a synchronous throw (async
promise rejections are forced to code === 'GenericFailure'), the two error
classes behave as follows — the human-readable message is the same in both cases:
- Synchronous throws (
err.codeis the stable code). Argument validation runs synchronously, before the method returns itsPromise. This covers, on their normal paths:UNKNOWN_SOURCE_KIND,UNKNOWN_REACTION_KIND,UNKNOWN_BOOTSTRAP_KIND,BOOTSTRAP_KIND_REQUIRED,MISSING_CONFIG_FIELD,NO_JS_SOURCE,JS_SOURCE_CLOSED,CHANGE_NOT_OBJECT,CHANGE_OP_REQUIRED,CHANGE_ID_REQUIRED,RELATION_REQUIRES_BOTH_ENDS,UNKNOWN_CHANGE_OP,STATE_STORE_PATH_REQUIRED, andUNKNOWN_STATE_STORE_KIND. Note this means validation errors surface as a synchronous throw rather than a rejected promise — transparent toawait/trycallers, but a barep = fn(); p.catch(...)(noawait) will not catch them. - Async fallbacks (message-only;
err.code === 'GenericFailure'). A few paths can only fail after the async work has begun: component creation insidefromConfig(plugin kinds resolve after the async plugin load) and the rare race where a JS source closes mid-pushChange. There the stable code is embedded in the message as a trailing[CODE]token (e.g.unknown source kind 'x' [UNKNOWN_SOURCE_KIND]) so a single check still works:
function drasiCode(err: unknown): string | undefined {
const e = err as { code?: string; message?: string };
if (e.code && e.code !== 'GenericFailure') return e.code; // sync throw
return e.message?.match(/\[([A-Z_]+)\]\s*$/)?.[1]; // async fallback
}How it works
A .node addon is itself a cdylib, so it can host drasi-lib and dlopen
plugin cdylibs through drasi-host-sdk — the same mechanism drasi-server uses.
Plugins are fully self-contained, each with their own async runtime, and talk to
the host across a stable #[repr(C)] FFI with load-time version negotiation. JS
callbacks are bridged with napi ThreadsafeFunctions; async methods return
Promises driven by a Tokio runtime.
Dependency strategy
The Drasi engine and host SDK are consumed from crates.io. The workspace versions
its crates on independent lines — only drasi-host-sdk and drasi-plugin-sdk
track the shared workspace version — so the pinned numbers differ by design
(drasi-host-sdk/drasi-plugin-sdk 0.10, drasi-lib 0.8, drasi-core 0.5), yet
their published dependency requirements resolve to a single coherent graph. Exact
versions are locked in Cargo.lock.
The FFI ABI is negotiated between drasi-host-sdk (host) and drasi-plugin-sdk
(plugins), which always share the workspace version, so any plugin built against a
matching drasi-plugin-sdk major.minor loads cleanly. The example plugins used
by the test suite are fetched from crates.io and built with --features
dynamic-plugin by scripts/build-plugins.mjs (the npm pretest step); bump their
pinned versions alongside drasi-plugin-sdk when upgrading the SDK.
Roadmap
Implemented: dynamic plugin loading (+ optional SHA-256 verification), OCI plugin
fetch from ghcr.io/drasi-project (pullPlugin/listPluginTags) with opt-in
cosign signature enforcement, JS sources (nodes + relations + bootstrap replay)
and reactions (including durable, checkpointed reactions), event & log
streaming, secret/env config resolution for plugins, bootstrap-provider wiring,
persistent state store (redb) and persistent query-index backend (RocksDB),
identity providers, plugin hot-reload, lifecycle/update APIs, concrete public
TypeScript types with typed error codes (DrasiErrorCode), plugin config-schema
accessors with tokenized config-validation errors, query-language validation, and
metrics accessors. Published to npm with cross-platform prebuilt binaries and
build provenance (see docs/releasing.md).
Still to come:
- Full in-engine JSON-schema config validation (schemas are already exposed via
sourceConfigSchema/reactionConfigSchemafor client-side validation today). - Prebuilt binaries for Intel macOS (
x86_64-apple-darwin); Intel-mac users currently build from source.
Measuring success
Adoption is tracked via npm downloads and dependents. See
docs/metrics.md for how to read the numbers — the public
npm downloads API, npmjs.com Insights/dependents, and third-party dashboards —
plus the baseline and where to record targets.
License
Licensed under the Apache License, Version 2.0 — see LICENSE.
Contributing
Contributions are welcome! See CONTRIBUTING.md for the development workflow and the DCO sign-off requirement, and CODE_OF_CONDUCT.md. To report a security issue, see SECURITY.md.
