@b9g/platform 0.1.11 → 0.1.13

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,43 +1,18 @@
1
1
  /// <reference types="./index.d.ts" />
2
- import "../chunk-P57PW2II.js";
3
-
4
2
  // src/index.ts
5
- import * as Path from "path";
6
- import { readFileSync } from "fs";
7
- import { CustomCacheStorage } from "@b9g/cache";
8
- import { MemoryCache } from "@b9g/cache/memory";
9
- import {
10
- ServiceWorkerPool
11
- } from "./worker-pool.js";
3
+ import { getLogger } from "@logtape/logtape";
12
4
  import {
13
- SingleThreadedRuntime
14
- } from "./single-threaded.js";
15
- import {
16
- ShovelServiceWorkerRegistration,
17
5
  ServiceWorkerGlobals,
18
- FetchEvent,
19
- InstallEvent,
20
- ActivateEvent,
21
- ExtendableEvent
6
+ ShovelServiceWorkerRegistration,
7
+ ShovelFetchEvent,
8
+ CustomLoggerStorage
22
9
  } from "./runtime.js";
10
+ import { validateConfig, ConfigValidationError } from "./config.js";
23
11
  import {
24
- RequestCookieStore,
25
- parseCookieHeader,
26
- serializeCookie,
27
- parseSetCookieHeader
28
- } from "./cookie-store.js";
29
- import { CustomBucketStorage } from "@b9g/filesystem";
30
- import {
31
- loadConfig,
32
- configureLogging,
33
- getCacheConfig,
34
- getBucketConfig,
35
- parseConfigExpr,
36
- processConfigValue,
37
- matchPattern,
38
- createBucketFactory,
39
- createCacheFactory
40
- } from "./config.js";
12
+ CustomDatabaseStorage,
13
+ createDatabaseFactory
14
+ } from "./runtime.js";
15
+ var logger = getLogger(["shovel", "platform"]);
41
16
  function detectRuntime() {
42
17
  if (typeof Bun !== "undefined" || process.versions?.bun) {
43
18
  return "bun";
@@ -47,53 +22,6 @@ function detectRuntime() {
47
22
  }
48
23
  return "node";
49
24
  }
50
- function detectPlatformFromPackageJSON(cwd) {
51
- if (!cwd && typeof process === "undefined") {
52
- return null;
53
- }
54
- try {
55
- const pkgPath = Path.join(cwd || process.cwd(), "package.json");
56
- const pkgContent = readFileSync(pkgPath, "utf8");
57
- const pkg = JSON.parse(pkgContent);
58
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
59
- return selectPlatformFromDeps(deps);
60
- } catch (err) {
61
- if (err.code !== "ENOENT") {
62
- throw err;
63
- }
64
- return null;
65
- }
66
- }
67
- function selectPlatformFromDeps(deps) {
68
- const hasBun = deps["@b9g/platform-bun"];
69
- const hasNode = deps["@b9g/platform-node"];
70
- const hasCloudflare = deps["@b9g/platform-cloudflare"];
71
- const installedCount = [hasBun, hasNode, hasCloudflare].filter(
72
- Boolean
73
- ).length;
74
- if (installedCount === 0)
75
- return null;
76
- if (installedCount === 1) {
77
- if (hasBun)
78
- return "bun";
79
- if (hasNode)
80
- return "node";
81
- if (hasCloudflare)
82
- return "cloudflare";
83
- }
84
- const runtime = detectRuntime();
85
- if (runtime === "bun" && hasBun)
86
- return "bun";
87
- if (runtime === "node" && hasNode)
88
- return "node";
89
- if (hasBun)
90
- return "bun";
91
- if (hasNode)
92
- return "node";
93
- if (hasCloudflare)
94
- return "cloudflare";
95
- return null;
96
- }
97
25
  function detectDeploymentPlatform() {
98
26
  if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
99
27
  return null;
@@ -105,10 +33,6 @@ function detectDeploymentPlatform() {
105
33
  return null;
106
34
  }
107
35
  function detectDevelopmentPlatform() {
108
- const pkgPlatform = detectPlatformFromPackageJSON();
109
- if (pkgPlatform) {
110
- return pkgPlatform;
111
- }
112
36
  const runtime = detectRuntime();
113
37
  switch (runtime) {
114
38
  case "bun":
@@ -139,27 +63,20 @@ function resolvePlatform(options) {
139
63
  async function createPlatform(platformName, options = {}) {
140
64
  switch (platformName) {
141
65
  case "node": {
142
- const modulePath = import.meta.resolve("@b9g/platform-node");
143
- const NodePlatform = await import(modulePath).then((m) => m.default);
66
+ const { default: NodePlatform } = await import("@b9g/platform-node");
144
67
  return new NodePlatform(options);
145
68
  }
146
69
  case "bun": {
147
- const modulePath = import.meta.resolve("@b9g/platform-bun");
148
- const BunPlatform = await import(modulePath).then((m) => m.default);
70
+ const { default: BunPlatform } = await import("@b9g/platform-bun");
149
71
  return new BunPlatform(options);
150
72
  }
151
- case "cloudflare":
152
- case "cloudflare-workers":
153
- case "cf": {
154
- const modulePath = import.meta.resolve("@b9g/platform-cloudflare");
155
- const CloudflarePlatform = await import(modulePath).then(
156
- (m) => m.default
157
- );
73
+ case "cloudflare": {
74
+ const { default: CloudflarePlatform } = await import("@b9g/platform-cloudflare");
158
75
  return new CloudflarePlatform(options);
159
76
  }
160
77
  default:
161
78
  throw new Error(
162
- `Unknown platform: ${platformName}. Available platforms: node, bun, cloudflare`
79
+ `Unknown platform: ${platformName}. Valid platforms: node, bun, cloudflare`
163
80
  );
164
81
  }
165
82
  }
@@ -168,15 +85,6 @@ var BasePlatform = class {
168
85
  constructor(config = {}) {
169
86
  this.config = config;
170
87
  }
171
- /**
172
- * Create cache storage
173
- * Returns empty CacheStorage - applications create caches on-demand via caches.open()
174
- */
175
- async createCaches() {
176
- return new CustomCacheStorage(
177
- (name) => new MemoryCache(name)
178
- );
179
- }
180
88
  };
181
89
  var DefaultPlatformRegistry = class {
182
90
  #platforms;
@@ -234,36 +142,468 @@ async function getPlatformAsync(name) {
234
142
  }
235
143
  return platform;
236
144
  }
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
+ async function createWebWorker(workerScript) {
226
+ if (typeof Worker !== "undefined") {
227
+ return new Worker(workerScript, { type: "module" });
228
+ }
229
+ const isNodeJs = typeof process !== "undefined" && process.versions?.node;
230
+ if (isNodeJs) {
231
+ try {
232
+ const { Worker: NodeWebWorker } = await import("@b9g/node-webworker");
233
+ logger.info("Using @b9g/node-webworker shim for Node.js", {});
234
+ return new NodeWebWorker(workerScript, {
235
+ type: "module"
236
+ });
237
+ } catch (shimError) {
238
+ logger.error(
239
+ "MISSING WEB STANDARD: Node.js lacks native Web Worker support",
240
+ {
241
+ canonicalIssue: "https://github.com/nodejs/node/issues/43583",
242
+ message: "This is a basic web standard from 2009 - help push for implementation!"
243
+ }
244
+ );
245
+ throw new Error(`\u274C Web Worker not available on Node.js
246
+
247
+ \u{1F517} Node.js doesn't implement the Web Worker standard yet.
248
+ CANONICAL ISSUE: https://github.com/nodejs/node/issues/43583
249
+
250
+ \u{1F5F3}\uFE0F Please \u{1F44D} react and comment to show demand for this basic web standard!
251
+
252
+ \u{1F4A1} Immediate workaround:
253
+ npm install @b9g/node-webworker
254
+
255
+ This installs our minimal, reliable Web Worker shim for Node.js.
256
+
257
+ \u{1F4DA} Learn more: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
258
+ }
259
+ }
260
+ const runtime = typeof Bun !== "undefined" ? "Bun" : typeof Deno !== "undefined" ? "Deno" : "Unknown";
261
+ throw new Error(`\u274C Web Worker not available on ${runtime}
262
+
263
+ This runtime should support Web Workers but the API is not available.
264
+ Please check your runtime version and configuration.
265
+
266
+ \u{1F4DA} Web Worker standard: https://developer.mozilla.org/en-US/docs/Web/API/Worker`);
267
+ }
268
+ var ServiceWorkerPool = class {
269
+ #workers;
270
+ #currentWorker;
271
+ #requestID;
272
+ #pendingRequests;
273
+ #pendingWorkerReady;
274
+ #options;
275
+ #appEntrypoint;
276
+ #cacheStorage;
277
+ // Waiters for when workers become available (used during reload)
278
+ #workerAvailableWaiters;
279
+ constructor(options = {}, appEntrypoint, cacheStorage) {
280
+ this.#workers = [];
281
+ this.#currentWorker = 0;
282
+ this.#requestID = 0;
283
+ this.#pendingRequests = /* @__PURE__ */ new Map();
284
+ this.#pendingWorkerReady = /* @__PURE__ */ new Map();
285
+ this.#workerAvailableWaiters = [];
286
+ this.#appEntrypoint = appEntrypoint;
287
+ this.#cacheStorage = cacheStorage;
288
+ this.#options = {
289
+ workerCount: 1,
290
+ requestTimeout: 3e4,
291
+ ...options
292
+ };
293
+ }
294
+ /**
295
+ * Initialize workers (must be called after construction)
296
+ */
297
+ async init() {
298
+ const promises = [];
299
+ for (let i = 0; i < this.#options.workerCount; i++) {
300
+ promises.push(this.#createWorker(this.#appEntrypoint));
301
+ }
302
+ await Promise.all(promises);
303
+ }
304
+ /**
305
+ * Create a worker from the unified bundle
306
+ * The bundle self-initializes and sends "ready" when done
307
+ */
308
+ async #createWorker(entrypoint) {
309
+ const worker = await createWebWorker(entrypoint);
310
+ const readyPromise = new Promise((resolve, reject) => {
311
+ const timeoutId = setTimeout(() => {
312
+ this.#pendingWorkerReady.delete(worker);
313
+ reject(
314
+ new Error(
315
+ `Worker failed to become ready within 30000ms (${entrypoint})`
316
+ )
317
+ );
318
+ }, 3e4);
319
+ this.#pendingWorkerReady.set(worker, {
320
+ resolve: () => {
321
+ clearTimeout(timeoutId);
322
+ resolve();
323
+ },
324
+ reject: (error) => {
325
+ clearTimeout(timeoutId);
326
+ reject(error);
327
+ }
328
+ });
329
+ });
330
+ worker.addEventListener("message", (event) => {
331
+ this.#handleWorkerMessage(worker, event.data || event);
332
+ });
333
+ worker.addEventListener("error", (event) => {
334
+ const errorMessage = event.message || event.error?.message || "Unknown worker error";
335
+ const error = new Error(`Worker error: ${errorMessage}`);
336
+ logger.error("Worker error: {error}", {
337
+ error: event.error || errorMessage,
338
+ filename: event.filename,
339
+ lineno: event.lineno,
340
+ colno: event.colno
341
+ });
342
+ const pending = this.#pendingWorkerReady.get(worker);
343
+ if (pending) {
344
+ this.#pendingWorkerReady.delete(worker);
345
+ pending.reject(error);
346
+ }
347
+ });
348
+ logger.debug("Waiting for worker ready signal", { entrypoint });
349
+ await readyPromise;
350
+ this.#pendingWorkerReady.delete(worker);
351
+ await new Promise((resolve) => setTimeout(resolve, 0));
352
+ this.#workers.push(worker);
353
+ logger.debug("Worker ready", { entrypoint });
354
+ const waiters = this.#workerAvailableWaiters;
355
+ this.#workerAvailableWaiters = [];
356
+ for (const waiter of waiters) {
357
+ waiter.resolve();
358
+ }
359
+ return worker;
360
+ }
361
+ #handleWorkerMessage(worker, message) {
362
+ logger.debug("Worker message received", { type: message.type });
363
+ switch (message.type) {
364
+ case "ready": {
365
+ const pending = this.#pendingWorkerReady.get(worker);
366
+ if (pending) {
367
+ pending.resolve();
368
+ }
369
+ logger.debug("ServiceWorker ready");
370
+ break;
371
+ }
372
+ case "response":
373
+ this.#handleResponse(message);
374
+ break;
375
+ case "error":
376
+ this.#handleError(message);
377
+ break;
378
+ default:
379
+ if (message.type?.startsWith("cache:")) {
380
+ logger.debug("Cache message received", { type: message.type });
381
+ if (this.#cacheStorage) {
382
+ const storage = this.#cacheStorage;
383
+ if (typeof storage.handleMessage === "function") {
384
+ storage.handleMessage(worker, message).catch((err) => {
385
+ logger.error("Cache message handling failed: {error}", {
386
+ error: err
387
+ });
388
+ });
389
+ }
390
+ }
391
+ }
392
+ break;
393
+ }
394
+ }
395
+ #handleResponse(message) {
396
+ const pending = this.#pendingRequests.get(message.requestID);
397
+ if (pending) {
398
+ if (pending.timeoutId) {
399
+ clearTimeout(pending.timeoutId);
400
+ }
401
+ const response = new Response(message.response.body, {
402
+ status: message.response.status,
403
+ statusText: message.response.statusText,
404
+ headers: message.response.headers
405
+ });
406
+ pending.resolve(response);
407
+ this.#pendingRequests.delete(message.requestID);
408
+ }
409
+ }
410
+ #handleError(message) {
411
+ logger.error("Worker error message received: {error}", {
412
+ error: message.error,
413
+ stack: message.stack,
414
+ requestID: message.requestID
415
+ });
416
+ if (message.requestID) {
417
+ const pending = this.#pendingRequests.get(message.requestID);
418
+ if (pending) {
419
+ if (pending.timeoutId) {
420
+ clearTimeout(pending.timeoutId);
421
+ }
422
+ pending.reject(new Error(message.error));
423
+ this.#pendingRequests.delete(message.requestID);
424
+ }
425
+ }
426
+ }
427
+ /**
428
+ * Handle HTTP request using round-robin worker selection
429
+ */
430
+ async handleRequest(request) {
431
+ if (this.#workers.length === 0) {
432
+ logger.debug("No workers available, waiting for worker to be ready");
433
+ await new Promise((resolve, reject) => {
434
+ const waiter = { resolve, reject };
435
+ this.#workerAvailableWaiters.push(waiter);
436
+ const timeoutId = setTimeout(() => {
437
+ const index = this.#workerAvailableWaiters.indexOf(waiter);
438
+ if (index !== -1) {
439
+ this.#workerAvailableWaiters.splice(index, 1);
440
+ reject(new Error("Timeout waiting for worker to become available"));
441
+ }
442
+ }, this.#options.requestTimeout);
443
+ const originalResolve = waiter.resolve;
444
+ const originalReject = waiter.reject;
445
+ waiter.resolve = () => {
446
+ clearTimeout(timeoutId);
447
+ originalResolve();
448
+ };
449
+ waiter.reject = (error) => {
450
+ clearTimeout(timeoutId);
451
+ originalReject(error);
452
+ };
453
+ });
454
+ }
455
+ const worker = this.#workers[this.#currentWorker];
456
+ logger.debug("Dispatching to worker", {
457
+ workerIndex: this.#currentWorker + 1,
458
+ totalWorkers: this.#workers.length
459
+ });
460
+ this.#currentWorker = (this.#currentWorker + 1) % this.#workers.length;
461
+ const requestID = ++this.#requestID;
462
+ return new Promise((resolve, reject) => {
463
+ const timeoutId = setTimeout(() => {
464
+ if (this.#pendingRequests.has(requestID)) {
465
+ this.#pendingRequests.delete(requestID);
466
+ reject(new Error("Request timeout"));
467
+ }
468
+ }, this.#options.requestTimeout);
469
+ this.#pendingRequests.set(requestID, { resolve, reject, timeoutId });
470
+ this.#sendRequest(worker, request, requestID).catch(reject);
471
+ });
472
+ }
473
+ async #sendRequest(worker, request, requestID) {
474
+ let body = null;
475
+ if (request.body) {
476
+ body = await request.arrayBuffer();
477
+ }
478
+ const workerRequest = {
479
+ type: "request",
480
+ request: {
481
+ url: request.url,
482
+ method: request.method,
483
+ headers: Object.fromEntries(request.headers.entries()),
484
+ body
485
+ },
486
+ requestID
487
+ };
488
+ if (body) {
489
+ worker.postMessage(workerRequest, [body]);
490
+ } else {
491
+ worker.postMessage(workerRequest);
492
+ }
493
+ }
494
+ /**
495
+ * Gracefully shutdown a worker by closing all resources first
496
+ */
497
+ async #gracefulShutdown(worker, timeout = 5e3) {
498
+ return new Promise((resolve) => {
499
+ let resolved = false;
500
+ const onMessage = (event) => {
501
+ const message = event.data || event;
502
+ if (message?.type === "shutdown-complete") {
503
+ if (!resolved) {
504
+ resolved = true;
505
+ worker.removeEventListener("message", onMessage);
506
+ resolve();
507
+ }
508
+ }
509
+ };
510
+ worker.addEventListener("message", onMessage);
511
+ worker.postMessage({ type: "shutdown" });
512
+ setTimeout(() => {
513
+ if (!resolved) {
514
+ resolved = true;
515
+ worker.removeEventListener("message", onMessage);
516
+ logger.warn("Worker shutdown timed out, forcing termination");
517
+ resolve();
518
+ }
519
+ }, timeout);
520
+ });
521
+ }
522
+ /**
523
+ * Reload workers with new entrypoint (hot reload)
524
+ *
525
+ * With unified builds, hot reload means:
526
+ * 1. Gracefully shutdown existing workers (close databases, etc.)
527
+ * 2. Terminate workers after resources are closed
528
+ * 3. Create new workers with the new bundle
529
+ */
530
+ async reloadWorkers(entrypoint) {
531
+ logger.debug("Reloading workers", { entrypoint });
532
+ this.#appEntrypoint = entrypoint;
533
+ const shutdownPromises = this.#workers.map(
534
+ (worker) => this.#gracefulShutdown(worker)
535
+ );
536
+ await Promise.allSettled(shutdownPromises);
537
+ const terminatePromises = this.#workers.map((worker) => worker.terminate());
538
+ await Promise.allSettled(terminatePromises);
539
+ this.#workers = [];
540
+ this.#currentWorker = 0;
541
+ try {
542
+ const createPromises = [];
543
+ for (let i = 0; i < this.#options.workerCount; i++) {
544
+ createPromises.push(this.#createWorker(entrypoint));
545
+ }
546
+ await Promise.all(createPromises);
547
+ logger.debug("All workers reloaded", { entrypoint });
548
+ } catch (error) {
549
+ const waiters = this.#workerAvailableWaiters;
550
+ this.#workerAvailableWaiters = [];
551
+ const reloadError = error instanceof Error ? error : new Error("Worker creation failed during reload");
552
+ for (const waiter of waiters) {
553
+ waiter.reject(reloadError);
554
+ }
555
+ throw error;
556
+ }
557
+ }
558
+ /**
559
+ * Graceful shutdown of all workers
560
+ */
561
+ async terminate() {
562
+ const shutdownPromises = this.#workers.map(
563
+ (worker) => this.#gracefulShutdown(worker)
564
+ );
565
+ await Promise.allSettled(shutdownPromises);
566
+ const terminatePromises = this.#workers.map((worker) => worker.terminate());
567
+ await Promise.allSettled(terminatePromises);
568
+ this.#workers = [];
569
+ this.#currentWorker = 0;
570
+ this.#pendingRequests.clear();
571
+ this.#pendingWorkerReady.clear();
572
+ const waiters = this.#workerAvailableWaiters;
573
+ this.#workerAvailableWaiters = [];
574
+ const terminateError = new Error("Worker pool terminated");
575
+ for (const waiter of waiters) {
576
+ waiter.reject(terminateError);
577
+ }
578
+ }
579
+ /**
580
+ * Get the number of active workers
581
+ */
582
+ get workerCount() {
583
+ return this.#workers.length;
584
+ }
585
+ /**
586
+ * Check if the pool is ready to handle requests
587
+ */
588
+ get ready() {
589
+ return this.#workers.length > 0;
590
+ }
591
+ };
237
592
  export {
238
- ActivateEvent,
239
593
  BasePlatform,
240
- CustomBucketStorage,
241
- ExtendableEvent,
242
- FetchEvent,
243
- InstallEvent,
244
- RequestCookieStore,
245
- ServiceWorkerGlobals,
594
+ ConfigValidationError,
595
+ CustomDatabaseStorage,
596
+ CustomLoggerStorage,
246
597
  ServiceWorkerPool,
247
- ShovelServiceWorkerRegistration,
248
598
  SingleThreadedRuntime,
249
- configureLogging,
250
- createBucketFactory,
251
- createCacheFactory,
599
+ createDatabaseFactory,
252
600
  createPlatform,
253
601
  detectDeploymentPlatform,
254
602
  detectDevelopmentPlatform,
255
603
  detectRuntime,
256
- getBucketConfig,
257
- getCacheConfig,
258
604
  getPlatform,
259
605
  getPlatformAsync,
260
- loadConfig,
261
- matchPattern,
262
- parseConfigExpr,
263
- parseCookieHeader,
264
- parseSetCookieHeader,
265
606
  platformRegistry,
266
- processConfigValue,
267
607
  resolvePlatform,
268
- serializeCookie
608
+ validateConfig
269
609
  };