@b9g/platform-bun 0.1.11 → 0.1.12-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/index.d.ts +85 -25
- package/src/index.js +264 -373
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/platform-bun",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12-beta.0",
|
|
4
4
|
"description": "Bun platform adapter for Shovel with hot reloading and built-in TypeScript/JSX support",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"shovel",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"@b9g/assets": "^0.2.0-beta.0",
|
|
16
16
|
"@b9g/cache": "^0.2.0-beta.0",
|
|
17
17
|
"@b9g/http-errors": "^0.2.0-beta.0",
|
|
18
|
-
"@b9g/platform": "^0.1.
|
|
18
|
+
"@b9g/platform": "^0.1.14-beta.0",
|
|
19
19
|
"@logtape/logtape": "^1.2.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
package/src/index.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides built-in TypeScript/JSX support and simplified server setup for Bun environments.
|
|
5
5
|
*/
|
|
6
|
-
import { BasePlatform, type PlatformConfig, type PlatformDefaults, type Handler, type Server, type ServerOptions, type ServiceWorkerOptions, type ServiceWorkerInstance, type EntryWrapperOptions, type PlatformESBuildConfig, ServiceWorkerPool, CustomLoggerStorage, CustomDatabaseStorage } from "@b9g/platform";
|
|
7
|
-
import { type ShovelConfig } from "@b9g/platform/runtime";
|
|
8
6
|
import { CustomCacheStorage } from "@b9g/cache";
|
|
9
7
|
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
8
|
+
import { BasePlatform, type PlatformConfig, type PlatformDefaults, type Handler, type Server, type ServerOptions, type PlatformESBuildConfig, type ProductionEntryPoints, ServiceWorkerPool, CustomLoggerStorage, CustomDatabaseStorage } from "@b9g/platform";
|
|
9
|
+
import { type ShovelConfig } from "@b9g/platform/runtime";
|
|
10
10
|
export interface BunPlatformOptions extends PlatformConfig {
|
|
11
11
|
/** Port for development server (default: 3000) */
|
|
12
12
|
port?: number;
|
|
@@ -19,6 +19,55 @@ export interface BunPlatformOptions extends PlatformConfig {
|
|
|
19
19
|
/** Shovel configuration (caches, directories, etc.) */
|
|
20
20
|
config?: ShovelConfig;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Bun ServiceWorkerContainer implementation
|
|
24
|
+
* Manages ServiceWorker registrations backed by native Web Workers
|
|
25
|
+
*
|
|
26
|
+
* Note: In Bun's production model, workers handle their own HTTP servers
|
|
27
|
+
* via reusePort, so the supervisor doesn't route requests through the pool.
|
|
28
|
+
* This container is mainly for worker lifecycle management.
|
|
29
|
+
*/
|
|
30
|
+
export declare class BunServiceWorkerContainer extends EventTarget implements ServiceWorkerContainer {
|
|
31
|
+
#private;
|
|
32
|
+
readonly controller: ServiceWorker | null;
|
|
33
|
+
oncontrollerchange: ((ev: Event) => unknown) | null;
|
|
34
|
+
onmessage: ((ev: MessageEvent) => unknown) | null;
|
|
35
|
+
onmessageerror: ((ev: MessageEvent) => unknown) | null;
|
|
36
|
+
constructor(platform: BunPlatform);
|
|
37
|
+
/**
|
|
38
|
+
* Register a ServiceWorker script
|
|
39
|
+
* Spawns Web Workers (each with their own HTTP server in production)
|
|
40
|
+
*/
|
|
41
|
+
register(scriptURL: string | URL, options?: RegistrationOptions): Promise<ServiceWorkerRegistration>;
|
|
42
|
+
/**
|
|
43
|
+
* Get registration for scope
|
|
44
|
+
*/
|
|
45
|
+
getRegistration(scope?: string): Promise<ServiceWorkerRegistration | undefined>;
|
|
46
|
+
/**
|
|
47
|
+
* Get all registrations
|
|
48
|
+
*/
|
|
49
|
+
getRegistrations(): Promise<readonly ServiceWorkerRegistration[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Start receiving messages (no-op in server context)
|
|
52
|
+
*/
|
|
53
|
+
startMessages(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Ready promise - resolves when a registration is active
|
|
56
|
+
*/
|
|
57
|
+
get ready(): Promise<ServiceWorkerRegistration>;
|
|
58
|
+
/**
|
|
59
|
+
* Internal: Get worker pool for request handling
|
|
60
|
+
*/
|
|
61
|
+
get pool(): ServiceWorkerPool | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Internal: Terminate workers and dispose cache storage
|
|
64
|
+
*/
|
|
65
|
+
terminate(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Internal: Reload workers (for hot reload)
|
|
68
|
+
*/
|
|
69
|
+
reloadWorkers(entrypoint: string): Promise<void>;
|
|
70
|
+
}
|
|
22
71
|
/**
|
|
23
72
|
* Bun platform implementation
|
|
24
73
|
* ServiceWorker entrypoint loader for Bun with native TypeScript/JSX support
|
|
@@ -26,6 +75,7 @@ export interface BunPlatformOptions extends PlatformConfig {
|
|
|
26
75
|
export declare class BunPlatform extends BasePlatform {
|
|
27
76
|
#private;
|
|
28
77
|
readonly name: string;
|
|
78
|
+
readonly serviceWorker: BunServiceWorkerContainer;
|
|
29
79
|
constructor(options?: BunPlatformOptions);
|
|
30
80
|
/**
|
|
31
81
|
* Get options for testing
|
|
@@ -38,26 +88,35 @@ export declare class BunPlatform extends BasePlatform {
|
|
|
38
88
|
config?: ShovelConfig;
|
|
39
89
|
};
|
|
40
90
|
/**
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* Create cache storage using config from shovel.json
|
|
47
|
-
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
91
|
+
* Create cache storage for Bun
|
|
92
|
+
*
|
|
93
|
+
* Default: MemoryCache (in-process LRU cache).
|
|
94
|
+
* Override via shovel.json caches config.
|
|
95
|
+
* Note: Used for dev/testing - production uses generated config module.
|
|
48
96
|
*/
|
|
49
97
|
createCaches(): Promise<CustomCacheStorage>;
|
|
50
98
|
/**
|
|
51
|
-
* Create directory storage
|
|
52
|
-
*
|
|
99
|
+
* Create directory storage for Bun
|
|
100
|
+
*
|
|
101
|
+
* Defaults:
|
|
102
|
+
* - server: NodeFSDirectory at cwd (app files)
|
|
103
|
+
* - public: NodeFSDirectory at cwd (static assets)
|
|
104
|
+
* - tmp: NodeFSDirectory at OS temp dir
|
|
105
|
+
*
|
|
106
|
+
* Override via shovel.json directories config.
|
|
53
107
|
*/
|
|
54
108
|
createDirectories(): Promise<CustomDirectoryStorage>;
|
|
55
109
|
/**
|
|
56
|
-
* Create logger storage
|
|
110
|
+
* Create logger storage for Bun
|
|
111
|
+
*
|
|
112
|
+
* Uses LogTape for structured logging.
|
|
57
113
|
*/
|
|
58
114
|
createLoggers(): Promise<CustomLoggerStorage>;
|
|
59
115
|
/**
|
|
60
|
-
* Create database storage
|
|
116
|
+
* Create database storage for Bun
|
|
117
|
+
*
|
|
118
|
+
* Returns undefined if no databases configured in shovel.json.
|
|
119
|
+
* Supports SQLite via bun:sqlite.
|
|
61
120
|
*/
|
|
62
121
|
createDatabases(configOverride?: BunPlatformOptions["config"]): CustomDatabaseStorage | undefined;
|
|
63
122
|
/**
|
|
@@ -65,28 +124,29 @@ export declare class BunPlatform extends BasePlatform {
|
|
|
65
124
|
*/
|
|
66
125
|
createServer(handler: Handler, options?: ServerOptions): Server;
|
|
67
126
|
/**
|
|
68
|
-
*
|
|
69
|
-
|
|
127
|
+
* Start listening for connections using pool's handlers
|
|
128
|
+
*/
|
|
129
|
+
listen(): Promise<Server>;
|
|
130
|
+
/**
|
|
131
|
+
* Close the server
|
|
70
132
|
*/
|
|
71
|
-
|
|
133
|
+
close(): Promise<void>;
|
|
72
134
|
/**
|
|
73
135
|
* Reload workers for hot reloading (called by CLI)
|
|
74
136
|
* @param entrypoint - Path to the new entrypoint (hashed filename)
|
|
75
137
|
*/
|
|
76
138
|
reloadWorkers(entrypoint: string): Promise<void>;
|
|
77
139
|
/**
|
|
78
|
-
* Get
|
|
140
|
+
* Get production entry points for bundling.
|
|
79
141
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* @param options.outDir - Output directory (required for "worker" type)
|
|
142
|
+
* Bun produces two files:
|
|
143
|
+
* - index.js: Supervisor that spawns workers and handles signals
|
|
144
|
+
* - worker.js: Worker with its own HTTP server (uses reusePort for multi-worker)
|
|
84
145
|
*
|
|
85
|
-
*
|
|
86
|
-
* -
|
|
87
|
-
* - "worker": Worker entry that sets up runtime and message loop
|
|
146
|
+
* Unlike Node.js, Bun workers each bind their own server with reusePort,
|
|
147
|
+
* allowing the OS to load-balance across workers without message passing overhead.
|
|
88
148
|
*/
|
|
89
|
-
|
|
149
|
+
getProductionEntryPoints(userEntryPath: string): ProductionEntryPoints;
|
|
90
150
|
/**
|
|
91
151
|
* Get Bun-specific esbuild configuration
|
|
92
152
|
*
|
package/src/index.js
CHANGED
|
@@ -2,162 +2,148 @@
|
|
|
2
2
|
// src/index.ts
|
|
3
3
|
import { builtinModules } from "node:module";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
+
import * as Path from "node:path";
|
|
6
|
+
import { getLogger } from "@logtape/logtape";
|
|
7
|
+
import { CustomCacheStorage } from "@b9g/cache";
|
|
8
|
+
import { MemoryCache } from "@b9g/cache/memory";
|
|
9
|
+
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
10
|
+
import { NodeFSDirectory } from "@b9g/filesystem/node-fs";
|
|
11
|
+
import { InternalServerError, isHTTPError } from "@b9g/http-errors";
|
|
5
12
|
import {
|
|
6
13
|
BasePlatform,
|
|
7
14
|
ServiceWorkerPool,
|
|
8
|
-
SingleThreadedRuntime,
|
|
9
15
|
CustomLoggerStorage,
|
|
10
16
|
CustomDatabaseStorage,
|
|
11
|
-
createDatabaseFactory
|
|
17
|
+
createDatabaseFactory,
|
|
18
|
+
mergeConfigWithDefaults
|
|
12
19
|
} from "@b9g/platform";
|
|
13
20
|
import {
|
|
21
|
+
ShovelServiceWorkerRegistration,
|
|
22
|
+
kServiceWorker,
|
|
14
23
|
createCacheFactory,
|
|
15
24
|
createDirectoryFactory
|
|
16
25
|
} from "@b9g/platform/runtime";
|
|
17
|
-
import { CustomCacheStorage } from "@b9g/cache";
|
|
18
|
-
import { MemoryCache } from "@b9g/cache/memory";
|
|
19
|
-
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
20
|
-
import { NodeFSDirectory } from "@b9g/filesystem/node-fs";
|
|
21
|
-
import { InternalServerError, isHTTPError } from "@b9g/http-errors";
|
|
22
|
-
import { getLogger } from "@logtape/logtape";
|
|
23
|
-
import * as Path from "path";
|
|
24
26
|
import { MemoryCache as MemoryCache2 } from "@b9g/cache/memory";
|
|
25
|
-
var entryTemplate = `// Bun Production Server Entry
|
|
26
|
-
import {tmpdir} from "os"; // For [tmpdir] config expressions
|
|
27
|
-
import {getLogger} from "@logtape/logtape";
|
|
28
|
-
import {configureLogging} from "@b9g/platform/runtime";
|
|
29
|
-
import {config} from "shovel:config"; // Virtual module - resolved at build time
|
|
30
|
-
import BunPlatform from "@b9g/platform-bun";
|
|
31
|
-
|
|
32
|
-
// Configure logging before anything else
|
|
33
|
-
await configureLogging(config.logging);
|
|
34
|
-
|
|
35
|
-
const logger = getLogger(["shovel", "platform"]);
|
|
36
|
-
|
|
37
|
-
// Configuration from shovel:config
|
|
38
|
-
const PORT = config.port;
|
|
39
|
-
const HOST = config.host;
|
|
40
|
-
const WORKERS = config.workers;
|
|
41
|
-
|
|
42
|
-
// Use explicit marker instead of Bun.isMainThread
|
|
43
|
-
// This handles the edge case where Shovel's build output is embedded in another worker
|
|
44
|
-
const isShovelWorker = process.env.SHOVEL_SPAWNED_WORKER === "1";
|
|
45
|
-
|
|
46
|
-
if (isShovelWorker) {
|
|
47
|
-
// Worker thread: runs BOTH server AND ServiceWorker
|
|
48
|
-
const platform = new BunPlatform({port: PORT, host: HOST, workers: 1});
|
|
49
|
-
const userCodePath = new URL("./server.js", import.meta.url).pathname;
|
|
50
|
-
const serviceWorker = await platform.loadServiceWorker(userCodePath);
|
|
51
|
-
|
|
52
|
-
Bun.serve({
|
|
53
|
-
port: PORT,
|
|
54
|
-
hostname: HOST,
|
|
55
|
-
// Only need reusePort for multi-worker (multiple listeners on same port)
|
|
56
|
-
reusePort: WORKERS > 1,
|
|
57
|
-
fetch: serviceWorker.handleRequest,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Signal ready to main thread
|
|
61
|
-
postMessage({type: "ready", thread: Bun.threadId});
|
|
62
|
-
logger.info("Worker started", {port: PORT, thread: Bun.threadId});
|
|
63
|
-
} else {
|
|
64
|
-
// Main thread: supervisor only - ALWAYS spawn workers (even for workers:1)
|
|
65
|
-
// This ensures ServiceWorker code always runs in a worker thread for dev/prod parity
|
|
66
|
-
|
|
67
|
-
// Port availability check - fail fast if port is in use
|
|
68
|
-
// Prevents accidental port sharing with other processes
|
|
69
|
-
const checkPort = async () => {
|
|
70
|
-
try {
|
|
71
|
-
const testServer = Bun.serve({port: PORT, hostname: HOST, fetch: () => new Response()});
|
|
72
|
-
testServer.stop();
|
|
73
|
-
} catch (err) {
|
|
74
|
-
logger.error("Port unavailable", {port: PORT, host: HOST, error: err});
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
await checkPort();
|
|
79
|
-
|
|
80
|
-
let shuttingDown = false;
|
|
81
|
-
const workers = [];
|
|
82
|
-
let readyCount = 0;
|
|
83
|
-
|
|
84
|
-
for (let i = 0; i < WORKERS; i++) {
|
|
85
|
-
const worker = new Worker(import.meta.path, {
|
|
86
|
-
env: {...process.env, SHOVEL_SPAWNED_WORKER: "1"},
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
worker.onmessage = (event) => {
|
|
90
|
-
if (event.data.type === "ready") {
|
|
91
|
-
readyCount++;
|
|
92
|
-
if (readyCount === WORKERS) {
|
|
93
|
-
logger.info("All workers ready", {count: WORKERS, port: PORT});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
worker.onerror = (error) => {
|
|
99
|
-
logger.error("Worker error", {error: error.message});
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// If a worker crashes, fail fast - let process supervisor handle restarts
|
|
103
|
-
worker.addEventListener("close", () => {
|
|
104
|
-
if (shuttingDown) return;
|
|
105
|
-
logger.error("Worker crashed, exiting");
|
|
106
|
-
process.exit(1);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
workers.push(worker);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
logger.info("Spawned workers", {count: WORKERS, port: PORT});
|
|
113
|
-
|
|
114
|
-
// Graceful shutdown
|
|
115
|
-
const shutdown = async () => {
|
|
116
|
-
shuttingDown = true;
|
|
117
|
-
logger.info("Shutting down workers");
|
|
118
|
-
for (const worker of workers) {
|
|
119
|
-
worker.terminate();
|
|
120
|
-
}
|
|
121
|
-
process.exit(0);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
process.on("SIGINT", shutdown);
|
|
125
|
-
process.on("SIGTERM", shutdown);
|
|
126
|
-
}
|
|
127
|
-
`;
|
|
128
|
-
var workerEntryTemplate = `// Worker Entry for ServiceWorkerPool
|
|
129
|
-
// This file sets up the ServiceWorker runtime and message loop
|
|
130
|
-
import {tmpdir} from "os"; // For [tmpdir] config expressions
|
|
131
|
-
import {config} from "shovel:config";
|
|
132
|
-
import {initWorkerRuntime, startWorkerMessageLoop, configureLogging} from "@b9g/platform/runtime";
|
|
133
|
-
|
|
134
|
-
// Configure logging before anything else
|
|
135
|
-
await configureLogging(config.logging);
|
|
136
|
-
|
|
137
|
-
// Initialize the worker runtime (installs ServiceWorker globals)
|
|
138
|
-
// Platform defaults and paths are already resolved at build time
|
|
139
|
-
const {registration, databases} = await initWorkerRuntime({config});
|
|
140
|
-
|
|
141
|
-
// Import user code (registers event handlers via addEventListener)
|
|
142
|
-
// Must use dynamic import to ensure globals are installed first
|
|
143
|
-
await import("__USER_ENTRY__");
|
|
144
|
-
|
|
145
|
-
// Run ServiceWorker lifecycle
|
|
146
|
-
await registration.install();
|
|
147
|
-
await registration.activate();
|
|
148
|
-
|
|
149
|
-
// Start the message loop (handles request/response messages from main thread)
|
|
150
|
-
// Pass databases so they can be closed on graceful shutdown
|
|
151
|
-
startWorkerMessageLoop({registration, databases});
|
|
152
|
-
`;
|
|
153
27
|
var logger = getLogger(["shovel", "platform"]);
|
|
28
|
+
var BunServiceWorkerContainer = class extends EventTarget {
|
|
29
|
+
#platform;
|
|
30
|
+
#pool;
|
|
31
|
+
#cacheStorage;
|
|
32
|
+
#registration;
|
|
33
|
+
#readyPromise;
|
|
34
|
+
#readyResolve;
|
|
35
|
+
// Standard ServiceWorkerContainer properties
|
|
36
|
+
controller;
|
|
37
|
+
oncontrollerchange;
|
|
38
|
+
onmessage;
|
|
39
|
+
onmessageerror;
|
|
40
|
+
constructor(platform) {
|
|
41
|
+
super();
|
|
42
|
+
this.#platform = platform;
|
|
43
|
+
this.#readyPromise = new Promise((resolve2) => {
|
|
44
|
+
this.#readyResolve = resolve2;
|
|
45
|
+
});
|
|
46
|
+
this.controller = null;
|
|
47
|
+
this.oncontrollerchange = null;
|
|
48
|
+
this.onmessage = null;
|
|
49
|
+
this.onmessageerror = null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Register a ServiceWorker script
|
|
53
|
+
* Spawns Web Workers (each with their own HTTP server in production)
|
|
54
|
+
*/
|
|
55
|
+
async register(scriptURL, options) {
|
|
56
|
+
const urlStr = typeof scriptURL === "string" ? scriptURL : scriptURL.toString();
|
|
57
|
+
const scope = options?.scope ?? "/";
|
|
58
|
+
let entryPath;
|
|
59
|
+
if (urlStr.startsWith("file://")) {
|
|
60
|
+
entryPath = new URL(urlStr).pathname;
|
|
61
|
+
} else {
|
|
62
|
+
entryPath = Path.resolve(this.#platform.options.cwd, urlStr);
|
|
63
|
+
}
|
|
64
|
+
let config = this.#platform.options.config;
|
|
65
|
+
const configPath = Path.join(Path.dirname(entryPath), "config.js");
|
|
66
|
+
try {
|
|
67
|
+
const configModule = await import(configPath);
|
|
68
|
+
config = configModule.config ?? config;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.debug`Using platform config (no config.js found): ${error}`;
|
|
71
|
+
}
|
|
72
|
+
if (!this.#cacheStorage && config?.caches) {
|
|
73
|
+
this.#cacheStorage = new CustomCacheStorage(
|
|
74
|
+
createCacheFactory({ configs: config.caches })
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (this.#pool) {
|
|
78
|
+
await this.#pool.terminate();
|
|
79
|
+
}
|
|
80
|
+
this.#pool = new ServiceWorkerPool(
|
|
81
|
+
{
|
|
82
|
+
workerCount: this.#platform.options.workers,
|
|
83
|
+
createWorker: (entrypoint) => new Worker(entrypoint)
|
|
84
|
+
},
|
|
85
|
+
entryPath,
|
|
86
|
+
this.#cacheStorage
|
|
87
|
+
);
|
|
88
|
+
await this.#pool.init();
|
|
89
|
+
this.#registration = new ShovelServiceWorkerRegistration(scope, urlStr);
|
|
90
|
+
this.#registration[kServiceWorker]._setState("activated");
|
|
91
|
+
this.#readyResolve?.(this.#registration);
|
|
92
|
+
return this.#registration;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get registration for scope
|
|
96
|
+
*/
|
|
97
|
+
async getRegistration(scope) {
|
|
98
|
+
if (scope === void 0 || scope === "/" || scope === this.#registration?.scope) {
|
|
99
|
+
return this.#registration;
|
|
100
|
+
}
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get all registrations
|
|
105
|
+
*/
|
|
106
|
+
async getRegistrations() {
|
|
107
|
+
return this.#registration ? [this.#registration] : [];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Start receiving messages (no-op in server context)
|
|
111
|
+
*/
|
|
112
|
+
startMessages() {
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Ready promise - resolves when a registration is active
|
|
116
|
+
*/
|
|
117
|
+
get ready() {
|
|
118
|
+
return this.#readyPromise;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Internal: Get worker pool for request handling
|
|
122
|
+
*/
|
|
123
|
+
get pool() {
|
|
124
|
+
return this.#pool;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Internal: Terminate workers and dispose cache storage
|
|
128
|
+
*/
|
|
129
|
+
async terminate() {
|
|
130
|
+
await this.#pool?.terminate();
|
|
131
|
+
this.#pool = void 0;
|
|
132
|
+
await this.#cacheStorage?.dispose();
|
|
133
|
+
this.#cacheStorage = void 0;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Internal: Reload workers (for hot reload)
|
|
137
|
+
*/
|
|
138
|
+
async reloadWorkers(entrypoint) {
|
|
139
|
+
await this.#pool?.reloadWorkers(entrypoint);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
154
142
|
var BunPlatform = class extends BasePlatform {
|
|
155
143
|
name;
|
|
144
|
+
serviceWorker;
|
|
156
145
|
#options;
|
|
157
|
-
#
|
|
158
|
-
#singleThreadedRuntime;
|
|
159
|
-
#cacheStorage;
|
|
160
|
-
#directoryStorage;
|
|
146
|
+
#server;
|
|
161
147
|
#databaseStorage;
|
|
162
148
|
constructor(options = {}) {
|
|
163
149
|
super(options);
|
|
@@ -170,6 +156,7 @@ var BunPlatform = class extends BasePlatform {
|
|
|
170
156
|
cwd,
|
|
171
157
|
config: options.config
|
|
172
158
|
};
|
|
159
|
+
this.serviceWorker = new BunServiceWorkerContainer(this);
|
|
173
160
|
}
|
|
174
161
|
/**
|
|
175
162
|
* Get options for testing
|
|
@@ -178,62 +165,55 @@ var BunPlatform = class extends BasePlatform {
|
|
|
178
165
|
return this.#options;
|
|
179
166
|
}
|
|
180
167
|
/**
|
|
181
|
-
*
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
set workerPool(pool) {
|
|
187
|
-
this.#workerPool = pool;
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Create cache storage using config from shovel.json
|
|
191
|
-
* Merges with runtime defaults (actual class references) for fallback behavior
|
|
168
|
+
* Create cache storage for Bun
|
|
169
|
+
*
|
|
170
|
+
* Default: MemoryCache (in-process LRU cache).
|
|
171
|
+
* Override via shovel.json caches config.
|
|
172
|
+
* Note: Used for dev/testing - production uses generated config module.
|
|
192
173
|
*/
|
|
193
174
|
async createCaches() {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const allNames = /* @__PURE__ */ new Set([
|
|
200
|
-
...Object.keys(runtimeDefaults),
|
|
201
|
-
...Object.keys(userCaches)
|
|
202
|
-
]);
|
|
203
|
-
for (const name of allNames) {
|
|
204
|
-
configs[name] = { ...runtimeDefaults[name], ...userCaches[name] };
|
|
205
|
-
}
|
|
175
|
+
const defaults = { default: { impl: MemoryCache } };
|
|
176
|
+
const configs = mergeConfigWithDefaults(
|
|
177
|
+
defaults,
|
|
178
|
+
this.#options.config?.caches
|
|
179
|
+
);
|
|
206
180
|
return new CustomCacheStorage(createCacheFactory({ configs }));
|
|
207
181
|
}
|
|
208
182
|
/**
|
|
209
|
-
* Create directory storage
|
|
210
|
-
*
|
|
183
|
+
* Create directory storage for Bun
|
|
184
|
+
*
|
|
185
|
+
* Defaults:
|
|
186
|
+
* - server: NodeFSDirectory at cwd (app files)
|
|
187
|
+
* - public: NodeFSDirectory at cwd (static assets)
|
|
188
|
+
* - tmp: NodeFSDirectory at OS temp dir
|
|
189
|
+
*
|
|
190
|
+
* Override via shovel.json directories config.
|
|
211
191
|
*/
|
|
212
192
|
async createDirectories() {
|
|
213
|
-
const
|
|
193
|
+
const defaults = {
|
|
214
194
|
server: { impl: NodeFSDirectory, path: this.#options.cwd },
|
|
215
195
|
public: { impl: NodeFSDirectory, path: this.#options.cwd },
|
|
216
196
|
tmp: { impl: NodeFSDirectory, path: tmpdir() }
|
|
217
197
|
};
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
...Object.keys(userDirs)
|
|
223
|
-
]);
|
|
224
|
-
for (const name of allNames) {
|
|
225
|
-
configs[name] = { ...runtimeDefaults[name], ...userDirs[name] };
|
|
226
|
-
}
|
|
198
|
+
const configs = mergeConfigWithDefaults(
|
|
199
|
+
defaults,
|
|
200
|
+
this.#options.config?.directories
|
|
201
|
+
);
|
|
227
202
|
return new CustomDirectoryStorage(createDirectoryFactory(configs));
|
|
228
203
|
}
|
|
229
204
|
/**
|
|
230
|
-
* Create logger storage
|
|
205
|
+
* Create logger storage for Bun
|
|
206
|
+
*
|
|
207
|
+
* Uses LogTape for structured logging.
|
|
231
208
|
*/
|
|
232
209
|
async createLoggers() {
|
|
233
210
|
return new CustomLoggerStorage((categories) => getLogger(categories));
|
|
234
211
|
}
|
|
235
212
|
/**
|
|
236
|
-
* Create database storage
|
|
213
|
+
* Create database storage for Bun
|
|
214
|
+
*
|
|
215
|
+
* Returns undefined if no databases configured in shovel.json.
|
|
216
|
+
* Supports SQLite via bun:sqlite.
|
|
237
217
|
*/
|
|
238
218
|
createDatabases(configOverride) {
|
|
239
219
|
const config = configOverride ?? this.#options.config;
|
|
@@ -249,9 +229,11 @@ var BunPlatform = class extends BasePlatform {
|
|
|
249
229
|
createServer(handler, options = {}) {
|
|
250
230
|
const requestedPort = options.port ?? this.#options.port;
|
|
251
231
|
const hostname = options.host ?? this.#options.host;
|
|
232
|
+
const reusePort = options.reusePort ?? false;
|
|
252
233
|
const server = Bun.serve({
|
|
253
234
|
port: requestedPort,
|
|
254
235
|
hostname,
|
|
236
|
+
reusePort,
|
|
255
237
|
async fetch(request) {
|
|
256
238
|
try {
|
|
257
239
|
return await handler(request);
|
|
@@ -284,207 +266,125 @@ var BunPlatform = class extends BasePlatform {
|
|
|
284
266
|
};
|
|
285
267
|
}
|
|
286
268
|
/**
|
|
287
|
-
*
|
|
288
|
-
* Uses native Web Workers with the common WorkerPool
|
|
269
|
+
* Start listening for connections using pool's handlers
|
|
289
270
|
*/
|
|
290
|
-
async
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return this.#loadServiceWorkerWithPool(entrypoint, options, workerCount);
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Load ServiceWorker directly in main thread (single-threaded mode)
|
|
299
|
-
* No postMessage overhead - maximum performance for production
|
|
300
|
-
*/
|
|
301
|
-
async #loadServiceWorkerDirect(entrypoint, _options) {
|
|
302
|
-
const entryPath = Path.resolve(this.#options.cwd, entrypoint);
|
|
303
|
-
let config = this.#options.config;
|
|
304
|
-
const configPath = Path.join(Path.dirname(entryPath), "config.js");
|
|
305
|
-
try {
|
|
306
|
-
const configModule = await import(configPath);
|
|
307
|
-
config = configModule.config ?? config;
|
|
308
|
-
} catch (err) {
|
|
309
|
-
logger.debug`Using platform config (no config.js): ${err}`;
|
|
310
|
-
}
|
|
311
|
-
if (!this.#cacheStorage) {
|
|
312
|
-
const runtimeCacheDefaults = {
|
|
313
|
-
default: { impl: MemoryCache }
|
|
314
|
-
};
|
|
315
|
-
const userCaches = config?.caches ?? {};
|
|
316
|
-
const cacheConfigs = {};
|
|
317
|
-
const allCacheNames = /* @__PURE__ */ new Set([
|
|
318
|
-
...Object.keys(runtimeCacheDefaults),
|
|
319
|
-
...Object.keys(userCaches)
|
|
320
|
-
]);
|
|
321
|
-
for (const name of allCacheNames) {
|
|
322
|
-
cacheConfigs[name] = {
|
|
323
|
-
...runtimeCacheDefaults[name],
|
|
324
|
-
...userCaches[name]
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
this.#cacheStorage = new CustomCacheStorage(
|
|
328
|
-
createCacheFactory({ configs: cacheConfigs })
|
|
271
|
+
async listen() {
|
|
272
|
+
const pool = this.serviceWorker.pool;
|
|
273
|
+
if (!pool) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
"No ServiceWorker registered - call serviceWorker.register() first"
|
|
329
276
|
);
|
|
330
277
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
public: { impl: NodeFSDirectory },
|
|
335
|
-
tmp: { impl: NodeFSDirectory }
|
|
336
|
-
};
|
|
337
|
-
const userDirs = config?.directories ?? {};
|
|
338
|
-
const dirConfigs = {};
|
|
339
|
-
const allDirNames = /* @__PURE__ */ new Set([
|
|
340
|
-
...Object.keys(runtimeDirDefaults),
|
|
341
|
-
...Object.keys(userDirs)
|
|
342
|
-
]);
|
|
343
|
-
for (const name of allDirNames) {
|
|
344
|
-
dirConfigs[name] = { ...runtimeDirDefaults[name], ...userDirs[name] };
|
|
345
|
-
}
|
|
346
|
-
this.#directoryStorage = new CustomDirectoryStorage(
|
|
347
|
-
createDirectoryFactory(dirConfigs)
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
if (!this.#databaseStorage) {
|
|
351
|
-
this.#databaseStorage = this.createDatabases(config);
|
|
352
|
-
}
|
|
353
|
-
if (this.#singleThreadedRuntime) {
|
|
354
|
-
await this.#singleThreadedRuntime.terminate();
|
|
355
|
-
}
|
|
356
|
-
if (this.#workerPool) {
|
|
357
|
-
await this.#workerPool.terminate();
|
|
358
|
-
this.#workerPool = void 0;
|
|
359
|
-
}
|
|
360
|
-
logger.info("Creating single-threaded ServiceWorker runtime", { entryPath });
|
|
361
|
-
this.#singleThreadedRuntime = new SingleThreadedRuntime({
|
|
362
|
-
caches: this.#cacheStorage,
|
|
363
|
-
directories: this.#directoryStorage,
|
|
364
|
-
databases: this.#databaseStorage,
|
|
365
|
-
loggers: new CustomLoggerStorage((cats) => getLogger(cats))
|
|
278
|
+
this.#server = this.createServer((request) => pool.handleRequest(request), {
|
|
279
|
+
port: this.#options.port,
|
|
280
|
+
host: this.#options.host
|
|
366
281
|
});
|
|
367
|
-
await this.#
|
|
368
|
-
|
|
369
|
-
const runtime = this.#singleThreadedRuntime;
|
|
370
|
-
const platform = this;
|
|
371
|
-
const instance = {
|
|
372
|
-
runtime,
|
|
373
|
-
handleRequest: async (request) => {
|
|
374
|
-
if (!platform.#singleThreadedRuntime) {
|
|
375
|
-
throw new Error("SingleThreadedRuntime not initialized");
|
|
376
|
-
}
|
|
377
|
-
return platform.#singleThreadedRuntime.handleRequest(request);
|
|
378
|
-
},
|
|
379
|
-
install: async () => {
|
|
380
|
-
logger.info("ServiceWorker installed", { method: "single_threaded" });
|
|
381
|
-
},
|
|
382
|
-
activate: async () => {
|
|
383
|
-
logger.info("ServiceWorker activated", { method: "single_threaded" });
|
|
384
|
-
},
|
|
385
|
-
get ready() {
|
|
386
|
-
return runtime?.ready ?? false;
|
|
387
|
-
},
|
|
388
|
-
dispose: async () => {
|
|
389
|
-
if (platform.#singleThreadedRuntime) {
|
|
390
|
-
await platform.#singleThreadedRuntime.terminate();
|
|
391
|
-
platform.#singleThreadedRuntime = void 0;
|
|
392
|
-
}
|
|
393
|
-
logger.info("ServiceWorker disposed", {});
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
|
-
logger.info("ServiceWorker loaded", {
|
|
397
|
-
features: ["single_threaded", "no_postmessage_overhead"]
|
|
398
|
-
});
|
|
399
|
-
return instance;
|
|
282
|
+
await this.#server.listen();
|
|
283
|
+
return this.#server;
|
|
400
284
|
}
|
|
401
285
|
/**
|
|
402
|
-
*
|
|
286
|
+
* Close the server
|
|
403
287
|
*/
|
|
404
|
-
async
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
this.#cacheStorage = await this.createCaches();
|
|
408
|
-
}
|
|
409
|
-
if (this.#singleThreadedRuntime) {
|
|
410
|
-
await this.#singleThreadedRuntime.terminate();
|
|
411
|
-
this.#singleThreadedRuntime = void 0;
|
|
412
|
-
}
|
|
413
|
-
if (this.#workerPool) {
|
|
414
|
-
await this.#workerPool.terminate();
|
|
415
|
-
}
|
|
416
|
-
const poolOptions = {
|
|
417
|
-
workerCount,
|
|
418
|
-
requestTimeout: 3e4,
|
|
419
|
-
cwd: this.#options.cwd
|
|
420
|
-
};
|
|
421
|
-
logger.info("Creating ServiceWorker pool", { entryPath, workerCount });
|
|
422
|
-
this.#workerPool = new ServiceWorkerPool(
|
|
423
|
-
poolOptions,
|
|
424
|
-
entryPath,
|
|
425
|
-
this.#cacheStorage
|
|
426
|
-
);
|
|
427
|
-
await this.#workerPool.init();
|
|
428
|
-
const workerPool = this.#workerPool;
|
|
429
|
-
const platform = this;
|
|
430
|
-
const instance = {
|
|
431
|
-
runtime: workerPool,
|
|
432
|
-
handleRequest: async (request) => {
|
|
433
|
-
if (!platform.#workerPool) {
|
|
434
|
-
throw new Error("WorkerPool not initialized");
|
|
435
|
-
}
|
|
436
|
-
return platform.#workerPool.handleRequest(request);
|
|
437
|
-
},
|
|
438
|
-
install: async () => {
|
|
439
|
-
logger.info("ServiceWorker installed", { method: "native_web_workers" });
|
|
440
|
-
},
|
|
441
|
-
activate: async () => {
|
|
442
|
-
logger.info("ServiceWorker activated", { method: "native_web_workers" });
|
|
443
|
-
},
|
|
444
|
-
get ready() {
|
|
445
|
-
return workerPool?.ready ?? false;
|
|
446
|
-
},
|
|
447
|
-
dispose: async () => {
|
|
448
|
-
if (platform.#workerPool) {
|
|
449
|
-
await platform.#workerPool.terminate();
|
|
450
|
-
platform.#workerPool = void 0;
|
|
451
|
-
}
|
|
452
|
-
logger.info("ServiceWorker disposed", {});
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
logger.info("ServiceWorker loaded", {
|
|
456
|
-
features: ["native_web_workers", "coordinated_caches"]
|
|
457
|
-
});
|
|
458
|
-
return instance;
|
|
288
|
+
async close() {
|
|
289
|
+
await this.#server?.close();
|
|
290
|
+
this.#server = void 0;
|
|
459
291
|
}
|
|
460
292
|
/**
|
|
461
293
|
* Reload workers for hot reloading (called by CLI)
|
|
462
294
|
* @param entrypoint - Path to the new entrypoint (hashed filename)
|
|
463
295
|
*/
|
|
464
296
|
async reloadWorkers(entrypoint) {
|
|
465
|
-
|
|
466
|
-
await this.#workerPool.reloadWorkers(entrypoint);
|
|
467
|
-
} else if (this.#singleThreadedRuntime) {
|
|
468
|
-
await this.#singleThreadedRuntime.load(entrypoint);
|
|
469
|
-
}
|
|
297
|
+
await this.serviceWorker.reloadWorkers(entrypoint);
|
|
470
298
|
}
|
|
471
299
|
/**
|
|
472
|
-
* Get
|
|
300
|
+
* Get production entry points for bundling.
|
|
473
301
|
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
* @param options.outDir - Output directory (required for "worker" type)
|
|
302
|
+
* Bun produces two files:
|
|
303
|
+
* - index.js: Supervisor that spawns workers and handles signals
|
|
304
|
+
* - worker.js: Worker with its own HTTP server (uses reusePort for multi-worker)
|
|
478
305
|
*
|
|
479
|
-
*
|
|
480
|
-
* -
|
|
481
|
-
* - "worker": Worker entry that sets up runtime and message loop
|
|
306
|
+
* Unlike Node.js, Bun workers each bind their own server with reusePort,
|
|
307
|
+
* allowing the OS to load-balance across workers without message passing overhead.
|
|
482
308
|
*/
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
309
|
+
getProductionEntryPoints(userEntryPath) {
|
|
310
|
+
const supervisorCode = `// Bun Production Supervisor
|
|
311
|
+
import {getLogger} from "@logtape/logtape";
|
|
312
|
+
import {configureLogging} from "@b9g/platform/runtime";
|
|
313
|
+
import BunPlatform from "@b9g/platform-bun";
|
|
314
|
+
import {config} from "shovel:config";
|
|
315
|
+
|
|
316
|
+
await configureLogging(config.logging);
|
|
317
|
+
const logger = getLogger(["shovel", "platform"]);
|
|
318
|
+
|
|
319
|
+
logger.info("Starting production server", {port: config.port, workers: config.workers});
|
|
320
|
+
|
|
321
|
+
// Initialize platform and register ServiceWorker (workers handle their own HTTP via reusePort)
|
|
322
|
+
const platform = new BunPlatform({port: config.port, host: config.host, workers: config.workers});
|
|
323
|
+
await platform.serviceWorker.register(new URL("./worker.js", import.meta.url).href);
|
|
324
|
+
await platform.serviceWorker.ready;
|
|
325
|
+
|
|
326
|
+
logger.info("All workers ready", {port: config.port, workers: config.workers});
|
|
327
|
+
|
|
328
|
+
// Graceful shutdown
|
|
329
|
+
const handleShutdown = async () => {
|
|
330
|
+
logger.info("Shutting down");
|
|
331
|
+
await platform.serviceWorker.terminate();
|
|
332
|
+
process.exit(0);
|
|
333
|
+
};
|
|
334
|
+
process.on("SIGINT", handleShutdown);
|
|
335
|
+
process.on("SIGTERM", handleShutdown);
|
|
336
|
+
`;
|
|
337
|
+
const workerCode = `// Bun Production Worker
|
|
338
|
+
import BunPlatform from "@b9g/platform-bun";
|
|
339
|
+
import {getLogger} from "@logtape/logtape";
|
|
340
|
+
import {configureLogging, initWorkerRuntime, runLifecycle, dispatchRequest} from "@b9g/platform/runtime";
|
|
341
|
+
import {config} from "shovel:config";
|
|
342
|
+
|
|
343
|
+
await configureLogging(config.logging);
|
|
344
|
+
const logger = getLogger(["shovel", "platform"]);
|
|
345
|
+
|
|
346
|
+
// Track resources for shutdown
|
|
347
|
+
let server;
|
|
348
|
+
let databases;
|
|
349
|
+
|
|
350
|
+
// Register shutdown handler before async startup
|
|
351
|
+
self.onmessage = async (event) => {
|
|
352
|
+
if (event.data.type === "shutdown") {
|
|
353
|
+
logger.info("Worker shutting down");
|
|
354
|
+
if (server) await server.close();
|
|
355
|
+
if (databases) await databases.closeAll();
|
|
356
|
+
postMessage({type: "shutdown-complete"});
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Initialize worker runtime (installs ServiceWorker globals)
|
|
361
|
+
const result = await initWorkerRuntime({config});
|
|
362
|
+
const registration = result.registration;
|
|
363
|
+
databases = result.databases;
|
|
364
|
+
|
|
365
|
+
// Import user code (registers event handlers)
|
|
366
|
+
await import("${userEntryPath}");
|
|
367
|
+
|
|
368
|
+
// Run ServiceWorker lifecycle (stage from config.lifecycle if present)
|
|
369
|
+
await runLifecycle(registration, config.lifecycle?.stage);
|
|
370
|
+
|
|
371
|
+
// Start server (skip in lifecycle-only mode)
|
|
372
|
+
if (!config.lifecycle) {
|
|
373
|
+
const platform = new BunPlatform({port: config.port, host: config.host});
|
|
374
|
+
server = platform.createServer(
|
|
375
|
+
(request) => dispatchRequest(registration, request),
|
|
376
|
+
{reusePort: config.workers > 1},
|
|
377
|
+
);
|
|
378
|
+
await server.listen();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
postMessage({type: "ready"});
|
|
382
|
+
logger.info("Worker started", {port: config.port});
|
|
383
|
+
`;
|
|
384
|
+
return {
|
|
385
|
+
index: supervisorCode,
|
|
386
|
+
worker: workerCode
|
|
387
|
+
};
|
|
488
388
|
}
|
|
489
389
|
/**
|
|
490
390
|
* Get Bun-specific esbuild configuration
|
|
@@ -535,18 +435,8 @@ var BunPlatform = class extends BasePlatform {
|
|
|
535
435
|
* Dispose of platform resources
|
|
536
436
|
*/
|
|
537
437
|
async dispose() {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
this.#singleThreadedRuntime = void 0;
|
|
541
|
-
}
|
|
542
|
-
if (this.#workerPool) {
|
|
543
|
-
await this.#workerPool.terminate();
|
|
544
|
-
this.#workerPool = void 0;
|
|
545
|
-
}
|
|
546
|
-
if (this.#cacheStorage) {
|
|
547
|
-
await this.#cacheStorage.dispose();
|
|
548
|
-
this.#cacheStorage = void 0;
|
|
549
|
-
}
|
|
438
|
+
await this.close();
|
|
439
|
+
await this.serviceWorker.terminate();
|
|
550
440
|
if (this.#databaseStorage) {
|
|
551
441
|
await this.#databaseStorage.closeAll();
|
|
552
442
|
this.#databaseStorage = void 0;
|
|
@@ -565,6 +455,7 @@ var BunPlatform = class extends BasePlatform {
|
|
|
565
455
|
var src_default = BunPlatform;
|
|
566
456
|
export {
|
|
567
457
|
BunPlatform,
|
|
458
|
+
BunServiceWorkerContainer,
|
|
568
459
|
MemoryCache2 as DefaultCache,
|
|
569
460
|
src_default as default
|
|
570
461
|
};
|