From 3009df67327e73ad18ac003ea3050e29c5795c54 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Fri, 26 Dec 2025 18:44:52 +0000 Subject: [PATCH 01/37] fix(.gitignore): add .npmrc to ignore list to prevent committing npm tokens --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 07a8410..e355f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ yarn-error.log* .env.local .env.*.local +# npm tokens - NEVER commit these! +.npmrc + # Temporary files *.tmp *.temp From db7a1e39db97105e377023b6df0b9d16184d66e6 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Fri, 26 Dec 2025 18:46:41 +0000 Subject: [PATCH 02/37] fix(ci): update publish conditions and refine package publishing steps --- .github/workflows/ci.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 306957b..c0efb76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: publish: needs: build runs-on: ubuntu-latest - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 @@ -53,9 +53,14 @@ jobs: run: npm ci - name: Build packages - run: npm run build + run: npm run build:packages + + - name: Publish @computekit/core + run: cd packages/core && npm publish --access public || echo "Already published or failed" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Publish to npm - run: npm publish --workspaces --access public + - name: Publish @computekit/react + run: cd packages/react && npm publish --access public || echo "Already published or failed" env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 45100c07f4d864c66aeb5c7c829cb830b025abc7 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Fri, 26 Dec 2025 18:51:35 +0000 Subject: [PATCH 03/37] chore: trigger publish workflow From 9e5acea7379290ed04a82eaeeea0fb75fde465b3 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Fri, 26 Dec 2025 18:58:31 +0000 Subject: [PATCH 04/37] ci: add dedicated publish workflow --- .github/workflows/publish.yml | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2fa05bf --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,37 @@ +name: Publish to npm + +on: + workflow_dispatch: + push: + branches: [main] + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 22.x + uses: actions/setup-node@v4 + with: + node-version: 22.x + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm ci + + - name: Build packages + run: npm run build:packages + + - name: Publish @computekit/core + run: cd packages/core && npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + continue-on-error: true + + - name: Publish @computekit/react + run: cd packages/react && npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + continue-on-error: true From 88b920c3f618e44321a7b0acb64f5a2178330b6f Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Fri, 26 Dec 2025 23:10:02 +0000 Subject: [PATCH 05/37] refactor: remove vanilla demo example and associated files - Deleted the `package.json`, `vite.config.ts`, and `src/main.js` files from the `examples/vanilla-demo` directory. - Updated the main `package.json` to remove the build and dev commands related to the vanilla demo. - Removed the `wasm` package and its associated files, including `asconfig.json`, `index.ts`, and `package.json`. - Added a new `blur.ts` file in the `compute` directory with optimized blur functions. - Updated the `react` package to include optional peer dependencies for `@types/react`. - Made minor adjustments to the `react` package's `index.tsx` and `tsconfig.json` for improved TypeScript support. - Added comprehensive README files for both `@computekit/core` and `@computekit/react` packages. --- README.md | 7 +- .../wasm/assembly/index.ts => compute/blur.ts | 0 compute/index.ts | 1 + examples/react-demo/public/compute.d.ts | 12 + examples/react-demo/public/compute.js | 6 + examples/react-demo/public/direct-test.html | 96 ----- examples/react-demo/public/minimal-test.html | 154 -------- examples/react-demo/public/test-worker.js | 72 ---- examples/react-demo/public/test.html | 149 -------- examples/vanilla-demo/index.html | 350 ------------------ examples/vanilla-demo/package.json | 17 - examples/vanilla-demo/src/main.js | 156 -------- examples/vanilla-demo/vite.config.ts | 14 - package.json | 3 +- packages/core/README.md | 220 +++++++++++ packages/react/README.md | 280 ++++++++++++++ packages/react/package.json | 8 +- packages/react/src/index.tsx | 4 +- packages/react/tsconfig.json | 3 +- packages/wasm/asconfig.json | 24 -- packages/wasm/package.json | 34 -- 21 files changed, 535 insertions(+), 1075 deletions(-) rename packages/wasm/assembly/index.ts => compute/blur.ts (100%) delete mode 100644 examples/react-demo/public/direct-test.html delete mode 100644 examples/react-demo/public/minimal-test.html delete mode 100644 examples/react-demo/public/test-worker.js delete mode 100644 examples/react-demo/public/test.html delete mode 100644 examples/vanilla-demo/index.html delete mode 100644 examples/vanilla-demo/package.json delete mode 100644 examples/vanilla-demo/src/main.js delete mode 100644 examples/vanilla-demo/vite.config.ts create mode 100644 packages/core/README.md create mode 100644 packages/react/README.md delete mode 100644 packages/wasm/asconfig.json delete mode 100644 packages/wasm/package.json diff --git a/README.md b/README.md index 30f3f7e..b69f141 100644 --- a/README.md +++ b/README.md @@ -500,13 +500,14 @@ computekit/ │ └── package.json │ ├── compute/ # AssemblyScript functions +│ ├── blur.ts │ ├── fibonacci.ts │ ├── mandelbrot.ts -│ └── matrix.ts +│ ├── matrix.ts +│ └── sum.ts │ ├── examples/ -│ ├── react-demo/ # React example app -│ └── vanilla-demo/ # Vanilla JS example +│ └── react-demo/ # React example app │ └── docs/ # Documentation ``` diff --git a/packages/wasm/assembly/index.ts b/compute/blur.ts similarity index 100% rename from packages/wasm/assembly/index.ts rename to compute/blur.ts diff --git a/compute/index.ts b/compute/index.ts index 9bfc9f1..8a25f04 100644 --- a/compute/index.ts +++ b/compute/index.ts @@ -15,3 +15,4 @@ export { vectorMagnitude, vectorNormalize, } from './matrix'; +export { getBufferPtr, blurImage } from './blur'; diff --git a/examples/react-demo/public/compute.d.ts b/examples/react-demo/public/compute.d.ts index 93c2d66..75e1b04 100644 --- a/examples/react-demo/public/compute.d.ts +++ b/examples/react-demo/public/compute.d.ts @@ -109,3 +109,15 @@ export declare function vectorMagnitude(v: Float64Array): number; * @returns `~lib/typedarray/Float64Array` */ export declare function vectorNormalize(v: Float64Array): Float64Array; +/** + * compute/blur/getBufferPtr + * @returns `usize` + */ +export declare function getBufferPtr(): number; +/** + * compute/blur/blurImage + * @param width `i32` + * @param height `i32` + * @param passes `i32` + */ +export declare function blurImage(width: number, height: number, passes: number): void; diff --git a/examples/react-demo/public/compute.js b/examples/react-demo/public/compute.js index d88d0d7..b0a3f29 100644 --- a/examples/react-demo/public/compute.js +++ b/examples/react-demo/public/compute.js @@ -99,6 +99,10 @@ async function instantiate(module, imports = {}) { v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); return __liftTypedArray(Float64Array, exports.vectorNormalize(v) >>> 0); }, + getBufferPtr() { + // compute/blur/getBufferPtr() => usize + return exports.getBufferPtr() >>> 0; + }, }, exports); function __liftString(pointer) { if (!pointer) return null; @@ -188,6 +192,8 @@ export const { dotProduct, vectorMagnitude, vectorNormalize, + getBufferPtr, + blurImage, } = await (async url => instantiate( await (async () => { const isNodeOrBun = typeof process != "undefined" && process.versions != null && (process.versions.node != null || process.versions.bun != null); diff --git a/examples/react-demo/public/direct-test.html b/examples/react-demo/public/direct-test.html deleted file mode 100644 index b223408..0000000 --- a/examples/react-demo/public/direct-test.html +++ /dev/null @@ -1,96 +0,0 @@ - - - - ComputeKit Direct Test - - - -

ComputeKit Direct Test

-

Open browser DevTools (F12) to see console logs.

- -
- -
- -

Output:

-
Click "Run Test" to begin...
- - - - diff --git a/examples/react-demo/public/minimal-test.html b/examples/react-demo/public/minimal-test.html deleted file mode 100644 index 2600914..0000000 --- a/examples/react-demo/public/minimal-test.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - Minimal Worker Test - - - -

Minimal Worker Test

-
- - - diff --git a/examples/react-demo/public/test-worker.js b/examples/react-demo/public/test-worker.js deleted file mode 100644 index 1c04a35..0000000 --- a/examples/react-demo/public/test-worker.js +++ /dev/null @@ -1,72 +0,0 @@ -// Test Worker - standalone version of the generated worker code -console.log('[Test Worker] Starting...'); - -const functions = { - double: (n) => n * 2, - fibonacci: (n) => { - if (n <= 1) return String(n); - let a = BigInt(0); - let b = BigInt(1); - for (let i = 2; i <= n; i++) { - const temp = a + b; - a = b; - b = temp; - } - return b.toString(); - }, -}; - -console.log('[Test Worker] Functions loaded:', Object.keys(functions)); - -self.onmessage = async function (e) { - console.log('[Test Worker] Received message:', e.data); - const { id, type, payload, timestamp } = e.data; - - if (type === 'execute') { - const { functionName, input } = payload; - console.log('[Test Worker] Executing:', functionName, 'with input:', input); - const fn = functions[functionName]; - - if (!fn) { - self.postMessage({ - id, - type: 'error', - payload: { message: 'Function not found: ' + functionName }, - timestamp: Date.now(), - }); - return; - } - - const startTime = performance.now(); - - try { - console.log('[Test Worker] Calling function...'); - const result = await fn(input); - console.log('[Test Worker] Function returned:', typeof result, result); - const duration = performance.now() - startTime; - - self.postMessage({ - id, - type: 'result', - payload: { data: result, duration }, - timestamp: Date.now(), - }); - console.log('[Test Worker] Result sent'); - } catch (err) { - console.error('[Test Worker] Error:', err); - self.postMessage({ - id, - type: 'error', - payload: { - message: err.message || 'Unknown error', - stack: err.stack, - }, - timestamp: Date.now(), - }); - } - } -}; - -// Signal ready -self.postMessage({ type: 'ready', timestamp: Date.now() }); -console.log('[Test Worker] Ready signal sent'); diff --git a/examples/react-demo/public/test.html b/examples/react-demo/public/test.html deleted file mode 100644 index 41da4ba..0000000 --- a/examples/react-demo/public/test.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - Worker Test - - - -

Worker Test

- - - - -
Click a button to test...
- - - - diff --git a/examples/vanilla-demo/index.html b/examples/vanilla-demo/index.html deleted file mode 100644 index b5e4bd3..0000000 --- a/examples/vanilla-demo/index.html +++ /dev/null @@ -1,350 +0,0 @@ - - - - - - ComputeKit Vanilla Demo - - - -
-
- - - - - ComputeKit Vanilla Demo -
- Ready -
- -
-

ComputeKit - Vanilla JavaScript

-

No framework needed. Just import and compute.

- -
-

📦 Basic Usage

-

Import ComputeKit, register a function, and run it asynchronously.

- -
- import { ComputeKit } - from - '@computekit/core';

- // Create instance
- const kit = new - ComputeKit();

- // Register compute function
- kit.register('fibonacci', (n) => {
-   if (n <= - 1) return n;
-   let a = 0, - b = 1;
-   for (let i - = 2; i <= n; i++) [a, b] = [b, a + b];
-   return b;
- });

- // Run in Web Worker (non-blocking!)
- const result = - await kit.run('fibonacci', 50);
- console.log(result); - // 12586269025 -
- -
- - -
-
- Click the button to compute... -
-
- -
-

⚡ Blocking vs Non-Blocking

-

Compare main thread execution (freezes UI) vs Worker execution (smooth).

- -
-
-

🔴 Main Thread (Blocking)

-
-
- UI will freeze -
-
-

🟢 Web Worker (Async)

-
-
- UI stays responsive -
-
- -
- - - -
-
- -
-

📊 Array Processing

-

Process large arrays without blocking the main thread.

- -
- - -
-
- Click to sum a large array... -
-
-
- - - - diff --git a/examples/vanilla-demo/package.json b/examples/vanilla-demo/package.json deleted file mode 100644 index 66f1d86..0000000 --- a/examples/vanilla-demo/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "vanilla-demo", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "@computekit/core": "*" - }, - "devDependencies": { - "vite": "^5.0.10" - } -} diff --git a/examples/vanilla-demo/src/main.js b/examples/vanilla-demo/src/main.js deleted file mode 100644 index a7641e2..0000000 --- a/examples/vanilla-demo/src/main.js +++ /dev/null @@ -1,156 +0,0 @@ -/** - * ComputeKit Vanilla JavaScript Demo - */ - -import { ComputeKit } from '@computekit/core'; - -// Initialize ComputeKit -const kit = new ComputeKit({ - maxWorkers: 4, - debug: true, -}); - -// Register compute functions -kit.register('fibonacci', (n) => { - if (n <= 1) return BigInt(n); - let a = 0n; - let b = 1n; - for (let i = 2; i <= n; i++) { - [a, b] = [b, a + b]; - } - return b.toString(); -}); - -kit.register('heavyTask', (iterations) => { - // Simulate CPU-intensive work - let result = 0; - for (let i = 0; i < iterations; i++) { - result += Math.sqrt(i) * Math.sin(i); - } - return result; -}); - -kit.register('sum', (arr) => { - let total = 0; - for (let i = 0; i < arr.length; i++) { - total += arr[i]; - } - return total; -}); - -// Helper functions -function setStatus(elementId, text, isLoading = false) { - const el = document.getElementById(elementId); - if (isLoading) { - el.innerHTML = ' ' + text + ''; - } else { - el.textContent = text; - } -} - -function formatNumber(num) { - return new Intl.NumberFormat().format(num); -} - -// Fibonacci Demo -document.getElementById('btn-fib').addEventListener('click', async () => { - const output = document.getElementById('fib-output'); - setStatus('fib-status', 'Computing...', true); - - const start = performance.now(); - - try { - const result = await kit.run('fibonacci', 50); - const duration = performance.now() - start; - - output.innerHTML = `Fibonacci(50) = ${result} -Time: ${duration.toFixed(2)}ms`; - setStatus('fib-status', '✓ Done'); - } catch (err) { - output.innerHTML = `Error: ${err.message}`; - setStatus('fib-status', '✗ Error'); - } - - setTimeout(() => setStatus('fib-status', ''), 2000); -}); - -// Blocking vs Async Comparison -const HEAVY_ITERATIONS = 50000000; - -function heavyTaskBlocking(iterations) { - let result = 0; - for (let i = 0; i < iterations; i++) { - result += Math.sqrt(i) * Math.sin(i); - } - return result; -} - -document.getElementById('btn-blocking').addEventListener('click', () => { - setStatus('compare-status', 'Running (UI will freeze)...', true); - document.getElementById('blocking-result').textContent = '...'; - - // Small delay to show the status before freezing - setTimeout(() => { - const start = performance.now(); - heavyTaskBlocking(HEAVY_ITERATIONS); - const duration = performance.now() - start; - - document.getElementById('blocking-result').textContent = `${duration.toFixed(0)}ms`; - setStatus('compare-status', ''); - }, 50); -}); - -document.getElementById('btn-async').addEventListener('click', async () => { - setStatus('compare-status', 'Running in Worker...', true); - document.getElementById('async-result').textContent = '...'; - - const start = performance.now(); - await kit.run('heavyTask', HEAVY_ITERATIONS); - const duration = performance.now() - start; - - document.getElementById('async-result').textContent = `${duration.toFixed(0)}ms`; - setStatus('compare-status', ''); -}); - -// Array Sum Demo -document.getElementById('btn-sum').addEventListener('click', async () => { - const output = document.getElementById('sum-output'); - setStatus('sum-status', 'Generating array...', true); - - // Generate large array - const size = 10_000_000; - const arr = new Array(size); - for (let i = 0; i < size; i++) { - arr[i] = Math.floor(Math.random() * 100); - } - - setStatus('sum-status', 'Computing sum...', true); - - const start = performance.now(); - - try { - const result = await kit.run('sum', arr); - const duration = performance.now() - start; - - output.innerHTML = `Sum of ${formatNumber(size)} random numbers: -${formatNumber(result)} -Time: ${duration.toFixed(0)}ms -Throughput: ${formatNumber(Math.round((size / duration) * 1000))} ops/sec`; - setStatus('sum-status', '✓ Done'); - } catch (err) { - output.innerHTML = `Error: ${err.message}`; - setStatus('sum-status', '✗ Error'); - } - - setTimeout(() => setStatus('sum-status', ''), 2000); -}); - -// Update global status -const globalStatus = document.getElementById('status'); -kit.on?.('task:start', () => { - globalStatus.innerHTML = ' Working...'; -}); - -// Log ready state -console.log('ComputeKit Demo Ready!'); -console.log('Pool Stats:', kit.getStats()); diff --git a/examples/vanilla-demo/vite.config.ts b/examples/vanilla-demo/vite.config.ts deleted file mode 100644 index d366faa..0000000 --- a/examples/vanilla-demo/vite.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'vite'; - -export default defineConfig({ - server: { - port: 3001, - headers: { - 'Cross-Origin-Opener-Policy': 'same-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', - }, - }, - optimizeDeps: { - exclude: ['@computekit/core'], - }, -}); diff --git a/package.json b/package.json index 6c3da92..a47d421 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,9 @@ "build:wasm": "asc compute/index.ts --outFile examples/react-demo/public/compute.wasm --bindings esm --optimize", "build:core": "npm run build -w @computekit/core", "build:react": "npm run build -w @computekit/react", - "build:examples": "npm run build -w examples/react-demo && npm run build -w examples/vanilla-demo", + "build:examples": "npm run build -w examples/react-demo", "build:packages": "npm run build:core && npm run build:react", "dev": "npm run dev -w examples/react-demo", - "dev:vanilla": "npm run dev -w examples/vanilla-demo", "test": "vitest", "lint": "eslint packages --ext .ts,.tsx", "clean": "rimraf packages/*/dist examples/*/dist", diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..e2697c6 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,220 @@ +# @computekit/core + +The core library for ComputeKit - run heavy computations in Web Workers with WASM support. + +## Installation + +```bash +npm install @computekit/core +``` + +## Quick Start + +```typescript +import { ComputeKit } from '@computekit/core'; + +// Create an instance +const kit = new ComputeKit(); + +// Register a compute function +kit.register('fibonacci', (n: number) => { + let a = 0, + b = 1; + for (let i = 0; i < n; i++) { + [a, b] = [b, a + b]; + } + return a; +}); + +// Run it (non-blocking!) +const result = await kit.run('fibonacci', 50); +console.log(result); // 12586269025 +``` + +## API Reference + +### `new ComputeKit(options?)` + +Create a new ComputeKit instance. + +```typescript +const kit = new ComputeKit({ + maxWorkers: 4, // Max workers (default: CPU cores) + timeout: 30000, // Default timeout in ms + debug: false, // Enable debug logging + useSharedMemory: true, // Use SharedArrayBuffer when available +}); +``` + +### `kit.register(name, fn)` + +Register a function to run in workers. + +```typescript +kit.register('sum', (numbers: number[]) => { + return numbers.reduce((a, b) => a + b, 0); +}); + +// Async functions work too +kit.register('fetchAndProcess', async (url: string) => { + const res = await fetch(url); + const data = await res.json(); + return processData(data); +}); +``` + +### `kit.run(name, input, options?)` + +Execute a registered function. + +```typescript +const result = await kit.run('sum', [1, 2, 3, 4, 5]); +console.log(result); // 15 +``` + +**Options:** + +```typescript +await kit.run('task', data, { + timeout: 5000, // Timeout in ms + priority: 10, // Priority (0-10, higher = first) + signal: abortController.signal, // AbortSignal for cancellation + onProgress: (p) => { + // Progress callback + console.log(`${p.percent}%`); + }, +}); +``` + +### `kit.runWithMetadata(name, input, options?)` + +Execute and get execution metadata. + +```typescript +const result = await kit.runWithMetadata('task', data); +console.log(result.data); // The result +console.log(result.duration); // Execution time in ms +console.log(result.workerId); // Which worker ran it +``` + +### `kit.getStats()` + +Get worker pool statistics. + +```typescript +const stats = kit.getStats(); +console.log(stats.activeWorkers); // Currently busy workers +console.log(stats.queueLength); // Tasks waiting +console.log(stats.tasksCompleted); // Total completed +``` + +### `kit.terminate()` + +Terminate all workers and clean up. + +```typescript +await kit.terminate(); +``` + +## WASM Support + +Load and use WebAssembly modules: + +```typescript +import { loadWasmModule, loadAssemblyScript } from '@computekit/core'; + +// Load a WASM module +const module = await loadWasmModule('/path/to/module.wasm'); + +// Load AssemblyScript with default imports +const { exports } = await loadAssemblyScript('/as-module.wasm'); +const result = exports.compute(data); +``` + +### WASM Utilities + +```typescript +import { + loadWasmModule, + loadAndInstantiate, + loadAssemblyScript, + getMemoryView, + copyToWasmMemory, + copyFromWasmMemory, + isWasmSupported, +} from '@computekit/core'; + +// Check support +if (isWasmSupported()) { + // Load and instantiate with custom imports + const { instance } = await loadAndInstantiate({ + source: '/module.wasm', + imports: { env: { log: console.log } }, + memory: { initial: 256, maximum: 512 }, + }); +} +``` + +## Events + +ComputeKit emits events for monitoring: + +```typescript +kit.on('worker:created', (info) => console.log('Worker created:', info.id)); +kit.on('worker:terminated', (info) => console.log('Worker terminated:', info.id)); +kit.on('task:start', (taskId, name) => console.log('Task started:', name)); +kit.on('task:complete', (taskId, duration) => console.log('Done in', duration, 'ms')); +kit.on('task:error', (taskId, error) => console.error('Task failed:', error)); +kit.on('task:progress', (taskId, progress) => console.log(progress.percent, '%')); +``` + +## Error Handling + +```typescript +try { + await kit.run('task', data); +} catch (error) { + if (error.message.includes('not registered')) { + // Function not found + } else if (error.message.includes('timed out')) { + // Timeout exceeded + } else if (error.message.includes('aborted')) { + // Cancelled via AbortSignal + } +} +``` + +## Cancellation + +```typescript +const controller = new AbortController(); + +// Start a long task +const promise = kit.run('heavyTask', data, { + signal: controller.signal, +}); + +// Cancel it +controller.abort(); + +try { + await promise; +} catch (error) { + // error.message contains 'aborted' +} +``` + +## TypeScript + +Full type safety: + +```typescript +// Generic types flow through +kit.register('double', (n: number) => n * 2); +const result = await kit.run('double', 21); +// result is typed as number +``` + +## License + +MIT diff --git a/packages/react/README.md b/packages/react/README.md new file mode 100644 index 0000000..232368b --- /dev/null +++ b/packages/react/README.md @@ -0,0 +1,280 @@ +# @computekit/react + +React bindings for ComputeKit - run heavy computations in Web Workers with simple hooks. + +## Installation + +```bash +npm install @computekit/core @computekit/react +``` + +## Quick Start + +```tsx +import { ComputeKitProvider, useComputeKit, useCompute } from '@computekit/react'; + +// 1. Wrap your app with the provider +function App() { + return ( + + + + ); +} + +// 2. Register functions and use them +function MyApp() { + const kit = useComputeKit(); + + useEffect(() => { + kit.register('fibonacci', (n: number) => { + let a = 0, + b = 1; + for (let i = 0; i < n; i++) { + [a, b] = [b, a + b]; + } + return a; + }); + }, [kit]); + + return ; +} + +// 3. Use the compute function +function FibCalculator() { + const { data, loading, error, run } = useCompute('fibonacci'); + + return ( +
+ + {loading &&

Computing...

} + {error &&

Error: {error.message}

} + {data !== null &&

Result: {data}

} +
+ ); +} +``` + +## API Reference + +### `` + +Provider component that creates and manages the ComputeKit instance. + +```tsx + + {children} + +``` + +**Props:** + +| Prop | Type | Description | +| ---------- | ------------------- | ------------------------------------- | +| `options` | `ComputeKitOptions` | Configuration options | +| `instance` | `ComputeKit` | Custom ComputeKit instance (optional) | +| `children` | `ReactNode` | Child components | + +### `useComputeKit()` + +Get the ComputeKit instance from context. + +```tsx +const kit = useComputeKit(); + +// Register functions +kit.register('myFunc', (input) => /* ... */); + +// Run directly +const result = await kit.run('myFunc', data); +``` + +### `useCompute(name, options?)` + +Hook for running compute functions with full state management. + +```tsx +const { + data, // Result (null until complete) + loading, // Boolean loading state + error, // Error object if failed + progress, // Progress info for long tasks + run, // Function to execute + reset, // Reset state + cancel, // Cancel ongoing computation +} = useCompute('fibonacci'); + +// Execute +await run(50); + +// With options +await run(50, { timeout: 5000 }); +``` + +**Options:** + +| Option | Type | Description | +| -------------- | -------------------- | -------------------------------------- | +| `timeout` | `number` | Operation timeout in ms | +| `autoRun` | `boolean` | Auto-run on mount | +| `initialInput` | `unknown` | Input for autoRun | +| `resetOnRun` | `boolean` | Reset state on new run (default: true) | +| `onProgress` | `(progress) => void` | Progress callback | + +### `useComputeCallback(name, options?)` + +Returns a memoized async function for simple use cases. + +```tsx +const calculate = useComputeCallback('sum'); + +const handleClick = async () => { + const result = await calculate([1, 2, 3, 4, 5]); + console.log(result); // 15 +}; +``` + +### `useComputeFunction(name, fn, options?)` + +Register and use a function in one hook. Useful for component-local compute functions. + +```tsx +const { data, loading, run } = useComputeFunction('double', (n: number) => n * 2); + +// Function is registered automatically +run(21); // data will be 42 +``` + +### `usePoolStats(refreshInterval?)` + +Get worker pool statistics. + +```tsx +const stats = usePoolStats(1000); // Refresh every second + +return ( +
+

+ Active: {stats.activeWorkers}/{stats.totalWorkers} +

+

Queue: {stats.queueLength}

+

Completed: {stats.tasksCompleted}

