@b9g/platform 0.1.13 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/platform",
3
- "version": "0.1.13",
3
+ "version": "0.1.14-beta.0",
4
4
  "description": "The portable meta-framework built on web standards.",
5
5
  "keywords": [
6
6
  "service-worker",
@@ -20,15 +20,15 @@
20
20
  "dependencies": {
21
21
  "@b9g/async-context": "^0.2.0-beta.0",
22
22
  "@b9g/cache": "^0.2.0-beta.0",
23
- "@b9g/filesystem": "^0.1.7",
23
+ "@b9g/filesystem": "^0.1.8",
24
24
  "@b9g/zen": "^0.1.6",
25
25
  "@logtape/logtape": "^1.2.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@b9g/libuild": "^0.1.20",
29
29
  "@b9g/node-webworker": "^0.2.0-beta.1",
30
- "@b9g/platform-bun": "^0.1.10",
31
- "@b9g/platform-node": "^0.1.12"
30
+ "@b9g/platform-bun": "^0.1.12-beta.0",
31
+ "@b9g/platform-node": "^0.1.14-beta.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@logtape/file": "^1.0.0",
package/src/index.d.ts CHANGED
@@ -8,11 +8,10 @@
8
8
  *
9
9
  * This module contains:
10
10
  * - Platform interface and base classes
11
- * - SingleThreadedRuntime for main-thread execution
12
11
  * - ServiceWorkerPool for multi-worker execution
13
12
  */
14
13
  import type { DirectoryStorage } from "@b9g/filesystem";
15
- import { CustomLoggerStorage, type LoggerStorage, type DatabaseStorage } from "./runtime.js";
14
+ import { CustomLoggerStorage, type LoggerStorage } from "./runtime.js";
16
15
  export { validateConfig, ConfigValidationError } from "./config.js";
17
16
  /**
18
17
  * Platform configuration
@@ -28,6 +27,8 @@ export interface ServerOptions {
28
27
  port?: number;
29
28
  /** Host to bind to */
30
29
  host?: string;
30
+ /** Enable SO_REUSEPORT for multi-worker deployments (Bun only) */
31
+ reusePort?: boolean;
31
32
  }
32
33
  /**
33
34
  * Request handler function (Web Fetch API compatible)
@@ -80,21 +81,14 @@ export interface ServiceWorkerInstance {
80
81
  dispose(): Promise<void>;
81
82
  }
82
83
  /**
83
- * Options for getEntryWrapper()
84
+ * Production entry points returned by getProductionEntryPoints().
85
+ * Each key is the output filename (without .js), value is the code.
86
+ *
87
+ * Examples:
88
+ * - Cloudflare: { "worker": "<code>" } - single worker file
89
+ * - Node/Bun: { "index": "<supervisor>", "worker": "<worker>" } - two files
84
90
  */
85
- export interface EntryWrapperOptions {
86
- /**
87
- * Type of entry wrapper to generate:
88
- * - "production": Production server entry (default) - runs the server directly
89
- * - "worker": Worker entry for ServiceWorkerPool - sets up runtime and message loop
90
- */
91
- type?: "production" | "worker";
92
- /**
93
- * Output directory for the build. Used to generate absolute paths for
94
- * directory defaults (server, public). Required for "worker" type.
95
- */
96
- outDir?: string;
97
- }
91
+ export type ProductionEntryPoints = Record<string, string>;
98
92
  /**
99
93
  * ESBuild configuration subset that platforms can customize
100
94
  */
@@ -107,16 +101,6 @@ export interface PlatformESBuildConfig {
107
101
  external?: string[];
108
102
  /** Compile-time defines */
109
103
  define?: Record<string, string>;
110
- /**
111
- * Whether the entry wrapper imports user code inline (bundled together)
112
- * or references it as a separate file (loaded at runtime).
113
- *
114
- * - true: User code is imported inline (e.g., Cloudflare: `import "user-entry"`)
115
- * - false: User code is loaded separately (e.g., Node/Bun: `loadServiceWorker("./server.js")`)
116
- *
117
- * Default: false (separate build)
118
- */
119
- bundlesUserCodeInline?: boolean;
120
104
  }
121
105
  /**
122
106
  * Default resource configuration for a named resource (cache, directory, etc.)
@@ -141,6 +125,19 @@ export interface PlatformDefaults {
141
125
  /** Default cache configuration (e.g., memory cache) */
142
126
  caches?: Record<string, ResourceDefault>;
143
127
  }
