@b9g/platform-node 0.1.13 → 0.1.14-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 +2 -2
- package/src/index.d.ts +87 -29
- package/src/index.js +254 -451
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.0",
|
|
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.0",
|
|
18
18
|
"@logtape/logtape": "^1.2.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
package/src/index.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides hot reloading, ESBuild integration, and optimized caching for Node.js environments.
|
|
5
5
|
*/
|
|
6
|
-
import { BasePlatform, type PlatformConfig, type PlatformDefaults, type Handler, type Server, type ServerOptions, type ServiceWorkerOptions, type ServiceWorkerInstance, type EntryWrapperOptions, type PlatformESBuildConfig, ServiceWorkerPool, CustomLoggerStorage, CustomDatabaseStorage } from "@b9g/platform";
|
|
7
|
-
import { type ShovelConfig } from "@b9g/platform/runtime";
|
|
8
6
|
import { CustomCacheStorage } from "@b9g/cache";
|
|
9
7
|
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
8
|
+
import { BasePlatform, type PlatformConfig, type PlatformDefaults, type Handler, type Server, type ServerOptions, type PlatformESBuildConfig, type ProductionEntryPoints, ServiceWorkerPool, CustomLoggerStorage, CustomDatabaseStorage } from "@b9g/platform";
|
|
9
|
+
import { type ShovelConfig } from "@b9g/platform/runtime";
|
|
10
10
|
export interface NodePlatformOptions extends PlatformConfig {
|
|
11
11
|
/** Port for development server (default: 3000) */
|
|
12
12
|
port?: number;
|
|
@@ -19,6 +19,51 @@ export interface NodePlatformOptions extends PlatformConfig {
|
|
|
19
19
|
/** Shovel configuration (caches, directories, etc.) */
|
|
20
20
|
config?: ShovelConfig;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Node.js ServiceWorkerContainer implementation
|
|
24
|
+
* Manages ServiceWorker registrations backed by worker threads
|
|
25
|
+
*/
|
|
26
|
+
export declare class NodeServiceWorkerContainer extends EventTarget implements ServiceWorkerContainer {
|
|
27
|
+
#private;
|
|
28
|
+
readonly controller: ServiceWorker | null;
|
|
29
|
+
oncontrollerchange: ((ev: Event) => unknown) | null;
|
|
30
|
+
onmessage: ((ev: MessageEvent) => unknown) | null;
|
|
31
|
+
onmessageerror: ((ev: MessageEvent) => unknown) | null;
|
|
32
|
+
constructor(platform: NodePlatform);
|
|
33
|
+
/**
|
|
34
|
+
* Register a ServiceWorker script
|
|
35
|
+
* Spawns worker threads and runs lifecycle
|
|
36
|
+
*/
|
|
37
|
+
register(scriptURL: string | URL, options?: RegistrationOptions): Promise<ServiceWorkerRegistration>;
|
|
38
|
+
/**
|
|
39
|
+
* Get registration for scope
|
|
40
|
+
*/
|
|
41
|
+
getRegistration(scope?: string): Promise<ServiceWorkerRegistration | undefined>;
|
|
42
|
+
/**
|
|
43
|
+
* Get all registrations
|
|
44
|
+
*/
|
|
45
|
+
getRegistrations(): Promise<readonly ServiceWorkerRegistration[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Start receiving messages (no-op in server context)
|
|
48
|
+
*/
|
|
49
|
+
startMessages(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Ready promise - resolves when a registration is active
|
|
52
|
+
*/
|
|
53
|
+
get ready(): Promise<ServiceWorkerRegistration>;
|
|
54
|
+
/**
|
|
55
|
+
* Internal: Get worker pool for request handling
|
|
56
|
+
*/
|
|
57
|
+
get pool(): ServiceWorkerPool | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Internal: Terminate workers and dispose cache storage
|
|
60
|
+
*/
|
|
61
|
+
terminate(): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Internal: Reload workers (for hot reload)
|
|
64
|
+
*/
|
|
65
|
+
reloadWorkers(entrypoint: string): Promise<void>;
|
|
66
|
+
}
|
|
22
67
|
/**
|
|
23
68
|
* Node.js platform implementation
|
|
24
69
|
* ServiceWorker entrypoint loader for Node.js with ESBuild VM system
|
|
@@ -26,7 +71,21 @@ export interface NodePlatformOptions extends PlatformConfig {
|
|
|
26
71
|
export declare class NodePlatform extends BasePlatform {
|
|
27
72
|
#private;
|
|
28
73
|
readonly name: string;
|
|
74
|
+
readonly serviceWorker: NodeServiceWorkerContainer;
|
|
29
75
|
constructor(options?: NodePlatformOptions);
|
|
76
|
+
/**
|
|
77
|
+
* Create a worker instance for the pool
|
|
78
|
+
* Can be overridden for testing
|
|
79
|
+
*/
|
|
80
|
+
createWorker(entrypoint: string): Promise<Worker>;
|
|
81
|
+
/**
|
|
82
|
+
* Start the HTTP server, routing requests to ServiceWorker
|
|
83
|
+
*/
|
|
84
|
+
listen(): Promise<Server>;
|
|
85
|
+
/**
|
|
86
|
+
* Close the server and terminate workers
|
|
87
|
+
*/
|
|
88
|
+
close(): Promise<void>;
|
|
30
89
|
/**
|
|
31
90
|
* Get options for testing
|
|
32
91
|
*/
|
|
@@ -38,33 +97,35 @@ export declare class NodePlatform extends BasePlatform {
|
|
|
38
97
|
config?: ShovelConfig;
|
|
39
98
|
};
|
|
40
99
|
/**
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* THE MAIN JOB - Load and run a ServiceWorker-style entrypoint in Node.js
|
|
47
|
-
* Uses Worker threads with coordinated cache storage for isolation and standards compliance
|
|
48
|
-
*/
|
|
49
|
-
loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
|
|
50
|
-
/**
|
|
51
|
-
* Create cache storage using config from shovel.json
|
|
52
|
-
* Used for testing - production uses the generated config module
|
|
53
|
-
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
100
|
+
* Create cache storage for Node.js
|
|
101
|
+
*
|
|
102
|
+
* Default: MemoryCache (in-process LRU cache).
|
|
103
|
+
* Override via shovel.json caches config.
|
|
104
|
+
* Note: Used for dev/testing - production uses generated config module.
|
|
54
105
|
*/
|
|
55
106
|
createCaches(): Promise<CustomCacheStorage>;
|
|
56
107
|
/**
|
|
57
|
-
* Create directory storage
|
|
58
|
-
*
|
|
59
|
-
*
|
|
108
|
+
* Create directory storage for Node.js
|
|
109
|
+
*
|
|
110
|
+
* Defaults:
|
|
111
|
+
* - server: NodeFSDirectory at cwd (app files)
|
|
112
|
+
* - public: NodeFSDirectory at cwd (static assets)
|
|
113
|
+
* - tmp: NodeFSDirectory at OS temp dir
|
|
114
|
+
*
|
|
115
|
+
* Override via shovel.json directories config.
|
|
60
116
|
*/
|
|
61
117
|
createDirectories(): Promise<CustomDirectoryStorage>;
|
|
62
118
|
/**
|
|
63
|
-
* Create logger storage
|
|
119
|
+
* Create logger storage for Node.js
|
|
120
|
+
*
|
|
121
|
+
* Uses LogTape for structured logging.
|
|
64
122
|
*/
|
|
65
123
|
createLoggers(): Promise<CustomLoggerStorage>;
|
|
66
124
|
/**
|
|
67
|
-
* Create database storage
|
|
125
|
+
* Create database storage for Node.js
|
|
126
|
+
*
|
|
127
|
+
* Returns undefined if no databases configured in shovel.json.
|
|
128
|
+
* Supports SQLite via better-sqlite3.
|
|
68
129
|
*/
|
|
69
130
|
createDatabases(configOverride?: NodePlatformOptions["config"]): CustomDatabaseStorage | undefined;
|
|
70
131
|
/**
|
|
@@ -73,21 +134,18 @@ export declare class NodePlatform extends BasePlatform {
|
|
|
73
134
|
createServer(handler: Handler, options?: ServerOptions): Server;
|
|
74
135
|
/**
|
|
75
136
|
* Reload workers for hot reloading (called by CLI)
|
|
137
|
+
* @deprecated Use serviceWorker.reloadWorkers() instead
|
|
76
138
|
* @param entrypoint - Path to the new entrypoint (hashed filename)
|
|
77
139
|
*/
|
|
78
140
|
reloadWorkers(entrypoint: string): Promise<void>;
|
|
79
141
|
/**
|
|
80
|
-
* Get
|
|
81
|
-
*
|
|
82
|
-
* @param entryPath - Absolute path to user's entrypoint file
|
|
83
|
-
* @param options - Entry wrapper options
|
|
84
|
-
* @param options.type - "production" (default) or "worker"
|
|
142
|
+
* Get production entry points for bundling.
|
|
85
143
|
*
|
|
86
|
-
*
|
|
87
|
-
* -
|
|
88
|
-
* -
|
|
144
|
+
* Node.js produces two files:
|
|
145
|
+
* - index.js: Supervisor that spawns workers and owns the HTTP server
|
|
146
|
+
* - worker.js: Worker that handles requests via message loop
|
|
89
147
|
*/
|
|
90
|
-
|
|
148
|
+
getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
|
|
91
149
|
/**
|
|
92
150
|
* Get Node.js-specific esbuild configuration
|
|
93
151
|
*
|
package/src/index.js
CHANGED
|
@@ -1,220 +1,151 @@
|
|
|
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 { MemoryCache } from "@b9g/cache/memory";
|
|
10
|
+
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
11
|
+
import { NodeFSDirectory } from "@b9g/filesystem/node-fs";
|
|
12
|
+
import { InternalServerError, isHTTPError } from "@b9g/http-errors";
|
|
5
13
|
import {
|
|
6
14
|
BasePlatform,
|
|
7
15
|
ServiceWorkerPool,
|
|
8
|
-
SingleThreadedRuntime,
|
|
9
16
|
CustomLoggerStorage,
|
|
10
17
|
CustomDatabaseStorage,
|
|
11
|
-
createDatabaseFactory
|
|
18
|
+
createDatabaseFactory,
|
|
19
|
+
mergeConfigWithDefaults
|
|
12
20
|
} from "@b9g/platform";
|
|
13
21
|
import {
|
|
22
|
+
ShovelServiceWorkerRegistration,
|
|
23
|
+
kServiceWorker,
|
|
14
24
|
createCacheFactory,
|
|
15
25
|
createDirectoryFactory
|
|
16
26
|
} from "@b9g/platform/runtime";
|
|
17
|
-
import { CustomCacheStorage } from "@b9g/cache";
|
|
18
|
-
import { MemoryCache } from "@b9g/cache/memory";
|
|
19
|
-
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
20
|
-
import { NodeFSDirectory } from "@b9g/filesystem/node-fs";
|
|
21
|
-
import { InternalServerError, isHTTPError } from "@b9g/http-errors";
|
|
22
|
-
import * as HTTP from "http";
|
|
23
|
-
import * as Path from "path";
|
|
24
|
-
import { getLogger } from "@logtape/logtape";
|
|
25
27
|
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
28
|
var logger = getLogger(["shovel", "platform"]);
|
|
29
|
+
var NodeServiceWorkerContainer = class extends EventTarget {
|
|
30
|
+
#platform;
|
|
31
|
+
#pool;
|
|
32
|
+
#cacheStorage;
|
|
33
|
+
#registration;
|
|
34
|
+
#readyPromise;
|
|
35
|
+
#readyResolve;
|
|
36
|
+
// Standard ServiceWorkerContainer properties
|
|
37
|
+
controller;
|
|
38
|
+
oncontrollerchange;
|
|
39
|
+
onmessage;
|
|
40
|
+
onmessageerror;
|
|
41
|
+
constructor(platform) {
|
|
42
|
+
super();
|
|
43
|
+
this.#platform = platform;
|
|
44
|
+
this.#readyPromise = new Promise((resolve2) => {
|
|
45
|
+
this.#readyResolve = resolve2;
|
|
46
|
+
});
|
|
47
|
+
this.controller = null;
|
|
48
|
+
this.oncontrollerchange = null;
|
|
49
|
+
this.onmessage = null;
|
|
50
|
+
this.onmessageerror = null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Register a ServiceWorker script
|
|
54
|
+
* Spawns worker threads and runs lifecycle
|
|
55
|
+
*/
|
|
56
|
+
async register(scriptURL, options) {
|
|
57
|
+
const urlStr = typeof scriptURL === "string" ? scriptURL : scriptURL.toString();
|
|
58
|
+
const scope = options?.scope ?? "/";
|
|
59
|
+
let entryPath;
|
|
60
|
+
if (urlStr.startsWith("file://")) {
|
|
61
|
+
entryPath = new URL(urlStr).pathname;
|
|
62
|
+
} else {
|
|
63
|
+
entryPath = Path.resolve(this.#platform.options.cwd, urlStr);
|
|
64
|
+
}
|
|
65
|
+
let config = this.#platform.options.config;
|
|
66
|
+
const configPath = Path.join(Path.dirname(entryPath), "config.js");
|
|
67
|
+
try {
|
|
68
|
+
const configModule = await import(configPath);
|
|
69
|
+
config = configModule.config ?? config;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.debug`Using platform config (no config.js found): ${error}`;
|
|
72
|
+
}
|
|
73
|
+
if (!this.#cacheStorage && config?.caches) {
|
|
74
|
+
this.#cacheStorage = new CustomCacheStorage(
|
|
75
|
+
createCacheFactory({ configs: config.caches })
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (this.#pool) {
|
|
79
|
+
await this.#pool.terminate();
|
|
80
|
+
}
|
|
81
|
+
this.#pool = new ServiceWorkerPool(
|
|
82
|
+
{
|
|
83
|
+
workerCount: this.#platform.options.workers,
|
|
84
|
+
createWorker: (entrypoint) => this.#platform.createWorker(entrypoint)
|
|
85
|
+
},
|
|
86
|
+
entryPath,
|
|
87
|
+
this.#cacheStorage
|
|
88
|
+
);
|
|
89
|
+
await this.#pool.init();
|
|
90
|
+
this.#registration = new ShovelServiceWorkerRegistration(scope, urlStr);
|
|
91
|
+
this.#registration[kServiceWorker]._setState("activated");
|
|
92
|
+
this.#readyResolve?.(this.#registration);
|
|
93
|
+
return this.#registration;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get registration for scope
|
|
97
|
+
*/
|
|
98
|
+
async getRegistration(scope) {
|
|
99
|
+
if (scope === void 0 || scope === "/" || scope === this.#registration?.scope) {
|
|
100
|
+
return this.#registration;
|
|
101
|
+
}
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get all registrations
|
|
106
|
+
*/
|
|
107
|
+
async getRegistrations() {
|
|
108
|
+
return this.#registration ? [this.#registration] : [];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Start receiving messages (no-op in server context)
|
|
112
|
+
*/
|
|
113
|
+
startMessages() {
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Ready promise - resolves when a registration is active
|
|
117
|
+
*/
|
|
118
|
+
get ready() {
|
|
119
|
+
return this.#readyPromise;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Internal: Get worker pool for request handling
|
|
123
|
+
*/
|
|
124
|
+
get pool() {
|
|
125
|
+
return this.#pool;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Internal: Terminate workers and dispose cache storage
|
|
129
|
+
*/
|
|
130
|
+
async terminate() {
|
|
131
|
+
await this.#pool?.terminate();
|
|
132
|
+
this.#pool = void 0;
|
|
133
|
+
await this.#cacheStorage?.dispose();
|
|
134
|
+
this.#cacheStorage = void 0;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Internal: Reload workers (for hot reload)
|
|
138
|
+
*/
|
|
139
|
+
async reloadWorkers(entrypoint) {
|
|
140
|
+
await this.#pool?.reloadWorkers(entrypoint);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
210
143
|
var NodePlatform = class extends BasePlatform {
|
|
211
144
|
name;
|
|
145
|
+
serviceWorker;
|
|
212
146
|
#options;
|
|
213
|
-
#workerPool;
|
|
214
|
-
#singleThreadedRuntime;
|
|
215
|
-
#cacheStorage;
|
|
216
|
-
#directoryStorage;
|
|
217
147
|
#databaseStorage;
|
|
148
|
+
#server;
|
|
218
149
|
constructor(options = {}) {
|
|
219
150
|
super(options);
|
|
220
151
|
this.name = "node";
|
|
@@ -226,264 +157,93 @@ var NodePlatform = class extends BasePlatform {
|
|
|
226
157
|
cwd,
|
|
227
158
|
config: options.config
|
|
228
159
|
};
|
|
160
|
+
this.serviceWorker = new NodeServiceWorkerContainer(this);
|
|
229
161
|
}
|
|
230
162
|
/**
|
|
231
|
-
*
|
|
163
|
+
* Create a worker instance for the pool
|
|
164
|
+
* Can be overridden for testing
|
|
232
165
|
*/
|
|
233
|
-
|
|
234
|
-
|
|
166
|
+
async createWorker(entrypoint) {
|
|
167
|
+
const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
|
|
168
|
+
return new NodeWebWorker(entrypoint);
|
|
235
169
|
}
|
|
236
170
|
/**
|
|
237
|
-
*
|
|
171
|
+
* Start the HTTP server, routing requests to ServiceWorker
|
|
238
172
|
*/
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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);
|
|
173
|
+
async listen() {
|
|
174
|
+
const pool = this.serviceWorker.pool;
|
|
175
|
+
if (!pool) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"No ServiceWorker registered. Call serviceWorker.register() first."
|
|
178
|
+
);
|
|
253
179
|
}
|
|
254
|
-
|
|
180
|
+
this.#server = this.createServer((request) => pool.handleRequest(request));
|
|
181
|
+
await this.#server.listen();
|
|
182
|
+
return this.#server;
|
|
255
183
|
}
|
|
256
184
|
/**
|
|
257
|
-
*
|
|
258
|
-
* No postMessage overhead - maximum performance for production
|
|
185
|
+
* Close the server and terminate workers
|
|
259
186
|
*/
|
|
260
|
-
async
|
|
261
|
-
|
|
262
|
-
|
|
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))
|
|
325
|
-
});
|
|
326
|
-
await this.#singleThreadedRuntime.init();
|
|
327
|
-
await this.#singleThreadedRuntime.load(entryPath);
|
|
328
|
-
const runtime = this.#singleThreadedRuntime;
|
|
329
|
-
const platform = this;
|
|
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;
|
|
187
|
+
async close() {
|
|
188
|
+
await this.#server?.close();
|
|
189
|
+
await this.serviceWorker.terminate();
|
|
359
190
|
}
|
|
360
191
|
/**
|
|
361
|
-
*
|
|
192
|
+
* Get options for testing
|
|
362
193
|
*/
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
let config = this.#options.config;
|
|
366
|
-
const configPath = Path.join(Path.dirname(entryPath), "config.js");
|
|
367
|
-
try {
|
|
368
|
-
const configModule = await import(configPath);
|
|
369
|
-
config = configModule.config ?? config;
|
|
370
|
-
} catch (err) {
|
|
371
|
-
logger.debug`Using platform config (no config.js): ${err}`;
|
|
372
|
-
}
|
|
373
|
-
if (!this.#cacheStorage) {
|
|
374
|
-
this.#cacheStorage = new CustomCacheStorage(
|
|
375
|
-
createCacheFactory({
|
|
376
|
-
configs: config?.caches ?? {}
|
|
377
|
-
})
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
if (this.#singleThreadedRuntime) {
|
|
381
|
-
await this.#singleThreadedRuntime.terminate();
|
|
382
|
-
this.#singleThreadedRuntime = void 0;
|
|
383
|
-
}
|
|
384
|
-
if (this.#workerPool) {
|
|
385
|
-
await this.#workerPool.terminate();
|
|
386
|
-
}
|
|
387
|
-
logger.info("Creating ServiceWorker pool", {
|
|
388
|
-
entryPath,
|
|
389
|
-
workerCount
|
|
390
|
-
});
|
|
391
|
-
this.#workerPool = new ServiceWorkerPool(
|
|
392
|
-
{
|
|
393
|
-
workerCount,
|
|
394
|
-
requestTimeout: 3e4,
|
|
395
|
-
cwd: this.#options.cwd
|
|
396
|
-
},
|
|
397
|
-
entryPath,
|
|
398
|
-
this.#cacheStorage
|
|
399
|
-
);
|
|
400
|
-
await this.#workerPool.init();
|
|
401
|
-
const workerPool = this.#workerPool;
|
|
402
|
-
const platform = this;
|
|
403
|
-
const instance = {
|
|
404
|
-
runtime: workerPool,
|
|
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;
|
|
194
|
+
get options() {
|
|
195
|
+
return this.#options;
|
|
436
196
|
}
|
|
437
197
|
/**
|
|
438
|
-
* Create cache storage
|
|
439
|
-
*
|
|
440
|
-
*
|
|
198
|
+
* Create cache storage for Node.js
|
|
199
|
+
*
|
|
200
|
+
* Default: MemoryCache (in-process LRU cache).
|
|
201
|
+
* Override via shovel.json caches config.
|
|
202
|
+
* Note: Used for dev/testing - production uses generated config module.
|
|
441
203
|
*/
|
|
442
204
|
async createCaches() {
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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] };
|
|
454
|
-
}
|
|
205
|
+
const defaults = { default: { impl: MemoryCache } };
|
|
206
|
+
const configs = mergeConfigWithDefaults(
|
|
207
|
+
defaults,
|
|
208
|
+
this.#options.config?.caches
|
|
209
|
+
);
|
|
455
210
|
return new CustomCacheStorage(createCacheFactory({ configs }));
|
|
456
211
|
}
|
|
457
212
|
/**
|
|
458
|
-
* Create directory storage
|
|
459
|
-
*
|
|
460
|
-
*
|
|
213
|
+
* Create directory storage for Node.js
|
|
214
|
+
*
|
|
215
|
+
* Defaults:
|
|
216
|
+
* - server: NodeFSDirectory at cwd (app files)
|
|
217
|
+
* - public: NodeFSDirectory at cwd (static assets)
|
|
218
|
+
* - tmp: NodeFSDirectory at OS temp dir
|
|
219
|
+
*
|
|
220
|
+
* Override via shovel.json directories config.
|
|
461
221
|
*/
|
|
462
222
|
async createDirectories() {
|
|
463
|
-
const
|
|
223
|
+
const defaults = {
|
|
464
224
|
server: { impl: NodeFSDirectory, path: this.#options.cwd },
|
|
465
225
|
public: { impl: NodeFSDirectory, path: this.#options.cwd },
|
|
466
226
|
tmp: { impl: NodeFSDirectory, path: tmpdir() }
|
|
467
227
|
};
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
...Object.keys(userDirs)
|
|
473
|
-
]);
|
|
474
|
-
for (const name of allNames) {
|
|
475
|
-
configs[name] = { ...runtimeDefaults[name], ...userDirs[name] };
|
|
476
|
-
}
|
|
228
|
+
const configs = mergeConfigWithDefaults(
|
|
229
|
+
defaults,
|
|
230
|
+
this.#options.config?.directories
|
|
231
|
+
);
|
|
477
232
|
return new CustomDirectoryStorage(createDirectoryFactory(configs));
|
|
478
233
|
}
|
|
479
234
|
/**
|
|
480
|
-
* Create logger storage
|
|
235
|
+
* Create logger storage for Node.js
|
|
236
|
+
*
|
|
237
|
+
* Uses LogTape for structured logging.
|
|
481
238
|
*/
|
|
482
239
|
async createLoggers() {
|
|
483
240
|
return new CustomLoggerStorage((categories) => getLogger(categories));
|
|
484
241
|
}
|
|
485
242
|
/**
|
|
486
|
-
* Create database storage
|
|
243
|
+
* Create database storage for Node.js
|
|
244
|
+
*
|
|
245
|
+
* Returns undefined if no databases configured in shovel.json.
|
|
246
|
+
* Supports SQLite via better-sqlite3.
|
|
487
247
|
*/
|
|
488
248
|
createDatabases(configOverride) {
|
|
489
249
|
const config = configOverride ?? this.#options.config;
|
|
@@ -584,31 +344,83 @@ var NodePlatform = class extends BasePlatform {
|
|
|
584
344
|
}
|
|
585
345
|
/**
|
|
586
346
|
* Reload workers for hot reloading (called by CLI)
|
|
347
|
+
* @deprecated Use serviceWorker.reloadWorkers() instead
|
|
587
348
|
* @param entrypoint - Path to the new entrypoint (hashed filename)
|
|
588
349
|
*/
|
|
589
350
|
async reloadWorkers(entrypoint) {
|
|
590
|
-
|
|
591
|
-
await this.#workerPool.reloadWorkers(entrypoint);
|
|
592
|
-
} else if (this.#singleThreadedRuntime) {
|
|
593
|
-
await this.#singleThreadedRuntime.load(entrypoint);
|
|
594
|
-
}
|
|
351
|
+
await this.serviceWorker.reloadWorkers(entrypoint);
|
|
595
352
|
}
|
|
596
353
|
/**
|
|
597
|
-
* Get
|
|
354
|
+
* Get production entry points for bundling.
|
|
598
355
|
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* Returns:
|
|
604
|
-
* - "production": Server entry that loads ServiceWorkerPool
|
|
605
|
-
* - "worker": Worker entry that sets up runtime and message loop
|
|
356
|
+
* Node.js produces two files:
|
|
357
|
+
* - index.js: Supervisor that spawns workers and owns the HTTP server
|
|
358
|
+
* - worker.js: Worker that handles requests via message loop
|
|
606
359
|
*/
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
360
|
+
getProductionEntryPoints(userEntryPath) {
|
|
361
|
+
const supervisorCode = `// Node.js Production Supervisor
|
|
362
|
+
import {Worker} from "@b9g/node-webworker";
|
|
363
|
+
import {getLogger} from "@logtape/logtape";
|
|
364
|
+
import {configureLogging} from "@b9g/platform/runtime";
|
|
365
|
+
import NodePlatform from "@b9g/platform-node";
|
|
366
|
+
import {config} from "shovel:config";
|
|
367
|
+
|
|
368
|
+
await configureLogging(config.logging);
|
|
369
|
+
const logger = getLogger(["shovel", "platform"]);
|
|
370
|
+
|
|
371
|
+
logger.info("Starting production server", {port: config.port, workers: config.workers});
|
|
372
|
+
|
|
373
|
+
// Initialize platform and register ServiceWorker
|
|
374
|
+
// Override createWorker to use the imported Worker class (avoids require() issues with ESM)
|
|
375
|
+
const platform = new NodePlatform({port: config.port, host: config.host, workers: config.workers});
|
|
376
|
+
platform.createWorker = (entrypoint) => new Worker(entrypoint);
|
|
377
|
+
await platform.serviceWorker.register(new URL("./worker.js", import.meta.url).href);
|
|
378
|
+
await platform.serviceWorker.ready;
|
|
379
|
+
|
|
380
|
+
// Start HTTP server
|
|
381
|
+
await platform.listen();
|
|
382
|
+
|
|
383
|
+
logger.info("Server started", {port: config.port, host: config.host, workers: config.workers});
|
|
384
|
+
|
|
385
|
+
// Graceful shutdown
|
|
386
|
+
const handleShutdown = async () => {
|
|
387
|
+
logger.info("Shutting down");
|
|
388
|
+
await platform.close();
|
|
389
|
+
process.exit(0);
|
|
390
|
+
};
|
|
391
|
+
process.on("SIGINT", handleShutdown);
|
|
392
|
+
process.on("SIGTERM", handleShutdown);
|
|
393
|
+
`;
|
|
394
|
+
const workerCode = `// Node.js Production Worker
|
|
395
|
+
import {parentPort} from "node:worker_threads";
|
|
396
|
+
import {configureLogging, initWorkerRuntime, runLifecycle, startWorkerMessageLoop} from "@b9g/platform/runtime";
|
|
397
|
+
import {config} from "shovel:config";
|
|
398
|
+
|
|
399
|
+
await configureLogging(config.logging);
|
|
400
|
+
|
|
401
|
+
// Initialize worker runtime (installs ServiceWorker globals)
|
|
402
|
+
const {registration, databases} = await initWorkerRuntime({config});
|
|
403
|
+
|
|
404
|
+
// Import user code (registers event handlers)
|
|
405
|
+
await import("${userEntryPath}");
|
|
406
|
+
|
|
407
|
+
// Run ServiceWorker lifecycle (stage from config.lifecycle if present)
|
|
408
|
+
await runLifecycle(registration, config.lifecycle?.stage);
|
|
409
|
+
|
|
410
|
+
// Start message loop for request handling, or signal ready and exit in lifecycle-only mode
|
|
411
|
+
if (config.lifecycle) {
|
|
412
|
+
parentPort?.postMessage({type: "ready"});
|
|
413
|
+
// Clean shutdown after lifecycle
|
|
414
|
+
if (databases) await databases.closeAll();
|
|
415
|
+
process.exit(0);
|
|
416
|
+
} else {
|
|
417
|
+
startWorkerMessageLoop({registration, databases});
|
|
418
|
+
}
|
|
419
|
+
`;
|
|
420
|
+
return {
|
|
421
|
+
index: supervisorCode,
|
|
422
|
+
worker: workerCode
|
|
423
|
+
};
|
|
612
424
|
}
|
|
613
425
|
/**
|
|
614
426
|
* Get Node.js-specific esbuild configuration
|
|
@@ -663,18 +475,8 @@ var NodePlatform = class extends BasePlatform {
|
|
|
663
475
|
* Dispose of platform resources
|
|
664
476
|
*/
|
|
665
477
|
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
|
-
}
|
|
478
|
+
await this.close();
|
|
479
|
+
await this.serviceWorker.terminate();
|
|
678
480
|
if (this.#databaseStorage) {
|
|
679
481
|
await this.#databaseStorage.closeAll();
|
|
680
482
|
this.#databaseStorage = void 0;
|
|
@@ -694,5 +496,6 @@ var src_default = NodePlatform;
|
|
|
694
496
|
export {
|
|
695
497
|
MemoryCache2 as DefaultCache,
|
|
696
498
|
NodePlatform,
|
|
499
|
+
NodeServiceWorkerContainer,
|
|
697
500
|
src_default as default
|
|
698
501
|
};
|