@b9g/platform-bun 0.1.10 → 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 +6 -7
  2. package/src/index.d.ts +117 -21
  3. package/src/index.js +344 -254
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/platform-bun",
3
- "version": "0.1.10",
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",
@@ -12,15 +12,14 @@
12
12
  "jsx"
13
13
  ],
14
14
  "dependencies": {
15
- "@b9g/assets": "^0.1.15",
16
- "@b9g/cache": "^0.1.5",
17
- "@b9g/http-errors": "^0.1.5",
18
- "@b9g/platform": "^0.1.12",
15
+ "@b9g/assets": "^0.2.0-beta.0",
16
+ "@b9g/cache": "^0.2.0-beta.0",
17
+ "@b9g/http-errors": "^0.2.0-beta.0",
18
+ "@b9g/platform": "^0.1.14-beta.0",
19
19
  "@logtape/logtape": "^1.2.0"
20
20
  },
21
21
  "devDependencies": {
22
- "@b9g/libuild": "^0.1.18",
23
- "bun-types": "latest"
22
+ "@b9g/libuild": "^0.1.18"
24
23
  },
25
24
  "type": "module",
26
25
  "types": "src/index.d.ts",
package/src/index.d.ts CHANGED
@@ -3,9 +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 Handler, type Server, type ServerOptions, type ServiceWorkerOptions, type ServiceWorkerInstance, type EntryWrapperOptions, type PlatformEsbuildConfig, ServiceWorkerPool } from "@b9g/platform";
7
6
  import { CustomCacheStorage } from "@b9g/cache";
8
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";
9
10
  export interface BunPlatformOptions extends PlatformConfig {
10
11
  /** Port for development server (default: 3000) */
11
12
  port?: number;
@@ -15,6 +16,57 @@ export interface BunPlatformOptions extends PlatformConfig {
15
16
  cwd?: string;
16
17
  /** Number of worker threads (default: 1) */
17
18
  workers?: number;
19
+ /** Shovel configuration (caches, directories, etc.) */
20
+ config?: ShovelConfig;
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>;
18
70
  }
19
71
  /**
20
72
  * Bun platform implementation
@@ -23,63 +75,107 @@ export interface BunPlatformOptions extends PlatformConfig {
23
75
  export declare class BunPlatform extends BasePlatform {
24
76
  #private;
25
77
  readonly name: string;
78
+ readonly serviceWorker: BunServiceWorkerContainer;
26
79
  constructor(options?: BunPlatformOptions);
27
80
  /**
28
81
  * Get options for testing
29
82
  */
30
- get options(): Required<BunPlatformOptions>;
83
+ get options(): {
84
+ port: number;
85
+ host: string;
86
+ cwd: string;
87
+ workers: number;
88
+ config?: ShovelConfig;
89
+ };
90
+ /**
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.
96
+ */
97
+ createCaches(): Promise<CustomCacheStorage>;
31
98
  /**
32
- * Get/set worker pool for testing
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.
33
107
  */
34
- get workerPool(): ServiceWorkerPool | undefined;
35
- set workerPool(pool: ServiceWorkerPool | undefined);
108
+ createDirectories(): Promise<CustomDirectoryStorage>;
36
109
  /**
37
- * Create cache storage (in-memory by default)
110
+ * Create logger storage for Bun
111
+ *
112
+ * Uses LogTape for structured logging.
38
113
  */
39
- createCaches(): Promise<CustomCacheStorage>;
114
+ createLoggers(): Promise<CustomLoggerStorage>;
40
115
  /**
41
- * Create directory storage for the given base directory
116
+ * Create database storage for Bun
117
+ *
118
+ * Returns undefined if no databases configured in shovel.json.
119
+ * Supports SQLite via bun:sqlite.
42
120
  */
43
- createDirectories(baseDir: string): CustomDirectoryStorage;
121
+ createDatabases(configOverride?: BunPlatformOptions["config"]): CustomDatabaseStorage | undefined;
44
122
  /**
45
123
  * Create HTTP server using Bun.serve
46
124
  */
47
125
  createServer(handler: Handler, options?: ServerOptions): Server;
48
126
  /**
49
- * Load and run a ServiceWorker-style entrypoint with Bun
50
- * 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
51
132
  */
52
- loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
133
+ close(): Promise<void>;
53
134
  /**
54
135
  * Reload workers for hot reloading (called by CLI)
55
136
  * @param entrypoint - Path to the new entrypoint (hashed filename)
56
137
  */
57
138
  reloadWorkers(entrypoint: string): Promise<void>;
58
139
  /**
59
- * Get virtual entry wrapper for Bun
140
+ * Get production entry points for bundling.
60
141
  *
61
- * Returns production server entry template that uses:
62
- * - shovel:config virtual module for configuration
63
- * - Bun.serve with reusePort for multi-worker scaling
64
- * - Direct import of user's server code
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)
65
145
  *
66
- * The template is a real .ts file (entry-template.ts) for better
67
- * IDE support and linting. It's imported with {type: "text"}.
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.
68
148
  */
69
- getEntryWrapper(_entryPath: string, _options?: EntryWrapperOptions): string;
149
+ getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
70
150
  /**
71
151
  * Get Bun-specific esbuild configuration
72
152
  *
73
153
  * Note: Bun natively supports import.meta.env, so no define alias is needed.
74
154
  * We use platform: "node" since Bun is Node-compatible for module resolution.
75
155
  */
76
- getEsbuildConfig(): PlatformEsbuildConfig;
156
+ getESBuildConfig(): PlatformESBuildConfig;
157
+ /**
158
+ * Get Bun-specific defaults for config generation
159
+ *
160
+ * Provides default directories (server, public, tmp) that work
161
+ * out of the box for Bun deployments.
162
+ */
163
+ getDefaults(): PlatformDefaults;
77
164
  /**
78
165
  * Dispose of platform resources
79
166
  */
80
167
  dispose(): Promise<void>;
168
+ /**
169
+ * Get the OS temp directory (Bun-specific implementation using node:os)
170
+ */
171
+ tmpdir(): string;
81
172
  }
