@b9g/platform-node 0.1.11 → 0.1.13
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/README.md +2 -2
- package/package.json +5 -5
- package/src/index.d.ts +66 -5
- package/src/index.js +408 -28
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Node.js platform adapter for Shovel. Runs ServiceWorker applications on Node.js
|
|
|
8
8
|
- Hot module reloading for development via VM module system
|
|
9
9
|
- Worker thread pool for concurrent request handling
|
|
10
10
|
- Memory and filesystem cache backends
|
|
11
|
-
- File System Access API implementation via
|
|
11
|
+
- File System Access API implementation via NodeFSDirectory
|
|
12
12
|
- ServiceWorker lifecycle support (install, activate, fetch events)
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
@@ -112,7 +112,7 @@ Loads and runs a ServiceWorker entrypoint.
|
|
|
112
112
|
Configured via `caches` option:
|
|
113
113
|
|
|
114
114
|
- `memory`: In-memory caching (MemoryCache)
|
|
115
|
-
- `filesystem`: File-based caching (
|
|
115
|
+
- `filesystem`: File-based caching (NodeFSDirectory)
|
|
116
116
|
|
|
117
117
|
## Worker Thread Architecture
|
|
118
118
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/platform-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Node.js platform adapter for Shovel with hot reloading and ESBuild integration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shovel",
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
"esbuild"
|
|
12
12
|
],
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@b9g/cache": "^0.
|
|
15
|
-
"@b9g/http-errors": "^0.
|
|
16
|
-
"@b9g/
|
|
14
|
+
"@b9g/cache": "^0.2.0-beta.0",
|
|
15
|
+
"@b9g/http-errors": "^0.2.0-beta.0",
|
|
16
|
+
"@b9g/node-webworker": "^0.2.0-beta.1",
|
|
17
|
+
"@b9g/platform": "^0.1.13",
|
|
17
18
|
"@logtape/logtape": "^1.2.0"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@b9g/libuild": "^0.1.18",
|
|
21
|
-
"bun-types": "latest",
|
|
22
22
|
"@types/node": "^18.0.0"
|
|
23
23
|
},
|
|
24
24
|
"type": "module",
|
package/src/index.d.ts
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides hot reloading, ESBuild integration, and optimized caching for Node.js environments.
|
|
5
5
|
*/
|
|
6
|
-
import { BasePlatform, PlatformConfig, Handler, Server, ServerOptions, ServiceWorkerOptions, ServiceWorkerInstance, ServiceWorkerPool } from "@b9g/platform";
|
|
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";
|
|
7
8
|
import { CustomCacheStorage } from "@b9g/cache";
|
|
8
|
-
|
|
9
|
+
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
9
10
|
export interface NodePlatformOptions extends PlatformConfig {
|
|
10
11
|
/** Port for development server (default: 3000) */
|
|
11
12
|
port?: number;
|
|
@@ -13,6 +14,10 @@ export interface NodePlatformOptions extends PlatformConfig {
|
|
|
13
14
|
host?: string;
|
|
14
15
|
/** Working directory for file resolution */
|
|
15
16
|
cwd?: string;
|
|
17
|
+
/** Number of worker threads (default: 1) */
|
|
18
|
+
workers?: number;
|
|
19
|
+
/** Shovel configuration (caches, directories, etc.) */
|
|
20
|
+
config?: ShovelConfig;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
23
|
* Node.js platform implementation
|
|
@@ -25,7 +30,13 @@ export declare class NodePlatform extends BasePlatform {
|
|
|
25
30
|
/**
|
|
26
31
|
* Get options for testing
|
|
27
32
|
*/
|
|
28
|
-
get options():
|
|
33
|
+
get options(): {
|
|
34
|
+
port: number;
|
|
35
|
+
host: string;
|
|
36
|
+
cwd: string;
|
|
37
|
+
workers: number;
|
|
38
|
+
config?: ShovelConfig;
|
|
39
|
+
};
|
|
29
40
|
/**
|
|
30
41
|
* Get/set worker pool for testing
|
|
31
42
|
*/
|
|
@@ -37,10 +48,25 @@ export declare class NodePlatform extends BasePlatform {
|
|
|
37
48
|
*/
|
|
38
49
|
loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
|
|
39
50
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
|
42
54
|
*/
|
|
43
55
|
createCaches(): Promise<CustomCacheStorage>;
|
|
56
|
+
/**
|
|
57
|
+
* Create directory storage using config from shovel.json
|
|
58
|
+
* Used for testing - production uses the generated config module
|
|
59
|
+
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
60
|
+
*/
|
|
61
|
+
createDirectories(): Promise<CustomDirectoryStorage>;
|
|
62
|
+
/**
|
|
63
|
+
* Create logger storage using config from shovel.json
|
|
64
|
+
*/
|
|
65
|
+
createLoggers(): Promise<CustomLoggerStorage>;
|
|
66
|
+
/**
|
|
67
|
+
* Create database storage from declarative config in shovel.json
|
|
68
|
+
*/
|
|
69
|
+
createDatabases(configOverride?: NodePlatformOptions["config"]): CustomDatabaseStorage | undefined;
|
|
44
70
|
/**
|
|
45
71
|
* SUPPORTING UTILITY - Create HTTP server for Node.js
|
|
46
72
|
*/
|
|
@@ -50,12 +76,47 @@ export declare class NodePlatform extends BasePlatform {
|
|
|
50
76
|
* @param entrypoint - Path to the new entrypoint (hashed filename)
|
|
51
77
|
*/
|
|
52
78
|
reloadWorkers(entrypoint: string): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Get virtual entry wrapper for Node.js
|
|
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"
|
|
85
|
+
*
|
|
86
|
+
* Returns:
|
|
87
|
+
* - "production": Server entry that loads ServiceWorkerPool
|
|
88
|
+
* - "worker": Worker entry that sets up runtime and message loop
|
|
89
|
+
*/
|
|
90
|
+
getEntryWrapper(entryPath: string, options?: EntryWrapperOptions): string;
|
|
91
|
+
/**
|
|
92
|
+
* Get Node.js-specific esbuild configuration
|
|
93
|
+
*
|
|
94
|
+
* Note: Node.js doesn't support import.meta.env natively, so we alias it
|
|
95
|
+
* to process.env for compatibility with code that uses Vite-style env access.
|
|
96
|
+
*/
|
|
97
|
+
getESBuildConfig(): PlatformESBuildConfig;
|
|
98
|
+
/**
|
|
99
|
+
* Get Node.js-specific defaults for config generation
|
|
100
|
+
*
|
|
101
|
+
* Provides default directories (server, public, tmp) that work
|
|
102
|
+
* out of the box for Node.js deployments.
|
|
103
|
+
*/
|
|
104
|
+
getDefaults(): PlatformDefaults;
|
|
53
105
|
/**
|
|
54
106
|
* Dispose of platform resources
|
|
55
107
|
*/
|
|
56
108
|
dispose(): Promise<void>;
|
|
109
|
+
/**
|
|
110
|
+
* Get the OS temp directory (Node.js-specific implementation)
|
|
111
|
+
*/
|
|
112
|
+
tmpdir(): string;
|
|
57
113
|
}
|
|
58
114
|
/**
|
|
59
115
|
* Default export for easy importing
|
|
60
116
|
*/
|
|
61
117
|
export default NodePlatform;
|
|
118
|
+
/**
|
|
119
|
+
* Platform's default cache implementation.
|
|
120
|
+
* Re-exported so config can reference: { module: "@b9g/platform-node", export: "DefaultCache" }
|
|
121
|
+
*/
|
|
122
|
+
export { MemoryCache as DefaultCache } from "@b9g/cache/memory";
|
package/src/index.js
CHANGED
|
@@ -1,36 +1,230 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
2
|
// src/index.ts
|
|
3
|
+
import { builtinModules } from "node:module";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
3
5
|
import {
|
|
4
6
|
BasePlatform,
|
|
5
7
|
ServiceWorkerPool,
|
|
6
8
|
SingleThreadedRuntime,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
CustomLoggerStorage,
|
|
10
|
+
CustomDatabaseStorage,
|
|
11
|
+
createDatabaseFactory
|
|
9
12
|
} from "@b9g/platform";
|
|
13
|
+
import {
|
|
14
|
+
createCacheFactory,
|
|
15
|
+
createDirectoryFactory
|
|
16
|
+
} from "@b9g/platform/runtime";
|
|
10
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";
|
|
11
21
|
import { InternalServerError, isHTTPError } from "@b9g/http-errors";
|
|
12
22
|
import * as HTTP from "http";
|
|
13
23
|
import * as Path from "path";
|
|
14
24
|
import { getLogger } from "@logtape/logtape";
|
|
15
|
-
|
|
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
|
+
var logger = getLogger(["shovel", "platform"]);
|
|
16
210
|
var NodePlatform = class extends BasePlatform {
|
|
17
211
|
name;
|
|
18
212
|
#options;
|
|
19
213
|
#workerPool;
|
|
20
214
|
#singleThreadedRuntime;
|
|
21
215
|
#cacheStorage;
|
|
22
|
-
#
|
|
216
|
+
#directoryStorage;
|
|
217
|
+
#databaseStorage;
|
|
23
218
|
constructor(options = {}) {
|
|
24
219
|
super(options);
|
|
25
220
|
this.name = "node";
|
|
26
221
|
const cwd = options.cwd || process.cwd();
|
|
27
|
-
this.#config = loadConfig(cwd);
|
|
28
|
-
logger.info("Loaded configuration", { config: this.#config });
|
|
29
222
|
this.#options = {
|
|
30
|
-
port: options.port ??
|
|
31
|
-
host: options.host ??
|
|
223
|
+
port: options.port ?? 3e3,
|
|
224
|
+
host: options.host ?? "localhost",
|
|
225
|
+
workers: options.workers ?? 1,
|
|
32
226
|
cwd,
|
|
33
|
-
|
|
227
|
+
config: options.config
|
|
34
228
|
};
|
|
35
229
|
}
|
|
36
230
|
/**
|
|
@@ -53,7 +247,7 @@ var NodePlatform = class extends BasePlatform {
|
|
|
53
247
|
* Uses Worker threads with coordinated cache storage for isolation and standards compliance
|
|
54
248
|
*/
|
|
55
249
|
async loadServiceWorker(entrypoint, options = {}) {
|
|
56
|
-
const workerCount = options.workerCount ?? this.#
|
|
250
|
+
const workerCount = options.workerCount ?? this.#options.workers;
|
|
57
251
|
if (workerCount === 1 && !options.hotReload) {
|
|
58
252
|
return this.#loadServiceWorkerDirect(entrypoint, options);
|
|
59
253
|
}
|
|
@@ -65,9 +259,55 @@ var NodePlatform = class extends BasePlatform {
|
|
|
65
259
|
*/
|
|
66
260
|
async #loadServiceWorkerDirect(entrypoint, _options) {
|
|
67
261
|
const entryPath = Path.resolve(this.#options.cwd, entrypoint);
|
|
68
|
-
|
|
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
|
+
}
|
|
69
270
|
if (!this.#cacheStorage) {
|
|
70
|
-
|
|
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);
|
|
71
311
|
}
|
|
72
312
|
if (this.#singleThreadedRuntime) {
|
|
73
313
|
await this.#singleThreadedRuntime.terminate();
|
|
@@ -78,12 +318,13 @@ var NodePlatform = class extends BasePlatform {
|
|
|
78
318
|
}
|
|
79
319
|
logger.info("Creating single-threaded ServiceWorker runtime", { entryPath });
|
|
80
320
|
this.#singleThreadedRuntime = new SingleThreadedRuntime({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
321
|
+
caches: this.#cacheStorage,
|
|
322
|
+
directories: this.#directoryStorage,
|
|
323
|
+
databases: this.#databaseStorage,
|
|
324
|
+
loggers: new CustomLoggerStorage((cats) => getLogger(cats))
|
|
84
325
|
});
|
|
85
326
|
await this.#singleThreadedRuntime.init();
|
|
86
|
-
await this.#singleThreadedRuntime.
|
|
327
|
+
await this.#singleThreadedRuntime.load(entryPath);
|
|
87
328
|
const runtime = this.#singleThreadedRuntime;
|
|
88
329
|
const platform = this;
|
|
89
330
|
const instance = {
|
|
@@ -121,8 +362,20 @@ var NodePlatform = class extends BasePlatform {
|
|
|
121
362
|
*/
|
|
122
363
|
async #loadServiceWorkerWithPool(entrypoint, _options, workerCount) {
|
|
123
364
|
const entryPath = Path.resolve(this.#options.cwd, entrypoint);
|
|
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
|
+
}
|
|
124
373
|
if (!this.#cacheStorage) {
|
|
125
|
-
this.#cacheStorage =
|
|
374
|
+
this.#cacheStorage = new CustomCacheStorage(
|
|
375
|
+
createCacheFactory({
|
|
376
|
+
configs: config?.caches ?? {}
|
|
377
|
+
})
|
|
378
|
+
);
|
|
126
379
|
}
|
|
127
380
|
if (this.#singleThreadedRuntime) {
|
|
128
381
|
await this.#singleThreadedRuntime.terminate();
|
|
@@ -142,11 +395,9 @@ var NodePlatform = class extends BasePlatform {
|
|
|
142
395
|
cwd: this.#options.cwd
|
|
143
396
|
},
|
|
144
397
|
entryPath,
|
|
145
|
-
this.#cacheStorage
|
|
146
|
-
this.#config
|
|
398
|
+
this.#cacheStorage
|
|
147
399
|
);
|
|
148
400
|
await this.#workerPool.init();
|
|
149
|
-
await this.#workerPool.reloadWorkers(entryPath);
|
|
150
401
|
const workerPool = this.#workerPool;
|
|
151
402
|
const platform = this;
|
|
152
403
|
const instance = {
|
|
@@ -184,11 +435,63 @@ var NodePlatform = class extends BasePlatform {
|
|
|
184
435
|
return instance;
|
|
185
436
|
}
|
|
186
437
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
438
|
+
* Create cache storage using config from shovel.json
|
|
439
|
+
* Used for testing - production uses the generated config module
|
|
440
|
+
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
189
441
|
*/
|
|
190
442
|
async createCaches() {
|
|
191
|
-
|
|
443
|
+
const runtimeDefaults = {
|
|
444
|
+
default: { impl: MemoryCache }
|
|
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] };
|
|
454
|
+
}
|
|
455
|
+
return new CustomCacheStorage(createCacheFactory({ configs }));
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Create directory storage using config from shovel.json
|
|
459
|
+
* Used for testing - production uses the generated config module
|
|
460
|
+
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
461
|
+
*/
|
|
462
|
+
async createDirectories() {
|
|
463
|
+
const runtimeDefaults = {
|
|
464
|
+
server: { impl: NodeFSDirectory, path: this.#options.cwd },
|
|
465
|
+
public: { impl: NodeFSDirectory, path: this.#options.cwd },
|
|
466
|
+
tmp: { impl: NodeFSDirectory, path: tmpdir() }
|
|
467
|
+
};
|
|
468
|
+
const userDirs = this.#options.config?.directories ?? {};
|
|
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));
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Create logger storage using config from shovel.json
|
|
481
|
+
*/
|
|
482
|
+
async createLoggers() {
|
|
483
|
+
return new CustomLoggerStorage((categories) => getLogger(categories));
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Create database storage from declarative config in shovel.json
|
|
487
|
+
*/
|
|
488
|
+
createDatabases(configOverride) {
|
|
489
|
+
const config = configOverride ?? this.#options.config;
|
|
490
|
+
if (config?.databases && Object.keys(config.databases).length > 0) {
|
|
491
|
+
const factory = createDatabaseFactory(config.databases);
|
|
492
|
+
return new CustomDatabaseStorage(factory);
|
|
493
|
+
}
|
|
494
|
+
return void 0;
|
|
192
495
|
}
|
|
193
496
|
/**
|
|
194
497
|
* SUPPORTING UTILITY - Create HTTP server for Node.js
|
|
@@ -228,10 +531,7 @@ var NodePlatform = class extends BasePlatform {
|
|
|
228
531
|
}
|
|
229
532
|
} catch (error) {
|
|
230
533
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
231
|
-
logger.error("Request error", {
|
|
232
|
-
error: err.message,
|
|
233
|
-
stack: err.stack
|
|
234
|
-
});
|
|
534
|
+
logger.error("Request error: {error}", { error: err });
|
|
235
535
|
const httpError = isHTTPError(error) ? error : new InternalServerError(err.message, { cause: err });
|
|
236
536
|
const isDev = import.meta.env?.MODE !== "production";
|
|
237
537
|
const response = httpError.toResponse(isDev);
|
|
@@ -290,9 +590,75 @@ var NodePlatform = class extends BasePlatform {
|
|
|
290
590
|
if (this.#workerPool) {
|
|
291
591
|
await this.#workerPool.reloadWorkers(entrypoint);
|
|
292
592
|
} else if (this.#singleThreadedRuntime) {
|
|
293
|
-
await this.#singleThreadedRuntime.
|
|
593
|
+
await this.#singleThreadedRuntime.load(entrypoint);
|
|
294
594
|
}
|
|
295
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Get virtual entry wrapper for Node.js
|
|
598
|
+
*
|
|
599
|
+
* @param entryPath - Absolute path to user's entrypoint file
|
|
600
|
+
* @param options - Entry wrapper options
|
|
601
|
+
* @param options.type - "production" (default) or "worker"
|
|
602
|
+
*
|
|
603
|
+
* Returns:
|
|
604
|
+
* - "production": Server entry that loads ServiceWorkerPool
|
|
605
|
+
* - "worker": Worker entry that sets up runtime and message loop
|
|
606
|
+
*/
|
|
607
|
+
getEntryWrapper(entryPath, options) {
|
|
608
|
+
if (options?.type === "worker") {
|
|
609
|
+
return workerEntryTemplate.replace("__USER_ENTRY__", entryPath);
|
|
610
|
+
}
|
|
611
|
+
return entryTemplate;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Get Node.js-specific esbuild configuration
|
|
615
|
+
*
|
|
616
|
+
* Note: Node.js doesn't support import.meta.env natively, so we alias it
|
|
617
|
+
* to process.env for compatibility with code that uses Vite-style env access.
|
|
618
|
+
*/
|
|
619
|
+
getESBuildConfig() {
|
|
620
|
+
return {
|
|
621
|
+
platform: "node",
|
|
622
|
+
external: ["node:*", ...builtinModules],
|
|
623
|
+
define: {
|
|
624
|
+
// Node.js doesn't support import.meta.env, alias to process.env
|
|
625
|
+
"import.meta.env": "process.env"
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Get Node.js-specific defaults for config generation
|
|
631
|
+
*
|
|
632
|
+
* Provides default directories (server, public, tmp) that work
|
|
633
|
+
* out of the box for Node.js deployments.
|
|
634
|
+
*/
|
|
635
|
+
getDefaults() {
|
|
636
|
+
return {
|
|
637
|
+
caches: {
|
|
638
|
+
default: {
|
|
639
|
+
module: "@b9g/cache/memory",
|
|
640
|
+
export: "MemoryCache"
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
directories: {
|
|
644
|
+
server: {
|
|
645
|
+
module: "@b9g/filesystem/node-fs",
|
|
646
|
+
export: "NodeFSDirectory",
|
|
647
|
+
path: "[outdir]/server"
|
|
648
|
+
},
|
|
649
|
+
public: {
|
|
650
|
+
module: "@b9g/filesystem/node-fs",
|
|
651
|
+
export: "NodeFSDirectory",
|
|
652
|
+
path: "[outdir]/public"
|
|
653
|
+
},
|
|
654
|
+
tmp: {
|
|
655
|
+
module: "@b9g/filesystem/node-fs",
|
|
656
|
+
export: "NodeFSDirectory",
|
|
657
|
+
path: "[tmpdir]"
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
296
662
|
/**
|
|
297
663
|
* Dispose of platform resources
|
|
298
664
|
*/
|
|
@@ -309,10 +675,24 @@ var NodePlatform = class extends BasePlatform {
|
|
|
309
675
|
await this.#cacheStorage.dispose();
|
|
310
676
|
this.#cacheStorage = void 0;
|
|
311
677
|
}
|
|
678
|
+
if (this.#databaseStorage) {
|
|
679
|
+
await this.#databaseStorage.closeAll();
|
|
680
|
+
this.#databaseStorage = void 0;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// =========================================================================
|
|
684
|
+
// Config Expression Method Overrides
|
|
685
|
+
// =========================================================================
|
|
686
|
+
/**
|
|
687
|
+
* Get the OS temp directory (Node.js-specific implementation)
|
|
688
|
+
*/
|
|
689
|
+
tmpdir() {
|
|
690
|
+
return tmpdir();
|
|
312
691
|
}
|
|
313
692
|
};
|
|
314
693
|
var src_default = NodePlatform;
|
|
315
694
|
export {
|
|
695
|
+
MemoryCache2 as DefaultCache,
|
|
316
696
|
NodePlatform,
|
|
317
697
|
src_default as default
|
|
318
698
|
};
|