@johnhenry/wsh 0.16.0 → 0.16.1
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/package.json +1 -1
- package/src/session.mjs +59 -7
package/package.json
CHANGED
package/src/session.mjs
CHANGED
|
@@ -20,6 +20,17 @@ const STATE_OPENING = 'opening';
|
|
|
20
20
|
const STATE_ACTIVE = 'active';
|
|
21
21
|
const STATE_CLOSED = 'closed';
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Stream-mode data and control streams are independently-multiplexed
|
|
25
|
+
* transport streams (QUIC/QMux) with no guaranteed relative delivery
|
|
26
|
+
* order (wsh #24). When the data stream reaches EOF, the server-sent
|
|
27
|
+
* `CLOSE` control message -- not the data-stream FIN -- is the
|
|
28
|
+
* authoritative close signal, so `_pumpDataStream()` waits up to this
|
|
29
|
+
* long for `CLOSE` to arrive before falling back to closing on data-EOF
|
|
30
|
+
* alone (a safety net for servers that never send `CLOSE`).
|
|
31
|
+
*/
|
|
32
|
+
const DATA_EOF_CLOSE_GRACE_MS = 300;
|
|
33
|
+
|
|
23
34
|
// ── Text encoding ─────────────────────────────────────────────────────
|
|
24
35
|
|
|
25
36
|
const textEncoder = new TextEncoder();
|
|
@@ -133,6 +144,16 @@ export class WshSession {
|
|
|
133
144
|
/** @type {number|null} Exit code received from the server. */
|
|
134
145
|
#exitCode = null;
|
|
135
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Pending fallback timer scheduled when the data stream hits EOF
|
|
149
|
+
* before a `CLOSE` control message has arrived (wsh #24). Cleared
|
|
150
|
+
* as soon as `CLOSE` arrives (or the session is otherwise closed) so
|
|
151
|
+
* a prompt `CLOSE` short-circuits the wait instead of idling out the
|
|
152
|
+
* full grace period.
|
|
153
|
+
* @type {ReturnType<typeof setTimeout>|null}
|
|
154
|
+
*/
|
|
155
|
+
#closeGraceTimer = null;
|
|
156
|
+
|
|
136
157
|
// ── Callbacks ───────────────────────────────────────────────────────
|
|
137
158
|
|
|
138
159
|
/**
|
|
@@ -580,6 +601,7 @@ export class WshSession {
|
|
|
580
601
|
// Optimistically mark closed so no further writes are accepted.
|
|
581
602
|
this.#state = STATE_CLOSED;
|
|
582
603
|
this.#abort.abort();
|
|
604
|
+
this.#clearCloseGraceTimer();
|
|
583
605
|
|
|
584
606
|
// Send CLOSE to the server (best-effort).
|
|
585
607
|
try {
|
|
@@ -697,7 +719,11 @@ export class WshSession {
|
|
|
697
719
|
}
|
|
698
720
|
|
|
699
721
|
case MSG.CLOSE: {
|
|
700
|
-
// Server-initiated close.
|
|
722
|
+
// Server-initiated close. This is the authoritative close signal
|
|
723
|
+
// for stream-mode sessions (wsh #24) -- it always short-circuits
|
|
724
|
+
// any pending data-EOF grace timer, regardless of which arrived
|
|
725
|
+
// first.
|
|
726
|
+
this.#clearCloseGraceTimer();
|
|
701
727
|
if (this.#state !== STATE_CLOSED) {
|
|
702
728
|
this.#state = STATE_CLOSED;
|
|
703
729
|
this.#abort.abort();
|
|
@@ -879,17 +905,43 @@ export class WshSession {
|
|
|
879
905
|
reader.releaseLock();
|
|
880
906
|
}
|
|
881
907
|
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
908
|
+
// The data stream ended, but its FIN carries no ordering guarantee
|
|
909
|
+
// relative to the control stream (wsh #24): a server that closes the
|
|
910
|
+
// data stream and sends EXIT/CLOSE around the same time can have
|
|
911
|
+
// this EOF arrive before those control messages. Closing the session
|
|
912
|
+
// here unconditionally would resolve onClose without onExit ever
|
|
913
|
+
// having fired. Instead, give the (expected) CLOSE control message a
|
|
914
|
+
// bounded grace period to arrive -- its handler clears this timer and
|
|
915
|
+
// performs the real transition -- and only self-close on timeout as a
|
|
916
|
+
// fallback for servers that never send CLOSE after ending the data
|
|
917
|
+
// stream.
|
|
918
|
+
if (this.#state !== STATE_CLOSED && this.#closeGraceTimer === null) {
|
|
919
|
+
this.#closeGraceTimer = setTimeout(() => {
|
|
920
|
+
this.#closeGraceTimer = null;
|
|
921
|
+
if (this.#state !== STATE_CLOSED) {
|
|
922
|
+
this.#state = STATE_CLOSED;
|
|
923
|
+
this.#releaseStreams();
|
|
924
|
+
this.#emitClose();
|
|
925
|
+
}
|
|
926
|
+
}, DATA_EOF_CLOSE_GRACE_MS);
|
|
927
|
+
this.#closeGraceTimer?.unref?.();
|
|
888
928
|
}
|
|
889
929
|
}
|
|
890
930
|
|
|
891
931
|
// ── Private helpers ─────────────────────────────────────────────────
|
|
892
932
|
|
|
933
|
+
/**
|
|
934
|
+
* Cancel any pending data-EOF close-grace timer (wsh #24). Safe to call
|
|
935
|
+
* whether or not a timer is currently scheduled.
|
|
936
|
+
* @private
|
|
937
|
+
*/
|
|
938
|
+
#clearCloseGraceTimer() {
|
|
939
|
+
if (this.#closeGraceTimer !== null) {
|
|
940
|
+
clearTimeout(this.#closeGraceTimer);
|
|
941
|
+
this.#closeGraceTimer = null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
893
945
|
/**
|
|
894
946
|
* Release stream resources without sending a CLOSE message.
|
|
895
947
|
* @private
|