@b9g/platform 0.1.1 → 0.1.3

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.
@@ -0,0 +1,271 @@
1
+ /// <reference types="./worker-pool.d.ts" />
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined")
6
+ return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
9
+
10
+ // src/worker-pool.ts
11
+ import * as Path from "path";
12
+ function resolveWorkerScript(entrypoint) {
13
+ if (entrypoint) {
14
+ const entryDir = Path.dirname(entrypoint);
15
+ const bundledWorker = Path.join(entryDir, "worker.js");
16
+ try {
17
+ if (typeof Bun !== "undefined") {
18
+ const file = Bun.file(bundledWorker);
19
+ if (file.size > 0) {
20
+ console.debug(`[WorkerPool] Using bundled worker: ${bundledWorker}`);
21
+ return bundledWorker;
22
+ }
23
+ } else if (typeof __require !== "undefined") {
24
+ const fs = __require("fs");
25
+ if (fs.existsSync(bundledWorker)) {
26
+ console.debug(`[WorkerPool] Using bundled worker: ${bundledWorker}`);
27
+ return bundledWorker;
28
+ }
29
+ }
30
+ } catch {
31
+ }
32
+ }
33
+ try {
34
+ const workerUrl = import.meta.resolve("@b9g/platform/worker-web.js");
35
+ let workerScript;
36
+ if (workerUrl.startsWith("file://")) {
37
+ workerScript = workerUrl.slice(7);
38
+ } else {
39
+ workerScript = workerUrl;
40
+ }
41
+ console.debug(`[WorkerPool] Using Web Worker-compatible script: ${workerScript}`);
42
+ return workerScript;
43
+ } catch (error) {
44
+ const bundledPath = entrypoint ? Path.join(Path.dirname(entrypoint), "worker-web.js") : "worker-web.js";
45
+ throw new Error(
46
+ `Could not resolve worker-web.js. Checked bundled path: ${bundledPath} and package: @b9g/platform/worker-web.js. Error: ${error instanceof Error ? error.message : String(error)}`
47
+ );
48
+ }
49
+ }
50
+ async function createWebWorker(workerScript) {
51
+ if (typeof Worker !== "undefined") {
52
+ return new Worker(workerScript, { type: "module" });
53
+ }
54
+ const isNodeJs = typeof process !== "undefined" && process.versions?.node;
55
+ if (isNodeJs) {
56
+ try {
57
+ const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
58
+ console.debug("[WorkerPool] Using @b9g/node-webworker shim for Node.js");
59
+ return new NodeWebWorker(workerScript, { type: "module" });
60
+ } catch (shimError) {
61
+ console.error("\n\u274C MISSING WEB STANDARD: Node.js lacks native Web Worker support");
62
+ console.error("\u{1F517} CANONICAL ISSUE: https://github.com/nodejs/node/issues/43583");
63
+ console.error("\u{1F4AC} This is a basic web standard from 2009 - help push for implementation!");
64
+ console.error("\u{1F5F3}\uFE0F Please \u{1F44D} react and comment on the issue to show demand\n");
65
+ throw new Error(`\u274C Web Worker not available on Node.js
66
+
67
+ \u{1F517} Node.js doesn't implement the Web Worker standard yet.
68
+ CANONICAL ISSUE: https://github.com/nodejs/node/issues/43583
69
+
70
+ \u{1F5F3}\uFE0F Please \u{1F44D} react and comment to show demand for this basic web standard!
71
+
72
+ \u{1F4A1} Immediate workaround:
73
+ npm install @b9g/node-webworker
74
+
75
+ This installs our minimal, reliable Web Worker shim for Node.js.
76
+
77
+ \u{1F4DA} Learn more: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
78
+ }
79
+ }
80
+ const runtime = typeof Bun !== "undefined" ? "Bun" : typeof Deno !== "undefined" ? "Deno" : "Unknown";
81
+ throw new Error(`\u274C Web Worker not available on ${runtime}
82
+
83
+ This runtime should support Web Workers but the API is not available.
84
+ Please check your runtime version and configuration.
85
+
86
+ \u{1F4DA} Web Worker standard: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
87
+ }
88
+ var WorkerPool = class {
89
+ workers = [];
90
+ currentWorker = 0;
91
+ requestId = 0;
92
+ pendingRequests = /* @__PURE__ */ new Map();
93
+ options;
94
+ cacheStorage;
95
+ appEntrypoint;
96
+ constructor(cacheStorage, options = {}, appEntrypoint) {
97
+ this.cacheStorage = cacheStorage;
98
+ this.appEntrypoint = appEntrypoint;
99
+ this.options = {
100
+ workerCount: 1,
101
+ requestTimeout: 3e4,
102
+ hotReload: process.env.NODE_ENV !== "production",
103
+ cwd: process.cwd(),
104
+ ...options
105
+ };
106
+ }
107
+ /**
108
+ * Initialize workers (must be called after construction)
109
+ */
110
+ async init() {
111
+ await this.initWorkers();
112
+ }
113
+ async initWorkers() {
114
+ for (let i = 0; i < this.options.workerCount; i++) {
115
+ await this.createWorker();
116
+ }
117
+ }
118
+ async createWorker() {
119
+ const workerScript = resolveWorkerScript(this.appEntrypoint);
120
+ const worker = await createWebWorker(workerScript);
121
+ worker.addEventListener("message", (event) => {
122
+ this.handleWorkerMessage(event.data || event);
123
+ });
124
+ worker.addEventListener("error", (error) => {
125
+ console.error("[WorkerPool] Worker error:", error);
126
+ });
127
+ this.workers.push(worker);
128
+ return worker;
129
+ }
130
+ handleWorkerMessage(message) {
131
+ if (message.type?.startsWith("cache:")) {
132
+ this.handleCacheMessage(message);
133
+ return;
134
+ }
135
+ switch (message.type) {
136
+ case "response":
137
+ this.handleResponse(message);
138
+ break;
139
+ case "error":
140
+ this.handleError(message);
141
+ break;
142
+ case "ready":
143
+ case "worker-ready":
144
+ this.handleReady(message);
145
+ break;
146
+ default:
147
+ console.warn("[WorkerPool] Unknown message type:", message.type);
148
+ }
149
+ }
150
+ handleResponse(message) {
151
+ const pending = this.pendingRequests.get(message.requestId);
152
+ if (pending) {
153
+ const response = new Response(message.response.body, {
154
+ status: message.response.status,
155
+ statusText: message.response.statusText,
156
+ headers: message.response.headers
157
+ });
158
+ pending.resolve(response);
159
+ this.pendingRequests.delete(message.requestId);
160
+ }
161
+ }
162
+ handleError(message) {
163
+ if (message.requestId) {
164
+ const pending = this.pendingRequests.get(message.requestId);
165
+ if (pending) {
166
+ pending.reject(new Error(message.error));
167
+ this.pendingRequests.delete(message.requestId);
168
+ }
169
+ } else {
170
+ console.error("[WorkerPool] Worker error:", message.error);
171
+ }
172
+ }
173
+ handleReady(message) {
174
+ if (message.type === "ready") {
175
+ console.info(`[WorkerPool] ServiceWorker ready (v${message.version})`);
176
+ } else if (message.type === "worker-ready") {
177
+ console.info("[WorkerPool] Worker initialized");
178
+ }
179
+ }
180
+ /**
181
+ * Platform-specific cache message handling
182
+ * Override this method in platform implementations for custom cache coordination
183
+ */
184
+ handleCacheMessage(message) {
185
+ }
186
+ /**
187
+ * Handle HTTP request using round-robin worker selection
188
+ */
189
+ async handleRequest(request) {
190
+ const worker = this.workers[this.currentWorker];
191
+ console.info(
192
+ `[WorkerPool] Dispatching to worker ${this.currentWorker + 1} of ${this.workers.length}`
193
+ );
194
+ this.currentWorker = (this.currentWorker + 1) % this.workers.length;
195
+ const requestId = ++this.requestId;
196
+ return new Promise((resolve, reject) => {
197
+ this.pendingRequests.set(requestId, { resolve, reject });
198
+ const workerRequest = {
199
+ type: "request",
200
+ request: {
201
+ url: request.url,
202
+ method: request.method,
203
+ headers: Object.fromEntries(request.headers.entries()),
204
+ body: request.body
205
+ },
206
+ requestId
207
+ };
208
+ worker.postMessage(workerRequest);
209
+ setTimeout(() => {
210
+ if (this.pendingRequests.has(requestId)) {
211
+ this.pendingRequests.delete(requestId);
212
+ reject(new Error("Request timeout"));
213
+ }
214
+ }, this.options.requestTimeout);
215
+ });
216
+ }
217
+ /**
218
+ * Reload ServiceWorker with new version (hot reload simulation)
219
+ */
220
+ async reloadWorkers(version = Date.now()) {
221
+ console.info(`[WorkerPool] Reloading ServiceWorker (v${version})`);
222
+ const loadPromises = this.workers.map((worker) => {
223
+ return new Promise((resolve) => {
224
+ const handleReady = (event) => {
225
+ const message = event.data || event;
226
+ if (message.type === "ready" && message.version === version) {
227
+ worker.removeEventListener("message", handleReady);
228
+ resolve();
229
+ }
230
+ };
231
+ console.info("[WorkerPool] Sending load message:", {
232
+ version,
233
+ entrypoint: this.appEntrypoint
234
+ });
235
+ worker.addEventListener("message", handleReady);
236
+ const loadMessage = {
237
+ type: "load",
238
+ version,
239
+ entrypoint: this.appEntrypoint
240
+ };
241
+ worker.postMessage(loadMessage);
242
+ });
243
+ });
244
+ await Promise.all(loadPromises);
245
+ console.info(`[WorkerPool] All workers reloaded (v${version})`);
246
+ }
247
+ /**
248
+ * Graceful shutdown of all workers
249
+ */
250
+ async terminate() {
251
+ const terminatePromises = this.workers.map((worker) => worker.terminate());
252
+ await Promise.allSettled(terminatePromises);
253
+ this.workers = [];
254
+ this.pendingRequests.clear();
255
+ }
256
+ /**
257
+ * Get the number of active workers
258
+ */
259
+ get workerCount() {
260
+ return this.workers.length;
261
+ }
262
+ /**
263
+ * Check if the pool is ready to handle requests
264
+ */
265
+ get ready() {
266
+ return this.workers.length > 0;
267
+ }
268
+ };
269
+ export {
270
+ WorkerPool
271
+ };
@@ -0,0 +1,119 @@
1
+ // src/worker-web.js
2
+ async function initializeWorker() {
3
+ let messagePort = self;
4
+ let sendMessage2 = (message) => postMessage(message);
5
+ onmessage = function(event) {
6
+ handleMessage(event.data);
7
+ };
8
+ return { messagePort, sendMessage: sendMessage2 };
9
+ }
10
+ var { createServiceWorkerGlobals, ServiceWorkerRuntime, createBucketStorage } = await import("./index.js");
11
+ var { CustomCacheStorage, PostMessageCache } = await import("@b9g/cache");
12
+ var caches = new CustomCacheStorage((name) => {
13
+ return new PostMessageCache(name, {
14
+ maxEntries: 1e3,
15
+ maxAge: 60 * 60 * 1e3
16
+ // 1 hour
17
+ });
18
+ });
19
+ var buckets = createBucketStorage(process.cwd() + "/dist");
20
+ var runtime = new ServiceWorkerRuntime();
21
+ createServiceWorkerGlobals(runtime, { caches, buckets });
22
+ var workerSelf = runtime;
23
+ var currentApp = null;
24
+ var serviceWorkerReady = false;
25
+ var loadedVersion = null;
26
+ async function handleFetchEvent(request) {
27
+ if (!currentApp || !serviceWorkerReady) {
28
+ throw new Error("ServiceWorker not ready");
29
+ }
30
+ try {
31
+ const response = await runtime.handleRequest(request);
32
+ return response;
33
+ } catch (error) {
34
+ console.error("[Worker] ServiceWorker request failed:", error);
35
+ const response = new Response("ServiceWorker request failed", {
36
+ status: 500
37
+ });
38
+ return response;
39
+ }
40
+ }
41
+ async function loadServiceWorker(version, entrypoint) {
42
+ try {
43
+ console.info("[Worker] loadServiceWorker called with:", { version, entrypoint });
44
+ const entrypointPath = process.env.SERVICEWORKER_PATH || entrypoint || `${process.cwd()}/dist/server/app.js`;
45
+ console.info("[Worker] Loading from:", entrypointPath);
46
+ if (loadedVersion !== null && loadedVersion !== version) {
47
+ console.info(`[Worker] Hot reload detected: ${loadedVersion} -> ${version}`);
48
+ console.info("[Worker] Creating completely fresh ServiceWorker context");
49
+ runtime = new ServiceWorkerRuntime();
50
+ createServiceWorkerGlobals(runtime, { caches, buckets });
51
+ workerSelf = runtime;
52
+ currentApp = null;
53
+ serviceWorkerReady = false;
54
+ }
55
+ if (loadedVersion === version) {
56
+ console.info("[Worker] ServiceWorker already loaded for version", version);
57
+ return;
58
+ }
59
+ globalThis.self = runtime;
60
+ globalThis.addEventListener = runtime.addEventListener.bind(runtime);
61
+ globalThis.removeEventListener = runtime.removeEventListener.bind(runtime);
62
+ globalThis.dispatchEvent = runtime.dispatchEvent.bind(runtime);
63
+ const appModule = await import(`${entrypointPath}?v=${version}`);
64
+ loadedVersion = version;
65
+ currentApp = appModule;
66
+ await runtime.install();
67
+ await runtime.activate();
68
+ serviceWorkerReady = true;
69
+ console.info(`[Worker] ServiceWorker loaded and activated (v${version}) from ${entrypointPath}`);
70
+ } catch (error) {
71
+ console.error("[Worker] Failed to load ServiceWorker:", error);
72
+ serviceWorkerReady = false;
73
+ throw error;
74
+ }
75
+ }
76
+ var workerId = Math.random().toString(36).substring(2, 8);
77
+ var sendMessage;
78
+ async function handleMessage(message) {
79
+ try {
80
+ if (message.type === "load") {
81
+ await loadServiceWorker(message.version, message.entrypoint);
82
+ sendMessage({ type: "ready", version: message.version });
83
+ } else if (message.type === "request") {
84
+ console.log(`[Worker-${workerId}] Handling request:`, message.request.url);
85
+ const request = new Request(message.request.url, {
86
+ method: message.request.method,
87
+ headers: message.request.headers,
88
+ body: message.request.body
89
+ });
90
+ const response = await handleFetchEvent(request);
91
+ sendMessage({
92
+ type: "response",
93
+ response: {
94
+ status: response.status,
95
+ statusText: response.statusText,
96
+ headers: Object.fromEntries(response.headers.entries()),
97
+ body: await response.text()
98
+ },
99
+ requestId: message.requestId
100
+ });
101
+ } else if (message.type.startsWith("cache:") || message.type.startsWith("cachestorage:")) {
102
+ } else {
103
+ console.warn("[Worker] Unknown message type:", message.type);
104
+ }
105
+ } catch (error) {
106
+ sendMessage({
107
+ type: "error",
108
+ error: error.message,
109
+ stack: error.stack,
110
+ requestId: message.requestId
111
+ });
112
+ }
113
+ }
114
+ initializeWorker().then(({ messagePort, sendMessage: send }) => {
115
+ sendMessage = send;
116
+ sendMessage({ type: "worker-ready" });
117
+ }).catch((error) => {
118
+ console.error("[Worker] Failed to initialize:", error);
119
+ });