@statewalker/webrun-http-browser 0.3.4 → 0.5.0
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 +164 -6
- package/dist/core/data-channels.d.ts.map +1 -1
- package/dist/core/deadline.d.ts +7 -0
- package/dist/core/deadline.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/message-target.d.ts +1 -15
- package/dist/core/message-target.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +72 -0
- package/dist/core/service-worker-control.d.ts.map +1 -0
- package/dist/http/http-send-recieve.d.ts +27 -14
- package/dist/http/http-send-recieve.d.ts.map +1 -1
- package/dist/index.js +485 -167
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +23 -6
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay-sw.js +864 -65
- package/dist/sw/sw-dispatcher.d.ts +22 -0
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw-worker.js +869 -64
- package/dist/sw.js +1096 -106
- package/package.json +8 -3
- package/src/core/data-channels.ts +42 -4
- package/src/core/deadline.ts +31 -0
- package/src/core/index.ts +1 -0
- package/src/core/message-target.ts +8 -18
- package/src/core/service-worker-control.ts +243 -0
- package/src/http/http-send-recieve.ts +27 -14
- package/src/relay/index-sw.ts +5 -0
- package/src/relay/index.ts +65 -47
- package/src/sw/sw-dispatcher.ts +98 -46
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region ../webrun-streams/
|
|
1
|
+
//#region ../webrun-streams/dist/index.js
|
|
2
2
|
/** Collect all items from an async iterable into an array. */
|
|
3
3
|
async function collect(input) {
|
|
4
4
|
const items = [];
|
|
@@ -28,8 +28,17 @@ async function collectString(input) {
|
|
|
28
28
|
for await (const chunk of input) result += chunk;
|
|
29
29
|
return result;
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Thrown by `emulateMux` and adapters when the underlying transport closes
|
|
33
|
+
* while one or more `Duplex` calls are in flight. Consumers can catch by
|
|
34
|
+
* `instanceof TransportClosedError` or by checking `error.name`.
|
|
35
|
+
*/
|
|
36
|
+
var TransportClosedError = class extends Error {
|
|
37
|
+
name = "TransportClosedError";
|
|
38
|
+
constructor(message = "transport closed") {
|
|
39
|
+
super(message);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
33
42
|
function serializeError(error) {
|
|
34
43
|
if (error instanceof Error) {
|
|
35
44
|
const out = {
|
|
@@ -53,19 +62,113 @@ function deserializeError(error) {
|
|
|
53
62
|
const payload = typeof error === "string" ? { message: error } : error;
|
|
54
63
|
return Object.assign(new Error(payload.message), payload);
|
|
55
64
|
}
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
function newCreditLedger(initial = 0) {
|
|
66
|
+
let available = initial;
|
|
67
|
+
let failure;
|
|
68
|
+
const waiters = [];
|
|
69
|
+
const pump = () => {
|
|
70
|
+
while (waiters.length > 0 && available > 0) {
|
|
71
|
+
const next = waiters[0];
|
|
72
|
+
if (!next) return;
|
|
73
|
+
waiters.shift();
|
|
74
|
+
const granted = Math.min(next.upTo, available);
|
|
75
|
+
available -= granted;
|
|
76
|
+
next.resolve(granted);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
get available() {
|
|
81
|
+
return available;
|
|
82
|
+
},
|
|
83
|
+
reserve(upTo) {
|
|
84
|
+
if (failure) return Promise.reject(failure);
|
|
85
|
+
if (!(upTo >= 1)) return Promise.reject(/* @__PURE__ */ new RangeError(`newCreditLedger: reserve(${upTo}) — upTo must be at least 1`));
|
|
86
|
+
if (waiters.length === 0 && available > 0) {
|
|
87
|
+
const granted = Math.min(upTo, available);
|
|
88
|
+
available -= granted;
|
|
89
|
+
return Promise.resolve(granted);
|
|
90
|
+
}
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
waiters.push({
|
|
93
|
+
upTo,
|
|
94
|
+
resolve,
|
|
95
|
+
reject
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
grant(units) {
|
|
100
|
+
if (failure) return;
|
|
101
|
+
available += units;
|
|
102
|
+
pump();
|
|
103
|
+
},
|
|
104
|
+
fail(err) {
|
|
105
|
+
failure ??= err;
|
|
106
|
+
while (waiters.length > 0) waiters.shift()?.reject(err);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
58
110
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* `
|
|
111
|
+
* Grants are batched: replenishing on every chunk while the receiver is
|
|
112
|
+
* behind would reinvent the per-frame ACK this change exists to remove.
|
|
113
|
+
* `threshold` is the fraction of the window that must drain before a grant is
|
|
114
|
+
* emitted.
|
|
115
|
+
*
|
|
116
|
+
* The batch is flushed unconditionally once the receive queue is empty, even
|
|
117
|
+
* below the threshold, so the receiver never sits on credit it owes. Note what
|
|
118
|
+
* this is and is not: paired with a {@link CreditLedger}, a sender blocks only
|
|
119
|
+
* at *exactly* zero credit, and at that point the receiver holds the entire
|
|
120
|
+
* window as `pending` — above any threshold at or below the whole window — so
|
|
121
|
+
* the threshold alone cannot deadlock that pairing. The flush is what keeps
|
|
122
|
+
* that from being an argument about a global accounting identity: it is
|
|
123
|
+
* locally decidable from one boolean, and it returns owed credit now rather
|
|
124
|
+
* than at the next threshold crossing. It costs an extra frame only when the
|
|
125
|
+
* consumer is keeping pace, which is exactly when the sender is not blocked
|
|
126
|
+
* and the frame is cheap.
|
|
62
127
|
*/
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
128
|
+
function newCreditGrantor(window, threshold = .5) {
|
|
129
|
+
const trigger = Math.max(1, Math.floor(window * threshold));
|
|
130
|
+
let pending = 0;
|
|
131
|
+
return { consumed(units, queueEmpty) {
|
|
132
|
+
pending += units;
|
|
133
|
+
if (pending === 0) return 0;
|
|
134
|
+
if (pending < trigger && !queueEmpty) return 0;
|
|
135
|
+
const grant = pending;
|
|
136
|
+
pending = 0;
|
|
137
|
+
return grant;
|
|
138
|
+
} };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Credit payloads are a single big-endian uint32, matching the frame header's
|
|
142
|
+
* byte order.
|
|
143
|
+
*
|
|
144
|
+
* Deliberately **not** re-exported from `index.ts`: this is an internal codec
|
|
145
|
+
* for the `emulateMux` wire format, not a compatibility commitment. Tests
|
|
146
|
+
* import it by path.
|
|
147
|
+
*/
|
|
148
|
+
/** The largest credit a single frame can advertise or grant: 2^32 - 1. */
|
|
149
|
+
const MAX_UINT32 = 4294967295;
|
|
150
|
+
/**
|
|
151
|
+
* Clamps rather than wraps. `n >>> 0` is the obvious spelling and it is wrong
|
|
152
|
+
* here: 2^32 becomes **0**, so a 4 GiB window advertises *zero credit* and the
|
|
153
|
+
* peer stalls forever with no error — the exact silent hang credit exists to
|
|
154
|
+
* remove. 2^32 + 5 becomes 5, which looks like a working window and is worse.
|
|
155
|
+
* Anything above the ceiling is advertised as the ceiling.
|
|
156
|
+
*/
|
|
157
|
+
function encodeUint32(n) {
|
|
158
|
+
const bytes = /* @__PURE__ */ new Uint8Array(4);
|
|
159
|
+
const clamped = Number.isFinite(n) ? Math.min(MAX_UINT32, Math.max(0, Math.floor(n))) : 0;
|
|
160
|
+
new DataView(bytes.buffer).setUint32(0, clamped, false);
|
|
161
|
+
return bytes;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Returns `undefined` rather than a garbage number when the payload is too
|
|
165
|
+
* short, so a truncated frame — or one from a peer predating credit — is
|
|
166
|
+
* detectable at the call site instead of silently granting nonsense.
|
|
167
|
+
*/
|
|
168
|
+
function decodeUint32(bytes) {
|
|
169
|
+
if (bytes.byteLength < 4) return void 0;
|
|
170
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, false);
|
|
171
|
+
}
|
|
69
172
|
const TYPE_OPEN = 1;
|
|
70
173
|
const TYPE_DATA = 2;
|
|
71
174
|
const TYPE_ACK = 3;
|
|
@@ -81,6 +184,7 @@ function emulateMux(channel, opts = {}) {
|
|
|
81
184
|
const maxStreams = opts.maxStreams ?? DEFAULT_MAX_STREAMS;
|
|
82
185
|
const mtu = opts.mtu ?? DEFAULT_MTU;
|
|
83
186
|
const maxStreamBuffer = opts.maxStreamBuffer ?? DEFAULT_MAX_STREAM_BUFFER;
|
|
187
|
+
if (!(maxStreamBuffer >= 1)) throw new RangeError(`emulateMux: maxStreamBuffer must be at least 1, got ${maxStreamBuffer}`);
|
|
84
188
|
const side = opts.side ?? "initiator";
|
|
85
189
|
const streams = /* @__PURE__ */ new Map();
|
|
86
190
|
let nextLocalId = side === "initiator" ? 2 : 1;
|
|
@@ -101,13 +205,9 @@ function emulateMux(channel, opts = {}) {
|
|
|
101
205
|
const teardownStream = (s, err) => {
|
|
102
206
|
if (s.closed) return;
|
|
103
207
|
s.closed = true;
|
|
104
|
-
|
|
105
|
-
const reject = s.rejectAck;
|
|
106
|
-
s.resolveAck = null;
|
|
107
|
-
s.rejectAck = null;
|
|
108
|
-
if (reject && err) reject(err);
|
|
109
|
-
else resolve?.();
|
|
208
|
+
s.outboundCredit.fail(err ?? new TransportClosedError("emulateMux: stream closed"));
|
|
110
209
|
s.doneIn(err);
|
|
210
|
+
s.cancelInput?.();
|
|
111
211
|
streams.delete(s.id);
|
|
112
212
|
};
|
|
113
213
|
/**
|
|
@@ -141,8 +241,8 @@ function emulateMux(channel, opts = {}) {
|
|
|
141
241
|
}),
|
|
142
242
|
pushIn: queue.push,
|
|
143
243
|
doneIn: queue.done,
|
|
144
|
-
|
|
145
|
-
|
|
244
|
+
outboundCredit: newCreditLedger(0),
|
|
245
|
+
grantor: newCreditGrantor(maxStreamBuffer),
|
|
146
246
|
closed: false,
|
|
147
247
|
inDone: false,
|
|
148
248
|
outDone: false,
|
|
@@ -151,20 +251,24 @@ function emulateMux(channel, opts = {}) {
|
|
|
151
251
|
return state;
|
|
152
252
|
};
|
|
153
253
|
const pumpOutbound = async (s, input) => {
|
|
254
|
+
const iterator = input[Symbol.asyncIterator]?.() ?? input[Symbol.iterator]();
|
|
255
|
+
s.cancelInput = () => {
|
|
256
|
+
iterator.return?.(void 0);
|
|
257
|
+
};
|
|
154
258
|
try {
|
|
155
|
-
for
|
|
259
|
+
for (;;) {
|
|
260
|
+
const next = await iterator.next();
|
|
261
|
+
if (next.done === true) break;
|
|
262
|
+
const chunk = next.value;
|
|
156
263
|
if (s.closed || muxClosed) return;
|
|
157
264
|
if (chunk.byteLength === 0) continue;
|
|
158
265
|
let off = 0;
|
|
159
266
|
while (off < chunk.byteLength) {
|
|
160
267
|
if (s.closed || muxClosed) return;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
s.resolveAck = resolve;
|
|
166
|
-
s.rejectAck = reject;
|
|
167
|
-
});
|
|
268
|
+
const take = await s.outboundCredit.reserve(Math.min(mtu, chunk.byteLength - off));
|
|
269
|
+
if (s.closed || muxClosed) return;
|
|
270
|
+
const end = off + take;
|
|
271
|
+
sendFrame(s.id, TYPE_DATA, chunk.subarray(off, end));
|
|
168
272
|
off = end;
|
|
169
273
|
}
|
|
170
274
|
}
|
|
@@ -222,6 +326,9 @@ function emulateMux(channel, opts = {}) {
|
|
|
222
326
|
}
|
|
223
327
|
const s = createStream(id);
|
|
224
328
|
streams.set(id, s);
|
|
329
|
+
const advertised = decodeUint32(payload);
|
|
330
|
+
if (advertised !== void 0) s.outboundCredit.grant(advertised);
|
|
331
|
+
sendFrame(id, TYPE_ACK, encodeUint32(maxStreamBuffer));
|
|
225
332
|
runHandler(s, handler);
|
|
226
333
|
return;
|
|
227
334
|
}
|
|
@@ -239,15 +346,15 @@ function emulateMux(channel, opts = {}) {
|
|
|
239
346
|
const copy = payload.byteLength === 0 ? payload : new Uint8Array(payload);
|
|
240
347
|
s.pushIn(copy).then((handled) => {
|
|
241
348
|
s.queuedBytes -= copy.byteLength;
|
|
242
|
-
if (handled
|
|
349
|
+
if (!handled || s.closed || muxClosed) return;
|
|
350
|
+
const grant = s.grantor.consumed(copy.byteLength, s.queuedBytes === 0);
|
|
351
|
+
if (grant > 0) sendFrame(id, TYPE_ACK, encodeUint32(grant));
|
|
243
352
|
});
|
|
244
353
|
return;
|
|
245
354
|
}
|
|
246
355
|
case TYPE_ACK: {
|
|
247
|
-
const
|
|
248
|
-
s.
|
|
249
|
-
s.rejectAck = null;
|
|
250
|
-
r?.();
|
|
356
|
+
const granted = decodeUint32(payload);
|
|
357
|
+
if (granted !== void 0) s.outboundCredit.grant(granted);
|
|
251
358
|
return;
|
|
252
359
|
}
|
|
253
360
|
case TYPE_END:
|
|
@@ -284,7 +391,7 @@ function emulateMux(channel, opts = {}) {
|
|
|
284
391
|
nextLocalId += 2;
|
|
285
392
|
const s = createStream(id);
|
|
286
393
|
streams.set(id, s);
|
|
287
|
-
sendFrame(id, TYPE_OPEN);
|
|
394
|
+
sendFrame(id, TYPE_OPEN, encodeUint32(maxStreamBuffer));
|
|
288
395
|
pumpOutbound(s, input);
|
|
289
396
|
return s.inbound;
|
|
290
397
|
};
|
|
@@ -424,8 +531,6 @@ function decodeError(buf) {
|
|
|
424
531
|
return new Error(textDecoder.decode(buf));
|
|
425
532
|
}
|
|
426
533
|
}
|
|
427
|
-
//#endregion
|
|
428
|
-
//#region ../webrun-streams/src/lines.ts
|
|
429
534
|
/** Split a stream of string chunks into individual lines (delimited by \n). */
|
|
430
535
|
async function* splitLines(input) {
|
|
431
536
|
let buffer = "";
|
|
@@ -444,14 +549,10 @@ async function* splitLines(input) {
|
|
|
444
549
|
async function* joinLines(input) {
|
|
445
550
|
for await (const line of input) yield `${line}\n`;
|
|
446
551
|
}
|
|
447
|
-
//#endregion
|
|
448
|
-
//#region ../webrun-streams/src/map.ts
|
|
449
552
|
/** Apply a sync or async function to each item in a stream. */
|
|
450
553
|
async function* map(input, fn) {
|
|
451
554
|
for await (const item of input) yield await fn(item);
|
|
452
555
|
}
|
|
453
|
-
//#endregion
|
|
454
|
-
//#region ../webrun-streams/src/jsonl.ts
|
|
455
556
|
/** Encode each value as a JSON line (terminated by \n). */
|
|
456
557
|
async function* encodeJsonl(input) {
|
|
457
558
|
yield* map(input, (item) => `${JSON.stringify(item)}\n`);
|
|
@@ -463,8 +564,6 @@ async function* decodeJsonl(input) {
|
|
|
463
564
|
if (trimmed) yield JSON.parse(trimmed);
|
|
464
565
|
}
|
|
465
566
|
}
|
|
466
|
-
//#endregion
|
|
467
|
-
//#region ../webrun-streams/src/new-async-generator.ts
|
|
468
567
|
/**
|
|
469
568
|
* The newAsyncGenerator function creates async generators from callback-based initialization
|
|
470
569
|
* functions, providing a bridge between imperative event handling and declarative async
|
|
@@ -617,8 +716,6 @@ async function* newAsyncGenerator(init, skipValues = false) {
|
|
|
617
716
|
drainQueue();
|
|
618
717
|
}
|
|
619
718
|
}
|
|
620
|
-
//#endregion
|
|
621
|
-
//#region ../webrun-streams/src/normalize.ts
|
|
622
719
|
const utf8 = new TextEncoder();
|
|
623
720
|
/**
|
|
624
721
|
* Coerce common byte-like inputs into a `Uint8Array`. Strings encode as UTF-8.
|
|
@@ -639,36 +736,65 @@ function describe(value) {
|
|
|
639
736
|
if (t !== "object") return t;
|
|
640
737
|
return value.constructor?.name ?? "object";
|
|
641
738
|
}
|
|
642
|
-
|
|
643
|
-
|
|
739
|
+
/**
|
|
740
|
+
* The iterator ↔ `ReadableStream` boundary.
|
|
741
|
+
*
|
|
742
|
+
* Both adapters must carry CANCELLATION, not just data: a response body leaves
|
|
743
|
+
* a handler as a `ReadableStream`, crosses a transport as an iterator, and
|
|
744
|
+
* becomes a `ReadableStream` again at the caller — so when the caller walks
|
|
745
|
+
* away, the only path back to the handler's producer runs through both of
|
|
746
|
+
* these functions. Teardown that stops at an adapter leaves a producer running
|
|
747
|
+
* for ever.
|
|
748
|
+
*/
|
|
644
749
|
function toReadableStream(it) {
|
|
645
|
-
return new ReadableStream({
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
750
|
+
return new ReadableStream({
|
|
751
|
+
/**
|
|
752
|
+
* One chunk per pull. An earlier version drained the whole iterator inside
|
|
753
|
+
* a single `pull`, which defeated the stream's own backpressure (every
|
|
754
|
+
* chunk was enqueued as fast as the producer could make them, however slow
|
|
755
|
+
* the reader was) and left no point between chunks at which a cancellation
|
|
756
|
+
* could take effect.
|
|
757
|
+
*/
|
|
758
|
+
async pull(controller) {
|
|
759
|
+
try {
|
|
649
760
|
const slot = await it.next();
|
|
650
|
-
if (!slot || slot.done)
|
|
651
|
-
|
|
652
|
-
|
|
761
|
+
if (!slot || slot.done) {
|
|
762
|
+
controller.close();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
controller.enqueue(await slot.value);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
controller.error(error);
|
|
653
768
|
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
769
|
+
},
|
|
770
|
+
/**
|
|
771
|
+
* Release the source. NOT awaited: `.return()` on an async generator that
|
|
772
|
+
* is parked awaiting its own source is queued behind that pending
|
|
773
|
+
* `next()`, so awaiting it here would hang `reader.cancel()` on exactly
|
|
774
|
+
* the producers that most need cancelling.
|
|
775
|
+
*/
|
|
776
|
+
cancel(reason) {
|
|
777
|
+
Promise.resolve(it.return?.(reason)).catch(() => {});
|
|
659
778
|
}
|
|
660
|
-
}
|
|
779
|
+
});
|
|
661
780
|
}
|
|
662
781
|
async function* fromReadableStream(stream) {
|
|
663
782
|
const reader = stream.getReader();
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
783
|
+
let drained = false;
|
|
784
|
+
try {
|
|
785
|
+
while (true) {
|
|
786
|
+
const { done, value } = await reader.read();
|
|
787
|
+
if (done) {
|
|
788
|
+
drained = true;
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
if (value !== void 0) yield value;
|
|
792
|
+
}
|
|
793
|
+
} finally {
|
|
794
|
+
if (drained) reader.releaseLock();
|
|
795
|
+
else await reader.cancel().catch(() => {});
|
|
668
796
|
}
|
|
669
797
|
}
|
|
670
|
-
//#endregion
|
|
671
|
-
//#region ../webrun-streams/src/recieve-iterator.ts
|
|
672
798
|
/**
|
|
673
799
|
* Inverse of {@link sendIterator}: turns a sequence of `{done, value, error}`
|
|
674
800
|
* chunks (delivered to the supplied callback by `installer`) into an async
|
|
@@ -691,8 +817,6 @@ function recieveIterator(installer) {
|
|
|
691
817
|
};
|
|
692
818
|
});
|
|
693
819
|
}
|
|
694
|
-
//#endregion
|
|
695
|
-
//#region ../webrun-streams/src/send-iterator.ts
|
|
696
820
|
/**
|
|
697
821
|
* Drain an async iterator into a sink that consumes one chunk at a time.
|
|
698
822
|
*
|
|
@@ -716,8 +840,6 @@ async function sendIterator(send, it) {
|
|
|
716
840
|
});
|
|
717
841
|
}
|
|
718
842
|
}
|
|
719
|
-
//#endregion
|
|
720
|
-
//#region ../webrun-streams/src/text.ts
|
|
721
843
|
/** Decode Uint8Array chunks to string chunks via TextDecoder, handling split multi-byte characters. */
|
|
722
844
|
async function* decodeText(input) {
|
|
723
845
|
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
@@ -733,8 +855,6 @@ async function* encodeText(input) {
|
|
|
733
855
|
const encoder = new TextEncoder();
|
|
734
856
|
for await (const str of input) yield encoder.encode(str);
|
|
735
857
|
}
|
|
736
|
-
//#endregion
|
|
737
|
-
//#region ../webrun-streams/src/to-chunks.ts
|
|
738
858
|
const DEFAULT_CHUNK_SIZE = 16384;
|
|
739
859
|
/**
|
|
740
860
|
* Curried `Duplex`-shaped transformer that splits incoming `Uint8Array`s into
|
|
@@ -877,11 +997,14 @@ async function* sendStream(communicationPort, input, params = {}) {
|
|
|
877
997
|
params
|
|
878
998
|
}, [messageChannel.port2]);
|
|
879
999
|
const channel = newStreamChannel(messageChannel.port1);
|
|
1000
|
+
let drained = false;
|
|
880
1001
|
try {
|
|
881
1002
|
await channel.start();
|
|
882
1003
|
channel.sendAll(input);
|
|
883
1004
|
yield* channel.recieveAll();
|
|
1005
|
+
drained = true;
|
|
884
1006
|
} finally {
|
|
1007
|
+
if (!drained) channel.cancel();
|
|
885
1008
|
await channel.close();
|
|
886
1009
|
}
|
|
887
1010
|
}
|
|
@@ -909,11 +1032,33 @@ function newStreamChannel(port) {
|
|
|
909
1032
|
const notifyAll = async (data) => {
|
|
910
1033
|
for (const listener of listeners) await listener(data);
|
|
911
1034
|
};
|
|
1035
|
+
/**
|
|
1036
|
+
* Release whatever we are sending. NOT awaited: `.return()` on an async
|
|
1037
|
+
* generator parked awaiting its own source is queued behind that pending
|
|
1038
|
+
* `next()`, so awaiting it here would block the message handler — and the
|
|
1039
|
+
* chunk that would unblock it can only arrive through that same handler.
|
|
1040
|
+
*/
|
|
1041
|
+
const cancelOutgoing = () => {
|
|
1042
|
+
for (const it of [...iterators]) {
|
|
1043
|
+
const iterable = it;
|
|
1044
|
+
Promise.resolve(iterable.return?.()).catch(() => {});
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
912
1047
|
const channel = newInvokationChannel({
|
|
913
1048
|
port,
|
|
914
|
-
handler: (data) =>
|
|
1049
|
+
handler: (data) => {
|
|
1050
|
+
const message = data;
|
|
1051
|
+
if (message?.cancel) {
|
|
1052
|
+
cancelOutgoing();
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
return notifyAll(message);
|
|
1056
|
+
}
|
|
915
1057
|
});
|
|
916
1058
|
const start = () => channel.start();
|
|
1059
|
+
const cancel = () => {
|
|
1060
|
+
channel.invoke({ cancel: true }).catch(() => {});
|
|
1061
|
+
};
|
|
917
1062
|
const close = async () => {
|
|
918
1063
|
await notifyAll({ done: true });
|
|
919
1064
|
for (const it of [...iterators]) await it.return?.();
|
|
@@ -940,6 +1085,7 @@ function newStreamChannel(port) {
|
|
|
940
1085
|
return {
|
|
941
1086
|
start,
|
|
942
1087
|
close,
|
|
1088
|
+
cancel,
|
|
943
1089
|
recieveAll,
|
|
944
1090
|
sendAll
|
|
945
1091
|
};
|
|
@@ -979,7 +1125,193 @@ function newRegistry(onError = console.error) {
|
|
|
979
1125
|
});
|
|
980
1126
|
}
|
|
981
1127
|
//#endregion
|
|
982
|
-
//#region
|
|
1128
|
+
//#region src/core/deadline.ts
|
|
1129
|
+
/**
|
|
1130
|
+
* Settles like `promise` if it settles before `deadline` (a `Date.now()`
|
|
1131
|
+
* value). Otherwise calls `onTimeout`: an `Error` it returns rejects, any
|
|
1132
|
+
* other value resolves. Internal; not re-exported.
|
|
1133
|
+
*/
|
|
1134
|
+
function withDeadline(deadline, promise, onTimeout) {
|
|
1135
|
+
return new Promise((resolve, reject) => {
|
|
1136
|
+
const timer = setTimeout(() => {
|
|
1137
|
+
const outcome = onTimeout();
|
|
1138
|
+
if (outcome instanceof Error) reject(outcome);
|
|
1139
|
+
else resolve(outcome);
|
|
1140
|
+
}, Math.max(0, deadline - Date.now()));
|
|
1141
|
+
promise.then((value) => {
|
|
1142
|
+
clearTimeout(timer);
|
|
1143
|
+
resolve(value);
|
|
1144
|
+
}, (error) => {
|
|
1145
|
+
clearTimeout(timer);
|
|
1146
|
+
reject(error);
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/core/service-worker-control.ts
|
|
1152
|
+
/**
|
|
1153
|
+
* How long, by default, the page waits for its ServiceWorker to activate and
|
|
1154
|
+
* to take control before giving up. Generous: a first install downloads and
|
|
1155
|
+
* evaluates the worker script, which on a slow link takes seconds.
|
|
1156
|
+
*/
|
|
1157
|
+
const DEFAULT_SERVICE_WORKER_TIMEOUT = 3e4;
|
|
1158
|
+
/**
|
|
1159
|
+
* How long the page waits for `controllerchange` once the worker has
|
|
1160
|
+
* answered the `CLAIM` request. `clients.claim()` resolves only after the
|
|
1161
|
+
* browser has queued that event, so this is a grace period for delivery, not
|
|
1162
|
+
* a second budget.
|
|
1163
|
+
*/
|
|
1164
|
+
const CLAIM_GRACE_MS = 1e3;
|
|
1165
|
+
/** Channel call a page sends to ask its ServiceWorker to `clients.claim()` it. */
|
|
1166
|
+
const CLAIM_CALL = "CLAIM";
|
|
1167
|
+
/**
|
|
1168
|
+
* Why a page could not get a working ServiceWorker. `reason` says which wait
|
|
1169
|
+
* failed; check it (or `name`) rather than `instanceof`, because each of this
|
|
1170
|
+
* package's bundles carries its own copy of this class.
|
|
1171
|
+
*/
|
|
1172
|
+
var ServiceWorkerControlError = class extends Error {
|
|
1173
|
+
reason;
|
|
1174
|
+
constructor(reason, message) {
|
|
1175
|
+
super(message);
|
|
1176
|
+
this.name = "ServiceWorkerControlError";
|
|
1177
|
+
this.reason = reason;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
/**
|
|
1181
|
+
* Resolves with the registration's worker once it is `activated`. Rejects
|
|
1182
|
+
* with a `ServiceWorkerControlError` (`reason: "activation-timeout"`) if that
|
|
1183
|
+
* takes longer than `timeout` — an install that throws or never finishes
|
|
1184
|
+
* would otherwise leave the caller waiting forever.
|
|
1185
|
+
*/
|
|
1186
|
+
async function awaitActiveServiceWorker(registration, { timeout = DEFAULT_SERVICE_WORKER_TIMEOUT } = {}) {
|
|
1187
|
+
return await withDeadline(Date.now() + timeout, waitForActivated(registration), () => {
|
|
1188
|
+
const worker = registration.installing ?? registration.waiting ?? registration.active;
|
|
1189
|
+
return new ServiceWorkerControlError("activation-timeout", `ServiceWorker ${scriptUrl(worker)} (scope ${registration.scope}) did not activate within ${timeout} ms` + (worker ? `; it is "${worker.state}"` : "") + ". Check the worker script for errors during install (DevTools → Application → Service Workers), or raise the `timeout` option.");
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Resolves with the ServiceWorker that controls this page, once `registration`
|
|
1194
|
+
* has an activated worker and the page is controlled by it.
|
|
1195
|
+
*
|
|
1196
|
+
* A page can stay uncontrolled while its worker is active, and then no
|
|
1197
|
+
* `controllerchange` ever fires on its own:
|
|
1198
|
+
* - a hard reload (Ctrl+Shift+R) bypasses the worker for that load, and the
|
|
1199
|
+
* worker's `clients.claim()` already ran when it activated;
|
|
1200
|
+
* - Firefox can leave a page loaded while the worker is running uncontrolled.
|
|
1201
|
+
*
|
|
1202
|
+
* So when the page is uncontrolled, this asks the active worker to claim it
|
|
1203
|
+
* again (a `CLAIM` channel call — this package's workers answer it) and waits
|
|
1204
|
+
* for `controllerchange`. Every wait is bounded by `timeout`. If control never
|
|
1205
|
+
* comes it reloads once (`reloadIfUncontrolled`) or rejects with a
|
|
1206
|
+
* `ServiceWorkerControlError` (`reason: "uncontrolled"`) that says what
|
|
1207
|
+
* happened and what to do.
|
|
1208
|
+
*/
|
|
1209
|
+
async function awaitServiceWorkerControl(registration, { timeout = DEFAULT_SERVICE_WORKER_TIMEOUT, reloadIfUncontrolled = false, container = navigator.serviceWorker } = {}) {
|
|
1210
|
+
const deadline = Date.now() + timeout;
|
|
1211
|
+
const controlled = waitForController(container);
|
|
1212
|
+
try {
|
|
1213
|
+
const active = await awaitActiveServiceWorker(registration, { timeout });
|
|
1214
|
+
let controller = container.controller;
|
|
1215
|
+
if (!controller) controller = await withDeadline(deadline, (async () => {
|
|
1216
|
+
await Promise.race([callChannel(active, CLAIM_CALL, {}), controlled.promise]);
|
|
1217
|
+
return await Promise.race([controlled.promise, delay(CLAIM_GRACE_MS, null)]);
|
|
1218
|
+
})(), () => null);
|
|
1219
|
+
if (controller) {
|
|
1220
|
+
forgetReload(registration);
|
|
1221
|
+
return controller;
|
|
1222
|
+
}
|
|
1223
|
+
if (reloadIfUncontrolled && markReload(registration)) {
|
|
1224
|
+
location.reload();
|
|
1225
|
+
return await new Promise(() => {});
|
|
1226
|
+
}
|
|
1227
|
+
forgetReload(registration);
|
|
1228
|
+
throw new ServiceWorkerControlError("uncontrolled", `This page is not controlled by its ServiceWorker ${scriptUrl(active)} (scope ${registration.scope}), although the worker is active, and the worker did not take control when asked (clients.claim()) within ${timeout} ms. This happens after a hard reload (Ctrl+Shift+R / Cmd+Shift+R), which bypasses ServiceWorkers for that load, and in Firefox for some pages opened while the worker was already running. Requests from this page would not reach the worker. Reload the page normally, pass \`reloadIfUncontrolled: true\` to do that automatically, check that the page is inside the worker's scope, and that the worker answers the "${CLAIM_CALL}" request (this package's workers do).`);
|
|
1229
|
+
} finally {
|
|
1230
|
+
controlled.cancel();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* ServiceWorker side: answers the page's `CLAIM` request with
|
|
1235
|
+
* `clients.claim()`, which takes over every uncontrolled client in scope.
|
|
1236
|
+
* Returns a function that stops answering.
|
|
1237
|
+
*/
|
|
1238
|
+
function handleClaimRequests(self) {
|
|
1239
|
+
return handleChannelCalls(self, CLAIM_CALL, (event) => {
|
|
1240
|
+
const claimed = self.clients.claim().then(() => true);
|
|
1241
|
+
event.waitUntil?.(claimed);
|
|
1242
|
+
return claimed;
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
function waitForActivated(registration) {
|
|
1246
|
+
return new Promise((resolve) => {
|
|
1247
|
+
const watched = /* @__PURE__ */ new Set();
|
|
1248
|
+
const check = () => {
|
|
1249
|
+
const active = registration.active;
|
|
1250
|
+
if (active?.state === "activated") {
|
|
1251
|
+
registration.removeEventListener("updatefound", watch);
|
|
1252
|
+
for (const worker of watched) worker.removeEventListener("statechange", check);
|
|
1253
|
+
resolve(active);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
watch();
|
|
1257
|
+
};
|
|
1258
|
+
function watch() {
|
|
1259
|
+
for (const worker of [
|
|
1260
|
+
registration.installing,
|
|
1261
|
+
registration.waiting,
|
|
1262
|
+
registration.active
|
|
1263
|
+
]) {
|
|
1264
|
+
if (!worker || watched.has(worker)) continue;
|
|
1265
|
+
watched.add(worker);
|
|
1266
|
+
worker.addEventListener("statechange", check);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
registration.addEventListener("updatefound", watch);
|
|
1270
|
+
check();
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
function waitForController(container) {
|
|
1274
|
+
let cancel = () => {};
|
|
1275
|
+
return {
|
|
1276
|
+
promise: new Promise((resolve) => {
|
|
1277
|
+
const onChange = () => {
|
|
1278
|
+
if (!container.controller) return;
|
|
1279
|
+
cancel();
|
|
1280
|
+
resolve(container.controller);
|
|
1281
|
+
};
|
|
1282
|
+
cancel = () => container.removeEventListener("controllerchange", onChange);
|
|
1283
|
+
container.addEventListener("controllerchange", onChange);
|
|
1284
|
+
}),
|
|
1285
|
+
cancel
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function delay(ms, value) {
|
|
1289
|
+
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
|
|
1290
|
+
}
|
|
1291
|
+
function scriptUrl(worker) {
|
|
1292
|
+
return worker?.scriptURL ? `"${worker.scriptURL}"` : "(no worker)";
|
|
1293
|
+
}
|
|
1294
|
+
function reloadKey(registration) {
|
|
1295
|
+
return `webrun-http-browser:reloaded-uncontrolled:${registration.scope}`;
|
|
1296
|
+
}
|
|
1297
|
+
/** Records the reload about to happen. `false` if one already happened, or if it cannot be recorded. */
|
|
1298
|
+
function markReload(registration) {
|
|
1299
|
+
try {
|
|
1300
|
+
const key = reloadKey(registration);
|
|
1301
|
+
if (sessionStorage.getItem(key)) return false;
|
|
1302
|
+
sessionStorage.setItem(key, "1");
|
|
1303
|
+
return true;
|
|
1304
|
+
} catch {
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
function forgetReload(registration) {
|
|
1309
|
+
try {
|
|
1310
|
+
sessionStorage.removeItem(reloadKey(registration));
|
|
1311
|
+
} catch {}
|
|
1312
|
+
}
|
|
1313
|
+
//#endregion
|
|
1314
|
+
//#region ../webrun-http-streams/dist/index.js
|
|
983
1315
|
const CR = 13;
|
|
984
1316
|
const LF = 10;
|
|
985
1317
|
const EMPTY = /* @__PURE__ */ new Uint8Array(0);
|
|
@@ -1104,8 +1436,6 @@ var ByteReader = class {
|
|
|
1104
1436
|
}
|
|
1105
1437
|
}
|
|
1106
1438
|
};
|
|
1107
|
-
//#endregion
|
|
1108
|
-
//#region ../webrun-http-streams/src/http1/errors.ts
|
|
1109
1439
|
/**
|
|
1110
1440
|
* Raised for any byte sequence this codec refuses to interpret. Every case is
|
|
1111
1441
|
* a refusal to guess: HTTP/1.1 parsers that guess are how request smuggling
|
|
@@ -1114,8 +1444,6 @@ var ByteReader = class {
|
|
|
1114
1444
|
var HttpParseError = class extends Error {
|
|
1115
1445
|
name = "HttpParseError";
|
|
1116
1446
|
};
|
|
1117
|
-
//#endregion
|
|
1118
|
-
//#region ../webrun-http-streams/src/envelope.ts
|
|
1119
1447
|
const NEWLINE = 10;
|
|
1120
1448
|
/** Mirrors the HTTP/1.1 codec's default `maxHeaderBytes`. */
|
|
1121
1449
|
const MAX_ENVELOPE_BYTES = 65536;
|
|
@@ -1199,8 +1527,6 @@ const jsonEnvelopeCodec = {
|
|
|
1199
1527
|
decodeRequest: (input) => decodeMessage(input),
|
|
1200
1528
|
decodeResponse: (input) => decodeMessage(input)
|
|
1201
1529
|
};
|
|
1202
|
-
//#endregion
|
|
1203
|
-
//#region ../webrun-http-streams/src/http1/headers.ts
|
|
1204
1530
|
const TCHAR = /* @__PURE__ */ new Set("!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
|
|
1205
1531
|
const SP = 32;
|
|
1206
1532
|
const HTAB = 9;
|
|
@@ -1376,8 +1702,6 @@ function resolveFraming(headers, version) {
|
|
|
1376
1702
|
};
|
|
1377
1703
|
return { kind: "none" };
|
|
1378
1704
|
}
|
|
1379
|
-
//#endregion
|
|
1380
|
-
//#region ../webrun-http-streams/src/http1/chunked.ts
|
|
1381
1705
|
const CRLF = new Uint8Array([13, 10]);
|
|
1382
1706
|
const LAST_CHUNK = new Uint8Array([
|
|
1383
1707
|
48,
|
|
@@ -1444,8 +1768,6 @@ async function* decodeChunked(reader, maxLineBytes) {
|
|
|
1444
1768
|
}
|
|
1445
1769
|
}
|
|
1446
1770
|
}
|
|
1447
|
-
//#endregion
|
|
1448
|
-
//#region ../webrun-http-streams/src/http1/decode.ts
|
|
1449
1771
|
const VERSIONS = /* @__PURE__ */ new Set(["HTTP/1.1", "HTTP/1.0"]);
|
|
1450
1772
|
const ABSOLUTE_FORM = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
|
|
1451
1773
|
/**
|
|
@@ -1583,8 +1905,6 @@ async function decodeResponse(input, opts, method) {
|
|
|
1583
1905
|
toHttpParseError(err);
|
|
1584
1906
|
}
|
|
1585
1907
|
}
|
|
1586
|
-
//#endregion
|
|
1587
|
-
//#region ../webrun-http-streams/src/http1/encode.ts
|
|
1588
1908
|
/** Headers the codec owns: a caller-supplied copy is dropped and re-derived. */
|
|
1589
1909
|
const REQUEST_OWNED = [
|
|
1590
1910
|
"host",
|
|
@@ -1680,8 +2000,6 @@ async function* encodeResponse(env, body, _opts, requestMethod) {
|
|
|
1680
2000
|
}
|
|
1681
2001
|
yield* emitBody(body, declared);
|
|
1682
2002
|
}
|
|
1683
|
-
//#endregion
|
|
1684
|
-
//#region ../webrun-http-streams/src/http1/index.ts
|
|
1685
2003
|
function newHttpCodec(options = {}) {
|
|
1686
2004
|
const opts = {
|
|
1687
2005
|
scheme: options.scheme ?? "http",
|
|
@@ -1698,8 +2016,6 @@ function newHttpCodec(options = {}) {
|
|
|
1698
2016
|
};
|
|
1699
2017
|
}
|
|
1700
2018
|
const httpCodec = newHttpCodec();
|
|
1701
|
-
//#endregion
|
|
1702
|
-
//#region ../webrun-http-streams/src/sniff.ts
|
|
1703
2019
|
/**
|
|
1704
2020
|
* Dispatches on byte 0. The formats are self-identifying — a JSON envelope
|
|
1705
2021
|
* always begins `{`, which is not a token character and so can never begin an
|
|
@@ -1745,8 +2061,6 @@ function newSniffingCodec(options) {
|
|
|
1745
2061
|
}
|
|
1746
2062
|
};
|
|
1747
2063
|
}
|
|
1748
|
-
//#endregion
|
|
1749
|
-
//#region ../webrun-http-streams/src/codec-default.ts
|
|
1750
2064
|
/**
|
|
1751
2065
|
* Writes HTTP/1.1; accepts HTTP/1.1 or the legacy JSON envelope. Used whenever
|
|
1752
2066
|
* no `codec` option is supplied.
|
|
@@ -1755,8 +2069,6 @@ const defaultCodec = newSniffingCodec({
|
|
|
1755
2069
|
write: httpCodec,
|
|
1756
2070
|
accept: [httpCodec, jsonEnvelopeCodec]
|
|
1757
2071
|
});
|
|
1758
|
-
//#endregion
|
|
1759
|
-
//#region ../webrun-http-streams/src/http-data.ts
|
|
1760
2072
|
/** Carries the serialized JS error between two webrun peers. */
|
|
1761
2073
|
const PEER_ERROR_HEADER = "x-webrun-error";
|
|
1762
2074
|
/** Hard cap on the peer-error header (M3): see `encodeErrorResponse`. */
|
|
@@ -1865,8 +2177,6 @@ function httpServe(handler, options = {}) {
|
|
|
1865
2177
|
yield* replyCodec.encodeResponse(result.envelope, result.body, { method: decoded.envelope.method });
|
|
1866
2178
|
};
|
|
1867
2179
|
}
|
|
1868
|
-
//#endregion
|
|
1869
|
-
//#region ../webrun-http-streams/src/request-streams.ts
|
|
1870
2180
|
/**
|
|
1871
2181
|
* The one place that knows a runtime may not implement request body streams,
|
|
1872
2182
|
* and the buffering both fallbacks need. `fetch.ts` and `http-stubs.ts` each
|
|
@@ -1935,8 +2245,6 @@ async function collectBytes$1(source) {
|
|
|
1935
2245
|
}
|
|
1936
2246
|
return out;
|
|
1937
2247
|
}
|
|
1938
|
-
//#endregion
|
|
1939
|
-
//#region ../webrun-http-streams/src/http-stubs.ts
|
|
1940
2248
|
const NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
|
|
1941
2249
|
101,
|
|
1942
2250
|
103,
|
|
@@ -2032,8 +2340,6 @@ function newHttpServerStub(handler) {
|
|
|
2032
2340
|
};
|
|
2033
2341
|
};
|
|
2034
2342
|
}
|
|
2035
|
-
//#endregion
|
|
2036
|
-
//#region ../webrun-http-streams/src/fetch.ts
|
|
2037
2343
|
/**
|
|
2038
2344
|
* Connection-scoped headers. The codec surfaces them verbatim (decision 12),
|
|
2039
2345
|
* but they are meaningless to a `Request`/`Response`, and re-emitting them
|
|
@@ -2184,8 +2490,6 @@ function abortReason(signal) {
|
|
|
2184
2490
|
err.name = "AbortError";
|
|
2185
2491
|
return err;
|
|
2186
2492
|
}
|
|
2187
|
-
//#endregion
|
|
2188
|
-
//#region ../webrun-http-streams/src/duplex-site-builder.ts
|
|
2189
2493
|
/**
|
|
2190
2494
|
* Host a `SiteHandler` over a `Connect/Serve` pair. The `start(serve, params)`
|
|
2191
2495
|
* call hands the handler to any `webrun-streams-*` adapter's `serve`.
|
|
@@ -2193,7 +2497,7 @@ function abortReason(signal) {
|
|
|
2193
2497
|
* ```ts
|
|
2194
2498
|
* import { SiteBuilder } from "@statewalker/webrun-site-builder";
|
|
2195
2499
|
* import { DuplexSiteBuilder } from "@statewalker/webrun-http-streams";
|
|
2196
|
-
* import { serve } from "@statewalker/webrun-
|
|
2500
|
+
* import { serve } from "@statewalker/webrun-rpc";
|
|
2197
2501
|
*
|
|
2198
2502
|
* const handler = new SiteBuilder()
|
|
2199
2503
|
* .setEndpoint("/api/time", () => new Response(new Date().toISOString()))
|
|
@@ -2224,8 +2528,6 @@ var DuplexSiteBuilder = class {
|
|
|
2224
2528
|
return serve(params, serveFetchOverDuplex(async (req) => handler(req), { codec: this.#codec }));
|
|
2225
2529
|
}
|
|
2226
2530
|
};
|
|
2227
|
-
//#endregion
|
|
2228
|
-
//#region ../webrun-http-streams/src/http-error.ts
|
|
2229
2531
|
var HttpError = class HttpError extends Error {
|
|
2230
2532
|
status;
|
|
2231
2533
|
statusText;
|
|
@@ -2308,13 +2610,21 @@ async function httpFromIterator(iterable) {
|
|
|
2308
2610
|
};
|
|
2309
2611
|
}
|
|
2310
2612
|
/**
|
|
2311
|
-
*
|
|
2312
|
-
*
|
|
2313
|
-
*
|
|
2314
|
-
*
|
|
2315
|
-
*
|
|
2316
|
-
* `
|
|
2317
|
-
* `
|
|
2613
|
+
* Serve an `HttpHandler` over a `MessageTarget`, using this package's own
|
|
2614
|
+
* `handleStreams` transport.
|
|
2615
|
+
*
|
|
2616
|
+
* @deprecated Prefer the port stack in `@statewalker/webrun-rpc`: open a port
|
|
2617
|
+
* (`multiplexPort` over one pipe, or `transferPortMux` where the platform can
|
|
2618
|
+
* transfer a real `MessagePort`), turn it into a `Duplex` with
|
|
2619
|
+
* `serveDuplexOverPort`, and serve HTTP on that with
|
|
2620
|
+
* `httpServe(handler, options)` from `@statewalker/webrun-http-streams`.
|
|
2621
|
+
*
|
|
2622
|
+
* That path has backpressure; **this one does not.** `sendStream`'s chunk
|
|
2623
|
+
* sender discards the promise it is given, so a fast producer over a slow
|
|
2624
|
+
* consumer accumulates without bound. It also has no per-stream timeout and no
|
|
2625
|
+
* chunking to a transport's message ceiling.
|
|
2626
|
+
*
|
|
2627
|
+
* Kept for existing ServiceWorker setups built on the `MessageTarget` surface.
|
|
2318
2628
|
*/
|
|
2319
2629
|
function handleHttpRequests(communicationPort, handler) {
|
|
2320
2630
|
const serverStub = newHttpServerStub(handler);
|
|
@@ -2324,13 +2634,18 @@ function handleHttpRequests(communicationPort, handler) {
|
|
|
2324
2634
|
});
|
|
2325
2635
|
}
|
|
2326
2636
|
/**
|
|
2327
|
-
*
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2330
|
-
*
|
|
2331
|
-
*
|
|
2332
|
-
*
|
|
2333
|
-
* `
|
|
2637
|
+
* Ship a `Request` over a `MessageTarget` and await the `Response`, using this
|
|
2638
|
+
* package's own `sendStream` transport.
|
|
2639
|
+
*
|
|
2640
|
+
* @deprecated Prefer the port stack in `@statewalker/webrun-rpc`: open a port
|
|
2641
|
+
* (`multiplexPort` over one pipe, or `transferPortMux` where the platform can
|
|
2642
|
+
* transfer a real `MessagePort`), turn it into a `Duplex` with
|
|
2643
|
+
* `duplexOverPort`, and drive HTTP over it with `httpFetch` from
|
|
2644
|
+
* `@statewalker/webrun-http-streams`.
|
|
2645
|
+
*
|
|
2646
|
+
* Same caveat as {@link handleHttpRequests}: the transport underneath this
|
|
2647
|
+
* helper has no backpressure, no per-stream timeout, and no chunking to a
|
|
2648
|
+
* transport's message ceiling. Kept for existing ServiceWorker setups.
|
|
2334
2649
|
*/
|
|
2335
2650
|
async function sendHttpRequest(communicationPort, request) {
|
|
2336
2651
|
return await newHttpClientStub(async (req) => {
|
|
@@ -2369,13 +2684,25 @@ function splitServiceUrl(url, separator = "~") {
|
|
|
2369
2684
|
//#endregion
|
|
2370
2685
|
//#region src/relay/index.ts
|
|
2371
2686
|
/**
|
|
2687
|
+
* The URL of this module, kept in a variable on purpose. Bundlers (Vite
|
|
2688
|
+
* among them) rewrite every literal `new URL("<path>", import.meta.url)` into
|
|
2689
|
+
* an emitted asset at build time, before tree-shaking — so the defaults below
|
|
2690
|
+
* made every Vite consumer of this entry emit a dead copy of the package's
|
|
2691
|
+
* own `dist/index.js` (`"../"` resolves to the package, hence to its `main`).
|
|
2692
|
+
* Resolving against a variable is the same URL at run time and invisible to
|
|
2693
|
+
* that transform.
|
|
2694
|
+
*/
|
|
2695
|
+
const moduleUrl = import.meta.url;
|
|
2696
|
+
/**
|
|
2372
2697
|
* Returns a MessagePort that transparently bridges messages to/from the
|
|
2373
|
-
* ServiceWorker controlling
|
|
2698
|
+
* page's ServiceWorker: the one controlling the page, or — when the page is
|
|
2699
|
+
* not controlled (a hard reload, or a page Firefox left uncontrolled) — the
|
|
2700
|
+
* active worker of `registration`, which answers messages all the same.
|
|
2374
2701
|
*/
|
|
2375
|
-
function newServiceWorkerPort() {
|
|
2702
|
+
function newServiceWorkerPort(registration) {
|
|
2376
2703
|
const channel = new MessageChannel();
|
|
2377
2704
|
channel.port1.onmessage = (event) => {
|
|
2378
|
-
navigator.serviceWorker.controller?.postMessage(event.data, [...event.ports]);
|
|
2705
|
+
(navigator.serviceWorker.controller ?? registration?.active)?.postMessage(event.data, [...event.ports]);
|
|
2379
2706
|
};
|
|
2380
2707
|
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
2381
2708
|
channel.port1.postMessage(event.data, [...event.ports]);
|
|
@@ -2383,45 +2710,26 @@ function newServiceWorkerPort() {
|
|
|
2383
2710
|
return channel.port2;
|
|
2384
2711
|
}
|
|
2385
2712
|
/**
|
|
2386
|
-
* Registers a ServiceWorker and resolves once it
|
|
2713
|
+
* Registers a ServiceWorker and resolves with it once it is activated: the
|
|
2714
|
+
* worker controlling the page, or the registration's active worker when the
|
|
2715
|
+
* page is not controlled. Messaging works either way, which is all the relay
|
|
2716
|
+
* needs; nothing here waits for control, because an uncontrolled page (hard
|
|
2717
|
+
* reload; Firefox) may never get it. Rejects with a
|
|
2718
|
+
* `ServiceWorkerControlError` if activation takes longer than `timeout`.
|
|
2387
2719
|
*/
|
|
2388
|
-
async function initServiceWorker(
|
|
2389
|
-
await
|
|
2720
|
+
async function initServiceWorker(options) {
|
|
2721
|
+
return (await registerServiceWorker(options)).worker;
|
|
2722
|
+
}
|
|
2723
|
+
async function registerServiceWorker({ swUrl, scopeUrl, type, timeout = DEFAULT_SERVICE_WORKER_TIMEOUT }) {
|
|
2724
|
+
const registration = await navigator.serviceWorker.register(swUrl, {
|
|
2390
2725
|
type,
|
|
2391
2726
|
scope: scopeUrl
|
|
2392
2727
|
});
|
|
2393
|
-
const
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
return new Promise((resolve) => {
|
|
2399
|
-
const container = navigator.serviceWorker;
|
|
2400
|
-
if (container.controller) {
|
|
2401
|
-
resolve(container.controller);
|
|
2402
|
-
return;
|
|
2403
|
-
}
|
|
2404
|
-
const onChange = () => {
|
|
2405
|
-
if (!container.controller) return;
|
|
2406
|
-
resolve(container.controller);
|
|
2407
|
-
container.removeEventListener("controllerchange", onChange);
|
|
2408
|
-
};
|
|
2409
|
-
container.addEventListener("controllerchange", onChange);
|
|
2410
|
-
});
|
|
2411
|
-
}
|
|
2412
|
-
function awaitServiceWorkerActivation(worker) {
|
|
2413
|
-
return new Promise((resolve) => {
|
|
2414
|
-
if (worker.state === "activated") {
|
|
2415
|
-
resolve();
|
|
2416
|
-
return;
|
|
2417
|
-
}
|
|
2418
|
-
const onStateChange = () => {
|
|
2419
|
-
if (worker.state !== "activated") return;
|
|
2420
|
-
worker.removeEventListener("statechange", onStateChange);
|
|
2421
|
-
resolve();
|
|
2422
|
-
};
|
|
2423
|
-
worker.addEventListener("statechange", onStateChange);
|
|
2424
|
-
});
|
|
2728
|
+
const active = await awaitActiveServiceWorker(registration, { timeout });
|
|
2729
|
+
return {
|
|
2730
|
+
registration,
|
|
2731
|
+
worker: navigator.serviceWorker.controller ?? active
|
|
2732
|
+
};
|
|
2425
2733
|
}
|
|
2426
2734
|
/**
|
|
2427
2735
|
* Registers `handler` as the server for the given service `key` on the relay.
|
|
@@ -2452,9 +2760,11 @@ async function callHttpService(request, { key, port }) {
|
|
|
2452
2760
|
/**
|
|
2453
2761
|
* Returns a `window.onmessage` handler for use inside the relay iframe:
|
|
2454
2762
|
* it accepts a CONNECT message, starts the relay ServiceWorker, and bridges
|
|
2455
|
-
* the parent's MessagePort with the SW.
|
|
2763
|
+
* the parent's MessagePort with the SW. If the worker cannot be started, every
|
|
2764
|
+
* call the parent makes on that port is answered with the error, so the
|
|
2765
|
+
* parent's `initHttpService` / `callHttpService` reject instead of waiting.
|
|
2456
2766
|
*/
|
|
2457
|
-
function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js",
|
|
2767
|
+
function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", moduleUrl)}`, scopeUrl = `${new URL("../", moduleUrl)}`, timeout } = {}) {
|
|
2458
2768
|
let externalPort;
|
|
2459
2769
|
return async (ev) => {
|
|
2460
2770
|
if (ev.data?.type !== "CONNECT") return;
|
|
@@ -2465,11 +2775,19 @@ function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", impo
|
|
|
2465
2775
|
return;
|
|
2466
2776
|
}
|
|
2467
2777
|
externalPort = newExternalPort;
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2778
|
+
let registration;
|
|
2779
|
+
try {
|
|
2780
|
+
({registration} = await registerServiceWorker({
|
|
2781
|
+
swUrl,
|
|
2782
|
+
scopeUrl,
|
|
2783
|
+
timeout
|
|
2784
|
+
}));
|
|
2785
|
+
} catch (error) {
|
|
2786
|
+
const serialized = serializeError(error);
|
|
2787
|
+
externalPort.onmessage = (event) => event.ports[0]?.postMessage({ error: serialized });
|
|
2788
|
+
throw error;
|
|
2789
|
+
}
|
|
2790
|
+
const serviceWorkerPort = newServiceWorkerPort(registration);
|
|
2473
2791
|
serviceWorkerPort.onmessage = (event) => {
|
|
2474
2792
|
externalPort?.postMessage(event.data, [...event.ports]);
|
|
2475
2793
|
};
|
|
@@ -2482,7 +2800,7 @@ function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", impo
|
|
|
2482
2800
|
* Embeds a hidden relay iframe, establishes a MessageChannel with it, and
|
|
2483
2801
|
* returns the port to be used with `initHttpService` / `callHttpService`.
|
|
2484
2802
|
*/
|
|
2485
|
-
async function newRemoteRelayChannel({ baseUrl = new URL("../public-relay/",
|
|
2803
|
+
async function newRemoteRelayChannel({ baseUrl = new URL("../public-relay/", moduleUrl), url = new URL("relay.html", baseUrl), container = document.body } = {}) {
|
|
2486
2804
|
const messageChannel = new MessageChannel();
|
|
2487
2805
|
const { iframe, promise } = newIFrame(url);
|
|
2488
2806
|
Object.assign(iframe.style, {
|
|
@@ -2551,4 +2869,4 @@ async function registerConnectionsHandler({ key, handler, communicationPort }) {
|
|
|
2551
2869
|
return cleanup;
|
|
2552
2870
|
}
|
|
2553
2871
|
//#endregion
|
|
2554
|
-
export { DuplexSiteBuilder, HttpError, HttpParseError, PEER_ERROR_HEADER, TransportClosedError, callChannel, callHttpService, collect, collectBytes, collectString, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleHttpRequests, handleStreams, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, joinLines, jsonEnvelopeCodec, map, newAsyncGenerator, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, sendHttpRequest, sendIterator, sendStream, serializeError, serveFetchOverDuplex, splitLines, splitServiceUrl, toChunks, toReadableStream };
|
|
2872
|
+
export { CLAIM_CALL, DEFAULT_SERVICE_WORKER_TIMEOUT, DuplexSiteBuilder, HttpError, HttpParseError, PEER_ERROR_HEADER, ServiceWorkerControlError, TransportClosedError, awaitActiveServiceWorker, awaitServiceWorkerControl, callChannel, callHttpService, collect, collectBytes, collectString, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleClaimRequests, handleHttpRequests, handleStreams, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, joinLines, jsonEnvelopeCodec, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, sendHttpRequest, sendIterator, sendStream, serializeError, serveFetchOverDuplex, splitLines, splitServiceUrl, toChunks, toReadableStream };
|