@b9g/platform 0.1.11 → 0.1.13

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.
@@ -1,390 +0,0 @@
1
- /// <reference types="./worker-pool.d.ts" />
2
- import {
3
- __require
4
- } from "../chunk-P57PW2II.js";
5
-
6
- // src/worker-pool.ts
7
- import * as Path from "path";
8
- import { existsSync } from "fs";
9
- import { getLogger } from "@logtape/logtape";
10
- var logger = getLogger(["worker"]);
11
- function resolveWorkerScript(entrypoint) {
12
- if (entrypoint) {
13
- const entryDir = Path.dirname(entrypoint);
14
- const bundledWorker = Path.join(entryDir, "worker.js");
15
- try {
16
- if (typeof Bun !== "undefined") {
17
- const file = Bun.file(bundledWorker);
18
- if (file.size > 0) {
19
- logger.info("Using bundled worker", { bundledWorker });
20
- return bundledWorker;
21
- }
22
- } else if (typeof __require !== "undefined") {
23
- if (existsSync(bundledWorker)) {
24
- logger.info("Using bundled worker", { bundledWorker });
25
- return bundledWorker;
26
- }
27
- }
28
- } catch (err) {
29
- if (err.code !== "ENOENT") {
30
- throw err;
31
- }
32
- }
33
- }
34
- try {
35
- const workerURL = import.meta.resolve("@b9g/platform/runtime.js");
36
- let workerScript;
37
- if (workerURL.startsWith("file://")) {
38
- workerScript = workerURL.slice(7);
39
- } else {
40
- workerScript = workerURL;
41
- }
42
- logger.info("Using worker runtime script", { workerScript });
43
- return workerScript;
44
- } catch (error) {
45
- const bundledPath = entrypoint ? Path.join(Path.dirname(entrypoint), "runtime.js") : "runtime.js";
46
- throw new Error(
47
- `Could not resolve runtime.js. Checked bundled path: ${bundledPath} and package: @b9g/platform/runtime.js. Error: ${error instanceof Error ? error.message : String(error)}`
48
- );
49
- }
50
- }
51
- async function createWebWorker(workerScript) {
52
- if (typeof Worker !== "undefined") {
53
- return new Worker(workerScript, { type: "module" });
54
- }
55
- const isNodeJs = typeof process !== "undefined" && process.versions?.node;
56
- if (isNodeJs) {
57
- try {
58
- const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
59
- logger.info("Using @b9g/node-webworker shim for Node.js", {});
60
- return new NodeWebWorker(workerScript, {
61
- type: "module"
62
- });
63
- } catch (shimError) {
64
- logger.error(
65
- "MISSING WEB STANDARD: Node.js lacks native Web Worker support",
66
- {
67
- canonicalIssue: "https://github.com/nodejs/node/issues/43583",
68
- message: "This is a basic web standard from 2009 - help push for implementation!"
69
- }
70
- );
71
- throw new Error(`\u274C Web Worker not available on Node.js
72
-
73
- \u{1F517} Node.js doesn't implement the Web Worker standard yet.
74
- CANONICAL ISSUE: https://github.com/nodejs/node/issues/43583
75
-
76
- \u{1F5F3}\uFE0F Please \u{1F44D} react and comment to show demand for this basic web standard!
77
-
78
- \u{1F4A1} Immediate workaround:
79
- npm install @b9g/node-webworker
80
-
81
- This installs our minimal, reliable Web Worker shim for Node.js.
82
-
83
- \u{1F4DA} Learn more: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
84
- }
85
- }
86
- const runtime = typeof Bun !== "undefined" ? "Bun" : typeof Deno !== "undefined" ? "Deno" : "Unknown";
87
- throw new Error(`\u274C Web Worker not available on ${runtime}
88
-
89
- This runtime should support Web Workers but the API is not available.
90
- Please check your runtime version and configuration.
91
-
92
- \u{1F4DA} Web Worker standard: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
93
- }
94
- var ServiceWorkerPool = class {
95
- #workers;
96
- #currentWorker;
97
- #requestID;
98
- #pendingRequests;
99
- #pendingWorkerInit;
100
- #options;
101
- #appEntrypoint;
102
- #cacheStorage;
103
- // CustomCacheStorage for cache coordination
104
- #config;
105
- // ShovelConfig from config.ts
106
- constructor(options = {}, appEntrypoint, cacheStorage, config) {
107
- this.#workers = [];
108
- this.#currentWorker = 0;
109
- this.#requestID = 0;
110
- this.#pendingRequests = /* @__PURE__ */ new Map();
111
- this.#pendingWorkerInit = /* @__PURE__ */ new Map();
112
- this.#appEntrypoint = appEntrypoint;
113
- this.#cacheStorage = cacheStorage;
114
- this.#config = config || {};
115
- this.#options = {
116
- workerCount: 1,
117
- requestTimeout: 3e4,
118
- ...options
119
- };
120
- }
121
- /**
122
- * Initialize workers (must be called after construction)
123
- */
124
- async init() {
125
- await this.#initWorkers();
126
- }
127
- async #initWorkers() {
128
- for (let i = 0; i < this.#options.workerCount; i++) {
129
- await this.#createWorker();
130
- }
131
- }
132
- async #createWorker() {
133
- const workerScript = resolveWorkerScript(this.#appEntrypoint);
134
- const worker = await createWebWorker(workerScript);
135
- const workerReadyPromise = new Promise((resolve) => {
136
- this.#pendingWorkerInit.set(worker, {
137
- workerReady: resolve
138
- });
139
- });
140
- worker.addEventListener("message", (event) => {
141
- this.#handleWorkerMessage(worker, event.data || event);
142
- });
143
- worker.addEventListener("error", (event) => {
144
- logger.error("Worker error", {
145
- message: event.message || event.error?.message,
146
- filename: event.filename,
147
- lineno: event.lineno,
148
- colno: event.colno,
149
- error: event.error,
150
- stack: event.error?.stack
151
- });
152
- });
153
- this.#workers.push(worker);
154
- logger.info("Waiting for worker-ready signal");
155
- await workerReadyPromise;
156
- logger.info("Received worker-ready signal");
157
- const initializedPromise = new Promise((resolve) => {
158
- const pending = this.#pendingWorkerInit.get(worker) || {};
159
- pending.initialized = resolve;
160
- this.#pendingWorkerInit.set(worker, pending);
161
- });
162
- if (!this.#appEntrypoint) {
163
- throw new Error(
164
- "ServiceWorkerPool requires an entrypoint to derive baseDir"
165
- );
166
- }
167
- const baseDir = Path.dirname(this.#appEntrypoint);
168
- const initMessage = {
169
- type: "init",
170
- config: this.#config,
171
- baseDir
172
- };
173
- logger.info("Sending init message", { config: this.#config, baseDir });
174
- worker.postMessage(initMessage);
175
- logger.info("Sent init message, waiting for initialized response");
176
- await initializedPromise;
177
- logger.info("Received initialized response");
178
- this.#pendingWorkerInit.delete(worker);
179
- return worker;
180
- }
181
- #handleWorkerMessage(worker, message) {
182
- logger.debug("Worker message received", { type: message.type });
183
- const pending = this.#pendingWorkerInit.get(worker);
184
- if (message.type === "worker-ready" && pending?.workerReady) {
185
- pending.workerReady();
186
- } else if (message.type === "initialized" && pending?.initialized) {
187
- pending.initialized();
188
- return;
189
- }
190
- switch (message.type) {
191
- case "response":
192
- this.#handleResponse(message);
193
- break;
194
- case "error":
195
- this.#handleError(message);
196
- break;
197
- case "ready":
198
- case "worker-ready":
199
- this.#handleReady(message);
200
- break;
201
- case "initialized":
202
- break;
203
- default:
204
- if (message.type?.startsWith("cache:")) {
205
- logger.debug("Cache message detected", {
206
- type: message.type,
207
- hasStorage: !!this.#cacheStorage
208
- });
209
- if (this.#cacheStorage) {
210
- const handleMessage = this.#cacheStorage.handleMessage;
211
- logger.debug("handleMessage check", {
212
- hasMethod: typeof handleMessage === "function"
213
- });
214
- if (typeof handleMessage === "function") {
215
- logger.debug("Calling handleMessage");
216
- void handleMessage.call(this.#cacheStorage, worker, message);
217
- }
218
- }
219
- }
220
- break;
221
- }
222
- }
223
- #handleResponse(message) {
224
- const pending = this.#pendingRequests.get(message.requestID);
225
- if (pending) {
226
- const response = new Response(message.response.body, {
227
- status: message.response.status,
228
- statusText: message.response.statusText,
229
- headers: message.response.headers
230
- });
231
- pending.resolve(response);
232
- this.#pendingRequests.delete(message.requestID);
233
- }
234
- }
235
- #handleError(message) {
236
- logger.error("Worker error message received", {
237
- error: message.error,
238
- stack: message.stack,
239
- requestID: message.requestID
240
- });
241
- if (message.requestID) {
242
- const pending = this.#pendingRequests.get(message.requestID);
243
- if (pending) {
244
- pending.reject(new Error(message.error));
245
- this.#pendingRequests.delete(message.requestID);
246
- }
247
- } else {
248
- logger.error("Worker error: {error}", { error: message.error });
249
- }
250
- }
251
- #handleReady(message) {
252
- if (message.type === "ready") {
253
- logger.info("ServiceWorker ready", { entrypoint: message.entrypoint });
254
- } else if (message.type === "worker-ready") {
255
- logger.info("Worker initialized", {});
256
- }
257
- }
258
- /**
259
- * Handle HTTP request using round-robin worker selection
260
- */
261
- async handleRequest(request) {
262
- const worker = this.#workers[this.#currentWorker];
263
- logger.info("Dispatching to worker", {
264
- workerIndex: this.#currentWorker + 1,
265
- totalWorkers: this.#workers.length
266
- });
267
- this.#currentWorker = (this.#currentWorker + 1) % this.#workers.length;
268
- const requestID = ++this.#requestID;
269
- return new Promise((resolve, reject) => {
270
- this.#pendingRequests.set(requestID, { resolve, reject });
271
- this.#sendRequest(worker, request, requestID).catch(reject);
272
- setTimeout(() => {
273
- if (this.#pendingRequests.has(requestID)) {
274
- this.#pendingRequests.delete(requestID);
275
- reject(new Error("Request timeout"));
276
- }
277
- }, this.#options.requestTimeout);
278
- });
279
- }
280
- /**
281
- * Send request to worker (async helper to avoid async promise executor)
282
- */
283
- async #sendRequest(worker, request, requestID) {
284
- let body = null;
285
- if (request.body) {
286
- body = await request.arrayBuffer();
287
- }
288
- const workerRequest = {
289
- type: "request",
290
- request: {
291
- url: request.url,
292
- method: request.method,
293
- headers: Object.fromEntries(request.headers.entries()),
294
- body
295
- },
296
- requestID
297
- };
298
- if (body) {
299
- worker.postMessage(workerRequest, [body]);
300
- } else {
301
- worker.postMessage(workerRequest);
302
- }
303
- }
304
- /**
305
- * Reload ServiceWorker with new entrypoint (hot reload)
306
- * The entrypoint path contains a content hash for cache busting
307
- */
308
- async reloadWorkers(entrypoint) {
309
- logger.info("Reloading ServiceWorker", { entrypoint });
310
- this.#appEntrypoint = entrypoint;
311
- const loadPromises = this.#workers.map((worker) => {
312
- return new Promise((resolve, reject) => {
313
- let timeoutId;
314
- const cleanup = () => {
315
- worker.removeEventListener("message", handleReady);
316
- worker.removeEventListener("error", handleError);
317
- if (timeoutId) {
318
- clearTimeout(timeoutId);
319
- }
320
- };
321
- const handleReady = (event) => {
322
- const message = event.data || event;
323
- if (message.type === "ready" && message.entrypoint === entrypoint) {
324
- cleanup();
325
- resolve();
326
- } else if (message.type === "error") {
327
- cleanup();
328
- reject(
329
- new Error(
330
- `Worker failed to load ServiceWorker: ${message.error}`
331
- )
332
- );
333
- }
334
- };
335
- const handleError = (error) => {
336
- cleanup();
337
- const errorMsg = error?.error?.message || error?.message || JSON.stringify(error);
338
- reject(new Error(`Worker failed to load ServiceWorker: ${errorMsg}`));
339
- };
340
- timeoutId = setTimeout(() => {
341
- cleanup();
342
- reject(
343
- new Error(
344
- `Worker failed to load ServiceWorker within 30000ms (entrypoint ${entrypoint})`
345
- )
346
- );
347
- }, 3e4);
348
- logger.info("Sending load message", {
349
- entrypoint
350
- });
351
- worker.addEventListener("message", handleReady);
352
- worker.addEventListener("error", handleError);
353
- const loadMessage = {
354
- type: "load",
355
- entrypoint
356
- };
357
- logger.debug("[WorkerPool] Sending load message", {
358
- entrypoint
359
- });
360
- worker.postMessage(loadMessage);
361
- });
362
- });
363
- await Promise.all(loadPromises);
364
- logger.info("All workers reloaded", { entrypoint });
365
- }
366
- /**
367
- * Graceful shutdown of all workers
368
- */
369
- async terminate() {
370
- const terminatePromises = this.#workers.map((worker) => worker.terminate());
371
- await Promise.allSettled(terminatePromises);
372
- this.#workers = [];
373
- this.#pendingRequests.clear();
374
- }
375
- /**
376
- * Get the number of active workers
377
- */
378
- get workerCount() {
379
- return this.#workers.length;
380
- }
381
- /**
382
- * Check if the pool is ready to handle requests
383
- */
384
- get ready() {
385
- return this.#workers.length > 0;
386
- }
387
- };
388
- export {
389
- ServiceWorkerPool
390
- };