Skip to content

fix: react-app and node build - #449

Merged
Ryan-Millard merged 38 commits into
Ryan-Millard:devfrom
Krasner:dev/node-js
Jun 26, 2026
Merged

fix: react-app and node build#449
Ryan-Millard merged 38 commits into
Ryan-Millard:devfrom
Krasner:dev/node-js

Conversation

@Krasner

@Krasner Krasner commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

What was changed & why

@Ryan-Millard and @Prachi-Gupta2808 taking both of your work in #433 and fixing up the vite.config.js bundler will build both the browser and node packages.

just clean packages-js
just build packages-js

will rebuild packages under packages/js/dist/browser and packages/js/dist/node

to check:

just react-js build
just react-js start


just console-js <test image>

both should now work. Let me know if this works for you too.
Still a gpu deallocation issue on the nodejs app that i'm not sure how to resolve yet

Done! SVG saved to output.svg
FATAL ERROR: Error::ThrowAsJavaScriptException napi_throw
----- Native stack trace -----

 1: 0xe46a74 node::OnFatalError(char const*, char const*) [node]
 2: 0xf692e5  [node]
 3: 0x773480483430  [/usr/src/app/node_modules/.pnpm/webgpu@0.4.0/node_modules/webgpu/dist/linux-x64.dawn.node]
 4: 0x77348073b525  [/usr/src/app/node_modules/.pnpm/webgpu@0.4.0/node_modules/webgpu/dist/linux-x64.dawn.node]
 5: 0x77348073b6c4  [/usr/src/app/node_modules/.pnpm/webgpu@0.4.0/node_modules/webgpu/dist/linux-x64.dawn.node]
 6: 0xf3cc65  [node]
 7: 0x77343bc0f5e2

----- JavaScript stack trace -----

1: processImmediate (node:internal/timers:484:21)


Aborted (core dumped)

Fixes: #

Changes

Testing & Verification

Additional Resources

@Krasner
Krasner requested a review from Ryan-Millard as a code owner June 21, 2026 01:59
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Ryan-Millard, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 51 minutes and 48 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 15cbd10e-0fee-4731-892e-9b0b584f4646

📥 Commits

Reviewing files that changed from the base of the PR and between 4ad6e54 and 9d5fe79.

