@statewalker/webrun-http-browser 0.3.2 → 0.3.4
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/LICENSE +1 -1
- package/README.md +321 -50
- package/demo/demo-1.html +109 -194
- package/demo/demo-2.html +119 -580
- package/dist/core/data-calls.d.ts +5 -0
- package/dist/core/data-calls.d.ts.map +1 -0
- package/dist/core/data-channels.d.ts +17 -0
- package/dist/core/data-channels.d.ts.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/message-target.d.ts +16 -0
- package/dist/core/message-target.d.ts.map +1 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/http/http-send-recieve.d.ts +23 -0
- package/dist/http/http-send-recieve.d.ts.map +1 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2541 -807
- package/dist/relay/index-sw.d.ts +7 -0
- package/dist/relay/index-sw.d.ts.map +1 -0
- package/dist/relay/index.d.ts +69 -0
- package/dist/relay/index.d.ts.map +1 -0
- package/dist/relay/split-service-url.d.ts +13 -0
- package/dist/relay/split-service-url.d.ts.map +1 -0
- package/dist/relay-sw.d.ts +2 -0
- package/dist/relay-sw.d.ts.map +1 -0
- package/dist/relay-sw.js +878 -0
- package/dist/sw/http-sw-dispatcher.d.ts +28 -0
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -0
- package/dist/sw/index.d.ts +3 -0
- package/dist/sw/index.d.ts.map +1 -0
- package/dist/sw/sw-dispatcher.d.ts +74 -0
- package/dist/sw/sw-dispatcher.d.ts.map +1 -0
- package/dist/sw-worker.d.ts +2 -0
- package/dist/sw-worker.d.ts.map +1 -0
- package/dist/sw-worker.js +813 -0
- package/dist/sw.d.ts +2 -0
- package/dist/sw.d.ts.map +1 -0
- package/dist/sw.js +1076 -0
- package/package.json +50 -37
- package/public/index.html +61 -97
- package/public/index.js +7 -4
- package/public/sw-worker.js +4 -0
- package/public-relay/heartbeat.js +4 -3
- package/public-relay/relay-sw.js +4 -6
- package/public-relay/relay.html +16 -11
- package/src/core/data-calls.ts +46 -0
- package/src/core/data-channels.ts +224 -0
- package/src/core/index.ts +9 -0
- package/src/core/message-target.ts +18 -0
- package/src/core/registry.ts +54 -0
- package/src/http/http-send-recieve.ts +81 -0
- package/src/http/index.ts +7 -0
- package/src/index.ts +3 -0
- package/src/relay/index-sw.ts +145 -0
- package/src/relay/index.ts +261 -0
- package/src/relay/split-service-url.ts +30 -0
- package/src/relay-sw.ts +7 -0
- package/src/sw/http-sw-dispatcher.ts +122 -0
- package/src/sw/index.ts +2 -0
- package/src/sw/sw-dispatcher.ts +307 -0
- package/src/sw-worker.ts +7 -0
- package/src/sw.ts +1 -0
- package/CHANGELOG.md +0 -1
- package/demo/demo-3.html +0 -167
- package/dist/index-sw-umd.js +0 -798
- package/dist/index-sw-umd.min.js +0 -2
- package/dist/index-sw.js +0 -791
- package/dist/index-sw.min.js +0 -2
- package/dist/index-umd.js +0 -853
- package/dist/index-umd.min.js +0 -2
- package/dist/index.min.js +0 -2
- package/index.js +0 -1
- package/src/core/data-calls.js +0 -36
- package/src/core/data-channels.js +0 -192
- package/src/core/data-send-recieve.js +0 -28
- package/src/core/errors.js +0 -13
- package/src/core/index.js +0 -4
- package/src/http/HttpError.js +0 -59
- package/src/http/http-send-recieve.js +0 -40
- package/src/http/http-stubs.js +0 -135
- package/src/http/index.js +0 -3
- package/src/http/readable-streams.js +0 -33
- package/src/index.js +0 -7
- package/src/relay/index-sw.js +0 -216
- package/src/relay/index.js +0 -263
- package/src/relay/splitServiceUrl.js +0 -24
- package/src/sw/http-sw-dispatcher.js +0 -128
- package/src/sw/sw-dispatcher.js +0 -264
package/dist/relay-sw.js
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
(function() {
|
|
2
|
+
//#region ../webrun-http-streams/src/bytes.ts
|
|
3
|
+
function toAsyncIterator(input) {
|
|
4
|
+
const asyncIter = input[Symbol.asyncIterator];
|
|
5
|
+
if (asyncIter) return asyncIter.call(input);
|
|
6
|
+
const syncIter = input[Symbol.iterator]();
|
|
7
|
+
return { next() {
|
|
8
|
+
return Promise.resolve(syncIter.next());
|
|
9
|
+
} };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Discard an iterable we are contractually forbidden from consuming — a body
|
|
13
|
+
* skipped for HEAD/204, or one abandoned because the peer reported an error.
|
|
14
|
+
*
|
|
15
|
+
* The `.next()` is not optional: `.return()` on a generator still in suspended
|
|
16
|
+
* start is a no-op, so the body never runs and its `try/finally` never unwinds.
|
|
17
|
+
* Without it a wrapped ReadableStream or socket is never cancelled.
|
|
18
|
+
*/
|
|
19
|
+
async function discard(source) {
|
|
20
|
+
if (source === void 0) return;
|
|
21
|
+
const it = toAsyncIterator(source);
|
|
22
|
+
try {
|
|
23
|
+
await it.next();
|
|
24
|
+
await it.return?.();
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region ../webrun-streams/src/errors.ts
|
|
29
|
+
function serializeError(error) {
|
|
30
|
+
if (error instanceof Error) {
|
|
31
|
+
const out = {
|
|
32
|
+
message: error.message,
|
|
33
|
+
stack: error.stack
|
|
34
|
+
};
|
|
35
|
+
const bag = error;
|
|
36
|
+
for (const key of Object.keys(bag)) out[key] = bag[key];
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
if (typeof error === "object" && error !== null) {
|
|
40
|
+
const bag = error;
|
|
41
|
+
return {
|
|
42
|
+
message: String(bag.message ?? error),
|
|
43
|
+
...bag
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { message: String(error) };
|
|
47
|
+
}
|
|
48
|
+
function deserializeError(error) {
|
|
49
|
+
const payload = typeof error === "string" ? { message: error } : error;
|
|
50
|
+
return Object.assign(new Error(payload.message), payload);
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region ../webrun-streams/src/new-async-generator.ts
|
|
54
|
+
/**
|
|
55
|
+
* The newAsyncGenerator function creates async generators from callback-based initialization
|
|
56
|
+
* functions, providing a bridge between imperative event handling and declarative async
|
|
57
|
+
* iteration patterns.
|
|
58
|
+
*
|
|
59
|
+
* The initialization function receives two callback functions that control the generator's
|
|
60
|
+
* behavior. The `next` function yields values to consumers, while the `done` function
|
|
61
|
+
* signals completion or error conditions. The initialization function can return a cleanup
|
|
62
|
+
* function that will be called when the generator is closed or an error occurs.
|
|
63
|
+
*
|
|
64
|
+
* The generator manages an internal queue of values and completion signals, ensuring that
|
|
65
|
+
* producers can yield values without overwhelming consumers. Proper backpressure is implemented
|
|
66
|
+
* by having the `next` and `done` functions return promises that resolve only after the
|
|
67
|
+
* consumer has processed the values. This prevents memory leaks and ensures that producers
|
|
68
|
+
* are aware of whether their values were successfully handled.
|
|
69
|
+
* The generator also handles cleanup by draining any remaining items in the queue and notifying
|
|
70
|
+
* producers that their values were not processed if the generator is closed early. This ensures
|
|
71
|
+
* that resources are properly managed and that no memory leaks occur.
|
|
72
|
+
*
|
|
73
|
+
* Example usage:
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const asyncGen = newAsyncGenerator<number>((next, done) => {
|
|
76
|
+
* let count = 0;
|
|
77
|
+
* const interval = setInterval(() => {
|
|
78
|
+
* if (count < 5) {
|
|
79
|
+
* next(count++);
|
|
80
|
+
* } else {
|
|
81
|
+
* done();
|
|
82
|
+
* clearInterval(interval);
|
|
83
|
+
* }
|
|
84
|
+
* }, 1000);
|
|
85
|
+
* return () => clearInterval(interval); // Cleanup function
|
|
86
|
+
* });
|
|
87
|
+
* (async () => {
|
|
88
|
+
* for await (const num of asyncGen) {
|
|
89
|
+
* console.log(num); // Logs numbers 0 to 4 at 1 second intervals
|
|
90
|
+
* }
|
|
91
|
+
* console.log("Completed");
|
|
92
|
+
* })();
|
|
93
|
+
* ```
|
|
94
|
+
* @template T The type of values yielded by the generator
|
|
95
|
+
* @template E The type of errors that can be thrown; defaults to Error
|
|
96
|
+
* @param init Initialization function that sets up the generator behavior with next/done callbacks;
|
|
97
|
+
* The first parameter - `next` function - is used to yield values to consumers;
|
|
98
|
+
* It returns a Promise<boolean> indicating whether the value was successfully handled;
|
|
99
|
+
* The second parameter - `done` function - is used to signal completion or error;
|
|
100
|
+
* It returns a Promise<boolean> indicating whether the completion was successfully handled;
|
|
101
|
+
* The initialization function can optionally return a cleanup function that will be called
|
|
102
|
+
* when the generator is closed or an error occurs;
|
|
103
|
+
* @param skipValues If true, only the most recent value is kept in the queue,
|
|
104
|
+
* skipping intermediate values (not consumed values are considered skipped);
|
|
105
|
+
* This is useful for scenarios where only the latest value matters, such as UI updates.
|
|
106
|
+
* Defaults to false, meaning all values are queued and processed in order.
|
|
107
|
+
* @returns AsyncGenerator that properly manages backpressure and resource cleanup
|
|
108
|
+
*/
|
|
109
|
+
async function* newAsyncGenerator(init, skipValues = false) {
|
|
110
|
+
let head;
|
|
111
|
+
let tail;
|
|
112
|
+
/** Flag to prevent new values from being queued after generator closes */
|
|
113
|
+
let closed = false;
|
|
114
|
+
/** Wake-up function to notify the generator loop of new items */
|
|
115
|
+
let wakeUp;
|
|
116
|
+
/**
|
|
117
|
+
* Drains the internal queue, notifying all pending producers that their values
|
|
118
|
+
* were not processed due to generator closure. This prevents memory leaks and
|
|
119
|
+
* ensures proper backpressure signaling.
|
|
120
|
+
*/
|
|
121
|
+
const drainQueue = () => {
|
|
122
|
+
for (; head; head = head.next) {
|
|
123
|
+
closed = closed || head.done;
|
|
124
|
+
head.resolve(false);
|
|
125
|
+
}
|
|
126
|
+
tail = void 0;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Enqueues a value or completion signal with a promise that resolves when the item
|
|
130
|
+
* is processed. This enables backpressure by allowing producers to know when their
|
|
131
|
+
* values have been consumed.
|
|
132
|
+
*/
|
|
133
|
+
const enqueue = (params) => {
|
|
134
|
+
if (skipValues) drainQueue();
|
|
135
|
+
return !closed ? new Promise((resolve) => {
|
|
136
|
+
const next = {
|
|
137
|
+
...params,
|
|
138
|
+
next: void 0,
|
|
139
|
+
resolve
|
|
140
|
+
};
|
|
141
|
+
if (tail) tail.next = next;
|
|
142
|
+
tail = next;
|
|
143
|
+
if (!head) head = tail;
|
|
144
|
+
wakeUp?.();
|
|
145
|
+
}) : Promise.resolve(false);
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Producer function to yield a value to consumers. Returns a promise that resolves
|
|
149
|
+
* to true if the value was successfully processed, false if the generator is closed
|
|
150
|
+
* or the value was skipped due to backpressure.
|
|
151
|
+
*/
|
|
152
|
+
const next = (value) => enqueue({
|
|
153
|
+
done: false,
|
|
154
|
+
value
|
|
155
|
+
});
|
|
156
|
+
/**
|
|
157
|
+
* Producer function to signal completion or error. Optional error parameter will
|
|
158
|
+
* cause the generator to throw that error to consumers.
|
|
159
|
+
*/
|
|
160
|
+
const done = (error) => enqueue({
|
|
161
|
+
done: true,
|
|
162
|
+
error
|
|
163
|
+
});
|
|
164
|
+
const unsubscribe = init(next, done);
|
|
165
|
+
try {
|
|
166
|
+
while (!closed) {
|
|
167
|
+
if (!head) {
|
|
168
|
+
await new Promise((resolve) => {
|
|
169
|
+
wakeUp = resolve;
|
|
170
|
+
}).then(() => {
|
|
171
|
+
wakeUp = void 0;
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const slot = head;
|
|
176
|
+
head = head.next;
|
|
177
|
+
if (tail === slot) tail = head;
|
|
178
|
+
try {
|
|
179
|
+
if (slot.done) {
|
|
180
|
+
closed = true;
|
|
181
|
+
if (slot.error !== void 0) throw slot.error;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
yield slot.value;
|
|
185
|
+
} finally {
|
|
186
|
+
slot.resolve(true);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} finally {
|
|
190
|
+
/**
|
|
191
|
+
* Cleanup phase - ensures proper resource management and notification of any
|
|
192
|
+
* remaining producers. This runs whether the generator completes normally,
|
|
193
|
+
* encounters an error, or is closed early by the consumer.
|
|
194
|
+
*/
|
|
195
|
+
closed = true;
|
|
196
|
+
wakeUp?.();
|
|
197
|
+
if (typeof unsubscribe === "function") await unsubscribe();
|
|
198
|
+
/**
|
|
199
|
+
* Drain any remaining items in the queue and notify their producers that
|
|
200
|
+
* the values were not processed. This prevents memory leaks and ensures
|
|
201
|
+
* proper backpressure signaling.
|
|
202
|
+
*/
|
|
203
|
+
drainQueue();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region ../webrun-streams/src/readable-streams.ts
|
|
208
|
+
function toReadableStream(it) {
|
|
209
|
+
return new ReadableStream({ async pull(controller) {
|
|
210
|
+
let handled = false;
|
|
211
|
+
try {
|
|
212
|
+
while (true) {
|
|
213
|
+
const slot = await it.next();
|
|
214
|
+
if (!slot || slot.done) break;
|
|
215
|
+
const value = await slot.value;
|
|
216
|
+
controller.enqueue(value);
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
handled = true;
|
|
220
|
+
controller.error(error);
|
|
221
|
+
} finally {
|
|
222
|
+
if (!handled) controller.close();
|
|
223
|
+
}
|
|
224
|
+
} });
|
|
225
|
+
}
|
|
226
|
+
async function* fromReadableStream(stream) {
|
|
227
|
+
const reader = stream.getReader();
|
|
228
|
+
while (true) {
|
|
229
|
+
const { done, value } = await reader.read();
|
|
230
|
+
if (done) break;
|
|
231
|
+
if (value !== void 0) yield value;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region ../webrun-streams/src/recieve-iterator.ts
|
|
236
|
+
/**
|
|
237
|
+
* Inverse of {@link sendIterator}: turns a sequence of `{done, value, error}`
|
|
238
|
+
* chunks (delivered to the supplied callback by `installer`) into an async
|
|
239
|
+
* generator.
|
|
240
|
+
*
|
|
241
|
+
* The `installer` is given a `deliver` function. It should call `deliver`
|
|
242
|
+
* once for each incoming chunk and may return a cleanup callback that
|
|
243
|
+
* runs when the consumer stops iterating.
|
|
244
|
+
*/
|
|
245
|
+
function recieveIterator(installer) {
|
|
246
|
+
return newAsyncGenerator((next, done) => {
|
|
247
|
+
const cleanup = installer(async (chunk = { done: true }) => {
|
|
248
|
+
const { done: isDone = true, value, error } = chunk;
|
|
249
|
+
if (error) return await done(error);
|
|
250
|
+
if (isDone) return await done();
|
|
251
|
+
return await next(value);
|
|
252
|
+
});
|
|
253
|
+
if (cleanup) return async () => {
|
|
254
|
+
await cleanup();
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region ../webrun-streams/src/send-iterator.ts
|
|
260
|
+
/**
|
|
261
|
+
* Drain an async iterator into a sink that consumes one chunk at a time.
|
|
262
|
+
*
|
|
263
|
+
* Each yielded value becomes `{ done: false, value }`. Completion emits
|
|
264
|
+
* `{ done: true }`. If the iterator throws, the error is caught and the
|
|
265
|
+
* final `done` chunk carries it so the peer can rethrow on its side.
|
|
266
|
+
*/
|
|
267
|
+
async function sendIterator(send, it) {
|
|
268
|
+
let error;
|
|
269
|
+
try {
|
|
270
|
+
for await (const value of it) await send({
|
|
271
|
+
done: false,
|
|
272
|
+
value
|
|
273
|
+
});
|
|
274
|
+
} catch (err) {
|
|
275
|
+
error = err;
|
|
276
|
+
} finally {
|
|
277
|
+
await send({
|
|
278
|
+
done: true,
|
|
279
|
+
error
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region ../webrun-http-streams/src/request-streams.ts
|
|
285
|
+
/**
|
|
286
|
+
* The one place that knows a runtime may not implement request body streams,
|
|
287
|
+
* and the buffering both fallbacks need. `fetch.ts` and `http-stubs.ts` each
|
|
288
|
+
* carry a client and a server direction that must agree on the answer, so the
|
|
289
|
+
* predicate lives here rather than being written out four times.
|
|
290
|
+
*/
|
|
291
|
+
/**
|
|
292
|
+
* Whether `Request.prototype` exposes a `body` accessor. Two call sites read
|
|
293
|
+
* that property, and two more depend on the constructor accepting a
|
|
294
|
+
* `ReadableStream` as `init.body`; this predicate answers for all four.
|
|
295
|
+
*
|
|
296
|
+
* What was verified, and all that is claimed here: Firefox (146 at the time of
|
|
297
|
+
* writing) has *neither*, and Chromium and Node have *both*. On Firefox it is
|
|
298
|
+
* not that `body` is `undefined` on the instance —
|
|
299
|
+
* `Object.getOwnPropertyDescriptor(Request.prototype, "body")` is `null`, the
|
|
300
|
+
* accessor is genuinely absent — and because a `ReadableStream` is then not a
|
|
301
|
+
* recognised `BodyInit`, the constructor falls through to the string branch and
|
|
302
|
+
* stores the literal text `[object ReadableStream]`.
|
|
303
|
+
*
|
|
304
|
+
* The two halves are NOT guaranteed to ship together, so do not read this as a
|
|
305
|
+
* test for "request streams" in general. Safari is the counterexample: it has
|
|
306
|
+
* had `Request.body` since 11.1 but only accepts a stream as `init.body` from
|
|
307
|
+
* Technology Preview 250, so a shipping Safari has the reader half without the
|
|
308
|
+
* upload half and this returns `true` there. That looks benign — WebKit appears
|
|
309
|
+
* to store the stream on the `Request` rather than stringify it, and its error
|
|
310
|
+
* comes from `fetch()`, which this package never calls on the objects it builds
|
|
311
|
+
* — but it is untested, and it is the case to look at first if a Safari report
|
|
312
|
+
* arrives. The sharper probe, if one is ever needed, is whether
|
|
313
|
+
* `new Request(url, {method:"POST", body:new ReadableStream(), duplex:"half"})`
|
|
314
|
+
* has a `content-type` of `text/plain;charset=UTF-8` (stringified) or `null`
|
|
315
|
+
* (stored); it is not used here because it costs a `Request` and a
|
|
316
|
+
* `ReadableStream` per call and agrees with the descriptor check on every
|
|
317
|
+
* runtime measured.
|
|
318
|
+
*
|
|
319
|
+
* A capability check, never a user-agent test: the question is what this
|
|
320
|
+
* runtime does, and the answer flips on its own the day Firefox ships request
|
|
321
|
+
* streams. Evaluated per call rather than cached at module load so that a test
|
|
322
|
+
* can install a `Request` without the capability and exercise the real branch
|
|
323
|
+
* under Node.
|
|
324
|
+
*/
|
|
325
|
+
function supportsRequestStreams() {
|
|
326
|
+
return typeof Request === "function" && Object.getOwnPropertyDescriptor(Request.prototype, "body") != null;
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region ../webrun-http-streams/src/http-stubs.ts
|
|
330
|
+
const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
|
|
331
|
+
101,
|
|
332
|
+
103,
|
|
333
|
+
204,
|
|
334
|
+
205,
|
|
335
|
+
304
|
|
336
|
+
]);
|
|
337
|
+
const REQUEST_FIELDS = [
|
|
338
|
+
"url",
|
|
339
|
+
"method",
|
|
340
|
+
"mode",
|
|
341
|
+
"credentials",
|
|
342
|
+
"cache",
|
|
343
|
+
"redirect",
|
|
344
|
+
"referrer",
|
|
345
|
+
"referrerPolicy",
|
|
346
|
+
"integrity",
|
|
347
|
+
"keepalive"
|
|
348
|
+
];
|
|
349
|
+
/** `SerializedHttpEnvelope.content` is never absent, only empty. */
|
|
350
|
+
async function* noBytes() {}
|
|
351
|
+
async function* oneChunk(chunk) {
|
|
352
|
+
yield chunk;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Returns an HTTP handler that serializes a Request, hands the envelope to
|
|
356
|
+
* `send` for transport, and deserializes the reply into a Response. Used on
|
|
357
|
+
* the caller side.
|
|
358
|
+
*/
|
|
359
|
+
function newHttpClientStub(send) {
|
|
360
|
+
return async (requestOrPromise) => {
|
|
361
|
+
const request = await requestOrPromise;
|
|
362
|
+
const headers = [...request.headers].map(([k, v]) => [k, v]);
|
|
363
|
+
const options = {
|
|
364
|
+
url: request.url,
|
|
365
|
+
headers
|
|
366
|
+
};
|
|
367
|
+
for (const field of REQUEST_FIELDS) {
|
|
368
|
+
const val = request[field];
|
|
369
|
+
if (val !== void 0 && field !== "url") options[field] = val;
|
|
370
|
+
}
|
|
371
|
+
let content;
|
|
372
|
+
if (request.body != null) content = fromReadableStream(request.body);
|
|
373
|
+
else if (!supportsRequestStreams()) content = oneChunk(new Uint8Array(await request.arrayBuffer()));
|
|
374
|
+
else content = noBytes();
|
|
375
|
+
const result = await send({
|
|
376
|
+
options,
|
|
377
|
+
content
|
|
378
|
+
});
|
|
379
|
+
if (!result) return new Response(null, {
|
|
380
|
+
status: 404,
|
|
381
|
+
statusText: "Error 404: Not Found"
|
|
382
|
+
});
|
|
383
|
+
const responseOptions = result.options;
|
|
384
|
+
const method = options.method;
|
|
385
|
+
if (method === "HEAD" || method === "OPTIONS" || NULL_BODY_STATUSES.has(responseOptions.status)) {
|
|
386
|
+
await discard(result.content);
|
|
387
|
+
return new Response(null, responseOptions);
|
|
388
|
+
}
|
|
389
|
+
return new Response(toReadableStream(result.content[Symbol.asyncIterator]()), responseOptions);
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region ../webrun-http-streams/src/http-error.ts
|
|
394
|
+
var HttpError = class HttpError extends Error {
|
|
395
|
+
status;
|
|
396
|
+
statusText;
|
|
397
|
+
constructor(options = {}) {
|
|
398
|
+
super(options.message ?? options.statusText ?? "HTTP Error");
|
|
399
|
+
this.status = options.status;
|
|
400
|
+
this.statusText = options.statusText;
|
|
401
|
+
}
|
|
402
|
+
getResponseOptions(options = {}) {
|
|
403
|
+
return {
|
|
404
|
+
...this.toJson(),
|
|
405
|
+
...options
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
toJson() {
|
|
409
|
+
return {
|
|
410
|
+
status: this.status,
|
|
411
|
+
statusText: this.statusText,
|
|
412
|
+
message: this.message
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
static fromError(error) {
|
|
416
|
+
if (error instanceof HttpError) return error;
|
|
417
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
418
|
+
return new HttpError({
|
|
419
|
+
status: 500,
|
|
420
|
+
statusText: "Bad Request",
|
|
421
|
+
message
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
static errorResourceNotFound(options = {}) {
|
|
425
|
+
return new HttpError({
|
|
426
|
+
status: 404,
|
|
427
|
+
statusText: "Error 404: Resource not found",
|
|
428
|
+
...options
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
static errorForbidden(options = {}) {
|
|
432
|
+
return new HttpError({
|
|
433
|
+
status: 403,
|
|
434
|
+
statusText: "Error 403: Forbidden",
|
|
435
|
+
...options
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
static errorResourceGone(options = {}) {
|
|
439
|
+
return new HttpError({
|
|
440
|
+
status: 410,
|
|
441
|
+
statusText: "Error 410: Resource Gone",
|
|
442
|
+
...options
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
static errorInternalError(options = {}) {
|
|
446
|
+
return new HttpError({
|
|
447
|
+
...options,
|
|
448
|
+
status: 500,
|
|
449
|
+
statusText: "Error 500: Internal error"
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region ../../node_modules/.pnpm/idb-keyval@6.3.0/node_modules/idb-keyval/dist/index.js
|
|
455
|
+
function promisifyRequest(request) {
|
|
456
|
+
return new Promise((resolve, reject) => {
|
|
457
|
+
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
|
458
|
+
request.onabort = request.onerror = () => reject(request.error);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function createStore(dbName, storeName) {
|
|
462
|
+
let dbp;
|
|
463
|
+
const getDB = () => {
|
|
464
|
+
if (dbp) return dbp;
|
|
465
|
+
const request = indexedDB.open(dbName);
|
|
466
|
+
request.onupgradeneeded = () => request.result.createObjectStore(storeName);
|
|
467
|
+
dbp = promisifyRequest(request);
|
|
468
|
+
dbp.then((db) => {
|
|
469
|
+
db.onclose = () => dbp = void 0;
|
|
470
|
+
}, () => {
|
|
471
|
+
dbp = void 0;
|
|
472
|
+
});
|
|
473
|
+
return dbp;
|
|
474
|
+
};
|
|
475
|
+
return (txMode, callback) => getDB().then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));
|
|
476
|
+
}
|
|
477
|
+
let defaultGetStoreFunc;
|
|
478
|
+
function defaultGetStore() {
|
|
479
|
+
if (!defaultGetStoreFunc) defaultGetStoreFunc = createStore("keyval-store", "keyval");
|
|
480
|
+
return defaultGetStoreFunc;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Get a value by its key.
|
|
484
|
+
*
|
|
485
|
+
* @param key
|
|
486
|
+
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
|
487
|
+
*/
|
|
488
|
+
function get(key, customStore = defaultGetStore()) {
|
|
489
|
+
return customStore("readonly", (store) => promisifyRequest(store.get(key)));
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Set a value with a key.
|
|
493
|
+
*
|
|
494
|
+
* @param key
|
|
495
|
+
* @param value
|
|
496
|
+
* @param customStore Method to get a custom store. Use with caution (see the docs).
|
|
497
|
+
*/
|
|
498
|
+
function set(key, value, customStore = defaultGetStore()) {
|
|
499
|
+
return customStore("readwrite", (store) => {
|
|
500
|
+
store.put(value, key);
|
|
501
|
+
return promisifyRequest(store.transaction);
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/core/data-calls.ts
|
|
506
|
+
function callChannel(target, callType, params, ...transfers) {
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const channel = new MessageChannel();
|
|
509
|
+
channel.port1.onmessage = (ev) => {
|
|
510
|
+
const { result, error } = ev.data;
|
|
511
|
+
if (error) reject(deserializeError(error));
|
|
512
|
+
else resolve(result);
|
|
513
|
+
};
|
|
514
|
+
target.postMessage({
|
|
515
|
+
type: callType,
|
|
516
|
+
params
|
|
517
|
+
}, [channel.port2, ...transfers]);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
function handleChannelCalls(target, callType, handler) {
|
|
521
|
+
const listener = async (event) => {
|
|
522
|
+
if (!event.data || event.data.type !== callType) return;
|
|
523
|
+
const [port, ...transfers] = event.ports ?? [];
|
|
524
|
+
const response = {};
|
|
525
|
+
try {
|
|
526
|
+
response.result = await handler(event, event.data.params, ...transfers);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
response.error = serializeError(error);
|
|
529
|
+
}
|
|
530
|
+
port?.postMessage(response);
|
|
531
|
+
};
|
|
532
|
+
target.addEventListener("message", listener);
|
|
533
|
+
target.start?.();
|
|
534
|
+
return () => target.removeEventListener("message", listener);
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/core/registry.ts
|
|
538
|
+
function newRegistry(onError = console.error) {
|
|
539
|
+
let counter = 0;
|
|
540
|
+
const registrations = {};
|
|
541
|
+
const register = (action) => {
|
|
542
|
+
const id = counter++;
|
|
543
|
+
const fn = Object.assign((skip) => {
|
|
544
|
+
try {
|
|
545
|
+
delete registrations[id];
|
|
546
|
+
return skip ? void 0 : action();
|
|
547
|
+
} catch (error) {
|
|
548
|
+
onError(error);
|
|
549
|
+
}
|
|
550
|
+
}, { action });
|
|
551
|
+
registrations[id] = fn;
|
|
552
|
+
return fn;
|
|
553
|
+
};
|
|
554
|
+
const unregister = (action) => {
|
|
555
|
+
for (const r of Object.values(registrations)) if (r.action === action) r(true);
|
|
556
|
+
};
|
|
557
|
+
const clear = () => {
|
|
558
|
+
for (const r of Object.values(registrations)) r();
|
|
559
|
+
};
|
|
560
|
+
return Object.assign([
|
|
561
|
+
register,
|
|
562
|
+
clear,
|
|
563
|
+
unregister
|
|
564
|
+
], {
|
|
565
|
+
register,
|
|
566
|
+
clear,
|
|
567
|
+
unregister
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/core/data-channels.ts
|
|
572
|
+
const MESSAGE_TYPE_REQUEST = "REQUEST";
|
|
573
|
+
const MESSAGE_TYPE_RESPONSE = "RESPONSE";
|
|
574
|
+
let __invocationCounter = 0;
|
|
575
|
+
function newInvokationChannel({ port, handler = () => {
|
|
576
|
+
throw new Error("Handler not implemented");
|
|
577
|
+
}, onError = console.error, newCallId = () => ++__invocationCounter }) {
|
|
578
|
+
const requests = {};
|
|
579
|
+
const listener = async (event) => {
|
|
580
|
+
const data = event.data ?? {};
|
|
581
|
+
if (data.type === MESSAGE_TYPE_REQUEST) try {
|
|
582
|
+
const result = await handler(data.request, ...event.ports);
|
|
583
|
+
const [response, ...transfers] = Array.isArray(result) ? result : result !== void 0 ? [result] : [];
|
|
584
|
+
port.postMessage({
|
|
585
|
+
type: MESSAGE_TYPE_RESPONSE,
|
|
586
|
+
callId: data.callId,
|
|
587
|
+
response
|
|
588
|
+
}, transfers);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
port.postMessage({
|
|
591
|
+
type: MESSAGE_TYPE_RESPONSE,
|
|
592
|
+
callId: data.callId,
|
|
593
|
+
error: serializeError(error)
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
else if (data.type === MESSAGE_TYPE_RESPONSE) {
|
|
597
|
+
const pending = requests[data.callId];
|
|
598
|
+
delete requests[data.callId];
|
|
599
|
+
if (!pending) return;
|
|
600
|
+
if (data.error) pending.reject(deserializeError(data.error));
|
|
601
|
+
else pending.resolve(data.response);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const start = async () => {
|
|
605
|
+
try {
|
|
606
|
+
port.addEventListener("message", listener);
|
|
607
|
+
await port.start?.();
|
|
608
|
+
} catch (e) {
|
|
609
|
+
onError(e);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
const close = async () => {
|
|
613
|
+
try {
|
|
614
|
+
port.removeEventListener("message", listener);
|
|
615
|
+
await port.close?.();
|
|
616
|
+
} catch (e) {
|
|
617
|
+
onError(e);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
const invoke = (request = {}, ...transfers) => {
|
|
621
|
+
const callId = newCallId();
|
|
622
|
+
return new Promise((resolve, reject) => {
|
|
623
|
+
try {
|
|
624
|
+
requests[callId] = {
|
|
625
|
+
resolve,
|
|
626
|
+
reject
|
|
627
|
+
};
|
|
628
|
+
port.postMessage({
|
|
629
|
+
type: MESSAGE_TYPE_REQUEST,
|
|
630
|
+
callId,
|
|
631
|
+
request
|
|
632
|
+
}, transfers);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
delete requests[callId];
|
|
635
|
+
reject(error);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
};
|
|
639
|
+
return {
|
|
640
|
+
start,
|
|
641
|
+
close,
|
|
642
|
+
invoke
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
async function* sendStream(communicationPort, input, params = {}) {
|
|
646
|
+
const messageChannel = new MessageChannel();
|
|
647
|
+
communicationPort.postMessage({
|
|
648
|
+
type: "START_CALL",
|
|
649
|
+
params
|
|
650
|
+
}, [messageChannel.port2]);
|
|
651
|
+
const channel = newStreamChannel(messageChannel.port1);
|
|
652
|
+
try {
|
|
653
|
+
await channel.start();
|
|
654
|
+
channel.sendAll(input);
|
|
655
|
+
yield* channel.recieveAll();
|
|
656
|
+
} finally {
|
|
657
|
+
await channel.close();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function newStreamChannel(port) {
|
|
661
|
+
let listeners = [];
|
|
662
|
+
let iterators = [];
|
|
663
|
+
const notifyAll = async (data) => {
|
|
664
|
+
for (const listener of listeners) await listener(data);
|
|
665
|
+
};
|
|
666
|
+
const channel = newInvokationChannel({
|
|
667
|
+
port,
|
|
668
|
+
handler: (data) => notifyAll(data)
|
|
669
|
+
});
|
|
670
|
+
const start = () => channel.start();
|
|
671
|
+
const close = async () => {
|
|
672
|
+
await notifyAll({ done: true });
|
|
673
|
+
for (const it of [...iterators]) await it.return?.();
|
|
674
|
+
await channel.close();
|
|
675
|
+
};
|
|
676
|
+
async function* recieveAll() {
|
|
677
|
+
yield* recieveIterator((deliver) => {
|
|
678
|
+
listeners.push(deliver);
|
|
679
|
+
return () => {
|
|
680
|
+
listeners = listeners.filter((l) => l !== deliver);
|
|
681
|
+
};
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
async function sendAll(it) {
|
|
685
|
+
await sendIterator((chunk) => void channel.invoke(chunk), (async function* () {
|
|
686
|
+
try {
|
|
687
|
+
iterators.push(it);
|
|
688
|
+
yield* it;
|
|
689
|
+
} finally {
|
|
690
|
+
iterators = iterators.filter((i) => i !== it);
|
|
691
|
+
}
|
|
692
|
+
})());
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
start,
|
|
696
|
+
close,
|
|
697
|
+
recieveAll,
|
|
698
|
+
sendAll
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region src/http/http-send-recieve.ts
|
|
703
|
+
async function* httpToIterator(envelopeOrPromise) {
|
|
704
|
+
const { options, content } = await envelopeOrPromise;
|
|
705
|
+
yield new TextEncoder().encode(JSON.stringify(options));
|
|
706
|
+
yield* content;
|
|
707
|
+
}
|
|
708
|
+
async function httpFromIterator(iterable) {
|
|
709
|
+
const it = (await iterable)[Symbol.asyncIterator]();
|
|
710
|
+
const { done, value } = await it.next();
|
|
711
|
+
let options = {};
|
|
712
|
+
if (!done && value) {
|
|
713
|
+
const str = new TextDecoder().decode(value);
|
|
714
|
+
options = JSON.parse(str);
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
options,
|
|
718
|
+
content: { [Symbol.asyncIterator]() {
|
|
719
|
+
return it;
|
|
720
|
+
} }
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* @deprecated For new code, prefer the `MessagePort`-based stack from
|
|
725
|
+
* `@statewalker/webrun-http-port/fetch`. Once the page and SW share a
|
|
726
|
+
* `MessagePort`, `fetchOverPort(port, request)` provides the same
|
|
727
|
+
* `Request → Response` semantics with multiplexing via `callBidi`, JSONL
|
|
728
|
+
* envelope framing, and native `AbortSignal` support. This helper remains for
|
|
729
|
+
* existing ServiceWorker setups; it will be reimplemented on top of
|
|
730
|
+
* `webrun-http-port` in a follow-up release.
|
|
731
|
+
*/
|
|
732
|
+
async function sendHttpRequest(communicationPort, request) {
|
|
733
|
+
return await newHttpClientStub(async (req) => {
|
|
734
|
+
return await httpFromIterator(sendStream(communicationPort, httpToIterator(req)));
|
|
735
|
+
})(request);
|
|
736
|
+
}
|
|
737
|
+
//#endregion
|
|
738
|
+
//#region src/relay/split-service-url.ts
|
|
739
|
+
/**
|
|
740
|
+
* Splits a URL of the form `<base>/<separator><key>/<path>` into parts.
|
|
741
|
+
* Example: `https://host/~FS/a/b` → `{ baseUrl: "https://host/~FS/", key: "FS", path: "a/b" }`.
|
|
742
|
+
*/
|
|
743
|
+
function splitServiceUrl(url, separator = "~") {
|
|
744
|
+
const str = `${url}`;
|
|
745
|
+
const idx = str.indexOf(separator);
|
|
746
|
+
let baseUrl = "";
|
|
747
|
+
let key = "";
|
|
748
|
+
let path = "";
|
|
749
|
+
if (idx >= 0) {
|
|
750
|
+
baseUrl = str.substring(0, idx + separator.length);
|
|
751
|
+
str.substring(idx + separator.length).replace(/^([^/]+)/, (match, $1) => {
|
|
752
|
+
baseUrl += match;
|
|
753
|
+
if (baseUrl.length < str.length) baseUrl += "/";
|
|
754
|
+
key = $1;
|
|
755
|
+
path = str.substring(baseUrl.length);
|
|
756
|
+
return "";
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
url: str,
|
|
761
|
+
key,
|
|
762
|
+
baseUrl,
|
|
763
|
+
path
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/relay/index-sw.ts
|
|
768
|
+
/**
|
|
769
|
+
* Boots the relay ServiceWorker: routes fetches shaped `<origin>/~<key>/…` to
|
|
770
|
+
* the client that registered `key`, and exposes REGISTER/UNREGISTER/CONNECT
|
|
771
|
+
* channel calls used by the page-side relay client.
|
|
772
|
+
*/
|
|
773
|
+
function startRelayServiceWorker(self) {
|
|
774
|
+
const [register, clear] = newRegistry();
|
|
775
|
+
if (typeof self.skipWaiting === "function") self.addEventListener("install", (e) => {
|
|
776
|
+
e.waitUntil(self.skipWaiting());
|
|
777
|
+
});
|
|
778
|
+
if (self.clients && typeof self.clients.claim === "function") self.addEventListener("activate", (e) => {
|
|
779
|
+
e.waitUntil(self.clients.claim());
|
|
780
|
+
});
|
|
781
|
+
const clientsRegistry = newClientsRegistry({ self });
|
|
782
|
+
register(handleChannelCalls(self, "REGISTER", async (event, data) => {
|
|
783
|
+
const source = event.source;
|
|
784
|
+
if (!source) return false;
|
|
785
|
+
const { key } = data;
|
|
786
|
+
return await clientsRegistry.addClient(key, source);
|
|
787
|
+
}));
|
|
788
|
+
register(handleChannelCalls(self, "UNREGISTER", async (_event, data) => {
|
|
789
|
+
const { key } = data;
|
|
790
|
+
return await clientsRegistry.removeClient(key);
|
|
791
|
+
}));
|
|
792
|
+
register(handleChannelCalls(self, "CONNECT", async (_event, data, port) => {
|
|
793
|
+
const { key } = data;
|
|
794
|
+
const client = await clientsRegistry.getClient(key);
|
|
795
|
+
if (!client) throw new Error(`Target client was not found. Target key: "${key}".`);
|
|
796
|
+
return await callChannel(client, "CONNECT", data, port);
|
|
797
|
+
}));
|
|
798
|
+
const fetchListener = (event) => {
|
|
799
|
+
const request = event.request;
|
|
800
|
+
const params = splitServiceUrl(request.url);
|
|
801
|
+
const { key } = params;
|
|
802
|
+
if (!key) return;
|
|
803
|
+
event.respondWith((async () => {
|
|
804
|
+
try {
|
|
805
|
+
const channel = new MessageChannel();
|
|
806
|
+
const client = await clientsRegistry.getClient(key);
|
|
807
|
+
if (!client) throw HttpError.errorResourceGone(params);
|
|
808
|
+
if (!await callChannel(client, "CONNECT", {
|
|
809
|
+
type: "http",
|
|
810
|
+
key
|
|
811
|
+
}, channel.port2)) throw HttpError.errorForbidden(params);
|
|
812
|
+
return await sendHttpRequest(channel.port1, request);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
const httpError = HttpError.fromError(error);
|
|
815
|
+
const options = httpError.getResponseOptions(params);
|
|
816
|
+
return new Response(JSON.stringify(options), {
|
|
817
|
+
status: httpError.status ?? 500,
|
|
818
|
+
statusText: httpError.statusText ?? "Internal Error",
|
|
819
|
+
headers: { "Content-Type": "application/json" }
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
})());
|
|
823
|
+
};
|
|
824
|
+
self.addEventListener("fetch", fetchListener);
|
|
825
|
+
register(() => self.removeEventListener("fetch", fetchListener));
|
|
826
|
+
return clear;
|
|
827
|
+
}
|
|
828
|
+
function newClientsRegistry({ self, key = "clientsIds" }) {
|
|
829
|
+
let _index;
|
|
830
|
+
async function loadClientsIndex() {
|
|
831
|
+
if (!_index) {
|
|
832
|
+
const entries = await get(key) ?? [];
|
|
833
|
+
_index = Object.fromEntries(entries);
|
|
834
|
+
}
|
|
835
|
+
return _index;
|
|
836
|
+
}
|
|
837
|
+
async function storeClientsIndex() {
|
|
838
|
+
const index = await loadClientsIndex();
|
|
839
|
+
await set(key, Object.entries(index));
|
|
840
|
+
return index;
|
|
841
|
+
}
|
|
842
|
+
async function addClient(clientKey, client) {
|
|
843
|
+
const index = await loadClientsIndex();
|
|
844
|
+
const clientId = client.id;
|
|
845
|
+
if (index[clientKey] === clientId) return false;
|
|
846
|
+
index[clientKey] = clientId;
|
|
847
|
+
await storeClientsIndex();
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
async function removeClient(clientKey) {
|
|
851
|
+
const index = await loadClientsIndex();
|
|
852
|
+
if (!(clientKey in index)) return false;
|
|
853
|
+
delete index[clientKey];
|
|
854
|
+
await storeClientsIndex();
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
async function getClient(clientKey) {
|
|
858
|
+
const index = await loadClientsIndex();
|
|
859
|
+
const clientId = index[clientKey];
|
|
860
|
+
if (!clientId) return void 0;
|
|
861
|
+
const client = await self.clients.get(clientId);
|
|
862
|
+
if (!client) {
|
|
863
|
+
delete index[clientKey];
|
|
864
|
+
await storeClientsIndex();
|
|
865
|
+
}
|
|
866
|
+
return client ?? void 0;
|
|
867
|
+
}
|
|
868
|
+
return {
|
|
869
|
+
getClient,
|
|
870
|
+
addClient,
|
|
871
|
+
removeClient
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region src/relay-sw.ts
|
|
876
|
+
startRelayServiceWorker(self);
|
|
877
|
+
//#endregion
|
|
878
|
+
})();
|