@b9g/platform 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/platform",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Base Platform interface and types for Shovel deployment adapters",
5
5
  "keywords": [
6
6
  "shovel",
@@ -104,6 +104,22 @@
104
104
  "./directory-storage.js": {
105
105
  "types": "./src/directory-storage.d.ts",
106
106
  "import": "./src/directory-storage.js"
107
+ },
108
+ "./worker-pool": {
109
+ "types": "./src/worker-pool.d.ts",
110
+ "import": "./src/worker-pool.js"
111
+ },
112
+ "./worker-pool.js": {
113
+ "types": "./src/worker-pool.d.ts",
114
+ "import": "./src/worker-pool.js"
115
+ },
116
+ "./worker-web": {
117
+ "types": "./src/worker-web.d.ts",
118
+ "import": "./src/worker-web.js"
119
+ },
120
+ "./worker-web.js": {
121
+ "types": "./src/worker-web.d.ts",
122
+ "import": "./src/worker-web.js"
107
123
  }
108
124
  }
109
125
  }
@@ -30,6 +30,10 @@ export declare class PlatformBucketStorage implements BucketStorageInterface {
30
30
  * List all available bucket names
31
31
  */
32
32
  keys(): Promise<string[]>;
33
+ /**
34
+ * Alias for open() - for compatibility with File System Access API naming
35
+ */
36
+ getDirectoryHandle(name: string): Promise<FileSystemDirectoryHandle>;
33
37
  }
34
38
  /**
35
39
  * Create a BucketStorage instance from a root path
@@ -37,6 +37,12 @@ var PlatformBucketStorage = class {
37
37
  async keys() {
38
38
  return await this.buckets.keys();
39
39
  }
40
+ /**
41
+ * Alias for open() - for compatibility with File System Access API naming
42
+ */
43
+ async getDirectoryHandle(name) {
44
+ return await this.open(name);
45
+ }
40
46
  };