⛔ Files ignored due to path filters (1)
  • output.svg is excluded by !**/*.svg
📒 Files selected for processing (1)
  • Justfile

Walkthrough

The JS package now builds separate browser and Node.js artifacts, routes worker setup through target-specific helpers, adds a Node console example, and updates build, CI, and documentation tooling to match the new package layout.

Changes

Node.js target for img2num JS package

Layer / File(s) Summary
Package build contracts and workspace
packages/js/package.json, packages/js/vite.config.js, bindings/js/CMakeLists.txt, pnpm-workspace.yaml
package.json defines separate browser and node entrypoints and build scripts; vite.config.js selects target-specific output and aliases from process.env.TARGET; CMakeLists.txt expands the Emscripten environment to include Node; pnpm-workspace.yaml adds the console example package and webgpu build allowance.
Worker and WASM runtime wiring
packages/js/src/target/browser/worker.js, packages/js/src/target/node/worker.js, packages/js/src/target/node/webgpu.js, packages/js/src/wasmClient.js, packages/js/src/safeWasmWrappers.js, packages/js/src/workers/wasmWorker.js
Target-specific createWorker() helpers are added for browser and Node, Node WebGPU lifecycle helpers are introduced, wasmClient.js switches to async worker setup and shared response routing, safeWasmWrappers.js awaits worker initialization, and wasmWorker.js adds shared message handling plus browser and Node wiring.
Console example app
example-apps/console-js/index.js, example-apps/console-js/package.json
A Node script reads an input image, decodes pixel data with sharp, calls img2num to generate SVG, and writes output.svg; the package is configured for ESM execution with img2num and sharp dependencies.
Tooling, CI, and docs updates
Justfile, .github/workflows/build-react-app.yml, example-apps/react-js/vite.config.js, packages/js/tsconfig.typedoc.json, docs/plugins/docusaurusPluginTypeDocConfig.js, eslint.config.js, core/include/internal/gpu.h
The Justfile adds build and runner targets, CI installs at repo level and prebuilds img2num for browser, the React Vite config updates worker and bundle settings, TypeDoc and docs config paths are adjusted, ESLint ignores packages/js/dist, and gpu.h removes an inline comment.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant wasmClient
  participant createWorker
  participant wasmWorker
  participant WebGPU

  Caller->>wasmClient: imageToSvg(...)
  wasmClient->>createWorker: await createWorker()
  createWorker-->>wasmClient: worker wrapper
  wasmClient->>wasmWorker: postMessage({ id, funcName, args })
  wasmWorker->>wasmWorker: handleMessage(data)
  wasmWorker-->>wasmClient: postMessage({ id, output, returnValue })
  wasmClient-->>Caller: resolve SVG result

  alt Node target
    wasmWorker->>WebGPU: initWebGPU()
    wasmWorker->>WebGPU: destroyWebGPU()
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

wasm

Suggested reviewers

  • Ryan-Millard

Poem

🐇 Two paths now hop in one bright code,
Browser and Node share the load.
Workers wake with signals clear,
SVGs bloom when images appear.
I twitch my nose and cheer with glee —
One build, two worlds, and all for me!


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
No Ai Slop Pr Description ❌ Error FAIL: The PR description keeps generic template comments and empty sections; only one paragraph gives concrete change details. Rewrite the description to keep only a concise, specific summary of what changed, why, and how it was verified; remove the copied template comments and blank headings.
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title is specific, uses a Conventional Commits prefix, and matches the React and Node build changes.
Description check ✅ Passed The description clearly discusses the same build and bundler updates described in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No Strangely-Named Root Markdown Files ✅ Passed HEAD’s root tree only contains README.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md, and LICENSE; no unconventional root markdown file was added.
Coderabbit Config Needs Update ✅ Passed No new language, linter/config, or uncataloged file extension appears in the diff, and .coderabbit.yaml already covers JS/packages, WGSL, and workflow files.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added c/c++ Changes to C or C++ files js/ts example-app Updates to code related to demonstration (example) applications core labels Jun 21, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/js/src/wasmClient.js`:
- Around line 87-94: The worker.onError handler currently rejects pending
callbacks and clears them, but does not reset the internal state (the
`initialized` flag and `worker` reference remain unchanged). This causes
subsequent calls to route to the dead worker instead of reinitializing. After
calling callbacks.clear() in the error handler, reset `initialized` to false and
clear or terminate the `worker` reference so that future operations will
properly reinitialize the worker.

In `@packages/js/src/workers/wasmWorker.js`:
- Around line 235-238: The message handler in `parentPort.on("message", async
(data) => {...})` is closing the worker port and destroying WebGPU after
handling each individual message, but the worker is designed to handle multiple
requests until explicitly terminated. Remove the `destroyWebGPU()` and
`parentPort.close()` calls from the message handler so the worker remains
available for subsequent calls to `callWasm()`. The cleanup logic should only
execute when the worker is explicitly terminated through the
`terminateWasmWorker()` function, not after every message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a2f0922b-b12b-4670-8d8a-0cc84a9436da

📥 Commits

Reviewing files that changed from the base of the PR and between 4a74983 and 5a64c1a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (10)
  • .github/workflows/build-react-app.yml
  • Justfile
  • eslint.config.js
  • packages/js/src/target/browser/worker.js
  • packages/js/src/target/node/webgpu.js
  • packages/js/src/target/node/worker.js
  • packages/js/src/wasmClient.js
  • packages/js/src/workers/wasmWorker.js
  • packages/js/tsconfig.typedoc.json
  • packages/js/vite.config.js
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: Build Documentation Site / Build Docusaurus Site
  • GitHub Check: Build C/C++ / Build WASM (bindings/js)
  • GitHub Check: Build C/C++ / Build Python
  • GitHub Check: Build C/C++ / Build C & C++
  • GitHub Check: Lint & Validate Code
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.editorconfig)

**/*.{js,ts,jsx,tsx}: Use 2-space indentation for JavaScript and TypeScript files
Maintain 200 character maximum line length for JavaScript/TypeScript files

Files:

  • packages/js/src/target/node/worker.js
  • packages/js/src/target/browser/worker.js
  • eslint.config.js
  • packages/js/src/workers/wasmWorker.js
  • packages/js/src/target/node/webgpu.js
  • packages/js/src/wasmClient.js
  • packages/js/vite.config.js
**

⚙️ CodeRabbit configuration file

**: # Contributing to Img2Num

Want to contribute to Img2Num? There are a few things you need to know.

We wrote a contribution guide to help you get started.

A few important points:

  • Add tests with your PR — new features and bug fixes must include tests where appropriate. PRs without tests are unlikely to be approved.
  • Follow the repository's coding style rules.
  • Use the issue and PR templates when filing issues or submitting code. Your PR will be rejected if you don't.

If you're unsure what to change, open a discussion and someone will assist you.

Questions?

If you have questions or need help:

Thank you for improving Img2Num! 🎨🚀

**: BasedOnStyle: LLVM
Standard: c++20

--- Basic formatting ---

IndentWidth: 4
ColumnLimit: 100
TabWidth: 4

