@b9g/platform 0.1.10 → 0.1.12

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/src/index.js CHANGED
@@ -1,41 +1,21 @@
1
1
  /// <reference types="./index.d.ts" />
2
+ import {
3
+ __require
4
+ } from "../chunk-P57PW2II.js";
5
+
2
6
  // src/index.ts
3
7
  import * as Path from "path";
4
- import { readFileSync } from "fs";
8
+ import { existsSync } from "fs";
9
+ import { fileURLToPath } from "url";
5
10
  import { CustomCacheStorage } from "@b9g/cache";
6
11
  import { MemoryCache } from "@b9g/cache/memory";
12
+ import { getLogger } from "@logtape/logtape";
7
13
  import {
8
- ServiceWorkerPool
9
- } from "./worker-pool.js";
10
- import {
11
- SingleThreadedRuntime
12
- } from "./single-threaded.js";
13
- import {
14
+ ServiceWorkerGlobals,
14
15
  ShovelServiceWorkerRegistration,
15
- ShovelGlobalScope,
16
- FetchEvent,
17
- InstallEvent,
18
- ActivateEvent,
19
- ExtendableEvent
16
+ CustomLoggerStorage
20
17
  } from "./runtime.js";
21
- import {
22
- RequestCookieStore,
23
- parseCookieHeader,
24
- serializeCookie,
25
- parseSetCookieHeader
26
- } from "./cookie-store.js";
27
- import { CustomBucketStorage } from "@b9g/filesystem";
28
- import {
29
- loadConfig,
30
- configureLogging,
31
- getCacheConfig,
32
- getBucketConfig,
33
- parseConfigExpr,
34
- processConfigValue,
35
- matchPattern,
36
- createBucketFactory,
37
- createCacheFactory
38
- } from "./config.js";
18
+ var logger = getLogger(["platform"]);
39
19
  function detectRuntime() {
40
20
  if (typeof Bun !== "undefined" || process.versions?.bun) {
41
21
  return "bun";
@@ -45,50 +25,6 @@ function detectRuntime() {
45
25
  }
46
26
  return "node";
47
27
  }
48
- function detectPlatformFromPackageJSON(cwd) {
49
- if (!cwd && typeof process === "undefined") {
50
- return null;
51
- }
52
- try {
53
- const pkgPath = Path.join(cwd || process.cwd(), "package.json");
54
- const pkgContent = readFileSync(pkgPath, "utf8");
55
- const pkg = JSON.parse(pkgContent);
56
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
57
- return selectPlatformFromDeps(deps);
58
- } catch {
59
- return null;
60
- }
61
- }
62
- function selectPlatformFromDeps(deps) {
63
- const hasBun = deps["@b9g/platform-bun"];
64
- const hasNode = deps["@b9g/platform-node"];
65
- const hasCloudflare = deps["@b9g/platform-cloudflare"];
66
- const installedCount = [hasBun, hasNode, hasCloudflare].filter(
67
- Boolean
68
- ).length;
69
- if (installedCount === 0)
70
- return null;
71
- if (installedCount === 1) {
72
- if (hasBun)
73
- return "bun";
74
- if (hasNode)
75
- return "node";
76
- if (hasCloudflare)
77
- return "cloudflare";
78
- }
79
- const runtime = detectRuntime();
80
- if (runtime === "bun" && hasBun)
81
- return "bun";
82
- if (runtime === "node" && hasNode)
83
- return "node";
84
- if (hasBun)
85
- return "bun";
86
- if (hasNode)
87
- return "node";
88
- if (hasCloudflare)
89
- return "cloudflare";
90
- return null;
91
- }
92
28
  function detectDeploymentPlatform() {
93
29
  if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
94
30
  return null;
@@ -100,10 +36,6 @@ function detectDeploymentPlatform() {
100
36
  return null;
101
37
  }
102
38
  function detectDevelopmentPlatform() {
103
- const pkgPlatform = detectPlatformFromPackageJSON();
104
- if (pkgPlatform) {
105
- return pkgPlatform;
106
- }
107
39
  const runtime = detectRuntime();
108
40
  switch (runtime) {
109
41
  case "bun":
@@ -122,6 +54,9 @@ function resolvePlatform(options) {
122
54
  if (options.target) {
123
55
  return options.target;
124
56
  }
57
+ if (options.config?.platform) {
58
+ return options.config.platform;
59
+ }
125
60
  const deploymentPlatform = detectDeploymentPlatform();
126
61
  if (deploymentPlatform) {
127
62
  return deploymentPlatform;
@@ -226,36 +161,494 @@ async function getPlatformAsync(name) {
226
161
  }
227
162
  return platform;
228
163
  }
164
+ var SingleThreadedRuntime = class {
165
+ #registration;
166
+ #scope;
167
+ #ready;
168
+ #entrypoint;
169
+ constructor(options) {
170
+ this.#ready = false;
171
+ this.#registration = new ShovelServiceWorkerRegistration();
172
+ this.#scope = new ServiceWorkerGlobals({
173
+ registration: this.#registration,
174
+ caches: options.caches,
175
+ directories: options.directories,
176
+ loggers: options.loggers
177
+ });
178
+ logger.info("SingleThreadedRuntime created");
179
+ }
180
+ /**
181
+ * Initialize the runtime (install ServiceWorker globals)
182
+ */
183
+ async init() {
184
+ this.#scope.install();
185
+ logger.info("SingleThreadedRuntime initialized - globals installed");
186
+ }
187
+ /**
188
+ * Load (or reload) a ServiceWorker entrypoint
189
+ * @param entrypoint - Path to the entrypoint file (content-hashed filename)
190
+ */
191
+ async load(entrypoint) {
192
+ const isReload = this.#entrypoint !== void 0;
193
+ if (isReload) {
194
+ logger.info("Reloading ServiceWorker", {
195
+ oldEntrypoint: this.#entrypoint,
196
+ newEntrypoint: entrypoint
197
+ });
198
+ this.#registration._serviceWorker._setState("parsed");
199
+ } else {
200
+ logger.info("Loading ServiceWorker entrypoint", { entrypoint });
201
+ }
202
+ this.#entrypoint = entrypoint;
203
+ this.#ready = false;
204
+ await import(entrypoint);
205
+ await this.#registration.install();
206
+ await this.#registration.activate();
207
+ this.#ready = true;
208
+ logger.info("ServiceWorker loaded and activated", { entrypoint });
209
+ }
210
+ /**
211
+ * Handle an HTTP request
212
+ * This is the key method - direct call, no postMessage!
213
+ */
214
+ async handleRequest(request) {
215
+ if (!this.#ready) {
216
+ throw new Error(
217
+ "SingleThreadedRuntime not ready - ServiceWorker not loaded"
218
+ );
219
+ }
220
+ return this.#registration.handleRequest(request);
221
+ }
222
+ /**
223
+ * Graceful shutdown
224
+ */
225
+ async terminate() {
226
+ this.#ready = false;
227
+ logger.info("SingleThreadedRuntime terminated");
228
+ }
229
+ /**
230
+ * Get the number of workers (always 1 for single-threaded)
231
+ */
232
+ get workerCount() {
233
+ return 1;
234
+ }
235
+ /**
236
+ * Check if ready to handle requests
237
+ */
238
+ get ready() {
239
+ return this.#ready;
240
+ }
241
+ };
242
+ function resolveWorkerScript(entrypoint) {
243
+ if (entrypoint) {
244
+ const entryDir = Path.dirname(entrypoint);
245
+ const bundledWorker = Path.join(entryDir, "worker.js");
246
+ try {
247
+ if (typeof Bun !== "undefined") {
248
+ const file = Bun.file(bundledWorker);
249
+ if (file.size > 0) {
250
+ logger.info("Using bundled worker", { bundledWorker });
251
+ return bundledWorker;
252
+ }
253
+ } else if (typeof __require !== "undefined") {
254
+ if (existsSync(bundledWorker)) {
255
+ logger.info("Using bundled worker", { bundledWorker });
256
+ return bundledWorker;
257
+ }
258
+ }
259
+ } catch (err) {
260
+ if (err.code !== "ENOENT") {
261
+ throw err;
262
+ }
263
+ }
264
+ }
265
+ try {
266
+ const workerURL = import.meta.resolve("@b9g/platform/worker.js");
267
+ let workerScript;
268
+ if (workerURL.startsWith("file://")) {
269
+ workerScript = fileURLToPath(workerURL);
270
+ } else {
271
+ workerScript = workerURL;
272
+ }
273
+ logger.info("Using worker entry script", { workerScript });
274
+ return workerScript;
275
+ } catch (error) {
276
+ const bundledPath = entrypoint ? Path.join(Path.dirname(entrypoint), "worker.js") : "worker.js";
277
+ throw new Error(
278
+ `Could not resolve worker.js. Checked bundled path: ${bundledPath} and package: @b9g/platform/worker.js. Error: ${error instanceof Error ? error.message : String(error)}`
279
+ );
280
+ }
281
+ }
282
+ async function createWebWorker(workerScript) {
283
+ if (typeof Worker !== "undefined") {
284
+ return new Worker(workerScript, { type: "module" });
285
+ }
286
+ const isNodeJs = typeof process !== "undefined" && process.versions?.node;
287
+ if (isNodeJs) {
288
+ try {
289
+ const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
290
+ logger.info("Using @b9g/node-webworker shim for Node.js", {});
291
+ return new NodeWebWorker(workerScript, {
292
+ type: "module"
293
+ });
294
+ } catch (shimError) {
295
+ logger.error(
296
+ "MISSING WEB STANDARD: Node.js lacks native Web Worker support",
297
+ {
298
+ canonicalIssue: "https://github.com/nodejs/node/issues/43583",
299
+ message: "This is a basic web standard from 2009 - help push for implementation!"
300
+ }
301
+ );
302
+ throw new Error(`\u274C Web Worker not available on Node.js
303
+
304
+ \u{1F517} Node.js doesn't implement the Web Worker standard yet.
305
+ CANONICAL ISSUE: https://github.com/nodejs/node/issues/43583
306
+
307
+ \u{1F5F3}\uFE0F Please \u{1F44D} react and comment to show demand for this basic web standard!
308
+
309
+ \u{1F4A1} Immediate workaround:
310
+ npm install @b9g/node-webworker
311
+
312
+ This installs our minimal, reliable Web Worker shim for Node.js.
313
+
314
+ \u{1F4DA} Learn more: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
315
+ }
316
+ }
317
+ const runtime = typeof Bun !== "undefined" ? "Bun" : typeof Deno !== "undefined" ? "Deno" : "Unknown";
318
+ throw new Error(`\u274C Web Worker not available on ${runtime}
319
+
320
+ This runtime should support Web Workers but the API is not available.
321
+ Please check your runtime version and configuration.
322
+
323
+ \u{1F4DA} Web Worker standard: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
324
+ }
325
+ var ServiceWorkerPool = class {
326
+ #workers;
327
+ #currentWorker;
328
+ #requestID;
329
+ #pendingRequests;
330
+ #pendingWorkerInit;
331
+ #options;
332
+ #appEntrypoint;
333
+ #cacheStorage;
334
+ // CustomCacheStorage for cache coordination
335
+ #config;
336
+ // ShovelConfig from config.ts
337
+ constructor(options = {}, appEntrypoint, cacheStorage, config) {
338
+ this.#workers = [];
339
+ this.#currentWorker = 0;
340
+ this.#requestID = 0;
341
+ this.#pendingRequests = /* @__PURE__ */ new Map();
342
+ this.#pendingWorkerInit = /* @__PURE__ */ new Map();
343
+ this.#appEntrypoint = appEntrypoint;
344
+ this.#cacheStorage = cacheStorage;
345
+ this.#config = config || {};
346
+ this.#options = {
347
+ workerCount: 1,
348
+ requestTimeout: 3e4,
349
+ ...options
350
+ };
351
+ }
352
+ /**
353
+ * Initialize workers (must be called after construction)
354
+ */
355
+ async init() {
356
+ await this.#initWorkers();
357
+ }
358
+ async #initWorkers() {
359
+ for (let i = 0; i < this.#options.workerCount; i++) {
360
+ await this.#createWorker();
361
+ }
362
+ }
363
+ async #createWorker() {
364
+ const workerScript = resolveWorkerScript(this.#appEntrypoint);
365
+ const worker = await createWebWorker(workerScript);
366
+ let rejectWorkerReady;
367
+ let workerReadyTimeoutId;
368
+ const workerReadyPromise = new Promise((resolve, reject) => {
369
+ rejectWorkerReady = reject;
370
+ workerReadyTimeoutId = setTimeout(() => {
371
+ reject(new Error("Worker failed to send ready signal within 30000ms"));
372
+ }, 3e4);
373
+ this.#pendingWorkerInit.set(worker, {
374
+ workerReady: () => {
375
+ clearTimeout(workerReadyTimeoutId);
376
+ resolve();
377
+ }
378
+ });
379
+ });
380
+ worker.addEventListener("message", (event) => {
381
+ this.#handleWorkerMessage(worker, event.data || event);
382
+ });
383
+ worker.addEventListener("error", (event) => {
384
+ const errorMessage = event.message || event.error?.message || "Unknown worker error";
385
+ const error = new Error(`Worker failed to start: ${errorMessage}`);
386
+ logger.error("Worker error: {error}", {
387
+ error: event.error || errorMessage,
388
+ filename: event.filename,
389
+ lineno: event.lineno,
390
+ colno: event.colno
391
+ });
392
+ clearTimeout(workerReadyTimeoutId);
393
+ rejectWorkerReady(error);
394
+ });
395
+ this.#workers.push(worker);
396
+ logger.info("Waiting for worker-ready signal");
397
+ await workerReadyPromise;
398
+ logger.info("Received worker-ready signal");
399
+ const initializedPromise = new Promise((resolve, reject) => {
400
+ const timeoutId = setTimeout(() => {
401
+ reject(
402
+ new Error("Worker failed to send initialized signal within 30000ms")
403
+ );
404
+ }, 3e4);
405
+ const pending = this.#pendingWorkerInit.get(worker) || {};
406
+ pending.initialized = () => {
407
+ clearTimeout(timeoutId);
408
+ resolve();
409
+ };
410
+ this.#pendingWorkerInit.set(worker, pending);
411
+ });
412
+ if (!this.#appEntrypoint) {
413
+ throw new Error(
414
+ "ServiceWorkerPool requires an entrypoint to derive baseDir"
415
+ );
416
+ }
417
+ const baseDir = Path.dirname(this.#appEntrypoint);
418
+ const initMessage = {
419
+ type: "init",
420
+ config: this.#config,
421
+ baseDir
422
+ };
423
+ logger.info("Sending init message", { config: this.#config, baseDir });
424
+ worker.postMessage(initMessage);
425
+ logger.info("Sent init message, waiting for initialized response");
426
+ await initializedPromise;
427
+ logger.info("Received initialized response");
428
+ this.#pendingWorkerInit.delete(worker);
429
+ return worker;
430
+ }
431
+ #handleWorkerMessage(worker, message) {
432
+ logger.debug("Worker message received", { type: message.type });
433
+ const pending = this.#pendingWorkerInit.get(worker);
434
+ if (message.type === "worker-ready" && pending?.workerReady) {
435
+ pending.workerReady();
436
+ } else if (message.type === "initialized" && pending?.initialized) {
437
+ pending.initialized();
438
+ return;
439
+ }
440
+ switch (message.type) {
441
+ case "response":
442
+ this.#handleResponse(message);
443
+ break;
444
+ case "error":
445
+ this.#handleError(message);
446
+ break;
447
+ case "ready":
448
+ case "worker-ready":
449
+ this.#handleReady(message);
450
+ break;
451
+ case "initialized":
452
+ break;
453
+ default:
454
+ if (message.type?.startsWith("cache:")) {
455
+ logger.debug("Cache message received", { type: message.type });
456
+ if (this.#cacheStorage) {
457
+ const storage = this.#cacheStorage;
458
+ if (typeof storage.handleMessage === "function") {
459
+ storage.handleMessage(worker, message).catch((err) => {
460
+ logger.error("Cache message handling failed: {error}", {
461
+ error: err
462
+ });
463
+ });
464
+ }
465
+ }
466
+ }
467
+ break;
468
+ }
469
+ }
470
+ #handleResponse(message) {
471
+ const pending = this.#pendingRequests.get(message.requestID);
472
+ if (pending) {
473
+ if (pending.timeoutId) {
474
+ clearTimeout(pending.timeoutId);
475
+ }
476
+ const response = new Response(message.response.body, {
477
+ status: message.response.status,
478
+ statusText: message.response.statusText,
479
+ headers: message.response.headers
480
+ });
481
+ pending.resolve(response);
482
+ this.#pendingRequests.delete(message.requestID);
483
+ }
484
+ }
485
+ #handleError(message) {
486
+ logger.error("Worker error message received: {error}", {
487
+ error: message.error,
488
+ stack: message.stack,
489
+ requestID: message.requestID
490
+ });
491
+ if (message.requestID) {
492
+ const pending = this.#pendingRequests.get(message.requestID);
493
+ if (pending) {
494
+ if (pending.timeoutId) {
495
+ clearTimeout(pending.timeoutId);
496
+ }
497
+ pending.reject(new Error(message.error));
498
+ this.#pendingRequests.delete(message.requestID);
499
+ }
500
+ } else {
501
+ logger.error("Worker error: {error}", { error: message.error });
502
+ }
503
+ }
504
+ #handleReady(message) {
505
+ if (message.type === "ready") {
506
+ logger.info("ServiceWorker ready", { entrypoint: message.entrypoint });
507
+ } else if (message.type === "worker-ready") {
508
+ logger.info("Worker initialized", {});
509
+ }
510
+ }
511
+ /**
512
+ * Handle HTTP request using round-robin worker selection
513
+ */
514
+ async handleRequest(request) {
515
+ const worker = this.#workers[this.#currentWorker];
516
+ logger.info("Dispatching to worker", {
517
+ workerIndex: this.#currentWorker + 1,
518
+ totalWorkers: this.#workers.length
519
+ });
520
+ this.#currentWorker = (this.#currentWorker + 1) % this.#workers.length;
521
+ const requestID = ++this.#requestID;
522
+ return new Promise((resolve, reject) => {
523
+ const timeoutId = setTimeout(() => {
524
+ if (this.#pendingRequests.has(requestID)) {
525
+ this.#pendingRequests.delete(requestID);
526
+ reject(new Error("Request timeout"));
527
+ }
528
+ }, this.#options.requestTimeout);
529
+ this.#pendingRequests.set(requestID, { resolve, reject, timeoutId });
530
+ this.#sendRequest(worker, request, requestID).catch(reject);
531
+ });
532
+ }
533
+ /**
534
+ * Send request to worker (async helper to avoid async promise executor)
535
+ */
536
+ async #sendRequest(worker, request, requestID) {
537
+ let body = null;
538
+ if (request.body) {
539
+ body = await request.arrayBuffer();
540
+ }
541
+ const workerRequest = {
542
+ type: "request",
543
+ request: {
544
+ url: request.url,
545
+ method: request.method,
546
+ headers: Object.fromEntries(request.headers.entries()),
547
+ body
548
+ },
549
+ requestID
550
+ };
551
+ if (body) {
552
+ worker.postMessage(workerRequest, [body]);
553
+ } else {
554
+ worker.postMessage(workerRequest);
555
+ }
556
+ }
557
+ /**
558
+ * Reload ServiceWorker with new entrypoint (hot reload)
559
+ * The entrypoint path contains a content hash for cache busting
560
+ */
561
+ async reloadWorkers(entrypoint) {
562
+ logger.info("Reloading ServiceWorker", { entrypoint });
563
+ this.#appEntrypoint = entrypoint;
564
+ const loadPromises = this.#workers.map((worker) => {
565
+ return new Promise((resolve, reject) => {
566
+ let timeoutId;
567
+ const cleanup = () => {
568
+ worker.removeEventListener("message", handleReady);
569
+ worker.removeEventListener("error", handleError);
570
+ if (timeoutId) {
571
+ clearTimeout(timeoutId);
572
+ }
573
+ };
574
+ const handleReady = (event) => {
575
+ const message = event.data || event;
576
+ if (message.type === "ready" && message.entrypoint === entrypoint) {
577
+ cleanup();
578
+ resolve();
579
+ } else if (message.type === "error") {
580
+ cleanup();
581
+ reject(
582
+ new Error(
583
+ `Worker failed to load ServiceWorker: ${message.error}`
584
+ )
585
+ );
586
+ }
587
+ };
588
+ const handleError = (error) => {
589
+ cleanup();
590
+ const errorMsg = error?.error?.message || error?.message || JSON.stringify(error);
591
+ reject(new Error(`Worker failed to load ServiceWorker: ${errorMsg}`));
592
+ };
593
+ timeoutId = setTimeout(() => {
594
+ cleanup();
595
+ reject(
596
+ new Error(
597
+ `Worker failed to load ServiceWorker within 30000ms (entrypoint ${entrypoint})`
598
+ )
599
+ );
600
+ }, 3e4);
601
+ logger.info("Sending load message", {
602
+ entrypoint
603
+ });
604
+ worker.addEventListener("message", handleReady);
605
+ worker.addEventListener("error", handleError);
606
+ const loadMessage = {
607
+ type: "load",
608
+ entrypoint
609
+ };
610
+ logger.debug("[WorkerPool] Sending load message", {
611
+ entrypoint
612
+ });
613
+ worker.postMessage(loadMessage);
614
+ });
615
+ });
616
+ await Promise.all(loadPromises);
617
+ logger.info("All workers reloaded", { entrypoint });
618
+ }
619
+ /**
620
+ * Graceful shutdown of all workers
621
+ */
622
+ async terminate() {
623
+ const terminatePromises = this.#workers.map((worker) => worker.terminate());
624
+ await Promise.allSettled(terminatePromises);
625
+ this.#workers = [];
626
+ this.#pendingRequests.clear();
627
+ }
628
+ /**
629
+ * Get the number of active workers
630
+ */
631
+ get workerCount() {
632
+ return this.#workers.length;
633
+ }
634
+ /**
635
+ * Check if the pool is ready to handle requests
636
+ */
637
+ get ready() {
638
+ return this.#workers.length > 0;
639
+ }
640
+ };
229
641
  export {
230
- ActivateEvent,
231
642
  BasePlatform,
232
- CustomBucketStorage,
233
- ExtendableEvent,
234
- FetchEvent,
235
- InstallEvent,
236
- RequestCookieStore,
643
+ CustomLoggerStorage,
237
644
  ServiceWorkerPool,
238
- ShovelGlobalScope,
239
- ShovelServiceWorkerRegistration,
240
645
  SingleThreadedRuntime,
241
- configureLogging,
242
- createBucketFactory,
243
- createCacheFactory,
244
646
  createPlatform,
245
647
  detectDeploymentPlatform,
246
648
  detectDevelopmentPlatform,
247
649
  detectRuntime,
248
- getBucketConfig,
249
- getCacheConfig,
250
650
  getPlatform,
251
651
  getPlatformAsync,
252
- loadConfig,
253
- matchPattern,
254
- parseConfigExpr,
255
- parseCookieHeader,
256
- parseSetCookieHeader,
257
652
  platformRegistry,
258
- processConfigValue,
259
- resolvePlatform,
260
- serializeCookie
653
+ resolvePlatform
261
654
  };