128
+ /**
129
+ * Extended ServiceWorkerContainer with internal methods for hot reload
130
+ */
131
+ export interface ShovelServiceWorkerContainer extends ServiceWorkerContainer {
132
+ /** Internal: Get the worker pool for request handling */
133
+ readonly pool?: {
134
+ handleRequest(request: Request): Promise<Response>;
135
+ };
136
+ /** Internal: Terminate all workers */
137
+ terminate(): Promise<void>;
138
+ /** Internal: Reload workers (for hot reload) */
139
+ reloadWorkers(entrypoint: string): Promise<void>;
140
+ }
144
141
  /**
145
142
  * Platform interface - ServiceWorker entrypoint loader for JavaScript runtimes
146
143
  *
@@ -152,29 +149,38 @@ export interface Platform {
152
149
  */
153
150
  readonly name: string;
154
151
  /**
155
- * Load and run a ServiceWorker-style entrypoint
156
- * This is where all the platform-specific complexity lives
152
+ * ServiceWorkerContainer for managing registrations (Node/Bun only)
153
+ * Similar to navigator.serviceWorker in browsers
154
+ */
155
+ readonly serviceWorker: ShovelServiceWorkerContainer;
156
+ /**
157
+ * Start HTTP server and route requests to ServiceWorker (Node/Bun only)
158
+ * Must call serviceWorker.register() first
157
159
  */
158
- loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
160
+ listen(): Promise<Server>;
161
+ /**
162
+ * Close server and terminate workers (Node/Bun only)
163
+ */
164
+ close(): Promise<void>;
159
165
  /**
160
166
  * SUPPORTING UTILITY - Create server instance for this platform
161
167
  */
162
168
  createServer(handler: Handler, options?: ServerOptions): Server;
163
169
  /**
164
- * BUILD SUPPORT - Get virtual entry wrapper template for user code
170
+ * BUILD SUPPORT - Get production entry points for bundling
171
+ *
172
+ * Returns a map of output filenames to their source code.
173
+ * The build system creates one output file per entry point.
165
174
  *
166
- * Returns a JavaScript/TypeScript string that:
167
- * 1. Initializes platform-specific runtime (polyfills, globals)
168
- * 2. Imports the user's entrypoint
169
- * 3. Exports any required handlers (e.g., ES module export for Cloudflare)
175
+ * Platform determines the structure:
176
+ * - Cloudflare: { "worker": "<code>" } - single worker file
177
+ * - Node/Bun: { "index": "<supervisor>", "worker": "<runtime + user code>" }
170
178
  *
171
- * The CLI uses this to create a virtual entry point for bundling.
172
- * Every platform must provide a wrapper - there is no "raw user code" mode.
179
+ * The user's entrypoint code is statically imported into the appropriate file.
173
180
  *
174
- * @param entryPath - Absolute path to user's entrypoint file
175
- * @param options - Additional options
181
+ * @param userEntryPath - Path to user's entrypoint (will be imported)
176
182
  */
177
- getEntryWrapper(entryPath: string, options?: EntryWrapperOptions): string;
183
+ getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
178
184
  /**
179
185
  * BUILD SUPPORT - Get platform-specific esbuild configuration
180
186
  *
@@ -205,6 +211,15 @@ export interface Platform {
205
211
  * Uses platform-specific defaults, overridable via shovel.json config
206
212
  */
207
213
  createLoggers(): Promise<LoggerStorage>;
214
+ /**
215
+ * Dispose of platform resources (worker pools, connections, etc.)
216
+ */
217
+ dispose(): Promise<void>;
218
+ /**
219
+ * HOT RELOAD - Reload workers with a new entrypoint (development only)
220
+ * Optional - only Node and Bun platforms implement this
221
+ */
222
+ reloadWorkers?(entrypoint: string): Promise<void>;
208
223
  }
209
224
  /**
210
225
  * Platform registry - internal implementation
@@ -253,7 +268,7 @@ export declare function resolvePlatform(options: {
253
268
  /**
254
269
  * Create platform instance based on name
255
270
  */
