@b9g/platform 0.1.13 → 0.1.14-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/index.d.ts +79 -90
- package/src/index.js +18 -88
- package/src/runtime.d.ts +62 -15
- package/src/runtime.js +55 -62
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/platform",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14-beta.0",
|
|
4
4
|
"description": "The portable meta-framework built on web standards.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"service-worker",
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@b9g/async-context": "^0.2.0-beta.0",
|
|
22
22
|
"@b9g/cache": "^0.2.0-beta.0",
|
|
23
|
-
"@b9g/filesystem": "^0.1.
|
|
23
|
+
"@b9g/filesystem": "^0.1.8",
|
|
24
24
|
"@b9g/zen": "^0.1.6",
|
|
25
25
|
"@logtape/logtape": "^1.2.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@b9g/libuild": "^0.1.20",
|
|
29
29
|
"@b9g/node-webworker": "^0.2.0-beta.1",
|
|
30
|
-
"@b9g/platform-bun": "^0.1.
|
|
31
|
-
"@b9g/platform-node": "^0.1.
|
|
30
|
+
"@b9g/platform-bun": "^0.1.12-beta.0",
|
|
31
|
+
"@b9g/platform-node": "^0.1.14-beta.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@logtape/file": "^1.0.0",
|
package/src/index.d.ts
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* This module contains:
|
|
10
10
|
* - Platform interface and base classes
|
|
11
|
-
* - SingleThreadedRuntime for main-thread execution
|
|
12
11
|
* - ServiceWorkerPool for multi-worker execution
|
|
13
12
|
*/
|
|
14
13
|
import type { DirectoryStorage } from "@b9g/filesystem";
|
|
15
|
-
import { CustomLoggerStorage, type LoggerStorage
|
|
14
|
+
import { CustomLoggerStorage, type LoggerStorage } from "./runtime.js";
|
|
16
15
|
export { validateConfig, ConfigValidationError } from "./config.js";
|
|
17
16
|
/**
|
|
18
17
|
* Platform configuration
|
|
@@ -28,6 +27,8 @@ export interface ServerOptions {
|
|
|
28
27
|
port?: number;
|
|
29
28
|
/** Host to bind to */
|
|
30
29
|
host?: string;
|
|
30
|
+
/** Enable SO_REUSEPORT for multi-worker deployments (Bun only) */
|
|
31
|
+
reusePort?: boolean;
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* Request handler function (Web Fetch API compatible)
|
|
@@ -80,21 +81,14 @@ export interface ServiceWorkerInstance {
|
|
|
80
81
|
dispose(): Promise<void>;
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
83
|
-
*
|
|
84
|
+
* Production entry points returned by getProductionEntryPoints().
|
|
85
|
+
* Each key is the output filename (without .js), value is the code.
|
|
86
|
+
*
|
|
87
|
+
* Examples:
|
|
88
|
+
* - Cloudflare: { "worker": "<code>" } - single worker file
|
|
89
|
+
* - Node/Bun: { "index": "<supervisor>", "worker": "<worker>" } - two files
|
|
84
90
|
*/
|
|
85
|
-
export
|
|
86
|
-
/**
|
|
87
|
-
* Type of entry wrapper to generate:
|
|
88
|
-
* - "production": Production server entry (default) - runs the server directly
|
|
89
|
-
* - "worker": Worker entry for ServiceWorkerPool - sets up runtime and message loop
|
|
90
|
-
*/
|
|
91
|
-
type?: "production" | "worker";
|
|
92
|
-
/**
|
|
93
|
-
* Output directory for the build. Used to generate absolute paths for
|
|
94
|
-
* directory defaults (server, public). Required for "worker" type.
|
|
95
|
-
*/
|
|
96
|
-
outDir?: string;
|
|
97
|
-
}
|
|
91
|
+
export type ProductionEntryPoints = Record<string, string>;
|
|
98
92
|
/**
|
|
99
93
|
* ESBuild configuration subset that platforms can customize
|
|
100
94
|
*/
|
|
@@ -107,16 +101,6 @@ export interface PlatformESBuildConfig {
|
|
|
107
101
|
external?: string[];
|
|
108
102
|
/** Compile-time defines */
|
|
109
103
|
define?: Record<string, string>;
|
|
110
|
-
/**
|
|
111
|
-
* Whether the entry wrapper imports user code inline (bundled together)
|
|
112
|
-
* or references it as a separate file (loaded at runtime).
|
|
113
|
-
*
|
|
114
|
-
* - true: User code is imported inline (e.g., Cloudflare: `import "user-entry"`)
|
|
115
|
-
* - false: User code is loaded separately (e.g., Node/Bun: `loadServiceWorker("./server.js")`)
|
|
116
|
-
*
|
|
117
|
-
* Default: false (separate build)
|
|
118
|
-
*/
|
|
119
|
-
bundlesUserCodeInline?: boolean;
|
|
120
104
|
}
|
|
121
105
|
/**
|
|
122
106
|
* Default resource configuration for a named resource (cache, directory, etc.)
|
|
@@ -141,6 +125,19 @@ export interface PlatformDefaults {
|
|
|
141
125
|
/** Default cache configuration (e.g., memory cache) */
|
|
142
126
|
caches?: Record<string, ResourceDefault>;
|
|
143
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Extended ServiceWorkerContainer with internal methods for hot reload
|
|
130
|
+
*/
|
|
131
|
+
export interface ShovelServiceWorkerContainer extends ServiceWorkerContainer {
|
|
132
|
+
/** Internal: Get the worker pool for request handling */
|
|
133
|
+
readonly pool?: {
|
|
134
|
+
handleRequest(request: Request): Promise<Response>;
|
|
135
|
+
};
|
|
136
|
+
/** Internal: Terminate all workers */
|
|
137
|
+
terminate(): Promise<void>;
|
|
138
|
+
/** Internal: Reload workers (for hot reload) */
|
|
139
|
+
reloadWorkers(entrypoint: string): Promise<void>;
|
|
140
|
+
}
|
|
144
141
|
/**
|
|
145
142
|
* Platform interface - ServiceWorker entrypoint loader for JavaScript runtimes
|
|
146
143
|
*
|
|
@@ -152,29 +149,38 @@ export interface Platform {
|
|
|
152
149
|
*/
|
|
153
150
|
readonly name: string;
|
|
154
151
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
152
|
+
* ServiceWorkerContainer for managing registrations (Node/Bun only)
|
|
153
|
+
* Similar to navigator.serviceWorker in browsers
|
|
154
|
+
*/
|
|
155
|
+
readonly serviceWorker: ShovelServiceWorkerContainer;
|
|
156
|
+
/**
|
|
157
|
+
* Start HTTP server and route requests to ServiceWorker (Node/Bun only)
|
|
158
|
+
* Must call serviceWorker.register() first
|
|
157
159
|
*/
|
|
158
|
-
|
|
160
|
+
listen(): Promise<Server>;
|
|
161
|
+
/**
|
|
162
|
+
* Close server and terminate workers (Node/Bun only)
|
|
163
|
+
*/
|
|
164
|
+
close(): Promise<void>;
|
|
159
165
|
/**
|
|
160
166
|
* SUPPORTING UTILITY - Create server instance for this platform
|
|
161
167
|
*/
|
|
162
168
|
createServer(handler: Handler, options?: ServerOptions): Server;
|
|
163
169
|
/**
|
|
164
|
-
* BUILD SUPPORT - Get
|
|
170
|
+
* BUILD SUPPORT - Get production entry points for bundling
|
|
171
|
+
*
|
|
172
|
+
* Returns a map of output filenames to their source code.
|
|
173
|
+
* The build system creates one output file per entry point.
|
|
165
174
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* 3. Exports any required handlers (e.g., ES module export for Cloudflare)
|
|
175
|
+
* Platform determines the structure:
|
|
176
|
+
* - Cloudflare: { "worker": "<code>" } - single worker file
|
|
177
|
+
* - Node/Bun: { "index": "<supervisor>", "worker": "<runtime + user code>" }
|
|
170
178
|
*
|
|
171
|
-
* The
|
|
172
|
-
* Every platform must provide a wrapper - there is no "raw user code" mode.
|
|
179
|
+
* The user's entrypoint code is statically imported into the appropriate file.
|
|
173
180
|
*
|
|
174
|
-
* @param
|
|
175
|
-
* @param options - Additional options
|
|
181
|
+
* @param userEntryPath - Path to user's entrypoint (will be imported)
|
|
176
182
|
*/
|
|
177
|
-
|
|
183
|
+
getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
|
|
178
184
|
/**
|
|
179
185
|
* BUILD SUPPORT - Get platform-specific esbuild configuration
|
|
180
186
|
*
|
|
@@ -205,6 +211,15 @@ export interface Platform {
|
|
|
205
211
|
* Uses platform-specific defaults, overridable via shovel.json config
|
|
206
212
|
*/
|
|
207
213
|
createLoggers(): Promise<LoggerStorage>;
|
|
214
|
+
/**
|
|
215
|
+
* Dispose of platform resources (worker pools, connections, etc.)
|
|
216
|
+
*/
|
|
217
|
+
dispose(): Promise<void>;
|
|
218
|
+
/**
|
|
219
|
+
* HOT RELOAD - Reload workers with a new entrypoint (development only)
|
|
220
|
+
* Optional - only Node and Bun platforms implement this
|
|
221
|
+
*/
|
|
222
|
+
reloadWorkers?(entrypoint: string): Promise<void>;
|
|
208
223
|
}
|
|
209
224
|
/**
|
|
210
225
|
* Platform registry - internal implementation
|
|
@@ -253,7 +268,7 @@ export declare function resolvePlatform(options: {
|
|
|
253
268
|
/**
|
|
254
269
|
* Create platform instance based on name
|
|
255
270
|
*/
|
|
256
|
-
export declare function createPlatform(platformName: string, options?: any): Promise<
|
|
271
|
+
export declare function createPlatform(platformName: string, options?: any): Promise<Platform>;
|
|
257
272
|
/**
|
|
258
273
|
* Base platform class with shared adapter loading logic
|
|
259
274
|
* Platform implementations extend this and provide platform-specific methods
|
|
@@ -262,13 +277,15 @@ export declare abstract class BasePlatform implements Platform {
|
|
|
262
277
|
config: PlatformConfig;
|
|
263
278
|
constructor(config?: PlatformConfig);
|
|
264
279
|
abstract readonly name: string;
|
|
265
|
-
abstract
|
|
280
|
+
abstract readonly serviceWorker: ShovelServiceWorkerContainer;
|
|
281
|
+
abstract listen(): Promise<Server>;
|
|
282
|
+
abstract close(): Promise<void>;
|
|
266
283
|
abstract createServer(handler: any, options?: any): any;
|
|
267
284
|
/**
|
|
268
|
-
* Get
|
|
269
|
-
* Subclasses must override to provide platform-specific
|
|
285
|
+
* Get production entry points for bundling
|
|
286
|
+
* Subclasses must override to provide platform-specific entry points
|
|
270
287
|
*/
|
|
271
|
-
abstract
|
|
288
|
+
abstract getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
|
|
272
289
|
/**
|
|
273
290
|
* Get platform-specific esbuild configuration
|
|
274
291
|
* Subclasses should override to provide platform-specific config
|
|
@@ -294,7 +311,23 @@ export declare abstract class BasePlatform implements Platform {
|
|
|
294
311
|
* Subclasses must override to provide platform-specific implementation
|
|
295
312
|
*/
|
|
296
313
|
abstract createLoggers(): Promise<LoggerStorage>;
|
|
314
|
+
/**
|
|
315
|
+
* Dispose of platform resources
|
|
316
|
+
* Subclasses should override to clean up worker pools, connections, etc.
|
|
317
|
+
*/
|
|
318
|
+
dispose(): Promise<void>;
|
|
297
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Merge platform defaults with user config
|
|
322
|
+
*
|
|
323
|
+
* Deep merges each entry so user can override specific options without
|
|
324
|
+
* losing the platform's default implementation class.
|
|
325
|
+
*
|
|
326
|
+
* @param defaults - Platform's runtime defaults (with actual class refs)
|
|
327
|
+
* @param userConfig - User's config from shovel.json (may be partial)
|
|
328
|
+
* @returns Merged config with all entries
|
|
329
|
+
*/
|
|
330
|
+
export declare function mergeConfigWithDefaults(defaults: Record<string, Record<string, unknown>>, userConfig: Record<string, Record<string, unknown>> | undefined): Record<string, Record<string, unknown>>;
|
|
298
331
|
/**
|
|
299
332
|
* Global platform registry
|
|
300
333
|
*/
|
|
@@ -328,52 +361,6 @@ export interface ServiceWorkerRuntime {
|
|
|
328
361
|
readonly workerCount: number;
|
|
329
362
|
readonly ready: boolean;
|
|
330
363
|
}
|
|
331
|
-
export interface SingleThreadedRuntimeOptions {
|
|
332
|
-
/** Cache storage for the runtime */
|
|
333
|
-
caches: CacheStorage;
|
|
334
|
-
/** Directory storage for the runtime */
|
|
335
|
-
directories: DirectoryStorage;
|
|
336
|
-
/** Database storage for the runtime */
|
|
337
|
-
databases?: DatabaseStorage;
|
|
338
|
-
/** Logger storage for the runtime */
|
|
339
|
-
loggers: LoggerStorage;
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Single-threaded ServiceWorker runtime
|
|
343
|
-
*
|
|
344
|
-
* Runs ServiceWorker code directly in the main thread.
|
|
345
|
-
* Implements ServiceWorkerRuntime interface for interchangeability with ServiceWorkerPool.
|
|
346
|
-
*/
|
|
347
|
-
export declare class SingleThreadedRuntime implements ServiceWorkerRuntime {
|
|
348
|
-
#private;
|
|
349
|
-
constructor(options: SingleThreadedRuntimeOptions);
|
|
350
|
-
/**
|
|
351
|
-
* Initialize the runtime (install ServiceWorker globals)
|
|
352
|
-
*/
|
|
353
|
-
init(): Promise<void>;
|
|
354
|
-
/**
|
|
355
|
-
* Load (or reload) a ServiceWorker entrypoint
|
|
356
|
-
* @param entrypoint - Path to the entrypoint file (content-hashed filename)
|
|
357
|
-
*/
|
|
358
|
-
load(entrypoint: string): Promise<void>;
|
|
359
|
-
/**
|
|
360
|
-
* Handle an HTTP request
|
|
361
|
-
* This is the key method - direct call, no postMessage!
|
|
362
|
-
*/
|
|
363
|
-
handleRequest(request: Request): Promise<Response>;
|
|
364
|
-
/**
|
|
365
|
-
* Graceful shutdown
|
|
366
|
-
*/
|
|
367
|
-
terminate(): Promise<void>;
|
|
368
|
-
/**
|
|
369
|
-
* Get the number of workers (always 1 for single-threaded)
|
|
370
|
-
*/
|
|
371
|
-
get workerCount(): number;
|
|
372
|
-
/**
|
|
373
|
-
* Check if ready to handle requests
|
|
374
|
-
*/
|
|
375
|
-
get ready(): boolean;
|
|
376
|
-
}
|
|
377
364
|
/**
|
|
378
365
|
* Worker pool options
|
|
379
366
|
*/
|
|
@@ -384,6 +371,8 @@ export interface WorkerPoolOptions {
|
|
|
384
371
|
requestTimeout?: number;
|
|
385
372
|
/** Working directory for file resolution */
|
|
386
373
|
cwd?: string;
|
|
374
|
+
/** Custom worker factory (if not provided, uses createWebWorker) */
|
|
375
|
+
createWorker?: (entrypoint: string) => Worker | Promise<Worker>;
|
|
387
376
|
}
|
|
388
377
|
export interface WorkerMessage {
|
|
389
378
|
type: string;
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
/// <reference types="./index.d.ts" />
|
|
2
2
|
// src/index.ts
|
|
3
3
|
import { getLogger } from "@logtape/logtape";
|
|
4
|
-
import {
|
|
5
|
-
ServiceWorkerGlobals,
|
|
6
|
-
ShovelServiceWorkerRegistration,
|
|
7
|
-
ShovelFetchEvent,
|
|
8
|
-
CustomLoggerStorage
|
|
9
|
-
} from "./runtime.js";
|
|
4
|
+
import { CustomLoggerStorage } from "./runtime.js";
|
|
10
5
|
import { validateConfig, ConfigValidationError } from "./config.js";
|
|
11
6
|
import {
|
|
12
7
|
CustomDatabaseStorage,
|
|
@@ -85,7 +80,22 @@ var BasePlatform = class {
|
|
|
85
80
|
constructor(config = {}) {
|
|
86
81
|
this.config = config;
|
|
87
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Dispose of platform resources
|
|
85
|
+
* Subclasses should override to clean up worker pools, connections, etc.
|
|
86
|
+
*/
|
|
87
|
+
async dispose() {
|
|
88
|
+
}
|
|
88
89
|
};
|
|
90
|
+
function mergeConfigWithDefaults(defaults, userConfig) {
|
|
91
|
+
const user = userConfig ?? {};
|
|
92
|
+
const allNames = /* @__PURE__ */ new Set([...Object.keys(defaults), ...Object.keys(user)]);
|
|
93
|
+
const merged = {};
|
|
94
|
+
for (const name of allNames) {
|
|
95
|
+
merged[name] = { ...defaults[name], ...user[name] };
|
|
96
|
+
}
|
|
97
|
+
return merged;
|
|
98
|
+
}
|
|
89
99
|
var DefaultPlatformRegistry = class {
|
|
90
100
|
#platforms;
|
|
91
101
|
constructor() {
|
|
@@ -142,86 +152,6 @@ async function getPlatformAsync(name) {
|
|
|
142
152
|
}
|
|
143
153
|
return platform;
|
|
144
154
|
}
|
|
145
|
-
var SingleThreadedRuntime = class {
|
|
146
|
-
#registration;
|
|
147
|
-
#scope;
|
|
148
|
-
#ready;
|
|
149
|
-
#entrypoint;
|
|
150
|
-
constructor(options) {
|
|
151
|
-
this.#ready = false;
|
|
152
|
-
this.#registration = new ShovelServiceWorkerRegistration();
|
|
153
|
-
this.#scope = new ServiceWorkerGlobals({
|
|
154
|
-
registration: this.#registration,
|
|
155
|
-
caches: options.caches,
|
|
156
|
-
directories: options.directories,
|
|
157
|
-
databases: options.databases,
|
|
158
|
-
loggers: options.loggers
|
|
159
|
-
});
|
|
160
|
-
logger.debug("SingleThreadedRuntime created");
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Initialize the runtime (install ServiceWorker globals)
|
|
164
|
-
*/
|
|
165
|
-
async init() {
|
|
166
|
-
this.#scope.install();
|
|
167
|
-
logger.debug("SingleThreadedRuntime initialized - globals installed");
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* Load (or reload) a ServiceWorker entrypoint
|
|
171
|
-
* @param entrypoint - Path to the entrypoint file (content-hashed filename)
|
|
172
|
-
*/
|
|
173
|
-
async load(entrypoint) {
|
|
174
|
-
const isReload = this.#entrypoint !== void 0;
|
|
175
|
-
if (isReload) {
|
|
176
|
-
logger.debug("Reloading ServiceWorker", {
|
|
177
|
-
oldEntrypoint: this.#entrypoint,
|
|
178
|
-
newEntrypoint: entrypoint
|
|
179
|
-
});
|
|
180
|
-
this.#registration._serviceWorker._setState("parsed");
|
|
181
|
-
} else {
|
|
182
|
-
logger.debug("Loading ServiceWorker entrypoint", { entrypoint });
|
|
183
|
-
}
|
|
184
|
-
this.#entrypoint = entrypoint;
|
|
185
|
-
this.#ready = false;
|
|
186
|
-
await import(entrypoint);
|
|
187
|
-
await this.#registration.install();
|
|
188
|
-
await this.#registration.activate();
|
|
189
|
-
this.#ready = true;
|
|
190
|
-
logger.debug("ServiceWorker loaded and activated", { entrypoint });
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Handle an HTTP request
|
|
194
|
-
* This is the key method - direct call, no postMessage!
|
|
195
|
-
*/
|
|
196
|
-
async handleRequest(request) {
|
|
197
|
-
if (!this.#ready) {
|
|
198
|
-
throw new Error(
|
|
199
|
-
"SingleThreadedRuntime not ready - ServiceWorker not loaded"
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
const event = new ShovelFetchEvent(request);
|
|
203
|
-
return this.#registration.handleRequest(event);
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Graceful shutdown
|
|
207
|
-
*/
|
|
208
|
-
async terminate() {
|
|
209
|
-
this.#ready = false;
|
|
210
|
-
logger.debug("SingleThreadedRuntime terminated");
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Get the number of workers (always 1 for single-threaded)
|
|
214
|
-
*/
|
|
215
|
-
get workerCount() {
|
|
216
|
-
return 1;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Check if ready to handle requests
|
|
220
|
-
*/
|
|
221
|
-
get ready() {
|
|
222
|
-
return this.#ready;
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
155
|
async function createWebWorker(workerScript) {
|
|
226
156
|
if (typeof Worker !== "undefined") {
|
|
227
157
|
return new Worker(workerScript, { type: "module" });
|
|
@@ -306,7 +236,7 @@ var ServiceWorkerPool = class {
|
|
|
306
236
|
* The bundle self-initializes and sends "ready" when done
|
|
307
237
|
*/
|
|
308
238
|
async #createWorker(entrypoint) {
|
|
309
|
-
const worker = await createWebWorker(entrypoint);
|
|
239
|
+
const worker = this.#options.createWorker ? await this.#options.createWorker(entrypoint) : await createWebWorker(entrypoint);
|
|
310
240
|
const readyPromise = new Promise((resolve, reject) => {
|
|
311
241
|
const timeoutId = setTimeout(() => {
|
|
312
242
|
this.#pendingWorkerReady.delete(worker);
|
|
@@ -595,7 +525,6 @@ export {
|
|
|
595
525
|
CustomDatabaseStorage,
|
|
596
526
|
CustomLoggerStorage,
|
|
597
527
|
ServiceWorkerPool,
|
|
598
|
-
SingleThreadedRuntime,
|
|
599
528
|
createDatabaseFactory,
|
|
600
529
|
createPlatform,
|
|
601
530
|
detectDeploymentPlatform,
|
|
@@ -603,6 +532,7 @@ export {
|
|
|
603
532
|
detectRuntime,
|
|
604
533
|
getPlatform,
|
|
605
534
|
getPlatformAsync,
|
|
535
|
+
mergeConfigWithDefaults,
|
|
606
536
|
platformRegistry,
|
|
607
537
|
resolvePlatform,
|
|
608
538
|
validateConfig
|
package/src/runtime.d.ts
CHANGED
|
@@ -354,6 +354,14 @@ export declare class ShovelNavigationPreloadManager implements NavigationPreload
|
|
|
354
354
|
}>;
|
|
355
355
|
setHeaderValue(_value: string): Promise<void>;
|
|
356
356
|
}
|
|
357
|
+
/** @internal Symbol for accessing the internal ServiceWorker instance */
|
|
358
|
+
export declare const kServiceWorker: unique symbol;
|
|
359
|
+
/** @internal Symbol for dispatching the install lifecycle event */
|
|
360
|
+
export declare const kDispatchInstall: unique symbol;
|
|
361
|
+
/** @internal Symbol for dispatching the activate lifecycle event */
|
|
362
|
+
export declare const kDispatchActivate: unique symbol;
|
|
363
|
+
/** @internal Symbol for handling fetch requests */
|
|
364
|
+
declare const kHandleRequest: unique symbol;
|
|
357
365
|
/**
|
|
358
366
|
* ShovelServiceWorkerRegistration - Internal implementation of ServiceWorkerRegistration
|
|
359
367
|
* This is also the Shovel ServiceWorker runtime - they are unified into one class
|
|
@@ -363,7 +371,7 @@ export declare class ShovelServiceWorkerRegistration extends EventTarget impleme
|
|
|
363
371
|
readonly scope: string;
|
|
364
372
|
readonly updateViaCache: "imports" | "all" | "none";
|
|
365
373
|
readonly navigationPreload: NavigationPreloadManager;
|
|
366
|
-
|
|
374
|
+
[kServiceWorker]: ShovelServiceWorker;
|
|
367
375
|
readonly cookies: any;
|
|
368
376
|
readonly pushManager: any;
|
|
369
377
|
onupdatefound: ((ev: Event) => any) | null;
|
|
@@ -377,27 +385,71 @@ export declare class ShovelServiceWorkerRegistration extends EventTarget impleme
|
|
|
377
385
|
unregister(): Promise<boolean>;
|
|
378
386
|
update(): Promise<ServiceWorkerRegistration>;
|
|
379
387
|
/**
|
|
380
|
-
*
|
|
388
|
+
* Dispatch the install lifecycle event
|
|
389
|
+
* @internal Use runLifecycle() instead of calling this directly
|
|
381
390
|
*/
|
|
382
|
-
|
|
391
|
+
[kDispatchInstall](): Promise<void>;
|
|
383
392
|
/**
|
|
384
|
-
*
|
|
393
|
+
* Dispatch the activate lifecycle event
|
|
394
|
+
* @internal Use runLifecycle() instead of calling this directly
|
|
385
395
|
*/
|
|
386
|
-
|
|
396
|
+
[kDispatchActivate](): Promise<void>;
|
|
387
397
|
/**
|
|
388
|
-
* Handle a fetch request
|
|
398
|
+
* Handle a fetch request
|
|
399
|
+
* @internal Use the kHandleRequest symbol to access this method
|
|
389
400
|
*
|
|
390
401
|
* Platforms create a ShovelFetchEvent (or subclass) with platform-specific
|
|
391
402
|
* properties and hooks, then pass it to this method for dispatching.
|
|
392
403
|
*
|
|
393
404
|
* @param event - The fetch event to handle (created by platform adapter)
|
|
394
405
|
*/
|
|
395
|
-
|
|
406
|
+
[kHandleRequest](event: ShovelFetchEvent): Promise<Response>;
|
|
396
407
|
/**
|
|
397
408
|
* Check if ready to handle requests (Shovel extension)
|
|
398
409
|
*/
|
|
399
410
|
get ready(): boolean;
|
|
400
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Run ServiceWorker lifecycle events on a registration.
|
|
414
|
+
*
|
|
415
|
+
* This is the proper way to trigger lifecycle events in Shovel's server-side runtime.
|
|
416
|
+
* Unlike browsers where lifecycle is automatic, server-side code must explicitly
|
|
417
|
+
* trigger these events after registering event handlers.
|
|
418
|
+
*
|
|
419
|
+
* @param registration - The ServiceWorkerRegistration to run lifecycle on
|
|
420
|
+
* @param stage - Which lifecycle stage to run:
|
|
421
|
+
* - "install": Run only the install event
|
|
422
|
+
* - "activate": Run install then activate (default)
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```typescript
|
|
426
|
+
* const {registration} = await initWorkerRuntime({config});
|
|
427
|
+
* await import("./server.js"); // Register event handlers
|
|
428
|
+
* await runLifecycle(registration); // Runs install + activate
|
|
429
|
+
* ```
|
|
430
|
+
*/
|
|
431
|
+
export declare function runLifecycle(registration: ShovelServiceWorkerRegistration, stage?: "install" | "activate"): Promise<void>;
|
|
432
|
+
/**
|
|
433
|
+
* Dispatch a fetch request to a ServiceWorker registration.
|
|
434
|
+
*
|
|
435
|
+
* This is the proper way to dispatch requests in Shovel's server-side runtime.
|
|
436
|
+
* Platforms should use this function rather than accessing internal methods directly.
|
|
437
|
+
*
|
|
438
|
+
* @param registration - The ServiceWorkerRegistration to dispatch to
|
|
439
|
+
* @param requestOrEvent - The Request to dispatch, or a pre-constructed ShovelFetchEvent
|
|
440
|
+
* @returns The Response from the ServiceWorker
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```typescript
|
|
444
|
+
* // Simple usage with a Request
|
|
445
|
+
* const response = await dispatchRequest(registration, request);
|
|
446
|
+
*
|
|
447
|
+
* // Platform-specific usage with a custom FetchEvent subclass
|
|
448
|
+
* const event = new CloudflareFetchEvent(request, {env, platformWaitUntil});
|
|
449
|
+
* const response = await dispatchRequest(registration, event);
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
export declare function dispatchRequest(registration: ShovelServiceWorkerRegistration, requestOrEvent: Request | ShovelFetchEvent): Promise<Response>;
|
|
401
453
|
/**
|
|
402
454
|
* ShovelServiceWorkerContainer - Internal implementation of ServiceWorkerContainer
|
|
403
455
|
* This is the registry that manages multiple ServiceWorkerRegistrations by scope
|
|
@@ -431,10 +483,6 @@ export declare class ShovelServiceWorkerContainer extends EventTarget implements
|
|
|
431
483
|
* Unregister a ServiceWorker registration
|
|
432
484
|
*/
|
|
433
485
|
unregister(scope: string): Promise<boolean>;
|
|
434
|
-
/**
|
|
435
|
-
* Route a request to the appropriate registration based on scope matching
|
|
436
|
-
*/
|
|
437
|
-
handleRequest(request: Request): Promise<Response | null>;
|
|
438
486
|
/**
|
|
439
487
|
* Install and activate all registrations
|
|
440
488
|
*/
|
|
@@ -759,16 +807,15 @@ export interface InitWorkerRuntimeResult {
|
|
|
759
807
|
* @example
|
|
760
808
|
* ```typescript
|
|
761
809
|
* import {config} from "shovel:config";
|
|
762
|
-
* import {initWorkerRuntime, startWorkerMessageLoop} from "@b9g/platform/runtime";
|
|
810
|
+
* import {initWorkerRuntime, runLifecycle, startWorkerMessageLoop} from "@b9g/platform/runtime";
|
|
763
811
|
*
|
|
764
812
|
* const {registration} = await initWorkerRuntime({config});
|
|
765
813
|
*
|
|
766
814
|
* // Import user code (registers event handlers)
|
|
767
|
-
* import
|
|
815
|
+
* await import("./server.js");
|
|
768
816
|
*
|
|
769
817
|
* // Run lifecycle and start message loop
|
|
770
|
-
* await registration
|
|
771
|
-
* await registration.activate();
|
|
818
|
+
* await runLifecycle(registration);
|
|
772
819
|
* startWorkerMessageLoop(registration);
|
|
773
820
|
* ```
|
|
774
821
|
*/
|
package/src/runtime.js
CHANGED
|
@@ -568,12 +568,16 @@ var ShovelNavigationPreloadManager = class {
|
|
|
568
568
|
async setHeaderValue(_value) {
|
|
569
569
|
}
|
|
570
570
|
};
|
|
571
|
+
var kServiceWorker = /* @__PURE__ */ Symbol("serviceWorker");
|
|
572
|
+
var kDispatchInstall = /* @__PURE__ */ Symbol("dispatchInstall");
|
|
573
|
+
var kDispatchActivate = /* @__PURE__ */ Symbol("dispatchActivate");
|
|
574
|
+
var kHandleRequest = /* @__PURE__ */ Symbol("handleRequest");
|
|
571
575
|
var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
572
576
|
scope;
|
|
573
577
|
updateViaCache;
|
|
574
578
|
navigationPreload;
|
|
575
|
-
// ServiceWorker
|
|
576
|
-
|
|
579
|
+
// Internal ServiceWorker instance (accessed via symbol for lifecycle management)
|
|
580
|
+
[kServiceWorker];
|
|
577
581
|
// Web API properties (not supported in server context, but required by interface)
|
|
578
582
|
cookies;
|
|
579
583
|
pushManager;
|
|
@@ -583,20 +587,20 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
583
587
|
this.scope = scope;
|
|
584
588
|
this.updateViaCache = "imports";
|
|
585
589
|
this.navigationPreload = new ShovelNavigationPreloadManager();
|
|
586
|
-
this
|
|
590
|
+
this[kServiceWorker] = new ShovelServiceWorker(scriptURL, "parsed");
|
|
587
591
|
this.cookies = null;
|
|
588
592
|
this.pushManager = null;
|
|
589
593
|
this.onupdatefound = null;
|
|
590
594
|
}
|
|
591
595
|
// Standard ServiceWorkerRegistration properties
|
|
592
596
|
get active() {
|
|
593
|
-
return this.
|
|
597
|
+
return this[kServiceWorker].state === "activated" ? this[kServiceWorker] : null;
|
|
594
598
|
}
|
|
595
599
|
get installing() {
|
|
596
|
-
return this.
|
|
600
|
+
return this[kServiceWorker].state === "installing" ? this[kServiceWorker] : null;
|
|
597
601
|
}
|
|
598
602
|
get waiting() {
|
|
599
|
-
return this.
|
|
603
|
+
return this[kServiceWorker].state === "installed" ? this[kServiceWorker] : null;
|
|
600
604
|
}
|
|
601
605
|
// Standard ServiceWorkerRegistration methods
|
|
602
606
|
async getNotifications(_options) {
|
|
@@ -613,13 +617,14 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
613
617
|
async update() {
|
|
614
618
|
return this;
|
|
615
619
|
}
|
|
616
|
-
//
|
|
620
|
+
// Internal lifecycle methods (accessed via symbols by runLifecycle)
|
|
617
621
|
/**
|
|
618
|
-
*
|
|
622
|
+
* Dispatch the install lifecycle event
|
|
623
|
+
* @internal Use runLifecycle() instead of calling this directly
|
|
619
624
|
*/
|
|
620
|
-
async
|
|
621
|
-
if (this.
|
|
622
|
-
this.
|
|
625
|
+
async [kDispatchInstall]() {
|
|
626
|
+
if (this[kServiceWorker].state !== "parsed") return;
|
|
627
|
+
this[kServiceWorker]._setState("installing");
|
|
623
628
|
return new Promise((resolve, reject) => {
|
|
624
629
|
const event = new ShovelInstallEvent();
|
|
625
630
|
process.nextTick(() => {
|
|
@@ -632,7 +637,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
632
637
|
}
|
|
633
638
|
const promises = event.getPromises();
|
|
634
639
|
if (promises.length === 0) {
|
|
635
|
-
this.
|
|
640
|
+
this[kServiceWorker]._setState("installed");
|
|
636
641
|
resolve();
|
|
637
642
|
} else {
|
|
638
643
|
promiseWithTimeout(
|
|
@@ -640,7 +645,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
640
645
|
3e4,
|
|
641
646
|
"ServiceWorker install event timed out after 30s - waitUntil promises did not resolve"
|
|
642
647
|
).then(() => {
|
|
643
|
-
this.
|
|
648
|
+
this[kServiceWorker]._setState("installed");
|
|
644
649
|
resolve();
|
|
645
650
|
}).catch(reject);
|
|
646
651
|
}
|
|
@@ -648,13 +653,14 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
648
653
|
});
|
|
649
654
|
}
|
|
650
655
|
/**
|
|
651
|
-
*
|
|
656
|
+
* Dispatch the activate lifecycle event
|
|
657
|
+
* @internal Use runLifecycle() instead of calling this directly
|
|
652
658
|
*/
|
|
653
|
-
async
|
|
654
|
-
if (this.
|
|
659
|
+
async [kDispatchActivate]() {
|
|
660
|
+
if (this[kServiceWorker].state !== "installed") {
|
|
655
661
|
throw new Error("ServiceWorker must be installed before activation");
|
|
656
662
|
}
|
|
657
|
-
this.
|
|
663
|
+
this[kServiceWorker]._setState("activating");
|
|
658
664
|
return new Promise((resolve, reject) => {
|
|
659
665
|
const event = new ShovelActivateEvent();
|
|
660
666
|
process.nextTick(() => {
|
|
@@ -667,7 +673,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
667
673
|
}
|
|
668
674
|
const promises = event.getPromises();
|
|
669
675
|
if (promises.length === 0) {
|
|
670
|
-
this.
|
|
676
|
+
this[kServiceWorker]._setState("activated");
|
|
671
677
|
resolve();
|
|
672
678
|
} else {
|
|
673
679
|
promiseWithTimeout(
|
|
@@ -675,7 +681,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
675
681
|
3e4,
|
|
676
682
|
"ServiceWorker activate event timed out after 30s - waitUntil promises did not resolve"
|
|
677
683
|
).then(() => {
|
|
678
|
-
this.
|
|
684
|
+
this[kServiceWorker]._setState("activated");
|
|
679
685
|
resolve();
|
|
680
686
|
}).catch(reject);
|
|
681
687
|
}
|
|
@@ -683,15 +689,16 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
683
689
|
});
|
|
684
690
|
}
|
|
685
691
|
/**
|
|
686
|
-
* Handle a fetch request
|
|
692
|
+
* Handle a fetch request
|
|
693
|
+
* @internal Use the kHandleRequest symbol to access this method
|
|
687
694
|
*
|
|
688
695
|
* Platforms create a ShovelFetchEvent (or subclass) with platform-specific
|
|
689
696
|
* properties and hooks, then pass it to this method for dispatching.
|
|
690
697
|
*
|
|
691
698
|
* @param event - The fetch event to handle (created by platform adapter)
|
|
692
699
|
*/
|
|
693
|
-
async
|
|
694
|
-
if (this.
|
|
700
|
+
async [kHandleRequest](event) {
|
|
701
|
+
if (this[kServiceWorker].state !== "activated") {
|
|
695
702
|
throw new Error("ServiceWorker not activated");
|
|
696
703
|
}
|
|
697
704
|
return cookieStoreStorage.run(event.cookieStore, async () => {
|
|
@@ -722,10 +729,20 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
722
729
|
* Check if ready to handle requests (Shovel extension)
|
|
723
730
|
*/
|
|
724
731
|
get ready() {
|
|
725
|
-
return this.
|
|
732
|
+
return this[kServiceWorker].state === "activated";
|
|
726
733
|
}
|
|
727
734
|
// Events: updatefound (standard), plus Shovel lifecycle events
|
|
728
735
|
};
|
|
736
|
+
async function runLifecycle(registration, stage = "activate") {
|
|
737
|
+
await registration[kDispatchInstall]();
|
|
738
|
+
if (stage === "activate") {
|
|
739
|
+
await registration[kDispatchActivate]();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async function dispatchRequest(registration, requestOrEvent) {
|
|
743
|
+
const event = requestOrEvent instanceof ShovelFetchEvent ? requestOrEvent : new ShovelFetchEvent(requestOrEvent);
|
|
744
|
+
return registration[kHandleRequest](event);
|
|
745
|
+
}
|
|
729
746
|
var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
730
747
|
#registrations;
|
|
731
748
|
controller;
|
|
@@ -765,8 +782,8 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
|
765
782
|
const scope = this.#normalizeScope(options?.scope || "/");
|
|
766
783
|
let registration = this.#registrations.get(scope);
|
|
767
784
|
if (registration) {
|
|
768
|
-
registration.
|
|
769
|
-
registration.
|
|
785
|
+
registration[kServiceWorker].scriptURL = url;
|
|
786
|
+
registration[kServiceWorker]._setState("parsed");
|
|
770
787
|
} else {
|
|
771
788
|
registration = new ShovelServiceWorkerRegistration(scope, url);
|
|
772
789
|
this.#registrations.set(scope, registration);
|
|
@@ -786,30 +803,13 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
|
786
803
|
}
|
|
787
804
|
return false;
|
|
788
805
|
}
|
|
789
|
-
/**
|
|
790
|
-
* Route a request to the appropriate registration based on scope matching
|
|
791
|
-
*/
|
|
792
|
-
async handleRequest(request) {
|
|
793
|
-
const url = new URL(request.url);
|
|
794
|
-
const pathname = url.pathname;
|
|
795
|
-
const matchingScope = this.#findMatchingScope(pathname);
|
|
796
|
-
if (matchingScope) {
|
|
797
|
-
const registration = this.#registrations.get(matchingScope);
|
|
798
|
-
if (registration && registration.ready) {
|
|
799
|
-
const event = new ShovelFetchEvent(request);
|
|
800
|
-
return await registration.handleRequest(event);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
805
806
|
/**
|
|
806
807
|
* Install and activate all registrations
|
|
807
808
|
*/
|
|
808
809
|
async installAll() {
|
|
809
810
|
const installations = Array.from(this.#registrations.values()).map(
|
|
810
811
|
async (registration) => {
|
|
811
|
-
await registration
|
|
812
|
-
await registration.activate();
|
|
812
|
+
await runLifecycle(registration);
|
|
813
813
|
}
|
|
814
814
|
);
|
|
815
815
|
await promiseWithTimeout(
|
|
@@ -838,19 +838,6 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
|
838
838
|
}
|
|
839
839
|
return scope;
|
|
840
840
|
}
|
|
841
|
-
/**
|
|
842
|
-
* Find the most specific scope that matches a pathname
|
|
843
|
-
*/
|
|
844
|
-
#findMatchingScope(pathname) {
|
|
845
|
-
const scopes = Array.from(this.#registrations.keys());
|
|
846
|
-
scopes.sort((a, b) => b.length - a.length);
|
|
847
|
-
for (const scope of scopes) {
|
|
848
|
-
if (pathname.startsWith(scope === "/" ? "/" : scope)) {
|
|
849
|
-
return scope;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
return null;
|
|
853
|
-
}
|
|
854
841
|
// Events: controllerchange, message, messageerror, updatefound
|
|
855
842
|
};
|
|
856
843
|
var Notification = class extends EventTarget {
|
|
@@ -1035,8 +1022,10 @@ var ServiceWorkerGlobals = class {
|
|
|
1035
1022
|
}
|
|
1036
1023
|
const request = new Request(new URL(urlString, "http://localhost"), init);
|
|
1037
1024
|
return fetchDepthStorage.run(currentDepth + 1, () => {
|
|
1038
|
-
|
|
1039
|
-
|
|
1025
|
+
return dispatchRequest(
|
|
1026
|
+
this.registration,
|
|
1027
|
+
request
|
|
1028
|
+
);
|
|
1040
1029
|
});
|
|
1041
1030
|
}
|
|
1042
1031
|
queueMicrotask(callback) {
|
|
@@ -1121,9 +1110,9 @@ var ServiceWorkerGlobals = class {
|
|
|
1121
1110
|
* Allows the ServiceWorker to activate immediately
|
|
1122
1111
|
*/
|
|
1123
1112
|
async skipWaiting() {
|
|
1124
|
-
getLogger(["shovel", "platform"]).
|
|
1113
|
+
getLogger(["shovel", "platform"]).debug("skipWaiting() called");
|
|
1125
1114
|
if (!this.#isDevelopment) {
|
|
1126
|
-
getLogger(["shovel", "platform"]).
|
|
1115
|
+
getLogger(["shovel", "platform"]).debug(
|
|
1127
1116
|
"skipWaiting() - production graceful restart not implemented"
|
|
1128
1117
|
);
|
|
1129
1118
|
}
|
|
@@ -1320,8 +1309,7 @@ function startWorkerMessageLoop(options) {
|
|
|
1320
1309
|
headers: message.request.headers,
|
|
1321
1310
|
body: message.request.body
|
|
1322
1311
|
});
|
|
1323
|
-
const
|
|
1324
|
-
const response = await registration.handleRequest(event);
|
|
1312
|
+
const response = await dispatchRequest(registration, request);
|
|
1325
1313
|
const body = await response.arrayBuffer();
|
|
1326
1314
|
const headers = Object.fromEntries(response.headers.entries());
|
|
1327
1315
|
if (!headers["Content-Type"] && !headers["content-type"]) {
|
|
@@ -1492,9 +1480,14 @@ export {
|
|
|
1492
1480
|
createCacheFactory,
|
|
1493
1481
|
createDatabaseFactory,
|
|
1494
1482
|
createDirectoryFactory,
|
|
1483
|
+
dispatchRequest,
|
|
1495
1484
|
initWorkerRuntime,
|
|
1485
|
+
kDispatchActivate,
|
|
1486
|
+
kDispatchInstall,
|
|
1487
|
+
kServiceWorker,
|
|
1496
1488
|
parseCookieHeader,
|
|
1497
1489
|
parseSetCookieHeader,
|
|
1490
|
+
runLifecycle,
|
|
1498
1491
|
serializeCookie,
|
|
1499
1492
|
startWorkerMessageLoop
|
|
1500
1493
|
};
|