@amritk/nish-x86_64-linux 0.29.0 → 0.30.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.
@@ -538,9 +538,22 @@ export class Http1ResponseParser {
538
538
  // --- the response: valid from its HEAD or INFORMATIONAL until the next ----
539
539
  /** The reason phrase, which RFC 9112 §4 says a client ignores; for a log. */
540
540
  reason: string = ""
541
- /** The field names, in lowercase, in the order they arrived; `values` beside them, OWS trimmed. */
541
+ /**
542
+ * The field names, in lowercase, in the order they arrived; `values` beside
543
+ * them, OWS trimmed. These and `reason` are filled only while the parser
544
+ * keeps strings, which it does until `fieldsAsSpans()`; `fields()`,
545
+ * `fieldName`, `fieldValue`, `reasonText` and `header` answer in either mode.
546
+ */
542
547
  names: string[]
543
548
  values: string[]
549
+ /**
550
+ * The response's reason phrase and kept fields, copied: `head[reasonStart ..
551
+ * reasonEnd)` is the reason, and field `i` is `head[spans[4i] ..
552
+ * spans[4i + 1])`, its name in lowercase, then `head[spans[4i + 2] ..
553
+ * spans[4i + 3])`, its value. Both arrays are reused from response to response.
554
+ */
555
+ head: u8[]
556
+ spans: i32[]
544
557
  /** The buffer the last `HTTP1_BODY`'s bytes are in, at `bodyOff`, `bodyLen` long. */
545
558
  body: u8[]
546
559
  /** What was wrong, once `next()` said `HTTP1_ERROR`; for a log. */
@@ -595,6 +608,13 @@ export class Http1ResponseParser {
595
608
  wasUpgrade: boolean = false
596
609
  /** Whether `finish()` said the server closed the connection. */
597
610
  closed: boolean = false
611
+ /** Whether `names`, `values` and `reason` are filled; `fieldsAsSpans()` clears it. */
612
+ keepStrings: boolean = true
613
+ /** Bytes of `head` in use, and the fields recorded in `spans`. */
614
+ headUsed: i32 = 0
615
+ spanCount: i32 = 0
616
+ reasonStart: i32 = 0
617
+ reasonEnd: i32 = 0
598
618
 
599
619
  /**
600
620
  * Each `i32` limit is clamped into 0 to 2^30 and `maxBody` into 0 to 2^53,
@@ -614,6 +634,46 @@ export class Http1ResponseParser {
614
634
  this.body = this.buf
615
635
  this.names = []
616
636
  this.values = []
637
+ this.head = []
638
+ this.spans = []
639
+ }
640
+
641
+ /**
642
+ * Stops filling `names`, `values` and `reason`, which then stay empty: a
643
+ * response's fields are read through `fields()`, `fieldName`, `fieldValue`,
644
+ * `reasonText` and `header`, which make a string only when asked. A parser
645
+ * kept for many responses allocates nothing per response this way.
646
+ */
647
+ fieldsAsSpans(): void {
648
+ this.keepStrings = false
649
+ this.names = []
650
+ this.values = []
651
+ this.reason = ""
652
+ }
653
+
654
+ /**
655
+ * Makes the parser as it was new, for the next connection, keeping its
656
+ * limits, its mode and its buffers: what was fed and not read is dropped,
657
+ * and an error or a close is forgotten.
658
+ */
659
+ reset(): void {
660
+ this.state = HTTP1_CLIENT_S_STATUS_LINE
661
+ this.start = 0
662
+ this.end = 0
663
+ this.scan = 0
664
+ this.lineStart = 0
665
+ this.lineEnd = 0
666
+ this.closed = false
667
+ this.error = 0
668
+ this.errorText = ""
669
+ this.informational = 0
670
+ this.wasHead = false
671
+ this.wasConnect = false
672
+ this.wasUpgrade = false
673
+ http1ClientStartHead(this)
674
+ this.left = toI64(0)
675
+ this.bodyOff = 0
676
+ this.body = this.buf
617
677
  }
618
678
 
619
679
  /**
@@ -704,16 +764,43 @@ export class Http1ResponseParser {
704
764
 
705
765
  /** The value of the first field named `name` (any case), or `null`. */
706
766
  header(name: string): string | null {
707
- const names: string[] = this.names
708
- const n: i32 = toI32(name.length)
709
- for (let i: i32 = 0; i < toI32(names.length) && i < toI32(this.values.length); i += 1) {
710
- if (http1SameWord(name, 0, n, names[i])) {
711
- return this.values[i]
767
+ for (let i: i32 = 0; i < this.spanCount; i += 1) {
768
+ if (http1ClientHeadSameWord(this.head, this.spans[i * 4], this.spans[i * 4 + 1], name)) {
769
+ return http1Text(this.head, this.spans[i * 4 + 2], this.spans[i * 4 + 3], false)
712
770
  }
713
771
  }
714
772
  return null
715
773
  }
716
774
 
775
+ /** How many fields the response last read has; trailers are not kept. */
776
+ fields(): i32 {
777
+ return this.spanCount
778
+ }
779
+
780
+ /** Field `i`'s name, in lowercase, as a fresh string; "" past the last field. */
781
+ fieldName(i: i32): string {
782
+ if (i < 0 || i >= this.spanCount) {
783
+ return ""
784
+ }
785
+ return http1Text(this.head, this.spans[i * 4], this.spans[i * 4 + 1], false)
786
+ }
787
+
788
+ /** Field `i`'s value, OWS trimmed, as a fresh string; "" past the last field. */
789
+ fieldValue(i: i32): string {
790
+ if (i < 0 || i >= this.spanCount) {
791
+ return ""
792
+ }
793
+ return http1Text(this.head, this.spans[i * 4 + 2], this.spans[i * 4 + 3], false)
794
+ }
795
+
796
+ /** The reason phrase, as a fresh string. */
797
+ reasonText(): string {
798
+ if (this.reasonEnd <= this.reasonStart) {
799
+ return ""
800
+ }
801
+ return http1Text(this.head, this.reasonStart, this.reasonEnd, false)
802
+ }
803
+
717
804
  /** How many bytes were fed and not consumed yet. */
718
805
  buffered(): i32 {
719
806
  return this.end - this.start
@@ -777,10 +864,17 @@ const http1ClientFail = (p: Http1ResponseParser, code: i32, text: string): i32 =
777
864
  /** Clears the per-response fields for the status line just taken. */
778
865
  const http1ClientStartHead = (p: Http1ResponseParser): void => {
779
866
  p.status = 0
780
- p.reason = ""
781
867
  p.minor = 1
782
- p.names = []
783
- p.values = []
868
+ // Fresh arrays, not cleared ones: a caller may hold the last response's.
869
+ if (p.keepStrings) {
870
+ p.reason = ""
871
+ p.names = []
872
+ p.values = []
873
+ }
874
+ p.headUsed = 0
875
+ p.spanCount = 0
876
+ p.reasonStart = 0
877
+ p.reasonEnd = 0
784
878
  p.contentLength = toI64(-1)
785
879
  p.chunked = false
786
880
  p.persistent = false
@@ -872,7 +966,13 @@ const http1ClientStatusLine = (p: Http1ResponseParser): i32 => {
872
966
  }
873
967
  p.minor = buf[a + 7] === 48 ? 0 : 1
874
968
  p.status = status
875
- p.reason = b - a > 13 ? http1Text(buf, a + 13, b, false) : ""
969
+ if (b - a > 13) {
970
+ p.reasonStart = http1ClientKeep(p, buf, a + 13, b, false)
971
+ p.reasonEnd = p.headUsed
972
+ if (p.keepStrings) {
973
+ p.reason = http1Text(buf, a + 13, b, false)
974
+ }
975
+ }
876
976
  p.state = HTTP1_CLIENT_S_FIELDS
877
977
  return HTTP1_NEED_MORE
878
978
  }
@@ -925,8 +1025,13 @@ const http1ClientFieldLine = (p: Http1ResponseParser, keep: boolean): i32 => {
925
1025
  ) {
926
1026
  valueEnd -= 1
927
1027
  }
928
- p.names.push(http1Text(buf, a, nameEnd, true))
929
- p.values.push(http1Text(buf, valueStart, valueEnd, false))
1028
+ const name: i32 = http1ClientKeep(p, buf, a, nameEnd, true)
1029
+ const value: i32 = http1ClientKeep(p, buf, valueStart, valueEnd, false)
1030
+ http1ClientSpan(p, name, value, value + (valueEnd - valueStart))
1031
+ if (p.keepStrings) {
1032
+ p.names.push(http1Text(buf, a, nameEnd, true))
1033
+ p.values.push(http1Text(buf, valueStart, valueEnd, false))
1034
+ }
930
1035
  }
931
1036
  return HTTP1_NEED_MORE
932
1037
  }
@@ -940,20 +1045,20 @@ const http1ClientFieldLine = (p: Http1ResponseParser, keep: boolean): i32 => {
940
1045
  * value past 2^53 is saturated to 2^53 + 1: over any `maxBody`. Saturated
941
1046
  * values cannot be told apart, so one seen with any other value is -2.
942
1047
  */
943
- const http1ClientLengthList = (value: string, seen: i64): i64 => {
944
- const n: i32 = toI32(value.length)
1048
+ const http1ClientLengthList = (head: u8[], from: i32, to: i32, seen: i64): i64 => {
1049
+ const n: i32 = to < toI32(head.length) ? to : toI32(head.length)
945
1050
  let found: i64 = seen
946
- let start: i32 = 0
947
- for (let k: i32 = 0; k <= n; k += 1) {
948
- if (k === n || http1ClientIsComma(value, k)) {
949
- const a: i32 = http1ClientTrimStart(value, start, k)
950
- const b: i32 = http1ClientTrimEnd(value, a, k)
1051
+ let start: i32 = from
1052
+ for (let k: i32 = from; k <= n; k += 1) {
1053
+ if (k === n || (k >= 0 && k < toI32(head.length) && head[k] === 44)) {
1054
+ const a: i32 = http1ClientHeadTrimStart(head, start, k)
1055
+ const b: i32 = http1ClientHeadTrimEnd(head, a, k)
951
1056
  if (a === b) {
952
1057
  return toI64(-1)
953
1058
  }
954
1059
  let v: i64 = toI64(0)
955
- for (let j: i32 = a; j >= 0 && j < b && j < n; j += 1) {
956
- const c: i32 = toI32(value.charCodeAt(j))
1060
+ for (let j: i32 = a; j >= 0 && j < b && j < toI32(head.length); j += 1) {
1061
+ const c: i32 = toI32(head[j])
957
1062
  if (c < 48 || c > 57) {
958
1063
  return toI64(-1)
959
1064
  }
@@ -982,20 +1087,19 @@ const http1ClientTally = (p: Http1ResponseParser, name: string, word: string): i
982
1087
  let elements: i32 = 0
983
1088
  let matches: i32 = 0
984
1089
  let last: boolean = false
985
- const names: string[] = p.names
986
- const values: string[] = p.values
987
- for (let i: i32 = 0; i < toI32(names.length) && i < toI32(values.length); i += 1) {
988
- if (names[i] === name) {
989
- const value: string = values[i]
990
- const n: i32 = toI32(value.length)
991
- let start: i32 = 0
992
- for (let k: i32 = 0; k <= n; k += 1) {
993
- if (k === n || http1ClientIsComma(value, k)) {
994
- const a: i32 = http1ClientTrimStart(value, start, k)
995
- const b: i32 = http1ClientTrimEnd(value, a, k)
1090
+ const head: u8[] = p.head
1091
+ for (let i: i32 = 0; i < p.spanCount; i += 1) {
1092
+ if (http1ClientHeadIs(head, p.spans[i * 4], p.spans[i * 4 + 1], name)) {
1093
+ const from: i32 = p.spans[i * 4 + 2]
1094
+ const n: i32 = p.spans[i * 4 + 3] < toI32(head.length) ? p.spans[i * 4 + 3] : toI32(head.length)
1095
+ let start: i32 = from
1096
+ for (let k: i32 = from; k <= n; k += 1) {
1097
+ if (k === n || (k >= 0 && k < toI32(head.length) && head[k] === 44)) {
1098
+ const a: i32 = http1ClientHeadTrimStart(head, start, k)
1099
+ const b: i32 = http1ClientHeadTrimEnd(head, a, k)
996
1100
  if (b > a) {
997
1101
  elements += 1
998
- last = http1SameWord(value, a, b, word)
1102
+ last = http1ClientHeadSameWord(head, a, b, word)
999
1103
  if (last) {
1000
1104
  matches += 1
1001
1105
  }
@@ -1010,15 +1114,139 @@ const http1ClientTally = (p: Http1ResponseParser, name: string, word: string): i
1010
1114
 
1011
1115
  /** Whether a `Connection` field of the response lists `word`. */
1012
1116
  const http1ClientConnectionHas = (p: Http1ResponseParser, word: string): boolean => {
1013
- const names: string[] = p.names
1014
- for (let i: i32 = 0; i < toI32(names.length) && i < toI32(p.values.length); i += 1) {
1015
- if (names[i] === "connection" && http1ClientListHas(p.values[i], word)) {
1117
+ for (let i: i32 = 0; i < p.spanCount; i += 1) {
1118
+ if (
1119
+ http1ClientHeadIs(p.head, p.spans[i * 4], p.spans[i * 4 + 1], "connection") &&
1120
+ http1ClientHeadListHas(p.head, p.spans[i * 4 + 2], p.spans[i * 4 + 3], word)
1121
+ ) {
1016
1122
  return true
1017
1123
  }
1018
1124
  }
1019
1125
  return false
1020
1126
  }
1021
1127
 
1128
+ /**
1129
+ * Copies `buf[a .. b)` to the end of `p.head`, lowercased when `lower`, and
1130
+ * answers where the copy starts. `head` grows by doubling and is kept, so a
1131
+ * parser reused for many responses stops growing at the largest head.
1132
+ */
1133
+ const http1ClientKeep = (p: Http1ResponseParser, buf: u8[], a: i32, b: i32, lower: boolean): i32 => {
1134
+ const n: i32 = b - a
1135
+ const at: i32 = p.headUsed
1136
+ if (n > toI32(p.head.length) - at) {
1137
+ let size: i32 = toI32(p.head.length) < 256 ? 256 : toI32(p.head.length)
1138
+ while (size - at < n) {
1139
+ if (size > 1073741823) {
1140
+ panic("Http1ResponseParser: more than 2^30 bytes of head")
1141
+ }
1142
+ size = size * 2
1143
+ }
1144
+ const grown: u8[] = new Array<u8>(size)
1145
+ copyBytes(grown, 0, p.head, 0, at)
1146
+ p.head = grown
1147
+ }
1148
+ copyBytes(p.head, at, buf, a, n)
1149
+ if (lower) {
1150
+ const head: u8[] = p.head
1151
+ for (let k: i32 = at; k >= 0 && k < at + n && k < toI32(head.length); k += 1) {
1152
+ const c: i32 = toI32(head[k])
1153
+ if (c >= 65 && c <= 90) {
1154
+ head[k] = toU8(c + 32)
1155
+ }
1156
+ }
1157
+ }
1158
+ p.headUsed = at + n
1159
+ return at
1160
+ }
1161
+
1162
+ /** Records the field whose name starts at `name` and whose value is `head[value .. valueEnd)`. */
1163
+ const http1ClientSpan = (p: Http1ResponseParser, name: i32, value: i32, valueEnd: i32): void => {
1164
+ const at: i32 = p.spanCount * 4
1165
+ if (at + 4 > toI32(p.spans.length)) {
1166
+ p.spans.push(name)
1167
+ p.spans.push(value)
1168
+ p.spans.push(value)
1169
+ p.spans.push(valueEnd)
1170
+ } else {
1171
+ p.spans[at] = name
1172
+ p.spans[at + 1] = value
1173
+ p.spans[at + 2] = value
1174
+ p.spans[at + 3] = valueEnd
1175
+ }
1176
+ p.spanCount += 1
1177
+ }
1178
+
1179
+ /** Whether `head[a .. b)` is `word`, byte for byte. */
1180
+ const http1ClientHeadIs = (head: u8[], a: i32, b: i32, word: string): boolean => {
1181
+ const n: i32 = toI32(word.length)
1182
+ if (b - a !== n || a < 0 || b > toI32(head.length)) {
1183
+ return false
1184
+ }
1185
+ for (let k: i32 = 0; k < n; k += 1) {
1186
+ if (toI32(head[a + k]) !== toI32(word.charCodeAt(k))) {
1187
+ return false
1188
+ }
1189
+ }
1190
+ return true
1191
+ }
1192
+
1193
+ /** Whether `head[a .. b)` is `word` in any case, ASCII letters folded. */
1194
+ const http1ClientHeadSameWord = (head: u8[], a: i32, b: i32, word: string): boolean => {
1195
+ const n: i32 = toI32(word.length)
1196
+ if (b - a !== n || a < 0 || b > toI32(head.length)) {
1197
+ return false
1198
+ }
1199
+ for (let k: i32 = 0; k < n; k += 1) {
1200
+ let x: i32 = toI32(head[a + k])
1201
+ let y: i32 = toI32(word.charCodeAt(k))
1202
+ if (x >= 65 && x <= 90) {
1203
+ x += 32
1204
+ }
1205
+ if (y >= 65 && y <= 90) {
1206
+ y += 32
1207
+ }
1208
+ if (x !== y) {
1209
+ return false
1210
+ }
1211
+ }
1212
+ return true
1213
+ }
1214
+
1215
+ /** Where `head[a .. b)` starts once its leading OWS is dropped. */
1216
+ const http1ClientHeadTrimStart = (head: u8[], a: i32, b: i32): i32 => {
1217
+ let i: i32 = a
1218
+ while (i >= 0 && i < b && i < toI32(head.length) && http1IsOws(toI32(head[i]))) {
1219
+ i += 1
1220
+ }
1221
+ return i
1222
+ }
1223
+
1224
+ /** Where `head[a .. b)` ends once its trailing OWS is dropped. */
1225
+ const http1ClientHeadTrimEnd = (head: u8[], a: i32, b: i32): i32 => {
1226
+ let i: i32 = b
1227
+ while (i > a && i > 0 && i <= toI32(head.length) && http1IsOws(toI32(head[i - 1]))) {
1228
+ i -= 1
1229
+ }
1230
+ return i
1231
+ }
1232
+
1233
+ /** Whether the comma-separated list in `head[from .. to)` holds `word` in any case, each element trimmed of OWS. */
1234
+ const http1ClientHeadListHas = (head: u8[], from: i32, to: i32, word: string): boolean => {
1235
+ const n: i32 = to < toI32(head.length) ? to : toI32(head.length)
1236
+ let start: i32 = from
1237
+ for (let k: i32 = from; k <= n; k += 1) {
1238
+ if (k === n || (k >= 0 && k < toI32(head.length) && head[k] === 44)) {
1239
+ const a: i32 = http1ClientHeadTrimStart(head, start, k)
1240
+ const b: i32 = http1ClientHeadTrimEnd(head, a, k)
1241
+ if (http1ClientHeadSameWord(head, a, b, word)) {
1242
+ return true
1243
+ }
1244
+ start = k + 1
1245
+ }
1246
+ }
1247
+ return false
1248
+ }
1249
+
1022
1250
  /**
1023
1251
  * A header section has ended: answer `HTTP1_INFORMATIONAL` for a 1xx, or
1024
1252
  * decide the final response's framing and the connection's fate (RFC 9112
@@ -1052,12 +1280,13 @@ const http1ClientEndOfHead = (p: Http1ResponseParser): i32 => {
1052
1280
  }
1053
1281
  let codings: boolean = false
1054
1282
  let length: i64 = toI64(-1)
1055
- const names: string[] = p.names
1056
- for (let i: i32 = 0; i < toI32(names.length) && i < toI32(p.values.length); i += 1) {
1057
- if (names[i] === "transfer-encoding") {
1283
+ for (let i: i32 = 0; i < p.spanCount; i += 1) {
1284
+ const a: i32 = p.spans[i * 4]
1285
+ const b: i32 = p.spans[i * 4 + 1]
1286
+ if (http1ClientHeadIs(p.head, a, b, "transfer-encoding")) {
1058
1287
  codings = true
1059
- } else if (names[i] === "content-length") {
1060
- length = http1ClientLengthList(p.values[i], length)
1288
+ } else if (http1ClientHeadIs(p.head, a, b, "content-length")) {
1289
+ length = http1ClientLengthList(p.head, p.spans[i * 4 + 2], p.spans[i * 4 + 3], length)
1061
1290
  if (length === toI64(-2)) {
1062
1291
  return http1ClientFail(
1063
1292
  p,
@@ -70,7 +70,11 @@
70
70
  * most once a second; a 1xx carries none. The pool's slots share one, so a
71
71
  * busy server formats a second once, not once a slot. A program that sets its
72
72
  * own `date` field sends that one instead, so a response has one `Date`.
73
- * `date.pin(unixMs)` fixes it, for a test that compares bytes.
73
+ * `Http1Server` reads the clock once per serve call — from `readable` or
74
+ * `writable` to the `flush` that ends it — and every response written in
75
+ * between carries that reading; one written outside a serve call, an answer
76
+ * given later, reads the clock then. `date.pin(unixMs)` fixes it, for a test
77
+ * that compares bytes.
74
78
  *
75
79
  * **Refusals.** Every refusal of `nish/net/http1`'s parser — the smuggling
76
80
  * shapes, a `Content-Length` with a `Transfer-Encoding`, two that disagree,
@@ -275,7 +279,8 @@ const http1ServerCopy = (out: u8[], at: i32, src: u8[], off: i32, n: i32): i32 =
275
279
  * it calls, `httpDateWrite`, stays in `nish/net/http-fields`, which compiles
276
280
  * for wasm32 (the split `nish/crypto/x509` and `x509-random` make). `pin`
277
281
  * replaces the clock with one instant, which is how a test asserts a response
278
- * byte for byte.
282
+ * byte for byte. `hold` reads the clock once for a run of responses, a serve
283
+ * call's, until `unhold`; `pin` wins over it.
279
284
  */
280
285
  export class HttpDate {
281
286
  /** The IMF-fixdate of `second`: what `current` answers, made once. */
@@ -284,9 +289,14 @@ export class HttpDate {
284
289
  second: i64 = -1
285
290
  /** The instant `pin` fixed, in milliseconds since 1970, while `pinned`. */
286
291
  pinnedMs: i64 = 0
292
+ /** The clock reading `hold` took, in milliseconds since 1970, while `held`. */
293
+ heldMs: i64 = 0
287
294
  /** How many times `bytes` has been rewritten. */
288
295
  rewrites: i64 = 0
296
+ /** How many times the wall clock has been read. */
297
+ reads: i64 = 0
289
298
  pinned: boolean = false
299
+ held: boolean = false
290
300
 
291
301
  /** A date on the wall clock, with nothing formatted yet. */
292
302
  constructor() {
@@ -301,7 +311,10 @@ export class HttpDate {
301
311
  * or past 9999 answers the nearer of the two ends.
302
312
  */
303
313
  current(): u8[] {
304
- const ms: i64 = this.pinned ? this.pinnedMs : toI64(Date.now())
314
+ let ms: i64 = this.pinnedMs
315
+ if (!this.pinned) {
316
+ ms = this.held ? this.heldMs : this.now()
317
+ }
305
318
  let second: i64 = ms / 1000
306
319
  if (ms < 0) {
307
320
  second = 0
@@ -335,6 +348,34 @@ export class HttpDate {
335
348
  this.pinned = false
336
349
  }
337
350
 
351
+ /**
352
+ * Reads the wall clock once and makes `current` answer that reading until
353
+ * `unhold`, so the responses of one serve call cost one read. A pinned date
354
+ * reads nothing. A second `hold` takes a fresh reading.
355
+ */
356
+ hold(): void {
357
+ if (!this.pinned) {
358
+ this.heldMs = this.now()
359
+ }
360
+ this.held = true
361
+ }
362
+
363
+ /** Puts `current` back on a clock read per call, for a response written outside a serve call. */
364
+ unhold(): void {
365
+ this.held = false
366
+ }
367
+
368
+ /** The wall clock, in milliseconds since 1970. */
369
+ now(): i64 {
370
+ this.reads += 1
371
+ return toI64(Date.now())
372
+ }
373
+
374
+ /** How many times the wall clock has been read: once per `hold`, and once per `current` outside one. */
375
+ clockReads(): i64 {
376
+ return this.reads
377
+ }
378
+
338
379
  /** How many times the bytes have been rewritten: one per second that `current` was asked for, at most. */
339
380
  formats(): i64 {
340
381
  return this.rewrites
@@ -1573,7 +1614,11 @@ export class Http1Server {
1573
1614
  * accepted into, and slot 0's once `connection` has asked for it.
1574
1615
  */
1575
1616
  connections: Http1Connection[]
1576
- /** The `Date` every slot's responses carry, formatted once a second for all of them. `date.pin` fixes its clock. */
1617
+ /**
1618
+ * The `Date` every slot's responses carry, formatted once a second for all
1619
+ * of them and read from the clock once per serve call: `readable` and
1620
+ * `writable` hold a reading and `flush` lets it go. `date.pin` fixes its clock.
1621
+ */
1577
1622
  date: HttpDate
1578
1623
  /**
1579
1624
  * Each slot's descriptor, or -1 while it is free, for the slots accepted
@@ -1729,12 +1774,15 @@ export class Http1Server {
1729
1774
 
1730
1775
  /**
1731
1776
  * The socket of `slot` is readable: reads it while the connection has room
1732
- * and the socket has more, then sends. Answers the interest.
1777
+ * and the socket has more, then sends. Answers the interest. It begins a
1778
+ * serve call, which the program's `flush` of the slot ends: the responses
1779
+ * written in between carry one clock reading as their `Date`.
1733
1780
  */
1734
1781
  readable(slot: i32): i32 {
1735
1782
  if (!this.holds(slot)) {
1736
1783
  return H1_DONE
1737
1784
  }
1785
+ this.date.hold()
1738
1786
  const conn: Http1Connection = this.connections[slot]
1739
1787
  const fd: i32 = this.fds[slot]
1740
1788
  let moved: boolean = false
@@ -1768,12 +1816,16 @@ export class Http1Server {
1768
1816
  conn.lastActive = now
1769
1817
  conn.headArrived(now)
1770
1818
  }
1771
- return this.flush(slot)
1819
+ return this.settle(slot)
1772
1820
  }
1773
1821
 
1774
- /** The socket of `slot` is writable. Answers the interest. */
1822
+ /** The socket of `slot` is writable: sends, and begins a serve call as `readable` does. Answers the interest. */
1775
1823
  writable(slot: i32): i32 {
1776
- return this.flush(slot)
1824
+ if (!this.holds(slot)) {
1825
+ return H1_DONE
1826
+ }
1827
+ this.date.hold()
1828
+ return this.settle(slot)
1777
1829
  }
1778
1830
 
1779
1831
  /**
@@ -1797,9 +1849,16 @@ export class Http1Server {
1797
1849
  /**
1798
1850
  * Sends what `slot`'s connection holds until the socket would block, and
1799
1851
  * answers the interest: `H1_DONE` once the connection is over and sent,
1800
- * otherwise read while it has room and write while it holds bytes.
1852
+ * otherwise read while it has room and write while it holds bytes. It ends
1853
+ * the serve call, so a response written after it reads the clock anew.
1801
1854
  */
1802
1855
  flush(slot: i32): i32 {
1856
+ this.date.unhold()
1857
+ return this.settle(slot)
1858
+ }
1859
+
1860
+ /** `flush` within a serve call: sends, answers the interest, and keeps the clock reading. */
1861
+ settle(slot: i32): i32 {
1803
1862
  if (!this.holds(slot)) {
1804
1863
  return H1_DONE
1805
1864
  }