@b9g/platform 0.1.12 → 0.1.14-beta.0
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 +25 -17
- package/src/config.d.ts +24 -0
- package/src/config.js +29 -0
- package/src/globals.d.ts +119 -0
- package/src/index.d.ts +167 -114
- package/src/index.js +176 -291
- package/src/runtime.d.ts +372 -72
- package/src/runtime.js +469 -181
- package/chunk-P57PW2II.js +0 -11
- package/src/worker.d.ts +0 -39
- package/src/worker.js +0 -285
package/src/index.js
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
|
-
import {
|
|
3
|
-
__require
|
|
4
|
-
} from "../chunk-P57PW2II.js";
|
|
5
|
-
|
|
6
2
|
// src/index.ts
|
|
7
|
-
import * as Path from "path";
|
|
8
|
-
import { existsSync } from "fs";
|
|
9
|
-
import { fileURLToPath } from "url";
|
|
10
|
-
import { CustomCacheStorage } from "@b9g/cache";
|
|
11
|
-
import { MemoryCache } from "@b9g/cache/memory";
|
|
12
3
|
import { getLogger } from "@logtape/logtape";
|
|
4
|
+
import { CustomLoggerStorage } from "./runtime.js";
|
|
5
|
+
import { validateConfig, ConfigValidationError } from "./config.js";
|
|
13
6
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
CustomLoggerStorage
|
|
7
|
+
CustomDatabaseStorage,
|
|
8
|
+
createDatabaseFactory
|
|
17
9
|
} from "./runtime.js";
|
|
18
|
-
var logger = getLogger(["platform"]);
|
|
10
|
+
var logger = getLogger(["shovel", "platform"]);
|
|
19
11
|
function detectRuntime() {
|
|
20
12
|
if (typeof Bun !== "undefined" || process.versions?.bun) {
|
|
21
13
|
return "bun";
|
|
@@ -66,27 +58,20 @@ function resolvePlatform(options) {
|
|
|
66
58
|
async function createPlatform(platformName, options = {}) {
|
|
67
59
|
switch (platformName) {
|
|
68
60
|
case "node": {
|
|
69
|
-
const
|
|
70
|
-
const NodePlatform = await import(modulePath).then((m) => m.default);
|
|
61
|
+
const { default: NodePlatform } = await import("@b9g/platform-node");
|
|
71
62
|
return new NodePlatform(options);
|
|
72
63
|
}
|
|
73
64
|
case "bun": {
|
|
74
|
-
const
|
|
75
|
-
const BunPlatform = await import(modulePath).then((m) => m.default);
|
|
65
|
+
const { default: BunPlatform } = await import("@b9g/platform-bun");
|
|
76
66
|
return new BunPlatform(options);
|
|
77
67
|
}
|
|
78
|
-
case "cloudflare":
|
|
79
|
-
|
|
80
|
-
case "cf": {
|
|
81
|
-
const modulePath = import.meta.resolve("@b9g/platform-cloudflare");
|
|
82
|
-
const CloudflarePlatform = await import(modulePath).then(
|
|
83
|
-
(m) => m.default
|
|
84
|
-
);
|
|
68
|
+
case "cloudflare": {
|
|
69
|
+
const { default: CloudflarePlatform } = await import("@b9g/platform-cloudflare");
|
|
85
70
|
return new CloudflarePlatform(options);
|
|
86
71
|
}
|
|
87
72
|
default:
|
|
88
73
|
throw new Error(
|
|
89
|
-
`Unknown platform: ${platformName}.
|
|
74
|
+
`Unknown platform: ${platformName}. Valid platforms: node, bun, cloudflare`
|
|
90
75
|
);
|
|
91
76
|
}
|
|
92
77
|
}
|
|
@@ -96,15 +81,21 @@ var BasePlatform = class {
|
|
|
96
81
|
this.config = config;
|
|
97
82
|
}
|
|
98
83
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
84
|
+
* Dispose of platform resources
|
|
85
|
+
* Subclasses should override to clean up worker pools, connections, etc.
|
|
101
86
|
*/
|
|
102
|
-
async
|
|
103
|
-
return new CustomCacheStorage(
|
|
104
|
-
(name) => new MemoryCache(name)
|
|
105
|
-
);
|
|
87
|
+
async dispose() {
|
|
106
88
|
}
|
|
107
89
|
};
|
|
90
|
+
function mergeConfigWithDefaults(defaults, userConfig) {
|
|
91
|
+
const user = userConfig ?? {};
|
|
92
|
+
const allNames = /* @__PURE__ */ new Set([...Object.keys(defaults), ...Object.keys(user)]);
|
|
93
|
+
const merged = {};
|
|
94
|
+
for (const name of allNames) {
|
|
95
|
+
merged[name] = { ...defaults[name], ...user[name] };
|
|
96
|
+
}
|
|
97
|
+
return merged;
|
|
98
|
+
}
|
|
108
99
|
var DefaultPlatformRegistry = class {
|
|
109
100
|
#platforms;
|
|
110
101
|
constructor() {
|
|
@@ -161,124 +152,6 @@ async function getPlatformAsync(name) {
|
|
|
161
152
|
}
|
|
162
153
|
return platform;
|
|
163
154
|
}
|
|
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
155
|
async function createWebWorker(workerScript) {
|
|
283
156
|
if (typeof Worker !== "undefined") {
|
|
284
157
|
return new Worker(workerScript, { type: "module" });
|
|
@@ -327,22 +200,21 @@ var ServiceWorkerPool = class {
|
|
|
327
200
|
#currentWorker;
|
|
328
201
|
#requestID;
|
|
329
202
|
#pendingRequests;
|
|
330
|
-
#
|
|
203
|
+
#pendingWorkerReady;
|
|
331
204
|
#options;
|
|
332
205
|
#appEntrypoint;
|
|
333
206
|
#cacheStorage;
|
|
334
|
-
//
|
|
335
|
-
#
|
|
336
|
-
|
|
337
|
-
constructor(options = {}, appEntrypoint, cacheStorage, config) {
|
|
207
|
+
// Waiters for when workers become available (used during reload)
|
|
208
|
+
#workerAvailableWaiters;
|
|
209
|
+
constructor(options = {}, appEntrypoint, cacheStorage) {
|
|
338
210
|
this.#workers = [];
|
|
339
211
|
this.#currentWorker = 0;
|
|
340
212
|
this.#requestID = 0;
|
|
341
213
|
this.#pendingRequests = /* @__PURE__ */ new Map();
|
|
342
|
-
this.#
|
|
214
|
+
this.#pendingWorkerReady = /* @__PURE__ */ new Map();
|
|
215
|
+
this.#workerAvailableWaiters = [];
|
|
343
216
|
this.#appEntrypoint = appEntrypoint;
|
|
344
217
|
this.#cacheStorage = cacheStorage;
|
|
345
|
-
this.#config = config || {};
|
|
346
218
|
this.#options = {
|
|
347
219
|
workerCount: 1,
|
|
348
220
|
requestTimeout: 3e4,
|
|
@@ -353,27 +225,35 @@ var ServiceWorkerPool = class {
|
|
|
353
225
|
* Initialize workers (must be called after construction)
|
|
354
226
|
*/
|
|
355
227
|
async init() {
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
async #initWorkers() {
|
|
228
|
+
const promises = [];
|
|
359
229
|
for (let i = 0; i < this.#options.workerCount; i++) {
|
|
360
|
-
|
|
230
|
+
promises.push(this.#createWorker(this.#appEntrypoint));
|
|
361
231
|
}
|
|
232
|
+
await Promise.all(promises);
|
|
362
233
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Create a worker from the unified bundle
|
|
236
|
+
* The bundle self-initializes and sends "ready" when done
|
|
237
|
+
*/
|
|
238
|
+
async #createWorker(entrypoint) {
|
|
239
|
+
const worker = this.#options.createWorker ? await this.#options.createWorker(entrypoint) : await createWebWorker(entrypoint);
|
|
240
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
241
|
+
const timeoutId = setTimeout(() => {
|
|
242
|
+
this.#pendingWorkerReady.delete(worker);
|
|
243
|
+
reject(
|
|
244
|
+
new Error(
|
|
245
|
+
`Worker failed to become ready within 30000ms (${entrypoint})`
|
|
246
|
+
)
|
|
247
|
+
);
|
|
372
248
|
}, 3e4);
|
|
373
|
-
this.#
|
|
374
|
-
|
|
375
|
-
clearTimeout(
|
|
249
|
+
this.#pendingWorkerReady.set(worker, {
|
|
250
|
+
resolve: () => {
|
|
251
|
+
clearTimeout(timeoutId);
|
|
376
252
|
resolve();
|
|
253
|
+
},
|
|
254
|
+
reject: (error) => {
|
|
255
|
+
clearTimeout(timeoutId);
|
|
256
|
+
reject(error);
|
|
377
257
|
}
|
|
378
258
|
});
|
|
379
259
|
});
|
|
@@ -382,74 +262,49 @@ var ServiceWorkerPool = class {
|
|
|
382
262
|
});
|
|
383
263
|
worker.addEventListener("error", (event) => {
|
|
384
264
|
const errorMessage = event.message || event.error?.message || "Unknown worker error";
|
|
385
|
-
const error = new Error(`Worker
|
|
265
|
+
const error = new Error(`Worker error: ${errorMessage}`);
|
|
386
266
|
logger.error("Worker error: {error}", {
|
|
387
267
|
error: event.error || errorMessage,
|
|
388
268
|
filename: event.filename,
|
|
389
269
|
lineno: event.lineno,
|
|
390
270
|
colno: event.colno
|
|
391
271
|
});
|
|
392
|
-
|
|
393
|
-
|
|
272
|
+
const pending = this.#pendingWorkerReady.get(worker);
|
|
273
|
+
if (pending) {
|
|
274
|
+
this.#pendingWorkerReady.delete(worker);
|
|
275
|
+
pending.reject(error);
|
|
276
|
+
}
|
|
394
277
|
});
|
|
278
|
+
logger.debug("Waiting for worker ready signal", { entrypoint });
|
|
279
|
+
await readyPromise;
|
|
280
|
+
this.#pendingWorkerReady.delete(worker);
|
|
281
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
395
282
|
this.#workers.push(worker);
|
|
396
|
-
logger.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
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
|
-
);
|
|
283
|
+
logger.debug("Worker ready", { entrypoint });
|
|
284
|
+
const waiters = this.#workerAvailableWaiters;
|
|
285
|
+
this.#workerAvailableWaiters = [];
|
|
286
|
+
for (const waiter of waiters) {
|
|
287
|
+
waiter.resolve();
|
|
416
288
|
}
|
|
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
289
|
return worker;
|
|
430
290
|
}
|
|
431
291
|
#handleWorkerMessage(worker, message) {
|
|
432
292
|
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
293
|
switch (message.type) {
|
|
294
|
+
case "ready": {
|
|
295
|
+
const pending = this.#pendingWorkerReady.get(worker);
|
|
296
|
+
if (pending) {
|
|
297
|
+
pending.resolve();
|
|
298
|
+
}
|
|
299
|
+
logger.debug("ServiceWorker ready");
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
441
302
|
case "response":
|
|
442
303
|
this.#handleResponse(message);
|
|
443
304
|
break;
|
|
444
305
|
case "error":
|
|
445
306
|
this.#handleError(message);
|
|
446
307
|
break;
|
|
447
|
-
case "ready":
|
|
448
|
-
case "worker-ready":
|
|
449
|
-
this.#handleReady(message);
|
|
450
|
-
break;
|
|
451
|
-
case "initialized":
|
|
452
|
-
break;
|
|
453
308
|
default:
|
|
454
309
|
if (message.type?.startsWith("cache:")) {
|
|
455
310
|
logger.debug("Cache message received", { type: message.type });
|
|
@@ -497,23 +352,38 @@ var ServiceWorkerPool = class {
|
|
|
497
352
|
pending.reject(new Error(message.error));
|
|
498
353
|
this.#pendingRequests.delete(message.requestID);
|
|
499
354
|
}
|
|
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
355
|
}
|
|
510
356
|
}
|
|
511
357
|
/**
|
|
512
358
|
* Handle HTTP request using round-robin worker selection
|
|
513
359
|
*/
|
|
514
360
|
async handleRequest(request) {
|
|
361
|
+
if (this.#workers.length === 0) {
|
|
362
|
+
logger.debug("No workers available, waiting for worker to be ready");
|
|
363
|
+
await new Promise((resolve, reject) => {
|
|
364
|
+
const waiter = { resolve, reject };
|
|
365
|
+
this.#workerAvailableWaiters.push(waiter);
|
|
366
|
+
const timeoutId = setTimeout(() => {
|
|
367
|
+
const index = this.#workerAvailableWaiters.indexOf(waiter);
|
|
368
|
+
if (index !== -1) {
|
|
369
|
+
this.#workerAvailableWaiters.splice(index, 1);
|
|
370
|
+
reject(new Error("Timeout waiting for worker to become available"));
|
|
371
|
+
}
|
|
372
|
+
}, this.#options.requestTimeout);
|
|
373
|
+
const originalResolve = waiter.resolve;
|
|
374
|
+
const originalReject = waiter.reject;
|
|
375
|
+
waiter.resolve = () => {
|
|
376
|
+
clearTimeout(timeoutId);
|
|
377
|
+
originalResolve();
|
|
378
|
+
};
|
|
379
|
+
waiter.reject = (error) => {
|
|
380
|
+
clearTimeout(timeoutId);
|
|
381
|
+
originalReject(error);
|
|
382
|
+
};
|
|
383
|
+
});
|
|
384
|
+
}
|
|
515
385
|
const worker = this.#workers[this.#currentWorker];
|
|
516
|
-
logger.
|
|
386
|
+
logger.debug("Dispatching to worker", {
|
|
517
387
|
workerIndex: this.#currentWorker + 1,
|
|
518
388
|
totalWorkers: this.#workers.length
|
|
519
389
|
});
|
|
@@ -530,9 +400,6 @@ var ServiceWorkerPool = class {
|
|
|
530
400
|
this.#sendRequest(worker, request, requestID).catch(reject);
|
|
531
401
|
});
|
|
532
402
|
}
|
|
533
|
-
/**
|
|
534
|
-
* Send request to worker (async helper to avoid async promise executor)
|
|
535
|
-
*/
|
|
536
403
|
async #sendRequest(worker, request, requestID) {
|
|
537
404
|
let body = null;
|
|
538
405
|
if (request.body) {
|
|
@@ -555,75 +422,89 @@ var ServiceWorkerPool = class {
|
|
|
555
422
|
}
|
|
556
423
|
}
|
|
557
424
|
/**
|
|
558
|
-
*
|
|
559
|
-
* The entrypoint path contains a content hash for cache busting
|
|
425
|
+
* Gracefully shutdown a worker by closing all resources first
|
|
560
426
|
*/
|
|
561
|
-
async
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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();
|
|
427
|
+
async #gracefulShutdown(worker, timeout = 5e3) {
|
|
428
|
+
return new Promise((resolve) => {
|
|
429
|
+
let resolved = false;
|
|
430
|
+
const onMessage = (event) => {
|
|
431
|
+
const message = event.data || event;
|
|
432
|
+
if (message?.type === "shutdown-complete") {
|
|
433
|
+
if (!resolved) {
|
|
434
|
+
resolved = true;
|
|
435
|
+
worker.removeEventListener("message", onMessage);
|
|
578
436
|
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
437
|
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
});
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
worker.addEventListener("message", onMessage);
|
|
441
|
+
worker.postMessage({ type: "shutdown" });
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
if (!resolved) {
|
|
444
|
+
resolved = true;
|
|
445
|
+
worker.removeEventListener("message", onMessage);
|
|
446
|
+
logger.warn("Worker shutdown timed out, forcing termination");
|
|
447
|
+
resolve();
|
|
448
|
+
}
|
|
449
|
+
}, timeout);
|
|
615
450
|
});
|
|
616
|
-
|
|
617
|
-
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Reload workers with new entrypoint (hot reload)
|
|
454
|
+
*
|
|
455
|
+
* With unified builds, hot reload means:
|
|
456
|
+
* 1. Gracefully shutdown existing workers (close databases, etc.)
|
|
457
|
+
* 2. Terminate workers after resources are closed
|
|
458
|
+
* 3. Create new workers with the new bundle
|
|
459
|
+
*/
|
|
460
|
+
async reloadWorkers(entrypoint) {
|
|
461
|
+
logger.debug("Reloading workers", { entrypoint });
|
|
462
|
+
this.#appEntrypoint = entrypoint;
|
|
463
|
+
const shutdownPromises = this.#workers.map(
|
|
464
|
+
(worker) => this.#gracefulShutdown(worker)
|
|
465
|
+
);
|
|
466
|
+
await Promise.allSettled(shutdownPromises);
|
|
467
|
+
const terminatePromises = this.#workers.map((worker) => worker.terminate());
|
|
468
|
+
await Promise.allSettled(terminatePromises);
|
|
469
|
+
this.#workers = [];
|
|
470
|
+
this.#currentWorker = 0;
|
|
471
|
+
try {
|
|
472
|
+
const createPromises = [];
|
|
473
|
+
for (let i = 0; i < this.#options.workerCount; i++) {
|
|
474
|
+
createPromises.push(this.#createWorker(entrypoint));
|
|
475
|
+
}
|
|
476
|
+
await Promise.all(createPromises);
|
|
477
|
+
logger.debug("All workers reloaded", { entrypoint });
|
|
478
|
+
} catch (error) {
|
|
479
|
+
const waiters = this.#workerAvailableWaiters;
|
|
480
|
+
this.#workerAvailableWaiters = [];
|
|
481
|
+
const reloadError = error instanceof Error ? error : new Error("Worker creation failed during reload");
|
|
482
|
+
for (const waiter of waiters) {
|
|
483
|
+
waiter.reject(reloadError);
|
|
484
|
+
}
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
618
487
|
}
|
|
619
488
|
/**
|
|
620
489
|
* Graceful shutdown of all workers
|
|
621
490
|
*/
|
|
622
491
|
async terminate() {
|
|
492
|
+
const shutdownPromises = this.#workers.map(
|
|
493
|
+
(worker) => this.#gracefulShutdown(worker)
|
|
494
|
+
);
|
|
495
|
+
await Promise.allSettled(shutdownPromises);
|
|
623
496
|
const terminatePromises = this.#workers.map((worker) => worker.terminate());
|
|
624
497
|
await Promise.allSettled(terminatePromises);
|
|
625
498
|
this.#workers = [];
|
|
499
|
+
this.#currentWorker = 0;
|
|
626
500
|
this.#pendingRequests.clear();
|
|
501
|
+
this.#pendingWorkerReady.clear();
|
|
502
|
+
const waiters = this.#workerAvailableWaiters;
|
|
503
|
+
this.#workerAvailableWaiters = [];
|
|
504
|
+
const terminateError = new Error("Worker pool terminated");
|
|
505
|
+
for (const waiter of waiters) {
|
|
506
|
+
waiter.reject(terminateError);
|
|
507
|
+
}
|
|
627
508
|
}
|
|
628
509
|
/**
|
|
629
510
|
* Get the number of active workers
|
|
@@ -640,15 +521,19 @@ var ServiceWorkerPool = class {
|
|
|
640
521
|
};
|
|
641
522
|
export {
|
|
642
523
|
BasePlatform,
|
|
524
|
+
ConfigValidationError,
|
|
525
|
+
CustomDatabaseStorage,
|
|
643
526
|
CustomLoggerStorage,
|
|
644
527
|
ServiceWorkerPool,
|
|
645
|
-
|
|
528
|
+
createDatabaseFactory,
|
|
646
529
|
createPlatform,
|
|
647
530
|
detectDeploymentPlatform,
|
|
648
531
|
detectDevelopmentPlatform,
|
|
649
532
|
detectRuntime,
|
|
650
533
|
getPlatform,
|
|
651
534
|
getPlatformAsync,
|
|
535
|
+
mergeConfigWithDefaults,
|
|
652
536
|
platformRegistry,
|
|
653
|
-
resolvePlatform
|
|
537
|
+
resolvePlatform,
|
|
538
|
+
validateConfig
|
|
654
539
|
};
|