@norskvideo/moq-net 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 +70 -0
- package/announced.d.ts +45 -0
- package/announced.d.ts.map +1 -0
- package/announced.js +70 -0
- package/announced.js.map +1 -0
- package/bandwidth.d.ts +11 -0
- package/bandwidth.d.ts.map +1 -0
- package/bandwidth.js +7 -0
- package/bandwidth.js.map +1 -0
- package/broadcast.d.ts +39 -0
- package/broadcast.d.ts.map +1 -0
- package/broadcast.js +76 -0
- package/broadcast.js.map +1 -0
- package/connection/accept.d.ts +15 -0
- package/connection/accept.d.ts.map +1 -0
- package/connection/accept.js +137 -0
- package/connection/accept.js.map +1 -0
- package/connection/cert.test.d.ts +2 -0
- package/connection/cert.test.d.ts.map +1 -0
- package/connection/cert.test.js +26 -0
- package/connection/cert.test.js.map +1 -0
- package/connection/connect.d.ts +37 -0
- package/connection/connect.d.ts.map +1 -0
- package/connection/connect.js +348 -0
- package/connection/connect.js.map +1 -0
- package/connection/established.d.ts +30 -0
- package/connection/established.d.ts.map +1 -0
- package/connection/established.js +3 -0
- package/connection/established.js.map +1 -0
- package/connection/handshake.d.ts +10 -0
- package/connection/handshake.d.ts.map +1 -0
- package/connection/handshake.js +48 -0
- package/connection/handshake.js.map +1 -0
- package/connection/index.d.ts +5 -0
- package/connection/index.d.ts.map +1 -0
- package/connection/index.js +6 -0
- package/connection/index.js.map +1 -0
- package/connection/reload.d.ts +47 -0
- package/connection/reload.d.ts.map +1 -0
- package/connection/reload.js +137 -0
- package/connection/reload.js.map +1 -0
- package/group.d.ts +66 -0
- package/group.d.ts.map +1 -0
- package/group.js +135 -0
- package/group.js.map +1 -0
- package/group.test.d.ts +2 -0
- package/group.test.d.ts.map +1 -0
- package/group.test.js +66 -0
- package/group.test.js.map +1 -0
- package/ietf/adapter.d.ts +65 -0
- package/ietf/adapter.d.ts.map +1 -0
- package/ietf/adapter.js +626 -0
- package/ietf/adapter.js.map +1 -0
- package/ietf/connection.d.ts +65 -0
- package/ietf/connection.d.ts.map +1 -0
- package/ietf/connection.js +248 -0
- package/ietf/connection.js.map +1 -0
- package/ietf/control.d.ts +135 -0
- package/ietf/control.d.ts.map +1 -0
- package/ietf/control.js +242 -0
- package/ietf/control.js.map +1 -0
- package/ietf/fetch.d.ts +64 -0
- package/ietf/fetch.d.ts.map +1 -0
- package/ietf/fetch.js +100 -0
- package/ietf/fetch.js.map +1 -0
- package/ietf/goaway.d.ts +15 -0
- package/ietf/goaway.d.ts.map +1 -0
- package/ietf/goaway.js +34 -0
- package/ietf/goaway.js.map +1 -0
- package/ietf/ietf.test.d.ts +2 -0
- package/ietf/ietf.test.d.ts.map +1 -0
- package/ietf/ietf.test.js +873 -0
- package/ietf/ietf.test.js.map +1 -0
- package/ietf/index.d.ts +18 -0
- package/ietf/index.d.ts.map +1 -0
- package/ietf/index.js +19 -0
- package/ietf/index.js.map +1 -0
- package/ietf/message.d.ts +4 -0
- package/ietf/message.d.ts.map +1 -0
- package/ietf/message.js +54 -0
- package/ietf/message.js.map +1 -0
- package/ietf/namespace.d.ts +5 -0
- package/ietf/namespace.d.ts.map +1 -0
- package/ietf/namespace.js +27 -0
- package/ietf/namespace.js.map +1 -0
- package/ietf/object.d.ts +40 -0
- package/ietf/object.d.ts.map +1 -0
- package/ietf/object.js +160 -0
- package/ietf/object.js.map +1 -0
- package/ietf/parameters.d.ts +56 -0
- package/ietf/parameters.d.ts.map +1 -0
- package/ietf/parameters.js +337 -0
- package/ietf/parameters.js.map +1 -0
- package/ietf/properties.d.ts +4 -0
- package/ietf/properties.d.ts.map +1 -0
- package/ietf/properties.js +33 -0
- package/ietf/properties.js.map +1 -0
- package/ietf/publish.d.ts +68 -0
- package/ietf/publish.d.ts.map +1 -0
- package/ietf/publish.js +192 -0
- package/ietf/publish.js.map +1 -0
- package/ietf/publish_namespace.d.ts +68 -0
- package/ietf/publish_namespace.d.ts.map +1 -0
- package/ietf/publish_namespace.js +176 -0
- package/ietf/publish_namespace.js.map +1 -0
- package/ietf/publisher.d.ts +50 -0
- package/ietf/publisher.d.ts.map +1 -0
- package/ietf/publisher.js +330 -0
- package/ietf/publisher.js.map +1 -0
- package/ietf/request.d.ts +52 -0
- package/ietf/request.d.ts.map +1 -0
- package/ietf/request.js +119 -0
- package/ietf/request.js.map +1 -0
- package/ietf/setup.d.ts +38 -0
- package/ietf/setup.d.ts.map +1 -0
- package/ietf/setup.js +129 -0
- package/ietf/setup.js.map +1 -0
- package/ietf/subscribe.d.ts +66 -0
- package/ietf/subscribe.d.ts.map +1 -0
- package/ietf/subscribe.js +276 -0
- package/ietf/subscribe.js.map +1 -0
- package/ietf/subscribe_namespace.d.ts +101 -0
- package/ietf/subscribe_namespace.d.ts.map +1 -0
- package/ietf/subscribe_namespace.js +256 -0
- package/ietf/subscribe_namespace.js.map +1 -0
- package/ietf/subscriber.d.ts +53 -0
- package/ietf/subscriber.d.ts.map +1 -0
- package/ietf/subscriber.js +404 -0
- package/ietf/subscriber.js.map +1 -0
- package/ietf/track.d.ts +40 -0
- package/ietf/track.d.ts.map +1 -0
- package/ietf/track.js +108 -0
- package/ietf/track.js.map +1 -0
- package/ietf/version.d.ts +56 -0
- package/ietf/version.d.ts.map +1 -0
- package/ietf/version.js +63 -0
- package/ietf/version.js.map +1 -0
- package/index.d.ts +22 -0
- package/index.d.ts.map +1 -0
- package/index.js +23 -0
- package/index.js.map +1 -0
- package/integration.test.d.ts +2 -0
- package/integration.test.d.ts.map +1 -0
- package/integration.test.js +90 -0
- package/integration.test.js.map +1 -0
- package/lite/announce.d.ts +35 -0
- package/lite/announce.d.ts.map +1 -0
- package/lite/announce.js +166 -0
- package/lite/announce.js.map +1 -0
- package/lite/connection.d.ts +47 -0
- package/lite/connection.d.ts.map +1 -0
- package/lite/connection.js +247 -0
- package/lite/connection.js.map +1 -0
- package/lite/fetch.d.ts +14 -0
- package/lite/fetch.d.ts.map +1 -0
- package/lite/fetch.js +47 -0
- package/lite/fetch.js.map +1 -0
- package/lite/goaway.d.ts +10 -0
- package/lite/goaway.d.ts.map +1 -0
- package/lite/goaway.js +37 -0
- package/lite/goaway.js.map +1 -0
- package/lite/group.d.ts +18 -0
- package/lite/group.d.ts.map +1 -0
- package/lite/group.js +46 -0
- package/lite/group.js.map +1 -0
- package/lite/index.d.ts +11 -0
- package/lite/index.d.ts.map +1 -0
- package/lite/index.js +12 -0
- package/lite/index.js.map +1 -0
- package/lite/message.d.ts +5 -0
- package/lite/message.d.ts.map +1 -0
- package/lite/message.js +51 -0
- package/lite/message.js.map +1 -0
- package/lite/origin.d.ts +25 -0
- package/lite/origin.d.ts.map +1 -0
- package/lite/origin.js +35 -0
- package/lite/origin.js.map +1 -0
- package/lite/probe.d.ts +12 -0
- package/lite/probe.d.ts.map +1 -0
- package/lite/probe.js +60 -0
- package/lite/probe.js.map +1 -0
- package/lite/publisher.d.ts +63 -0
- package/lite/publisher.d.ts.map +1 -0
- package/lite/publisher.js +350 -0
- package/lite/publisher.js.map +1 -0
- package/lite/session.d.ts +36 -0
- package/lite/session.d.ts.map +1 -0
- package/lite/session.js +130 -0
- package/lite/session.js.map +1 -0
- package/lite/setup.d.ts +18 -0
- package/lite/setup.d.ts.map +1 -0
- package/lite/setup.js +55 -0
- package/lite/setup.js.map +1 -0
- package/lite/stream.d.ts +23 -0
- package/lite/stream.d.ts.map +1 -0
- package/lite/stream.js +20 -0
- package/lite/stream.js.map +1 -0
- package/lite/subscribe.d.ts +111 -0
- package/lite/subscribe.d.ts.map +1 -0
- package/lite/subscribe.js +341 -0
- package/lite/subscribe.js.map +1 -0
- package/lite/subscriber.d.ts +80 -0
- package/lite/subscriber.d.ts.map +1 -0
- package/lite/subscriber.js +316 -0
- package/lite/subscriber.js.map +1 -0
- package/lite/track.d.ts +38 -0
- package/lite/track.d.ts.map +1 -0
- package/lite/track.js +81 -0
- package/lite/track.js.map +1 -0
- package/lite/track.test.d.ts +2 -0
- package/lite/track.test.d.ts.map +1 -0
- package/lite/track.test.js +53 -0
- package/lite/track.test.js.map +1 -0
- package/lite/version.d.ts +16 -0
- package/lite/version.d.ts.map +1 -0
- package/lite/version.js +41 -0
- package/lite/version.js.map +1 -0
- package/mock.d.ts +16 -0
- package/mock.d.ts.map +1 -0
- package/mock.js +154 -0
- package/mock.js.map +1 -0
- package/package.json +27 -0
- package/path.d.ts +111 -0
- package/path.d.ts.map +1 -0
- package/path.js +133 -0
- package/path.js.map +1 -0
- package/path.test.d.ts +2 -0
- package/path.test.d.ts.map +1 -0
- package/path.test.js +169 -0
- package/path.test.js.map +1 -0
- package/stream.d.ts +57 -0
- package/stream.d.ts.map +1 -0
- package/stream.js +326 -0
- package/stream.js.map +1 -0
- package/stream.test.d.ts +2 -0
- package/stream.test.d.ts.map +1 -0
- package/stream.test.js +210 -0
- package/stream.test.js.map +1 -0
- package/time.d.ts +109 -0
- package/time.d.ts.map +1 -0
- package/time.js +94 -0
- package/time.js.map +1 -0
- package/track.d.ts +85 -0
- package/track.d.ts.map +1 -0
- package/track.js +227 -0
- package/track.js.map +1 -0
- package/track.test.d.ts +2 -0
- package/track.test.d.ts.map +1 -0
- package/track.test.js +56 -0
- package/track.test.js.map +1 -0
- package/util/error.d.ts +3 -0
- package/util/error.d.ts.map +1 -0
- package/util/error.js +9 -0
- package/util/error.js.map +1 -0
- package/util/hex.d.ts +3 -0
- package/util/hex.d.ts.map +1 -0
- package/util/hex.js +16 -0
- package/util/hex.js.map +1 -0
- package/util/index.d.ts +2 -0
- package/util/index.d.ts.map +1 -0
- package/util/index.js +3 -0
- package/util/index.js.map +1 -0
- package/varint.d.ts +36 -0
- package/varint.d.ts.map +1 -0
- package/varint.js +288 -0
- package/varint.js.map +1 -0
- package/varint.test.d.ts +2 -0
- package/varint.test.d.ts.map +1 -0
- package/varint.test.js +106 -0
- package/varint.test.js.map +1 -0
- package/zod.d.ts +13 -0
- package/zod.d.ts.map +1 -0
- package/zod.js +19 -0
- package/zod.js.map +1 -0
package/ietf/adapter.js
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/* @ts-self-types="./adapter.d.ts" */
|
|
2
|
+
import { Mutex } from "async-mutex";
|
|
3
|
+
import { Reader, Stream } from "../stream.js";
|
|
4
|
+
import * as Varint from "../varint.js";
|
|
5
|
+
import * as Namespace from "./namespace.js";
|
|
6
|
+
import { Version } from "./version.js";
|
|
7
|
+
/**
|
|
8
|
+
* v17 native session — thin wrapper around WebTransport.
|
|
9
|
+
* Each request gets its own real bidi stream; no control stream multiplexing.
|
|
10
|
+
*/
|
|
11
|
+
export class NativeSession {
|
|
12
|
+
#quic;
|
|
13
|
+
#requestId = 0n;
|
|
14
|
+
version;
|
|
15
|
+
constructor(quic, version) {
|
|
16
|
+
this.#quic = quic;
|
|
17
|
+
this.version = version;
|
|
18
|
+
}
|
|
19
|
+
async openBi() {
|
|
20
|
+
return Stream.open(this.#quic, this.version);
|
|
21
|
+
}
|
|
22
|
+
async acceptBi() {
|
|
23
|
+
return Stream.accept(this.#quic, this.version);
|
|
24
|
+
}
|
|
25
|
+
async nextRequestId() {
|
|
26
|
+
const id = this.#requestId;
|
|
27
|
+
this.#requestId += 2n;
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Route classification for control stream messages.
|
|
32
|
+
const Route = {
|
|
33
|
+
NewRequest: 0, // Create virtual bidi stream, push initial message
|
|
34
|
+
Response: 1, // Push message to existing stream (keep open)
|
|
35
|
+
ErrorResponse: 2, // Push message to existing stream, then close
|
|
36
|
+
CloseStream: 3, // Close stream recv (no bytes pushed)
|
|
37
|
+
FollowUp: 4, // Push follow-up message to existing stream
|
|
38
|
+
MaxRequestId: 5, // Update flow control
|
|
39
|
+
Ignore: 6, // Connection-level, no routing
|
|
40
|
+
GoAway: 7, // Terminal
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Converts v14-v16 control stream multiplexing into virtual bidi streams.
|
|
44
|
+
*
|
|
45
|
+
* Reads control messages, classifies them, and routes to virtual Stream objects.
|
|
46
|
+
* Each request/response pair gets its own virtual Stream, making all versions
|
|
47
|
+
* look like v17's stream-per-request model.
|
|
48
|
+
*/
|
|
49
|
+
export class ControlStreamAdapter {
|
|
50
|
+
// WebTransport session (for opening real bidi streams in v16)
|
|
51
|
+
#quic;
|
|
52
|
+
// Control stream
|
|
53
|
+
#reader;
|
|
54
|
+
#writer;
|
|
55
|
+
#writeMutex = new Mutex();
|
|
56
|
+
version;
|
|
57
|
+
// Virtual streams keyed by requestId
|
|
58
|
+
#streams = new Map();
|
|
59
|
+
// Namespace → requestId reverse lookup (v14/v15 namespace-keyed messages)
|
|
60
|
+
#namespaces = new Map();
|
|
61
|
+
// requestId → namespace reverse lookup (for cleanup in #closeStream)
|
|
62
|
+
#namespacesByRequestId = new Map();
|
|
63
|
+
// SubscribeNamespace requestIds — for routing 0x08/0x0E entries that lack requestId (v14/v15)
|
|
64
|
+
#subscribeNamespaces = new Set();
|
|
65
|
+
// Incoming stream queue (for acceptBi)
|
|
66
|
+
#incomingQueue = [];
|
|
67
|
+
#incomingWaiters = [];
|
|
68
|
+
// Request ID flow control
|
|
69
|
+
#requestId = 0n;
|
|
70
|
+
#maxRequestId;
|
|
71
|
+
#maxRequestIdResolves = [];
|
|
72
|
+
#closed = false;
|
|
73
|
+
constructor(quic, controlStream, version, maxRequestId) {
|
|
74
|
+
this.#quic = quic;
|
|
75
|
+
this.#reader = controlStream.reader;
|
|
76
|
+
this.#reader.version = version;
|
|
77
|
+
this.#writer = controlStream.writer;
|
|
78
|
+
this.#writer.version = version;
|
|
79
|
+
this.version = version;
|
|
80
|
+
this.#maxRequestId = maxRequestId;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Accept the next incoming virtual bidi stream.
|
|
84
|
+
* Blocks until a new request arrives on the control stream.
|
|
85
|
+
*/
|
|
86
|
+
async acceptBi() {
|
|
87
|
+
if (this.#closed)
|
|
88
|
+
return undefined;
|
|
89
|
+
const queued = this.#incomingQueue.shift();
|
|
90
|
+
if (queued)
|
|
91
|
+
return queued;
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
this.#incomingWaiters.push(resolve);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Open an outgoing virtual bidi stream.
|
|
98
|
+
* Buffers writes until the first full message is available, parses the
|
|
99
|
+
* requestId (and namespace for PublishNamespace), self-registers, then
|
|
100
|
+
* flushes. Subsequent writes go directly to the control stream.
|
|
101
|
+
*/
|
|
102
|
+
openBi() {
|
|
103
|
+
let controller;
|
|
104
|
+
let registeredRequestId;
|
|
105
|
+
const readable = new ReadableStream({
|
|
106
|
+
start(c) {
|
|
107
|
+
controller = c;
|
|
108
|
+
},
|
|
109
|
+
cancel: () => {
|
|
110
|
+
if (registeredRequestId !== undefined) {
|
|
111
|
+
this.#streams.delete(registeredRequestId);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
let buffer = new Uint8Array(0);
|
|
116
|
+
let registered = false;
|
|
117
|
+
const sendWritable = new WritableStream({
|
|
118
|
+
write: async (chunk) => {
|
|
119
|
+
// Always accumulate bytes and flush only complete messages
|
|
120
|
+
// to prevent interleaving of partial messages from concurrent virtual streams.
|
|
121
|
+
const newBuf = new Uint8Array(buffer.length + chunk.length);
|
|
122
|
+
newBuf.set(buffer);
|
|
123
|
+
newBuf.set(chunk, buffer.length);
|
|
124
|
+
buffer = newBuf;
|
|
125
|
+
// Try to flush complete messages from the buffer
|
|
126
|
+
for (;;) {
|
|
127
|
+
const boundary = this.#messageSize(buffer);
|
|
128
|
+
if (boundary === undefined)
|
|
129
|
+
break;
|
|
130
|
+
const toFlush = buffer.subarray(0, boundary);
|
|
131
|
+
buffer = buffer.subarray(boundary);
|
|
132
|
+
if (!registered) {
|
|
133
|
+
// First message: extract requestId and register before flushing
|
|
134
|
+
const parsed = this.#tryParseOutgoing(toFlush);
|
|
135
|
+
if (parsed) {
|
|
136
|
+
registeredRequestId = parsed.requestId;
|
|
137
|
+
this.#streams.set(parsed.requestId, { controller });
|
|
138
|
+
registered = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
await this.#writeMutex.runExclusive(() => this.#writer.write(toFlush));
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const stream = new Stream({ readable, writable: sendWritable });
|
|
146
|
+
stream.reader.version = this.version;
|
|
147
|
+
stream.writer.version = this.version;
|
|
148
|
+
return stream;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Open a real WebTransport bidi stream (for v16 SubscribeNamespace).
|
|
152
|
+
*/
|
|
153
|
+
async openNativeBi() {
|
|
154
|
+
return Stream.open(this.#quic, this.version);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Allocate the next request ID, blocking if flow control limit reached.
|
|
158
|
+
*/
|
|
159
|
+
async nextRequestId() {
|
|
160
|
+
for (;;) {
|
|
161
|
+
if (this.#closed)
|
|
162
|
+
return undefined;
|
|
163
|
+
const id = this.#requestId;
|
|
164
|
+
if (id < this.#maxRequestId) {
|
|
165
|
+
this.#requestId += 2n;
|
|
166
|
+
return id;
|
|
167
|
+
}
|
|
168
|
+
await new Promise((resolve) => {
|
|
169
|
+
this.#maxRequestIdResolves.push(resolve);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Main run loop — reads control stream messages and routes to virtual streams.
|
|
175
|
+
* Must be called after construction. Runs until the control stream closes.
|
|
176
|
+
*/
|
|
177
|
+
async run() {
|
|
178
|
+
try {
|
|
179
|
+
// v16: also accept real bidi streams (for SubscribeNamespace)
|
|
180
|
+
if (this.version === Version.DRAFT_16) {
|
|
181
|
+
void this.#acceptNativeBidis();
|
|
182
|
+
}
|
|
183
|
+
for (;;) {
|
|
184
|
+
const done = await this.#reader.done();
|
|
185
|
+
if (done)
|
|
186
|
+
break;
|
|
187
|
+
const typeId = await this.#reader.u53();
|
|
188
|
+
const size = await this.#reader.u16();
|
|
189
|
+
const body = await this.#reader.read(size);
|
|
190
|
+
const classified = await this.#classify(typeId, body);
|
|
191
|
+
if (classified.route === Route.GoAway) {
|
|
192
|
+
console.warn("received GOAWAY on control stream");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const { route, requestId } = classified;
|
|
196
|
+
switch (route) {
|
|
197
|
+
case Route.NewRequest:
|
|
198
|
+
this.#newRequest(typeId, size, body, requestId);
|
|
199
|
+
break;
|
|
200
|
+
case Route.Response:
|
|
201
|
+
this.#pushMessage(requestId, typeId, size, body);
|
|
202
|
+
break;
|
|
203
|
+
case Route.ErrorResponse:
|
|
204
|
+
this.#pushMessage(requestId, typeId, size, body);
|
|
205
|
+
this.#closeStream(requestId);
|
|
206
|
+
break;
|
|
207
|
+
case Route.CloseStream:
|
|
208
|
+
this.#closeStream(requestId);
|
|
209
|
+
break;
|
|
210
|
+
case Route.FollowUp:
|
|
211
|
+
this.#pushMessage(requestId, typeId, size, body);
|
|
212
|
+
break;
|
|
213
|
+
case Route.MaxRequestId:
|
|
214
|
+
this.#maxRequestId = requestId;
|
|
215
|
+
for (const resolve of this.#maxRequestIdResolves)
|
|
216
|
+
resolve();
|
|
217
|
+
this.#maxRequestIdResolves = [];
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
this.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Accept real WebTransport bidi streams and queue them for acceptBi (v16). */
|
|
227
|
+
async #acceptNativeBidis() {
|
|
228
|
+
try {
|
|
229
|
+
for (;;) {
|
|
230
|
+
const stream = await Stream.accept(this.#quic, this.version);
|
|
231
|
+
if (!stream)
|
|
232
|
+
break;
|
|
233
|
+
const waiter = this.#incomingWaiters.shift();
|
|
234
|
+
if (waiter) {
|
|
235
|
+
waiter(stream);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
this.#incomingQueue.push(stream);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// Session closed
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
#newRequest(typeId, size, body, requestId) {
|
|
247
|
+
let controller;
|
|
248
|
+
const readable = new ReadableStream({
|
|
249
|
+
start(c) {
|
|
250
|
+
controller = c;
|
|
251
|
+
},
|
|
252
|
+
cancel: () => {
|
|
253
|
+
this.#streams.delete(requestId);
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
const sendWritable = this.#createSendWritable();
|
|
257
|
+
const stream = new Stream({ readable, writable: sendWritable });
|
|
258
|
+
stream.reader.version = this.version;
|
|
259
|
+
stream.writer.version = this.version;
|
|
260
|
+
this.#streams.set(requestId, { controller });
|
|
261
|
+
// Push initial message bytes so the dispatcher can read typeId + decode
|
|
262
|
+
controller.enqueue(this.#encodeRaw(typeId, size, body));
|
|
263
|
+
// Queue for acceptBi
|
|
264
|
+
const waiter = this.#incomingWaiters.shift();
|
|
265
|
+
if (waiter) {
|
|
266
|
+
waiter(stream);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
this.#incomingQueue.push(stream);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
#pushMessage(requestId, typeId, size, body) {
|
|
273
|
+
const entry = this.#streams.get(requestId);
|
|
274
|
+
if (!entry) {
|
|
275
|
+
console.warn(`adapter: no stream for requestId=${requestId} typeId=0x${typeId.toString(16)}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
entry.controller.enqueue(this.#encodeRaw(typeId, size, body));
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Stream already closed
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
#closeStream(requestId) {
|
|
286
|
+
const entry = this.#streams.get(requestId);
|
|
287
|
+
if (!entry)
|
|
288
|
+
return;
|
|
289
|
+
console.debug(`adapter: closing stream requestId=${requestId}`);
|
|
290
|
+
this.#streams.delete(requestId);
|
|
291
|
+
this.#subscribeNamespaces.delete(requestId);
|
|
292
|
+
const namespace = this.#namespacesByRequestId.get(requestId);
|
|
293
|
+
if (namespace !== undefined) {
|
|
294
|
+
this.#namespaces.delete(namespace);
|
|
295
|
+
this.#namespacesByRequestId.delete(requestId);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
entry.controller.close();
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Already closed
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Returns the total byte size of the first complete message in buffer,
|
|
306
|
+
* or undefined if the buffer doesn't contain a complete message yet.
|
|
307
|
+
* Message format: [typeId varint][size u16 BE][body of `size` bytes]
|
|
308
|
+
*/
|
|
309
|
+
#messageSize(buffer) {
|
|
310
|
+
if (buffer.length === 0)
|
|
311
|
+
return undefined;
|
|
312
|
+
const typeSize = 1 << ((buffer[0] & 0xc0) >> 6);
|
|
313
|
+
if (buffer.length < typeSize)
|
|
314
|
+
return undefined;
|
|
315
|
+
const [, afterType] = Varint.decode(buffer);
|
|
316
|
+
if (afterType.length < 2)
|
|
317
|
+
return undefined;
|
|
318
|
+
const size = (afterType[0] << 8) | afterType[1];
|
|
319
|
+
const totalSize = buffer.length - afterType.length + 2 + size;
|
|
320
|
+
if (buffer.length < totalSize)
|
|
321
|
+
return undefined;
|
|
322
|
+
return totalSize;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Try to parse the first outgoing message from accumulated bytes.
|
|
326
|
+
* Returns the requestId if enough data is available, undefined otherwise.
|
|
327
|
+
*/
|
|
328
|
+
#tryParseOutgoing(buffer) {
|
|
329
|
+
if (buffer.length === 0)
|
|
330
|
+
return undefined;
|
|
331
|
+
// Check typeId varint size before decoding
|
|
332
|
+
const typeSize = 1 << ((buffer[0] & 0xc0) >> 6);
|
|
333
|
+
if (buffer.length < typeSize)
|
|
334
|
+
return undefined;
|
|
335
|
+
const [typeId, afterType] = Varint.decode(buffer);
|
|
336
|
+
// Need 2 bytes for u16 size
|
|
337
|
+
if (afterType.length < 2)
|
|
338
|
+
return undefined;
|
|
339
|
+
const size = (afterType[0] << 8) | afterType[1];
|
|
340
|
+
const bodyStart = afterType.subarray(2);
|
|
341
|
+
// Need full body
|
|
342
|
+
if (bodyStart.length < size)
|
|
343
|
+
return undefined;
|
|
344
|
+
const body = bodyStart.subarray(0, size);
|
|
345
|
+
// Decode requestId from body (QUIC varint)
|
|
346
|
+
const [reqId] = Varint.decode(body);
|
|
347
|
+
const requestId = BigInt(reqId);
|
|
348
|
+
// PublishNamespace (0x06): also parse namespace for v14/v15 reverse lookup
|
|
349
|
+
if (typeId === 0x06) {
|
|
350
|
+
try {
|
|
351
|
+
const [, afterReqId] = Varint.decode(body);
|
|
352
|
+
this.#parseAndRegisterNamespace(afterReqId, requestId);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// Non-critical: only needed for v14/v15 PublishNamespaceDone/Cancel
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// SubscribeNamespace (0x11): register for follow-up routing
|
|
359
|
+
if (typeId === 0x11) {
|
|
360
|
+
this.#subscribeNamespaces.add(requestId);
|
|
361
|
+
}
|
|
362
|
+
return { requestId };
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Parse a namespace from raw bytes and register it for reverse lookup.
|
|
366
|
+
*/
|
|
367
|
+
#parseAndRegisterNamespace(buf, requestId) {
|
|
368
|
+
const decoder = new TextDecoder();
|
|
369
|
+
const [partCount, afterCount] = Varint.decode(buf);
|
|
370
|
+
let cursor = afterCount;
|
|
371
|
+
const parts = [];
|
|
372
|
+
for (let i = 0; i < partCount; i++) {
|
|
373
|
+
const [len, afterLen] = Varint.decode(cursor);
|
|
374
|
+
parts.push(decoder.decode(afterLen.subarray(0, len)));
|
|
375
|
+
cursor = afterLen.subarray(len);
|
|
376
|
+
}
|
|
377
|
+
const namespace = parts.join("/");
|
|
378
|
+
this.#namespaces.set(namespace, requestId);
|
|
379
|
+
this.#namespacesByRequestId.set(requestId, namespace);
|
|
380
|
+
}
|
|
381
|
+
/** Create a WritableStream that buffers and writes complete messages to the control stream under mutex. */
|
|
382
|
+
#createSendWritable() {
|
|
383
|
+
let buffer = new Uint8Array(0);
|
|
384
|
+
return new WritableStream({
|
|
385
|
+
write: async (chunk) => {
|
|
386
|
+
const newBuf = new Uint8Array(buffer.length + chunk.length);
|
|
387
|
+
newBuf.set(buffer);
|
|
388
|
+
newBuf.set(chunk, buffer.length);
|
|
389
|
+
buffer = newBuf;
|
|
390
|
+
for (;;) {
|
|
391
|
+
const boundary = this.#messageSize(buffer);
|
|
392
|
+
if (boundary === undefined)
|
|
393
|
+
break;
|
|
394
|
+
const toFlush = buffer.subarray(0, boundary);
|
|
395
|
+
buffer = buffer.subarray(boundary);
|
|
396
|
+
await this.#writeMutex.runExclusive(() => this.#writer.write(toFlush));
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/** Encode raw message bytes: [typeId varint][size u16 BE][body] */
|
|
402
|
+
#encodeRaw(typeId, size, body) {
|
|
403
|
+
// v14-v16 always use QUIC varint
|
|
404
|
+
const typeIdBytes = Varint.encodeTo(new ArrayBuffer(9), typeId);
|
|
405
|
+
const result = new Uint8Array(typeIdBytes.byteLength + 2 + body.byteLength);
|
|
406
|
+
result.set(typeIdBytes, 0);
|
|
407
|
+
const sizeView = new DataView(result.buffer, typeIdBytes.byteLength, 2);
|
|
408
|
+
sizeView.setUint16(0, size);
|
|
409
|
+
result.set(body, typeIdBytes.byteLength + 2);
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Classify a control message and extract its requestId for routing.
|
|
414
|
+
*/
|
|
415
|
+
async #classify(typeId, body) {
|
|
416
|
+
const readRequestId = async () => {
|
|
417
|
+
const r = new Reader(undefined, body, this.version);
|
|
418
|
+
return await r.u62();
|
|
419
|
+
};
|
|
420
|
+
const readNamespaceRequestId = async () => {
|
|
421
|
+
const r = new Reader(undefined, body, this.version);
|
|
422
|
+
const namespace = await Namespace.decode(r);
|
|
423
|
+
const requestId = this.#namespaces.get(namespace);
|
|
424
|
+
if (requestId === undefined)
|
|
425
|
+
throw new Error(`unknown namespace: ${namespace}`);
|
|
426
|
+
this.#namespaces.delete(namespace);
|
|
427
|
+
return requestId;
|
|
428
|
+
};
|
|
429
|
+
switch (typeId) {
|
|
430
|
+
// === FollowUp: route to existing stream ===
|
|
431
|
+
case 0x02: {
|
|
432
|
+
// SubscribeUpdate / REQUEST_UPDATE
|
|
433
|
+
const requestId = await readRequestId();
|
|
434
|
+
return { route: Route.FollowUp, requestId };
|
|
435
|
+
}
|
|
436
|
+
// === NewRequest: create virtual stream ===
|
|
437
|
+
case 0x03: // Subscribe
|
|
438
|
+
case 0x16: // Fetch
|
|
439
|
+
case 0x1d: // Publish
|
|
440
|
+
case 0x0d: {
|
|
441
|
+
// TrackStatusRequest
|
|
442
|
+
const requestId = await readRequestId();
|
|
443
|
+
return { route: Route.NewRequest, requestId };
|
|
444
|
+
}
|
|
445
|
+
case 0x06: {
|
|
446
|
+
// PublishNamespace — also store namespace for v14/v15 reverse lookup
|
|
447
|
+
const r = new Reader(undefined, body, this.version);
|
|
448
|
+
const requestId = await r.u62();
|
|
449
|
+
const namespace = await Namespace.decode(r);
|
|
450
|
+
this.#namespaces.set(namespace, requestId);
|
|
451
|
+
this.#namespacesByRequestId.set(requestId, namespace);
|
|
452
|
+
return { route: Route.NewRequest, requestId };
|
|
453
|
+
}
|
|
454
|
+
case 0x11: {
|
|
455
|
+
// SubscribeNamespace (v14/v15 only on control stream)
|
|
456
|
+
if (this.version !== Version.DRAFT_14 && this.version !== Version.DRAFT_15) {
|
|
457
|
+
throw new Error("unexpected SubscribeNamespace on control stream");
|
|
458
|
+
}
|
|
459
|
+
const requestId = await readRequestId();
|
|
460
|
+
return { route: Route.NewRequest, requestId };
|
|
461
|
+
}
|
|
462
|
+
// === Response: push bytes, keep stream open ===
|
|
463
|
+
case 0x04: {
|
|
464
|
+
// SubscribeOk
|
|
465
|
+
const requestId = await readRequestId();
|
|
466
|
+
return { route: Route.Response, requestId };
|
|
467
|
+
}
|
|
468
|
+
case 0x18: {
|
|
469
|
+
// FetchOk
|
|
470
|
+
const requestId = await readRequestId();
|
|
471
|
+
return { route: Route.Response, requestId };
|
|
472
|
+
}
|
|
473
|
+
case 0x1e: {
|
|
474
|
+
// PublishOk
|
|
475
|
+
const requestId = await readRequestId();
|
|
476
|
+
return { route: Route.Response, requestId };
|
|
477
|
+
}
|
|
478
|
+
case 0x07: {
|
|
479
|
+
// v14: PublishNamespaceOk, v15+: RequestOk
|
|
480
|
+
const requestId = await readRequestId();
|
|
481
|
+
return { route: Route.Response, requestId };
|
|
482
|
+
}
|
|
483
|
+
case 0x12: {
|
|
484
|
+
// SubscribeNamespaceOk (v14 only)
|
|
485
|
+
if (this.version !== Version.DRAFT_14)
|
|
486
|
+
throw new Error("unexpected SubscribeNamespaceOk");
|
|
487
|
+
const requestId = await readRequestId();
|
|
488
|
+
return { route: Route.Response, requestId };
|
|
489
|
+
}
|
|
490
|
+
// === ErrorResponse: push bytes + close ===
|
|
491
|
+
case 0x05: {
|
|
492
|
+
// SubscribeError (v14) / RequestError (v15+)
|
|
493
|
+
const requestId = await readRequestId();
|
|
494
|
+
return { route: Route.ErrorResponse, requestId };
|
|
495
|
+
}
|
|
496
|
+
case 0x19: {
|
|
497
|
+
// FetchError (v14 only)
|
|
498
|
+
if (this.version !== Version.DRAFT_14)
|
|
499
|
+
throw new Error("unexpected FetchError");
|
|
500
|
+
const requestId = await readRequestId();
|
|
501
|
+
return { route: Route.ErrorResponse, requestId };
|
|
502
|
+
}
|
|
503
|
+
case 0x1f: {
|
|
504
|
+
// PublishError (v14 only)
|
|
505
|
+
if (this.version !== Version.DRAFT_14)
|
|
506
|
+
throw new Error("unexpected PublishError");
|
|
507
|
+
const requestId = await readRequestId();
|
|
508
|
+
return { route: Route.ErrorResponse, requestId };
|
|
509
|
+
}
|
|
510
|
+
case 0x08: {
|
|
511
|
+
if (this.version === Version.DRAFT_14) {
|
|
512
|
+
// PublishNamespaceError
|
|
513
|
+
const requestId = await readRequestId();
|
|
514
|
+
return { route: Route.ErrorResponse, requestId };
|
|
515
|
+
}
|
|
516
|
+
// v15: Namespace entry (no requestId) — route to SubscribeNamespace stream
|
|
517
|
+
const subNs08 = this.#subscribeNamespaces.values().next().value;
|
|
518
|
+
if (subNs08 === undefined)
|
|
519
|
+
throw new Error("unexpected message 0x08: no SubscribeNamespace stream");
|
|
520
|
+
return { route: Route.FollowUp, requestId: subNs08 };
|
|
521
|
+
}
|
|
522
|
+
case 0x0e: {
|
|
523
|
+
// v15: NamespaceDone entry (no requestId) — route to SubscribeNamespace stream
|
|
524
|
+
const subNs0e = this.#subscribeNamespaces.values().next().value;
|
|
525
|
+
if (subNs0e === undefined)
|
|
526
|
+
throw new Error("unexpected message 0x0e: no SubscribeNamespace stream");
|
|
527
|
+
return { route: Route.FollowUp, requestId: subNs0e };
|
|
528
|
+
}
|
|
529
|
+
case 0x13: {
|
|
530
|
+
// SubscribeNamespaceError (v14 only)
|
|
531
|
+
if (this.version !== Version.DRAFT_14)
|
|
532
|
+
throw new Error("unexpected SubscribeNamespaceError");
|
|
533
|
+
const requestId = await readRequestId();
|
|
534
|
+
return { route: Route.ErrorResponse, requestId };
|
|
535
|
+
}
|
|
536
|
+
// === CloseStream: close recv (no bytes pushed) ===
|
|
537
|
+
case 0x0a: {
|
|
538
|
+
// Unsubscribe
|
|
539
|
+
const requestId = await readRequestId();
|
|
540
|
+
return { route: Route.CloseStream, requestId };
|
|
541
|
+
}
|
|
542
|
+
case 0x0b: {
|
|
543
|
+
// PublishDone
|
|
544
|
+
const requestId = await readRequestId();
|
|
545
|
+
return { route: Route.CloseStream, requestId };
|
|
546
|
+
}
|
|
547
|
+
case 0x17: {
|
|
548
|
+
// FetchCancel
|
|
549
|
+
const requestId = await readRequestId();
|
|
550
|
+
return { route: Route.CloseStream, requestId };
|
|
551
|
+
}
|
|
552
|
+
case 0x09: {
|
|
553
|
+
// PublishNamespaceDone: v16 uses requestId, v14/v15 uses namespace
|
|
554
|
+
if (this.version === Version.DRAFT_16) {
|
|
555
|
+
const requestId = await readRequestId();
|
|
556
|
+
return { route: Route.CloseStream, requestId };
|
|
557
|
+
}
|
|
558
|
+
const requestId = await readNamespaceRequestId();
|
|
559
|
+
return { route: Route.CloseStream, requestId };
|
|
560
|
+
}
|
|
561
|
+
case 0x0c: {
|
|
562
|
+
// PublishNamespaceCancel: v16 uses requestId, v14/v15 uses namespace
|
|
563
|
+
if (this.version === Version.DRAFT_16) {
|
|
564
|
+
const requestId = await readRequestId();
|
|
565
|
+
return { route: Route.CloseStream, requestId };
|
|
566
|
+
}
|
|
567
|
+
const requestId = await readNamespaceRequestId();
|
|
568
|
+
return { route: Route.CloseStream, requestId };
|
|
569
|
+
}
|
|
570
|
+
case 0x14: {
|
|
571
|
+
// UnsubscribeNamespace (v14/v15 only)
|
|
572
|
+
if (this.version !== Version.DRAFT_14 && this.version !== Version.DRAFT_15) {
|
|
573
|
+
throw new Error("unexpected UnsubscribeNamespace");
|
|
574
|
+
}
|
|
575
|
+
const requestId = await readRequestId();
|
|
576
|
+
return { route: Route.CloseStream, requestId };
|
|
577
|
+
}
|
|
578
|
+
// === Utility ===
|
|
579
|
+
case 0x15: {
|
|
580
|
+
// MaxRequestId
|
|
581
|
+
const requestId = await readRequestId();
|
|
582
|
+
return { route: Route.MaxRequestId, requestId };
|
|
583
|
+
}
|
|
584
|
+
case 0x1a: {
|
|
585
|
+
// RequestsBlocked — connection-level, consume and ignore
|
|
586
|
+
await readRequestId();
|
|
587
|
+
return { route: Route.Ignore, requestId: 0n };
|
|
588
|
+
}
|
|
589
|
+
// === Terminal ===
|
|
590
|
+
case 0x10: // GoAway
|
|
591
|
+
return { route: Route.GoAway };
|
|
592
|
+
default:
|
|
593
|
+
throw new Error(`unknown control message type: 0x${typeId.toString(16)}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
close() {
|
|
597
|
+
if (this.#closed)
|
|
598
|
+
return;
|
|
599
|
+
this.#closed = true;
|
|
600
|
+
console.debug("adapter: close() called");
|
|
601
|
+
// Close all virtual streams
|
|
602
|
+
for (const entry of this.#streams.values()) {
|
|
603
|
+
try {
|
|
604
|
+
entry.controller.close();
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
// Already closed
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
this.#streams.clear();
|
|
611
|
+
// Resolve any waiting acceptBi callers
|
|
612
|
+
for (const waiter of this.#incomingWaiters) {
|
|
613
|
+
waiter(undefined);
|
|
614
|
+
}
|
|
615
|
+
this.#incomingWaiters = [];
|
|
616
|
+
// Clear namespace mappings
|
|
617
|
+
this.#namespaces.clear();
|
|
618
|
+
this.#namespacesByRequestId.clear();
|
|
619
|
+
this.#subscribeNamespaces.clear();
|
|
620
|
+
// Unblock maxRequestId waiters
|
|
621
|
+
for (const resolve of this.#maxRequestIdResolves)
|
|
622
|
+
resolve();
|
|
623
|
+
this.#maxRequestIdResolves = [];
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
//# sourceMappingURL=adapter.js.map
|