256
- export declare function createPlatform(platformName: string, options?: any): Promise<any>;
271
+ export declare function createPlatform(platformName: string, options?: any): Promise<Platform>;
257
272
  /**
258
273
  * Base platform class with shared adapter loading logic
259
274
  * Platform implementations extend this and provide platform-specific methods
@@ -262,13 +277,15 @@ export declare abstract class BasePlatform implements Platform {
262
277
  config: PlatformConfig;
263
278
  constructor(config?: PlatformConfig);
264
279
  abstract readonly name: string;
265
- abstract loadServiceWorker(entrypoint: string, options?: any): Promise<any>;
280
+ abstract readonly serviceWorker: ShovelServiceWorkerContainer;
281
+ abstract listen(): Promise<Server>;
282
+ abstract close(): Promise<void>;
266
283
  abstract createServer(handler: any, options?: any): any;
267
284
  /**
268
- * Get virtual entry wrapper template for user code
269
- * Subclasses must override to provide platform-specific wrappers
285
+ * Get production entry points for bundling
286
+ * Subclasses must override to provide platform-specific entry points
270
287
  */
271
- abstract getEntryWrapper(entryPath: string, options?: EntryWrapperOptions): string;
288
+ abstract getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
272
289
  /**
273
290
  * Get platform-specific esbuild configuration
274
291
  * Subclasses should override to provide platform-specific config
@@ -294,7 +311,23 @@ export declare abstract class BasePlatform implements Platform {
294
311
  * Subclasses must override to provide platform-specific implementation
295
312
  */
296
313
  abstract createLoggers(): Promise<LoggerStorage>;
314
+ /**
315
+ * Dispose of platform resources
316
+ * Subclasses should override to clean up worker pools, connections, etc.
317
+ */
318
+ dispose(): Promise<void>;
297
319
  }
320
+ /**
321
+ * Merge platform defaults with user config
322
+ *
323
+ * Deep merges each entry so user can override specific options without
324
+ * losing the platform's default implementation class.
325
+ *
326
+ * @param defaults - Platform's runtime defaults (with actual class refs)
327
+ * @param userConfig - User's config from shovel.json (may be partial)
328
+ * @returns Merged config with all entries
329
+ */
330
+ export declare function mergeConfigWithDefaults(defaults: Record<string, Record<string, unknown>>, userConfig: Record<string, Record<string, unknown>> | undefined): Record<string, Record<string, unknown>>;
298
331
  /**
299
332
  * Global platform registry
300
333
  */
@@ -328,52 +361,6 @@ export interface ServiceWorkerRuntime {
328
361
  readonly workerCount: number;
329
362
  readonly ready: boolean;
330
363
  }
331
- export interface SingleThreadedRuntimeOptions {
332
- /** Cache storage for the runtime */
333
- caches: CacheStorage;
334
- /** Directory storage for the runtime */
335
- directories: DirectoryStorage;
336
- /** Database storage for the runtime */
337
- databases?: DatabaseStorage;
338
- /** Logger storage for the runtime */
339
- loggers: LoggerStorage;
340
- }
341
- /**
342
- * Single-threaded ServiceWorker runtime
343
- *
344
- * Runs ServiceWorker code directly in the main thread.
345
- * Implements ServiceWorkerRuntime interface for interchangeability with ServiceWorkerPool.
346
- */
347
- export declare class SingleThreadedRuntime implements ServiceWorkerRuntime {
348
- #private;
349
- constructor(options: SingleThreadedRuntimeOptions);
350
- /**
351
- * Initialize the runtime (install ServiceWorker globals)
352
- */
353
- init(): Promise<void>;
354
- /**
355
- * Load (or reload) a ServiceWorker entrypoint
356
- * @param entrypoint - Path to the entrypoint file (content-hashed filename)
357
- */
358
- load(entrypoint: string): Promise<void>;
359
- /**
360
- * Handle an HTTP request
361
- * This is the key method - direct call, no postMessage!
362
- */
363
- handleRequest(request: Request): Promise<Response>;
364
- /**
365
- * Graceful shutdown
366
- */
367
- terminate(): Promise<void>;
368
- /**
369
- * Get the number of workers (always 1 for single-threaded)
370
- */
371
- get workerCount(): number;
372
- /**
373
- * Check if ready to handle requests
374
- */
375
- get ready(): boolean;
376
- }
377
364
  /**
378
365
  * Worker pool options
379
366
  */