41
47
  function createBucketStorage(rootPath = "./dist") {
42
48
  return new PlatformBucketStorage(rootPath);
package/src/index.d.ts CHANGED
@@ -12,3 +12,4 @@ export { platformRegistry, detectPlatform, getPlatform, getPlatformAsync, } from
12
12
  export { detectRuntime, detectDevelopmentPlatform, detectPlatforms, getBestPlatformDetection, resolvePlatform, createPlatform, displayPlatformInfo, } from "./detection.js";
13
13
  export { parseTTL, mergeCacheConfig, validateCacheConfig, createCorsHeaders, mergeHeaders, isPreflightRequest, createPreflightResponse, } from "./utils.js";
14
14
  export { getDirectoryHandle, getBucket, getFileSystemRoot } from "./filesystem.js";
15
+ export { WorkerPool, type WorkerFactory, type PlatformWorker, type WorkerPoolOptions } from "./worker-pool.js";
package/src/index.js CHANGED
@@ -34,10 +34,12 @@ import {
34
34
  createPreflightResponse
35
35
  } from "./utils.js";
36
36
  import { getDirectoryHandle, getBucket, getFileSystemRoot } from "./filesystem.js";
37
+ import { WorkerPool } from "./worker-pool.js";
37
38
  export {
38
39
  BasePlatform,
39
40
  PlatformBucketStorage,
40
41
  ServiceWorkerRuntime,
42
+ WorkerPool,
41
43
  createBucketStorage,
42
44
  createCorsHeaders,
43
45
  createPlatform,
@@ -3,39 +3,56 @@
3
3
  *
4
4
  * Provides ServiceWorker APIs (self, addEventListener, etc.) in any JavaScript runtime
5
5
  */
6
+ /**
7
+ * ExtendableEvent base class following ServiceWorker spec
8
+ */
9
+ export declare class ExtendableEvent extends Event {
10
+ private promises;
11
+ private pendingPromises;
12
+ constructor(type: string, pendingPromises: Set<Promise<any>>);
13
+ waitUntil(promise: Promise<any>): void;
14
+ getPromises(): Promise<any>[];
15
+ }
6
16
  /**
7
17
  * ServiceWorker-style fetch event
8
18
  */
9
- export interface ShovelFetchEvent extends Event {
10
- readonly type: "fetch";
19
+ export declare class FetchEvent extends ExtendableEvent {
11
20
  readonly request: Request;
21
+ private responsePromise;
22
+ private responded;
23
+ constructor(request: Request, pendingPromises: Set<Promise<any>>);
12
24
  respondWith(response: Response | Promise<Response>): void;
13
- waitUntil(promise: Promise<any>): void;
25
+ getResponse(): Promise<Response> | null;
26
+ hasResponded(): boolean;
14
27
  }
15
28
  /**
16
29
  * ServiceWorker-style install event
17
30
  */
18
- export interface ShovelInstallEvent extends Event {
19
- readonly type: "install";
20
- waitUntil(promise: Promise<any>): void;
31
+ export declare class InstallEvent extends ExtendableEvent {
32
+ constructor(pendingPromises: Set<Promise<any>>);
21
33
  }
22
34
  /**
23
35
  * ServiceWorker-style activate event
24
36
  */
25
- export interface ShovelActivateEvent extends Event {
26
- readonly type: "activate";
27
- waitUntil(promise: Promise<any>): void;
37
+ export declare class ActivateEvent extends ExtendableEvent {
38
+ constructor(pendingPromises: Set<Promise<any>>);
28
39
  }
29
40
  /**
30
- * Static generation event for collecting routes
41
+ * Legacy interfaces for backward compatibility
31
42
  */
32
- export interface ShovelStaticEvent extends Event {
33
- readonly type: "static";
34
- readonly detail: {
35
- outDir: string;
36
- baseUrl?: string;
37
- };
38
- waitUntil(promise: Promise<string[]>): void;
43
+ export interface ShovelFetchEvent extends Event {
44
+ readonly type: "fetch";
45
+ readonly request: Request;
46
+ respondWith(response: Response | Promise<Response>): void;
47
+ waitUntil(promise: Promise<any>): void;
48
+ }
49
+ export interface ShovelInstallEvent extends Event {
50
+ readonly type: "install";
51
+ waitUntil(promise: Promise<any>): void;
52
+ }
53
+ export interface ShovelActivateEvent extends Event {
54
+ readonly type: "activate";
55
+ waitUntil(promise: Promise<any>): void;
39
56
  }
40
57
  /**
41
58
  * ServiceWorker runtime that can be embedded in any platform
@@ -44,7 +61,10 @@ export declare class ServiceWorkerRuntime extends EventTarget {
44
61
  private pendingPromises;
45
62
  private isInstalled;
46
63
  private isActivated;
64
+ private eventListeners;
47
65
  constructor();
66
+ addEventListener(type: string, listener: Function): void;
67
+ removeEventListener(type: string, listener: Function): void;
48
68
  /**
49
69
  * Create a fetch event and dispatch it
50
70
  */
@@ -57,10 +77,6 @@ export declare class ServiceWorkerRuntime extends EventTarget {
57
77
  * Activate the ServiceWorker
58
78
  */
59
79
  activate(): Promise<void>;
60
- /**
61
- * Collect static routes for pre-rendering
62
- */
63
- collectStaticRoutes(outDir: string, baseUrl?: string): Promise<string[]>;
64
80
  /**
65
81
  * Check if ready to handle requests
66
82
  */
@@ -84,6 +100,10 @@ export interface BucketStorage {
84
100
  * Well-known names: 'assets', 'static', 'uploads', 'temp'
85
101
  */
86
102
  open(name: string): Promise<FileSystemDirectoryHandle>;
103
+ /**
104
+ * Alias for open() - for compatibility with File System Access API naming
105
+ */
106
+ getDirectoryHandle(name: string): Promise<FileSystemDirectoryHandle>;
87
107
  /**
88
108
  * Check if a named bucket exists
89
109
  */
@@ -103,16 +123,9 @@ export interface BucketStorage {
103
123
  export declare function createServiceWorkerGlobals(runtime: ServiceWorkerRuntime, options?: {
104
124
  caches?: any;
105
125
  buckets?: BucketStorage;
126
+ isDevelopment?: boolean;
127
+ hotReload?: () => Promise<void>;
106
128
  }): {
107
- self: ServiceWorkerRuntime;
108
- addEventListener: any;
109
- removeEventListener: any;
110
- dispatchEvent: any;
111
- skipWaiting: () => Promise<void>;
112
- clients: {
113
- claim: () => Promise<void>;
114
- matchAll: () => Promise<any[]>;
115
- };
116
129
  console: Console;
117
130
  setTimeout: typeof setTimeout;
118
131
  clearTimeout: typeof clearTimeout;
@@ -146,4 +159,17 @@ export declare function createServiceWorkerGlobals(runtime: ServiceWorkerRuntime
146
159
  new (init?: string[][] | Record<string, string> | string | URLSearchParams): URLSearchParams;
147
160
  prototype: URLSearchParams;
148
161
  };
162
+ caches: any;
163
+ buckets: BucketStorage;
164
+ self: ServiceWorkerRuntime;
165
+ addEventListener: any;
166
+ removeEventListener: any;
167
+ dispatchEvent: any;
168
+ skipWaiting: () => Promise<void>;
169
+ clients: {
170
+ claim(): Promise<void>;
171
+ get(id: string): Promise<any>;
172
+ matchAll(options?: any): Promise<any[]>;
173
+ openWindow(url: string | URL): Promise<any>;
174
+ };
149
175
  };
@@ -1,12 +1,81 @@
1
1
  /// <reference types="./service-worker.d.ts" />
2
2
  // src/service-worker.ts
3
+ var ExtendableEvent = class extends Event {
4
+ promises = [];
5
+ pendingPromises;
6
+ constructor(type, pendingPromises) {
7
+ super(type);
8
+ this.pendingPromises = pendingPromises;
9
+ }
10
+ waitUntil(promise) {
11
+ this.promises.push(promise);
12
+ this.pendingPromises.add(promise);
13
+ promise.finally(() => this.pendingPromises.delete(promise));
14
+ }
15
+ getPromises() {
16
+ return [...this.promises];
17
+ }
18
+ };
19
+ var FetchEvent = class extends ExtendableEvent {
20
+ request;
21
+ responsePromise = null;
22
+ responded = false;
23
+ constructor(request, pendingPromises) {
24
+ super("fetch", pendingPromises);
25
+ this.request = request;
26
+ }
27
+ respondWith(response) {
28
+ if (this.responded) {
29
+ throw new Error("respondWith() already called");
30
+ }
31
+ this.responded = true;
32
+ this.responsePromise = Promise.resolve(response);
33
+ }
34
+ getResponse() {
35
+ return this.responsePromise;
36
+ }
37
+ hasResponded() {
38
+ return this.responded;
39
+ }
40
+ };
41
+ var InstallEvent = class extends ExtendableEvent {
42
+ constructor(pendingPromises) {
43
+ super("install", pendingPromises);
44
+ }
45
+ };
46
+ var ActivateEvent = class extends ExtendableEvent {
47
+ constructor(pendingPromises) {
48
+ super("activate", pendingPromises);
49
+ }
50
+ };
3
51
  var ServiceWorkerRuntime = class extends EventTarget {
4
52
  pendingPromises = /* @__PURE__ */ new Set();
5
53
  isInstalled = false;
6
54
  isActivated = false;
55
+ eventListeners = /* @__PURE__ */ new Map();
7
56
  constructor() {
8
57
  super();
9
58
  }
59
+ addEventListener(type, listener) {
60
+ super.addEventListener(type, listener);
61
+ if (!this.eventListeners.has(type)) {
62
+ this.eventListeners.set(type, []);
63
+ }
64
+ this.eventListeners.get(type).push(listener);
65
+ }
66
+ removeEventListener(type, listener) {
67
+ super.removeEventListener(type, listener);
68
+ if (this.eventListeners.has(type)) {
69
+ const listeners = this.eventListeners.get(type);
70
+ const index = listeners.indexOf(listener);
71
+ if (index > -1) {
72
+ listeners.splice(index, 1);
73
+ if (listeners.length === 0) {
74
+ this.eventListeners.delete(type);
75
+ }
76
+ }
77
+ }
78
+ }
10
79
  /**
11
80
  * Create a fetch event and dispatch it
12
81
  */
@@ -15,28 +84,22 @@ var ServiceWorkerRuntime = class extends EventTarget {
15
84
  throw new Error("ServiceWorker not activated");
16
85
  }
17
86
  return new Promise((resolve, reject) => {
18
- let responded = false;
19
- const promises = [];
20
- const event = Object.assign(new Event("fetch"), {
21
- request,
22
- respondWith: (response) => {
23
- if (responded) {
24
- throw new Error("respondWith() already called");
25
- }
26
- responded = true;
27
- Promise.resolve(response).then(resolve).catch(reject);
28
- },
29
- waitUntil: (promise) => {
30
- promises.push(promise);
31
- this.pendingPromises.add(promise);
32
- promise.finally(() => this.pendingPromises.delete(promise));
87
+ const event = new FetchEvent(request, this.pendingPromises);
88
+ process.nextTick(() => {
89
+ this.dispatchEvent(event);
90
+ const promises = event.getPromises();
91
+ if (promises.length > 0) {
92
+ Promise.allSettled(promises).catch(console.error);
33
93
  }
34
94
  });
35
- this.dispatchEvent(event);
36
- if (!responded) {
37
- reject(new Error("No response provided for fetch event"));
38
- }
39
- Promise.allSettled(promises).catch(console.error);
95
+ setTimeout(() => {
96
+ if (event.hasResponded()) {
97
+ const responsePromise = event.getResponse();
98
+ responsePromise.then(resolve).catch(reject);
99
+ } else {
100
+ reject(new Error("No response provided for fetch event"));
101
+ }
102
+ }, 0);
40
103
  });
41
104
  }
42
105
  /**
@@ -46,22 +109,20 @@ var ServiceWorkerRuntime = class extends EventTarget {
46
109
  if (this.isInstalled)
47
110
  return;
48
111
  return new Promise((resolve, reject) => {
49
- const promises = [];
50
- let installCancelled = false;
51
- const event = Object.assign(new Event("install"), {
52
- waitUntil: (promise) => {
53
- promises.push(promise);
54
- this.pendingPromises.add(promise);
55
- promise.finally(() => this.pendingPromises.delete(promise));
56
- }
57
- });
58
- this.dispatchEvent(event);
59
- Promise.allSettled(promises).then(() => {
60
- if (!installCancelled) {
112
+ const event = new InstallEvent(this.pendingPromises);
113
+ process.nextTick(() => {
114
+ this.dispatchEvent(event);
115
+ const promises = event.getPromises();
116
+ if (promises.length === 0) {
61
117
  this.isInstalled = true;
62
118
  resolve();
119
+ } else {
120
+ Promise.all(promises).then(() => {
121
+ this.isInstalled = true;
122
+ resolve();
123
+ }).catch(reject);
63
124
  }
64
- }).catch(reject);
125
+ });
65
126
  });
66
127
  }
67
128
  /**
@@ -74,46 +135,20 @@ var ServiceWorkerRuntime = class extends EventTarget {
74
135
  if (this.isActivated)
75
136
  return;
76
137
  return new Promise((resolve, reject) => {
77
- const promises = [];
78
- const event = Object.assign(new Event("activate"), {
79
- waitUntil: (promise) => {
80
- promises.push(promise);
81
- this.pendingPromises.add(promise);
82
- promise.finally(() => this.pendingPromises.delete(promise));
83
- }
84
- });
85
- this.dispatchEvent(event);
86
- Promise.allSettled(promises).then(() => {
87
- this.isActivated = true;
88
- resolve();
89
- }).catch(reject);
90
- });
91
- }
92
- /**
93
- * Collect static routes for pre-rendering
94
- */
95
- async collectStaticRoutes(outDir, baseUrl) {
96
- return new Promise((resolve, reject) => {
97
- let routes = [];
98
- const promises = [];
99
- const event = Object.assign(new Event("static"), {
100
- detail: { outDir, baseUrl },
101
- waitUntil: (promise) => {
102
- promises.push(
103
- promise.then((routeList) => {
104
- routes = routes.concat(routeList);
105
- })
106
- );
107
- this.pendingPromises.add(promise);
108
- promise.finally(() => this.pendingPromises.delete(promise));
138
+ const event = new ActivateEvent(this.pendingPromises);
139
+ process.nextTick(() => {
140
+ this.dispatchEvent(event);
141
+ const promises = event.getPromises();
142
+ if (promises.length === 0) {
143
+ this.isActivated = true;
144
+ resolve();
145
+ } else {
146
+ Promise.all(promises).then(() => {
147
+ this.isActivated = true;
148
+ resolve();
149
+ }).catch(reject);
109
150
  }
110
151
  });
111
- this.dispatchEvent(event);
112
- if (promises.length === 0) {
113
- resolve([]);
114
- } else {
115
- Promise.allSettled(promises).then(() => resolve([...new Set(routes)])).catch(reject);
116
- }
117
152
  });
118
153
  }
119
154
  /**
@@ -137,12 +172,12 @@ var ServiceWorkerRuntime = class extends EventTarget {
137
172
  this.isInstalled = false;
138
173
  this.isActivated = false;
139
174
  this.pendingPromises.clear();
140
- const listeners = this._listeners;
141
- if (listeners) {
142
- for (const type in listeners) {
143
- delete listeners[type];
175
+ for (const [type, listeners] of this.eventListeners) {
176
+ for (const listener of listeners) {
177
+ super.removeEventListener(type, listener);
144
178
  }
145
179
  }
180
+ this.eventListeners.clear();
146
181
  }
147
182
  };
148
183
  function createServiceWorkerGlobals(runtime, options = {}) {
@@ -152,17 +187,38 @@ function createServiceWorkerGlobals(runtime, options = {}) {
152
187
  if (options.buckets) {
153
188
  runtime.buckets = options.buckets;
154
189
  }
155
- return {
190
+ const skipWaiting = async () => {
191
+ if (options.isDevelopment && options.hotReload) {
192
+ console.info("[ServiceWorker] skipWaiting() - triggering hot reload");
193
+ await options.hotReload();
194
+ } else if (!options.isDevelopment) {
195
+ console.info("[ServiceWorker] skipWaiting() - production graceful restart not implemented");
196
+ }
197
+ };
198
+ const clients = {
199
+ async claim() {
200
+ },
201
+ async get(id) {
202
+ return void 0;
203
+ },
204
+ async matchAll(options2) {
205
+ return [];
206
+ },
207
+ async openWindow(url) {
208
+ return null;
209
+ }
210
+ };
211
+ const globals = {
156
212
  self: runtime,
157
213
  addEventListener: runtime.addEventListener.bind(runtime),
158
214
  removeEventListener: runtime.removeEventListener.bind(runtime),
159
215
  dispatchEvent: runtime.dispatchEvent.bind(runtime),
160
- // ServiceWorker-specific globals that might be useful
161
- skipWaiting: () => Promise.resolve(),
162
- clients: {
163
- claim: () => Promise.resolve(),
164
- matchAll: () => Promise.resolve([])
165
- },
216
+ // ServiceWorker-specific globals with proper implementations
217
+ skipWaiting,
218
+ clients,
219
+ // Platform resources
220
+ ...options.buckets && { buckets: options.buckets },
221
+ ...options.caches && { caches: options.caches },
166
222
  // Standard globals
167
223
  console,
168
224
  setTimeout,
@@ -176,8 +232,14 @@ function createServiceWorkerGlobals(runtime, options = {}) {
176
232
  URL,
177
233
  URLSearchParams
178
234
  };
235
+ Object.assign(globalThis, globals);
236
+ return globals;
179
237
  }
180
238
  export {
239
+ ActivateEvent,
240
+ ExtendableEvent,
241
+ FetchEvent,
242
+ InstallEvent,
181
243
  ServiceWorkerRuntime,
182
244
  createServiceWorkerGlobals
183
245
  };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Common WorkerPool abstraction based on web standards
3
+ * Provides platform-agnostic worker management for ServiceWorker execution
4
+ */
5
+ import { CustomCacheStorage } from "@b9g/cache";
6
+ export interface WorkerPoolOptions {
7
+ /** Number of workers in the pool (default: 1) */
8
+ workerCount?: number;
9
+ /** Request timeout in milliseconds (default: 30000) */
10
+ requestTimeout?: number;
11
+ /** Enable hot reloading (default: true in development) */
12
+ hotReload?: boolean;
13
+ /** Working directory for file resolution */
14
+ cwd?: string;
15
+ }
16
+ export interface WorkerMessage {
17
+ type: string;
18
+ [key: string]: any;
19
+ }
20
+ export interface WorkerRequest extends WorkerMessage {
21
+ type: "request";
22
+ request: {
23
+ url: string;
24
+ method: string;
25
+ headers: Record<string, string>;
26
+ body?: any;
27
+ };
28
+ requestId: number;
29
+ }
30
+ export interface WorkerResponse extends WorkerMessage {
31
+ type: "response";
32
+ response: {
33
+ status: number;
34
+ statusText: string;
35
+ headers: Record<string, string>;
36
+ body: string;
37
+ };
38
+ requestId: number;
39
+ }
40
+ export interface WorkerLoadMessage extends WorkerMessage {
41
+ type: "load";
42
+ version: number | string;
43
+ entrypoint?: string;
44
+ }
45
+ export interface WorkerReadyMessage extends WorkerMessage {
46
+ type: "ready" | "worker-ready";
47
+ version?: number | string;
48
+ }
49
+ export interface WorkerErrorMessage extends WorkerMessage {
50
+ type: "error";
51
+ error: string;
52
+ stack?: string;
53
+ requestId?: number;
54
+ }
55
+ /**
56
+ * Common WorkerPool implementation based on web standards
57
+ * Provides round-robin request handling, hot reloading, and cache coordination
58
+ */
59
+ export declare class WorkerPool {
60
+ private workers;
61
+ private currentWorker;
62
+ private requestId;
63
+ private pendingRequests;
64
+ private options;
65
+ private cacheStorage;
66
+ private appEntrypoint?;
67
+ constructor(cacheStorage: CustomCacheStorage, options?: WorkerPoolOptions, appEntrypoint?: string);
68
+ /**
69
+ * Initialize workers (must be called after construction)
70
+ */
71
+ init(): Promise<void>;
72
+ private initWorkers;
73
+ private createWorker;
74
+ private handleWorkerMessage;
75
+ private handleResponse;
76
+ private handleError;
77
+ private handleReady;
78
+ /**
79
+ * Platform-specific cache message handling
80
+ * Override this method in platform implementations for custom cache coordination
81
+ */
82
+ protected handleCacheMessage(message: WorkerMessage): void;
83
+ /**
84
+ * Handle HTTP request using round-robin worker selection
85
+ */
86
+ handleRequest(request: Request): Promise<Response>;
87
+ /**
88
+ * Reload ServiceWorker with new version (hot reload simulation)
89
+ */
90
+ reloadWorkers(version?: number | string): Promise<void>;
91
+ /**
92
+ * Graceful shutdown of all workers
93
+ */
94
+ terminate(): Promise<void>;
95
+ /**
96
+ * Get the number of active workers
97
+ */
98
+ get workerCount(): number;
99
+ /**
100
+ * Check if the pool is ready to handle requests
101
+ */
102
+ get ready(): boolean;
103
+ }
@@ -0,0 +1,271 @@
1
+ /// <reference types="./worker-pool.d.ts" />
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined")
6
+ return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
9
+
10
+ // src/worker-pool.ts
11
+ import * as Path from "path";
12
+ function resolveWorkerScript(entrypoint) {
13
+ if (entrypoint) {
14
+ const entryDir = Path.dirname(entrypoint);
15
+ const bundledWorker = Path.join(entryDir, "worker.js");
16
+ try {
17
+ if (typeof Bun !== "undefined") {
18
+ const file = Bun.file(bundledWorker);
19
+ if (file.size > 0) {
20
+ console.debug(`[WorkerPool] Using bundled worker: ${bundledWorker}`);
21
+ return bundledWorker;
22
+ }
23
+ } else if (typeof __require !== "undefined") {
24
+ const fs = __require("fs");
25
+ if (fs.existsSync(bundledWorker)) {
26
+ console.debug(`[WorkerPool] Using bundled worker: ${bundledWorker}`);
27
+ return bundledWorker;
28
+ }
29
+ }
30
+ } catch {
31
+ }
32
+ }
33
+ try {
34
+ const workerUrl = import.meta.resolve("@b9g/platform/worker-web.js");
35
+ let workerScript;
36
+ if (workerUrl.startsWith("file://")) {
37
+ workerScript = workerUrl.slice(7);
38
+ } else {
39
+ workerScript = workerUrl;
40
+ }
41
+ console.debug(`[WorkerPool] Using Web Worker-compatible script: ${workerScript}`);
42
+ return workerScript;
43
+ } catch (error) {
44
+ const bundledPath = entrypoint ? Path.join(Path.dirname(entrypoint), "worker-web.js") : "worker-web.js";
45
+ throw new Error(
46
+ `Could not resolve worker-web.js. Checked bundled path: ${bundledPath} and package: @b9g/platform/worker-web.js. Error: ${error instanceof Error ? error.message : String(error)}`
47
+ );
48
+ }
49
+ }
50
+ async function createWebWorker(workerScript) {
51
+ if (typeof Worker !== "undefined") {
52
+ return new Worker(workerScript, { type: "module" });
53
+ }
54
+ const isNodeJs = typeof process !== "undefined" && process.versions?.node;
55
+ if (isNodeJs) {
56
+ try {
57
+ const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
58
+ console.debug("[WorkerPool] Using @b9g/node-webworker shim for Node.js");
59
+ return new NodeWebWorker(workerScript, { type: "module" });
60
+ } catch (shimError) {
61
+ console.error("\n\u274C MISSING WEB STANDARD: Node.js lacks native Web Worker support");
62
+ console.error("\u{1F517} CANONICAL ISSUE: https://github.com/nodejs/node/issues/43583");
63
+ console.error("\u{1F4AC} This is a basic web standard from 2009 - help push for implementation!");
64
+ console.error("\u{1F5F3}\uFE0F Please \u{1F44D} react and comment on the issue to show demand\n");
65
+ throw new Error(`\u274C Web Worker not available on Node.js
66
+
67
+ \u{1F517} Node.js doesn't implement the Web Worker standard yet.
68
+ CANONICAL ISSUE: https://github.com/nodejs/node/issues/43583
69
+
70
+ \u{1F5F3}\uFE0F Please \u{1F44D} react and comment to show demand for this basic web standard!
71
+
72
+ \u{1F4A1} Immediate workaround:
73
+ npm install @b9g/node-webworker
74
+
75
+ This installs our minimal, reliable Web Worker shim for Node.js.
76
+
77
+ \u{1F4DA} Learn more: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
78
+ }
79
+ }
80
+ const runtime = typeof Bun !== "undefined" ? "Bun" : typeof Deno !== "undefined" ? "Deno" : "Unknown";
81
+ throw new Error(`\u274C Web Worker not available on ${runtime}
82
+
83
+ This runtime should support Web Workers but the API is not available.
84
+ Please check your runtime version and configuration.
85
+
86
+ \u{1F4DA} Web Worker standard: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
87
+ }
88
+ var WorkerPool = class {
89
+ workers = [];
90
+ currentWorker = 0;
91
+ requestId = 0;
92
+ pendingRequests = /* @__PURE__ */ new Map();
93
+ options;
94
+ cacheStorage;
95
+ appEntrypoint;
96
+ constructor(cacheStorage, options = {}, appEntrypoint) {
97
+ this.cacheStorage = cacheStorage;
98
+ this.appEntrypoint = appEntrypoint;
99
+ this.options = {
100
+ workerCount: 1,
101
+ requestTimeout: 3e4,
102
+ hotReload: process.env.NODE_ENV !== "production",
103
+ cwd: process.cwd(),
104
+ ...options
105
+ };
106
+ }
107
+ /**
108
+ * Initialize workers (must be called after construction)
109
+ */
110
+ async init() {
111
+ await this.initWorkers();
112
+ }
113
+ async initWorkers() {
114
+ for (let i = 0; i < this.options.workerCount; i++) {
115
+ await this.createWorker();
116
+ }
117
+ }
118
+ async createWorker() {
119
+ const workerScript = resolveWorkerScript(this.appEntrypoint);
120
+ const worker = await createWebWorker(workerScript);
121
+ worker.addEventListener("message", (event) => {
122
+ this.handleWorkerMessage(event.data || event);
123
+ });
124
+ worker.addEventListener("error", (error) => {
125
+ console.error("[WorkerPool] Worker error:", error);
126
+ });
127
+ this.workers.push(worker);
128
+ return worker;
129
+ }
130
+ handleWorkerMessage(message) {
131
+ if (message.type?.startsWith("cache:")) {
132
+ this.handleCacheMessage(message);
133
+ return;
134
+ }
135
+ switch (message.type) {
136
+ case "response":
137
+ this.handleResponse(message);
138
+ break;
139
+ case "error":
140
+ this.handleError(message);
141
+ break;
142
+ case "ready":
143
+ case "worker-ready":
144
+ this.handleReady(message);
145
+ break;
146
+ default:
147
+ console.warn("[WorkerPool] Unknown message type:", message.type);
148
+ }
149
+ }
150
+ handleResponse(message) {
151
+ const pending = this.pendingRequests.get(message.requestId);
152
+ if (pending) {
153
+ const response = new Response(message.response.body, {
154
+ status: message.response.status,
155
+ statusText: message.response.statusText,
156
+ headers: message.response.headers
157
+ });
158
+ pending.resolve(response);
159
+ this.pendingRequests.delete(message.requestId);
160
+ }
161
+ }
162
+ handleError(message) {
163
+ if (message.requestId) {
164
+ const pending = this.pendingRequests.get(message.requestId);
165
+ if (pending) {
166
+ pending.reject(new Error(message.error));
167
+ this.pendingRequests.delete(message.requestId);
168
+ }
169
+ } else {
170
+ console.error("[WorkerPool] Worker error:", message.error);
171
+ }
172
+ }
173
+ handleReady(message) {
174
+ if (message.type === "ready") {
175
+ console.info(`[WorkerPool] ServiceWorker ready (v${message.version})`);
176
+ } else if (message.type === "worker-ready") {
177
+ console.info("[WorkerPool] Worker initialized");
178
+ }
179
+ }
180
+ /**
181
+ * Platform-specific cache message handling
182
+ * Override this method in platform implementations for custom cache coordination
183
+ */
184
+ handleCacheMessage(message) {
185
+ }
186
+ /**
187
+ * Handle HTTP request using round-robin worker selection
188
+ */
189
+ async handleRequest(request) {
190
+ const worker = this.workers[this.currentWorker];
191
+ console.info(
192
+ `[WorkerPool] Dispatching to worker ${this.currentWorker + 1} of ${this.workers.length}`
193
+ );
194
+ this.currentWorker = (this.currentWorker + 1) % this.workers.length;
195
+ const requestId = ++this.requestId;
196
+ return new Promise((resolve, reject) => {
197
+ this.pendingRequests.set(requestId, { resolve, reject });
198
+ const workerRequest = {
199
+ type: "request",
200
+ request: {
201
+ url: request.url,
202
+ method: request.method,
203
+ headers: Object.fromEntries(request.headers.entries()),
204
+ body: request.body
205
+ },
206
+ requestId
207
+ };
208
+ worker.postMessage(workerRequest);
209
+ setTimeout(() => {
210
+ if (this.pendingRequests.has(requestId)) {
211
+ this.pendingRequests.delete(requestId);
212
+ reject(new Error("Request timeout"));
213
+ }
214
+ }, this.options.requestTimeout);
215
+ });
216
+ }
217
+ /**
218
+ * Reload ServiceWorker with new version (hot reload simulation)
219
+ */
220
+ async reloadWorkers(version = Date.now()) {
221
+ console.info(`[WorkerPool] Reloading ServiceWorker (v${version})`);
222
+ const loadPromises = this.workers.map((worker) => {
223
+ return new Promise((resolve) => {
224
+ const handleReady = (event) => {
225
+ const message = event.data || event;
226
+ if (message.type === "ready" && message.version === version) {
227
+ worker.removeEventListener("message", handleReady);
228
+ resolve();
229
+ }
230
+ };
231
+ console.info("[WorkerPool] Sending load message:", {
232
+ version,
233
+ entrypoint: this.appEntrypoint
234
+ });
235
+ worker.addEventListener("message", handleReady);
236
+ const loadMessage = {
237
+ type: "load",
238
+ version,
239
+ entrypoint: this.appEntrypoint
240
+ };
241
+ worker.postMessage(loadMessage);
242
+ });
243
+ });
244
+ await Promise.all(loadPromises);
245
+ console.info(`[WorkerPool] All workers reloaded (v${version})`);
246
+ }
247
+ /**
248
+ * Graceful shutdown of all workers
249
+ */
250
+ async terminate() {
251
+ const terminatePromises = this.workers.map((worker) => worker.terminate());
252
+ await Promise.allSettled(terminatePromises);
253
+ this.workers = [];
254
+ this.pendingRequests.clear();
255
+ }
256
+ /**
257
+ * Get the number of active workers
258
+ */
259
+ get workerCount() {
260
+ return this.workers.length;
261
+ }
262
+ /**
263
+ * Check if the pool is ready to handle requests
264
+ */
265
+ get ready() {
266
+ return this.workers.length > 0;
267
+ }
268
+ };
269
+ export {
270
+ WorkerPool
271
+ };
@@ -0,0 +1,119 @@
1
+ // src/worker-web.js
2
+ async function initializeWorker() {
3
+ let messagePort = self;
4
+ let sendMessage2 = (message) => postMessage(message);
5
+ onmessage = function(event) {
6
+ handleMessage(event.data);
7
+ };
8
+ return { messagePort, sendMessage: sendMessage2 };
9
+ }
10
+ var { createServiceWorkerGlobals, ServiceWorkerRuntime, createBucketStorage } = await import("./index.js");
11
+ var { CustomCacheStorage, PostMessageCache } = await import("@b9g/cache");
12
+ var caches = new CustomCacheStorage((name) => {
13
+ return new PostMessageCache(name, {
14
+ maxEntries: 1e3,
15
+ maxAge: 60 * 60 * 1e3
16
+ // 1 hour
17
+ });
18
+ });
19
+ var buckets = createBucketStorage(process.cwd() + "/dist");
20
+ var runtime = new ServiceWorkerRuntime();
21
+ createServiceWorkerGlobals(runtime, { caches, buckets });
22
+ var workerSelf = runtime;
23
+ var currentApp = null;
24
+ var serviceWorkerReady = false;
25
+ var loadedVersion = null;
26
+ async function handleFetchEvent(request) {
27
+ if (!currentApp || !serviceWorkerReady) {
28
+ throw new Error("ServiceWorker not ready");
29
+ }
30
+ try {
31
+ const response = await runtime.handleRequest(request);
32
+ return response;
33
+ } catch (error) {
34
+ console.error("[Worker] ServiceWorker request failed:", error);
35
+ const response = new Response("ServiceWorker request failed", {
36
+ status: 500
37
+ });
38
+ return response;
39
+ }
40
+ }
41
+ async function loadServiceWorker(version, entrypoint) {
42
+ try {
43
+ console.info("[Worker] loadServiceWorker called with:", { version, entrypoint });
44
+ const entrypointPath = process.env.SERVICEWORKER_PATH || entrypoint || `${process.cwd()}/dist/server/app.js`;
45
+ console.info("[Worker] Loading from:", entrypointPath);
46
+ if (loadedVersion !== null && loadedVersion !== version) {
47
+ console.info(`[Worker] Hot reload detected: ${loadedVersion} -> ${version}`);
48
+ console.info("[Worker] Creating completely fresh ServiceWorker context");
49
+ runtime = new ServiceWorkerRuntime();
50
+ createServiceWorkerGlobals(runtime, { caches, buckets });
51
+ workerSelf = runtime;
52
+ currentApp = null;
53
+ serviceWorkerReady = false;
54
+ }
55
+ if (loadedVersion === version) {
56
+ console.info("[Worker] ServiceWorker already loaded for version", version);
57
+ return;
58
+ }
59
+ globalThis.self = runtime;
60
+ globalThis.addEventListener = runtime.addEventListener.bind(runtime);
61
+ globalThis.removeEventListener = runtime.removeEventListener.bind(runtime);
62
+ globalThis.dispatchEvent = runtime.dispatchEvent.bind(runtime);
63
+ const appModule = await import(`${entrypointPath}?v=${version}`);
64
+ loadedVersion = version;
65
+ currentApp = appModule;
66
+ await runtime.install();
67
+ await runtime.activate();
68
+ serviceWorkerReady = true;
69
+ console.info(`[Worker] ServiceWorker loaded and activated (v${version}) from ${entrypointPath}`);
70
+ } catch (error) {
71
+ console.error("[Worker] Failed to load ServiceWorker:", error);
72
+ serviceWorkerReady = false;
73
+ throw error;
74
+ }
75
+ }
76
+ var workerId = Math.random().toString(36).substring(2, 8);
77
+ var sendMessage;
78
+ async function handleMessage(message) {
79
+ try {
80
+ if (message.type === "load") {
81
+ await loadServiceWorker(message.version, message.entrypoint);
82
+ sendMessage({ type: "ready", version: message.version });
83
+ } else if (message.type === "request") {
84
+ console.log(`[Worker-${workerId}] Handling request:`, message.request.url);
85
+ const request = new Request(message.request.url, {
86
+ method: message.request.method,
87
+ headers: message.request.headers,
88
+ body: message.request.body
89
+ });
90
+ const response = await handleFetchEvent(request);
91
+ sendMessage({
92
+ type: "response",
93
+ response: {
94
+ status: response.status,
95
+ statusText: response.statusText,
96
+ headers: Object.fromEntries(response.headers.entries()),
97
+ body: await response.text()
98
+ },
99
+ requestId: message.requestId
100
+ });
101
+ } else if (message.type.startsWith("cache:") || message.type.startsWith("cachestorage:")) {
102
+ } else {
103
+ console.warn("[Worker] Unknown message type:", message.type);
104
+ }
105
+ } catch (error) {
106
+ sendMessage({
107
+ type: "error",
108
+ error: error.message,
109
+ stack: error.stack,
110
+ requestId: message.requestId
111
+ });
112
+ }
113
+ }
114
+ initializeWorker().then(({ messagePort, sendMessage: send }) => {
115
+ sendMessage = send;
116
+ sendMessage({ type: "worker-ready" });
117
+ }).catch((error) => {
118
+ console.error("[Worker] Failed to initialize:", error);
119
+ });