@b9g/platform-node 0.1.12 → 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/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 NodeDirectory
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 (NodeDirectory)
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.12",
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",
@@ -11,14 +11,14 @@
11
11
  "esbuild"
12
12
  ],
13
13
  "dependencies": {
14
- "@b9g/cache": "^0.1.5",
15
- "@b9g/http-errors": "^0.1.5",
16
- "@b9g/platform": "^0.1.12",
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.14-beta.0",
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, type PlatformConfig, type Handler, type Server, type ServerOptions, type ServiceWorkerOptions, type ServiceWorkerInstance, type EntryWrapperOptions, type PlatformEsbuildConfig, ServiceWorkerPool } from "@b9g/platform";
7
6
  import { CustomCacheStorage } from "@b9g/cache";
8
7
  import { CustomDirectoryStorage } from "@b9g/filesystem";
8
+ import { BasePlatform, type PlatformConfig, type PlatformDefaults, type Handler, type Server, type ServerOptions, type PlatformESBuildConfig, type ProductionEntryPoints, ServiceWorkerPool, CustomLoggerStorage, CustomDatabaseStorage } from "@b9g/platform";
9
+ import { type ShovelConfig } from "@b9g/platform/runtime";
9
10
  export interface NodePlatformOptions extends PlatformConfig {
10
11
  /** Port for development server (default: 3000) */
11
12
  port?: number;
@@ -15,6 +16,53 @@ export interface NodePlatformOptions extends PlatformConfig {
15
16
  cwd?: string;
16
17
  /** Number of worker threads (default: 1) */
17
18
  workers?: number;
19
+ /** Shovel configuration (caches, directories, etc.) */
20
+ config?: ShovelConfig;
21
+ }
22
+ /**
23
+ * 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>;
18
66
  }
19
67
  /**
20
68
  * Node.js platform implementation
@@ -23,63 +71,110 @@ export interface NodePlatformOptions extends PlatformConfig {
23
71
  export declare class NodePlatform extends BasePlatform {
24
72
  #private;
25
73
  readonly name: string;
74
+ readonly serviceWorker: NodeServiceWorkerContainer;
26
75
  constructor(options?: NodePlatformOptions);
27
76
  /**
28
- * Get options for testing
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
29
83
  */
30
- get options(): Required<NodePlatformOptions>;
84
+ listen(): Promise<Server>;
31
85
  /**
32
- * Get/set worker pool for testing
86
+ * Close the server and terminate workers
33
87
  */
34
- get workerPool(): ServiceWorkerPool | undefined;
35
- set workerPool(pool: ServiceWorkerPool | undefined);
88
+ close(): Promise<void>;
36
89
  /**
37
- * THE MAIN JOB - Load and run a ServiceWorker-style entrypoint in Node.js
38
- * Uses Worker threads with coordinated cache storage for isolation and standards compliance
90
+ * Get options for testing
39
91
  */
40
- loadServiceWorker(entrypoint: string, options?: ServiceWorkerOptions): Promise<ServiceWorkerInstance>;
92
+ get options(): {
93
+ port: number;
94
+ host: string;
95
+ cwd: string;
96
+ workers: number;
97
+ config?: ShovelConfig;
98
+ };
41
99
  /**
42
- * Create cache storage (in-memory by default)
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.
43
105
  */
44
106
  createCaches(): Promise<CustomCacheStorage>;
45
107
  /**
46
- * Create directory storage for the given base directory
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.
47
116
  */
48
- createDirectories(baseDir: string): CustomDirectoryStorage;
117
+ createDirectories(): Promise<CustomDirectoryStorage>;
118
+ /**
119
+ * Create logger storage for Node.js
120
+ *
121
+ * Uses LogTape for structured logging.
122
+ */
123
+ createLoggers(): Promise<CustomLoggerStorage>;
124
+ /**
125
+ * Create database storage for Node.js
126
+ *
127
+ * Returns undefined if no databases configured in shovel.json.
128
+ * Supports SQLite via better-sqlite3.
129
+ */
130
+ createDatabases(configOverride?: NodePlatformOptions["config"]): CustomDatabaseStorage | undefined;
49
131
  /**
50
132
  * SUPPORTING UTILITY - Create HTTP server for Node.js
51
133
  */
52
134
  createServer(handler: Handler, options?: ServerOptions): Server;
53
135
  /**
54
136
  * Reload workers for hot reloading (called by CLI)
137
+ * @deprecated Use serviceWorker.reloadWorkers() instead
55
138
  * @param entrypoint - Path to the new entrypoint (hashed filename)
56
139
  */
57
140
  reloadWorkers(entrypoint: string): Promise<void>;
58
141
  /**
59
- * Get virtual entry wrapper for Node.js
60
- *
61
- * Returns production server entry template that uses:
62
- * - shovel:config virtual module for configuration
63
- * - Worker threads via ServiceWorkerPool for multi-worker scaling
64
- * - Platform's loadServiceWorker and createServer methods
142
+ * Get production entry points for bundling.
65
143
  *
66
- * The template is a real .ts file (entry-template.ts) for better
67
- * IDE support and linting. It's imported with {type: "text"}.
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
68
147
  */
69
- getEntryWrapper(_entryPath: string, _options?: EntryWrapperOptions): string;
148
+ getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
70
149
  /**
71
150
  * Get Node.js-specific esbuild configuration
72
151
  *
73
152
  * Note: Node.js doesn't support import.meta.env natively, so we alias it
74
153
  * to process.env for compatibility with code that uses Vite-style env access.
75
154
  */
76
- getEsbuildConfig(): PlatformEsbuildConfig;
155
+ getESBuildConfig(): PlatformESBuildConfig;
156
+ /**
157
+ * Get Node.js-specific defaults for config generation
158
+ *
159
+ * Provides default directories (server, public, tmp) that work
160
+ * out of the box for Node.js deployments.
161
+ */
162
+ getDefaults(): PlatformDefaults;
77
163
  /**
78
164
  * Dispose of platform resources
79
165
  */
80
166
  dispose(): Promise<void>;
167
+ /**
168
+ * Get the OS temp directory (Node.js-specific implementation)
169
+ */
170
+ tmpdir(): string;
81
171
  }
