@johnhenry/wsh 0.2.0 → 0.3.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/package.json +1 -1
- package/src/transport-ws.mjs +48 -1
- package/src/transport.mjs +13 -0
package/package.json
CHANGED
package/src/transport-ws.mjs
CHANGED
|
@@ -206,6 +206,27 @@ export class WebSocketTransport extends WshTransport {
|
|
|
206
206
|
/** Tracks whether we initiated the close. */
|
|
207
207
|
#closedByUs = false;
|
|
208
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Queue of raw inbound messages awaiting dispatch, plus a drain-in-progress
|
|
211
|
+
* flag. Needed because some `WebSocket` implementations (notably Node's
|
|
212
|
+
* `ws` package parsing several frames out of one TCP read) can fire
|
|
213
|
+
* multiple synchronous `message` events within a single JS task, all
|
|
214
|
+
* before any microtask gets to run. If we dispatched each message inline,
|
|
215
|
+
* a handler that resolves a pending waiter (e.g. SERVER_HELLO resolving
|
|
216
|
+
* the "wait for SERVER_HELLO or CHALLENGE" promise) wouldn't get a chance
|
|
217
|
+
* to run its `await`'d continuation — which registers the *next* waiter
|
|
218
|
+
* (e.g. for CHALLENGE) — before the next queued message arrives. That
|
|
219
|
+
* continuation is a microtask; a same-task, no-yield dispatch loop starves
|
|
220
|
+
* it, so the next message's waiter isn't registered yet and gets dropped.
|
|
221
|
+
* Draining the queue with an `await Promise.resolve()` between each
|
|
222
|
+
* message yields to the microtask queue after every dispatch, and since
|
|
223
|
+
* microtasks run FIFO, any continuation queued by handling message N
|
|
224
|
+
* (which runs first) completes before message N+1 is dispatched.
|
|
225
|
+
* @type {ArrayBuffer[]}
|
|
226
|
+
*/
|
|
227
|
+
#messageQueue = [];
|
|
228
|
+
#draining = false;
|
|
229
|
+
|
|
209
230
|
// ── Lifecycle ──────────────────────────────────────────────────────
|
|
210
231
|
|
|
211
232
|
/** @override */
|
|
@@ -215,6 +236,8 @@ export class WebSocketTransport extends WshTransport {
|
|
|
215
236
|
this.#nextLocalId = 1;
|
|
216
237
|
this.#openResolvers.clear();
|
|
217
238
|
this.#closedByUs = false;
|
|
239
|
+
this.#messageQueue.length = 0;
|
|
240
|
+
this.#draining = false;
|
|
218
241
|
|
|
219
242
|
return new Promise((resolve, reject) => {
|
|
220
243
|
// Normalize URL scheme: wsh:// → wss://, http:// → ws://.
|
|
@@ -242,7 +265,8 @@ export class WebSocketTransport extends WshTransport {
|
|
|
242
265
|
});
|
|
243
266
|
|
|
244
267
|
ws.addEventListener('message', (ev) => {
|
|
245
|
-
this.#
|
|
268
|
+
this.#messageQueue.push(ev.data);
|
|
269
|
+
this.#drainMessageQueue();
|
|
246
270
|
});
|
|
247
271
|
});
|
|
248
272
|
}
|
|
@@ -295,6 +319,29 @@ export class WebSocketTransport extends WshTransport {
|
|
|
295
319
|
|
|
296
320
|
// ── Inbound message dispatch ───────────────────────────────────────
|
|
297
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Drain `#messageQueue` one message at a time, yielding to the microtask
|
|
324
|
+
* queue between each dispatch. See the `#messageQueue` field doc for why:
|
|
325
|
+
* this is what lets a waiter registered by an earlier message's `await`
|
|
326
|
+
* continuation (e.g. registering the CHALLENGE waiter after SERVER_HELLO
|
|
327
|
+
* resolves) actually be in place before the next queued message arrives,
|
|
328
|
+
* even when the underlying WebSocket implementation delivered several
|
|
329
|
+
* messages synchronously in one task.
|
|
330
|
+
*/
|
|
331
|
+
async #drainMessageQueue() {
|
|
332
|
+
if (this.#draining) return;
|
|
333
|
+
this.#draining = true;
|
|
334
|
+
try {
|
|
335
|
+
while (this.#messageQueue.length > 0) {
|
|
336
|
+
const raw = this.#messageQueue.shift();
|
|
337
|
+
this.#handleMessage(raw);
|
|
338
|
+
await Promise.resolve();
|
|
339
|
+
}
|
|
340
|
+
} finally {
|
|
341
|
+
this.#draining = false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
298
345
|
/**
|
|
299
346
|
* Handle a raw WebSocket message (ArrayBuffer).
|
|
300
347
|
* @param {ArrayBuffer} raw
|
package/src/transport.mjs
CHANGED
|
@@ -281,6 +281,19 @@ export class WebTransportTransport extends WshTransport {
|
|
|
281
281
|
const messages = this.#decoder.feed(value);
|
|
282
282
|
for (const msg of messages) {
|
|
283
283
|
this._emitControl(msg);
|
|
284
|
+
// Yield to the microtask queue between messages. A QUIC stream
|
|
285
|
+
// has no per-message framing at the transport layer, so a single
|
|
286
|
+
// read() can return bytes for several messages at once (e.g.
|
|
287
|
+
// SERVER_HELLO immediately followed by CHALLENGE). Emitting them
|
|
288
|
+
// in a tight synchronous loop starves any `await`'d continuation
|
|
289
|
+
// triggered by handling an earlier message (e.g. registering the
|
|
290
|
+
// CHALLENGE waiter after SERVER_HELLO resolves) — that
|
|
291
|
+
// continuation is a microtask, and without a yield here it never
|
|
292
|
+
// gets to run before the next message is dispatched. Since
|
|
293
|
+
// microtasks run FIFO, a continuation queued while handling msg
|
|
294
|
+
// N always completes before this yield's own continuation
|
|
295
|
+
// resumes to dispatch msg N+1.
|
|
296
|
+
await Promise.resolve();
|
|
284
297
|
}
|
|
285
298
|
}
|
|
286
299
|
} catch (err) {
|