@amritk/nish-x86_64-linux 0.16.0 → 0.17.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.
Files changed (55) hide show
  1. package/INSTALL.md +43 -34
  2. package/bin/nish +0 -0
  3. package/package.json +1 -1
  4. package/runtime/nish.d.ts +117 -8
  5. package/runtime/nish.h +30 -7
  6. package/runtime/nish.mjs +71 -7
  7. package/runtime/runtime-host.c +43 -24
  8. package/runtime/runtime-net.c +57 -10
  9. package/runtime/runtime-os.c +62 -20
  10. package/runtime/runtime-wasm.c +1 -0
  11. package/runtime/runtime.c +46 -8
  12. package/runtime/shim.mjs +114 -5
  13. package/scripts/build.sh +1 -3
  14. package/std/README.md +166 -35
  15. package/std/crypto/aes.ts +51 -8
  16. package/std/crypto/base64url.ts +25 -12
  17. package/std/crypto/chacha20poly1305.ts +43 -29
  18. package/std/crypto/ct.ts +6 -1
  19. package/std/crypto/hkdf.ts +364 -3
  20. package/std/crypto/hmac.ts +307 -7
  21. package/std/crypto/p256.ts +148 -27
  22. package/std/crypto/sha1.ts +147 -0
  23. package/std/crypto/sha256.ts +3 -2
  24. package/std/crypto/x25519.ts +88 -36
  25. package/std/crypto/x509.ts +130 -47
  26. package/std/net/hpack.ts +1094 -0
  27. package/std/net/http-fields.ts +604 -0
  28. package/std/net/http1-server.ts +1389 -0
  29. package/std/net/http1.ts +1492 -0
  30. package/std/net/http2-frame.ts +745 -0
  31. package/std/net/http2-tls.ts +216 -0
  32. package/std/net/http2.ts +1433 -0
  33. package/std/net/http3-frame.ts +457 -0
  34. package/std/net/http3-server.ts +905 -0
  35. package/std/net/http3.ts +2145 -0
  36. package/std/net/qpack.ts +1163 -0
  37. package/std/net/quic-conn-ack.ts +206 -0
  38. package/std/net/quic-conn-cid.ts +425 -0
  39. package/std/net/quic-conn-params.ts +392 -0
  40. package/std/net/quic-datagram.ts +130 -0
  41. package/std/net/quic-frame.ts +1206 -0
  42. package/std/net/quic-listener.ts +653 -0
  43. package/std/net/quic-packet.ts +1474 -0
  44. package/std/net/quic-recovery.ts +849 -0
  45. package/std/net/quic-stream.ts +1603 -0
  46. package/std/net/quic.ts +3214 -0
  47. package/std/net/tls/codec.ts +1089 -0
  48. package/std/net/tls/record-server.ts +1173 -0
  49. package/std/net/tls/record.ts +621 -0
  50. package/std/net/tls/schedule.ts +429 -0
  51. package/std/net/tls-tcp.ts +410 -0
  52. package/std/net/tls.ts +1368 -0
  53. package/std/net/websocket.ts +823 -0
  54. package/std/net/webtransport.ts +1334 -0
  55. package/std/secret.ts +62 -0
