@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.
@@ -3,39 +3,56 @@
3
3
  *
4
4
  * Provides ServiceWorker APIs (self, addEventListener, etc.) in any JavaScript runtime
5
5
  */
6
+ /**
7
+ * ExtendableEvent base class following ServiceWorker spec
8
+ */
9
+ export declare class ExtendableEvent extends Event {
10
+ private promises;
11
+ private pendingPromises;
12
+ constructor(type: string, pendingPromises: Set<Promise<any>>);
13
+ waitUntil(promise: Promise<any>): void;
14
+ getPromises(): Promise<any>[];
15
+ }
6
16
  /**
7
17
  * ServiceWorker-style fetch event
8
18
  */
9
- export interface ShovelFetchEvent extends Event {
10
- readonly type: "fetch";
19
+ export declare class FetchEvent extends ExtendableEvent {
11
20
  readonly request: Request;
21
+ private responsePromise;
22
+ private responded;
23
+ constructor(request: Request, pendingPromises: Set<Promise<any>>);
12
24
  respondWith(response: Response | Promise<Response>): void;
13
- waitUntil(promise: Promise<any>): void;
25
+ getResponse(): Promise<Response> | null;
26
+ hasResponded(): boolean;
14
27
  }
15
28
  /**
16
29
  * ServiceWorker-style install event
17
30
  */
18
- export interface ShovelInstallEvent extends Event {
19
- readonly type: "install";
20
- waitUntil(promise: Promise<any>): void;
31
+ export declare class InstallEvent extends ExtendableEvent {
32
+ constructor(pendingPromises: Set<Promise<any>>);
21
33
  }
22
34
  /**
23
35
  * ServiceWorker-style activate event
24
36
  */
25
- export interface ShovelActivateEvent extends Event {
26
- readonly type: "activate";
27
- waitUntil(promise: Promise<any>): void;
37
+ export declare class ActivateEvent extends ExtendableEvent {
38
+ constructor(pendingPromises: Set<Promise<any>>);
28
39
  }
29
40
  /**
30
- * Static generation event for collecting routes
41
+ * Legacy interfaces for backward compatibility
31
42
  */
32
- export interface ShovelStaticEvent extends Event {
33
- readonly type: "static";
34
- readonly detail: {
35
- outDir: string;
36
- baseUrl?: string;
37
- };
38
- waitUntil(promise: Promise<string[]>): void;
43
+ export interface ShovelFetchEvent extends Event {
44
+ readonly type: "fetch";
45
+ readonly request: Request;
46
+ respondWith(response: Response | Promise<Response>): void;
47
+ waitUntil(promise: Promise<any>): void;
48
+ }
49
+ export interface ShovelInstallEvent extends Event {
50
+ readonly type: "install";
51
+ waitUntil(promise: Promise<any>): void;
52
+ }
53
+ export interface ShovelActivateEvent extends Event {
54
+ readonly type: "activate";
55
+ waitUntil(promise: Promise<any>): void;
39
56
  }
40
57
  /**
41
58
  * ServiceWorker runtime that can be embedded in any platform
@@ -44,7 +61,10 @@ export declare class ServiceWorkerRuntime extends EventTarget {
44
61
  private pendingPromises;
45
62
  private isInstalled;
46
63
  private isActivated;
64
+ private eventListeners;
47
65
  constructor();
66
+ addEventListener(type: string, listener: Function): void;
67
+ removeEventListener(type: string, listener: Function): void;
48
68
  /**
49
69
  * Create a fetch event and dispatch it
50
70
  */
@@ -57,10 +77,6 @@ export declare class ServiceWorkerRuntime extends EventTarget {
57
77
  * Activate the ServiceWorker
58
78
  */
59
79
  activate(): Promise<void>;
60
- /**
61
- * Collect static routes for pre-rendering
62
- */
63
- collectStaticRoutes(outDir: string, baseUrl?: string): Promise<string[]>;
64
80
  /**
65
81
  * Check if ready to handle requests
66
82
  */
@@ -75,25 +91,29 @@ export declare class ServiceWorkerRuntime extends EventTarget {
75
91
  reset(): void;
76
92
  }
77
93
  /**
78
- * Directory storage interface - parallels CacheStorage for filesystem access
94
+ * Bucket storage interface - parallels CacheStorage for filesystem access
79
95
  * This could become a future web standard
80
96
  */
81
- export interface DirectoryStorage {
97
+ export interface BucketStorage {
82
98
  /**
83
- * Open a named directory - returns FileSystemDirectoryHandle
84
- * Well-known names: 'assets', 'static', 'server', 'client'
99
+ * Open a named bucket - returns FileSystemDirectoryHandle (root of that bucket)
100
+ * Well-known names: 'assets', 'static', 'uploads', 'temp'
85
101
  */
86
102
  open(name: string): Promise<FileSystemDirectoryHandle>;
87
103
  /**
88
- * Check if a named directory exists
104
+ * Alias for open() - for compatibility with File System Access API naming
105
+ */
106
+ getDirectoryHandle(name: string): Promise<FileSystemDirectoryHandle>;
107
+ /**
108
+ * Check if a named bucket exists
89
109
  */
90
110
  has(name: string): Promise<boolean>;
91
111
  /**
92
- * Delete a named directory and all its contents
112
+ * Delete a named bucket and all its contents
93
113
  */
94
114
  delete(name: string): Promise<boolean>;
95
115
  /**
96
- * List all available directory names
116
+ * List all available bucket names
97
117
  */
98
118
  keys(): Promise<string[]>;
99
119
  }
@@ -102,17 +122,10 @@ export interface DirectoryStorage {
102
122
  */
103
123
  export declare function createServiceWorkerGlobals(runtime: ServiceWorkerRuntime, options?: {
104
124
  caches?: any;
105
- dirs?: DirectoryStorage;
125
+ buckets?: BucketStorage;
126
+ isDevelopment?: boolean;
127
+ hotReload?: () => Promise<void>;
106
128
  }): {
107
- self: ServiceWorkerRuntime;
108
- addEventListener: any;
109
- removeEventListener: any;
110
- dispatchEvent: any;
111
- skipWaiting: () => Promise<void>;
112
- clients: {
113
- claim: () => Promise<void>;
114
- matchAll: () => Promise<any[]>;
115
- };
116
129
  console: Console;
117
130
  setTimeout: typeof setTimeout;
118
131
  clearTimeout: typeof clearTimeout;
@@ -146,4 +159,17 @@ export declare function createServiceWorkerGlobals(runtime: ServiceWorkerRuntime
146
159
  new (init?: string[][] | Record<string, string> | string | URLSearchParams): URLSearchParams;
147
160
  prototype: URLSearchParams;
148
161
  };
162
+ caches: any;
163
+ buckets: BucketStorage;
164
+ self: ServiceWorkerRuntime;
165
+ addEventListener: any;
166
+ removeEventListener: any;
167
+ dispatchEvent: any;
168
+ skipWaiting: () => Promise<void>;
169
+ clients: {
170
+ claim(): Promise<void>;
171
+ get(id: string): Promise<any>;
172
+ matchAll(options?: any): Promise<any[]>;
173
+ openWindow(url: string | URL): Promise<any>;
174
+ };
149
175
  };
@@ -1,12 +1,81 @@
1
1
  /// <reference types="./service-worker.d.ts" />
2
2
  // src/service-worker.ts
3
+ var ExtendableEvent = class extends Event {
4
+ promises = [];
5
+ pendingPromises;
6
+ constructor(type, pendingPromises) {
7
+ super(type);
8
+ this.pendingPromises = pendingPromises;
9
+ }
10
+ waitUntil(promise) {
11
+ this.promises.push(promise);
12
+ this.pendingPromises.add(promise);
13
+ promise.finally(() => this.pendingPromises.delete(promise));
14
+ }
15
+ getPromises() {
16
+ return [...this.promises];
17
+ }
18
+ };
19
+ var FetchEvent = class extends ExtendableEvent {
20
+ request;
21
+ responsePromise = null;
22
+ responded = false;
23
+ constructor(request, pendingPromises) {
24
+ super("fetch", pendingPromises);
25
+ this.request = request;
26
+ }
27
+ respondWith(response) {
28
+ if (this.responded) {
29
+ throw new Error("respondWith() already called");
30
+ }
31
+ this.responded = true;
32
+ this.responsePromise = Promise.resolve(response);
33
+ }
34
+ getResponse() {
35
+ return this.responsePromise;
36
+ }
37
+ hasResponded() {
38
+ return this.responded;
39
+ }
40
+ };
41
+ var InstallEvent = class extends ExtendableEvent {
42
+ constructor(pendingPromises) {
43
+ super("install", pendingPromises);
44
+ }
45
+ };
46
+ var ActivateEvent = class extends ExtendableEvent {
47
+ constructor(pendingPromises) {
48
+ super("activate", pendingPromises);
49
+ }
50
+ };
3
51
  var ServiceWorkerRuntime = class extends EventTarget {
4
52
  pendingPromises = /* @__PURE__ */ new Set();
5
53
  isInstalled = false;
6
54
  isActivated = false;
55
+ eventListeners = /* @__PURE__ */ new Map();
7
56
  constructor() {
8
57
  super();
9
58
  }
59
+ addEventListener(type, listener) {
60
+ super.addEventListener(type, listener);
61
+ if (!this.eventListeners.has(type)) {
62
+ this.eventListeners.set(type, []);
63
+ }
64
+ this.eventListeners.get(type).push(listener);
65
+ }
66
+ removeEventListener(type, listener) {
67
+ super.removeEventListener(type, listener);
68
+ if (this.eventListeners.has(type)) {
69
+ const listeners = this.eventListeners.get(type);
70
+ const index = listeners.indexOf(listener);
71
+ if (index > -1) {
72
+ listeners.splice(index, 1);
73
+ if (listeners.length === 0) {
74
+ this.eventListeners.delete(type);
75
+ }
76
+ }
77
+ }
78
+ }
10
79
  /**
11
80
  * Create a fetch event and dispatch it
12
81
  */
@@ -15,28 +84,22 @@ var ServiceWorkerRuntime = class extends EventTarget {
15
84
  throw new Error("ServiceWorker not activated");
16
85
  }
17
86
  return new Promise((resolve, reject) => {
18
- let responded = false;
19
- const promises = [];
20
- const event = Object.assign(new Event("fetch"), {
21
- request,
22
- respondWith: (response) => {
23
- if (responded) {
24
- throw new Error("respondWith() already called");
25
- }
26
- responded = true;
27
- Promise.resolve(response).then(resolve).catch(reject);
28
- },
29
- waitUntil: (promise) => {
30
- promises.push(promise);
31
- this.pendingPromises.add(promise);
32
- promise.finally(() => this.pendingPromises.delete(promise));
87
+ const event = new FetchEvent(request, this.pendingPromises);
88
+ process.nextTick(() => {
89
+ this.dispatchEvent(event);
90
+ const promises = event.getPromises();
91
+ if (promises.length > 0) {
92
+ Promise.allSettled(promises).catch(console.error);
33
93
  }
34
94
  });
35
- this.dispatchEvent(event);
36
- if (!responded) {
37
- reject(new Error("No response provided for fetch event"));
38
- }
39
- Promise.allSettled(promises).catch(console.error);
95
+ setTimeout(() => {
96
+ if (event.hasResponded()) {
97
+ const responsePromise = event.getResponse();
98
+ responsePromise.then(resolve).catch(reject);
99
+ } else {
100
+ reject(new Error("No response provided for fetch event"));
101
+ }
102
+ }, 0);
40
103
  });
41
104
  }
42
105
  /**
@@ -46,22 +109,20 @@ var ServiceWorkerRuntime = class extends EventTarget {
46
109
  if (this.isInstalled)
47
110
  return;
48
111
  return new Promise((resolve, reject) => {
49
- const promises = [];
50
- let installCancelled = false;
51
- const event = Object.assign(new Event("install"), {
52
- waitUntil: (promise) => {
53
- promises.push(promise);
54
- this.pendingPromises.add(promise);
55
- promise.finally(() => this.pendingPromises.delete(promise));
56
- }
57
- });
58
- this.dispatchEvent(event);
59
- Promise.allSettled(promises).then(() => {
60
- if (!installCancelled) {
112
+ const event = new InstallEvent(this.pendingPromises);
113
+ process.nextTick(() => {
114
+ this.dispatchEvent(event);
115
+ const promises = event.getPromises();
116
+ if (promises.length === 0) {
61
117
  this.isInstalled = true;
62
118
  resolve();
119
+ } else {
120
+ Promise.all(promises).then(() => {
121
+ this.isInstalled = true;
122
+ resolve();
123
+ }).catch(reject);
63
124
  }
64
- }).catch(reject);
125
+ });
65
126
  });
66
127
  }
67
128
  /**
@@ -74,46 +135,20 @@ var ServiceWorkerRuntime = class extends EventTarget {
74
135
  if (this.isActivated)
75
136
  return;
76
137
  return new Promise((resolve, reject) => {
77
- const promises = [];
78
- const event = Object.assign(new Event("activate"), {
79
- waitUntil: (promise) => {
80
- promises.push(promise);
81
- this.pendingPromises.add(promise);
82
- promise.finally(() => this.pendingPromises.delete(promise));
83
- }
84
- });
85
- this.dispatchEvent(event);
86
- Promise.allSettled(promises).then(() => {
87
- this.isActivated = true;
88
- resolve();
89
- }).catch(reject);
90
- });
91
- }
92
- /**
93
- * Collect static routes for pre-rendering
94
- */
95
- async collectStaticRoutes(outDir, baseUrl) {
96
- return new Promise((resolve, reject) => {
97
- let routes = [];
98
- const promises = [];
99
- const event = Object.assign(new Event("static"), {
100
- detail: { outDir, baseUrl },
101
- waitUntil: (promise) => {
102
- promises.push(
103
- promise.then((routeList) => {
104
- routes = routes.concat(routeList);
105
- })
106
- );
107
- this.pendingPromises.add(promise);
108
- promise.finally(() => this.pendingPromises.delete(promise));
138
+ const event = new ActivateEvent(this.pendingPromises);
139
+ process.nextTick(() => {
140
+ this.dispatchEvent(event);
141
+ const promises = event.getPromises();
142
+ if (promises.length === 0) {
143
+ this.isActivated = true;
144
+ resolve();
145
+ } else {
146
+ Promise.all(promises).then(() => {
147
+ this.isActivated = true;
148
+ resolve();
149
+ }).catch(reject);
109
150
  }
110
151
  });
111
- this.dispatchEvent(event);
112
- if (promises.length === 0) {
113
- resolve([]);
114
- } else {
115
- Promise.allSettled(promises).then(() => resolve([...new Set(routes)])).catch(reject);
116
- }
117
152
  });
118
153
  }
119
154
  /**
@@ -137,32 +172,53 @@ var ServiceWorkerRuntime = class extends EventTarget {
137
172
  this.isInstalled = false;
138
173
  this.isActivated = false;
139
174
  this.pendingPromises.clear();
140
- const listeners = this._listeners;
141
- if (listeners) {
142
- for (const type in listeners) {
143
- delete listeners[type];
175
+ for (const [type, listeners] of this.eventListeners) {
176
+ for (const listener of listeners) {
177
+ super.removeEventListener(type, listener);
144
178
  }
145
179
  }
180
+ this.eventListeners.clear();
146
181
  }
147
182
  };
148
183
  function createServiceWorkerGlobals(runtime, options = {}) {
149
184
  if (options.caches) {
150
185
  runtime.caches = options.caches;
151
186
  }
152
- if (options.dirs) {
153
- runtime.dirs = options.dirs;
187
+ if (options.buckets) {
188
+ runtime.buckets = options.buckets;
154
189
  }
155
- return {
190
+ const skipWaiting = async () => {
191
+ if (options.isDevelopment && options.hotReload) {
192
+ console.info("[ServiceWorker] skipWaiting() - triggering hot reload");
193
+ await options.hotReload();
194
+ } else if (!options.isDevelopment) {
195
+ console.info("[ServiceWorker] skipWaiting() - production graceful restart not implemented");
196
+ }
197
+ };
198
+ const clients = {
199
+ async claim() {
200
+ },
201
+ async get(id) {
202
+ return void 0;
203
+ },
204
+ async matchAll(options2) {
205
+ return [];
206
+ },
207
+ async openWindow(url) {
208
+ return null;
209
+ }
210
+ };
211
+ const globals = {
156
212
  self: runtime,
157
213
  addEventListener: runtime.addEventListener.bind(runtime),
158
214
  removeEventListener: runtime.removeEventListener.bind(runtime),
159
215
  dispatchEvent: runtime.dispatchEvent.bind(runtime),
160
- // ServiceWorker-specific globals that might be useful
161
- skipWaiting: () => Promise.resolve(),
162
- clients: {
163
- claim: () => Promise.resolve(),
164
- matchAll: () => Promise.resolve([])
165
- },
216
+ // ServiceWorker-specific globals with proper implementations
217
+ skipWaiting,
218
+ clients,
219
+ // Platform resources
220
+ ...options.buckets && { buckets: options.buckets },
221
+ ...options.caches && { caches: options.caches },
166
222
  // Standard globals
167
223
  console,
168
224
  setTimeout,
@@ -176,8 +232,14 @@ function createServiceWorkerGlobals(runtime, options = {}) {
176
232
  URL,
177
233
  URLSearchParams
178
234
  };
235
+ Object.assign(globalThis, globals);
236
+ return globals;
179
237
  }
180
238
  export {
239
+ ActivateEvent,
240
+ ExtendableEvent,
241
+ FetchEvent,
242
+ InstallEvent,
181
243
  ServiceWorkerRuntime,
182
244
  createServiceWorkerGlobals
183
245
  };
package/src/types.d.ts CHANGED
@@ -87,6 +87,8 @@ export interface FilesystemConfig {
87
87
  secretAccessKey?: string;
88
88
  token?: string;
89
89
  };
90
+ /** Factory function for creating directory storage */
91
+ factory?: any;
90
92
  /** Additional adapter-specific options */
91
93
  [key: string]: any;
92
94
  }
@@ -166,13 +168,18 @@ export interface ServiceWorkerInstance {
166
168
  */
167
169
  export interface Server {
168
170
  /** Start listening for requests */
169
- listen(): Promise<void> | void;
171
+ listen(): Promise<void>;
170
172
  /** Stop the server */
171
- close(): Promise<void> | void;
173
+ close(): Promise<void>;
174
+ /** Get server address information */
175
+ address(): {
176
+ port: number;
177
+ host: string;
178
+ };
172
179
  /** Get server URL */
173
- url?: string;
174
- /** Platform-specific server instance */
175
- instance?: any;
180
+ readonly url: string;
181
+ /** Whether server is ready to accept requests */
182
+ readonly ready: boolean;
176
183
  }
177
184
  /**
178
185
  * Platform interface - ServiceWorker entrypoint loader for JavaScript runtimes
@@ -184,11 +191,6 @@ export interface Platform {
184
191
  * Platform name for identification
185
192
  */
186
193
  readonly name: string;
187
- /**
188
- * Build artifacts filesystem (install-time only)
189
- * Available during install handlers to copy built files to runtime storage
190
- */
191
- readonly distDir: FileSystemDirectoryHandle;
192
194
  /**
193
195
  * THE MAIN JOB - Load and run a ServiceWorker-style entrypoint
194
196
  * This is where all the platform-specific complexity lives
@@ -202,16 +204,21 @@ export interface Platform {
202
204
  * - Bun: filesystem with optimized writes
203
205
  */
204
206
  createCaches(config?: CacheConfig): Promise<CacheStorage>;
207
+ /**
208
+ * SUPPORTING UTILITY - Create bucket storage with platform-optimized backends
209
+ * Uses factory pattern to route bucket names to different filesystem adapters
210
+ */
211
+ createBuckets(config?: FilesystemConfig): Promise<any>;
205
212
  /**
206
213
  * SUPPORTING UTILITY - Create server instance for this platform
207
214
  */
208
215
  createServer(handler: Handler, options?: ServerOptions): Server;
209
216
  /**
210
- * SUPPORTING UTILITY - Get filesystem root for bucket/container name
217
+ * SUPPORTING UTILITY - Get filesystem directory handle
211
218
  * Maps directly to cloud storage buckets (S3, R2) or local directories
212
- * @param bucketName - The bucket/container/directory name
219
+ * @param name - Directory name. Use "" for root directory
213
220
  */
214
- getFileSystemRoot(bucketName: string): Promise<FileSystemDirectoryHandle>;
221
+ getDirectoryHandle(name: string): Promise<FileSystemDirectoryHandle>;
215
222
  }
216
223
  /**
217
224
  * Platform detection result
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Common WorkerPool abstraction based on web standards
3
+ * Provides platform-agnostic worker management for ServiceWorker execution
4
+ */
5
+ import { CustomCacheStorage } from "@b9g/cache";
6
+ export interface WorkerPoolOptions {
7
+ /** Number of workers in the pool (default: 1) */
8
+ workerCount?: number;
9
+ /** Request timeout in milliseconds (default: 30000) */
10
+ requestTimeout?: number;
11
+ /** Enable hot reloading (default: true in development) */
12
+ hotReload?: boolean;
13
+ /** Working directory for file resolution */
14
+ cwd?: string;
15
+ }
16
+ export interface WorkerMessage {
17
+ type: string;
18
+ [key: string]: any;
19
+ }
20
+ export interface WorkerRequest extends WorkerMessage {
21
+ type: "request";
22
+ request: {
23
+ url: string;
24
+ method: string;
25
+ headers: Record<string, string>;
26
+ body?: any;
27
+ };
28
+ requestId: number;
29
+ }
30
+ export interface WorkerResponse extends WorkerMessage {
31
+ type: "response";
32
+ response: {
33
+ status: number;
34
+ statusText: string;
35
+ headers: Record<string, string>;
36
+ body: string;
37
+ };
38
+ requestId: number;
39
+ }
40
+ export interface WorkerLoadMessage extends WorkerMessage {
41
+ type: "load";
42
+ version: number | string;
43
+ entrypoint?: string;
44
+ }
45
+ export interface WorkerReadyMessage extends WorkerMessage {
46
+ type: "ready" | "worker-ready";
47
+ version?: number | string;
48
+ }
49
+ export interface WorkerErrorMessage extends WorkerMessage {
50
+ type: "error";
51
+ error: string;
52
+ stack?: string;
53
+ requestId?: number;
54
+ }
55
+ /**
56
+ * Common WorkerPool implementation based on web standards
57
+ * Provides round-robin request handling, hot reloading, and cache coordination
58
+ */
59
+ export declare class WorkerPool {
60
+ private workers;
61
+ private currentWorker;
62
+ private requestId;
63
+ private pendingRequests;
64
+ private options;
65
+ private cacheStorage;
66
+ private appEntrypoint?;
67
+ constructor(cacheStorage: CustomCacheStorage, options?: WorkerPoolOptions, appEntrypoint?: string);
68
+ /**
69
+ * Initialize workers (must be called after construction)
70
+ */
71
+ init(): Promise<void>;
72
+ private initWorkers;
73
+ private createWorker;
74
+ private handleWorkerMessage;
75
+ private handleResponse;
76
+ private handleError;
77
+ private handleReady;
78
+ /**
79
+ * Platform-specific cache message handling
80
+ * Override this method in platform implementations for custom cache coordination
81
+ */
82
+ protected handleCacheMessage(message: WorkerMessage): void;
83
+ /**
84
+ * Handle HTTP request using round-robin worker selection
85
+ */
86
+ handleRequest(request: Request): Promise<Response>;
87
+ /**
88
+ * Reload ServiceWorker with new version (hot reload simulation)
89
+ */
90
+ reloadWorkers(version?: number | string): Promise<void>;
91
+ /**
92
+ * Graceful shutdown of all workers
93
+ */
94
+ terminate(): Promise<void>;
95
+ /**
96
+ * Get the number of active workers
97
+ */
98
+ get workerCount(): number;
99
+ /**
100
+ * Check if the pool is ready to handle requests
101
+ */
102
+ get ready(): boolean;
103
+ }