@b9g/platform 0.1.14-beta.3 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -203,8 +203,6 @@ const platform = new CloudflarePlatform({
203
203
 
204
204
  ### Types
205
205
 
206
- - `Platform` - Platform interface
207
- - `PlatformConfig` - Platform configuration options
208
206
  - `ServerOptions` - Server configuration options
209
207
  - `Handler` - Request handler function type
210
208
  - `Server` - Server interface
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/platform",
3
- "version": "0.1.14-beta.3",
3
+ "version": "0.1.15",
4
4
  "description": "The portable meta-framework built on web standards.",
5
5
  "keywords": [
6
6
  "service-worker",
@@ -18,16 +18,16 @@
18
18
  "shovel"
19
19
  ],
20
20
  "dependencies": {
21
- "@b9g/async-context": "^0.2.0-beta.0",
22
- "@b9g/cache": "^0.2.0-beta.0",
21
+ "@b9g/async-context": "^0.2.0",
22
+ "@b9g/cache": "^0.2.0",
23
23
  "@b9g/filesystem": "^0.1.8",
24
24
  "@logtape/logtape": "^1.2.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@b9g/libuild": "^0.1.20",
28
- "@b9g/node-webworker": "^0.2.0-beta.1",
29
- "@b9g/platform-bun": "^0.1.12-beta.0",
30
- "@b9g/platform-node": "^0.1.14-beta.0"
28
+ "@b9g/node-webworker": "^0.2.0",
29
+ "@b9g/platform-bun": "^0.1.13",
30
+ "@b9g/platform-node": "^0.1.15"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@b9g/zen": "^0.1.6"
@@ -71,6 +71,14 @@
71
71
  "./config.js": {
72
72
  "types": "./src/config.d.ts",
73
73
  "import": "./src/config.js"
74
+ },
75
+ "./module": {
76
+ "types": "./src/module.d.ts",
77
+ "import": "./src/module.js"
78
+ },
79
+ "./module.js": {
80
+ "types": "./src/module.d.ts",
81
+ "import": "./src/module.js"
74
82
  }
75
83
  }
76
84
  }
package/src/index.d.ts CHANGED
@@ -10,15 +10,8 @@
10
10
  * - Platform interface and base classes
11
11
  * - ServiceWorkerPool for multi-worker execution
12
12
  */
13
- import type { DirectoryStorage } from "@b9g/filesystem";
14
13
  import { CustomLoggerStorage, type LoggerStorage } from "./runtime.js";
15
14
  export { validateConfig, ConfigValidationError } from "./config.js";
16
- /**
17
- * Platform configuration
18
- * Extended by platform-specific implementations (NodePlatformOptions, etc.)
19
- */
20
- export interface PlatformConfig {
21
- }
22
15
  /**
23
16
  * Server options for platform implementations
24
17
  */
@@ -81,14 +74,16 @@ export interface ServiceWorkerInstance {
81
74
  dispose(): Promise<void>;
82
75
  }
83
76
  /**
84
- * Production entry points returned by getProductionEntryPoints().
77
+ * Entry points returned by getEntryPoints().
85
78
  * Each key is the output filename (without .js), value is the code.
86
79
  *
87
80
  * Examples:
88
81
  * - Cloudflare: { "worker": "<code>" } - single worker file
89
82
  * - Node/Bun: { "index": "<supervisor>", "worker": "<worker>" } - two files
90
83
  */
91
- export type ProductionEntryPoints = Record<string, string>;
84
+ export type EntryPoints = Record<string, string>;
85
+ /** @deprecated Use EntryPoints instead */
86
+ export type ProductionEntryPoints = EntryPoints;
92
87
  /**
93
88
  * ESBuild configuration subset that platforms can customize
94
89
  */
@@ -138,100 +133,6 @@ export interface ShovelServiceWorkerContainer extends ServiceWorkerContainer {
138
133
  /** Internal: Reload workers (for hot reload) */
139
134
  reloadWorkers(entrypoint: string): Promise<void>;
140
135
  }
141
- /**
142
- * Platform interface - ServiceWorker entrypoint loader for JavaScript runtimes
143
- *
144
- * The core responsibility: "Take a ServiceWorker-style app file and make it run in this environment"
145
- */
146
- export interface Platform {
147
- /**
148
- * Platform name for identification
149
- */
150
- readonly name: string;
151
- /**
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
159
- */
160
- listen(): Promise<Server>;
161
- /**
162
- * Close server and terminate workers (Node/Bun only)
163
- */
164
- close(): Promise<void>;
165
- /**
166
- * SUPPORTING UTILITY - Create server instance for this platform
167
- */
168
- createServer(handler: Handler, options?: ServerOptions): Server;
169
- /**
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.
174
- *
175
- * Platform determines the structure:
176
- * - Cloudflare: { "worker": "<code>" } - single worker file
177
- * - Node/Bun: { "index": "<supervisor>", "worker": "<runtime + user code>" }
178
- *
179
- * The user's entrypoint code is statically imported into the appropriate file.
180
- *
181
- * @param userEntryPath - Path to user's entrypoint (will be imported)
182
- */
183
- getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
184
- /**
185
- * BUILD SUPPORT - Get platform-specific esbuild configuration
186
- *
187
- * Returns partial esbuild config that the CLI merges with common settings.
188
- * Includes platform target, conditions, externals, and defines.
189
- */
190
- getESBuildConfig(): PlatformESBuildConfig;
191
- /**
192
- * BUILD SUPPORT - Get platform-specific defaults for config generation
193
- *
194
- * Returns defaults for directories, caches, etc. that get merged with
195
- * user config at build time. These are used by generateConfigModule()
196
- * to create static imports for the default implementations.
197
- */
198
- getDefaults(): PlatformDefaults;
199
- /**
200
- * Create cache storage for this platform
201
- * Uses platform-specific defaults, overridable via shovel.json config
202
- */
203
- createCaches(): Promise<CacheStorage>;
204
- /**
205
- * Create directory storage for this platform
206
- * Uses platform-specific defaults, overridable via shovel.json config
207
- */
208
- createDirectories(): Promise<DirectoryStorage>;
209
- /**
210
- * Create logger storage for this platform
211
- * Uses platform-specific defaults, overridable via shovel.json config
212
- */
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>;
223
- }
224
- /**
225
- * Platform registry - internal implementation
226
- */
227
- interface PlatformRegistry {
228
- /** Register a platform implementation */
229
- register(name: string, platform: any): void;
230
- /** Get platform by name */
231
- get(name: string): any | undefined;
232
- /** Get all registered platforms */
233
- list(): string[];
234
- }
235
136
  /**
236
137
  * Detect the current JavaScript runtime
237
138
  */
@@ -265,54 +166,6 @@ export declare function resolvePlatform(options: {
265
166
  platform?: string;
266
167
  };
267
168
  }): string;