82
172
  /**
83
173
  * Default export for easy importing
84
174
  */
85
175
  export default NodePlatform;
176
+ /**
177
+ * Platform's default cache implementation.
178
+ * Re-exported so config can reference: { module: "@b9g/platform-node", export: "DefaultCache" }
179
+ */
180
+ export { MemoryCache as DefaultCache } from "@b9g/cache/memory";
package/src/index.js CHANGED
@@ -1,82 +1,151 @@
1
1
  /// <reference types="./index.d.ts" />
2
2
  // src/index.ts
3
+ import * as HTTP from "node:http";
4
+ import { builtinModules } from "node:module";
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";
3
13
  import {
4
14
  BasePlatform,
5
15
  ServiceWorkerPool,
6
- SingleThreadedRuntime,
7
- CustomLoggerStorage
16
+ CustomLoggerStorage,
17
+ CustomDatabaseStorage,
18
+ createDatabaseFactory,
19
+ mergeConfigWithDefaults
8
20
  } from "@b9g/platform";
9
- import { CustomCacheStorage } from "@b9g/cache";
10
- import { CustomDirectoryStorage } from "@b9g/filesystem";
11
- import { MemoryCache } from "@b9g/cache/memory";
12
- import { NodeDirectory } from "@b9g/filesystem/node";
13
- import { InternalServerError, isHTTPError } from "@b9g/http-errors";
14
- import * as HTTP from "http";
15
- import * as Path from "path";
16
- import { getLogger } from "@logtape/logtape";
17
- var entryTemplate = `// Node.js Production Server Entry
18
- // This file is imported as text and used as the entry wrapper template
19
- import {getLogger} from "@logtape/logtape";
20
- import {configureLogging} from "@b9g/platform/runtime";
21
- import {config} from "shovel:config"; // Virtual module - resolved at build time
22
- import Platform from "@b9g/platform-node";
23
-
24
- // Configure logging before anything else
25
- await configureLogging(config.logging);
26
-
27
- const logger = getLogger(["platform"]);
28
-
29
- // Configuration from shovel:config (with process.env fallbacks baked in)
30
- const PORT = config.port;
31
- const HOST = config.host;
32
- const WORKER_COUNT = config.workers;
33
-
34
- logger.info("Starting production server", {});
35
- logger.info("Workers", {count: WORKER_COUNT});
36
-
37
- // Create platform instance
38
- const platform = new Platform();
39
-
40
- // Get the path to the user's ServiceWorker code
41
- const userCodeURL = new URL("./server.js", import.meta.url);
42
- const userCodePath = userCodeURL.pathname;
43
-
44
- // Load ServiceWorker with worker pool
45
- const serviceWorker = await platform.loadServiceWorker(userCodePath, {
46
- workerCount: WORKER_COUNT,
47
- });
48
-
49
- // Create HTTP server
50
- const server = platform.createServer(serviceWorker.handleRequest, {
51
- port: PORT,
52
- host: HOST,
53
- });
54
-
55
- await server.listen();
56
- logger.info("Server running", {url: \`http://\${HOST}:\${PORT}\`});
57
- logger.info("Load balancing", {workers: WORKER_COUNT});
58
-
59
- // Graceful shutdown
60
- const shutdown = async () => {
61
- logger.info("Shutting down server", {});
62
- await serviceWorker.dispose();
63
- await platform.dispose();
64
- await server.close();
65
- logger.info("Server stopped", {});
66
- process.exit(0);
21
+ import {
22
+ ShovelServiceWorkerRegistration,
23
+ kServiceWorker,
24
+ createCacheFactory,
25
+ createDirectoryFactory
26
+ } from "@b9g/platform/runtime";
27
+ import { MemoryCache as MemoryCache2 } from "@b9g/cache/memory";
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
+ }
67
142
  };
68
-
69
- process.on("SIGINT", shutdown);
70
- process.on("SIGTERM", shutdown);
71
- `;
72
- var logger = getLogger(["platform"]);
73
143
  var NodePlatform = class extends BasePlatform {
74
144
  name;
145
+ serviceWorker;
75
146
  #options;
76
- #workerPool;
77
- #singleThreadedRuntime;
78
- #cacheStorage;
79
- #directoryStorage;
147
+ #databaseStorage;
148
+ #server;
80
149
  constructor(options = {}) {
81
150
  super(options);
82
151
  this.name = "node";
@@ -86,184 +155,103 @@ var NodePlatform = class extends BasePlatform {
86
155
  host: options.host ?? "localhost",
87
156
  workers: options.workers ?? 1,
88
157
  cwd,
89
- ...options
158
+ config: options.config
90
159
  };
160
+ this.serviceWorker = new NodeServiceWorkerContainer(this);
91
161
  }
92
162
  /**
93
- * Get options for testing
163
+ * Create a worker instance for the pool
164
+ * Can be overridden for testing
94
165
  */
95
- get options() {
96
- return this.#options;
166
+ async createWorker(entrypoint) {
167
+ const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
168
+ return new NodeWebWorker(entrypoint);
97
169
  }
98
170
  /**
99
- * Get/set worker pool for testing
171
+ * Start the HTTP server, routing requests to ServiceWorker
100
172
  */
101
- get workerPool() {
102
- return this.#workerPool;
103
- }
104
- set workerPool(pool) {
105
- this.#workerPool = pool;
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
+ );
179
+ }
180
+ this.#server = this.createServer((request) => pool.handleRequest(request));
181
+ await this.#server.listen();
182
+ return this.#server;
106
183
  }
