@b9g/platform 0.1.12 → 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/src/index.js CHANGED
@@ -1,21 +1,18 @@
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";
13
4
  import {
14
5
  ServiceWorkerGlobals,
15
6
  ShovelServiceWorkerRegistration,
7
+ ShovelFetchEvent,
16
8
  CustomLoggerStorage
17
9
  } from "./runtime.js";
18
- var logger = getLogger(["platform"]);
10
+ import { validateConfig, ConfigValidationError } from "./config.js";
11
+ import {
12
+ CustomDatabaseStorage,
13
+ createDatabaseFactory
14
+ } from "./runtime.js";
15
+ var logger = getLogger(["shovel", "platform"]);
19
16
  function detectRuntime() {
20
17
  if (typeof Bun !== "undefined" || process.versions?.bun) {
21
18
  return "bun";
@@ -66,27 +63,20 @@ function resolvePlatform(options) {
66
63
  async function createPlatform(platformName, options = {}) {
67
64
  switch (platformName) {
68
65
  case "node": {
69
- const modulePath = import.meta.resolve("@b9g/platform-node");
70
- const NodePlatform = await import(modulePath).then((m) => m.default);
66
+ const { default: NodePlatform } = await import("@b9g/platform-node");
71
67
  return new NodePlatform(options);
72
68
  }
73
69
  case "bun": {
74
- const modulePath = import.meta.resolve("@b9g/platform-bun");
75
- const BunPlatform = await import(modulePath).then((m) => m.default);
70
+ const { default: BunPlatform } = await import("@b9g/platform-bun");
76
71
  return new BunPlatform(options);
77
72
  }
78
- case "cloudflare":
79
- case "cloudflare-workers":
80
- case "cf": {
81
- const modulePath = import.meta.resolve("@b9g/platform-cloudflare");
82
- const CloudflarePlatform = await import(modulePath).then(
83
- (m) => m.default
84
- );
73
+ case "cloudflare": {
74
+ const { default: CloudflarePlatform } = await import("@b9g/platform-cloudflare");
85
75
  return new CloudflarePlatform(options);
86
76
  }
87
77
  default:
88
78
  throw new Error(
89
- `Unknown platform: ${platformName}. Available platforms: node, bun, cloudflare`
79
+ `Unknown platform: ${platformName}. Valid platforms: node, bun, cloudflare`
90
80
  );
91
81
  }
92
82
  }
@@ -95,15 +85,6 @@ var BasePlatform = class {
95
85
  constructor(config = {}) {
96
86
  this.config = config;
97
87
  }
98
- /**
99
- * Create cache storage
100
- * Returns empty CacheStorage - applications create caches on-demand via caches.open()
101
- */
102
- async createCaches() {
103
- return new CustomCacheStorage(
104
- (name) => new MemoryCache(name)
105
- );
106
- }
107
88
  };
108
89
  var DefaultPlatformRegistry = class {
109
90
  #platforms;
@@ -173,16 +154,17 @@ var SingleThreadedRuntime = class {
173
154
  registration: this.#registration,
174
155
  caches: options.caches,
175
156
  directories: options.directories,
157
+ databases: options.databases,
176
158
  loggers: options.loggers
177
159
  });
178
- logger.info("SingleThreadedRuntime created");
160
+ logger.debug("SingleThreadedRuntime created");
179
161
  }
180
162
  /**
181
163
  * Initialize the runtime (install ServiceWorker globals)
182
164
  */
183
165
  async init() {
184
166
  this.#scope.install();
185
- logger.info("SingleThreadedRuntime initialized - globals installed");
167
+ logger.debug("SingleThreadedRuntime initialized - globals installed");
186
168
  }
187
169
  /**
188
170
  * Load (or reload) a ServiceWorker entrypoint
@@ -191,13 +173,13 @@ var SingleThreadedRuntime = class {
191
173
  async load(entrypoint) {
192
174
  const isReload = this.#entrypoint !== void 0;
193
175
  if (isReload) {
194
- logger.info("Reloading ServiceWorker", {
176
+ logger.debug("Reloading ServiceWorker", {
195
177
  oldEntrypoint: this.#entrypoint,
196
178
  newEntrypoint: entrypoint
197
179
  });
198
180
  this.#registration._serviceWorker._setState("parsed");
199
181
  } else {
200
- logger.info("Loading ServiceWorker entrypoint", { entrypoint });
182
+ logger.debug("Loading ServiceWorker entrypoint", { entrypoint });
201
183
  }
202
184
  this.#entrypoint = entrypoint;
203
185
  this.#ready = false;
@@ -205,7 +187,7 @@ var SingleThreadedRuntime = class {
205
187
  await this.#registration.install();
206
188
  await this.#registration.activate();
207
189
  this.#ready = true;
208
- logger.info("ServiceWorker loaded and activated", { entrypoint });
190
+ logger.debug("ServiceWorker loaded and activated", { entrypoint });
209
191
  }
210
192
  /**
211
193
  * Handle an HTTP request
@@ -217,14 +199,15 @@ var SingleThreadedRuntime = class {
217
199
  "SingleThreadedRuntime not ready - ServiceWorker not loaded"
218
200
  );
219
201
  }
220
- return this.#registration.handleRequest(request);
202
+ const event = new ShovelFetchEvent(request);
203
+ return this.#registration.handleRequest(event);
221
204
  }
222
205
  /**
223
206
  * Graceful shutdown
224
207
  */
225
208
  async terminate() {
226
209
  this.#ready = false;
227
- logger.info("SingleThreadedRuntime terminated");
210
+ logger.debug("SingleThreadedRuntime terminated");
228
211
  }
229
212
  /**
230
213
  * Get the number of workers (always 1 for single-threaded)
@@ -239,46 +222,6 @@ var SingleThreadedRuntime = class {
239
222
  return this.#ready;
240
223
  }
241
224
  };
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
225
  async function createWebWorker(workerScript) {
283
226
  if (typeof Worker !== "undefined") {
284
227
  return new Worker(workerScript, { type: "module" });
@@ -327,22 +270,21 @@ var ServiceWorkerPool = class {
327
270
  #currentWorker;
328
271
  #requestID;
329
272
  #pendingRequests;
330
- #pendingWorkerInit;
273
+ #pendingWorkerReady;
331
274
  #options;
332
275
  #appEntrypoint;
333
276
  #cacheStorage;
334
- // CustomCacheStorage for cache coordination
335
- #config;
336
- // ShovelConfig from config.ts
337
- constructor(options = {}, appEntrypoint, cacheStorage, config) {
277
+ // Waiters for when workers become available (used during reload)
278
+ #workerAvailableWaiters;
279
+ constructor(options = {}, appEntrypoint, cacheStorage) {
338
280
  this.#workers = [];
339
281
  this.#currentWorker = 0;
340
282
  this.#requestID = 0;
341
283
  this.#pendingRequests = /* @__PURE__ */ new Map();
342
- this.#pendingWorkerInit = /* @__PURE__ */ new Map();
284
+ this.#pendingWorkerReady = /* @__PURE__ */ new Map();
285
+ this.#workerAvailableWaiters = [];
343
286
  this.#appEntrypoint = appEntrypoint;
344
287
  this.#cacheStorage = cacheStorage;
345
- this.#config = config || {};
346
288
  this.#options = {
347
289
  workerCount: 1,
348
290
  requestTimeout: 3e4,
@@ -353,27 +295,35 @@ var ServiceWorkerPool = class {
353
295
  * Initialize workers (must be called after construction)
354
296
  */
355
297
  async init() {
356
- await this.#initWorkers();
357
- }
358
- async #initWorkers() {
298
+ const promises = [];
359
299
  for (let i = 0; i < this.#options.workerCount; i++) {
360
- await this.#createWorker();
300
+ promises.push(this.#createWorker(this.#appEntrypoint));
361
301
  }
302
+ await Promise.all(promises);
362
303
  }
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"));
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
+ );
372
318
  }, 3e4);
373
- this.#pendingWorkerInit.set(worker, {
374
- workerReady: () => {
375
- clearTimeout(workerReadyTimeoutId);
319
+ this.#pendingWorkerReady.set(worker, {
320
+ resolve: () => {
321
+ clearTimeout(timeoutId);
376
322
  resolve();
323
+ },
324
+ reject: (error) => {
325
+ clearTimeout(timeoutId);
326
+ reject(error);
377
327
  }
378
328
  });