@@ -384,6 +371,8 @@ export interface WorkerPoolOptions {
384
371
  requestTimeout?: number;
385
372
  /** Working directory for file resolution */
386
373
  cwd?: string;
374
+ /** Custom worker factory (if not provided, uses createWebWorker) */
375
+ createWorker?: (entrypoint: string) => Worker | Promise<Worker>;
387
376
  }
388
377
  export interface WorkerMessage {
389
378
  type: string;
package/src/index.js CHANGED
@@ -1,12 +1,7 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  // src/index.ts
3
3
  import { getLogger } from "@logtape/logtape";
4
- import {
5
- ServiceWorkerGlobals,
6
- ShovelServiceWorkerRegistration,
7
- ShovelFetchEvent,
8
- CustomLoggerStorage
9
- } from "./runtime.js";
4
+ import { CustomLoggerStorage } from "./runtime.js";
10
5
  import { validateConfig, ConfigValidationError } from "./config.js";
11
6
  import {
12
7
  CustomDatabaseStorage,
@@ -85,7 +80,22 @@ var BasePlatform = class {
85
80
  constructor(config = {}) {
86
81
  this.config = config;
87
82
  }
83
+ /**
84
+ * Dispose of platform resources
85
+ * Subclasses should override to clean up worker pools, connections, etc.
86
+ */
87
+ async dispose() {
88
+ }
88
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
+ }
89
99
  var DefaultPlatformRegistry = class {
90
100
  #platforms;
91
101
  constructor() {
@@ -142,86 +152,6 @@ async function getPlatformAsync(name) {
142
152
  }
143
153
  return platform;
144
154
  }
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
155
  async function createWebWorker(workerScript) {
226
156
  if (typeof Worker !== "undefined") {
227
157
  return new Worker(workerScript, { type: "module" });
@@ -306,7 +236,7 @@ var ServiceWorkerPool = class {
306
236
  * The bundle self-initializes and sends "ready" when done
307
237
  */
308
238
  async #createWorker(entrypoint) {
309
- const worker = await createWebWorker(entrypoint);
239
+ const worker = this.#options.createWorker ? await this.#options.createWorker(entrypoint) : await createWebWorker(entrypoint);
310
240
  const readyPromise = new Promise((resolve, reject) => {
311
241
  const timeoutId = setTimeout(() => {
312
242
  this.#pendingWorkerReady.delete(worker);
@@ -595,7 +525,6 @@ export {
595
525
  CustomDatabaseStorage,
596
526
  CustomLoggerStorage,
597
527
  ServiceWorkerPool,
598
- SingleThreadedRuntime,
599
528
  createDatabaseFactory,
600
529
  createPlatform,
601
530
  detectDeploymentPlatform,
@@ -603,6 +532,7 @@ export {
603
532
  detectRuntime,
604
533
  getPlatform,
605
534
  getPlatformAsync,
535
+ mergeConfigWithDefaults,
606
536
  platformRegistry,
607
537
  resolvePlatform,
608
538
  validateConfig
package/src/runtime.d.ts CHANGED
@@ -354,6 +354,14 @@ export declare class ShovelNavigationPreloadManager implements NavigationPreload
354
354
  }>;
355
355
  setHeaderValue(_value: string): Promise<void>;
356
356
  }
357
+ /** @internal Symbol for accessing the internal ServiceWorker instance */
358
+ export declare const kServiceWorker: unique symbol;
359
+ /** @internal Symbol for dispatching the install lifecycle event */
360
+ export declare const kDispatchInstall: unique symbol;
361
+ /** @internal Symbol for dispatching the activate lifecycle event */
362
+ export declare const kDispatchActivate: unique symbol;
363
+ /** @internal Symbol for handling fetch requests */
364
+ declare const kHandleRequest: unique symbol;
357
365
  /**
358
366
  * ShovelServiceWorkerRegistration - Internal implementation of ServiceWorkerRegistration
359
367
  * This is also the Shovel ServiceWorker runtime - they are unified into one class
@@ -363,7 +371,7 @@ export declare class ShovelServiceWorkerRegistration extends EventTarget impleme
363
371
  readonly scope: string;
364
372
  readonly updateViaCache: "imports" | "all" | "none";
365
373
  readonly navigationPreload: NavigationPreloadManager;
366
- _serviceWorker: ShovelServiceWorker;
374
+ [kServiceWorker]: ShovelServiceWorker;
367
375
  readonly cookies: any;
368
376
  readonly pushManager: any;
369
377
  onupdatefound: ((ev: Event) => any) | null;
@@ -377,27 +385,71 @@ export declare class ShovelServiceWorkerRegistration extends EventTarget impleme
377
385
  unregister(): Promise<boolean>;
378
386
  update(): Promise<ServiceWorkerRegistration>;
379
387
  /**
380
- * Install the ServiceWorker (Shovel extension)
388
+ * Dispatch the install lifecycle event
389
+ * @internal Use runLifecycle() instead of calling this directly
381
390
  */
382
- install(): Promise<void>;
391
+ [kDispatchInstall](): Promise<void>;
383
392
  /**
384
- * Activate the ServiceWorker (Shovel extension)
393
+ * Dispatch the activate lifecycle event
394
+ * @internal Use runLifecycle() instead of calling this directly
385
395
  */
386
- activate(): Promise<void>;
396
+ [kDispatchActivate](): Promise<void>;
387
397
  /**
388
- * Handle a fetch request (Shovel extension)
398
+ * Handle a fetch request
399
+ * @internal Use the kHandleRequest symbol to access this method
389
400
  *
390
401
  * Platforms create a ShovelFetchEvent (or subclass) with platform-specific
391
402
  * properties and hooks, then pass it to this method for dispatching.
392
403
  *
393
404
  * @param event - The fetch event to handle (created by platform adapter)
394
405
  */
395
- handleRequest(event: ShovelFetchEvent): Promise<Response>;
406
+ [kHandleRequest](event: ShovelFetchEvent): Promise<Response>;
396
407
  /**
397
408
  * Check if ready to handle requests (Shovel extension)
398
409
  */
399
410
  get ready(): boolean;
400
411
  }
412
+ /**
413
+ * Run ServiceWorker lifecycle events on a registration.
414
+ *
415
+ * This is the proper way to trigger lifecycle events in Shovel's server-side runtime.
416
+ * Unlike browsers where lifecycle is automatic, server-side code must explicitly
417
+ * trigger these events after registering event handlers.
418
+ *
419
+ * @param registration - The ServiceWorkerRegistration to run lifecycle on
420
+ * @param stage - Which lifecycle stage to run:
421
+ * - "install": Run only the install event
422
+ * - "activate": Run install then activate (default)
423
+ *
424
+ * @example
425
+ * ```typescript
426
+ * const {registration} = await initWorkerRuntime({config});
427
+ * await import("./server.js"); // Register event handlers
428
+ * await runLifecycle(registration); // Runs install + activate
429
+ * ```
430
+ */
431
+ export declare function runLifecycle(registration: ShovelServiceWorkerRegistration, stage?: "install" | "activate"): Promise<void>;
432
+ /**
433
+ * Dispatch a fetch request to a ServiceWorker registration.
434
+ *
435
+ * This is the proper way to dispatch requests in Shovel's server-side runtime.
436
+ * Platforms should use this function rather than accessing internal methods directly.
437
+ *
438
+ * @param registration - The ServiceWorkerRegistration to dispatch to
439
+ * @param requestOrEvent - The Request to dispatch, or a pre-constructed ShovelFetchEvent
440
+ * @returns The Response from the ServiceWorker
441
+ *
442
+ * @example
443
+ * ```typescript
444
+ * // Simple usage with a Request
445
+ * const response = await dispatchRequest(registration, request);
446
+ *
447
+ * // Platform-specific usage with a custom FetchEvent subclass
448
+ * const event = new CloudflareFetchEvent(request, {env, platformWaitUntil});
449
+ * const response = await dispatchRequest(registration, event);
450
+ * ```
451
+ */
452
+ export declare function dispatchRequest(registration: ShovelServiceWorkerRegistration, requestOrEvent: Request | ShovelFetchEvent): Promise<Response>;
401
453
  /**
402
454
  * ShovelServiceWorkerContainer - Internal implementation of ServiceWorkerContainer
403
455
  * This is the registry that manages multiple ServiceWorkerRegistrations by scope
@@ -431,10 +483,6 @@ export declare class ShovelServiceWorkerContainer extends EventTarget implements
431
483
  * Unregister a ServiceWorker registration
432
484
  */
433
485
  unregister(scope: string): Promise<boolean>;
434
- /**
435
- * Route a request to the appropriate registration based on scope matching
436
- */
437
- handleRequest(request: Request): Promise<Response | null>;
438
486
  /**
439
487
  * Install and activate all registrations
440
488
  */
@@ -759,16 +807,15 @@ export interface InitWorkerRuntimeResult {
759
807
  * @example
760
808
  * ```typescript
761
809
  * import {config} from "shovel:config";
762
- * import {initWorkerRuntime, startWorkerMessageLoop} from "@b9g/platform/runtime";
810
+ * import {initWorkerRuntime, runLifecycle, startWorkerMessageLoop} from "@b9g/platform/runtime";
763
811
  *
764
812
  * const {registration} = await initWorkerRuntime({config});
765
813
  *
766
814
  * // Import user code (registers event handlers)
767
- * import "./server.js";
815
+ * await import("./server.js");
768
816
  *
769
817
  * // Run lifecycle and start message loop
770
- * await registration.install();
771
- * await registration.activate();
818
+ * await runLifecycle(registration);
772
819
  * startWorkerMessageLoop(registration);
773
820
  * ```
774
821
  */
package/src/runtime.js CHANGED
@@ -568,12 +568,16 @@ var ShovelNavigationPreloadManager = class {
568
568
  async setHeaderValue(_value) {
569
569
  }
570
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");
571
575
  var ShovelServiceWorkerRegistration = class extends EventTarget {
572
576
  scope;
573
577
  updateViaCache;
574
578
  navigationPreload;
575
- // ServiceWorker instances representing different lifecycle states
576
- _serviceWorker;
579
+ // Internal ServiceWorker instance (accessed via symbol for lifecycle management)
580
+ [kServiceWorker];
577
581
  // Web API properties (not supported in server context, but required by interface)
578
582
  cookies;
579
583
  pushManager;
@@ -583,20 +587,20 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
583
587
  this.scope = scope;
584
588
  this.updateViaCache = "imports";
585
589
  this.navigationPreload = new ShovelNavigationPreloadManager();
586
- this._serviceWorker = new ShovelServiceWorker(scriptURL, "parsed");
590
+ this[kServiceWorker] = new ShovelServiceWorker(scriptURL, "parsed");
587
591
  this.cookies = null;
588
592
  this.pushManager = null;
589
593
  this.onupdatefound = null;
590
594
  }
591
595
  // Standard ServiceWorkerRegistration properties
592
596
  get active() {
593
- return this._serviceWorker.state === "activated" ? this._serviceWorker : null;
597
+ return this[kServiceWorker].state === "activated" ? this[kServiceWorker] : null;
594
598
  }
595
599
  get installing() {
596
- return this._serviceWorker.state === "installing" ? this._serviceWorker : null;
600
+ return this[kServiceWorker].state === "installing" ? this[kServiceWorker] : null;
597
601
  }
598
602
  get waiting() {
599
- return this._serviceWorker.state === "installed" ? this._serviceWorker : null;
603
+ return this[kServiceWorker].state === "installed" ? this[kServiceWorker] : null;
600
604
  }
601
605
  // Standard ServiceWorkerRegistration methods
602
606
  async getNotifications(_options) {
@@ -613,13 +617,14 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
613
617
  async update() {
614
618
  return this;
615
619
  }
616
- // Shovel runtime extensions (non-standard but needed for platforms)
620
+ // Internal lifecycle methods (accessed via symbols by runLifecycle)
617
621
  /**
618
- * Install the ServiceWorker (Shovel extension)
622
+ * Dispatch the install lifecycle event
623
+ * @internal Use runLifecycle() instead of calling this directly
619
624
  */
620
- async install() {
621
- if (this._serviceWorker.state !== "parsed") return;
622
- this._serviceWorker._setState("installing");
625
+ async [kDispatchInstall]() {
626
+ if (this[kServiceWorker].state !== "parsed") return;
627
+ this[kServiceWorker]._setState("installing");
623
628
  return new Promise((resolve, reject) => {
624
629
  const event = new ShovelInstallEvent();
625
630
  process.nextTick(() => {
@@ -632,7 +637,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
632
637
  }
633
638
  const promises = event.getPromises();
634
639
  if (promises.length === 0) {
635
- this._serviceWorker._setState("installed");
640
+ this[kServiceWorker]._setState("installed");
636
641
  resolve();
637
642
  } else {
638
643
  promiseWithTimeout(
@@ -640,7 +645,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
640
645
  3e4,
641
646
  "ServiceWorker install event timed out after 30s - waitUntil promises did not resolve"
642
647
  ).then(() => {
643
- this._serviceWorker._setState("installed");
648
+ this[kServiceWorker]._setState("installed");
644
649
  resolve();
645
650
  }).catch(reject);
646
651
  }
@@ -648,13 +653,14 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
648
653
  });
649
654
  }
650
655
  /**
651
- * Activate the ServiceWorker (Shovel extension)
656
+ * Dispatch the activate lifecycle event
657
+ * @internal Use runLifecycle() instead of calling this directly
652
658
  */
653
- async activate() {
654
- if (this._serviceWorker.state !== "installed") {
659
+ async [kDispatchActivate]() {
660
+ if (this[kServiceWorker].state !== "installed") {
655
661
  throw new Error("ServiceWorker must be installed before activation");
656
662
  }
657
- this._serviceWorker._setState("activating");
663
+ this[kServiceWorker]._setState("activating");
658
664
  return new Promise((resolve, reject) => {
659
665
  const event = new ShovelActivateEvent();
660
666
  process.nextTick(() => {
@@ -667,7 +673,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
667
673
  }
668
674
  const promises = event.getPromises();
669
675
  if (promises.length === 0) {
670
- this._serviceWorker._setState("activated");
676
+ this[kServiceWorker]._setState("activated");
671
677
  resolve();
672
678
  } else {
673
679
  promiseWithTimeout(
@@ -675,7 +681,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
675
681
  3e4,
676
682
  "ServiceWorker activate event timed out after 30s - waitUntil promises did not resolve"
677
683
  ).then(() => {
678
- this._serviceWorker._setState("activated");
684
+ this[kServiceWorker]._setState("activated");
679
685
  resolve();
680
686
  }).catch(reject);
681
687
  }
@@ -683,15 +689,16 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
683
689
  });
684
690
  }
685
691
  /**
686
- * Handle a fetch request (Shovel extension)
692
+ * Handle a fetch request
693
+ * @internal Use the kHandleRequest symbol to access this method
687
694
  *
688
695
  * Platforms create a ShovelFetchEvent (or subclass) with platform-specific
689
696
  * properties and hooks, then pass it to this method for dispatching.
690
697
  *
691
698
  * @param event - The fetch event to handle (created by platform adapter)
692
699
  */
693
- async handleRequest(event) {
694
- if (this._serviceWorker.state !== "activated") {
700
+ async [kHandleRequest](event) {
701
+ if (this[kServiceWorker].state !== "activated") {
695
702
  throw new Error("ServiceWorker not activated");
696
703
  }
697
704
  return cookieStoreStorage.run(event.cookieStore, async () => {
@@ -722,10 +729,20 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
722
729
  * Check if ready to handle requests (Shovel extension)
723
730
  */
724
731
  get ready() {
725
- return this._serviceWorker.state === "activated";
732
+ return this[kServiceWorker].state === "activated";
726
733
  }
727
734
  // Events: updatefound (standard), plus Shovel lifecycle events
728
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
+ }
729
746
  var ShovelServiceWorkerContainer = class extends EventTarget {
730
747
  #registrations;
731
748
  controller;
@@ -765,8 +782,8 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
765
782
  const scope = this.#normalizeScope(options?.scope || "/");
766
783
  let registration = this.#registrations.get(scope);
767
784
  if (registration) {
768
- registration._serviceWorker.scriptURL = url;
769
- registration._serviceWorker._setState("parsed");
785
+ registration[kServiceWorker].scriptURL = url;
786
+ registration[kServiceWorker]._setState("parsed");
770
787
  } else {
771
788
  registration = new ShovelServiceWorkerRegistration(scope, url);
772
789
  this.#registrations.set(scope, registration);
@@ -786,30 +803,13 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
786
803
  }
787
804
  return false;
788
805
  }
789
- /**
790
- * Route a request to the appropriate registration based on scope matching
791
- */
792
- async handleRequest(request) {
793
- const url = new URL(request.url);
794
- const pathname = url.pathname;
795
- const matchingScope = this.#findMatchingScope(pathname);
796
- if (matchingScope) {
797
- const registration = this.#registrations.get(matchingScope);
798
- if (registration && registration.ready) {
799
- const event = new ShovelFetchEvent(request);
800
- return await registration.handleRequest(event);
801
- }
802
- }
803
- return null;
804
- }
805
806
  /**
806
807
  * Install and activate all registrations
807
808
  */
808
809
  async installAll() {
809
810
  const installations = Array.from(this.#registrations.values()).map(
810
811
  async (registration) => {
811
- await registration.install();
812
- await registration.activate();
812
+ await runLifecycle(registration);
813
813
  }
814
814
  );
815
815
  await promiseWithTimeout(
@@ -838,19 +838,6 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
838
838
  }
839
839
  return scope;
840
840
  }
841
- /**
842
- * Find the most specific scope that matches a pathname
843
- */
844
- #findMatchingScope(pathname) {
845
- const scopes = Array.from(this.#registrations.keys());
846
- scopes.sort((a, b) => b.length - a.length);
847
- for (const scope of scopes) {
848
- if (pathname.startsWith(scope === "/" ? "/" : scope)) {
849
- return scope;
850
- }
851
- }
852
- return null;
853
- }
854
841
  // Events: controllerchange, message, messageerror, updatefound
855
842
  };
856
843
  var Notification = class extends EventTarget {
@@ -1035,8 +1022,10 @@ var ServiceWorkerGlobals = class {
1035
1022
  }
1036
1023
  const request = new Request(new URL(urlString, "http://localhost"), init);
1037
1024
  return fetchDepthStorage.run(currentDepth + 1, () => {
1038
- const event = new ShovelFetchEvent(request);
1039
- return this.registration.handleRequest(event);
1025
+ return dispatchRequest(
1026
+ this.registration,
1027
+ request
1028
+ );
1040
1029
  });
1041
1030
  }
1042
1031
  queueMicrotask(callback) {
@@ -1121,9 +1110,9 @@ var ServiceWorkerGlobals = class {
1121
1110
  * Allows the ServiceWorker to activate immediately
1122
1111
  */
1123
1112
  async skipWaiting() {
1124
- getLogger(["shovel", "platform"]).info("skipWaiting() called");
1113
+ getLogger(["shovel", "platform"]).debug("skipWaiting() called");
1125
1114
  if (!this.#isDevelopment) {
1126
- getLogger(["shovel", "platform"]).info(
1115
+ getLogger(["shovel", "platform"]).debug(
1127
1116
  "skipWaiting() - production graceful restart not implemented"
1128
1117
  );
1129
1118
  }
@@ -1320,8 +1309,7 @@ function startWorkerMessageLoop(options) {
1320
1309
  headers: message.request.headers,
1321
1310
  body: message.request.body
1322
1311
  });
1323
- const event = new ShovelFetchEvent(request);
1324
- const response = await registration.handleRequest(event);
1312
+ const response = await dispatchRequest(registration, request);
1325
1313
  const body = await response.arrayBuffer();
1326
1314
  const headers = Object.fromEntries(response.headers.entries());
1327
1315
  if (!headers["Content-Type"] && !headers["content-type"]) {
@@ -1492,9 +1480,14 @@ export {
1492
1480
  createCacheFactory,
1493
1481
  createDatabaseFactory,
1494
1482
  createDirectoryFactory,
1483
+ dispatchRequest,
1495
1484
  initWorkerRuntime,
1485
+ kDispatchActivate,
1486
+ kDispatchInstall,
1487
+ kServiceWorker,
1496
1488
  parseCookieHeader,
1497
1489
  parseSetCookieHeader,
1490
+ runLifecycle,
1498
1491
  serializeCookie,
1499
1492
  startWorkerMessageLoop
1500
1493
  };