@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/src/runtime.js CHANGED
@@ -1,15 +1,17 @@
1
1
  /// <reference types="./runtime.d.ts" />
2
- import "../chunk-P57PW2II.js";
3
-
4
2
  // src/runtime.ts
5
3
  import { AsyncContext } from "@b9g/async-context";
4
+ import { getLogger, getConsoleSink } from "@logtape/logtape";
5
+ import { CustomDirectoryStorage } from "@b9g/filesystem";
6
+ import { CustomCacheStorage } from "@b9g/cache";
7
+ import { handleCacheResponse, PostMessageCache } from "@b9g/cache/postmessage";
6
8
  import {
7
9
  configure
8
10
  } from "@logtape/logtape";
11
+ import { Database } from "@b9g/zen";
9
12
  function parseCookieHeader(cookieHeader) {
10
13
  const cookies = /* @__PURE__ */ new Map();
11
- if (!cookieHeader)
12
- return cookies;
14
+ if (!cookieHeader) return cookies;
13
15
  const pairs = cookieHeader.split(/;\s*/);
14
16
  for (const pair of pairs) {
15
17
  const [name, ...valueParts] = pair.split("=");
@@ -114,8 +116,7 @@ var RequestCookieStore = class extends EventTarget {
114
116
  }
115
117
  if (this.#changes.has(name)) {
116
118
  const change = this.#changes.get(name);
117
- if (change === null || change === void 0)
118
- return null;
119
+ if (change === null || change === void 0) return null;
119
120
  return {
120
121
  name: change.name,
121
122
  value: change.value,
@@ -136,8 +137,7 @@ var RequestCookieStore = class extends EventTarget {
136
137
  ...this.#changes.keys()
137
138
  ]);
138
139
  for (const cookieName of allNames) {
139
- if (name && cookieName !== name)
140
- continue;
140
+ if (name && cookieName !== name) continue;
141
141
  if (this.#changes.has(cookieName) && this.#changes.get(cookieName) === null) {
142
142
  continue;
143
143
  }
@@ -216,10 +216,122 @@ var CustomLoggerStorage = class {
216
216
  constructor(factory) {
217
217
  this.#factory = factory;
218
218
  }
219
- open(...categories) {
220
- return this.#factory(...categories);
219
+ get(categories) {
220
+ return this.#factory(categories);
221
+ }
222
+ };
223
+ var CustomDatabaseStorage = class {
224
+ #factory;
225
+ #databases;
226
+ #closers;
227
+ #pending;
228
+ constructor(factory) {
229
+ this.#factory = factory;
230
+ this.#databases = /* @__PURE__ */ new Map();
231
+ this.#closers = /* @__PURE__ */ new Map();
232
+ this.#pending = /* @__PURE__ */ new Map();
233
+ }
234
+ async open(name, version, onUpgrade) {
235
+ const existing = this.#databases.get(name);
236
+ if (existing) {
237
+ return existing;
238
+ }
239
+ const pending = this.#pending.get(name);
240
+ if (pending) {
241
+ return pending;
242
+ }
243
+ const promise = (async () => {
244
+ const { db, close } = await this.#factory(name);
245
+ if (onUpgrade) {
246
+ db.addEventListener("upgradeneeded", (e) => {
247
+ const event = e;
248
+ onUpgrade({
249
+ db,
250
+ oldVersion: event.oldVersion,
251
+ newVersion: event.newVersion,
252
+ waitUntil: (p) => event.waitUntil(p)
253
+ });
254
+ });
255
+ }
256
+ try {
257
+ await db.open(version);
258
+ } catch (err) {
259
+ await close();
260
+ throw err;
261
+ }
262
+ this.#databases.set(name, db);
263
+ this.#closers.set(name, close);
264
+ return db;
265
+ })().finally(() => {
266
+ this.#pending.delete(name);
267
+ });
268
+ this.#pending.set(name, promise);
269
+ return promise;
270
+ }
271
+ get(name) {
272
+ const db = this.#databases.get(name);
273
+ if (!db) {
274
+ throw new Error(
275
+ `Database "${name}" has not been opened. Call self.databases.open("${name}", version) in your activate handler first.`
276
+ );
277
+ }
278
+ return db;
279
+ }
280
+ async close(name) {
281
+ const pending = this.#pending.get(name);
282
+ if (pending) {
283
+ try {
284
+ await pending;
285
+ } catch (_err) {
286
+ return;
287
+ }
288
+ }
289
+ const closer = this.#closers.get(name);
290
+ if (closer) {
291
+ await closer();
292
+ this.#databases.delete(name);
293
+ this.#closers.delete(name);
294
+ }
295
+ }
296
+ async closeAll() {
297
+ if (this.#pending.size > 0) {
298
+ await Promise.allSettled(this.#pending.values());
299
+ }
300
+ const promises = Array.from(this.#databases.keys()).map(
301
+ (name) => this.close(name)
302
+ );
303
+ await Promise.allSettled(promises);
221
304
  }
222
305
  };
306
+ function createDatabaseFactory(configs) {
307
+ return async (name) => {
308
+ const config = configs[name];
309
+ if (!config) {
310
+ throw new Error(
311
+ `Database "${name}" is not configured. Available databases: ${Object.keys(configs).join(", ") || "(none)"}`
312
+ );
313
+ }
314
+ const { impl, url, ...driverOptions } = config;
315
+ if (!impl) {
316
+ throw new Error(
317
+ `Database "${name}" has no impl. Ensure the database module is configured in shovel.json.`
318
+ );
319
+ }
320
+ if (!url) {
321
+ throw new Error(
322
+ `Database "${name}" has no url. Ensure the database URL is configured.`
323
+ );
324
+ }
325
+ const driver = new impl(url, driverOptions);
326
+ const db = new Database(driver);
327
+ return {
328
+ db,
329
+ close: async () => {
330
+ await driver.close();
331
+ }
332
+ };
333
+ };
334
+ }
223
335
  var SERVICE_WORKER_EVENTS = ["fetch", "install", "activate"];
224
336
  function isServiceWorkerEvent(type) {
225
337
  return SERVICE_WORKER_EVENTS.includes(type);
@@ -235,6 +347,7 @@ var PATCHED_KEYS = [
235
347
  "fetch",
236
348
  "caches",
237
349
  "directories",
350
+ "databases",
238
351
  "loggers",
239
352
  "registration",
240
353
  "clients",
@@ -254,9 +367,9 @@ function promiseWithTimeout(promise, timeoutMs, errorMessage) {
254
367
  )
255
368
  ]);
256
369
  }
257
- var kEndDispatchPhase = Symbol.for("shovel.endDispatchPhase");
258
- var kCanExtend = Symbol.for("shovel.canExtend");
259
- var ExtendableEvent = class extends Event {
370
+ var kEndDispatchPhase = /* @__PURE__ */ Symbol.for("shovel.endDispatchPhase");
371
+ var kCanExtend = /* @__PURE__ */ Symbol.for("shovel.canExtend");
372
+ var ShovelExtendableEvent = class extends Event {
260
373
  #promises;
261
374
  #dispatchPhase;
262
375
  #pendingCount;
@@ -293,17 +406,33 @@ var ExtendableEvent = class extends Event {
293
406
  return this.#dispatchPhase || this.#pendingCount > 0;
294
407
  }
295
408
  };
296
- var FetchEvent = class extends ExtendableEvent {
409
+ var ShovelFetchEvent = class extends ShovelExtendableEvent {
297
410
  request;
298
411
  cookieStore;
412
+ clientId;
413
+ handled;
414
+ preloadResponse;
415
+ resultingClientId;
299
416
  #responsePromise;
300
417
  #responded;
301
- constructor(request, eventInitDict) {
302
- super("fetch", eventInitDict);
418
+ #platformWaitUntil;
419
+ constructor(request, options) {
420
+ super("fetch", options);
303
421
  this.request = request;
304
422
  this.cookieStore = new RequestCookieStore(request);
423
+ this.clientId = "";
424
+ this.handled = Promise.resolve(void 0);
425
+ this.preloadResponse = Promise.resolve(void 0);
426
+ this.resultingClientId = "";
305
427
  this.#responsePromise = null;
306
428
  this.#responded = false;
429
+ this.#platformWaitUntil = options?.platformWaitUntil;
430
+ }
431
+ waitUntil(promise) {
432
+ if (this.#platformWaitUntil) {
433
+ this.#platformWaitUntil(promise);
434
+ }
435
+ super.waitUntil(promise);
307
436
  }
308
437
  respondWith(response) {
309
438
  if (this.#responded) {
@@ -325,13 +454,17 @@ var FetchEvent = class extends ExtendableEvent {
325
454
  hasResponded() {
326
455
  return this.#responded;
327
456
  }
457
+ /** The URL of the request (convenience property) */
458
+ get url() {
459
+ return this.request.url;
460
+ }
328
461
  };
329
- var InstallEvent = class extends ExtendableEvent {
462
+ var ShovelInstallEvent = class extends ShovelExtendableEvent {
330
463
  constructor(eventInitDict) {
331
464
  super("install", eventInitDict);
332
465
  }
333
466
  };
334
- var ActivateEvent = class extends ExtendableEvent {
467
+ var ShovelActivateEvent = class extends ShovelExtendableEvent {
335
468
  constructor(eventInitDict) {
336
469
  super("activate", eventInitDict);
337
470
  }
@@ -383,7 +516,7 @@ var ShovelClients = class {
383
516
  return null;
384
517
  }
385
518
  };
386
- var ExtendableMessageEvent = class extends ExtendableEvent {
519
+ var ExtendableMessageEvent = class extends ShovelExtendableEvent {
387
520
  data;
388
521
  origin;
389
522
  lastEventId;
@@ -435,12 +568,16 @@ var ShovelNavigationPreloadManager = class {
435
568
  async setHeaderValue(_value) {
436
569
  }
437
570
  };
571
+ var kServiceWorker = /* @__PURE__ */ Symbol("serviceWorker");
572
+ var kDispatchInstall = /* @__PURE__ */ Symbol("dispatchInstall");
573
+ var kDispatchActivate = /* @__PURE__ */ Symbol("dispatchActivate");
574
+ var kHandleRequest = /* @__PURE__ */ Symbol("handleRequest");
438
575
  var ShovelServiceWorkerRegistration = class extends EventTarget {
439
576
  scope;
440
577
  updateViaCache;
441
578
  navigationPreload;
442
- // ServiceWorker instances representing different lifecycle states
443
- _serviceWorker;
579
+ // Internal ServiceWorker instance (accessed via symbol for lifecycle management)
580
+ [kServiceWorker];
444
581
  // Web API properties (not supported in server context, but required by interface)
445
582
  cookies;
446
583
  pushManager;
@@ -450,20 +587,20 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
450
587
  this.scope = scope;
451
588
  this.updateViaCache = "imports";
452
589
  this.navigationPreload = new ShovelNavigationPreloadManager();
453
- this._serviceWorker = new ShovelServiceWorker(scriptURL, "parsed");
590
+ this[kServiceWorker] = new ShovelServiceWorker(scriptURL, "parsed");
454
591
  this.cookies = null;
455
592
  this.pushManager = null;
456
593
  this.onupdatefound = null;
457
594
  }
458
595
  // Standard ServiceWorkerRegistration properties
459
596
  get active() {
460
- return this._serviceWorker.state === "activated" ? this._serviceWorker : null;
597
+ return this[kServiceWorker].state === "activated" ? this[kServiceWorker] : null;
461
598
  }
462
599
  get installing() {
463
- return this._serviceWorker.state === "installing" ? this._serviceWorker : null;
600
+ return this[kServiceWorker].state === "installing" ? this[kServiceWorker] : null;
464
601
  }
465
602
  get waiting() {
466
- return this._serviceWorker.state === "installed" ? this._serviceWorker : null;
603
+ return this[kServiceWorker].state === "installed" ? this[kServiceWorker] : null;
467
604
  }
468
605
  // Standard ServiceWorkerRegistration methods
469
606
  async getNotifications(_options) {
@@ -480,16 +617,16 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
480
617
  async update() {
481
618
  return this;
482
619
  }
483
- // Shovel runtime extensions (non-standard but needed for platforms)
620
+ // Internal lifecycle methods (accessed via symbols by runLifecycle)
484
621
  /**
485
- * Install the ServiceWorker (Shovel extension)
622
+ * Dispatch the install lifecycle event
623
+ * @internal Use runLifecycle() instead of calling this directly
486
624
  */
487
- async install() {
488
- if (this._serviceWorker.state !== "parsed")
489
- return;
490
- this._serviceWorker._setState("installing");
625
+ async [kDispatchInstall]() {
626
+ if (this[kServiceWorker].state !== "parsed") return;
627
+ this[kServiceWorker]._setState("installing");
491
628
  return new Promise((resolve, reject) => {
492
- const event = new InstallEvent();
629
+ const event = new ShovelInstallEvent();
493
630
  process.nextTick(() => {
494
631
  try {
495
632
  this.dispatchEvent(event);
@@ -500,7 +637,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
500
637
  }
501
638
  const promises = event.getPromises();
502
639
  if (promises.length === 0) {
503
- this._serviceWorker._setState("installed");
640
+ this[kServiceWorker]._setState("installed");
504
641
  resolve();
505
642
  } else {
506
643
  promiseWithTimeout(
@@ -508,7 +645,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
508
645
  3e4,
509
646
  "ServiceWorker install event timed out after 30s - waitUntil promises did not resolve"
510
647
  ).then(() => {
511
- this._serviceWorker._setState("installed");
648
+ this[kServiceWorker]._setState("installed");
512
649
  resolve();
513
650
  }).catch(reject);
514
651
  }
@@ -516,15 +653,16 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
516
653
  });
517
654
  }
518
655
  /**
519
- * Activate the ServiceWorker (Shovel extension)
656
+ * Dispatch the activate lifecycle event
657
+ * @internal Use runLifecycle() instead of calling this directly
520
658
  */
521
- async activate() {
522
- if (this._serviceWorker.state !== "installed") {
659
+ async [kDispatchActivate]() {
660
+ if (this[kServiceWorker].state !== "installed") {
523
661
  throw new Error("ServiceWorker must be installed before activation");
524
662
  }
525
- this._serviceWorker._setState("activating");
663
+ this[kServiceWorker]._setState("activating");
526
664
  return new Promise((resolve, reject) => {
527
- const event = new ActivateEvent();
665
+ const event = new ShovelActivateEvent();
528
666
  process.nextTick(() => {
529
667
  try {
530
668
  this.dispatchEvent(event);
@@ -535,7 +673,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
535
673
  }
536
674
  const promises = event.getPromises();
537
675
  if (promises.length === 0) {
538
- this._serviceWorker._setState("activated");
676
+ this[kServiceWorker]._setState("activated");
539
677
  resolve();
540
678
  } else {
541
679
  promiseWithTimeout(
@@ -543,7 +681,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
543
681
  3e4,
544
682
  "ServiceWorker activate event timed out after 30s - waitUntil promises did not resolve"
545
683
  ).then(() => {
546
- this._serviceWorker._setState("activated");
684
+ this[kServiceWorker]._setState("activated");
547
685
  resolve();
548
686
  }).catch(reject);
549
687
  }
@@ -551,13 +689,18 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
551
689
  });
552
690
  }
553
691
  /**
554
- * Handle a fetch request (Shovel extension)
692
+ * Handle a fetch request
693
+ * @internal Use the kHandleRequest symbol to access this method
694
+ *
695
+ * Platforms create a ShovelFetchEvent (or subclass) with platform-specific
696
+ * properties and hooks, then pass it to this method for dispatching.
697
+ *
698
+ * @param event - The fetch event to handle (created by platform adapter)
555
699
  */
556
- async handleRequest(request) {
557
- if (this._serviceWorker.state !== "activated") {
700
+ async [kHandleRequest](event) {
701
+ if (this[kServiceWorker].state !== "activated") {
558
702
  throw new Error("ServiceWorker not activated");
559
703
  }
560
- const event = new FetchEvent(request);
561
704
  return cookieStoreStorage.run(event.cookieStore, async () => {
562
705
  this.dispatchEvent(event);
563
706
  event[kEndDispatchPhase]();
@@ -567,12 +710,6 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
567
710
  );
568
711
  }
569
712
  const response = await event.getResponse();
570
- const promises = event.getPromises();
571
- if (promises.length > 0) {
572
- Promise.allSettled(promises).catch(
573
- (err) => self.loggers.open("platform").error`waitUntil error: ${err}`
574
- );
575
- }
576
713
  if (event.cookieStore.hasChanges()) {
577
714
  const setCookieHeaders = event.cookieStore.getSetCookieHeaders();
578
715
  const headers = new Headers(response.headers);
@@ -592,10 +729,20 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
592
729
  * Check if ready to handle requests (Shovel extension)
593
730
  */
594
731
  get ready() {
595
- return this._serviceWorker.state === "activated";
732
+ return this[kServiceWorker].state === "activated";
596
733
  }
597
734
  // Events: updatefound (standard), plus Shovel lifecycle events
598
735
  };
736
+ async function runLifecycle(registration, stage = "activate") {
737
+ await registration[kDispatchInstall]();
738
+ if (stage === "activate") {
739
+ await registration[kDispatchActivate]();
740
+ }
741
+ }
742
+ async function dispatchRequest(registration, requestOrEvent) {
743
+ const event = requestOrEvent instanceof ShovelFetchEvent ? requestOrEvent : new ShovelFetchEvent(requestOrEvent);
744
+ return registration[kHandleRequest](event);
745
+ }
599
746
  var ShovelServiceWorkerContainer = class extends EventTarget {
600
747
  #registrations;
601
748
  controller;
@@ -635,8 +782,8 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
635
782
  const scope = this.#normalizeScope(options?.scope || "/");
636
783
  let registration = this.#registrations.get(scope);
637
784
  if (registration) {
638
- registration._serviceWorker.scriptURL = url;
639
- registration._serviceWorker._setState("parsed");
785
+ registration[kServiceWorker].scriptURL = url;
786
+ registration[kServiceWorker]._setState("parsed");
640
787
  } else {
641
788
  registration = new ShovelServiceWorkerRegistration(scope, url);
642
789
  this.#registrations.set(scope, registration);
@@ -656,29 +803,13 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
656
803
  }
657
804
  return false;
658
805
  }
659
- /**
660
- * Route a request to the appropriate registration based on scope matching
661
- */
662
- async handleRequest(request) {
663
- const url = new URL(request.url);
664
- const pathname = url.pathname;
665
- const matchingScope = this.#findMatchingScope(pathname);
666
- if (matchingScope) {
667
- const registration = this.#registrations.get(matchingScope);
668
- if (registration && registration.ready) {
669
- return await registration.handleRequest(request);
670
- }
671
- }
672
- return null;
673
- }
674
806
  /**
675
807
  * Install and activate all registrations
676
808
  */
677
809
  async installAll() {
678
810
  const installations = Array.from(this.#registrations.values()).map(
679
811
  async (registration) => {
680
- await registration.install();
681
- await registration.activate();
812
+ await runLifecycle(registration);
682
813
  }
683
814
  );
684
815
  await promiseWithTimeout(
@@ -707,19 +838,6 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
707
838
  }
708
839
  return scope;
709
840
  }
710
- /**
711
- * Find the most specific scope that matches a pathname
712
- */
713
- #findMatchingScope(pathname) {
714
- const scopes = Array.from(this.#registrations.keys());
715
- scopes.sort((a, b) => b.length - a.length);
716
- for (const scope of scopes) {
717
- if (pathname.startsWith(scope === "/" ? "/" : scope)) {
718
- return scope;
719
- }
720
- }
721
- return null;
722
- }
723
841
  // Events: controllerchange, message, messageerror, updatefound
724
842
  };
725
843
  var Notification = class extends EventTarget {
@@ -775,7 +893,7 @@ var Notification = class extends EventTarget {
775
893
  // Events: click, close, error, show
776
894
  };
777
895
  Notification.permission = "denied";
778
- var NotificationEvent = class extends ExtendableEvent {
896
+ var NotificationEvent = class extends ShovelExtendableEvent {
779
897
  action;
780
898
  notification;
781
899
  reply;
@@ -786,7 +904,7 @@ var NotificationEvent = class extends ExtendableEvent {
786
904
  this.reply = eventInitDict.reply ?? null;
787
905
  }
788
906
  };
789
- var PushEvent = class extends ExtendableEvent {
907
+ var PushEvent = class extends ShovelExtendableEvent {
790
908
  data;
791
909
  constructor(type, eventInitDict) {
792
910
  super(type, eventInitDict);
@@ -819,7 +937,7 @@ var ShovelPushMessageData = class {
819
937
  return new TextDecoder().decode(this._data);
820
938
  }
821
939
  };
822
- var SyncEvent = class extends ExtendableEvent {
940
+ var SyncEvent = class extends ShovelExtendableEvent {
823
941
  tag;
824
942
  lastChance;
825
943
  constructor(type, eventInitDict) {
@@ -842,6 +960,7 @@ var ServiceWorkerGlobals = class {
842
960
  // Storage APIs
843
961
  caches;
844
962
  directories;
963
+ databases;
845
964
  loggers;
846
965
  // Clients API
847
966
  // Our custom Clients implementation provides core functionality compatible with the Web API
@@ -903,14 +1022,17 @@ var ServiceWorkerGlobals = class {
903
1022
  }
904
1023
  const request = new Request(new URL(urlString, "http://localhost"), init);
905
1024
  return fetchDepthStorage.run(currentDepth + 1, () => {
906
- return this.registration.handleRequest(request);
1025
+ return dispatchRequest(
1026
+ this.registration,
1027
+ request
1028
+ );
907
1029
  });
908
1030
  }
909
1031
  queueMicrotask(callback) {
910
1032
  globalThis.queueMicrotask(callback);
911
1033
  }
912
1034
  reportError(e) {
913
- self.loggers.open("platform").error`reportError: ${e}`;
1035
+ getLogger(["shovel", "platform"]).error`reportError: ${e}`;
914
1036
  }
915
1037
  setInterval(handler, timeout, ...args) {
916
1038
  return globalThis.setInterval(handler, timeout, ...args);
@@ -951,6 +1073,7 @@ var ServiceWorkerGlobals = class {
951
1073
  this.registration = options.registration;
952
1074
  this.caches = options.caches;
953
1075
  this.directories = options.directories;
1076
+ this.databases = options.databases;
954
1077
  this.loggers = options.loggers;
955
1078
  this.#isDevelopment = options.isDevelopment ?? false;
956
1079
  this.clients = this.#createClientsAPI();
@@ -987,9 +1110,11 @@ var ServiceWorkerGlobals = class {
987
1110
  * Allows the ServiceWorker to activate immediately
988
1111
  */
989
1112
  async skipWaiting() {
990
- self.loggers.open("platform").info("skipWaiting() called");
1113
+ getLogger(["shovel", "platform"]).debug("skipWaiting() called");
991
1114
  if (!this.#isDevelopment) {
992
- self.loggers.open("platform").info("skipWaiting() - production graceful restart not implemented");
1115
+ getLogger(["shovel", "platform"]).debug(
1116
+ "skipWaiting() - production graceful restart not implemented"
1117
+ );
993
1118
  }
994
1119
  }
995
1120
  /**
@@ -997,8 +1122,7 @@ var ServiceWorkerGlobals = class {
997
1122
  * other events (like "message" for worker threads) go to native handler
998
1123
  */
999
1124
  addEventListener(type, listener, options) {
1000
- if (!listener)
1001
- return;
1125
+ if (!listener) return;
1002
1126
  if (isServiceWorkerEvent(type)) {
1003
1127
  this.registration.addEventListener(type, listener, options);
1004
1128
  } else {
@@ -1009,8 +1133,7 @@ var ServiceWorkerGlobals = class {
1009
1133
  }
1010
1134
  }
1011
1135
  removeEventListener(type, listener, options) {
1012
- if (!listener)
1013
- return;
1136
+ if (!listener) return;
1014
1137
  if (isServiceWorkerEvent(type)) {
1015
1138
  this.registration.removeEventListener(type, listener, options);
1016
1139
  } else {
@@ -1053,6 +1176,9 @@ var ServiceWorkerGlobals = class {
1053
1176
  g.dispatchEvent = this.dispatchEvent.bind(this);
1054
1177
  g.caches = this.caches;
1055
1178
  g.directories = this.directories;
1179
+ if (this.databases) {
1180
+ g.databases = this.databases;
1181
+ }
1056
1182
  g.loggers = this.loggers;
1057
1183
  g.registration = this.registration;
1058
1184
  g.skipWaiting = this.skipWaiting.bind(this);
@@ -1079,117 +1205,269 @@ var ServiceWorkerGlobals = class {
1079
1205
  }
1080
1206
  }
1081
1207
  };
1082
- var SHOVEL_CATEGORIES = [
1083
- "cli",
1084
- "build",
1085
- "platform",
1086
- "watcher",
1087
- "worker",
1088
- "single-threaded",
1089
- "assets",
1090
- "platform-node",
1091
- "platform-bun",
1092
- "platform-cloudflare",
1093
- "cache",
1094
- "cache-redis",
1095
- "router"
1096
- ];
1097
- var BUILTIN_SINK_PROVIDERS = {
1098
- console: { module: "@logtape/logtape", factory: "getConsoleSink" },
1099
- file: { module: "@logtape/file", factory: "getFileSink" },
1100
- rotating: { module: "@logtape/file", factory: "getRotatingFileSink" },
1101
- "stream-file": { module: "@logtape/file", factory: "getStreamFileSink" },
1102
- otel: { module: "@logtape/otel", factory: "getOpenTelemetrySink" },
1103
- sentry: { module: "@logtape/sentry", factory: "getSentrySink" },
1104
- syslog: { module: "@logtape/syslog", factory: "getSyslogSink" },
1105
- cloudwatch: {
1106
- module: "@logtape/cloudwatch-logs",
1107
- factory: "getCloudWatchLogsSink"
1108
- }
1109
- };
1110
- async function createSink(config) {
1111
- const { provider, factory: preImportedFactory, ...sinkOptions } = config;
1112
- if (preImportedFactory) {
1113
- return preImportedFactory(sinkOptions);
1114
- }
1115
- const builtin = BUILTIN_SINK_PROVIDERS[provider];
1116
- const modulePath = builtin?.module || provider;
1117
- const factoryName = builtin?.factory || "default";
1118
- const module = await import(modulePath);
1119
- const factory = module[factoryName] || module.default;
1120
- if (!factory) {
1121
- throw new Error(
1122
- `Sink module "${modulePath}" has no export "${factoryName}"`
1123
- );
1124
- }
1125
- return factory(sinkOptions);
1208
+ function isClass(fn) {
1209
+ return typeof fn === "function" && fn.prototype !== void 0;
1210
+ }
1211
+ function createDirectoryFactory(configs) {
1212
+ return async (name) => {
1213
+ const config = configs[name];
1214
+ if (!config) {
1215
+ throw new Error(
1216
+ `Directory "${name}" is not configured. Available directories: ${Object.keys(configs).join(", ") || "(none)"}`
1217
+ );
1218
+ }
1219
+ const { impl, ...dirOptions } = config;
1220
+ if (!impl) {
1221
+ throw new Error(
1222
+ `Directory "${name}" has no impl. Ensure the directory module is configured.`
1223
+ );
1224
+ }
1225
+ if (isClass(impl)) {
1226
+ return new impl(name, dirOptions);
1227
+ } else {
1228
+ return impl(name, dirOptions);
1229
+ }
1230
+ };
1231
+ }
1232
+ function createCacheFactory(options) {
1233
+ const { configs, usePostMessage = false } = options;
1234
+ return async (name) => {
1235
+ if (usePostMessage) {
1236
+ return new PostMessageCache(name);
1237
+ }
1238
+ const config = configs[name];
1239
+ if (!config) {
1240
+ throw new Error(
1241
+ `Cache "${name}" is not configured. Available caches: ${Object.keys(configs).join(", ") || "(none)"}`
1242
+ );
1243
+ }
1244
+ const { impl, ...cacheOptions } = config;
1245
+ if (!impl) {
1246
+ throw new Error(
1247
+ `Cache "${name}" has no impl. Ensure the cache module is configured.`
1248
+ );
1249
+ }
1250
+ if (isClass(impl)) {
1251
+ return new impl(name, cacheOptions);
1252
+ } else {
1253
+ return impl(name, cacheOptions);
1254
+ }
1255
+ };
1256
+ }
1257
+ async function initWorkerRuntime(options) {
1258
+ const { config } = options;
1259
+ const runtimeLogger = getLogger(["shovel", "platform"]);
1260
+ if (config?.logging) {
1261
+ await configureLogging(config.logging);
1262
+ }
1263
+ runtimeLogger.debug("Initializing worker runtime");
1264
+ const caches = new CustomCacheStorage(
1265
+ createCacheFactory({
1266
+ configs: config?.caches ?? {},
1267
+ usePostMessage: true
1268
+ })
1269
+ );
1270
+ const directories = new CustomDirectoryStorage(
1271
+ createDirectoryFactory(config?.directories ?? {})
1272
+ );
1273
+ let databases;
1274
+ if (config?.databases && Object.keys(config.databases).length > 0) {
1275
+ const factory = createDatabaseFactory(config.databases);
1276
+ databases = new CustomDatabaseStorage(factory);
1277
+ }
1278
+ const loggers = new CustomLoggerStorage(
1279
+ (categories) => getLogger(categories)
1280
+ );
1281
+ const registration = new ShovelServiceWorkerRegistration();
1282
+ const scope = new ServiceWorkerGlobals({
1283
+ registration,
1284
+ caches,
1285
+ directories,
1286
+ databases,
1287
+ loggers
1288
+ });
1289
+ scope.install();
1290
+ runtimeLogger.debug("Worker runtime initialized");
1291
+ return { registration, scope, caches, directories, databases, loggers };
1126
1292
  }
1127
- async function configureLogging(loggingConfig, options = {}) {
1128
- const level = loggingConfig.level || "info";
1129
- const defaultSinkConfigs = loggingConfig.sinks || [{ provider: "console" }];
1130
- const categories = loggingConfig.categories || {};
1131
- const reset = options.reset !== false;
1132
- const sinkByKey = /* @__PURE__ */ new Map();
1133
- for (const config of defaultSinkConfigs) {
1134
- const key = JSON.stringify(config);
1135
- if (!sinkByKey.has(key)) {
1136
- sinkByKey.set(key, { config, name: `sink_${sinkByKey.size}` });
1293
+ function startWorkerMessageLoop(options) {
1294
+ const registration = options instanceof ShovelServiceWorkerRegistration ? options : options.registration;
1295
+ const databases = options instanceof ShovelServiceWorkerRegistration ? void 0 : options.databases;
1296
+ const messageLogger = getLogger(["shovel", "platform"]);
1297
+ const workerId = Math.random().toString(36).substring(2, 8);
1298
+ function sendMessage(message, transfer) {
1299
+ if (transfer && transfer.length > 0) {
1300
+ postMessage(message, transfer);
1301
+ } else {
1302
+ postMessage(message);
1137
1303
  }
1138
1304
  }
1139
- for (const [_, categoryConfig] of Object.entries(categories)) {
1140
- if (categoryConfig.sinks) {
1141
- for (const config of categoryConfig.sinks) {
1142
- const key = JSON.stringify(config);
1143
- if (!sinkByKey.has(key)) {
1144
- sinkByKey.set(key, { config, name: `sink_${sinkByKey.size}` });
1145
- }
1305
+ async function handleFetchRequest(message) {
1306
+ try {
1307
+ const request = new Request(message.request.url, {
1308
+ method: message.request.method,
1309
+ headers: message.request.headers,
1310
+ body: message.request.body
1311
+ });
1312
+ const response = await dispatchRequest(registration, request);
1313
+ const body = await response.arrayBuffer();
1314
+ const headers = Object.fromEntries(response.headers.entries());
1315
+ if (!headers["Content-Type"] && !headers["content-type"]) {
1316
+ headers["Content-Type"] = "text/plain; charset=utf-8";
1146
1317
  }
1318
+ const responseMsg = {
1319
+ type: "response",
1320
+ response: {
1321
+ status: response.status,
1322
+ statusText: response.statusText,
1323
+ headers,
1324
+ body
1325
+ },
1326
+ requestID: message.requestID
1327
+ };
1328
+ sendMessage(responseMsg, [body]);
1329
+ } catch (error) {
1330
+ messageLogger.error(`[Worker-${workerId}] Request failed: {error}`, {
1331
+ error
1332
+ });
1333
+ const errorMsg = {
1334
+ type: "error",
1335
+ error: error instanceof Error ? error.message : String(error),
1336
+ stack: error instanceof Error ? error.stack : void 0,
1337
+ requestID: message.requestID
1338
+ };
1339
+ sendMessage(errorMsg);
1147
1340
  }
1148
1341
  }
1149
- const sinks = {};
1150
- for (const { config, name } of sinkByKey.values()) {
1151
- sinks[name] = await createSink(config);
1342
+ function handleMessage(event) {
1343
+ const message = event.data;
1344
+ if (message?.type === "cache:response" || message?.type === "cache:error") {
1345
+ messageLogger.debug(`[Worker-${workerId}] Forwarding cache message`, {
1346
+ type: message.type,
1347
+ requestID: message.requestID
1348
+ });
1349
+ handleCacheResponse(message);
1350
+ return;
1351
+ }
1352
+ if (message?.type === "request") {
1353
+ handleFetchRequest(message).catch((error) => {
1354
+ messageLogger.error(`[Worker-${workerId}] Unhandled error: {error}`, {
1355
+ error
1356
+ });
1357
+ });
1358
+ return;
1359
+ }
1360
+ if (message?.type === "shutdown") {
1361
+ messageLogger.debug(`[Worker-${workerId}] Received shutdown signal`);
1362
+ (async () => {
1363
+ try {
1364
+ if (databases) {
1365
+ await databases.closeAll();
1366
+ messageLogger.debug(`[Worker-${workerId}] Databases closed`);
1367
+ }
1368
+ sendMessage({ type: "shutdown-complete" });
1369
+ messageLogger.debug(`[Worker-${workerId}] Shutdown complete`);
1370
+ } catch (error) {
1371
+ messageLogger.error(`[Worker-${workerId}] Shutdown error: {error}`, {
1372
+ error
1373
+ });
1374
+ sendMessage({ type: "shutdown-complete" });
1375
+ }
1376
+ })();
1377
+ return;
1378
+ }
1379
+ if (message?.type) {
1380
+ messageLogger.debug(`[Worker-${workerId}] Unknown message type`, {
1381
+ type: message.type
1382
+ });
1383
+ }
1152
1384
  }
1153
- const getSinkNames = (configs) => {
1154
- return configs.map((config) => sinkByKey.get(JSON.stringify(config))?.name ?? "").filter(Boolean);
1385
+ self.addEventListener("message", handleMessage);
1386
+ sendMessage({ type: "ready" });
1387
+ messageLogger.debug(`[Worker-${workerId}] Message loop started`);
1388
+ }
1389
+ var SHOVEL_DEFAULT_LOGGERS = [
1390
+ { category: ["shovel"], level: "info", sinks: ["console"] },
1391
+ { category: ["logtape", "meta"], level: "warning", sinks: ["console"] }
1392
+ ];
1393
+ async function createSink(config) {
1394
+ const {
1395
+ impl,
1396
+ path,
1397
+ // Extract path for file-based sinks
1398
+ ...sinkOptions
1399
+ } = config;
1400
+ if (!impl) {
1401
+ throw new Error(
1402
+ `Sink has no impl. Ensure the sink module is configured in shovel.json.`
1403
+ );
1404
+ }
1405
+ if (path !== void 0) {
1406
+ return impl(path, sinkOptions);
1407
+ } else if (Object.keys(sinkOptions).length > 0) {
1408
+ return impl(sinkOptions);
1409
+ } else {
1410
+ return impl();
1411
+ }
1412
+ }
1413
+ function normalizeCategory(category) {
1414
+ return typeof category === "string" ? [category] : category;
1415
+ }
1416
+ async function configureLogging(loggingConfig) {
1417
+ const userSinks = loggingConfig.sinks || {};
1418
+ const userLoggers = loggingConfig.loggers || [];
1419
+ const sinks = {
1420
+ console: getConsoleSink()
1155
1421
  };
1156
- const defaultSinkNames = getSinkNames(defaultSinkConfigs);
1157
- const loggers = SHOVEL_CATEGORIES.map((category) => {
1158
- const categoryConfig = categories[category];
1159
- const categoryLevel = categoryConfig?.level || level;
1160
- const categorySinks = categoryConfig?.sinks ? getSinkNames(categoryConfig.sinks) : defaultSinkNames;
1161
- return {
1162
- category: [category],
1163
- level: categoryLevel,
1164
- sinks: categorySinks
1422
+ for (const [name, config] of Object.entries(userSinks)) {
1423
+ sinks[name] = await createSink(config);
1424
+ }
1425
+ const userCategoryKeys = new Set(
1426
+ userLoggers.map((l) => JSON.stringify(normalizeCategory(l.category)))
1427
+ );
1428
+ const mergedLoggers = [
1429
+ // Shovel defaults (unless overridden by user)
1430
+ ...SHOVEL_DEFAULT_LOGGERS.filter(
1431
+ (l) => !userCategoryKeys.has(JSON.stringify(normalizeCategory(l.category)))
1432
+ ),
1433
+ // User loggers
1434
+ ...userLoggers
1435
+ ];
1436
+ const loggers = mergedLoggers.map((loggerConfig) => {
1437
+ const result = {
1438
+ category: normalizeCategory(loggerConfig.category)
1165
1439
  };
1166
- });
1167
- loggers.push({
1168
- category: ["logtape", "meta"],
1169
- level: "warning",
1170
- sinks: []
1440
+ if (loggerConfig.level) {
1441
+ result.lowestLevel = loggerConfig.level;
1442
+ }
1443
+ result.sinks = loggerConfig.sinks ?? ["console"];
1444
+ if (loggerConfig.parentSinks) {
1445
+ result.parentSinks = loggerConfig.parentSinks;
1446
+ }
1447
+ return result;
1171
1448
  });
1172
1449
  await configure({
1173
- reset,
1450
+ reset: true,
1174
1451
  sinks,
1175
1452
  loggers
1176
1453
  });
1177
1454
  }
1178
1455
  export {
1179
- ActivateEvent,
1456
+ CustomDatabaseStorage,
1180
1457
  CustomLoggerStorage,
1181
1458
  DedicatedWorkerGlobalScope,
1182
- ExtendableEvent,
1183
1459
  ExtendableMessageEvent,
1184
- FetchEvent,
1185
- InstallEvent,
1186
1460
  Notification,
1187
1461
  NotificationEvent,
1188
1462
  PushEvent,
1189
1463
  RequestCookieStore,
1190
1464
  ServiceWorkerGlobals,
1465
+ ShovelActivateEvent,
1191
1466
  ShovelClient,
1192
1467
  ShovelClients,
1468
+ ShovelExtendableEvent,
1469
+ ShovelFetchEvent,
1470
+ ShovelInstallEvent,
1193
1471
  ShovelNavigationPreloadManager,
1194
1472
  ShovelPushMessageData,
1195
1473
  ShovelServiceWorker,
@@ -1199,7 +1477,17 @@ export {
1199
1477
  SyncEvent,
1200
1478
  WorkerGlobalScope,
1201
1479
  configureLogging,
1480
+ createCacheFactory,
1481
+ createDatabaseFactory,
1482
+ createDirectoryFactory,
1483
+ dispatchRequest,
1484
+ initWorkerRuntime,
1485
+ kDispatchActivate,
1486
+ kDispatchInstall,
1487
+ kServiceWorker,
1202
1488
  parseCookieHeader,
1203
1489
  parseSetCookieHeader,
1204
- serializeCookie
1490
+ runLifecycle,
1491
+ serializeCookie,
1492
+ startWorkerMessageLoop
1205
1493
  };