Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
186 commits
Select commit Hold shift + click to select a range
e5dfe19
docs: add design for multiple isolated DataWeave engines per process
mlischetti Aug 7, 2026
a11bc18
W-23692110: handle-keyed ScriptRuntime registry with per-engine resol…
mlischetti Aug 10, 2026
0f93ac8
W-23692110: per-engine resolver bridge and handle-based N-API methods
mlischetti Aug 10, 2026
037349d
W-23692110: per-instance engine handles in Node binding + isolation r…
mlischetti Aug 10, 2026
a410fd8
W-23692110: fix native library ref-count leak on partial DataWeave.in…
mlischetti Aug 10, 2026
6372279
W-23692110: document independent per-instance engines
mlischetti Aug 10, 2026
1f8c970
W-23692110: Make Node engine bridge teardown safe against in-flight o…
mlischetti Aug 10, 2026
ffc3293
W-23692110: Reject invalid engine handles and fix resolver-buffer lea…
mlischetti Aug 10, 2026
fb5212a
W-23692110: Use bridge_finalize in create_engine_with_resolver reject…
mlischetti Aug 10, 2026
b045cc5
Require per-engine ABI symbols at load and align resolver log policy
mlischetti Aug 10, 2026
bc5a7e1
Make default resolver-error log fully content-free, not just message-…
mlischetti Aug 10, 2026
a8b4a91
test(node): add lifecycle/error coverage for F1/F4/F6 remediation
mlischetti Aug 10, 2026
af53000
W-23692110: Add native-level test for unknown engine handle contract
mlischetti Aug 10, 2026
e4f294f
chore: remove PR-157 code review process notes from repo
mlischetti Aug 10, 2026
e4d116b
docs: add design for cleanup()-during-active-stream deadlock fix
mlischetti Aug 11, 2026
887b8cb
Add process-global active-op accounting for streaming/transform
mlischetti Aug 11, 2026
6963eaf
Make napi_cleanup async: defer isolate teardown until active ops drain
mlischetti Aug 11, 2026
c2cf077
Block initialize() while an isolate teardown is pending
mlischetti Aug 11, 2026
cda491b
Fix signal-stealing deadlock: use broadcast instead of signal for op-…
mlischetti Aug 11, 2026
909ad63
Change DataWeave.cleanup() to return Promise<void>
mlischetti Aug 11, 2026
4d43330
Await the now-async DataWeave.cleanup() in existing tests
mlischetti Aug 11, 2026
21b6a57
Add cleanup()-during-active-stream/transform deadlock regression tests
mlischetti Aug 11, 2026
2061f67
Fix napi_initialize deadlock: decrement g_active_ops from the worker …
mlischetti Aug 11, 2026
4dbe2ed
Document DataWeave.cleanup()'s Promise<void> signature
mlischetti Aug 11, 2026
5809fb1
W-23692110: Unwind pre-spawn state on streaming/transform worker spaw…
mlischetti Aug 11, 2026
196b588
W-23692110: Roll back pending-teardown state on waiter spawn failure
mlischetti Aug 11, 2026
101c688
fix(native-lib/node): signal read waiter on env==NULL teardown (F3)
mlischetti Aug 11, 2026
444ee85
W-23692110: Drain in-flight ops via beforeExit before exit fallback
mlischetti Aug 11, 2026
86b25dc
fix: reset cleanupStarted guard after singleton teardown completes
mlischetti Aug 11, 2026
86bdfe2
fix(node): guard uv_thread_join on spawn-failure in three sync paths …
mlischetti Aug 11, 2026
55823d6
Fix resource leak in write-completion callbacks when env == NULL
mlischetti Aug 11, 2026
4de608b
Only clear global isolate state in teardown_waiter_thread_fn on success
mlischetti Aug 11, 2026
a683174
W-23692110: Clear initialized in finally so a failed cleanup() doesn'…
mlischetti Aug 12, 2026
f0eb08d
Fix dead-env napi_ref deletion and orphaned-isolate cleanup race
mlischetti Aug 12, 2026
58f81ec
fix(node): finalize worker-side state when the completion sentinel en…
mlischetti Aug 13, 2026
8fc4a03
fix(node): drop tsfn releases already discharged by napi_closing (F1 …
mlischetti Aug 13, 2026
a5daf20
Fix(node): guard cross-thread destroyEngine for resolver-backed engin…
mlischetti Aug 13, 2026
9d4c845
fix(node): check N-API allocation results in teardown_waiter_create (F3)
mlischetti Aug 13, 2026
d67d6af
fix(node): coalesce concurrent DataWeave.cleanup() calls (F1)
mlischetti Aug 13, 2026
268cd24
Fix F2: Free the teardown waiter when its completion enqueue fails
mlischetti Aug 13, 2026
5e78bf0
Fix napi_initialize deadlock: adopt live isolate during pending teardown
mlischetti Aug 14, 2026
c2deb31
Add deterministic regression test for napi_initialize teardown deadlock
mlischetti Aug 14, 2026
d60b39b
docs: design spec for round-6 instance-lifecycle-state fix (W-23692110)
mlischetti Aug 14, 2026
e6c3e9d
fix(node): model DataWeave instance lifecycle explicitly (round-6 #1/#3)
mlischetti Aug 14, 2026
04757b5
fix(node): coalesce cleanup() before the not-ready guard (task-1 revi…
mlischetti Aug 14, 2026
c6408d5
fix(node): make stream/transform admission atomic under g_mutex (roun…
mlischetti Aug 14, 2026
108a5bc
fix(node): validate napi_get_value_int64 at handle-read sites (round-…
mlischetti Aug 14, 2026
7d60442
fix(node): balance ffi.initialize()/cleanup() in handle-validation test
mlischetti Aug 14, 2026
1411153
docs: design spec for round-7 FFI admission & conversion sweep (W-236…
mlischetti Aug 18, 2026
aabc28b
fix(node): reserve g_active_ops across run() isolate window (round-7 #1)
mlischetti Aug 18, 2026
e645236
fix(node): admit adopted-but-cancelled isolates at all 3 FFI admissio…
mlischetti Aug 18, 2026
2beddeb
fix(node): check every napi_get_value_* status in FFI entrypoints (ro…
mlischetti Aug 18, 2026
aaae61b
docs(node): await async cleanup() in external-modules examples (round…
mlischetti Aug 18, 2026
faf9304
docs: design spec for round-8 OOM-safe streaming/transform setup (W-2…
mlischetti Aug 18, 2026
0207841
fix(node): NULL-check allocations in streaming/transform setup (round…
mlischetti Aug 18, 2026
e77406d
docs: mark ga-cleanup backlog item 6 resolved by round-8 OOM fix (W-2…
mlischetti Aug 18, 2026
ccf1bdd
docs: round-9 design spec — engine lifecycle & worker-OOM hardening
mlischetti Aug 18, 2026
67f7715
docs: correct round-9 spec — #1 is not deterministically testable
mlischetti Aug 18, 2026
d38cbc1
fix(node): OOM-safe worker/callback allocations (round-9 P2)
mlischetti Aug 18, 2026
2577f05
fix(node): check N-API resource creation after reservation (round-9 P3)
mlischetti Aug 18, 2026
dde8cf2
fix(node): defer engine registry removal until admitted ops drain (ro…
mlischetti Aug 18, 2026
f94b126
docs(node): fix stale bridge comments after round-9 all-engines recor…
mlischetti Aug 18, 2026
60d77ad
W-23692110: Remove Java registry entry during Worker/env teardown (ro…
mlischetti Aug 19, 2026
c8364bd
docs: round-10 design spec (worker-teardown dangling resolver ctx + s…
mlischetti Aug 19, 2026
b8735dd
docs: round-11 design spec (engine-pin at admission + all-engines cle…
mlischetti Aug 19, 2026
09ebe3b
W-23692110: Extract bridge_begin_op_locked for atomic admission-time …
mlischetti Aug 19, 2026
b904fcf
W-23692110: Pin engine in the admission transaction for streaming/tra…
mlischetti Aug 19, 2026
8cb13d1
W-23692110: Pin the engine for synchronous runScriptEngine (round 11 #3)
mlischetti Aug 19, 2026
6ee30a5
W-23692110: Register env cleanup hook for every engine + extend owner…
mlischetti Aug 19, 2026
eb3a2f4
W-23692110: Register process exit hooks once, not per singleton (roun…
mlischetti Aug 19, 2026
4eda1f9
W-23692110: Node integration tests for *_engine unknown/destroyed-han…
mlischetti Aug 19, 2026
8789176
W-23692110: Fix initialize/cleanup ref-count imbalance in engine-hand…
mlischetti Aug 19, 2026
d38884c
W-23692110: Fix two cleanup() doc bugs in Node README (round 12 #7)
mlischetti Aug 19, 2026
646d162
W-23692110: Add round-12 design spec (worker ref-leak & teardown-race…
mlischetti Aug 19, 2026
1ac35b2
W-23692110: Amend round-12 spec #3 to transient-reservation mechanism
mlischetti Aug 19, 2026
2ac5afb
W-23692110: Extract release_isolate_ref_locked from napi_cleanup (rou…
mlischetti Aug 19, 2026
a2b4873
W-23692110: Split bridge_finalize; guard registry attach with transie…
mlischetti Aug 20, 2026
26c82cb
W-23692110: Release the init reference on abandoned-env teardown (rou…
mlischetti Aug 20, 2026
9f73680
W-23692110: Document the 1:1 initialize<->engine-bridge assumption (r…
mlischetti Aug 20, 2026
f2e2d75
W-23692110: Re-check readiness in runTransform after input pre-buffer…
mlischetti Aug 20, 2026
095dd75
W-23692110: Make engine creation all-or-nothing on cleanup-hook failu…
mlischetti Aug 20, 2026
078d7a8
W-23692110: Fix double-release of init ref in engine-creation hook-fa…
mlischetti Aug 20, 2026
550e481
W-23692110: Coalesce overlapping module-level cleanup() calls (round …
mlischetti Aug 20, 2026
c06cb39
W-23692110: Require success for the admitted run-vs-destroy ordering …
mlischetti Aug 20, 2026
17e9f54
W-23692110: Add worker_threads lifecycle integration suite (round 12 #9)
mlischetti Aug 20, 2026
28ff46e
Fix module-level cleanup() orphaning a revived singleton; harden term…
mlischetti Aug 20, 2026
443dc1c
W-23692110: Add round-13 design spec (per-env init-reference ownershi…
mlischetti Aug 20, 2026
1140464
W-23692110: Add per-env init-reference record and helpers (round 13 #…
mlischetti Aug 20, 2026
53f196e
W-23692110: Add bounded isolate_ref_release_n_locked; core release wr…
mlischetti Aug 21, 2026
a03a2d8
W-23692110: Acquire a per-env init reference in initialize() at all t…
mlischetti Aug 21, 2026
ea62edc
W-23692110: Add env_init_cleanup env-death hook releasing a dead env'…
mlischetti Aug 21, 2026
f52968c
W-23692110: Stop the per-engine cleanup hook from releasing the init …
mlischetti Aug 21, 2026
ab3661e
W-23692110: Gate cleanup() on the calling env's init-reference owners…
mlischetti Aug 21, 2026
f94e648
W-23692110: Add per-env init-reference ownership integration tests (r…
mlischetti Aug 21, 2026
642560a
W-23692110: Tear down the just-built isolate on create-path init-reco…
mlischetti Aug 21, 2026
0ab98cd
W-23692110: Design spec for review #5 remediation (round 14)
mlischetti Aug 21, 2026
7380677
W-23692110: Gate engine creation on ownership + teardown state + op r…
mlischetti Aug 21, 2026
ff264c7
W-23692110: Retry stranded teardown so a failed last-release cannot o…
mlischetti Aug 21, 2026
8af14b6
W-23692110: Arm the retry signal in release_isolate_ref_locked Case 4…
mlischetti Aug 21, 2026
7b8d639
W-23692110: Release the native init reference even when destroyEngine…
mlischetti Aug 21, 2026
bcedf8b
W-23692110: Reject every nonzero Worker exit in the test helper (roun…
mlischetti Aug 21, 2026
2c84595
W-23692110: Add cross-env Worker regression pinning the round-12 over…
mlischetti Aug 21, 2026
20a8b4e
W-23692110: Add await dw.cleanup() to resolver quick-start docs (roun…
mlischetti Aug 21, 2026
5bb6dd7
docs: add review #6 remediation design spec (round 15)
mlischetti Aug 21, 2026
f810652
chore: stop tracking local scratch docs (review notes + ga-cleanup ba…
mlischetti Aug 21, 2026
0d0d5c1
fix(node): construct-then-publish module singleton (review #6 #1)
mlischetti Aug 21, 2026
7a610f6
fix(node): surface rejected native stream start instead of hanging (r…
mlischetti Aug 21, 2026
883281b
fix(node): honor graal teardown return code + arm async-waiter strand…
mlischetti Aug 21, 2026
e93d9cd
fix(node): complete a stranded teardown from the next initialize() (r…
mlischetti Aug 21, 2026
0f0c1ca
test(node): surface worker destroy errors + isolate cross-env test st…
mlischetti Aug 21, 2026
3eac9f0
test(node): assert reinitialization actually re-creates the engine (r…
mlischetti Aug 21, 2026
27980c4
docs: review #7 remediation design spec (W-23692110)
mlischetti Aug 24, 2026
3655833
fix(node): detach the phantom GraalVM thread on a failed teardown (re…
mlischetti Aug 24, 2026
4b10d70
fix(node): arm retry after a failed init-hook rollback so reinit cann…
mlischetti Aug 24, 2026
698c56d
fix(node): observe the initialize() rollback release and gate reinit …
mlischetti Aug 24, 2026
1e2ee4d
fix(node): track native stream rejection by settlement state, not val…
mlischetti Aug 24, 2026
1545531
test(node): fail worker-lifecycle test when balancing cleanup fails o…
mlischetti Aug 24, 2026
cbf1df2
docs(node): state the final-reference condition in instance cleanup()…
mlischetti Aug 24, 2026
e3cb2f5
docs: update root README Node cleanup examples to await + accurate ho…
mlischetti Aug 24, 2026
9f1f24f
docs(node): correct the #2 retry comment — transient-only recovery, n…
mlischetti Aug 24, 2026
d4413b6
fix(node): fail deterministically instead of deadlocking after a stra…
mlischetti Aug 24, 2026
be60c36
fix(node): survive a synchronous rollback ffi.cleanup() throw in init…
mlischetti Aug 24, 2026
e903109
test(node): always release the native init reference when destroyEngi…
mlischetti Aug 24, 2026
a1823ca
docs(node): capture streaming terminal metadata via manual next() ite…
mlischetti Aug 24, 2026
fce7943
docs(node): correct runTransform input-streaming memory claims (async…
mlischetti Aug 25, 2026
c929e7d
docs(node): show required cleanup() in resolver examples (review #8 #6)
mlischetti Aug 25, 2026
9e3a719
docs: qualify root README cleanup drain as final-reference-only (revi…
mlischetti Aug 25, 2026
56bd6b9
test(node): suppress balancing cleanup error too when the worker-life…
mlischetti Aug 25, 2026
136e914
docs(node): soften Streaming Large Files bounded-memory claim (readFi…
mlischetti Aug 25, 2026
959dcac
test(node): assert re-init actually re-invokes createEngine, not the …
mlischetti Aug 25, 2026
98ee3f1
test(node): retain and await the resolver cleanup-race promise in try…
mlischetti Aug 25, 2026
5ed8656
test(node): surface instance-lifecycle cleanup() failures when the te…
mlischetti Aug 25, 2026
34c6c93
test(node): assert Worker references were released via the not-initia…
mlischetti Aug 25, 2026
03787c0
fix(node): reject non-null non-string inputCharset at the transform b…
mlischetti Aug 25, 2026
c8556c6
docs(node): fix invalid import-before-%dw headers, sync-run blocking …
mlischetti Aug 25, 2026
8d838ac
docs: consolidate multi-engine design into single final-state doc
mlischetti Aug 25, 2026
8b47baf
docs(specs): unify Node & Python on one handle-based engine model, re…
mlischetti Aug 26, 2026
8ab2af2
refactor(native-lib): remove ScriptRuntime singleton and legacy C ent…
mlischetti Aug 26, 2026
ab3a842
feat(python): add ctx argument to RESOLVE_MODULE_CALLBACK for per-eng…
mlischetti Aug 26, 2026
e502bd0
feat(python): shared refcounted isolate with handle-addressed engines…
mlischetti Aug 26, 2026
3b3135d
fix(python): route engine create/destroy through per-OS-thread isolat…
mlischetti Aug 26, 2026
1652ef4
feat(python): per-engine resolver ctx trampoline with owner-thread fa…
mlischetti Aug 26, 2026
9a3839f
feat(python): route DataWeave execution through the engine handle; re…
mlischetti Aug 26, 2026
3dfa8ea
test(python): multi-instance refcount teardown regression (W-23692110)
mlischetti Aug 26, 2026
5720e2f
refactor(node): remove legacy run_script path; engine ABI only (W-236…
mlischetti Aug 26, 2026
29867dd
docs: correct Python notes + add shared engine-lifecycle contract (W-…
mlischetti Aug 26, 2026
e340515
fix(python): detach isolate bootstrap thread + attach-on-demand so cr…
mlischetti Aug 26, 2026
5b32d71
docs: align teardown-failure + Python-isolation notes with the shippe…
mlischetti Aug 26, 2026
6a66760
docs: consolidate Node + Python multi-engine designs into one spec
mlischetti Aug 26, 2026
8111af0
refactor(native-lib): add no-arg ScriptRuntime constructor for built-…
mlischetti Aug 27, 2026
1ca9891
test(node): assert throwing-resolver run() surfaces an error message
mlischetti Aug 27, 2026
a8b5632
fix(python): roll back resolver token when _acquire_isolate fails dur…
mlischetti Aug 27, 2026
98571f1
fix(python): retain live isolate and retry on failed teardown instead…
mlischetti Aug 27, 2026
e5200d5
test(python): reset _teardown_needed and _lib_path in unit conftest's…
mlischetti Aug 27, 2026
a13d989
fix(python): tear down just-created isolate on bootstrap-detach failu…
mlischetti Aug 27, 2026
4e897f2
fix(python): make resolver-backed DataWeave.initialize() idempotent (…
mlischetti Aug 27, 2026
a01cfc2
fix(python): serialize per-instance initialize()/cleanup() to prevent…
mlischetti Aug 27, 2026
2306534
fix(node): retain engine bridge when destroy is skipped on attach fai…
mlischetti Aug 27, 2026
94e51bf
fix(node): pre-allocate streaming/transform completion sentinel so OO…
mlischetti Aug 27, 2026
410b013
fix(node): validate raw napi_initialize lib-path argument (review #10…
mlischetti Aug 27, 2026
040acc5
fix(node): guard unknown/torn-down handle cleanup against attaching a…
mlischetti Aug 27, 2026
b3f0bc5
fix(java): cancel-then-join transform feeder before returning so a sl…
mlischetti Aug 27, 2026
942e241
test(node): mark only the TCK output-equality assertion as expected-f…
mlischetti Aug 27, 2026
ba7a05a
test(node): recategorize 6 execution-failure TCK cases from output-mi…
mlischetti Aug 27, 2026
10139fa
test(node): fail the dedicated TCK job when the corpus is absent/empt…
mlischetti Aug 27, 2026
2bb72ea
ci(node): require a staged TCK corpus on the dedicated master TCK lan…
mlischetti Aug 27, 2026
68d670d
docs: document custom-module resolution scope (run() only) and record…
mlischetti Aug 27, 2026
97a8bdc
test(node): sync tck-policy counts to recategorized ignore-list (38/1…
mlischetti Aug 28, 2026
bb91394
test(node): record empirically-verified execution-failure evidence fo…
mlischetti Aug 28, 2026
32e37aa
fix(native-lib): close input session and convert exceptions to error …
mlischetti Aug 28, 2026
e80ea82
fix(native-lib): reject out-of-range input callback lengths instead o…
mlischetti Aug 28, 2026
1fb6e02
fix(python): detach teardown worker on failed isolate teardown so cro…
mlischetti Aug 28, 2026
ba0c20f
fix(python): guard module-level global engine creation/cleanup with a…
mlischetti Aug 28, 2026
67480ac
fix(native-lib): fail closed on malformed input JSON instead of execu…
mlischetti Aug 28, 2026
31d3df9
test(tck): run known execution failures as strict expected failures i…
mlischetti Aug 28, 2026
c27deb6
test(tck): table-driven policy checks for expected-execution-failure …
mlischetti Aug 28, 2026
3a6daff
docs(native-lib): correct feeder-error read comment to state the vola…
mlischetti Aug 28, 2026
7422a95
fix(native-lib): join input feeder before reading its terminal error …
mlischetti Aug 28, 2026
90ab3fb
test(native-lib): add regression guard for join-before-getError order…
mlischetti Aug 28, 2026
182b7ce
fix(python): serialize resolver install and engine init under a per-i…
mlischetti Aug 28, 2026
08981bc
fix(node): keep the owner-env cleanup hook for stranded resolver brid…
mlischetti Aug 28, 2026
b06b917
fix(node): remove the env cleanup hook on the destroyEngine defer pat…
mlischetti Aug 28, 2026
680e15b
test(node): replace crashing terminate-mid-flight regression with hon…
mlischetti Aug 28, 2026
b5b7aa5
fix(python): retain the bootstrap thread on double-failure so teardow…
mlischetti Aug 28, 2026
453f319
docs(native-lib): correct stale isolate-lifecycle and run_script ABI …
mlischetti Aug 28, 2026
722c0ab
test(python): update README lifecycle guard to the corrected shared-i…
mlischetti Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/actions/node/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ runs:

- name: Run Node.js TCK Conformance
if: always() && inputs.run-tck == 'true'
# This is the dedicated (master-only) TCK lane. The corpus is staged once
# by the shared TCK staging step earlier in the job (see main.yml). Set the
# require-corpus flag so a missing/empty staged corpus fails this job loudly
# instead of skipping silently (review #10 #7); local dev without the flag
# still skips.
env:
DATAWEAVE_TCK_REQUIRE_CORPUS: '1'
run: |
cd native-lib/node && npm run test:tck
shell: bash
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ grimoires/
# Superpowers implementation plans are local scratch artifacts, never commit them.
/docs/superpowers/plans/
/docs/superpowers/plans/**/*.md

# PR follow-up code-review notes are local scratch, keep them untracked.
/docs/pr-*-follow-up-*code-review*.md
# GA cleanup backlog is a local working note, keep it untracked.
/docs/ga-cleanup-backlog.md
734 changes: 734 additions & 0 deletions docs/superpowers/specs/2026-08-07-native-lib-multi-engine-design.md

Large diffs are not rendered by default.

57 changes: 44 additions & 13 deletions native-lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,29 @@ The main purpose is to allow non-JVM consumers (most notably the Python package
│ ┌────────────────────────────────────────┐ │
│ │ Native Shared Library (dwlib) │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ GraalVM Isolate │ │ │
│ │ │ - NativeLib.run_script() │ │ │
│ │ │ GraalVM Isolate (process-wide) │ │ │
│ │ │ - create_engine / │ │ │
│ │ │ create_engine_with_resolver │ │ │
│ │ │ - run_script_engine / │ │ │
│ │ │ run_script_callback_engine / │ │ │
│ │ │ run_script_input_output_ │ │ │
│ │ │ callback_engine │ │ │
│ │ │ - destroy_engine │ │ │
│ │ │ - DataWeave script execution │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```

Each engine is a handle-addressed object created with `create_engine` (or
`create_engine_with_resolver`, which additionally registers a module-resolve
callback) and run via `run_script_engine`, `run_script_callback_engine`, or
`run_script_input_output_callback_engine`, then released with
`destroy_engine`. The underlying GraalVM isolate is a single process-wide
isolate, created and attached via `graal_create_isolate` / `graal_attach_thread`
on first use and torn down via `graal_tear_down_isolate` once the last engine
across the process has been destroyed.

## Building with Gradle

### Prerequisites
Expand Down Expand Up @@ -450,14 +465,21 @@ import { DataWeave } from "@dataweave/native";

const dw = new DataWeave();
dw.initialize();

const r1 = dw.run("2 + 2");
const r2 = dw.run("x + y", { x: 10, y: 32 });

console.log(r1.getString()); // "4"
console.log(r2.getString()); // "42"

dw.cleanup();
try {
const r1 = dw.run("2 + 2");
const r2 = dw.run("x + y", { x: 10, y: 32 });

console.log(r1.getString()); // "4"
console.log(r2.getString()); // "42"
} finally {
// cleanup() returns a Promise; await it. When this releases the FINAL shared
// native reference in the process, it drains any in-flight streaming/transform
// op and completes isolate teardown before resolving (so a subsequent
// initialize() does not race a still-tearing-down isolate). When other
// initialized instances remain, it resolves as soon as this instance is
// released, leaving the shared isolate live for them.
await dw.cleanup();
}
```

### 5) Error handling
Expand Down Expand Up @@ -635,11 +657,20 @@ for await (const chunk of gen) {

### 9) Cleanup

The module registers a `process.on('exit')` handler to clean up automatically. For explicit control:
The module registers two process hooks to clean up automatically: `beforeExit`
(async — it awaits cleanup so an in-flight streaming/transform op drains before
the process exits normally) and `exit` (a synchronous best-effort fallback for
`process.exit()` and uncaught exceptions, which cannot await the drain). Neither
hook fires on `SIGTERM`/`SIGINT`/`SIGKILL`, so install your own signal handler
that awaits `cleanup()` if you need a graceful drain on termination. For explicit
control:

```typescript
import { cleanup } from "@dataweave/native";

// When done with all DataWeave operations
cleanup();
// When done with all DataWeave operations. cleanup() returns a Promise; await it.
// Draining in-flight streaming/transform work and tearing down the isolate happen
// only when this releases the final shared native reference; if other initialized
// instances remain, it resolves as soon as this instance is released.
await cleanup();
```
141 changes: 96 additions & 45 deletions native-lib/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,17 @@ const generator = runStreaming(
'%dw 2.0\noutput application/json\n---\n[1, 2, 3, 4, 5]'
);

for await (const chunk of generator) {
console.log('Chunk:', chunk.toString());
// Iterate manually with next() to capture the terminal return value. A
// `for await` loop consumes the generator's return value internally, so a later
// generator.return() would yield { value: undefined } -- drive next() yourself
// and read the metadata off the terminal { done: true, value: StreamingResult }.
let meta;
while (true) {
const { value, done } = await generator.next();
if (done) { meta = value; break; }
console.log('Chunk:', value.toString());
}

// Generator return value contains metadata:
const meta = await generator.return();
console.log('MIME type:', meta.value.mimeType);
console.log('MIME type:', meta.mimeType);
```

**Parameters:**
Expand All @@ -156,12 +160,21 @@ Execute a DataWeave script with streaming input and output (bidirectional stream

```javascript
import { runTransform } from '@dataweave/native';
import { createReadStream } from 'fs';
import { readFileSync } from 'fs';

// The native read callback is synchronous, so an ASYNC input iterable (e.g.
// fs.createReadStream) is fully pre-buffered into memory before the transform
// starts. A SYNCHRONOUS iterable is instead consumed on demand -- one chunk at a
// time -- so the transform makes no extra full copy of the input (it does NOT by
// itself bound total memory: a source like readFileSync still holds the whole
// input). (See "Sync vs async input and memory" below.)
function* chunked(buf, size = 65536) {
for (let i = 0; i < buf.length; i += size) yield buf.subarray(i, i + size);
}

// Transform a large CSV file to JSON without loading it all into memory
const generator = runTransform(
'%dw 2.0\noutput application/json\n---\npayload',
createReadStream('large-file.csv'),
chunked(readFileSync('large-file.csv')),
{
inputName: 'payload',
mimeType: 'application/csv',
Expand All @@ -175,28 +188,39 @@ for await (const chunk of generator) {
}
```

> **Sync vs async input and memory.** The native read callback runs synchronously
> on the JS thread. **Synchronous** iterables (arrays, generators) are consumed
> on demand — the transform holds only one chunk at a time and makes no extra
> full copy of the input. This bounds the transform's *added* memory, not total
> memory: if the source itself already holds the whole input (e.g. `readFileSync`),
> that memory is still resident. **Async** iterables (e.g. `fs.createReadStream()`)
> are **fully pre-buffered** into memory before the transform starts, because their
> `.next()` returns a Promise that cannot be awaited inside the synchronous
> callback. For large inputs, prefer a synchronous generator so the transform adds
> no second copy.

**Parameters:**
- `script` (string): DataWeave script
- `input` (AsyncIterable<Buffer> | Iterable<Buffer>): Streaming input data
- `opts` (object, optional): Options
- `inputName` (string): Name of input variable (default: "payload")
- `mimeType` (string): Input MIME type (default: "application/json")
- `charset` (string | null): Input character encoding
- `charset` (string, optional): Input character encoding
- `inputs` (object): Additional input variables

**Yields:** `Buffer` chunks as they're produced

**Returns:** `StreamingResult`

#### `cleanup(): void`
#### `cleanup(): Promise<void>`

Clean up the global DataWeave runtime instance. Called automatically on process exit.
Clean up the global DataWeave runtime instance. Called automatically on process shutdown via two hooks: `beforeExit` awaits it, so a streaming/transform operation still in flight drains gracefully before the process exits normally; `exit` is a synchronous last-ditch fallback for `process.exit()` and uncaught exceptions — cases where `beforeExit` never fires — and cannot await the drain. Neither hook fires on `SIGTERM`, `SIGINT`, or `SIGKILL` (Node does not emit `exit` for signals), so install your own signal handler that calls `cleanup()` if you need a graceful drain on termination. Called manually, it releases this instance's reference to the native runtime; the shared native isolate is torn down only when the **last** initialized instance in the process is released. When this call releases that final reference, it resolves once native teardown has actually finished, waiting for any still-in-flight streaming/transform operation to drain first; otherwise (other instances remain initialized) it resolves as soon as this instance is released, without draining process-wide work.

```javascript
import { cleanup } from '@dataweave/native';

// Manual cleanup (usually not needed)
cleanup();
await cleanup();
```

### Class-Based API
Expand All @@ -213,13 +237,13 @@ try {
const result = dw.run('2 + 2');
console.log(result.getString());
} finally {
dw.cleanup();
await dw.cleanup();
}
```

**Methods:**
- `initialize()`: Initialize the native library
- `cleanup()`: Release native resources
- `cleanup(): Promise<void>`: Release this instance's native resources. When it releases the last initialized instance in the process, it resolves once the shared isolate has finished tearing down (draining any in-flight streaming/transform op first); otherwise it resolves as soon as this instance is released, leaving the isolate live for other instances.
- `run(script, inputs?, opts?)`: Same as module-level `run()`
- `runStreaming(script, inputs?)`: Same as module-level `runStreaming()`
- `runTransform(script, input, opts?)`: Same as module-level `runTransform()`
Expand All @@ -231,29 +255,42 @@ DataWeave scripts can import external modules using the `resolveModule` option.
```typescript
import { DataWeave, composeResolvers, modulesFromDirectory, modulesFromJars } from '@dataweave/native';

// Inside an async function (uses `await` for modulesFromJars and cleanup()).
const dw = new DataWeave({
resolveModule: composeResolvers(
modulesFromDirectory('./my-modules'),
await modulesFromJars(['./libs/dw-utils.jar'])
)
});
dw.initialize();

const result = dw.run(`
%dw 2.0
import org::company::utils
output application/json
---
utils::doSomething()
`);

if (result.success) {
console.log(result.getString());
try {
const result = dw.run(`
%dw 2.0
import org::company::utils
output application/json
---
utils::doSomething()
`);

if (result.success) {
console.log(result.getString());
}
} finally {
// Release the engine and resolver closure when done.
await dw.cleanup();
}
```

See [docs/external-modules.md](docs/external-modules.md) for complete documentation, resolver factories, error handling, and dependency management. Note: a resolver runs with full process permissions (no sandboxing) — see the "Security / Trust Model" section there before pointing one at untrusted sources.

### Custom module resolution scope

- A `resolveModule` you configure applies to `run()`.
- Built-in modules (e.g. `dw::core::*`) resolve everywhere — `run()`, `runStreaming()`, and `runTransform()`.
- Custom modules do **not** resolve inside `runStreaming()`/`runTransform()`: those execute on a background thread that must not call back into your resolver, so a streamed/transformed script that imports a custom module fails closed (reports the module as not found) rather than making an unsafe cross-thread call. If you need a custom module in a streamed/transform script, resolve it via `run()` instead, or inline the module into the script.

See [docs/external-modules.md](docs/external-modules.md#multiple-independent-engines) for the full explanation, including the Worker-thread ownership rules.

### Input Formats

Inputs can be provided in multiple formats:
Expand Down Expand Up @@ -366,7 +403,7 @@ console.log(result.getString()); // "300"

```javascript
import { runTransform } from '@dataweave/native';
import { createReadStream, createWriteStream } from 'fs';
import { readFileSync, createWriteStream } from 'fs';

const script = `
%dw 2.0
Expand All @@ -375,9 +412,18 @@ output application/json
payload filter $.amount > 1000
`;

// A synchronous generator is consumed on demand: the transform does not make a
// second full copy of the input. Note readFileSync still holds the whole file in
// memory, so this bounds the transform's *added* memory, not total memory -- the
// native read callback is synchronous, so there is no fully-streaming-from-disk
// path (an async createReadStream would instead be pre-buffered in full first).
function* chunked(buf, size = 65536) {
for (let i = 0; i < buf.length; i += size) yield buf.subarray(i, i + size);
}

const generator = runTransform(
script,
createReadStream('large-transactions.csv'),
chunked(readFileSync('large-transactions.csv')),
{ mimeType: 'application/csv' }
);

Expand Down Expand Up @@ -422,12 +468,17 @@ try {
```javascript
try {
const generator = runStreaming('invalid syntax');
for await (const chunk of generator) {
// Process chunks
// Drive next() manually so the terminal { done: true, value: StreamingResult }
// is captured; a `for await` loop would consume it and a later
// generator.return() would give { value: undefined }.
let meta;
while (true) {
const { value, done } = await generator.next();
if (done) { meta = value; break; }
// Process chunk `value`
}
const meta = await generator.return();
if (!meta.value.success) {
console.error('Streaming error:', meta.value.error);
if (!meta.success) {
console.error('Streaming error:', meta.error);
}
} catch (err) {
console.error('Native error:', err);
Expand All @@ -440,20 +491,20 @@ The Node.js binding uses **N-API** (Node-API) for C addon integration:

- **Thread-safe**: N-API calls are serialized on the Node.js event loop
- **Async operations**: Streaming operations yield control to the event loop between chunks
- **No blocking**: Long-running scripts execute on the native side without blocking the event loop
- **No event-loop blocking for streaming**: `runStreaming`/`runTransform` execute on a background worker and yield to the event loop between chunks. Note the **synchronous** `run()` runs native work directly on the calling JS thread and *does* block it until the script completes — use the streaming methods for long-running work you cannot block on.

**Important:** Do not share a single `DataWeave` instance across Worker threads. Use the module-level functions (which use a global singleton) or create separate instances per thread.

**Custom module resolvers and Worker threads:** the native layer installs at
most one resolver callback for the whole process lifetime, and it is bound to
the Worker (main thread or a `worker_threads` Worker) that registered it
first — see [External Modules: Multiple Resolvers](docs/external-modules.md#multiple-resolvers-in-one-process).
**Custom module resolvers and Worker threads:** each resolver-backed
`DataWeave` instance's native engine is bound to the thread that created it
(main thread or a `worker_threads` Worker) — see
[External Modules: Multiple Independent Engines](docs/external-modules.md#multiple-independent-engines).
Custom-module resolution attempted from any *other* thread is not routed to
that thread's own `resolveModule` callback; it silently falls back to
built-in modules only (custom module paths resolve as "not found" rather than
crashing or hanging). If you need per-Worker custom modules, resolve them on
the thread that first constructs a resolver-backed `DataWeave` instance, or
avoid resolver-backed instances in worker pools altogether.
that engine's `resolveModule` callback; it silently falls back to built-in
modules only (custom module paths resolve as "not found" rather than
crashing or hanging). If you need custom modules on multiple Workers,
construct and use a separate resolver-backed `DataWeave` instance on each
Worker, created on that Worker itself.

## Platform Support

Expand Down Expand Up @@ -541,12 +592,12 @@ Tests use **Vitest** and cover:

- **Buffered execution** (`run`): Best for small scripts with sub-MB outputs
- **Streaming execution** (`runStreaming`): Best for large outputs (MB+), reduces memory footprint
- **Bidirectional streaming** (`runTransform`): Best for large inputs and outputs, constant memory usage
- **Bidirectional streaming** (`runTransform`): Best for large outputs; input memory is bounded only with a **synchronous** input iterable (async streams are pre-buffered — see the `runTransform` memory note above)

Benchmark (1MB JSON transformation):
- `run()`: ~50ms, 2MB peak memory
- `runStreaming()`: ~55ms, 500KB peak memory
- `runTransform()`: ~60ms, 256KB peak memory (streaming input)
- `runTransform()`: ~60ms, 256KB peak memory (synchronous input iterable; an async stream is pre-buffered, so peak memory scales with input size)

## See Also

Expand Down
Loading
Loading