379
329
  });
@@ -382,74 +332,49 @@ var ServiceWorkerPool = class {
382
332
  });
383
333
  worker.addEventListener("error", (event) => {
384
334
  const errorMessage = event.message || event.error?.message || "Unknown worker error";
385
- const error = new Error(`Worker failed to start: ${errorMessage}`);
335
+ const error = new Error(`Worker error: ${errorMessage}`);
386
336
  logger.error("Worker error: {error}", {
387
337
  error: event.error || errorMessage,
388
338
  filename: event.filename,
389
339
  lineno: event.lineno,
390
340
  colno: event.colno
391
341
  });
392
- clearTimeout(workerReadyTimeoutId);
393
- rejectWorkerReady(error);
342
+ const pending = this.#pendingWorkerReady.get(worker);
343
+ if (pending) {
344
+ this.#pendingWorkerReady.delete(worker);
345
+ pending.reject(error);
346
+ }
394
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));
395
352
  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
- );
353
+ logger.debug("Worker ready", { entrypoint });
354
+ const waiters = this.#workerAvailableWaiters;
355
+ this.#workerAvailableWaiters = [];
356
+ for (const waiter of waiters) {
357
+ waiter.resolve();
416
358
  }
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
359
  return worker;
430
360
  }
431
361
  #handleWorkerMessage(worker, message) {
