@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.
- package/package.json +6 -7
- package/src/index.d.ts +117 -21
- 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.
|
|
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.
|
|
16
|
-
"@b9g/cache": "^0.
|
|
17
|
-
"@b9g/http-errors": "^0.
|
|
18
|
-
"@b9g/platform": "^0.1.
|
|
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():
|
|
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
|
-
*
|
|
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
|
-
|
|
35
|
-
set workerPool(pool: ServiceWorkerPool | undefined);
|
|
108
|
+
createDirectories(): Promise<CustomDirectoryStorage>;
|
|
36
109
|
/**
|
|
37
|
-
* Create
|
|
110
|
+
* Create logger storage for Bun
|
|
111
|
+
*
|
|
112
|
+
* Uses LogTape for structured logging.
|
|
38
113
|
*/
|
|
39
|
-
|
|
114
|
+
createLoggers(): Promise<CustomLoggerStorage>;
|
|
40
115
|
/**
|
|
41
|
-
* Create
|
|
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
|
-
|
|
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
|
-
*
|
|
50
|
-
|
|
127
|
+
* Start listening for connections using pool's handlers
|
|
128
|
+
*/
|
|
129
|
+
listen(): Promise<Server>;
|
|
130
|
+
/**
|
|
131
|
+
* Close the server
|
|
51
132
|
*/
|
|
52
|
-
|
|
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
|
|
140
|
+
* Get production entry points for bundling.
|
|
60
141
|
*
|
|
61
|
-
*
|
|
62
|
-
* -
|
|
63
|
-
* -
|
|
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
|
-
*
|
|
67
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
15
|
+
CustomLoggerStorage,
|
|
16
|
+
CustomDatabaseStorage,
|
|
17
|
+
createDatabaseFactory,
|
|
18
|
+
mergeConfigWithDefaults
|
|
8
19
|
} from "@b9g/platform";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
var
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
#
|
|
87
|
-
#
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
205
|
+
* Create logger storage for Bun
|
|
206
|
+
*
|
|
207
|
+
* Uses LogTape for structured logging.
|
|
119
208
|
*/
|
|
120
|
-
async
|
|
121
|
-
return new
|
|
209
|
+
async createLoggers() {
|
|
210
|
+
return new CustomLoggerStorage((categories) => getLogger(categories));
|
|
122
211
|
}
|
|
123
212
|
/**
|
|
124
|
-
* Create
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
*
|
|
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
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
this.#
|
|
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
|
-
|
|
282
|
+
await this.#server.listen();
|
|
283
|
+
return this.#server;
|
|
249
284
|
}
|
|
250
285
|
/**
|
|
251
|
-
*
|
|
286
|
+
* Close the server
|
|
252
287
|
*/
|
|
253
|
-
async
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
|
300
|
+
* Get production entry points for bundling.
|
|
325
301
|
*
|
|
326
|
-
*
|
|
327
|
-
* -
|
|
328
|
-
* -
|
|
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
|
-
*
|
|
332
|
-
*
|
|
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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
};
|