268
- /**
269
- * Base platform class with shared adapter loading logic
270
- * Platform implementations extend this and provide platform-specific methods
271
- */
272
- export declare abstract class BasePlatform implements Platform {
273
- config: PlatformConfig;
274
- constructor(config?: PlatformConfig);
275
- abstract readonly name: string;
276
- abstract readonly serviceWorker: ShovelServiceWorkerContainer;
277
- abstract listen(): Promise<Server>;
278
- abstract close(): Promise<void>;
279
- abstract createServer(handler: any, options?: any): any;
280
- /**
281
- * Get production entry points for bundling
282
- * Subclasses must override to provide platform-specific entry points
283
- */
284
- abstract getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
285
- /**
286
- * Get platform-specific esbuild configuration
287
- * Subclasses should override to provide platform-specific config
288
- */
289
- abstract getESBuildConfig(): PlatformESBuildConfig;
290
- /**
291
- * Get platform-specific defaults for config generation
292
- * Subclasses should override to provide platform-specific defaults
293
- */
294
- abstract getDefaults(): PlatformDefaults;
295
- /**
296
- * Create cache storage for this platform
297
- * Subclasses must override to provide platform-specific implementation
298
- */
299
- abstract createCaches(): Promise<CacheStorage>;
300
- /**
301
- * Create directory storage for this platform
302
- * Subclasses must override to provide platform-specific implementation
303
- */
304
- abstract createDirectories(): Promise<DirectoryStorage>;
305
- /**
306
- * Create logger storage for this platform
307
- * Subclasses must override to provide platform-specific implementation
308
- */
309
- abstract createLoggers(): Promise<LoggerStorage>;
310
- /**
311
- * Dispose of platform resources
312
- * Subclasses should override to clean up worker pools, connections, etc.
313
- */
314
- dispose(): Promise<void>;
315
- }
316
169
  /**
317
170
  * Merge platform defaults with user config
318
171
  *
@@ -324,39 +177,6 @@ export declare abstract class BasePlatform implements Platform {
324
177
  * @returns Merged config with all entries
325
178
  */
326
179
  export declare function mergeConfigWithDefaults(defaults: Record<string, Record<string, unknown>>, userConfig: Record<string, Record<string, unknown>> | undefined): Record<string, Record<string, unknown>>;
327
- /**
328
- * Global platform registry
329
- */
330
- declare class DefaultPlatformRegistry implements PlatformRegistry {
331
- #private;
332
- constructor();
333
- register(name: string, platform: Platform): void;
334
- get(name: string): Platform | undefined;
335
- list(): string[];
336
- }
337
- /**
338
- * Global platform registry instance
339
- */
340
- export declare const platformRegistry: DefaultPlatformRegistry;
341
- /**
342
- * Get platform by name with error handling
343
- */
344
- export declare function getPlatform(name?: string): Platform;
345
- /**
346
- * Get platform with async auto-registration fallback
347
- */
348
- export declare function getPlatformAsync(name?: string): Promise<Platform>;
349
- /**
350
- * Common interface for ServiceWorker runtimes
351
- */
352
- export interface ServiceWorkerRuntime {
353
- init(): Promise<void>;
354
- load(entrypoint: string): Promise<void>;
355
- handleRequest(request: Request): Promise<Response>;
356
- terminate(): Promise<void>;
357
- readonly workerCount: number;
358
- readonly ready: boolean;
359
- }
360
180
  /**
361
181
  * Worker pool options
362
182
  */
@@ -370,39 +190,6 @@ export interface WorkerPoolOptions {
370
190
  /** Custom worker factory (if not provided, uses createWebWorker) */
371
191
  createWorker?: (entrypoint: string) => Worker | Promise<Worker>;
372
192
  }
373
- export interface WorkerMessage {
374
- type: string;
375
- [key: string]: any;
376
- }
377
- export interface WorkerRequest extends WorkerMessage {
378
- type: "request";
379
- request: {
380
- url: string;
381
- method: string;
382
- headers: Record<string, string>;
383
- body?: ArrayBuffer | null;
384
- };
385
- requestID: number;
386
- }
387
- export interface WorkerResponse extends WorkerMessage {
388
- type: "response";
389
- response: {
390
- status: number;
391
- statusText: string;
392
- headers: Record<string, string>;
393
- body: ArrayBuffer;
394
- };
395
- requestID: number;
396
- }
397
- export interface WorkerReadyMessage extends WorkerMessage {
398
- type: "ready";
399
- }
400
- export interface WorkerErrorMessage extends WorkerMessage {
401
- type: "error";
402
- error: string;
403
- stack?: string;
404
- requestID?: number;
405
- }
406
193
  /**
407
194
  * ServiceWorkerPool - manages a pool of ServiceWorker instances
408
195
  *
package/src/index.js CHANGED
@@ -55,18 +55,6 @@ function resolvePlatform(options) {
55
55
  }
56
56
  return detectDevelopmentPlatform();
57
57
  }
58
- var BasePlatform = class {
59
- config;
60
- constructor(config = {}) {
61
- this.config = config;
62
- }
63
- /**
64
- * Dispose of platform resources
65
- * Subclasses should override to clean up worker pools, connections, etc.
66
- */
67
- async dispose() {
68
- }
69
- };
70
58
  function mergeConfigWithDefaults(defaults, userConfig) {
71
59
  const user = userConfig ?? {};
72
60
  const allNames = /* @__PURE__ */ new Set([...Object.keys(defaults), ...Object.keys(user)]);
@@ -76,62 +64,6 @@ function mergeConfigWithDefaults(defaults, userConfig) {
76
64
  }
77
65
  return merged;
78
66
  }
79
- var DefaultPlatformRegistry = class {
80
- #platforms;
81
- constructor() {
82
- this.#platforms = /* @__PURE__ */ new Map();
83
- }
84
- register(name, platform) {
85
- this.#platforms.set(name, platform);
86
- }
87
- get(name) {
88
- return this.#platforms.get(name);
89
- }
90
- list() {
91
- return Array.from(this.#platforms.keys());
92
- }
93
- };
94
- var platformRegistry = new DefaultPlatformRegistry();
95
- function getPlatform(name) {
96
- if (name) {
97
- const platform2 = platformRegistry.get(name);
98
- if (!platform2) {
99
- const available = platformRegistry.list();
100
- throw new Error(
101
- `Platform '${name}' not found. Available platforms: ${available.join(", ")}`
102
- );
103
- }
104
- return platform2;
105
- }
106
- const platformName = detectDeploymentPlatform() || detectDevelopmentPlatform();
107
- const platform = platformRegistry.get(platformName);
108
- if (!platform) {
109
- throw new Error(
110
- `Detected platform '${platformName}' not registered. Please register it manually or specify a platform name.`
111
- );
112
- }
113
- return platform;
114
- }
115
- async function getPlatformAsync(name) {
116
- if (name) {
117
- const platform2 = platformRegistry.get(name);
118
- if (!platform2) {
119
- const available = platformRegistry.list();
120
- throw new Error(
121
- `Platform '${name}' not found. Available platforms: ${available.join(", ")}`
122
- );
123
- }
124
- return platform2;
125
- }
126
- const platformName = detectDeploymentPlatform() || detectDevelopmentPlatform();
127
- const platform = platformRegistry.get(platformName);
128
- if (!platform) {
129
- throw new Error(
130
- `Detected platform '${platformName}' not registered. Please register it manually using platformRegistry.register().`
131
- );
132
- }
133
- return platform;
134
- }
135
67
  async function createWebWorker(workerScript) {
136
68
  if (typeof Worker !== "undefined") {
137
69
  return new Worker(workerScript, { type: "module" });
@@ -454,7 +386,9 @@ var ServiceWorkerPool = class {
454
386
  createPromises.push(this.#createWorker(entrypoint));
455
387
  }
456
388
  await Promise.all(createPromises);
457
- logger.debug("All workers reloaded", { entrypoint });
389
+ logger.info("Reloaded {count} workers", {
390
+ count: this.#options.workerCount
391
+ });
458
392
  } catch (error) {
459
393
  const waiters = this.#workerAvailableWaiters;
460
394
  this.#workerAvailableWaiters = [];
@@ -500,7 +434,6 @@ var ServiceWorkerPool = class {
500
434
  }
501
435
  };
502
436
  export {
503
- BasePlatform,
504
437
  ConfigValidationError,
505
438
  CustomDatabaseStorage,
506
439
  CustomLoggerStorage,
@@ -509,10 +442,7 @@ export {
509
442
  detectDeploymentPlatform,
510
443
  detectDevelopmentPlatform,
511
444
  detectRuntime,
512
- getPlatform,
513
- getPlatformAsync,
514
445
  mergeConfigWithDefaults,
515
- platformRegistry,
516
446
  resolvePlatform,
517
447
  validateConfig
518
448
  };
@@ -0,0 +1,142 @@
1
+ /// <reference path="./globals.d.ts" />
2
+ /// <reference path="./shovel-config.d.ts" />
3
+ /**
4
+ * Platform Module Interface
5
+ *
6
+ * Platforms are modules, not classes. Each platform exports functions
7
+ * that the CLI and generated entry code use.
8
+ *
9
+ * Build-time functions: Used by CLI/bundler, never bundled into prod
10
+ * Runtime functions: Imported by generated entry code, bundled into prod
11
+ *
12
+ * This separation enables tree-shaking - dev dependencies like Miniflare
13
+ * never end up in production bundles.
14
+ */
15
+ /**
16
+ * Entry points returned by getEntryPoints().
17
+ * Maps output filename (without .js) to source code.
18
+ */
19
+ export type EntryPoints = Record<string, string>;
20
+ /**
21
+ * ESBuild configuration that platforms can customize.
22
+ */
23
+ export interface ESBuildConfig {
24
+ /** Target platform: "node" or "browser" */
25
+ platform?: "node" | "browser";
26
+ /** Export conditions for package.json resolution */
27
+ conditions?: string[];
28
+ /** Modules to exclude from bundling */
29
+ external?: string[];
30
+ /** Define replacements */
31
+ define?: Record<string, string>;
32
+ }
33
+ /**
34
+ * Platform defaults for config generation.
35
+ * Module paths that get statically imported at build time.
36
+ */
37
+ export interface PlatformDefaults {
38
+ caches?: Record<string, {
39
+ module: string;
40
+ export?: string;
41
+ [key: string]: unknown;
42
+ }>;
43
+ directories?: Record<string, {
44
+ module: string;
45
+ export?: string;
46
+ [key: string]: unknown;
47
+ }>;
48
+ }
49
+ /**
50
+ * Options for creating a dev server.
51
+ */
52
+ export interface DevServerOptions {
53
+ /** Port to listen on */
54
+ port: number;
55
+ /** Host to bind to */
56
+ host: string;
57
+ /** Path to the built worker entry */
58
+ workerPath: string;
59
+ /** Number of workers (Node/Bun only) */
60
+ workers?: number;
61
+ }
62
+ /**
63
+ * Dev server instance returned by createDevServer().
64
+ * Abstracts over Miniflare, worker pools, etc.
65
+ */
66
+ export interface DevServer {
67
+ /** Server URL */
68
+ readonly url: string;
69
+ /** Reload workers with new entry */
70
+ reload(workerPath: string): Promise<void>;
71
+ /** Shut down the server */
72
+ close(): Promise<void>;
73
+ }
74
+ /**
75
+ * Result from installGlobals().
76
+ */
77
+ export interface RuntimeContext {
78
+ /** ServiceWorker registration for dispatching events */
79
+ registration: ServiceWorkerRegistration;
80
+ }
81
+ /**
82
+ * Server instance for Node/Bun.
83
+ */
84
+ export interface Server {
85
+ /** Start listening */
86
+ listen(): Promise<void>;
87
+ /** Stop the server */
88
+ close(): Promise<void>;
89
+ /** Server address */
90
+ readonly url: string;
91
+ }
92
+ /**
93
+ * Handler function type for HTTP requests.
94
+ */
95
+ export type RequestHandler = (request: Request) => Response | Promise<Response>;
96
+ /**
97
+ * Fetch handler for Cloudflare Workers.
98
+ * ExecutionContext is Cloudflare-specific, so we use a generic type here.
99
+ */
100
+ export type FetchHandler = (request: Request, env: unknown, ctx: {
101
+ waitUntil(promise: Promise<unknown>): void;
102
+ passThroughOnException(): void;
103
+ }) => Response | Promise<Response>;
104
+ /**
105
+ * What a platform module exports.
106
+ *
107
+ * This is not enforced at runtime - it's documentation of the contract.
108
+ * Each platform module should export these functions.
109
+ *
110
+ * Build-time exports (from main module):
111
+ * - name: string
112
+ * - getEntryPoints(userPath, mode): EntryPoints
113
+ * - getESBuildConfig(): ESBuildConfig
114
+ * - getDefaults(): PlatformDefaults
115
+ * - createDevServer(options): Promise<DevServer>
116
+ *
117
+ * Runtime exports (from /runtime subpath):
118
+ * - installGlobals(config): Promise<RuntimeContext>
119
+ * - Platform-specific: createServer, createFetchHandler, etc.
120
+ */
121
+ export interface PlatformModule {
122
+ /** Platform identifier */
123
+ readonly name: string;
124
+ /** Generate entry point code for bundling */
125
+ getEntryPoints(userEntryPath: string, mode: "development" | "production"): EntryPoints;
126
+ /** Get ESBuild configuration for this platform */
127
+ getESBuildConfig(): ESBuildConfig;
128
+ /** Get default configs for caches, directories, etc. */
129
+ getDefaults(): PlatformDefaults;
130
+ /** Create a dev server (imports heavy deps like Miniflare) */
131
+ createDevServer(options: DevServerOptions): Promise<DevServer>;
132
+ }
133
+ /**
134
+ * What a platform runtime module exports.
135
+ *
136
+ * Each platform's /runtime subpath exports these.
137
+ * The specific functions vary by platform.
138
+ */
139
+ export interface PlatformRuntimeModule {
140
+ /** Install ServiceWorker globals (caches, directories, loggers) */
141
+ installGlobals(config: unknown): Promise<RuntimeContext>;
142
+ }
package/src/module.js ADDED
@@ -0,0 +1 @@
1
+ /// <reference types="./module.d.ts" />
package/src/runtime.js CHANGED
@@ -324,9 +324,10 @@ function createDatabaseFactory(configs) {
324
324
  let Database;
325
325
  try {
326
326
  ({ Database } = await import("@b9g/zen"));
327
- } catch {
327
+ } catch (e) {
328
328
  throw new Error(
329
- "@b9g/zen is required for database support. Install it with: npm install @b9g/zen"
329
+ "@b9g/zen is required for database support. Install it with: npm install @b9g/zen",
330
+ { cause: e }
330
331
  );
331
332
  }
332
333
  const driver = new impl(url, driverOptions);
@@ -634,11 +635,11 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
634
635
  this[kServiceWorker]._setState("installing");
635
636
  return new Promise((resolve, reject) => {
636
637
  const event = new ShovelInstallEvent();
637
- process.nextTick(() => {
638
+ queueMicrotask(() => {
638
639
  try {
639
640
  this.dispatchEvent(event);
640
641
  } catch (error) {
641
- process.nextTick(() => {
642
+ queueMicrotask(() => {
642
643
  throw error;
643
644
  });
644
645
  }
@@ -670,11 +671,11 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
670
671
  this[kServiceWorker]._setState("activating");
671
672
  return new Promise((resolve, reject) => {
672
673
  const event = new ShovelActivateEvent();
673
- process.nextTick(() => {
674
+ queueMicrotask(() => {
674
675
  try {
675
676
  this.dispatchEvent(event);
676
677
  } catch (error) {
677
- process.nextTick(() => {
678
+ queueMicrotask(() => {
678
679
  throw error;
679
680
  });
680
681
  }
@@ -705,8 +706,8 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
705
706
  * @param event - The fetch event to handle (created by platform adapter)
706
707
  */
707
708
  async [kHandleRequest](event) {
708
- if (this[kServiceWorker].state !== "activated") {
709
- throw new Error("ServiceWorker not activated");
709
+ if (this[kServiceWorker].state === "parsed") {
710
+ throw new Error("ServiceWorker not initialized");
710
711
  }
711
712
  return cookieStoreStorage.run(event.cookieStore, async () => {
712
713
  this.dispatchEvent(event);