diff --git a/.gitignore b/.gitignore index 73632f4d6b382d777898b8cd3af2961a11c7fa06..218838580b866627e5ae406f83e54414694063df 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,8 @@ node_modules/ # Build output dist/ +# Generated files +src/worker/embedded-worker.ts + # Credential database (contains encrypted API keys) credentials.db diff --git a/example-project/public/test-inline.html b/example-project/public/test-inline.html new file mode 100644 index 0000000000000000000000000000000000000000..7a29291ecbfd8ee0b41205df4d51f55e1ac1a572 --- /dev/null +++ b/example-project/public/test-inline.html @@ -0,0 +1,60 @@ + + +
+This test verifies the inline worker works without a server.
+ + + + + + diff --git a/package.json b/package.json index 033d59242dd740bfbbe7d43d69ad32cc9d695d40..38a7c1fb5755ca0db8e857ec889e285fb19e2651 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@peerverity/ai-delegate", - "version": "0.3.3", + "version": "0.3.4", "description": "AI request manager with credential management and weight-based model selection", "main": "dist/ai-delegate.js", "types": "dist/index.d.ts", @@ -25,12 +25,18 @@ "dist" ], "scripts": { - "build": "webpack --mode production && npm run build:types", - "build:dev": "webpack --mode development && npm run build:types", + "prebuild": "node scripts/ensure-embedded-stub.js", + "build": "npm run build:worker && npm run embed-worker && npm run build:main && npm run build:types", + "build:dev": "node scripts/ensure-embedded-stub.js && npm run build:worker:dev && npm run embed-worker && npm run build:main:dev && npm run build:types", + "build:worker": "webpack --mode production --config webpack.worker.config.js", + "build:worker:dev": "webpack --mode development --config webpack.worker.config.js", + "build:main": "webpack --mode production --config webpack.main.config.js", + "build:main:dev": "webpack --mode development --config webpack.main.config.js", "build:types": "tsc --emitDeclarationOnly --declaration --declarationMap", + "embed-worker": "node scripts/embed-worker.js", "prepare": "npm run build", - "watch": "webpack --mode development --watch", - "clean": "rimraf dist", + "watch": "npm run build:worker:dev && npm run embed-worker && webpack --mode development --config webpack.main.config.js --watch", + "clean": "rimraf dist src/worker/embedded-worker.ts", "serve": "node scripts/serve.js", "example:install": "npm --prefix example-project install", "example:start": "npm --prefix example-project start", diff --git a/scripts/embed-worker.js b/scripts/embed-worker.js new file mode 100755 index 0000000000000000000000000000000000000000..20bdaecb5339ebcc63584f2085ffe18b249ba72e --- /dev/null +++ b/scripts/embed-worker.js @@ -0,0 +1,104 @@ +#!/usr/bin/env node +/** + * Embeds the compiled worker bundle into a TypeScript file + * that can be imported by the main bundle. + * + * This enables inline worker creation via Blob URL for offline/local mode. + * + * We use base64 encoding to safely embed the worker code without escaping issues. + */ + +const fs = require('fs'); +const path = require('path'); + +const WORKER_PATH = path.resolve(__dirname, '../dist/ai-delegate-worker.js'); +const OUTPUT_PATH = path.resolve(__dirname, '../src/worker/embedded-worker.ts'); + +function embedWorker() { + console.log('[embed-worker] Reading worker bundle...'); + + if (!fs.existsSync(WORKER_PATH)) { + console.error(`[embed-worker] ERROR: Worker bundle not found at ${WORKER_PATH}`); + console.error('[embed-worker] Run "npm run build:worker" first.'); + process.exit(1); + } + + const workerCode = fs.readFileSync(WORKER_PATH, 'utf8'); + const version = extractVersion(workerCode); + + // Base64 encode the worker code to avoid any escaping issues + const base64Code = Buffer.from(workerCode, 'utf8').toString('base64'); + + const output = `/** + * AUTO-GENERATED FILE - DO NOT EDIT + * + * This file contains the embedded worker bundle for inline worker creation. + * Generated by scripts/embed-worker.js + * + * To regenerate: npm run build:worker && npm run embed-worker + */ + +/** + * The worker source code as a base64-encoded string. + */ +const EMBEDDED_WORKER_BASE64 = '${base64Code}'; + +/** + * Decode the base64-encoded worker source. + */ +function decodeWorkerSource(): string { + // Use atob for browser environment + if (typeof atob === 'function') { + return atob(EMBEDDED_WORKER_BASE64); + } + // Fallback for Node.js environment (for testing) + return Buffer.from(EMBEDDED_WORKER_BASE64, 'base64').toString('utf8'); +} + +/** + * The worker source code as a string. + * Lazily decoded on first access. + */ +let _cachedSource: string | null = null; +export function getWorkerSource(): string { + if (_cachedSource === null) { + _cachedSource = decodeWorkerSource(); + } + return _cachedSource; +} + +/** + * Creates a Blob URL for the embedded worker. + * The URL should be revoked with URL.revokeObjectURL() when no longer needed. + */ +export function createWorkerBlobUrl(): string { + const source = getWorkerSource(); + const blob = new Blob([source], { type: 'application/javascript' }); + return URL.createObjectURL(blob); +} + +/** + * Version marker to help with debugging cache issues. + * This is extracted from the worker bundle if present. + */ +export const EMBEDDED_WORKER_VERSION: string = '${version}'; +`; + + console.log('[embed-worker] Writing embedded worker source...'); + fs.writeFileSync(OUTPUT_PATH, output, 'utf8'); + + const stats = fs.statSync(WORKER_PATH); + const base64Size = base64Code.length; + console.log(`[embed-worker] Original size: ${(stats.size / 1024).toFixed(1)}KB`); + console.log(`[embed-worker] Base64 size: ${(base64Size / 1024).toFixed(1)}KB`); + console.log(`[embed-worker] Worker version: ${version}`); + console.log(`[embed-worker] Output: ${OUTPUT_PATH}`); +} + +function extractVersion(code) { + // Try to extract version from worker code (looks for Version: X.Y.Z pattern) + const match = code.match(/Version:\s*([\d.]+)/); + return match ? match[1] : 'unknown'; +} + +embedWorker(); diff --git a/scripts/ensure-embedded-stub.js b/scripts/ensure-embedded-stub.js new file mode 100644 index 0000000000000000000000000000000000000000..223e0d392bfd30f82592d7e07c96252c350d30c5 --- /dev/null +++ b/scripts/ensure-embedded-stub.js @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/** + * Creates a stub embedded-worker.ts if it doesn't exist. + * This allows the TypeScript compilation to succeed before the + * real embedded worker is generated. + */ + +const fs = require('fs'); +const path = require('path'); + +const OUTPUT_PATH = path.resolve(__dirname, '../src/worker/embedded-worker.ts'); + +if (fs.existsSync(OUTPUT_PATH)) { + console.log('[ensure-embedded-stub] embedded-worker.ts already exists, skipping.'); + process.exit(0); +} + +const stub = `/** + * PLACEHOLDER - AUTO-GENERATED + * This stub is created before the build and will be replaced + * by the real embedded worker after build:worker completes. + */ + +export const EMBEDDED_WORKER_VERSION: string = '0.0.0-stub'; + +export function createWorkerBlobUrl(): string { + throw new Error('Worker not yet embedded - this is a build-time stub'); +} + +export function getWorkerSource(): string { + throw new Error('Worker not yet embedded - this is a build-time stub'); +} +`; + +console.log('[ensure-embedded-stub] Creating placeholder embedded-worker.ts...'); +fs.writeFileSync(OUTPUT_PATH, stub, 'utf8'); +console.log('[ensure-embedded-stub] Done.'); diff --git a/src/core/AiDelegate.ts b/src/core/AiDelegate.ts index 54891b6a4327011886de81b66fce49c1d0dc2bd1..77a5c647aee1d2cb3a6098b93f622465629e2397 100644 --- a/src/core/AiDelegate.ts +++ b/src/core/AiDelegate.ts @@ -123,8 +123,10 @@ export class AiDelegate { return; } - const workerUrl = this.getWorkerUrl(); - await this.worker.initialize(workerUrl, this.credentialServerUrl, this.allowLocalMode); + await this.worker.initialize( + this.credentialServerUrl, + this.allowLocalMode + ); // Set up prompt handler for worker this.setupPromptHandler(); @@ -151,18 +153,6 @@ export class AiDelegate { } } - /** - * Get the URL for the worker script based on the configured endpoint - */ - private getWorkerUrl(): string { - if (!this.endpoint) { - throw new Error('Cannot get worker URL: endpoint not configured'); - } - - // Worker is served at the endpoint path - return `${this.endpoint}/ai-delegate-worker.js`; - } - /** * Set up handler for prompt requests from worker */ diff --git a/src/worker/WorkerWrapper.ts b/src/worker/WorkerWrapper.ts index 5eb7262e4cd400761863331f9914205933f0dc01..9451f48610ab71ef3d96f3ff761220d4f3cda240 100644 --- a/src/worker/WorkerWrapper.ts +++ b/src/worker/WorkerWrapper.ts @@ -17,6 +17,7 @@ import { InitPayload } from './types'; import { MessageProtocol } from './MessageProtocol'; +import { createWorkerBlobUrl, EMBEDDED_WORKER_VERSION } from './embedded-worker'; /** * Main thread wrapper for worker communication @@ -24,6 +25,7 @@ import { MessageProtocol } from './MessageProtocol'; */ export class WorkerWrapper { private worker: Worker | null = null; + private blobUrl: string | null = null; // Track blob URL for cleanup private pendingRequests: Map