@b9g/platform 0.1.10 → 0.1.12

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