--- Braces ---

Cpp11BracedListStyle: true
SpaceBeforeCpp11BracedList: true
BreakBeforeBraces: Attach

--- Braced initializers ---

Cpp11BracedListStyle: true

--- Constructor initializer lists ---

PackConstructorInitializers: Never
BreakConstructorInitializers: BeforeComma
ConstructorInitializerIndentWidth: 4

--- Alignment ---

AlignAfterOpenBracket: BlockIndent

--- Pointers ---

PointerAlignment: Left

--- Includes ---

IncludeBlocks: Regroup
SortIncludes: CaseInsensitive

--- Extern "C" cleanliness ---

IndentExternBlock: NoIndent

--- Lambdas ---

AllowShortLambdasOnASingleLine: Inline

--- Functions ---

AllowShortFunctionsOnASingleLine: None

**: root = true

-------------------------

Global defaults

-------------------------

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
max_line_lengt...

Files:

  • packages/js/src/target/node/worker.js
  • packages/js/src/target/browser/worker.js
  • eslint.config.js
  • packages/js/tsconfig.typedoc.json
  • packages/js/src/workers/wasmWorker.js
  • packages/js/src/target/node/webgpu.js
  • packages/js/src/wasmClient.js
  • packages/js/vite.config.js
  • Justfile
.github/workflows/**

⚙️ CodeRabbit configuration file

.github/workflows/**: GitHub Actions workflows. Review for:

  • SHA-pinned action versions for third-party actions (security best practice).
  • Secrets accessed only via ${{ secrets.* }} — never hardcoded.
  • Least-privilege permissions on each job/workflow.
  • Correct job dependency ordering (needs:) and if/condition logic.

Files:

  • .github/workflows/build-react-app.yml
**/*.json

📄 CodeRabbit inference engine (.editorconfig)

Do not trim trailing whitespace in JSON files

Files:

  • packages/js/tsconfig.typedoc.json
🧠 Learnings (2)
📚 Learning: 2026-05-01T22:50:11.527Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 339
File: release-please-config.json:18-47
Timestamp: 2026-05-01T22:50:11.527Z
Learning: In this repo, release-please-action v4 preserves '/' verbatim in slash-containing path-based package keys when emitting GitHub Actions output names (e.g., `bindings/c--release_created`). When referencing these step outputs in `job.outputs` (and other expressions), use bracket notation with the exact output name: `${{ steps.release.outputs['bindings/c--release_created'] }}` rather than dot notation. If needed, map the complex step output to a clean job-level output alias so downstream jobs can use dot notation via that alias.

Applied to files:

  • .github/workflows/build-react-app.yml
📚 Learning: 2026-05-19T17:30:09.565Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 375
File: .github/workflows/cmake-build.yml:86-88
Timestamp: 2026-05-19T17:30:09.565Z
Learning: In Ryan-Millard/Img2Num CI/workflow YAMLs, any `uv sync` command used for the Python package build must include `--no-build-isolation` (do not remove it). If you need deterministic dependency installs for CI, you may add `--frozen` alongside it (e.g., `uv sync --frozen --no-build-isolation`), and it should not conflict with the repo’s build setup.

Applied to files:

  • .github/workflows/build-react-app.yml
🪛 ast-grep (0.43.0)
packages/js/src/target/node/webgpu.js

[warning] 33-33: Avoid using the initial state variable in setState
Context: setTimeout(resolve, 50)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🔇 Additional comments (8)
Justfile (1)

109-109: LGTM!

.github/workflows/build-react-app.yml (1)

28-28: LGTM!

Also applies to: 38-39

packages/js/tsconfig.typedoc.json (1)

13-21: LGTM!

eslint.config.js (1)

18-18: LGTM!

packages/js/vite.config.js (1)

4-4: LGTM!

packages/js/src/target/browser/worker.js (1)

7-7: LGTM!

packages/js/src/target/node/worker.js (1)

3-3: LGTM!

Also applies to: 17-17

packages/js/src/target/node/webgpu.js (1)

31-35: LGTM!