107
184
  /**
108
- * THE MAIN JOB - Load and run a ServiceWorker-style entrypoint in Node.js
109
- * Uses Worker threads with coordinated cache storage for isolation and standards compliance
185
+ * Close the server and terminate workers
110
186
  */
111
- async loadServiceWorker(entrypoint, options = {}) {
112
- const workerCount = options.workerCount ?? this.#options.workers;
113
- if (workerCount === 1 && !options.hotReload) {
114
- return this.#loadServiceWorkerDirect(entrypoint, options);
115
- }
116
- return this.#loadServiceWorkerWithPool(entrypoint, options, workerCount);
187
+ async close() {
188
+ await this.#server?.close();
189
+ await this.serviceWorker.terminate();
117
190
  }
118
191
  /**
119
- * Load ServiceWorker directly in main thread (single-threaded mode)
120
- * No postMessage overhead - maximum performance for production
192
+ * Get options for testing
121
193
  */
122
- async #loadServiceWorkerDirect(entrypoint, _options) {
123
- const entryPath = Path.resolve(this.#options.cwd, entrypoint);
124
- const entryDir = Path.dirname(entryPath);
125
- if (!this.#cacheStorage) {
126
- this.#cacheStorage = await this.createCaches();
127
- }
128
- if (!this.#directoryStorage) {
129
- this.#directoryStorage = this.createDirectories(entryDir);
130
- }
131
- if (this.#singleThreadedRuntime) {
132
- await this.#singleThreadedRuntime.terminate();
133
- }
134
- if (this.#workerPool) {
135
- await this.#workerPool.terminate();
136
- this.#workerPool = void 0;
137
- }
138
- logger.info("Creating single-threaded ServiceWorker runtime", { entryPath });
139
- this.#singleThreadedRuntime = new SingleThreadedRuntime({
140
- caches: this.#cacheStorage,
141
- directories: this.#directoryStorage,
142
- loggers: new CustomLoggerStorage((...cats) => getLogger(cats))
143
- });
144
- await this.#singleThreadedRuntime.init();
145
- await this.#singleThreadedRuntime.load(entryPath);
146
- const runtime = this.#singleThreadedRuntime;
147
- const platform = this;
148
- const instance = {
149
- runtime,
150
- handleRequest: async (request) => {
151
- if (!platform.#singleThreadedRuntime) {
152
- throw new Error("SingleThreadedRuntime not initialized");
153
- }
154
- return platform.#singleThreadedRuntime.handleRequest(request);
155
- },
156
- install: async () => {
157
- logger.info("ServiceWorker installed", { method: "single_threaded" });
158
- },
159
- activate: async () => {
160
- logger.info("ServiceWorker activated", { method: "single_threaded" });
161
- },
162
- get ready() {
163
- return runtime?.ready ?? false;
164
- },
165
- dispose: async () => {
166
- if (platform.#singleThreadedRuntime) {
167
- await platform.#singleThreadedRuntime.terminate();
168
- platform.#singleThreadedRuntime = void 0;
169
- }
170
- logger.info("ServiceWorker disposed", {});
171
- }
172
- };
173
- logger.info("ServiceWorker loaded", {
174
- features: ["single_threaded", "no_postmessage_overhead"]
175
- });
176
- return instance;
194
+ get options() {
195
+ return this.#options;
177
196
  }