+
+); +``` + +**Returns `PoolStats`:** + +| Property | Type | Description | +| ---------------- | -------- | ---------------------- | +| `totalWorkers` | `number` | Total worker count | +| `activeWorkers` | `number` | Currently busy workers | +| `idleWorkers` | `number` | Currently idle workers | +| `queueLength` | `number` | Tasks waiting in queue | +| `tasksCompleted` | `number` | Total completed tasks | +| `tasksFailed` | `number` | Total failed tasks | + +### `useWasmSupport()` + +Check if WebAssembly is supported. + +```tsx +const isSupported = useWasmSupport(); + +if (!isSupported) { + return

WebAssembly not supported

; +} +``` + +## Patterns + +### Cancellation on Unmount + +The `useCompute` hook automatically cancels pending operations when the component unmounts: + +```tsx +function MyComponent() { + const { run, loading } = useCompute('heavyTask'); + + useEffect(() => { + run(data); // Automatically cancelled if component unmounts + }, []); + + // ... +} +``` + +### Manual Cancellation + +```tsx +function MyComponent() { + const { run, cancel, loading } = useCompute('heavyTask'); + + return ( + <> + + + + ); +} +``` + +### Progress Tracking + +```tsx +function MyComponent() { + const { run, progress, loading } = useCompute('heavyTask', { + onProgress: (p) => console.log(`${p.percent}%`), + }); + + return ( +
{loading && progress && }
+ ); +} +``` + +### With AbortController + +```tsx +function MyComponent() { + const controller = useRef(new AbortController()); + const { run } = useCompute('task'); + + const handleRun = () => { + run(data, { signal: controller.current.signal }); + }; + + const handleCancel = () => { + controller.current.abort(); + controller.current = new AbortController(); + }; +} +``` + +## TypeScript + +Full type inference is supported: + +```tsx +// Types are inferred from registration +kit.register('add', (nums: number[]) => nums.reduce((a, b) => a + b, 0)); + +// Explicit types for hooks +const { data, run } = useCompute('add'); +// data: number | null +// run: (input: number[]) => Promise +``` + +## License + +MIT diff --git a/packages/react/package.json b/packages/react/package.json index 13b9c85..e6d2ee2 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -27,7 +27,13 @@ "@computekit/core": "*" }, "peerDependencies": { - "react": ">=17.0.0" + "react": ">=17.0.0", + "@types/react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } }, "devDependencies": { "@types/react": "^18.2.45", diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 5ad7014..e9b06eb 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -3,7 +3,7 @@ * React hooks and utilities for ComputeKit */ -import { +import React, { useState, useEffect, useCallback, @@ -60,7 +60,7 @@ export function ComputeKitProvider({ options, instance, children, -}: ComputeKitProviderProps): JSX.Element { +}: ComputeKitProviderProps): React.ReactElement { const kit = useMemo(() => { return instance ?? new ComputeKit(options); }, [instance, options]); diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 54ffb39..33b100f 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "./dist", "rootDir": "./src", - "jsx": "react-jsx" + "jsx": "react-jsx", + "jsxImportSource": "react" }, "include": [ "src/**/*" diff --git a/packages/wasm/asconfig.json b/packages/wasm/asconfig.json deleted file mode 100644 index aa9bd3b..0000000 --- a/packages/wasm/asconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "targets": { - "debug": { - "outFile": "build/debug.wasm", - "textFile": "build/debug.wat", - "sourceMap": true, - "debug": true - }, - "release": { - "outFile": "build/release.wasm", - "textFile": "build/release.wat", - "sourceMap": true, - "optimizeLevel": 3, - "shrinkLevel": 0, - "converge": false, - "noAssert": false - } - }, - "options": { - "bindings": "esm", - "exportRuntime": true, - "enable": ["simd"] - } -} diff --git a/packages/wasm/package.json b/packages/wasm/package.json deleted file mode 100644 index 09df823..0000000 --- a/packages/wasm/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@computekit/wasm", - "version": "0.1.0", - "description": "WASM compute functions for ComputeKit", - "type": "module", - "main": "./build/release.js", - "types": "./build/release.d.ts", - "exports": { - ".": { - "types": "./build/release.d.ts", - "import": "./build/release.js" - }, - "./wasm": "./build/release.wasm" - }, - "files": [ - "build", - "assembly" - ], - "scripts": { - "build": "npm run asbuild:release", - "asbuild:debug": "asc assembly/index.ts --target debug", - "asbuild:release": "asc assembly/index.ts --target release" - }, - "author": "Ghassen Lassoued ", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/tapava/compute-kit", - "directory": "packages/wasm" - }, - "devDependencies": { - "assemblyscript": "^0.27.0" - } -} From 0e8cef317a4a34f4ddb45b3fda05da8a460b9d67 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Fri, 26 Dec 2025 23:19:20 +0000 Subject: [PATCH 06/37] chore: include README.md in package files for core and react packages --- packages/core/package.json | 3 ++- packages/react/package.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 097b0ca..03c8201 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,7 +20,8 @@ }, "files": [ "dist", - "src" + "src", + "README.md" ], "scripts": { "build": "tsup", diff --git a/packages/react/package.json b/packages/react/package.json index e6d2ee2..a61c447 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -15,7 +15,8 @@ }, "files": [ "dist", - "src" + "src", + "README.md" ], "scripts": { "build": "tsup", From 1dbde14260f79f3e4388bbac33c3511b1dff1356 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Fri, 26 Dec 2025 23:28:47 +0000 Subject: [PATCH 07/37] chore: bump version to 0.1.1 for core and react packages --- packages/core/package.json | 2 +- packages/react/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 03c8201..815d2fd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@computekit/core", - "version": "0.1.0", + "version": "0.1.1", "description": "Core WASM + Worker toolkit for running heavy computations without blocking the UI", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/react/package.json b/packages/react/package.json index a61c447..2b022ef 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@computekit/react", - "version": "0.1.0", + "version": "0.1.1", "description": "React bindings for ComputeKit - WASM + Worker toolkit", "type": "module", "main": "./dist/index.cjs", From babfaaa16ba03073eff5baa4aa5e70f87dc7b487 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Fri, 26 Dec 2025 23:53:40 +0000 Subject: [PATCH 08/37] feat: add support for remote dependencies in ComputeKit and update README --- README.md | 36 +++++++++++++++++++++++++++++------- packages/core/README.md | 21 +++++++++++++++++++++ packages/core/package.json | 2 +- packages/core/src/pool.ts | 10 ++++++++++ packages/core/src/types.ts | 2 ++ packages/react/README.md | 18 ++++++++++++++++++ packages/react/package.json | 2 +- packages/react/src/index.tsx | 25 +++++++++++++++++++++++-- 8 files changed, 105 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index b69f141..4d05237 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ *Run heavy computations with React hooks. Use WASM for native-speed performance. Keep your UI at 60fps.* [![npm version](https://img.shields.io/npm/v/@computekit/core.svg)](https://www.npmjs.com/package/@computekit/core) -[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@computekit/core)](https://bundlephobia.com/package/@computekit/core) +[![Bundle Size Core](https://img.shields.io/bundlephobia/minzip/@computekit/core?label=core%20size)](https://bundlephobia.com/package/@computekit/core) +[![Bundle Size React](https://img.shields.io/bundlephobia/minzip/@computekit/react?label=react%20size)](https://bundlephobia.com/package/@computekit/react) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/) @@ -323,11 +324,29 @@ const kit = new ComputeKit(options?: ComputeKitOptions); #### Options -| Option | Type | Default | Description | -| ------------ | --------- | ------------------------------- | ----------------------- | -| `maxWorkers` | `number` | `navigator.hardwareConcurrency` | Max workers in the pool | -| `timeout` | `number` | `30000` | Default timeout in ms | -| `debug` | `boolean` | `false` | Enable debug logging | +| Option | Type | Default | Description | +| -------------------- | ---------- | ------------------------------- | ----------------------------------- | +| `maxWorkers` | `number` | `navigator.hardwareConcurrency` | Max workers in the pool | +| `timeout` | `number` | `30000` | Default timeout in ms | +| `debug` | `boolean` | `false` | Enable debug logging | +| `remoteDependencies` | `string[]` | `[]` | External scripts to load in workers | + +### Remote Dependencies + +Load external libraries inside your workers: + +```typescript +const kit = new ComputeKit({ + remoteDependencies: [ + 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js', + ], +}); + +kit.register('processData', (data: number[]) => { + // @ts-ignore - lodash loaded via importScripts + return _.chunk(data, 3); +}); +``` #### Methods @@ -363,12 +382,15 @@ const { loading, // Boolean loading state error, // Error if failed progress, // Progress info + status, // 'idle' | 'running' | 'success' | 'error' | 'cancelled' run, // Function to execute reset, // Reset state cancel, // Cancel current operation } = useCompute(functionName, options?); ``` +```` + ### `useComputeCallback` Returns a memoized async function (similar to `useCallback`). @@ -376,7 +398,7 @@ Returns a memoized async function (similar to `useCallback`). ```typescript const calculate = useComputeCallback('sum'); const result = await calculate([1, 2, 3, 4, 5]); -``` +```` ### `usePoolStats` diff --git a/packages/core/README.md b/packages/core/README.md index e2697c6..4602c6c 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -43,9 +43,30 @@ const kit = new ComputeKit({ timeout: 30000, // Default timeout in ms debug: false, // Enable debug logging useSharedMemory: true, // Use SharedArrayBuffer when available + remoteDependencies: [], // External scripts to load in workers }); ``` +### Remote Dependencies + +Load external scripts inside workers using `remoteDependencies`: + +```typescript +const kit = new ComputeKit({ + remoteDependencies: [ + 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js', + ], +}); + +// Now you can use lodash inside your compute functions +kit.register('processData', (data: number[]) => { + // @ts-ignore - lodash is loaded via importScripts + return _.chunk(data, 3); +}); +``` + +**Note:** Remote scripts must be served with proper CORS headers. + ### `kit.register(name, fn)` Register a function to run in workers. diff --git a/packages/core/package.json b/packages/core/package.json index 815d2fd..f4a4ed7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@computekit/core", - "version": "0.1.1", + "version": "0.1.2", "description": "Core WASM + Worker toolkit for running heavy computations without blocking the UI", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/core/src/pool.ts b/packages/core/src/pool.ts index 6ee1282..ca0a431 100644 --- a/packages/core/src/pool.ts +++ b/packages/core/src/pool.ts @@ -86,6 +86,7 @@ export class WorkerPool { debug: options.debug ?? false, workerPath: options.workerPath ?? '', useSharedMemory: options.useSharedMemory ?? true, + remoteDependencies: options.remoteDependencies ?? [], }; this.logger = createLogger('ComputeKit:Pool', this.options.debug); @@ -308,7 +309,16 @@ export class WorkerPool { Array.from(this.functions.keys()) ); + // Generate importScripts for remote dependencies + const remoteDeps = this.options.remoteDependencies; + const importScriptsCode = + remoteDeps.length > 0 + ? `importScripts(${remoteDeps.map((url) => `"${url}"`).join(', ')});` + : ''; + const workerCode = ` +${importScriptsCode} + const functions = { ${functionsCode} }; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index adb658f..9253667 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -15,6 +15,8 @@ export interface ComputeKitOptions { workerPath?: string; /** Whether to use SharedArrayBuffer when available (default: true) */ useSharedMemory?: boolean; + /** Remote scripts to load in workers via importScripts */ + remoteDependencies?: string[]; } /** Options for individual compute operations */ diff --git a/packages/react/README.md b/packages/react/README.md index 232368b..c084294 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -107,6 +107,7 @@ const { loading, // Boolean loading state error, // Error object if failed progress, // Progress info for long tasks + status, // 'idle' | 'running' | 'success' | 'error' | 'cancelled' run, // Function to execute reset, // Reset state cancel, // Cancel ongoing computation @@ -117,8 +118,25 @@ await run(50); // With options await run(50, { timeout: 5000 }); + +// React to status changes +if (status === 'success') { + console.log('Completed!', data); +} else if (status === 'error') { + console.error('Failed:', error); +} ``` +**Status values:** + +| Status | Description | +| ----------- | ----------------------------------- | +| `idle` | Initial state, no computation yet | +| `running` | Computation in progress | +| `success` | Completed successfully | +| `error` | Failed with an error | +| `cancelled` | Cancelled via `cancel()` or unmount | + **Options:** | Option | Type | Description | diff --git a/packages/react/package.json b/packages/react/package.json index 2b022ef..9753254 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@computekit/react", - "version": "0.1.1", + "version": "0.1.2", "description": "React bindings for ComputeKit - WASM + Worker toolkit", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index e9b06eb..85f8ffd 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -92,6 +92,11 @@ export function useComputeKit(): ComputeKit { // useCompute Hook // ============================================================================ +/** + * Status of a compute operation + */ +export type ComputeStatus = 'idle' | 'running' | 'success' | 'error' | 'cancelled'; + /** * State returned by useCompute */ @@ -104,6 +109,8 @@ export interface UseComputeState { error: Error | null; /** Progress information */ progress: ComputeProgress | null; + /** Current status of the computation */ + status: ComputeStatus; } /** @@ -163,27 +170,37 @@ export function useCompute( ): UseComputeReturn { const kit = useComputeKit(); const abortControllerRef = useRef(null); + const cancelledRef = useRef(false); const [state, setState] = useState>({ data: null, loading: false, error: null, progress: null, + status: 'idle', }); const reset = useCallback(() => { + cancelledRef.current = false; setState({ data: null, loading: false, error: null, progress: null, + status: 'idle', }); }, []); const cancel = useCallback(() => { if (abortControllerRef.current) { + cancelledRef.current = true; abortControllerRef.current.abort(); abortControllerRef.current = null; + setState((prev) => ({ + ...prev, + loading: false, + status: 'cancelled', + })); } }, []); @@ -191,6 +208,7 @@ export function useCompute( async (input: TInput, runOptions?: ComputeOptions) => { // Cancel any ongoing computation cancel(); + cancelledRef.current = false; // Create new abort controller const abortController = new AbortController(); @@ -203,9 +221,10 @@ export function useCompute( loading: true, error: null, progress: null, + status: 'running', })); } else { - setState((prev) => ({ ...prev, loading: true })); + setState((prev) => ({ ...prev, loading: true, status: 'running' })); } try { @@ -226,15 +245,17 @@ export function useCompute( loading: false, error: null, progress: null, + status: 'success', }); } } catch (err) { - if (!abortController.signal.aborted) { + if (!abortController.signal.aborted && !cancelledRef.current) { setState({ data: null, loading: false, error: err instanceof Error ? err : new Error(String(err)), progress: null, + status: 'error', }); } } From 7c503897fb3969aa694122460450912825fe7372 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Fri, 26 Dec 2025 23:57:43 +0000 Subject: [PATCH 09/37] chore: update license attribution in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d05237..49f52fe 100644 --- a/README.md +++ b/README.md @@ -562,7 +562,7 @@ npm test ## 📄 License -MIT © [Your Name](https://github.com/your-username) +MIT © [Ghassen Lassoued](https://github.com/tapava) --- From b8a0be84c92f3e4bde7b286213b13bc937b34e08 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 00:18:09 +0000 Subject: [PATCH 10/37] feat: add TODO list for future enhancements and features in ComputeKit --- TODO.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..cc89743 --- /dev/null +++ b/TODO.md @@ -0,0 +1,21 @@ +# ComputeKit TODO + +## Features + +- [ ] **Progress throttling** - Add optional `progressThrottle` option to prevent state flooding when compute functions fire progress updates in tight loops. Should throttle/debounce progress callbacks to avoid choking the main thread with re-renders. + - Add `progressThrottle?: number` option (ms) to `ComputeOptions` + - Throttle `onProgress` calls in the React hook + - Consider both throttle (regular intervals) and debounce (wait for pause) strategies + +## Improvements + +- [ ] Add more WASM examples (Rust, C++) +- [ ] Benchmark suite for comparing JS vs WASM performance +- [ ] Documentation site (Docusaurus or similar) + +## Ideas + +- [ ] `useComputeMultiple` hook for managing multiple parallel tasks +- [ ] `useComputeFile` hook for loading functions from separate files +- [ ] Built-in caching for compute results +- [ ] Streaming results for very large outputs From bfbfa54f9ba3d84d8f11cfc1d2b6a87285e8ddae Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 10:38:28 +0000 Subject: [PATCH 11/37] feat: enhance WASM loading with error handling and absolute path resolution --- TODO.md | 5 + examples/react-demo/public/compute.js | 240 +++++++++++++++----------- examples/react-demo/src/wasmLoader.ts | 16 +- 3 files changed, 154 insertions(+), 107 deletions(-) diff --git a/TODO.md b/TODO.md index cc89743..c806272 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,11 @@ - Throttle `onProgress` calls in the React hook - Consider both throttle (regular intervals) and debounce (wait for pause) strategies +- [ ] **Typed registry** - Add TypeScript support to narrow function names to only registered functions for autocomplete and type safety. + - Make `useCompute('functionName')` autocomplete only registered function names + - Type safety on input/output based on registered function signatures + - Consider using TypeScript's string literal types or const assertions + ## Improvements - [ ] Add more WASM examples (Rust, C++) diff --git a/examples/react-demo/public/compute.js b/examples/react-demo/public/compute.js index b0a3f29..6080d96 100644 --- a/examples/react-demo/public/compute.js +++ b/examples/react-demo/public/compute.js @@ -16,103 +16,117 @@ async function instantiate(module, imports = {}) { }; const { exports } = await WebAssembly.instantiate(module, adaptedImports); const memory = exports.memory || imports.env.memory; - const adaptedExports = Object.setPrototypeOf({ - sum(arr) { - // compute/sum/sum(~lib/typedarray/Int32Array) => i32 - arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); - return exports.sum(arr); - }, - sumFloat(arr) { - // compute/sum/sumFloat(~lib/typedarray/Float64Array) => f64 - arr = __lowerTypedArray(Float64Array, 5, 3, arr) || __notnull(); - return exports.sumFloat(arr); - }, - average(arr) { - // compute/sum/average(~lib/typedarray/Int32Array) => f64 - arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); - return exports.average(arr); - }, - fibonacciSequence(n) { - // compute/fibonacci/fibonacciSequence(i32) => ~lib/typedarray/Int64Array - return __liftTypedArray(BigInt64Array, exports.fibonacciSequence(n) >>> 0); - }, - isFibonacci(num) { - // compute/fibonacci/isFibonacci(i64) => bool - num = num || 0n; - return exports.isFibonacci(num) != 0; - }, - mandelbrot(width, height, zoom, panX, panY, maxIter) { - // compute/mandelbrot/mandelbrot(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array - return __liftTypedArray(Uint32Array, exports.mandelbrot(width, height, zoom, panX, panY, maxIter) >>> 0); - }, - julia(width, height, cRe, cIm, zoom, maxIter) { - // compute/mandelbrot/julia(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array - return __liftTypedArray(Uint32Array, exports.julia(width, height, cRe, cIm, zoom, maxIter) >>> 0); - }, - matrixMultiply(a, b, aRows, aCols, bCols) { - // compute/matrix/matrixMultiply(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array, i32, i32, i32) => ~lib/typedarray/Float64Array - a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); - b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); - try { - return __liftTypedArray(Float64Array, exports.matrixMultiply(a, b, aRows, aCols, bCols) >>> 0); - } finally { - __release(a); - } - }, - matrixTranspose(matrix, rows, cols) { - // compute/matrix/matrixTranspose(~lib/typedarray/Float64Array, i32, i32) => ~lib/typedarray/Float64Array - matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); - return __liftTypedArray(Float64Array, exports.matrixTranspose(matrix, rows, cols) >>> 0); - }, - matrixAdd(a, b) { - // compute/matrix/matrixAdd(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array - a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); - b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); - try { - return __liftTypedArray(Float64Array, exports.matrixAdd(a, b) >>> 0); - } finally { - __release(a); - } - }, - matrixScale(matrix, scalar) { - // compute/matrix/matrixScale(~lib/typedarray/Float64Array, f64) => ~lib/typedarray/Float64Array - matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); - return __liftTypedArray(Float64Array, exports.matrixScale(matrix, scalar) >>> 0); - }, - dotProduct(a, b) { - // compute/matrix/dotProduct(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => f64 - a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); - b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); - try { - return exports.dotProduct(a, b); - } finally { - __release(a); - } - }, - vectorMagnitude(v) { - // compute/matrix/vectorMagnitude(~lib/typedarray/Float64Array) => f64 - v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); - return exports.vectorMagnitude(v); - }, - vectorNormalize(v) { - // compute/matrix/vectorNormalize(~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array - v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); - return __liftTypedArray(Float64Array, exports.vectorNormalize(v) >>> 0); - }, - getBufferPtr() { - // compute/blur/getBufferPtr() => usize - return exports.getBufferPtr() >>> 0; + const adaptedExports = Object.setPrototypeOf( + { + sum(arr) { + // compute/sum/sum(~lib/typedarray/Int32Array) => i32 + arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); + return exports.sum(arr); + }, + sumFloat(arr) { + // compute/sum/sumFloat(~lib/typedarray/Float64Array) => f64 + arr = __lowerTypedArray(Float64Array, 5, 3, arr) || __notnull(); + return exports.sumFloat(arr); + }, + average(arr) { + // compute/sum/average(~lib/typedarray/Int32Array) => f64 + arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); + return exports.average(arr); + }, + fibonacciSequence(n) { + // compute/fibonacci/fibonacciSequence(i32) => ~lib/typedarray/Int64Array + return __liftTypedArray(BigInt64Array, exports.fibonacciSequence(n) >>> 0); + }, + isFibonacci(num) { + // compute/fibonacci/isFibonacci(i64) => bool + num = num || 0n; + return exports.isFibonacci(num) != 0; + }, + mandelbrot(width, height, zoom, panX, panY, maxIter) { + // compute/mandelbrot/mandelbrot(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array + return __liftTypedArray( + Uint32Array, + exports.mandelbrot(width, height, zoom, panX, panY, maxIter) >>> 0 + ); + }, + julia(width, height, cRe, cIm, zoom, maxIter) { + // compute/mandelbrot/julia(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array + return __liftTypedArray( + Uint32Array, + exports.julia(width, height, cRe, cIm, zoom, maxIter) >>> 0 + ); + }, + matrixMultiply(a, b, aRows, aCols, bCols) { + // compute/matrix/matrixMultiply(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array, i32, i32, i32) => ~lib/typedarray/Float64Array + a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); + b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); + try { + return __liftTypedArray( + Float64Array, + exports.matrixMultiply(a, b, aRows, aCols, bCols) >>> 0 + ); + } finally { + __release(a); + } + }, + matrixTranspose(matrix, rows, cols) { + // compute/matrix/matrixTranspose(~lib/typedarray/Float64Array, i32, i32) => ~lib/typedarray/Float64Array + matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); + return __liftTypedArray( + Float64Array, + exports.matrixTranspose(matrix, rows, cols) >>> 0 + ); + }, + matrixAdd(a, b) { + // compute/matrix/matrixAdd(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array + a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); + b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); + try { + return __liftTypedArray(Float64Array, exports.matrixAdd(a, b) >>> 0); + } finally { + __release(a); + } + }, + matrixScale(matrix, scalar) { + // compute/matrix/matrixScale(~lib/typedarray/Float64Array, f64) => ~lib/typedarray/Float64Array + matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); + return __liftTypedArray(Float64Array, exports.matrixScale(matrix, scalar) >>> 0); + }, + dotProduct(a, b) { + // compute/matrix/dotProduct(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => f64 + a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); + b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); + try { + return exports.dotProduct(a, b); + } finally { + __release(a); + } + }, + vectorMagnitude(v) { + // compute/matrix/vectorMagnitude(~lib/typedarray/Float64Array) => f64 + v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); + return exports.vectorMagnitude(v); + }, + vectorNormalize(v) { + // compute/matrix/vectorNormalize(~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array + v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); + return __liftTypedArray(Float64Array, exports.vectorNormalize(v) >>> 0); + }, + getBufferPtr() { + // compute/blur/getBufferPtr() => usize + return exports.getBufferPtr() >>> 0; + }, }, - }, exports); + exports + ); function __liftString(pointer) { if (!pointer) return null; - const - end = pointer + new Uint32Array(memory.buffer)[pointer - 4 >>> 2] >>> 1, + const end = (pointer + new Uint32Array(memory.buffer)[(pointer - 4) >>> 2]) >>> 1, memoryU16 = new Uint16Array(memory.buffer); - let - start = pointer >>> 1, - string = ""; - while (end - start > 1024) string += String.fromCharCode(...memoryU16.subarray(start, start += 1024)); + let start = pointer >>> 1, + string = ''; + while (end - start > 1024) + string += String.fromCharCode(...memoryU16.subarray(start, (start += 1024))); return string + String.fromCharCode(...memoryU16.subarray(start, end)); } function __liftTypedArray(constructor, pointer) { @@ -125,8 +139,7 @@ async function instantiate(module, imports = {}) { } function __lowerTypedArray(constructor, id, align, values) { if (values == null) return 0; - const - length = values.length, + const length = values.length, buffer = exports.__pin(exports.__new(length << align, 1)) >>> 0, header = exports.__new(12, id) >>> 0; __setU32(header + 0, buffer); @@ -148,13 +161,13 @@ async function instantiate(module, imports = {}) { function __release(pointer) { if (pointer) { const refcount = refcounts.get(pointer); - if (refcount === 1) exports.__unpin(pointer), refcounts.delete(pointer); + if (refcount === 1) (exports.__unpin(pointer), refcounts.delete(pointer)); else if (refcount) refcounts.set(pointer, refcount - 1); else throw Error(`invalid refcount '${refcount}' for reference '${pointer}'`); } } function __notnull() { - throw TypeError("value must not be null"); + throw TypeError('value must not be null'); } let __dataview = new DataView(memory.buffer); function __setU32(pointer, value) { @@ -194,11 +207,28 @@ export const { vectorNormalize, getBufferPtr, blurImage, -} = await (async url => instantiate( - await (async () => { - const isNodeOrBun = typeof process != "undefined" && process.versions != null && (process.versions.node != null || process.versions.bun != null); - if (isNodeOrBun) { return globalThis.WebAssembly.compile(await (await import("node:fs/promises")).readFile(url)); } - else { return await globalThis.WebAssembly.compileStreaming(globalThis.fetch(url)); } - })(), { - } -))(new URL("compute.wasm", import.meta.url)); +} = await (async (url) => + instantiate( + await (async () => { + const isNodeOrBun = + typeof process != 'undefined' && + process.versions != null && + (process.versions.node != null || process.versions.bun != null); + if (isNodeOrBun) { + return globalThis.WebAssembly.compile( + await (await import('node:fs/promises')).readFile(url) + ); + } else { + const response = await globalThis.fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch WASM: ${response.status}`); + } + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/wasm')) { + return await globalThis.WebAssembly.compileStreaming(response); + } + return await globalThis.WebAssembly.compile(await response.arrayBuffer()); + } + })(), + {} + ))(new URL('/compute.wasm', globalThis.location?.origin || import.meta.url)); diff --git a/examples/react-demo/src/wasmLoader.ts b/examples/react-demo/src/wasmLoader.ts index 5397e37..a71f4b4 100644 --- a/examples/react-demo/src/wasmLoader.ts +++ b/examples/react-demo/src/wasmLoader.ts @@ -77,8 +77,20 @@ export async function loadWasm(): Promise { } loadingPromise = (async () => { - const wasmUrl = new URL('/compute.wasm', import.meta.url); - const module = await WebAssembly.compileStreaming(fetch(wasmUrl)); + // Use absolute path from origin - works in both dev and production + const wasmUrl = new URL('/compute.wasm', window.location.origin).href; + const response = await fetch(wasmUrl); + if (!response.ok) { + throw new Error(`Failed to fetch WASM: ${response.status} ${response.statusText}`); + } + const module = await WebAssembly.compileStreaming( + // Create a new Response with the correct MIME type if needed + response.headers.get('content-type')?.includes('application/wasm') + ? response + : new Response(await response.arrayBuffer(), { + headers: { 'Content-Type': 'application/wasm' }, + }) + ); cachedExports = await instantiate(module, {}); return cachedExports; })(); From 0aae49c4abbad2ac23476d84a2bbb65438ed5cd0 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 10:43:52 +0000 Subject: [PATCH 12/37] feat: implement WASM loading and configuration for Vercel deployment --- examples/react-demo/public/compute.js | 240 +++++++++++--------------- examples/react-demo/vercel.json | 19 ++ examples/react-demo/vite.config.ts | 4 + vercel.json | 21 +++ 4 files changed, 149 insertions(+), 135 deletions(-) create mode 100644 examples/react-demo/vercel.json create mode 100644 vercel.json diff --git a/examples/react-demo/public/compute.js b/examples/react-demo/public/compute.js index 6080d96..b0a3f29 100644 --- a/examples/react-demo/public/compute.js +++ b/examples/react-demo/public/compute.js @@ -16,117 +16,103 @@ async function instantiate(module, imports = {}) { }; const { exports } = await WebAssembly.instantiate(module, adaptedImports); const memory = exports.memory || imports.env.memory; - const adaptedExports = Object.setPrototypeOf( - { - sum(arr) { - // compute/sum/sum(~lib/typedarray/Int32Array) => i32 - arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); - return exports.sum(arr); - }, - sumFloat(arr) { - // compute/sum/sumFloat(~lib/typedarray/Float64Array) => f64 - arr = __lowerTypedArray(Float64Array, 5, 3, arr) || __notnull(); - return exports.sumFloat(arr); - }, - average(arr) { - // compute/sum/average(~lib/typedarray/Int32Array) => f64 - arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); - return exports.average(arr); - }, - fibonacciSequence(n) { - // compute/fibonacci/fibonacciSequence(i32) => ~lib/typedarray/Int64Array - return __liftTypedArray(BigInt64Array, exports.fibonacciSequence(n) >>> 0); - }, - isFibonacci(num) { - // compute/fibonacci/isFibonacci(i64) => bool - num = num || 0n; - return exports.isFibonacci(num) != 0; - }, - mandelbrot(width, height, zoom, panX, panY, maxIter) { - // compute/mandelbrot/mandelbrot(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array - return __liftTypedArray( - Uint32Array, - exports.mandelbrot(width, height, zoom, panX, panY, maxIter) >>> 0 - ); - }, - julia(width, height, cRe, cIm, zoom, maxIter) { - // compute/mandelbrot/julia(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array - return __liftTypedArray( - Uint32Array, - exports.julia(width, height, cRe, cIm, zoom, maxIter) >>> 0 - ); - }, - matrixMultiply(a, b, aRows, aCols, bCols) { - // compute/matrix/matrixMultiply(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array, i32, i32, i32) => ~lib/typedarray/Float64Array - a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); - b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); - try { - return __liftTypedArray( - Float64Array, - exports.matrixMultiply(a, b, aRows, aCols, bCols) >>> 0 - ); - } finally { - __release(a); - } - }, - matrixTranspose(matrix, rows, cols) { - // compute/matrix/matrixTranspose(~lib/typedarray/Float64Array, i32, i32) => ~lib/typedarray/Float64Array - matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); - return __liftTypedArray( - Float64Array, - exports.matrixTranspose(matrix, rows, cols) >>> 0 - ); - }, - matrixAdd(a, b) { - // compute/matrix/matrixAdd(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array - a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); - b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); - try { - return __liftTypedArray(Float64Array, exports.matrixAdd(a, b) >>> 0); - } finally { - __release(a); - } - }, - matrixScale(matrix, scalar) { - // compute/matrix/matrixScale(~lib/typedarray/Float64Array, f64) => ~lib/typedarray/Float64Array - matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); - return __liftTypedArray(Float64Array, exports.matrixScale(matrix, scalar) >>> 0); - }, - dotProduct(a, b) { - // compute/matrix/dotProduct(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => f64 - a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); - b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); - try { - return exports.dotProduct(a, b); - } finally { - __release(a); - } - }, - vectorMagnitude(v) { - // compute/matrix/vectorMagnitude(~lib/typedarray/Float64Array) => f64 - v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); - return exports.vectorMagnitude(v); - }, - vectorNormalize(v) { - // compute/matrix/vectorNormalize(~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array - v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); - return __liftTypedArray(Float64Array, exports.vectorNormalize(v) >>> 0); - }, - getBufferPtr() { - // compute/blur/getBufferPtr() => usize - return exports.getBufferPtr() >>> 0; - }, + const adaptedExports = Object.setPrototypeOf({ + sum(arr) { + // compute/sum/sum(~lib/typedarray/Int32Array) => i32 + arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); + return exports.sum(arr); + }, + sumFloat(arr) { + // compute/sum/sumFloat(~lib/typedarray/Float64Array) => f64 + arr = __lowerTypedArray(Float64Array, 5, 3, arr) || __notnull(); + return exports.sumFloat(arr); + }, + average(arr) { + // compute/sum/average(~lib/typedarray/Int32Array) => f64 + arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); + return exports.average(arr); }, - exports - ); + fibonacciSequence(n) { + // compute/fibonacci/fibonacciSequence(i32) => ~lib/typedarray/Int64Array + return __liftTypedArray(BigInt64Array, exports.fibonacciSequence(n) >>> 0); + }, + isFibonacci(num) { + // compute/fibonacci/isFibonacci(i64) => bool + num = num || 0n; + return exports.isFibonacci(num) != 0; + }, + mandelbrot(width, height, zoom, panX, panY, maxIter) { + // compute/mandelbrot/mandelbrot(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array + return __liftTypedArray(Uint32Array, exports.mandelbrot(width, height, zoom, panX, panY, maxIter) >>> 0); + }, + julia(width, height, cRe, cIm, zoom, maxIter) { + // compute/mandelbrot/julia(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array + return __liftTypedArray(Uint32Array, exports.julia(width, height, cRe, cIm, zoom, maxIter) >>> 0); + }, + matrixMultiply(a, b, aRows, aCols, bCols) { + // compute/matrix/matrixMultiply(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array, i32, i32, i32) => ~lib/typedarray/Float64Array + a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); + b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); + try { + return __liftTypedArray(Float64Array, exports.matrixMultiply(a, b, aRows, aCols, bCols) >>> 0); + } finally { + __release(a); + } + }, + matrixTranspose(matrix, rows, cols) { + // compute/matrix/matrixTranspose(~lib/typedarray/Float64Array, i32, i32) => ~lib/typedarray/Float64Array + matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); + return __liftTypedArray(Float64Array, exports.matrixTranspose(matrix, rows, cols) >>> 0); + }, + matrixAdd(a, b) { + // compute/matrix/matrixAdd(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array + a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); + b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); + try { + return __liftTypedArray(Float64Array, exports.matrixAdd(a, b) >>> 0); + } finally { + __release(a); + } + }, + matrixScale(matrix, scalar) { + // compute/matrix/matrixScale(~lib/typedarray/Float64Array, f64) => ~lib/typedarray/Float64Array + matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); + return __liftTypedArray(Float64Array, exports.matrixScale(matrix, scalar) >>> 0); + }, + dotProduct(a, b) { + // compute/matrix/dotProduct(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => f64 + a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); + b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); + try { + return exports.dotProduct(a, b); + } finally { + __release(a); + } + }, + vectorMagnitude(v) { + // compute/matrix/vectorMagnitude(~lib/typedarray/Float64Array) => f64 + v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); + return exports.vectorMagnitude(v); + }, + vectorNormalize(v) { + // compute/matrix/vectorNormalize(~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array + v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); + return __liftTypedArray(Float64Array, exports.vectorNormalize(v) >>> 0); + }, + getBufferPtr() { + // compute/blur/getBufferPtr() => usize + return exports.getBufferPtr() >>> 0; + }, + }, exports); function __liftString(pointer) { if (!pointer) return null; - const end = (pointer + new Uint32Array(memory.buffer)[(pointer - 4) >>> 2]) >>> 1, + const + end = pointer + new Uint32Array(memory.buffer)[pointer - 4 >>> 2] >>> 1, memoryU16 = new Uint16Array(memory.buffer); - let start = pointer >>> 1, - string = ''; - while (end - start > 1024) - string += String.fromCharCode(...memoryU16.subarray(start, (start += 1024))); + let + start = pointer >>> 1, + string = ""; + while (end - start > 1024) string += String.fromCharCode(...memoryU16.subarray(start, start += 1024)); return string + String.fromCharCode(...memoryU16.subarray(start, end)); } function __liftTypedArray(constructor, pointer) { @@ -139,7 +125,8 @@ async function instantiate(module, imports = {}) { } function __lowerTypedArray(constructor, id, align, values) { if (values == null) return 0; - const length = values.length, + const + length = values.length, buffer = exports.__pin(exports.__new(length << align, 1)) >>> 0, header = exports.__new(12, id) >>> 0; __setU32(header + 0, buffer); @@ -161,13 +148,13 @@ async function instantiate(module, imports = {}) { function __release(pointer) { if (pointer) { const refcount = refcounts.get(pointer); - if (refcount === 1) (exports.__unpin(pointer), refcounts.delete(pointer)); + if (refcount === 1) exports.__unpin(pointer), refcounts.delete(pointer); else if (refcount) refcounts.set(pointer, refcount - 1); else throw Error(`invalid refcount '${refcount}' for reference '${pointer}'`); } } function __notnull() { - throw TypeError('value must not be null'); + throw TypeError("value must not be null"); } let __dataview = new DataView(memory.buffer); function __setU32(pointer, value) { @@ -207,28 +194,11 @@ export const { vectorNormalize, getBufferPtr, blurImage, -} = await (async (url) => - instantiate( - await (async () => { - const isNodeOrBun = - typeof process != 'undefined' && - process.versions != null && - (process.versions.node != null || process.versions.bun != null); - if (isNodeOrBun) { - return globalThis.WebAssembly.compile( - await (await import('node:fs/promises')).readFile(url) - ); - } else { - const response = await globalThis.fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch WASM: ${response.status}`); - } - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/wasm')) { - return await globalThis.WebAssembly.compileStreaming(response); - } - return await globalThis.WebAssembly.compile(await response.arrayBuffer()); - } - })(), - {} - ))(new URL('/compute.wasm', globalThis.location?.origin || import.meta.url)); +} = await (async url => instantiate( + await (async () => { + const isNodeOrBun = typeof process != "undefined" && process.versions != null && (process.versions.node != null || process.versions.bun != null); + if (isNodeOrBun) { return globalThis.WebAssembly.compile(await (await import("node:fs/promises")).readFile(url)); } + else { return await globalThis.WebAssembly.compileStreaming(globalThis.fetch(url)); } + })(), { + } +))(new URL("compute.wasm", import.meta.url)); diff --git a/examples/react-demo/vercel.json b/examples/react-demo/vercel.json new file mode 100644 index 0000000..a4067a0 --- /dev/null +++ b/examples/react-demo/vercel.json @@ -0,0 +1,19 @@ +{ + "headers": [ + { + "source": "/(.*).wasm", + "headers": [ + { + "key": "Content-Type", + "value": "application/wasm" + } + ] + } + ], + "rewrites": [ + { + "source": "/((?!assets|compute\\.wasm|compute\\.js|compute\\.d\\.ts).*)", + "destination": "/index.html" + } + ] +} diff --git a/examples/react-demo/vite.config.ts b/examples/react-demo/vite.config.ts index dac4d68..e049025 100644 --- a/examples/react-demo/vite.config.ts +++ b/examples/react-demo/vite.config.ts @@ -11,6 +11,10 @@ export default defineConfig({ // 'Cross-Origin-Embedder-Policy': 'require-corp', // }, }, + build: { + // Ensure static assets from public folder are copied + copyPublicDir: true, + }, optimizeDeps: { exclude: ['@computekit/core', '@computekit/react'], }, diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..8037e1d --- /dev/null +++ b/vercel.json @@ -0,0 +1,21 @@ +{ + "buildCommand": "npm run build", + "outputDirectory": "examples/react-demo/dist", + "installCommand": "npm install", + "framework": "vite", + "headers": [ + { + "source": "/(.*).wasm", + "headers": [ + { + "key": "Content-Type", + "value": "application/wasm" + }, + { + "key": "Cache-Control", + "value": "public, max-age=31536000, immutable" + } + ] + } + ] +} From f7a6340c89d18ff8122246690fef38ed9b82a52d Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 10:49:56 +0000 Subject: [PATCH 13/37] fix: update vercel.json with version 2 and fix regex pattern --- vercel.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/vercel.json b/vercel.json index 8037e1d..8abe898 100644 --- a/vercel.json +++ b/vercel.json @@ -1,19 +1,15 @@ { + "version": 2, "buildCommand": "npm run build", "outputDirectory": "examples/react-demo/dist", "installCommand": "npm install", - "framework": "vite", "headers": [ { - "source": "/(.*).wasm", + "source": "/(.*)\\.wasm", "headers": [ { "key": "Content-Type", "value": "application/wasm" - }, - { - "key": "Cache-Control", - "value": "public, max-age=31536000, immutable" } ] } From e6aaa7a3bf601fa808ab41bdc8fc23a23e901bd8 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 10:56:10 +0000 Subject: [PATCH 14/37] fix: include compute.wasm in git for Vercel deployment --- .gitignore | 2 ++ examples/react-demo/public/compute.wasm | Bin 0 -> 9703 bytes 2 files changed, 2 insertions(+) create mode 100644 examples/react-demo/public/compute.wasm diff --git a/.gitignore b/.gitignore index e355f9b..c7007ef 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ dist/ build/ *.wasm *.wasm.map +# But include the public WASM file for deployment +!examples/react-demo/public/compute.wasm # IDE .idea/ diff --git a/examples/react-demo/public/compute.wasm b/examples/react-demo/public/compute.wasm new file mode 100644 index 0000000000000000000000000000000000000000..f2f5f92f579d700abe24aeb8dacfcddc26a102bc GIT binary patch literal 9703 zcmb_iUub04c|YgeduQ&<8ENj?>)N~P*yr9E$D26Lwl5Bbf^>v;lXdIZ!D(KlS!s5y zt66C^JCg09NP2KuQ`}-oOG7AlR_owE8weq=Nio<^(z-MW#xxLG3i}WjQu1JE`qahl z?|06rKE#3$Y;P^79dz)4;b3QH z8xy`&jBat1la-yFt*tG;TW&9e7{wB~jVZOY#ZN5T=ISl65OJ1k)effAlnlD7n^EW7 zT7Mv>g5N&XP*Tb;tf*=fh11PyETobOEY|9kM2J{xaU>8ya92ILyB3HjkU~~w8#~ep zePuOMAo>_9n5W zUDf&dja9bpuFcOcuJwA|#eq1g=jZ!_!SejM&gE`UuUzb2T`3v2zSIu}-#2OCRW@%dup%WM6Mo!;^vcEzb!{#HZY zQ~%}Msk178f{n1^_ruva(bQ`O$E!7bTyL;Dl|-#DlP6VZx7JhP$jL8)oZwKfZUb~I^jNb) zX%mTs-CvM^MsLP0W2wUh6$s_oRLoq z>xYG*G)>*A5~G!@;NJ0@gslK7X$M;WiIU+Cj}RG#3TUot`--yDj-LR!z*OU`6Av?g zHlEp4XR$f3;bvqXv^PHb=%c{k@vjE`UQWGHh z*CkOWY($2_ECjZpi71YV(2h(<>?tC)hXWJj04t}(EKn}VwF@SK+-^lN7uszTJiksP zO+e%VzW|5`OsVJBA5#KWqyIjT5wO546bYvONfsC!gy)|}Q#Q;Ue1Iv>LgJ`^qvIe< zDm;=al$*xsFr))JQl+sMj&6l?sGU3`T@9#UJ1`C5M#08d zXTK`h4gK*d_nrhH|8q2jqmATtWxZm7@q}U(#+{0xSOvufR4fdc0v3u@RBW_QvHw$& z7np#`lv5cLMs--2DmbE6ViQ1oj5!VOTg$RFd4Vk-1ZyM(B!8?cacXdeaAo$x7yYJb+eG7-K4R1c!RP z0j-aA{CQv_?$AcP0s2M~oulv=1Qtd=wGNPC4$8h`zLcB*#9k!<)M(++kcW5-8~Bz< zRsWSlyqtkWF!CaFfcUJi;!6<{U^pwZ-GxJfjIhIR`3B=AbU+hiEWv#4fVLu)n1f7y z4%(2^#%RR?<`Af*3)F&57f#&?uYXI;h`*gn{wRsrZ`btu0Yadf^{iH!`=*0iaSHa^$OvsXcN@Xg3)Y4;wv~1Q zM_DN$Ws_hUNK{V)T_+{13>oZwX^%Q9^GV5R%ee!T3!{iy!byP55MZT^9=ZXd0u35@z&6({dMAnu1Njj^U?kqGzmF8^4um7SDQ}5J!N|0&Ls6Ah1-SdK z81O3%8~azuWGY=qpz(y8Dbkm;A0paI8WL>AA}3u~8RS;Nv0(D{LH40l9~i)*a#?^= z2SWePO7CRgfkzWh95`6dn3;r;&tW_SaW}UBjgtEv49a|B5pedKGc^j}|E7F?2Nm@k z&YqwG$+Z4)$bc{GubH`kDvA-R<{QdBNTgb#B{8aI?QW7P7_Lg{0J_BkTH?EN@5t@B zUy9u~>~5U5Wdl(hrvK!W+lZ|z7(Z+41I%eCYNRlvZkyyl$3;^H5gdz1{L{dG$|;C} zyumu5!8)M~Svrdn?Sw-#P>6oJ>S+KJm6~9anurQbL?%M0inI+4(h}kcqVoV6`68f% zxVhckg>5C9S-=?n}_gmo#MDC`rkp3 zJV68biYAvTDs^9>+eeI$2@nTEk*OeTIunKVa}(uR^Ac^7-gEJX}FAN7_jZvU24ZFq!0kW(kN-g%&sx=4~AP+h8nbd3Sxuy7w}|eCYD(6 z#DXr4$l-|!L5cC$h#MOAsprx1nN!k@z4^h8-YBF)2?l3h@s(|`?n*H9{XvKo)zhwe zNy4ZFqdo)k2+p`-APHHP=x3zdHexQq$95*?!mp<|ZZ6zHTRaOh5sahqN3Y$eWcc3x ziuM6OjAq4X&WNvo;Omi$wv2p26n~v?wrF3UNyWHXuE1y*6~iCQj5c1M8BVnKu4KU# z7^ZmQ58U6+XNFlxK!`;~iV=w+!vqv?B`e9^H@i2Ue+zWSy_uyGS-6KS3guuy{Kqqk zQ^Eu(vg?Rlj+FE7&+tH)QL+C0853UFE0*FcP(zk8%7rWBb?BYkRK_bHheziFNGV1= zNOEE%)(107;TXpc-Woy?aMOa<^6W9MrLfl>_vL)aYh}!9bSqv*;J!7?N(Mc+We@D8 zC@gzk?(V+O zFSYF#*?7?J>x#f!sNag@w95lMSF7wIo%2eg25E&M*~CvWnxp|bs3g&WDl5G#G*yO- zkTmkFhG@VVNxEAqBd}ayutRM&$m;OxlrgNHo0`#irXLjwEsU?^S)=IWSskrBw}10< z(X&llWlG+$TpBEs1}o&m3W#AaDL5%ODaMmNim}M@7h`fnW)RqNM7J6*fQ)Mamjo=y zD2b_>%8k09AQ1#+BKCq*8U0rvc%IfU3;+TEkT$RZ6S|?pYid%Dq$(y+vtHI%!My^OE`VMP{DtG^tm?e?c zUlTJZnFg%vSFGG@OZ!sY?$+thFPDwodONhQ*X_^^f2V8=>+OpDfwte%T>Znc@jcy+ z^_*y-I$CLKeEnvMF7PVuQg7o8y}AMr1u(X?zz2SGB)Ki6Iq`)6tN4Yx6$@-_8@RAEG9n2LQ3Z_K zBAJ@#@V+vh@|S7<97=7n6dlI)-Pqo3ANT5{2MtiVNziqro!<)*=rs&Vy9w$X8&h|- zPcgjWbBc5a;7D&h!6vU6c;Yu~*guZ#+oIsBw66j{qZ}Ge@fw=gzO%z%s0E?V?AOHi zl~Q;%Pi&!$NrNr$cXv5!HSYsx zYQm+k6lxs-VuCTU`+ce5oa~@4eGT{DeP|pOC&uVhfeykX(ZLt(2dcLU=upQb%|nxeRVaBp%r9u z=HKL+sb4Yio(YF(1vLW;2w|F>YF;xnHx&#sjUug*Lf`2$7DQ&63#z8Nmo=bDwUk; z8X#FE*{VZwT;G&P*7O~EN_42FFpm7ccnb26QI9jJcu&cd#4nKSjpEv2YCe#;j~N9m z01y|W;mk-9$9d7_HIJbSS-G$%m#5`x`U@ZL+eJl@+?SRNqMCR~D*h5%@HiQs-SU0B zA6jJ`o2)Y6XRy`eI2{VHbAQ!Ppkma)fVV5hss9u5P@*G}d&f|TRPsGj$&=%09Re_r^FD|e2;b`AhL#+M_G_h|xp-o59-J5}uoZW> zTGF1pDvW&xf-?scUnV{%|KG{OtIE;}n7pyQs5Q&n1N)50tWlI6KZQ|xaO|b${TL;r zmlBgjWfLQ+;-=BVWe;K#Ih2{naqu3{WZdNocS1DXH5I2aPvXnjdUTNY_oNEE4KtkH zMt8Rr!ugs=j=5~lbPA2g9-^3x@I?!lMs=B2K6x>8NSs^+&z% zsUpyUM?k!Nj$6p&Qge7vrte5xuqgB461igPminM5WGOiYe({(Q{CW;i93`XRf{791 zcogNCM~t}zrw)DmDsr>u{E+B>63-b2!BHTY;3{=>3t>kc3E1(YF6;=fPh#zL z>^EcGQbLIP@jfe7#ir&%3u$rFeV&sAti06cT6YGCh*mq+6DA#`)?`^m8oH&I& zeY`s2MR87Si1T=L(f>SlUKaL--}!P)pW@Dwcw6l3i8Zl^Jp-J!hI0&BYk2Wo(z^f} z_RX3L$0xa_ zj5zBC_5qQXwLZapM-QA;(Eo{c^naoCLtOX3fpw=qzXyKKmmIy)a67))7;`k1VgS@G z;rt~hwMVf$$?y9`L;ns>`Yhg01K}L27o7~$RUf?$cE1Q|E&+{IyhzQyGUZR58soly i_cC}iSiR Date: Sat, 27 Dec 2025 10:57:14 +0000 Subject: [PATCH 15/37] refactor: build WASM during deployment instead of committing it --- .gitignore | 2 -- examples/react-demo/public/compute.wasm | Bin 9703 -> 0 bytes 2 files changed, 2 deletions(-) delete mode 100644 examples/react-demo/public/compute.wasm diff --git a/.gitignore b/.gitignore index c7007ef..e355f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,6 @@ dist/ build/ *.wasm *.wasm.map -# But include the public WASM file for deployment -!examples/react-demo/public/compute.wasm # IDE .idea/ diff --git a/examples/react-demo/public/compute.wasm b/examples/react-demo/public/compute.wasm deleted file mode 100644 index f2f5f92f579d700abe24aeb8dacfcddc26a102bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9703 zcmb_iUub04c|YgeduQ&<8ENj?>)N~P*yr9E$D26Lwl5Bbf^>v;lXdIZ!D(KlS!s5y zt66C^JCg09NP2KuQ`}-oOG7AlR_owE8weq=Nio<^(z-MW#xxLG3i}WjQu1JE`qahl z?|06rKE#3$Y;P^79dz)4;b3QH z8xy`&jBat1la-yFt*tG;TW&9e7{wB~jVZOY#ZN5T=ISl65OJ1k)effAlnlD7n^EW7 zT7Mv>g5N&XP*Tb;tf*=fh11PyETobOEY|9kM2J{xaU>8ya92ILyB3HjkU~~w8#~ep zePuOMAo>_9n5W zUDf&dja9bpuFcOcuJwA|#eq1g=jZ!_!SejM&gE`UuUzb2T`3v2zSIu}-#2OCRW@%dup%WM6Mo!;^vcEzb!{#HZY zQ~%}Msk178f{n1^_ruva(bQ`O$E!7bTyL;Dl|-#DlP6VZx7JhP$jL8)oZwKfZUb~I^jNb) zX%mTs-CvM^MsLP0W2wUh6$s_oRLoq z>xYG*G)>*A5~G!@;NJ0@gslK7X$M;WiIU+Cj}RG#3TUot`--yDj-LR!z*OU`6Av?g zHlEp4XR$f3;bvqXv^PHb=%c{k@vjE`UQWGHh z*CkOWY($2_ECjZpi71YV(2h(<>?tC)hXWJj04t}(EKn}VwF@SK+-^lN7uszTJiksP zO+e%VzW|5`OsVJBA5#KWqyIjT5wO546bYvONfsC!gy)|}Q#Q;Ue1Iv>LgJ`^qvIe< zDm;=al$*xsFr))JQl+sMj&6l?sGU3`T@9#UJ1`C5M#08d zXTK`h4gK*d_nrhH|8q2jqmATtWxZm7@q}U(#+{0xSOvufR4fdc0v3u@RBW_QvHw$& z7np#`lv5cLMs--2DmbE6ViQ1oj5!VOTg$RFd4Vk-1ZyM(B!8?cacXdeaAo$x7yYJb+eG7-K4R1c!RP z0j-aA{CQv_?$AcP0s2M~oulv=1Qtd=wGNPC4$8h`zLcB*#9k!<)M(++kcW5-8~Bz< zRsWSlyqtkWF!CaFfcUJi;!6<{U^pwZ-GxJfjIhIR`3B=AbU+hiEWv#4fVLu)n1f7y z4%(2^#%RR?<`Af*3)F&57f#&?uYXI;h`*gn{wRsrZ`btu0Yadf^{iH!`=*0iaSHa^$OvsXcN@Xg3)Y4;wv~1Q zM_DN$Ws_hUNK{V)T_+{13>oZwX^%Q9^GV5R%ee!T3!{iy!byP55MZT^9=ZXd0u35@z&6({dMAnu1Njj^U?kqGzmF8^4um7SDQ}5J!N|0&Ls6Ah1-SdK z81O3%8~azuWGY=qpz(y8Dbkm;A0paI8WL>AA}3u~8RS;Nv0(D{LH40l9~i)*a#?^= z2SWePO7CRgfkzWh95`6dn3;r;&tW_SaW}UBjgtEv49a|B5pedKGc^j}|E7F?2Nm@k z&YqwG$+Z4)$bc{GubH`kDvA-R<{QdBNTgb#B{8aI?QW7P7_Lg{0J_BkTH?EN@5t@B zUy9u~>~5U5Wdl(hrvK!W+lZ|z7(Z+41I%eCYNRlvZkyyl$3;^H5gdz1{L{dG$|;C} zyumu5!8)M~Svrdn?Sw-#P>6oJ>S+KJm6~9anurQbL?%M0inI+4(h}kcqVoV6`68f% zxVhckg>5C9S-=?n}_gmo#MDC`rkp3 zJV68biYAvTDs^9>+eeI$2@nTEk*OeTIunKVa}(uR^Ac^7-gEJX}FAN7_jZvU24ZFq!0kW(kN-g%&sx=4~AP+h8nbd3Sxuy7w}|eCYD(6 z#DXr4$l-|!L5cC$h#MOAsprx1nN!k@z4^h8-YBF)2?l3h@s(|`?n*H9{XvKo)zhwe zNy4ZFqdo)k2+p`-APHHP=x3zdHexQq$95*?!mp<|ZZ6zHTRaOh5sahqN3Y$eWcc3x ziuM6OjAq4X&WNvo;Omi$wv2p26n~v?wrF3UNyWHXuE1y*6~iCQj5c1M8BVnKu4KU# z7^ZmQ58U6+XNFlxK!`;~iV=w+!vqv?B`e9^H@i2Ue+zWSy_uyGS-6KS3guuy{Kqqk zQ^Eu(vg?Rlj+FE7&+tH)QL+C0853UFE0*FcP(zk8%7rWBb?BYkRK_bHheziFNGV1= zNOEE%)(107;TXpc-Woy?aMOa<^6W9MrLfl>_vL)aYh}!9bSqv*;J!7?N(Mc+We@D8 zC@gzk?(V+O zFSYF#*?7?J>x#f!sNag@w95lMSF7wIo%2eg25E&M*~CvWnxp|bs3g&WDl5G#G*yO- zkTmkFhG@VVNxEAqBd}ayutRM&$m;OxlrgNHo0`#irXLjwEsU?^S)=IWSskrBw}10< z(X&llWlG+$TpBEs1}o&m3W#AaDL5%ODaMmNim}M@7h`fnW)RqNM7J6*fQ)Mamjo=y zD2b_>%8k09AQ1#+BKCq*8U0rvc%IfU3;+TEkT$RZ6S|?pYid%Dq$(y+vtHI%!My^OE`VMP{DtG^tm?e?c zUlTJZnFg%vSFGG@OZ!sY?$+thFPDwodONhQ*X_^^f2V8=>+OpDfwte%T>Znc@jcy+ z^_*y-I$CLKeEnvMF7PVuQg7o8y}AMr1u(X?zz2SGB)Ki6Iq`)6tN4Yx6$@-_8@RAEG9n2LQ3Z_K zBAJ@#@V+vh@|S7<97=7n6dlI)-Pqo3ANT5{2MtiVNziqro!<)*=rs&Vy9w$X8&h|- zPcgjWbBc5a;7D&h!6vU6c;Yu~*guZ#+oIsBw66j{qZ}Ge@fw=gzO%z%s0E?V?AOHi zl~Q;%Pi&!$NrNr$cXv5!HSYsx zYQm+k6lxs-VuCTU`+ce5oa~@4eGT{DeP|pOC&uVhfeykX(ZLt(2dcLU=upQb%|nxeRVaBp%r9u z=HKL+sb4Yio(YF(1vLW;2w|F>YF;xnHx&#sjUug*Lf`2$7DQ&63#z8Nmo=bDwUk; z8X#FE*{VZwT;G&P*7O~EN_42FFpm7ccnb26QI9jJcu&cd#4nKSjpEv2YCe#;j~N9m z01y|W;mk-9$9d7_HIJbSS-G$%m#5`x`U@ZL+eJl@+?SRNqMCR~D*h5%@HiQs-SU0B zA6jJ`o2)Y6XRy`eI2{VHbAQ!Ppkma)fVV5hss9u5P@*G}d&f|TRPsGj$&=%09Re_r^FD|e2;b`AhL#+M_G_h|xp-o59-J5}uoZW> zTGF1pDvW&xf-?scUnV{%|KG{OtIE;}n7pyQs5Q&n1N)50tWlI6KZQ|xaO|b${TL;r zmlBgjWfLQ+;-=BVWe;K#Ih2{naqu3{WZdNocS1DXH5I2aPvXnjdUTNY_oNEE4KtkH zMt8Rr!ugs=j=5~lbPA2g9-^3x@I?!lMs=B2K6x>8NSs^+&z% zsUpyUM?k!Nj$6p&Qge7vrte5xuqgB461igPminM5WGOiYe({(Q{CW;i93`XRf{791 zcogNCM~t}zrw)DmDsr>u{E+B>63-b2!BHTY;3{=>3t>kc3E1(YF6;=fPh#zL z>^EcGQbLIP@jfe7#ir&%3u$rFeV&sAti06cT6YGCh*mq+6DA#`)?`^m8oH&I& zeY`s2MR87Si1T=L(f>SlUKaL--}!P)pW@Dwcw6l3i8Zl^Jp-J!hI0&BYk2Wo(z^f} z_RX3L$0xa_ zj5zBC_5qQXwLZapM-QA;(Eo{c^naoCLtOX3fpw=qzXyKKmmIy)a67))7;`k1VgS@G z;rt~hwMVf$$?y9`L;ns>`Yhg01K}L27o7~$RUf?$cE1Q|E&+{IyhzQyGUZR58soly i_cC}iSiR Date: Sat, 27 Dec 2025 11:02:16 +0000 Subject: [PATCH 16/37] trigger redeploy From 84603d91acae8e142ca5ea49d8ebb5b8c557a9e8 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 11:13:25 +0000 Subject: [PATCH 17/37] feat: add @computekit/react-query package for TanStack Query integration --- package-lock.json | 67 ++++++- package.json | 5 +- packages/react-query/README.md | 156 ++++++++++++++++ packages/react-query/package.json | 63 +++++++ packages/react-query/src/index.tsx | 268 ++++++++++++++++++++++++++++ packages/react-query/tsconfig.json | 18 ++ packages/react-query/tsup.config.ts | 15 ++ 7 files changed, 583 insertions(+), 9 deletions(-) create mode 100644 packages/react-query/README.md create mode 100644 packages/react-query/package.json create mode 100644 packages/react-query/src/index.tsx create mode 100644 packages/react-query/tsconfig.json create mode 100644 packages/react-query/tsup.config.ts diff --git a/package-lock.json b/package-lock.json index f0ecfcf..2d7160f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ }, "examples/vanilla-demo": { "version": "0.0.0", + "extraneous": true, "dependencies": { "@computekit/core": "*" }, @@ -418,8 +419,8 @@ "resolved": "packages/react", "link": true }, - "node_modules/@computekit/wasm": { - "resolved": "packages/wasm", + "node_modules/@computekit/react-query": { + "resolved": "packages/react-query", "link": true }, "node_modules/@csstools/color-helpers": { @@ -1583,6 +1584,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@tanstack/query-core": { + "version": "5.90.12", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz", + "integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.12", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz", + "integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2212,6 +2241,7 @@ "integrity": "sha512-YtY5k3PiV3SyUQ6gRlR2OCn8dcVRwkpiG/k2T5buoL2ymH/Z/YbaYWbk/f9mO2HTgEtGWjPiAQrIuvA7G/63Gg==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "binaryen": "116.0.0-nightly.20240114", "long": "^5.2.4" @@ -5079,10 +5109,6 @@ "punycode": "^2.1.0" } }, - "node_modules/vanilla-demo": { - "resolved": "examples/vanilla-demo", - "link": true - }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -5973,7 +5999,7 @@ }, "packages/core": { "name": "@computekit/core", - "version": "0.1.0", + "version": "0.1.2", "license": "MIT", "devDependencies": { "@types/node": "^20.10.0", @@ -5992,12 +6018,37 @@ }, "packages/react": { "name": "@computekit/react", + "version": "0.1.2", + "license": "MIT", + "dependencies": { + "@computekit/core": "*" + }, + "devDependencies": { + "@types/react": "^18.2.45", + "react": "^18.2.0", + "tsup": "^8.0.1", + "typescript": "^5.3.3", + "vitest": "^1.1.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "packages/react-query": { + "name": "@computekit/react-query", "version": "0.1.0", "license": "MIT", "dependencies": { "@computekit/core": "*" }, "devDependencies": { + "@tanstack/react-query": "^5.17.0", "@types/react": "^18.2.45", "react": "^18.2.0", "tsup": "^8.0.1", @@ -6005,12 +6056,14 @@ "vitest": "^1.1.0" }, "peerDependencies": { + "@tanstack/react-query": ">=4.0.0", "react": ">=17.0.0" } }, "packages/wasm": { "name": "@computekit/wasm", "version": "0.1.0", + "extraneous": true, "devDependencies": { "assemblyscript": "^0.27.0" } diff --git a/package.json b/package.json index a47d421..5f71518 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,13 @@ "examples/*" ], "scripts": { - "build": "npm run build:wasm && npm run build:core && npm run build:react && npm run build:examples", + "build": "npm run build:wasm && npm run build:core && npm run build:react && npm run build:react-query && npm run build:examples", "build:wasm": "asc compute/index.ts --outFile examples/react-demo/public/compute.wasm --bindings esm --optimize", "build:core": "npm run build -w @computekit/core", "build:react": "npm run build -w @computekit/react", + "build:react-query": "npm run build -w @computekit/react-query", "build:examples": "npm run build -w examples/react-demo", - "build:packages": "npm run build:core && npm run build:react", + "build:packages": "npm run build:core && npm run build:react && npm run build:react-query", "dev": "npm run dev -w examples/react-demo", "test": "vitest", "lint": "eslint packages --ext .ts,.tsx", diff --git a/packages/react-query/README.md b/packages/react-query/README.md new file mode 100644 index 0000000..ebcd7d0 --- /dev/null +++ b/packages/react-query/README.md @@ -0,0 +1,156 @@ +# @computekit/react-query + +TanStack Query integration for [ComputeKit](https://github.com/tapava/compute-kit) - run heavy computations in Web Workers with automatic caching, background refetching, and all the goodies from React Query. + +## Why? + +If you're already using TanStack Query (React Query), this package lets you use ComputeKit as a "fetcher" while React Query handles: + +- ✅ Caching & deduplication +- ✅ Background refetching +- ✅ Stale-while-revalidate +- ✅ Retry logic +- ✅ DevTools support +- ✅ Optimistic updates (via mutations) + +## Installation + +```bash +npm install @computekit/react-query @computekit/core @tanstack/react-query +``` + +## Quick Start + +### 1. Setup Providers + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ComputeKitProvider } from '@computekit/react-query'; + +const queryClient = new QueryClient(); + +function App() { + return ( + + + + + + ); +} +``` + +### 2. Register Compute Functions + +```tsx +import { useComputeKit } from '@computekit/react-query'; + +function Setup() { + const kit = useComputeKit(); + + useEffect(() => { + kit.register('fibonacci', (n: number) => { + let a = 0, + b = 1; + for (let i = 2; i <= n; i++) [a, b] = [b, a + b]; + return b; + }); + }, [kit]); + + return ; +} +``` + +### 3. Use with React Query + +```tsx +import { useComputeQuery } from '@computekit/react-query'; + +function Fibonacci({ n }: { n: number }) { + const { data, isLoading, error } = useComputeQuery('fibonacci', n); + + if (isLoading) return
Computing...
; + if (error) return
Error: {error.message}
; + return ( +
+ fib({n}) = {data} +
+ ); +} +``` + +## API + +### `useComputeQuery(name, input, options?)` + +Execute a compute function with React Query's `useQuery`. + +```tsx +const { data, isLoading, error, refetch } = useComputeQuery('functionName', inputData, { + // React Query options + staleTime: 1000 * 60 * 5, // 5 minutes + retry: 3, + enabled: shouldRun, + + // ComputeKit options + computeOptions: { + priority: 'high', + timeout: 5000, + }, +}); +``` + +### `useComputeMutation(name, options?)` + +Execute a compute function manually with React Query's `useMutation`. + +```tsx +function ImageProcessor() { + const { mutate, data, isPending } = useComputeMutation('blur'); + + return ( + + ); +} +``` + +### `createComputeHooks(kit)` + +Create hooks without using the context provider - useful for multiple instances or custom setups. + +```tsx +import { ComputeKit } from '@computekit/core'; +import { createComputeHooks } from '@computekit/react-query'; + +const kit = new ComputeKit(); +kit.register('fibonacci', (n) => /* ... */); + +const { useQuery, useMutation } = createComputeHooks(kit); + +// Now use these hooks directly +function MyComponent() { + const { data } = useQuery('fibonacci', 50); + return
{data}
; +} +``` + +## Comparison with @computekit/react + +| Feature | `@computekit/react` | `@computekit/react-query` | +| ------------------------- | ------------------- | ------------------------- | +| Built-in state management | ✅ Yes | ❌ Uses React Query | +| Caching | ❌ Manual | ✅ Automatic | +| Background refetch | ❌ No | ✅ Yes | +| DevTools | ❌ No | ✅ React Query DevTools | +| Progress tracking | ✅ Yes | ❌ Not yet | +| Bundle size | Smaller | Requires React Query | + +**Use `@computekit/react`** if you want a simple, standalone solution. + +**Use `@computekit/react-query`** if you're already using TanStack Query and want consistent patterns across your app. + +## License + +MIT diff --git a/packages/react-query/package.json b/packages/react-query/package.json new file mode 100644 index 0000000..ca9d55d --- /dev/null +++ b/packages/react-query/package.json @@ -0,0 +1,63 @@ +{ + "name": "@computekit/react-query", + "version": "0.1.0", + "description": "TanStack Query integration for ComputeKit - lightweight async compute with caching", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist", + "src", + "README.md" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@computekit/core": "*" + }, + "peerDependencies": { + "react": ">=17.0.0", + "@tanstack/react-query": ">=4.0.0" + }, + "devDependencies": { + "@tanstack/react-query": "^5.17.0", + "@types/react": "^18.2.45", + "react": "^18.2.0", + "tsup": "^8.0.1", + "typescript": "^5.3.3", + "vitest": "^1.1.0" + }, + "keywords": [ + "react", + "react-query", + "tanstack-query", + "wasm", + "webassembly", + "workers", + "compute", + "async" + ], + "author": "Ghassen Lassoued ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tapava/compute-kit.git", + "directory": "packages/react-query" + }, + "homepage": "https://github.com/tapava/compute-kit#readme", + "bugs": { + "url": "https://github.com/tapava/compute-kit/issues" + } +} diff --git a/packages/react-query/src/index.tsx b/packages/react-query/src/index.tsx new file mode 100644 index 0000000..13c75c6 --- /dev/null +++ b/packages/react-query/src/index.tsx @@ -0,0 +1,268 @@ +/** + * ComputeKit React Query Integration + * Lightweight TanStack Query bindings for ComputeKit + */ + +import { useMemo, createContext, useContext, type ReactNode } from 'react'; +import { + useQuery, + useMutation, + type UseQueryOptions, + type UseMutationOptions, + type QueryKey, +} from '@tanstack/react-query'; +import { + ComputeKit, + type ComputeKitOptions, + type ComputeOptions, +} from '@computekit/core'; + +// ============================================================================ +// Context +// ============================================================================ + +const ComputeKitContext = createContext(null); + +export interface ComputeKitProviderProps { + /** ComputeKit options */ + options?: ComputeKitOptions; + /** Custom ComputeKit instance (if you want to share with @computekit/react) */ + instance?: ComputeKit; + /** Children */ + children: ReactNode; +} + +/** + * Provider component for ComputeKit with React Query + * + * @example + * ```tsx + * import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + * import { ComputeKitProvider } from '@computekit/react-query'; + * + * const queryClient = new QueryClient(); + * + * function App() { + * return ( + * + * + * + * + * + * ); + * } + * ``` + */ +export function ComputeKitProvider({ + options, + instance, + children, +}: ComputeKitProviderProps): React.ReactElement { + const kit = useMemo(() => { + return instance ?? new ComputeKit(options); + }, [instance, options]); + + return {children}; +} + +/** + * Get the ComputeKit instance from context + */ +export function useComputeKit(): ComputeKit { + const kit = useContext(ComputeKitContext); + if (!kit) { + throw new Error('useComputeKit must be used within a ComputeKitProvider'); + } + return kit; +} + +// ============================================================================ +// Query Hook +// ============================================================================ + +export interface UseComputeQueryOptions extends Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' +> { + /** ComputeKit run options */ + computeOptions?: ComputeOptions; +} + +/** + * Execute a registered compute function with React Query + * + * This hook integrates ComputeKit with TanStack Query, giving you: + * - Automatic caching + * - Background refetching + * - Stale-while-revalidate + * - Retry logic + * - DevTools support + * + * @example + * ```tsx + * // First, register the function + * kit.register('fibonacci', (n: number) => { + * let a = 0, b = 1; + * for (let i = 2; i <= n; i++) [a, b] = [b, a + b]; + * return b; + * }); + * + * // Then use it in your component + * function Fibonacci({ n }: { n: number }) { + * const { data, isLoading, error } = useComputeQuery('fibonacci', n); + * + * if (isLoading) return
Computing...
; + * if (error) return
Error: {error.message}
; + * return
Result: {data}
; + * } + * ``` + */ +export function useComputeQuery( + /** Name of the registered compute function */ + name: string, + /** Input to pass to the function */ + input: TInput, + /** React Query and ComputeKit options */ + options?: UseComputeQueryOptions +) { + const kit = useComputeKit(); + const { computeOptions, ...queryOptions } = options ?? {}; + + return useQuery({ + queryKey: ['compute', name, input] as const, + queryFn: async () => { + const result = await kit.run(name, input, computeOptions); + return result; + }, + ...queryOptions, + }); +} + +// ============================================================================ +// Mutation Hook +// ============================================================================ + +export interface UseComputeMutationOptions extends Omit< + UseMutationOptions, + 'mutationFn' +> { + /** ComputeKit run options */ + computeOptions?: ComputeOptions; +} + +/** + * Execute a registered compute function as a mutation + * + * Use this when you want to trigger computation manually (e.g., on button click) + * rather than automatically on mount/input change. + * + * @example + * ```tsx + * function ImageProcessor() { + * const { mutate, data, isPending, error } = useComputeMutation('blur'); + * + * return ( + *
+ * + * {data && } + *
+ * ); + * } + * ``` + */ +export function useComputeMutation( + /** Name of the registered compute function */ + name: string, + /** React Query and ComputeKit options */ + options?: UseComputeMutationOptions +) { + const kit = useComputeKit(); + const { computeOptions, ...mutationOptions } = options ?? {}; + + return useMutation({ + mutationFn: async (input: TInput) => { + const result = await kit.run(name, input, computeOptions); + return result; + }, + ...mutationOptions, + }); +} + +// ============================================================================ +// Factory for standalone usage (without context) +// ============================================================================ + +/** + * Create compute query/mutation hooks bound to a specific ComputeKit instance + * + * Use this if you don't want to use the context provider, or need multiple + * ComputeKit instances. + * + * @example + * ```tsx + * import { ComputeKit } from '@computekit/core'; + * import { createComputeHooks } from '@computekit/react-query'; + * + * const kit = new ComputeKit(); + * kit.register('fibonacci', (n: number) => { ... }); + * + * const { useQuery, useMutation } = createComputeHooks(kit); + * + * function MyComponent() { + * const { data } = useQuery('fibonacci', 50); + * return
{data}
; + * } + * ``` + */ +export function createComputeHooks(kit: ComputeKit) { + return { + /** + * Query hook bound to this ComputeKit instance + */ + useQuery: ( + name: string, + input: TInput, + options?: Omit, 'computeOptions'> & { + computeOptions?: ComputeOptions; + } + ) => { + const { computeOptions, ...queryOptions } = options ?? {}; + + return useQuery({ + queryKey: ['compute', name, input] as const, + queryFn: async () => kit.run(name, input, computeOptions), + ...queryOptions, + }); + }, + + /** + * Mutation hook bound to this ComputeKit instance + */ + useMutation: ( + name: string, + options?: Omit, 'computeOptions'> & { + computeOptions?: ComputeOptions; + } + ) => { + const { computeOptions, ...mutationOptions } = options ?? {}; + + return useMutation({ + mutationFn: async (input: TInput) => + kit.run(name, input, computeOptions), + ...mutationOptions, + }); + }, + + /** The ComputeKit instance */ + kit, + }; +} + +// ============================================================================ +// Exports +// ============================================================================ + +export type { ComputeKitOptions, ComputeOptions } from '@computekit/core'; +export { ComputeKit } from '@computekit/core'; diff --git a/packages/react-query/tsconfig.json b/packages/react-query/tsconfig.json new file mode 100644 index 0000000..33b100f --- /dev/null +++ b/packages/react-query/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx", + "jsxImportSource": "react" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.test.tsx" + ] +} \ No newline at end of file diff --git a/packages/react-query/tsup.config.ts b/packages/react-query/tsup.config.ts new file mode 100644 index 0000000..b0752cc --- /dev/null +++ b/packages/react-query/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.tsx'], + format: ['esm', 'cjs'], + dts: true, + clean: true, + sourcemap: true, + minify: false, + splitting: false, + treeshake: true, + target: 'es2022', + outDir: 'dist', + external: ['react', '@computekit/core', '@tanstack/react-query'], +}); From db0195e8ea488e107d1a97f375dc626701d9db2d Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 11:14:36 +0000 Subject: [PATCH 18/37] ci: add @computekit/react-query to publish workflow --- .github/workflows/publish.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2fa05bf..7999013 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,3 +35,9 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} continue-on-error: true + + - name: Publish @computekit/react-query + run: cd packages/react-query && npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + continue-on-error: true From 5927ca1c993d727a7e869e7610a72905cbf3fe07 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 13:46:20 +0000 Subject: [PATCH 19/37] doc: Add comprehensive documentation for ComputeKit --- .github/workflows/docs.yml | 45 +++++ README.md | 9 +- docs/.gitignore | 6 + docs/Gemfile | 6 + docs/_config.yml | 67 +++++++ docs/api-reference.md | 332 ++++++++++++++++++++++++++++++ docs/api.md | 86 ++++---- docs/assets/logo.svg | 40 ++++ docs/examples.md | 402 +++++++++++++++++++++++++++++++++++++ docs/getting-started.md | 204 +++++++++++++++++++ docs/index.md | 181 +++++++++++++++++ docs/react-hooks.md | 310 ++++++++++++++++++++++++++++ docs/react-query.md | 139 +++++++++++++ docs/wasm.md | 357 ++++++++++++++++++++++++++++++++ 14 files changed, 2142 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/.gitignore create mode 100644 docs/Gemfile create mode 100644 docs/_config.yml create mode 100644 docs/api-reference.md create mode 100644 docs/assets/logo.svg create mode 100644 docs/examples.md create mode 100644 docs/getting-started.md create mode 100644 docs/index.md create mode 100644 docs/react-hooks.md create mode 100644 docs/react-query.md create mode 100644 docs/wasm.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..ed54dde --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,45 @@ +name: Deploy Documentation to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: 'pages' + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./docs + destination: ./_site + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + + deploy: + runs-on: ubuntu-latest + needs: build + permissions: + pages: write + id-token: write + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 49f52fe..83e17fa 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/) -[Getting Started](#-getting-started) • [Examples](#-examples) • [API](#-api) • [React Hooks](#-react-hooks) • [WASM](#-webassembly-support) +[📚 Documentation](https://tapava.github.io/compute-kit) • [Getting Started](#-getting-started) • [Examples](#-examples) • [API](#-api) • [React Hooks](#-react-hooks) • [WASM](#-webassembly-support) @@ -542,8 +542,8 @@ Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) ```bash # Clone the repo -git clone https://github.com/your-username/computekit.git -cd computekit +git clone https://github.com/tapava/compute-kit.git +cd compute-kit # Install dependencies npm install @@ -571,6 +571,7 @@ MIT © [Ghassen Lassoued](https://github.com/tapava) Built with ❤️ for the web platform