Comment on lines +87 to +94
worker.onError((event) => {
const output = event.message || "WASM worker error";
const err = new Error(`[Img2Num wasmClient] Error: ${output}`);
for (const [_id, cb] of callbacks) {
cb.reject(err);
}
callbacks.clear();
};
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset worker state after error to allow recovery.

After Line 87 error handling runs, pending promises are rejected, but initialized stays true and worker remains set. Subsequent calls can route to a dead worker instead of reinitializing. Clear runtime state in the error path (and terminate if still alive).

Suggested fix
   worker.onError((event) => {
     const output = event.message || "WASM worker error";
     const err = new Error(`[Img2Num wasmClient] Error: ${output}`);
     for (const [_id, cb] of callbacks) {
       cb.reject(err);
     }
     callbacks.clear();
+    worker?.terminate();
+    worker = null;
+    initialized = false;
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/js/src/wasmClient.js` around lines 87 - 94, The worker.onError
handler currently rejects pending callbacks and clears them, but does not reset
the internal state (the `initialized` flag and `worker` reference remain
unchanged). This causes subsequent calls to route to the dead worker instead of
reinitializing. After calling callbacks.clear() in the error handler, reset
`initialized` to false and clear or terminate the `worker` reference so that
future operations will properly reinitialize the worker.

Comment on lines +235 to +238
parentPort.on("message", async (data) => {
await handleMessage(data);
await destroyWebGPU();
parentPort.close();

@coderabbitai coderabbitai Bot Jun 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Worker teardown per message
rg -n -C3 'parentPort\.on\("message"|destroyWebGPU\(\)|parentPort\.close\(\)' packages/js/src/workers/wasmWorker.js

# Client-side persistent lifecycle expectations
rg -n -C4 'initialized|initWasmWorker|terminateWasmWorker|worker\.postMessage' packages/js/src/wasmClient.js

Repository: Ryan-Millard/Img2Num

Length of output: 3319


Do not close the worker after handling each message.

Lines 237–238 tear down GPU and close the worker port after every message, but wasmClient initializes the worker once and expects it to handle multiple requests until terminateWasmWorker() is explicitly called. This creates a lifecycle mismatch: subsequent calls to callWasm() will fail or hang against a closed worker.

The worker should remain open for the duration of its lifetime and only shut down when explicitly instructed by the client.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/js/src/workers/wasmWorker.js` around lines 235 - 238, The message
handler in `parentPort.on("message", async (data) => {...})` is closing the
worker port and destroying WebGPU after handling each individual message, but
the worker is designed to handle multiple requests until explicitly terminated.
Remove the `destroyWebGPU()` and `parentPort.close()` calls from the message
handler so the worker remains available for subsequent calls to `callWasm()`.
The cleanup logic should only execute when the worker is explicitly terminated
through the `terminateWasmWorker()` function, not after every message.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

what is your suggestion?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Krasner, here is the concrete fix. Move cleanup out of the per-message handler and into a parentPort.on("close", ...) listener. That event fires when the parent calls worker.terminate() (i.e., terminateWasmWorker()), so WebGPU is destroyed exactly once at the end of the worker's lifetime instead of after every request.

Current code (problematic):

parentPort.on("message", async (data) => {
  await handleMessage(data);
  await destroyWebGPU();   // ← tears down GPU after every message
  parentPort.close();      // ← closes the port after every message
});

Suggested fix:

parentPort.on("message", async (data) => {
  await handleMessage(data);
  // Worker stays alive for subsequent callWasm() requests
});

// Clean up when the parent terminates the worker via worker.terminate()
parentPort.on("close", async () => {
  await destroyWebGPU();
});

This keeps the worker reusable for the full lifecycle managed by wasmClient.js, and cleanup still fires exactly once when terminateWasmWorker() is called from the parent side.

Note on the console-js example: If that app sends exactly one message and immediately exits, the per-message teardown appeared to "work" locally — but it would break any Node consumer that calls imageToSvg more than once. The fix above is safe for both single- and multi-call scenarios.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

when I use

parentPort.on("close", async () => {
  await destroyWebGPU();
});

never gets called

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Krasner, that's a known Node.js gotcha. When the parent calls worker.terminate(), the thread is killed forcefully — parentPort.on("close", ...) is not guaranteed to fire before termination, so async cleanup never runs.

The reliable pattern is a sentinel shutdown message. The parent sends a special message before terminating, the worker runs its async cleanup and then closes itself.

packages/js/src/workers/wasmWorker.js — replace the per-message teardown:

parentPort.on("message", async (data) => {
  if (data?.__shutdown === true) {
    // Ordered async teardown triggered by the parent
    try {
      await destroyWebGPU();
    } finally {
      parentPort.close(); // now it's the worker closing its own port — fires reliably
    }
    return;
  }
  await handleMessage(data);
});

packages/js/src/wasmClient.js — update terminateWasmWorker() to send the sentinel before terminating:

export async function terminateWasmWorker() {
  if (!worker) return;
  // Signal the worker to run async cleanup
  worker.postMessage({ __shutdown: true });
  // Give the worker a moment to finish cleanup, then hard-terminate as a fallback
  await new Promise((resolve) => setTimeout(resolve, 200));
  worker.terminate();
  worker = null;
}

This way:

  1. The shutdown message arrives before the thread is killed.
  2. The worker runs destroyWebGPU() and closes the port on its own terms.
  3. The 200 ms timeout is a safety net in case the GPU teardown hangs (matching the known NAPI crash described in the PR).

For the console-js single-shot app, calling terminateWasmWorker() after imageToSvg() resolves will trigger the same clean path — no special casing needed.

(ʕ·ᴥ·ʔ)? ← curious about whether the GPU crash still surfaces after this

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@Krasner I think that this should be in the src/target/*/worker.js files because they have terminate functions. I'm not quite sure what is going on here. It sucks that we have to include the WebGPU stuff in the library and that Node doesn't have it at all.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@Krasner

Krasner commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

@Ryan-Millard there is still some weird gpu deallocation issue with node js.
I think part of it is that the C++ code never explicitly deallocates the GPU - instead it relies on RAII to automatically clean up the GPU instance - which never happens on the JS side because of EMSCRIPTEN_KEEPALIVE

I put a PR review on my own branch: Krasner#5
Can you take a look and check if it makes sense. I've added a img2num::terminate() call that explicitly calls the GPU class destructor.

With this I think I reliably don't get any more gpu deallocation errors.

@Ryan-Millard

Copy link
Copy Markdown
Owner

Hi @Krasner. I'll have a look at this when I get a chance. I'm just currently busy with fixing the workflow failures on dev because those are what got in the way of us publishing previously.

@Krasner

Krasner commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

Hi @Krasner. I'll have a look at this when I get a chance. I'm just currently busy with fixing the workflow failures on dev because those are what got in the way of us publishing previously.

No worries!

@Krasner

Krasner commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

@Ryan-Millard there is still some weird gpu deallocation issue with node js. I think part of it is that the C++ code never explicitly deallocates the GPU - instead it relies on RAII to automatically clean up the GPU instance - which never happens on the JS side because of EMSCRIPTEN_KEEPALIVE

I put a PR review on my own branch: Krasner#5 Can you take a look and check if it makes sense. I've added a img2num::terminate() call that explicitly calls the GPU class destructor.

With this I think I reliably don't get any more gpu deallocation errors.

@Ryan-Millard can you review this? I need your insight here

@Ryan-Millard

Copy link
Copy Markdown
Owner

@Ryan-Millard there is still some weird gpu deallocation issue with node js. I think part of it is that the C++ code never explicitly deallocates the GPU - instead it relies on RAII to automatically clean up the GPU instance - which never happens on the JS side because of EMSCRIPTEN_KEEPALIVE
I put a PR review on my own branch: Krasner#5 Can you take a look and check if it makes sense. I've added a img2num::terminate() call that explicitly calls the GPU class destructor.
With this I think I reliably don't get any more gpu deallocation errors.

@Ryan-Millard can you review this? I need your insight here

I'll have a look at it when I get a chance. I think I'll be able to do it tomorrow.

@coderabbitai coderabbitai Bot added the wasm label Jun 26, 2026
@Ryan-Millard

Ryan-Millard commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Hi @Krasner. I'm sorry for not looking at this yesterday - my power went out just as I woke up (good old South Africa).

After having a look at it now, I agree that your solution does fix it and probably is the best option. I think it's a Dawn teardown issue that may be coming from it freeing up it's own resources (internally) before RAII ever gets the chance to.

I think this is a problem to address after this PR:

  1. It's not overly important because everything works fine as-is.
  2. PyPI is threatening to disactivate our library because we haven't published anything, so I'd like to publish this stuff now if possible.
  3. I think we need to look at how to internally (within our library) resolve this matter rather than leave it up to the user to manage (we set up and used the resource, so we should do the housekeeping).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Justfile`:
- Around line 107-109: The console-js Just target currently runs
example-apps/console-js/index.js without guaranteeing the img2num JavaScript
package has been built first, so it can fail from a clean checkout. Update the
console-js recipe to depend on the existing JS build target used for the package
artifact (the same build step that satisfies react-js build), ensuring the JS
package is built before invoking the Node example.
- Around line 101-105: The echoed command in the Justfile is stale and does not
match the executable actually run by the recipe. Update the echo in the
console-c recipe so it prints the same binary path used by the execution line,
matching the existing console-cpp pattern and the actual executable name in the
console-c block. Keep the echoed command and the invoked command in sync so
copy-paste works correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 67c1a8e8-e5db-48c3-9cfc-23f6024acbdb

📥 Commits

Reviewing files that changed from the base of the PR and between 5a64c1a and 4ad6e54.

📒 Files selected for processing (1)
  • Justfile
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: Build C/C++ / Build Python
  • GitHub Check: Build C/C++ / Build C & C++
  • GitHub Check: Build C/C++ / Build WASM (bindings/js)
  • GitHub Check: Build Documentation Site / Build Docusaurus Site
  • GitHub Check: Lint & Validate Code
🧰 Additional context used
📓 Path-based instructions (1)
**

⚙️ CodeRabbit configuration file

**: # Contributing to Img2Num

Want to contribute to Img2Num? There are a few things you need to know.

We wrote a contribution guide to help you get started.

A few important points:

  • Add tests with your PR — new features and bug fixes must include tests where appropriate. PRs without tests are unlikely to be approved.
  • Follow the repository's coding style rules.
  • Use the issue and PR templates when filing issues or submitting code. Your PR will be rejected if you don't.

If you're unsure what to change, open a discussion and someone will assist you.

Questions?

If you have questions or need help:

Thank you for improving Img2Num! 🎨🚀

**: BasedOnStyle: LLVM
Standard: c++20

--- Basic formatting ---

IndentWidth: 4
ColumnLimit: 100
TabWidth: 4

--- Braces ---

Cpp11BracedListStyle: true
SpaceBeforeCpp11BracedList: true
BreakBeforeBraces: Attach

--- Braced initializers ---

Cpp11BracedListStyle: true

--- Constructor initializer lists ---

PackConstructorInitializers: Never
BreakConstructorInitializers: BeforeComma
ConstructorInitializerIndentWidth: 4

--- Alignment ---

AlignAfterOpenBracket: BlockIndent

--- Pointers ---

PointerAlignment: Left

--- Includes ---

IncludeBlocks: Regroup
SortIncludes: CaseInsensitive

--- Extern "C" cleanliness ---

IndentExternBlock: NoIndent

--- Lambdas ---

AllowShortLambdasOnASingleLine: Inline

--- Functions ---

AllowShortFunctionsOnASingleLine: None

**: root = true

-------------------------

Global defaults

-------------------------

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
max_line_lengt...

Files:

  • Justfile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Justfile`:
- Around line 107-109: The console-js Just target currently runs
example-apps/console-js/index.js without guaranteeing the img2num JavaScript
package has been built first, so it can fail from a clean checkout. Update the
console-js recipe to depend on the existing JS build target used for the package
artifact (the same build step that satisfies react-js build), ensuring the JS
package is built before invoking the Node example.
- Around line 101-105: The echoed command in the Justfile is stale and does not
match the executable actually run by the recipe. Update the echo in the
console-c recipe so it prints the same binary path used by the execution line,
matching the existing console-cpp pattern and the actual executable name in the
console-c block. Keep the echoed command and the invoked command in sync so
copy-paste works correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 67c1a8e8-e5db-48c3-9cfc-23f6024acbdb

📥 Commits

Reviewing files that changed from the base of the PR and between 5a64c1a and 4ad6e54.

📒 Files selected for processing (1)
  • Justfile
🛑 Comments failed to post (2)
Justfile (2)

101-105: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Echo the actual executable names.

The logged commands are now out of sync with the binaries you execute, so anyone copying the echoed command gets the wrong path.

Suggested fix
 console-cpp input:
-    `@echo` "./build-c-cpp/example-apps/console-cpp/console_cpp_app {{ input }}"
+    `@echo` "./build-c-cpp/example-apps/console-cpp/Img2NumExample_console_cpp {{ input }}"
     ./build-c-cpp/example-apps/console-cpp/Img2NumExample_console_cpp "{{ input }}"

 console-c input:
-    `@echo` "./build-c-cpp/example-apps/console-c/console_c_app {{ input }}"
+    `@echo` "./build-c-cpp/example-apps/console-c/CImg2NumExample_console_c {{ input }}"
     ./build-c-cpp/example-apps/console-c/CImg2NumExample_console_c "{{ input }}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    `@echo` "./build-c-cpp/example-apps/console-cpp/Img2NumExample_console_cpp {{ input }}"
    ./build-c-cpp/example-apps/console-cpp/Img2NumExample_console_cpp "{{ input }}"

console-c input:
    `@echo` "./build-c-cpp/example-apps/console-c/Img2NumExample_console_c {{ input }}"
    ./build-c-cpp/example-apps/console-c/CImg2NumExample_console_c "{{ input }}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Justfile` around lines 101 - 105, The echoed command in the Justfile is stale
and does not match the executable actually run by the recipe. Update the echo in
the console-c recipe so it prints the same binary path used by the execution
line, matching the existing console-cpp pattern and the actual executable name
in the console-c block. Keep the echoed command and the invoked command in sync
so copy-paste works correctly.

107-109: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make console-js build the JS package first.

This target now runs the Node example without ensuring the img2num node artifact exists. From a clean checkout—or right after just clean packages-jsjust console-js … can fail on the same missing-build contract that already affected react-js build.

Suggested fix
-console-js input:
+console-js input: build-packages-js
     `@echo` "node example-apps/console-js/index.js {{ input }}"
     node example-apps/console-js/index.js "{{ input }}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

console-js input: build-packages-js
    `@echo` "node example-apps/console-js/index.js {{ input }}"
    node example-apps/console-js/index.js "{{ input }}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Justfile` around lines 107 - 109, The console-js Just target currently runs
example-apps/console-js/index.js without guaranteeing the img2num JavaScript
package has been built first, so it can fail from a clean checkout. Update the
console-js recipe to depend on the existing JS build target used for the package
artifact (the same build step that satisfies react-js build), ensuring the JS
package is built before invoking the Node example.

@Krasner

Krasner commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

2. PyPI is threatening to disactivate our library because we haven't published anything, so I'd like to publish this stuff now if possible.

Yes go ahead!

@Ryan-Millard

Copy link
Copy Markdown
Owner

@coderabbitai please generate a commit message and description that I can use when I squash this PR into the dev branch.

Please also give me a full breakdown of the changes on this branch.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@Ryan-Millard Here’s a squash commit message + description you can use, followed by a full breakdown of the branch changes.

Suggested squash commit message

fix(js): support browser and node builds from shared Vite setup

Suggested squash commit description

Fix the JS packaging/build pipeline so the img2num package can be built for both browser and Node.js from a shared Vite configuration.

Highlights:
- add dual-target JS builds with outputs under packages/js/dist/browser and packages/js/dist/node
- update package exports/entrypoints so browser and Node consumers resolve the correct bundle
- fix the React app build workflow by installing workspace dependencies at the repo root and building img2num before the app
- add shared worker creation logic for browser and Node runtimes
- update Node worker/WebGPU setup and teardown handling
- add a console-js example app for running img2num from Node
- update docs, typedoc, eslint, and workspace configuration to match the new package layout

Also:
- expand the JS/WASM runtime environment to include Node.js
- remove the old imageToUint8ClampedArray test file as part of the packaging/runtime refactor

Note:
- explicit GPU destruction/terminate handling was discussed separately and is not the main scope of this PR

Full breakdown of the changes on this branch

1. CI / workflow fixes

.github/workflows/build-react-app.yml

  • Changed dependency installation to run pnpm install at the repository root instead of only inside the React example.
  • Added a step to build the img2num browser package before building the React app.
  • This aligns with the earlier observation in the thread that the app build depends on img2num already being built.

2. Justfile / developer workflow updates

Justfile

  • Added a new packages-js build target.
  • Added build-packages-js to build the JS/WASM bindings and then build the img2num package.
  • Updated react-js build to use build-packages-js instead of only build-wasm.
  • Added console-js <input_image> target for the new Node example app.
  • Added clean packages-js support to remove packages/js/dist/.
  • Added clean packages-py support to remove packages/py/build-py/.
  • Updated init to run pnpm install after submodule initialization.

3. WASM / native runtime targeting

bindings/js/CMakeLists.txt

  • Expanded Emscripten’s target environment from web,worker to web,worker,node.
  • This is the native-side change that enables the JS bindings to support Node runtime targets as well.

core/include/internal/gpu.h

  • Only a comment cleanup; no functional GPU API change in this PR.

4. JS package structure and publishing changes

packages/js/package.json

  • Changed the main entrypoint from the old root entry to ./dist/browser/img2num.js.
  • Added build scripts:
    • build:browser
    • build:node
    • build
  • Added an exports map so consumers get the right build:
    • node./dist/node/img2num.js
    • browser./dist/browser/img2num.js
    • import/default → browser build
  • Added webgpu as an optional dependency.
  • Removed the older packaging metadata that no longer fits the new build layout.

packages/js/vite.config.js

  • Added the central shared Vite config for both browser and Node builds.
  • Uses process.env.TARGET to switch between browser and node.
  • Outputs to:
    • dist/browser
    • dist/node
  • Browser build:
    • standard ES module library build
  • Node build:
    • SSR-style build targeting Node 18
    • multi-entry output including both:
      • src/index.js
      • src/workers/wasmWorker.js
  • Added aliases for:
    • @__TARGET__
    • @wasm
    • @workers
  • Configured worker format as ES modules.
  • Externalized Node-specific modules in Node builds.
  • Added custom chunking/asset naming behavior for the worker/WASM output.

5. Worker abstraction and WASM client refactor

packages/js/src/target/browser/worker.js

  • Added createWorker() for browser runtime.
  • Creates a module worker using new URL(..., import.meta.url).
  • Wraps the raw worker with a small interface:
    • postMessage
    • onMessage
    • onError
    • terminate

packages/js/src/target/node/worker.js

  • Added createWorker() for Node runtime.
  • Resolves the worker file path from import.meta.url.
  • Uses Node Worker from worker_threads.
  • Exposes the same wrapper interface as the browser implementation.

packages/js/src/wasmClient.js

  • Refactored to use createWorker() instead of constructing workers inline.
  • initWasmWorker() is now async.
  • Centralized worker message handling.
  • Improved callback resolution/rejection handling by request id.
  • Added worker error handling that rejects all pending requests and clears the callback map.

packages/js/src/safeWasmWrappers.js

  • Switched from fire-and-forget worker init to top-level await initWasmWorker().
  • Ensures the WASM worker is ready before use.

6. Node-specific WebGPU lifecycle handling

packages/js/src/target/node/webgpu.js

  • Added initWebGPU():
    • lazily creates a Vulkan-backed GPU
    • stores it on globalThis.navigator.gpu
    • caches initialization with gpuInitPromise
  • Added destroyWebGPU():
    • removes navigator.gpu
    • cleans up navigator when empty
    • resets init state
    • waits briefly to allow native teardown to settle

packages/js/src/workers/wasmWorker.js

  • Updated WASM import path to use @wasm/index.js.
  • Refactored message handling into an async handleMessage(data) function.
  • For Node:
    • dynamically imports node:worker_threads
    • initializes WebGPU
    • forwards messages via parentPort
    • calls destroyWebGPU() after handling the message
    • closes the worker port
  • For browser:
    • keeps standard worker message handling through globalThis.onmessage

This is the main runtime-side work that makes the same JS package usable in both environments.

7. React app bundling adjustments

example-apps/react-js/vite.config.js

  • Set worker.format = "es" to preserve top-level await behavior in workers.
  • Set build target to esnext.
  • Externalized several modules from the browser bundle:
    • webgpu
    • worker_threads
    • url
    • path
    • fs

This reduces browser build issues from Node-only modules leaking into the app bundle.

8. New Node example application

example-apps/console-js/package.json

  • Added a new workspace package named console-js.
  • Uses ESM ("type": "module").
  • Adds dependencies on:
    • img2num
    • sharp

example-apps/console-js/index.js

  • Added a CLI-style Node example that:
    • reads an image path from process.argv
    • uses sharp to decode it
    • converts pixels to Uint8ClampedArray
    • calls imageToSvg
    • writes output.svg

9. Workspace, docs, and linting updates

pnpm-workspace.yaml

  • Added example-apps/console-js to the workspace.
  • Added webgpu to onlyBuiltDependencies.

docs/plugins/docusaurusPluginTypeDocConfig.js

  • Updated TypeDoc entrypoint to packages/js/src/index.js.
  • Excluded dist/ from docs generation.

packages/js/tsconfig.typedoc.json

  • Expanded included source globs so Typedoc covers the new source layout.
  • Added skipLibCheck.
  • Explicitly set lib to ESNext.
  • Added typedocOptions.exclude for node_modules/ and dist/.

eslint.config.js

  • Added packages/js/dist to ignored paths.

10. Test changes

packages/js/imageToUint8ClampedArray.test.js

  • Removed the existing test file entirely.
  • Worth noting in the squash description because it reduces current JS-side automated coverage.

Shorter alternative squash message

If you want a slightly more release-oriented title:

fix(build): unify img2num browser/node JS packaging and React app build

If you want, I can also turn this into:

  1. a GitHub squash title + body formatted exactly for the UI, or
  2. a release-notes/changelog version for merging into dev.

@Ryan-Millard
Ryan-Millard merged commit 0b2467c into Ryan-Millard:dev Jun 26, 2026
10 checks passed
@Ryan-Millard

Copy link
Copy Markdown
Owner

@Prachi-Gupta2808 thank you again for the help with this!

I'm glad that we finally found a solution and that everything works now. :)

Ryan-Millard added a commit that referenced this pull request Jun 26, 2026
Update release.yml to support the post-#449 layout and fix the
dev-branch release pipeline.

- use dry-run-specific draft release tags for native, JS, and Python
- update JS/WASM pipeline to use Node 24, PNPM 11.4.0, and pnpm publish
- build WASM in a dedicated build-wasm directory with explicit CMake flags
- package JS dist and WASM artifacts separately
- add workspace allowBuilds entries required by PNPM
- clarify that C bindings are required for WASM builds in CMakeLists.txt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c/c++ Changes to C or C++ files ci core documentation example-app Updates to code related to demonstration (example) applications js/ts scripts tooling wasm

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants