@b9g/platform-bun 0.1.11 → 0.1.12-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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/index.d.ts +85 -25
  3. package/src/index.js +264 -373
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/platform-bun",
3
- "version": "0.1.11",
3
+ "version": "0.1.12-beta.0",
4
4
  "description": "Bun platform adapter for Shovel with hot reloading and built-in TypeScript/JSX support",
5
5
  "keywords": [
6
6
  "shovel",
@@ -15,7 +15,7 @@
15
15
  "@b9g/assets": "^0.2.0-beta.0",
16
16
  "@b9g/cache": "^0.2.0-beta.0",
17
17
  "@b9g/http-errors": "^0.2.0-beta.0",
18
- "@b9g/platform": "^0.1.13",
18
+ "@b9g/platform": "^0.1.14-beta.0",
19
19
  "@logtape/logtape": "^1.2.0"
20
20
  },
21
21
  "devDependencies": {
package/src/index.d.ts CHANGED
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Provides built-in TypeScript/JSX support and simplified server setup for Bun environments.
5
5
  */
6
- import { BasePlatform, type PlatformConfig, type PlatformDefaults, type Handler, type Server, type ServerOptions, type ServiceWorkerOptions, type ServiceWorkerInstance, type EntryWrapperOptions, type PlatformESBuildConfig, ServiceWorkerPool, CustomLoggerStorage, CustomDatabaseStorage } from "@b9g/platform";
7
- import { type ShovelConfig } from "@b9g/platform/runtime";
8
6
  import { CustomCacheStorage } from "@b9g/cache";
9
7
  import { CustomDirectoryStorage } from "@b9g/filesystem";
8
+ import { BasePlatform, type PlatformConfig, type PlatformDefaults, type Handler, type Server, type ServerOptions, type PlatformESBuildConfig, type ProductionEntryPoints, ServiceWorkerPool, CustomLoggerStorage, CustomDatabaseStorage } from "@b9g/platform";
9
+ import { type ShovelConfig } from "@b9g/platform/runtime";
10
10
  export interface BunPlatformOptions extends PlatformConfig {
11
11
  /** Port for development server (default: 3000) */
12
12
  port?: number;
@@ -19,6 +19,55 @@ export interface BunPlatformOptions extends PlatformConfig {
19
19
  /** Shovel configuration (caches, directories, etc.) */
20
20
  config?: ShovelConfig;
21
21
  }
22
+ /**
23
+ * Bun ServiceWorkerContainer implementation
24
+ * Manages ServiceWorker registrations backed by native Web Workers
25
+ *
26
+ * Note: In Bun's production model, workers handle their own HTTP servers
27
+ * via reusePort, so the supervisor doesn't route requests through the pool.
28
+ * This container is mainly for worker lifecycle management.
29
+ */
30
+ export declare class BunServiceWorkerContainer extends EventTarget implements ServiceWorkerContainer {
31
+ #private;
32
+ readonly controller: ServiceWorker | null;
33
+ oncontrollerchange: ((ev: Event) => unknown) | null;
34
+ onmessage: ((ev: MessageEvent) => unknown) | null;
35
+ onmessageerror: ((ev: MessageEvent) => unknown) | null;
36
+ constructor(platform: BunPlatform);
37
+ /**
38
+ * Register a ServiceWorker script
39
+ * Spawns Web Workers (each with their own HTTP server in production)
40
+ */
41
+ register(scriptURL: string | URL, options?: RegistrationOptions): Promise<ServiceWorkerRegistration>;
42
+ /**
43
+ * Get registration for scope
44
+ */
45
+ getRegistration(scope?: string): Promise<ServiceWorkerRegistration | undefined>;
46
+ /**
47
+ * Get all registrations
48
+ */
49
+ getRegistrations(): Promise<readonly ServiceWorkerRegistration[]>;
50
+ /**
51
+ * Start receiving messages (no-op in server context)
52
+ */
53
+ startMessages(): void;
54
+ /**
55
+ * Ready promise - resolves when a registration is active
56
+ */
57
+ get ready(): Promise<ServiceWorkerRegistration>;
58
+ /**
59
+ * Internal: Get worker pool for request handling
60
+ */
61
+ get pool(): ServiceWorkerPool | undefined;
62
+ /**
63
+ * Internal: Terminate workers and dispose cache storage
64
+ */
65
+ terminate(): Promise<void>;
66
+ /**
67
+ * Internal: Reload workers (for hot reload)
68
+ */
69
+ reloadWorkers(entrypoint: string): Promise<void>;
70
+ }
22
71
  /**
23
72
  * Bun platform implementation
24
73
  * ServiceWorker entrypoint loader for Bun with native TypeScript/JSX support
@@ -26,6 +75,7 @@ export interface BunPlatformOptions extends PlatformConfig {
26
75
  export declare class BunPlatform extends BasePlatform {
27
76
  #private;
28
77
  readonly name: string;
78
+ readonly serviceWorker: BunServiceWorkerContainer;
29
79
  constructor(options?: BunPlatformOptions);
30
80
  /**
31
81
  * Get options for testing
@@ -38,26 +88,35 @@ export declare class BunPlatform extends BasePlatform {
38
88
  config?: ShovelConfig;
39
89
  };
40
90
  /**
41
- * Get/set worker pool for testing
42
- */
43
- get workerPool(): ServiceWorkerPool | undefined;
44
- set workerPool(pool: ServiceWorkerPool | undefined);
45
- /**
46
- * Create cache storage using config from shovel.json
47
- * Merges with runtime defaults (actual class references) for fallback behavior
91
+ * Create cache storage for Bun
92
+ *
93
+ * Default: MemoryCache (in-process LRU cache).
94
+ * Override via shovel.json caches config.
95
+ * Note: Used for dev/testing - production uses generated config module.
48
96
  */
49
97
  createCaches(): Promise<CustomCacheStorage>;
50
98
  /**
51
- * Create directory storage using config from shovel.json
52
- * Merges with runtime defaults (actual class references) for fallback behavior
99
+ * Create directory storage for Bun
100
+ *
101
+ * Defaults:
102
+ * - server: NodeFSDirectory at cwd (app files)
103
+ * - public: NodeFSDirectory at cwd (static assets)
104
+ * - tmp: NodeFSDirectory at OS temp dir
105
+ *
106
+ * Override via shovel.json directories config.
53
107
  */
54
108
  createDirectories(): Promise<CustomDirectoryStorage>;
55
109
  /**
56
- * Create logger storage using config from shovel.json
110
+ * Create logger storage for Bun
111
+ *
112
+ * Uses LogTape for structured logging.
57
113
  */
58
114
  createLoggers(): Promise<CustomLoggerStorage>;
59
115
  /**
60
- * Create database storage from declarative config in shovel.json
116
+ * Create database storage for Bun
117
+ *
118
+ * Returns undefined if no databases configured in shovel.json.
119
+ * Supports SQLite via bun:sqlite.
61
120
  */
62
121
  createDatabases(configOverride?: BunPlatformOptions["config"]): CustomDatabaseStorage | undefined;
63
122
  /**
@@ -65,28 +124,29 @@ export declare class BunPlatform extends BasePlatform {
65
124
  */
66
125
  createServer(handler: Handler, options?: ServerOptions): Server;
67
126
  /**
68
- * Load and run a ServiceWorker-style entrypoint with Bun
69
- * Uses native Web Workers with the common WorkerPool
127
+ * Start listening for connections using pool's handlers
128
+ */
129
+ listen(): Promise<Server>;
130
+ /**
131
+ * Close the server
70
132
  */
71
- loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
133
+ close(): Promise<void>;
72
134
  /**
73
135
  * Reload workers for hot reloading (called by CLI)
74
136
  * @param entrypoint - Path to the new entrypoint (hashed filename)
75
137
  */
76
138
  reloadWorkers(entrypoint: string): Promise<void>;
77
139
  /**
78
- * Get virtual entry wrapper for Bun
140
+ * Get production entry points for bundling.
79
141
  *
80
- * @param entryPath - Absolute path to user's entrypoint file
81
- * @param options - Entry wrapper options
82
- * @param options.type - "production" (default) or "worker"
83
- * @param options.outDir - Output directory (required for "worker" type)
142
+ * Bun produces two files:
143
+ * - index.js: Supervisor that spawns workers and handles signals
144
+ * - worker.js: Worker with its own HTTP server (uses reusePort for multi-worker)
84
145
  *
85
- * Returns:
86
- * - "production": Server entry with Bun.serve and reusePort
87
- * - "worker": Worker entry that sets up runtime and message loop
146
+ * Unlike Node.js, Bun workers each bind their own server with reusePort,
147
+ * allowing the OS to load-balance across workers without message passing overhead.
88
148
  */
89
- getEntryWrapper(entryPath: string, options?: EntryWrapperOptions): string;
149
+ getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
90
150
  /**
91
151
  * Get Bun-specific esbuild configuration
92
152
  *
package/src/index.js CHANGED
@@ -2,162 +2,148 @@
2
2
  // src/index.ts
3
3
  import { builtinModules } from "node:module";
4
4
  import { tmpdir } from "node:os";
5
+ import * as Path from "node:path";
6
+ import { getLogger } from "@logtape/logtape";
7
+ import { CustomCacheStorage } from "@b9g/cache";
8
+ import { MemoryCache } from "@b9g/cache/memory";
9
+ import { CustomDirectoryStorage } from "@b9g/filesystem";
10
+ import { NodeFSDirectory } from "@b9g/filesystem/node-fs";
11
+ import { InternalServerError, isHTTPError } from "@b9g/http-errors";
5
12
  import {
6
13
  BasePlatform,
7
14
  ServiceWorkerPool,
8
- SingleThreadedRuntime,
9
15
  CustomLoggerStorage,
10
16
  CustomDatabaseStorage,
11
- createDatabaseFactory
17
+ createDatabaseFactory,
18
+ mergeConfigWithDefaults
12
19
  } from "@b9g/platform";
13
20
  import {
21
+ ShovelServiceWorkerRegistration,
22
+ kServiceWorker,
14
23
  createCacheFactory,
15
24
  createDirectoryFactory
16
25
  } from "@b9g/platform/runtime";
17
- import { CustomCacheStorage } from "@b9g/cache";
18
- import { MemoryCache } from "@b9g/cache/memory";
19
- import { CustomDirectoryStorage } from "@b9g/filesystem";
20
- import { NodeFSDirectory } from "@b9g/filesystem/node-fs";
21
- import { InternalServerError, isHTTPError } from "@b9g/http-errors";
22
- import { getLogger } from "@logtape/logtape";
23
- import * as Path from "path";
24
26
  import { MemoryCache as MemoryCache2 } from "@b9g/cache/memory";
25
- var entryTemplate = `// Bun Production Server Entry
26
- import {tmpdir} from "os"; // For [tmpdir] config expressions
27
- import {getLogger} from "@logtape/logtape";
28
- import {configureLogging} from "@b9g/platform/runtime";
29
- import {config} from "shovel:config"; // Virtual module - resolved at build time
30
- import BunPlatform from "@b9g/platform-bun";
31
-
32
- // Configure logging before anything else
33
- await configureLogging(config.logging);
34
-
35
- const logger = getLogger(["shovel", "platform"]);
36
-
37
- // Configuration from shovel:config
38
- const PORT = config.port;
39
- const HOST = config.host;
40
- const WORKERS = config.workers;
41
-
42
- // Use explicit marker instead of Bun.isMainThread
43
- // This handles the edge case where Shovel's build output is embedded in another worker
44
- const isShovelWorker = process.env.SHOVEL_SPAWNED_WORKER === "1";
45
-
46
- if (isShovelWorker) {
47
- // Worker thread: runs BOTH server AND ServiceWorker
48
- const platform = new BunPlatform({port: PORT, host: HOST, workers: 1});
49
- const userCodePath = new URL("./server.js", import.meta.url).pathname;
50
- const serviceWorker = await platform.loadServiceWorker(userCodePath);
51
-
52
- Bun.serve({
53
- port: PORT,
54
- hostname: HOST,
55
- // Only need reusePort for multi-worker (multiple listeners on same port)
56
- reusePort: WORKERS > 1,
57
- fetch: serviceWorker.handleRequest,
58
- });
59
-
60
- // Signal ready to main thread
61
- postMessage({type: "ready", thread: Bun.threadId});
62
- logger.info("Worker started", {port: PORT, thread: Bun.threadId});
63
- } else {
64
- // Main thread: supervisor only - ALWAYS spawn workers (even for workers:1)
65
- // This ensures ServiceWorker code always runs in a worker thread for dev/prod parity
66
-
67
- // Port availability check - fail fast if port is in use
68
- // Prevents accidental port sharing with other processes
69
- const checkPort = async () => {
70
- try {
71
- const testServer = Bun.serve({port: PORT, hostname: HOST, fetch: () => new Response()});
72
- testServer.stop();
73
- } catch (err) {
74
- logger.error("Port unavailable", {port: PORT, host: HOST, error: err});
75
- process.exit(1);
76
- }
77
- };
78
- await checkPort();
79
-
80
- let shuttingDown = false;
81
- const workers = [];
82
- let readyCount = 0;
83
-
84
- for (let i = 0; i < WORKERS; i++) {
85
- const worker = new Worker(import.meta.path, {
86
- env: {...process.env, SHOVEL_SPAWNED_WORKER: "1"},
87
- });
88
-
89
- worker.onmessage = (event) => {
90
- if (event.data.type === "ready") {
91
- readyCount++;
92
- if (readyCount === WORKERS) {
93
- logger.info("All workers ready", {count: WORKERS, port: PORT});
94
- }
95
- }
96
- };
97
-
98
- worker.onerror = (error) => {
99
- logger.error("Worker error", {error: error.message});
100
- };
101
-
102
- // If a worker crashes, fail fast - let process supervisor handle restarts
103
- worker.addEventListener("close", () => {
104
- if (shuttingDown) return;
105
- logger.error("Worker crashed, exiting");
106
- process.exit(1);
107
- });
108
-
109
- workers.push(worker);
110
- }
111
-
112
- logger.info("Spawned workers", {count: WORKERS, port: PORT});
113
-
114
- // Graceful shutdown
115
- const shutdown = async () => {
116
- shuttingDown = true;
117
- logger.info("Shutting down workers");
118
- for (const worker of workers) {
119
- worker.terminate();
120
- }
121
- process.exit(0);
122
- };
123
-
124
- process.on("SIGINT", shutdown);
125
- process.on("SIGTERM", shutdown);
126
- }
127
- `;
128
- var workerEntryTemplate = `// Worker Entry for ServiceWorkerPool
129
- // This file sets up the ServiceWorker runtime and message loop
130
- import {tmpdir} from "os"; // For [tmpdir] config expressions
131
- import {config} from "shovel:config";
132
- import {initWorkerRuntime, startWorkerMessageLoop, configureLogging} from "@b9g/platform/runtime";
133
-
134
- // Configure logging before anything else
135
- await configureLogging(config.logging);
136
-
137
- // Initialize the worker runtime (installs ServiceWorker globals)
138
- // Platform defaults and paths are already resolved at build time
139
- const {registration, databases} = await initWorkerRuntime({config});
140
-
141
- // Import user code (registers event handlers via addEventListener)
142
- // Must use dynamic import to ensure globals are installed first
143
- await import("__USER_ENTRY__");
144
-
145
- // Run ServiceWorker lifecycle
146
- await registration.install();
147
- await registration.activate();
148
-
149
- // Start the message loop (handles request/response messages from main thread)
150
- // Pass databases so they can be closed on graceful shutdown
151
- startWorkerMessageLoop({registration, databases});
152
- `;
153
27
  var logger = getLogger(["shovel", "platform"]);
28
+ var BunServiceWorkerContainer = class extends EventTarget {
29
+ #platform;
30
+ #pool;
31
+ #cacheStorage;
32
+ #registration;
33
+ #readyPromise;
34
+ #readyResolve;
35
+ // Standard ServiceWorkerContainer properties
36
+ controller;
37
+ oncontrollerchange;
38
+ onmessage;
39
+ onmessageerror;
40
+ constructor(platform) {
41
+ super();
42
+ this.#platform = platform;
43
+ this.#readyPromise = new Promise((resolve2) => {
44
+ this.#readyResolve = resolve2;
45
+ });
46
+ this.controller = null;
47
+ this.oncontrollerchange = null;
48
+ this.onmessage = null;
49
+ this.onmessageerror = null;
50
+ }
51
+ /**
52
+ * Register a ServiceWorker script
53
+ * Spawns Web Workers (each with their own HTTP server in production)
54
+ */
55
+ async register(scriptURL, options) {
56
+ const urlStr = typeof scriptURL === "string" ? scriptURL : scriptURL.toString();
57
+ const scope = options?.scope ?? "/";
58
+ let entryPath;
59
+ if (urlStr.startsWith("file://")) {
60
+ entryPath = new URL(urlStr).pathname;
61
+ } else {
62
+ entryPath = Path.resolve(this.#platform.options.cwd, urlStr);
63
+ }
64
+ let config = this.#platform.options.config;
65
+ const configPath = Path.join(Path.dirname(entryPath), "config.js");
66
+ try {
67
+ const configModule = await import(configPath);
68
+ config = configModule.config ?? config;
69
+ } catch (error) {
70
+ logger.debug`Using platform config (no config.js found): ${error}`;
71
+ }
72
+ if (!this.#cacheStorage && config?.caches) {
73
+ this.#cacheStorage = new CustomCacheStorage(
74
+ createCacheFactory({ configs: config.caches })
75
+ );
76
+ }
77
+ if (this.#pool) {
78
+ await this.#pool.terminate();
79
+ }
80
+ this.#pool = new ServiceWorkerPool(
81
+ {
82
+ workerCount: this.#platform.options.workers,
83
+ createWorker: (entrypoint) => new Worker(entrypoint)
84
+ },
85
+ entryPath,
86
+ this.#cacheStorage
87
+ );
88
+ await this.#pool.init();
89
+ this.#registration = new ShovelServiceWorkerRegistration(scope, urlStr);
90
+ this.#registration[kServiceWorker]._setState("activated");
91
+ this.#readyResolve?.(this.#registration);
92
+ return this.#registration;
93
+ }
94
+ /**
95
+ * Get registration for scope
96
+ */
97
+ async getRegistration(scope) {
98
+ if (scope === void 0 || scope === "/" || scope === this.#registration?.scope) {
99
+ return this.#registration;
100
+ }
101
+ return void 0;
102
+ }
103
+ /**
104
+ * Get all registrations
105
+ */
106
+ async getRegistrations() {
107
+ return this.#registration ? [this.#registration] : [];
108
+ }
109
+ /**
110
+ * Start receiving messages (no-op in server context)
111
+ */
112
+ startMessages() {
113
+ }
114
+ /**
115
+ * Ready promise - resolves when a registration is active
116
+ */
117
+ get ready() {
118
+ return this.#readyPromise;
119
+ }
120
+ /**
121
+ * Internal: Get worker pool for request handling
122
+ */
123
+ get pool() {
124
+ return this.#pool;
125
+ }
126
+ /**
127
+ * Internal: Terminate workers and dispose cache storage
128
+ */
129
+ async terminate() {
130
+ await this.#pool?.terminate();
131
+ this.#pool = void 0;
132
+ await this.#cacheStorage?.dispose();
133
+ this.#cacheStorage = void 0;
134
+ }
135
+ /**
136
+ * Internal: Reload workers (for hot reload)
137
+ */
138
+ async reloadWorkers(entrypoint) {
139
+ await this.#pool?.reloadWorkers(entrypoint);
140
+ }
141
+ };
154
142
  var BunPlatform = class extends BasePlatform {
155
143
  name;
144
+ serviceWorker;
156
145
  #options;
157
- #workerPool;
158
- #singleThreadedRuntime;
159
- #cacheStorage;
160
- #directoryStorage;
146
+ #server;
161
147
  #databaseStorage;
162
148
  constructor(options = {}) {
163
149
  super(options);
@@ -170,6 +156,7 @@ var BunPlatform = class extends BasePlatform {
170
156
  cwd,
171
157
  config: options.config
172
158
  };
159
+ this.serviceWorker = new BunServiceWorkerContainer(this);
173
160
  }
174
161
  /**
175
162
  * Get options for testing
@@ -178,62 +165,55 @@ var BunPlatform = class extends BasePlatform {
178
165
  return this.#options;
179
166
  }
180
167
  /**
181
- * Get/set worker pool for testing
182
- */
183
- get workerPool() {
184
- return this.#workerPool;
185
- }
186
- set workerPool(pool) {
187
- this.#workerPool = pool;
188
- }
189
- /**
190
- * Create cache storage using config from shovel.json
191
- * Merges with runtime defaults (actual class references) for fallback behavior
168
+ * Create cache storage for Bun
169
+ *
170
+ * Default: MemoryCache (in-process LRU cache).
171
+ * Override via shovel.json caches config.
172
+ * Note: Used for dev/testing - production uses generated config module.
192
173
  */
193
174
  async createCaches() {
194
- const runtimeDefaults = {
195
- default: { impl: MemoryCache }
196
- };
197
- const userCaches = this.#options.config?.caches ?? {};
198
- const configs = {};
199
- const allNames = /* @__PURE__ */ new Set([
200
- ...Object.keys(runtimeDefaults),
201
- ...Object.keys(userCaches)
202
- ]);
203
- for (const name of allNames) {
204
- configs[name] = { ...runtimeDefaults[name], ...userCaches[name] };
205
- }
175
+ const defaults = { default: { impl: MemoryCache } };
176
+ const configs = mergeConfigWithDefaults(
177
+ defaults,
178
+ this.#options.config?.caches
179
+ );
206
180
  return new CustomCacheStorage(createCacheFactory({ configs }));
207
181
  }
208
182
  /**
209
- * Create directory storage using config from shovel.json
210
- * Merges with runtime defaults (actual class references) for fallback behavior
183
+ * Create directory storage for Bun
184
+ *
185
+ * Defaults:
186
+ * - server: NodeFSDirectory at cwd (app files)
187
+ * - public: NodeFSDirectory at cwd (static assets)
188
+ * - tmp: NodeFSDirectory at OS temp dir
189
+ *
190
+ * Override via shovel.json directories config.
211
191
  */
212
192
  async createDirectories() {
213
- const runtimeDefaults = {
193
+ const defaults = {
214
194
  server: { impl: NodeFSDirectory, path: this.#options.cwd },
215
195
  public: { impl: NodeFSDirectory, path: this.#options.cwd },
216
196
  tmp: { impl: NodeFSDirectory, path: tmpdir() }
217
197
  };
218
- const userDirs = this.#options.config?.directories ?? {};
219
- const configs = {};
220
- const allNames = /* @__PURE__ */ new Set([
221
- ...Object.keys(runtimeDefaults),
222
- ...Object.keys(userDirs)
223
- ]);
224
- for (const name of allNames) {
225
- configs[name] = { ...runtimeDefaults[name], ...userDirs[name] };
226
- }
198
+ const configs = mergeConfigWithDefaults(
199
+ defaults,
200
+ this.#options.config?.directories
201
+ );
227
202
  return new CustomDirectoryStorage(createDirectoryFactory(configs));
228
203
  }
229
204
  /**
230
- * Create logger storage using config from shovel.json
205
+ * Create logger storage for Bun
206
+ *
207
+ * Uses LogTape for structured logging.
231
208
  */
232
209
  async createLoggers() {
233
210
  return new CustomLoggerStorage((categories) => getLogger(categories));
234
211
  }
235
212
  /**
236
- * Create database storage from declarative config in shovel.json
213
+ * Create database storage for Bun
214
+ *
215
+ * Returns undefined if no databases configured in shovel.json.
216
+ * Supports SQLite via bun:sqlite.
237
217
  */
238
218
  createDatabases(configOverride) {
239
219
  const config = configOverride ?? this.#options.config;
@@ -249,9 +229,11 @@ var BunPlatform = class extends BasePlatform {
249
229
  createServer(handler, options = {}) {
250
230
  const requestedPort = options.port ?? this.#options.port;
251
231
  const hostname = options.host ?? this.#options.host;
232
+ const reusePort = options.reusePort ?? false;
252
233
  const server = Bun.serve({
253
234
  port: requestedPort,
254
235
  hostname,
236
+ reusePort,
255
237
  async fetch(request) {
256
238
  try {
257
239
  return await handler(request);
@@ -284,207 +266,125 @@ var BunPlatform = class extends BasePlatform {
284
266
  };
285
267
  }
286
268
  /**
287
- * Load and run a ServiceWorker-style entrypoint with Bun
288
- * Uses native Web Workers with the common WorkerPool
269
+ * Start listening for connections using pool's handlers
289
270
  */
290
- async loadServiceWorker(entrypoint, options = {}) {
291
- const workerCount = options.workerCount ?? this.#options.workers;
292
- if (workerCount === 1 && !options.hotReload) {
293
- return this.#loadServiceWorkerDirect(entrypoint, options);
294
- }
295
- return this.#loadServiceWorkerWithPool(entrypoint, options, workerCount);
296
- }
297
- /**
298
- * Load ServiceWorker directly in main thread (single-threaded mode)
299
- * No postMessage overhead - maximum performance for production
300
- */
301
- async #loadServiceWorkerDirect(entrypoint, _options) {
302
- const entryPath = Path.resolve(this.#options.cwd, entrypoint);
303
- let config = this.#options.config;
304
- const configPath = Path.join(Path.dirname(entryPath), "config.js");
305
- try {
306
- const configModule = await import(configPath);
307
- config = configModule.config ?? config;
308
- } catch (err) {
309
- logger.debug`Using platform config (no config.js): ${err}`;
310
- }
311
- if (!this.#cacheStorage) {
312
- const runtimeCacheDefaults = {
313
- default: { impl: MemoryCache }
314
- };
315
- const userCaches = config?.caches ?? {};
316
- const cacheConfigs = {};
317
- const allCacheNames = /* @__PURE__ */ new Set([
318
- ...Object.keys(runtimeCacheDefaults),
319
- ...Object.keys(userCaches)
320
- ]);
321
- for (const name of allCacheNames) {
322
- cacheConfigs[name] = {
323
- ...runtimeCacheDefaults[name],
324
- ...userCaches[name]
325
- };
326
- }
327
- this.#cacheStorage = new CustomCacheStorage(
328
- createCacheFactory({ configs: cacheConfigs })
271
+ async listen() {
272
+ const pool = this.serviceWorker.pool;
273
+ if (!pool) {
274
+ throw new Error(
275
+ "No ServiceWorker registered - call serviceWorker.register() first"
329
276
  );
330
277
  }
331
- if (!this.#directoryStorage) {
332
- const runtimeDirDefaults = {
333
- server: { impl: NodeFSDirectory },
334
- public: { impl: NodeFSDirectory },
335
- tmp: { impl: NodeFSDirectory }
336
- };
337
- const userDirs = config?.directories ?? {};
338
- const dirConfigs = {};
339
- const allDirNames = /* @__PURE__ */ new Set([
340
- ...Object.keys(runtimeDirDefaults),
341
- ...Object.keys(userDirs)
342
- ]);
343
- for (const name of allDirNames) {
344
- dirConfigs[name] = { ...runtimeDirDefaults[name], ...userDirs[name] };
345
- }
346
- this.#directoryStorage = new CustomDirectoryStorage(
347
- createDirectoryFactory(dirConfigs)
348
- );
349
- }
350
- if (!this.#databaseStorage) {
351
- this.#databaseStorage = this.createDatabases(config);
352
- }
353
- if (this.#singleThreadedRuntime) {
354
- await this.#singleThreadedRuntime.terminate();
355
- }
356
- if (this.#workerPool) {
357
- await this.#workerPool.terminate();
358
- this.#workerPool = void 0;
359
- }
360
- logger.info("Creating single-threaded ServiceWorker runtime", { entryPath });
361
- this.#singleThreadedRuntime = new SingleThreadedRuntime({
362
- caches: this.#cacheStorage,
363
- directories: this.#directoryStorage,
364
- databases: this.#databaseStorage,
365
- loggers: new CustomLoggerStorage((cats) => getLogger(cats))
278
+ this.#server = this.createServer((request) => pool.handleRequest(request), {
279
+ port: this.#options.port,
280
+ host: this.#options.host
366
281
  });
367
- await this.#singleThreadedRuntime.init();
368
- await this.#singleThreadedRuntime.load(entryPath);
369
- const runtime = this.#singleThreadedRuntime;
370
- const platform = this;
371
- const instance = {
372
- runtime,
373
- handleRequest: async (request) => {
374
- if (!platform.#singleThreadedRuntime) {
375
- throw new Error("SingleThreadedRuntime not initialized");
376
- }
377
- return platform.#singleThreadedRuntime.handleRequest(request);
378
- },
379
- install: async () => {
380
- logger.info("ServiceWorker installed", { method: "single_threaded" });
381
- },
382
- activate: async () => {
383
- logger.info("ServiceWorker activated", { method: "single_threaded" });
384
- },
385
- get ready() {
386
- return runtime?.ready ?? false;
387
- },
388
- dispose: async () => {
389
- if (platform.#singleThreadedRuntime) {
390
- await platform.#singleThreadedRuntime.terminate();
391
- platform.#singleThreadedRuntime = void 0;
392
- }
393
- logger.info("ServiceWorker disposed", {});
394
- }
395
- };
396
- logger.info("ServiceWorker loaded", {
397
- features: ["single_threaded", "no_postmessage_overhead"]
398
- });
399
- return instance;
282
+ await this.#server.listen();
283
+ return this.#server;
400
284
  }
401
285
  /**
402
- * Load ServiceWorker using worker pool (multi-threaded mode or dev mode)
286
+ * Close the server
403
287
  */
404
- async #loadServiceWorkerWithPool(entrypoint, _options, workerCount) {
405
- const entryPath = Path.resolve(this.#options.cwd, entrypoint);
406
- if (!this.#cacheStorage) {
407
- this.#cacheStorage = await this.createCaches();
408
- }
409
- if (this.#singleThreadedRuntime) {
410
- await this.#singleThreadedRuntime.terminate();
411
- this.#singleThreadedRuntime = void 0;
412
- }
413
- if (this.#workerPool) {
414
- await this.#workerPool.terminate();
415
- }
416
- const poolOptions = {
417
- workerCount,
418
- requestTimeout: 3e4,
419
- cwd: this.#options.cwd
420
- };
421
- logger.info("Creating ServiceWorker pool", { entryPath, workerCount });
422
- this.#workerPool = new ServiceWorkerPool(
423
- poolOptions,
424
- entryPath,
425
- this.#cacheStorage
426
- );
427
- await this.#workerPool.init();
428
- const workerPool = this.#workerPool;
429
- const platform = this;
430
- const instance = {
431
- runtime: workerPool,
432
- handleRequest: async (request) => {
433
- if (!platform.#workerPool) {
434
- throw new Error("WorkerPool not initialized");
435
- }
436
- return platform.#workerPool.handleRequest(request);
437
- },
438
- install: async () => {
439
- logger.info("ServiceWorker installed", { method: "native_web_workers" });
440
- },
441
- activate: async () => {
442
- logger.info("ServiceWorker activated", { method: "native_web_workers" });
443
- },
444
- get ready() {
445
- return workerPool?.ready ?? false;
446
- },
447
- dispose: async () => {
448
- if (platform.#workerPool) {
449
- await platform.#workerPool.terminate();
450
- platform.#workerPool = void 0;
451
- }
452
- logger.info("ServiceWorker disposed", {});
453
- }
454
- };
455
- logger.info("ServiceWorker loaded", {
456
- features: ["native_web_workers", "coordinated_caches"]
457
- });
458
- return instance;
288
+ async close() {
289
+ await this.#server?.close();
290
+ this.#server = void 0;
459
291
  }
460
292
  /**
461
293
  * Reload workers for hot reloading (called by CLI)
462
294
  * @param entrypoint - Path to the new entrypoint (hashed filename)
463
295
  */
464
296
  async reloadWorkers(entrypoint) {
465
- if (this.#workerPool) {
466
- await this.#workerPool.reloadWorkers(entrypoint);
467
- } else if (this.#singleThreadedRuntime) {
468
- await this.#singleThreadedRuntime.load(entrypoint);
469
- }
297
+ await this.serviceWorker.reloadWorkers(entrypoint);
470
298
  }
471
299
  /**
472
- * Get virtual entry wrapper for Bun
300
+ * Get production entry points for bundling.
473
301
  *
474
- * @param entryPath - Absolute path to user's entrypoint file
475
- * @param options - Entry wrapper options
476
- * @param options.type - "production" (default) or "worker"
477
- * @param options.outDir - Output directory (required for "worker" type)
302
+ * Bun produces two files:
303
+ * - index.js: Supervisor that spawns workers and handles signals
304
+ * - worker.js: Worker with its own HTTP server (uses reusePort for multi-worker)
478
305
  *
479
- * Returns:
480
- * - "production": Server entry with Bun.serve and reusePort
481
- * - "worker": Worker entry that sets up runtime and message loop
306
+ * Unlike Node.js, Bun workers each bind their own server with reusePort,
307
+ * allowing the OS to load-balance across workers without message passing overhead.
482
308
  */
483
- getEntryWrapper(entryPath, options) {
484
- if (options?.type === "worker") {
485
- return workerEntryTemplate.replace("__USER_ENTRY__", entryPath);
486
- }
487
- return entryTemplate;
309
+ getProductionEntryPoints(userEntryPath) {
310
+ const supervisorCode = `// Bun Production Supervisor
311
+ import {getLogger} from "@logtape/logtape";
312
+ import {configureLogging} from "@b9g/platform/runtime";
313
+ import BunPlatform from "@b9g/platform-bun";
314
+ import {config} from "shovel:config";
315
+
316
+ await configureLogging(config.logging);
317
+ const logger = getLogger(["shovel", "platform"]);
318
+
319
+ logger.info("Starting production server", {port: config.port, workers: config.workers});
320
+
321
+ // Initialize platform and register ServiceWorker (workers handle their own HTTP via reusePort)
322
+ const platform = new BunPlatform({port: config.port, host: config.host, workers: config.workers});
323
+ await platform.serviceWorker.register(new URL("./worker.js", import.meta.url).href);
324
+ await platform.serviceWorker.ready;
325
+
326
+ logger.info("All workers ready", {port: config.port, workers: config.workers});
327
+
328
+ // Graceful shutdown
329
+ const handleShutdown = async () => {
330
+ logger.info("Shutting down");
331
+ await platform.serviceWorker.terminate();
332
+ process.exit(0);
333
+ };
334
+ process.on("SIGINT", handleShutdown);
335
+ process.on("SIGTERM", handleShutdown);
336
+ `;
337
+ const workerCode = `// Bun Production Worker
338
+ import BunPlatform from "@b9g/platform-bun";
339
+ import {getLogger} from "@logtape/logtape";
340
+ import {configureLogging, initWorkerRuntime, runLifecycle, dispatchRequest} from "@b9g/platform/runtime";
341
+ import {config} from "shovel:config";
342
+
343
+ await configureLogging(config.logging);
344
+ const logger = getLogger(["shovel", "platform"]);
345
+
346
+ // Track resources for shutdown
347
+ let server;
348
+ let databases;
349
+
350
+ // Register shutdown handler before async startup
351
+ self.onmessage = async (event) => {
352
+ if (event.data.type === "shutdown") {
353
+ logger.info("Worker shutting down");
354
+ if (server) await server.close();
355
+ if (databases) await databases.closeAll();
356
+ postMessage({type: "shutdown-complete"});
357
+ }
358
+ };
359
+
360
+ // Initialize worker runtime (installs ServiceWorker globals)
361
+ const result = await initWorkerRuntime({config});
362
+ const registration = result.registration;
363
+ databases = result.databases;
364
+
365
+ // Import user code (registers event handlers)
366
+ await import("${userEntryPath}");
367
+
368
+ // Run ServiceWorker lifecycle (stage from config.lifecycle if present)
369
+ await runLifecycle(registration, config.lifecycle?.stage);
370
+
371
+ // Start server (skip in lifecycle-only mode)
372
+ if (!config.lifecycle) {
373
+ const platform = new BunPlatform({port: config.port, host: config.host});
374
+ server = platform.createServer(
375
+ (request) => dispatchRequest(registration, request),
376
+ {reusePort: config.workers > 1},
377
+ );
378
+ await server.listen();
379
+ }
380
+
381
+ postMessage({type: "ready"});
382
+ logger.info("Worker started", {port: config.port});
383
+ `;
384
+ return {
385
+ index: supervisorCode,
386
+ worker: workerCode
387
+ };
488
388
  }
489
389
  /**
490
390
  * Get Bun-specific esbuild configuration
@@ -535,18 +435,8 @@ var BunPlatform = class extends BasePlatform {
535
435
  * Dispose of platform resources
536
436
  */
537
437
  async dispose() {
538
- if (this.#singleThreadedRuntime) {
539
- await this.#singleThreadedRuntime.terminate();
540
- this.#singleThreadedRuntime = void 0;
541
- }
542
- if (this.#workerPool) {
543
- await this.#workerPool.terminate();
544
- this.#workerPool = void 0;
545
- }
546
- if (this.#cacheStorage) {
547
- await this.#cacheStorage.dispose();
548
- this.#cacheStorage = void 0;
549
- }
438
+ await this.close();
439
+ await this.serviceWorker.terminate();
550
440
  if (this.#databaseStorage) {
551
441
  await this.#databaseStorage.closeAll();
552
442
  this.#databaseStorage = void 0;
@@ -565,6 +455,7 @@ var BunPlatform = class extends BasePlatform {
565
455
  var src_default = BunPlatform;
566
456
  export {
567
457
  BunPlatform,
458
+ BunServiceWorkerContainer,
568
459
  MemoryCache2 as DefaultCache,
569
460
  src_default as default
570
461
  };