transport-io 0.0.1 → 0.1.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 +13 -6
- package/dist/adapter.d.ts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/codec.js +1 -1
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +1 -1
- package/dist/datagram.js +1 -1
- package/dist/errors.js +1 -1
- package/dist/framer.d.ts +3 -3
- package/dist/framer.js +6 -6
- package/dist/handshake.d.ts +1 -1
- package/dist/handshake.js +1 -1
- package/dist/hub.d.ts +1 -1
- package/dist/hub.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/origin.d.ts +1 -1
- package/dist/origin.js +3 -3
- package/dist/protocol.d.ts +12 -12
- package/dist/protocol.js +12 -12
- package/dist/queue.d.ts +2 -2
- package/dist/queue.js +3 -3
- package/dist/server.js +2 -2
- package/dist/session.d.ts +5 -5
- package/dist/session.js +17 -17
- package/dist/transport/browser.js +1 -1
- package/dist/transport/fails.node.js +2 -2
- package/dist/transport/moq.node.js +1 -1
- package/dist/transport/parity-suite.d.ts +2 -2
- package/dist/transport/parity-suite.js +2 -2
- package/dist/transport/types.d.ts +2 -2
- package/dist/transport/unreliable.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/transport-io/transport-io/main/assets/brand/transport-io-lockup-bone.svg">
|
|
4
|
+
<img alt="transport-io" src="https://raw.githubusercontent.com/transport-io/transport-io/main/assets/brand/transport-io-lockup-ink.svg" width="340">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
8
|
Real-time apps over WebTransport. Socket.IO's shape, on a transport with multiple streams
|
|
4
9
|
and datagrams, without Socket.IO's mistakes.
|
|
@@ -7,13 +12,15 @@ Framing, length prefixes, buffer accumulation, stream lifecycle and backpressure
|
|
|
7
12
|
hidden. Reliability is not: an event declares `stream` or `datagram` in the contract, and
|
|
8
13
|
"this message may be dropped" is a property of your data that lives in the type system.
|
|
9
14
|
|
|
10
|
-
**
|
|
11
|
-
|
|
15
|
+
**Read [KNOWN-ISSUES.md](https://github.com/transport-io/transport-io/blob/main/KNOWN-ISSUES.md)
|
|
16
|
+
before you start.** It is what this library refuses to do and will not change, plus the one
|
|
17
|
+
measured defect. Full documentation is in the
|
|
18
|
+
[repository README](https://github.com/transport-io/transport-io#readme). The short version:
|
|
12
19
|
|
|
13
|
-
- **WebTransport only.** No WebSocket fallback, deliberately
|
|
20
|
+
- **WebTransport only.** No WebSocket fallback, deliberately - a fallback would silently
|
|
14
21
|
make the datagram lane reliable and ordered, which is a lie about your data.
|
|
15
22
|
- **Chrome and Firefox.** Safari cannot talk to a quiche-backed server and is unsupported.
|
|
16
|
-
- **The server needs a separate native install**, and its Linux prebuild needs glibc 2.38
|
|
23
|
+
- **The server needs a separate native install**, and its Linux prebuild needs glibc 2.38 -
|
|
17
24
|
no default Node `-slim` image has it, and Alpine has no prebuild at all.
|
|
18
25
|
- **Each `call()` leaks ~5.95 KB of server memory**, upstream in the QUIC binding, not in
|
|
19
26
|
this library. `emit` and datagrams are flat.
|
|
@@ -23,6 +30,6 @@ hidden. Reliability is not: an event declares `stream` or `datagram` in the cont
|
|
|
23
30
|
npm install transport-io
|
|
24
31
|
```
|
|
25
32
|
|
|
26
|
-
A git install does not work
|
|
33
|
+
A git install does not work - the repository root is a private monorepo package.
|
|
27
34
|
|
|
28
35
|
MIT © #V0ID
|
package/dist/adapter.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Rules core obeys (D40): every method is async even in memory, frames cross as bytes and
|
|
6
6
|
* never as live objects, `PeerId` is a stable cross-process string, no node assumes it
|
|
7
7
|
* knows a room's full membership, a frame for a room with no local members is dropped
|
|
8
|
-
* silently rather than erroring, and any method may reject
|
|
8
|
+
* silently rather than erroring, and any method may reject - core degrades rather than
|
|
9
9
|
* crashing.
|
|
10
10
|
*/
|
|
11
11
|
export type PeerId = string;
|
package/dist/client.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Client surface.
|
|
3
3
|
*
|
|
4
4
|
* Constructible without I/O: nothing here touches `window` or `WebTransport` at module
|
|
5
|
-
* scope, so importing this on a server
|
|
5
|
+
* scope, so importing this on a server - which Next.js will do - is safe. Feature
|
|
6
6
|
* detection happens inside connect().
|
|
7
7
|
*/
|
|
8
8
|
import { type AnyMap, type CallableOf, type Contract } from './contract.ts';
|
package/dist/client.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Client surface.
|
|
3
3
|
*
|
|
4
4
|
* Constructible without I/O: nothing here touches `window` or `WebTransport` at module
|
|
5
|
-
* scope, so importing this on a server
|
|
5
|
+
* scope, so importing this on a server - which Next.js will do - is safe. Feature
|
|
6
6
|
* detection happens inside connect().
|
|
7
7
|
*/
|
|
8
8
|
import { buildEventTable } from './contract.js';
|
package/dist/codec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** JSON over UTF-8. PROTOCOL.md §5.3
|
|
1
|
+
/** JSON over UTF-8. PROTOCOL.md §5.3 - codec 0x01, the only one this version speaks. */
|
|
2
2
|
import { TransportError } from './errors.js';
|
|
3
3
|
const encoder = new TextEncoder();
|
|
4
4
|
const decoder = new TextDecoder('utf-8', { fatal: true });
|
package/dist/contract.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type Schema = StandardSchemaV1;
|
|
|
12
12
|
* property checking against a *union* admits any property present on any member, so
|
|
13
13
|
* `{ lane: 'datagram', payload, returns }` compiled happily, `CallableOf` admitted it, and
|
|
14
14
|
* `call()` served it over a bidirectional stream. A contract that says "may be dropped"
|
|
15
|
-
* produced a guaranteed ordered message with the type system agreeing
|
|
15
|
+
* produced a guaranteed ordered message with the type system agreeing - a direct violation
|
|
16
16
|
* of D1, the first decision this project made.
|
|
17
17
|
*/
|
|
18
18
|
export type EventDef = {
|
|
@@ -30,7 +30,7 @@ export type Contract = Readonly<Record<string, EventDef>>;
|
|
|
30
30
|
export type Infer<S extends StandardTypedV1> = StandardTypedV1.InferOutput<S>;
|
|
31
31
|
/**
|
|
32
32
|
* The plain payload/returns map every public signature is written against, so that no
|
|
33
|
-
* method hover ever has to print a validator's internal types. See D57
|
|
33
|
+
* method hover ever has to print a validator's internal types. See D57 - declaring
|
|
34
34
|
* `interface AppMap extends MapOf<typeof contract> {}` is what keeps hover at 126
|
|
35
35
|
* characters instead of 303.
|
|
36
36
|
*/
|
|
@@ -68,7 +68,7 @@ export interface EventTable {
|
|
|
68
68
|
wire(): readonly [string, number, Lane][];
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* PROTOCOL.md §5.4
|
|
71
|
+
* PROTOCOL.md §5.4 - the first four bytes of SHA-256 of the event name, big-endian.
|
|
72
72
|
*
|
|
73
73
|
* Async because `crypto.subtle` is, and because writing SHA-256 by hand would mean typing
|
|
74
74
|
* its round constants from memory, which is the one thing D58 forbids. Both `connect()`
|
package/dist/contract.js
CHANGED
|
@@ -14,7 +14,7 @@ export function type$() {
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* PROTOCOL.md §5.4
|
|
17
|
+
* PROTOCOL.md §5.4 - the first four bytes of SHA-256 of the event name, big-endian.
|
|
18
18
|
*
|
|
19
19
|
* Async because `crypto.subtle` is, and because writing SHA-256 by hand would mean typing
|
|
20
20
|
* its round constants from memory, which is the one thing D58 forbids. Both `connect()`
|
package/dist/datagram.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Datagram header. PROTOCOL.md §7.1
|
|
1
|
+
/** Datagram header. PROTOCOL.md §7.1 - codec, event id, origin, sequence, then payload. */
|
|
2
2
|
import { TransportError } from './errors.js';
|
|
3
3
|
import { Codec, DATAGRAM_CONSERVATIVE_FLOOR, DATAGRAM_HEADER_BYTES, EVENT_ID_NOT_APPLICABLE, } from './protocol.js';
|
|
4
4
|
/**
|
package/dist/errors.js
CHANGED
package/dist/framer.d.ts
CHANGED
|
@@ -5,12 +5,12 @@ export interface Frame {
|
|
|
5
5
|
readonly eventId: number;
|
|
6
6
|
readonly payload: Uint8Array;
|
|
7
7
|
}
|
|
8
|
-
/** §5.1
|
|
8
|
+
/** §5.1 - the cap is per frame type: a call is the documented home for a large payload. */
|
|
9
9
|
export declare function maxPayloadFor(type: FrameType): number;
|
|
10
10
|
export declare function encodeFrame(frame: Frame): Uint8Array;
|
|
11
11
|
/**
|
|
12
|
-
* Incremental decoder. Feed it whatever the transport hands you
|
|
13
|
-
* frames, or a frame split across many reads
|
|
12
|
+
* Incremental decoder. Feed it whatever the transport hands you - a fragment, several
|
|
13
|
+
* frames, or a frame split across many reads - and it yields whole frames only.
|
|
14
14
|
*/
|
|
15
15
|
export declare class FrameDecoder {
|
|
16
16
|
#private;
|
package/dist/framer.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* QUIC streams are byte streams and do not preserve write boundaries. Measured on the
|
|
5
5
|
* reference transport, 51 writes arrived as 217 reads, and the large write fragmented
|
|
6
|
-
* while the small ones happened to survive
|
|
6
|
+
* while the small ones happened to survive - which is the worst case, because naive
|
|
7
7
|
* boundary-trusting code passes in development and fails under load. The length prefix
|
|
8
8
|
* is the only thing that recovers frame boundaries, and nobody using this library should
|
|
9
9
|
* ever have to think about it.
|
|
10
10
|
*/
|
|
11
11
|
import { TransportError } from './errors.js';
|
|
12
12
|
import { Codec, FrameType, isFrameType, LENGTH_PREFIX_BYTES, MAX_CALL_PAYLOAD_BYTES, MAX_EMIT_PAYLOAD_BYTES, MIN_LENGTH, STREAM_HEADER_BYTES, } from './protocol.js';
|
|
13
|
-
/** §5.1
|
|
13
|
+
/** §5.1 - the cap is per frame type: a call is the documented home for a large payload. */
|
|
14
14
|
export function maxPayloadFor(type) {
|
|
15
15
|
return type === FrameType.CALL_REQUEST || type === FrameType.CALL_RESPONSE
|
|
16
16
|
? MAX_CALL_PAYLOAD_BYTES
|
|
@@ -36,8 +36,8 @@ export function encodeFrame(frame) {
|
|
|
36
36
|
return out;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* Incremental decoder. Feed it whatever the transport hands you
|
|
40
|
-
* frames, or a frame split across many reads
|
|
39
|
+
* Incremental decoder. Feed it whatever the transport hands you - a fragment, several
|
|
40
|
+
* frames, or a frame split across many reads - and it yields whole frames only.
|
|
41
41
|
*/
|
|
42
42
|
export class FrameDecoder {
|
|
43
43
|
#buf = new Uint8Array(0);
|
|
@@ -63,8 +63,8 @@ export class FrameDecoder {
|
|
|
63
63
|
}
|
|
64
64
|
const payloadLength = length - STREAM_HEADER_BYTES;
|
|
65
65
|
// §5.3 gives calls 16 MiB and everything else 1 MiB. This enforced the call cap for
|
|
66
|
-
// every frame type, so a peer could declare 16 MiB on the emit lane
|
|
67
|
-
// its documented cap
|
|
66
|
+
// every frame type, so a peer could declare 16 MiB on the emit lane - sixteen times
|
|
67
|
+
// its documented cap - and the decoder would buffer toward it.
|
|
68
68
|
//
|
|
69
69
|
// The type byte is at offset 4, so it is readable as soon as five bytes are, which is
|
|
70
70
|
// before any payload has to be held. Until then the universal cap applies, which
|
package/dist/handshake.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Handshake. PROTOCOL.md §4
|
|
2
|
+
* Handshake. PROTOCOL.md §4 - frame 0 of the emit stream.
|
|
3
3
|
*
|
|
4
4
|
* Being frame 0 of a stream that already exists is what removes the early-traffic race:
|
|
5
5
|
* in-order delivery within a stream means nothing can arrive before it. There is no
|
package/dist/handshake.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Handshake. PROTOCOL.md §4
|
|
2
|
+
* Handshake. PROTOCOL.md §4 - frame 0 of the emit stream.
|
|
3
3
|
*
|
|
4
4
|
* Being frame 0 of a stream that already exists is what removes the early-traffic race:
|
|
5
5
|
* in-order delivery within a stream means nothing can arrive before it. There is no
|
package/dist/hub.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare class Hub {
|
|
|
26
26
|
* Teardown runs to completion whatever the bus does.
|
|
27
27
|
*
|
|
28
28
|
* `broadcast` already wrapped its adapter call; this did not, so a rejection on the
|
|
29
|
-
* first room threw straight out of the loop
|
|
29
|
+
* first room threw straight out of the loop - rooms 2..N kept their `Member` record,
|
|
30
30
|
* each holding a live Session, and `#peerRooms.delete(id)` never ran. Nothing retries,
|
|
31
31
|
* because `conn.closed` resolves exactly once. A later `to(room).emit()` then fanned
|
|
32
32
|
* frames into a session that was already gone.
|
package/dist/hub.js
CHANGED
|
@@ -13,7 +13,7 @@ export class Hub {
|
|
|
13
13
|
this.#adapter = adapter;
|
|
14
14
|
this.#table = table;
|
|
15
15
|
// A node receiving its own publish back is normal, so core dedupes by origin node
|
|
16
|
-
// rather than relying on the adapter to suppress it
|
|
16
|
+
// rather than relying on the adapter to suppress it - and dedupes against the
|
|
17
17
|
// *adapter's* id, which is the one stamped into the envelope. It used to compare
|
|
18
18
|
// against the Server's separate `nodeId`, so any deployment where those differed
|
|
19
19
|
// delivered every local broadcast twice, in silence.
|
|
@@ -30,7 +30,7 @@ export class Hub {
|
|
|
30
30
|
// the emit path's swallowing catch, so nothing surfaced, and the teardown that would
|
|
31
31
|
// have removed it had already run.
|
|
32
32
|
if (session.disposed) {
|
|
33
|
-
throw new TransportError('WT_SESSION_CLOSED', `peer ${id} disconnected before it could join '${room}'`, 'Check the peer is still connected after any await, or ignore this
|
|
33
|
+
throw new TransportError('WT_SESSION_CLOSED', `peer ${id} disconnected before it could join '${room}'`, 'Check the peer is still connected after any await, or ignore this - it is routine.');
|
|
34
34
|
}
|
|
35
35
|
let members = this.#rooms.get(room);
|
|
36
36
|
if (members === undefined) {
|
|
@@ -40,7 +40,7 @@ export class Hub {
|
|
|
40
40
|
// The bus first, local state second. Mutating before the await left a rejected join
|
|
41
41
|
// half-applied: the hub fanned broadcasts to a peer the bus had no record of, and the
|
|
42
42
|
// client was never notified it had joined. For a room whose join is gated on
|
|
43
|
-
// authorization, that is traffic reaching someone who was refused
|
|
43
|
+
// authorization, that is traffic reaching someone who was refused - permanently,
|
|
44
44
|
// because nothing rolls it back and nothing retries.
|
|
45
45
|
await this.#adapter.join(room, id);
|
|
46
46
|
members.set(id, { id, session });
|
|
@@ -69,7 +69,7 @@ export class Hub {
|
|
|
69
69
|
* Teardown runs to completion whatever the bus does.
|
|
70
70
|
*
|
|
71
71
|
* `broadcast` already wrapped its adapter call; this did not, so a rejection on the
|
|
72
|
-
* first room threw straight out of the loop
|
|
72
|
+
* first room threw straight out of the loop - rooms 2..N kept their `Member` record,
|
|
73
73
|
* each holding a live Session, and `#peerRooms.delete(id)` never ran. Nothing retries,
|
|
74
74
|
* because `conn.closed` resolves exactly once. A later `to(room).emit()` then fanned
|
|
75
75
|
* frames into a session that was already gone.
|
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type { SessionStats } from './session.ts';
|
|
|
12
12
|
* `isolatedDeclarations` forbids inferring it from an import and a version that drifts from
|
|
13
13
|
* its manifest is a lie in the one place a user checks first.
|
|
14
14
|
*
|
|
15
|
-
* `changeset version` moves the manifest, so this moves in the same commit
|
|
15
|
+
* `changeset version` moves the manifest, so this moves in the same commit - which the test
|
|
16
16
|
* enforces rather than trusting anyone to remember.
|
|
17
17
|
*/
|
|
18
18
|
export declare const VERSION: string;
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,8 @@ export { createServer, Server, } from './server.js';
|
|
|
11
11
|
* `isolatedDeclarations` forbids inferring it from an import and a version that drifts from
|
|
12
12
|
* its manifest is a lie in the one place a user checks first.
|
|
13
13
|
*
|
|
14
|
-
* `changeset version` moves the manifest, so this moves in the same commit
|
|
14
|
+
* `changeset version` moves the manifest, so this moves in the same commit - which the test
|
|
15
15
|
* enforces rather than trusting anyone to remember.
|
|
16
16
|
*/
|
|
17
|
-
export const VERSION = '0.0
|
|
17
|
+
export const VERSION = '0.1.0';
|
|
18
18
|
//# sourceMappingURL=index.js.map
|
package/dist/origin.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export declare class OriginAllocator {
|
|
|
3
3
|
/**
|
|
4
4
|
* `counterSpace` exists so the exhaustion branch can be reached in a test. Exhausting the
|
|
5
5
|
* real space means 4,194,304 allocations, which is why §7.3's "MUST refuse new sessions"
|
|
6
|
-
* went unproven
|
|
6
|
+
* went unproven - and an unreachable branch in an allocator is exactly the kind of code
|
|
7
7
|
* that is wrong the first time it runs. Production never passes it.
|
|
8
8
|
*/
|
|
9
9
|
constructor(hostOrdinal?: number, quarantineMs?: number, counterSpace?: number);
|
package/dist/origin.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* where a collision is close to undebuggable, since two peers would share a sequence space
|
|
6
6
|
* and each would silently discard the other's datagrams as stale.
|
|
7
7
|
*
|
|
8
|
-
* Quarantined, not retired: never reusing turns the counter into a clock
|
|
9
|
-
* 100 sessions per second exhausts in under twelve hours
|
|
8
|
+
* Quarantined, not retired: never reusing turns the counter into a clock - 2^22 values at
|
|
9
|
+
* 100 sessions per second exhausts in under twelve hours - so a busy host would stop
|
|
10
10
|
* accepting sessions and need a restart.
|
|
11
11
|
*/
|
|
12
12
|
import { TransportError } from './errors.js';
|
|
@@ -23,7 +23,7 @@ export class OriginAllocator {
|
|
|
23
23
|
/**
|
|
24
24
|
* `counterSpace` exists so the exhaustion branch can be reached in a test. Exhausting the
|
|
25
25
|
* real space means 4,194,304 allocations, which is why §7.3's "MUST refuse new sessions"
|
|
26
|
-
* went unproven
|
|
26
|
+
* went unproven - and an unreachable branch in an allocator is exactly the kind of code
|
|
27
27
|
* that is wrong the first time it runs. Production never passes it.
|
|
28
28
|
*/
|
|
29
29
|
constructor(hostOrdinal = 0, quarantineMs = ORIGIN_QUARANTINE_MS, counterSpace = COUNTER_SPACE) {
|
package/dist/protocol.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* these values, so a document and an implementation cannot drift. Change one and the
|
|
6
6
|
* build fails until you change the other.
|
|
7
7
|
*/
|
|
8
|
-
/** PROTOCOL.md §4.2
|
|
8
|
+
/** PROTOCOL.md §4.2 - Stage 0 requires exact equality and refuses otherwise. */
|
|
9
9
|
export declare const PROTOCOL_VERSION = 0;
|
|
10
10
|
/** PROTOCOL.md §5.2 */
|
|
11
11
|
export declare const FrameType: {
|
|
@@ -24,9 +24,9 @@ export declare const Codec: {
|
|
|
24
24
|
readonly JSON: 1;
|
|
25
25
|
};
|
|
26
26
|
export type Codec = (typeof Codec)[keyof typeof Codec];
|
|
27
|
-
/** PROTOCOL.md §5.4
|
|
27
|
+
/** PROTOCOL.md §5.4 - used by frames whose meaning comes from the stream, not the table. */
|
|
28
28
|
export declare const EVENT_ID_NOT_APPLICABLE = 0;
|
|
29
|
-
/** PROTOCOL.md §5.1
|
|
29
|
+
/** PROTOCOL.md §5.1 - stream frame field budget. */
|
|
30
30
|
export declare const LENGTH_PREFIX_BYTES = 4;
|
|
31
31
|
export declare const STREAM_HEADER_BYTES = 8;
|
|
32
32
|
export declare const STREAM_FRAME_OVERHEAD_BYTES: number;
|
|
@@ -34,31 +34,31 @@ export declare const STREAM_FRAME_OVERHEAD_BYTES: number;
|
|
|
34
34
|
export declare const MIN_LENGTH: number;
|
|
35
35
|
export declare const MAX_EMIT_PAYLOAD_BYTES = 1048576;
|
|
36
36
|
export declare const MAX_CALL_PAYLOAD_BYTES = 16777216;
|
|
37
|
-
/** PROTOCOL.md §7.2
|
|
37
|
+
/** PROTOCOL.md §7.2 - datagram field budget. */
|
|
38
38
|
export declare const DATAGRAM_HEADER_BYTES = 13;
|
|
39
|
-
/** PROTOCOL.md §7.4
|
|
39
|
+
/** PROTOCOL.md §7.4 - used only when the transport reports nothing usable. */
|
|
40
40
|
export declare const DATAGRAM_CONSERVATIVE_FLOOR = 1024;
|
|
41
41
|
export declare const DATAGRAM_CONSERVATIVE_PAYLOAD_MAX: number;
|
|
42
|
-
/** PROTOCOL.md §9
|
|
42
|
+
/** PROTOCOL.md §9 - per-peer bounds. Numbers, not adjectives. */
|
|
43
43
|
export declare const DATAGRAM_QUEUE_MAX = 64;
|
|
44
44
|
export declare const EMIT_QUEUE_MAX = 256;
|
|
45
|
-
/** PROTOCOL.md §10.1 code 9
|
|
45
|
+
/** PROTOCOL.md §10.1 code 9 - a rejected open resets that stream, never the session. */
|
|
46
46
|
export declare const MAX_CONCURRENT_CALL_STREAMS = 256;
|
|
47
47
|
/** Checked at dequeue, not enqueue: overflow handles a burst, TTL handles a stall. */
|
|
48
48
|
export declare const DATAGRAM_TTL_MS = 150;
|
|
49
|
-
/** PROTOCOL.md §7.3
|
|
49
|
+
/** PROTOCOL.md §7.3 - a receiver discards (origin, event) sequence state after this idle. */
|
|
50
50
|
export declare const SEQUENCE_STATE_RETENTION_MS = 60000;
|
|
51
|
-
/** PROTOCOL.md §7.3
|
|
51
|
+
/** PROTOCOL.md §7.3 - a released Origin waits this long before returning to the pool. */
|
|
52
52
|
export declare const ORIGIN_QUARANTINE_MS = 120000;
|
|
53
|
-
/** PROTOCOL.md §7.3
|
|
53
|
+
/** PROTOCOL.md §7.3 - a departed host's ordinal waits this long before reallocation. */
|
|
54
54
|
export declare const HOST_ORDINAL_QUARANTINE_MS = 300000;
|
|
55
|
-
/** PROTOCOL.md §7.3
|
|
55
|
+
/** PROTOCOL.md §7.3 - stated ceiling on concurrent session hosts. */
|
|
56
56
|
export declare const MAX_SESSION_HOSTS = 1024;
|
|
57
57
|
/** PROTOCOL.md §4.1 */
|
|
58
58
|
export declare const HANDSHAKE_DEADLINE_MS = 5000;
|
|
59
59
|
/** PROTOCOL.md §10.2 */
|
|
60
60
|
export declare const CLOSE_REASON_MAX_BYTES = 1024;
|
|
61
|
-
/** PROTOCOL.md §10.1
|
|
61
|
+
/** PROTOCOL.md §10.1 - one byte, because the browser API clamps stream codes to an octet. */
|
|
62
62
|
export declare const ResetCode: {
|
|
63
63
|
readonly WT_NO_ERROR: 0;
|
|
64
64
|
readonly WT_ABORTED: 1;
|
package/dist/protocol.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* these values, so a document and an implementation cannot drift. Change one and the
|
|
6
6
|
* build fails until you change the other.
|
|
7
7
|
*/
|
|
8
|
-
/** PROTOCOL.md §4.2
|
|
8
|
+
/** PROTOCOL.md §4.2 - Stage 0 requires exact equality and refuses otherwise. */
|
|
9
9
|
export const PROTOCOL_VERSION = 0;
|
|
10
10
|
/** PROTOCOL.md §5.2 */
|
|
11
11
|
export const FrameType = {
|
|
@@ -23,9 +23,9 @@ export function isFrameType(v) {
|
|
|
23
23
|
}
|
|
24
24
|
/** PROTOCOL.md §5.3. `0x00` is permanently reserved so a zero-filled buffer cannot parse. */
|
|
25
25
|
export const Codec = { JSON: 0x01 };
|
|
26
|
-
/** PROTOCOL.md §5.4
|
|
26
|
+
/** PROTOCOL.md §5.4 - used by frames whose meaning comes from the stream, not the table. */
|
|
27
27
|
export const EVENT_ID_NOT_APPLICABLE = 0x00000000;
|
|
28
|
-
/** PROTOCOL.md §5.1
|
|
28
|
+
/** PROTOCOL.md §5.1 - stream frame field budget. */
|
|
29
29
|
export const LENGTH_PREFIX_BYTES = 4;
|
|
30
30
|
export const STREAM_HEADER_BYTES = 8;
|
|
31
31
|
export const STREAM_FRAME_OVERHEAD_BYTES = LENGTH_PREFIX_BYTES + STREAM_HEADER_BYTES;
|
|
@@ -33,31 +33,31 @@ export const STREAM_FRAME_OVERHEAD_BYTES = LENGTH_PREFIX_BYTES + STREAM_HEADER_B
|
|
|
33
33
|
export const MIN_LENGTH = STREAM_HEADER_BYTES + 1;
|
|
34
34
|
export const MAX_EMIT_PAYLOAD_BYTES = 1_048_576;
|
|
35
35
|
export const MAX_CALL_PAYLOAD_BYTES = 16_777_216;
|
|
36
|
-
/** PROTOCOL.md §7.2
|
|
36
|
+
/** PROTOCOL.md §7.2 - datagram field budget. */
|
|
37
37
|
export const DATAGRAM_HEADER_BYTES = 13;
|
|
38
|
-
/** PROTOCOL.md §7.4
|
|
38
|
+
/** PROTOCOL.md §7.4 - used only when the transport reports nothing usable. */
|
|
39
39
|
export const DATAGRAM_CONSERVATIVE_FLOOR = 1024;
|
|
40
40
|
export const DATAGRAM_CONSERVATIVE_PAYLOAD_MAX = DATAGRAM_CONSERVATIVE_FLOOR - DATAGRAM_HEADER_BYTES;
|
|
41
|
-
/** PROTOCOL.md §9
|
|
41
|
+
/** PROTOCOL.md §9 - per-peer bounds. Numbers, not adjectives. */
|
|
42
42
|
export const DATAGRAM_QUEUE_MAX = 64;
|
|
43
43
|
export const EMIT_QUEUE_MAX = 256;
|
|
44
|
-
/** PROTOCOL.md §10.1 code 9
|
|
44
|
+
/** PROTOCOL.md §10.1 code 9 - a rejected open resets that stream, never the session. */
|
|
45
45
|
export const MAX_CONCURRENT_CALL_STREAMS = 256;
|
|
46
46
|
/** Checked at dequeue, not enqueue: overflow handles a burst, TTL handles a stall. */
|
|
47
47
|
export const DATAGRAM_TTL_MS = 150;
|
|
48
|
-
/** PROTOCOL.md §7.3
|
|
48
|
+
/** PROTOCOL.md §7.3 - a receiver discards (origin, event) sequence state after this idle. */
|
|
49
49
|
export const SEQUENCE_STATE_RETENTION_MS = 60_000;
|
|
50
|
-
/** PROTOCOL.md §7.3
|
|
50
|
+
/** PROTOCOL.md §7.3 - a released Origin waits this long before returning to the pool. */
|
|
51
51
|
export const ORIGIN_QUARANTINE_MS = 120_000;
|
|
52
|
-
/** PROTOCOL.md §7.3
|
|
52
|
+
/** PROTOCOL.md §7.3 - a departed host's ordinal waits this long before reallocation. */
|
|
53
53
|
export const HOST_ORDINAL_QUARANTINE_MS = 300_000;
|
|
54
|
-
/** PROTOCOL.md §7.3
|
|
54
|
+
/** PROTOCOL.md §7.3 - stated ceiling on concurrent session hosts. */
|
|
55
55
|
export const MAX_SESSION_HOSTS = 1024;
|
|
56
56
|
/** PROTOCOL.md §4.1 */
|
|
57
57
|
export const HANDSHAKE_DEADLINE_MS = 5000;
|
|
58
58
|
/** PROTOCOL.md §10.2 */
|
|
59
59
|
export const CLOSE_REASON_MAX_BYTES = 1024;
|
|
60
|
-
/** PROTOCOL.md §10.1
|
|
60
|
+
/** PROTOCOL.md §10.1 - one byte, because the browser API clamps stream codes to an octet. */
|
|
61
61
|
export const ResetCode = {
|
|
62
62
|
WT_NO_ERROR: 0,
|
|
63
63
|
WT_ABORTED: 1,
|
package/dist/queue.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface QueueStats {
|
|
|
7
7
|
* Datagram lane: bounded ring, drop OLDEST on overflow, TTL checked at DEQUEUE.
|
|
8
8
|
*
|
|
9
9
|
* The two axes are different problems. Drop-oldest handles a burst. It does nothing for a
|
|
10
|
-
* peer that stalls two seconds and resumes
|
|
10
|
+
* peer that stalls two seconds and resumes - the ring never overflows, so a backlog of
|
|
11
11
|
* stale positions is delivered and the application renders history, which is worse than
|
|
12
12
|
* delivering nothing. TTL at dequeue is what fixes that, and the two causes are counted
|
|
13
13
|
* separately so an operator can tell a slow consumer from a slow network.
|
|
@@ -35,7 +35,7 @@ export declare class EmitQueue<T> {
|
|
|
35
35
|
push(item: T): void;
|
|
36
36
|
/**
|
|
37
37
|
* The head, left in place. An item leaves this queue only when its write has actually
|
|
38
|
-
* completed
|
|
38
|
+
* completed - `shift()` on hand-off would make depth a measure of nothing, which is
|
|
39
39
|
* precisely how the bound came to be unreachable.
|
|
40
40
|
*/
|
|
41
41
|
peek(): T | undefined;
|
package/dist/queue.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Backpressure. PROTOCOL.md §9
|
|
2
|
+
* Backpressure. PROTOCOL.md §9 - three lanes, three answers, because they make different
|
|
3
3
|
* promises.
|
|
4
4
|
*
|
|
5
5
|
* The transport swallows its own "blocked" indication along with "too big", so a refused
|
|
@@ -11,7 +11,7 @@ import { DATAGRAM_QUEUE_MAX, DATAGRAM_TTL_MS, EMIT_QUEUE_MAX } from './protocol.
|
|
|
11
11
|
* Datagram lane: bounded ring, drop OLDEST on overflow, TTL checked at DEQUEUE.
|
|
12
12
|
*
|
|
13
13
|
* The two axes are different problems. Drop-oldest handles a burst. It does nothing for a
|
|
14
|
-
* peer that stalls two seconds and resumes
|
|
14
|
+
* peer that stalls two seconds and resumes - the ring never overflows, so a backlog of
|
|
15
15
|
* stale positions is delivered and the application renders history, which is worse than
|
|
16
16
|
* delivering nothing. TTL at dequeue is what fixes that, and the two causes are counted
|
|
17
17
|
* separately so an operator can tell a slow consumer from a slow network.
|
|
@@ -78,7 +78,7 @@ export class EmitQueue {
|
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
80
|
* The head, left in place. An item leaves this queue only when its write has actually
|
|
81
|
-
* completed
|
|
81
|
+
* completed - `shift()` on hand-off would make depth a measure of nothing, which is
|
|
82
82
|
* precisely how the bound came to be unreachable.
|
|
83
83
|
*/
|
|
84
84
|
peek() {
|
package/dist/server.js
CHANGED
|
@@ -42,7 +42,7 @@ export class Server {
|
|
|
42
42
|
// Sweeping current peers rather than the ones present at registration: a session
|
|
43
43
|
// accepted in between picked the handler up from `#callHandlers`, so it has to be
|
|
44
44
|
// revoked too. Deleting from the map alone left every already-connected peer still
|
|
45
|
-
// being answered
|
|
45
|
+
// being answered - revoking a privileged responder did nothing for anyone connected.
|
|
46
46
|
for (const { session } of this.#peers.values())
|
|
47
47
|
session.unhandle(event);
|
|
48
48
|
};
|
|
@@ -95,7 +95,7 @@ export class Server {
|
|
|
95
95
|
})
|
|
96
96
|
// Teardown is the last thing that runs for this peer; there is no caller left to
|
|
97
97
|
// hand a rejection to. Without this it was an unhandled rejection, which ends the
|
|
98
|
-
// process under Node's default
|
|
98
|
+
// process under Node's default - the opposite of what ADR/0005 and D40 promise.
|
|
99
99
|
.catch(() => undefined);
|
|
100
100
|
await session.start();
|
|
101
101
|
for (const cb of this.#onPeer)
|
package/dist/session.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export declare class Session {
|
|
|
50
50
|
* identifiers, no pending map, and a stalled call blocks nothing else.
|
|
51
51
|
*
|
|
52
52
|
* There is no default timeout. A dead peer is detected by the QUIC idle timeout, which
|
|
53
|
-
* closes the session and rejects every pending call
|
|
53
|
+
* closes the session and rejects every pending call - the case a timeout is usually
|
|
54
54
|
* reached for is already handled. Pass `AbortSignal.timeout(ms)` for a slow but live
|
|
55
55
|
* responder.
|
|
56
56
|
*/
|
|
@@ -69,8 +69,8 @@ export declare class Session {
|
|
|
69
69
|
* The only door out of a session, and every internal path now uses it.
|
|
70
70
|
*
|
|
71
71
|
* Guarding this method alone was not enough: four call sites reached `#conn.close()`
|
|
72
|
-
* directly
|
|
73
|
-
* protocol error on the read loop
|
|
72
|
+
* directly - the handshake deadline, the peer-too-slow bound, an emit write failure and a
|
|
73
|
+
* protocol error on the read loop - so the guard covered the one path that already had
|
|
74
74
|
* the fewest duplicates. A soak still produced 619,422 `close sent twice` complaints from
|
|
75
75
|
* quiche after the first fix, which is what a partial guard looks like from the outside.
|
|
76
76
|
*/
|
|
@@ -78,13 +78,13 @@ export declare class Session {
|
|
|
78
78
|
/**
|
|
79
79
|
* Idempotent, and wired to `conn.closed` in `start()` so it cannot be forgotten.
|
|
80
80
|
*
|
|
81
|
-
* It was forgotten. `clearInterval` appeared in exactly one place
|
|
81
|
+
* It was forgotten. `clearInterval` appeared in exactly one place - `close()` - and
|
|
82
82
|
* neither teardown path called it: the server's `conn.closed` continuation freed the
|
|
83
83
|
* origin and removed the peer, and the client's patched a snapshot. Whichever side did
|
|
84
84
|
* not *initiate* the close kept a live interval whose callback closes over `this`,
|
|
85
85
|
* retaining the Session, its Connection, the frame decoder, both queues, the sequence
|
|
86
86
|
* gate and every handler set. At 100 sessions a second that is 360,000 unreclaimable
|
|
87
|
-
* Sessions an hour, and `unref()` does nothing about it
|
|
87
|
+
* Sessions an hour, and `unref()` does nothing about it - it stops a timer holding the
|
|
88
88
|
* event loop open, not holding memory.
|
|
89
89
|
*/
|
|
90
90
|
dispose(): void;
|
package/dist/session.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One side of a session. Both the server's view of a peer and the client's view of the
|
|
3
|
-
* server are this class
|
|
3
|
+
* server are this class - the handshake is symmetric, and so is everything after it.
|
|
4
4
|
*/
|
|
5
5
|
import { decodePayload, encodePayload, validate } from './codec.js';
|
|
6
6
|
import { decodeDatagram, encodeDatagram, SequenceGate } from './datagram.js';
|
|
@@ -26,7 +26,7 @@ function closeCodeFor(e) {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* §10.2 caps the reason at 1024 **bytes**. Slicing to 1024 characters overshoots by up to
|
|
29
|
-
* threefold on non-ASCII
|
|
29
|
+
* threefold on non-ASCII - and event names, which appear in mismatch messages, are the
|
|
30
30
|
* user's own domain language. Truncated on a code-point boundary so the result is never
|
|
31
31
|
* a broken surrogate pair.
|
|
32
32
|
*/
|
|
@@ -95,7 +95,7 @@ export class Session {
|
|
|
95
95
|
this.#handshakeReject = rej;
|
|
96
96
|
});
|
|
97
97
|
// `ready` is rejected from the emit-stream read loop, which can reach a refusal before
|
|
98
|
-
// start() has got as far as awaiting it
|
|
98
|
+
// start() has got as far as awaiting it - the peer's handshake is frame 0 and may be
|
|
99
99
|
// decoded during our own `openEmitStream()`. An unobserved rejection terminates a Node
|
|
100
100
|
// server by default, so it is observed here. start() still surfaces it to its caller.
|
|
101
101
|
void this.ready.catch(() => undefined);
|
|
@@ -114,7 +114,7 @@ export class Session {
|
|
|
114
114
|
* Armed before the stream is opened, and raced against every await that follows.
|
|
115
115
|
*
|
|
116
116
|
* It used to be armed *after* `openEmitStream()` and after our own handshake write, so
|
|
117
|
-
* if either never settled
|
|
117
|
+
* if either never settled - precisely the stalled-peer case this deadline exists for -
|
|
118
118
|
* no timer was ever armed and `connect()` hung for ever. Racing `ready` instead would
|
|
119
119
|
* not work: a peer whose handshake arrives before we have opened our own stream
|
|
120
120
|
* resolves `ready` early, and the race would fire on success.
|
|
@@ -229,7 +229,7 @@ export class Session {
|
|
|
229
229
|
* identifiers, no pending map, and a stalled call blocks nothing else.
|
|
230
230
|
*
|
|
231
231
|
* There is no default timeout. A dead peer is detected by the QUIC idle timeout, which
|
|
232
|
-
* closes the session and rejects every pending call
|
|
232
|
+
* closes the session and rejects every pending call - the case a timeout is usually
|
|
233
233
|
* reached for is already handled. Pass `AbortSignal.timeout(ms)` for a slow but live
|
|
234
234
|
* responder.
|
|
235
235
|
*/
|
|
@@ -241,7 +241,7 @@ export class Session {
|
|
|
241
241
|
// An already-aborted signal must not open a stream just to tear it down.
|
|
242
242
|
// §11: a peer that has detected its counterpart is gone MUST NOT reuse a stream from
|
|
243
243
|
// that session. Opening a new one on a dead session is the same mistake wearing a
|
|
244
|
-
// different hat
|
|
244
|
+
// different hat - the transport may even accept it, and the call then hangs.
|
|
245
245
|
if (this.#disposed) {
|
|
246
246
|
throw new TransportError('WT_SESSION_CLOSED', 'the session is closed, so no call stream can be opened on it', 'Reconnect. A reconnect is a new session and does not restore room membership (D4).');
|
|
247
247
|
}
|
|
@@ -249,7 +249,7 @@ export class Session {
|
|
|
249
249
|
throw abortToTransportError(opts.signal.reason);
|
|
250
250
|
// An event that declares no `returns` has no response to wait for. The type system
|
|
251
251
|
// already excludes it from `CallableOf`; this is the same refusal for a caller that
|
|
252
|
-
// reached the wire without the types
|
|
252
|
+
// reached the wire without the types - and it names the actual problem instead of
|
|
253
253
|
// travelling to the responder to come back as "no handler registered", which is a
|
|
254
254
|
// different fault with a different remedy.
|
|
255
255
|
if (entry.lane === 'stream' && entry.def.returns === undefined) {
|
|
@@ -268,7 +268,7 @@ export class Session {
|
|
|
268
268
|
catch (e) {
|
|
269
269
|
// D18 removes the default call timeout on the grounds that `AbortSignal.timeout(ms)`
|
|
270
270
|
// is the documented substitute, so aborting is the most-documented failure this
|
|
271
|
-
// library has
|
|
271
|
+
// library has - and it rejected with a raw DOMException carrying no code and no
|
|
272
272
|
// remedy, which the error helper printed in API.md reports as 'unknown'.
|
|
273
273
|
// Read through a call so narrowing from the pre-check above does not apply: the
|
|
274
274
|
// signal can abort at any point during the call, which is the whole reason it exists.
|
|
@@ -285,7 +285,7 @@ export class Session {
|
|
|
285
285
|
/**
|
|
286
286
|
* The cap is a receiver-side refusal or it is nothing. `call()` declining to open a
|
|
287
287
|
* 257th stream protects the peer from us; it does nothing about a peer that opens 10,000
|
|
288
|
-
*
|
|
288
|
+
* - a Go implementation written from PROTOCOL.md, or a browser calling
|
|
289
289
|
* `createBidirectionalStream()` directly. That is the case §10.1 code 9 exists for.
|
|
290
290
|
*
|
|
291
291
|
* Refused before the request is read, deliberately: the cost this bound exists to bound
|
|
@@ -336,15 +336,15 @@ export class Session {
|
|
|
336
336
|
* The only door out of a session, and every internal path now uses it.
|
|
337
337
|
*
|
|
338
338
|
* Guarding this method alone was not enough: four call sites reached `#conn.close()`
|
|
339
|
-
* directly
|
|
340
|
-
* protocol error on the read loop
|
|
339
|
+
* directly - the handshake deadline, the peer-too-slow bound, an emit write failure and a
|
|
340
|
+
* protocol error on the read loop - so the guard covered the one path that already had
|
|
341
341
|
* the fewest duplicates. A soak still produced 619,422 `close sent twice` complaints from
|
|
342
342
|
* quiche after the first fix, which is what a partial guard looks like from the outside.
|
|
343
343
|
*/
|
|
344
344
|
close(code, reason) {
|
|
345
345
|
// Idempotent in both halves. `dispose()` already was; `conn.close()` was not, so a
|
|
346
|
-
// second close
|
|
347
|
-
// down, which is ordinary
|
|
346
|
+
// second close - a client disconnecting while the server is tearing the same session
|
|
347
|
+
// down, which is ordinary - reached the transport twice. quiche logs
|
|
348
348
|
// "WebTransportHttp3 close sent twice" and refuses it, which is a protocol-level
|
|
349
349
|
// complaint we were generating and then ignoring.
|
|
350
350
|
if (this.#disposed)
|
|
@@ -355,13 +355,13 @@ export class Session {
|
|
|
355
355
|
/**
|
|
356
356
|
* Idempotent, and wired to `conn.closed` in `start()` so it cannot be forgotten.
|
|
357
357
|
*
|
|
358
|
-
* It was forgotten. `clearInterval` appeared in exactly one place
|
|
358
|
+
* It was forgotten. `clearInterval` appeared in exactly one place - `close()` - and
|
|
359
359
|
* neither teardown path called it: the server's `conn.closed` continuation freed the
|
|
360
360
|
* origin and removed the peer, and the client's patched a snapshot. Whichever side did
|
|
361
361
|
* not *initiate* the close kept a live interval whose callback closes over `this`,
|
|
362
362
|
* retaining the Session, its Connection, the frame decoder, both queues, the sequence
|
|
363
363
|
* gate and every handler set. At 100 sessions a second that is 360,000 unreclaimable
|
|
364
|
-
* Sessions an hour, and `unref()` does nothing about it
|
|
364
|
+
* Sessions an hour, and `unref()` does nothing about it - it stops a timer holding the
|
|
365
365
|
* event loop open, not holding memory.
|
|
366
366
|
*/
|
|
367
367
|
dispose() {
|
|
@@ -449,7 +449,7 @@ export class Session {
|
|
|
449
449
|
return;
|
|
450
450
|
}
|
|
451
451
|
// The request is fully read at this point, so nothing is watching the stream any
|
|
452
|
-
// more
|
|
452
|
+
// more - which is why an abort never reached the handler. The initiator's abort
|
|
453
453
|
// resets its send side AND cancels its read side, and that STOP_SENDING surfaces here
|
|
454
454
|
// as a rejection on our writer. Watch it, or `ctx.signal` is decoration.
|
|
455
455
|
void writer.closed.catch(() => controller.abort());
|
|
@@ -534,7 +534,7 @@ export class Session {
|
|
|
534
534
|
* same turn as the push and appended each frame to an unbounded promise chain, so depth
|
|
535
535
|
* returned to zero after every push and `EmitQueue`'s bound could never be reached from
|
|
536
536
|
* a Session. The backlog did not go away, it went somewhere that could not disconnect
|
|
537
|
-
* anyone
|
|
537
|
+
* anyone - and whose `.catch(() => undefined)` discarded every write failure on the lane
|
|
538
538
|
* that advertises reliable ordered delivery.
|
|
539
539
|
*
|
|
540
540
|
* Nothing flushes before the handshake, so frame 0 keeps its position by construction
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The browser Connection, over the platform's own `WebTransport`.
|
|
3
3
|
*
|
|
4
|
-
* Nothing here is imported at module scope
|
|
4
|
+
* Nothing here is imported at module scope - `WebTransport` is read inside connect() -
|
|
5
5
|
* so importing this on a server, which Next.js will do, is safe. There is no native
|
|
6
6
|
* addon and no binding, which is why this file is not `*.node.ts`.
|
|
7
7
|
*/
|
|
@@ -158,7 +158,7 @@ export async function connectHttp3(opts) {
|
|
|
158
158
|
// The binding loads its native transport through a dynamic import and throws
|
|
159
159
|
// `Lib quiche loading attempt did not end` if a client is constructed before it
|
|
160
160
|
// settles. A process that also runs a server never sees this, because the server
|
|
161
|
-
// awaits the same promise on the way up
|
|
161
|
+
// awaits the same promise on the way up - which is exactly why it went unnoticed
|
|
162
162
|
// until a client ran on its own.
|
|
163
163
|
await quicheLoaded;
|
|
164
164
|
const wt = new WebTransport(opts.url, {
|
|
@@ -168,7 +168,7 @@ export async function connectHttp3(opts) {
|
|
|
168
168
|
requireUnreliable: true,
|
|
169
169
|
});
|
|
170
170
|
// `closed` rejects independently of `ready`. If the handshake fails, nothing has
|
|
171
|
-
// attached to it yet and Node sees an unhandled rejection
|
|
171
|
+
// attached to it yet and Node sees an unhandled rejection - which terminates a server
|
|
172
172
|
// by default. Claim it before awaiting `ready`; FailsConnection re-reads the same
|
|
173
173
|
// settled promise, so nothing is lost.
|
|
174
174
|
const closedGuard = wt.closed.catch(() => undefined);
|
|
@@ -68,7 +68,7 @@ function toReadable(recv) {
|
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
cancel(reason) {
|
|
71
|
-
// STOP_SENDING with an explicit code
|
|
71
|
+
// STOP_SENDING with an explicit code - no message-string parsing needed here.
|
|
72
72
|
void recv.stop(codeOf(reason)).catch(() => undefined);
|
|
73
73
|
},
|
|
74
74
|
});
|
|
@@ -11,7 +11,7 @@ export interface UnderTest {
|
|
|
11
11
|
*
|
|
12
12
|
* `false` is a real capability gap, not a preference. moq surfaces STOP_SENDING only on
|
|
13
13
|
* the next write, and a long-running handler never makes one, so the handler is not
|
|
14
|
-
* told to stop. The caller still rejects either way
|
|
14
|
+
* told to stop. The caller still rejects either way - the work just keeps running.
|
|
15
15
|
*/
|
|
16
16
|
readonly propagatesAbortToHandler: boolean;
|
|
17
17
|
readonly name: string;
|
|
@@ -30,7 +30,7 @@ export interface UnderTest {
|
|
|
30
30
|
/**
|
|
31
31
|
* One transport per process, deliberately.
|
|
32
32
|
*
|
|
33
|
-
* Running both bindings' servers concurrently in a single process hangs
|
|
33
|
+
* Running both bindings' servers concurrently in a single process hangs - verified: each
|
|
34
34
|
* works alone and both can bind, but sessions on both at once deadlock. That is a
|
|
35
35
|
* property of running two native QUIC stacks side by side, not of transport-io, and no
|
|
36
36
|
* deployment would do it. Splitting by process is also better isolation.
|
|
@@ -23,7 +23,7 @@ const contract = defineContract({
|
|
|
23
23
|
slow: { lane: 'stream', payload: type$(), returns: type$() },
|
|
24
24
|
});
|
|
25
25
|
// Random high ports. A fixed port makes this suite fail for a reason that has nothing to
|
|
26
|
-
// do with the code
|
|
26
|
+
// do with the code - an orphan from a previous killed run still holding the socket, which
|
|
27
27
|
// cost an hour to diagnose once already.
|
|
28
28
|
const dir = mkdtempSync(join(tmpdir(), 'parity-'));
|
|
29
29
|
execFileSync('openssl', [
|
|
@@ -59,7 +59,7 @@ const settle = async (ms = 400) => {
|
|
|
59
59
|
/**
|
|
60
60
|
* One transport per process, deliberately.
|
|
61
61
|
*
|
|
62
|
-
* Running both bindings' servers concurrently in a single process hangs
|
|
62
|
+
* Running both bindings' servers concurrently in a single process hangs - verified: each
|
|
63
63
|
* works alone and both can bind, but sessions on both at once deadlock. That is a
|
|
64
64
|
* property of running two native QUIC stacks side by side, not of transport-io, and no
|
|
65
65
|
* deployment would do it. Splitting by process is also better isolation.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The transport seam. Internal
|
|
2
|
+
* The transport seam. Internal - not exported from the package (D21).
|
|
3
3
|
*
|
|
4
4
|
* It exists so the reference transport's defects stay in one place: it silently swallows
|
|
5
5
|
* oversized and blocked datagrams, its error type omits the specification's
|
|
@@ -25,7 +25,7 @@ export interface Connection {
|
|
|
25
25
|
/** A property of the path, not a constant. Queried at send time, never cached. */
|
|
26
26
|
maxDatagramSize(): number;
|
|
27
27
|
/**
|
|
28
|
-
* `undefined` where the runtime does not implement the attribute
|
|
28
|
+
* `undefined` where the runtime does not implement the attribute - which includes the
|
|
29
29
|
* dominant browser. Treated as "unknown, allowed"; only an explicit 'reliable-only' is
|
|
30
30
|
* refused. See D10.
|
|
31
31
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* reordering, duplicating and delayed, on command.
|
|
4
4
|
*
|
|
5
5
|
* Test-only. The plain loopback delivers datagrams reliably and in order, which is
|
|
6
|
-
* convenient and a lie
|
|
6
|
+
* convenient and a lie - every guarantee the datagram lane deliberately does not make
|
|
7
7
|
* goes unexercised against it. This exists so loss, reordering and duplication are forced
|
|
8
8
|
* deliberately rather than waited for.
|
|
9
9
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transport-io",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Real-time apps over WebTransport. Two lanes, one contract, no fallback.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"author": "#V0ID <hey@v0id.me>",
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|
|
52
|
-
"url": "git+https://github.com/
|
|
52
|
+
"url": "git+https://github.com/transport-io/transport-io.git",
|
|
53
53
|
"directory": "packages/core"
|
|
54
54
|
}
|
|
55
55
|
}
|