@norskvideo/moq-net 0.1.8 → 0.2.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 +2 -2
- package/announce.d.ts +7 -0
- package/announce.d.ts.map +1 -0
- package/announce.js +8 -0
- package/announce.js.map +1 -0
- package/announced.d.ts +49 -91
- package/announced.d.ts.map +1 -1
- package/announced.js +21 -156
- package/announced.js.map +1 -1
- package/bandwidth.d.ts +163 -0
- package/bandwidth.d.ts.map +1 -0
- package/bandwidth.js +304 -0
- package/bandwidth.js.map +1 -0
- package/bandwidth_api.d.ts +7 -0
- package/bandwidth_api.d.ts.map +1 -0
- package/bandwidth_api.js +8 -0
- package/bandwidth_api.js.map +1 -0
- package/broadcast.d.ts +44 -35
- package/broadcast.d.ts.map +1 -1
- package/broadcast.js +104 -60
- package/broadcast.js.map +1 -1
- package/connection/accept.d.ts +16 -1
- package/connection/accept.d.ts.map +1 -1
- package/connection/accept.js +52 -28
- package/connection/accept.js.map +1 -1
- package/connection/browser.d.ts.map +1 -1
- package/connection/browser.js +9 -7
- package/connection/browser.js.map +1 -1
- package/connection/connect.d.ts +30 -6
- package/connection/connect.d.ts.map +1 -1
- package/connection/connect.js +110 -54
- package/connection/connect.js.map +1 -1
- package/connection/established.d.ts +17 -21
- package/connection/established.d.ts.map +1 -1
- package/connection/established.js.map +1 -1
- package/connection/forward.d.ts +2 -0
- package/connection/forward.d.ts.map +1 -0
- package/connection/forward.js +173 -0
- package/connection/forward.js.map +1 -0
- package/connection/handshake.d.ts +1 -0
- package/connection/handshake.d.ts.map +1 -1
- package/connection/handshake.js +5 -2
- package/connection/handshake.js.map +1 -1
- package/connection/index.d.ts +5 -5
- package/connection/index.d.ts.map +1 -1
- package/connection/index.js +4 -5
- package/connection/index.js.map +1 -1
- package/connection/pool.d.ts +186 -0
- package/connection/pool.d.ts.map +1 -0
- package/connection/pool.js +361 -0
- package/connection/pool.js.map +1 -0
- package/connection/reload.d.ts +14 -93
- package/connection/reload.d.ts.map +1 -1
- package/connection/reload.js +217 -81
- package/connection/reload.js.map +1 -1
- package/connection/stats.d.ts +3 -26
- package/connection/stats.d.ts.map +1 -1
- package/connection/stats.js.map +1 -1
- package/connection/transport.d.ts +0 -7
- package/connection/transport.d.ts.map +1 -1
- package/consume.d.ts +1 -43
- package/consume.d.ts.map +1 -1
- package/consume.js +1 -1
- package/consume.js.map +1 -1
- package/error.d.ts +180 -29
- package/error.d.ts.map +1 -1
- package/error.js +331 -16
- package/error.js.map +1 -1
- package/errors.d.ts +7 -0
- package/errors.d.ts.map +1 -0
- package/errors.js +8 -0
- package/errors.js.map +1 -0
- package/group.d.ts +9 -43
- package/group.d.ts.map +1 -1
- package/group.js +284 -69
- package/group.js.map +1 -1
- package/hop.d.ts +115 -0
- package/hop.d.ts.map +1 -0
- package/hop.js +119 -0
- package/hop.js.map +1 -0
- package/ietf/adapter.d.ts +5 -1
- package/ietf/adapter.d.ts.map +1 -1
- package/ietf/adapter.js +105 -60
- package/ietf/adapter.js.map +1 -1
- package/ietf/aliases.d.ts +1 -78
- package/ietf/aliases.d.ts.map +1 -1
- package/ietf/cluster.d.ts +9 -123
- package/ietf/cluster.d.ts.map +1 -1
- package/ietf/cluster.js +84 -44
- package/ietf/cluster.js.map +1 -1
- package/ietf/connection.d.ts +6 -72
- package/ietf/connection.d.ts.map +1 -1
- package/ietf/connection.js +62 -56
- package/ietf/connection.js.map +1 -1
- package/ietf/error.d.ts +11 -0
- package/ietf/error.d.ts.map +1 -0
- package/ietf/error.js +193 -0
- package/ietf/error.js.map +1 -0
- package/ietf/fetch.d.ts +7 -20
- package/ietf/fetch.d.ts.map +1 -1
- package/ietf/fetch.js +52 -22
- package/ietf/fetch.js.map +1 -1
- package/ietf/filter.d.ts +2 -0
- package/ietf/filter.d.ts.map +1 -1
- package/ietf/filter.js +10 -1
- package/ietf/filter.js.map +1 -1
- package/ietf/goaway.d.ts.map +1 -1
- package/ietf/goaway.js +22 -5
- package/ietf/goaway.js.map +1 -1
- package/ietf/hidden.d.ts +2 -0
- package/ietf/hidden.d.ts.map +1 -0
- package/ietf/hidden.js +30 -0
- package/ietf/hidden.js.map +1 -0
- package/ietf/index.d.ts +2 -0
- package/ietf/index.d.ts.map +1 -1
- package/ietf/index.js +2 -0
- package/ietf/index.js.map +1 -1
- package/ietf/object.d.ts +15 -8
- package/ietf/object.d.ts.map +1 -1
- package/ietf/object.js +51 -36
- package/ietf/object.js.map +1 -1
- package/ietf/parameters.d.ts +14 -2
- package/ietf/parameters.d.ts.map +1 -1
- package/ietf/parameters.js +97 -29
- package/ietf/parameters.js.map +1 -1
- package/ietf/properties.d.ts +1 -0
- package/ietf/properties.d.ts.map +1 -1
- package/ietf/properties.js +14 -0
- package/ietf/properties.js.map +1 -1
- package/ietf/publish.d.ts +19 -2
- package/ietf/publish.d.ts.map +1 -1
- package/ietf/publish.js +40 -6
- package/ietf/publish.js.map +1 -1
- package/ietf/publish_namespace.d.ts +25 -0
- package/ietf/publish_namespace.d.ts.map +1 -1
- package/ietf/publish_namespace.js +63 -0
- package/ietf/publish_namespace.js.map +1 -1
- package/ietf/publisher.d.ts +1 -82
- package/ietf/publisher.d.ts.map +1 -1
- package/ietf/publisher.js +483 -235
- package/ietf/publisher.js.map +1 -1
- package/ietf/solicit.d.ts +1 -40
- package/ietf/solicit.d.ts.map +1 -1
- package/ietf/subscribe.d.ts +8 -6
- package/ietf/subscribe.d.ts.map +1 -1
- package/ietf/subscribe.js +33 -27
- package/ietf/subscribe.js.map +1 -1
- package/ietf/subscribe_namespace.d.ts +8 -2
- package/ietf/subscribe_namespace.d.ts.map +1 -1
- package/ietf/subscribe_namespace.js +18 -8
- package/ietf/subscribe_namespace.js.map +1 -1
- package/ietf/subscriber.d.ts +1 -65
- package/ietf/subscriber.d.ts.map +1 -1
- package/ietf/subscriber.js +337 -123
- package/ietf/subscriber.js.map +1 -1
- package/ietf/token.d.ts +2 -0
- package/ietf/token.d.ts.map +1 -0
- package/ietf/token.js +99 -0
- package/ietf/token.js.map +1 -0
- package/ietf/track.d.ts +4 -0
- package/ietf/track.d.ts.map +1 -1
- package/ietf/track.js +6 -20
- package/ietf/track.js.map +1 -1
- package/ietf/version.d.ts +12 -1
- package/ietf/version.d.ts.map +1 -1
- package/ietf/version.js +13 -0
- package/ietf/version.js.map +1 -1
- package/index.d.ts +12 -7
- package/index.d.ts.map +1 -1
- package/index.js +10 -5
- package/index.js.map +1 -1
- package/internal.d.ts +115 -1
- package/internal.d.ts.map +1 -1
- package/internal.js +108 -0
- package/internal.js.map +1 -1
- package/lite/announce.d.ts +60 -10
- package/lite/announce.d.ts.map +1 -1
- package/lite/announce.js +176 -31
- package/lite/announce.js.map +1 -1
- package/lite/connection.d.ts +9 -59
- package/lite/connection.d.ts.map +1 -1
- package/lite/connection.js +34 -35
- package/lite/connection.js.map +1 -1
- package/lite/datagram.d.ts +3 -2
- package/lite/datagram.d.ts.map +1 -1
- package/lite/datagram.js +7 -8
- package/lite/datagram.js.map +1 -1
- package/lite/fetch.d.ts +15 -1
- package/lite/fetch.d.ts.map +1 -1
- package/lite/fetch.js +39 -7
- package/lite/fetch.js.map +1 -1
- package/lite/goaway.d.ts.map +1 -1
- package/lite/goaway.js +7 -1
- package/lite/goaway.js.map +1 -1
- package/lite/group.d.ts +30 -12
- package/lite/group.d.ts.map +1 -1
- package/lite/group.js +68 -26
- package/lite/group.js.map +1 -1
- package/lite/message.d.ts +2 -2
- package/lite/message.d.ts.map +1 -1
- package/lite/message.js +14 -5
- package/lite/message.js.map +1 -1
- package/lite/priority.d.ts +1 -61
- package/lite/priority.d.ts.map +1 -1
- package/lite/priority.js +4 -5
- package/lite/priority.js.map +1 -1
- package/lite/publisher.d.ts +1 -69
- package/lite/publisher.d.ts.map +1 -1
- package/lite/publisher.js +630 -262
- package/lite/publisher.js.map +1 -1
- package/lite/setup.d.ts +8 -8
- package/lite/setup.d.ts.map +1 -1
- package/lite/setup.js +33 -30
- package/lite/setup.js.map +1 -1
- package/lite/subscribe.d.ts +71 -17
- package/lite/subscribe.d.ts.map +1 -1
- package/lite/subscribe.js +205 -53
- package/lite/subscribe.js.map +1 -1
- package/lite/subscriber.d.ts +15 -56
- package/lite/subscriber.d.ts.map +1 -1
- package/lite/subscriber.js +451 -247
- package/lite/subscriber.js.map +1 -1
- package/lite/track.d.ts +4 -10
- package/lite/track.d.ts.map +1 -1
- package/lite/track.js +34 -29
- package/lite/track.js.map +1 -1
- package/lite/version.d.ts +42 -6
- package/lite/version.d.ts.map +1 -1
- package/lite/version.js +126 -10
- package/lite/version.js.map +1 -1
- package/origin.d.ts +256 -29
- package/origin.d.ts.map +1 -1
- package/origin.js +1427 -37
- package/origin.js.map +1 -1
- package/package.json +8 -3
- package/path.d.ts +25 -7
- package/path.d.ts.map +1 -1
- package/path.js +5 -3
- package/path.js.map +1 -1
- package/stream.d.ts +73 -14
- package/stream.d.ts.map +1 -1
- package/stream.js +372 -141
- package/stream.js.map +1 -1
- package/tail.d.ts +18 -0
- package/tail.d.ts.map +1 -0
- package/tail.js +167 -0
- package/tail.js.map +1 -0
- package/time.d.ts +15 -2
- package/time.d.ts.map +1 -1
- package/time.js +28 -9
- package/time.js.map +1 -1
- package/track.d.ts +211 -83
- package/track.d.ts.map +1 -1
- package/track.js +816 -205
- package/track.js.map +1 -1
- package/util/abort.d.ts +2 -0
- package/util/abort.d.ts.map +1 -0
- package/util/abort.js +20 -0
- package/util/abort.js.map +1 -0
- package/util/log.d.ts +5 -0
- package/util/log.d.ts.map +1 -0
- package/util/log.js +17 -0
- package/util/log.js.map +1 -0
- package/util/u64.d.ts +39 -0
- package/util/u64.d.ts.map +1 -0
- package/util/u64.js +83 -0
- package/util/u64.js.map +1 -0
- package/util/varint.d.ts +29 -0
- package/util/varint.d.ts.map +1 -0
- package/util/varint.js +198 -0
- package/util/varint.js.map +1 -0
- package/varint.d.ts +10 -6
- package/varint.d.ts.map +1 -1
- package/varint.js +40 -237
- package/varint.js.map +1 -1
- package/wire.d.ts +80 -0
- package/wire.d.ts.map +1 -0
- package/wire.js +32 -0
- package/wire.js.map +1 -0
- package/zod.d.ts +1 -1
- package/zod.d.ts.map +1 -1
- package/zod.js.map +1 -1
- package/mock.d.ts +0 -66
- package/mock.d.ts.map +0 -1
- package/mock.js +0 -243
- package/mock.js.map +0 -1
package/hop.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint identity within a hop chain, shared by both wire protocols.
|
|
3
|
+
*
|
|
4
|
+
* moq-lite carries these natively on every announcement; moq-transport carries them
|
|
5
|
+
* via the MoQ Cluster extension (see `ietf/cluster.ts`). Mirrors `Hop` in
|
|
6
|
+
* `rs/moq-net`.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import * as z from "zod/mini";
|
|
11
|
+
/**
|
|
12
|
+
* One relay's identity in a broadcast's hop chain, encoded as a 62-bit varint on the wire.
|
|
13
|
+
*
|
|
14
|
+
* Names a *hop*, not an {@link !Origin | Origin} routing table: this is the id a relay
|
|
15
|
+
* stamps into a chain as an announcement passes through, so a receiver can spot its own
|
|
16
|
+
* id and reject a loop. The SETUP parameter that carries it session-wide is `Hop` too.
|
|
17
|
+
*
|
|
18
|
+
* The {@link HopSchema} validates any incoming value and brands it so the type system
|
|
19
|
+
* enforces "only validated ids flow into hop chains." Internal code that synthesizes one
|
|
20
|
+
* (e.g. {@link randomHop}) uses `HopSchema.parse(...)` to brand a raw bigint.
|
|
21
|
+
*/
|
|
22
|
+
export declare const HopSchema: z.core.$ZodBranded<z.ZodMiniBigInt<bigint>, "Hop", "out">;
|
|
23
|
+
export type Hop = z.infer<typeof HopSchema>;
|
|
24
|
+
/**
|
|
25
|
+
* The reserved id 0, meaning "no identity".
|
|
26
|
+
*
|
|
27
|
+
* It stands in for an endpoint that never declared one, and any number of endpoints can
|
|
28
|
+
* be 0, so it identifies nothing: it is never a loop, never a publisher two chains have
|
|
29
|
+
* in common, and never excluded from an advertisement. A chain that holds a 0 anywhere
|
|
30
|
+
* is anonymous for route selection.
|
|
31
|
+
*/
|
|
32
|
+
export declare const UNKNOWN_HOP: Hop;
|
|
33
|
+
/**
|
|
34
|
+
* Maximum length of a hop chain. Must match `MAX_HOPS` in Rust's `model/origin.rs`.
|
|
35
|
+
*
|
|
36
|
+
* Broadcasts with longer chains are rejected, which bounds loop detection and rejects
|
|
37
|
+
* pathological announcements across clusters with unbounded forwarding.
|
|
38
|
+
*/
|
|
39
|
+
export declare const MAX_HOPS = 32;
|
|
40
|
+
/**
|
|
41
|
+
* Generate a fresh hop with a random non-zero id.
|
|
42
|
+
*
|
|
43
|
+
* `crypto.getRandomValues` is overkill for best-effort loop detection, but
|
|
44
|
+
* used for slightly better distribution than `Math.random` at negligible cost.
|
|
45
|
+
*
|
|
46
|
+
* TEMPORARY: the wire format allows 62 bits, but older `@moq/lite` JS clients
|
|
47
|
+
* decode `AnnounceInterest.exclude_hop` as a u53 (number) and throw on anything
|
|
48
|
+
* > 2^53-1. To keep those clients alive against fresh peers, we cap the random
|
|
49
|
+
* id at 53 bits. Restore to 62 bits once the u62 fix has propagated to deployed
|
|
50
|
+
* bundles. Mirrors `Hop::random` in rs/moq-net.
|
|
51
|
+
*/
|
|
52
|
+
export declare function randomHop(): Hop;
|
|
53
|
+
/**
|
|
54
|
+
* Name an unknown original publisher: put `stamp`, the receiving connection's own random id,
|
|
55
|
+
* in front of a chain that starts with 0, and turn an empty chain into `[stamp, 0]`. A
|
|
56
|
+
* publisher that reconnects then reads downstream as a new first hop, while the 0 after the
|
|
57
|
+
* stamp keeps the route ranked as anonymous. `undefined` if the chain is full or already holds
|
|
58
|
+
* `stamp`. Mirrors `Hops::stamp` in rs/moq-net.
|
|
59
|
+
*/
|
|
60
|
+
export declare function stampHops(hops: readonly Hop[], stamp: Hop): Hop[] | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* What pulling content via a route costs, in two magnitudes accumulated together
|
|
63
|
+
* and compared in that order: lower {@link Cost.warm} wins, and {@link Cost.cold}
|
|
64
|
+
* breaks the tie.
|
|
65
|
+
*
|
|
66
|
+
* Both price the same path against different cache states. `warm` is what one more
|
|
67
|
+
* subscription would cost the mesh right now, so it collapses to zero at any relay
|
|
68
|
+
* already carrying the broadcast. `cold` prices the identical path as if nothing were
|
|
69
|
+
* cached, so it keeps flowing through a warm relay unchanged and still says which of
|
|
70
|
+
* two warm relays sits closer to the publisher.
|
|
71
|
+
*/
|
|
72
|
+
export interface Cost {
|
|
73
|
+
/** The cost as the mesh stands today, discounted to zero at every carrying relay. */
|
|
74
|
+
warm: bigint;
|
|
75
|
+
/** The same path with every warm discount removed. */
|
|
76
|
+
cold: bigint;
|
|
77
|
+
}
|
|
78
|
+
/** Constructors for {@link Cost}. */
|
|
79
|
+
export declare const Cost: {
|
|
80
|
+
/** A free path in both magnitudes: what a live publisher seeds. */
|
|
81
|
+
zero: Cost;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The path a route took through the mesh and what using it costs.
|
|
85
|
+
*
|
|
86
|
+
* The metadata half of an advertisement: an origin `dynamic()` pairs it with the
|
|
87
|
+
* pattern it covers, a broadcast `announce()` with the broadcast's exact path, and
|
|
88
|
+
* an announce event carries it so consumers can read it back.
|
|
89
|
+
*/
|
|
90
|
+
export interface Route {
|
|
91
|
+
/** The chain of hops the route has traversed, oldest first. */
|
|
92
|
+
hops: Hop[];
|
|
93
|
+
/** What pulling content via this route costs; lower wins. */
|
|
94
|
+
cost: Cost;
|
|
95
|
+
}
|
|
96
|
+
/** Constructors and helpers for {@link Route}. */
|
|
97
|
+
export declare const Route: {
|
|
98
|
+
/** An empty hop chain at zero cost: what a publisher seeds for a live broadcast. */
|
|
99
|
+
default: Route;
|
|
100
|
+
/** Normalize a partial route, treating a bare bigint cost as both magnitudes alike. */
|
|
101
|
+
normalize(route?: Route | {
|
|
102
|
+
hops?: readonly Hop[];
|
|
103
|
+
cost?: Cost | bigint;
|
|
104
|
+
}): Route;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Whether this route passed through an anonymous hop (id 0) at any depth.
|
|
108
|
+
*
|
|
109
|
+
* An empty chain is a local announcement, not the anonymous mark. Ingress fills a
|
|
110
|
+
* received empty list with {@link UNKNOWN_HOP} before yielding it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function isAnonymous(route: Route): boolean;
|
|
113
|
+
/** Whether two routes name the same hop chain and cost. */
|
|
114
|
+
export declare function routesEqual(a: Route | undefined, b: Route | undefined): boolean;
|
|
115
|
+
//# sourceMappingURL=hop.d.ts.map
|
package/hop.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hop.d.ts","sourceRoot":"","sources":["../src/hop.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,CAAC,MAAM,UAAU,CAAC;AAE9B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,2DAGR,CAAC;AAEf,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAE5C;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,GAAyB,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,KAAK,CAAC;AAE3B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,IAAI,GAAG,CAO/B;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,GAAG,GAAG,EAAE,GAAG,SAAS,CAK7E;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,IAAI;IACpB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;CACb;AAED,qCAAqC;AACrC,eAAO,MAAM,IAAI;IAChB,mEAAmE;IACnE,IAAI,EAA4B,IAAI;CACpC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,KAAK;IACrB,+DAA+D;IAC/D,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,6DAA6D;IAC7D,IAAI,EAAE,IAAI,CAAC;CACX;AAED,kDAAkD;AAClD,eAAO,MAAM,KAAK;IACjB,oFAAoF;IACpF,OAAO,EAAmC,KAAK;IAE/C,uFAAuF;IACvF,SAAS,SAAQ,KAAK,GAAG;QAAE,IAAI,CAAC,EAAE,SAAS,GAAG,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;KAAE,GAAQ,KAAK;CAOrF,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAEjD;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,GAAG,SAAS,EAAE,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,OAAO,CAS/E"}
|
package/hop.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/* @ts-self-types="./hop.d.ts" */
|
|
2
|
+
/**
|
|
3
|
+
* Endpoint identity within a hop chain, shared by both wire protocols.
|
|
4
|
+
*
|
|
5
|
+
* moq-lite carries these natively on every announcement; moq-transport carries them
|
|
6
|
+
* via the MoQ Cluster extension (see `ietf/cluster.ts`). Mirrors `Hop` in
|
|
7
|
+
* `rs/moq-net`.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import * as z from "zod/mini";
|
|
12
|
+
/**
|
|
13
|
+
* One relay's identity in a broadcast's hop chain, encoded as a 62-bit varint on the wire.
|
|
14
|
+
*
|
|
15
|
+
* Names a *hop*, not an {@link !Origin | Origin} routing table: this is the id a relay
|
|
16
|
+
* stamps into a chain as an announcement passes through, so a receiver can spot its own
|
|
17
|
+
* id and reject a loop. The SETUP parameter that carries it session-wide is `Hop` too.
|
|
18
|
+
*
|
|
19
|
+
* The {@link HopSchema} validates any incoming value and brands it so the type system
|
|
20
|
+
* enforces "only validated ids flow into hop chains." Internal code that synthesizes one
|
|
21
|
+
* (e.g. {@link randomHop}) uses `HopSchema.parse(...)` to brand a raw bigint.
|
|
22
|
+
*/
|
|
23
|
+
export const HopSchema = z
|
|
24
|
+
.bigint()
|
|
25
|
+
.check(z.refine((value) => value >= 0n && value < 1n << 62n, "Hop must be a non-negative 62-bit integer"))
|
|
26
|
+
.brand("Hop");
|
|
27
|
+
/**
|
|
28
|
+
* The reserved id 0, meaning "no identity".
|
|
29
|
+
*
|
|
30
|
+
* It stands in for an endpoint that never declared one, and any number of endpoints can
|
|
31
|
+
* be 0, so it identifies nothing: it is never a loop, never a publisher two chains have
|
|
32
|
+
* in common, and never excluded from an advertisement. A chain that holds a 0 anywhere
|
|
33
|
+
* is anonymous for route selection.
|
|
34
|
+
*/
|
|
35
|
+
export const UNKNOWN_HOP = HopSchema.parse(0n);
|
|
36
|
+
/**
|
|
37
|
+
* Maximum length of a hop chain. Must match `MAX_HOPS` in Rust's `model/origin.rs`.
|
|
38
|
+
*
|
|
39
|
+
* Broadcasts with longer chains are rejected, which bounds loop detection and rejects
|
|
40
|
+
* pathological announcements across clusters with unbounded forwarding.
|
|
41
|
+
*/
|
|
42
|
+
export const MAX_HOPS = 32;
|
|
43
|
+
/**
|
|
44
|
+
* Generate a fresh hop with a random non-zero id.
|
|
45
|
+
*
|
|
46
|
+
* `crypto.getRandomValues` is overkill for best-effort loop detection, but
|
|
47
|
+
* used for slightly better distribution than `Math.random` at negligible cost.
|
|
48
|
+
*
|
|
49
|
+
* TEMPORARY: the wire format allows 62 bits, but older `@moq/lite` JS clients
|
|
50
|
+
* decode `AnnounceInterest.exclude_hop` as a u53 (number) and throw on anything
|
|
51
|
+
* > 2^53-1. To keep those clients alive against fresh peers, we cap the random
|
|
52
|
+
* id at 53 bits. Restore to 62 bits once the u62 fix has propagated to deployed
|
|
53
|
+
* bundles. Mirrors `Hop::random` in rs/moq-net.
|
|
54
|
+
*/
|
|
55
|
+
export function randomHop() {
|
|
56
|
+
const buf = new BigUint64Array(1);
|
|
57
|
+
crypto.getRandomValues(buf);
|
|
58
|
+
// Mask to 53 bits.
|
|
59
|
+
const raw = buf[0] & 0x1fffffffffffffn;
|
|
60
|
+
// Guard against the (astronomically unlikely) zero draw.
|
|
61
|
+
return HopSchema.parse(raw === 0n ? 1n : raw);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Name an unknown original publisher: put `stamp`, the receiving connection's own random id,
|
|
65
|
+
* in front of a chain that starts with 0, and turn an empty chain into `[stamp, 0]`. A
|
|
66
|
+
* publisher that reconnects then reads downstream as a new first hop, while the 0 after the
|
|
67
|
+
* stamp keeps the route ranked as anonymous. `undefined` if the chain is full or already holds
|
|
68
|
+
* `stamp`. Mirrors `Hops::stamp` in rs/moq-net.
|
|
69
|
+
*/
|
|
70
|
+
export function stampHops(hops, stamp) {
|
|
71
|
+
if (hops.length === 0)
|
|
72
|
+
return [stamp, UNKNOWN_HOP];
|
|
73
|
+
if (hops[0] !== UNKNOWN_HOP)
|
|
74
|
+
return [...hops];
|
|
75
|
+
if (hops.length >= MAX_HOPS || hops.includes(stamp))
|
|
76
|
+
return undefined;
|
|
77
|
+
return [stamp, ...hops];
|
|
78
|
+
}
|
|
79
|
+
/** Constructors for {@link Cost}. */
|
|
80
|
+
export const Cost = {
|
|
81
|
+
/** A free path in both magnitudes: what a live publisher seeds. */
|
|
82
|
+
zero: { warm: 0n, cold: 0n },
|
|
83
|
+
};
|
|
84
|
+
/** Constructors and helpers for {@link Route}. */
|
|
85
|
+
export const Route = {
|
|
86
|
+
/** An empty hop chain at zero cost: what a publisher seeds for a live broadcast. */
|
|
87
|
+
default: { hops: [], cost: Cost.zero },
|
|
88
|
+
/** Normalize a partial route, treating a bare bigint cost as both magnitudes alike. */
|
|
89
|
+
normalize(route = {}) {
|
|
90
|
+
const hops = route.hops ? [...route.hops] : [];
|
|
91
|
+
const cost = route.cost;
|
|
92
|
+
if (cost === undefined)
|
|
93
|
+
return { hops, cost: Cost.zero };
|
|
94
|
+
if (typeof cost === "bigint")
|
|
95
|
+
return { hops, cost: { warm: cost, cold: cost } };
|
|
96
|
+
return { hops, cost: { warm: cost.warm, cold: cost.cold } };
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Whether this route passed through an anonymous hop (id 0) at any depth.
|
|
101
|
+
*
|
|
102
|
+
* An empty chain is a local announcement, not the anonymous mark. Ingress fills a
|
|
103
|
+
* received empty list with {@link UNKNOWN_HOP} before yielding it.
|
|
104
|
+
*/
|
|
105
|
+
export function isAnonymous(route) {
|
|
106
|
+
return route.hops.includes(UNKNOWN_HOP);
|
|
107
|
+
}
|
|
108
|
+
/** Whether two routes name the same hop chain and cost. */
|
|
109
|
+
export function routesEqual(a, b) {
|
|
110
|
+
if (a === b)
|
|
111
|
+
return true;
|
|
112
|
+
if (!a || !b)
|
|
113
|
+
return false;
|
|
114
|
+
return (a.cost.warm === b.cost.warm &&
|
|
115
|
+
a.cost.cold === b.cost.cold &&
|
|
116
|
+
a.hops.length === b.hops.length &&
|
|
117
|
+
a.hops.every((hop, i) => hop === b.hops[i]));
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=hop.js.map
|
package/hop.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hop.js","sourceRoot":"","sources":["../src/hop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Endpoint identity within a hop chain, shared by both wire protocols.\n *\n * moq-lite carries these natively on every announcement; moq-transport carries them\n * via the MoQ Cluster extension (see `ietf/cluster.ts`). Mirrors `Hop` in\n * `rs/moq-net`.\n *\n * @module\n */\nimport * as z from \"zod/mini\";\n\n/**\n * One relay's identity in a broadcast's hop chain, encoded as a 62-bit varint on the wire.\n *\n * Names a *hop*, not an {@link !Origin | Origin} routing table: this is the id a relay\n * stamps into a chain as an announcement passes through, so a receiver can spot its own\n * id and reject a loop. The SETUP parameter that carries it session-wide is `Hop` too.\n *\n * The {@link HopSchema} validates any incoming value and brands it so the type system\n * enforces \"only validated ids flow into hop chains.\" Internal code that synthesizes one\n * (e.g. {@link randomHop}) uses `HopSchema.parse(...)` to brand a raw bigint.\n */\nexport const HopSchema = z\n\t.bigint()\n\t.check(z.refine((value) => value >= 0n && value < 1n << 62n, \"Hop must be a non-negative 62-bit integer\"))\n\t.brand(\"Hop\");\n\nexport type Hop = z.infer<typeof HopSchema>;\n\n/**\n * The reserved id 0, meaning \"no identity\".\n *\n * It stands in for an endpoint that never declared one, and any number of endpoints can\n * be 0, so it identifies nothing: it is never a loop, never a publisher two chains have\n * in common, and never excluded from an advertisement. A chain that holds a 0 anywhere\n * is anonymous for route selection.\n */\nexport const UNKNOWN_HOP: Hop = HopSchema.parse(0n);\n\n/**\n * Maximum length of a hop chain. Must match `MAX_HOPS` in Rust's `model/origin.rs`.\n *\n * Broadcasts with longer chains are rejected, which bounds loop detection and rejects\n * pathological announcements across clusters with unbounded forwarding.\n */\nexport const MAX_HOPS = 32;\n\n/**\n * Generate a fresh hop with a random non-zero id.\n *\n * `crypto.getRandomValues` is overkill for best-effort loop detection, but\n * used for slightly better distribution than `Math.random` at negligible cost.\n *\n * TEMPORARY: the wire format allows 62 bits, but older `@moq/lite` JS clients\n * decode `AnnounceInterest.exclude_hop` as a u53 (number) and throw on anything\n * > 2^53-1. To keep those clients alive against fresh peers, we cap the random\n * id at 53 bits. Restore to 62 bits once the u62 fix has propagated to deployed\n * bundles. Mirrors `Hop::random` in rs/moq-net.\n */\nexport function randomHop(): Hop {\n\tconst buf = new BigUint64Array(1);\n\tcrypto.getRandomValues(buf);\n\t// Mask to 53 bits.\n\tconst raw = buf[0] & 0x1f_ffff_ffff_ffffn;\n\t// Guard against the (astronomically unlikely) zero draw.\n\treturn HopSchema.parse(raw === 0n ? 1n : raw);\n}\n\n/**\n * Name an unknown original publisher: put `stamp`, the receiving connection's own random id,\n * in front of a chain that starts with 0, and turn an empty chain into `[stamp, 0]`. A\n * publisher that reconnects then reads downstream as a new first hop, while the 0 after the\n * stamp keeps the route ranked as anonymous. `undefined` if the chain is full or already holds\n * `stamp`. Mirrors `Hops::stamp` in rs/moq-net.\n */\nexport function stampHops(hops: readonly Hop[], stamp: Hop): Hop[] | undefined {\n\tif (hops.length === 0) return [stamp, UNKNOWN_HOP];\n\tif (hops[0] !== UNKNOWN_HOP) return [...hops];\n\tif (hops.length >= MAX_HOPS || hops.includes(stamp)) return undefined;\n\treturn [stamp, ...hops];\n}\n\n/**\n * What pulling content via a route costs, in two magnitudes accumulated together\n * and compared in that order: lower {@link Cost.warm} wins, and {@link Cost.cold}\n * breaks the tie.\n *\n * Both price the same path against different cache states. `warm` is what one more\n * subscription would cost the mesh right now, so it collapses to zero at any relay\n * already carrying the broadcast. `cold` prices the identical path as if nothing were\n * cached, so it keeps flowing through a warm relay unchanged and still says which of\n * two warm relays sits closer to the publisher.\n */\nexport interface Cost {\n\t/** The cost as the mesh stands today, discounted to zero at every carrying relay. */\n\twarm: bigint;\n\t/** The same path with every warm discount removed. */\n\tcold: bigint;\n}\n\n/** Constructors for {@link Cost}. */\nexport const Cost = {\n\t/** A free path in both magnitudes: what a live publisher seeds. */\n\tzero: { warm: 0n, cold: 0n } as Cost,\n};\n\n/**\n * The path a route took through the mesh and what using it costs.\n *\n * The metadata half of an advertisement: an origin `dynamic()` pairs it with the\n * pattern it covers, a broadcast `announce()` with the broadcast's exact path, and\n * an announce event carries it so consumers can read it back.\n */\nexport interface Route {\n\t/** The chain of hops the route has traversed, oldest first. */\n\thops: Hop[];\n\t/** What pulling content via this route costs; lower wins. */\n\tcost: Cost;\n}\n\n/** Constructors and helpers for {@link Route}. */\nexport const Route = {\n\t/** An empty hop chain at zero cost: what a publisher seeds for a live broadcast. */\n\tdefault: { hops: [], cost: Cost.zero } as Route,\n\n\t/** Normalize a partial route, treating a bare bigint cost as both magnitudes alike. */\n\tnormalize(route: Route | { hops?: readonly Hop[]; cost?: Cost | bigint } = {}): Route {\n\t\tconst hops = route.hops ? [...route.hops] : [];\n\t\tconst cost = route.cost;\n\t\tif (cost === undefined) return { hops, cost: Cost.zero };\n\t\tif (typeof cost === \"bigint\") return { hops, cost: { warm: cost, cold: cost } };\n\t\treturn { hops, cost: { warm: cost.warm, cold: cost.cold } };\n\t},\n};\n\n/**\n * Whether this route passed through an anonymous hop (id 0) at any depth.\n *\n * An empty chain is a local announcement, not the anonymous mark. Ingress fills a\n * received empty list with {@link UNKNOWN_HOP} before yielding it.\n */\nexport function isAnonymous(route: Route): boolean {\n\treturn route.hops.includes(UNKNOWN_HOP);\n}\n\n/** Whether two routes name the same hop chain and cost. */\nexport function routesEqual(a: Route | undefined, b: Route | undefined): boolean {\n\tif (a === b) return true;\n\tif (!a || !b) return false;\n\treturn (\n\t\ta.cost.warm === b.cost.warm &&\n\t\ta.cost.cold === b.cost.cold &&\n\t\ta.hops.length === b.hops.length &&\n\t\ta.hops.every((hop, i) => hop === b.hops[i])\n\t);\n}\n"]}
|
package/ietf/adapter.d.ts
CHANGED
|
@@ -62,6 +62,10 @@ export declare class ControlStreamAdapter implements Session {
|
|
|
62
62
|
* Must be called after construction. Runs until the control stream closes.
|
|
63
63
|
*/
|
|
64
64
|
run(): Promise<void>;
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Ends every virtual stream: cleanly for a deliberate close, or with `err` when the
|
|
67
|
+
* control stream died under them, since every request riding it was cut off.
|
|
68
|
+
*/
|
|
69
|
+
close(err?: Error): void;
|
|
66
70
|
}
|
|
67
71
|
//# sourceMappingURL=adapter.d.ts.map
|
package/ietf/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/ietf/adapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/ietf/adapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAU,MAAM,EAAe,MAAM,cAAc,CAAC;AAG3D,OAAO,EAAE,KAAK,WAAW,EAAW,MAAM,cAAc,CAAC;AAEzD;;;GAGG;AACH,MAAM,WAAW,OAAO;IACvB,MAAM,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,YAAY,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACxC,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;CAC9B;AAED;;;GAGG;AACH,qBAAa,aAAc,YAAW,OAAO;;IAK5C,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAE9B,YAAY,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAIpE;IAEK,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAE9B;IAEK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE5C;IAEK,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAIjD;IAED,kDAAkD;IAClD,KAAK,SAEJ;CACD;AAqBD;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,OAAO;;IAQnD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAmC9B,YACC,IAAI,EAAE,YAAY,EAClB,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,OAAO,EAUf;IAED;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS5C;IAED;;;;;OAKG;IACH,MAAM,IAAI,MAAM,CAoDf;IAED;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAEpC;IAED;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAYjD;IAED;;;OAGG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CA2DzB;IAqaD;;;OAGG;IACH,KAAK,CAAC,GAAG,CAAC,EAAE,KAAK,QA8BhB;CACD"}
|
package/ietf/adapter.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* @ts-self-types="./adapter.d.ts" */
|
|
2
2
|
import { Mutex } from "async-mutex";
|
|
3
|
+
import { error, ProtocolViolation } from "../error.js";
|
|
3
4
|
import { Reader, Stream } from "../stream.js";
|
|
4
|
-
import { decodeUtf8 } from "../util/utf8.js";
|
|
5
5
|
import * as Varint from "../varint.js";
|
|
6
6
|
import * as Namespace from "./namespace.js";
|
|
7
7
|
import { Version } from "./version.js";
|
|
@@ -40,7 +40,7 @@ export class NativeSession {
|
|
|
40
40
|
const Route = {
|
|
41
41
|
NewRequest: 0, // Create virtual bidi stream, push initial message
|
|
42
42
|
Response: 1, // Push message to existing stream (keep open)
|
|
43
|
-
ErrorResponse: 2, // Push message to existing stream, then close
|
|
43
|
+
ErrorResponse: 2, // Push a final message to existing stream, then close
|
|
44
44
|
CloseStream: 3, // Close stream recv (no bytes pushed)
|
|
45
45
|
FollowUp: 4, // Push follow-up message to existing stream
|
|
46
46
|
MaxRequestId: 5, // Update flow control
|
|
@@ -64,9 +64,13 @@ export class ControlStreamAdapter {
|
|
|
64
64
|
version;
|
|
65
65
|
// Virtual streams keyed by requestId
|
|
66
66
|
#streams = new Map();
|
|
67
|
-
// Namespace → requestId reverse
|
|
68
|
-
|
|
69
|
-
//
|
|
67
|
+
// Namespace → requestId reverse lookups for v14/v15 namespace-keyed messages.
|
|
68
|
+
// DONE names an incoming announcement and CANCEL names an outgoing one, so a relay
|
|
69
|
+
// may legitimately hold the same namespace in both maps at once.
|
|
70
|
+
#incomingNamespaces = new Map();
|
|
71
|
+
#outgoingNamespaces = new Map();
|
|
72
|
+
// requestId → namespace reverse lookup, one entry per request including a refused
|
|
73
|
+
// duplicate, so #forget can clean up whichever request goes away.
|
|
70
74
|
#namespacesByRequestId = new Map();
|
|
71
75
|
// SubscribeNamespace requestIds — for routing 0x08/0x0E entries that lack requestId (v14/v15)
|
|
72
76
|
#subscribeNamespaces = new Set();
|
|
@@ -123,7 +127,7 @@ export class ControlStreamAdapter {
|
|
|
123
127
|
},
|
|
124
128
|
cancel: () => {
|
|
125
129
|
if (registeredRequestId !== undefined) {
|
|
126
|
-
this.#
|
|
130
|
+
this.#forget(registeredRequestId);
|
|
127
131
|
}
|
|
128
132
|
},
|
|
129
133
|
});
|
|
@@ -146,7 +150,7 @@ export class ControlStreamAdapter {
|
|
|
146
150
|
buffer = buffer.subarray(boundary);
|
|
147
151
|
if (!registered) {
|
|
148
152
|
// First message: extract requestId and register before flushing
|
|
149
|
-
const parsed = this.#tryParseOutgoing(toFlush);
|
|
153
|
+
const parsed = await this.#tryParseOutgoing(toFlush);
|
|
150
154
|
if (parsed) {
|
|
151
155
|
registeredRequestId = parsed.requestId;
|
|
152
156
|
this.#streams.set(parsed.requestId, { controller });
|
|
@@ -157,9 +161,7 @@ export class ControlStreamAdapter {
|
|
|
157
161
|
}
|
|
158
162
|
},
|
|
159
163
|
});
|
|
160
|
-
const stream = new Stream({ readable, writable: sendWritable });
|
|
161
|
-
stream.reader.version = this.version;
|
|
162
|
-
stream.writer.version = this.version;
|
|
164
|
+
const stream = new Stream({ readable, writable: sendWritable, version: this.version });
|
|
163
165
|
return stream;
|
|
164
166
|
}
|
|
165
167
|
/**
|
|
@@ -190,6 +192,8 @@ export class ControlStreamAdapter {
|
|
|
190
192
|
* Must be called after construction. Runs until the control stream closes.
|
|
191
193
|
*/
|
|
192
194
|
async run() {
|
|
195
|
+
// Why the virtual streams end: undefined only for a GOAWAY, which is not a failure.
|
|
196
|
+
let cause;
|
|
193
197
|
try {
|
|
194
198
|
// v16: also accept real bidi streams (for SubscribeNamespace)
|
|
195
199
|
if (this.version === Version.DRAFT_16) {
|
|
@@ -197,8 +201,10 @@ export class ControlStreamAdapter {
|
|
|
197
201
|
}
|
|
198
202
|
for (;;) {
|
|
199
203
|
const done = await this.#reader.done();
|
|
200
|
-
if (done)
|
|
204
|
+
if (done) {
|
|
205
|
+
cause = new ProtocolViolation("control stream closed");
|
|
201
206
|
break;
|
|
207
|
+
}
|
|
202
208
|
const typeId = await this.#reader.u53();
|
|
203
209
|
const size = await this.#reader.u16();
|
|
204
210
|
const body = await this.#reader.read(size);
|
|
@@ -234,8 +240,12 @@ export class ControlStreamAdapter {
|
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
cause = error(err);
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
237
247
|
finally {
|
|
238
|
-
this.close();
|
|
248
|
+
this.close(cause);
|
|
239
249
|
}
|
|
240
250
|
}
|
|
241
251
|
/** Accept real WebTransport bidi streams and queue them for acceptBi (v16). */
|
|
@@ -265,13 +275,11 @@ export class ControlStreamAdapter {
|
|
|
265
275
|
controller = c;
|
|
266
276
|
},
|
|
267
277
|
cancel: () => {
|
|
268
|
-
this.#
|
|
278
|
+
this.#forget(requestId);
|
|
269
279
|
},
|
|
270
280
|
});
|
|
271
281
|
const sendWritable = this.#createSendWritable();
|
|
272
|
-
const stream = new Stream({ readable, writable: sendWritable });
|
|
273
|
-
stream.reader.version = this.version;
|
|
274
|
-
stream.writer.version = this.version;
|
|
282
|
+
const stream = new Stream({ readable, writable: sendWritable, version: this.version });
|
|
275
283
|
this.#streams.set(requestId, { controller });
|
|
276
284
|
// Push initial message bytes so the dispatcher can read typeId + decode
|
|
277
285
|
controller.enqueue(this.#encodeRaw(typeId, size, body));
|
|
@@ -299,16 +307,10 @@ export class ControlStreamAdapter {
|
|
|
299
307
|
}
|
|
300
308
|
#closeStream(requestId) {
|
|
301
309
|
const entry = this.#streams.get(requestId);
|
|
310
|
+
this.#forget(requestId);
|
|
302
311
|
if (!entry)
|
|
303
312
|
return;
|
|
304
313
|
console.debug(`adapter: closing stream requestId=${requestId}`);
|
|
305
|
-
this.#streams.delete(requestId);
|
|
306
|
-
this.#subscribeNamespaces.delete(requestId);
|
|
307
|
-
const namespace = this.#namespacesByRequestId.get(requestId);
|
|
308
|
-
if (namespace !== undefined) {
|
|
309
|
-
this.#namespaces.delete(namespace);
|
|
310
|
-
this.#namespacesByRequestId.delete(requestId);
|
|
311
|
-
}
|
|
312
314
|
try {
|
|
313
315
|
entry.controller.close();
|
|
314
316
|
}
|
|
@@ -316,6 +318,26 @@ export class ControlStreamAdapter {
|
|
|
316
318
|
// Already closed
|
|
317
319
|
}
|
|
318
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Drop every routing entry a request owns, keeping the two namespace maps in step.
|
|
323
|
+
*
|
|
324
|
+
* Runs even when the virtual stream is already gone, so a request that was torn down
|
|
325
|
+
* locally still gives its namespace back.
|
|
326
|
+
*/
|
|
327
|
+
#forget(requestId) {
|
|
328
|
+
this.#streams.delete(requestId);
|
|
329
|
+
this.#subscribeNamespaces.delete(requestId);
|
|
330
|
+
const registered = this.#namespacesByRequestId.get(requestId);
|
|
331
|
+
if (registered === undefined)
|
|
332
|
+
return;
|
|
333
|
+
this.#namespacesByRequestId.delete(requestId);
|
|
334
|
+
const namespaces = this.#namespaceRequests(registered.direction);
|
|
335
|
+
// Only the request holding the live announcement may retract it. A refused duplicate
|
|
336
|
+
// carries the same name, and letting its close withdraw the name would strand the
|
|
337
|
+
// original: its own DONE would then resolve to nothing.
|
|
338
|
+
if (namespaces.get(registered.namespace) === requestId)
|
|
339
|
+
namespaces.delete(registered.namespace);
|
|
340
|
+
}
|
|
319
341
|
/**
|
|
320
342
|
* Returns the total byte size of the first complete message in buffer,
|
|
321
343
|
* or undefined if the buffer doesn't contain a complete message yet.
|
|
@@ -340,7 +362,7 @@ export class ControlStreamAdapter {
|
|
|
340
362
|
* Try to parse the first outgoing message from accumulated bytes.
|
|
341
363
|
* Returns the requestId if enough data is available, undefined otherwise.
|
|
342
364
|
*/
|
|
343
|
-
#tryParseOutgoing(buffer) {
|
|
365
|
+
async #tryParseOutgoing(buffer) {
|
|
344
366
|
if (buffer.length === 0)
|
|
345
367
|
return undefined;
|
|
346
368
|
// Check typeId varint size before decoding
|
|
@@ -363,8 +385,7 @@ export class ControlStreamAdapter {
|
|
|
363
385
|
// PublishNamespace (0x06): also parse namespace for v14/v15 reverse lookup
|
|
364
386
|
if (typeId === 0x06) {
|
|
365
387
|
try {
|
|
366
|
-
|
|
367
|
-
this.#parseAndRegisterNamespace(afterReqId, requestId);
|
|
388
|
+
await this.#registerNamespace(body, "outgoing");
|
|
368
389
|
}
|
|
369
390
|
catch {
|
|
370
391
|
// Non-critical: only needed for v14/v15 PublishNamespaceDone/Cancel
|
|
@@ -377,20 +398,28 @@ export class ControlStreamAdapter {
|
|
|
377
398
|
return { requestId };
|
|
378
399
|
}
|
|
379
400
|
/**
|
|
380
|
-
*
|
|
401
|
+
* Decode a PublishNamespace body and register its namespace for the v14/v15
|
|
402
|
+
* namespace-keyed withdrawals. Returns the request ID it carried.
|
|
403
|
+
*
|
|
404
|
+
* The one decode site for both directions: outgoing announcements go through
|
|
405
|
+
* #tryParseOutgoing and incoming ones through #classify. The direction selects the
|
|
406
|
+
* map that the peer's DONE or CANCEL will later consult.
|
|
381
407
|
*/
|
|
382
|
-
#
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
408
|
+
async #registerNamespace(body, direction) {
|
|
409
|
+
const r = new Reader(undefined, body, this.version);
|
|
410
|
+
const requestId = await r.u62();
|
|
411
|
+
const namespace = await Namespace.decode(r);
|
|
412
|
+
const namespaces = this.#namespaceRequests(direction);
|
|
413
|
+
this.#namespacesByRequestId.set(requestId, { namespace, direction });
|
|
414
|
+
// First announcement in this direction wins. A duplicate is refused with 409, and
|
|
415
|
+
// overwriting here would point the first request's withdrawal at the refused one,
|
|
416
|
+
// which has no stream left, leaving the namespace announced forever.
|
|
417
|
+
if (!namespaces.has(namespace))
|
|
418
|
+
namespaces.set(namespace, requestId);
|
|
419
|
+
return requestId;
|
|
420
|
+
}
|
|
421
|
+
#namespaceRequests(direction) {
|
|
422
|
+
return direction === "incoming" ? this.#incomingNamespaces : this.#outgoingNamespaces;
|
|
394
423
|
}
|
|
395
424
|
/** Create a WritableStream that buffers and writes complete messages to the control stream under mutex. */
|
|
396
425
|
#createSendWritable() {
|
|
@@ -431,13 +460,17 @@ export class ControlStreamAdapter {
|
|
|
431
460
|
const r = new Reader(undefined, body, this.version);
|
|
432
461
|
return await r.u62();
|
|
433
462
|
};
|
|
434
|
-
|
|
463
|
+
// v14/v15 name their withdrawals instead of numbering them. A name we have no
|
|
464
|
+
// announcement for is dropped, not fatal: it belongs to a duplicate we already
|
|
465
|
+
// refused, or to a request that is already gone, so there is nothing left to close.
|
|
466
|
+
// The mapping itself is released by #forget when the stream goes.
|
|
467
|
+
const readNamespaceRequestId = async (direction) => {
|
|
435
468
|
const r = new Reader(undefined, body, this.version);
|
|
436
469
|
const namespace = await Namespace.decode(r);
|
|
437
|
-
const
|
|
470
|
+
const namespaces = this.#namespaceRequests(direction);
|
|
471
|
+
const requestId = namespaces.get(namespace);
|
|
438
472
|
if (requestId === undefined)
|
|
439
|
-
|
|
440
|
-
this.#namespaces.delete(namespace);
|
|
473
|
+
console.warn(`adapter: no announcement for namespace: ${namespace}`);
|
|
441
474
|
return requestId;
|
|
442
475
|
};
|
|
443
476
|
switch (typeId) {
|
|
@@ -457,12 +490,8 @@ export class ControlStreamAdapter {
|
|
|
457
490
|
return { route: Route.NewRequest, requestId };
|
|
458
491
|
}
|
|
459
492
|
case 0x06: {
|
|
460
|
-
// PublishNamespace
|
|
461
|
-
const
|
|
462
|
-
const requestId = await r.u62();
|
|
463
|
-
const namespace = await Namespace.decode(r);
|
|
464
|
-
this.#namespaces.set(namespace, requestId);
|
|
465
|
-
this.#namespacesByRequestId.set(requestId, namespace);
|
|
493
|
+
// PublishNamespace: also store the namespace for v14/v15 reverse lookup
|
|
494
|
+
const requestId = await this.#registerNamespace(body, "incoming");
|
|
466
495
|
return { route: Route.NewRequest, requestId };
|
|
467
496
|
}
|
|
468
497
|
case 0x11: {
|
|
@@ -534,7 +563,10 @@ export class ControlStreamAdapter {
|
|
|
534
563
|
return { route: Route.FollowUp, requestId: subNs08 };
|
|
535
564
|
}
|
|
536
565
|
case 0x0e: {
|
|
537
|
-
|
|
566
|
+
if (this.version === Version.DRAFT_14 || this.version === Version.DRAFT_15) {
|
|
567
|
+
throw new Error("unexpected message 0x0e");
|
|
568
|
+
}
|
|
569
|
+
// v16+: NamespaceDone entry (no requestId) — route to SubscribeNamespace stream
|
|
538
570
|
const subNs0e = this.#subscribeNamespaces.values().next().value;
|
|
539
571
|
if (subNs0e === undefined)
|
|
540
572
|
throw new Error("unexpected message 0x0e: no SubscribeNamespace stream");
|
|
@@ -554,9 +586,9 @@ export class ControlStreamAdapter {
|
|
|
554
586
|
return { route: Route.CloseStream, requestId };
|
|
555
587
|
}
|
|
556
588
|
case 0x0b: {
|
|
557
|
-
// PublishDone
|
|
589
|
+
// PublishDone: the subscriber reads its status and stream count before the end.
|
|
558
590
|
const requestId = await readRequestId();
|
|
559
|
-
return { route: Route.
|
|
591
|
+
return { route: Route.ErrorResponse, requestId };
|
|
560
592
|
}
|
|
561
593
|
case 0x17: {
|
|
562
594
|
// FetchCancel
|
|
@@ -564,21 +596,27 @@ export class ControlStreamAdapter {
|
|
|
564
596
|
return { route: Route.CloseStream, requestId };
|
|
565
597
|
}
|
|
566
598
|
case 0x09: {
|
|
567
|
-
// PublishNamespaceDone: v16 uses requestId, v14/v15
|
|
599
|
+
// PublishNamespaceDone: v16 uses requestId, v14/v15 names the incoming
|
|
600
|
+
// announcement its sender is withdrawing.
|
|
568
601
|
if (this.version === Version.DRAFT_16) {
|
|
569
602
|
const requestId = await readRequestId();
|
|
570
603
|
return { route: Route.CloseStream, requestId };
|
|
571
604
|
}
|
|
572
|
-
const requestId = await readNamespaceRequestId();
|
|
605
|
+
const requestId = await readNamespaceRequestId("incoming");
|
|
606
|
+
if (requestId === undefined)
|
|
607
|
+
return { route: Route.Ignore, requestId: 0n };
|
|
573
608
|
return { route: Route.CloseStream, requestId };
|
|
574
609
|
}
|
|
575
610
|
case 0x0c: {
|
|
576
|
-
// PublishNamespaceCancel: v16 uses requestId, v14/v15
|
|
611
|
+
// PublishNamespaceCancel: v16 uses requestId, v14/v15 names the outgoing
|
|
612
|
+
// announcement its sender is rejecting.
|
|
577
613
|
if (this.version === Version.DRAFT_16) {
|
|
578
614
|
const requestId = await readRequestId();
|
|
579
615
|
return { route: Route.CloseStream, requestId };
|
|
580
616
|
}
|
|
581
|
-
const requestId = await readNamespaceRequestId();
|
|
617
|
+
const requestId = await readNamespaceRequestId("outgoing");
|
|
618
|
+
if (requestId === undefined)
|
|
619
|
+
return { route: Route.Ignore, requestId: 0n };
|
|
582
620
|
return { route: Route.CloseStream, requestId };
|
|
583
621
|
}
|
|
584
622
|
case 0x14: {
|
|
@@ -607,15 +645,21 @@ export class ControlStreamAdapter {
|
|
|
607
645
|
throw new Error(`unknown control message type: 0x${typeId.toString(16)}`);
|
|
608
646
|
}
|
|
609
647
|
}
|
|
610
|
-
|
|
648
|
+
/**
|
|
649
|
+
* Ends every virtual stream: cleanly for a deliberate close, or with `err` when the
|
|
650
|
+
* control stream died under them, since every request riding it was cut off.
|
|
651
|
+
*/
|
|
652
|
+
close(err) {
|
|
611
653
|
if (this.#closed)
|
|
612
654
|
return;
|
|
613
655
|
this.#closed = true;
|
|
614
656
|
console.debug("adapter: close() called");
|
|
615
|
-
// Close all virtual streams
|
|
616
657
|
for (const entry of this.#streams.values()) {
|
|
617
658
|
try {
|
|
618
|
-
|
|
659
|
+
if (err)
|
|
660
|
+
entry.controller.error(err);
|
|
661
|
+
else
|
|
662
|
+
entry.controller.close();
|
|
619
663
|
}
|
|
620
664
|
catch {
|
|
621
665
|
// Already closed
|
|
@@ -628,7 +672,8 @@ export class ControlStreamAdapter {
|
|
|
628
672
|
}
|
|
629
673
|
this.#incomingWaiters = [];
|
|
630
674
|
// Clear namespace mappings
|
|
631
|
-
this.#
|
|
675
|
+
this.#incomingNamespaces.clear();
|
|
676
|
+
this.#outgoingNamespaces.clear();
|
|
632
677
|
this.#namespacesByRequestId.clear();
|
|
633
678
|
this.#subscribeNamespaces.clear();
|
|
634
679
|
// Unblock maxRequestId waiters
|