@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.
- package/README.md +1 -1
- package/package.json +22 -38
- package/src/config.d.ts +15 -163
- package/src/config.js +18 -630
- package/src/globals.d.ts +119 -0
- package/src/index.d.ts +294 -25
- package/src/index.js +466 -126
- package/src/runtime.d.ts +423 -22
- package/src/runtime.js +693 -250
- package/src/shovel-config.d.ts +10 -0
- package/chunk-P57PW2II.js +0 -11
- 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,18 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
|
-
import "../chunk-P57PW2II.js";
|
|
3
|
-
|
|
4
2
|
// src/index.ts
|
|
5
|
-
import
|
|
6
|
-
import { readFileSync } from "fs";
|
|
7
|
-
import { CustomCacheStorage } from "@b9g/cache";
|
|
8
|
-
import { MemoryCache } from "@b9g/cache/memory";
|
|
9
|
-
import {
|
|
10
|
-
ServiceWorkerPool
|
|
11
|
-
} from "./worker-pool.js";
|
|
3
|
+
import { getLogger } from "@logtape/logtape";
|
|
12
4
|
import {
|
|
13
|
-
SingleThreadedRuntime
|
|
14
|
-
} from "./single-threaded.js";
|
|
15
|
-
import {
|
|
16
|
-
ShovelServiceWorkerRegistration,
|
|
17
5
|
ServiceWorkerGlobals,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ExtendableEvent
|
|
6
|
+
ShovelServiceWorkerRegistration,
|
|
7
|
+
ShovelFetchEvent,
|
|
8
|
+
CustomLoggerStorage
|
|
22
9
|
} from "./runtime.js";
|
|
10
|
+
import { validateConfig, ConfigValidationError } from "./config.js";
|
|
23
11
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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";
|
|
12
|
+
CustomDatabaseStorage,
|
|
13
|
+
createDatabaseFactory
|
|
14
|
+
} from "./runtime.js";
|
|
15
|
+
var logger = getLogger(["shovel", "platform"]);
|
|
41
16
|
function detectRuntime() {
|
|
42
17
|
if (typeof Bun !== "undefined" || process.versions?.bun) {
|
|
43
18
|
return "bun";
|
|
@@ -47,53 +22,6 @@ function detectRuntime() {
|
|
|
47
22
|
}
|
|
48
23
|
return "node";
|
|
49
24
|
}
|
|
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
25
|
function detectDeploymentPlatform() {
|
|
98
26
|
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
|
|
99
27
|
return null;
|
|
@@ -105,10 +33,6 @@ function detectDeploymentPlatform() {
|
|
|
105
33
|
return null;
|
|
106
34
|
}
|
|
107
35
|
function detectDevelopmentPlatform() {
|
|
108
|
-
const pkgPlatform = detectPlatformFromPackageJSON();
|
|
109
|
-
if (pkgPlatform) {
|
|
110
|
-
return pkgPlatform;
|
|
111
|
-
}
|
|
112
36
|
const runtime = detectRuntime();
|
|
113
37
|
switch (runtime) {
|
|
114
38
|
case "bun":
|
|
@@ -139,27 +63,20 @@ function resolvePlatform(options) {
|
|
|
139
63
|
async function createPlatform(platformName, options = {}) {
|
|
140
64
|
switch (platformName) {
|
|
141
65
|
case "node": {
|
|
142
|
-
const
|
|
143
|
-
const NodePlatform = await import(modulePath).then((m) => m.default);
|
|
66
|
+
const { default: NodePlatform } = await import("@b9g/platform-node");
|
|
144
67
|
return new NodePlatform(options);
|
|
145
68
|
}
|
|
146
69
|
case "bun": {
|
|
147
|
-
const
|
|
148
|
-
const BunPlatform = await import(modulePath).then((m) => m.default);
|
|
70
|
+
const { default: BunPlatform } = await import("@b9g/platform-bun");
|
|
149
71
|
return new BunPlatform(options);
|
|
150
72
|
}
|
|
151
|
-
case "cloudflare":
|
|
152
|
-
|
|
153
|
-
case "cf": {
|
|
154
|
-
const modulePath = import.meta.resolve("@b9g/platform-cloudflare");
|
|
155
|
-
const CloudflarePlatform = await import(modulePath).then(
|
|
156
|
-
(m) => m.default
|
|
157
|
-
);
|
|
73
|
+
case "cloudflare": {
|
|
74
|
+
const { default: CloudflarePlatform } = await import("@b9g/platform-cloudflare");
|
|
158
75
|
return new CloudflarePlatform(options);
|
|
159
76
|
}
|
|
160
77
|
default:
|
|
161
78
|
throw new Error(
|
|
162
|
-
`Unknown platform: ${platformName}.
|
|
79
|
+
`Unknown platform: ${platformName}. Valid platforms: node, bun, cloudflare`
|
|
163
80
|
);
|
|
164
81
|
}
|
|
165
82
|
}
|
|
@@ -168,15 +85,6 @@ var BasePlatform = class {
|
|
|
168
85
|
constructor(config = {}) {
|
|
169
86
|
this.config = config;
|
|
170
87
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Create cache storage
|
|
173
|
-
* Returns empty CacheStorage - applications create caches on-demand via caches.open()
|
|
174
|
-
*/
|
|
175
|
-
async createCaches() {
|
|
176
|
-
return new CustomCacheStorage(
|
|
177
|
-
(name) => new MemoryCache(name)
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
88
|
};
|
|
181
89
|
var DefaultPlatformRegistry = class {
|
|
182
90
|
#platforms;
|
|
@@ -234,36 +142,468 @@ async function getPlatformAsync(name) {
|
|
|
234
142
|
}
|
|
235
143
|
return platform;
|
|
236
144
|
}
|
|
145
|
+
var SingleThreadedRuntime = class {
|
|
146
|
+
#registration;
|
|
147
|
+
#scope;
|
|
148
|
+
#ready;
|
|
149
|
+
#entrypoint;
|
|
150
|
+
constructor(options) {
|
|
151
|
+
this.#ready = false;
|
|
152
|
+
this.#registration = new ShovelServiceWorkerRegistration();
|
|
153
|
+
this.#scope = new ServiceWorkerGlobals({
|
|
154
|
+
registration: this.#registration,
|
|
155
|
+
caches: options.caches,
|
|
156
|
+
directories: options.directories,
|
|
157
|
+
databases: options.databases,
|
|
158
|
+
loggers: options.loggers
|
|
159
|
+
});
|
|
160
|
+
logger.debug("SingleThreadedRuntime created");
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Initialize the runtime (install ServiceWorker globals)
|
|
164
|
+
*/
|
|
165
|
+
async init() {
|
|
166
|
+
this.#scope.install();
|
|
167
|
+
logger.debug("SingleThreadedRuntime initialized - globals installed");
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Load (or reload) a ServiceWorker entrypoint
|
|
171
|
+
* @param entrypoint - Path to the entrypoint file (content-hashed filename)
|
|
172
|
+
*/
|
|
173
|
+
async load(entrypoint) {
|
|
174
|
+
const isReload = this.#entrypoint !== void 0;
|
|
175
|
+
if (isReload) {
|
|
176
|
+
logger.debug("Reloading ServiceWorker", {
|
|
177
|
+
oldEntrypoint: this.#entrypoint,
|
|
178
|
+
newEntrypoint: entrypoint
|
|
179
|
+
});
|
|
180
|
+
this.#registration._serviceWorker._setState("parsed");
|
|
181
|
+
} else {
|
|
182
|
+
logger.debug("Loading ServiceWorker entrypoint", { entrypoint });
|
|
183
|
+
}
|
|
184
|
+
this.#entrypoint = entrypoint;
|
|
185
|
+
this.#ready = false;
|
|
186
|
+
await import(entrypoint);
|
|
187
|
+
await this.#registration.install();
|
|
188
|
+
await this.#registration.activate();
|
|
189
|
+
this.#ready = true;
|
|
190
|
+
logger.debug("ServiceWorker loaded and activated", { entrypoint });
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Handle an HTTP request
|
|
194
|
+
* This is the key method - direct call, no postMessage!
|
|
195
|
+
*/
|
|
196
|
+
async handleRequest(request) {
|
|
197
|
+
if (!this.#ready) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"SingleThreadedRuntime not ready - ServiceWorker not loaded"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const event = new ShovelFetchEvent(request);
|
|
203
|
+
return this.#registration.handleRequest(event);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Graceful shutdown
|
|
207
|
+
*/
|
|
208
|
+
async terminate() {
|
|
209
|
+
this.#ready = false;
|
|
210
|
+
logger.debug("SingleThreadedRuntime terminated");
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get the number of workers (always 1 for single-threaded)
|
|
214
|
+
*/
|
|
215
|
+
get workerCount() {
|
|
216
|
+
return 1;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Check if ready to handle requests
|
|
220
|
+
*/
|
|
221
|
+
get ready() {
|
|
222
|
+
return this.#ready;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
async function createWebWorker(workerScript) {
|
|
226
|
+
if (typeof Worker !== "undefined") {
|
|
227
|
+
return new Worker(workerScript, { type: "module" });
|
|
228
|
+
}
|
|
229
|
+
const isNodeJs = typeof process !== "undefined" && process.versions?.node;
|
|
230
|
+
if (isNodeJs) {
|
|
231
|
+
try {
|
|
232
|
+
const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
|
|
233
|
+
logger.info("Using @b9g/node-webworker shim for Node.js", {});
|
|
234
|
+
return new NodeWebWorker(workerScript, {
|
|
235
|
+
type: "module"
|
|
236
|
+
});
|
|
237
|
+
} catch (shimError) {
|
|
238
|
+
logger.error(
|
|
239
|
+
"MISSING WEB STANDARD: Node.js lacks native Web Worker support",
|
|
240
|
+
{
|
|
241
|
+
canonicalIssue: "https://github.com/nodejs/node/issues/43583",
|
|
242
|
+
message: "This is a basic web standard from 2009 - help push for implementation!"
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
throw new Error(`\u274C Web Worker not available on Node.js
|
|
246
|
+
|
|
247
|
+
\u{1F517} Node.js doesn't implement the Web Worker standard yet.
|
|
248
|
+
CANONICAL ISSUE: https://github.com/nodejs/node/issues/43583
|
|
249
|
+
|
|
250
|
+
\u{1F5F3}\uFE0F Please \u{1F44D} react and comment to show demand for this basic web standard!
|
|
251
|
+
|
|
252
|
+
\u{1F4A1} Immediate workaround:
|
|
253
|
+
npm install @b9g/node-webworker
|
|
254
|
+
|
|
255
|
+
This installs our minimal, reliable Web Worker shim for Node.js.
|
|
256
|
+
|
|
257
|
+
\u{1F4DA} Learn more: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const runtime = typeof Bun !== "undefined" ? "Bun" : typeof Deno !== "undefined" ? "Deno" : "Unknown";
|
|
261
|
+
throw new Error(`\u274C Web Worker not available on ${runtime}
|
|
262
|
+
|
|
263
|
+
This runtime should support Web Workers but the API is not available.
|
|
264
|
+
Please check your runtime version and configuration.
|
|
265
|
+
|
|
266
|
+
\u{1F4DA} Web Worker standard: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
|
|
267
|
+
}
|
|
268
|
+
var ServiceWorkerPool = class {
|
|
269
|
+
#workers;
|
|
270
|
+
#currentWorker;
|
|
271
|
+
#requestID;
|
|
272
|
+
#pendingRequests;
|
|
273
|
+
#pendingWorkerReady;
|
|
274
|
+
#options;
|
|
275
|
+
#appEntrypoint;
|
|
276
|
+
#cacheStorage;
|
|
277
|
+
// Waiters for when workers become available (used during reload)
|
|
278
|
+
#workerAvailableWaiters;
|
|
279
|
+
constructor(options = {}, appEntrypoint, cacheStorage) {
|
|
280
|
+
this.#workers = [];
|
|
281
|
+
this.#currentWorker = 0;
|
|
282
|
+
this.#requestID = 0;
|
|
283
|
+
this.#pendingRequests = /* @__PURE__ */ new Map();
|
|
284
|
+
this.#pendingWorkerReady = /* @__PURE__ */ new Map();
|
|
285
|
+
this.#workerAvailableWaiters = [];
|
|
286
|
+
this.#appEntrypoint = appEntrypoint;
|
|
287
|
+
this.#cacheStorage = cacheStorage;
|
|
288
|
+
this.#options = {
|
|
289
|
+
workerCount: 1,
|
|
290
|
+
requestTimeout: 3e4,
|
|
291
|
+
...options
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Initialize workers (must be called after construction)
|
|
296
|
+
*/
|
|
297
|
+
async init() {
|
|
298
|
+
const promises = [];
|
|
299
|
+
for (let i = 0; i < this.#options.workerCount; i++) {
|
|
300
|
+
promises.push(this.#createWorker(this.#appEntrypoint));
|
|
301
|
+
}
|
|
302
|
+
await Promise.all(promises);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Create a worker from the unified bundle
|
|
306
|
+
* The bundle self-initializes and sends "ready" when done
|
|
307
|
+
*/
|
|
308
|
+
async #createWorker(entrypoint) {
|
|
309
|
+
const worker = await createWebWorker(entrypoint);
|
|
310
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
311
|
+
const timeoutId = setTimeout(() => {
|
|
312
|
+
this.#pendingWorkerReady.delete(worker);
|
|
313
|
+
reject(
|
|
314
|
+
new Error(
|
|
315
|
+
`Worker failed to become ready within 30000ms (${entrypoint})`
|
|
316
|
+
)
|
|
317
|
+
);
|
|
318
|
+
}, 3e4);
|
|
319
|
+
this.#pendingWorkerReady.set(worker, {
|
|
320
|
+
resolve: () => {
|
|
321
|
+
clearTimeout(timeoutId);
|
|
322
|
+
resolve();
|
|
323
|
+
},
|
|
324
|
+
reject: (error) => {
|
|
325
|
+
clearTimeout(timeoutId);
|
|
326
|
+
reject(error);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
worker.addEventListener("message", (event) => {
|
|
331
|
+
this.#handleWorkerMessage(worker, event.data || event);
|
|
332
|
+
});
|
|
333
|
+
worker.addEventListener("error", (event) => {
|
|
334
|
+
const errorMessage = event.message || event.error?.message || "Unknown worker error";
|
|
335
|
+
const error = new Error(`Worker error: ${errorMessage}`);
|
|
336
|
+
logger.error("Worker error: {error}", {
|
|
337
|
+
error: event.error || errorMessage,
|
|
338
|
+
filename: event.filename,
|
|
339
|
+
lineno: event.lineno,
|
|
340
|
+
colno: event.colno
|
|
341
|
+
});
|
|
342
|
+
const pending = this.#pendingWorkerReady.get(worker);
|
|
343
|
+
if (pending) {
|
|
344
|
+
this.#pendingWorkerReady.delete(worker);
|
|
345
|
+
pending.reject(error);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
logger.debug("Waiting for worker ready signal", { entrypoint });
|
|
349
|
+
await readyPromise;
|
|
350
|
+
this.#pendingWorkerReady.delete(worker);
|
|
351
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
352
|
+
this.#workers.push(worker);
|
|
353
|
+
logger.debug("Worker ready", { entrypoint });
|
|
354
|
+
const waiters = this.#workerAvailableWaiters;
|
|
355
|
+
this.#workerAvailableWaiters = [];
|
|
356
|
+
for (const waiter of waiters) {
|
|
357
|
+
waiter.resolve();
|
|
358
|
+
}
|
|
359
|
+
return worker;
|
|
360
|
+
}
|
|
361
|
+
#handleWorkerMessage(worker, message) {
|
|
362
|
+
logger.debug("Worker message received", { type: message.type });
|
|
363
|
+
switch (message.type) {
|
|
364
|
+
case "ready": {
|
|
365
|
+
const pending = this.#pendingWorkerReady.get(worker);
|
|
366
|
+
if (pending) {
|
|
367
|
+
pending.resolve();
|
|
368
|
+
}
|
|
369
|
+
logger.debug("ServiceWorker ready");
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
case "response":
|
|
373
|
+
this.#handleResponse(message);
|
|
374
|
+
break;
|
|
375
|
+
case "error":
|
|
376
|
+
this.#handleError(message);
|
|
377
|
+
break;
|
|
378
|
+
default:
|
|
379
|
+
if (message.type?.startsWith("cache:")) {
|
|
380
|
+
logger.debug("Cache message received", { type: message.type });
|
|
381
|
+
if (this.#cacheStorage) {
|
|
382
|
+
const storage = this.#cacheStorage;
|
|
383
|
+
if (typeof storage.handleMessage === "function") {
|
|
384
|
+
storage.handleMessage(worker, message).catch((err) => {
|
|
385
|
+
logger.error("Cache message handling failed: {error}", {
|
|
386
|
+
error: err
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
#handleResponse(message) {
|
|
396
|
+
const pending = this.#pendingRequests.get(message.requestID);
|
|
397
|
+
if (pending) {
|
|
398
|
+
if (pending.timeoutId) {
|
|
399
|
+
clearTimeout(pending.timeoutId);
|
|
400
|
+
}
|
|
401
|
+
const response = new Response(message.response.body, {
|
|
402
|
+
status: message.response.status,
|
|
403
|
+
statusText: message.response.statusText,
|
|
404
|
+
headers: message.response.headers
|
|
405
|
+
});
|
|
406
|
+
pending.resolve(response);
|
|
407
|
+
this.#pendingRequests.delete(message.requestID);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
#handleError(message) {
|
|
411
|
+
logger.error("Worker error message received: {error}", {
|
|
412
|
+
error: message.error,
|
|
413
|
+
stack: message.stack,
|
|
414
|
+
requestID: message.requestID
|
|
415
|
+
});
|
|
416
|
+
if (message.requestID) {
|
|
417
|
+
const pending = this.#pendingRequests.get(message.requestID);
|
|
418
|
+
if (pending) {
|
|
419
|
+
if (pending.timeoutId) {
|
|
420
|
+
clearTimeout(pending.timeoutId);
|
|
421
|
+
}
|
|
422
|
+
pending.reject(new Error(message.error));
|
|
423
|
+
this.#pendingRequests.delete(message.requestID);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Handle HTTP request using round-robin worker selection
|
|
429
|
+
*/
|
|
430
|
+
async handleRequest(request) {
|
|
431
|
+
if (this.#workers.length === 0) {
|
|
432
|
+
logger.debug("No workers available, waiting for worker to be ready");
|
|
433
|
+
await new Promise((resolve, reject) => {
|
|
434
|
+
const waiter = { resolve, reject };
|
|
435
|
+
this.#workerAvailableWaiters.push(waiter);
|
|
436
|
+
const timeoutId = setTimeout(() => {
|
|
437
|
+
const index = this.#workerAvailableWaiters.indexOf(waiter);
|
|
438
|
+
if (index !== -1) {
|
|
439
|
+
this.#workerAvailableWaiters.splice(index, 1);
|
|
440
|
+
reject(new Error("Timeout waiting for worker to become available"));
|
|
441
|
+
}
|
|
442
|
+
}, this.#options.requestTimeout);
|
|
443
|
+
const originalResolve = waiter.resolve;
|
|
444
|
+
const originalReject = waiter.reject;
|
|
445
|
+
waiter.resolve = () => {
|
|
446
|
+
clearTimeout(timeoutId);
|
|
447
|
+
originalResolve();
|
|
448
|
+
};
|
|
449
|
+
waiter.reject = (error) => {
|
|
450
|
+
clearTimeout(timeoutId);
|
|
451
|
+
originalReject(error);
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
const worker = this.#workers[this.#currentWorker];
|
|
456
|
+
logger.debug("Dispatching to worker", {
|
|
457
|
+
workerIndex: this.#currentWorker + 1,
|
|
458
|
+
totalWorkers: this.#workers.length
|
|
459
|
+
});
|
|
460
|
+
this.#currentWorker = (this.#currentWorker + 1) % this.#workers.length;
|
|
461
|
+
const requestID = ++this.#requestID;
|
|
462
|
+
return new Promise((resolve, reject) => {
|
|
463
|
+
const timeoutId = setTimeout(() => {
|
|
464
|
+
if (this.#pendingRequests.has(requestID)) {
|
|
465
|
+
this.#pendingRequests.delete(requestID);
|
|
466
|
+
reject(new Error("Request timeout"));
|
|
467
|
+
}
|
|
468
|
+
}, this.#options.requestTimeout);
|
|
469
|
+
this.#pendingRequests.set(requestID, { resolve, reject, timeoutId });
|
|
470
|
+
this.#sendRequest(worker, request, requestID).catch(reject);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async #sendRequest(worker, request, requestID) {
|
|
474
|
+
let body = null;
|
|
475
|
+
if (request.body) {
|
|
476
|
+
body = await request.arrayBuffer();
|
|
477
|
+
}
|
|
478
|
+
const workerRequest = {
|
|
479
|
+
type: "request",
|
|
480
|
+
request: {
|
|
481
|
+
url: request.url,
|
|
482
|
+
method: request.method,
|
|
483
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
484
|
+
body
|
|
485
|
+
},
|
|
486
|
+
requestID
|
|
487
|
+
};
|
|
488
|
+
if (body) {
|
|
489
|
+
worker.postMessage(workerRequest, [body]);
|
|
490
|
+
} else {
|
|
491
|
+
worker.postMessage(workerRequest);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Gracefully shutdown a worker by closing all resources first
|
|
496
|
+
*/
|
|
497
|
+
async #gracefulShutdown(worker, timeout = 5e3) {
|
|
498
|
+
return new Promise((resolve) => {
|
|
499
|
+
let resolved = false;
|
|
500
|
+
const onMessage = (event) => {
|
|
501
|
+
const message = event.data || event;
|
|
502
|
+
if (message?.type === "shutdown-complete") {
|
|
503
|
+
if (!resolved) {
|
|
504
|
+
resolved = true;
|
|
505
|
+
worker.removeEventListener("message", onMessage);
|
|
506
|
+
resolve();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
worker.addEventListener("message", onMessage);
|
|
511
|
+
worker.postMessage({ type: "shutdown" });
|
|
512
|
+
setTimeout(() => {
|
|
513
|
+
if (!resolved) {
|
|
514
|
+
resolved = true;
|
|
515
|
+
worker.removeEventListener("message", onMessage);
|
|
516
|
+
logger.warn("Worker shutdown timed out, forcing termination");
|
|
517
|
+
resolve();
|
|
518
|
+
}
|
|
519
|
+
}, timeout);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Reload workers with new entrypoint (hot reload)
|
|
524
|
+
*
|
|
525
|
+
* With unified builds, hot reload means:
|
|
526
|
+
* 1. Gracefully shutdown existing workers (close databases, etc.)
|
|
527
|
+
* 2. Terminate workers after resources are closed
|
|
528
|
+
* 3. Create new workers with the new bundle
|
|
529
|
+
*/
|
|
530
|
+
async reloadWorkers(entrypoint) {
|
|
531
|
+
logger.debug("Reloading workers", { entrypoint });
|
|
532
|
+
this.#appEntrypoint = entrypoint;
|
|
533
|
+
const shutdownPromises = this.#workers.map(
|
|
534
|
+
(worker) => this.#gracefulShutdown(worker)
|
|
535
|
+
);
|
|
536
|
+
await Promise.allSettled(shutdownPromises);
|
|
537
|
+
const terminatePromises = this.#workers.map((worker) => worker.terminate());
|
|
538
|
+
await Promise.allSettled(terminatePromises);
|
|
539
|
+
this.#workers = [];
|
|
540
|
+
this.#currentWorker = 0;
|
|
541
|
+
try {
|
|
542
|
+
const createPromises = [];
|
|
543
|
+
for (let i = 0; i < this.#options.workerCount; i++) {
|
|
544
|
+
createPromises.push(this.#createWorker(entrypoint));
|
|
545
|
+
}
|
|
546
|
+
await Promise.all(createPromises);
|
|
547
|
+
logger.debug("All workers reloaded", { entrypoint });
|
|
548
|
+
} catch (error) {
|
|
549
|
+
const waiters = this.#workerAvailableWaiters;
|
|
550
|
+
this.#workerAvailableWaiters = [];
|
|
551
|
+
const reloadError = error instanceof Error ? error : new Error("Worker creation failed during reload");
|
|
552
|
+
for (const waiter of waiters) {
|
|
553
|
+
waiter.reject(reloadError);
|
|
554
|
+
}
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Graceful shutdown of all workers
|
|
560
|
+
*/
|
|
561
|
+
async terminate() {
|
|
562
|
+
const shutdownPromises = this.#workers.map(
|
|
563
|
+
(worker) => this.#gracefulShutdown(worker)
|
|
564
|
+
);
|
|
565
|
+
await Promise.allSettled(shutdownPromises);
|
|
566
|
+
const terminatePromises = this.#workers.map((worker) => worker.terminate());
|
|
567
|
+
await Promise.allSettled(terminatePromises);
|
|
568
|
+
this.#workers = [];
|
|
569
|
+
this.#currentWorker = 0;
|
|
570
|
+
this.#pendingRequests.clear();
|
|
571
|
+
this.#pendingWorkerReady.clear();
|
|
572
|
+
const waiters = this.#workerAvailableWaiters;
|
|
573
|
+
this.#workerAvailableWaiters = [];
|
|
574
|
+
const terminateError = new Error("Worker pool terminated");
|
|
575
|
+
for (const waiter of waiters) {
|
|
576
|
+
waiter.reject(terminateError);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Get the number of active workers
|
|
581
|
+
*/
|
|
582
|
+
get workerCount() {
|
|
583
|
+
return this.#workers.length;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Check if the pool is ready to handle requests
|
|
587
|
+
*/
|
|
588
|
+
get ready() {
|
|
589
|
+
return this.#workers.length > 0;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
237
592
|
export {
|
|
238
|
-
ActivateEvent,
|
|
239
593
|
BasePlatform,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
InstallEvent,
|
|
244
|
-
RequestCookieStore,
|
|
245
|
-
ServiceWorkerGlobals,
|
|
594
|
+
ConfigValidationError,
|
|
595
|
+
CustomDatabaseStorage,
|
|
596
|
+
CustomLoggerStorage,
|
|
246
597
|
ServiceWorkerPool,
|
|
247
|
-
ShovelServiceWorkerRegistration,
|
|
248
598
|
SingleThreadedRuntime,
|
|
249
|
-
|
|
250
|
-
createBucketFactory,
|
|
251
|
-
createCacheFactory,
|
|
599
|
+
createDatabaseFactory,
|
|
252
600
|
createPlatform,
|
|
253
601
|
detectDeploymentPlatform,
|
|
254
602
|
detectDevelopmentPlatform,
|
|
255
603
|
detectRuntime,
|
|
256
|
-
getBucketConfig,
|
|
257
|
-
getCacheConfig,
|
|
258
604
|
getPlatform,
|
|
259
605
|
getPlatformAsync,
|
|
260
|
-
loadConfig,
|
|
261
|
-
matchPattern,
|
|
262
|
-
parseConfigExpr,
|
|
263
|
-
parseCookieHeader,
|
|
264
|
-
parseSetCookieHeader,
|
|
265
606
|
platformRegistry,
|
|
266
|
-
processConfigValue,
|
|
267
607
|
resolvePlatform,
|
|
268
|
-
|
|
608
|
+
validateConfig
|
|
269
609
|
};
|