432
362
  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
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
+ }
441
372
  case "response":
442
373
  this.#handleResponse(message);
443
374
  break;
444
375
  case "error":
445
376
  this.#handleError(message);
446
377
  break;
447
- case "ready":
448
- case "worker-ready":
449
- this.#handleReady(message);
450
- break;
451
- case "initialized":
452
- break;
453
378
  default:
454
379
  if (message.type?.startsWith("cache:")) {
455
380
  logger.debug("Cache message received", { type: message.type });
@@ -497,23 +422,38 @@ var ServiceWorkerPool = class {
497
422
  pending.reject(new Error(message.error));
498
423
  this.#pendingRequests.delete(message.requestID);
499
424
  }
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
425
  }
510
426
  }
511
427
  /**
512
428
  * Handle HTTP request using round-robin worker selection
513
429
  */
514
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
+ }
515
455
  const worker = this.#workers[this.#currentWorker];
516
- logger.info("Dispatching to worker", {
456
+ logger.debug("Dispatching to worker", {
517
457
  workerIndex: this.#currentWorker + 1,
518
458
  totalWorkers: this.#workers.length
519
459
  });
@@ -530,9 +470,6 @@ var ServiceWorkerPool = class {
530
470
  this.#sendRequest(worker, request, requestID).catch(reject);
531
471
  });
532
472
  }
533
- /**
534
- * Send request to worker (async helper to avoid async promise executor)
535
- */
536
473
  async #sendRequest(worker, request, requestID) {
537
474
  let body = null;
538
475
  if (request.body) {
@@ -555,75 +492,89 @@ var ServiceWorkerPool = class {
555
492
  }
556
493
  }
557
494
  /**
558
- * Reload ServiceWorker with new entrypoint (hot reload)
559
- * The entrypoint path contains a content hash for cache busting
495
+ * Gracefully shutdown a worker by closing all resources first
560
496
  */
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();
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);
578
506
  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
507
  }
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
- });
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);
615
520
  });
616
- await Promise.all(loadPromises);
617
- logger.info("All workers reloaded", { entrypoint });
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
+ }
618
557
  }
619
558
  /**
620
559
  * Graceful shutdown of all workers
621
560
  */
622
561
  async terminate() {
562
+ const shutdownPromises = this.#workers.map(
563
+ (worker) => this.#gracefulShutdown(worker)
564
+ );
565
+ await Promise.allSettled(shutdownPromises);
623
566
  const terminatePromises = this.#workers.map((worker) => worker.terminate());
624
567
  await Promise.allSettled(terminatePromises);
625
568
  this.#workers = [];
569
+ this.#currentWorker = 0;
626
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
+ }
627
578
  }
628
579
  /**
629
580
  * Get the number of active workers
@@ -640,9 +591,12 @@ var ServiceWorkerPool = class {
640
591
  };
641
592
  export {
642
593
  BasePlatform,
594
+ ConfigValidationError,
595
+ CustomDatabaseStorage,
643
596
  CustomLoggerStorage,
644
597
  ServiceWorkerPool,
645
598
  SingleThreadedRuntime,
599
+ createDatabaseFactory,
646
600
  createPlatform,
647
601
  detectDeploymentPlatform,
648
602
  detectDevelopmentPlatform,
@@ -650,5 +604,6 @@ export {
650
604
  getPlatform,
651
605
  getPlatformAsync,
652
606
  platformRegistry,
653
- resolvePlatform
607
+ resolvePlatform,
608
+ validateConfig
654
609
  };