@b9g/platform 0.1.11 → 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.
- package/README.md +1 -1
- package/package.json +7 -31
- package/src/index.d.ts +216 -11
- package/src/index.js +488 -103
- package/src/runtime.d.ts +155 -7
- package/src/runtime.js +393 -245
- package/src/shovel-config.d.ts +10 -0
- package/src/worker.d.ts +39 -0
- package/src/worker.js +285 -0
- package/src/config.d.ts +0 -172
- package/src/config.js +0 -641
- package/src/cookie-store.d.ts +0 -80
- package/src/cookie-store.js +0 -233
- package/src/single-threaded.d.ts +0 -59
- package/src/single-threaded.js +0 -114
- package/src/worker-pool.d.ts +0 -93
- package/src/worker-pool.js +0 -390
package/src/index.js
CHANGED
|
@@ -1,43 +1,21 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
__require
|
|
4
|
+
} from "../chunk-P57PW2II.js";
|
|
3
5
|
|
|
4
6
|
// src/index.ts
|
|
5
7
|
import * as Path from "path";
|
|
6
|
-
import {
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
7
10
|
import { CustomCacheStorage } from "@b9g/cache";
|
|
8
11
|
import { MemoryCache } from "@b9g/cache/memory";
|
|
12
|
+
import { getLogger } from "@logtape/logtape";
|
|
9
13
|
import {
|
|
10
|
-
ServiceWorkerPool
|
|
11
|
-
} from "./worker-pool.js";
|
|
12
|
-
import {
|
|
13
|
-
SingleThreadedRuntime
|
|
14
|
-
} from "./single-threaded.js";
|
|
15
|
-
import {
|
|
16
|
-
ShovelServiceWorkerRegistration,
|
|
17
14
|
ServiceWorkerGlobals,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
ActivateEvent,
|
|
21
|
-
ExtendableEvent
|
|
15
|
+
ShovelServiceWorkerRegistration,
|
|
16
|
+
CustomLoggerStorage
|
|
22
17
|
} from "./runtime.js";
|
|
23
|
-
|
|
24
|
-
RequestCookieStore,
|
|
25
|
-
parseCookieHeader,
|
|
26
|
-
serializeCookie,
|
|
27
|
-
parseSetCookieHeader
|
|
28
|
-
} from "./cookie-store.js";
|
|
29
|
-
import { CustomBucketStorage } from "@b9g/filesystem";
|
|
30
|
-
import {
|
|
31
|
-
loadConfig,
|
|
32
|
-
configureLogging,
|
|
33
|
-
getCacheConfig,
|
|
34
|
-
getBucketConfig,
|
|
35
|
-
parseConfigExpr,
|
|
36
|
-
processConfigValue,
|
|
37
|
-
matchPattern,
|
|
38
|
-
createBucketFactory,
|
|
39
|
-
createCacheFactory
|
|
40
|
-
} from "./config.js";
|
|
18
|
+
var logger = getLogger(["platform"]);
|
|
41
19
|
function detectRuntime() {
|
|
42
20
|
if (typeof Bun !== "undefined" || process.versions?.bun) {
|
|
43
21
|
return "bun";
|
|
@@ -47,53 +25,6 @@ function detectRuntime() {
|
|
|
47
25
|
}
|
|
48
26
|
return "node";
|
|
49
27
|
}
|
|
50
|
-
function detectPlatformFromPackageJSON(cwd) {
|
|
51
|
-
if (!cwd && typeof process === "undefined") {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
try {
|
|
55
|
-
const pkgPath = Path.join(cwd || process.cwd(), "package.json");
|
|
56
|
-
const pkgContent = readFileSync(pkgPath, "utf8");
|
|
57
|
-
const pkg = JSON.parse(pkgContent);
|
|
58
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
59
|
-
return selectPlatformFromDeps(deps);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
if (err.code !== "ENOENT") {
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function selectPlatformFromDeps(deps) {
|
|
68
|
-
const hasBun = deps["@b9g/platform-bun"];
|
|
69
|
-
const hasNode = deps["@b9g/platform-node"];
|
|
70
|
-
const hasCloudflare = deps["@b9g/platform-cloudflare"];
|
|
71
|
-
const installedCount = [hasBun, hasNode, hasCloudflare].filter(
|
|
72
|
-
Boolean
|
|
73
|
-
).length;
|
|
74
|
-
if (installedCount === 0)
|
|
75
|
-
return null;
|
|
76
|
-
if (installedCount === 1) {
|
|
77
|
-
if (hasBun)
|
|
78
|
-
return "bun";
|
|
79
|
-
if (hasNode)
|
|
80
|
-
return "node";
|
|
81
|
-
if (hasCloudflare)
|
|
82
|
-
return "cloudflare";
|
|
83
|
-
}
|
|
84
|
-
const runtime = detectRuntime();
|
|
85
|
-
if (runtime === "bun" && hasBun)
|
|
86
|
-
return "bun";
|
|
87
|
-
if (runtime === "node" && hasNode)
|
|
88
|
-
return "node";
|
|
89
|
-
if (hasBun)
|
|
90
|
-
return "bun";
|
|
91
|
-
if (hasNode)
|
|
92
|
-
return "node";
|
|
93
|
-
if (hasCloudflare)
|
|
94
|
-
return "cloudflare";
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
28
|
function detectDeploymentPlatform() {
|
|
98
29
|
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
|
|
99
30
|
return null;
|
|
@@ -105,10 +36,6 @@ function detectDeploymentPlatform() {
|
|
|
105
36
|
return null;
|
|
106
37
|
}
|
|
107
38
|
function detectDevelopmentPlatform() {
|
|
108
|
-
const pkgPlatform = detectPlatformFromPackageJSON();
|
|
109
|
-
if (pkgPlatform) {
|
|
110
|
-
return pkgPlatform;
|
|
111
|
-
}
|
|
112
39
|
const runtime = detectRuntime();
|
|
113
40
|
switch (runtime) {
|
|
114
41
|
case "bun":
|
|
@@ -234,36 +161,494 @@ async function getPlatformAsync(name) {
|
|
|
234
161
|
}
|
|
235
162
|
return platform;
|
|
236
163
|
}
|
|
164
|
+
var SingleThreadedRuntime = class {
|
|
165
|
+
#registration;
|
|
166
|
+
#scope;
|
|
167
|
+
#ready;
|
|
168
|
+
#entrypoint;
|
|
169
|
+
constructor(options) {
|
|
170
|
+
this.#ready = false;
|
|
171
|
+
this.#registration = new ShovelServiceWorkerRegistration();
|
|
172
|
+
this.#scope = new ServiceWorkerGlobals({
|
|
173
|
+
registration: this.#registration,
|
|
174
|
+
caches: options.caches,
|
|
175
|
+
directories: options.directories,
|
|
176
|
+
loggers: options.loggers
|
|
177
|
+
});
|
|
178
|
+
logger.info("SingleThreadedRuntime created");
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Initialize the runtime (install ServiceWorker globals)
|
|
182
|
+
*/
|
|
183
|
+
async init() {
|
|
184
|
+
this.#scope.install();
|
|
185
|
+
logger.info("SingleThreadedRuntime initialized - globals installed");
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Load (or reload) a ServiceWorker entrypoint
|
|
189
|
+
* @param entrypoint - Path to the entrypoint file (content-hashed filename)
|
|
190
|
+
*/
|
|
191
|
+
async load(entrypoint) {
|
|
192
|
+
const isReload = this.#entrypoint !== void 0;
|
|
193
|
+
if (isReload) {
|
|
194
|
+
logger.info("Reloading ServiceWorker", {
|
|
195
|
+
oldEntrypoint: this.#entrypoint,
|
|
196
|
+
newEntrypoint: entrypoint
|
|
197
|
+
});
|
|
198
|
+
this.#registration._serviceWorker._setState("parsed");
|
|
199
|
+
} else {
|
|
200
|
+
logger.info("Loading ServiceWorker entrypoint", { entrypoint });
|
|
201
|
+
}
|
|
202
|
+
this.#entrypoint = entrypoint;
|
|
203
|
+
this.#ready = false;
|
|
204
|
+
await import(entrypoint);
|
|
205
|
+
await this.#registration.install();
|
|
206
|
+
await this.#registration.activate();
|
|
207
|
+
this.#ready = true;
|
|
208
|
+
logger.info("ServiceWorker loaded and activated", { entrypoint });
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Handle an HTTP request
|
|
212
|
+
* This is the key method - direct call, no postMessage!
|
|
213
|
+
*/
|
|
214
|
+
async handleRequest(request) {
|
|
215
|
+
if (!this.#ready) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
"SingleThreadedRuntime not ready - ServiceWorker not loaded"
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return this.#registration.handleRequest(request);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Graceful shutdown
|
|
224
|
+
*/
|
|
225
|
+
async terminate() {
|
|
226
|
+
this.#ready = false;
|
|
227
|
+
logger.info("SingleThreadedRuntime terminated");
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get the number of workers (always 1 for single-threaded)
|
|
231
|
+
*/
|
|
232
|
+
get workerCount() {
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Check if ready to handle requests
|
|
237
|
+
*/
|
|
238
|
+
get ready() {
|
|
239
|
+
return this.#ready;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
function resolveWorkerScript(entrypoint) {
|
|
243
|
+
if (entrypoint) {
|
|
244
|
+
const entryDir = Path.dirname(entrypoint);
|
|
245
|
+
const bundledWorker = Path.join(entryDir, "worker.js");
|
|
246
|
+
try {
|
|
247
|
+
if (typeof Bun !== "undefined") {
|
|
248
|
+
const file = Bun.file(bundledWorker);
|
|
249
|
+
if (file.size > 0) {
|
|
250
|
+
logger.info("Using bundled worker", { bundledWorker });
|
|
251
|
+
return bundledWorker;
|
|
252
|
+
}
|
|
253
|
+
} else if (typeof __require !== "undefined") {
|
|
254
|
+
if (existsSync(bundledWorker)) {
|
|
255
|
+
logger.info("Using bundled worker", { bundledWorker });
|
|
256
|
+
return bundledWorker;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err.code !== "ENOENT") {
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const workerURL = import.meta.resolve("@b9g/platform/worker.js");
|
|
267
|
+
let workerScript;
|
|
268
|
+
if (workerURL.startsWith("file://")) {
|
|
269
|
+
workerScript = fileURLToPath(workerURL);
|
|
270
|
+
} else {
|
|
271
|
+
workerScript = workerURL;
|
|
272
|
+
}
|
|
273
|
+
logger.info("Using worker entry script", { workerScript });
|
|
274
|
+
return workerScript;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const bundledPath = entrypoint ? Path.join(Path.dirname(entrypoint), "worker.js") : "worker.js";
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Could not resolve worker.js. Checked bundled path: ${bundledPath} and package: @b9g/platform/worker.js. Error: ${error instanceof Error ? error.message : String(error)}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function createWebWorker(workerScript) {
|
|
283
|
+
if (typeof Worker !== "undefined") {
|
|
284
|
+
return new Worker(workerScript, { type: "module" });
|
|
285
|
+
}
|
|
286
|
+
const isNodeJs = typeof process !== "undefined" && process.versions?.node;
|
|
287
|
+
if (isNodeJs) {
|
|
288
|
+
try {
|
|
289
|
+
const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
|
|
290
|
+
logger.info("Using @b9g/node-webworker shim for Node.js", {});
|
|
291
|
+
return new NodeWebWorker(workerScript, {
|
|
292
|
+
type: "module"
|
|
293
|
+
});
|
|
294
|
+
} catch (shimError) {
|
|
295
|
+
logger.error(
|
|
296
|
+
"MISSING WEB STANDARD: Node.js lacks native Web Worker support",
|
|
297
|
+
{
|
|
298
|
+
canonicalIssue: "https://github.com/nodejs/node/issues/43583",
|
|
299
|
+
message: "This is a basic web standard from 2009 - help push for implementation!"
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
throw new Error(`\u274C Web Worker not available on Node.js
|
|
303
|
+
|
|
304
|
+
\u{1F517} Node.js doesn't implement the Web Worker standard yet.
|
|
305
|
+
CANONICAL ISSUE: https://github.com/nodejs/node/issues/43583
|
|
306
|
+
|
|
307
|
+
\u{1F5F3}\uFE0F Please \u{1F44D} react and comment to show demand for this basic web standard!
|
|
308
|
+
|
|
309
|
+
\u{1F4A1} Immediate workaround:
|
|
310
|
+
npm install @b9g/node-webworker
|
|
311
|
+
|
|
312
|
+
This installs our minimal, reliable Web Worker shim for Node.js.
|
|
313
|
+
|
|
314
|
+
\u{1F4DA} Learn more: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const runtime = typeof Bun !== "undefined" ? "Bun" : typeof Deno !== "undefined" ? "Deno" : "Unknown";
|
|
318
|
+
throw new Error(`\u274C Web Worker not available on ${runtime}
|
|
319
|
+
|
|
320
|
+
This runtime should support Web Workers but the API is not available.
|
|
321
|
+
Please check your runtime version and configuration.
|
|
322
|
+
|
|
323
|
+
\u{1F4DA} Web Worker standard: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
|
|
324
|
+
}
|
|
325
|
+
var ServiceWorkerPool = class {
|
|
326
|
+
#workers;
|
|
327
|
+
#currentWorker;
|
|
328
|
+
#requestID;
|
|
329
|
+
#pendingRequests;
|
|
330
|
+
#pendingWorkerInit;
|
|
331
|
+
#options;
|
|
332
|
+
#appEntrypoint;
|
|
333
|
+
#cacheStorage;
|
|
334
|
+
// CustomCacheStorage for cache coordination
|
|
335
|
+
#config;
|
|
336
|
+
// ShovelConfig from config.ts
|
|
337
|
+
constructor(options = {}, appEntrypoint, cacheStorage, config) {
|
|
338
|
+
this.#workers = [];
|
|
339
|
+
this.#currentWorker = 0;
|
|
340
|
+
this.#requestID = 0;
|
|
341
|
+
this.#pendingRequests = /* @__PURE__ */ new Map();
|
|
342
|
+
this.#pendingWorkerInit = /* @__PURE__ */ new Map();
|
|
343
|
+
this.#appEntrypoint = appEntrypoint;
|
|
344
|
+
this.#cacheStorage = cacheStorage;
|
|
345
|
+
this.#config = config || {};
|
|
346
|
+
this.#options = {
|
|
347
|
+
workerCount: 1,
|
|
348
|
+
requestTimeout: 3e4,
|
|
349
|
+
...options
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Initialize workers (must be called after construction)
|
|
354
|
+
*/
|
|
355
|
+
async init() {
|
|
356
|
+
await this.#initWorkers();
|
|
357
|
+
}
|
|
358
|
+
async #initWorkers() {
|
|
359
|
+
for (let i = 0; i < this.#options.workerCount; i++) {
|
|
360
|
+
await this.#createWorker();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async #createWorker() {
|
|
364
|
+
const workerScript = resolveWorkerScript(this.#appEntrypoint);
|
|
365
|
+
const worker = await createWebWorker(workerScript);
|
|
366
|
+
let rejectWorkerReady;
|
|
367
|
+
let workerReadyTimeoutId;
|
|
368
|
+
const workerReadyPromise = new Promise((resolve, reject) => {
|
|
369
|
+
rejectWorkerReady = reject;
|
|
370
|
+
workerReadyTimeoutId = setTimeout(() => {
|
|
371
|
+
reject(new Error("Worker failed to send ready signal within 30000ms"));
|
|
372
|
+
}, 3e4);
|
|
373
|
+
this.#pendingWorkerInit.set(worker, {
|
|
374
|
+
workerReady: () => {
|
|
375
|
+
clearTimeout(workerReadyTimeoutId);
|
|
376
|
+
resolve();
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
worker.addEventListener("message", (event) => {
|
|
381
|
+
this.#handleWorkerMessage(worker, event.data || event);
|
|
382
|
+
});
|
|
383
|
+
worker.addEventListener("error", (event) => {
|
|
384
|
+
const errorMessage = event.message || event.error?.message || "Unknown worker error";
|
|
385
|
+
const error = new Error(`Worker failed to start: ${errorMessage}`);
|
|
386
|
+
logger.error("Worker error: {error}", {
|
|
387
|
+
error: event.error || errorMessage,
|
|
388
|
+
filename: event.filename,
|
|
389
|
+
lineno: event.lineno,
|
|
390
|
+
colno: event.colno
|
|
391
|
+
});
|
|
392
|
+
clearTimeout(workerReadyTimeoutId);
|
|
393
|
+
rejectWorkerReady(error);
|
|
394
|
+
});
|
|
395
|
+
this.#workers.push(worker);
|
|
396
|
+
logger.info("Waiting for worker-ready signal");
|
|
397
|
+
await workerReadyPromise;
|
|
398
|
+
logger.info("Received worker-ready signal");
|
|
399
|
+
const initializedPromise = new Promise((resolve, reject) => {
|
|
400
|
+
const timeoutId = setTimeout(() => {
|
|
401
|
+
reject(
|
|
402
|
+
new Error("Worker failed to send initialized signal within 30000ms")
|
|
403
|
+
);
|
|
404
|
+
}, 3e4);
|
|
405
|
+
const pending = this.#pendingWorkerInit.get(worker) || {};
|
|
406
|
+
pending.initialized = () => {
|
|
407
|
+
clearTimeout(timeoutId);
|
|
408
|
+
resolve();
|
|
409
|
+
};
|
|
410
|
+
this.#pendingWorkerInit.set(worker, pending);
|
|
411
|
+
});
|
|
412
|
+
if (!this.#appEntrypoint) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
"ServiceWorkerPool requires an entrypoint to derive baseDir"
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const baseDir = Path.dirname(this.#appEntrypoint);
|
|
418
|
+
const initMessage = {
|
|
419
|
+
type: "init",
|
|
420
|
+
config: this.#config,
|
|
421
|
+
baseDir
|
|
422
|
+
};
|
|
423
|
+
logger.info("Sending init message", { config: this.#config, baseDir });
|
|
424
|
+
worker.postMessage(initMessage);
|
|
425
|
+
logger.info("Sent init message, waiting for initialized response");
|
|
426
|
+
await initializedPromise;
|
|
427
|
+
logger.info("Received initialized response");
|
|
428
|
+
this.#pendingWorkerInit.delete(worker);
|
|
429
|
+
return worker;
|
|
430
|
+
}
|
|
431
|
+
#handleWorkerMessage(worker, message) {
|
|
432
|
+
logger.debug("Worker message received", { type: message.type });
|
|
433
|
+
const pending = this.#pendingWorkerInit.get(worker);
|
|
434
|
+
if (message.type === "worker-ready" && pending?.workerReady) {
|
|
435
|
+
pending.workerReady();
|
|
436
|
+
} else if (message.type === "initialized" && pending?.initialized) {
|
|
437
|
+
pending.initialized();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
switch (message.type) {
|
|
441
|
+
case "response":
|
|
442
|
+
this.#handleResponse(message);
|
|
443
|
+
break;
|
|
444
|
+
case "error":
|
|
445
|
+
this.#handleError(message);
|
|
446
|
+
break;
|
|
447
|
+
case "ready":
|
|
448
|
+
case "worker-ready":
|
|
449
|
+
this.#handleReady(message);
|
|
450
|
+
break;
|
|
451
|
+
case "initialized":
|
|
452
|
+
break;
|
|
453
|
+
default:
|
|
454
|
+
if (message.type?.startsWith("cache:")) {
|
|
455
|
+
logger.debug("Cache message received", { type: message.type });
|
|
456
|
+
if (this.#cacheStorage) {
|
|
457
|
+
const storage = this.#cacheStorage;
|
|
458
|
+
if (typeof storage.handleMessage === "function") {
|
|
459
|
+
storage.handleMessage(worker, message).catch((err) => {
|
|
460
|
+
logger.error("Cache message handling failed: {error}", {
|
|
461
|
+
error: err
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
#handleResponse(message) {
|
|
471
|
+
const pending = this.#pendingRequests.get(message.requestID);
|
|
472
|
+
if (pending) {
|
|
473
|
+
if (pending.timeoutId) {
|
|
474
|
+
clearTimeout(pending.timeoutId);
|
|
475
|
+
}
|
|
476
|
+
const response = new Response(message.response.body, {
|
|
477
|
+
status: message.response.status,
|
|
478
|
+
statusText: message.response.statusText,
|
|
479
|
+
headers: message.response.headers
|
|
480
|
+
});
|
|
481
|
+
pending.resolve(response);
|
|
482
|
+
this.#pendingRequests.delete(message.requestID);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
#handleError(message) {
|
|
486
|
+
logger.error("Worker error message received: {error}", {
|
|
487
|
+
error: message.error,
|
|
488
|
+
stack: message.stack,
|
|
489
|
+
requestID: message.requestID
|
|
490
|
+
});
|
|
491
|
+
if (message.requestID) {
|
|
492
|
+
const pending = this.#pendingRequests.get(message.requestID);
|
|
493
|
+
if (pending) {
|
|
494
|
+
if (pending.timeoutId) {
|
|
495
|
+
clearTimeout(pending.timeoutId);
|
|
496
|
+
}
|
|
497
|
+
pending.reject(new Error(message.error));
|
|
498
|
+
this.#pendingRequests.delete(message.requestID);
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
logger.error("Worker error: {error}", { error: message.error });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
#handleReady(message) {
|
|
505
|
+
if (message.type === "ready") {
|
|
506
|
+
logger.info("ServiceWorker ready", { entrypoint: message.entrypoint });
|
|
507
|
+
} else if (message.type === "worker-ready") {
|
|
508
|
+
logger.info("Worker initialized", {});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Handle HTTP request using round-robin worker selection
|
|
513
|
+
*/
|
|
514
|
+
async handleRequest(request) {
|
|
515
|
+
const worker = this.#workers[this.#currentWorker];
|
|
516
|
+
logger.info("Dispatching to worker", {
|
|
517
|
+
workerIndex: this.#currentWorker + 1,
|
|
518
|
+
totalWorkers: this.#workers.length
|
|
519
|
+
});
|
|
520
|
+
this.#currentWorker = (this.#currentWorker + 1) % this.#workers.length;
|
|
521
|
+
const requestID = ++this.#requestID;
|
|
522
|
+
return new Promise((resolve, reject) => {
|
|
523
|
+
const timeoutId = setTimeout(() => {
|
|
524
|
+
if (this.#pendingRequests.has(requestID)) {
|
|
525
|
+
this.#pendingRequests.delete(requestID);
|
|
526
|
+
reject(new Error("Request timeout"));
|
|
527
|
+
}
|
|
528
|
+
}, this.#options.requestTimeout);
|
|
529
|
+
this.#pendingRequests.set(requestID, { resolve, reject, timeoutId });
|
|
530
|
+
this.#sendRequest(worker, request, requestID).catch(reject);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Send request to worker (async helper to avoid async promise executor)
|
|
535
|
+
*/
|
|
536
|
+
async #sendRequest(worker, request, requestID) {
|
|
537
|
+
let body = null;
|
|
538
|
+
if (request.body) {
|
|
539
|
+
body = await request.arrayBuffer();
|
|
540
|
+
}
|
|
541
|
+
const workerRequest = {
|
|
542
|
+
type: "request",
|
|
543
|
+
request: {
|
|
544
|
+
url: request.url,
|
|
545
|
+
method: request.method,
|
|
546
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
547
|
+
body
|
|
548
|
+
},
|
|
549
|
+
requestID
|
|
550
|
+
};
|
|
551
|
+
if (body) {
|
|
552
|
+
worker.postMessage(workerRequest, [body]);
|
|
553
|
+
} else {
|
|
554
|
+
worker.postMessage(workerRequest);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Reload ServiceWorker with new entrypoint (hot reload)
|
|
559
|
+
* The entrypoint path contains a content hash for cache busting
|
|
560
|
+
*/
|
|
561
|
+
async reloadWorkers(entrypoint) {
|
|
562
|
+
logger.info("Reloading ServiceWorker", { entrypoint });
|
|
563
|
+
this.#appEntrypoint = entrypoint;
|
|
564
|
+
const loadPromises = this.#workers.map((worker) => {
|
|
565
|
+
return new Promise((resolve, reject) => {
|
|
566
|
+
let timeoutId;
|
|
567
|
+
const cleanup = () => {
|
|
568
|
+
worker.removeEventListener("message", handleReady);
|
|
569
|
+
worker.removeEventListener("error", handleError);
|
|
570
|
+
if (timeoutId) {
|
|
571
|
+
clearTimeout(timeoutId);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
const handleReady = (event) => {
|
|
575
|
+
const message = event.data || event;
|
|
576
|
+
if (message.type === "ready" && message.entrypoint === entrypoint) {
|
|
577
|
+
cleanup();
|
|
578
|
+
resolve();
|
|
579
|
+
} else if (message.type === "error") {
|
|
580
|
+
cleanup();
|
|
581
|
+
reject(
|
|
582
|
+
new Error(
|
|
583
|
+
`Worker failed to load ServiceWorker: ${message.error}`
|
|
584
|
+
)
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
const handleError = (error) => {
|
|
589
|
+
cleanup();
|
|
590
|
+
const errorMsg = error?.error?.message || error?.message || JSON.stringify(error);
|
|
591
|
+
reject(new Error(`Worker failed to load ServiceWorker: ${errorMsg}`));
|
|
592
|
+
};
|
|
593
|
+
timeoutId = setTimeout(() => {
|
|
594
|
+
cleanup();
|
|
595
|
+
reject(
|
|
596
|
+
new Error(
|
|
597
|
+
`Worker failed to load ServiceWorker within 30000ms (entrypoint ${entrypoint})`
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
}, 3e4);
|
|
601
|
+
logger.info("Sending load message", {
|
|
602
|
+
entrypoint
|
|
603
|
+
});
|
|
604
|
+
worker.addEventListener("message", handleReady);
|
|
605
|
+
worker.addEventListener("error", handleError);
|
|
606
|
+
const loadMessage = {
|
|
607
|
+
type: "load",
|
|
608
|
+
entrypoint
|
|
609
|
+
};
|
|
610
|
+
logger.debug("[WorkerPool] Sending load message", {
|
|
611
|
+
entrypoint
|
|
612
|
+
});
|
|
613
|
+
worker.postMessage(loadMessage);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
await Promise.all(loadPromises);
|
|
617
|
+
logger.info("All workers reloaded", { entrypoint });
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Graceful shutdown of all workers
|
|
621
|
+
*/
|
|
622
|
+
async terminate() {
|
|
623
|
+
const terminatePromises = this.#workers.map((worker) => worker.terminate());
|
|
624
|
+
await Promise.allSettled(terminatePromises);
|
|
625
|
+
this.#workers = [];
|
|
626
|
+
this.#pendingRequests.clear();
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Get the number of active workers
|
|
630
|
+
*/
|
|
631
|
+
get workerCount() {
|
|
632
|
+
return this.#workers.length;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Check if the pool is ready to handle requests
|
|
636
|
+
*/
|
|
637
|
+
get ready() {
|
|
638
|
+
return this.#workers.length > 0;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
237
641
|
export {
|
|
238
|
-
ActivateEvent,
|
|
239
642
|
BasePlatform,
|
|
240
|
-
|
|
241
|
-
ExtendableEvent,
|
|
242
|
-
FetchEvent,
|
|
243
|
-
InstallEvent,
|
|
244
|
-
RequestCookieStore,
|
|
245
|
-
ServiceWorkerGlobals,
|
|
643
|
+
CustomLoggerStorage,
|
|
246
644
|
ServiceWorkerPool,
|
|
247
|
-
ShovelServiceWorkerRegistration,
|
|
248
645
|
SingleThreadedRuntime,
|
|
249
|
-
configureLogging,
|
|
250
|
-
createBucketFactory,
|
|
251
|
-
createCacheFactory,
|
|
252
646
|
createPlatform,
|
|
253
647
|
detectDeploymentPlatform,
|
|
254
648
|
detectDevelopmentPlatform,
|
|
255
649
|
detectRuntime,
|
|
256
|
-
getBucketConfig,
|
|
257
|
-
getCacheConfig,
|
|
258
650
|
getPlatform,
|
|
259
651
|
getPlatformAsync,
|
|
260
|
-
loadConfig,
|
|
261
|
-
matchPattern,
|
|
262
|
-
parseConfigExpr,
|
|
263
|
-
parseCookieHeader,
|
|
264
|
-
parseSetCookieHeader,
|
|
265
652
|
platformRegistry,
|
|
266
|
-
|
|
267
|
-
resolvePlatform,
|
|
268
|
-
serializeCookie
|
|
653
|
+
resolvePlatform
|
|
269
654
|
};
|