@@ -0,0 +1,849 @@
1
+ /**
2
+ * `nish/net/quic-recovery` — QUIC loss detection and congestion control
3
+ * (RFC 9002): the packets each packet number space has in flight, the
4
+ * round-trip estimate, loss by packet and by time threshold, the probe
5
+ * timeout with its backoff, NewReno, and a pacer. Sans-IO: it never sees a
6
+ * byte of a packet, only packet numbers, sizes and the caller's clock.
7
+ *
8
+ * import { QuicRecovery, QUIC_RECOVERY_TIMEOUT_PTO } from "nish/net/quic-recovery";
9
+ *
10
+ * const recovery = new QuicRecovery();
11
+ * const slot: i32 = recovery.onPacketSent(space, pn, size, now); // every ack-eliciting packet
12
+ * recovery.onAck(space, frame.ackRanges, frame.ackRangeCount, delay, now);
13
+ * … recovery.spaces[space].acked / .lost name the slots it settled …
14
+ * if (now >= recovery.deadline(blocked)) { recovery.onTimeout(now, blocked); }
15
+ *
16
+ * **What it tracks.** Each space keeps its ack-eliciting packets in a fixed
17
+ * ring (`QuicSentPackets`), in the order they were sent, so the memory is
18
+ * fixed when the connection is made and nothing is allocated per packet. A
19
+ * packet that elicits no acknowledgement (one that carries only ACK, PADDING
20
+ * or CONNECTION_CLOSE) is not recorded: it is never in flight, and neither
21
+ * an RTT sample nor a loss is ever taken from it (RFC 9002 §2, §5.1). The
22
+ * caller keeps what each packet carried in its own arrays, indexed by the
23
+ * slot `onPacketSent` answers, and reads `acked` and `lost` after each
24
+ * `onAck` and `onTimeout` to learn which slots were settled. A slot named
25
+ * there keeps its packet number, time and size until the next call.
26
+ *
27
+ * **Time and units.** Every time and every duration is the caller's
28
+ * monotonic clock in milliseconds, which is also the timer granularity
29
+ * (`QUIC_RECOVERY_GRANULARITY`, §6.1.2), and the arithmetic is Appendix A's
30
+ * and Appendix B's in integers: `smoothed_rtt = (7 × smoothed_rtt +
31
+ * adjusted_rtt) / 8`, `rttvar = (3 × rttvar + |smoothed_rtt −
32
+ * adjusted_rtt|) / 4`, each division truncating. Sizes are bytes.
33
+ *
34
+ * **What the server leaves out.** It is written for the server side of a
35
+ * connection: there is no client anti-deadlock probe (§6.2.2.1), since a
36
+ * server never has to send to unblock its peer, and nothing reads ECN
37
+ * (§7.1, B.7). A congestion window that the sender does not use is not
38
+ * grown (§7.8): an acknowledgement grows it only if, when it arrived, the
39
+ * bytes in flight were at least half the window.
40
+ *
41
+ * **What a peer cannot do.** Every input is checked: the ranges of an
42
+ * acknowledgement are read only up to the count given and the array's
43
+ * length, a packet acknowledged twice counts once, and
44
+ * the backoff stops doubling at `QUIC_RECOVERY_MAX_BACKOFF`, so no duration
45
+ * overflows. A peer that never acknowledges anything costs a probe each
46
+ * timeout, at a rate that halves each time, until the idle timeout ends the
47
+ * connection.
48
+ *
49
+ * Written from RFC 9002 §5 to §7 and its Appendices A and B, in this
50
+ * module's own structure; nothing here is ported from another
51
+ * implementation. Private names carry the `quicRecovery` prefix
52
+ * (`docs/wp26-stdlib.md` §3e).
53
+ */
54
+ import { QUIC_MAX_VARINT } from "nish/net/quic-packet"
55
+
56
+ /** The Initial space's index, which is `TLS_LEVEL_INITIAL`. */
57
+ export const QUIC_RECOVERY_INITIAL: i32 = 0
58
+ /** The Handshake space's index. */
59
+ export const QUIC_RECOVERY_HANDSHAKE: i32 = 1
60
+ /** The Application Data space's index. */
61
+ export const QUIC_RECOVERY_APPLICATION: i32 = 2
62
+ /**
63
+ * How many packets the Initial and the Handshake space each keep in flight.
64
+ * A server's handshake flight is a few datagrams, and the anti-amplification
65
+ * limit holds it to three times what the client sent until its address is
66
+ * validated, so 16 leaves room for several probes on top.
67
+ */
68
+ export const QUIC_RECOVERY_HANDSHAKE_CAPACITY: i32 = 16
69
+ /**
70
+ * How many packets the Application Data space keeps in flight: 128
71
+ * full-sized datagrams is 150 KiB in flight, a 12 Mbit/s path at a 100 ms
72
+ * round trip, against what the record costs every connection from the
73
+ * start. When the ring is full the space sends nothing that would elicit an
74
+ * acknowledgement until one arrives, as if the congestion window were full.
75
+ */
76
+ export const QUIC_RECOVERY_APPLICATION_CAPACITY: i32 = 128
77
+ /** kPacketThreshold (§6.1.1): a packet three numbers below one acknowledged is lost. */
78
+ export const QUIC_RECOVERY_PACKET_THRESHOLD: i64 = 3
79
+ /** kGranularity (§6.1.2), in milliseconds: the timer resolution, and the least any loss delay or variance term is. */
80
+ export const QUIC_RECOVERY_GRANULARITY: i64 = 1
81
+ /** kInitialRtt (§6.2.2), in milliseconds: the round trip assumed before the first sample. */
82
+ export const QUIC_RECOVERY_INITIAL_RTT: i64 = 333
83
+ /** max_datagram_size (B.1): the 1200 bytes every datagram this server sends is at most. */
84
+ export const QUIC_RECOVERY_DATAGRAM_SIZE: i64 = 1200
85
+ /** kInitialWindow (§7.2): min(10 × 1200, max(14720, 2 × 1200)), so 12000 bytes. */
86
+ export const QUIC_RECOVERY_INITIAL_WINDOW: i64 = 12000
87
+ /** kMinimumWindow (§7.2): two datagrams, 2400 bytes. */
88
+ export const QUIC_RECOVERY_MINIMUM_WINDOW: i64 = 2400
89
+ /** kPersistentCongestionThreshold (§7.6.1): lost packets spanning three probe timeouts collapse the window. */
90
+ export const QUIC_RECOVERY_PERSISTENT_THRESHOLD: i64 = 3
91
+ /** The default max_ack_delay (RFC 9000 §18.2), in milliseconds, until the peer's transport parameters say otherwise. */
92
+ export const QUIC_RECOVERY_DEFAULT_MAX_ACK_DELAY: i64 = 25
93
+ /**
94
+ * The most times the probe timeout doubles (§6.2.1). Sixteen doublings of
95
+ * the longest probe timeout a peer can cause still fit an `i64`, and by then
96
+ * the idle timeout has ended the connection.
97
+ */
98
+ export const QUIC_RECOVERY_MAX_BACKOFF: i32 = 16
99
+
100
+ /** `onTimeout` ran nothing: no timer was due. */
101
+ export const QUIC_RECOVERY_TIMEOUT_NONE: i32 = 0
102
+ /** `onTimeout` declared packets lost by the time threshold; `lost` in `timeoutSpace` names them. */
103
+ export const QUIC_RECOVERY_TIMEOUT_LOSS: i32 = 1
104
+ /** `onTimeout` was a probe timeout: the caller sends a probe in `timeoutSpace` (§6.2.4). */
105
+ export const QUIC_RECOVERY_TIMEOUT_PTO: i32 = 2
106
+
107
+ /** A slot's states: free, a packet in flight, one acknowledged, one declared lost, and one this call declared lost. */
108
+ const QUIC_RECOVERY_FREE: i32 = 0
109
+ const QUIC_RECOVERY_SENT: i32 = 1
110
+ const QUIC_RECOVERY_ACKED: i32 = 2
111
+ const QUIC_RECOVERY_LOST: i32 = 3
112
+ const QUIC_RECOVERY_LOST_NOW: i32 = 4
113
+
114
+ /** The larger of two times or durations. */
115
+ const quicRecoveryMax = (a: i64, b: i64): i64 => (a > b ? a : b)
116
+
117
+ /**
118
+ * The ack-eliciting packets one space sent, in a ring of `capacity` slots
119
+ * in the order they went out: slot `slot(k)` is the `k`-th from the oldest
120
+ * still kept, for `k` below `count`. A packet stays in its slot after it is
121
+ * acknowledged or lost until every older one is settled too, which is what
122
+ * lets persistent congestion see that nothing between two lost packets was
123
+ * acknowledged (§7.6.2).
124
+ */
125
+ export class QuicSentPackets {
126
+ pn: i64[]
127
+ timeSent: i64[]
128
+ size: i32[]
129
+ state: u8[]
130
+ /** The slots the last `onAck` acknowledged, `ackedCount` of them, oldest first. */
131
+ acked: i32[]
132
+ /** The slots the last `onAck` or `onTimeout` declared lost, `lostCount` of them, oldest first. */
133
+ lost: i32[]
134
+ /** The largest packet number the peer acknowledged in this space, or -1. */
135
+ largestAcked: i64 = -1
136
+ /** The packet number of the last packet recorded, or -1: each must be larger (RFC 9000 §12.3). */
137
+ lastPn: i64 = -1
138
+ /** loss_time (A.3): when the oldest packet not yet lost by the packet threshold becomes lost by time, or -1. */
139
+ lossTime: i64 = -1
140
+ /** time_of_last_ack_eliciting_packet (A.3), or -1 before the first. */
141
+ lastAckEliciting: i64 = -1
142
+ /** The bytes of this space's packets in flight. */
143
+ bytesInFlight: i64 = 0
144
+ capacity: i32 = 0
145
+ head: i32 = 0
146
+ count: i32 = 0
147
+ /** How many recorded packets are still in flight: neither acknowledged nor lost. */
148
+ inFlight: i32 = 0
149
+ ackedCount: i32 = 0
150
+ lostCount: i32 = 0
151
+
152
+ constructor(capacity: i32) {
153
+ this.capacity = capacity
154
+ this.pn = new Array<i64>(capacity)
155
+ this.timeSent = new Array<i64>(capacity)
156
+ this.size = new Array<i32>(capacity)
157
+ this.state = new Array<u8>(capacity)
158
+ this.acked = new Array<i32>(capacity)
159
+ this.lost = new Array<i32>(capacity)
160
+ }
161
+
162
+ /** The ring slot of the `k`-th packet kept, counting from the oldest. */
163
+ slot(k: i32): i32 {
164
+ return (this.head + k) % this.capacity
165
+ }
166
+
167
+ /** The state of slot `s`, or free for a slot outside the ring. */
168
+ stateAt(s: i32): i32 {
169
+ if (s < 0 || s >= toI32(this.state.length)) {
170
+ return QUIC_RECOVERY_FREE
171
+ }
172
+ return toI32(this.state[s])
173
+ }
174
+
175
+ /** Sets slot `s`'s state. */
176
+ setState(s: i32, value: i32): void {
177
+ if (s >= 0 && s < toI32(this.state.length)) {
178
+ this.state[s] = toU8(value)
179
+ }
180
+ }
181
+
182
+ /** The packet number in slot `s`, or -1. */
183
+ pnAt(s: i32): i64 {
184
+ return s >= 0 && s < toI32(this.pn.length) ? this.pn[s] : -1
185
+ }
186
+
187
+ /** When the packet in slot `s` was sent, or -1. */
188
+ timeAt(s: i32): i64 {
189
+ return s >= 0 && s < toI32(this.timeSent.length) ? this.timeSent[s] : -1
190
+ }
191
+
192
+ /** The size of the packet in slot `s`, or 0. */
193
+ sizeAt(s: i32): i32 {
194
+ return s >= 0 && s < toI32(this.size.length) ? this.size[s] : 0
195
+ }
196
+
197
+ /** The `k`-th slot the last call acknowledged, or -1. */
198
+ ackedSlot(k: i32): i32 {
199
+ return k >= 0 && k < this.ackedCount && k < toI32(this.acked.length) ? this.acked[k] : -1
200
+ }
201
+
202
+ /** The `k`-th slot the last call declared lost, or -1. */
203
+ lostSlot(k: i32): i32 {
204
+ return k >= 0 && k < this.lostCount && k < toI32(this.lost.length) ? this.lost[k] : -1
205
+ }
206
+
207
+ /** Whether slot `s` holds a packet still in flight. */
208
+ inFlightAt(s: i32): boolean {
209
+ return this.stateAt(s) === QUIC_RECOVERY_SENT
210
+ }
211
+
212
+ /** Whether every slot is taken, so nothing more can be recorded until a packet is settled. */
213
+ full(): boolean {
214
+ this.compact()
215
+ return this.count >= this.capacity
216
+ }
217
+
218
+ /** Frees the oldest slots that are settled, acknowledged or lost. */
219
+ compact(): void {
220
+ while (this.count > 0 && this.stateAt(this.head) !== QUIC_RECOVERY_SENT) {
221
+ this.setState(this.head, QUIC_RECOVERY_FREE)
222
+ this.head = (this.head + 1) % this.capacity
223
+ this.count = this.count - 1
224
+ }
225
+ if (this.count === 0) {
226
+ this.head = 0
227
+ }
228
+ }
229
+
230
+ /** Forgets the results of the last call. */
231
+ clearResults(): void {
232
+ this.ackedCount = 0
233
+ this.lostCount = 0
234
+ }
235
+
236
+ /** Takes packet `s` out of flight. */
237
+ settle(s: i32, state: i32): void {
238
+ this.setState(s, state)
239
+ this.inFlight = this.inFlight - 1
240
+ this.bytesInFlight = this.bytesInFlight - toI64(this.sizeAt(s))
241
+ }
242
+
243
+ /** Settles packet `s` as lost and lists it in `lost`. */
244
+ markLost(s: i32): void {
245
+ this.settle(s, QUIC_RECOVERY_LOST_NOW)
246
+ if (this.lostCount < toI32(this.lost.length)) {
247
+ this.lost[this.lostCount] = s
248
+ this.lostCount = this.lostCount + 1
249
+ }
250
+ }
251
+
252
+ /** Drops every packet, as when the space's keys are discarded (A.11). */
253
+ clear(): void {
254
+ for (let s: i32 = 0; s < toI32(this.state.length); s += 1) {
255
+ this.state[s] = toU8(QUIC_RECOVERY_FREE)
256
+ }
257
+ this.head = 0
258
+ this.count = 0
259
+ this.inFlight = 0
260
+ this.bytesInFlight = 0
261
+ this.lossTime = -1
262
+ this.lastAckEliciting = -1
263
+ this.clearResults()
264
+ }
265
+
266
+ /** Empties the ring and forgets every number, for a connection slot reused for another peer. */
267
+ reset(): void {
268
+ this.clear()
269
+ this.largestAcked = -1
270
+ this.lastPn = -1
271
+ }
272
+ }
273
+
274
+ /** Whether `pn` falls in one of the first `count` `[smallest, largest]` pairs of `ranges`. */
275
+ const quicRecoveryInRanges = (ranges: i64[], count: i32, pn: i64): boolean => {
276
+ for (let k: i32 = 0; k < count; k += 1) {
277
+ const low: i32 = k * 2
278
+ const high: i32 = low + 1
279
+ if (low < 0 || low >= toI32(ranges.length) || high < 0 || high >= toI32(ranges.length)) {
280
+ return false
281
+ }
282
+ if (pn >= ranges[low] && pn <= ranges[high]) {
283
+ return true
284
+ }
285
+ }
286
+ return false
287
+ }
288
+
289
+ /**
290
+ * One connection's loss recovery and congestion control, across its three
291
+ * packet number spaces. The fields are readable: the RTT estimate
292
+ * (`latestRtt`, `smoothedRtt`, `rttVar`, `minRtt`), the window
293
+ * (`congestionWindow`, `ssthresh`, `recoveryStart`), `ptoCount`, and the
294
+ * counts a test or a log reads (`congestionEvents`, `persistentCongestions`).
295
+ */
296
+ export class QuicRecovery {
297
+ spaces: QuicSentPackets[]
298
+ /** latest_rtt (§5.1): the last sample, in milliseconds. */
299
+ latestRtt: i64 = 0
300
+ /** smoothed_rtt (§5.3): kInitialRtt until the first sample. */
301
+ smoothedRtt: i64 = 0
302
+ /** rttvar (§5.3): kInitialRtt / 2 until the first sample. */
303
+ rttVar: i64 = 0
304
+ /** min_rtt (§5.2): 0 until the first sample. */
305
+ minRtt: i64 = 0
306
+ /** When the first RTT sample was taken, or -1 before it: persistent congestion counts only packets sent since (§7.6.2). */
307
+ firstSampleTime: i64 = -1
308
+ /** The peer's max_ack_delay in milliseconds (RFC 9000 §18.2), which the caller sets once it has read it. */
309
+ maxAckDelay: i64 = 0
310
+ /** congestion_window (B.2), in bytes. */
311
+ congestionWindow: i64 = 0
312
+ /** ssthresh (B.2): unbounded (2^62 − 1) until the first congestion event. */
313
+ ssthresh: i64 = 0
314
+ /** congestion_recovery_start_time (B.2), or -1 outside recovery. */
315
+ recoveryStart: i64 = -1
316
+ /** The pacer's credit in bytes (§7.7), and the time it was last topped up, or -1. */
317
+ pacerBudget: i64 = 0
318
+ pacerTime: i64 = -1
319
+ /** pto_count (A.3): how many probe timeouts fired since the last acknowledgement. */
320
+ ptoCount: i32 = 0
321
+ /** The space the last `onTimeout` acted on, or -1. */
322
+ timeoutSpace: i32 = -1
323
+ /** How many congestion events reduced the window, and how many of them were persistent congestion. */
324
+ congestionEvents: i32 = 0
325
+ persistentCongestions: i32 = 0
326
+ /** Whether the handshake is confirmed (RFC 9001 §4.1.2), which the caller sets: the peer's max_ack_delay then bounds its ACK delays, and the Application Data space has a probe timeout. */
327
+ handshakeConfirmed: boolean = false
328
+
329
+ constructor() {
330
+ this.spaces = [
331
+ new QuicSentPackets(QUIC_RECOVERY_HANDSHAKE_CAPACITY),
332
+ new QuicSentPackets(QUIC_RECOVERY_HANDSHAKE_CAPACITY),
333
+ new QuicSentPackets(QUIC_RECOVERY_APPLICATION_CAPACITY),
334
+ ]
335
+ this.reset()
336
+ }
337
+
338
+ /**
339
+ * Puts everything back as a new connection has it: every space empty, the
340
+ * RTT estimate and the window at their initial values. A connection slot
341
+ * reused for another peer calls it; the rings are kept.
342
+ */
343
+ reset(): void {
344
+ for (const sp of this.spaces) {
345
+ sp.reset()
346
+ }
347
+ this.latestRtt = 0
348
+ this.smoothedRtt = QUIC_RECOVERY_INITIAL_RTT
349
+ this.rttVar = QUIC_RECOVERY_INITIAL_RTT / 2
350
+ this.minRtt = 0
351
+ this.firstSampleTime = -1
352
+ this.maxAckDelay = QUIC_RECOVERY_DEFAULT_MAX_ACK_DELAY
353
+ this.congestionWindow = QUIC_RECOVERY_INITIAL_WINDOW
354
+ this.ssthresh = QUIC_MAX_VARINT
355
+ this.recoveryStart = -1
356
+ this.pacerBudget = QUIC_RECOVERY_INITIAL_WINDOW
357
+ this.pacerTime = -1
358
+ this.ptoCount = 0
359
+ this.timeoutSpace = -1
360
+ this.congestionEvents = 0
361
+ this.persistentCongestions = 0
362
+ this.handshakeConfirmed = false
363
+ }
364
+
365
+ /** Space `space`'s packets, or `null` for an index that names no space. */
366
+ space(space: i32): QuicSentPackets | null {
367
+ if (space < 0 || space >= toI32(this.spaces.length)) {
368
+ return null
369
+ }
370
+ return this.spaces[space]
371
+ }
372
+
373
+ /** bytes_in_flight (B.2): every space's packets in flight. */
374
+ bytesInFlight(): i64 {
375
+ let total: i64 = 0
376
+ for (const sp of this.spaces) {
377
+ total = total + sp.bytesInFlight
378
+ }
379
+ return total
380
+ }
381
+
382
+ /**
383
+ * Whether the window has room for another full-sized datagram (B.4). The
384
+ * caller sends nothing ack-eliciting while it has not, except a probe.
385
+ */
386
+ canSend(): boolean {
387
+ return this.bytesInFlight() + QUIC_RECOVERY_DATAGRAM_SIZE <= this.congestionWindow
388
+ }
389
+
390
+ /**
391
+ * Records an ack-eliciting packet (A.5): number `pn` of space `space`,
392
+ * `size` bytes on the wire, sent at `now`. Answers its slot, under which
393
+ * the caller keeps what it carried, or -1, recording nothing, for a space
394
+ * that does not exist, a size that is not positive, a packet number that is
395
+ * not above the last one recorded, or a ring that is full.
396
+ */
397
+ onPacketSent(space: i32, pn: i64, size: i32, now: i64): i32 {
398
+ const sp: QuicSentPackets | null = this.space(space)
399
+ if (sp === null || size <= 0 || pn <= sp.lastPn || pn > QUIC_MAX_VARINT || sp.full()) {
400
+ return -1
401
+ }
402
+ const s: i32 = sp.slot(sp.count)
403
+ if (s < 0 || s >= toI32(sp.pn.length) || s >= toI32(sp.timeSent.length) || s >= toI32(sp.size.length)) {
404
+ return -1
405
+ }
406
+ sp.pn[s] = pn
407
+ sp.timeSent[s] = now
408
+ sp.size[s] = size
409
+ sp.setState(s, QUIC_RECOVERY_SENT)
410
+ sp.count = sp.count + 1
411
+ sp.inFlight = sp.inFlight + 1
412
+ sp.bytesInFlight = sp.bytesInFlight + toI64(size)
413
+ sp.lastPn = pn
414
+ sp.lastAckEliciting = now
415
+ return s
416
+ }
417
+
418
+ /**
419
+ * An ACK frame for space `space` (A.7): `ranges` holds `count`
420
+ * `[smallest, largest]` pairs, highest first, as `QuicFrame.ackRanges`
421
+ * does, and `ackDelay` is the peer's ACK Delay in milliseconds. The
422
+ * packets newly acknowledged are listed in the space's `acked`, those it
423
+ * shows lost in `lost`; an RTT sample is taken when the largest
424
+ * acknowledged is newly acknowledged (§5.1), the window grows or shrinks
425
+ * (B.5, B.8), and the probe backoff resets. Answers how many packets were
426
+ * newly acknowledged, or -1, changing nothing, when the space does not
427
+ * exist or there is no range. A packet number the space never sent is the
428
+ * caller's to refuse (RFC 9000 §13.1): only it knows the packets that
429
+ * elicit no acknowledgement, which are not recorded here.
430
+ */
431
+ onAck(space: i32, ranges: i64[], count: i32, ackDelay: i64, now: i64): i32 {
432
+ const sp: QuicSentPackets | null = this.space(space)
433
+ if (sp === null || count <= 0 || toI32(ranges.length) < 2) {
434
+ return -1
435
+ }
436
+ const largest: i64 = ranges[1]
437
+ sp.compact()
438
+ sp.clearResults()
439
+ if (largest > sp.largestAcked) {
440
+ sp.largestAcked = largest
441
+ }
442
+ // The bytes in flight before this ACK decide whether the window was in
443
+ // use, and so whether it may grow (§7.8).
444
+ const inUse: boolean = this.bytesInFlight() * 2 >= this.congestionWindow
445
+ let sampleAt: i32 = -1
446
+ for (let k: i32 = 0; k < sp.count; k += 1) {
447
+ const s: i32 = sp.slot(k)
448
+ // The ring is in packet-number order, so nothing past the largest acknowledged is.
449
+ if (sp.pnAt(s) > largest) {
450
+ break
451
+ }
452
+ if (sp.stateAt(s) === QUIC_RECOVERY_SENT && quicRecoveryInRanges(ranges, count, sp.pnAt(s))) {
453
+ sp.settle(s, QUIC_RECOVERY_ACKED)
454
+ if (sp.ackedCount < toI32(sp.acked.length)) {
455
+ sp.acked[sp.ackedCount] = s
456
+ sp.ackedCount = sp.ackedCount + 1
457
+ }
458
+ if (sp.pnAt(s) === largest) {
459
+ sampleAt = s
460
+ }
461
+ }
462
+ }
463
+ if (sp.ackedCount === 0) {
464
+ return 0
465
+ }
466
+ if (sampleAt >= 0) {
467
+ this.latestRtt = quicRecoveryMax(now - sp.timeAt(sampleAt), 0)
468
+ // §5.3: the Initial space's ACKs are not delayed by the peer, so their delay is ignored.
469
+ const none: i64 = 0
470
+ this.updateRtt(space === QUIC_RECOVERY_INITIAL ? none : quicRecoveryMax(ackDelay, none), now)
471
+ }
472
+ this.detectLoss(sp, now)
473
+ this.packetsLost(sp, now)
474
+ for (let k: i32 = 0; k < sp.ackedCount; k += 1) {
475
+ this.packetAcked(sp, sp.ackedSlot(k), inUse)
476
+ }
477
+ this.ptoCount = 0
478
+ return sp.ackedCount
479
+ }
480
+
481
+ /** UpdateRtt (§5.3, A.7) with a sample in `latestRtt`, `ackDelay` the peer's delay in milliseconds. */
482
+ updateRtt(ackDelay: i64, now: i64): void {
483
+ const latest: i64 = this.latestRtt
484
+ if (this.firstSampleTime < 0) {
485
+ this.minRtt = latest
486
+ this.smoothedRtt = latest
487
+ this.rttVar = latest / 2
488
+ this.firstSampleTime = now
489
+ return
490
+ }
491
+ if (latest < this.minRtt) {
492
+ this.minRtt = latest
493
+ }
494
+ let delay: i64 = ackDelay
495
+ if (this.handshakeConfirmed && delay > this.maxAckDelay) {
496
+ delay = this.maxAckDelay
497
+ }
498
+ // The delay is taken off only where that leaves the sample at or above min_rtt.
499
+ let adjusted: i64 = latest
500
+ if (latest >= this.minRtt + delay) {
501
+ adjusted = latest - delay
502
+ }
503
+ const gap: i64 = this.smoothedRtt > adjusted ? this.smoothedRtt - adjusted : adjusted - this.smoothedRtt
504
+ this.rttVar = (3 * this.rttVar + gap) / 4
505
+ this.smoothedRtt = (7 * this.smoothedRtt + adjusted) / 8
506
+ }
507
+
508
+ /** The loss delay (§6.1.2): 9/8 of the larger of the latest and the smoothed RTT, at least the granularity. */
509
+ lossDelay(): i64 {
510
+ const delay: i64 = (9 * quicRecoveryMax(this.latestRtt, this.smoothedRtt)) / 8
511
+ return quicRecoveryMax(delay, QUIC_RECOVERY_GRANULARITY)
512
+ }
513
+
514
+ /**
515
+ * DetectAndRemoveLostPackets (A.10): every packet in flight at or below
516
+ * the largest acknowledged is lost once it was sent a loss delay ago, or
517
+ * three numbers below the largest acknowledged; the first that is neither
518
+ * sets `lossTime`.
519
+ */
520
+ detectLoss(sp: QuicSentPackets, now: i64): void {
521
+ sp.lossTime = -1
522
+ const delay: i64 = this.lossDelay()
523
+ const lostSendTime: i64 = now - delay
524
+ for (let k: i32 = 0; k < sp.count; k += 1) {
525
+ const s: i32 = sp.slot(k)
526
+ const pn: i64 = sp.pnAt(s)
527
+ if (pn > sp.largestAcked) {
528
+ break
529
+ }
530
+ if (sp.stateAt(s) !== QUIC_RECOVERY_SENT) {
531
+ continue
532
+ }
533
+ if (sp.timeAt(s) <= lostSendTime || sp.largestAcked >= pn + QUIC_RECOVERY_PACKET_THRESHOLD) {
534
+ sp.markLost(s)
535
+ } else {
536
+ const at: i64 = sp.timeAt(s) + delay
537
+ if (sp.lossTime < 0 || at < sp.lossTime) {
538
+ sp.lossTime = at
539
+ }
540
+ }
541
+ }
542
+ }
543
+
544
+ /**
545
+ * OnPacketsLost (B.8) for what `detectLoss` just declared lost: one
546
+ * congestion event for the newest of them, and persistent congestion
547
+ * when they span long enough.
548
+ */
549
+ packetsLost(sp: QuicSentPackets, now: i64): void {
550
+ if (sp.lostCount === 0) {
551
+ return
552
+ }
553
+ let newest: i64 = -1
554
+ for (let k: i32 = 0; k < sp.lostCount; k += 1) {
555
+ newest = quicRecoveryMax(newest, sp.timeAt(sp.lostSlot(k)))
556
+ }
557
+ this.congestionEvent(newest, now)
558
+ if (this.persistentCongestion(sp)) {
559
+ this.congestionWindow = QUIC_RECOVERY_MINIMUM_WINDOW
560
+ this.recoveryStart = -1
561
+ this.persistentCongestions = this.persistentCongestions + 1
562
+ }
563
+ for (let k: i32 = 0; k < sp.lostCount; k += 1) {
564
+ sp.setState(sp.lostSlot(k), QUIC_RECOVERY_LOST)
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Whether the packets just declared lost establish persistent congestion
570
+ * (§7.6.2): a run of lost ack-eliciting packets, all sent since the first
571
+ * RTT sample, with nothing acknowledged between the first and the last,
572
+ * spanning more than three probe timeouts with max_ack_delay, and holding
573
+ * at least one packet this call declared lost.
574
+ */
575
+ persistentCongestion(sp: QuicSentPackets): boolean {
576
+ if (this.firstSampleTime < 0) {
577
+ return false
578
+ }
579
+ const span: i64 = (this.ptoBase() + this.maxAckDelay) * QUIC_RECOVERY_PERSISTENT_THRESHOLD
580
+ let first: i64 = -1
581
+ let fresh: boolean = false
582
+ for (let k: i32 = 0; k < sp.count; k += 1) {
583
+ const s: i32 = sp.slot(k)
584
+ const state: i32 = sp.stateAt(s)
585
+ const lost: boolean = state === QUIC_RECOVERY_LOST || state === QUIC_RECOVERY_LOST_NOW
586
+ if (state === QUIC_RECOVERY_ACKED || (lost && sp.timeAt(s) < this.firstSampleTime)) {
587
+ first = -1
588
+ fresh = false
589
+ continue
590
+ }
591
+ if (!lost) {
592
+ continue
593
+ }
594
+ if (first < 0) {
595
+ first = sp.timeAt(s)
596
+ }
597
+ fresh = fresh || state === QUIC_RECOVERY_LOST_NOW
598
+ if (fresh && sp.timeAt(s) - first > span) {
599
+ return true
600
+ }
601
+ }
602
+ return false
603
+ }
604
+
605
+ /** Whether a packet sent at `sentTime` was sent during the current recovery period (B.4). */
606
+ inRecovery(sentTime: i64): boolean {
607
+ return this.recoveryStart >= 0 && sentTime <= this.recoveryStart
608
+ }
609
+
610
+ /** OnCongestionEvent (B.6): halve the window, once per recovery period. */
611
+ congestionEvent(sentTime: i64, now: i64): void {
612
+ if (this.inRecovery(sentTime)) {
613
+ return
614
+ }
615
+ this.recoveryStart = now
616
+ this.ssthresh = this.congestionWindow / 2
617
+ this.congestionWindow = quicRecoveryMax(this.ssthresh, QUIC_RECOVERY_MINIMUM_WINDOW)
618
+ this.congestionEvents = this.congestionEvents + 1
619
+ }
620
+
621
+ /**
622
+ * OnPacketAcked (B.5) for slot `s`: in slow start the window grows by the
623
+ * packet's size, in congestion avoidance by 1200 × size / window; not at
624
+ * all for a packet sent during recovery, or when `inUse` says the window
625
+ * was not being used (§7.8).
626
+ */
627
+ packetAcked(sp: QuicSentPackets, s: i32, inUse: boolean): void {
628
+ if (!inUse || this.inRecovery(sp.timeAt(s))) {
629
+ return
630
+ }
631
+ const size: i64 = toI64(sp.sizeAt(s))
632
+ if (this.congestionWindow < this.ssthresh) {
633
+ this.congestionWindow = this.congestionWindow + size
634
+ } else {
635
+ this.congestionWindow =
636
+ this.congestionWindow + (QUIC_RECOVERY_DATAGRAM_SIZE * size) / this.congestionWindow
637
+ }
638
+ }
639
+
640
+ /**
641
+ * The probe timeout's period without backoff (§6.2.1): smoothed_rtt +
642
+ * max(4 × rttvar, kGranularity), plus the peer's max_ack_delay once the
643
+ * handshake is confirmed. It is also what a key update's old keys are kept
644
+ * for and what the idle timeout is at least three of.
645
+ */
646
+ probeTimeout(): i64 {
647
+ const base: i64 = this.ptoBase()
648
+ return this.handshakeConfirmed ? base + this.maxAckDelay : base
649
+ }
650
+
651
+ /** smoothed_rtt + max(4 × rttvar, kGranularity): every probe timeout's period before max_ack_delay and the backoff. */
652
+ ptoBase(): i64 {
653
+ return this.smoothedRtt + quicRecoveryMax(4 * this.rttVar, QUIC_RECOVERY_GRANULARITY)
654
+ }
655
+
656
+ /**
657
+ * GetPtoTimeAndSpace (A.8): when the probe timeout fires, the earliest
658
+ * over the spaces with an ack-eliciting packet in flight, with the backoff
659
+ * applied; the Application Data space counts only once the handshake is
660
+ * confirmed. Answers -1 with nothing in flight. `ptoSpace()` names the
661
+ * space it chose.
662
+ */
663
+ ptoTime(): i64 {
664
+ const space: i32 = this.ptoSpace()
665
+ if (space < 0) {
666
+ return -1
667
+ }
668
+ return this.ptoTimeOf(space)
669
+ }
670
+
671
+ /** The probe timeout of space `space`, from its last ack-eliciting packet, with the backoff. */
672
+ ptoTimeOf(space: i32): i64 {
673
+ const sp: QuicSentPackets | null = this.space(space)
674
+ if (sp === null || sp.lastAckEliciting < 0) {
675
+ return -1
676
+ }
677
+ const backoff: i64 = toI64(1) << toI64(this.ptoCount)
678
+ let duration: i64 = this.ptoBase()
679
+ if (space === QUIC_RECOVERY_APPLICATION) {
680
+ duration = duration + this.maxAckDelay
681
+ }
682
+ return sp.lastAckEliciting + duration * backoff
683
+ }
684
+
685
+ /** The space whose probe timeout fires first, or -1 when no space may have one. */
686
+ ptoSpace(): i32 {
687
+ let best: i32 = -1
688
+ let bestTime: i64 = -1
689
+ for (let space: i32 = 0; space < toI32(this.spaces.length); space += 1) {
690
+ const sp: QuicSentPackets = this.spaces[space]
691
+ if (sp.inFlight === 0 || (space === QUIC_RECOVERY_APPLICATION && !this.handshakeConfirmed)) {
692
+ continue
693
+ }
694
+ const at: i64 = this.ptoTimeOf(space)
695
+ if (best < 0 || at < bestTime) {
696
+ best = space
697
+ bestTime = at
698
+ }
699
+ }
700
+ return best
701
+ }
702
+
703
+ /** The space with the earliest `lossTime`, or -1 when none is set. */
704
+ lossSpace(): i32 {
705
+ let best: i32 = -1
706
+ let bestTime: i64 = -1
707
+ for (let space: i32 = 0; space < toI32(this.spaces.length); space += 1) {
708
+ const at: i64 = this.spaces[space].lossTime
709
+ if (at >= 0 && (best < 0 || at < bestTime)) {
710
+ best = space
711
+ bestTime = at
712
+ }
713
+ }
714
+ return best
715
+ }
716
+
717
+ /**
718
+ * SetLossDetectionTimer (A.8): when `onTimeout` is next due — the earliest
719
+ * time-threshold loss, else the probe timeout — or -1 for none. With
720
+ * `amplificationBlocked`, when the server may send nothing until the client
721
+ * does (RFC 9000 §8.1), there is no probe timeout (§6.2.2.1).
722
+ */
723
+ deadline(amplificationBlocked: boolean): i64 {
724
+ const loss: i32 = this.lossSpace()
725
+ if (loss >= 0) {
726
+ return this.spaces[loss].lossTime
727
+ }
728
+ if (amplificationBlocked) {
729
+ return -1
730
+ }
731
+ return this.ptoTime()
732
+ }
733
+
734
+ /**
735
+ * OnLossDetectionTimeout (A.9), at `now`. When a time-threshold loss is
736
+ * due, the space's packets are checked again and the lost ones listed in
737
+ * its `lost`: `QUIC_RECOVERY_TIMEOUT_LOSS`. Otherwise, when the probe
738
+ * timeout is due, the backoff doubles and the caller is to send a probe in
739
+ * `timeoutSpace`: `QUIC_RECOVERY_TIMEOUT_PTO`. Before either is due, or
740
+ * when nothing is timed, `QUIC_RECOVERY_TIMEOUT_NONE`.
741
+ */
742
+ onTimeout(now: i64, amplificationBlocked: boolean): i32 {
743
+ for (const sp of this.spaces) {
744
+ sp.compact()
745
+ sp.clearResults()
746
+ }
747
+ this.timeoutSpace = -1
748
+ const due: i64 = this.deadline(amplificationBlocked)
749
+ if (due < 0 || now < due) {
750
+ return QUIC_RECOVERY_TIMEOUT_NONE
751
+ }
752
+ const loss: i32 = this.lossSpace()
753
+ if (loss >= 0) {
754
+ const sp: QuicSentPackets = this.spaces[loss]
755
+ this.detectLoss(sp, now)
756
+ this.packetsLost(sp, now)
757
+ this.timeoutSpace = loss
758
+ return QUIC_RECOVERY_TIMEOUT_LOSS
759
+ }
760
+ this.timeoutSpace = this.ptoSpace()
761
+ if (this.ptoCount < QUIC_RECOVERY_MAX_BACKOFF) {
762
+ this.ptoCount = this.ptoCount + 1
763
+ }
764
+ return QUIC_RECOVERY_TIMEOUT_PTO
765
+ }
766
+
767
+ /**
768
+ * Gives up the oldest packet still in flight in space `space`, taking it
769
+ * out of flight without a congestion event, so that a probe can be recorded
770
+ * when the ring is full. The caller has already queued what the space had
771
+ * in flight again, which is what a probe sends. Answers the slot, or -1
772
+ * when nothing is in flight there.
773
+ */
774
+ evictOldest(space: i32): i32 {
775
+ const sp: QuicSentPackets | null = this.space(space)
776
+ if (sp === null) {
777
+ return -1
778
+ }
779
+ sp.compact()
780
+ for (let k: i32 = 0; k < sp.count; k += 1) {
781
+ const s: i32 = sp.slot(k)
782
+ if (sp.stateAt(s) === QUIC_RECOVERY_SENT) {
783
+ sp.settle(s, QUIC_RECOVERY_LOST)
784
+ return s
785
+ }
786
+ }
787
+ return -1
788
+ }
789
+
790
+ /**
791
+ * OnPacketNumberSpaceDiscarded (A.11): the space's keys are gone, so its
792
+ * packets leave flight uncounted, its timers stop, and the backoff resets.
793
+ */
794
+ discardSpace(space: i32): void {
795
+ const sp: QuicSentPackets | null = this.space(space)
796
+ if (sp === null) {
797
+ return
798
+ }
799
+ sp.clear()
800
+ this.ptoCount = 0
801
+ }
802
+
803
+ /**
804
+ * Tops the pacer up to `now` (§7.7): it earns 5/4 of the window per
805
+ * smoothed RTT, and holds at most the initial window, which is the largest
806
+ * burst it allows.
807
+ */
808
+ pacerRefill(now: i64): void {
809
+ if (this.pacerTime < 0 || now < this.pacerTime) {
810
+ this.pacerTime = now
811
+ return
812
+ }
813
+ // A minute is far past what fills the budget, and keeps the product in range.
814
+ let elapsed: i64 = now - this.pacerTime
815
+ if (elapsed > 60000) {
816
+ elapsed = 60000
817
+ }
818
+ const rtt: i64 = quicRecoveryMax(this.smoothedRtt, QUIC_RECOVERY_GRANULARITY)
819
+ const earned: i64 = (elapsed * this.congestionWindow * 5) / (rtt * 4)
820
+ this.pacerBudget = this.pacerBudget + earned
821
+ if (this.pacerBudget > QUIC_RECOVERY_INITIAL_WINDOW) {
822
+ this.pacerBudget = QUIC_RECOVERY_INITIAL_WINDOW
823
+ }
824
+ this.pacerTime = now
825
+ }
826
+
827
+ /**
828
+ * How many milliseconds after `now` the pacer lets a datagram of `size`
829
+ * bytes go: 0 when it may go now, else at least 1. The rate is 5/4 of the
830
+ * window per smoothed RTT (§7.7's N of 1.25).
831
+ */
832
+ pacerDelay(now: i64, size: i32): i64 {
833
+ this.pacerRefill(now)
834
+ const need: i64 = toI64(size) - this.pacerBudget
835
+ if (need <= 0) {
836
+ return 0
837
+ }
838
+ const rtt: i64 = quicRecoveryMax(this.smoothedRtt, QUIC_RECOVERY_GRANULARITY)
839
+ const rate: i64 = quicRecoveryMax(this.congestionWindow * 5, 1)
840
+ const wait: i64 = (need * rtt * 4 + rate - 1) / rate
841
+ return quicRecoveryMax(wait, 1)
842
+ }
843
+
844
+ /** Charges the pacer for a datagram of `size` bytes sent at `now`. */
845
+ onPaced(now: i64, size: i32): void {
846
+ this.pacerRefill(now)
847
+ this.pacerBudget = this.pacerBudget - toI64(size)
848
+ }
849
+ }