@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/README.md +1 -1
- package/package.json +22 -38
- package/src/config.d.ts +15 -163
- package/src/config.js +18 -630
- package/src/globals.d.ts +119 -0
- package/src/index.d.ts +294 -25
- package/src/index.js +466 -126
- package/src/runtime.d.ts +423 -22
- package/src/runtime.js +693 -250
- package/src/shovel-config.d.ts +10 -0
- package/chunk-P57PW2II.js +0 -11
- package/src/cookie-store.d.ts +0 -80
- package/src/cookie-store.js +0 -233
- package/src/single-threaded.d.ts +0 -59
- package/src/single-threaded.js +0 -114
- package/src/worker-pool.d.ts +0 -93
- package/src/worker-pool.js +0 -390
package/src/runtime.js
CHANGED
|
@@ -1,14 +1,341 @@
|
|
|
1
1
|
/// <reference types="./runtime.d.ts" />
|
|
2
|
-
import "../chunk-P57PW2II.js";
|
|
3
|
-
|
|
4
2
|
// src/runtime.ts
|
|
5
|
-
import { RequestCookieStore } from "./cookie-store.js";
|
|
6
3
|
import { AsyncContext } from "@b9g/async-context";
|
|
7
|
-
import {
|
|
4
|
+
import { getLogger, getConsoleSink } from "@logtape/logtape";
|
|
5
|
+
import { CustomDirectoryStorage } from "@b9g/filesystem";
|
|
8
6
|
import { CustomCacheStorage } from "@b9g/cache";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
7
|
+
import { handleCacheResponse, PostMessageCache } from "@b9g/cache/postmessage";
|
|
8
|
+
import {
|
|
9
|
+
configure
|
|
10
|
+
} from "@logtape/logtape";
|
|
11
|
+
import { Database } from "@b9g/zen";
|
|
12
|
+
function parseCookieHeader(cookieHeader) {
|
|
13
|
+
const cookies = /* @__PURE__ */ new Map();
|
|
14
|
+
if (!cookieHeader) return cookies;
|
|
15
|
+
const pairs = cookieHeader.split(/;\s*/);
|
|
16
|
+
for (const pair of pairs) {
|
|
17
|
+
const [name, ...valueParts] = pair.split("=");
|
|
18
|
+
if (name) {
|
|
19
|
+
const value = valueParts.join("=");
|
|
20
|
+
cookies.set(name.trim(), decodeURIComponent(value || ""));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return cookies;
|
|
24
|
+
}
|
|
25
|
+
function serializeCookie(cookie) {
|
|
26
|
+
let header = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
|
|
27
|
+
if (cookie.expires !== void 0 && cookie.expires !== null) {
|
|
28
|
+
const date = new Date(cookie.expires);
|
|
29
|
+
header += `; Expires=${date.toUTCString()}`;
|
|
30
|
+
}
|
|
31
|
+
if (cookie.domain) {
|
|
32
|
+
header += `; Domain=${cookie.domain}`;
|
|
33
|
+
}
|
|
34
|
+
if (cookie.path) {
|
|
35
|
+
header += `; Path=${cookie.path}`;
|
|
36
|
+
} else {
|
|
37
|
+
header += `; Path=/`;
|
|
38
|
+
}
|
|
39
|
+
if (cookie.sameSite) {
|
|
40
|
+
header += `; SameSite=${cookie.sameSite.charAt(0).toUpperCase() + cookie.sameSite.slice(1)}`;
|
|
41
|
+
} else {
|
|
42
|
+
header += `; SameSite=Strict`;
|
|
43
|
+
}
|
|
44
|
+
if (cookie.partitioned) {
|
|
45
|
+
header += `; Partitioned`;
|
|
46
|
+
}
|
|
47
|
+
header += `; Secure`;
|
|
48
|
+
return header;
|
|
49
|
+
}
|
|
50
|
+
function parseSetCookieHeader(setCookieHeader) {
|
|
51
|
+
const parts = setCookieHeader.split(/;\s*/);
|
|
52
|
+
const [nameValue, ...attributes] = parts;
|
|
53
|
+
const [name, ...valueParts] = nameValue.split("=");
|
|
54
|
+
const value = valueParts.join("=");
|
|
55
|
+
const cookie = {
|
|
56
|
+
name: decodeURIComponent(name.trim()),
|
|
57
|
+
value: decodeURIComponent(value || "")
|
|
58
|
+
};
|
|
59
|
+
for (const attr of attributes) {
|
|
60
|
+
const [key, ...attrValueParts] = attr.split("=");
|
|
61
|
+
const attrKey = key.trim().toLowerCase();
|
|
62
|
+
const attrValue = attrValueParts.join("=").trim();
|
|
63
|
+
switch (attrKey) {
|
|
64
|
+
case "expires":
|
|
65
|
+
cookie.expires = new Date(attrValue).getTime();
|
|
66
|
+
break;
|
|
67
|
+
case "max-age":
|
|
68
|
+
cookie.expires = Date.now() + parseInt(attrValue, 10) * 1e3;
|
|
69
|
+
break;
|
|
70
|
+
case "domain":
|
|
71
|
+
cookie.domain = attrValue;
|
|
72
|
+
break;
|
|
73
|
+
case "path":
|
|
74
|
+
cookie.path = attrValue;
|
|
75
|
+
break;
|
|
76
|
+
case "secure":
|
|
77
|
+
cookie.secure = true;
|
|
78
|
+
break;
|
|
79
|
+
case "samesite":
|
|
80
|
+
cookie.sameSite = attrValue.toLowerCase();
|
|
81
|
+
break;
|
|
82
|
+
case "partitioned":
|
|
83
|
+
cookie.partitioned = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return cookie;
|
|
88
|
+
}
|
|
89
|
+
var RequestCookieStore = class extends EventTarget {
|
|
90
|
+
#cookies;
|
|
91
|
+
#changes;
|
|
92
|
+
// null = deleted
|
|
93
|
+
#request;
|
|
94
|
+
// Event handler for cookie changes (spec compliance)
|
|
95
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
96
|
+
onchange = null;
|
|
97
|
+
constructor(request) {
|
|
98
|
+
super();
|
|
99
|
+
this.#cookies = /* @__PURE__ */ new Map();
|
|
100
|
+
this.#changes = /* @__PURE__ */ new Map();
|
|
101
|
+
this.#request = request || null;
|
|
102
|
+
if (request) {
|
|
103
|
+
const cookieHeader = request.headers.get("cookie");
|
|
104
|
+
if (cookieHeader) {
|
|
105
|
+
const parsed = parseCookieHeader(cookieHeader);
|
|
106
|
+
for (const [name, value] of parsed) {
|
|
107
|
+
this.#cookies.set(name, { name, value });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async get(nameOrOptions) {
|
|
113
|
+
const name = typeof nameOrOptions === "string" ? nameOrOptions : nameOrOptions.name;
|
|
114
|
+
if (!name) {
|
|
115
|
+
throw new TypeError("Cookie name is required");
|
|
116
|
+
}
|
|
117
|
+
if (this.#changes.has(name)) {
|
|
118
|
+
const change = this.#changes.get(name);
|
|
119
|
+
if (change === null || change === void 0) return null;
|
|
120
|
+
return {
|
|
121
|
+
name: change.name,
|
|
122
|
+
value: change.value,
|
|
123
|
+
domain: change.domain ?? void 0,
|
|
124
|
+
path: change.path,
|
|
125
|
+
expires: change.expires ?? void 0,
|
|
126
|
+
sameSite: change.sameSite,
|
|
127
|
+
partitioned: change.partitioned
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return this.#cookies.get(name) || null;
|
|
131
|
+
}
|
|
132
|
+
async getAll(nameOrOptions) {
|
|
133
|
+
const name = typeof nameOrOptions === "string" ? nameOrOptions : nameOrOptions?.name;
|
|
134
|
+
const result = [];
|
|
135
|
+
const allNames = /* @__PURE__ */ new Set([
|
|
136
|
+
...this.#cookies.keys(),
|
|
137
|
+
...this.#changes.keys()
|
|
138
|
+
]);
|
|
139
|
+
for (const cookieName of allNames) {
|
|
140
|
+
if (name && cookieName !== name) continue;
|
|
141
|
+
if (this.#changes.has(cookieName) && this.#changes.get(cookieName) === null) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const cookie = await this.get(cookieName);
|
|
145
|
+
if (cookie) {
|
|
146
|
+
result.push(cookie);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
async set(nameOrOptions, value) {
|
|
152
|
+
let cookie;
|
|
153
|
+
if (typeof nameOrOptions === "string") {
|
|
154
|
+
if (value === void 0) {
|
|
155
|
+
throw new TypeError("Cookie value is required");
|
|
156
|
+
}
|
|
157
|
+
cookie = {
|
|
158
|
+
name: nameOrOptions,
|
|
159
|
+
value,
|
|
160
|
+
path: "/",
|
|
161
|
+
sameSite: "strict"
|
|
162
|
+
};
|
|
163
|
+
} else {
|
|
164
|
+
cookie = {
|
|
165
|
+
path: "/",
|
|
166
|
+
sameSite: "strict",
|
|
167
|
+
...nameOrOptions
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const size = cookie.name.length + cookie.value.length;
|
|
171
|
+
if (size > 4096) {
|
|
172
|
+
throw new TypeError(
|
|
173
|
+
`Cookie name+value too large: ${size} bytes (max 4096)`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
this.#changes.set(cookie.name, cookie);
|
|
177
|
+
}
|
|
178
|
+
async delete(nameOrOptions) {
|
|
179
|
+
const name = typeof nameOrOptions === "string" ? nameOrOptions : nameOrOptions.name;
|
|
180
|
+
if (!name) {
|
|
181
|
+
throw new TypeError("Cookie name is required");
|
|
182
|
+
}
|
|
183
|
+
this.#changes.set(name, null);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get Set-Cookie headers for all changes
|
|
187
|
+
* This should be called when constructing the Response
|
|
188
|
+
*/
|
|
189
|
+
getSetCookieHeaders() {
|
|
190
|
+
const headers = [];
|
|
191
|
+
for (const [name, change] of this.#changes) {
|
|
192
|
+
if (change === null) {
|
|
193
|
+
headers.push(
|
|
194
|
+
serializeCookie({
|
|
195
|
+
name,
|
|
196
|
+
value: "",
|
|
197
|
+
expires: 0,
|
|
198
|
+
path: "/"
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
headers.push(serializeCookie(change));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return headers;
|
|
206
|
+
}
|
|
207
|
+
hasChanges() {
|
|
208
|
+
return this.#changes.size > 0;
|
|
209
|
+
}
|
|
210
|
+
clearChanges() {
|
|
211
|
+
this.#changes.clear();
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var CustomLoggerStorage = class {
|
|
215
|
+
#factory;
|
|
216
|
+
constructor(factory) {
|
|
217
|
+
this.#factory = factory;
|
|
218
|
+
}
|
|
219
|
+
get(categories) {
|
|
220
|
+
return this.#factory(categories);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var CustomDatabaseStorage = class {
|
|
224
|
+
#factory;
|
|
225
|
+
#databases;
|
|
226
|
+
#closers;
|
|
227
|
+
#pending;
|
|
228
|
+
constructor(factory) {
|
|
229
|
+
this.#factory = factory;
|
|
230
|
+
this.#databases = /* @__PURE__ */ new Map();
|
|
231
|
+
this.#closers = /* @__PURE__ */ new Map();
|
|
232
|
+
this.#pending = /* @__PURE__ */ new Map();
|
|
233
|
+
}
|
|
234
|
+
async open(name, version, onUpgrade) {
|
|
235
|
+
const existing = this.#databases.get(name);
|
|
236
|
+
if (existing) {
|
|
237
|
+
return existing;
|
|
238
|
+
}
|
|
239
|
+
const pending = this.#pending.get(name);
|
|
240
|
+
if (pending) {
|
|
241
|
+
return pending;
|
|
242
|
+
}
|
|
243
|
+
const promise = (async () => {
|
|
244
|
+
const { db, close } = await this.#factory(name);
|
|
245
|
+
if (onUpgrade) {
|
|
246
|
+
db.addEventListener("upgradeneeded", (e) => {
|
|
247
|
+
const event = e;
|
|
248
|
+
onUpgrade({
|
|
249
|
+
db,
|
|
250
|
+
oldVersion: event.oldVersion,
|
|
251
|
+
newVersion: event.newVersion,
|
|
252
|
+
waitUntil: (p) => event.waitUntil(p)
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
await db.open(version);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
await close();
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
this.#databases.set(name, db);
|
|
263
|
+
this.#closers.set(name, close);
|
|
264
|
+
return db;
|
|
265
|
+
})().finally(() => {
|
|
266
|
+
this.#pending.delete(name);
|
|
267
|
+
});
|
|
268
|
+
this.#pending.set(name, promise);
|
|
269
|
+
return promise;
|
|
270
|
+
}
|
|
271
|
+
get(name) {
|
|
272
|
+
const db = this.#databases.get(name);
|
|
273
|
+
if (!db) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Database "${name}" has not been opened. Call self.databases.open("${name}", version) in your activate handler first.`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return db;
|
|
279
|
+
}
|
|
280
|
+
async close(name) {
|
|
281
|
+
const pending = this.#pending.get(name);
|
|
282
|
+
if (pending) {
|
|
283
|
+
try {
|
|
284
|
+
await pending;
|
|
285
|
+
} catch (_err) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const closer = this.#closers.get(name);
|
|
290
|
+
if (closer) {
|
|
291
|
+
await closer();
|
|
292
|
+
this.#databases.delete(name);
|
|
293
|
+
this.#closers.delete(name);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async closeAll() {
|
|
297
|
+
if (this.#pending.size > 0) {
|
|
298
|
+
await Promise.allSettled(this.#pending.values());
|
|
299
|
+
}
|
|
300
|
+
const promises = Array.from(this.#databases.keys()).map(
|
|
301
|
+
(name) => this.close(name)
|
|
302
|
+
);
|
|
303
|
+
await Promise.allSettled(promises);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
function createDatabaseFactory(configs) {
|
|
307
|
+
return async (name) => {
|
|
308
|
+
const config = configs[name];
|
|
309
|
+
if (!config) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Database "${name}" is not configured. Available databases: ${Object.keys(configs).join(", ") || "(none)"}`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const { impl, url, ...driverOptions } = config;
|
|
315
|
+
if (!impl) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Database "${name}" has no impl. Ensure the database module is configured in shovel.json.`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (!url) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Database "${name}" has no url. Ensure the database URL is configured.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const driver = new impl(url, driverOptions);
|
|
326
|
+
const db = new Database(driver);
|
|
327
|
+
return {
|
|
328
|
+
db,
|
|
329
|
+
close: async () => {
|
|
330
|
+
await driver.close();
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
var SERVICE_WORKER_EVENTS = ["fetch", "install", "activate"];
|
|
336
|
+
function isServiceWorkerEvent(type) {
|
|
337
|
+
return SERVICE_WORKER_EVENTS.includes(type);
|
|
338
|
+
}
|
|
12
339
|
if (import.meta.env && !import.meta.env.MODE && import.meta.env.NODE_ENV) {
|
|
13
340
|
import.meta.env.MODE = import.meta.env.NODE_ENV;
|
|
14
341
|
}
|
|
@@ -19,7 +346,9 @@ var PATCHED_KEYS = [
|
|
|
19
346
|
"self",
|
|
20
347
|
"fetch",
|
|
21
348
|
"caches",
|
|
22
|
-
"
|
|
349
|
+
"directories",
|
|
350
|
+
"databases",
|
|
351
|
+
"loggers",
|
|
23
352
|
"registration",
|
|
24
353
|
"clients",
|
|
25
354
|
"skipWaiting",
|
|
@@ -38,9 +367,9 @@ function promiseWithTimeout(promise, timeoutMs, errorMessage) {
|
|
|
38
367
|
)
|
|
39
368
|
]);
|
|
40
369
|
}
|
|
41
|
-
var kEndDispatchPhase = Symbol.for("shovel.endDispatchPhase");
|
|
42
|
-
var kCanExtend = Symbol.for("shovel.canExtend");
|
|
43
|
-
var
|
|
370
|
+
var kEndDispatchPhase = /* @__PURE__ */ Symbol.for("shovel.endDispatchPhase");
|
|
371
|
+
var kCanExtend = /* @__PURE__ */ Symbol.for("shovel.canExtend");
|
|
372
|
+
var ShovelExtendableEvent = class extends Event {
|
|
44
373
|
#promises;
|
|
45
374
|
#dispatchPhase;
|
|
46
375
|
#pendingCount;
|
|
@@ -77,17 +406,33 @@ var ExtendableEvent = class extends Event {
|
|
|
77
406
|
return this.#dispatchPhase || this.#pendingCount > 0;
|
|
78
407
|
}
|
|
79
408
|
};
|
|
80
|
-
var
|
|
409
|
+
var ShovelFetchEvent = class extends ShovelExtendableEvent {
|
|
81
410
|
request;
|
|
82
411
|
cookieStore;
|
|
412
|
+
clientId;
|
|
413
|
+
handled;
|
|
414
|
+
preloadResponse;
|
|
415
|
+
resultingClientId;
|
|
83
416
|
#responsePromise;
|
|
84
417
|
#responded;
|
|
85
|
-
|
|
86
|
-
|
|
418
|
+
#platformWaitUntil;
|
|
419
|
+
constructor(request, options) {
|
|
420
|
+
super("fetch", options);
|
|
87
421
|
this.request = request;
|
|
88
422
|
this.cookieStore = new RequestCookieStore(request);
|
|
423
|
+
this.clientId = "";
|
|
424
|
+
this.handled = Promise.resolve(void 0);
|
|
425
|
+
this.preloadResponse = Promise.resolve(void 0);
|
|
426
|
+
this.resultingClientId = "";
|
|
89
427
|
this.#responsePromise = null;
|
|
90
428
|
this.#responded = false;
|
|
429
|
+
this.#platformWaitUntil = options?.platformWaitUntil;
|
|
430
|
+
}
|
|
431
|
+
waitUntil(promise) {
|
|
432
|
+
if (this.#platformWaitUntil) {
|
|
433
|
+
this.#platformWaitUntil(promise);
|
|
434
|
+
}
|
|
435
|
+
super.waitUntil(promise);
|
|
91
436
|
}
|
|
92
437
|
respondWith(response) {
|
|
93
438
|
if (this.#responded) {
|
|
@@ -109,13 +454,17 @@ var FetchEvent = class extends ExtendableEvent {
|
|
|
109
454
|
hasResponded() {
|
|
110
455
|
return this.#responded;
|
|
111
456
|
}
|
|
457
|
+
/** The URL of the request (convenience property) */
|
|
458
|
+
get url() {
|
|
459
|
+
return this.request.url;
|
|
460
|
+
}
|
|
112
461
|
};
|
|
113
|
-
var
|
|
462
|
+
var ShovelInstallEvent = class extends ShovelExtendableEvent {
|
|
114
463
|
constructor(eventInitDict) {
|
|
115
464
|
super("install", eventInitDict);
|
|
116
465
|
}
|
|
117
466
|
};
|
|
118
|
-
var
|
|
467
|
+
var ShovelActivateEvent = class extends ShovelExtendableEvent {
|
|
119
468
|
constructor(eventInitDict) {
|
|
120
469
|
super("activate", eventInitDict);
|
|
121
470
|
}
|
|
@@ -133,9 +482,7 @@ var ShovelClient = class {
|
|
|
133
482
|
}
|
|
134
483
|
// Implementation
|
|
135
484
|
postMessage(_message, _transferOrOptions) {
|
|
136
|
-
|
|
137
|
-
"[ServiceWorker] Client.postMessage() not supported in server context"
|
|
138
|
-
);
|
|
485
|
+
self.loggers.open("platform").warn("Client.postMessage() not supported in server context");
|
|
139
486
|
}
|
|
140
487
|
};
|
|
141
488
|
var ShovelWindowClient = class extends ShovelClient {
|
|
@@ -147,15 +494,11 @@ var ShovelWindowClient = class extends ShovelClient {
|
|
|
147
494
|
this.visibilityState = options.visibilityState || "hidden";
|
|
148
495
|
}
|
|
149
496
|
async focus() {
|
|
150
|
-
|
|
151
|
-
"[ServiceWorker] WindowClient.focus() not supported in server context"
|
|
152
|
-
);
|
|
497
|
+
self.loggers.open("platform").warn("WindowClient.focus() not supported in server context");
|
|
153
498
|
return this;
|
|
154
499
|
}
|
|
155
500
|
async navigate(_url) {
|
|
156
|
-
|
|
157
|
-
"[ServiceWorker] WindowClient.navigate() not supported in server context"
|
|
158
|
-
);
|
|
501
|
+
self.loggers.open("platform").warn("WindowClient.navigate() not supported in server context");
|
|
159
502
|
return null;
|
|
160
503
|
}
|
|
161
504
|
};
|
|
@@ -169,13 +512,11 @@ var ShovelClients = class {
|
|
|
169
512
|
return [];
|
|
170
513
|
}
|
|
171
514
|
async openWindow(_url) {
|
|
172
|
-
|
|
173
|
-
"[ServiceWorker] Clients.openWindow() not supported in server context"
|
|
174
|
-
);
|
|
515
|
+
self.loggers.open("platform").warn("Clients.openWindow() not supported in server context");
|
|
175
516
|
return null;
|
|
176
517
|
}
|
|
177
518
|
};
|
|
178
|
-
var ExtendableMessageEvent = class extends
|
|
519
|
+
var ExtendableMessageEvent = class extends ShovelExtendableEvent {
|
|
179
520
|
data;
|
|
180
521
|
origin;
|
|
181
522
|
lastEventId;
|
|
@@ -205,9 +546,7 @@ var ShovelServiceWorker = class extends EventTarget {
|
|
|
205
546
|
}
|
|
206
547
|
// Implementation
|
|
207
548
|
postMessage(_message, _transferOrOptions) {
|
|
208
|
-
|
|
209
|
-
"[ServiceWorker] ServiceWorker.postMessage() not implemented in server context"
|
|
210
|
-
);
|
|
549
|
+
self.loggers.open("platform").warn("ServiceWorker.postMessage() not implemented in server context");
|
|
211
550
|
}
|
|
212
551
|
// Internal method to update state and dispatch statechange event
|
|
213
552
|
_setState(newState) {
|
|
@@ -239,9 +578,9 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
239
578
|
cookies;
|
|
240
579
|
pushManager;
|
|
241
580
|
onupdatefound;
|
|
242
|
-
constructor(
|
|
581
|
+
constructor(scope = "/", scriptURL = "/") {
|
|
243
582
|
super();
|
|
244
|
-
this.scope =
|
|
583
|
+
this.scope = scope;
|
|
245
584
|
this.updateViaCache = "imports";
|
|
246
585
|
this.navigationPreload = new ShovelNavigationPreloadManager();
|
|
247
586
|
this._serviceWorker = new ShovelServiceWorker(scriptURL, "parsed");
|
|
@@ -264,9 +603,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
264
603
|
return [];
|
|
265
604
|
}
|
|
266
605
|
async showNotification(_title, _options) {
|
|
267
|
-
|
|
268
|
-
"[ServiceWorker] Notifications not supported in server context"
|
|
269
|
-
);
|
|
606
|
+
self.loggers.open("platform").warn("Notifications not supported in server context");
|
|
270
607
|
}
|
|
271
608
|
async sync() {
|
|
272
609
|
}
|
|
@@ -281,11 +618,10 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
281
618
|
* Install the ServiceWorker (Shovel extension)
|
|
282
619
|
*/
|
|
283
620
|
async install() {
|
|
284
|
-
if (this._serviceWorker.state !== "parsed")
|
|
285
|
-
return;
|
|
621
|
+
if (this._serviceWorker.state !== "parsed") return;
|
|
286
622
|
this._serviceWorker._setState("installing");
|
|
287
623
|
return new Promise((resolve, reject) => {
|
|
288
|
-
const event = new
|
|
624
|
+
const event = new ShovelInstallEvent();
|
|
289
625
|
process.nextTick(() => {
|
|
290
626
|
try {
|
|
291
627
|
this.dispatchEvent(event);
|
|
@@ -320,7 +656,7 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
320
656
|
}
|
|
321
657
|
this._serviceWorker._setState("activating");
|
|
322
658
|
return new Promise((resolve, reject) => {
|
|
323
|
-
const event = new
|
|
659
|
+
const event = new ShovelActivateEvent();
|
|
324
660
|
process.nextTick(() => {
|
|
325
661
|
try {
|
|
326
662
|
this.dispatchEvent(event);
|
|
@@ -348,12 +684,16 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
348
684
|
}
|
|
349
685
|
/**
|
|
350
686
|
* Handle a fetch request (Shovel extension)
|
|
687
|
+
*
|
|
688
|
+
* Platforms create a ShovelFetchEvent (or subclass) with platform-specific
|
|
689
|
+
* properties and hooks, then pass it to this method for dispatching.
|
|
690
|
+
*
|
|
691
|
+
* @param event - The fetch event to handle (created by platform adapter)
|
|
351
692
|
*/
|
|
352
|
-
async handleRequest(
|
|
693
|
+
async handleRequest(event) {
|
|
353
694
|
if (this._serviceWorker.state !== "activated") {
|
|
354
695
|
throw new Error("ServiceWorker not activated");
|
|
355
696
|
}
|
|
356
|
-
const event = new FetchEvent(request);
|
|
357
697
|
return cookieStoreStorage.run(event.cookieStore, async () => {
|
|
358
698
|
this.dispatchEvent(event);
|
|
359
699
|
event[kEndDispatchPhase]();
|
|
@@ -363,10 +703,6 @@ var ShovelServiceWorkerRegistration = class extends EventTarget {
|
|
|
363
703
|
);
|
|
364
704
|
}
|
|
365
705
|
const response = await event.getResponse();
|
|
366
|
-
const promises = event.getPromises();
|
|
367
|
-
if (promises.length > 0) {
|
|
368
|
-
Promise.allSettled(promises).catch(logger.error);
|
|
369
|
-
}
|
|
370
706
|
if (event.cookieStore.hasChanges()) {
|
|
371
707
|
const setCookieHeaders = event.cookieStore.getSetCookieHeaders();
|
|
372
708
|
const headers = new Headers(response.headers);
|
|
@@ -412,8 +748,8 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
|
412
748
|
/**
|
|
413
749
|
* Get registration for a specific scope
|
|
414
750
|
*/
|
|
415
|
-
async getRegistration(
|
|
416
|
-
return this.#registrations.get(
|
|
751
|
+
async getRegistration(scope = "/") {
|
|
752
|
+
return this.#registrations.get(scope);
|
|
417
753
|
}
|
|
418
754
|
/**
|
|
419
755
|
* Get all registrations
|
|
@@ -426,26 +762,26 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
|
426
762
|
*/
|
|
427
763
|
async register(scriptURL, options) {
|
|
428
764
|
const url = typeof scriptURL === "string" ? scriptURL : scriptURL.toString();
|
|
429
|
-
const
|
|
430
|
-
let
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
765
|
+
const scope = this.#normalizeScope(options?.scope || "/");
|
|
766
|
+
let registration = this.#registrations.get(scope);
|
|
767
|
+
if (registration) {
|
|
768
|
+
registration._serviceWorker.scriptURL = url;
|
|
769
|
+
registration._serviceWorker._setState("parsed");
|
|
434
770
|
} else {
|
|
435
|
-
|
|
436
|
-
this.#registrations.set(
|
|
771
|
+
registration = new ShovelServiceWorkerRegistration(scope, url);
|
|
772
|
+
this.#registrations.set(scope, registration);
|
|
437
773
|
this.dispatchEvent(new Event("updatefound"));
|
|
438
774
|
}
|
|
439
|
-
return
|
|
775
|
+
return registration;
|
|
440
776
|
}
|
|
441
777
|
/**
|
|
442
778
|
* Unregister a ServiceWorker registration
|
|
443
779
|
*/
|
|
444
|
-
async unregister(
|
|
445
|
-
const
|
|
446
|
-
if (
|
|
447
|
-
await
|
|
448
|
-
this.#registrations.delete(
|
|
780
|
+
async unregister(scope) {
|
|
781
|
+
const registration = this.#registrations.get(scope);
|
|
782
|
+
if (registration) {
|
|
783
|
+
await registration.unregister();
|
|
784
|
+
this.#registrations.delete(scope);
|
|
449
785
|
return true;
|
|
450
786
|
}
|
|
451
787
|
return false;
|
|
@@ -458,9 +794,10 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
|
458
794
|
const pathname = url.pathname;
|
|
459
795
|
const matchingScope = this.#findMatchingScope(pathname);
|
|
460
796
|
if (matchingScope) {
|
|
461
|
-
const
|
|
462
|
-
if (
|
|
463
|
-
|
|
797
|
+
const registration = this.#registrations.get(matchingScope);
|
|
798
|
+
if (registration && registration.ready) {
|
|
799
|
+
const event = new ShovelFetchEvent(request);
|
|
800
|
+
return await registration.handleRequest(event);
|
|
464
801
|
}
|
|
465
802
|
}
|
|
466
803
|
return null;
|
|
@@ -470,9 +807,9 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
|
470
807
|
*/
|
|
471
808
|
async installAll() {
|
|
472
809
|
const installations = Array.from(this.#registrations.values()).map(
|
|
473
|
-
async (
|
|
474
|
-
await
|
|
475
|
-
await
|
|
810
|
+
async (registration) => {
|
|
811
|
+
await registration.install();
|
|
812
|
+
await registration.activate();
|
|
476
813
|
}
|
|
477
814
|
);
|
|
478
815
|
await promiseWithTimeout(
|
|
@@ -492,14 +829,14 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
|
492
829
|
/**
|
|
493
830
|
* Normalize scope to ensure it starts and ends correctly
|
|
494
831
|
*/
|
|
495
|
-
#normalizeScope(
|
|
496
|
-
if (!
|
|
497
|
-
|
|
832
|
+
#normalizeScope(scope) {
|
|
833
|
+
if (!scope.startsWith("/")) {
|
|
834
|
+
scope = "/" + scope;
|
|
498
835
|
}
|
|
499
|
-
if (
|
|
500
|
-
|
|
836
|
+
if (scope !== "/" && !scope.endsWith("/")) {
|
|
837
|
+
scope = scope + "/";
|
|
501
838
|
}
|
|
502
|
-
return
|
|
839
|
+
return scope;
|
|
503
840
|
}
|
|
504
841
|
/**
|
|
505
842
|
* Find the most specific scope that matches a pathname
|
|
@@ -507,9 +844,9 @@ var ShovelServiceWorkerContainer = class extends EventTarget {
|
|
|
507
844
|
#findMatchingScope(pathname) {
|
|
508
845
|
const scopes = Array.from(this.#registrations.keys());
|
|
509
846
|
scopes.sort((a, b) => b.length - a.length);
|
|
510
|
-
for (const
|
|
511
|
-
if (pathname.startsWith(
|
|
512
|
-
return
|
|
847
|
+
for (const scope of scopes) {
|
|
848
|
+
if (pathname.startsWith(scope === "/" ? "/" : scope)) {
|
|
849
|
+
return scope;
|
|
513
850
|
}
|
|
514
851
|
}
|
|
515
852
|
return null;
|
|
@@ -561,9 +898,7 @@ var Notification = class extends EventTarget {
|
|
|
561
898
|
this.onshow = null;
|
|
562
899
|
}
|
|
563
900
|
close() {
|
|
564
|
-
|
|
565
|
-
"[ServiceWorker] Notification.close() not supported in server context"
|
|
566
|
-
);
|
|
901
|
+
self.loggers.open("platform").warn("Notification.close() not supported in server context");
|
|
567
902
|
}
|
|
568
903
|
static async requestPermission() {
|
|
569
904
|
return "denied";
|
|
@@ -571,7 +906,7 @@ var Notification = class extends EventTarget {
|
|
|
571
906
|
// Events: click, close, error, show
|
|
572
907
|
};
|
|
573
908
|
Notification.permission = "denied";
|
|
574
|
-
var NotificationEvent = class extends
|
|
909
|
+
var NotificationEvent = class extends ShovelExtendableEvent {
|
|
575
910
|
action;
|
|
576
911
|
notification;
|
|
577
912
|
reply;
|
|
@@ -582,7 +917,7 @@ var NotificationEvent = class extends ExtendableEvent {
|
|
|
582
917
|
this.reply = eventInitDict.reply ?? null;
|
|
583
918
|
}
|
|
584
919
|
};
|
|
585
|
-
var PushEvent = class extends
|
|
920
|
+
var PushEvent = class extends ShovelExtendableEvent {
|
|
586
921
|
data;
|
|
587
922
|
constructor(type, eventInitDict) {
|
|
588
923
|
super(type, eventInitDict);
|
|
@@ -615,7 +950,7 @@ var ShovelPushMessageData = class {
|
|
|
615
950
|
return new TextDecoder().decode(this._data);
|
|
616
951
|
}
|
|
617
952
|
};
|
|
618
|
-
var SyncEvent = class extends
|
|
953
|
+
var SyncEvent = class extends ShovelExtendableEvent {
|
|
619
954
|
tag;
|
|
620
955
|
lastChance;
|
|
621
956
|
constructor(type, eventInitDict) {
|
|
@@ -637,7 +972,9 @@ var ServiceWorkerGlobals = class {
|
|
|
637
972
|
registration;
|
|
638
973
|
// Storage APIs
|
|
639
974
|
caches;
|
|
640
|
-
|
|
975
|
+
directories;
|
|
976
|
+
databases;
|
|
977
|
+
loggers;
|
|
641
978
|
// Clients API
|
|
642
979
|
// Our custom Clients implementation provides core functionality compatible with the Web API
|
|
643
980
|
clients;
|
|
@@ -664,9 +1001,7 @@ var ServiceWorkerGlobals = class {
|
|
|
664
1001
|
crypto;
|
|
665
1002
|
// WorkerGlobalScope methods (stubs for server context)
|
|
666
1003
|
importScripts(..._urls) {
|
|
667
|
-
|
|
668
|
-
"[ServiceWorker] importScripts() not supported in server context"
|
|
669
|
-
);
|
|
1004
|
+
self.loggers.open("platform").warn("importScripts() not supported in server context");
|
|
670
1005
|
}
|
|
671
1006
|
atob(data) {
|
|
672
1007
|
return globalThis.atob(data);
|
|
@@ -700,14 +1035,15 @@ var ServiceWorkerGlobals = class {
|
|
|
700
1035
|
}
|
|
701
1036
|
const request = new Request(new URL(urlString, "http://localhost"), init);
|
|
702
1037
|
return fetchDepthStorage.run(currentDepth + 1, () => {
|
|
703
|
-
|
|
1038
|
+
const event = new ShovelFetchEvent(request);
|
|
1039
|
+
return this.registration.handleRequest(event);
|
|
704
1040
|
});
|
|
705
1041
|
}
|
|
706
1042
|
queueMicrotask(callback) {
|
|
707
1043
|
globalThis.queueMicrotask(callback);
|
|
708
1044
|
}
|
|
709
1045
|
reportError(e) {
|
|
710
|
-
|
|
1046
|
+
getLogger(["shovel", "platform"]).error`reportError: ${e}`;
|
|
711
1047
|
}
|
|
712
1048
|
setInterval(handler, timeout, ...args) {
|
|
713
1049
|
return globalThis.setInterval(handler, timeout, ...args);
|
|
@@ -747,7 +1083,9 @@ var ServiceWorkerGlobals = class {
|
|
|
747
1083
|
this.self = globalThis;
|
|
748
1084
|
this.registration = options.registration;
|
|
749
1085
|
this.caches = options.caches;
|
|
750
|
-
this.
|
|
1086
|
+
this.directories = options.directories;
|
|
1087
|
+
this.databases = options.databases;
|
|
1088
|
+
this.loggers = options.loggers;
|
|
751
1089
|
this.#isDevelopment = options.isDevelopment ?? false;
|
|
752
1090
|
this.clients = this.#createClientsAPI();
|
|
753
1091
|
this.serviceWorker = null;
|
|
@@ -783,28 +1121,48 @@ var ServiceWorkerGlobals = class {
|
|
|
783
1121
|
* Allows the ServiceWorker to activate immediately
|
|
784
1122
|
*/
|
|
785
1123
|
async skipWaiting() {
|
|
786
|
-
|
|
1124
|
+
getLogger(["shovel", "platform"]).info("skipWaiting() called");
|
|
787
1125
|
if (!this.#isDevelopment) {
|
|
788
|
-
|
|
789
|
-
"
|
|
1126
|
+
getLogger(["shovel", "platform"]).info(
|
|
1127
|
+
"skipWaiting() - production graceful restart not implemented"
|
|
790
1128
|
);
|
|
791
1129
|
}
|
|
792
1130
|
}
|
|
793
1131
|
/**
|
|
794
|
-
* Event target delegation to registration
|
|
1132
|
+
* Event target delegation - ServiceWorker events go to registration,
|
|
1133
|
+
* other events (like "message" for worker threads) go to native handler
|
|
795
1134
|
*/
|
|
796
1135
|
addEventListener(type, listener, options) {
|
|
797
|
-
if (listener)
|
|
1136
|
+
if (!listener) return;
|
|
1137
|
+
if (isServiceWorkerEvent(type)) {
|
|
798
1138
|
this.registration.addEventListener(type, listener, options);
|
|
1139
|
+
} else {
|
|
1140
|
+
const original = this.#originals.addEventListener;
|
|
1141
|
+
if (original) {
|
|
1142
|
+
original.call(globalThis, type, listener, options);
|
|
1143
|
+
}
|
|
799
1144
|
}
|
|
800
1145
|
}
|
|
801
1146
|
removeEventListener(type, listener, options) {
|
|
802
|
-
if (listener)
|
|
1147
|
+
if (!listener) return;
|
|
1148
|
+
if (isServiceWorkerEvent(type)) {
|
|
803
1149
|
this.registration.removeEventListener(type, listener, options);
|
|
1150
|
+
} else {
|
|
1151
|
+
const original = this.#originals.removeEventListener;
|
|
1152
|
+
if (original) {
|
|
1153
|
+
original.call(globalThis, type, listener, options);
|
|
1154
|
+
}
|
|
804
1155
|
}
|
|
805
1156
|
}
|
|
806
1157
|
dispatchEvent(event) {
|
|
807
|
-
|
|
1158
|
+
if (isServiceWorkerEvent(event.type)) {
|
|
1159
|
+
return this.registration.dispatchEvent(event);
|
|
1160
|
+
}
|
|
1161
|
+
const original = this.#originals.dispatchEvent;
|
|
1162
|
+
if (original) {
|
|
1163
|
+
return original.call(globalThis, event);
|
|
1164
|
+
}
|
|
1165
|
+
return false;
|
|
808
1166
|
}
|
|
809
1167
|
/**
|
|
810
1168
|
* Create Clients API implementation
|
|
@@ -828,7 +1186,11 @@ var ServiceWorkerGlobals = class {
|
|
|
828
1186
|
g.removeEventListener = this.removeEventListener.bind(this);
|
|
829
1187
|
g.dispatchEvent = this.dispatchEvent.bind(this);
|
|
830
1188
|
g.caches = this.caches;
|
|
831
|
-
g.
|
|
1189
|
+
g.directories = this.directories;
|
|
1190
|
+
if (this.databases) {
|
|
1191
|
+
g.databases = this.databases;
|
|
1192
|
+
}
|
|
1193
|
+
g.loggers = this.loggers;
|
|
832
1194
|
g.registration = this.registration;
|
|
833
1195
|
g.skipWaiting = this.skipWaiting.bind(this);
|
|
834
1196
|
g.clients = this.clients;
|
|
@@ -854,150 +1216,112 @@ var ServiceWorkerGlobals = class {
|
|
|
854
1216
|
}
|
|
855
1217
|
}
|
|
856
1218
|
};
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1219
|
+
function isClass(fn) {
|
|
1220
|
+
return typeof fn === "function" && fn.prototype !== void 0;
|
|
1221
|
+
}
|
|
1222
|
+
function createDirectoryFactory(configs) {
|
|
1223
|
+
return async (name) => {
|
|
1224
|
+
const config = configs[name];
|
|
1225
|
+
if (!config) {
|
|
1226
|
+
throw new Error(
|
|
1227
|
+
`Directory "${name}" is not configured. Available directories: ${Object.keys(configs).join(", ") || "(none)"}`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const { impl, ...dirOptions } = config;
|
|
1231
|
+
if (!impl) {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`Directory "${name}" has no impl. Ensure the directory module is configured.`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (isClass(impl)) {
|
|
1237
|
+
return new impl(name, dirOptions);
|
|
863
1238
|
} else {
|
|
864
|
-
|
|
1239
|
+
return impl(name, dirOptions);
|
|
865
1240
|
}
|
|
866
1241
|
};
|
|
867
|
-
onmessage = function(event) {
|
|
868
|
-
void handleMessage(event.data);
|
|
869
|
-
};
|
|
870
|
-
return { messagePort, sendMessage: sendMessage2 };
|
|
871
|
-
}
|
|
872
|
-
var registration = null;
|
|
873
|
-
var scope = null;
|
|
874
|
-
var _workerSelf = null;
|
|
875
|
-
var currentApp = null;
|
|
876
|
-
var serviceWorkerReady = false;
|
|
877
|
-
var loadedEntrypoint = null;
|
|
878
|
-
var caches;
|
|
879
|
-
var buckets;
|
|
880
|
-
async function handleFetchEvent(request) {
|
|
881
|
-
if (!currentApp || !serviceWorkerReady) {
|
|
882
|
-
throw new Error("ServiceWorker not ready");
|
|
883
|
-
}
|
|
884
|
-
if (!registration) {
|
|
885
|
-
throw new Error("ServiceWorker runtime not initialized");
|
|
886
|
-
}
|
|
887
|
-
try {
|
|
888
|
-
const response = await registration.handleRequest(request);
|
|
889
|
-
return response;
|
|
890
|
-
} catch (error) {
|
|
891
|
-
logger.error("[Worker] ServiceWorker request failed: {error}", { error });
|
|
892
|
-
console.error("[Worker] ServiceWorker request failed:", error);
|
|
893
|
-
const response = new Response("ServiceWorker request failed", {
|
|
894
|
-
status: 500
|
|
895
|
-
});
|
|
896
|
-
return response;
|
|
897
|
-
}
|
|
898
1242
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
|
|
908
|
-
`
|
|
1243
|
+
function createCacheFactory(options) {
|
|
1244
|
+
const { configs, usePostMessage = false } = options;
|
|
1245
|
+
return async (name) => {
|
|
1246
|
+
if (usePostMessage) {
|
|
1247
|
+
return new PostMessageCache(name);
|
|
1248
|
+
}
|
|
1249
|
+
const config = configs[name];
|
|
1250
|
+
if (!config) {
|
|
1251
|
+
throw new Error(
|
|
1252
|
+
`Cache "${name}" is not configured. Available caches: ${Object.keys(configs).join(", ") || "(none)"}`
|
|
909
1253
|
);
|
|
910
|
-
logger.info("[Worker] Creating completely fresh ServiceWorker context");
|
|
911
|
-
registration = new ShovelServiceWorkerRegistration();
|
|
912
|
-
if (!caches || !buckets) {
|
|
913
|
-
throw new Error("Runtime not initialized - missing caches or buckets");
|
|
914
|
-
}
|
|
915
|
-
scope = new ServiceWorkerGlobals({
|
|
916
|
-
registration,
|
|
917
|
-
caches,
|
|
918
|
-
buckets
|
|
919
|
-
});
|
|
920
|
-
scope.install();
|
|
921
|
-
_workerSelf = scope;
|
|
922
|
-
currentApp = null;
|
|
923
|
-
serviceWorkerReady = false;
|
|
924
1254
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1255
|
+
const { impl, ...cacheOptions } = config;
|
|
1256
|
+
if (!impl) {
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Cache "${name}" has no impl. Ensure the cache module is configured.`
|
|
1259
|
+
);
|
|
930
1260
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
throw new Error("ServiceWorker runtime not initialized");
|
|
1261
|
+
if (isClass(impl)) {
|
|
1262
|
+
return new impl(name, cacheOptions);
|
|
1263
|
+
} else {
|
|
1264
|
+
return impl(name, cacheOptions);
|
|
936
1265
|
}
|
|
937
|
-
|
|
938
|
-
await registration.activate();
|
|
939
|
-
serviceWorkerReady = true;
|
|
940
|
-
logger.info(
|
|
941
|
-
`[Worker] ServiceWorker loaded and activated from ${entrypoint}`
|
|
942
|
-
);
|
|
943
|
-
} catch (error) {
|
|
944
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
945
|
-
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
946
|
-
logger.error("[Worker] Failed to load ServiceWorker", {
|
|
947
|
-
error: errorMessage,
|
|
948
|
-
stack: errorStack,
|
|
949
|
-
entrypoint
|
|
950
|
-
});
|
|
951
|
-
serviceWorkerReady = false;
|
|
952
|
-
throw error;
|
|
953
|
-
}
|
|
1266
|
+
};
|
|
954
1267
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1268
|
+
async function initWorkerRuntime(options) {
|
|
1269
|
+
const { config } = options;
|
|
1270
|
+
const runtimeLogger = getLogger(["shovel", "platform"]);
|
|
1271
|
+
if (config?.logging) {
|
|
1272
|
+
await configureLogging(config.logging);
|
|
1273
|
+
}
|
|
1274
|
+
runtimeLogger.debug("Initializing worker runtime");
|
|
1275
|
+
const caches = new CustomCacheStorage(
|
|
1276
|
+
createCacheFactory({
|
|
1277
|
+
configs: config?.caches ?? {},
|
|
1278
|
+
usePostMessage: true
|
|
1279
|
+
})
|
|
1280
|
+
);
|
|
1281
|
+
const directories = new CustomDirectoryStorage(
|
|
1282
|
+
createDirectoryFactory(config?.directories ?? {})
|
|
1283
|
+
);
|
|
1284
|
+
let databases;
|
|
1285
|
+
if (config?.databases && Object.keys(config.databases).length > 0) {
|
|
1286
|
+
const factory = createDatabaseFactory(config.databases);
|
|
1287
|
+
databases = new CustomDatabaseStorage(factory);
|
|
1288
|
+
}
|
|
1289
|
+
const loggers = new CustomLoggerStorage(
|
|
1290
|
+
(categories) => getLogger(categories)
|
|
1291
|
+
);
|
|
1292
|
+
const registration = new ShovelServiceWorkerRegistration();
|
|
1293
|
+
const scope = new ServiceWorkerGlobals({
|
|
1294
|
+
registration,
|
|
1295
|
+
caches,
|
|
1296
|
+
directories,
|
|
1297
|
+
databases,
|
|
1298
|
+
loggers
|
|
1299
|
+
});
|
|
1300
|
+
scope.install();
|
|
1301
|
+
runtimeLogger.debug("Worker runtime initialized");
|
|
1302
|
+
return { registration, scope, caches, directories, databases, loggers };
|
|
1303
|
+
}
|
|
1304
|
+
function startWorkerMessageLoop(options) {
|
|
1305
|
+
const registration = options instanceof ShovelServiceWorkerRegistration ? options : options.registration;
|
|
1306
|
+
const databases = options instanceof ShovelServiceWorkerRegistration ? void 0 : options.databases;
|
|
1307
|
+
const messageLogger = getLogger(["shovel", "platform"]);
|
|
1308
|
+
const workerId = Math.random().toString(36).substring(2, 8);
|
|
1309
|
+
function sendMessage(message, transfer) {
|
|
1310
|
+
if (transfer && transfer.length > 0) {
|
|
1311
|
+
postMessage(message, transfer);
|
|
1312
|
+
} else {
|
|
1313
|
+
postMessage(message);
|
|
961
1314
|
}
|
|
962
|
-
logger.info(`[Worker-${workerId}] Initializing runtime with config`, {
|
|
963
|
-
config,
|
|
964
|
-
baseDir
|
|
965
|
-
});
|
|
966
|
-
logger.info(`[Worker-${workerId}] Creating cache storage`);
|
|
967
|
-
caches = new CustomCacheStorage(createCacheFactory({ config }));
|
|
968
|
-
logger.info(`[Worker-${workerId}] Creating bucket storage`);
|
|
969
|
-
buckets = new CustomBucketStorage(createBucketFactory({ baseDir, config }));
|
|
970
|
-
logger.info(`[Worker-${workerId}] Creating and installing scope`);
|
|
971
|
-
registration = new ShovelServiceWorkerRegistration();
|
|
972
|
-
scope = new ServiceWorkerGlobals({ registration, caches, buckets });
|
|
973
|
-
scope.install();
|
|
974
|
-
_workerSelf = scope;
|
|
975
|
-
logger.info(`[Worker-${workerId}] Runtime initialized successfully`);
|
|
976
|
-
} catch (error) {
|
|
977
|
-
logger.error(`[Worker-${workerId}] Failed to initialize runtime`, { error });
|
|
978
|
-
throw error;
|
|
979
1315
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
await initializeRuntime(initMsg.config, initMsg.baseDir);
|
|
987
|
-
logger.info(`[Worker-${workerId}] Sending initialized message`);
|
|
988
|
-
sendMessage({ type: "initialized" });
|
|
989
|
-
} else if (message.type === "load") {
|
|
990
|
-
const loadMsg = message;
|
|
991
|
-
await loadServiceWorker(loadMsg.entrypoint);
|
|
992
|
-
sendMessage({ type: "ready", entrypoint: loadMsg.entrypoint });
|
|
993
|
-
} else if (message.type === "request") {
|
|
994
|
-
const reqMsg = message;
|
|
995
|
-
const request = new Request(reqMsg.request.url, {
|
|
996
|
-
method: reqMsg.request.method,
|
|
997
|
-
headers: reqMsg.request.headers,
|
|
998
|
-
body: reqMsg.request.body
|
|
1316
|
+
async function handleFetchRequest(message) {
|
|
1317
|
+
try {
|
|
1318
|
+
const request = new Request(message.request.url, {
|
|
1319
|
+
method: message.request.method,
|
|
1320
|
+
headers: message.request.headers,
|
|
1321
|
+
body: message.request.body
|
|
999
1322
|
});
|
|
1000
|
-
const
|
|
1323
|
+
const event = new ShovelFetchEvent(request);
|
|
1324
|
+
const response = await registration.handleRequest(event);
|
|
1001
1325
|
const body = await response.arrayBuffer();
|
|
1002
1326
|
const headers = Object.fromEntries(response.headers.entries());
|
|
1003
1327
|
if (!headers["Content-Type"] && !headers["content-type"]) {
|
|
@@ -1011,41 +1335,151 @@ async function handleMessage(message) {
|
|
|
1011
1335
|
headers,
|
|
1012
1336
|
body
|
|
1013
1337
|
},
|
|
1014
|
-
requestID:
|
|
1338
|
+
requestID: message.requestID
|
|
1015
1339
|
};
|
|
1016
1340
|
sendMessage(responseMsg, [body]);
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
messageLogger.error(`[Worker-${workerId}] Request failed: {error}`, {
|
|
1343
|
+
error
|
|
1344
|
+
});
|
|
1345
|
+
const errorMsg = {
|
|
1346
|
+
type: "error",
|
|
1347
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1348
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1349
|
+
requestID: message.requestID
|
|
1350
|
+
};
|
|
1351
|
+
sendMessage(errorMsg);
|
|
1017
1352
|
}
|
|
1018
|
-
} catch (error) {
|
|
1019
|
-
const errorMsg = {
|
|
1020
|
-
type: "error",
|
|
1021
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1022
|
-
stack: error instanceof Error ? error.stack : void 0,
|
|
1023
|
-
requestID: message.requestID
|
|
1024
|
-
};
|
|
1025
|
-
sendMessage(errorMsg);
|
|
1026
1353
|
}
|
|
1354
|
+
function handleMessage(event) {
|
|
1355
|
+
const message = event.data;
|
|
1356
|
+
if (message?.type === "cache:response" || message?.type === "cache:error") {
|
|
1357
|
+
messageLogger.debug(`[Worker-${workerId}] Forwarding cache message`, {
|
|
1358
|
+
type: message.type,
|
|
1359
|
+
requestID: message.requestID
|
|
1360
|
+
});
|
|
1361
|
+
handleCacheResponse(message);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (message?.type === "request") {
|
|
1365
|
+
handleFetchRequest(message).catch((error) => {
|
|
1366
|
+
messageLogger.error(`[Worker-${workerId}] Unhandled error: {error}`, {
|
|
1367
|
+
error
|
|
1368
|
+
});
|
|
1369
|
+
});
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (message?.type === "shutdown") {
|
|
1373
|
+
messageLogger.debug(`[Worker-${workerId}] Received shutdown signal`);
|
|
1374
|
+
(async () => {
|
|
1375
|
+
try {
|
|
1376
|
+
if (databases) {
|
|
1377
|
+
await databases.closeAll();
|
|
1378
|
+
messageLogger.debug(`[Worker-${workerId}] Databases closed`);
|
|
1379
|
+
}
|
|
1380
|
+
sendMessage({ type: "shutdown-complete" });
|
|
1381
|
+
messageLogger.debug(`[Worker-${workerId}] Shutdown complete`);
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
messageLogger.error(`[Worker-${workerId}] Shutdown error: {error}`, {
|
|
1384
|
+
error
|
|
1385
|
+
});
|
|
1386
|
+
sendMessage({ type: "shutdown-complete" });
|
|
1387
|
+
}
|
|
1388
|
+
})();
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (message?.type) {
|
|
1392
|
+
messageLogger.debug(`[Worker-${workerId}] Unknown message type`, {
|
|
1393
|
+
type: message.type
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
self.addEventListener("message", handleMessage);
|
|
1398
|
+
sendMessage({ type: "ready" });
|
|
1399
|
+
messageLogger.debug(`[Worker-${workerId}] Message loop started`);
|
|
1027
1400
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1401
|
+
var SHOVEL_DEFAULT_LOGGERS = [
|
|
1402
|
+
{ category: ["shovel"], level: "info", sinks: ["console"] },
|
|
1403
|
+
{ category: ["logtape", "meta"], level: "warning", sinks: ["console"] }
|
|
1404
|
+
];
|
|
1405
|
+
async function createSink(config) {
|
|
1406
|
+
const {
|
|
1407
|
+
impl,
|
|
1408
|
+
path,
|
|
1409
|
+
// Extract path for file-based sinks
|
|
1410
|
+
...sinkOptions
|
|
1411
|
+
} = config;
|
|
1412
|
+
if (!impl) {
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
`Sink has no impl. Ensure the sink module is configured in shovel.json.`
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
if (path !== void 0) {
|
|
1418
|
+
return impl(path, sinkOptions);
|
|
1419
|
+
} else if (Object.keys(sinkOptions).length > 0) {
|
|
1420
|
+
return impl(sinkOptions);
|
|
1421
|
+
} else {
|
|
1422
|
+
return impl();
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
function normalizeCategory(category) {
|
|
1426
|
+
return typeof category === "string" ? [category] : category;
|
|
1427
|
+
}
|
|
1428
|
+
async function configureLogging(loggingConfig) {
|
|
1429
|
+
const userSinks = loggingConfig.sinks || {};
|
|
1430
|
+
const userLoggers = loggingConfig.loggers || [];
|
|
1431
|
+
const sinks = {
|
|
1432
|
+
console: getConsoleSink()
|
|
1433
|
+
};
|
|
1434
|
+
for (const [name, config] of Object.entries(userSinks)) {
|
|
1435
|
+
sinks[name] = await createSink(config);
|
|
1436
|
+
}
|
|
1437
|
+
const userCategoryKeys = new Set(
|
|
1438
|
+
userLoggers.map((l) => JSON.stringify(normalizeCategory(l.category)))
|
|
1439
|
+
);
|
|
1440
|
+
const mergedLoggers = [
|
|
1441
|
+
// Shovel defaults (unless overridden by user)
|
|
1442
|
+
...SHOVEL_DEFAULT_LOGGERS.filter(
|
|
1443
|
+
(l) => !userCategoryKeys.has(JSON.stringify(normalizeCategory(l.category)))
|
|
1444
|
+
),
|
|
1445
|
+
// User loggers
|
|
1446
|
+
...userLoggers
|
|
1447
|
+
];
|
|
1448
|
+
const loggers = mergedLoggers.map((loggerConfig) => {
|
|
1449
|
+
const result = {
|
|
1450
|
+
category: normalizeCategory(loggerConfig.category)
|
|
1451
|
+
};
|
|
1452
|
+
if (loggerConfig.level) {
|
|
1453
|
+
result.lowestLevel = loggerConfig.level;
|
|
1454
|
+
}
|
|
1455
|
+
result.sinks = loggerConfig.sinks ?? ["console"];
|
|
1456
|
+
if (loggerConfig.parentSinks) {
|
|
1457
|
+
result.parentSinks = loggerConfig.parentSinks;
|
|
1458
|
+
}
|
|
1459
|
+
return result;
|
|
1460
|
+
});
|
|
1461
|
+
await configure({
|
|
1462
|
+
reset: true,
|
|
1463
|
+
sinks,
|
|
1464
|
+
loggers
|
|
1034
1465
|
});
|
|
1035
1466
|
}
|
|
1036
1467
|
export {
|
|
1037
|
-
|
|
1468
|
+
CustomDatabaseStorage,
|
|
1469
|
+
CustomLoggerStorage,
|
|
1038
1470
|
DedicatedWorkerGlobalScope,
|
|
1039
|
-
ExtendableEvent,
|
|
1040
1471
|
ExtendableMessageEvent,
|
|
1041
|
-
FetchEvent,
|
|
1042
|
-
InstallEvent,
|
|
1043
1472
|
Notification,
|
|
1044
1473
|
NotificationEvent,
|
|
1045
1474
|
PushEvent,
|
|
1475
|
+
RequestCookieStore,
|
|
1046
1476
|
ServiceWorkerGlobals,
|
|
1477
|
+
ShovelActivateEvent,
|
|
1047
1478
|
ShovelClient,
|
|
1048
1479
|
ShovelClients,
|
|
1480
|
+
ShovelExtendableEvent,
|
|
1481
|
+
ShovelFetchEvent,
|
|
1482
|
+
ShovelInstallEvent,
|
|
1049
1483
|
ShovelNavigationPreloadManager,
|
|
1050
1484
|
ShovelPushMessageData,
|
|
1051
1485
|
ShovelServiceWorker,
|
|
@@ -1053,5 +1487,14 @@ export {
|
|
|
1053
1487
|
ShovelServiceWorkerRegistration,
|
|
1054
1488
|
ShovelWindowClient,
|
|
1055
1489
|
SyncEvent,
|
|
1056
|
-
WorkerGlobalScope
|
|
1490
|
+
WorkerGlobalScope,
|
|
1491
|
+
configureLogging,
|
|
1492
|
+
createCacheFactory,
|
|
1493
|
+
createDatabaseFactory,
|
|
1494
|
+
createDirectoryFactory,
|
|
1495
|
+
initWorkerRuntime,
|
|
1496
|
+
parseCookieHeader,
|
|
1497
|
+
parseSetCookieHeader,
|
|
1498
|
+
serializeCookie,
|
|
1499
|
+
startWorkerMessageLoop
|
|
1057
1500
|
};
|