diff --git a/.github/workflows/deploy-playground-react.yml b/.github/workflows/deploy-playground-react.yml new file mode 100644 index 0000000..0f6d34c --- /dev/null +++ b/.github/workflows/deploy-playground-react.yml @@ -0,0 +1,27 @@ +name: Deploy React Playground + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + - run: bun install --frozen-lockfile + # Ensure comlink-worker-pool (dependency for the react hook) and the react hook itself are built + - run: bun run -F comlink-worker-pool build + - run: bun run -F comlink-worker-pool-react build + # Build the React playground + - run: bun run -F @comlink-worker-pool/playground-react build + - name: Deploy React Playground to GitHub Pages + id: deployment-react-playground # Changed ID as requested + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./packages/playground-react/dist # Updated publish_dir + repository: natanelia/comlink-plus # This should ideally be dynamic or checked if it's correct diff --git a/package.json b/package.json index 0411967..d9b1b6d 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "playground:lint": "bun run -F comlink-worker-pool-playground lint", "playground:build": "bun run -F comlink-worker-pool-playground build", "playground:dev": "bun run -F comlink-worker-pool-playground dev", + "dev:react-playground": "bun --filter @comlink-worker-pool/playground-react dev", "lint": "bun run -F comlink-worker-pool-react -F comlink-worker-pool lint", "build": "bun run -F comlink-worker-pool-react -F comlink-worker-pool build", "build:watch": "bun run -F comlink-worker-pool-react -F comlink-worker-pool build", diff --git a/packages/playground-react/README.md b/packages/playground-react/README.md new file mode 100644 index 0000000..48600a6 --- /dev/null +++ b/packages/playground-react/README.md @@ -0,0 +1,3 @@ +# Playground React + +This package is a playground for React components. diff --git a/packages/playground-react/index.html b/packages/playground-react/index.html new file mode 100644 index 0000000..5af464e --- /dev/null +++ b/packages/playground-react/index.html @@ -0,0 +1,12 @@ + + + + + + Comlink Worker Pool React Playground + + +
+ + + diff --git a/packages/playground-react/package.json b/packages/playground-react/package.json new file mode 100644 index 0000000..26cfe50 --- /dev/null +++ b/packages/playground-react/package.json @@ -0,0 +1,30 @@ +{ + "name": "@comlink-worker-pool/playground-react", + "private": true, + "scripts": { + "dev": "vite", + "dev:react": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "biome check ." + }, + "dependencies": { + "react": "^19.1.0", + "react-dom": "^19.1.0", + "comlink": "^4.4.2", + "comlink-worker-pool": "workspace:packages/comlink-worker-pool", + "comlink-worker-pool-react": "*" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.4", + "@types/bun": "^1.2.10", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "^10.4.21", + "oxlint": "^0.16.6", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.4", + "vite": "^6.3.2" + } +} diff --git a/packages/playground/postcss.config.mjs b/packages/playground-react/postcss.config.mjs similarity index 100% rename from packages/playground/postcss.config.mjs rename to packages/playground-react/postcss.config.mjs diff --git a/packages/playground/src/App.tsx b/packages/playground-react/src/App.tsx similarity index 93% rename from packages/playground/src/App.tsx rename to packages/playground-react/src/App.tsx index 164f5e1..1d0444b 100644 --- a/packages/playground/src/App.tsx +++ b/packages/playground-react/src/App.tsx @@ -1,5 +1,6 @@ import * as Comlink from "comlink"; -import { WorkerPool, type WorkerPoolStats } from "comlink-worker-pool"; +import { useWorkerPool } from "comlink-worker-pool-react"; +import type { WorkerPoolStats } from "comlink-worker-pool"; import { useEffect, useRef, useState } from "react"; import "./index.css"; @@ -15,7 +16,6 @@ type WorkerApi = { const proxyFactory = (worker: Worker) => Comlink.wrap(worker); function App() { - const [pool, setPool] = useState | null>(null); const [inputNumber, setInputNumber] = useState(40); const [taskCount, setTaskCount] = useState(10); const [inputText, setInputText] = useState(""); @@ -23,28 +23,11 @@ function App() { const [logs, setLogs] = useState<{ key: string; text: string }[]>([]); const logsListRef = useRef(null); - useEffect(() => { - const size = navigator.hardwareConcurrency || 4; - const p = new WorkerPool({ - size, - workerFactory, - proxyFactory, - onUpdateStats: setStats, - workerIdleTimeoutMs: 1000, - }); - setPool(p); - setStats(p.getStats()); - return () => { - p.terminateAll(); - }; - }, []); - - const [stats, setStats] = useState({ - size: 0, - available: 0, - queue: 0, - workers: 0, - idleWorkers: 0, + const { pool, stats, setStats } = useWorkerPool({ + size: navigator.hardwareConcurrency || 4, + workerFactory, + proxyFactory, + workerIdleTimeoutMs: 1000, }); // Utility to format log messages @@ -138,31 +121,31 @@ function App() {
Workers - {stats.size} + {stats?.size ?? 0}
Available - {stats.available} + {stats?.available ?? 0}
Queue - {stats.queue} + {stats?.queue ?? 0}
Active Workers - {stats.workers} + {stats?.workers ?? 0}
Idle Workers - {stats.idleWorkers} + {stats?.idleWorkers ?? 0}
diff --git a/packages/playground/src/index.css b/packages/playground-react/src/index.css similarity index 100% rename from packages/playground/src/index.css rename to packages/playground-react/src/index.css diff --git a/packages/playground/src/main.tsx b/packages/playground-react/src/main.tsx similarity index 100% rename from packages/playground/src/main.tsx rename to packages/playground-react/src/main.tsx diff --git a/packages/playground-react/src/worker.ts b/packages/playground-react/src/worker.ts new file mode 100644 index 0000000..51f701f --- /dev/null +++ b/packages/playground-react/src/worker.ts @@ -0,0 +1,38 @@ +import * as Comlink from "comlink"; + +// CPU-intensive Fibonacci +function fib(n: number): number { + if (n <= 1) return n; + return fib(n - 1) + fib(n - 2); +} + +// simulate variable workload +function sleep(ms: number) { + return new Promise((res) => setTimeout(res, ms)); +} + +export async function fibAsync(n: number): Promise { + if (typeof n !== "number") throw new Error("Input must be a number"); + return fib(n); +} + +// Example API methods for proxified callbacks +export async function countWords(text: string): Promise { + // Simulate processing delay + const delay = Math.floor(Math.random() * 800) + 200; + await sleep(delay); + if (typeof text !== "string") throw new Error("Input must be a string"); + return text.trim().split(/\s+/).filter(Boolean).length; +} + +export async function reverseString(text: string): Promise { + const delay = Math.floor(Math.random() * 800) + 200; + await sleep(delay); + if (typeof text !== "string") throw new Error("Input must be a string"); + return text.split("").reverse().join(""); +} + +const api = { fibAsync, countWords, reverseString }; +export type WorkerApi = typeof api; + +Comlink.expose(api); diff --git a/packages/playground-react/tsconfig.json b/packages/playground-react/tsconfig.json new file mode 100644 index 0000000..a6ba4cc --- /dev/null +++ b/packages/playground-react/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Node", + "types": ["bun-types"], + "outDir": "dist", + "declaration": true, + "declarationDir": "dist/types", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "allowJs": true, + "checkJs": false, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/playground-react/vite.config.ts b/packages/playground-react/vite.config.ts new file mode 100644 index 0000000..bb31027 --- /dev/null +++ b/packages/playground-react/vite.config.ts @@ -0,0 +1,15 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + base: '/comlink-plus/', + build: { + outDir: "./dist", + emptyOutDir: true, + }, + server: { + open: true, + port: 5174, + }, + plugins: [react()], +}); diff --git a/packages/playground/index.html b/packages/playground/index.html index 64c38fd..4195002 100644 --- a/packages/playground/index.html +++ b/packages/playground/index.html @@ -1,12 +1,80 @@ - - - - Comlink Worker Pool Playground - - -
- - + + + + + Comlink Worker Pool - Vanilla JS Playground + + + + +
+

Comlink Worker Pool - Vanilla JS Playground

+ +
+

Controls

+
+ + +
+
+ +
+

Worker Pool Stats

+
+
Pool Size
0
+
Available
0
+
Queue
0
+
Active Workers
0
+
Idle Workers
0
+
+
+ +
+

Fibonacci Calculator

+ + + +
+ +
+

Word Counter

+ + + +
+ +
+

String Reverser

+ + + +
+ +
+
+

Logs

+ +
+
+
+
+ diff --git a/packages/playground/package.json b/packages/playground/package.json index 78339b3..fb727b1 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -8,17 +8,12 @@ "lint": "biome check ." }, "dependencies": { - "react": "^19.1.0", - "react-dom": "^19.1.0", "comlink": "^4.4.2", "comlink-worker-pool": "workspace:packages/comlink-worker-pool" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.4", "@types/bun": "^1.2.10", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", - "@vitejs/plugin-react": "^4.4.1", "autoprefixer": "^10.4.21", "oxlint": "^0.16.6", "postcss": "^8.5.3", diff --git a/packages/playground/src/main.js b/packages/playground/src/main.js new file mode 100644 index 0000000..14bad55 --- /dev/null +++ b/packages/playground/src/main.js @@ -0,0 +1,114 @@ +import * as Comlink from "comlink"; +import { WorkerPool } from "comlink-worker-pool"; + +// DOM Elements +const taskCountInput = document.getElementById("taskCount"); +const fibInput = document.getElementById("fibInput"); +const wordCountInput = document.getElementById("wordCountInput"); +const reverseStringInput = document.getElementById("reverseStringInput"); + +const runFibButton = document.getElementById("runFib"); +const runCountWordsButton = document.getElementById("runCountWords"); +const runReverseStringButton = document.getElementById("runReverseString"); + +const logsContainer = document.getElementById("logsContainer"); +const clearLogsButton = document.getElementById("clearLogs"); + +const statSize = document.getElementById("statSize"); +const statAvailable = document.getElementById("statAvailable"); +const statQueue = document.getElementById("statQueue"); +const statWorkers = document.getElementById("statWorkers"); +const statIdleWorkers = document.getElementById("statIdleWorkers"); + +let pool; + +const workerFactory = () => + new Worker(new URL("./worker.ts", import.meta.url), { type: "module" }); + +const proxyFactory = (worker) => Comlink.wrap(worker); + +function updateStatsDisplay(stats) { + if (!stats) return; + statSize.textContent = stats.size; + statAvailable.textContent = stats.available; + statQueue.textContent = stats.queue; + statWorkers.textContent = stats.workers; + statIdleWorkers.textContent = stats.idleWorkers; +} + +function initializePool() { + const size = navigator.hardwareConcurrency || 4; + pool = new WorkerPool({ + size, + workerFactory, + proxyFactory, + onUpdateStats: updateStatsDisplay, + workerIdleTimeoutMs: 1000, + }); + updateStatsDisplay(pool.getStats()); +} + +function addLog(message) { + const logEntry = document.createElement("div"); + logEntry.className = "log-entry"; + logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; + logsContainer.appendChild(logEntry); + logsContainer.scrollTop = logsContainer.scrollHeight; // Auto-scroll +} + +function clearLogs() { + logsContainer.innerHTML = ""; +} + +clearLogsButton.addEventListener("click", clearLogs); + +async function runAndLogTasks(taskFn, label, input) { + if (!pool) return; + const api = pool.getApi(); + const numTasks = parseInt(taskCountInput.value, 10); + addLog(`Starting ${numTasks} "${label}" tasks for input: ${input}...`); + + const tasks = []; + for (let i = 0; i < numTasks; i++) { + tasks.push( + (async () => { + try { + const result = await taskFn(api, input); + addLog(`"${label}" result: ${result}`); + } catch (error) { + addLog(`"${label}" error: ${error.message}`); + console.error(error); + } + })(), + ); + } + await Promise.all(tasks); + addLog(`Finished ${numTasks} "${label}" tasks.`); +} + +runFibButton.addEventListener("click", () => { + const n = parseInt(fibInput.value, 10); + runAndLogTasks(async (api, val) => api.fibAsync(val), "Fibonacci", n); +}); + +runCountWordsButton.addEventListener("click", () => { + const text = wordCountInput.value; + runAndLogTasks(async (api, val) => api.countWords(val), "Count Words", text); +}); + +runReverseStringButton.addEventListener("click", () => { + const text = reverseStringInput.value; + runAndLogTasks(async (api, val) => api.reverseString(val), "Reverse String", text); +}); + +// Initialize +initializePool(); + +// Handle page unload to terminate workers +window.addEventListener("beforeunload", () => { + if (pool) { + pool.terminateAll(); + } +}); + +addLog("Playground initialized. Worker pool created."); diff --git a/packages/playground/vite.config.ts b/packages/playground/vite.config.ts index 8b3418f..a2b7c61 100644 --- a/packages/playground/vite.config.ts +++ b/packages/playground/vite.config.ts @@ -1,4 +1,3 @@ -import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; export default defineConfig({ @@ -9,6 +8,8 @@ export default defineConfig({ }, server: { open: true, + port: 5173, // Default port for the non-React playground }, - plugins: [react()], + // No React-specific plugins needed for the vanilla JS playground + plugins: [], });