178
197
  /**
179
- * Load ServiceWorker using worker pool (multi-threaded mode or dev mode)
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.
180
203
  */
181
- async #loadServiceWorkerWithPool(entrypoint, _options, workerCount) {
182
- const entryPath = Path.resolve(this.#options.cwd, entrypoint);
183
- if (!this.#cacheStorage) {
184
- this.#cacheStorage = await this.createCaches();
185
- }
186
- if (this.#singleThreadedRuntime) {
187
- await this.#singleThreadedRuntime.terminate();
188
- this.#singleThreadedRuntime = void 0;
189
- }
190
- if (this.#workerPool) {
191
- await this.#workerPool.terminate();
192
- }
193
- logger.info("Creating ServiceWorker pool", {
194
- entryPath,
195
- workerCount
196
- });
197
- this.#workerPool = new ServiceWorkerPool(
198
- {
199
- workerCount,
200
- requestTimeout: 3e4,
201
- cwd: this.#options.cwd
202
- },
203
- entryPath,
204
- this.#cacheStorage,
205
- {}
206
- // Empty config - use defaults
204
+ async createCaches() {
205
+ const defaults = { default: { impl: MemoryCache } };
206
+ const configs = mergeConfigWithDefaults(
207
+ defaults,
208
+ this.#options.config?.caches
207
209
  );
208
- await this.#workerPool.init();
209
- await this.#workerPool.reloadWorkers(entryPath);
210
- const workerPool = this.#workerPool;
211
- const platform = this;
212
- const instance = {
213
- runtime: workerPool,
214
- handleRequest: async (request) => {
215
- if (!platform.#workerPool) {
216
- throw new Error("ServiceWorkerPool not initialized");
217
- }
218
- return platform.#workerPool.handleRequest(request);
219
- },
220
- install: async () => {
221
- logger.info("ServiceWorker installed", {
222
- method: "worker_threads"
223
- });
224
- },
225
- activate: async () => {
226
- logger.info("ServiceWorker activated", {
227
- method: "worker_threads"
228
- });
229
- },
230
- get ready() {
231
- return workerPool?.ready ?? false;
232
- },
233
- dispose: async () => {
234
- if (platform.#workerPool) {
235
- await platform.#workerPool.terminate();
236
- platform.#workerPool = void 0;
237
- }
238
- logger.info("ServiceWorker disposed", {});
239
- }
210
+ return new CustomCacheStorage(createCacheFactory({ configs }));
211
+ }
212
+ /**
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.
221
+ */
222
+ async createDirectories() {
223
+ const defaults = {
224
+ server: { impl: NodeFSDirectory, path: this.#options.cwd },
225
+ public: { impl: NodeFSDirectory, path: this.#options.cwd },
226
+ tmp: { impl: NodeFSDirectory, path: tmpdir() }
240
227
  };
241
- logger.info("ServiceWorker loaded", {
242
- features: ["worker_threads", "coordinated_caches"]
243
- });
244
- return instance;
228
+ const configs = mergeConfigWithDefaults(
229
+ defaults,
230
+ this.#options.config?.directories
231
+ );
232
+ return new CustomDirectoryStorage(createDirectoryFactory(configs));
245
233
  }
246
234
  /**
247
- * Create cache storage (in-memory by default)
235
+ * Create logger storage for Node.js
236
+ *
237
+ * Uses LogTape for structured logging.
248
238
  */
249
- async createCaches() {
250
- return new CustomCacheStorage((name) => new MemoryCache(name));
239
+ async createLoggers() {
240
+ return new CustomLoggerStorage((categories) => getLogger(categories));
251
241
  }
252
242
  /**
253
- * Create directory storage for the given base directory
243
+ * Create database storage for Node.js
244
+ *
245
+ * Returns undefined if no databases configured in shovel.json.
246
+ * Supports SQLite via better-sqlite3.
254
247
  */
255
- createDirectories(baseDir) {
256
- return new CustomDirectoryStorage((name) => {
257
- let dirPath;
258
- if (name === "static") {
259
- dirPath = Path.resolve(baseDir, "../static");
260
- } else if (name === "server") {
261
- dirPath = baseDir;
262
- } else {
263
- dirPath = Path.resolve(baseDir, `../${name}`);
264
- }
265
- return Promise.resolve(new NodeDirectory(dirPath));
266
- });
248
+ createDatabases(configOverride) {
249
+ const config = configOverride ?? this.#options.config;
250
+ if (config?.databases && Object.keys(config.databases).length > 0) {
251
+ const factory = createDatabaseFactory(config.databases);
252
+ return new CustomDatabaseStorage(factory);
253
+ }
254
+ return void 0;
267
255
  }
268
256
  /**
269
257
  * SUPPORTING UTILITY - Create HTTP server for Node.js
@@ -356,28 +344,83 @@ var NodePlatform = class extends BasePlatform {
356
344
  }
357
345
  /**
358
346
  * Reload workers for hot reloading (called by CLI)
347
+ * @deprecated Use serviceWorker.reloadWorkers() instead
359
348
  * @param entrypoint - Path to the new entrypoint (hashed filename)
360
349
  */
361
350
  async reloadWorkers(entrypoint) {
362
- if (this.#workerPool) {
363
- await this.#workerPool.reloadWorkers(entrypoint);
364
- } else if (this.#singleThreadedRuntime) {
365
- await this.#singleThreadedRuntime.load(entrypoint);
366
- }
351
+ await this.serviceWorker.reloadWorkers(entrypoint);
367
352
  }
368
353
  /**
369
- * Get virtual entry wrapper for Node.js
370
- *
371
- * Returns production server entry template that uses:
372
- * - shovel:config virtual module for configuration
373
- * - Worker threads via ServiceWorkerPool for multi-worker scaling
374
- * - Platform's loadServiceWorker and createServer methods
354
+ * Get production entry points for bundling.
375
355
  *
376
- * The template is a real .ts file (entry-template.ts) for better
377
- * IDE support and linting. It's imported with {type: "text"}.
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
378
359
  */
379
- getEntryWrapper(_entryPath, _options) {
380
- return entryTemplate;
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
+ };
381
424
  }
382
425
  /**
383
426
  * Get Node.js-specific esbuild configuration
@@ -385,36 +428,74 @@ var NodePlatform = class extends BasePlatform {
385
428
  * Note: Node.js doesn't support import.meta.env natively, so we alias it
386
429
  * to process.env for compatibility with code that uses Vite-style env access.
387
430
  */
388
- getEsbuildConfig() {
431
+ getESBuildConfig() {
389
432
  return {
390
433
  platform: "node",
391
- external: ["node:*"],
434
+ external: ["node:*", ...builtinModules],
392
435
  define: {
393
436
  // Node.js doesn't support import.meta.env, alias to process.env
394
437
  "import.meta.env": "process.env"
395
438
  }
396
439
  };
397
440
  }
441
+ /**
442
+ * Get Node.js-specific defaults for config generation
443
+ *
444
+ * Provides default directories (server, public, tmp) that work
445
+ * out of the box for Node.js deployments.
446
+ */
447
+ getDefaults() {
448
+ return {
449
+ caches: {
450
+ default: {
451
+ module: "@b9g/cache/memory",
452
+ export: "MemoryCache"
453
+ }
454
+ },
455
+ directories: {
456
+ server: {
457
+ module: "@b9g/filesystem/node-fs",
458
+ export: "NodeFSDirectory",
459
+ path: "[outdir]/server"
460
+ },
461
+ public: {
462
+ module: "@b9g/filesystem/node-fs",
463
+ export: "NodeFSDirectory",
464
+ path: "[outdir]/public"
465
+ },
466
+ tmp: {
467
+ module: "@b9g/filesystem/node-fs",
468
+ export: "NodeFSDirectory",
469
+ path: "[tmpdir]"
470
+ }
471
+ }
472
+ };
473
+ }
398
474
  /**
399
475
  * Dispose of platform resources
400
476
  */
401
477
  async dispose() {
402
- if (this.#singleThreadedRuntime) {
403
- await this.#singleThreadedRuntime.terminate();
404
- this.#singleThreadedRuntime = void 0;
405
- }
406
- if (this.#workerPool) {
407
- await this.#workerPool.terminate();
408
- this.#workerPool = void 0;
409
- }
410
- if (this.#cacheStorage) {
411
- await this.#cacheStorage.dispose();
412
- this.#cacheStorage = void 0;
478
+ await this.close();
479
+ await this.serviceWorker.terminate();
480
+ if (this.#databaseStorage) {
481
+ await this.#databaseStorage.closeAll();
482
+ this.#databaseStorage = void 0;
413
483
  }
414
484
  }
485
+ // =========================================================================
486
+ // Config Expression Method Overrides
487
+ // =========================================================================
488
+ /**
489
+ * Get the OS temp directory (Node.js-specific implementation)
490
+ */
491
+ tmpdir() {
492
+ return tmpdir();
493
+ }
415
494
  };
416
495
  var src_default = NodePlatform;
417
496
  export {
497
+ MemoryCache2 as DefaultCache,
418
498
  NodePlatform,
499
+ NodeServiceWorkerContainer,
419
500
  src_default as default
420
501
  };