- ⭐ Star on GitHub + 📚 Read the Docs • + ⭐ Star on GitHub

diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..e913d3b --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,6 @@ +# Ignore Jekyll build output +_site/ +.sass-cache/ +.jekyll-cache/ +.jekyll-metadata +vendor/ diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..23ecc60 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +gem "jekyll", "~> 4.3" +gem "just-the-docs", "~> 0.8" +gem "jekyll-seo-tag" +gem "jekyll-include-cache" diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..42447c1 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,67 @@ +title: ComputeKit +description: The React-first toolkit for WASM and Web Workers +url: 'https://tapava.github.io' +baseurl: '/compute-kit' + +remote_theme: just-the-docs/just-the-docs@v0.8.2 + +color_scheme: dark + +logo: '/assets/logo.svg' + +aux_links: + GitHub: https://github.com/tapava/compute-kit + npm: https://www.npmjs.com/package/@computekit/core + +aux_links_new_tab: true + +heading_anchors: true + +search_enabled: true +search: + heading_level: 2 + previews: 3 + preview_words_before: 5 + preview_words_after: 10 + tokenizer_separator: /[\s/]+/ + rel_url: true + button: false + +nav_enabled: true +nav_sort: case_insensitive + +back_to_top: true +back_to_top_text: 'Back to top' + +footer_content: 'Copyright © 2024-2025 Ghassen Lassoued. Distributed under the MIT license.' + +ga_tracking: +ga_tracking_anonymize_ip: true + +callouts: + warning: + title: Warning + color: yellow + note: + title: Note + color: blue + tip: + title: Tip + color: green + +plugins: + - jekyll-seo-tag + - jekyll-include-cache + +kramdown: + syntax_highlighter_opts: + block: + line_numbers: false + +compress_html: + clippings: all + comments: all + endings: all + startings: [] + blanklines: false + profile: false diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..55d7df9 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,332 @@ +--- +layout: default +title: API Reference +nav_order: 4 +description: 'Complete API reference for ComputeKit' +permalink: /api-reference +--- + +# API Reference + +{: .no_toc } + +Complete API documentation for the ComputeKit core library. +{: .fs-6 .fw-300 } + +## Table of contents + +{: .no_toc .text-delta } + +1. TOC + {:toc} + +--- + +## ComputeKit Class + +The main entry point for using ComputeKit. + +### Constructor + +```typescript +new ComputeKit(options?: ComputeKitOptions) +``` + +### ComputeKitOptions + +| Property | Type | Default | Description | +| -------------------- | ---------- | -------------------------------------- | ------------------------------------- | +| `maxWorkers` | `number` | `navigator.hardwareConcurrency \|\| 4` | Maximum number of workers in the pool | +| `timeout` | `number` | `30000` | Default timeout for operations (ms) | +| `debug` | `boolean` | `false` | Enable debug logging | +| `workerPath` | `string` | `''` | Custom path to worker script | +| `useSharedMemory` | `boolean` | `true` | Use SharedArrayBuffer when available | +| `remoteDependencies` | `string[]` | `[]` | External scripts to load in workers | + +--- + +## Methods + +### initialize() + +Manually initialize the worker pool. Called automatically on first `run()`. + +```typescript +const kit = new ComputeKit(); +await kit.initialize(); // Optional: eager initialization +``` + +### register() + +Register a compute function. + +```typescript +register( + name: string, + fn: (input: TInput, context: ComputeContext) => TOutput | Promise +): this +``` + +**Parameters:** + +- `name` - Unique identifier for the function +- `fn` - The function to execute (runs in a Web Worker) + +**Returns:** `this` (for chaining) + +```typescript +kit.register('double', (n: number) => n * 2); + +kit.register('asyncTask', async (data, { reportProgress }) => { + // Report progress during long operations + reportProgress({ percent: 50 }); + return await processData(data); +}); + +// Chaining +kit.register('add', (a, b) => a + b).register('multiply', (a, b) => a * b); +``` + +### run() + +Execute a registered function. + +```typescript +run( + name: string, + input: TInput, + options?: ComputeOptions +): Promise +``` + +**Parameters:** + +- `name` - Name of the registered function +- `input` - Input data (will be serialized) +- `options` - Optional execution options + +```typescript +const result = await kit.run('double', 21); +console.log(result); // 42 +``` + +### runWithMetadata() + +Execute a function and receive metadata about the execution. + +```typescript +runWithMetadata( + name: string, + input: TInput, + options?: ComputeOptions +): Promise> +``` + +```typescript +const result = await kit.runWithMetadata('heavy', data); +console.log(`Took ${result.duration}ms on worker ${result.workerId}`); +``` + +### getStats() + +Get current worker pool statistics. + +```typescript +const stats = kit.getStats(); +console.log(`Active workers: ${stats.activeWorkers}`); +console.log(`Queue length: ${stats.queueLength}`); +``` + +### isWasmSupported() + +Check if WebAssembly is supported in the current environment. + +```typescript +if (kit.isWasmSupported()) { + // Load WASM module +} +``` + +### terminate() + +Terminate all workers and clean up resources. + +```typescript +await kit.terminate(); +``` + +--- + +## ComputeOptions + +Options for individual compute operations. + +| Property | Type | Description | +| ------------ | ------------------------------------- | ------------------------------------- | +| `timeout` | `number` | Operation timeout in ms | +| `transfer` | `ArrayBuffer[]` | ArrayBuffers to transfer (not copy) | +| `priority` | `number` | Priority level (0-10, higher = first) | +| `signal` | `AbortSignal` | Abort signal for cancellation | +| `onProgress` | `(progress: ComputeProgress) => void` | Progress callback | + +```typescript +const controller = new AbortController(); + +await kit.run('task', data, { + timeout: 5000, + priority: 10, + signal: controller.signal, + onProgress: (p) => console.log(`${p.percent}%`), +}); + +// Cancel the operation +controller.abort(); +``` + +--- + +## ComputeProgress + +Progress information for long-running tasks. + +| Property | Type | Description | +| ------------------------ | ---------- | --------------------------- | +| `percent` | `number` | Progress percentage (0-100) | +| `phase` | `string?` | Current phase name | +| `estimatedTimeRemaining` | `number?` | Estimated ms remaining | +| `data` | `unknown?` | Additional custom data | + +--- + +## ComputeResult + +Result wrapper with execution metadata. + +| Property | Type | Description | +| ---------- | --------- | ------------------------------------ | +| `data` | `T` | The computed result | +| `duration` | `number` | Execution time in ms | +| `cached` | `boolean` | Whether result was cached | +| `workerId` | `string` | ID of the worker that processed this | + +--- + +## PoolStats + +Worker pool statistics. + +| Property | Type | Description | +| --------------------- | -------------- | -------------------------- | +| `workers` | `WorkerInfo[]` | Info about each worker | +| `totalWorkers` | `number` | Total worker count | +| `activeWorkers` | `number` | Currently busy workers | +| `idleWorkers` | `number` | Currently idle workers | +| `queueLength` | `number` | Tasks waiting in queue | +| `tasksCompleted` | `number` | Total completed tasks | +| `tasksFailed` | `number` | Total failed tasks | +| `averageTaskDuration` | `number` | Average task duration (ms) | + +--- + +## Event Handling + +ComputeKit extends EventEmitter and emits events: + +```typescript +kit.on('worker:created', (info) => { + console.log('New worker:', info.id); +}); + +kit.on('worker:terminated', (info) => { + console.log('Worker terminated:', info.id); +}); + +kit.on('task:start', (taskId, name) => { + console.log(`Starting ${name}`); +}); + +kit.on('task:complete', (taskId, duration) => { + console.log(`Done in ${duration}ms`); +}); + +kit.on('task:error', (taskId, error) => { + console.error('Task failed:', error); +}); + +kit.on('task:progress', (taskId, progress) => { + console.log(`${progress.percent}%`); +}); +``` + +--- + +## Utility Functions + +### isWasmSupported() + +Check if WebAssembly is available. + +```typescript +import { isWasmSupported } from '@computekit/core'; + +if (isWasmSupported()) { + // Use WASM +} +``` + +### isSharedArrayBufferAvailable() + +Check if SharedArrayBuffer is available. + +```typescript +import { isSharedArrayBufferAvailable } from '@computekit/core'; + +if (isSharedArrayBufferAvailable()) { + // Use shared memory +} +``` + +### getHardwareConcurrency() + +Get the number of logical CPU cores. + +```typescript +import { getHardwareConcurrency } from '@computekit/core'; + +const cores = getHardwareConcurrency(); +console.log(`${cores} CPU cores available`); +``` + +### findTransferables() + +Detect transferable objects in data for efficient worker communication. + +```typescript +import { findTransferables } from '@computekit/core'; + +const data = { buffer: new ArrayBuffer(1024), values: [1, 2, 3] }; +const transferables = findTransferables(data); +// [ArrayBuffer(1024)] +``` + +--- + +## Error Handling + +ComputeKit throws errors in these cases: + +```typescript +try { + await kit.run('unknown', data); +} catch (error) { + if (error.message.includes('not registered')) { + // Function not registered + } else if (error.message.includes('timed out')) { + // Timeout + } else if (error.message.includes('aborted')) { + // Cancelled via AbortSignal + } else { + // Worker error + } +} +``` diff --git a/docs/api.md b/docs/api.md index a9814da..ab2a40c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,3 +1,11 @@ +--- +layout: default +title: Core API (Detailed) +nav_order: 8 +description: 'Detailed API reference for @computekit/core' +permalink: /core-api +--- + # @computekit/core API Reference Complete API documentation for the ComputeKit core library. @@ -24,13 +32,13 @@ new ComputeKit(options?: ComputeKitOptions) #### ComputeKitOptions -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `maxWorkers` | `number` | `navigator.hardwareConcurrency \|\| 4` | Maximum number of workers in the pool | -| `timeout` | `number` | `30000` | Default timeout for operations (ms) | -| `debug` | `boolean` | `false` | Enable debug logging | -| `workerPath` | `string` | `''` | Custom path to worker script | -| `useSharedMemory` | `boolean` | `true` | Use SharedArrayBuffer when available | +| Property | Type | Default | Description | +| ----------------- | --------- | -------------------------------------- | ------------------------------------- | +| `maxWorkers` | `number` | `navigator.hardwareConcurrency \|\| 4` | Maximum number of workers in the pool | +| `timeout` | `number` | `30000` | Default timeout for operations (ms) | +| `debug` | `boolean` | `false` | Enable debug logging | +| `workerPath` | `string` | `''` | Custom path to worker script | +| `useSharedMemory` | `boolean` | `true` | Use SharedArrayBuffer when available | ### Methods @@ -56,6 +64,7 @@ kit.register('asyncTask', async (data) => { ``` **Parameters:** + - `name` - Unique identifier for the function - `fn` - The function to execute (runs in a Web Worker) @@ -71,6 +80,7 @@ console.log(result); // 42 ``` **Parameters:** + - `name` - Name of the registered function - `input` - Input data (will be serialized) - `options` - Optional execution options @@ -115,13 +125,13 @@ await kit.terminate(); Options for individual compute operations. -| Property | Type | Description | -|----------|------|-------------| -| `timeout` | `number` | Operation timeout in ms | -| `transfer` | `ArrayBuffer[]` | ArrayBuffers to transfer (not copy) | -| `priority` | `number` | Priority level (0-10, higher = first) | -| `signal` | `AbortSignal` | Abort signal for cancellation | -| `onProgress` | `(progress: ComputeProgress) => void` | Progress callback | +| Property | Type | Description | +| ------------ | ------------------------------------- | ------------------------------------- | +| `timeout` | `number` | Operation timeout in ms | +| `transfer` | `ArrayBuffer[]` | ArrayBuffers to transfer (not copy) | +| `priority` | `number` | Priority level (0-10, higher = first) | +| `signal` | `AbortSignal` | Abort signal for cancellation | +| `onProgress` | `(progress: ComputeProgress) => void` | Progress callback | ```typescript const controller = new AbortController(); @@ -140,12 +150,12 @@ await kit.run('task', data, { Progress information for long-running tasks. -| Property | Type | Description | -|----------|------|-------------| -| `percent` | `number` | Progress percentage (0-100) | -| `phase` | `string?` | Current phase name | -| `estimatedTimeRemaining` | `number?` | Estimated ms remaining | -| `data` | `unknown?` | Additional data | +| Property | Type | Description | +| ------------------------ | ---------- | --------------------------- | +| `percent` | `number` | Progress percentage (0-100) | +| `phase` | `string?` | Current phase name | +| `estimatedTimeRemaining` | `number?` | Estimated ms remaining | +| `data` | `unknown?` | Additional data | --- @@ -153,12 +163,12 @@ Progress information for long-running tasks. Result wrapper with execution metadata. -| Property | Type | Description | -|----------|------|-------------| -| `data` | `T` | The computed result | -| `duration` | `number` | Execution time in ms | -| `cached` | `boolean` | Whether result was cached | -| `workerId` | `string` | ID of the worker that processed this | +| Property | Type | Description | +| ---------- | --------- | ------------------------------------ | +| `data` | `T` | The computed result | +| `duration` | `number` | Execution time in ms | +| `cached` | `boolean` | Whether result was cached | +| `workerId` | `string` | ID of the worker that processed this | --- @@ -166,16 +176,16 @@ Result wrapper with execution metadata. Worker pool statistics. -| Property | Type | Description | -|----------|------|-------------| -| `workers` | `WorkerInfo[]` | Info about each worker | -| `totalWorkers` | `number` | Total worker count | -| `activeWorkers` | `number` | Currently busy workers | -| `idleWorkers` | `number` | Currently idle workers | -| `queueLength` | `number` | Tasks waiting in queue | -| `tasksCompleted` | `number` | Total completed tasks | -| `tasksFailed` | `number` | Total failed tasks | -| `averageTaskDuration` | `number` | Average task duration (ms) | +| Property | Type | Description | +| --------------------- | -------------- | -------------------------- | +| `workers` | `WorkerInfo[]` | Info about each worker | +| `totalWorkers` | `number` | Total worker count | +| `activeWorkers` | `number` | Currently busy workers | +| `idleWorkers` | `number` | Currently idle workers | +| `queueLength` | `number` | Tasks waiting in queue | +| `tasksCompleted` | `number` | Total completed tasks | +| `tasksFailed` | `number` | Total failed tasks | +| `averageTaskDuration` | `number` | Average task duration (ms) | --- @@ -192,7 +202,7 @@ Load a WASM module from various sources. const module = await loadWasmModule('/path/to/module.wasm'); // From ArrayBuffer -const bytes = await fetch('/module.wasm').then(r => r.arrayBuffer()); +const bytes = await fetch('/module.wasm').then((r) => r.arrayBuffer()); const module = await loadWasmModule(bytes); // From base64 @@ -207,7 +217,7 @@ Load and instantiate a WASM module. const { module, instance } = await loadAndInstantiate({ source: '/module.wasm', imports: { - env: { log: console.log } + env: { log: console.log }, }, memory: { initial: 256, diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..44ecb90 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..cf22ebd --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,402 @@ +--- +layout: default +title: Examples +nav_order: 6 +description: 'Real-world examples using ComputeKit' +permalink: /examples +--- + +# Examples + +{: .no_toc } + +Real-world examples demonstrating ComputeKit's capabilities. +{: .fs-6 .fw-300 } + +## Table of contents + +{: .no_toc .text-delta } + +1. TOC + {:toc} + +--- + +## Basic Examples + +### Fibonacci Sequence + +Calculate large Fibonacci numbers without freezing the UI: + +```typescript +import { ComputeKit } from '@computekit/core'; + +const kit = new ComputeKit(); + +kit.register('fibonacci', (n: number) => { + if (n <= 1) return n; + let a = 0n, + b = 1n; + for (let i = 2; i <= n; i++) { + [a, b] = [b, a + b]; + } + return b.toString(); +}); + +// Calculate fib(1000) without blocking +const result = await kit.run('fibonacci', 1000); +console.log(result); // Very large number! +``` + +### Sum Large Array + +Process millions of items without blocking: + +```typescript +kit.register('sum', (arr: number[]) => { + return arr.reduce((a, b) => a + b, 0); +}); + +const bigArray = Array.from({ length: 10_000_000 }, () => Math.random()); +const sum = await kit.run('sum', bigArray); +console.log(sum); +``` + +--- + +## Image Processing + +### Grayscale Conversion + +```typescript +kit.register('grayscale', (imageData: number[]) => { + const result = new Uint8ClampedArray(imageData.length); + + for (let i = 0; i < imageData.length; i += 4) { + const avg = (imageData[i] + imageData[i + 1] + imageData[i + 2]) / 3; + result[i] = avg; // R + result[i + 1] = avg; // G + result[i + 2] = avg; // B + result[i + 3] = imageData[i + 3]; // A (preserve alpha) + } + + return Array.from(result); +}); + +// Usage with Canvas +const canvas = document.querySelector('canvas'); +const ctx = canvas.getContext('2d'); +const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + +const grayscaleData = await kit.run('grayscale', Array.from(imageData.data)); +const newImageData = new ImageData( + new Uint8ClampedArray(grayscaleData), + canvas.width, + canvas.height +); +ctx.putImageData(newImageData, 0, 0); +``` + +### Image Blur with Progress + +```typescript +kit.register('blur', async (input, { reportProgress }) => { + const { data, width, height, radius } = input; + const result = new Uint8ClampedArray(data.length); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + let r = 0, + g = 0, + b = 0, + count = 0; + + for (let dy = -radius; dy <= radius; dy++) { + for (let dx = -radius; dx <= radius; dx++) { + const nx = x + dx; + const ny = y + dy; + + if (nx >= 0 && nx < width && ny >= 0 && ny < height) { + const i = (ny * width + nx) * 4; + r += data[i]; + g += data[i + 1]; + b += data[i + 2]; + count++; + } + } + } + + const i = (y * width + x) * 4; + result[i] = r / count; + result[i + 1] = g / count; + result[i + 2] = b / count; + result[i + 3] = data[i + 3]; + } + + // Report progress every row + if (y % 10 === 0) { + reportProgress({ percent: (y / height) * 100 }); + } + } + + return Array.from(result); +}); +``` + +--- + +## Data Processing + +### CSV Parsing + +```typescript +kit.register('parseCSV', (csv: string) => { + const lines = csv.split('\n'); + const headers = lines[0].split(',').map((h) => h.trim()); + + return lines.slice(1).map((line) => { + const values = line.split(','); + return headers.reduce( + (obj, header, i) => { + obj[header] = values[i]?.trim(); + return obj; + }, + {} as Record + ); + }); +}); + +const csvData = await fetch('/large-data.csv').then((r) => r.text()); +const parsed = await kit.run('parseCSV', csvData); +``` + +### JSON Processing with Progress + +```typescript +kit.register('processRecords', async (records, { reportProgress }) => { + const total = records.length; + const results = []; + + for (let i = 0; i < total; i++) { + // Expensive processing + const processed = { + ...records[i], + score: calculateScore(records[i]), + category: categorize(records[i]), + }; + results.push(processed); + + // Report every 1000 records + if (i % 1000 === 0) { + reportProgress({ + percent: (i / total) * 100, + phase: 'Processing', + data: { processed: i, total }, + }); + } + } + + return results; +}); +``` + +--- + +## Mathematical Computations + +### Mandelbrot Set + +```typescript +kit.register( + 'mandelbrot', + (config: { + width: number; + height: number; + xMin: number; + xMax: number; + yMin: number; + yMax: number; + maxIterations: number; + }) => { + const { width, height, xMin, xMax, yMin, yMax, maxIterations } = config; + const data = new Uint8Array(width * height * 4); + + for (let py = 0; py < height; py++) { + for (let px = 0; px < width; px++) { + const x0 = xMin + (px / width) * (xMax - xMin); + const y0 = yMin + (py / height) * (yMax - yMin); + + let x = 0, + y = 0; + let iteration = 0; + + while (x * x + y * y <= 4 && iteration < maxIterations) { + const xTemp = x * x - y * y + x0; + y = 2 * x * y + y0; + x = xTemp; + iteration++; + } + + const i = (py * width + px) * 4; + const color = iteration === maxIterations ? 0 : (iteration / maxIterations) * 255; + + data[i] = color; + data[i + 1] = color * 0.5; + data[i + 2] = color * 2; + data[i + 3] = 255; + } + } + + return Array.from(data); + } +); +``` + +### Matrix Multiplication + +```typescript +kit.register('matrixMultiply', (input: { a: number[][]; b: number[][] }) => { + const { a, b } = input; + const rows = a.length; + const cols = b[0].length; + const n = b.length; + + const result: number[][] = Array(rows) + .fill(null) + .map(() => Array(cols).fill(0)); + + for (let i = 0; i < rows; i++) { + for (let j = 0; j < cols; j++) { + for (let k = 0; k < n; k++) { + result[i][j] += a[i][k] * b[k][j]; + } + } + } + + return result; +}); +``` + +--- + +## React Examples + +### Debounced Search with Compute + +```tsx +import { useCompute } from '@computekit/react'; +import { useState, useMemo } from 'react'; +import { useDebouncedValue } from './hooks'; + +function SearchComponent() { + const [query, setQuery] = useState(''); + const debouncedQuery = useDebouncedValue(query, 300); + + const { data: results, loading } = useCompute<{ query: string; items: Item[] }, Item[]>( + 'fuzzySearch', + { + runOnMount: false, + } + ); + + // Register the fuzzy search function + useEffect(() => { + kit.register('fuzzySearch', ({ query, items }) => { + return items + .filter( + (item) => + item.name.toLowerCase().includes(query.toLowerCase()) || + item.description.toLowerCase().includes(query.toLowerCase()) + ) + .sort((a, b) => { + // Score by relevance + const aScore = getMatchScore(a, query); + const bScore = getMatchScore(b, query); + return bScore - aScore; + }); + }); + }, []); + + return ( +
+ setQuery(e.target.value)} + placeholder="Search..." + /> + {loading && } + {results?.map((item) => ( + + ))} +
+ ); +} +``` + +### Real-time Data Visualization + +```tsx +function DataVisualization() { + const { data, loading, run } = useCompute('processData'); + const stats = usePoolStats(500); + + useEffect(() => { + const interval = setInterval(() => { + const newData = generateRandomData(10000); + run(newData); + }, 1000); + + return () => clearInterval(interval); + }, [run]); + + return ( +
+
+ Workers: {stats.activeWorkers}/{stats.totalWorkers} +
+ {loading && } + {data && } +
+ ); +} +``` + +--- + +## Using External Libraries + +Load external libraries inside workers: + +```typescript +const kit = new ComputeKit({ + remoteDependencies: [ + 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/mathjs/11.8.0/math.min.js', + ], +}); + +kit.register('advancedMath', (expression: string) => { + // @ts-ignore - math.js loaded via importScripts + return math.evaluate(expression); +}); + +kit.register('processData', (data: number[]) => { + // @ts-ignore - lodash loaded via importScripts + return _.chain(data) + .filter((n) => n > 0) + .map((n) => n * 2) + .sortBy() + .value(); +}); + +const result = await kit.run('advancedMath', 'sqrt(16) + sin(pi/2)'); +console.log(result); // 5 +``` + +--- + +## Live Demo + +Try the interactive demo: + +[View Live Demo]({{ site.baseurl }}/demo.html){: .btn .btn-primary } diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..5840c06 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,204 @@ +--- +layout: default +title: Getting Started +nav_order: 2 +description: 'Get started with ComputeKit in minutes' +permalink: /getting-started +--- + +# Getting Started + +{: .no_toc } + +Get up and running with ComputeKit in just a few minutes. +{: .fs-6 .fw-300 } + +## Table of contents + +{: .no_toc .text-delta } + +1. TOC + {:toc} + +--- + +## Installation + +### Core Package + +```bash +npm install @computekit/core +``` + +### With React Bindings + +```bash +npm install @computekit/core @computekit/react +``` + +### With React Query Integration + +```bash +npm install @computekit/core @computekit/react-query @tanstack/react-query +``` + +--- + +## Basic Usage + +### Vanilla JavaScript/TypeScript + +```typescript +import { ComputeKit } from '@computekit/core'; + +// Create an instance +const kit = new ComputeKit(); + +// Register compute functions +kit.register('fibonacci', (n: number) => { + if (n <= 1) return n; + let a = 0, + b = 1; + for (let i = 2; i <= n; i++) { + [a, b] = [b, a + b]; + } + return b; +}); + +kit.register('sum', (arr: number[]) => { + return arr.reduce((a, b) => a + b, 0); +}); + +// Run computations (non-blocking!) +const fib = await kit.run('fibonacci', 50); +console.log(fib); // 12586269025 + +const total = await kit.run('sum', [1, 2, 3, 4, 5]); +console.log(total); // 15 +``` + +### React + +```tsx +import { ComputeKitProvider, useComputeKit, useCompute } from '@computekit/react'; +import { useEffect } from 'react'; + +// Wrap your app +function App() { + return ( + + + + ); +} + +// Register functions once +function MyApp() { + const kit = useComputeKit(); + + useEffect(() => { + kit.register('fibonacci', (n: number) => { + if (n <= 1) return n; + let a = 0, + b = 1; + for (let i = 2; i <= n; i++) { + [a, b] = [b, a + b]; + } + return b; + }); + }, [kit]); + + return ; +} + +// Use in components +function Calculator() { + const { data, loading, error, run } = useCompute('fibonacci'); + + return ( +
+ + {data &&

Result: {data}

} + {error &&

Error: {error.message}

} +
+ ); +} +``` + +--- + +## Configuration Options + +```typescript +const kit = new ComputeKit({ + // Maximum number of workers in the pool + maxWorkers: navigator.hardwareConcurrency || 4, + + // Default timeout for operations (ms) + timeout: 30000, + + // Enable debug logging + debug: false, + + // Custom worker script path + workerPath: '', + + // Use SharedArrayBuffer when available + useSharedMemory: true, + + // External scripts to load in workers + remoteDependencies: [ + 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js', + ], +}); +``` + +--- + +## Vite/Webpack Configuration + +For SharedArrayBuffer support, you need to add COOP/COEP headers: + +### Vite + +```typescript +// vite.config.ts +export default { + server: { + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }, +}; +``` + +### Webpack (Next.js) + +```javascript +// next.config.js +module.exports = { + async headers() { + return [ + { + source: '/(.*)', + headers: [ + { key: 'Cross-Origin-Opener-Policy', value: 'same-origin' }, + { key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' }, + ], + }, + ]; + }, +}; +``` + +--- + +## Next Steps + +- Learn about [React Hooks]({{ site.baseurl }}/react-hooks) for the full React experience +- Check the [API Reference]({{ site.baseurl }}/api-reference) for all available methods +- Explore [WASM integration]({{ site.baseurl }}/wasm) for maximum performance +- See [Examples]({{ site.baseurl }}/examples) for real-world use cases diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..1ebe461 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,181 @@ +--- +layout: home +title: Home +nav_order: 1 +description: 'ComputeKit - The React-first toolkit for WASM and Web Workers' +permalink: / +--- + +
+ ComputeKit Logo +
+ +# ComputeKit + +{: .fs-9 } + +The React-first toolkit for WASM and Web Workers +{: .fs-6 .fw-300 } + +Run heavy computations with React hooks. Use WASM for native-speed performance. Keep your UI at 60fps. +{: .fs-5 .fw-300 } + +[Get Started](#getting-started){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } +[View on GitHub](https://github.com/tapava/compute-kit){: .btn .fs-5 .mb-4 .mb-md-0 } + +--- + +## ✨ Features + +| Feature | Description | +| :----------------------- | :----------------------------------------------------------------------------- | +| ⚛️ **React-first** | Purpose-built hooks like `useCompute` with loading, error, and progress states | +| 🦀 **WASM integration** | Load and call AssemblyScript/Rust WASM modules with zero boilerplate | +| 🚀 **Non-blocking** | Everything runs in Web Workers, keeping your UI at 60fps | +| 🔧 **Zero config** | No manual worker files, postMessage handlers, or WASM glue code | +| 📦 **Tiny** | Core library is ~3KB gzipped | +| 🎯 **TypeScript** | Full type safety for your compute functions and WASM bindings | +| 🔄 **Worker pool** | Automatic load balancing across CPU cores | +| 📊 **Progress tracking** | Built-in progress reporting for long-running tasks | + +--- + +## 🤔 Why ComputeKit? + +You _can_ use Web Workers and WASM without a library. But here's the reality: + +| Task | Without ComputeKit | With ComputeKit | +| ----------------- | ------------------------------------------------------------------- | ---------------------------------- | +| Web Worker setup | Create separate `.js` files, handle `postMessage`, manage callbacks | `kit.register('fn', myFunc)` | +| WASM loading | Fetch, instantiate, memory management, glue code | `await loadWasmModule('/my.wasm')` | +| React integration | Manual state, effects, cleanup, abort handling | `useCompute()` hook | +| Worker pooling | Build your own pool, queue, and load balancer | Built-in | +| TypeScript | Tricky worker typing, no WASM types | Full type inference | +| Error handling | Try-catch across message boundaries | Automatic with React error states | + +**ComputeKit's unique value:** The only library that combines **React hooks + WASM + Worker pool** into one cohesive, type-safe developer experience. + +--- + +## 🎯 When to use ComputeKit + +| ✅ Use ComputeKit | ❌ Don't use ComputeKit | +| ---------------------------------- | ---------------------------- | +| Image/video processing | Simple DOM updates | +| Data transformations (100K+ items) | Small array operations | +| Mathematical computations | API calls (use native fetch) | +| Parsing large files | String formatting | +| Cryptographic operations | UI state management | +| Real-time data analysis | Small form validations | + +--- + +## 📦 Installation + +```bash +# npm +npm install @computekit/core + +# With React bindings +npm install @computekit/core @computekit/react + +# pnpm +pnpm add @computekit/core @computekit/react + +# yarn +yarn add @computekit/core @computekit/react +``` + +--- + +## Getting Started + +### Basic Usage (Vanilla JS) + +```typescript +import { ComputeKit } from '@computekit/core'; + +// 1. Create a ComputeKit instance +const kit = new ComputeKit(); + +// 2. Register a compute function +kit.register('fibonacci', (n: number) => { + if (n <= 1) return n; + let a = 0, + b = 1; + for (let i = 2; i <= n; i++) { + [a, b] = [b, a + b]; + } + return b; +}); + +// 3. Run it (non-blocking!) +const result = await kit.run('fibonacci', 50); +console.log(result); // 12586269025 — UI never froze! +``` + +### React Usage + +```tsx +import { ComputeKitProvider, useComputeKit, useCompute } from '@computekit/react'; +import { useEffect } from 'react'; + +// 1. Wrap your app with the provider +function App() { + return ( + + + + ); +} + +// 2. Register functions at the app level +function AppContent() { + const kit = useComputeKit(); + + useEffect(() => { + kit.register('fibonacci', (n: number) => { + if (n <= 1) return n; + let a = 0, + b = 1; + for (let i = 2; i <= n; i++) { + [a, b] = [b, a + b]; + } + return b; + }); + }, [kit]); + + return ; +} + +// 3. Use the hook in any component +function Calculator() { + const { data, loading, error, run } = useCompute('fibonacci'); + + return ( +
+ + {data &&

Result: {data}

} + {error &&

Error: {error.message}

} +
+ ); +} +``` + +--- + +## Quick Links + +- [Getting Started Guide]({{ site.baseurl }}/getting-started) +- [React Hooks Reference]({{ site.baseurl }}/react-hooks) +- [API Reference]({{ site.baseurl }}/api-reference) +- [WASM Guide]({{ site.baseurl }}/wasm) +- [Examples]({{ site.baseurl }}/examples) + +--- + +## 📄 License + +MIT © [Ghassen Lassoued](https://github.com/tapava) diff --git a/docs/react-hooks.md b/docs/react-hooks.md new file mode 100644 index 0000000..722328d --- /dev/null +++ b/docs/react-hooks.md @@ -0,0 +1,310 @@ +--- +layout: default +title: React Hooks +nav_order: 3 +description: 'React hooks for ComputeKit' +permalink: /react-hooks +--- + +# React Hooks + +{: .no_toc } + +ComputeKit provides purpose-built React hooks for seamless integration. +{: .fs-6 .fw-300 } + +## Table of contents + +{: .no_toc .text-delta } + +1. TOC + {:toc} + +--- + +## ComputeKitProvider + +Wrap your application with the provider to enable all hooks. + +```tsx +import { ComputeKitProvider } from '@computekit/react'; + +function App() { + return ( + + + + ); +} +``` + +### Provider Options + +| Option | Type | Default | Description | +| -------------------- | ---------- | ------------------------------- | ---------------------------- | +| `maxWorkers` | `number` | `navigator.hardwareConcurrency` | Max workers in the pool | +| `timeout` | `number` | `30000` | Default timeout in ms | +| `debug` | `boolean` | `false` | Enable debug logging | +| `remoteDependencies` | `string[]` | `[]` | External scripts for workers | + +--- + +## useComputeKit + +Access the ComputeKit instance directly. + +```tsx +import { useComputeKit } from '@computekit/react'; + +function MyComponent() { + const kit = useComputeKit(); + + useEffect(() => { + // Register functions + kit.register('myFunction', (data) => { + // Heavy computation + return result; + }); + }, [kit]); + + return
...
; +} +``` + +--- + +## useCompute + +The primary hook for running compute functions. + +```tsx +import { useCompute } from '@computekit/react'; + +function Calculator() { + const { + data, // Result data (TOutput | undefined) + loading, // Boolean loading state + error, // Error if failed (Error | null) + progress, // Progress info (ComputeProgress | undefined) + status, // 'idle' | 'running' | 'success' | 'error' | 'cancelled' + run, // Function to execute + reset, // Reset state to idle + cancel, // Cancel current operation + } = useCompute('functionName'); + + return ( +
+ + + {progress && } + {data &&

Result: {data}

} + {error &&

{error.message}

} +
+ ); +} +``` + +### Options + +```tsx +const { run } = useCompute('functionName', { + // Initial input to run on mount + initialInput: undefined, + + // Run immediately on mount + runOnMount: false, + + // Timeout for this specific function + timeout: 5000, + + // Progress callback + onProgress: (progress) => { + console.log(`${progress.percent}% complete`); + }, + + // Success callback + onSuccess: (data) => { + console.log('Completed:', data); + }, + + // Error callback + onError: (error) => { + console.error('Failed:', error); + }, +}); +``` + +--- + +## useComputeCallback + +Returns a memoized async function, similar to `useCallback`. + +```tsx +import { useComputeCallback } from '@computekit/react'; + +function MyComponent() { + const calculate = useComputeCallback('sum'); + + const handleClick = async () => { + const result = await calculate([1, 2, 3, 4, 5]); + console.log(result); // 15 + }; + + return ; +} +``` + +--- + +## useComputeFunction + +Register and use a function in a single hook. + +```tsx +import { useComputeFunction } from '@computekit/react'; + +function MyComponent() { + const { data, loading, run } = useComputeFunction('double', (n: number) => n * 2); + + return ( + + ); +} +``` + +--- + +## usePoolStats + +Monitor worker pool performance in real-time. + +```tsx +import { usePoolStats } from '@computekit/react'; + +function PoolMonitor() { + // Refresh every 1000ms + const stats = usePoolStats(1000); + + return ( +
+

Total Workers: {stats.totalWorkers}

+

Active: {stats.activeWorkers}

+

Idle: {stats.idleWorkers}

+

Queue: {stats.queueLength}

+

Completed: {stats.tasksCompleted}

+

Failed: {stats.tasksFailed}

+

Avg Duration: {stats.averageTaskDuration.toFixed(2)}ms

+
+ ); +} +``` + +--- + +## Progress Reporting + +Track progress for long-running operations: + +```tsx +// Register function with progress reporting +kit.register('longTask', async (data, { reportProgress }) => { + const total = data.items.length; + const results = []; + + for (let i = 0; i < total; i++) { + results.push(await process(data.items[i])); + + // Report progress + reportProgress({ + percent: ((i + 1) / total) * 100, + phase: 'Processing', + data: { current: i + 1, total }, + }); + } + + return results; +}); + +// Use with progress tracking +function LongTaskComponent() { + const { progress, loading, run } = useCompute('longTask'); + + return ( +
+ + + {loading && progress && ( +
+ + + {progress.phase}: {progress.percent.toFixed(0)}% + +
+ )} +
+ ); +} +``` + +--- + +## Cancellation + +Cancel running operations using AbortController: + +```tsx +function CancellableTask() { + const { data, loading, run, cancel } = useCompute('longTask'); + + return ( +
+ + + +
+ ); +} +``` + +--- + +## TypeScript Support + +Full type inference for inputs and outputs: + +```tsx +// Define your types +interface ImageInput { + data: number[]; + width: number; + height: number; +} + +interface ImageOutput { + data: number[]; + processingTime: number; +} + +// Types are inferred in the hook +const { data, run } = useCompute('processImage'); + +// data is ImageOutput | undefined +// run expects ImageInput +run({ data: [...], width: 256, height: 256 }); +``` + +--- + +## Next Steps + +- Check the [API Reference]({{ site.baseurl }}/api-reference) for the complete API +- Learn about [WASM integration]({{ site.baseurl }}/wasm) for native-speed performance diff --git a/docs/react-query.md b/docs/react-query.md new file mode 100644 index 0000000..dd434f1 --- /dev/null +++ b/docs/react-query.md @@ -0,0 +1,139 @@ +--- +layout: default +title: React Query +nav_order: 7 +description: 'React Query integration for ComputeKit' +permalink: /react-query +--- + +# React Query Integration + +{: .no_toc } + +Seamlessly integrate ComputeKit with TanStack React Query. +{: .fs-6 .fw-300 } + +## Table of contents + +{: .no_toc .text-delta } + +1. TOC + {:toc} + +--- + +## Installation + +```bash +npm install @computekit/core @computekit/react-query @tanstack/react-query +``` + +--- + +## Setup + +Wrap your app with both providers: + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ComputeKitProvider } from '@computekit/react'; + +const queryClient = new QueryClient(); + +function App() { + return ( + + + + + + ); +} +``` + +--- + +## useComputeQuery + +Use ComputeKit functions with React Query's caching and refetching. + +```tsx +import { useComputeQuery } from '@computekit/react-query'; + +function DataProcessor() { + const { data, isLoading, error, refetch } = useComputeQuery( + ['processData', dataId], // Query key + 'heavyProcess', // Function name + inputData, // Input + { + staleTime: 5 * 60 * 1000, // 5 minutes + cacheTime: 30 * 60 * 1000, // 30 minutes + } + ); + + return ( +
+ {isLoading && } + {data && } + +
+ ); +} +``` + +--- + +## useComputeMutation + +For on-demand computations: + +```tsx +import { useComputeMutation } from '@computekit/react-query'; + +function ImageEditor() { + const mutation = useComputeMutation('processImage', { + onSuccess: (data) => { + console.log('Processed:', data); + }, + onError: (error) => { + console.error('Failed:', error); + }, + }); + + return ( +
+ +
+ ); +} +``` + +--- + +## Benefits + +| Feature | Description | +| ---------------------- | ------------------------------------------------------ | +| **Caching** | Results are cached and reused automatically | +| **Background Updates** | Stale data is refreshed in the background | +| **Deduplication** | Identical queries are deduplicated | +| **DevTools** | Use React Query DevTools to inspect compute operations | +| **Suspense** | Works with React Suspense for loading states | + +--- + +## Example: Cached Computation + +```tsx +function ExpensiveCalculation({ params }) { + const { data } = useComputeQuery(['calculate', params], 'expensiveCalc', params, { + staleTime: Infinity, // Never refetch automatically + cacheTime: 60 * 60 * 1000, // Keep in cache for 1 hour + }); + + // If the same params are used again, the cached result is returned instantly + return ; +} +``` diff --git a/docs/wasm.md b/docs/wasm.md new file mode 100644 index 0000000..b62e1e4 --- /dev/null +++ b/docs/wasm.md @@ -0,0 +1,357 @@ +--- +layout: default +title: WebAssembly +nav_order: 5 +description: 'WASM integration with ComputeKit' +permalink: /wasm +--- + +# WebAssembly Integration + +{: .no_toc } + +Use WASM for native-speed performance in your compute functions. +{: .fs-6 .fw-300 } + +## Table of contents + +{: .no_toc .text-delta } + +1. TOC + {:toc} + +--- + +## Overview + +ComputeKit provides seamless WebAssembly integration, allowing you to: + +- Load WASM modules from URLs, ArrayBuffers, or base64 strings +- Use AssemblyScript for easy TypeScript-to-WASM compilation +- Combine WASM with Web Workers for maximum performance +- Manage WASM memory efficiently + +--- + +## WASM Utilities + +### loadWasmModule() + +Load a WASM module from various sources. + +```typescript +import { loadWasmModule } from '@computekit/core'; + +// From URL +const module = await loadWasmModule('/path/to/module.wasm'); + +// From ArrayBuffer +const bytes = await fetch('/module.wasm').then((r) => r.arrayBuffer()); +const module = await loadWasmModule(bytes); + +// From base64 +const module = await loadWasmModule('data:application/wasm;base64,...'); +``` + +### loadAndInstantiate() + +Load and instantiate a WASM module with custom imports. + +```typescript +import { loadAndInstantiate } from '@computekit/core'; + +const { module, instance } = await loadAndInstantiate({ + source: '/module.wasm', + imports: { + env: { + log: (value: number) => console.log(value), + abort: () => { + throw new Error('WASM abort'); + }, + }, + }, + memory: { + initial: 256, // 256 pages = 16MB + maximum: 512, // 512 pages = 32MB + shared: true, // Use SharedArrayBuffer + }, +}); + +// Call exported functions +const result = instance.exports.compute(42); +``` + +### loadAssemblyScript() + +Load an AssemblyScript-compiled WASM module with default imports. + +```typescript +import { loadAssemblyScript } from '@computekit/core'; + +const { exports } = await loadAssemblyScript('/as-module.wasm'); + +// Call exported functions directly +const sum = exports.computeSum(new Int32Array([1, 2, 3, 4, 5])); +``` + +--- + +## Memory Utilities + +### getMemoryView() + +Create a typed array view into WASM memory. + +```typescript +import { getMemoryView } from '@computekit/core'; + +const view = getMemoryView(memory, Float64Array, 0, 100); +// Now you can read/write to WASM memory through `view` +``` + +### copyToWasmMemory() + +Copy data to WASM memory. + +```typescript +import { copyToWasmMemory } from '@computekit/core'; + +const data = new Uint8Array([1, 2, 3, 4]); +copyToWasmMemory(wasmMemory, data, 0); +``` + +### copyFromWasmMemory() + +Copy data from WASM memory. + +```typescript +import { copyFromWasmMemory } from '@computekit/core'; + +const result = copyFromWasmMemory(wasmMemory, 0, 4); +// Uint8Array([1, 2, 3, 4]) +``` + +--- + +## Cache Management + +### clearWasmCache() + +Clear the WASM module cache. + +```typescript +import { clearWasmCache } from '@computekit/core'; + +clearWasmCache(); +``` + +### getWasmCacheStats() + +Get WASM cache statistics. + +```typescript +import { getWasmCacheStats } from '@computekit/core'; + +const stats = getWasmCacheStats(); +console.log(`Cached modules: ${stats.modules}`); +console.log(`Cached instances: ${stats.instances}`); +``` + +--- + +## AssemblyScript Guide + +### 1. Write AssemblyScript + +Create an AssemblyScript file with your compute functions: + +```typescript +// compute/sum.ts +export function sum(arr: Int32Array): i32 { + let total: i32 = 0; + for (let i = 0; i < arr.length; i++) { + total += unchecked(arr[i]); + } + return total; +} + +export function fibonacci(n: i32): i64 { + if (n <= 1) return n as i64; + let a: i64 = 0; + let b: i64 = 1; + for (let i: i32 = 2; i <= n; i++) { + let temp = a + b; + a = b; + b = temp; + } + return b; +} +``` + +### 2. Install AssemblyScript + +```bash +npm install --save-dev assemblyscript +npx asinit . +``` + +### 3. Compile to WASM + +```bash +npx asc compute/sum.ts -o public/sum.wasm --optimize +``` + +Or add a script to `package.json`: + +```json +{ + "scripts": { + "build:wasm": "asc compute/sum.ts -o public/sum.wasm --optimize" + } +} +``` + +### 4. Use in ComputeKit + +```typescript +import { ComputeKit, loadAssemblyScript } from '@computekit/core'; + +const kit = new ComputeKit(); + +kit.register('wasmSum', async (data: number[]) => { + const { exports } = await loadAssemblyScript('/sum.wasm'); + const arr = new Int32Array(data); + return exports.sum(arr); +}); + +const result = await kit.run('wasmSum', [1, 2, 3, 4, 5]); +console.log(result); // 15 +``` + +--- + +## React + WASM Example + +Combine `useCompute` with WASM for the ultimate performance: + +```tsx +import { ComputeKitProvider, useComputeKit, useCompute } from '@computekit/react'; +import { loadAssemblyScript } from '@computekit/core'; +import { useEffect, useRef } from 'react'; + +function App() { + return ( + + + + ); +} + +function ImageProcessor() { + const kit = useComputeKit(); + const canvasRef = useRef(null); + + useEffect(() => { + // Register WASM-powered blur function + kit.register( + 'blurImage', + async (input: { + data: number[]; + width: number; + height: number; + passes: number; + }) => { + const { exports, memory } = await loadAssemblyScript('/blur.wasm'); + const { data, width, height, passes } = input; + + // Copy input to WASM memory + const ptr = exports.getBufferPtr(); + const wasmMem = new Uint8ClampedArray(memory.buffer, ptr, data.length); + wasmMem.set(data); + + // Run WASM blur + exports.blurImage(width, height, passes); + + // Return result + return Array.from(new Uint8ClampedArray(memory.buffer, ptr, data.length)); + } + ); + }, [kit]); + + const { data, loading, run } = useCompute< + { data: number[]; width: number; height: number; passes: number }, + number[] + >('blurImage'); + + const handleBlur = () => { + const canvas = canvasRef.current!; + const ctx = canvas.getContext('2d')!; + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + run({ + data: Array.from(imageData.data), + width: canvas.width, + height: canvas.height, + passes: 10, + }); + }; + + useEffect(() => { + if (data && canvasRef.current) { + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d')!; + const imageData = new ImageData( + new Uint8ClampedArray(data), + canvas.width, + canvas.height + ); + ctx.putImageData(imageData, 0, 0); + } + }, [data]); + + return ( +
+ + +
+ ); +} +``` + +--- + +## Performance Tips + +{: .note } +WASM functions can be 10-100x faster than JavaScript for numeric computations. + +1. **Minimize memory copies** - Use typed arrays and transfer data efficiently +2. **Batch operations** - Process large chunks of data at once +3. **Use SIMD** - AssemblyScript supports SIMD for parallel math operations +4. **Pre-allocate memory** - Avoid growing WASM memory during computation + +```typescript +// ❌ Slow: Many small WASM calls +for (const pixel of pixels) { + wasm.processPixel(pixel); +} + +// ✅ Fast: One batched WASM call +wasm.processAllPixels(pixelBuffer, width, height); +``` + +--- + +## Browser Support + +| Browser | WASM | SharedArrayBuffer | +| ----------- | ---- | ----------------- | +| Chrome 57+ | ✅ | ✅ (with headers) | +| Firefox 52+ | ✅ | ✅ (with headers) | +| Safari 11+ | ✅ | ✅ (Safari 15.2+) | +| Edge 16+ | ✅ | ✅ (with headers) | + +{: .warning } +SharedArrayBuffer requires Cross-Origin Isolation headers. See the [Getting Started]({{ site.baseurl }}/getting-started#vitepwebpack-configuration) guide for configuration. From f703f2cf5061ad2371af1f0c80a65bb6e7012ae2 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 13:47:34 +0000 Subject: [PATCH 20/37] fix: ensure pages setup is correctly enabled in workflow --- .github/workflows/docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ed54dde..4544052 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,6 +23,8 @@ jobs: - name: Setup Pages uses: actions/configure-pages@v4 + with: + enablement: true - name: Build with Jekyll uses: actions/jekyll-build-pages@v1 From 5a58ae8c51b48e206ebcbb03266186c2a39b9eb6 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 13:50:59 +0000 Subject: [PATCH 21/37] fix: remove unnecessary enablement configuration in Setup Pages step --- .github/workflows/docs.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4544052..ed54dde 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,8 +23,6 @@ jobs: - name: Setup Pages uses: actions/configure-pages@v4 - with: - enablement: true - name: Build with Jekyll uses: actions/jekyll-build-pages@v1 From bc6cf594f4a857d79c0172cbf0185f9feaaa4959 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 13:57:17 +0000 Subject: [PATCH 22/37] feat: update theme colors and add custom styles for ComputeKit --- docs/_config.yml | 2 +- docs/_sass/custom/custom.scss | 438 ++++++++++++++++++++++++++++++++++ docs/assets/logo.svg | 48 ++-- 3 files changed, 463 insertions(+), 25 deletions(-) create mode 100644 docs/_sass/custom/custom.scss diff --git a/docs/_config.yml b/docs/_config.yml index 42447c1..62f77d1 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -5,7 +5,7 @@ baseurl: '/compute-kit' remote_theme: just-the-docs/just-the-docs@v0.8.2 -color_scheme: dark +color_scheme: light logo: '/assets/logo.svg' diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss new file mode 100644 index 0000000..99c4573 --- /dev/null +++ b/docs/_sass/custom/custom.scss @@ -0,0 +1,438 @@ +// Modern ComputeKit Theme - Inspired by TanStack +// Clean, professional, and readable + +// Import Inter font for modern typography +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +// Root variables +:root { + // Colors - Modern purple/blue accent like TanStack + --color-primary: #7c3aed; + --color-primary-light: #a78bfa; + --color-primary-dark: #5b21b6; + --color-accent: #06b6d4; + + // Background + --color-bg: #ffffff; + --color-bg-secondary: #f8fafc; + --color-bg-tertiary: #f1f5f9; + + // Text + --color-text: #1e293b; + --color-text-secondary: #64748b; + --color-text-muted: #94a3b8; + + // Borders + --color-border: #e2e8f0; + --color-border-light: #f1f5f9; + + // Code + --color-code-bg: #f8fafc; + --color-code-border: #e2e8f0; + + // Shadows + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + + // Fonts + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace; +} + +// Base typography +body { + font-family: var(--font-body) !important; + font-size: 16px; + line-height: 1.7; + color: var(--color-text); + background-color: var(--color-bg); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// Headings +h1, +h2, +h3, +h4, +h5, +h6, +.site-title { + font-family: var(--font-body) !important; + font-weight: 700; + color: var(--color-text); + letter-spacing: -0.02em; +} + +h1 { + font-size: 2.5rem !important; + margin-bottom: 1rem; +} + +h2 { + font-size: 1.75rem !important; + margin-top: 2.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-border); +} + +h3 { + font-size: 1.35rem !important; + margin-top: 2rem; +} + +// Sidebar styles +.side-bar { + background-color: var(--color-bg-secondary) !important; + border-right: 1px solid var(--color-border); +} + +.site-title { + font-size: 1.25rem !important; + font-weight: 700 !important; + color: var(--color-primary) !important; +} + +.site-logo { + max-height: 2.5rem; +} + +// Navigation +.nav-list { + font-size: 0.9rem; + + .nav-list-item { + margin: 0; + + .nav-list-link { + padding: 0.5rem 1rem; + border-radius: 0.5rem; + margin: 0.125rem 0.5rem; + color: var(--color-text-secondary); + font-weight: 500; + transition: all 0.15s ease; + + &:hover { + background-color: var(--color-bg-tertiary); + color: var(--color-primary); + } + + &.active { + background-color: rgba(124, 58, 237, 0.1); + color: var(--color-primary); + font-weight: 600; + } + } + } +} + +// Main content +.main { + background-color: var(--color-bg); +} + +.main-content { + max-width: 52rem; + padding: 2rem 3rem; + + p { + margin-bottom: 1.25rem; + color: var(--color-text); + } + + a { + color: var(--color-primary); + text-decoration: none; + font-weight: 500; + + &:hover { + text-decoration: underline; + color: var(--color-primary-dark); + } + } +} + +// Code blocks +code { + font-family: var(--font-mono) !important; + font-size: 0.875em; + background-color: var(--color-code-bg); + padding: 0.2em 0.4em; + border-radius: 0.375rem; + color: var(--color-primary-dark); +} + +pre { + background-color: #1e293b !important; + border: none !important; + border-radius: 0.75rem !important; + padding: 1.25rem !important; + margin: 1.5rem 0 !important; + box-shadow: var(--shadow-md); + + code { + background-color: transparent !important; + color: #e2e8f0 !important; + padding: 0; + font-size: 0.875rem; + } +} + +// Syntax highlighting (dark theme for code blocks) +.highlight { + background-color: #1e293b !important; + border-radius: 0.75rem; + + .c, + .c1, + .cm { + color: #64748b; + } // Comments + .k, + .kd, + .kn, + .kp, + .kr { + color: #c084fc; + } // Keywords + .s, + .s1, + .s2, + .sb, + .sc, + .sd, + .se, + .sh, + .si, + .sx, + .sr, + .ss { + color: #86efac; + } // Strings + .n, + .na, + .nb, + .nc, + .nd, + .ne, + .nf, + .ni, + .nl, + .nn, + .no, + .nt, + .nv { + color: #7dd3fc; + } // Names + .m, + .mf, + .mh, + .mi, + .mo { + color: #fbbf24; + } // Numbers + .o, + .ow { + color: #f472b6; + } // Operators + .p { + color: #e2e8f0; + } // Punctuation +} + +// Tables +table { + border-collapse: collapse; + width: 100%; + margin: 1.5rem 0; + font-size: 0.9rem; + border-radius: 0.5rem; + overflow: hidden; + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); +} + +th { + background-color: var(--color-bg-tertiary); + font-weight: 600; + text-align: left; + padding: 0.75rem 1rem; + border-bottom: 2px solid var(--color-border); +} + +td { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--color-border-light); +} + +tr:last-child td { + border-bottom: none; +} + +tr:hover { + background-color: var(--color-bg-secondary); +} + +// Buttons +.btn { + font-family: var(--font-body); + font-weight: 600; + font-size: 0.9rem; + padding: 0.625rem 1.25rem; + border-radius: 0.5rem; + transition: all 0.15s ease; + text-decoration: none !important; +} + +.btn-primary { + background-color: var(--color-primary) !important; + color: white !important; + border: none; + box-shadow: var(--shadow-sm); + + &:hover { + background-color: var(--color-primary-dark) !important; + transform: translateY(-1px); + box-shadow: var(--shadow-md); + } +} + +// Callouts +.warning, +.note, +.tip { + border-radius: 0.75rem; + padding: 1rem 1.25rem; + margin: 1.5rem 0; + border-left: 4px solid; + + p:last-child { + margin-bottom: 0; + } +} + +.note { + background-color: #eff6ff; + border-left-color: #3b82f6; +} + +.warning { + background-color: #fef3c7; + border-left-color: #f59e0b; +} + +.tip { + background-color: #ecfdf5; + border-left-color: #10b981; +} + +// Search +.search-input { + font-family: var(--font-body); + border-radius: 0.5rem; + border: 1px solid var(--color-border); + padding: 0.625rem 1rem; + font-size: 0.9rem; + + &:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1); + outline: none; + } +} + +// TOC +.toc { + background-color: var(--color-bg-secondary); + border-radius: 0.75rem; + padding: 1rem 1.25rem; + border: 1px solid var(--color-border); +} + +// Footer +.site-footer { + border-top: 1px solid var(--color-border); + color: var(--color-text-muted); + font-size: 0.875rem; +} + +// Aux links (GitHub, npm) +.aux-nav { + .aux-nav-list-item { + a { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-text-secondary); + + &:hover { + color: var(--color-primary); + } + } + } +} + +// Home page hero adjustments +.fs-9 { + font-size: 3rem !important; + font-weight: 800 !important; + background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.fs-6 { + font-size: 1.35rem !important; + color: var(--color-text-secondary) !important; +} + +// Lists +ul, +ol { + margin-bottom: 1.25rem; + + li { + margin-bottom: 0.5rem; + + &::marker { + color: var(--color-primary); + } + } +} + +// Blockquotes +blockquote { + border-left: 3px solid var(--color-primary); + padding-left: 1rem; + color: var(--color-text-secondary); + font-style: italic; +} + +// Back to top button +.back-to-top { + background-color: var(--color-primary); + color: white; + border-radius: 9999px; + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; + + &:hover { + background-color: var(--color-primary-dark); + } +} + +// Responsive +@media (max-width: 800px) { + .main-content { + padding: 1.5rem; + } + + h1 { + font-size: 2rem !important; + } + + .fs-9 { + font-size: 2.25rem !important; + } +} diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg index 44ecb90..da53943 100644 --- a/docs/assets/logo.svg +++ b/docs/assets/logo.svg @@ -1,40 +1,40 @@ - + - + - + - - - - + + + + - - - - - + + + + + - - - + + + - - - + + + - - - + + + - - - + + + From e8d1da83e30f40a78fed6dc4232ab0d7368c753b Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 14:01:09 +0000 Subject: [PATCH 23/37] feat: enhance search component with improved styling and functionality --- docs/_sass/custom/custom.scss | 112 +++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss index 99c4573..939121b 100644 --- a/docs/_sass/custom/custom.scss +++ b/docs/_sass/custom/custom.scss @@ -327,12 +327,32 @@ tr:hover { } // Search +.search { + position: relative; + margin-bottom: 1rem; + padding: 0 1rem; +} + +.search-input-wrap { + display: flex; + align-items: center; + position: relative; +} + .search-input { font-family: var(--font-body); - border-radius: 0.5rem; - border: 1px solid var(--color-border); - padding: 0.625rem 1rem; + width: 100%; + padding: 0.625rem 1rem 0.625rem 2.5rem; font-size: 0.9rem; + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 0.5rem; + color: var(--color-text); + transition: all 0.15s ease; + + &::placeholder { + color: var(--color-text-muted); + } &:focus { border-color: var(--color-primary); @@ -341,6 +361,92 @@ tr:hover { } } +.search-label { + position: absolute; + left: 0.75rem; + top: 50%; + transform: translateY(-50%); + color: var(--color-text-muted); + pointer-events: none; + + .search-icon { + width: 1rem; + height: 1rem; + } +} + +.search-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 0.5rem; + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 0.5rem; + box-shadow: var(--shadow-md); + max-height: 20rem; + overflow-y: auto; + z-index: 100; +} + +.search-results-list { + list-style: none; + padding: 0.5rem; + margin: 0; +} + +.search-results-list-item { + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + cursor: pointer; + + &:hover, + &.active { + background-color: var(--color-bg-tertiary); + } +} + +.search-result-title { + font-weight: 600; + color: var(--color-text); + font-size: 0.9rem; +} + +.search-result-doc { + font-size: 0.8rem; + color: var(--color-text-secondary); + margin-top: 0.125rem; + + .search-result-doc-title { + font-weight: 500; + } +} + +.search-result-previews { + font-size: 0.8rem; + color: var(--color-text-muted); + margin-top: 0.25rem; +} + +.search-result-preview + .search-result-preview { + margin-top: 0.125rem; +} + +.search-result-highlight { + background-color: rgba(124, 58, 237, 0.15); + color: var(--color-primary-dark); + padding: 0.125em 0.25em; + border-radius: 0.25rem; +} + +.search-no-result { + padding: 1rem; + text-align: center; + color: var(--color-text-muted); + font-size: 0.9rem; +} + // TOC .toc { background-color: var(--color-bg-secondary); From 4f98e33af179da915018486a8146da2bba22e9d8 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 14:04:31 +0000 Subject: [PATCH 24/37] fix: update code block styling for improved readability and dark theme support --- docs/_sass/custom/custom.scss | 124 ++++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 28 deletions(-) diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss index 939121b..952f62a 100644 --- a/docs/_sass/custom/custom.scss +++ b/docs/_sass/custom/custom.scss @@ -163,38 +163,56 @@ code { } pre { - background-color: #1e293b !important; + background-color: #0d1117 !important; border: none !important; border-radius: 0.75rem !important; padding: 1.25rem !important; margin: 1.5rem 0 !important; box-shadow: var(--shadow-md); + overflow-x: auto; code { background-color: transparent !important; - color: #e2e8f0 !important; + color: #e6edf3 !important; padding: 0; font-size: 0.875rem; + line-height: 1.6; } } -// Syntax highlighting (dark theme for code blocks) +// Syntax highlighting - GitHub Dark theme (high contrast) .highlight { - background-color: #1e293b !important; + background-color: #0d1117 !important; border-radius: 0.75rem; + // Plain text + .p, + .w { + color: #e6edf3; + } + + // Comments .c, .c1, - .cm { - color: #64748b; - } // Comments + .cm, + .cs, + .cp { + color: #8b949e; + font-style: italic; + } + + // Keywords (import, from, const, function, async, await, return) .k, .kd, .kn, .kp, - .kr { - color: #c084fc; - } // Keywords + .kr, + .kc, + .kt { + color: #ff7b72; + } + + // Strings .s, .s1, .s2, @@ -207,37 +225,87 @@ pre { .sx, .sr, .ss { - color: #86efac; - } // Strings + color: #a5d6ff; + } + + // Function names and method calls + .nf, + .fm { + color: #d2a8ff; + } + + // Class names and types + .nc, + .nn, + .nx { + color: #ffa657; + } + + // Variables and identifiers .n, .na, .nb, - .nc, - .nd, - .ne, - .nf, .ni, .nl, - .nn, .no, - .nt, - .nv { - color: #7dd3fc; - } // Names + .nv, + .ne, + .nd { + color: #e6edf3; + } + + // HTML/JSX tags + .nt { + color: #7ee787; + } + + // Numbers .m, .mf, .mh, .mi, - .mo { - color: #fbbf24; - } // Numbers + .mo, + .il { + color: #79c0ff; + } + + // Operators .o, .ow { - color: #f472b6; - } // Operators + color: #ff7b72; + } + + // Punctuation (braces, parentheses, etc) .p { - color: #e2e8f0; - } // Punctuation + color: #e6edf3; + } + + // Property names + .py, + .na { + color: #79c0ff; + } + + // Special - imports and exports + .kn { + color: #ff7b72; + } + + // Decorators + .nd { + color: #d2a8ff; + } + + // Built-in functions + .nb { + color: #ffa657; + } + + // Error + .err { + color: #ffa198; + background-color: transparent; + } } // Tables From 4e22a11c82a32136adf1eb61dff8b7ecfa454ed2 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sat, 27 Dec 2025 14:11:17 +0000 Subject: [PATCH 25/37] feat: replace static table of contents with collapsible details in documentation --- docs/api-reference.md | 13 +++++++------ docs/examples.md | 13 +++++++------ docs/getting-started.md | 13 +++++++------ docs/react-hooks.md | 13 +++++++------ docs/react-query.md | 13 +++++++------ docs/wasm.md | 13 +++++++------ 6 files changed, 42 insertions(+), 36 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 55d7df9..5635d46 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -13,12 +13,13 @@ permalink: /api-reference Complete API documentation for the ComputeKit core library. {: .fs-6 .fw-300 } -## Table of contents - -{: .no_toc .text-delta } - -1. TOC - {:toc} + +
+ Table of contents + {: .text-delta } +- TOC +{:toc} +
--- diff --git a/docs/examples.md b/docs/examples.md index cf22ebd..803e0db 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -13,12 +13,13 @@ permalink: /examples Real-world examples demonstrating ComputeKit's capabilities. {: .fs-6 .fw-300 } -## Table of contents - -{: .no_toc .text-delta } - -1. TOC - {:toc} + +
+ Table of contents + {: .text-delta } +- TOC +{:toc} +
--- diff --git a/docs/getting-started.md b/docs/getting-started.md index 5840c06..b247596 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -13,12 +13,13 @@ permalink: /getting-started Get up and running with ComputeKit in just a few minutes. {: .fs-6 .fw-300 } -## Table of contents - -{: .no_toc .text-delta } - -1. TOC - {:toc} + +
+ Table of contents + {: .text-delta } +- TOC +{:toc} +
--- diff --git a/docs/react-hooks.md b/docs/react-hooks.md index 722328d..5fea34b 100644 --- a/docs/react-hooks.md +++ b/docs/react-hooks.md @@ -13,12 +13,13 @@ permalink: /react-hooks ComputeKit provides purpose-built React hooks for seamless integration. {: .fs-6 .fw-300 } -## Table of contents - -{: .no_toc .text-delta } - -1. TOC - {:toc} + +
+ Table of contents + {: .text-delta } +- TOC +{:toc} +
--- diff --git a/docs/react-query.md b/docs/react-query.md index dd434f1..98fab94 100644 --- a/docs/react-query.md +++ b/docs/react-query.md @@ -13,12 +13,13 @@ permalink: /react-query Seamlessly integrate ComputeKit with TanStack React Query. {: .fs-6 .fw-300 } -## Table of contents - -{: .no_toc .text-delta } - -1. TOC - {:toc} + +
+ Table of contents + {: .text-delta } +- TOC +{:toc} +
--- diff --git a/docs/wasm.md b/docs/wasm.md index b62e1e4..357509b 100644 --- a/docs/wasm.md +++ b/docs/wasm.md @@ -13,12 +13,13 @@ permalink: /wasm Use WASM for native-speed performance in your compute functions. {: .fs-6 .fw-300 } -## Table of contents - -{: .no_toc .text-delta } - -1. TOC - {:toc} + +
+ Table of contents + {: .text-delta } +- TOC +{:toc} +
--- From 7ea8a3b1d81bf7a42703ffd0f41de71af6e6d3ce Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sun, 28 Dec 2025 11:15:30 +0000 Subject: [PATCH 26/37] Add comprehensive documentation for debugging, performance, and multi-stage pipelines in ComputeKit - Introduced a detailed guide on debugging worker code, including error handling, Chrome DevTools integration, and common issues. - Added performance optimization strategies focusing on data transfer methods, including structured cloning, transferable objects, and SharedArrayBuffer. - Documented the use of multi-stage pipelines for complex workflows, including examples of stage configuration, input/output transformation, and error handling. - Enhanced the user experience with progress tracking, metrics, and execution reports for pipelines. --- docs/debugging.md | 461 +++++++++++ docs/getting-started.md | 2 + docs/index.md | 42 +- docs/performance.md | 458 +++++++++++ docs/pipeline.md | 542 ++++++++++++ docs/react-hooks.md | 23 + packages/core/src/index.ts | 14 + packages/core/src/pool.ts | 10 +- packages/core/src/types.ts | 222 +++++ packages/core/src/utils.ts | 74 ++ packages/core/src/worker/runtime.ts | 9 +- packages/react/src/index.tsx | 1175 +++++++++++++++++++++++++++ 12 files changed, 3012 insertions(+), 20 deletions(-) create mode 100644 docs/debugging.md create mode 100644 docs/performance.md create mode 100644 docs/pipeline.md diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 0000000..302157a --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,461 @@ +--- +layout: default +title: Debugging +nav_order: 8 +--- + +# Debugging ComputeKit + +{: .no_toc } + +Learn how to debug worker code, understand errors, and troubleshoot issues in ComputeKit. +{: .fs-6 .fw-300 } + +## Table of contents + +{: .no_toc .text-delta } + +1. TOC + {:toc} + +--- + +## Overview + +Debugging Web Workers can be tricky because they run in a separate thread with their own global scope. ComputeKit provides several tools to make debugging easier: + +- Enhanced error messages with context +- Debug mode with verbose logging +- Console forwarding from workers +- Chrome DevTools integration +- Validation mode for main-thread debugging + +--- + +## Enable Debug Mode + +The simplest way to start debugging is to enable debug mode: + +```typescript +import { ComputeKit } from '@computekit/core'; + +const kit = new ComputeKit({ + debug: true, // Enable verbose logging +}); +``` + +With React: + +```tsx + + + +``` + +Debug mode logs: + +- Worker creation and termination +- Function registration +- Task execution (start, complete, error) +- Message passing between main thread and workers +- Payload sizes for data transfer + +--- + +## Understanding Error Messages + +When a compute function throws an error, ComputeKit captures and enriches it with context: + +```typescript +kit.register('riskyFunction', (input: number) => { + if (input < 0) { + throw new Error('Input must be non-negative'); + } + return Math.sqrt(input); +}); + +try { + await kit.run('riskyFunction', -5); +} catch (error) { + console.error(error); + // ComputeError: Input must be non-negative + // Function: riskyFunction + // Worker: worker-abc123 + // Duration: 2ms + // Stack: ...original stack trace... +} +``` + +### Error Properties + +All errors from ComputeKit include: + +| Property | Description | +| -------------- | ---------------------------------------- | +| `message` | The original error message | +| `functionName` | Name of the compute function that failed | +| `workerId` | ID of the worker that processed the task | +| `duration` | Time elapsed before the error occurred | +| `stack` | Full stack trace from the worker | +| `inputSize` | Size of the input data (in debug mode) | + +--- + +## Chrome DevTools Integration + +### Debugging Workers Directly + +Chrome DevTools supports debugging Web Workers: + +1. Open DevTools (F12) +2. Go to **Sources** tab +3. In the left panel, find **Threads** section +4. Click on a worker thread to debug it + +### Setting Breakpoints + +To set breakpoints in your compute functions: + +1. Enable source maps in your bundler (see below) +2. In DevTools Sources, find your worker code +3. Set breakpoints as normal +4. Use the worker thread selector to switch contexts + +### Console Output + +Worker `console.log()` calls appear in the main DevTools console, prefixed with the worker context. Enable **Verbose** log level to see all worker output: + +```typescript +kit.register('debugMe', (input: number[]) => { + console.log('Processing', input.length, 'items'); // Visible in DevTools + console.time('processing'); + + const result = input.map((x) => x * 2); + + console.timeEnd('processing'); + console.log('Result:', result.slice(0, 5), '...'); + + return result; +}); +``` + +--- + +## Source Maps + +For the best debugging experience, configure your bundler to generate source maps for workers. + +### Vite + +```typescript +// vite.config.ts +export default defineConfig({ + build: { + sourcemap: true, + }, + worker: { + format: 'es', + rollupOptions: { + output: { + sourcemap: true, + }, + }, + }, +}); +``` + +### Webpack + +```javascript +// webpack.config.js +module.exports = { + devtool: 'source-map', + module: { + rules: [ + { + test: /\.worker\.ts$/, + use: { + loader: 'worker-loader', + options: { + inline: 'fallback', + }, + }, + }, + ], + }, +}; +``` + +### esbuild + +```typescript +import * as esbuild from 'esbuild'; + +await esbuild.build({ + entryPoints: ['src/index.ts'], + bundle: true, + sourcemap: true, + outfile: 'dist/bundle.js', +}); +``` + +--- + +## Validation Mode (Main Thread Execution) + +For difficult bugs, run your compute function on the main thread to use standard debugging tools: + +```typescript +import { ComputeKit } from '@computekit/core'; + +const kit = new ComputeKit({ + // Force main thread execution (bypasses workers) + forceMainThread: true, +}); + +// Now you can set breakpoints directly in your compute functions +kit.register('myFunction', (input) => { + debugger; // This will pause execution in DevTools + return processData(input); +}); +``` + +{: .warning } + +> **forceMainThread** should only be used for debugging. It defeats the purpose of using workers and will block the UI thread. + +--- + +## Common Issues + +### "Function not found in worker" + +This error occurs when a compute function isn't registered before calling `run()`: + +```typescript +// ❌ Wrong - function not registered +await kit.run('myFunction', data); + +// ✅ Correct - register first +kit.register('myFunction', (data) => processData(data)); +await kit.run('myFunction', data); +``` + +**In React**, ensure registration happens before the component that uses the function mounts: + +```tsx +// ❌ Wrong - might race with child components +function App() { + const kit = useComputeKit(); + + useEffect(() => { + kit.register('myFunction', fn); + }, [kit]); + + return ; +} + +// ✅ Correct - use a registration component or context +function App() { + return ( + + + + + ); +} + +function RegisterFunctions() { + const kit = useComputeKit(); + + useEffect(() => { + kit.register('myFunction', fn); + }, [kit]); + + return null; +} +``` + +### "DataCloneError: Failed to execute 'postMessage'" + +This error occurs when trying to transfer non-cloneable data: + +```typescript +// ❌ Wrong - functions can't be cloned +kit.register('bad', () => { + return { + data: [1, 2, 3], + callback: () => console.log('hi'), // Can't clone functions! + }; +}); + +// ✅ Correct - return only cloneable data +kit.register('good', () => { + return { + data: [1, 2, 3], + callbackName: 'logHi', // Return a reference instead + }; +}); +``` + +**Non-cloneable types include:** + +- Functions +- DOM nodes +- Error objects (use `{ message, stack }` instead) +- Symbols +- WeakMap/WeakSet + +### Timeout Errors + +If tasks are timing out unexpectedly: + +```typescript +// Increase timeout for long-running operations +const result = await kit.run('heavyComputation', data, { + timeout: 120000, // 2 minutes +}); + +// Or set a global default +const kit = new ComputeKit({ + timeout: 60000, // 1 minute default +}); +``` + +### Worker Creation Failures + +If workers fail to create, check: + +1. **Content Security Policy** - Ensure your CSP allows `worker-src 'self' blob:` +2. **Cross-origin issues** - Workers must be same-origin +3. **Memory limits** - Each worker uses ~2-5MB of memory + +--- + +## Logging Events + +Subscribe to ComputeKit events for detailed logging: + +```typescript +const kit = new ComputeKit(); + +kit.on('worker:created', ({ info }) => { + console.log(`Worker ${info.id} created`); +}); + +kit.on('worker:error', ({ error, info }) => { + console.error(`Worker ${info.id} error:`, error); +}); + +kit.on('task:start', ({ taskId, functionName }) => { + console.log(`Task ${taskId} started: ${functionName}`); +}); + +kit.on('task:complete', ({ taskId, duration }) => { + console.log(`Task ${taskId} completed in ${duration}ms`); +}); + +kit.on('task:error', ({ taskId, error }) => { + console.error(`Task ${taskId} failed:`, error); +}); + +kit.on('task:progress', ({ taskId, progress }) => { + console.log(`Task ${taskId}: ${progress.percent}%`); +}); +``` + +--- + +## Pool Statistics + +Monitor worker pool health: + +```typescript +const stats = kit.getStats(); + +console.log(stats); +// { +// workers: [ +// { id: 'w1', state: 'idle', tasksCompleted: 42, errors: 0 }, +// { id: 'w2', state: 'busy', tasksCompleted: 38, errors: 1 }, +// ], +// totalWorkers: 2, +// activeWorkers: 1, +// idleWorkers: 1, +// queueLength: 0, +// tasksCompleted: 80, +// tasksFailed: 1, +// averageTaskDuration: 145, +// } +``` + +In React, use the `usePoolStats` hook: + +```tsx +function PoolMonitor() { + const stats = usePoolStats(); + + return ( +
+

+ Workers: {stats.activeWorkers}/{stats.totalWorkers} active +

+

Queue: {stats.queueLength} pending

+

Completed: {stats.tasksCompleted}

+

Failed: {stats.tasksFailed}

+
+ ); +} +``` + +--- + +## Debugging Pipelines + +For multi-stage pipelines, use the built-in debugging features: + +```tsx +const pipeline = usePipeline(stages); + +// Get a detailed report +const report = pipeline.getReport(); +console.log(report.summary); +console.log(report.timeline); +console.log(report.insights); + +// Access metrics +console.log(pipeline.metrics); +// { +// totalStages: 4, +// completedStages: 3, +// failedStages: 1, +// slowestStage: { id: 'process', duration: 2340 }, +// timeline: [...] +// } + +// Check individual stage status +pipeline.stages.forEach((stage) => { + console.log(`${stage.name}: ${stage.status}`); + if (stage.error) { + console.error(` Error: ${stage.error.message}`); + } + if (stage.duration) { + console.log(` Duration: ${stage.duration}ms`); + } +}); +``` + +--- + +## Getting Help + +If you're stuck: + +1. Enable `debug: true` and check the console +2. Check the [Common Issues](#common-issues) section +3. Use `forceMainThread: true` to debug on the main thread +4. [Open an issue](https://github.com/pzaino/compute-kit/issues) with: + - ComputeKit version + - Browser and version + - Minimal reproduction code + - Console output with debug mode enabled diff --git a/docs/getting-started.md b/docs/getting-started.md index b247596..da68b63 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -200,6 +200,8 @@ module.exports = { ## Next Steps - Learn about [React Hooks]({{ site.baseurl }}/react-hooks) for the full React experience +- Read the [Debugging Guide]({{ site.baseurl }}/debugging) for troubleshooting tips +- Understand [Performance & Data Transfer]({{ site.baseurl }}/performance) to optimize payloads - Check the [API Reference]({{ site.baseurl }}/api-reference) for all available methods - Explore [WASM integration]({{ site.baseurl }}/wasm) for maximum performance - See [Examples]({{ site.baseurl }}/examples) for real-world use cases diff --git a/docs/index.md b/docs/index.md index 1ebe461..de3e94f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,16 +27,17 @@ Run heavy computations with React hooks. Use WASM for native-speed performance. ## ✨ Features -| Feature | Description | -| :----------------------- | :----------------------------------------------------------------------------- | -| ⚛️ **React-first** | Purpose-built hooks like `useCompute` with loading, error, and progress states | -| 🦀 **WASM integration** | Load and call AssemblyScript/Rust WASM modules with zero boilerplate | -| 🚀 **Non-blocking** | Everything runs in Web Workers, keeping your UI at 60fps | -| 🔧 **Zero config** | No manual worker files, postMessage handlers, or WASM glue code | -| 📦 **Tiny** | Core library is ~3KB gzipped | -| 🎯 **TypeScript** | Full type safety for your compute functions and WASM bindings | -| 🔄 **Worker pool** | Automatic load balancing across CPU cores | -| 📊 **Progress tracking** | Built-in progress reporting for long-running tasks | +| Feature | Description | +| :--------------------------- | :----------------------------------------------------------------------------- | +| ⚛️ **React-first** | Purpose-built hooks like `useCompute` with loading, error, and progress states | +| 🦀 **WASM integration** | Load and call AssemblyScript/Rust WASM modules with zero boilerplate | +| 🚀 **Non-blocking** | Everything runs in Web Workers, keeping your UI at 60fps | +| 🔧 **Zero config** | No manual worker files, postMessage handlers, or WASM glue code | +| 📦 **Tiny** | Core library is ~3KB gzipped | +| 🎯 **TypeScript** | Full type safety for your compute functions and WASM bindings | +| 🔄 **Worker pool** | Automatic load balancing across CPU cores | +| 📊 **Progress tracking** | Built-in progress reporting for long-running tasks | +| 🔗 **Multi-stage pipelines** | Chain compute operations with `usePipeline` for complex workflows | --- @@ -44,14 +45,15 @@ Run heavy computations with React hooks. Use WASM for native-speed performance. You _can_ use Web Workers and WASM without a library. But here's the reality: -| Task | Without ComputeKit | With ComputeKit | -| ----------------- | ------------------------------------------------------------------- | ---------------------------------- | -| Web Worker setup | Create separate `.js` files, handle `postMessage`, manage callbacks | `kit.register('fn', myFunc)` | -| WASM loading | Fetch, instantiate, memory management, glue code | `await loadWasmModule('/my.wasm')` | -| React integration | Manual state, effects, cleanup, abort handling | `useCompute()` hook | -| Worker pooling | Build your own pool, queue, and load balancer | Built-in | -| TypeScript | Tricky worker typing, no WASM types | Full type inference | -| Error handling | Try-catch across message boundaries | Automatic with React error states | +| Task | Without ComputeKit | With ComputeKit | +| --------------------- | ------------------------------------------------------------------- | ---------------------------------- | +| Web Worker setup | Create separate `.js` files, handle `postMessage`, manage callbacks | `kit.register('fn', myFunc)` | +| WASM loading | Fetch, instantiate, memory management, glue code | `await loadWasmModule('/my.wasm')` | +| React integration | Manual state, effects, cleanup, abort handling | `useCompute()` hook | +| Worker pooling | Build your own pool, queue, and load balancer | Built-in | +| Multi-stage workflows | Manual chaining, error handling per stage, retry logic | `usePipeline()` hook | +| TypeScript | Tricky worker typing, no WASM types | Full type inference | +| Error handling | Try-catch across message boundaries | Automatic with React error states | **ComputeKit's unique value:** The only library that combines **React hooks + WASM + Worker pool** into one cohesive, type-safe developer experience. @@ -67,6 +69,7 @@ You _can_ use Web Workers and WASM without a library. But here's the reality: | Parsing large files | String formatting | | Cryptographic operations | UI state management | | Real-time data analysis | Small form validations | +| Multi-file processing pipelines | Simple single-step tasks | --- @@ -170,6 +173,9 @@ function Calculator() { - [Getting Started Guide]({{ site.baseurl }}/getting-started) - [React Hooks Reference]({{ site.baseurl }}/react-hooks) +- [Multi-Stage Pipelines]({{ site.baseurl }}/pipeline) +- [Debugging Guide]({{ site.baseurl }}/debugging) +- [Performance & Data Transfer]({{ site.baseurl }}/performance) - [API Reference]({{ site.baseurl }}/api-reference) - [WASM Guide]({{ site.baseurl }}/wasm) - [Examples]({{ site.baseurl }}/examples) diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..3c90dfc --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,458 @@ +--- +layout: default +title: Performance & Data Transfer +nav_order: 9 +--- + +# Performance & Data Transfer + +{: .no_toc } + +Understand data transfer costs, optimize payloads, and get the best performance from ComputeKit. +{: .fs-6 .fw-300 } + +## Table of contents + +{: .no_toc .text-delta } + +1. TOC + {:toc} + +--- + +## Overview + +When using Web Workers, data must be transferred between the main thread and worker threads. Understanding how this works is crucial for optimal performance. + +**Key concepts:** + +- **Structured Cloning** - Default method; copies data (slow for large payloads) +- **Transferable Objects** - Zero-copy transfer; ownership moves (fast, but original becomes unusable) +- **SharedArrayBuffer** - Shared memory; no transfer needed (fastest, but requires setup) + +--- + +## How Data Transfer Works + +### Structured Cloning (Default) + +When you pass data to a compute function, JavaScript uses the [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) to copy the data: + +```typescript +kit.register('processImages', (images: ImageData[]) => { + // images is a COPY of the original data + return images.map((img) => applyFilter(img)); +}); + +// This copies ALL data to the worker, then copies the result back +const result = await kit.run('processImages', largeImageArray); +``` + +**Performance characteristics:** +| Payload Size | Clone Time (approx) | +|--------------|---------------------| +| 1 KB | < 1ms | +| 100 KB | 1-5ms | +| 1 MB | 10-50ms | +| 10 MB | 100-500ms | +| 100 MB | 1-5 seconds | + +{: .warning } + +> For payloads over **1 MB**, consider using Transferables or SharedArrayBuffer to avoid cloning overhead. + +### What Can Be Cloned? + +✅ **Cloneable types:** + +- Primitives (strings, numbers, booleans, null, undefined) +- Arrays and typed arrays (Uint8Array, Float32Array, etc.) +- Plain objects +- Map, Set +- Date, RegExp +- Blob, File, FileList +- ImageData +- ArrayBuffer + +❌ **NOT cloneable:** + +- Functions +- DOM nodes +- Error objects (clone `{ message, stack }` instead) +- Symbols +- WeakMap, WeakSet + +--- + +## Transferable Objects + +Transferables use zero-copy transfer by moving ownership of memory: + +```typescript +// Input: ArrayBuffer that will be transferred (not copied) +const buffer = new ArrayBuffer(10_000_000); // 10MB + +kit.register('processBuffer', (data: ArrayBuffer) => { + const view = new Uint8Array(data); + // Process the data... + return view.buffer; // Return the same buffer (transferred back) +}); + +// Use the transfer option +const result = await kit.run('processBuffer', buffer, { + transfer: [buffer], // Transfer ownership to worker +}); + +// ⚠️ buffer is now "neutered" - unusable on main thread +console.log(buffer.byteLength); // 0 +``` + +### Transferable Types + +- `ArrayBuffer` +- `MessagePort` +- `ImageBitmap` +- `OffscreenCanvas` +- `ReadableStream`, `WritableStream`, `TransformStream` + +### Automatic Transfer Detection + +ComputeKit automatically detects and transfers ArrayBuffers in your return values: + +```typescript +kit.register('createBuffer', () => { + const buffer = new ArrayBuffer(1000000); + const view = new Float32Array(buffer); + // Fill with data... + return buffer; // Automatically transferred back +}); + +// Result is transferred, not cloned +const result = await kit.run('createBuffer', null); +``` + +--- + +## SharedArrayBuffer + +For the highest performance, use SharedArrayBuffer to share memory directly: + +```typescript +import { ComputeKit } from '@computekit/core'; + +const kit = new ComputeKit({ + useSharedMemory: true, // Enable SharedArrayBuffer support +}); + +// Create shared memory +const shared = new SharedArrayBuffer(10_000_000); // 10MB +const view = new Float32Array(shared); + +kit.register('processShared', (sharedBuffer: SharedArrayBuffer) => { + const view = new Float32Array(sharedBuffer); + // Modify in place - changes visible to main thread! + for (let i = 0; i < view.length; i++) { + view[i] = view[i] * 2; + } + return { processed: view.length }; +}); + +// No data transfer - just passes a reference +await kit.run('processShared', shared); + +// Main thread sees the changes immediately +console.log(view[0]); // Modified value +``` + +### SharedArrayBuffer Requirements + +{: .important } + +> SharedArrayBuffer requires specific HTTP headers for security: + +``` +Cross-Origin-Opener-Policy: same-origin +Cross-Origin-Embedder-Policy: require-corp +``` + +**Vite configuration:** + +```typescript +// vite.config.ts +export default defineConfig({ + server: { + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }, +}); +``` + +**Express configuration:** + +```javascript +app.use((req, res, next) => { + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); + next(); +}); +``` + +--- + +## Measuring Payload Size + +### Debug Mode Size Reporting + +Enable debug mode to see payload sizes in the console: + +```typescript +const kit = new ComputeKit({ debug: true }); + +kit.register('process', (data) => transform(data)); + +await kit.run('process', largeData); +// Console: [ComputeKit] Task xyz: input=4.2MB, output=3.8MB, duration=145ms +``` + +### Manual Size Estimation + +```typescript +/** + * Estimate the size of a value for structured cloning + */ +function estimateSize(value: unknown): number { + if (value === null || value === undefined) return 0; + if (typeof value === 'boolean') return 4; + if (typeof value === 'number') return 8; + if (typeof value === 'string') return value.length * 2; + + if (value instanceof ArrayBuffer) return value.byteLength; + if (ArrayBuffer.isView(value)) return value.byteLength; + + if (Array.isArray(value)) { + return value.reduce((sum, item) => sum + estimateSize(item), 0); + } + + if (typeof value === 'object') { + return Object.entries(value).reduce( + (sum, [key, val]) => sum + key.length * 2 + estimateSize(val), + 0 + ); + } + + return 0; +} + +// Usage +const size = estimateSize(myData); +console.log(`Payload: ${(size / 1024 / 1024).toFixed(2)} MB`); +``` + +--- + +## Optimization Strategies + +### 1. Minimize Data Transfer + +Only send what's needed: + +```typescript +// ❌ Sending entire objects +kit.register('processUsers', (users: User[]) => { + return users.map((u) => ({ id: u.id, score: calculateScore(u.age) })); +}); +await kit.run('processUsers', fullUserObjects); // Transfers everything + +// ✅ Send only required fields +kit.register('processAges', (ages: number[]) => { + return ages.map((age) => calculateScore(age)); +}); +const ages = users.map((u) => u.age); +await kit.run('processAges', ages); // Much smaller payload +``` + +### 2. Use Typed Arrays + +Typed arrays are more efficient than regular arrays: + +```typescript +// ❌ Regular array of numbers +const numbers = [1.5, 2.3, 3.7, ...]; // Each number is a JS object + +// ✅ Typed array +const numbers = new Float32Array([1.5, 2.3, 3.7, ...]); // Compact binary +``` + +**Size comparison for 1 million numbers:** +| Type | Approx Size | +|------|-------------| +| `number[]` | ~8-16 MB | +| `Float64Array` | 8 MB | +| `Float32Array` | 4 MB | +| `Int16Array` | 2 MB | +| `Uint8Array` | 1 MB | + +### 3. Batch Operations + +Reduce transfer overhead by batching: + +```typescript +// ❌ Many small transfers +for (const item of items) { + await kit.run('process', item); // Transfer overhead for each call +} + +// ✅ One large transfer +await kit.run('processBatch', items); // Single transfer +``` + +### 4. Use Transferables for Large ArrayBuffers + +```typescript +// Processing a large image +const imageBuffer = await fetchImageAsArrayBuffer(url); + +// Transfer instead of clone +const result = await kit.run('processImage', imageBuffer, { + transfer: [imageBuffer], +}); +``` + +### 5. Return Minimal Results + +```typescript +// ❌ Returning large intermediate data +kit.register('analyze', (data: number[]) => { + const allResults = heavyComputation(data); + return allResults; // Might be huge +}); + +// ✅ Return only what's needed +kit.register('analyze', (data: number[]) => { + const allResults = heavyComputation(data); + return { + summary: summarize(allResults), + count: allResults.length, + // Don't return allResults unless needed + }; +}); +``` + +--- + +## Performance Benchmarking + +### Built-in Timing + +Every compute result includes timing information: + +```typescript +const result = await kit.run('myFunction', data); +// Returns: { data: ..., duration: 145, workerId: 'w1', cached: false } +``` + +### Comparing Strategies + +```typescript +async function benchmark() { + const largeArray = new Float32Array(1_000_000); + + // Strategy 1: Structured clone + console.time('clone'); + await kit.run('process', largeArray.slice()); // Copy + console.timeEnd('clone'); + + // Strategy 2: Transfer + console.time('transfer'); + const toTransfer = largeArray.slice(); + await kit.run('process', toTransfer.buffer, { + transfer: [toTransfer.buffer], + }); + console.timeEnd('transfer'); + + // Strategy 3: Shared memory + console.time('shared'); + const shared = new SharedArrayBuffer(largeArray.byteLength); + new Float32Array(shared).set(largeArray); + await kit.run('processShared', shared); + console.timeEnd('shared'); +} +``` + +**Typical results (10MB payload):** +| Strategy | Transfer Time | Processing Time | +|----------|---------------|-----------------| +| Clone | ~50-100ms | + computation | +| Transfer | ~1-5ms | + computation | +| Shared | ~0ms | + computation | + +--- + +## When to Use Each Strategy + +| Scenario | Recommended Approach | +| -------------------------------- | --------------------------- | +| Small data (< 100KB) | Structured clone (default) | +| Large ArrayBuffers | Transferables | +| Multiple operations on same data | SharedArrayBuffer | +| Read-only shared data | SharedArrayBuffer | +| Data needed after transfer | Structured clone | +| Maximum performance | SharedArrayBuffer + Atomics | + +--- + +## Avoiding Common Pitfalls + +### Pitfall 1: Accidental Large Clones + +```typescript +// ❌ Accidentally including large data +const context = { + config: { threshold: 0.5 }, + cache: hugeCache, // Oops! This gets cloned +}; +await kit.run('process', context); + +// ✅ Send only what's needed +await kit.run('process', { threshold: 0.5 }); +``` + +### Pitfall 2: Using Transferred Data + +```typescript +const buffer = new ArrayBuffer(1000); +await kit.run('process', buffer, { transfer: [buffer] }); + +// ❌ Error! Buffer is neutered +console.log(buffer.byteLength); // 0 + +// ✅ Copy first if you need the original +const copy = buffer.slice(); +await kit.run('process', buffer, { transfer: [buffer] }); +console.log(copy.byteLength); // 1000 +``` + +### Pitfall 3: Circular References + +```typescript +// ❌ Circular reference - can't be cloned +const obj: any = { name: 'test' }; +obj.self = obj; +await kit.run('process', obj); // Error! + +// ✅ Remove circular references first +const { self, ...safeObj } = obj; +await kit.run('process', safeObj); +``` + +--- + +## Summary + +1. **Under 100KB**: Don't worry about it, structured cloning is fine +2. **100KB - 1MB**: Consider typed arrays and minimal payloads +3. **Over 1MB**: Use Transferables for ArrayBuffers +4. **Repeated operations**: Use SharedArrayBuffer +5. **Always measure**: Enable `debug: true` to see actual payload sizes diff --git a/docs/pipeline.md b/docs/pipeline.md new file mode 100644 index 0000000..91ab6c0 --- /dev/null +++ b/docs/pipeline.md @@ -0,0 +1,542 @@ +--- +layout: default +title: Multi-Stage Pipelines +nav_order: 5 +--- + +# Multi-Stage Pipelines + +{: .no_toc } + +Build complex, debuggable multi-stage compute workflows with automatic progress tracking, error handling, and execution reports. +{: .fs-6 .fw-300 } + +## Table of contents + +{: .no_toc .text-delta } + +1. TOC + {:toc} + +--- + +## Overview + +ComputeKit's pipeline system enables you to chain multiple compute operations together, with each stage's output becoming the next stage's input. This is perfect for workflows like: + +- **File Processing**: Download → Parse → Transform → Compress +- **Data Pipelines**: Fetch → Validate → Process → Aggregate +- **Image Processing**: Load → Resize → Filter → Encode +- **ML Workflows**: Preprocess → Inference → Postprocess + +## Quick Start + +```tsx +import { usePipeline } from '@computekit/react'; + +function FileProcessor() { + const pipeline = usePipeline([ + { id: 'download', name: 'Download Files', functionName: 'downloadFiles' }, + { id: 'process', name: 'Process Files', functionName: 'processFiles' }, + { id: 'compress', name: 'Compress Output', functionName: 'compressFiles' }, + ]); + + return ( +
+ + + + + {pipeline.currentStage &&

Current: {pipeline.currentStage.name}

} +
+ ); +} +``` + +## The `usePipeline` Hook + +### Basic Usage + +```tsx +const pipeline = usePipeline(stages, options); +``` + +### Stage Configuration + +Each stage is defined with a `StageConfig` object: + +```tsx +interface StageConfig { + // Required + id: string; // Unique identifier + name: string; // Display name + functionName: string; // Registered compute function name + + // Optional + transformInput?: (input, previousResults) => any; // Transform before execution + transformOutput?: (output) => any; // Transform after execution + shouldSkip?: (input, previousResults) => boolean; // Conditionally skip + maxRetries?: number; // Retry attempts on failure (default: 0) + retryDelay?: number; // Delay between retries in ms (default: 1000) + options?: ComputeOptions; // Per-stage compute options +} +``` + +### Pipeline State + +The hook returns comprehensive state for debugging and UI: + +```tsx +const { + // Status + status, // 'idle' | 'running' | 'paused' | 'completed' | 'failed' | 'cancelled' + isRunning, // boolean + isComplete, // boolean + isFailed, // boolean + + // Progress + progress, // Overall progress (0-100) + currentStage, // Current StageInfo or null + currentStageIndex, // Index of current stage (-1 if not running) + stages, // Array of all StageInfo objects + + // Data + input, // Original input + output, // Final output (when complete) + stageResults, // Array of each stage's output + error, // Error if failed + + // Timing + startedAt, // Start timestamp + completedAt, // End timestamp + totalDuration, // Total duration in ms + + // Metrics (for debugging) + metrics, // PipelineMetrics object + + // Actions + run, // (input) => Promise + cancel, // () => void + reset, // () => void + pause, // () => void + resume, // () => void + retry, // () => Promise + getReport, // () => PipelineReport + + // Helpers + isStageComplete, // (stageId) => boolean + getStage, // (stageId) => StageInfo | undefined +} = usePipeline(stages, options); +``` + +## Examples + +### Complete Pipeline UI + +```tsx +function DataPipeline() { + const pipeline = usePipeline([ + { id: 'fetch', name: 'Fetch Data', functionName: 'fetchData' }, + { id: 'validate', name: 'Validate', functionName: 'validateData' }, + { id: 'transform', name: 'Transform', functionName: 'transformData' }, + { id: 'save', name: 'Save Results', functionName: 'saveData' }, + ]); + + return ( +
+ {/* Controls */} +
+ + + {pipeline.isRunning && ( + <> + + + + )} + + {pipeline.status === 'paused' && ( + + )} + + {pipeline.isFailed && } +
+ + {/* Overall Progress */} +
+
+
+
+ {pipeline.progress.toFixed(0)}% +
+ + {/* Stage List */} +
+ {pipeline.stages.map((stage, index) => ( +
+
+ {stage.status === 'completed' && '✓'} + {stage.status === 'running' && '⟳'} + {stage.status === 'failed' && '✗'} + {stage.status === 'pending' && '○'} + {stage.status === 'skipped' && '–'} +
+ +
+ {stage.name} + {stage.duration && ( + {stage.duration.toFixed(0)}ms + )} + {stage.error && {stage.error.message}} +
+ + {stage.status === 'running' && stage.progress && ( +
{stage.progress.toFixed(0)}%
+ )} +
+ ))} +
+ + {/* Error Display */} + {pipeline.error && ( +
+

Pipeline Failed

+

{pipeline.error.message}

+ +
+ )} + + {/* Success + Report */} + {pipeline.isComplete && ( +
+

Pipeline Complete!

+
{pipeline.getReport().summary}
+
+ )} +
+ ); +} +``` + +### Conditional Stage Skipping + +```tsx +const pipeline = usePipeline([ + { + id: 'fetch', + name: 'Fetch Data', + functionName: 'fetchData', + }, + { + id: 'cache-check', + name: 'Check Cache', + functionName: 'checkCache', + }, + { + id: 'process', + name: 'Process Data', + functionName: 'processData', + // Skip processing if cache hit + shouldSkip: (input, previousResults) => { + const cacheResult = previousResults[1]; + return cacheResult?.cacheHit === true; + }, + }, + { + id: 'save', + name: 'Save Results', + functionName: 'saveResults', + }, +]); +``` + +### Input/Output Transformation + +```tsx +const pipeline = usePipeline([ + { + id: 'fetch', + name: 'Fetch Users', + functionName: 'fetchUsers', + }, + { + id: 'enrich', + name: 'Enrich Data', + functionName: 'enrichUsers', + // Extract just the users array from previous result + transformInput: (input, previousResults) => { + const fetchResult = previousResults[0]; + return fetchResult.users; + }, + // Wrap the result + transformOutput: (output) => ({ + enrichedUsers: output, + timestamp: Date.now(), + }), + }, +]); +``` + +### With Retries + +```tsx +const pipeline = usePipeline([ + { + id: 'upload', + name: 'Upload Files', + functionName: 'uploadFiles', + maxRetries: 3, + retryDelay: 2000, // Wait 2s between retries + }, + { + id: 'process', + name: 'Process on Server', + functionName: 'serverProcess', + maxRetries: 2, + retryDelay: 5000, + }, +]); +``` + +## Parallel Batch Processing + +For processing multiple items in parallel within a single stage, use `useParallelBatch`: + +```tsx +import { useParallelBatch } from '@computekit/react'; + +function ImageProcessor() { + const batch = useParallelBatch('processImage', { + concurrency: 4, // Process 4 images at a time + }); + + const handleProcess = async () => { + const result = await batch.run(imageUrls); + + console.log(`Processed ${result.successful.length} images`); + console.log(`Failed: ${result.failed.length}`); + console.log(`Success rate: ${(result.successRate * 100).toFixed(0)}%`); + }; + + return ( +
+ + + {batch.loading && ( +
+ Processing: {batch.completedCount}/{batch.totalCount}( + {batch.progress.toFixed(0)}%) +
+ )} + + {batch.result && ( +
+

✓ {batch.result.successful.length} succeeded

+

✗ {batch.result.failed.length} failed

+

Duration: {batch.result.totalDuration.toFixed(0)}ms

+
+ )} +
+ ); +} +``` + +## Debugging & Reports + +### Pipeline Metrics + +Access detailed metrics for debugging: + +```tsx +const { metrics } = pipeline; + +console.log(metrics); +// { +// totalStages: 4, +// completedStages: 4, +// failedStages: 0, +// skippedStages: 0, +// totalRetries: 1, +// slowestStage: { id: 'process', name: 'Process Files', duration: 2340 }, +// fastestStage: { id: 'fetch', name: 'Fetch Data', duration: 120 }, +// averageStageDuration: 890, +// timeline: [...] // Detailed event timeline +// } +``` + +### Execution Report + +Generate a human-readable report: + +```tsx +const report = pipeline.getReport(); + +console.log(report.summary); +// Pipeline Status: COMPLETED +// Stages: 4/4 completed +// Success Rate: 100% +// Total Duration: 3.56s + +console.log(report.stageDetails); +// [ +// { name: 'Fetch Data', status: 'completed', duration: '120ms' }, +// { name: 'Validate', status: 'completed', duration: '45ms' }, +// { name: 'Process', status: 'completed', duration: '2.34s' }, +// { name: 'Save', status: 'completed', duration: '1.05s' }, +// ] + +console.log(report.timeline); +// [ +// '[10:30:01] Fetch Data: started', +// '[10:30:01] Fetch Data: completed (120ms)', +// '[10:30:01] Validate: started', +// ... +// ] + +console.log(report.insights); +// [ +// 'Slowest stage: Process Files (2.34s)', +// 'Fastest stage: Validate (45ms)', +// 'Average stage duration: 890ms', +// ] +``` + +### Timeline Visualization + +Build a timeline UI from the metrics: + +```tsx +function PipelineTimeline({ metrics }: { metrics: PipelineMetrics }) { + const startTime = metrics.timeline[0]?.timestamp ?? 0; + + return ( +
+ {metrics.timeline.map((event, i) => ( +
+
+
+ {event.stageName}: {event.event} + {event.duration && ` (${event.duration.toFixed(0)}ms)`} +
+
+ ))} +
+ ); +} +``` + +## Pipeline Options + +```tsx +const pipeline = usePipeline(stages, { + // Stop pipeline on first stage failure (default: true) + stopOnError: true, + + // Global timeout for entire pipeline + timeout: 60000, + + // Track detailed timeline (default: true) + trackTimeline: true, + + // Auto-run on mount + autoRun: false, + initialInput: undefined, + + // Callbacks + onStateChange: (state) => console.log('State:', state.status), + onStageStart: (stage) => console.log('Starting:', stage.name), + onStageComplete: (stage) => console.log('Completed:', stage.name), + onStageError: (stage, error) => console.error('Failed:', stage.name, error), + onStageRetry: (stage, attempt) => console.log('Retry:', stage.name, attempt), +}); +``` + +## Combining Pipeline with Parallel Batch + +For the user's original use case (multi-file download → process → compress): + +```tsx +function MultiFileProcessor() { + const kit = useComputeKit(); + + // Register functions that handle batches + useEffect(() => { + kit.register('downloadBatch', async (urls: string[]) => { + // Download all files in parallel + return Promise.all(urls.map((url) => fetch(url).then((r) => r.arrayBuffer()))); + }); + + kit.register('processBatch', async (files: ArrayBuffer[]) => { + // Process all files in parallel + return Promise.all(files.map((file) => processFile(file))); + }); + + kit.register('compressBatch', async (files: ProcessedFile[]) => { + // Compress all files + return Promise.all(files.map((file) => compress(file))); + }); + }, [kit]); + + const pipeline = usePipeline([ + { id: 'download', name: 'Download Files', functionName: 'downloadBatch' }, + { id: 'process', name: 'Process Files', functionName: 'processBatch' }, + { id: 'compress', name: 'Compress Files', functionName: 'compressBatch' }, + ]); + + return ( +
+ + + + + {pipeline.isComplete && ( +
+ Processed {pipeline.output?.length} files in{' '} + {(pipeline.totalDuration! / 1000).toFixed(2)}s +
+ )} +
+ ); +} +``` + +## Type Safety + +The pipeline is fully typed: + +```tsx +interface InputData { + urls: string[]; + options: ProcessOptions; +} + +interface OutputData { + files: ProcessedFile[]; + stats: ProcessingStats; +} + +const pipeline = usePipeline([ + // ... stages +]); + +// pipeline.input is InputData | null +// pipeline.output is OutputData | null +// TypeScript will enforce types throughout +``` diff --git a/docs/react-hooks.md b/docs/react-hooks.md index 5fea34b..03e3ea9 100644 --- a/docs/react-hooks.md +++ b/docs/react-hooks.md @@ -305,7 +305,30 @@ run({ data: [...], width: 256, height: 256 }); --- +## usePipeline & useParallelBatch + +For complex multi-stage workflows and parallel batch processing, see the dedicated [Multi-Stage Pipelines]({{ site.baseurl }}/pipeline) guide. + +Quick preview: + +```tsx +// Multi-stage pipeline +const pipeline = usePipeline([ + { id: 'download', name: 'Download', functionName: 'downloadFiles' }, + { id: 'process', name: 'Process', functionName: 'processFiles' }, + { id: 'compress', name: 'Compress', functionName: 'compressFiles' }, +]); + +// Parallel batch processing +const batch = useParallelBatch('processFile', { + concurrency: 4, +}); +``` + +--- + ## Next Steps +- Check the [Multi-Stage Pipelines]({{ site.baseurl }}/pipeline) for complex workflows - Check the [API Reference]({{ site.baseurl }}/api-reference) for the complete API - Learn about [WASM integration]({{ site.baseurl }}/wasm) for native-speed performance diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bee103d..cce2b7c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -205,6 +205,20 @@ export type { WorkerInfo, WasmModuleConfig, ComputeKitEvents, + // Pipeline types + StageStatus, + StageInfo, + StageConfig, + PipelineMode, + PipelineStatus, + PipelineState, + PipelineMetrics, + PipelineOptions, + PipelineEvents, + // Parallel batch types + ParallelBatchConfig, + BatchItemResult, + ParallelBatchResult, } from './types'; // Re-export utilities diff --git a/packages/core/src/pool.ts b/packages/core/src/pool.ts index ca0a431..30880ec 100644 --- a/packages/core/src/pool.ts +++ b/packages/core/src/pool.ts @@ -24,6 +24,8 @@ import { findTransferables, getHardwareConcurrency, createLogger, + estimatePayloadSize, + formatBytes, type Deferred, type Logger, } from './utils'; @@ -427,8 +429,14 @@ self.postMessage({ type: 'ready' }); this.stats.tasksCompleted++; this.stats.totalDuration += resultPayload.duration; + // Log with payload sizes in debug mode + const inputSize = estimatePayloadSize(task.input); + const outputSize = resultPayload.outputSize ?? 0; this.logger.debug( - `Task ${id} completed in ${resultPayload.duration.toFixed(2)}ms` + `Task ${id} (${task.functionName}): ` + + `input=${formatBytes(inputSize)}, ` + + `output=${formatBytes(outputSize)}, ` + + `duration=${resultPayload.duration.toFixed(2)}ms` ); task.deferred.resolve(resultPayload.data); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 9253667..dcd8eef 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -55,6 +55,10 @@ export interface ComputeResult { cached: boolean; /** Worker ID that processed this */ workerId: string; + /** Size of input data in bytes (available in debug mode) */ + inputSize?: number; + /** Size of output data in bytes (available in debug mode) */ + outputSize?: number; } /** Function definition for registration */ @@ -111,6 +115,8 @@ export interface ResultPayload { data: T; duration: number; transfer?: ArrayBuffer[]; + /** Size of the output data in bytes */ + outputSize?: number; } /** Error message payload */ @@ -118,6 +124,10 @@ export interface ErrorPayload { message: string; stack?: string; code?: string; + /** Name of the function that failed */ + functionName?: string; + /** Duration before the error occurred in ms */ + duration?: number; } /** Progress message payload */ @@ -213,6 +223,174 @@ export type ComputeFn = ( /** Registry of compute functions */ export type ComputeRegistry = Map; +// ============================================================================ +// Pipeline Types - Multi-stage Processing +// ============================================================================ + +/** Status of a pipeline stage */ +export type StageStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped'; + +/** Detailed information about a single pipeline stage */ +export interface StageInfo { + /** Unique identifier for the stage */ + id: string; + /** Display name for the stage */ + name: string; + /** Name of the registered compute function to execute */ + functionName: string; + /** Current status of this stage */ + status: StageStatus; + /** Input data for this stage (set when stage starts) */ + input?: TInput; + /** Output data from this stage (set when stage completes) */ + output?: TOutput; + /** Error if stage failed */ + error?: Error; + /** Start timestamp (ms since epoch) */ + startedAt?: number; + /** End timestamp (ms since epoch) */ + completedAt?: number; + /** Duration in milliseconds */ + duration?: number; + /** Progress within this stage (0-100) */ + progress?: number; + /** Number of retry attempts */ + retryCount: number; + /** Compute options specific to this stage */ + options?: ComputeOptions; +} + +/** Pipeline execution mode */ +export type PipelineMode = + | 'sequential' // Each stage runs after previous completes + | 'parallel'; // All items in a stage run in parallel, then next stage + +/** Configuration for a pipeline stage */ +export interface StageConfig { + /** Unique identifier for the stage */ + id: string; + /** Display name for the stage */ + name: string; + /** Name of the registered compute function */ + functionName: string; + /** Transform input before passing to compute function */ + transformInput?: (input: TInput, previousResults: unknown[]) => unknown; + /** Transform output after compute function returns */ + transformOutput?: (output: unknown) => TOutput; + /** Whether to skip this stage based on previous results */ + shouldSkip?: (input: TInput, previousResults: unknown[]) => boolean; + /** Maximum retry attempts on failure (default: 0) */ + maxRetries?: number; + /** Delay between retries in ms (default: 1000) */ + retryDelay?: number; + /** Compute options for this stage */ + options?: ComputeOptions; +} + +/** Overall pipeline status */ +export type PipelineStatus = + | 'idle' // Not started + | 'running' // Currently executing + | 'paused' // Paused mid-execution + | 'completed' // All stages completed successfully + | 'failed' // A stage failed (and wasn't recovered) + | 'cancelled'; // User cancelled + +/** Comprehensive pipeline state for debugging */ +export interface PipelineState { + /** Overall pipeline status */ + status: PipelineStatus; + /** All stage information */ + stages: StageInfo[]; + /** Index of currently executing stage (-1 if not running) */ + currentStageIndex: number; + /** Current stage info (convenience) */ + currentStage: StageInfo | null; + /** Overall progress percentage (0-100) */ + progress: number; + /** Final output from the last stage */ + output: TOutput | null; + /** Initial input that started the pipeline */ + input: TInput | null; + /** Error that caused pipeline failure */ + error: Error | null; + /** Pipeline start timestamp */ + startedAt: number | null; + /** Pipeline completion timestamp */ + completedAt: number | null; + /** Total duration in milliseconds */ + totalDuration: number | null; + /** Results from each completed stage */ + stageResults: unknown[]; + /** Execution metrics for debugging */ + metrics: PipelineMetrics; +} + +/** Metrics for pipeline debugging and reporting */ +export interface PipelineMetrics { + /** Total stages in pipeline */ + totalStages: number; + /** Number of completed stages */ + completedStages: number; + /** Number of failed stages */ + failedStages: number; + /** Number of skipped stages */ + skippedStages: number; + /** Total retry attempts across all stages */ + totalRetries: number; + /** Slowest stage info */ + slowestStage: { id: string; name: string; duration: number } | null; + /** Fastest stage info */ + fastestStage: { id: string; name: string; duration: number } | null; + /** Average stage duration */ + averageStageDuration: number; + /** Timestamp of each stage transition for timeline view */ + timeline: Array<{ + stageId: string; + stageName: string; + event: 'started' | 'completed' | 'failed' | 'skipped' | 'retry'; + timestamp: number; + duration?: number; + error?: string; + }>; +} + +/** Pipeline configuration options */ +export interface PipelineOptions { + /** Execution mode (default: 'sequential') */ + mode?: PipelineMode; + /** Stop pipeline on first stage failure (default: true) */ + stopOnError?: boolean; + /** Global timeout for entire pipeline in ms */ + timeout?: number; + /** Enable detailed timeline tracking (default: true) */ + trackTimeline?: boolean; + /** Called when pipeline state changes */ + onStateChange?: (state: PipelineState) => void; + /** Called when a stage starts */ + onStageStart?: (stage: StageInfo) => void; + /** Called when a stage completes */ + onStageComplete?: (stage: StageInfo) => void; + /** Called when a stage fails */ + onStageError?: (stage: StageInfo, error: Error) => void; + /** Called when a stage is retried */ + onStageRetry?: (stage: StageInfo, attempt: number) => void; +} + +/** Events emitted by Pipeline */ +export interface PipelineEvents { + 'pipeline:start': { input: unknown }; + 'pipeline:complete': { output: unknown; duration: number }; + 'pipeline:error': { error: Error; stageId: string }; + 'pipeline:cancel': { stageId: string }; + 'stage:start': StageInfo; + 'stage:progress': { stageId: string; progress: number }; + 'stage:complete': StageInfo; + 'stage:error': { stage: StageInfo; error: Error }; + 'stage:skip': StageInfo; + 'stage:retry': { stage: StageInfo; attempt: number }; +} + /** Transferable types */ export type Transferable = ArrayBuffer | MessagePort | ImageBitmap | OffscreenCanvas; @@ -229,3 +407,47 @@ export type Serializable = | ArrayBufferView | Map | Set; + +// ============================================================================ +// Parallel Batch Types - For parallel processing within stages +// ============================================================================ + +/** Configuration for parallel batch processing */ +export interface ParallelBatchConfig { + /** Items to process in parallel */ + items: TItem[]; + /** Name of the registered compute function */ + functionName: string; + /** Maximum concurrent executions (default: all) */ + concurrency?: number; + /** Compute options for batch items */ + options?: ComputeOptions; +} + +/** Result of a single item in parallel batch */ +export interface BatchItemResult { + /** Index of the item in original array */ + index: number; + /** Whether this item succeeded */ + success: boolean; + /** Result if successful */ + data?: TOutput; + /** Error if failed */ + error?: Error; + /** Duration in ms */ + duration: number; +} + +/** Aggregate result of parallel batch processing */ +export interface ParallelBatchResult { + /** All individual results */ + results: BatchItemResult[]; + /** Successfully processed items */ + successful: TOutput[]; + /** Failed items with their errors */ + failed: Array<{ index: number; error: Error }>; + /** Total duration */ + totalDuration: number; + /** Success rate (0-1) */ + successRate: number; +} diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 9acc22b..d0b0d6b 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -279,6 +279,80 @@ export function serializeFunction(fn: Function): string { return fn.toString(); } +/** + * Estimate the byte size of a value for structured cloning. + * This is an approximation useful for debugging and performance monitoring. + */ +export function estimatePayloadSize(value: unknown): number { + if (value === null || value === undefined) return 0; + if (typeof value === 'boolean') return 4; + if (typeof value === 'number') return 8; + if (typeof value === 'string') return value.length * 2; // UTF-16 + + if (value instanceof ArrayBuffer) return value.byteLength; + if (ArrayBuffer.isView(value)) return value.byteLength; + if (value instanceof Blob) return value.size; + + const seen = new WeakSet(); + + function traverse(obj: unknown): number { + if (obj === null || typeof obj !== 'object') { + if (typeof obj === 'boolean') return 4; + if (typeof obj === 'number') return 8; + if (typeof obj === 'string') return (obj as string).length * 2; + return 0; + } + + if (seen.has(obj)) return 0; // Avoid infinite loops + seen.add(obj); + + if (obj instanceof ArrayBuffer) return obj.byteLength; + if (ArrayBuffer.isView(obj)) return obj.byteLength; + if (obj instanceof Blob) return obj.size; + if (obj instanceof Date) return 8; + if (obj instanceof RegExp) return obj.source.length * 2; + + if (Array.isArray(obj)) { + return obj.reduce((sum, item) => sum + traverse(item), 0); + } + + if (obj instanceof Map) { + let size = 0; + obj.forEach((val, key) => { + size += traverse(key) + traverse(val); + }); + return size; + } + + if (obj instanceof Set) { + let size = 0; + obj.forEach((val) => { + size += traverse(val); + }); + return size; + } + + // Plain object + return Object.entries(obj).reduce( + (sum, [key, val]) => sum + key.length * 2 + traverse(val), + 0 + ); + } + + return traverse(value); +} + +/** + * Format bytes to human-readable string + */ +export function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / Math.pow(k, i)).toFixed(i > 0 ? 1 : 0)} ${sizes[i]}`; +} + /** * Logger utility */ diff --git a/packages/core/src/worker/runtime.ts b/packages/core/src/worker/runtime.ts index ad0d9bc..4178592 100644 --- a/packages/core/src/worker/runtime.ts +++ b/packages/core/src/worker/runtime.ts @@ -11,7 +11,7 @@ import type { ComputeProgress, } from '../types'; -import { generateId, findTransferables } from '../utils'; +import { generateId, findTransferables, estimatePayloadSize } from '../utils'; /** Registry of compute functions available in the worker */ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type @@ -91,12 +91,16 @@ async function handleMessage(event: MessageEvent): Promise // Find transferable objects in result const transfer = findTransferables(result); + // Estimate output size for debugging + const outputSize = estimatePayloadSize(result); + const response: WorkerMessage = { id, type: 'result', payload: { data: result, duration, + outputSize, }, timestamp: Date.now(), }; @@ -104,6 +108,7 @@ async function handleMessage(event: MessageEvent): Promise self.postMessage(response, transfer as Transferable[]); } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); + const duration = performance.now() - startTime; const response: WorkerMessage = { id, @@ -111,6 +116,8 @@ async function handleMessage(event: MessageEvent): Promise payload: { message: error.message, stack: error.stack, + functionName, + duration, }, timestamp: Date.now(), }; diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 85f8ffd..63e87a5 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -22,6 +22,161 @@ import { type PoolStats, } from '@computekit/core'; +// ============================================================================ +// Pipeline Types (defined here for React, also exported from @computekit/core) +// ============================================================================ + +/** Status of a pipeline stage */ +export type StageStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped'; + +/** Detailed information about a single pipeline stage */ +export interface StageInfo { + /** Unique identifier for the stage */ + id: string; + /** Display name for the stage */ + name: string; + /** Name of the registered compute function to execute */ + functionName: string; + /** Current status of this stage */ + status: StageStatus; + /** Input data for this stage (set when stage starts) */ + input?: TInput; + /** Output data from this stage (set when stage completes) */ + output?: TOutput; + /** Error if stage failed */ + error?: Error; + /** Start timestamp (ms since epoch) */ + startedAt?: number; + /** End timestamp (ms since epoch) */ + completedAt?: number; + /** Duration in milliseconds */ + duration?: number; + /** Progress within this stage (0-100) */ + progress?: number; + /** Number of retry attempts */ + retryCount: number; + /** Compute options specific to this stage */ + options?: ComputeOptions; +} + +/** Configuration for a pipeline stage */ +export interface StageConfig { + /** Unique identifier for the stage */ + id: string; + /** Display name for the stage */ + name: string; + /** Name of the registered compute function */ + functionName: string; + /** Transform input before passing to compute function */ + transformInput?: (input: TInput, previousResults: unknown[]) => unknown; + /** Transform output after compute function returns */ + transformOutput?: (output: unknown) => TOutput; + /** Whether to skip this stage based on previous results */ + shouldSkip?: (input: TInput, previousResults: unknown[]) => boolean; + /** Maximum retry attempts on failure (default: 0) */ + maxRetries?: number; + /** Delay between retries in ms (default: 1000) */ + retryDelay?: number; + /** Compute options for this stage */ + options?: ComputeOptions; +} + +/** Overall pipeline status */ +export type PipelineStatus = + | 'idle' // Not started + | 'running' // Currently executing + | 'paused' // Paused mid-execution + | 'completed' // All stages completed successfully + | 'failed' // A stage failed (and wasn't recovered) + | 'cancelled'; // User cancelled + +/** Metrics for pipeline debugging and reporting */ +export interface PipelineMetrics { + /** Total stages in pipeline */ + totalStages: number; + /** Number of completed stages */ + completedStages: number; + /** Number of failed stages */ + failedStages: number; + /** Number of skipped stages */ + skippedStages: number; + /** Total retry attempts across all stages */ + totalRetries: number; + /** Slowest stage info */ + slowestStage: { id: string; name: string; duration: number } | null; + /** Fastest stage info */ + fastestStage: { id: string; name: string; duration: number } | null; + /** Average stage duration */ + averageStageDuration: number; + /** Timestamp of each stage transition for timeline view */ + timeline: Array<{ + stageId: string; + stageName: string; + event: 'started' | 'completed' | 'failed' | 'skipped' | 'retry'; + timestamp: number; + duration?: number; + error?: string; + }>; +} + +/** Comprehensive pipeline state for debugging */ +export interface PipelineState { + /** Overall pipeline status */ + status: PipelineStatus; + /** All stage information */ + stages: StageInfo[]; + /** Index of currently executing stage (-1 if not running) */ + currentStageIndex: number; + /** Current stage info (convenience) */ + currentStage: StageInfo | null; + /** Overall progress percentage (0-100) */ + progress: number; + /** Final output from the last stage */ + output: TOutput | null; + /** Initial input that started the pipeline */ + input: TInput | null; + /** Error that caused pipeline failure */ + error: Error | null; + /** Pipeline start timestamp */ + startedAt: number | null; + /** Pipeline completion timestamp */ + completedAt: number | null; + /** Total duration in milliseconds */ + totalDuration: number | null; + /** Results from each completed stage */ + stageResults: unknown[]; + /** Execution metrics for debugging */ + metrics: PipelineMetrics; +} + +/** Result of a single item in parallel batch */ +export interface BatchItemResult { + /** Index of the item in original array */ + index: number; + /** Whether this item succeeded */ + success: boolean; + /** Result if successful */ + data?: TOutput; + /** Error if failed */ + error?: Error; + /** Duration in ms */ + duration: number; +} + +/** Aggregate result of parallel batch processing */ +export interface ParallelBatchResult { + /** All individual results */ + results: BatchItemResult[]; + /** Successfully processed items */ + successful: TOutput[]; + /** Failed items with their errors */ + failed: Array<{ index: number; error: Error }>; + /** Total duration */ + totalDuration: number; + /** Success rate (0-1) */ + successRate: number; +} + // ============================================================================ // Context // ============================================================================ @@ -417,6 +572,1024 @@ export function useWasmSupport(): boolean { return kit.isWasmSupported(); } +// ============================================================================ +// usePipeline Hook - Multi-stage Processing +// ============================================================================ + +/** + * Options for usePipeline hook + */ +export interface UsePipelineOptions { + /** Stop pipeline on first stage failure (default: true) */ + stopOnError?: boolean; + /** Global timeout for entire pipeline in ms */ + timeout?: number; + /** Enable detailed timeline tracking (default: true) */ + trackTimeline?: boolean; + /** Called when pipeline state changes */ + onStateChange?: (state: PipelineState) => void; + /** Called when a stage starts */ + onStageStart?: (stage: StageInfo) => void; + /** Called when a stage completes */ + onStageComplete?: (stage: StageInfo) => void; + /** Called when a stage fails */ + onStageError?: (stage: StageInfo, error: Error) => void; + /** Called when a stage is retried */ + onStageRetry?: (stage: StageInfo, attempt: number) => void; + /** Automatically run pipeline on mount */ + autoRun?: boolean; + /** Initial input for autoRun */ + initialInput?: unknown; +} + +/** + * Actions returned by usePipeline + */ +export interface UsePipelineActions { + /** Start the pipeline with input */ + run: (input: TInput) => Promise; + /** Cancel the running pipeline */ + cancel: () => void; + /** Reset pipeline to initial state */ + reset: () => void; + /** Pause the pipeline (if supported) */ + pause: () => void; + /** Resume a paused pipeline */ + resume: () => void; + /** Retry failed stages */ + retry: () => Promise; + /** Get a formatted report of the pipeline execution */ + getReport: () => PipelineReport; +} + +/** + * Formatted report for debugging + */ +export interface PipelineReport { + /** Human-readable summary */ + summary: string; + /** Detailed stage-by-stage breakdown */ + stageDetails: Array<{ + name: string; + status: StageStatus; + duration: string; + error?: string; + }>; + /** Timeline of events */ + timeline: string[]; + /** Performance insights */ + insights: string[]; + /** Raw metrics */ + metrics: PipelineMetrics; +} + +/** + * Return type for usePipeline + */ +export type UsePipelineReturn = PipelineState & + UsePipelineActions & { + /** Whether pipeline is currently running */ + isRunning: boolean; + /** Whether pipeline completed successfully */ + isComplete: boolean; + /** Whether pipeline has failed */ + isFailed: boolean; + /** Quick access to check if a specific stage is done */ + isStageComplete: (stageId: string) => boolean; + /** Get a specific stage by ID */ + getStage: (stageId: string) => StageInfo | undefined; + }; + +/** + * Create initial pipeline state + */ +function createInitialPipelineState( + stages: StageConfig[] +): PipelineState { + return { + status: 'idle', + stages: stages.map((config) => ({ + id: config.id, + name: config.name, + functionName: config.functionName, + status: 'pending' as StageStatus, + retryCount: 0, + options: config.options, + })), + currentStageIndex: -1, + currentStage: null, + progress: 0, + output: null, + input: null, + error: null, + startedAt: null, + completedAt: null, + totalDuration: null, + stageResults: [], + metrics: { + totalStages: stages.length, + completedStages: 0, + failedStages: 0, + skippedStages: 0, + totalRetries: 0, + slowestStage: null, + fastestStage: null, + averageStageDuration: 0, + timeline: [], + }, + }; +} + +/** + * Format duration in human-readable format + */ +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms.toFixed(0)}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`; + return `${(ms / 60000).toFixed(2)}min`; +} + +/** + * Hook for multi-stage pipeline processing + * + * Provides comprehensive debugging, progress tracking, and error handling + * for complex multi-stage compute workflows. + * + * @example + * ```tsx + * function FileProcessor() { + * const pipeline = usePipeline([ + * { id: 'download', name: 'Download Files', functionName: 'downloadFiles' }, + * { id: 'process', name: 'Process Files', functionName: 'processFiles' }, + * { id: 'compress', name: 'Compress Output', functionName: 'compressFiles' }, + * ]); + * + * return ( + *
+ * + * + *
Status: {pipeline.status}
+ *
Progress: {pipeline.progress.toFixed(0)}%
+ * + * {pipeline.currentStage && ( + *
Current: {pipeline.currentStage.name}
+ * )} + * + * {pipeline.stages.map(stage => ( + *
+ * {stage.name}: {stage.status} + * {stage.duration && ` (${stage.duration}ms)`} + *
+ * ))} + * + * {pipeline.isFailed && ( + * + * )} + * + * {pipeline.isComplete && ( + *
{JSON.stringify(pipeline.getReport(), null, 2)}
+ * )} + *
+ * ); + * } + * ``` + */ +export function usePipeline( + stageConfigs: StageConfig[], + options: UsePipelineOptions = {} +): UsePipelineReturn { + const kit = useComputeKit(); + const abortControllerRef = useRef(null); + const pausedRef = useRef(false); + const resumePromiseRef = useRef<{ + resolve: () => void; + reject: (err: Error) => void; + } | null>(null); + + const [state, setState] = useState>(() => + createInitialPipelineState(stageConfigs) + ); + + // Memoize stage configs to prevent unnecessary re-renders + const stages = useMemo(() => stageConfigs, [stageConfigs]); + + /** + * Add event to timeline + */ + const addTimelineEvent = useCallback( + ( + stageId: string, + stageName: string, + event: 'started' | 'completed' | 'failed' | 'skipped' | 'retry', + duration?: number, + error?: string + ) => { + if (options.trackTimeline === false) return; + + setState((prev) => ({ + ...prev, + metrics: { + ...prev.metrics, + timeline: [ + ...prev.metrics.timeline, + { + stageId, + stageName, + event, + timestamp: Date.now(), + duration, + error, + }, + ], + }, + })); + }, + [options.trackTimeline] + ); + + /** + * Update metrics after stage completion + */ + const updateMetrics = useCallback( + (_completedStage: StageInfo, allStages: StageInfo[]) => { + const completedStages = allStages.filter((s) => s.status === 'completed'); + const durations = completedStages + .filter((s) => s.duration !== undefined) + .map((s) => ({ id: s.id, name: s.name, duration: s.duration! })); + + const slowest = durations.length + ? durations.reduce((a, b) => (a.duration > b.duration ? a : b)) + : null; + const fastest = durations.length + ? durations.reduce((a, b) => (a.duration < b.duration ? a : b)) + : null; + const avgDuration = durations.length + ? durations.reduce((sum, d) => sum + d.duration, 0) / durations.length + : 0; + + return { + totalStages: allStages.length, + completedStages: completedStages.length, + failedStages: allStages.filter((s) => s.status === 'failed').length, + skippedStages: allStages.filter((s) => s.status === 'skipped').length, + totalRetries: allStages.reduce((sum, s) => sum + s.retryCount, 0), + slowestStage: slowest, + fastestStage: fastest, + averageStageDuration: avgDuration, + }; + }, + [] + ); + + /** + * Execute a single stage with retries + */ + const executeStage = useCallback( + async ( + stageConfig: StageConfig, + stageIndex: number, + input: unknown, + previousResults: unknown[], + signal: AbortSignal + ): Promise<{ success: boolean; output?: unknown; error?: Error }> => { + const maxRetries = stageConfig.maxRetries ?? 0; + const retryDelay = stageConfig.retryDelay ?? 1000; + let lastError: Error | undefined; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + // Check for abort + if (signal.aborted) { + return { success: false, error: new Error('Pipeline cancelled') }; + } + + // Check for pause + if (pausedRef.current) { + await new Promise((resolve, reject) => { + resumePromiseRef.current = { resolve, reject }; + }); + } + + // Check if should skip + if (stageConfig.shouldSkip?.(input as never, previousResults)) { + setState((prev) => { + const newStages = [...prev.stages]; + newStages[stageIndex] = { + ...newStages[stageIndex], + status: 'skipped', + }; + return { + ...prev, + stages: newStages, + }; + }); + addTimelineEvent(stageConfig.id, stageConfig.name, 'skipped'); + options.onStageComplete?.(state.stages[stageIndex]); + return { success: true, output: previousResults[previousResults.length - 1] }; + } + + // Transform input if needed + const transformedInput = stageConfig.transformInput + ? stageConfig.transformInput(input as never, previousResults) + : input; + + const startTime = performance.now(); + + // Update stage to running + setState((prev) => { + const newStages = [...prev.stages]; + newStages[stageIndex] = { + ...newStages[stageIndex], + status: 'running', + input: transformedInput, + startedAt: Date.now(), + retryCount: attempt, + }; + return { + ...prev, + stages: newStages, + currentStageIndex: stageIndex, + currentStage: newStages[stageIndex], + }; + }); + + if (attempt === 0) { + addTimelineEvent(stageConfig.id, stageConfig.name, 'started'); + options.onStageStart?.(state.stages[stageIndex]); + } else { + addTimelineEvent(stageConfig.id, stageConfig.name, 'retry'); + options.onStageRetry?.(state.stages[stageIndex], attempt); + } + + try { + const result = await kit.run(stageConfig.functionName, transformedInput, { + ...stageConfig.options, + signal, + onProgress: (progress) => { + setState((prev) => { + const newStages = [...prev.stages]; + newStages[stageIndex] = { + ...newStages[stageIndex], + progress: progress.percent, + }; + // Calculate overall progress + const stageProgress = progress.percent / 100; + const overallProgress = + ((stageIndex + stageProgress) / stages.length) * 100; + return { + ...prev, + stages: newStages, + progress: overallProgress, + }; + }); + }, + }); + + const duration = performance.now() - startTime; + + // Transform output if needed + const transformedOutput = stageConfig.transformOutput + ? stageConfig.transformOutput(result) + : result; + + // Update stage to completed + setState((prev) => { + const newStages = [...prev.stages]; + newStages[stageIndex] = { + ...newStages[stageIndex], + status: 'completed', + output: transformedOutput, + completedAt: Date.now(), + duration, + progress: 100, + }; + + const newMetrics = { + ...prev.metrics, + ...updateMetrics(newStages[stageIndex], newStages), + }; + + return { + ...prev, + stages: newStages, + metrics: newMetrics, + progress: ((stageIndex + 1) / stages.length) * 100, + }; + }); + + addTimelineEvent(stageConfig.id, stageConfig.name, 'completed', duration); + options.onStageComplete?.(state.stages[stageIndex]); + + return { success: true, output: transformedOutput }; + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)); + + if (attempt < maxRetries) { + // Wait before retry + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + continue; + } + + // Final failure + const duration = performance.now() - startTime; + + setState((prev) => { + const newStages = [...prev.stages]; + newStages[stageIndex] = { + ...newStages[stageIndex], + status: 'failed', + error: lastError, + completedAt: Date.now(), + duration, + }; + return { + ...prev, + stages: newStages, + metrics: { + ...prev.metrics, + failedStages: prev.metrics.failedStages + 1, + }, + }; + }); + + addTimelineEvent( + stageConfig.id, + stageConfig.name, + 'failed', + duration, + lastError.message + ); + options.onStageError?.(state.stages[stageIndex], lastError); + + return { success: false, error: lastError }; + } + } + + return { success: false, error: lastError }; + }, + [kit, stages, state.stages, addTimelineEvent, updateMetrics, options] + ); + + /** + * Run the pipeline + */ + const run = useCallback( + async (input: TInput): Promise => { + // Cancel any existing run + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + const abortController = new AbortController(); + abortControllerRef.current = abortController; + pausedRef.current = false; + + const startTime = Date.now(); + + // Reset state + setState(() => ({ + ...createInitialPipelineState(stages), + status: 'running', + input, + startedAt: startTime, + })); + + const stageResults: unknown[] = []; + let currentInput: unknown = input; + let finalError: Error | null = null; + + for (let i = 0; i < stages.length; i++) { + if (abortController.signal.aborted) { + setState((prev) => ({ + ...prev, + status: 'cancelled', + completedAt: Date.now(), + totalDuration: Date.now() - startTime, + })); + return; + } + + const result = await executeStage( + stages[i], + i, + currentInput, + stageResults, + abortController.signal + ); + + if (!result.success) { + finalError = result.error ?? new Error('Stage failed'); + + if (options.stopOnError !== false) { + setState((prev) => ({ + ...prev, + status: 'failed', + error: finalError, + stageResults, + completedAt: Date.now(), + totalDuration: Date.now() - startTime, + })); + return; + } + } + + if (result.output !== undefined) { + stageResults.push(result.output); + currentInput = result.output; + } + } + + // Pipeline completed + setState((prev) => ({ + ...prev, + status: finalError ? 'failed' : 'completed', + output: (currentInput as TOutput) ?? null, + error: finalError, + stageResults, + completedAt: Date.now(), + totalDuration: Date.now() - startTime, + currentStageIndex: -1, + currentStage: null, + progress: 100, + })); + + options.onStateChange?.(state); + }, + [stages, executeStage, options, state] + ); + + /** + * Cancel the pipeline + */ + const cancel = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + if (resumePromiseRef.current) { + resumePromiseRef.current.reject(new Error('Pipeline cancelled')); + resumePromiseRef.current = null; + } + setState((prev) => ({ + ...prev, + status: 'cancelled', + completedAt: Date.now(), + totalDuration: prev.startedAt ? Date.now() - prev.startedAt : null, + })); + }, []); + + /** + * Reset the pipeline + */ + const reset = useCallback(() => { + cancel(); + setState(createInitialPipelineState(stages)); + }, [cancel, stages]); + + /** + * Pause the pipeline + */ + const pause = useCallback(() => { + pausedRef.current = true; + setState((prev) => ({ + ...prev, + status: 'paused', + })); + }, []); + + /** + * Resume the pipeline + */ + const resume = useCallback(() => { + pausedRef.current = false; + if (resumePromiseRef.current) { + resumePromiseRef.current.resolve(); + resumePromiseRef.current = null; + } + setState((prev) => ({ + ...prev, + status: 'running', + })); + }, []); + + /** + * Retry failed stages + */ + const retry = useCallback(async (): Promise => { + if (state.status !== 'failed' || !state.input) return; + + // Find first failed stage + const failedIndex = state.stages.findIndex((s) => s.status === 'failed'); + if (failedIndex === -1) return; + + // Get input for failed stage (output of previous stage or original input) + const retryInput = + failedIndex === 0 ? state.input : state.stageResults[failedIndex - 1]; + + // Create new abort controller + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + setState((prev) => ({ + ...prev, + status: 'running', + error: null, + })); + + const stageResults = [...state.stageResults.slice(0, failedIndex)]; + let currentInput = retryInput; + + for (let i = failedIndex; i < stages.length; i++) { + if (abortController.signal.aborted) { + setState((prev) => ({ ...prev, status: 'cancelled' })); + return; + } + + const result = await executeStage( + stages[i], + i, + currentInput, + stageResults, + abortController.signal + ); + + if (!result.success) { + setState((prev) => ({ + ...prev, + status: 'failed', + error: result.error ?? new Error('Stage failed'), + stageResults, + })); + return; + } + + if (result.output !== undefined) { + stageResults.push(result.output); + currentInput = result.output; + } + } + + setState((prev) => ({ + ...prev, + status: 'completed', + output: currentInput as TOutput, + stageResults, + completedAt: Date.now(), + totalDuration: prev.startedAt ? Date.now() - prev.startedAt : null, + progress: 100, + })); + }, [state, stages, executeStage]); + + /** + * Generate execution report + */ + const getReport = useCallback((): PipelineReport => { + const stageDetails = state.stages.map((stage) => ({ + name: stage.name, + status: stage.status, + duration: stage.duration ? formatDuration(stage.duration) : '-', + error: stage.error?.message, + })); + + const timeline = state.metrics.timeline.map((event) => { + const time = new Date(event.timestamp).toISOString().split('T')[1].split('.')[0]; + const duration = event.duration ? ` (${formatDuration(event.duration)})` : ''; + const error = event.error ? ` - ${event.error}` : ''; + return `[${time}] ${event.stageName}: ${event.event}${duration}${error}`; + }); + + const insights: string[] = []; + + if (state.metrics.slowestStage) { + insights.push( + `Slowest stage: ${state.metrics.slowestStage.name} (${formatDuration( + state.metrics.slowestStage.duration + )})` + ); + } + + if (state.metrics.fastestStage) { + insights.push( + `Fastest stage: ${state.metrics.fastestStage.name} (${formatDuration( + state.metrics.fastestStage.duration + )})` + ); + } + + if (state.metrics.totalRetries > 0) { + insights.push(`Total retries: ${state.metrics.totalRetries}`); + } + + if (state.metrics.averageStageDuration > 0) { + insights.push( + `Average stage duration: ${formatDuration(state.metrics.averageStageDuration)}` + ); + } + + const successRate = + state.metrics.totalStages > 0 + ? (state.metrics.completedStages / state.metrics.totalStages) * 100 + : 0; + + const summary = [ + `Pipeline Status: ${state.status.toUpperCase()}`, + `Stages: ${state.metrics.completedStages}/${state.metrics.totalStages} completed`, + `Success Rate: ${successRate.toFixed(0)}%`, + state.totalDuration ? `Total Duration: ${formatDuration(state.totalDuration)}` : '', + state.error ? `Error: ${state.error.message}` : '', + ] + .filter(Boolean) + .join('\n'); + + return { + summary, + stageDetails, + timeline, + insights, + metrics: state.metrics, + }; + }, [state]); + + /** + * Check if a stage is complete + */ + const isStageComplete = useCallback( + (stageId: string): boolean => { + const stage = state.stages.find((s) => s.id === stageId); + return stage?.status === 'completed'; + }, + [state.stages] + ); + + /** + * Get a stage by ID + */ + const getStage = useCallback( + (stageId: string): StageInfo | undefined => { + return state.stages.find((s) => s.id === stageId); + }, + [state.stages] + ); + + // Auto-run on mount if configured + useEffect(() => { + if (options.autoRun && options.initialInput !== undefined) { + run(options.initialInput as TInput); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Cleanup on unmount + useEffect(() => { + return () => { + cancel(); + }; + }, [cancel]); + + return { + ...state, + run, + cancel, + reset, + pause, + resume, + retry, + getReport, + isRunning: state.status === 'running', + isComplete: state.status === 'completed', + isFailed: state.status === 'failed', + isStageComplete, + getStage, + }; +} + +// ============================================================================ +// useParallelBatch Hook - Parallel Processing Within Stages +// ============================================================================ + +/** + * Result type for useParallelBatch + */ +export interface UseParallelBatchReturn { + /** Execute batch processing */ + run: (items: TItem[]) => Promise>; + /** Current batch result */ + result: ParallelBatchResult | null; + /** Loading state */ + loading: boolean; + /** Current progress (0-100) */ + progress: number; + /** Number of completed items */ + completedCount: number; + /** Total items in current batch */ + totalCount: number; + /** Cancel batch processing */ + cancel: () => void; + /** Reset state */ + reset: () => void; +} + +/** + * Hook for parallel batch processing + * + * Useful for processing multiple items in parallel within a pipeline stage. + * + * @example + * ```tsx + * function BatchProcessor() { + * const batch = useParallelBatch('processFile', { + * concurrency: 4 + * }); + * + * return ( + *
+ * + * + * {batch.loading && ( + *
+ * Processing: {batch.completedCount}/{batch.totalCount} + * ({batch.progress.toFixed(0)}%) + *
+ * )} + * + * {batch.result && ( + *
+ * Success: {batch.result.successful.length} + * Failed: {batch.result.failed.length} + *
+ * )} + *
+ * ); + * } + * ``` + */ +export function useParallelBatch( + functionName: string, + options: { + concurrency?: number; + computeOptions?: ComputeOptions; + } = {} +): UseParallelBatchReturn { + const kit = useComputeKit(); + const abortControllerRef = useRef(null); + + const [state, setState] = useState<{ + result: ParallelBatchResult | null; + loading: boolean; + progress: number; + completedCount: number; + totalCount: number; + }>({ + result: null, + loading: false, + progress: 0, + completedCount: 0, + totalCount: 0, + }); + + const run = useCallback( + async (items: TItem[]): Promise> => { + // Cancel any existing batch + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + setState({ + result: null, + loading: true, + progress: 0, + completedCount: 0, + totalCount: items.length, + }); + + const startTime = performance.now(); + const results: BatchItemResult[] = []; + const concurrency = options.concurrency ?? items.length; + + // Process in batches based on concurrency + for (let i = 0; i < items.length; i += concurrency) { + if (abortController.signal.aborted) { + break; + } + + const batch = items.slice(i, i + concurrency); + const batchPromises = batch.map(async (item, batchIndex) => { + const index = i + batchIndex; + const itemStart = performance.now(); + + try { + const data = await kit.run(functionName, item, { + ...options.computeOptions, + signal: abortController.signal, + }); + + const itemResult: BatchItemResult = { + index, + success: true, + data, + duration: performance.now() - itemStart, + }; + + return itemResult; + } catch (err) { + const itemResult: BatchItemResult = { + index, + success: false, + error: err instanceof Error ? err : new Error(String(err)), + duration: performance.now() - itemStart, + }; + + return itemResult; + } + }); + + const batchResults = await Promise.all(batchPromises); + results.push(...batchResults); + + // Update progress + const completed = results.length; + setState((prev) => ({ + ...prev, + completedCount: completed, + progress: (completed / items.length) * 100, + })); + } + + const totalDuration = performance.now() - startTime; + const successful = results + .filter((r) => r.success && r.data !== undefined) + .map((r) => r.data as TOutput); + const failed = results + .filter((r) => !r.success) + .map((r) => ({ index: r.index, error: r.error! })); + + const finalResult: ParallelBatchResult = { + results, + successful, + failed, + totalDuration, + successRate: successful.length / items.length, + }; + + setState({ + result: finalResult, + loading: false, + progress: 100, + completedCount: items.length, + totalCount: items.length, + }); + + return finalResult; + }, + [kit, functionName, options.concurrency, options.computeOptions] + ); + + const cancel = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + setState((prev) => ({ + ...prev, + loading: false, + })); + }, []); + + const reset = useCallback(() => { + cancel(); + setState({ + result: null, + loading: false, + progress: 0, + completedCount: 0, + totalCount: 0, + }); + }, [cancel]); + + // Cleanup on unmount + useEffect(() => { + return () => { + cancel(); + }; + }, [cancel]); + + return { + ...state, + run, + cancel, + reset, + }; +} + // ============================================================================ // Exports // ============================================================================ @@ -429,3 +1602,5 @@ export type { } from '@computekit/core'; export { ComputeKit } from '@computekit/core'; + +// Pipeline types are exported from interface declarations above From a20c9faa9ff79a2b1dd54f86e1d28516f8f195d3 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sun, 28 Dec 2025 11:34:20 +0000 Subject: [PATCH 27/37] feat: add raw block support for code snippets in documentation --- docs/pipeline.md | 8 ++++++++ docs/react-hooks.md | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/docs/pipeline.md b/docs/pipeline.md index 91ab6c0..ba956e8 100644 --- a/docs/pipeline.md +++ b/docs/pipeline.md @@ -135,6 +135,8 @@ const { ### Complete Pipeline UI +{% raw %} + ```tsx function DataPipeline() { const pipeline = usePipeline([ @@ -225,6 +227,8 @@ function DataPipeline() { } ``` +{% endraw %} + ### Conditional Stage Skipping ```tsx @@ -414,6 +418,8 @@ console.log(report.insights); Build a timeline UI from the metrics: +{% raw %} + ```tsx function PipelineTimeline({ metrics }: { metrics: PipelineMetrics }) { const startTime = metrics.timeline[0]?.timestamp ?? 0; @@ -440,6 +446,8 @@ function PipelineTimeline({ metrics }: { metrics: PipelineMetrics }) { } ``` +{% endraw %} + ## Pipeline Options ```tsx diff --git a/docs/react-hooks.md b/docs/react-hooks.md index 03e3ea9..1cfe32a 100644 --- a/docs/react-hooks.md +++ b/docs/react-hooks.md @@ -27,6 +27,8 @@ ComputeKit provides purpose-built React hooks for seamless integration. Wrap your application with the provider to enable all hooks. +{% raw %} + ```tsx import { ComputeKitProvider } from '@computekit/react'; @@ -39,6 +41,8 @@ function App() { } ``` +{% endraw %} + ### Provider Options | Option | Type | Default | Description | From 76b8822583f5c6a476bf832cc9025128fca0f49a Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sun, 28 Dec 2025 11:37:24 +0000 Subject: [PATCH 28/37] fix: update table of contents formatting for improved readability --- docs/debugging.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/debugging.md b/docs/debugging.md index 302157a..aa84018 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -11,12 +11,13 @@ nav_order: 8 Learn how to debug worker code, understand errors, and troubleshoot issues in ComputeKit. {: .fs-6 .fw-300 } -## Table of contents - -{: .no_toc .text-delta } - -1. TOC - {:toc} + +
+ Table of contents + {: .text-delta } +- TOC +{:toc} +
--- From 6aade4a6aad455605f156477ef30ca0ed00ec1ea Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sun, 28 Dec 2025 11:40:52 +0000 Subject: [PATCH 29/37] chore: bump package versions for core, react-query, and react to latest releases --- packages/core/package.json | 2 +- packages/react-query/package.json | 2 +- packages/react/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index f4a4ed7..fe06224 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@computekit/core", - "version": "0.1.2", + "version": "0.1.3", "description": "Core WASM + Worker toolkit for running heavy computations without blocking the UI", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/react-query/package.json b/packages/react-query/package.json index ca9d55d..2b4f159 100644 --- a/packages/react-query/package.json +++ b/packages/react-query/package.json @@ -1,6 +1,6 @@ { "name": "@computekit/react-query", - "version": "0.1.0", + "version": "0.1.1", "description": "TanStack Query integration for ComputeKit - lightweight async compute with caching", "type": "module", "main": "./dist/index.cjs", diff --git a/packages/react/package.json b/packages/react/package.json index 9753254..7ea338e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@computekit/react", - "version": "0.1.2", + "version": "0.1.3", "description": "React bindings for ComputeKit - WASM + Worker toolkit", "type": "module", "main": "./dist/index.cjs", From 163bf633e434bff60a5786d342d786513db953e9 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Sun, 28 Dec 2025 11:54:40 +0000 Subject: [PATCH 30/37] fix: add live demo link to documentation for better accessibility --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83e17fa..09aa354 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/) -[📚 Documentation](https://tapava.github.io/compute-kit) • [Getting Started](#-getting-started) • [Examples](#-examples) • [API](#-api) • [React Hooks](#-react-hooks) • [WASM](#-webassembly-support) +[📚 Documentation](https://tapava.github.io/compute-kit) • [Live Demo](https://computekit-demo.vercel.app/) • [Getting Started](#-getting-started) • [Examples](#-examples) • [API](#-api) • [React Hooks](#-react-hooks) • [WASM](#-webassembly-support) From 6cf6d100039eafe35b81907677482a469dd5f26c Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Mon, 29 Dec 2025 11:42:26 +0000 Subject: [PATCH 31/37] fix: update WASM loading logic to support multiple paths and improve error handling --- examples/react-demo/src/wasmLoader.ts | 35 +++++++++++++++++++++++---- examples/react-demo/vite.config.ts | 1 + 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/examples/react-demo/src/wasmLoader.ts b/examples/react-demo/src/wasmLoader.ts index a71f4b4..1a951c1 100644 --- a/examples/react-demo/src/wasmLoader.ts +++ b/examples/react-demo/src/wasmLoader.ts @@ -77,12 +77,37 @@ export async function loadWasm(): Promise { } loadingPromise = (async () => { - // Use absolute path from origin - works in both dev and production - const wasmUrl = new URL('/compute.wasm', window.location.origin).href; - const response = await fetch(wasmUrl); - if (!response.ok) { - throw new Error(`Failed to fetch WASM: ${response.status} ${response.statusText}`); + // Use relative path that works in both local dev and StackBlitz + // Try multiple paths to handle different environments + const possiblePaths = [ + new URL('/compute.wasm', import.meta.url).href, + new URL('../public/compute.wasm', import.meta.url).href, + '/compute.wasm', + ]; + + let response: Response | null = null; + let lastError: Error | null = null; + + for (const wasmUrl of possiblePaths) { + try { + const res = await fetch(wasmUrl); + // Check if we got a valid WASM response (not an HTML fallback) + const contentType = res.headers.get('content-type') || ''; + if (res.ok && !contentType.includes('text/html')) { + response = res; + break; + } + } catch (e) { + lastError = e as Error; + } } + + if (!response) { + throw new Error( + `Failed to fetch WASM from any path. Last error: ${lastError?.message}` + ); + } + const module = await WebAssembly.compileStreaming( // Create a new Response with the correct MIME type if needed response.headers.get('content-type')?.includes('application/wasm') diff --git a/examples/react-demo/vite.config.ts b/examples/react-demo/vite.config.ts index e049025..8901c47 100644 --- a/examples/react-demo/vite.config.ts +++ b/examples/react-demo/vite.config.ts @@ -18,4 +18,5 @@ export default defineConfig({ optimizeDeps: { exclude: ['@computekit/core', '@computekit/react'], }, + assetsInclude: ['**/*.wasm'], }); From d0a43d4d5e93e12d0ff77c1a7f7a36c1e7015657 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Mon, 29 Dec 2025 11:48:39 +0000 Subject: [PATCH 32/37] fix: enhance WASM loading logic to support multiple paths and improve error handling --- examples/react-demo/src/wasmLoader.ts | 51 +++++++++++++++------------ 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/examples/react-demo/src/wasmLoader.ts b/examples/react-demo/src/wasmLoader.ts index 1a951c1..14acb8f 100644 --- a/examples/react-demo/src/wasmLoader.ts +++ b/examples/react-demo/src/wasmLoader.ts @@ -77,45 +77,50 @@ export async function loadWasm(): Promise { } loadingPromise = (async () => { - // Use relative path that works in both local dev and StackBlitz - // Try multiple paths to handle different environments + // Try multiple paths to handle different environments (local dev, StackBlitz, production) const possiblePaths = [ - new URL('/compute.wasm', import.meta.url).href, - new URL('../public/compute.wasm', import.meta.url).href, '/compute.wasm', + './compute.wasm', + new URL('/compute.wasm', import.meta.url).href, ]; - let response: Response | null = null; - let lastError: Error | null = null; + let arrayBuffer: ArrayBuffer | null = null; + let lastError: string = ''; for (const wasmUrl of possiblePaths) { try { const res = await fetch(wasmUrl); - // Check if we got a valid WASM response (not an HTML fallback) - const contentType = res.headers.get('content-type') || ''; - if (res.ok && !contentType.includes('text/html')) { - response = res; + if (!res.ok) { + lastError = `${wasmUrl}: ${res.status} ${res.statusText}`; + continue; + } + + const buffer = await res.arrayBuffer(); + // Check for WASM magic number (0x00 0x61 0x73 0x6D = "\0asm") + const magic = new Uint8Array(buffer.slice(0, 4)); + if ( + magic[0] === 0x00 && + magic[1] === 0x61 && + magic[2] === 0x73 && + magic[3] === 0x6d + ) { + arrayBuffer = buffer; break; + } else { + lastError = `${wasmUrl}: Invalid WASM (got ${Array.from(magic) + .map((b) => b.toString(16)) + .join(' ')})`; } } catch (e) { - lastError = e as Error; + lastError = `${wasmUrl}: ${(e as Error).message}`; } } - if (!response) { - throw new Error( - `Failed to fetch WASM from any path. Last error: ${lastError?.message}` - ); + if (!arrayBuffer) { + throw new Error(`Failed to fetch WASM from any path. Last error: ${lastError}`); } - const module = await WebAssembly.compileStreaming( - // Create a new Response with the correct MIME type if needed - response.headers.get('content-type')?.includes('application/wasm') - ? response - : new Response(await response.arrayBuffer(), { - headers: { 'Content-Type': 'application/wasm' }, - }) - ); + const module = await WebAssembly.compile(arrayBuffer); cachedExports = await instantiate(module, {}); return cachedExports; })(); From bafb4323e1838019878d0e761137534173affc0e Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Mon, 29 Dec 2025 12:18:34 +0000 Subject: [PATCH 33/37] fix: update WASM loading logic to use Vite-resolved URL and improve error handling --- examples/react-demo/src/vite-env.d.ts | 6 +++ examples/react-demo/src/wasmLoader.ts | 62 ++++++++++----------------- package.json | 2 +- 3 files changed, 29 insertions(+), 41 deletions(-) create mode 100644 examples/react-demo/src/vite-env.d.ts diff --git a/examples/react-demo/src/vite-env.d.ts b/examples/react-demo/src/vite-env.d.ts new file mode 100644 index 0000000..819b3e6 --- /dev/null +++ b/examples/react-demo/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module '*.wasm?url' { + const url: string; + export default url; +} diff --git a/examples/react-demo/src/wasmLoader.ts b/examples/react-demo/src/wasmLoader.ts index 14acb8f..be1248d 100644 --- a/examples/react-demo/src/wasmLoader.ts +++ b/examples/react-demo/src/wasmLoader.ts @@ -1,5 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +// Import WASM as URL - Vite handles asset resolution across all environments +import wasmUrl from './compute.wasm?url'; + interface WasmImports { env?: Record; } @@ -77,50 +80,29 @@ export async function loadWasm(): Promise { } loadingPromise = (async () => { - // Try multiple paths to handle different environments (local dev, StackBlitz, production) - const possiblePaths = [ - '/compute.wasm', - './compute.wasm', - new URL('/compute.wasm', import.meta.url).href, - ]; - - let arrayBuffer: ArrayBuffer | null = null; - let lastError: string = ''; - - for (const wasmUrl of possiblePaths) { - try { - const res = await fetch(wasmUrl); - if (!res.ok) { - lastError = `${wasmUrl}: ${res.status} ${res.statusText}`; - continue; - } - - const buffer = await res.arrayBuffer(); - // Check for WASM magic number (0x00 0x61 0x73 0x6D = "\0asm") - const magic = new Uint8Array(buffer.slice(0, 4)); - if ( - magic[0] === 0x00 && - magic[1] === 0x61 && - magic[2] === 0x73 && - magic[3] === 0x6d - ) { - arrayBuffer = buffer; - break; - } else { - lastError = `${wasmUrl}: Invalid WASM (got ${Array.from(magic) - .map((b) => b.toString(16)) - .join(' ')})`; - } - } catch (e) { - lastError = `${wasmUrl}: ${(e as Error).message}`; - } + // Use the Vite-resolved WASM URL (works in StackBlitz and local dev) + const res = await fetch(wasmUrl); + if (!res.ok) { + throw new Error(`Failed to fetch WASM: ${res.status} ${res.statusText}`); } - if (!arrayBuffer) { - throw new Error(`Failed to fetch WASM from any path. Last error: ${lastError}`); + const buffer = await res.arrayBuffer(); + // Verify WASM magic number (0x00 0x61 0x73 0x6D = "\0asm") + const magic = new Uint8Array(buffer.slice(0, 4)); + if ( + magic[0] !== 0x00 || + magic[1] !== 0x61 || + magic[2] !== 0x73 || + magic[3] !== 0x6d + ) { + throw new Error( + `Invalid WASM file (got ${Array.from(magic) + .map((b) => b.toString(16).padStart(2, '0')) + .join(' ')})` + ); } - const module = await WebAssembly.compile(arrayBuffer); + const module = await WebAssembly.compile(buffer); cachedExports = await instantiate(module, {}); return cachedExports; })(); diff --git a/package.json b/package.json index 5f71518..2670102 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ ], "scripts": { "build": "npm run build:wasm && npm run build:core && npm run build:react && npm run build:react-query && npm run build:examples", - "build:wasm": "asc compute/index.ts --outFile examples/react-demo/public/compute.wasm --bindings esm --optimize", + "build:wasm": "asc compute/index.ts --outFile examples/react-demo/src/compute.wasm --bindings esm --optimize", "build:core": "npm run build -w @computekit/core", "build:react": "npm run build -w @computekit/react", "build:react-query": "npm run build -w @computekit/react-query", From 28f184727583b95e81ca8a62b04eb1d3b4059151 Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Mon, 29 Dec 2025 12:22:42 +0000 Subject: [PATCH 34/37] Include WASM file for StackBlitz --- .gitignore | 3 + examples/react-demo/public/compute.wasm | Bin 0 -> 9703 bytes examples/react-demo/src/compute.d.ts | 123 ++++++++++++++ examples/react-demo/src/compute.js | 204 ++++++++++++++++++++++++ examples/react-demo/src/vite-env.d.ts | 6 - examples/react-demo/src/wasmLoader.ts | 7 +- package.json | 2 +- 7 files changed, 333 insertions(+), 12 deletions(-) create mode 100644 examples/react-demo/public/compute.wasm create mode 100644 examples/react-demo/src/compute.d.ts create mode 100644 examples/react-demo/src/compute.js delete mode 100644 examples/react-demo/src/vite-env.d.ts diff --git a/.gitignore b/.gitignore index e355f9b..6b2d1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ build/ *.wasm *.wasm.map +# But include the demo WASM file for StackBlitz +!examples/react-demo/public/compute.wasm + # IDE .idea/ .vscode/ diff --git a/examples/react-demo/public/compute.wasm b/examples/react-demo/public/compute.wasm new file mode 100644 index 0000000000000000000000000000000000000000..f2f5f92f579d700abe24aeb8dacfcddc26a102bc GIT binary patch literal 9703 zcmb_iUub04c|YgeduQ&<8ENj?>)N~P*yr9E$D26Lwl5Bbf^>v;lXdIZ!D(KlS!s5y zt66C^JCg09NP2KuQ`}-oOG7AlR_owE8weq=Nio<^(z-MW#xxLG3i}WjQu1JE`qahl z?|06rKE#3$Y;P^79dz)4;b3QH z8xy`&jBat1la-yFt*tG;TW&9e7{wB~jVZOY#ZN5T=ISl65OJ1k)effAlnlD7n^EW7 zT7Mv>g5N&XP*Tb;tf*=fh11PyETobOEY|9kM2J{xaU>8ya92ILyB3HjkU~~w8#~ep zePuOMAo>_9n5W zUDf&dja9bpuFcOcuJwA|#eq1g=jZ!_!SejM&gE`UuUzb2T`3v2zSIu}-#2OCRW@%dup%WM6Mo!;^vcEzb!{#HZY zQ~%}Msk178f{n1^_ruva(bQ`O$E!7bTyL;Dl|-#DlP6VZx7JhP$jL8)oZwKfZUb~I^jNb) zX%mTs-CvM^MsLP0W2wUh6$s_oRLoq z>xYG*G)>*A5~G!@;NJ0@gslK7X$M;WiIU+Cj}RG#3TUot`--yDj-LR!z*OU`6Av?g zHlEp4XR$f3;bvqXv^PHb=%c{k@vjE`UQWGHh z*CkOWY($2_ECjZpi71YV(2h(<>?tC)hXWJj04t}(EKn}VwF@SK+-^lN7uszTJiksP zO+e%VzW|5`OsVJBA5#KWqyIjT5wO546bYvONfsC!gy)|}Q#Q;Ue1Iv>LgJ`^qvIe< zDm;=al$*xsFr))JQl+sMj&6l?sGU3`T@9#UJ1`C5M#08d zXTK`h4gK*d_nrhH|8q2jqmATtWxZm7@q}U(#+{0xSOvufR4fdc0v3u@RBW_QvHw$& z7np#`lv5cLMs--2DmbE6ViQ1oj5!VOTg$RFd4Vk-1ZyM(B!8?cacXdeaAo$x7yYJb+eG7-K4R1c!RP z0j-aA{CQv_?$AcP0s2M~oulv=1Qtd=wGNPC4$8h`zLcB*#9k!<)M(++kcW5-8~Bz< zRsWSlyqtkWF!CaFfcUJi;!6<{U^pwZ-GxJfjIhIR`3B=AbU+hiEWv#4fVLu)n1f7y z4%(2^#%RR?<`Af*3)F&57f#&?uYXI;h`*gn{wRsrZ`btu0Yadf^{iH!`=*0iaSHa^$OvsXcN@Xg3)Y4;wv~1Q zM_DN$Ws_hUNK{V)T_+{13>oZwX^%Q9^GV5R%ee!T3!{iy!byP55MZT^9=ZXd0u35@z&6({dMAnu1Njj^U?kqGzmF8^4um7SDQ}5J!N|0&Ls6Ah1-SdK z81O3%8~azuWGY=qpz(y8Dbkm;A0paI8WL>AA}3u~8RS;Nv0(D{LH40l9~i)*a#?^= z2SWePO7CRgfkzWh95`6dn3;r;&tW_SaW}UBjgtEv49a|B5pedKGc^j}|E7F?2Nm@k z&YqwG$+Z4)$bc{GubH`kDvA-R<{QdBNTgb#B{8aI?QW7P7_Lg{0J_BkTH?EN@5t@B zUy9u~>~5U5Wdl(hrvK!W+lZ|z7(Z+41I%eCYNRlvZkyyl$3;^H5gdz1{L{dG$|;C} zyumu5!8)M~Svrdn?Sw-#P>6oJ>S+KJm6~9anurQbL?%M0inI+4(h}kcqVoV6`68f% zxVhckg>5C9S-=?n}_gmo#MDC`rkp3 zJV68biYAvTDs^9>+eeI$2@nTEk*OeTIunKVa}(uR^Ac^7-gEJX}FAN7_jZvU24ZFq!0kW(kN-g%&sx=4~AP+h8nbd3Sxuy7w}|eCYD(6 z#DXr4$l-|!L5cC$h#MOAsprx1nN!k@z4^h8-YBF)2?l3h@s(|`?n*H9{XvKo)zhwe zNy4ZFqdo)k2+p`-APHHP=x3zdHexQq$95*?!mp<|ZZ6zHTRaOh5sahqN3Y$eWcc3x ziuM6OjAq4X&WNvo;Omi$wv2p26n~v?wrF3UNyWHXuE1y*6~iCQj5c1M8BVnKu4KU# z7^ZmQ58U6+XNFlxK!`;~iV=w+!vqv?B`e9^H@i2Ue+zWSy_uyGS-6KS3guuy{Kqqk zQ^Eu(vg?Rlj+FE7&+tH)QL+C0853UFE0*FcP(zk8%7rWBb?BYkRK_bHheziFNGV1= zNOEE%)(107;TXpc-Woy?aMOa<^6W9MrLfl>_vL)aYh}!9bSqv*;J!7?N(Mc+We@D8 zC@gzk?(V+O zFSYF#*?7?J>x#f!sNag@w95lMSF7wIo%2eg25E&M*~CvWnxp|bs3g&WDl5G#G*yO- zkTmkFhG@VVNxEAqBd}ayutRM&$m;OxlrgNHo0`#irXLjwEsU?^S)=IWSskrBw}10< z(X&llWlG+$TpBEs1}o&m3W#AaDL5%ODaMmNim}M@7h`fnW)RqNM7J6*fQ)Mamjo=y zD2b_>%8k09AQ1#+BKCq*8U0rvc%IfU3;+TEkT$RZ6S|?pYid%Dq$(y+vtHI%!My^OE`VMP{DtG^tm?e?c zUlTJZnFg%vSFGG@OZ!sY?$+thFPDwodONhQ*X_^^f2V8=>+OpDfwte%T>Znc@jcy+ z^_*y-I$CLKeEnvMF7PVuQg7o8y}AMr1u(X?zz2SGB)Ki6Iq`)6tN4Yx6$@-_8@RAEG9n2LQ3Z_K zBAJ@#@V+vh@|S7<97=7n6dlI)-Pqo3ANT5{2MtiVNziqro!<)*=rs&Vy9w$X8&h|- zPcgjWbBc5a;7D&h!6vU6c;Yu~*guZ#+oIsBw66j{qZ}Ge@fw=gzO%z%s0E?V?AOHi zl~Q;%Pi&!$NrNr$cXv5!HSYsx zYQm+k6lxs-VuCTU`+ce5oa~@4eGT{DeP|pOC&uVhfeykX(ZLt(2dcLU=upQb%|nxeRVaBp%r9u z=HKL+sb4Yio(YF(1vLW;2w|F>YF;xnHx&#sjUug*Lf`2$7DQ&63#z8Nmo=bDwUk; z8X#FE*{VZwT;G&P*7O~EN_42FFpm7ccnb26QI9jJcu&cd#4nKSjpEv2YCe#;j~N9m z01y|W;mk-9$9d7_HIJbSS-G$%m#5`x`U@ZL+eJl@+?SRNqMCR~D*h5%@HiQs-SU0B zA6jJ`o2)Y6XRy`eI2{VHbAQ!Ppkma)fVV5hss9u5P@*G}d&f|TRPsGj$&=%09Re_r^FD|e2;b`AhL#+M_G_h|xp-o59-J5}uoZW> zTGF1pDvW&xf-?scUnV{%|KG{OtIE;}n7pyQs5Q&n1N)50tWlI6KZQ|xaO|b${TL;r zmlBgjWfLQ+;-=BVWe;K#Ih2{naqu3{WZdNocS1DXH5I2aPvXnjdUTNY_oNEE4KtkH zMt8Rr!ugs=j=5~lbPA2g9-^3x@I?!lMs=B2K6x>8NSs^+&z% zsUpyUM?k!Nj$6p&Qge7vrte5xuqgB461igPminM5WGOiYe({(Q{CW;i93`XRf{791 zcogNCM~t}zrw)DmDsr>u{E+B>63-b2!BHTY;3{=>3t>kc3E1(YF6;=fPh#zL z>^EcGQbLIP@jfe7#ir&%3u$rFeV&sAti06cT6YGCh*mq+6DA#`)?`^m8oH&I& zeY`s2MR87Si1T=L(f>SlUKaL--}!P)pW@Dwcw6l3i8Zl^Jp-J!hI0&BYk2Wo(z^f} z_RX3L$0xa_ zj5zBC_5qQXwLZapM-QA;(Eo{c^naoCLtOX3fpw=qzXyKKmmIy)a67))7;`k1VgS@G z;rt~hwMVf$$?y9`L;ns>`Yhg01K}L27o7~$RUf?$cE1Q|E&+{IyhzQyGUZR58soly i_cC}iSiR void + message = __liftString(message >>> 0); + fileName = __liftString(fileName >>> 0); + lineNumber = lineNumber >>> 0; + columnNumber = columnNumber >>> 0; + (() => { + // @external.js + throw Error(`${message} in ${fileName}:${lineNumber}:${columnNumber}`); + })(); + }, + }), + }; + const { exports } = await WebAssembly.instantiate(module, adaptedImports); + const memory = exports.memory || imports.env.memory; + const adaptedExports = Object.setPrototypeOf({ + sum(arr) { + // compute/sum/sum(~lib/typedarray/Int32Array) => i32 + arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); + return exports.sum(arr); + }, + sumFloat(arr) { + // compute/sum/sumFloat(~lib/typedarray/Float64Array) => f64 + arr = __lowerTypedArray(Float64Array, 5, 3, arr) || __notnull(); + return exports.sumFloat(arr); + }, + average(arr) { + // compute/sum/average(~lib/typedarray/Int32Array) => f64 + arr = __lowerTypedArray(Int32Array, 4, 2, arr) || __notnull(); + return exports.average(arr); + }, + fibonacciSequence(n) { + // compute/fibonacci/fibonacciSequence(i32) => ~lib/typedarray/Int64Array + return __liftTypedArray(BigInt64Array, exports.fibonacciSequence(n) >>> 0); + }, + isFibonacci(num) { + // compute/fibonacci/isFibonacci(i64) => bool + num = num || 0n; + return exports.isFibonacci(num) != 0; + }, + mandelbrot(width, height, zoom, panX, panY, maxIter) { + // compute/mandelbrot/mandelbrot(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array + return __liftTypedArray(Uint32Array, exports.mandelbrot(width, height, zoom, panX, panY, maxIter) >>> 0); + }, + julia(width, height, cRe, cIm, zoom, maxIter) { + // compute/mandelbrot/julia(i32, i32, f64, f64, f64, i32) => ~lib/typedarray/Uint32Array + return __liftTypedArray(Uint32Array, exports.julia(width, height, cRe, cIm, zoom, maxIter) >>> 0); + }, + matrixMultiply(a, b, aRows, aCols, bCols) { + // compute/matrix/matrixMultiply(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array, i32, i32, i32) => ~lib/typedarray/Float64Array + a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); + b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); + try { + return __liftTypedArray(Float64Array, exports.matrixMultiply(a, b, aRows, aCols, bCols) >>> 0); + } finally { + __release(a); + } + }, + matrixTranspose(matrix, rows, cols) { + // compute/matrix/matrixTranspose(~lib/typedarray/Float64Array, i32, i32) => ~lib/typedarray/Float64Array + matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); + return __liftTypedArray(Float64Array, exports.matrixTranspose(matrix, rows, cols) >>> 0); + }, + matrixAdd(a, b) { + // compute/matrix/matrixAdd(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array + a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); + b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); + try { + return __liftTypedArray(Float64Array, exports.matrixAdd(a, b) >>> 0); + } finally { + __release(a); + } + }, + matrixScale(matrix, scalar) { + // compute/matrix/matrixScale(~lib/typedarray/Float64Array, f64) => ~lib/typedarray/Float64Array + matrix = __lowerTypedArray(Float64Array, 5, 3, matrix) || __notnull(); + return __liftTypedArray(Float64Array, exports.matrixScale(matrix, scalar) >>> 0); + }, + dotProduct(a, b) { + // compute/matrix/dotProduct(~lib/typedarray/Float64Array, ~lib/typedarray/Float64Array) => f64 + a = __retain(__lowerTypedArray(Float64Array, 5, 3, a) || __notnull()); + b = __lowerTypedArray(Float64Array, 5, 3, b) || __notnull(); + try { + return exports.dotProduct(a, b); + } finally { + __release(a); + } + }, + vectorMagnitude(v) { + // compute/matrix/vectorMagnitude(~lib/typedarray/Float64Array) => f64 + v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); + return exports.vectorMagnitude(v); + }, + vectorNormalize(v) { + // compute/matrix/vectorNormalize(~lib/typedarray/Float64Array) => ~lib/typedarray/Float64Array + v = __lowerTypedArray(Float64Array, 5, 3, v) || __notnull(); + return __liftTypedArray(Float64Array, exports.vectorNormalize(v) >>> 0); + }, + getBufferPtr() { + // compute/blur/getBufferPtr() => usize + return exports.getBufferPtr() >>> 0; + }, + }, exports); + function __liftString(pointer) { + if (!pointer) return null; + const + end = pointer + new Uint32Array(memory.buffer)[pointer - 4 >>> 2] >>> 1, + memoryU16 = new Uint16Array(memory.buffer); + let + start = pointer >>> 1, + string = ""; + while (end - start > 1024) string += String.fromCharCode(...memoryU16.subarray(start, start += 1024)); + return string + String.fromCharCode(...memoryU16.subarray(start, end)); + } + function __liftTypedArray(constructor, pointer) { + if (!pointer) return null; + return new constructor( + memory.buffer, + __getU32(pointer + 4), + __dataview.getUint32(pointer + 8, true) / constructor.BYTES_PER_ELEMENT + ).slice(); + } + function __lowerTypedArray(constructor, id, align, values) { + if (values == null) return 0; + const + length = values.length, + buffer = exports.__pin(exports.__new(length << align, 1)) >>> 0, + header = exports.__new(12, id) >>> 0; + __setU32(header + 0, buffer); + __dataview.setUint32(header + 4, buffer, true); + __dataview.setUint32(header + 8, length << align, true); + new constructor(memory.buffer, buffer, length).set(values); + exports.__unpin(buffer); + return header; + } + const refcounts = new Map(); + function __retain(pointer) { + if (pointer) { + const refcount = refcounts.get(pointer); + if (refcount) refcounts.set(pointer, refcount + 1); + else refcounts.set(exports.__pin(pointer), 1); + } + return pointer; + } + function __release(pointer) { + if (pointer) { + const refcount = refcounts.get(pointer); + if (refcount === 1) exports.__unpin(pointer), refcounts.delete(pointer); + else if (refcount) refcounts.set(pointer, refcount - 1); + else throw Error(`invalid refcount '${refcount}' for reference '${pointer}'`); + } + } + function __notnull() { + throw TypeError("value must not be null"); + } + let __dataview = new DataView(memory.buffer); + function __setU32(pointer, value) { + try { + __dataview.setUint32(pointer, value, true); + } catch { + __dataview = new DataView(memory.buffer); + __dataview.setUint32(pointer, value, true); + } + } + function __getU32(pointer) { + try { + return __dataview.getUint32(pointer, true); + } catch { + __dataview = new DataView(memory.buffer); + return __dataview.getUint32(pointer, true); + } + } + return adaptedExports; +} +export const { + memory, + sum, + sumFloat, + average, + fibonacci, + fibonacciSequence, + isFibonacci, + mandelbrot, + julia, + matrixMultiply, + matrixTranspose, + matrixAdd, + matrixScale, + dotProduct, + vectorMagnitude, + vectorNormalize, + getBufferPtr, + blurImage, +} = await (async url => instantiate( + await (async () => { + const isNodeOrBun = typeof process != "undefined" && process.versions != null && (process.versions.node != null || process.versions.bun != null); + if (isNodeOrBun) { return globalThis.WebAssembly.compile(await (await import("node:fs/promises")).readFile(url)); } + else { return await globalThis.WebAssembly.compileStreaming(globalThis.fetch(url)); } + })(), { + } +))(new URL("compute.wasm", import.meta.url)); diff --git a/examples/react-demo/src/vite-env.d.ts b/examples/react-demo/src/vite-env.d.ts deleted file mode 100644 index 819b3e6..0000000 --- a/examples/react-demo/src/vite-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// - -declare module '*.wasm?url' { - const url: string; - export default url; -} diff --git a/examples/react-demo/src/wasmLoader.ts b/examples/react-demo/src/wasmLoader.ts index be1248d..aaccae9 100644 --- a/examples/react-demo/src/wasmLoader.ts +++ b/examples/react-demo/src/wasmLoader.ts @@ -1,8 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -// Import WASM as URL - Vite handles asset resolution across all environments -import wasmUrl from './compute.wasm?url'; - interface WasmImports { env?: Record; } @@ -80,8 +77,8 @@ export async function loadWasm(): Promise { } loadingPromise = (async () => { - // Use the Vite-resolved WASM URL (works in StackBlitz and local dev) - const res = await fetch(wasmUrl); + // Fetch from public folder - works in local dev, production, and StackBlitz + const res = await fetch('/compute.wasm'); if (!res.ok) { throw new Error(`Failed to fetch WASM: ${res.status} ${res.statusText}`); } diff --git a/package.json b/package.json index 2670102..5f71518 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ ], "scripts": { "build": "npm run build:wasm && npm run build:core && npm run build:react && npm run build:react-query && npm run build:examples", - "build:wasm": "asc compute/index.ts --outFile examples/react-demo/src/compute.wasm --bindings esm --optimize", + "build:wasm": "asc compute/index.ts --outFile examples/react-demo/public/compute.wasm --bindings esm --optimize", "build:core": "npm run build -w @computekit/core", "build:react": "npm run build -w @computekit/react", "build:react-query": "npm run build -w @computekit/react-query", From 08cebffcad9e5d9e7292f56dde4355307b80aa9c Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Mon, 29 Dec 2025 12:29:34 +0000 Subject: [PATCH 35/37] fix: add StackBlitz links to README and index documentation for easier access --- README.md | 1 + docs/index.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 09aa354..1ccd40e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ [![Bundle Size React](https://img.shields.io/bundlephobia/minzip/@computekit/react?label=react%20size)](https://bundlephobia.com/package/@computekit/react) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)](https://www.typescriptlang.org/) +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/edit/compute-kit?file=README.md) [📚 Documentation](https://tapava.github.io/compute-kit) • [Live Demo](https://computekit-demo.vercel.app/) • [Getting Started](#-getting-started) • [Examples](#-examples) • [API](#-api) • [React Hooks](#-react-hooks) • [WASM](#-webassembly-support) diff --git a/docs/index.md b/docs/index.md index de3e94f..ad33f2b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,6 +22,7 @@ Run heavy computations with React hooks. Use WASM for native-speed performance. [Get Started](#getting-started){: .btn .btn-primary .fs-5 .mb-4 .mb-md-0 .mr-2 } [View on GitHub](https://github.com/tapava/compute-kit){: .btn .fs-5 .mb-4 .mb-md-0 } +[Try on StackBlitz](https://stackblitz.com/edit/compute-kit?file=README.md){: .btn .btn-outline .fs-5 .mb-4 .mb-md-0 } --- From c605f69c81feed898972b847abe93d2d2dfd3cbe Mon Sep 17 00:00:00 2001 From: Ghassen Ben Hadj Lassoued Date: Fri, 2 Jan 2026 11:14:46 +0000 Subject: [PATCH 36/37] Revise README description and tagline Updated the project description and tagline for clarity. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1ccd40e..680865d 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ # ComputeKit - **The React-first toolkit for WASM and Web Workers** + **The toolkit for non-blocking heavy computations** - *Run heavy computations with React hooks. Use WASM for native-speed performance. Keep your UI at 60fps.* + *Run heavy computations off the main thread and keep web UIs responsive — with optional React integration and WASM support* [![npm version](https://img.shields.io/npm/v/@computekit/core.svg)](https://www.npmjs.com/package/@computekit/core) [![Bundle Size Core](https://img.shields.io/bundlephobia/minzip/@computekit/core?label=core%20size)](https://bundlephobia.com/package/@computekit/core) From ece8b0e1481e8d02fb60a774b80446a1fa8a95ab Mon Sep 17 00:00:00 2001 From: Ghassen-Lassoued Date: Wed, 7 Jan 2026 12:32:16 +0000 Subject: [PATCH 37/37] Update documentation --- README.md | 36 +- docs/demo.html | 1446 ++++++++++++++++++++++++++---------------------- docs/index.md | 10 +- 3 files changed, 816 insertions(+), 676 deletions(-) diff --git a/README.md b/README.md index 1ccd40e..3c6d4e5 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ # ComputeKit - **The React-first toolkit for WASM and Web Workers** + **A tiny toolkit for heavy computations using Web Workers** - *Run heavy computations with React hooks. Use WASM for native-speed performance. Keep your UI at 60fps.* + *Integration with React hooks and WASM* [![npm version](https://img.shields.io/npm/v/@computekit/core.svg)](https://www.npmjs.com/package/@computekit/core) [![Bundle Size Core](https://img.shields.io/bundlephobia/minzip/@computekit/core?label=core%20size)](https://bundlephobia.com/package/@computekit/core) @@ -22,14 +22,14 @@ ## ✨ Features -- ⚛️ **React-first** — Purpose-built hooks like `useCompute` with loading, error, and progress states -- 🦀 **WASM integration** — Load and call AssemblyScript/Rust WASM modules with zero boilerplate -- 🚀 **Non-blocking** — Everything runs in Web Workers, keeping your UI at 60fps -- 🔧 **Zero config** — No manual worker files, postMessage handlers, or WASM glue code -- 📦 **Tiny** — Core library is ~3KB gzipped -- 🎯 **TypeScript** — Full type safety for your compute functions and WASM bindings -- 🔄 **Worker pool** — Automatic load balancing across CPU cores -- 📊 **Progress tracking** — Built-in progress reporting for long-running tasks +- 🔄 **Worker pool** : Automatic load balancing across CPU cores +- ⚛️ **React-first** : Provides hooks like `useCompute` with loading, error, and progress states +- 🦀 **WASM integration** : Easily load and call AssemblyScript/Rust WASM modules +- 🚀 **Non-blocking** : Everything runs in Web Workers +- 🔧 **Zero config** : No manual worker files or postMessage handlers +- 📦 **Tiny** : Core library is ~5KB gzipped +- 🎯 **TypeScript** : Full type safety for your compute functions and WASM bindings +- 📊 **Progress tracking** : Built-in progress reporting for long-running tasks --- @@ -50,7 +50,7 @@ You _can_ use Web Workers and WASM without a library. But here's the reality: --- -## 🎯 When to use ComputeKit +## 🎯 When to use this toolkit (And when not to use it) | ✅ Use ComputeKit | ❌ Don't use ComputeKit | | ---------------------------------- | ---------------------------- | @@ -104,7 +104,7 @@ kit.register('fibonacci', (n: number) => { // 3. Run it (non-blocking!) const result = await kit.run('fibonacci', 50); -console.log(result); // 12586269025 — UI never froze! +console.log(result); // 12586269025 : UI never froze! ``` ### React Usage @@ -160,7 +160,7 @@ function Calculator() { ### React + WASM (Full Example) -This is where ComputeKit shines — combining `useCompute` with WASM for native-speed performance: +This is where ComputeKit shines : combining `useCompute` with WASM for native-speed performance: ```tsx import { ComputeKitProvider, useComputeKit, useCompute } from '@computekit/react'; @@ -254,7 +254,7 @@ function ImageProcessor() { **Key benefits:** -- WASM runs in a Web Worker via `useCompute` — UI stays responsive +- WASM runs in a Web Worker via `useCompute` : UI stays responsive - Same familiar `loading`, `data`, `error` pattern as other compute functions - WASM memory management encapsulated in the registered function - Can easily add progress reporting, cancellation, etc. @@ -456,13 +456,13 @@ const wasmModule = await loadWasmModule('/compute/sum.wasm'); ## ⚡ Performance Tips -1. **Transfer large data** — Use typed arrays (Uint8Array, Float64Array) for automatic transfer optimization +1. **Transfer large data** : Use typed arrays (Uint8Array, Float64Array) for automatic transfer optimization -2. **Batch small operations** — Combine many small tasks into one larger task +2. **Batch small operations** : Combine many small tasks into one larger task -3. **Right-size your pool** — More workers ≠ better. Match to CPU cores. +3. **Right-size your pool** : More workers ≠ better. Match to CPU cores. -4. **Use WASM for math** — AssemblyScript functions can be 10-100x faster for numeric work +4. **Use WASM for math** : AssemblyScript functions can be 10-100x faster for numeric work ```typescript // ❌ Slow: Many small calls diff --git a/docs/demo.html b/docs/demo.html index 45ff747..f85b473 100644 --- a/docs/demo.html +++ b/docs/demo.html @@ -1,459 +1,594 @@ - + - - - + + + ComputeKit | WASM + Worker Toolkit + + +
+
+ + + + + ComputeKit Demo +
+ +
- .modal-header { - padding: 15px 20px; - border-bottom: 1px solid var(--border); - display: flex; - justify-content: space-between; - align-items: center; - } - .modal-body { - padding: 20px; - overflow-y: auto; - line-height: 1.6; - font-size: 0.95rem; - } - .modal-body code { background: rgba(110,118,129,0.2); padding: 2px 4px; border-radius: 4px; font-family: var(--font-code); } - .modal-body h3 { margin-top: 0; color: var(--text-main); } - - .close-btn { - background: none; border: none; color: var(--text-muted); - font-size: 1.5rem; cursor: pointer; - } +
+ - /* Responsive */ - @media (max-width: 800px) { - main { grid-template-columns: 1fr; grid-template-rows: 1fr 1fr; } - .sidebar { border-right: none; border-bottom: 1px solid var(--border); } - } - - - +
+ -
-
- - ComputeKit Demo +
+
+
Computing...
- -
-
- - -
- - -
-
-
Computing...
-
- -
-
- Time: 0ms -
-
- Mode: Idle -
-
- Resolution: 0x0 -
-
-
+
+
+ Time: 0ms +
+
+ Mode: Idle +
+
+ Resolution: 0x0 +
+
+