82
173
  /**
83
174
  * Default export for easy importing
84
175
  */
85
176
  export default BunPlatform;
177
+ /**
178
+ * Platform's default cache implementation.
179
+ * Re-exported so config can reference: { module: "@b9g/platform-bun", export: "DefaultCache" }
180
+ */
181
+ export { MemoryCache as DefaultCache } from "@b9g/cache/memory";
package/src/index.js CHANGED
@@ -1,92 +1,150 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  // src/index.ts
3
+ import { builtinModules } from "node:module";
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";
3
12
  import {
4
13
  BasePlatform,
5
14
  ServiceWorkerPool,
6
- SingleThreadedRuntime,
7
- CustomLoggerStorage
15
+ CustomLoggerStorage,
16
+ CustomDatabaseStorage,
17
+ createDatabaseFactory,
18
+ mergeConfigWithDefaults
8
19
  } from "@b9g/platform";
9
- import { CustomCacheStorage } from "@b9g/cache";
10
- import { CustomDirectoryStorage } from "@b9g/filesystem";
11
- import { MemoryCache } from "@b9g/cache/memory";
12
- import { NodeDirectory } from "@b9g/filesystem/node";
13
- import { InternalServerError, isHTTPError } from "@b9g/http-errors";
14
- import { getLogger } from "@logtape/logtape";
15
- import * as Path from "path";
16
- var entryTemplate = `// Bun Production Server Entry
17
- import {getLogger} from "@logtape/logtape";
18
- import {configureLogging} from "@b9g/platform/runtime";
19
- import {config} from "shovel:config"; // Virtual module - resolved at build time
20
- import BunPlatform from "@b9g/platform-bun";
21
-
22
- // Configure logging before anything else
23
- await configureLogging(config.logging);
24
-
25
- const logger = getLogger(["platform"]);
26
-
27
- // Configuration from shovel:config
28
- const PORT = config.port;
29
- const HOST = config.host;
30
- const WORKERS = config.workers;
31
- const isWorker = !Bun.isMainThread;
32
-
33
- // Worker thread entry - each worker runs its own Bun.serve with reusePort
34
- if (isWorker) {
35
- const platform = new BunPlatform({port: PORT, host: HOST, workers: 1});
36
- const userCodePath = new URL("./server.js", import.meta.url).pathname;
37
- const serviceWorker = await platform.loadServiceWorker(userCodePath);
38
-
39
- Bun.serve({
40
- port: PORT,
41
- hostname: HOST,
42
- reusePort: true,
43
- fetch: serviceWorker.handleRequest,
44
- });
45
-
46
- logger.info("Worker started", {port: PORT, thread: Bun.threadId});
47
- } else {
48
- // Main thread - spawn worker threads, each binds to same port with reusePort
49
- if (WORKERS > 1) {
50
- for (let i = 0; i < WORKERS; i++) {
51
- new Worker(import.meta.path);
52
- }
53
- logger.info("Spawned workers", {count: WORKERS, port: PORT});
54
- } else {
55
- // Single worker mode - run directly in main thread
56
- const platform = new BunPlatform({port: PORT, host: HOST, workers: 1});
57
- const userCodePath = new URL("./server.js", import.meta.url).pathname;
58
- const serviceWorker = await platform.loadServiceWorker(userCodePath);
59
-
60
- const server = platform.createServer(serviceWorker.handleRequest, {
61
- port: PORT,
62
- host: HOST,
63
- });
64
- await server.listen();
65
-
66
- logger.info("Server started", {url: server.url});
67
-
68
- // Graceful shutdown
69
- const shutdown = async () => {
70
- logger.info("Shutting down");
71
- await serviceWorker.dispose();
72
- await platform.dispose();
73
- await server.close();
74
- process.exit(0);
75
- };
76
-
77
- process.on("SIGINT", shutdown);
78
- process.on("SIGTERM", shutdown);
79
- }
80
- }
81
- `;
82
- var logger = getLogger(["platform"]);
20
+ import {
21
+ ShovelServiceWorkerRegistration,
22
+ kServiceWorker,
23
+ createCacheFactory,
24
+ createDirectoryFactory
25
+ } from "@b9g/platform/runtime";
26
+ import { MemoryCache as MemoryCache2 } from "@b9g/cache/memory";
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
+ };
83
142
  var BunPlatform = class extends BasePlatform {
84
143
  name;
144
+ serviceWorker;
85
145
  #options;
86
- #workerPool;
87
- #singleThreadedRuntime;
88
- #cacheStorage;
89
- #directoryStorage;
146
+ #server;
147
+ #databaseStorage;
90
148
  constructor(options = {}) {
91
149
  super(options);
92
150
  this.name = "bun";
@@ -96,8 +154,9 @@ var BunPlatform = class extends BasePlatform {
96
154
  host: options.host ?? "localhost",
97
155
  workers: options.workers ?? 1,
98
156
  cwd,
99
- ...options
157
+ config: options.config
100
158
  };
159
+ this.serviceWorker = new BunServiceWorkerContainer(this);
101
160
  }
102
161
  /**
103
162
  * Get options for testing
@@ -106,35 +165,63 @@ var BunPlatform = class extends BasePlatform {
106
165
  return this.#options;
107
166
  }
108
167
  /**
109
- * Get/set worker pool for testing
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.
110
173
  */
111
- get workerPool() {
112
- return this.#workerPool;
174
+ async createCaches() {
175
+ const defaults = { default: { impl: MemoryCache } };
176
+ const configs = mergeConfigWithDefaults(
177
+ defaults,
178
+ this.#options.config?.caches
179
+ );
180
+ return new CustomCacheStorage(createCacheFactory({ configs }));
113
181
  }
114
- set workerPool(pool) {
115
- this.#workerPool = pool;
182
+ /**
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.
191
+ */
192
+ async createDirectories() {
193
+ const defaults = {
194
+ server: { impl: NodeFSDirectory, path: this.#options.cwd },
195
+ public: { impl: NodeFSDirectory, path: this.#options.cwd },
196
+ tmp: { impl: NodeFSDirectory, path: tmpdir() }
197
+ };
198
+ const configs = mergeConfigWithDefaults(
199
+ defaults,
200
+ this.#options.config?.directories
201
+ );
202
+ return new CustomDirectoryStorage(createDirectoryFactory(configs));
116
203
  }
117
204
  /**
118
- * Create cache storage (in-memory by default)
205
+ * Create logger storage for Bun
206
+ *
207
+ * Uses LogTape for structured logging.
119
208
  */
120
- async createCaches() {
121
- return new CustomCacheStorage((name) => new MemoryCache(name));
209
+ async createLoggers() {
210
+ return new CustomLoggerStorage((categories) => getLogger(categories));
122
211
  }
123
212
  /**
124
- * Create directory storage for the given base directory
213
+ * Create database storage for Bun
214
+ *
215
+ * Returns undefined if no databases configured in shovel.json.
216
+ * Supports SQLite via bun:sqlite.
125
217
  */
126
- createDirectories(baseDir) {
127
- return new CustomDirectoryStorage((name) => {
128
- let dirPath;
129
- if (name === "static") {
130
- dirPath = Path.resolve(baseDir, "../static");
131
- } else if (name === "server") {
132
- dirPath = baseDir;
133
- } else {
134
- dirPath = Path.resolve(baseDir, `../${name}`);
135
- }
136
- return Promise.resolve(new NodeDirectory(dirPath));
137
- });
218
+ createDatabases(configOverride) {
219
+ const config = configOverride ?? this.#options.config;
220
+ if (config?.databases && Object.keys(config.databases).length > 0) {
221
+ const factory = createDatabaseFactory(config.databases);
222
+ return new CustomDatabaseStorage(factory);
223
+ }
224
+ return void 0;
138
225
  }
139
226
  /**
140
227
  * Create HTTP server using Bun.serve
@@ -142,9 +229,11 @@ var BunPlatform = class extends BasePlatform {
142
229
  createServer(handler, options = {}) {
143
230
  const requestedPort = options.port ?? this.#options.port;
144
231
  const hostname = options.host ?? this.#options.host;
232
+ const reusePort = options.reusePort ?? false;
145
233
  const server = Bun.serve({
146
234
  port: requestedPort,
147
235
  hostname,
236
+ reusePort,
148
237
  async fetch(request) {
149
238
  try {
150
239
  return await handler(request);
@@ -177,162 +266,125 @@ var BunPlatform = class extends BasePlatform {
177
266
  };
178
267
  }
179
268
  /**
180
- * Load and run a ServiceWorker-style entrypoint with Bun
181
- * Uses native Web Workers with the common WorkerPool
182
- */
183
- async loadServiceWorker(entrypoint, options = {}) {
184
- const workerCount = options.workerCount ?? this.#options.workers;
185
- if (workerCount === 1 && !options.hotReload) {
186
- return this.#loadServiceWorkerDirect(entrypoint, options);
187
- }
188
- return this.#loadServiceWorkerWithPool(entrypoint, options, workerCount);
189
- }
190
- /**
191
- * Load ServiceWorker directly in main thread (single-threaded mode)
192
- * No postMessage overhead - maximum performance for production
269
+ * Start listening for connections using pool's handlers
193
270
  */
194
- async #loadServiceWorkerDirect(entrypoint, _options) {
195
- const entryPath = Path.resolve(this.#options.cwd, entrypoint);
196
- const entryDir = Path.dirname(entryPath);
197
- if (!this.#cacheStorage) {
198
- this.#cacheStorage = await this.createCaches();
199
- }
200
- if (!this.#directoryStorage) {
201
- this.#directoryStorage = this.createDirectories(entryDir);
202
- }
203
- if (this.#singleThreadedRuntime) {
204
- await this.#singleThreadedRuntime.terminate();
271
+ async listen() {
272
+ const pool = this.serviceWorker.pool;
273
+ if (!pool) {
274
+ throw new Error(
275
+ "No ServiceWorker registered - call serviceWorker.register() first"
276
+ );
205
277
  }
206
- if (this.#workerPool) {
207
- await this.#workerPool.terminate();
208
- this.#workerPool = void 0;
209
- }
210
- logger.info("Creating single-threaded ServiceWorker runtime", { entryPath });
211
- this.#singleThreadedRuntime = new SingleThreadedRuntime({
212
- caches: this.#cacheStorage,
213
- directories: this.#directoryStorage,
214
- loggers: new CustomLoggerStorage((...cats) => getLogger(cats))
215
- });
216
- await this.#singleThreadedRuntime.init();
217
- await this.#singleThreadedRuntime.load(entryPath);
218
- const runtime = this.#singleThreadedRuntime;
219
- const platform = this;
220
- const instance = {
221
- runtime,
222
- handleRequest: async (request) => {
223
- if (!platform.#singleThreadedRuntime) {
224
- throw new Error("SingleThreadedRuntime not initialized");
225
- }
226
- return platform.#singleThreadedRuntime.handleRequest(request);
227
- },
228
- install: async () => {
229
- logger.info("ServiceWorker installed", { method: "single_threaded" });
230
- },
231
- activate: async () => {
232
- logger.info("ServiceWorker activated", { method: "single_threaded" });
233
- },
234
- get ready() {
235
- return runtime?.ready ?? false;
236
- },
237
- dispose: async () => {
238
- if (platform.#singleThreadedRuntime) {
239
- await platform.#singleThreadedRuntime.terminate();
240
- platform.#singleThreadedRuntime = void 0;
241
- }
242
- logger.info("ServiceWorker disposed", {});
243
- }
244
- };
245
- logger.info("ServiceWorker loaded", {
246
- features: ["single_threaded", "no_postmessage_overhead"]
278
+ this.#server = this.createServer((request) => pool.handleRequest(request), {
279
+ port: this.#options.port,
280
+ host: this.#options.host
247
281
  });
248
- return instance;
282
+ await this.#server.listen();
283
+ return this.#server;
249
284
  }
250
285
  /**
251
- * Load ServiceWorker using worker pool (multi-threaded mode or dev mode)
286
+ * Close the server
252
287
  */
253
- async #loadServiceWorkerWithPool(entrypoint, _options, workerCount) {
254
- const entryPath = Path.resolve(this.#options.cwd, entrypoint);
255
- if (!this.#cacheStorage) {
256
- this.#cacheStorage = await this.createCaches();
257
- }
258
- if (this.#singleThreadedRuntime) {
259
- await this.#singleThreadedRuntime.terminate();
260
- this.#singleThreadedRuntime = void 0;
261
- }
262
- if (this.#workerPool) {
263
- await this.#workerPool.terminate();
264
- }
265
- const poolOptions = {
266
- workerCount,
267
- requestTimeout: 3e4,
268
- cwd: this.#options.cwd
269
- };
270
- logger.info("Creating ServiceWorker pool", { entryPath, workerCount });
271
- this.#workerPool = new ServiceWorkerPool(
272
- poolOptions,
273
- entryPath,
274
- this.#cacheStorage,
275
- {}
276
- // Empty config - use defaults
277
- );
278
- await this.#workerPool.init();
279
- await this.#workerPool.reloadWorkers(entryPath);
280
- const workerPool = this.#workerPool;
281
- const platform = this;
282
- const instance = {
283
- runtime: workerPool,
284
- handleRequest: async (request) => {
285
- if (!platform.#workerPool) {
286
- throw new Error("WorkerPool not initialized");
287
- }
288
- return platform.#workerPool.handleRequest(request);
289
- },
290
- install: async () => {
291
- logger.info("ServiceWorker installed", { method: "native_web_workers" });
292
- },
293
- activate: async () => {
294
- logger.info("ServiceWorker activated", { method: "native_web_workers" });
295
- },
296
- get ready() {
297
- return workerPool?.ready ?? false;
298
- },
299
- dispose: async () => {
300
- if (platform.#workerPool) {
301
- await platform.#workerPool.terminate();
302
- platform.#workerPool = void 0;
303
- }
304
- logger.info("ServiceWorker disposed", {});
305
- }
306
- };
307
- logger.info("ServiceWorker loaded", {
308
- features: ["native_web_workers", "coordinated_caches"]
309
- });
310
- return instance;
288
+ async close() {
289
+ await this.#server?.close();
290
+ this.#server = void 0;
311
291
  }
312
292
  /**
313
293
  * Reload workers for hot reloading (called by CLI)
314
294
  * @param entrypoint - Path to the new entrypoint (hashed filename)
315
295
  */
316
296
  async reloadWorkers(entrypoint) {
317
- if (this.#workerPool) {
318
- await this.#workerPool.reloadWorkers(entrypoint);
319
- } else if (this.#singleThreadedRuntime) {
320
- await this.#singleThreadedRuntime.load(entrypoint);
321
- }
297
+ await this.serviceWorker.reloadWorkers(entrypoint);
322
298
  }
323
299
  /**
324
- * Get virtual entry wrapper for Bun
300
+ * Get production entry points for bundling.
325
301
  *
326
- * Returns production server entry template that uses:
327
- * - shovel:config virtual module for configuration
328
- * - Bun.serve with reusePort for multi-worker scaling
329
- * - Direct import of user's server code
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)
330
305
  *
331
- * The template is a real .ts file (entry-template.ts) for better
332
- * IDE support and linting. It's imported with {type: "text"}.
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.
333
308
  */
334
- getEntryWrapper(_entryPath, _options) {
335
- 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
+ };
336
388
  }
337
389
  /**
338
390
  * Get Bun-specific esbuild configuration
@@ -340,32 +392,70 @@ var BunPlatform = class extends BasePlatform {
340
392
  * Note: Bun natively supports import.meta.env, so no define alias is needed.
341
393
  * We use platform: "node" since Bun is Node-compatible for module resolution.
342
394
  */
343
- getEsbuildConfig() {
395
+ getESBuildConfig() {
344
396
  return {
345
397
  platform: "node",
346
- external: ["node:*"]
398
+ external: ["node:*", "bun", "bun:*", ...builtinModules]
399
+ };
400
+ }
401
+ /**
402
+ * Get Bun-specific defaults for config generation
403
+ *
404
+ * Provides default directories (server, public, tmp) that work
405
+ * out of the box for Bun deployments.
406
+ */
407
+ getDefaults() {
408
+ return {
409
+ caches: {
410
+ default: {
411
+ module: "@b9g/cache/memory",
412
+ export: "MemoryCache"
413
+ }
414
+ },
415
+ directories: {
416
+ server: {
417
+ module: "@b9g/filesystem/node-fs",
418
+ export: "NodeFSDirectory",
419
+ path: "[outdir]/server"
420
+ },
421
+ public: {
422
+ module: "@b9g/filesystem/node-fs",
423
+ export: "NodeFSDirectory",
424
+ path: "[outdir]/public"
425
+ },
426
+ tmp: {
427
+ module: "@b9g/filesystem/node-fs",
428
+ export: "NodeFSDirectory",
429
+ path: "[tmpdir]"
430
+ }
431
+ }
347
432
  };
348
433
  }
349
434
  /**
350
435
  * Dispose of platform resources
351
436
  */
352
437
  async dispose() {
353
- if (this.#singleThreadedRuntime) {
354
- await this.#singleThreadedRuntime.terminate();
355
- this.#singleThreadedRuntime = void 0;
356
- }
357
- if (this.#workerPool) {
358
- await this.#workerPool.terminate();
359
- this.#workerPool = void 0;
360
- }
361
- if (this.#cacheStorage) {
362
- await this.#cacheStorage.dispose();
363
- this.#cacheStorage = void 0;
438
+ await this.close();
439
+ await this.serviceWorker.terminate();
440
+ if (this.#databaseStorage) {
441
+ await this.#databaseStorage.closeAll();
442
+ this.#databaseStorage = void 0;
364
443
  }
365
444
  }
445
+ // =========================================================================
446
+ // Config Expression Method Overrides
447
+ // =========================================================================
448
+ /**
449
+ * Get the OS temp directory (Bun-specific implementation using node:os)
450
+ */
451
+ tmpdir() {
452
+ return tmpdir();
453
+ }
366
454
  };
367
455
  var src_default = BunPlatform;
368
456
  export {
369
457
  BunPlatform,
458
+ BunServiceWorkerContainer,
459
+ MemoryCache2 as DefaultCache,
370
460
  src_default as default
371
461
  };