@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 +2 -2
- package/package.json +5 -5
- package/src/index.d.ts +117 -22
- package/src/index.js +332 -251
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.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.
|
|
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.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
|
-
*
|
|
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
|
-
|
|
84
|
+
listen(): Promise<Server>;
|
|
31
85
|
/**
|
|
32
|
-
*
|
|
86
|
+
* Close the server and terminate workers
|
|
33
87
|
*/
|
|
34
|
-
|
|
35
|
-
set workerPool(pool: ServiceWorkerPool | undefined);
|
|
88
|
+
close(): Promise<void>;
|
|
36
89
|
/**
|
|
37
|
-
*
|
|
38
|
-
* Uses Worker threads with coordinated cache storage for isolation and standards compliance
|
|
90
|
+
* Get options for testing
|
|
39
91
|
*/
|
|
40
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
67
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
16
|
+
CustomLoggerStorage,
|
|
17
|
+
CustomDatabaseStorage,
|
|
18
|
+
createDatabaseFactory,
|
|
19
|
+
mergeConfigWithDefaults
|
|
8
20
|
} from "@b9g/platform";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
var
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
#
|
|
77
|
-
#
|
|
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
|
-
|
|
158
|
+
config: options.config
|
|
90
159
|
};
|
|
160
|
+
this.serviceWorker = new NodeServiceWorkerContainer(this);
|
|
91
161
|
}
|
|
92
162
|
/**
|
|
93
|
-
*
|
|
163
|
+
* Create a worker instance for the pool
|
|
164
|
+
* Can be overridden for testing
|
|
94
165
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
166
|
+
async createWorker(entrypoint) {
|
|
167
|
+
const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
|
|
168
|
+
return new NodeWebWorker(entrypoint);
|
|
97
169
|
}
|
|
98
170
|
/**
|
|
99
|
-
*
|
|
171
|
+
* Start the HTTP server, routing requests to ServiceWorker
|
|
100
172
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
*
|
|
109
|
-
* Uses Worker threads with coordinated cache storage for isolation and standards compliance
|
|
185
|
+
* Close the server and terminate workers
|
|
110
186
|
*/
|
|
111
|
-
async
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
*
|
|
120
|
-
* No postMessage overhead - maximum performance for production
|
|
192
|
+
* Get options for testing
|
|
121
193
|
*/
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
*
|
|
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
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
228
|
+
const configs = mergeConfigWithDefaults(
|
|
229
|
+
defaults,
|
|
230
|
+
this.#options.config?.directories
|
|
231
|
+
);
|
|
232
|
+
return new CustomDirectoryStorage(createDirectoryFactory(configs));
|
|
245
233
|
}
|
|
246
234
|
/**
|
|
247
|
-
* Create
|
|
235
|
+
* Create logger storage for Node.js
|
|
236
|
+
*
|
|
237
|
+
* Uses LogTape for structured logging.
|
|
248
238
|
*/
|
|
249
|
-
async
|
|
250
|
-
return new
|
|
239
|
+
async createLoggers() {
|
|
240
|
+
return new CustomLoggerStorage((categories) => getLogger(categories));
|
|
251
241
|
}
|
|
252
242
|
/**
|
|
253
|
-
* Create
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
377
|
-
*
|
|
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
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
};
|