@b9g/platform-node 0.1.13 → 0.1.14-beta.1
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 +10 -2
- package/src/index.d.ts +71 -44
- package/src/index.js +227 -485
- package/src/platform.d.ts +40 -0
- package/src/platform.js +146 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/platform-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14-beta.1",
|
|
4
4
|
"description": "Node.js platform adapter for Shovel with hot reloading and ESBuild integration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shovel",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"@b9g/cache": "^0.2.0-beta.0",
|
|
15
15
|
"@b9g/http-errors": "^0.2.0-beta.0",
|
|
16
16
|
"@b9g/node-webworker": "^0.2.0-beta.1",
|
|
17
|
-
"@b9g/platform": "^0.1.
|
|
17
|
+
"@b9g/platform": "^0.1.14-beta.4",
|
|
18
18
|
"@logtape/logtape": "^1.2.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -37,6 +37,14 @@
|
|
|
37
37
|
"./index.js": {
|
|
38
38
|
"types": "./src/index.d.ts",
|
|
39
39
|
"import": "./src/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./platform": {
|
|
42
|
+
"types": "./src/platform.d.ts",
|
|
43
|
+
"import": "./src/platform.js"
|
|
44
|
+
},
|
|
45
|
+
"./platform.js": {
|
|
46
|
+
"types": "./src/platform.d.ts",
|
|
47
|
+
"import": "./src/platform.js"
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides hot reloading, ESBuild integration, and optimized caching for Node.js environments.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { type PlatformDefaults, type Handler, type Server, type ServerOptions, type PlatformESBuildConfig, type EntryPoints, ServiceWorkerPool } from "@b9g/platform";
|
|
7
7
|
import { type ShovelConfig } from "@b9g/platform/runtime";
|
|
8
|
-
|
|
9
|
-
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
10
|
-
export interface NodePlatformOptions extends PlatformConfig {
|
|
8
|
+
export interface NodePlatformOptions {
|
|
11
9
|
/** Port for development server (default: 3000) */
|
|
12
10
|
port?: number;
|
|
13
11
|
/** Host for development server (default: localhost) */
|
|
@@ -20,74 +18,103 @@ export interface NodePlatformOptions extends PlatformConfig {
|
|
|
20
18
|
config?: ShovelConfig;
|
|
21
19
|
}
|
|
22
20
|
/**
|
|
23
|
-
* Node.js
|
|
24
|
-
* ServiceWorker
|
|
21
|
+
* Node.js ServiceWorkerContainer implementation
|
|
22
|
+
* Manages ServiceWorker registrations backed by worker threads
|
|
25
23
|
*/
|
|
26
|
-
export declare class
|
|
24
|
+
export declare class NodeServiceWorkerContainer extends EventTarget implements ServiceWorkerContainer {
|
|
27
25
|
#private;
|
|
28
|
-
readonly
|
|
29
|
-
|
|
26
|
+
readonly controller: ServiceWorker | null;
|
|
27
|
+
oncontrollerchange: ((ev: Event) => unknown) | null;
|
|
28
|
+
onmessage: ((ev: MessageEvent) => unknown) | null;
|
|
29
|
+
onmessageerror: ((ev: MessageEvent) => unknown) | null;
|
|
30
|
+
constructor(platform: NodePlatform);
|
|
30
31
|
/**
|
|
31
|
-
*
|
|
32
|
+
* Register a ServiceWorker script
|
|
33
|
+
* Spawns worker threads and runs lifecycle
|
|
32
34
|
*/
|
|
33
|
-
|
|
34
|
-
port: number;
|
|
35
|
-
host: string;
|
|
36
|
-
cwd: string;
|
|
37
|
-
workers: number;
|
|
38
|
-
config?: ShovelConfig;
|
|
39
|
-
};
|
|
35
|
+
register(scriptURL: string | URL, options?: RegistrationOptions): Promise<ServiceWorkerRegistration>;
|
|
40
36
|
/**
|
|
41
|
-
* Get
|
|
37
|
+
* Get registration for scope
|
|
42
38
|
*/
|
|
43
|
-
|
|
44
|
-
set workerPool(pool: ServiceWorkerPool | undefined);
|
|
39
|
+
getRegistration(scope?: string): Promise<ServiceWorkerRegistration | undefined>;
|
|
45
40
|
/**
|
|
46
|
-
*
|
|
47
|
-
* Uses Worker threads with coordinated cache storage for isolation and standards compliance
|
|
41
|
+
* Get all registrations
|
|
48
42
|
*/
|
|
49
|
-
|
|
43
|
+
getRegistrations(): Promise<readonly ServiceWorkerRegistration[]>;
|
|
50
44
|
/**
|
|
51
|
-
*
|
|
52
|
-
* Used for testing - production uses the generated config module
|
|
53
|
-
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
45
|
+
* Start receiving messages (no-op in server context)
|
|
54
46
|
*/
|
|
55
|
-
|
|
47
|
+
startMessages(): void;
|
|
56
48
|
/**
|
|
57
|
-
*
|
|
58
|
-
* Used for testing - production uses the generated config module
|
|
59
|
-
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
49
|
+
* Ready promise - resolves when a registration is active
|
|
60
50
|
*/
|
|
61
|
-
|
|
51
|
+
get ready(): Promise<ServiceWorkerRegistration>;
|
|
62
52
|
/**
|
|
63
|
-
*
|
|
53
|
+
* Internal: Get worker pool for request handling
|
|
64
54
|
*/
|
|
65
|
-
|
|
55
|
+
get pool(): ServiceWorkerPool | undefined;
|
|
66
56
|
/**
|
|
67
|
-
*
|
|
57
|
+
* Internal: Terminate workers and dispose cache storage
|
|
68
58
|
*/
|
|
69
|
-
|
|
59
|
+
terminate(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Internal: Reload workers (for hot reload)
|
|
62
|
+
*/
|
|
63
|
+
reloadWorkers(entrypoint: string): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Node.js platform implementation
|
|
67
|
+
* ServiceWorker entrypoint loader for Node.js with ESBuild VM system
|
|
68
|
+
*/
|
|
69
|
+
export declare class NodePlatform {
|
|
70
|
+
#private;
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly serviceWorker: NodeServiceWorkerContainer;
|
|
73
|
+
constructor(options?: NodePlatformOptions);
|
|
74
|
+
/**
|
|
75
|
+
* Create a worker instance for the pool
|
|
76
|
+
* Can be overridden for testing
|
|
77
|
+
*/
|
|
78
|
+
createWorker(entrypoint: string): Promise<Worker>;
|
|
79
|
+
/**
|
|
80
|
+
* Start the HTTP server, routing requests to ServiceWorker
|
|
81
|
+
*/
|
|
82
|
+
listen(): Promise<Server>;
|
|
83
|
+
/**
|
|
84
|
+
* Close the server and terminate workers
|
|
85
|
+
*/
|
|
86
|
+
close(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Get options for testing
|
|
89
|
+
*/
|
|
90
|
+
get options(): {
|
|
91
|
+
port: number;
|
|
92
|
+
host: string;
|
|
93
|
+
cwd: string;
|
|
94
|
+
workers: number;
|
|
95
|
+
config?: ShovelConfig;
|
|
96
|
+
};
|
|
70
97
|
/**
|
|
71
|
-
*
|
|
98
|
+
* Create HTTP server for Node.js
|
|
72
99
|
*/
|
|
73
100
|
createServer(handler: Handler, options?: ServerOptions): Server;
|
|
74
101
|
/**
|
|
75
102
|
* Reload workers for hot reloading (called by CLI)
|
|
103
|
+
* @deprecated Use serviceWorker.reloadWorkers() instead
|
|
76
104
|
* @param entrypoint - Path to the new entrypoint (hashed filename)
|
|
77
105
|
*/
|
|
78
106
|
reloadWorkers(entrypoint: string): Promise<void>;
|
|
79
107
|
/**
|
|
80
|
-
* Get
|
|
108
|
+
* Get entry points for bundling.
|
|
81
109
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* @param options.type - "production" (default) or "worker"
|
|
110
|
+
* Development mode:
|
|
111
|
+
* - worker.js: Single worker with message loop (develop command acts as supervisor)
|
|
85
112
|
*
|
|
86
|
-
*
|
|
87
|
-
* -
|
|
88
|
-
* -
|
|
113
|
+
* Production mode:
|
|
114
|
+
* - index.js: Supervisor that spawns workers and owns the HTTP server
|
|
115
|
+
* - worker.js: Worker that handles requests via message loop
|
|
89
116
|
*/
|
|
90
|
-
|
|
117
|
+
getEntryPoints(userEntryPath: string, mode: "development" | "production"): EntryPoints;
|
|
91
118
|
/**
|
|
92
119
|
* Get Node.js-specific esbuild configuration
|
|
93
120
|
*
|
package/src/index.js
CHANGED
|
@@ -1,500 +1,190 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
2
|
// src/index.ts
|
|
3
|
+
import * as HTTP from "node:http";
|
|
3
4
|
import { builtinModules } from "node:module";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
6
|
+
import * as Path from "node:path";
|
|
7
|
+
import { getLogger } from "@logtape/logtape";
|
|
8
|
+
import { CustomCacheStorage } from "@b9g/cache";
|
|
9
|
+
import { InternalServerError, isHTTPError } from "@b9g/http-errors";
|
|
5
10
|
import {
|
|
6
|
-
|
|
7
|
-
ServiceWorkerPool,
|
|
8
|
-
SingleThreadedRuntime,
|
|
9
|
-
CustomLoggerStorage,
|
|
10
|
-
CustomDatabaseStorage,
|
|
11
|
-
createDatabaseFactory
|
|
11
|
+
ServiceWorkerPool
|
|
12
12
|
} from "@b9g/platform";
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
ShovelServiceWorkerRegistration,
|
|
15
|
+
kServiceWorker,
|
|
16
|
+
createCacheFactory
|
|
16
17
|
} from "@b9g/platform/runtime";
|
|
17
|
-
import { CustomCacheStorage } from "@b9g/cache";
|
|
18
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 * as HTTP from "http";
|
|
23
|
-
import * as Path from "path";
|
|
24
|
-
import { getLogger } from "@logtape/logtape";
|
|
25
|
-
import { MemoryCache as MemoryCache2 } from "@b9g/cache/memory";
|
|
26
|
-
var entryTemplate = `// Node.js Production Server Entry
|
|
27
|
-
import {tmpdir} from "os"; // For [tmpdir] config expressions
|
|
28
|
-
import * as HTTP from "http";
|
|
29
|
-
import {parentPort} from "worker_threads";
|
|
30
|
-
import {Worker} from "@b9g/node-webworker";
|
|
31
|
-
import {getLogger} from "@logtape/logtape";
|
|
32
|
-
import {configureLogging} from "@b9g/platform/runtime";
|
|
33
|
-
import {config} from "shovel:config"; // Virtual module - resolved at build time
|
|
34
|
-
import Platform from "@b9g/platform-node";
|
|
35
|
-
|
|
36
|
-
// Configure logging before anything else
|
|
37
|
-
await configureLogging(config.logging);
|
|
38
|
-
|
|
39
|
-
const logger = getLogger(["shovel", "platform"]);
|
|
40
|
-
|
|
41
|
-
// Configuration from shovel:config
|
|
42
|
-
const PORT = config.port;
|
|
43
|
-
const HOST = config.host;
|
|
44
|
-
const WORKERS = config.workers;
|
|
45
|
-
|
|
46
|
-
// Use explicit marker instead of isMainThread
|
|
47
|
-
// This handles the edge case where Shovel's build output is embedded in another worker
|
|
48
|
-
const isShovelWorker = process.env.SHOVEL_SPAWNED_WORKER === "1";
|
|
49
|
-
|
|
50
|
-
if (WORKERS === 1) {
|
|
51
|
-
// Single worker mode: worker owns server (same as Bun)
|
|
52
|
-
// No reusePort needed since there's only one listener
|
|
53
|
-
if (isShovelWorker) {
|
|
54
|
-
// Worker thread: runs BOTH server AND ServiceWorker
|
|
55
|
-
const platform = new Platform({port: PORT, host: HOST, workers: 1});
|
|
56
|
-
|
|
57
|
-
// Track resources for shutdown - these get assigned during startup
|
|
58
|
-
let server;
|
|
59
|
-
let serviceWorker;
|
|
60
|
-
|
|
61
|
-
// Register shutdown handler BEFORE async startup to prevent race condition
|
|
62
|
-
// where SIGINT arrives during startup and the shutdown message is dropped
|
|
63
|
-
self.onmessage = async (event) => {
|
|
64
|
-
if (event.data.type === "shutdown") {
|
|
65
|
-
logger.info("Worker shutting down");
|
|
66
|
-
if (server) await server.close();
|
|
67
|
-
if (serviceWorker) await serviceWorker.dispose();
|
|
68
|
-
await platform.dispose();
|
|
69
|
-
postMessage({type: "shutdown-complete"});
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const userCodePath = new URL("./server.js", import.meta.url).pathname;
|
|
74
|
-
serviceWorker = await platform.loadServiceWorker(userCodePath);
|
|
75
|
-
|
|
76
|
-
server = platform.createServer(serviceWorker.handleRequest, {
|
|
77
|
-
port: PORT,
|
|
78
|
-
host: HOST,
|
|
79
|
-
});
|
|
80
|
-
await server.listen();
|
|
81
|
-
|
|
82
|
-
// Signal ready to main thread
|
|
83
|
-
postMessage({type: "ready"});
|
|
84
|
-
logger.info("Worker started with server", {port: PORT});
|
|
85
|
-
} else {
|
|
86
|
-
// Main thread: supervisor only - spawn single worker
|
|
87
|
-
logger.info("Starting production server (single worker)", {port: PORT});
|
|
88
|
-
|
|
89
|
-
// Port availability check - fail fast if port is in use
|
|
90
|
-
const checkPort = () => new Promise((resolve, reject) => {
|
|
91
|
-
const testServer = HTTP.createServer();
|
|
92
|
-
testServer.once("error", reject);
|
|
93
|
-
testServer.listen(PORT, HOST, () => {
|
|
94
|
-
testServer.close(() => resolve(undefined));
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
try {
|
|
98
|
-
await checkPort();
|
|
99
|
-
} catch (err) {
|
|
100
|
-
logger.error("Port unavailable", {port: PORT, host: HOST, error: err});
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let shuttingDown = false;
|
|
105
|
-
|
|
106
|
-
const worker = new Worker(new URL(import.meta.url), {
|
|
107
|
-
env: {SHOVEL_SPAWNED_WORKER: "1"},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
worker.onmessage = (event) => {
|
|
111
|
-
if (event.data.type === "ready") {
|
|
112
|
-
logger.info("Worker ready", {port: PORT});
|
|
113
|
-
} else if (event.data.type === "shutdown-complete") {
|
|
114
|
-
logger.info("Worker shutdown complete");
|
|
115
|
-
process.exit(0);
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
worker.onerror = (event) => {
|
|
120
|
-
logger.error("Worker error", {error: event.error});
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// If a worker crashes, fail fast - let process supervisor handle restarts
|
|
124
|
-
worker.addEventListener("close", (event) => {
|
|
125
|
-
if (shuttingDown) return;
|
|
126
|
-
if (event.code === 0) return; // Clean exit
|
|
127
|
-
logger.error("Worker crashed", {exitCode: event.code});
|
|
128
|
-
process.exit(1);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Graceful shutdown
|
|
132
|
-
const shutdown = () => {
|
|
133
|
-
shuttingDown = true;
|
|
134
|
-
logger.info("Shutting down");
|
|
135
|
-
worker.postMessage({type: "shutdown"});
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
process.on("SIGINT", shutdown);
|
|
139
|
-
process.on("SIGTERM", shutdown);
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
// Multi-worker mode: main thread owns server (no reusePort in Node.js)
|
|
143
|
-
// Workers handle ServiceWorker code via postMessage
|
|
144
|
-
if (isShovelWorker) {
|
|
145
|
-
// Worker: ServiceWorker only, respond via message loop
|
|
146
|
-
// This path uses the workerEntryTemplate, not this template
|
|
147
|
-
throw new Error("Multi-worker mode uses workerEntryTemplate, not entryTemplate");
|
|
148
|
-
} else {
|
|
149
|
-
// Main thread: HTTP server + dispatch to worker pool
|
|
150
|
-
const platform = new Platform({port: PORT, host: HOST, workers: WORKERS});
|
|
151
|
-
const userCodePath = new URL("./server.js", import.meta.url).pathname;
|
|
152
|
-
|
|
153
|
-
logger.info("Starting production server (multi-worker)", {workers: WORKERS, port: PORT});
|
|
154
|
-
|
|
155
|
-
// Load ServiceWorker with worker pool
|
|
156
|
-
const serviceWorker = await platform.loadServiceWorker(userCodePath, {
|
|
157
|
-
workerCount: WORKERS,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// Create HTTP server
|
|
161
|
-
const server = platform.createServer(serviceWorker.handleRequest, {
|
|
162
|
-
port: PORT,
|
|
163
|
-
host: HOST,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
await server.listen();
|
|
167
|
-
logger.info("Server running", {url: \`http://\${HOST}:\${PORT}\`, workers: WORKERS});
|
|
168
|
-
|
|
169
|
-
// Graceful shutdown
|
|
170
|
-
const shutdown = async () => {
|
|
171
|
-
logger.info("Shutting down server");
|
|
172
|
-
await serviceWorker.dispose();
|
|
173
|
-
await platform.dispose();
|
|
174
|
-
await server.close();
|
|
175
|
-
logger.info("Server stopped");
|
|
176
|
-
process.exit(0);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
process.on("SIGINT", shutdown);
|
|
180
|
-
process.on("SIGTERM", shutdown);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
`;
|
|
184
|
-
var workerEntryTemplate = `// Worker Entry for ServiceWorkerPool
|
|
185
|
-
// This file sets up the ServiceWorker runtime and message loop
|
|
186
|
-
import {tmpdir} from "os"; // For [tmpdir] config expressions
|
|
187
|
-
import {config} from "shovel:config";
|
|
188
|
-
import {initWorkerRuntime, startWorkerMessageLoop, configureLogging} from "@b9g/platform/runtime";
|
|
189
|
-
|
|
190
|
-
// Configure logging before anything else
|
|
191
|
-
await configureLogging(config.logging);
|
|
192
|
-
|
|
193
|
-
// Initialize the worker runtime (installs ServiceWorker globals)
|
|
194
|
-
// Platform defaults and paths are already resolved at build time
|
|
195
|
-
const {registration, databases} = await initWorkerRuntime({config});
|
|
196
|
-
|
|
197
|
-
// Import user code (registers event handlers via addEventListener)
|
|
198
|
-
// Must use dynamic import to ensure globals are installed first
|
|
199
|
-
await import("__USER_ENTRY__");
|
|
200
|
-
|
|
201
|
-
// Run ServiceWorker lifecycle
|
|
202
|
-
await registration.install();
|
|
203
|
-
await registration.activate();
|
|
204
|
-
|
|
205
|
-
// Start the message loop (handles request/response messages from main thread)
|
|
206
|
-
// Pass databases for graceful shutdown (close connections before termination)
|
|
207
|
-
startWorkerMessageLoop({registration, databases});
|
|
208
|
-
`;
|
|
209
19
|
var logger = getLogger(["shovel", "platform"]);
|
|
210
|
-
var
|
|
211
|
-
|
|
212
|
-
#
|
|
213
|
-
#workerPool;
|
|
214
|
-
#singleThreadedRuntime;
|
|
20
|
+
var NodeServiceWorkerContainer = class extends EventTarget {
|
|
21
|
+
#platform;
|
|
22
|
+
#pool;
|
|
215
23
|
#cacheStorage;
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Get options for testing
|
|
232
|
-
*/
|
|
233
|
-
get options() {
|
|
234
|
-
return this.#options;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Get/set worker pool for testing
|
|
238
|
-
*/
|
|
239
|
-
get workerPool() {
|
|
240
|
-
return this.#workerPool;
|
|
241
|
-
}
|
|
242
|
-
set workerPool(pool) {
|
|
243
|
-
this.#workerPool = pool;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* THE MAIN JOB - Load and run a ServiceWorker-style entrypoint in Node.js
|
|
247
|
-
* Uses Worker threads with coordinated cache storage for isolation and standards compliance
|
|
248
|
-
*/
|
|
249
|
-
async loadServiceWorker(entrypoint, options = {}) {
|
|
250
|
-
const workerCount = options.workerCount ?? this.#options.workers;
|
|
251
|
-
if (workerCount === 1 && !options.hotReload) {
|
|
252
|
-
return this.#loadServiceWorkerDirect(entrypoint, options);
|
|
253
|
-
}
|
|
254
|
-
return this.#loadServiceWorkerWithPool(entrypoint, options, workerCount);
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Load ServiceWorker directly in main thread (single-threaded mode)
|
|
258
|
-
* No postMessage overhead - maximum performance for production
|
|
259
|
-
*/
|
|
260
|
-
async #loadServiceWorkerDirect(entrypoint, _options) {
|
|
261
|
-
const entryPath = Path.resolve(this.#options.cwd, entrypoint);
|
|
262
|
-
let config = this.#options.config;
|
|
263
|
-
const configPath = Path.join(Path.dirname(entryPath), "config.js");
|
|
264
|
-
try {
|
|
265
|
-
const configModule = await import(configPath);
|
|
266
|
-
config = configModule.config ?? config;
|
|
267
|
-
} catch (err) {
|
|
268
|
-
logger.debug`Using platform config (no config.js): ${err}`;
|
|
269
|
-
}
|
|
270
|
-
if (!this.#cacheStorage) {
|
|
271
|
-
const runtimeCacheDefaults = {
|
|
272
|
-
default: { impl: MemoryCache }
|
|
273
|
-
};
|
|
274
|
-
const userCaches = config?.caches ?? {};
|
|
275
|
-
const cacheConfigs = {};
|
|
276
|
-
const allCacheNames = /* @__PURE__ */ new Set([
|
|
277
|
-
...Object.keys(runtimeCacheDefaults),
|
|
278
|
-
...Object.keys(userCaches)
|
|
279
|
-
]);
|
|
280
|
-
for (const name of allCacheNames) {
|
|
281
|
-
cacheConfigs[name] = {
|
|
282
|
-
...runtimeCacheDefaults[name],
|
|
283
|
-
...userCaches[name]
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
this.#cacheStorage = new CustomCacheStorage(
|
|
287
|
-
createCacheFactory({ configs: cacheConfigs })
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
if (!this.#directoryStorage) {
|
|
291
|
-
const runtimeDirDefaults = {
|
|
292
|
-
server: { impl: NodeFSDirectory },
|
|
293
|
-
public: { impl: NodeFSDirectory },
|
|
294
|
-
tmp: { impl: NodeFSDirectory }
|
|
295
|
-
};
|
|
296
|
-
const userDirs = config?.directories ?? {};
|
|
297
|
-
const dirConfigs = {};
|
|
298
|
-
const allDirNames = /* @__PURE__ */ new Set([
|
|
299
|
-
...Object.keys(runtimeDirDefaults),
|
|
300
|
-
...Object.keys(userDirs)
|
|
301
|
-
]);
|
|
302
|
-
for (const name of allDirNames) {
|
|
303
|
-
dirConfigs[name] = { ...runtimeDirDefaults[name], ...userDirs[name] };
|
|
304
|
-
}
|
|
305
|
-
this.#directoryStorage = new CustomDirectoryStorage(
|
|
306
|
-
createDirectoryFactory(dirConfigs)
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
if (!this.#databaseStorage) {
|
|
310
|
-
this.#databaseStorage = this.createDatabases(config);
|
|
311
|
-
}
|
|
312
|
-
if (this.#singleThreadedRuntime) {
|
|
313
|
-
await this.#singleThreadedRuntime.terminate();
|
|
314
|
-
}
|
|
315
|
-
if (this.#workerPool) {
|
|
316
|
-
await this.#workerPool.terminate();
|
|
317
|
-
this.#workerPool = void 0;
|
|
318
|
-
}
|
|
319
|
-
logger.info("Creating single-threaded ServiceWorker runtime", { entryPath });
|
|
320
|
-
this.#singleThreadedRuntime = new SingleThreadedRuntime({
|
|
321
|
-
caches: this.#cacheStorage,
|
|
322
|
-
directories: this.#directoryStorage,
|
|
323
|
-
databases: this.#databaseStorage,
|
|
324
|
-
loggers: new CustomLoggerStorage((cats) => getLogger(cats))
|
|
24
|
+
#registration;
|
|
25
|
+
#readyPromise;
|
|
26
|
+
#readyResolve;
|
|
27
|
+
// Standard ServiceWorkerContainer properties
|
|
28
|
+
controller;
|
|
29
|
+
oncontrollerchange;
|
|
30
|
+
onmessage;
|
|
31
|
+
onmessageerror;
|
|
32
|
+
constructor(platform) {
|
|
33
|
+
super();
|
|
34
|
+
this.#platform = platform;
|
|
35
|
+
this.#readyPromise = new Promise((resolve2) => {
|
|
36
|
+
this.#readyResolve = resolve2;
|
|
325
37
|
});
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const instance = {
|
|
331
|
-
runtime,
|
|
332
|
-
handleRequest: async (request) => {
|
|
333
|
-
if (!platform.#singleThreadedRuntime) {
|
|
334
|
-
throw new Error("SingleThreadedRuntime not initialized");
|
|
335
|
-
}
|
|
336
|
-
return platform.#singleThreadedRuntime.handleRequest(request);
|
|
337
|
-
},
|
|
338
|
-
install: async () => {
|
|
339
|
-
logger.info("ServiceWorker installed", { method: "single_threaded" });
|
|
340
|
-
},
|
|
341
|
-
activate: async () => {
|
|
342
|
-
logger.info("ServiceWorker activated", { method: "single_threaded" });
|
|
343
|
-
},
|
|
344
|
-
get ready() {
|
|
345
|
-
return runtime?.ready ?? false;
|
|
346
|
-
},
|
|
347
|
-
dispose: async () => {
|
|
348
|
-
if (platform.#singleThreadedRuntime) {
|
|
349
|
-
await platform.#singleThreadedRuntime.terminate();
|
|
350
|
-
platform.#singleThreadedRuntime = void 0;
|
|
351
|
-
}
|
|
352
|
-
logger.info("ServiceWorker disposed", {});
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
logger.info("ServiceWorker loaded", {
|
|
356
|
-
features: ["single_threaded", "no_postmessage_overhead"]
|
|
357
|
-
});
|
|
358
|
-
return instance;
|
|
38
|
+
this.controller = null;
|
|
39
|
+
this.oncontrollerchange = null;
|
|
40
|
+
this.onmessage = null;
|
|
41
|
+
this.onmessageerror = null;
|
|
359
42
|
}
|
|
360
43
|
/**
|
|
361
|
-
*
|
|
44
|
+
* Register a ServiceWorker script
|
|
45
|
+
* Spawns worker threads and runs lifecycle
|
|
362
46
|
*/
|
|
363
|
-
async
|
|
364
|
-
const
|
|
365
|
-
|
|
47
|
+
async register(scriptURL, options) {
|
|
48
|
+
const urlStr = typeof scriptURL === "string" ? scriptURL : scriptURL.toString();
|
|
49
|
+
const scope = options?.scope ?? "/";
|
|
50
|
+
let entryPath;
|
|
51
|
+
if (urlStr.startsWith("file://")) {
|
|
52
|
+
entryPath = new URL(urlStr).pathname;
|
|
53
|
+
} else {
|
|
54
|
+
entryPath = Path.resolve(this.#platform.options.cwd, urlStr);
|
|
55
|
+
}
|
|
56
|
+
let config = this.#platform.options.config;
|
|
366
57
|
const configPath = Path.join(Path.dirname(entryPath), "config.js");
|
|
367
58
|
try {
|
|
368
59
|
const configModule = await import(configPath);
|
|
369
60
|
config = configModule.config ?? config;
|
|
370
|
-
} catch (
|
|
371
|
-
logger.debug`Using platform config (no config.js): ${
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.debug`Using platform config (no config.js found): ${error}`;
|
|
372
63
|
}
|
|
373
|
-
if (!this.#cacheStorage) {
|
|
64
|
+
if (!this.#cacheStorage && config?.caches) {
|
|
374
65
|
this.#cacheStorage = new CustomCacheStorage(
|
|
375
|
-
createCacheFactory({
|
|
376
|
-
configs: config?.caches ?? {}
|
|
377
|
-
})
|
|
66
|
+
createCacheFactory({ configs: config.caches })
|
|
378
67
|
);
|
|
379
68
|
}
|
|
380
|
-
if (this.#
|
|
381
|
-
await this.#
|
|
382
|
-
this.#singleThreadedRuntime = void 0;
|
|
69
|
+
if (this.#pool) {
|
|
70
|
+
await this.#pool.terminate();
|
|
383
71
|
}
|
|
384
|
-
|
|
385
|
-
await this.#workerPool.terminate();
|
|
386
|
-
}
|
|
387
|
-
logger.info("Creating ServiceWorker pool", {
|
|
388
|
-
entryPath,
|
|
389
|
-
workerCount
|
|
390
|
-
});
|
|
391
|
-
this.#workerPool = new ServiceWorkerPool(
|
|
72
|
+
this.#pool = new ServiceWorkerPool(
|
|
392
73
|
{
|
|
393
|
-
workerCount,
|
|
394
|
-
|
|
395
|
-
cwd: this.#options.cwd
|
|
74
|
+
workerCount: this.#platform.options.workers,
|
|
75
|
+
createWorker: (entrypoint) => this.#platform.createWorker(entrypoint)
|
|
396
76
|
},
|
|
397
77
|
entryPath,
|
|
398
78
|
this.#cacheStorage
|
|
399
79
|
);
|
|
400
|
-
await this.#
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
handleRequest: async (request) => {
|
|
406
|
-
if (!platform.#workerPool) {
|
|
407
|
-
throw new Error("ServiceWorkerPool not initialized");
|
|
408
|
-
}
|
|
409
|
-
return platform.#workerPool.handleRequest(request);
|
|
410
|
-
},
|
|
411
|
-
install: async () => {
|
|
412
|
-
logger.info("ServiceWorker installed", {
|
|
413
|
-
method: "worker_threads"
|
|
414
|
-
});
|
|
415
|
-
},
|
|
416
|
-
activate: async () => {
|
|
417
|
-
logger.info("ServiceWorker activated", {
|
|
418
|
-
method: "worker_threads"
|
|
419
|
-
});
|
|
420
|
-
},
|
|
421
|
-
get ready() {
|
|
422
|
-
return workerPool?.ready ?? false;
|
|
423
|
-
},
|
|
424
|
-
dispose: async () => {
|
|
425
|
-
if (platform.#workerPool) {
|
|
426
|
-
await platform.#workerPool.terminate();
|
|
427
|
-
platform.#workerPool = void 0;
|
|
428
|
-
}
|
|
429
|
-
logger.info("ServiceWorker disposed", {});
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
logger.info("ServiceWorker loaded", {
|
|
433
|
-
features: ["worker_threads", "coordinated_caches"]
|
|
434
|
-
});
|
|
435
|
-
return instance;
|
|
80
|
+
await this.#pool.init();
|
|
81
|
+
this.#registration = new ShovelServiceWorkerRegistration(scope, urlStr);
|
|
82
|
+
this.#registration[kServiceWorker]._setState("activated");
|
|
83
|
+
this.#readyResolve?.(this.#registration);
|
|
84
|
+
return this.#registration;
|
|
436
85
|
}
|
|
437
86
|
/**
|
|
438
|
-
*
|
|
439
|
-
* Used for testing - production uses the generated config module
|
|
440
|
-
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
87
|
+
* Get registration for scope
|
|
441
88
|
*/
|
|
442
|
-
async
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
};
|
|
446
|
-
const userCaches = this.#options.config?.caches ?? {};
|
|
447
|
-
const configs = {};
|
|
448
|
-
const allNames = /* @__PURE__ */ new Set([
|
|
449
|
-
...Object.keys(runtimeDefaults),
|
|
450
|
-
...Object.keys(userCaches)
|
|
451
|
-
]);
|
|
452
|
-
for (const name of allNames) {
|
|
453
|
-
configs[name] = { ...runtimeDefaults[name], ...userCaches[name] };
|
|
89
|
+
async getRegistration(scope) {
|
|
90
|
+
if (scope === void 0 || scope === "/" || scope === this.#registration?.scope) {
|
|
91
|
+
return this.#registration;
|
|
454
92
|
}
|
|
455
|
-
return
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get all registrations
|
|
97
|
+
*/
|
|
98
|
+
async getRegistrations() {
|
|
99
|
+
return this.#registration ? [this.#registration] : [];
|
|
456
100
|
}
|
|
457
101
|
/**
|
|
458
|
-
*
|
|
459
|
-
* Used for testing - production uses the generated config module
|
|
460
|
-
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
102
|
+
* Start receiving messages (no-op in server context)
|
|
461
103
|
*/
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
104
|
+
startMessages() {
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Ready promise - resolves when a registration is active
|
|
108
|
+
*/
|
|
109
|
+
get ready() {
|
|
110
|
+
return this.#readyPromise;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Internal: Get worker pool for request handling
|
|
114
|
+
*/
|
|
115
|
+
get pool() {
|
|
116
|
+
return this.#pool;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Internal: Terminate workers and dispose cache storage
|
|
120
|
+
*/
|
|
121
|
+
async terminate() {
|
|
122
|
+
await this.#pool?.terminate();
|
|
123
|
+
this.#pool = void 0;
|
|
124
|
+
await this.#cacheStorage?.dispose();
|
|
125
|
+
this.#cacheStorage = void 0;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Internal: Reload workers (for hot reload)
|
|
129
|
+
*/
|
|
130
|
+
async reloadWorkers(entrypoint) {
|
|
131
|
+
await this.#pool?.reloadWorkers(entrypoint);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var NodePlatform = class {
|
|
135
|
+
name;
|
|
136
|
+
serviceWorker;
|
|
137
|
+
#options;
|
|
138
|
+
#server;
|
|
139
|
+
constructor(options = {}) {
|
|
140
|
+
this.name = "node";
|
|
141
|
+
const cwd = options.cwd || process.cwd();
|
|
142
|
+
this.#options = {
|
|
143
|
+
port: options.port ?? 3e3,
|
|
144
|
+
host: options.host ?? "localhost",
|
|
145
|
+
workers: options.workers ?? 1,
|
|
146
|
+
cwd,
|
|
147
|
+
config: options.config
|
|
467
148
|
};
|
|
468
|
-
|
|
469
|
-
const configs = {};
|
|
470
|
-
const allNames = /* @__PURE__ */ new Set([
|
|
471
|
-
...Object.keys(runtimeDefaults),
|
|
472
|
-
...Object.keys(userDirs)
|
|
473
|
-
]);
|
|
474
|
-
for (const name of allNames) {
|
|
475
|
-
configs[name] = { ...runtimeDefaults[name], ...userDirs[name] };
|
|
476
|
-
}
|
|
477
|
-
return new CustomDirectoryStorage(createDirectoryFactory(configs));
|
|
149
|
+
this.serviceWorker = new NodeServiceWorkerContainer(this);
|
|
478
150
|
}
|
|
479
151
|
/**
|
|
480
|
-
* Create
|
|
152
|
+
* Create a worker instance for the pool
|
|
153
|
+
* Can be overridden for testing
|
|
481
154
|
*/
|
|
482
|
-
async
|
|
483
|
-
|
|
155
|
+
async createWorker(entrypoint) {
|
|
156
|
+
const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
|
|
157
|
+
return new NodeWebWorker(entrypoint);
|
|
484
158
|
}
|
|
485
159
|
/**
|
|
486
|
-
*
|
|
160
|
+
* Start the HTTP server, routing requests to ServiceWorker
|
|
487
161
|
*/
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
|
|
162
|
+
async listen() {
|
|
163
|
+
const pool = this.serviceWorker.pool;
|
|
164
|
+
if (!pool) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
"No ServiceWorker registered. Call serviceWorker.register() first."
|
|
167
|
+
);
|
|
493
168
|
}
|
|
494
|
-
|
|
169
|
+
this.#server = this.createServer((request) => pool.handleRequest(request));
|
|
170
|
+
await this.#server.listen();
|
|
171
|
+
return this.#server;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Close the server and terminate workers
|
|
175
|
+
*/
|
|
176
|
+
async close() {
|
|
177
|
+
await this.#server?.close();
|
|
178
|
+
await this.serviceWorker.terminate();
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get options for testing
|
|
182
|
+
*/
|
|
183
|
+
get options() {
|
|
184
|
+
return this.#options;
|
|
495
185
|
}
|
|
496
186
|
/**
|
|
497
|
-
*
|
|
187
|
+
* Create HTTP server for Node.js
|
|
498
188
|
*/
|
|
499
189
|
createServer(handler, options = {}) {
|
|
500
190
|
const port = options.port ?? this.#options.port;
|
|
@@ -531,8 +221,15 @@ var NodePlatform = class extends BasePlatform {
|
|
|
531
221
|
}
|
|
532
222
|
} catch (error) {
|
|
533
223
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
534
|
-
logger.error("Request error: {error}", { error: err });
|
|
535
224
|
const httpError = isHTTPError(error) ? error : new InternalServerError(err.message, { cause: err });
|
|
225
|
+
if (httpError.status >= 500) {
|
|
226
|
+
logger.error("Request error: {error}", { error: err });
|
|
227
|
+
} else {
|
|
228
|
+
logger.warn("Request error: {status} {error}", {
|
|
229
|
+
status: httpError.status,
|
|
230
|
+
error: err
|
|
231
|
+
});
|
|
232
|
+
}
|
|
536
233
|
const isDev = import.meta.env?.MODE !== "production";
|
|
537
234
|
const response = httpError.toResponse(isDev);
|
|
538
235
|
res.statusCode = response.status;
|
|
@@ -584,31 +281,89 @@ var NodePlatform = class extends BasePlatform {
|
|
|
584
281
|
}
|
|
585
282
|
/**
|
|
586
283
|
* Reload workers for hot reloading (called by CLI)
|
|
284
|
+
* @deprecated Use serviceWorker.reloadWorkers() instead
|
|
587
285
|
* @param entrypoint - Path to the new entrypoint (hashed filename)
|
|
588
286
|
*/
|
|
589
287
|
async reloadWorkers(entrypoint) {
|
|
590
|
-
|
|
591
|
-
await this.#workerPool.reloadWorkers(entrypoint);
|
|
592
|
-
} else if (this.#singleThreadedRuntime) {
|
|
593
|
-
await this.#singleThreadedRuntime.load(entrypoint);
|
|
594
|
-
}
|
|
288
|
+
await this.serviceWorker.reloadWorkers(entrypoint);
|
|
595
289
|
}
|
|
596
290
|
/**
|
|
597
|
-
* Get
|
|
291
|
+
* Get entry points for bundling.
|
|
598
292
|
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
* @param options.type - "production" (default) or "worker"
|
|
293
|
+
* Development mode:
|
|
294
|
+
* - worker.js: Single worker with message loop (develop command acts as supervisor)
|
|
602
295
|
*
|
|
603
|
-
*
|
|
604
|
-
* -
|
|
605
|
-
* -
|
|
296
|
+
* Production mode:
|
|
297
|
+
* - index.js: Supervisor that spawns workers and owns the HTTP server
|
|
298
|
+
* - worker.js: Worker that handles requests via message loop
|
|
606
299
|
*/
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
300
|
+
getEntryPoints(userEntryPath, mode) {
|
|
301
|
+
const workerCode = `// Node.js Worker
|
|
302
|
+
import {parentPort} from "node:worker_threads";
|
|
303
|
+
import {configureLogging, initWorkerRuntime, runLifecycle, startWorkerMessageLoop} from "@b9g/platform/runtime";
|
|
304
|
+
import {config} from "shovel:config";
|
|
305
|
+
|
|
306
|
+
await configureLogging(config.logging);
|
|
307
|
+
|
|
308
|
+
// Initialize worker runtime (installs ServiceWorker globals)
|
|
309
|
+
const {registration, databases} = await initWorkerRuntime({config});
|
|
310
|
+
|
|
311
|
+
// Import user code (registers event handlers)
|
|
312
|
+
await import("${userEntryPath}");
|
|
313
|
+
|
|
314
|
+
// Run ServiceWorker lifecycle (stage from config.lifecycle if present)
|
|
315
|
+
await runLifecycle(registration, config.lifecycle?.stage);
|
|
316
|
+
|
|
317
|
+
// Start message loop for request handling, or signal ready and exit in lifecycle-only mode
|
|
318
|
+
if (config.lifecycle) {
|
|
319
|
+
parentPort?.postMessage({type: "ready"});
|
|
320
|
+
// Clean shutdown after lifecycle
|
|
321
|
+
if (databases) await databases.closeAll();
|
|
322
|
+
process.exit(0);
|
|
323
|
+
} else {
|
|
324
|
+
startWorkerMessageLoop({registration, databases});
|
|
325
|
+
}
|
|
326
|
+
`;
|
|
327
|
+
if (mode === "development") {
|
|
328
|
+
return { worker: workerCode };
|
|
610
329
|
}
|
|
611
|
-
|
|
330
|
+
const supervisorCode = `// Node.js Production Supervisor
|
|
331
|
+
import {Worker} from "@b9g/node-webworker";
|
|
332
|
+
import {getLogger} from "@logtape/logtape";
|
|
333
|
+
import {configureLogging} from "@b9g/platform/runtime";
|
|
334
|
+
import NodePlatform from "@b9g/platform-node";
|
|
335
|
+
import {config} from "shovel:config";
|
|
336
|
+
|
|
337
|
+
await configureLogging(config.logging);
|
|
338
|
+
const logger = getLogger(["shovel", "platform"]);
|
|
339
|
+
|
|
340
|
+
logger.info("Starting production server", {port: config.port, workers: config.workers});
|
|
341
|
+
|
|
342
|
+
// Initialize platform and register ServiceWorker
|
|
343
|
+
// Override createWorker to use the imported Worker class (avoids require() issues with ESM)
|
|
344
|
+
const platform = new NodePlatform({port: config.port, host: config.host, workers: config.workers});
|
|
345
|
+
platform.createWorker = (entrypoint) => new Worker(entrypoint);
|
|
346
|
+
await platform.serviceWorker.register(new URL("./worker.js", import.meta.url).href);
|
|
347
|
+
await platform.serviceWorker.ready;
|
|
348
|
+
|
|
349
|
+
// Start HTTP server
|
|
350
|
+
await platform.listen();
|
|
351
|
+
|
|
352
|
+
logger.info("Server started", {port: config.port, host: config.host, workers: config.workers});
|
|
353
|
+
|
|
354
|
+
// Graceful shutdown
|
|
355
|
+
const handleShutdown = async () => {
|
|
356
|
+
logger.info("Shutting down");
|
|
357
|
+
await platform.close();
|
|
358
|
+
process.exit(0);
|
|
359
|
+
};
|
|
360
|
+
process.on("SIGINT", handleShutdown);
|
|
361
|
+
process.on("SIGTERM", handleShutdown);
|
|
362
|
+
`;
|
|
363
|
+
return {
|
|
364
|
+
supervisor: supervisorCode,
|
|
365
|
+
worker: workerCode
|
|
366
|
+
};
|
|
612
367
|
}
|
|
613
368
|
/**
|
|
614
369
|
* Get Node.js-specific esbuild configuration
|
|
@@ -663,22 +418,8 @@ var NodePlatform = class extends BasePlatform {
|
|
|
663
418
|
* Dispose of platform resources
|
|
664
419
|
*/
|
|
665
420
|
async dispose() {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
this.#singleThreadedRuntime = void 0;
|
|
669
|
-
}
|
|
670
|
-
if (this.#workerPool) {
|
|
671
|
-
await this.#workerPool.terminate();
|
|
672
|
-
this.#workerPool = void 0;
|
|
673
|
-
}
|
|
674
|
-
if (this.#cacheStorage) {
|
|
675
|
-
await this.#cacheStorage.dispose();
|
|
676
|
-
this.#cacheStorage = void 0;
|
|
677
|
-
}
|
|
678
|
-
if (this.#databaseStorage) {
|
|
679
|
-
await this.#databaseStorage.closeAll();
|
|
680
|
-
this.#databaseStorage = void 0;
|
|
681
|
-
}
|
|
421
|
+
await this.close();
|
|
422
|
+
await this.serviceWorker.terminate();
|
|
682
423
|
}
|
|
683
424
|
// =========================================================================
|
|
684
425
|
// Config Expression Method Overrides
|
|
@@ -692,7 +433,8 @@ var NodePlatform = class extends BasePlatform {
|
|
|
692
433
|
};
|
|
693
434
|
var src_default = NodePlatform;
|
|
694
435
|
export {
|
|
695
|
-
|
|
436
|
+
MemoryCache as DefaultCache,
|
|
696
437
|
NodePlatform,
|
|
438
|
+
NodeServiceWorkerContainer,
|
|
697
439
|
src_default as default
|
|
698
440
|
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js Platform Module
|
|
3
|
+
*
|
|
4
|
+
* Build-time and dev-time functions for Node.js.
|
|
5
|
+
* Runtime functions are in ./runtime.ts
|
|
6
|
+
*/
|
|
7
|
+
import type { EntryPoints, ESBuildConfig, PlatformDefaults, DevServerOptions, DevServer } from "@b9g/platform/module";
|
|
8
|
+
export declare const name = "node";
|
|
9
|
+
/**
|
|
10
|
+
* Get entry points for bundling.
|
|
11
|
+
*
|
|
12
|
+
* Development mode:
|
|
13
|
+
* - worker.js: Single worker with message loop (develop command acts as supervisor)
|
|
14
|
+
*
|
|
15
|
+
* Production mode:
|
|
16
|
+
* - supervisor.js: Spawns workers and owns the HTTP server
|
|
17
|
+
* - worker.js: Handles requests via message loop
|
|
18
|
+
*/
|
|
19
|
+
export declare function getEntryPoints(userEntryPath: string, mode: "development" | "production"): EntryPoints;
|
|
20
|
+
/**
|
|
21
|
+
* Get ESBuild configuration for Node.js.
|
|
22
|
+
*
|
|
23
|
+
* Note: Node.js doesn't support import.meta.env natively, so we alias it
|
|
24
|
+
* to process.env for compatibility with code that uses Vite-style env access.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getESBuildConfig(): ESBuildConfig;
|
|
27
|
+
/**
|
|
28
|
+
* Get platform defaults for config generation.
|
|
29
|
+
*
|
|
30
|
+
* Provides default directories (server, public, tmp) that work
|
|
31
|
+
* out of the box for Node.js deployments.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getDefaults(): PlatformDefaults;
|
|
34
|
+
/**
|
|
35
|
+
* Create a dev server using ServiceWorkerPool.
|
|
36
|
+
*
|
|
37
|
+
* Dynamically imports the platform class to keep heavy dependencies
|
|
38
|
+
* out of production bundles.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createDevServer(options: DevServerOptions): Promise<DevServer>;
|
package/src/platform.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/// <reference types="./platform.d.ts" />
|
|
2
|
+
// src/platform.ts
|
|
3
|
+
import { builtinModules } from "node:module";
|
|
4
|
+
import { getLogger } from "@logtape/logtape";
|
|
5
|
+
var logger = getLogger(["shovel", "platform"]);
|
|
6
|
+
var name = "node";
|
|
7
|
+
function getEntryPoints(userEntryPath, mode) {
|
|
8
|
+
const safePath = JSON.stringify(userEntryPath);
|
|
9
|
+
const workerCode = `// Node.js Worker
|
|
10
|
+
import {parentPort} from "node:worker_threads";
|
|
11
|
+
import {configureLogging, initWorkerRuntime, runLifecycle, startWorkerMessageLoop} from "@b9g/platform/runtime";
|
|
12
|
+
import {config} from "shovel:config";
|
|
13
|
+
|
|
14
|
+
await configureLogging(config.logging);
|
|
15
|
+
|
|
16
|
+
// Initialize worker runtime (installs ServiceWorker globals)
|
|
17
|
+
const {registration, databases} = await initWorkerRuntime({config});
|
|
18
|
+
|
|
19
|
+
// Import user code (registers event handlers)
|
|
20
|
+
await import(${safePath});
|
|
21
|
+
|
|
22
|
+
// Run ServiceWorker lifecycle (stage from config.lifecycle if present)
|
|
23
|
+
await runLifecycle(registration, config.lifecycle?.stage);
|
|
24
|
+
|
|
25
|
+
// Start message loop for request handling, or signal ready and exit in lifecycle-only mode
|
|
26
|
+
if (config.lifecycle) {
|
|
27
|
+
parentPort?.postMessage({type: "ready"});
|
|
28
|
+
// Clean shutdown after lifecycle
|
|
29
|
+
if (databases) await databases.closeAll();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
} else {
|
|
32
|
+
startWorkerMessageLoop({registration, databases});
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
if (mode === "development") {
|
|
36
|
+
return { worker: workerCode };
|
|
37
|
+
}
|
|
38
|
+
const supervisorCode = `// Node.js Production Supervisor
|
|
39
|
+
import {Worker} from "@b9g/node-webworker";
|
|
40
|
+
import {getLogger} from "@logtape/logtape";
|
|
41
|
+
import {configureLogging} from "@b9g/platform/runtime";
|
|
42
|
+
import NodePlatform from "@b9g/platform-node";
|
|
43
|
+
import {config} from "shovel:config";
|
|
44
|
+
|
|
45
|
+
await configureLogging(config.logging);
|
|
46
|
+
const logger = getLogger(["shovel", "platform"]);
|
|
47
|
+
|
|
48
|
+
logger.info("Starting production server", {port: config.port, workers: config.workers});
|
|
49
|
+
|
|
50
|
+
// Initialize platform and register ServiceWorker
|
|
51
|
+
// Override createWorker to use the imported Worker class (avoids require() issues with ESM)
|
|
52
|
+
const platform = new NodePlatform({port: config.port, host: config.host, workers: config.workers});
|
|
53
|
+
platform.createWorker = (entrypoint) => new Worker(entrypoint);
|
|
54
|
+
await platform.serviceWorker.register(new URL("./worker.js", import.meta.url).href);
|
|
55
|
+
await platform.serviceWorker.ready;
|
|
56
|
+
|
|
57
|
+
// Start HTTP server
|
|
58
|
+
await platform.listen();
|
|
59
|
+
|
|
60
|
+
logger.info("Server started", {port: config.port, host: config.host, workers: config.workers});
|
|
61
|
+
|
|
62
|
+
// Graceful shutdown
|
|
63
|
+
const handleShutdown = async () => {
|
|
64
|
+
logger.info("Shutting down");
|
|
65
|
+
await platform.close();
|
|
66
|
+
process.exit(0);
|
|
67
|
+
};
|
|
68
|
+
process.on("SIGINT", handleShutdown);
|
|
69
|
+
process.on("SIGTERM", handleShutdown);
|
|
70
|
+
`;
|
|
71
|
+
return {
|
|
72
|
+
supervisor: supervisorCode,
|
|
73
|
+
worker: workerCode
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function getESBuildConfig() {
|
|
77
|
+
return {
|
|
78
|
+
platform: "node",
|
|
79
|
+
external: ["node:*", ...builtinModules],
|
|
80
|
+
define: {
|
|
81
|
+
// Node.js doesn't support import.meta.env, alias to process.env
|
|
82
|
+
"import.meta.env": "process.env"
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function getDefaults() {
|
|
87
|
+
return {
|
|
88
|
+
caches: {
|
|
89
|
+
default: {
|
|
90
|
+
module: "@b9g/cache/memory",
|
|
91
|
+
export: "MemoryCache"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
directories: {
|
|
95
|
+
server: {
|
|
96
|
+
module: "@b9g/filesystem/node-fs",
|
|
97
|
+
export: "NodeFSDirectory",
|
|
98
|
+
path: "[outdir]/server"
|
|
99
|
+
},
|
|
100
|
+
public: {
|
|
101
|
+
module: "@b9g/filesystem/node-fs",
|
|
102
|
+
export: "NodeFSDirectory",
|
|
103
|
+
path: "[outdir]/public"
|
|
104
|
+
},
|
|
105
|
+
tmp: {
|
|
106
|
+
module: "@b9g/filesystem/node-fs",
|
|
107
|
+
export: "NodeFSDirectory",
|
|
108
|
+
path: "[tmpdir]"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function createDevServer(options) {
|
|
114
|
+
const { port, host, workerPath, workers = 1 } = options;
|
|
115
|
+
logger.info("Starting Node.js dev server", { workerPath, workers });
|
|
116
|
+
const { default: NodePlatform } = await import("./index.js");
|
|
117
|
+
const platform = new NodePlatform({
|
|
118
|
+
port,
|
|
119
|
+
host,
|
|
120
|
+
workers
|
|
121
|
+
});
|
|
122
|
+
await platform.serviceWorker.register(workerPath);
|
|
123
|
+
await platform.serviceWorker.ready;
|
|
124
|
+
await platform.listen();
|
|
125
|
+
logger.info("Node.js dev server ready");
|
|
126
|
+
const url = `http://${host}:${port}`;
|
|
127
|
+
return {
|
|
128
|
+
url,
|
|
129
|
+
async reload(newWorkerPath) {
|
|
130
|
+
logger.info("Reloading workers", { workerPath: newWorkerPath });
|
|
131
|
+
await platform.serviceWorker.reloadWorkers(newWorkerPath);
|
|
132
|
+
logger.info("Workers reloaded");
|
|
133
|
+
},
|
|
134
|
+
async close() {
|
|
135
|
+
logger.info("Stopping Node.js dev server");
|
|
136
|
+
await platform.dispose();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
createDevServer,
|
|
142
|
+
getDefaults,
|
|
143
|
+
getESBuildConfig,
|
|
144
|
+
getEntryPoints,
|
|
145
|
+
name
|
|
146
|
+
};
|