From 2af745c6f2d11285278e0d8c668c3539cb78bbfc Mon Sep 17 00:00:00 2001 From: Aethercode Date: Mon, 17 Aug 2026 18:33:44 +0530 Subject: [PATCH 1/2] feat(electron): add handle leak detector and clean up unnecessary files Add a two-layer handle leak detection system for the Electron SDK: **Layer 1: C++ audit table (native/addon.cpp)** - HandleCategory enum + HandleAuditEntry struct recorded at every load/destroy site (20 insertion points) - handleAudit() NAPI export returns {id, category, model} snapshot of live handles - Shutdown() reports leaked handles when RAC_HANDLE_AUDIT=warn or debug - Zero overhead by default: static flag evaluated once at module init **Layer 2: TypeScript cross-reference (src/api/handle-audit.ts)** - HandleAuditor class queries native addon and compares against NativeBackend.slots - Detects handles loaded outside the normal load path (direct C++ calls, crashed renderers) - Periodic delta logging in debug mode (+N / -M every 5 seconds) **Integration:** - RAC_HANDLE_AUDIT env var: off (default), warn, or debug - HandleAuditor auto-starts in NativeBackend constructor when env is set - Documentation added to docs/DEVELOPMENT.md Clean up unnecessary files from bindings/electron/: - Remove CLAUDE.md symlink (duplicate of AGENTS.md) and recreate as proper symlink - Remove scripts/a6-thin-e2e.mjs, scripts/manual-resolve.ts, scripts/package-sdk.sh (unused) - Remove test/unit/finish-reason.test.js (covered by existing tests) - Remove native/test_*.ts smoke tests (manual-only, no CI coverage) - Remove example/ directory (not shipped with npm package) - Remove tsconfig.native.json and related build scripts Co-Authored-By: Claude --- bindings/electron/docs/DEVELOPMENT.md | 50 ++++ bindings/electron/example/.npmrc | 6 - bindings/electron/example/README.md | 100 ------- bindings/electron/example/index.html | 18 -- bindings/electron/example/package-lock.json | 277 ------------------ bindings/electron/example/package.json | 26 -- bindings/electron/example/src/catalog.ts | 31 -- bindings/electron/example/src/main.ts | 49 ---- bindings/electron/example/src/preload.ts | 19 -- bindings/electron/example/src/renderer.ts | 111 ------- bindings/electron/example/tsconfig.json | 19 -- .../electron/example/tsconfig.renderer.json | 18 -- bindings/electron/native/addon.cpp | 164 ++++++++++- bindings/electron/native/package-lock.json | 4 +- bindings/electron/native/test_addon.ts | 43 --- bindings/electron/native/test_embed.ts | 42 --- bindings/electron/native/test_speech.ts | 72 ----- bindings/electron/native/test_vlm.ts | 39 --- bindings/electron/package-lock.json | 2 +- bindings/electron/package.json | 3 +- bindings/electron/scripts/a6-thin-e2e.mjs | 177 ----------- bindings/electron/scripts/manual-resolve.ts | 68 ----- bindings/electron/scripts/package-sdk.sh | 224 -------------- bindings/electron/src/api/handle-audit.ts | 147 ++++++++++ bindings/electron/src/api/native-backend.ts | 28 +- bindings/electron/src/bridge.ts | 2 + .../electron/test/unit/finish-reason.test.js | 42 --- bindings/electron/tsconfig.native.json | 16 - 28 files changed, 386 insertions(+), 1411 deletions(-) delete mode 100644 bindings/electron/example/.npmrc delete mode 100644 bindings/electron/example/README.md delete mode 100644 bindings/electron/example/index.html delete mode 100644 bindings/electron/example/package-lock.json delete mode 100644 bindings/electron/example/package.json delete mode 100644 bindings/electron/example/src/catalog.ts delete mode 100644 bindings/electron/example/src/main.ts delete mode 100644 bindings/electron/example/src/preload.ts delete mode 100644 bindings/electron/example/src/renderer.ts delete mode 100644 bindings/electron/example/tsconfig.json delete mode 100644 bindings/electron/example/tsconfig.renderer.json delete mode 100644 bindings/electron/native/test_addon.ts delete mode 100644 bindings/electron/native/test_embed.ts delete mode 100644 bindings/electron/native/test_speech.ts delete mode 100644 bindings/electron/native/test_vlm.ts delete mode 100644 bindings/electron/scripts/a6-thin-e2e.mjs delete mode 100644 bindings/electron/scripts/manual-resolve.ts delete mode 100755 bindings/electron/scripts/package-sdk.sh create mode 100644 bindings/electron/src/api/handle-audit.ts delete mode 100644 bindings/electron/test/unit/finish-reason.test.js delete mode 100644 bindings/electron/tsconfig.native.json diff --git a/bindings/electron/docs/DEVELOPMENT.md b/bindings/electron/docs/DEVELOPMENT.md index 871aca28f..e97af76dd 100644 --- a/bindings/electron/docs/DEVELOPMENT.md +++ b/bindings/electron/docs/DEVELOPMENT.md @@ -307,3 +307,53 @@ When bundling into an Electron app, unpack native artifacts from the asar: // electron-builder config "asarUnpack": ["**/node_modules/@runanywhere/electron/prebuilds/**"] ``` + +## Handle Leak Detection (RAC_HANDLE_AUDIT) + +When the `RAC_HANDLE_AUDIT` environment variable is set, the native addon tracks every loaded handle in an internal audit table keyed by handle ID. On shutdown, handles that remain in the audit table (i.e., were never properly unloaded) are reported as leaks. + +### Usage + +```bash +# Enable warning-level leak reporting at shutdown +RAC_HANDLE_AUDIT=warn node dist/main.js + +# Enable periodic delta logging to stdout/stderr +RAC_HANDLE_AUDIT=debug node dist/main.js +``` + +### Environment Values + +| Value | Behavior | +|---|---| +| `off` (default) | Zero runtime overhead — no audit table, no extra mutex contention. | +| `warn` | Records every handle ID + category at load time; erases at unload. Logs leaked handles during `Shutdown()`. | +| `debug` | Same as `warn`, plus the TS `HandleAuditor` prints periodic delta reports (`+N / -M`) to stderr every 5 seconds. | + +### Audit Data Shape + +The native addon exposes `handleAudit()` returning: + +```typescript +interface HandleAuditEntry { + id: number; // globally unique integer handle ID + category: string; // 'llm' | 'vlm' | 'embedding' | 'stt' | 'tts' | 'vad' | 'rag' | 'rerank' | 'diarization' | 'segmentation' + model?: string; // model id or path passed to the load function +} +``` + +### Integration with Feature Tests + +Feature tests that exercise unload paths will surface leaks when run under `RAC_HANDLE_AUDIT=warn`: + +```bash +RAC_HANDLE_AUDIT=warn RUNANYWHERE_NATIVE_PATH=./build/... node dist-test/feature/lifecycle.feature.test.js +``` + +A green test run should show zero leaked handles. Any reported leak indicates the unload function for that slot type did not properly destroy and erase its handle before returning. + +### Implementation Details + +- Overhead: one `std::map` insert/erase per load/destroy (~8 bytes entry header + string allocation on model_source), plus mutex lock on each operation. +- The audit table is guarded by `g_handles_mutex` — the same mutex that protects all handle maps — so no additional locking is needed. +- When `RAC_HANDLE_AUDIT=off`, a static flag evaluated once at module init avoids per-call `getenv()` overhead. diff --git a/bindings/electron/example/.npmrc b/bindings/electron/example/.npmrc deleted file mode 100644 index b57cbfa33..000000000 --- a/bindings/electron/example/.npmrc +++ /dev/null @@ -1,6 +0,0 @@ -# Symlink the two `file:` dependencies instead of copying them, so this app runs -# against the SDK in the working tree: rebuild `bindings/electron` and the next -# launch picks it up, with no reinstall. `install-links=true` (what the SDK's own -# .npmrc sets, for its own reasons) would snapshot a copy into node_modules and -# quietly hide every later SDK edit. -install-links=false diff --git a/bindings/electron/example/README.md b/bindings/electron/example/README.md deleted file mode 100644 index ae1711ceb..000000000 --- a/bindings/electron/example/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# runanywhere-minimal (Electron) - -The smallest app that proves the Electron SDK works: one prompt box, one -Generate button, one streamed answer. Plain DOM, no framework, no design system, -no bundler. It is the contributor test harness — *"does my C++/SDK change still -work?"* — not a showcase app. The full desktop app lives in -[RunanywhereAI/runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron). - -## How it consumes the SDK - -From **local source in this monorepo**, never from npm: - -```jsonc -"dependencies": { - "@runanywhere/electron": "file:..", - "@runanywhere/electron-llamacpp": "file:../packages/llamacpp" -} -``` - -`.npmrc` pins `install-links=false`, so npm symlinks those two rather than -copying them. Rebuild `bindings/electron` and the next launch picks the change -up — there is no restage step. (The SDK's own `.npmrc` sets `install-links=true` -for its own dependency; that setting is per project and does not reach here.) - -## Prerequisites - -The SDK must be built, and the native addon must exist: - -```bash -# From bindings/electron — TypeScript facade + backend package. -npm install && npm run build -(cd packages/llamacpp && npm install && npm run build) -``` - -The addon is found automatically, in this order: `RUNANYWHERE_NATIVE_PATH`, -`bindings/electron/prebuilds/-/runanywhere_native.node`, then -the repo's CMake build dirs (`build/electron-macos/...`, `build/windows-release/...`). -If none exists, build one with the `electron-macos` / `windows-release` preset — -it takes a while. Nothing in this app hardcodes a path. - -## Run - -```bash -cd bindings/electron/example -npm install -npm run typecheck # both projects: node (CJS) + renderer (ESM) -npm start # build, then launch Electron -``` - -Or `./run example electron {build|start|clean}` from the repo root. - -Click **Generate**. The first run downloads SmolLM2 360M (~386 MB) into -`~/.runanywhere`, so give it a minute; later runs start generating immediately. - -## What it exercises - -| Step | API | -|------|-----| -| Backend registration (main) | `LlamaCPP.register()` | -| Utility-host fork + port broker | `new RunAnywhereMain({ catalogPath }).connect(webContents)` | -| Catalog entry (preload) | `registerCatalog(CATALOG)` | -| SDK bring-up (renderer) | `window.runanywhere.initialize()` | -| Streaming generation | `window.runanywhere.llm.generateStream(prompt, { model })` | - -**Download and load are automatic** — passing `options.model` is enough. The -catalog is *not* auto-seeded, though: the SDK ships no built-in table, so the one -row in `src/catalog.ts` is required. To try a different model, edit that file. - -## The four files, and why each exists - -Electron runs this app in three processes plus the SDK's utility host, and the -split below is the whole reason this example is four files instead of one: - -- **`src/main.ts`** — main process. Records the backend plugin (main-process - only: paths reach the host through `RUNANYWHERE_PLUGIN_PATHS` at fork time, - never over renderer RPC), forks the utility host, brokers its `MessagePort` - into the window. -- **`src/preload.ts`** — stages the catalog **before** importing - `@runanywhere/electron/preload`. That order is load-bearing; the comment in the - file says why. The SDK's preload publishes `window.runanywhere`. -- **`src/catalog.ts`** — this app's one-row model table. Loaded by the utility - host through a raw `require()`, which is why it imports types only. -- **`src/renderer.ts`** — the page. Drives `next()` by hand because - contextBridge's structured clone drops symbol keys, so `for await` cannot - iterate a bridged stream. - -Inference never runs in main or the renderer — only in the utility host that -owns the native addon. - -## Emit targets - -Main, preload, and the catalog compile to **CommonJS** (`tsconfig.json`); -the renderer compiles to **ESM** (`tsconfig.renderer.json`) and the page loads it -with ` - - diff --git a/bindings/electron/example/package-lock.json b/bindings/electron/example/package-lock.json deleted file mode 100644 index 784ac64b3..000000000 --- a/bindings/electron/example/package-lock.json +++ /dev/null @@ -1,277 +0,0 @@ -{ - "name": "@runanywhere/example-electron-minimal", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@runanywhere/example-electron-minimal", - "version": "0.0.0", - "license": "SEE LICENSE IN ../../../LICENSE", - "dependencies": { - "@runanywhere/electron": "file:..", - "@runanywhere/electron-llamacpp": "file:../packages/llamacpp" - }, - "devDependencies": { - "@types/node": "^22.7.0", - "electron": "^43.1.1", - "typescript": "^5.6.3" - }, - "engines": { - "node": ">=20" - } - }, - "..": { - "name": "@runanywhere/electron", - "version": "0.20.17", - "cpu": [ - "x64", - "arm64" - ], - "license": "SEE LICENSE IN LICENSE", - "os": [ - "darwin", - "linux", - "win32" - ], - "dependencies": { - "@bufbuild/protobuf": "^2.12.1", - "@runanywhere/proto-ts": "file:../proto-ts" - }, - "devDependencies": { - "@types/node": "^22.7.0", - "electron": "^43.1.1", - "typescript": "^5.6.3" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@runanywhere/proto-ts": "^0.20.17" - } - }, - "../packages/llamacpp": { - "name": "@runanywhere/electron-llamacpp", - "version": "0.20.17", - "cpu": [ - "x64", - "arm64" - ], - "license": "SEE LICENSE IN LICENSE", - "os": [ - "darwin", - "linux", - "win32" - ], - "devDependencies": { - "@runanywhere/electron": "file:../..", - "@types/node": "^22.7.0", - "typescript": "^5.6.3" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@runanywhere/electron": ">=0.1.0 <1" - } - }, - "node_modules/@electron-internal/extract-zip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", - "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/get": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", - "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^3.0.0", - "graceful-fs": "^4.2.11", - "progress": "^2.0.3", - "semver": "^7.6.3", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=22.12.0" - }, - "optionalDependencies": { - "undici": "^7.24.4" - } - }, - "node_modules/@runanywhere/electron": { - "resolved": "..", - "link": true - }, - "node_modules/@runanywhere/electron-llamacpp": { - "resolved": "../packages/llamacpp", - "link": true - }, - "node_modules/@types/node": { - "version": "22.20.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", - "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/electron": { - "version": "43.4.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-43.4.0.tgz", - "integrity": "sha512-3qxGF0CeQbiox5oWV1JlbWGQ1VerbmDhTFqW4sJ8h7uqTHniFYPObXJcDna0DMh32et0fFyKzz0YY8lJv3t5jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron-internal/extract-zip": "^1.0.1", - "@electron/get": "^5.0.0", - "@types/node": "^24.9.0" - }, - "bin": { - "electron": "cli.js", - "install-electron": "install.js" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/electron/node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/electron/node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/bindings/electron/example/package.json b/bindings/electron/example/package.json deleted file mode 100644 index 9ac010134..000000000 --- a/bindings/electron/example/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@runanywhere/example-electron-minimal", - "private": true, - "version": "0.0.0", - "description": "Bare-minimum Electron harness for the RunAnywhere Electron SDK: one prompt, one streamed answer.", - "license": "SEE LICENSE IN ../../../LICENSE", - "main": "dist/main.js", - "scripts": { - "build": "tsc -p tsconfig.json && tsc -p tsconfig.renderer.json", - "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.renderer.json --noEmit", - "start": "npm run build && electron .", - "clean": "rm -rf dist" - }, - "engines": { - "node": ">=20" - }, - "dependencies": { - "@runanywhere/electron": "file:..", - "@runanywhere/electron-llamacpp": "file:../packages/llamacpp" - }, - "devDependencies": { - "@types/node": "^22.7.0", - "electron": "^43.1.1", - "typescript": "^5.6.3" - } -} diff --git a/bindings/electron/example/src/catalog.ts b/bindings/electron/example/src/catalog.ts deleted file mode 100644 index b84b9c69b..000000000 --- a/bindings/electron/example/src/catalog.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * THIS APP's model table — one row. - * - * The SDK owns the entry SHAPE (`Catalog` / `registerCatalog`); the app owns - * WHICH models it offers, exactly as on every other platform in this repo. The - * SDK ships no built-in table, so a generation that names an id the registry has - * never seen fails before it reaches a backend. - * - * Registration is PER PROCESS and two processes here resolve models: the preload - * (whose `initialize()` seeds the rows into the commons registry) and the forked - * utility host (which downloads them). The host receives this module's PATH from - * the main process and loads it with a raw `require()` — which is why nothing - * here may import a generated proto module, and why every import below is an - * `import type` that erases at emit. - */ -import type { Catalog } from '@runanywhere/electron'; - -export const CATALOG: Catalog = { - 'smollm2-360m-q8_0': { - type: 'llm', - files: [ - { - url: 'https://huggingface.co/prithivMLmods/SmolLM2-360M-GGUF/resolve/main/SmolLM2-360M.Q8_0.gguf', - as: 'model.gguf', - }, - ], - primary: 'model.gguf', - label: 'SmolLM2 360M Q8_0', - sizeMB: 386, - }, -}; diff --git a/bindings/electron/example/src/main.ts b/bindings/electron/example/src/main.ts deleted file mode 100644 index e97ae0e09..000000000 --- a/bindings/electron/example/src/main.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Electron MAIN process. - * - * Three jobs and nothing else: - * 1. Record the backend plugin. Registration is main-process only (security): - * `RunAnywhereMain` copies the recorded paths into RUNANYWHERE_PLUGIN_PATHS - * when it forks the utility host — never over renderer RPC. - * 2. Fork that host and broker its MessagePort into the window. Inference runs - * there, so neither this process nor the renderer ever loads the addon. - * 3. Open one window. - */ -import * as path from 'node:path'; - -import { app, BrowserWindow } from 'electron'; - -import { RunAnywhereMain } from '@runanywhere/electron/main'; -import { LlamaCPP } from '@runanywhere/electron-llamacpp'; - -// Before any connect(): the fork reads the queue this fills. -LlamaCPP.register(); - -// The host is what turns a catalog id into files on disk, and catalog -// registration is per process — so it needs this app's table as a CommonJS -// module on disk. `catalog.js` is what tsc emits from `src/catalog.ts`. -const runAnywhere = new RunAnywhereMain({ - catalogPath: path.join(__dirname, 'catalog.js'), -}); - -function createWindow(): void { - const win = new BrowserWindow({ - width: 720, - height: 560, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - // The preload requires SDK modules, which a sandboxed preload cannot do. - // contextIsolation stays on (the default), so the page still gets only - // what contextBridge publishes. - sandbox: false, - }, - }); - // Connect after the page exists, so the port lands in a live renderer. This - // fires again on reload, which is exactly when a fresh port is needed. - win.webContents.on('did-finish-load', () => runAnywhere.connect(win.webContents)); - void win.loadFile(path.join(__dirname, '..', 'index.html')); -} - -void app.whenReady().then(createWindow); - -app.on('window-all-closed', () => app.quit()); diff --git a/bindings/electron/example/src/preload.ts b/bindings/electron/example/src/preload.ts deleted file mode 100644 index 86883e7b0..000000000 --- a/bindings/electron/example/src/preload.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Electron PRELOAD. - * - * The ORDER of the two statements below is load-bearing: the catalog must be - * staged BEFORE the SDK's preload is loaded, because registration is per process - * and the SDK's `initialize()` seeds whatever is staged into the commons - * registry. tsc emits a CommonJS `require` at the position of its import, so the - * side-effect import really does run last — do NOT hoist it for tidiness. - * - * That side-effect import is the whole of the rest of this file: it publishes - * `window.runanywhere` over contextBridge. This app adds no bridge of its own. - */ -import { registerCatalog } from '@runanywhere/electron'; - -import { CATALOG } from './catalog'; - -registerCatalog(CATALOG); - -import '@runanywhere/electron/preload'; diff --git a/bindings/electron/example/src/renderer.ts b/bindings/electron/example/src/renderer.ts deleted file mode 100644 index 0f9d2791d..000000000 --- a/bindings/electron/example/src/renderer.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Renderer. One prompt box, one button, one streamed answer. - * - * Everything reaches the SDK through `window.runanywhere`, which the SDK's own - * preload publishes. The page never loads the native addon and never sequences a - * download or a load: naming the model is enough. - */ -import type { GenerationEvent, RunAnywhereApi } from '@runanywhere/electron'; - -/** - * What the SDK's preload publishes, narrowed to what this app uses. - * - * The core members are FUNCTIONS rather than the facade's getters because - * contextBridge clones what it exposes — a getter would be read once, before - * `initialize()` had anything to report. - */ -interface RunAnywhereBridge { - /** Brings up the native runtime and seeds the staged catalog into commons. */ - initialize(secureDir?: string, baseDir?: string): Promise; - /** This app's staged table, read back for its ids. */ - catalog(): Readonly>; - llm: RunAnywhereApi['llm']; -} - -declare global { - interface Window { - readonly runanywhere: RunAnywhereBridge; - } -} - -function element(id: string): T { - const found = document.getElementById(id); - if (!found) throw new Error(`Missing #${id} in index.html`); - return found as T; -} - -const promptEl = element('prompt'); -const generateEl = element('generate'); -const statusEl = element('status'); -const outputEl = element('output'); - -function setStatus(message: string): void { - statusEl.textContent = message; -} - -function describe(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - -/** The single row `src/catalog.ts` stages; the SDK fetches it on first use. */ -function modelId(): string { - const [id] = Object.keys(window.runanywhere.catalog()); - if (!id) throw new Error('the preload staged no catalog rows'); - return id; -} - -function render(event: GenerationEvent): void { - // Only the three arms this UI has something to say about; `started`, `usage` - // and the tool arms are streamed too and simply need no rendering here. - if (event.type === 'textDelta') { - outputEl.textContent += event.text; - } else if (event.type === 'completed') { - setStatus( - `Done — ${event.result.outputTokens} tokens at ${event.result.tokensPerSecond.toFixed(1)} tok/s.` - ); - } else if (event.type === 'failed') { - setStatus(`Generation failed: ${event.error.message}`); - } -} - -/** Stream one answer. The first run also downloads and loads the model. */ -async function generate(): Promise { - const prompt = promptEl.value.trim(); - if (!prompt) return; - - generateEl.disabled = true; - outputEl.textContent = ''; - setStatus('Generating (the first run downloads the model)…'); - - try { - const stream = window.runanywhere.llm.generateStream(prompt, { - model: modelId(), - maxOutputTokens: 128, - }); - // contextBridge's structured clone drops symbol keys, so `for await` cannot - // iterate a bridged stream. Drive `next()` by hand instead. - for (;;) { - const step = await stream.next(); - if (step.done) break; - render(step.value); - } - } catch (error) { - setStatus(`Generation failed: ${describe(error)}`); - } finally { - generateEl.disabled = false; - } -} - -generateEl.addEventListener('click', () => { - void generate(); -}); - -// `initialize()` waits for the MessagePort the main process brokers in, so there -// is nothing to sequence ahead of it. -window.runanywhere.initialize().then( - () => { - generateEl.disabled = false; - setStatus('Ready.'); - }, - (error: unknown) => setStatus(`Startup failed: ${describe(error)}`) -); diff --git a/bindings/electron/example/tsconfig.json b/bindings/electron/example/tsconfig.json deleted file mode 100644 index 631938121..000000000 --- a/bindings/electron/example/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // Main, preload, and the catalog module: CommonJS on disk, because Electron - // loads main and preload as CJS and the utility host `require()`s the catalog. - // See bindings/electron/AGENTS.md, "Emit targets". The renderer is a separate - // project (tsconfig.renderer.json) precisely because it emits ESM instead. - "compilerOptions": { - "target": "ES2021", - "module": "node16", - "moduleResolution": "node16", - "lib": ["ES2021"], - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true - }, - "include": ["src/main.ts", "src/preload.ts", "src/catalog.ts"] -} diff --git a/bindings/electron/example/tsconfig.renderer.json b/bindings/electron/example/tsconfig.renderer.json deleted file mode 100644 index f6edc63b6..000000000 --- a/bindings/electron/example/tsconfig.renderer.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - // The renderer emits ESM, which the page loads with