@exhumer/signalr-client 1.0.2 → 2.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/dist/index.mjs ADDED
@@ -0,0 +1,2912 @@
1
+ import { PassThrough, Readable } from "node:stream";
2
+ import { WebSocket, fetch, getGlobalDispatcher, pipeline, request, stream } from "undici";
3
+ import { CookieAgent, CookieAgent as CookieAgent$1, cookie } from "@exhumer/undici-cookie-agent";
4
+ import { CookieJar, CookieJar as CookieJar$1 } from "tough-cookie";
5
+ import { Encoder, decode } from "@msgpack/msgpack";
6
+ import * as net from "node:net";
7
+ import * as tls from "node:tls";
8
+ import * as crypto from "node:crypto";
9
+ import { EventEmitter } from "node:events";
10
+ //#region src/http-client.ts
11
+ /**
12
+ * http-client.ts
13
+ *
14
+ * Five HTTP client implementations backed by undici, each wrapping a different
15
+ * undici primitive. All five satisfy the `IHttpClient` contract so any one
16
+ * can be used wherever an HTTP client is required (negotiate, SSE, polling…).
17
+ *
18
+ * ┌────────────────────┬───────────────────────────────────────────────────┐
19
+ * │ Class │ undici primitive │
20
+ * ├────────────────────┼───────────────────────────────────────────────────┤
21
+ * │ RequestHttpClient │ undici.request() │
22
+ * │ FetchHttpClient │ undici.fetch() (WHATWG-compatible) │
23
+ * │ StreamHttpClient │ undici.stream() (factory-based streaming) │
24
+ * │ PipelineHttpClient │ undici.pipeline() (Duplex pipe chain) │
25
+ * │ DispatchHttpClient │ Dispatcher#dispatch (low-level handler interface) ← DEFAULT │
26
+ * └────────────────────┴───────────────────────────────────────────────────┘
27
+ *
28
+ * Dispatcher (session-level):
29
+ * Every client accepts an optional `dispatcher` option. Pass an undici
30
+ * `Agent`, `Pool`, `Client`, `ProxyAgent`, etc. to customise connection
31
+ * pooling, proxy routing, TLS, or any other transport-level behaviour for
32
+ * the entire client session. When omitted, undici's process-global
33
+ * default agent is used (`getGlobalDispatcher()`).
34
+ *
35
+ * HttpClient:
36
+ * An alias for `DispatchHttpClient` so existing code that imported
37
+ * `HttpClient` continues to work without changes.
38
+ */
39
+ /**
40
+ * Serialise the `body` option to a `Buffer` (or `null` for bodyless requests).
41
+ * Returns the buffer AND the inferred Content-Type so callers can set the
42
+ * header automatically.
43
+ */
44
+ function prepareBody(body) {
45
+ if (body == null) return {
46
+ buf: null,
47
+ inferredContentType: null
48
+ };
49
+ if (body instanceof Uint8Array) return {
50
+ buf: Buffer.from(body.buffer, body.byteOffset, body.byteLength),
51
+ inferredContentType: null
52
+ };
53
+ if (typeof body === "string") return {
54
+ buf: Buffer.from(body, "utf8"),
55
+ inferredContentType: null
56
+ };
57
+ return {
58
+ buf: Buffer.from(JSON.stringify(body), "utf8"),
59
+ inferredContentType: "application/json"
60
+ };
61
+ }
62
+ /**
63
+ * Merge session-level default headers with per-request headers.
64
+ * Per-request headers win on collision.
65
+ * Injects Content-Length and Content-Type when a body is present.
66
+ */
67
+ function buildHeaders(defaults, overrides, body, inferredContentType) {
68
+ const merged = {
69
+ ...defaults,
70
+ ...overrides ?? {}
71
+ };
72
+ if (body !== null) {
73
+ merged["content-length"] = String(body.length);
74
+ if (!merged["content-type"] && inferredContentType !== null) merged["content-type"] = inferredContentType;
75
+ }
76
+ return merged;
77
+ }
78
+ /**
79
+ * Normalise an undici response headers object
80
+ * (`Record<string, string | string[]>`) to our `HttpResponse.headers` shape
81
+ * (`Record<string, string | string[] | undefined>`).
82
+ */
83
+ function normaliseHeaders(h) {
84
+ return h;
85
+ }
86
+ function assertBodySize(size, maximum) {
87
+ if (size > maximum) throw new Error(`HTTP response body exceeds the ${maximum} byte limit.`);
88
+ }
89
+ /**
90
+ * Holds the session-level options and exposes the convenience wrappers
91
+ * (`get`, `post`, `delete`) that delegate to the abstract `request` method.
92
+ *
93
+ * Concrete subclasses only need to implement `request()` and `stream()`.
94
+ */
95
+ var BaseUndiciClient = class {
96
+ /** Default headers merged into every outgoing request. */
97
+ defaultHeaders;
98
+ /** Session-level request timeout in milliseconds. */
99
+ defaultTimeout;
100
+ /**
101
+ * Shared undici Dispatcher. May be an Agent, Pool, Client, ProxyAgent, etc.
102
+ * Defaults to undici's process-global agent when not specified.
103
+ */
104
+ dispatcher;
105
+ maximumResponseBodySize;
106
+ constructor(options = {}) {
107
+ this.defaultHeaders = options.headers ?? {};
108
+ this.defaultTimeout = options.timeout ?? 3e4;
109
+ this.dispatcher = options.dispatcher ?? getGlobalDispatcher();
110
+ this.maximumResponseBodySize = options.maximumResponseBodySize ?? 33554432;
111
+ }
112
+ get(url, options) {
113
+ return this.request("GET", url, options);
114
+ }
115
+ post(url, options) {
116
+ return this.request("POST", url, options);
117
+ }
118
+ delete(url, options) {
119
+ return this.request("DELETE", url, options);
120
+ }
121
+ };
122
+ /**
123
+ * HTTP client backed by `undici.request()`.
124
+ *
125
+ * `undici.request` is the recommended high-level API for most use-cases.
126
+ * It returns a Promise that resolves with `{ statusCode, headers, body }`
127
+ * where `body` is an undici `BodyReadable` (a Node `Readable` subclass).
128
+ *
129
+ * For buffered responses (`request()`) we call `body.text()` to collect the
130
+ * full response. For streaming responses (`stream()`) we return `body`
131
+ * directly as the `StreamResult.body` Readable, giving the caller zero-copy
132
+ * access to the response stream.
133
+ *
134
+ */
135
+ var RequestHttpClient = class extends BaseUndiciClient {
136
+ /**
137
+ * Buffered request - collects the entire response body as a string.
138
+ *
139
+ * Timeout is implemented via undici's built-in `headersTimeout` /
140
+ * `bodyTimeout` options rather than an external `setTimeout`.
141
+ */
142
+ async request(method, url, options = {}) {
143
+ const { buf, inferredContentType } = prepareBody(options.body);
144
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
145
+ const timeout = options.timeout ?? this.defaultTimeout;
146
+ const { statusCode, headers: respHeaders, body } = await request(url, {
147
+ method,
148
+ headers,
149
+ ...buf !== null && { body: buf },
150
+ headersTimeout: timeout,
151
+ bodyTimeout: timeout,
152
+ dispatcher: this.dispatcher
153
+ });
154
+ const text = await body.text();
155
+ assertBodySize(Buffer.byteLength(text), this.maximumResponseBodySize);
156
+ return {
157
+ status: statusCode,
158
+ headers: normaliseHeaders(respHeaders),
159
+ body: text
160
+ };
161
+ }
162
+ /**
163
+ * Streaming request - returns the response body `Readable` without
164
+ * buffering. Used by SSE and long-polling transports.
165
+ *
166
+ * The `abort()` method destroys the underlying socket and the body stream.
167
+ */
168
+ async stream(method, url, options = {}) {
169
+ const { buf, inferredContentType } = prepareBody(options.body);
170
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
171
+ const { statusCode, headers: respHeaders, body } = await request(url, {
172
+ method,
173
+ headers,
174
+ ...buf !== null && { body: buf },
175
+ headersTimeout: options.timeout ?? this.defaultTimeout,
176
+ dispatcher: this.dispatcher
177
+ });
178
+ return {
179
+ statusCode,
180
+ headers: normaliseHeaders(respHeaders),
181
+ body,
182
+ abort: () => body.destroy()
183
+ };
184
+ }
185
+ };
186
+ /**
187
+ * HTTP client backed by `undici.fetch()`.
188
+ *
189
+ * `undici.fetch` is undici's WHATWG-compatible Fetch implementation.
190
+ * It accepts the same options as the browser `fetch()` API, plus an
191
+ * undici-specific `dispatcher` field for custom transport control.
192
+ *
193
+ * Useful when you need browser API compatibility (e.g., tests that run in
194
+ * both Node and a browser-like environment) or when consuming WHATWG `Response`
195
+ * helpers such as `res.json()` / `res.formData()`.
196
+ *
197
+ * For `stream()` we convert the WHATWG `ReadableStream<Uint8Array>` body to a
198
+ * Node.js `Readable` via `Readable.fromWeb()` (Node 17+).
199
+ */
200
+ var FetchHttpClient = class extends BaseUndiciClient {
201
+ async request(method, url, options = {}) {
202
+ const { buf, inferredContentType } = prepareBody(options.body);
203
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
204
+ const ac = new AbortController();
205
+ const timeout = options.timeout ?? this.defaultTimeout;
206
+ const timer = timeout > 0 ? setTimeout(() => ac.abort(/* @__PURE__ */ new Error(`HTTP request timed out after ${timeout} ms`)), timeout) : null;
207
+ try {
208
+ const res = await fetch(url, {
209
+ method,
210
+ headers,
211
+ ...buf !== null && { body: buf },
212
+ signal: ac.signal,
213
+ dispatcher: this.dispatcher
214
+ });
215
+ const text = await res.text();
216
+ assertBodySize(Buffer.byteLength(text), this.maximumResponseBodySize);
217
+ if (timer !== null) clearTimeout(timer);
218
+ const respHeaders = {};
219
+ res.headers.forEach((value, key) => {
220
+ respHeaders[key] = value;
221
+ });
222
+ return {
223
+ status: res.status,
224
+ headers: respHeaders,
225
+ body: text
226
+ };
227
+ } catch (err) {
228
+ if (timer !== null) clearTimeout(timer);
229
+ throw err;
230
+ }
231
+ }
232
+ async stream(method, url, options = {}) {
233
+ const { buf, inferredContentType } = prepareBody(options.body);
234
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
235
+ const ac = new AbortController();
236
+ const res = await fetch(url, {
237
+ method,
238
+ headers,
239
+ ...buf !== null && { body: buf },
240
+ signal: ac.signal,
241
+ dispatcher: this.dispatcher
242
+ });
243
+ const body = Readable.fromWeb(res.body);
244
+ const respHeaders = {};
245
+ res.headers.forEach((value, key) => {
246
+ respHeaders[key] = value;
247
+ });
248
+ return {
249
+ statusCode: res.status,
250
+ headers: respHeaders,
251
+ body,
252
+ abort: () => {
253
+ ac.abort();
254
+ body.destroy();
255
+ }
256
+ };
257
+ }
258
+ };
259
+ /**
260
+ * HTTP client backed by `undici.stream()`.
261
+ *
262
+ * `undici.stream` uses a *factory callback* pattern: you supply a function
263
+ * that receives `{ statusCode, headers, body }` and must return a
264
+ * `Writable` to which the response body is piped.
265
+ *
266
+ * This makes it ideal when you want to pipe response bytes directly to a
267
+ * destination (e.g., a file, a transform, or a compression stream) without
268
+ * ever holding the entire response in memory.
269
+ *
270
+ * For buffered `request()` we use a `PassThrough` as the sink and collect
271
+ * its output. For streaming `stream()` we expose the PassThrough's
272
+ * readable side as `StreamResult.body`.
273
+ */
274
+ var StreamHttpClient = class extends BaseUndiciClient {
275
+ request(method, url, options = {}) {
276
+ const { buf, inferredContentType } = prepareBody(options.body);
277
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
278
+ const timeout = options.timeout ?? this.defaultTimeout;
279
+ let statusCode = 0;
280
+ let respHeaders = {};
281
+ const chunks = [];
282
+ return stream(url, {
283
+ method,
284
+ headers,
285
+ ...buf !== null && { body: buf },
286
+ headersTimeout: timeout,
287
+ bodyTimeout: timeout,
288
+ dispatcher: this.dispatcher
289
+ }, ({ statusCode: sc, headers: h }) => {
290
+ statusCode = sc;
291
+ respHeaders = h;
292
+ const sink = new PassThrough();
293
+ sink.on("data", (chunk) => chunks.push(chunk));
294
+ return sink;
295
+ }).then(() => ({
296
+ status: statusCode,
297
+ headers: normaliseHeaders(respHeaders),
298
+ body: (() => {
299
+ const body = Buffer.concat(chunks);
300
+ assertBodySize(body.length, this.maximumResponseBodySize);
301
+ return body.toString("utf8");
302
+ })()
303
+ }));
304
+ }
305
+ stream(method, url, options = {}) {
306
+ const { buf, inferredContentType } = prepareBody(options.body);
307
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
308
+ return new Promise((resolve, reject) => {
309
+ const pt = new PassThrough();
310
+ stream(url, {
311
+ method,
312
+ headers,
313
+ ...buf !== null && { body: buf },
314
+ headersTimeout: options.timeout ?? this.defaultTimeout,
315
+ dispatcher: this.dispatcher
316
+ }, ({ statusCode, headers: h }) => {
317
+ resolve({
318
+ statusCode,
319
+ headers: normaliseHeaders(h),
320
+ body: pt,
321
+ abort: () => pt.destroy()
322
+ });
323
+ return pt;
324
+ }).catch((err) => {
325
+ pt.destroy();
326
+ reject(err);
327
+ });
328
+ });
329
+ }
330
+ };
331
+ /**
332
+ * HTTP client backed by `undici.pipeline()`.
333
+ *
334
+ * `undici.pipeline` returns a `stream.Duplex`:
335
+ * - **Writable side**: the request body (write / pipe into it).
336
+ * - **Readable side**: whatever your handler function *returns*.
337
+ *
338
+ * The handler receives `{ statusCode, headers, body }` and must return a
339
+ * `Readable`. Whatever that readable emits becomes the duplex's output.
340
+ *
341
+ * Typical use: response transformation pipelines
342
+ * `pipeline(url, opts, ({ body }) => body.pipe(gunzip()))`
343
+ *
344
+ * For `request()` we return the raw body from the handler, collect the duplex
345
+ * output into a buffer, and resolve with the buffered string.
346
+ *
347
+ * For `stream()` we return the raw body from the handler and give the caller
348
+ * the duplex's readable side directly - zero extra buffering.
349
+ */
350
+ var PipelineHttpClient = class extends BaseUndiciClient {
351
+ request(method, url, options = {}) {
352
+ const { buf, inferredContentType } = prepareBody(options.body);
353
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
354
+ const timeout = options.timeout ?? this.defaultTimeout;
355
+ return new Promise((resolve, reject) => {
356
+ let statusCode = 0;
357
+ let respHeaders = {};
358
+ const chunks = [];
359
+ const duplex = pipeline(url, {
360
+ method,
361
+ headers,
362
+ ...buf !== null && { body: buf },
363
+ headersTimeout: timeout,
364
+ bodyTimeout: timeout,
365
+ dispatcher: this.dispatcher
366
+ }, ({ statusCode: sc, headers: h, body }) => {
367
+ statusCode = sc;
368
+ respHeaders = h;
369
+ return body;
370
+ });
371
+ duplex.on("data", (chunk) => chunks.push(chunk));
372
+ duplex.on("end", () => resolve({
373
+ status: statusCode,
374
+ headers: normaliseHeaders(respHeaders),
375
+ body: (() => {
376
+ const body = Buffer.concat(chunks);
377
+ assertBodySize(body.length, this.maximumResponseBodySize);
378
+ return body.toString("utf8");
379
+ })()
380
+ }));
381
+ duplex.on("error", reject);
382
+ if (buf !== null) duplex.write(buf);
383
+ duplex.end();
384
+ });
385
+ }
386
+ stream(method, url, options = {}) {
387
+ const { buf, inferredContentType } = prepareBody(options.body);
388
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
389
+ return new Promise((resolve, reject) => {
390
+ let resolved = false;
391
+ let duplex;
392
+ duplex = pipeline(url, {
393
+ method,
394
+ headers,
395
+ ...buf !== null && { body: buf },
396
+ headersTimeout: options.timeout ?? this.defaultTimeout,
397
+ dispatcher: this.dispatcher
398
+ }, ({ statusCode, headers: h, body }) => {
399
+ if (!resolved) {
400
+ resolved = true;
401
+ resolve({
402
+ statusCode,
403
+ headers: normaliseHeaders(h),
404
+ body: duplex,
405
+ abort: () => duplex.destroy()
406
+ });
407
+ }
408
+ return body;
409
+ });
410
+ duplex.on("error", (err) => {
411
+ if (!resolved) reject(err);
412
+ });
413
+ if (buf !== null) duplex.write(buf);
414
+ duplex.end();
415
+ });
416
+ }
417
+ };
418
+ /**
419
+ * HTTP client backed by `Dispatcher#dispatch()`.
420
+ *
421
+ * `dispatch` is the *lowest-level* undici API. It exposes the full
422
+ * request/response lifecycle as a handler interface (undici 8+):
423
+ *
424
+ * onRequestStart → request is about to be sent; receives DispatchController
425
+ * onResponseStart → response status + headers received
426
+ * onResponseData → response body chunk
427
+ * onResponseEnd → response fully consumed
428
+ * onResponseError → error at any stage
429
+ *
430
+ * This gives you maximum control: you can abort mid-stream, transform
431
+ * chunks on the fly, or implement custom back-pressure logic.
432
+ *
433
+ * Unlike the other clients, `DispatchHttpClient` *requires* a `Dispatcher`
434
+ * (an `Agent`, `Pool`, or `Client` instance). If none is provided the
435
+ * process-global agent is used.
436
+ *
437
+ * Note: `dispatcher.dispatch()` needs the URL split into `origin` + `path`
438
+ * (for `Agent` / `Pool`) or just `path` (for `Client` whose origin is
439
+ * already bound). We always pass `origin` for maximum compatibility.
440
+ */
441
+ var DispatchHttpClient = class extends BaseUndiciClient {
442
+ request(method, url, options = {}) {
443
+ const { buf, inferredContentType } = prepareBody(options.body);
444
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
445
+ const parsed = new URL(url);
446
+ const timeout = options.timeout ?? this.defaultTimeout;
447
+ return new Promise((resolve, reject) => {
448
+ let statusCode = 0;
449
+ let respHeaders = {};
450
+ const chunks = [];
451
+ let responseSize = 0;
452
+ let controller = null;
453
+ let settled = false;
454
+ let timer = null;
455
+ if (timeout > 0) timer = setTimeout(() => {
456
+ /* istanbul ignore next -- timer is cleared on every settled path */
457
+ if (settled) return;
458
+ settled = true;
459
+ const error = /* @__PURE__ */ new Error(`HTTP request timed out after ${timeout} ms`);
460
+ controller?.abort(error);
461
+ reject(error);
462
+ }, timeout);
463
+ const clearTimer = () => {
464
+ if (timer !== null) {
465
+ clearTimeout(timer);
466
+ timer = null;
467
+ }
468
+ };
469
+ this.dispatcher.dispatch({
470
+ origin: parsed.origin,
471
+ path: parsed.pathname + parsed.search,
472
+ method,
473
+ headers,
474
+ ...buf !== null && { body: buf }
475
+ }, {
476
+ onRequestStart: (ctrl, _context) => {
477
+ controller = ctrl;
478
+ },
479
+ onResponseError: (_controller, err) => {
480
+ clearTimer();
481
+ /* istanbul ignore next -- undici does not end after reporting an error */
482
+ if (settled) return;
483
+ settled = true;
484
+ reject(err);
485
+ },
486
+ onResponseStart: (_controller, sc, headers) => {
487
+ statusCode = sc;
488
+ respHeaders = headers;
489
+ },
490
+ onResponseData: (_controller, chunk) => {
491
+ chunks.push(Buffer.from(chunk));
492
+ responseSize += chunk.length;
493
+ if (responseSize > this.maximumResponseBodySize) _controller.abort(/* @__PURE__ */ new Error(`HTTP response body exceeds the ${this.maximumResponseBodySize} byte limit.`));
494
+ },
495
+ onResponseEnd: (_controller, _trailers) => {
496
+ clearTimer();
497
+ /* istanbul ignore next -- undici does not end after reporting an error */
498
+ if (settled) return;
499
+ settled = true;
500
+ resolve({
501
+ status: statusCode,
502
+ headers: respHeaders,
503
+ body: Buffer.concat(chunks).toString("utf8")
504
+ });
505
+ }
506
+ });
507
+ });
508
+ }
509
+ stream(method, url, options = {}) {
510
+ const { buf, inferredContentType } = prepareBody(options.body);
511
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
512
+ const parsed = new URL(url);
513
+ return new Promise((resolve, reject) => {
514
+ let resolved = false;
515
+ const pt = new PassThrough();
516
+ let controller = null;
517
+ this.dispatcher.dispatch({
518
+ origin: parsed.origin,
519
+ path: parsed.pathname + parsed.search,
520
+ method,
521
+ headers,
522
+ ...buf !== null && { body: buf },
523
+ headersTimeout: options.timeout ?? this.defaultTimeout
524
+ }, {
525
+ onRequestStart: (ctrl, _context) => {
526
+ controller = ctrl;
527
+ },
528
+ onResponseError: (_ctrl, err) => {
529
+ if (!resolved) {
530
+ resolved = true;
531
+ pt.destroy();
532
+ reject(err);
533
+ } else pt.destroy(err);
534
+ },
535
+ onResponseStart: (_ctrl, statusCode, headers) => {
536
+ if (!resolved) {
537
+ resolved = true;
538
+ resolve({
539
+ statusCode,
540
+ headers,
541
+ body: pt,
542
+ abort: () => {
543
+ controller?.abort(/* @__PURE__ */ new Error("Stream aborted"));
544
+ pt.destroy();
545
+ }
546
+ });
547
+ }
548
+ },
549
+ onResponseData: (_ctrl, chunk) => {
550
+ if (!pt.write(chunk)) {
551
+ controller?.pause();
552
+ pt.once("drain", () => controller?.resume());
553
+ }
554
+ },
555
+ onResponseEnd: (_ctrl, _trailers) => {
556
+ pt.end();
557
+ }
558
+ });
559
+ });
560
+ }
561
+ };
562
+ //#endregion
563
+ //#region src/constants.ts
564
+ /**
565
+ * constants.ts
566
+ *
567
+ * All SignalR protocol constants, enumerations, and timing defaults.
568
+ * Uses `as const satisfies` (TS 4.9+) so values carry their narrow literal
569
+ * types while being verified against a constraint at compile time.
570
+ */
571
+ const MessageType = Object.freeze({
572
+ Invocation: 1,
573
+ StreamItem: 2,
574
+ Completion: 3,
575
+ StreamInvocation: 4,
576
+ CancelInvocation: 5,
577
+ Ping: 6,
578
+ Close: 7
579
+ });
580
+ const HubConnectionState = Object.freeze({
581
+ Disconnected: "Disconnected",
582
+ Connecting: "Connecting",
583
+ Connected: "Connected",
584
+ Disconnecting: "Disconnecting",
585
+ Reconnecting: "Reconnecting"
586
+ });
587
+ const LogLevel = {
588
+ Trace: 0,
589
+ Debug: 1,
590
+ Information: 2,
591
+ Warning: 3,
592
+ Error: 4,
593
+ Critical: 5,
594
+ None: 6
595
+ };
596
+ const HttpTransportType = {
597
+ None: 0,
598
+ WebSockets: 1,
599
+ ServerSentEvents: 2,
600
+ LongPolling: 4
601
+ };
602
+ const TransferFormat = {
603
+ Text: 1,
604
+ Binary: 2
605
+ };
606
+ /** ASCII 30 - the byte that terminates every SignalR JSON message. */
607
+ const RECORD_SEPARATOR = "";
608
+ /** Negotiate version we advertise to the server. */
609
+ const NEGOTIATE_VERSION = 1;
610
+ const DEFAULT_TIMEOUT_IN_MS = 3e4;
611
+ const DEFAULT_PING_INTERVAL_IN_MS = 15e3;
612
+ const DEFAULT_SERVER_TIMEOUT_IN_MS = 3e4;
613
+ //#endregion
614
+ //#region src/messages.ts
615
+ /** Cast a plain string to an InvocationId (only used inside the library). */
616
+ function toInvocationId(s) {
617
+ return s;
618
+ }
619
+ /**
620
+ * Asserts a code path is unreachable at compile time.
621
+ * Use at the `default` branch of a `switch` over `HubMessage['type']`.
622
+ */
623
+ function assertNever(_x, label) {
624
+ throw new Error(`Unexpected value${label ? ` for ${label}` : ""}: ${JSON.stringify(_x)}`);
625
+ }
626
+ function isInvocationMessage(msg) {
627
+ return msg.type === 1;
628
+ }
629
+ function isStreamItemMessage(msg) {
630
+ return msg.type === 2;
631
+ }
632
+ function isCompletionMessage(msg) {
633
+ return msg.type === 3;
634
+ }
635
+ function isStreamInvocationMessage(msg) {
636
+ return msg.type === 4;
637
+ }
638
+ function isCancelInvocationMessage(msg) {
639
+ return msg.type === 5;
640
+ }
641
+ function isPingMessage(msg) {
642
+ return msg.type === 6;
643
+ }
644
+ function isCloseMessage(msg) {
645
+ return msg.type === 7;
646
+ }
647
+ //#endregion
648
+ //#region src/protocols/json-hub-protocol.ts
649
+ /**
650
+ * json-hub-protocol.ts
651
+ *
652
+ * Implements the SignalR Hub Protocol over JSON (version 1).
653
+ *
654
+ * Wire format: every message is JSON followed by ASCII 0x1e (RECORD_SEPARATOR).
655
+ * A single transport payload may contain multiple messages.
656
+ *
657
+ * Reference:
658
+ * https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/HubProtocol.md
659
+ */
660
+ const HANDSHAKE_REQUEST = JSON.stringify({
661
+ protocol: "json",
662
+ version: 1
663
+ }) + "";
664
+ /**
665
+ * Parse the first record from `data` as the server handshake response.
666
+ * @throws {Error} if the record separator is missing or the server returned an error.
667
+ */
668
+ function parseHandshakeResponse(data) {
669
+ const idx = data.indexOf("");
670
+ if (idx === -1) throw new Error("Handshake response is missing the record separator.");
671
+ const raw = data.slice(0, idx);
672
+ const response = JSON.parse(raw);
673
+ if (response.error) throw new Error(`Server rejected the handshake with the error: "${response.error}"`);
674
+ return { remainder: data.slice(idx + 1) };
675
+ }
676
+ var JsonHubProtocol = class {
677
+ name = "json";
678
+ version = 1;
679
+ transferFormat = TransferFormat.Text;
680
+ parseMessages(input, logger) {
681
+ const text = typeof input === "string" ? input : new TextDecoder().decode(input);
682
+ if (!text) return [];
683
+ const messages = [];
684
+ const parts = text.split("");
685
+ for (const part of parts) {
686
+ const trimmed = part.trim();
687
+ if (!trimmed) continue;
688
+ let raw;
689
+ try {
690
+ raw = JSON.parse(trimmed);
691
+ } catch (e) {
692
+ throw new Error(`Error parsing message: ${e.message}. Raw value: "${trimmed}"`);
693
+ }
694
+ const msg = this.#coerce(raw);
695
+ if (msg !== null) messages.push(msg);
696
+ else logger.log(3, `Ignoring message with unknown type ${String(raw.type)}.`);
697
+ }
698
+ return messages;
699
+ }
700
+ writeMessage(message) {
701
+ return JSON.stringify(message) + "";
702
+ }
703
+ /**
704
+ * Validate and narrow a raw parsed object into a typed `HubMessage`.
705
+ * Returns `null` for unknown types (not an error - new protocol versions
706
+ * may add types we don't know about).
707
+ */
708
+ #coerce(raw) {
709
+ const type = raw.type;
710
+ if (typeof type !== "number") throw new Error(`Invalid message: 'type' must be a number, got ${JSON.stringify(type)}.`);
711
+ switch (type) {
712
+ case MessageType.Invocation: return this.#coerceInvocation(raw);
713
+ case MessageType.StreamItem: return this.#coerceStreamItem(raw);
714
+ case MessageType.Completion: return this.#coerceCompletion(raw);
715
+ case MessageType.StreamInvocation: return this.#coerceStreamInvocation(raw);
716
+ case MessageType.CancelInvocation: return this.#coerceCancelInvocation(raw);
717
+ case MessageType.Ping: return this.#coercePing();
718
+ case MessageType.Close: return this.#coerceClose(raw);
719
+ default: return null;
720
+ }
721
+ }
722
+ #coerceInvocation(raw) {
723
+ if (typeof raw.target !== "string") throw new Error("Invalid Invocation message: 'target' must be a string.");
724
+ if (!Array.isArray(raw.arguments)) throw new Error("Invalid Invocation message: 'arguments' must be an array.");
725
+ return {
726
+ type: MessageType.Invocation,
727
+ target: raw.target,
728
+ arguments: raw.arguments,
729
+ ...raw.invocationId != null && { invocationId: parseInvocationId$1(raw.invocationId) },
730
+ ...raw.streamIds != null && { streamIds: parseStreamIds$1(raw.streamIds) },
731
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
732
+ };
733
+ }
734
+ #coerceStreamItem(raw) {
735
+ if (raw.invocationId == null) throw new Error("Invalid StreamItem message: 'invocationId' is required.");
736
+ return {
737
+ type: MessageType.StreamItem,
738
+ invocationId: parseInvocationId$1(raw.invocationId),
739
+ item: raw.item,
740
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
741
+ };
742
+ }
743
+ #coerceCompletion(raw) {
744
+ if (raw.invocationId == null) throw new Error("Invalid Completion message: 'invocationId' is required.");
745
+ const hasResult = Object.prototype.hasOwnProperty.call(raw, "result");
746
+ if (raw.error != null && hasResult) throw new Error("Invalid Completion message: 'error' and 'result' are mutually exclusive.");
747
+ return {
748
+ type: MessageType.Completion,
749
+ invocationId: parseInvocationId$1(raw.invocationId),
750
+ ...raw.error != null && { error: String(raw.error) },
751
+ ...hasResult && { result: raw.result },
752
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
753
+ };
754
+ }
755
+ #coerceStreamInvocation(raw) {
756
+ if (raw.invocationId == null) throw new Error("Invalid StreamInvocation message: 'invocationId' is required.");
757
+ if (typeof raw.target !== "string") throw new Error("Invalid StreamInvocation message: 'target' must be a string.");
758
+ if (!Array.isArray(raw.arguments)) throw new Error("Invalid StreamInvocation message: 'arguments' must be an array.");
759
+ return {
760
+ type: MessageType.StreamInvocation,
761
+ invocationId: parseInvocationId$1(raw.invocationId),
762
+ target: raw.target,
763
+ arguments: raw.arguments,
764
+ ...raw.streamIds != null && { streamIds: parseStreamIds$1(raw.streamIds) },
765
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
766
+ };
767
+ }
768
+ #coerceCancelInvocation(raw) {
769
+ if (raw.invocationId == null) throw new Error("Invalid CancelInvocation message: 'invocationId' is required.");
770
+ return {
771
+ type: MessageType.CancelInvocation,
772
+ invocationId: parseInvocationId$1(raw.invocationId),
773
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
774
+ };
775
+ }
776
+ #coercePing() {
777
+ return { type: MessageType.Ping };
778
+ }
779
+ #coerceClose(raw) {
780
+ if (raw.allowReconnect != null && typeof raw.allowReconnect !== "boolean") throw new Error("Invalid Close message: 'allowReconnect' must be a boolean.");
781
+ return {
782
+ type: MessageType.Close,
783
+ ...raw.error != null && { error: String(raw.error) },
784
+ ...raw.allowReconnect != null && { allowReconnect: raw.allowReconnect },
785
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
786
+ };
787
+ }
788
+ static invocation(invocationId, target, args, streamIds) {
789
+ return {
790
+ type: MessageType.Invocation,
791
+ invocationId,
792
+ target,
793
+ arguments: args,
794
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
795
+ };
796
+ }
797
+ /**
798
+ * Fire-and-forget (no `invocationId` → server won't send a Completion).
799
+ */
800
+ static send(target, args, streamIds) {
801
+ return {
802
+ type: MessageType.Invocation,
803
+ target,
804
+ arguments: args,
805
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
806
+ };
807
+ }
808
+ static streamInvocation(invocationId, target, args, streamIds) {
809
+ return {
810
+ type: MessageType.StreamInvocation,
811
+ invocationId,
812
+ target,
813
+ arguments: args,
814
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
815
+ };
816
+ }
817
+ static cancelInvocation(invocationId) {
818
+ return {
819
+ type: MessageType.CancelInvocation,
820
+ invocationId
821
+ };
822
+ }
823
+ static streamItem(invocationId, item) {
824
+ return {
825
+ type: MessageType.StreamItem,
826
+ invocationId,
827
+ item
828
+ };
829
+ }
830
+ static completion(invocationId, result, error) {
831
+ return {
832
+ type: MessageType.Completion,
833
+ invocationId,
834
+ ...error != null && { error },
835
+ ...result != null && { result }
836
+ };
837
+ }
838
+ static ping() {
839
+ return { type: MessageType.Ping };
840
+ }
841
+ };
842
+ function parseInvocationId$1(value) {
843
+ if (typeof value !== "string") throw new Error("Invalid message: 'invocationId' must be a string.");
844
+ return toInvocationId(value);
845
+ }
846
+ function parseStreamIds$1(value) {
847
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) throw new Error("Invalid message: 'streamIds' must be an array of strings.");
848
+ return value;
849
+ }
850
+ function parseHeaders$1(value) {
851
+ if (value === null || typeof value !== "object" || Array.isArray(value) || !Object.values(value).every((entry) => typeof entry === "string")) throw new Error("Invalid message: 'headers' must be an object containing string values.");
852
+ return value;
853
+ }
854
+ Object.freeze({
855
+ Connecting: 0,
856
+ Open: 1,
857
+ Closing: 2,
858
+ Closed: 3
859
+ });
860
+ var WebSocketTransport = class {
861
+ name = "WebSockets";
862
+ #accessTokenFactory;
863
+ #logger;
864
+ #extraHeaders;
865
+ #dispatcher;
866
+ #ws = null;
867
+ #onreceive = null;
868
+ #pendingMessages = [];
869
+ onclose = null;
870
+ get onreceive() {
871
+ return this.#onreceive;
872
+ }
873
+ set onreceive(handler) {
874
+ this.#onreceive = handler;
875
+ /* istanbul ignore else -- HubConnection installs a receiver after connect */
876
+ if (handler) {
877
+ const pending = this.#pendingMessages;
878
+ this.#pendingMessages = [];
879
+ for (const message of pending)
880
+ /* istanbul ignore next -- requires a server frame in the connect/onreceive microtask gap */
881
+ handler(message);
882
+ }
883
+ }
884
+ /**
885
+ * @param accessTokenFactory Async factory for Bearer tokens, or null.
886
+ * @param logger Logger instance.
887
+ * @param extraHeaders Headers forwarded on every upgrade request.
888
+ * @param dispatcher Optional undici Dispatcher (Agent/Pool/Client/
889
+ * ProxyAgent). When omitted, undici's default
890
+ * global agent is used. Pass the same dispatcher
891
+ * used by your HTTP clients to share the pool.
892
+ */
893
+ constructor(accessTokenFactory, logger, extraHeaders = {}, dispatcher) {
894
+ this.#accessTokenFactory = accessTokenFactory;
895
+ this.#logger = logger;
896
+ this.#extraHeaders = extraHeaders;
897
+ this.#dispatcher = dispatcher;
898
+ }
899
+ async connect(url, _transferFormat) {
900
+ this.#logger.log(LogLevel.Trace, `(WebSockets transport) Connecting to ${url}.`);
901
+ const headers = { ...this.#extraHeaders };
902
+ if (this.#accessTokenFactory) {
903
+ const token = await this.#accessTokenFactory();
904
+ if (token) headers["Authorization"] = `Bearer ${token}`;
905
+ }
906
+ const wsUrl = url.replace(/^http/, "ws");
907
+ const ws = new WebSocket(wsUrl, {
908
+ headers,
909
+ ...this.#dispatcher !== void 0 && { dispatcher: this.#dispatcher }
910
+ });
911
+ this.#ws = ws;
912
+ ws.binaryType = "arraybuffer";
913
+ ws.addEventListener("message", (event) => {
914
+ const data = event.data;
915
+ const message = typeof data === "string" ? data : Buffer.from(data);
916
+ this.#logger.log(LogLevel.Trace, typeof message === "string" ? `(WebSockets transport) Received text (${message.length} chars).` : `(WebSockets transport) Received binary (${message.length} bytes).`);
917
+ if (this.#onreceive) this.#onreceive(message);
918
+ else this.#pendingMessages.push(message);
919
+ });
920
+ await new Promise((resolve, reject) => {
921
+ const cleanup = () => {
922
+ ws.removeEventListener("open", onOpen);
923
+ ws.removeEventListener("error", onError);
924
+ };
925
+ const onOpen = () => {
926
+ cleanup();
927
+ this.#logger.log(LogLevel.Information, "(WebSockets transport) Connected.");
928
+ resolve();
929
+ };
930
+ const onError = (event) => {
931
+ cleanup();
932
+ const e = event;
933
+ let err;
934
+ if (e.error instanceof Error) err = e.error.message ? e.error : e.error.cause instanceof Error ? e.error.cause : /* @__PURE__ */ new Error("WebSocket connection failed");
935
+ else err = new Error(String(e.message ?? "WebSocket error"));
936
+ this.#logger.log(LogLevel.Error, `(WebSockets transport) Error during connect: ${err.message}`);
937
+ this.#ws = null;
938
+ ws.close();
939
+ reject(err);
940
+ };
941
+ ws.addEventListener("open", onOpen);
942
+ ws.addEventListener("error", onError);
943
+ });
944
+ ws.addEventListener("close", (event) => {
945
+ const closeEvent = event;
946
+ this.#logger.log(LogLevel.Information, `(WebSockets transport) Socket closed (code=${closeEvent.code}).`);
947
+ if (this.#ws !== null) {
948
+ this.#ws = null;
949
+ this.onclose?.();
950
+ }
951
+ });
952
+ ws.addEventListener("error", (event) => {
953
+ if (ws.readyState !== WebSocket.CONNECTING) {
954
+ const e = event;
955
+ const err = e.error instanceof Error ? e.error : new Error(String(e.message ?? "WebSocket error"));
956
+ this.#logger.log(LogLevel.Error, `(WebSockets transport) Error: ${err.message}`);
957
+ if (this.#ws !== null) {
958
+ this.#ws = null;
959
+ this.onclose?.(err);
960
+ }
961
+ }
962
+ });
963
+ }
964
+ send(data) {
965
+ if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) return Promise.reject(/* @__PURE__ */ new Error("WebSocket is not open."));
966
+ try {
967
+ this.#ws.send(data);
968
+ return Promise.resolve();
969
+ } catch (err) {
970
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
971
+ }
972
+ }
973
+ stop() {
974
+ if (this.#ws) {
975
+ const ws = this.#ws;
976
+ this.#ws = null;
977
+ ws.close(1e3, "Normal closure");
978
+ }
979
+ return Promise.resolve();
980
+ }
981
+ };
982
+ //#endregion
983
+ //#region src/transports/sse-transport.ts
984
+ var ServerSentEventsTransport = class {
985
+ name = "ServerSentEvents";
986
+ #httpClient;
987
+ #accessTokenFactory;
988
+ #logger;
989
+ #extraHeaders;
990
+ #url = null;
991
+ #abort = null;
992
+ #sseBuffer = "";
993
+ onreceive = null;
994
+ onclose = null;
995
+ constructor(httpClient, accessTokenFactory, logger, extraHeaders = {}) {
996
+ this.#httpClient = httpClient;
997
+ this.#accessTokenFactory = accessTokenFactory;
998
+ this.#logger = logger;
999
+ this.#extraHeaders = extraHeaders;
1000
+ }
1001
+ async connect(url, transferFormat) {
1002
+ if (transferFormat === TransferFormat.Binary) throw new Error("The Server-Sent Events transport only supports the Text transfer format.");
1003
+ this.#logger.log(LogLevel.Trace, `(SSE transport) Connecting to ${url}.`);
1004
+ this.#url = url;
1005
+ const headers = {
1006
+ Accept: "text/event-stream",
1007
+ ...this.#extraHeaders
1008
+ };
1009
+ if (this.#accessTokenFactory) {
1010
+ const token = await this.#accessTokenFactory();
1011
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1012
+ }
1013
+ const result = await this.#httpClient.stream("GET", url, { headers });
1014
+ if (result.statusCode !== 200) {
1015
+ result.body.on("error", () => {});
1016
+ result.abort();
1017
+ throw new Error(`Unexpected status code ${result.statusCode} from SSE endpoint.`);
1018
+ }
1019
+ this.#abort = result.abort;
1020
+ result.body.setEncoding("utf8");
1021
+ result.body.on("data", (chunk) => this.#onData(chunk));
1022
+ result.body.on("end", () => {
1023
+ this.#logger.log(LogLevel.Information, "(SSE transport) EventSource closed.");
1024
+ this.#abort = null;
1025
+ this.onclose?.();
1026
+ });
1027
+ result.body.on("error", (err) => {
1028
+ this.#logger.log(LogLevel.Error, `(SSE transport) Stream error: ${err.message}`);
1029
+ this.#abort = null;
1030
+ this.onclose?.(err);
1031
+ });
1032
+ this.#logger.log(LogLevel.Information, "(SSE transport) Connected.");
1033
+ }
1034
+ async send(data) {
1035
+ if (!this.#url) throw new Error("SSE transport is not connected.");
1036
+ const headers = {
1037
+ "Content-Type": typeof data === "string" ? "text/plain;charset=UTF-8" : "application/octet-stream",
1038
+ ...this.#extraHeaders
1039
+ };
1040
+ if (this.#accessTokenFactory) {
1041
+ const token = await this.#accessTokenFactory();
1042
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1043
+ }
1044
+ const result = await this.#httpClient.post(this.#url, {
1045
+ headers,
1046
+ body: data
1047
+ });
1048
+ if (result.status < 200 || result.status >= 300) throw new Error(`SSE send failed with status ${result.status}.`);
1049
+ }
1050
+ stop() {
1051
+ if (this.#abort) {
1052
+ this.#abort();
1053
+ this.#abort = null;
1054
+ }
1055
+ return Promise.resolve();
1056
+ }
1057
+ #onData(chunk) {
1058
+ this.#sseBuffer += chunk;
1059
+ let boundary;
1060
+ while ((boundary = findEventBoundary(this.#sseBuffer)) !== -1) {
1061
+ const block = this.#sseBuffer.slice(0, boundary);
1062
+ this.#sseBuffer = this.#sseBuffer.slice(boundary).replace(/^[\r\n]+/, "");
1063
+ this.#dispatchEvent(block);
1064
+ }
1065
+ }
1066
+ #dispatchEvent(block) {
1067
+ const lines = block.split(/\r?\n/);
1068
+ const dataParts = [];
1069
+ for (const line of lines) if (line.startsWith("data:")) dataParts.push(line.slice(5).replace(/^ /, ""));
1070
+ if (dataParts.length === 0) return;
1071
+ const message = dataParts.join("\n");
1072
+ this.#logger.log(LogLevel.Trace, `(SSE transport) Received data: ${message}`);
1073
+ this.onreceive?.(message);
1074
+ }
1075
+ };
1076
+ function findEventBoundary(str) {
1077
+ const i1 = str.indexOf("\n\n");
1078
+ const i2 = str.indexOf("\r\n\r\n");
1079
+ if (i1 === -1 && i2 === -1) return -1;
1080
+ if (i1 === -1) return i2 + 4;
1081
+ if (i2 === -1) return i1 + 2;
1082
+ return Math.min(i1 + 2, i2 + 4);
1083
+ }
1084
+ //#endregion
1085
+ //#region src/transports/long-polling-transport.ts
1086
+ /** How long a single poll GET can hang before we issue a new one. */
1087
+ const POLL_TIMEOUT_MS = 1e5;
1088
+ var LongPollingTransport = class {
1089
+ name = "LongPolling";
1090
+ #httpClient;
1091
+ #accessTokenFactory;
1092
+ #logger;
1093
+ #extraHeaders;
1094
+ #url = null;
1095
+ #running = false;
1096
+ #pollImmediate = null;
1097
+ #activeAbort = null;
1098
+ #transferFormat = TransferFormat.Text;
1099
+ onreceive = null;
1100
+ onclose = null;
1101
+ constructor(httpClient, accessTokenFactory, logger, extraHeaders = {}) {
1102
+ this.#httpClient = httpClient;
1103
+ this.#accessTokenFactory = accessTokenFactory;
1104
+ this.#logger = logger;
1105
+ this.#extraHeaders = extraHeaders;
1106
+ }
1107
+ async connect(url, transferFormat) {
1108
+ this.#logger.log(LogLevel.Trace, `(LongPolling transport) Connecting to ${url}.`);
1109
+ this.#url = url;
1110
+ this.#running = true;
1111
+ this.#transferFormat = transferFormat;
1112
+ await this.#poll(url, true);
1113
+ this.#schedulePoll();
1114
+ this.#logger.log(LogLevel.Information, "(LongPolling transport) Connected.");
1115
+ }
1116
+ async send(data) {
1117
+ if (!this.#running || !this.#url) throw new Error("LongPolling transport is not connected.");
1118
+ const headers = await this.#buildHeaders({ "Content-Type": typeof data === "string" ? "text/plain;charset=UTF-8" : "application/octet-stream" });
1119
+ const result = await this.#httpClient.post(this.#url, {
1120
+ headers,
1121
+ body: data
1122
+ });
1123
+ if (result.status < 200 || result.status >= 300) throw new Error(`Long polling send failed with status ${result.status}.`);
1124
+ }
1125
+ async stop() {
1126
+ this.#running = false;
1127
+ if (this.#pollImmediate !== null) {
1128
+ clearImmediate(this.#pollImmediate);
1129
+ this.#pollImmediate = null;
1130
+ }
1131
+ this.#activeAbort?.();
1132
+ this.#activeAbort = null;
1133
+ if (this.#url) try {
1134
+ const headers = await this.#buildHeaders();
1135
+ await this.#httpClient.delete(this.#url, { headers });
1136
+ } catch (e) {
1137
+ this.#logger.log(LogLevel.Warning, `(LongPolling transport) Error sending DELETE: ${e.message}`);
1138
+ }
1139
+ }
1140
+ #schedulePoll() {
1141
+ if (!this.#running) return;
1142
+ this.#pollImmediate = setImmediate(() => void this.#pollLoop());
1143
+ }
1144
+ async #pollLoop() {
1145
+ /* istanbul ignore next - #schedulePoll()'s !#running guard and
1146
+ clearImmediate() in stop() together prevent #pollLoop from ever
1147
+ being called once the transport has stopped; this is a
1148
+ belt-and-suspenders defence. */
1149
+ if (!this.#running || !this.#url) return;
1150
+ try {
1151
+ await this.#poll(this.#url, false);
1152
+ } catch (err) {
1153
+ /* istanbul ignore else - between #poll re-throwing and this catch,
1154
+ no macrotask can run to set #running=false; the false branch is
1155
+ a belt-and-suspenders defence against a theoretical race. */
1156
+ if (this.#running) {
1157
+ this.#logger.log(LogLevel.Error, `(LongPolling transport) Poll error: ${err.message}`);
1158
+ this.#running = false;
1159
+ this.onclose?.(err instanceof Error ? err : new Error(String(err)));
1160
+ return;
1161
+ }
1162
+ }
1163
+ this.#schedulePoll();
1164
+ }
1165
+ async #poll(url, isConnect) {
1166
+ const headers = await this.#buildHeaders();
1167
+ let res;
1168
+ try {
1169
+ res = await this.#httpClient.stream("GET", url, {
1170
+ headers,
1171
+ timeout: POLL_TIMEOUT_MS
1172
+ });
1173
+ this.#activeAbort = res.abort;
1174
+ } catch (err) {
1175
+ if (!this.#running) return;
1176
+ throw err;
1177
+ }
1178
+ if (res.statusCode === 204) {
1179
+ await readAll(res.body);
1180
+ this.#activeAbort = null;
1181
+ this.#logger.log(LogLevel.Information, "(LongPolling transport) Server closed (204).");
1182
+ this.#running = false;
1183
+ this.onclose?.();
1184
+ return;
1185
+ }
1186
+ if (res.statusCode < 200 || res.statusCode >= 300) {
1187
+ await readAll(res.body);
1188
+ this.#activeAbort = null;
1189
+ const err = /* @__PURE__ */ new Error(`Long polling received unexpected status ${res.statusCode}.`);
1190
+ if (!isConnect) {
1191
+ this.#running = false;
1192
+ this.onclose?.(err);
1193
+ } else throw err;
1194
+ return;
1195
+ }
1196
+ let body;
1197
+ try {
1198
+ body = await readAll(res.body);
1199
+ } catch (err) {
1200
+ /* istanbul ignore next -- only an explicit stop aborts the active body */
1201
+ if (!this.#running) return;
1202
+ /* istanbul ignore next -- body errors are propagated to the poll loop */
1203
+ throw err;
1204
+ } finally {
1205
+ this.#activeAbort = null;
1206
+ }
1207
+ if (body.length > 0) {
1208
+ const data = this.#transferFormat === TransferFormat.Binary ? new Uint8Array(body.buffer, body.byteOffset, body.byteLength) : body.toString("utf8");
1209
+ this.#logger.log(LogLevel.Trace, `(LongPolling transport) Received ${body.length} bytes.`);
1210
+ this.onreceive?.(data);
1211
+ }
1212
+ }
1213
+ async #buildHeaders(extra = {}) {
1214
+ const headers = {
1215
+ ...this.#extraHeaders,
1216
+ ...extra
1217
+ };
1218
+ if (this.#accessTokenFactory) {
1219
+ const token = await this.#accessTokenFactory();
1220
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1221
+ }
1222
+ return headers;
1223
+ }
1224
+ };
1225
+ function readAll(stream) {
1226
+ return new Promise((resolve, reject) => {
1227
+ const chunks = [];
1228
+ stream.on("data", (chunk) => {
1229
+ /* istanbul ignore next -- Node HTTP response streams emit Buffer chunks */
1230
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1231
+ });
1232
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
1233
+ stream.on("error", reject);
1234
+ });
1235
+ }
1236
+ //#endregion
1237
+ //#region src/logger.ts
1238
+ /**
1239
+ * logger.ts
1240
+ *
1241
+ * Built-in ILogger implementations.
1242
+ * ConsoleLogger - writes to console with timestamps and severity labels.
1243
+ * NullLogger - discards everything (default when no logger is configured).
1244
+ */
1245
+ var ConsoleLogger = class {
1246
+ #minimumLevel;
1247
+ constructor(minimumLevel = LogLevel.Information) {
1248
+ this.#minimumLevel = minimumLevel;
1249
+ }
1250
+ log(level, message) {
1251
+ if (level < this.#minimumLevel) return;
1252
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${logLevelName(level)}] ${message}`;
1253
+ switch (level) {
1254
+ case LogLevel.Trace:
1255
+ case LogLevel.Debug:
1256
+ console.debug(line);
1257
+ break;
1258
+ case LogLevel.Warning:
1259
+ console.warn(line);
1260
+ break;
1261
+ case LogLevel.Error:
1262
+ case LogLevel.Critical:
1263
+ console.error(line);
1264
+ break;
1265
+ default: console.log(line);
1266
+ }
1267
+ }
1268
+ };
1269
+ var NullLogger = class NullLogger {
1270
+ /** Singleton - no state, so one instance is enough. */
1271
+ static instance = new NullLogger();
1272
+ log(_logLevel, _message) {}
1273
+ };
1274
+ /** Reverse-map a LogLevel number to its string label. */
1275
+ function logLevelName(level) {
1276
+ return Object.entries(LogLevel).find(([, v]) => v === level)?.[0] ?? String(level);
1277
+ }
1278
+ /** Normalise a LogLevel number or ILogger into an ILogger. */
1279
+ function resolveLogger(logLevelOrLogger) {
1280
+ if (typeof logLevelOrLogger === "number") return new ConsoleLogger(logLevelOrLogger);
1281
+ if (logLevelOrLogger != null && typeof logLevelOrLogger === "object" && typeof logLevelOrLogger.log === "function") return logLevelOrLogger;
1282
+ throw new TypeError("configureLogging expects a LogLevel number or an object with a log(level, message) method.");
1283
+ }
1284
+ //#endregion
1285
+ //#region src/errors.ts
1286
+ /**
1287
+ * errors.ts
1288
+ *
1289
+ * Typed error classes for the SignalR client. Each has a distinct `name`
1290
+ * so callers can use `instanceof` or switch on `error.name` to distinguish
1291
+ * error categories at runtime.
1292
+ */
1293
+ /** Thrown when a hub method invocation fails with a server-side error. */
1294
+ var HubError = class extends Error {
1295
+ name = "HubError";
1296
+ constructor(message) {
1297
+ super(message);
1298
+ Object.setPrototypeOf(this, new.target.prototype);
1299
+ }
1300
+ };
1301
+ /** Thrown when an in-flight operation is cancelled (e.g. connection closed). */
1302
+ var AbortError = class extends Error {
1303
+ name = "AbortError";
1304
+ constructor(message = "The operation was aborted.") {
1305
+ super(message);
1306
+ Object.setPrototypeOf(this, new.target.prototype);
1307
+ }
1308
+ };
1309
+ /** Thrown on network-level transport failures. */
1310
+ var TransportError = class extends Error {
1311
+ name = "TransportError";
1312
+ statusCode;
1313
+ constructor(message, statusCode) {
1314
+ super(message);
1315
+ this.statusCode = statusCode;
1316
+ Object.setPrototypeOf(this, new.target.prototype);
1317
+ }
1318
+ };
1319
+ /** Thrown when the SignalR protocol handshake is rejected by the server. */
1320
+ var HandshakeError = class extends Error {
1321
+ name = "HandshakeError";
1322
+ constructor(message) {
1323
+ super(message);
1324
+ Object.setPrototypeOf(this, new.target.prototype);
1325
+ }
1326
+ };
1327
+ /** Thrown when no acceptable transport could be negotiated. */
1328
+ var UnsupportedTransportError = class extends Error {
1329
+ name = "UnsupportedTransportError";
1330
+ transport;
1331
+ constructor(message, transport) {
1332
+ super(message);
1333
+ this.transport = transport;
1334
+ Object.setPrototypeOf(this, new.target.prototype);
1335
+ }
1336
+ };
1337
+ function isHubError(e) {
1338
+ return e instanceof HubError;
1339
+ }
1340
+ function isAbortError(e) {
1341
+ return e instanceof AbortError;
1342
+ }
1343
+ function isTransportError(e) {
1344
+ return e instanceof TransportError;
1345
+ }
1346
+ //#endregion
1347
+ //#region src/hub-connection.ts
1348
+ /**
1349
+ * hub-connection.ts
1350
+ *
1351
+ * Core HubConnection - the main object users interact with.
1352
+ *
1353
+ * Mirrors the Microsoft SignalR JavaScript client API with full TypeScript types:
1354
+ * start() / stop()
1355
+ * invoke<T>(method, ...args) → Promise<T>
1356
+ * send(method, ...args) → Promise<void>
1357
+ * stream<T>(method, ...args) → IStreamResult<T>
1358
+ * on(method, handler)
1359
+ * off(method, [handler])
1360
+ * onclose / onreconnecting / onreconnected
1361
+ */
1362
+ const MAX_REDIRECTS = 100;
1363
+ var StreamSubscription = class {
1364
+ #disposed = false;
1365
+ #cancel;
1366
+ constructor(cancel) {
1367
+ this.#cancel = cancel;
1368
+ }
1369
+ dispose() {
1370
+ if (!this.#disposed) {
1371
+ this.#disposed = true;
1372
+ this.#cancel();
1373
+ }
1374
+ }
1375
+ /** TS 5.2+ `using` keyword support. */
1376
+ [Symbol.dispose]() {
1377
+ this.dispose();
1378
+ }
1379
+ };
1380
+ var HubConnection = class {
1381
+ #url;
1382
+ #logger;
1383
+ #accessTokenFactory;
1384
+ #requestedTransport;
1385
+ #extraHeaders;
1386
+ #skipNegotiation;
1387
+ #serverTimeout;
1388
+ #pingInterval;
1389
+ #handshakeTimeout;
1390
+ #reconnectPolicy;
1391
+ #protocol;
1392
+ #http;
1393
+ #dispatcher;
1394
+ #ownsDispatcher;
1395
+ #maximumReceiveMessageSize;
1396
+ #transport = null;
1397
+ #connectionId = null;
1398
+ #connectionToken = null;
1399
+ #state = HubConnectionState.Disconnected;
1400
+ #stopping = false;
1401
+ /** Pending `invoke()` calls awaiting a Completion message. */
1402
+ #callbacks = /* @__PURE__ */ new Map();
1403
+ /** Active server-streaming subscriptions awaiting StreamItem/Completion. */
1404
+ #streamCbs = /* @__PURE__ */ new Map();
1405
+ /** Client-method handlers registered via `on()`. */
1406
+ #methods = /* @__PURE__ */ new Map();
1407
+ /** Registered `onclose` callbacks. */
1408
+ #closedCallbacks = [];
1409
+ /** Registered `onreconnecting` callbacks. */
1410
+ #reconnectingCallbacks = [];
1411
+ /** Registered `onreconnected` callbacks. */
1412
+ #reconnectedCallbacks = [];
1413
+ #pingTimer = null;
1414
+ #serverTimeoutTimer = null;
1415
+ #handshakeTimer = null;
1416
+ #handshakeResolver = null;
1417
+ #handshakeRejecter = null;
1418
+ #handshakeBuffer = Buffer.alloc(0);
1419
+ #dispatcherClosePromise = null;
1420
+ #invocationSeq = 0;
1421
+ constructor(url, options = {}) {
1422
+ this.#url = url;
1423
+ this.#logger = options.logger ?? NullLogger.instance;
1424
+ this.#accessTokenFactory = options.accessTokenFactory ?? null;
1425
+ this.#requestedTransport = options.transport ?? HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling;
1426
+ this.#extraHeaders = options.headers ?? {};
1427
+ this.#skipNegotiation = options.skipNegotiation ?? false;
1428
+ this.#serverTimeout = options.serverTimeoutInMilliseconds ?? 3e4;
1429
+ this.#pingInterval = options.keepAliveIntervalInMilliseconds ?? 15e3;
1430
+ this.#handshakeTimeout = options.handshakeTimeoutInMilliseconds ?? 15e3;
1431
+ this.#reconnectPolicy = options.reconnectPolicy ?? null;
1432
+ this.#protocol = options.protocol ?? new JsonHubProtocol();
1433
+ this.#dispatcher = options.dispatcher;
1434
+ this.#ownsDispatcher = options.ownsDispatcher ?? false;
1435
+ this.#maximumReceiveMessageSize = options.maximumReceiveMessageSize ?? 33554432;
1436
+ this.#http = options.httpClient ?? new DispatchHttpClient({
1437
+ headers: this.#extraHeaders,
1438
+ ...options.dispatcher !== void 0 && { dispatcher: options.dispatcher }
1439
+ });
1440
+ }
1441
+ get state() {
1442
+ return this.#state;
1443
+ }
1444
+ get connectionId() {
1445
+ return this.#connectionId;
1446
+ }
1447
+ async start() {
1448
+ if (this.#state !== HubConnectionState.Disconnected) throw new Error(`Cannot start: connection is not Disconnected (current: ${this.#state}).`);
1449
+ this.#state = HubConnectionState.Connecting;
1450
+ this.#stopping = false;
1451
+ this.#logger.log(LogLevel.Debug, "Starting HubConnection.");
1452
+ try {
1453
+ await this.#startInternal();
1454
+ this.#state = HubConnectionState.Connected;
1455
+ this.#logger.log(LogLevel.Information, "HubConnection connected.");
1456
+ } catch (err) {
1457
+ this.#clearTimers();
1458
+ this.#handshakeResolver = null;
1459
+ this.#handshakeRejecter = null;
1460
+ this.#handshakeBuffer = Buffer.alloc(0);
1461
+ if (this.#transport) {
1462
+ this.#transport.onclose = null;
1463
+ await this.#transport.stop().catch(
1464
+ /* istanbul ignore next -- cleanup is best-effort */
1465
+ () => {}
1466
+ );
1467
+ this.#transport = null;
1468
+ }
1469
+ this.#state = HubConnectionState.Disconnected;
1470
+ this.#logger.log(LogLevel.Error, `Failed to start: ${err.message}`);
1471
+ throw err;
1472
+ }
1473
+ }
1474
+ async stop() {
1475
+ if (this.#state === HubConnectionState.Disconnected) return;
1476
+ this.#stopping = true;
1477
+ this.#state = HubConnectionState.Disconnecting;
1478
+ this.#logger.log(LogLevel.Debug, "Stopping HubConnection.");
1479
+ this.#clearTimers();
1480
+ const stopErr = new AbortError("Connection stopped before the invocation completed.");
1481
+ this.#rejectAllPending(stopErr);
1482
+ /* istanbul ignore else -- non-disconnected connections normally own a transport */
1483
+ if (this.#transport) {
1484
+ this.#transport.onclose = null;
1485
+ try {
1486
+ await this.#transport.stop();
1487
+ } catch {}
1488
+ this.#transport = null;
1489
+ }
1490
+ this.#completeClose();
1491
+ await this.#closeOwnedDispatcher();
1492
+ this.#logger.log(LogLevel.Information, "HubConnection stopped.");
1493
+ }
1494
+ /**
1495
+ * Invoke a hub method and return the server's result.
1496
+ *
1497
+ * @typeParam T The expected return type. Asserted at runtime - the server
1498
+ * decides the actual value.
1499
+ */
1500
+ invoke(methodName, ...args) {
1501
+ this.#assertConnected("invoke");
1502
+ const id = this.#nextId();
1503
+ const msg = this.#encode(JsonHubProtocol.invocation(id, methodName, args));
1504
+ return new Promise((resolve, reject) => {
1505
+ this.#callbacks.set(id, {
1506
+ resolve,
1507
+ reject
1508
+ });
1509
+ this.#send(msg).catch((err) => {
1510
+ this.#callbacks.delete(id);
1511
+ reject(err);
1512
+ });
1513
+ });
1514
+ }
1515
+ /**
1516
+ * Fire-and-forget hub method invocation.
1517
+ * The server does not send a completion message.
1518
+ */
1519
+ send(methodName, ...args) {
1520
+ this.#assertConnected("send");
1521
+ const msg = this.#encode(JsonHubProtocol.send(methodName, args));
1522
+ return this.#send(msg);
1523
+ }
1524
+ /**
1525
+ * Start a server-streaming invocation immediately.
1526
+ * The returned `ISubscription` implements `Symbol.dispose` for `using`.
1527
+ *
1528
+ * @typeParam T Type of each streamed item.
1529
+ *
1530
+ * @example
1531
+ * ```ts
1532
+ * using sub = connection.stream<number>('Counter', 10).subscribe({
1533
+ * next: (n) => console.log(n),
1534
+ * complete: () => console.log('done'),
1535
+ * });
1536
+ * ```
1537
+ */
1538
+ stream(methodName, ...args) {
1539
+ this.#assertConnected("stream");
1540
+ const id = this.#nextId();
1541
+ const queued = [];
1542
+ let activeSubscriber = null;
1543
+ let subscribed = false;
1544
+ const deliver = (notification) => {
1545
+ if (!activeSubscriber) {
1546
+ queued.push(notification);
1547
+ return;
1548
+ }
1549
+ if (notification.kind === "next") activeSubscriber.next?.(notification.value);
1550
+ else if (notification.kind === "error") activeSubscriber.error?.(notification.error);
1551
+ else activeSubscriber.complete?.();
1552
+ };
1553
+ this.#streamCbs.set(id, {
1554
+ next: (value) => deliver({
1555
+ kind: "next",
1556
+ value
1557
+ }),
1558
+ error: (error) => deliver({
1559
+ kind: "error",
1560
+ error
1561
+ }),
1562
+ complete: () => deliver({ kind: "complete" })
1563
+ });
1564
+ const msg = this.#encode(JsonHubProtocol.streamInvocation(id, methodName, args));
1565
+ this.#send(msg).catch((error) => {
1566
+ this.#streamCbs.delete(id);
1567
+ deliver({
1568
+ kind: "error",
1569
+ error
1570
+ });
1571
+ });
1572
+ return { subscribe: (subscriber) => {
1573
+ if (subscribed) throw new Error("A stream result can only be subscribed to once.");
1574
+ subscribed = true;
1575
+ activeSubscriber = subscriber;
1576
+ for (const notification of queued.splice(0)) deliver(notification);
1577
+ return new StreamSubscription(() => {
1578
+ if (this.#streamCbs.has(id)) {
1579
+ this.#streamCbs.delete(id);
1580
+ this.#send(this.#encode(JsonHubProtocol.cancelInvocation(id))).catch(
1581
+ /* istanbul ignore next -- cancellation is intentionally best-effort */
1582
+ () => {}
1583
+ );
1584
+ }
1585
+ });
1586
+ } };
1587
+ }
1588
+ /**
1589
+ * Register a handler for a server→client hub method.
1590
+ * Multiple handlers for the same method are supported.
1591
+ *
1592
+ * @typeParam TArgs Tuple of argument types for the hub method.
1593
+ */
1594
+ on(methodName, handler) {
1595
+ const key = methodName.toLowerCase();
1596
+ if (!this.#methods.has(key)) this.#methods.set(key, /* @__PURE__ */ new Set());
1597
+ this.#methods.get(key).add(handler);
1598
+ }
1599
+ /**
1600
+ * Remove a previously registered handler, or all handlers for a method.
1601
+ */
1602
+ off(methodName, handler) {
1603
+ const key = methodName.toLowerCase();
1604
+ if (!this.#methods.has(key)) return;
1605
+ if (handler == null) this.#methods.delete(key);
1606
+ else {
1607
+ this.#methods.get(key).delete(handler);
1608
+ if (this.#methods.get(key).size === 0) this.#methods.delete(key);
1609
+ }
1610
+ }
1611
+ onclose(callback) {
1612
+ this.#closedCallbacks.push(callback);
1613
+ }
1614
+ onreconnecting(callback) {
1615
+ this.#reconnectingCallbacks.push(callback);
1616
+ }
1617
+ onreconnected(callback) {
1618
+ this.#reconnectedCallbacks.push(callback);
1619
+ }
1620
+ async #startInternal() {
1621
+ let url = this.#url;
1622
+ if (this.#skipNegotiation) {
1623
+ if (this.#requestedTransport !== HttpTransportType.WebSockets) throw new Error("skipNegotiation requires WebSockets as the only transport.");
1624
+ this.#transport = this.#createTransport(HttpTransportType.WebSockets);
1625
+ await this.#connectTransport(url, this.#protocol.transferFormat);
1626
+ } else {
1627
+ let redirectCount = 0;
1628
+ for (;;) {
1629
+ const neg = await this.#negotiate(url);
1630
+ if (neg.url) {
1631
+ if (redirectCount++ >= MAX_REDIRECTS) throw new Error("Too many negotiate redirects.");
1632
+ url = neg.url;
1633
+ if (neg.accessToken) {
1634
+ const staticToken = neg.accessToken;
1635
+ this.#overrideAccessToken(() => Promise.resolve(staticToken));
1636
+ }
1637
+ continue;
1638
+ }
1639
+ /* istanbul ignore next -- a successful negotiate response must include connectionId */
1640
+ this.#connectionId = neg.connectionId ?? null;
1641
+ /* istanbul ignore next -- the protocol requires a token or connectionId */
1642
+ this.#connectionToken = neg.connectionToken ?? neg.connectionId ?? null;
1643
+ /* istanbul ignore next -- the protocol requires availableTransports */
1644
+ await this.#selectTransport(url, neg.availableTransports ?? []);
1645
+ break;
1646
+ }
1647
+ }
1648
+ this.#transport.onreceive = (data) => this.#processIncoming(data);
1649
+ this.#transport.onclose = (err) => this.#onTransportClosed(err);
1650
+ await this.#performHandshake();
1651
+ this.#resetPingTimer();
1652
+ this.#resetServerTimeoutTimer();
1653
+ }
1654
+ #_accessTokenFactory = null;
1655
+ #overrideAccessToken(factory) {
1656
+ this.#_accessTokenFactory = factory;
1657
+ }
1658
+ #getAccessTokenFactory() {
1659
+ return this.#_accessTokenFactory ?? this.#accessTokenFactory;
1660
+ }
1661
+ async #negotiate(baseUrl) {
1662
+ const negotiateUrl = new URL(baseUrl);
1663
+ negotiateUrl.pathname = negotiateUrl.pathname.replace(/\/$/, "") + "/negotiate";
1664
+ negotiateUrl.searchParams.set("negotiateVersion", String(1));
1665
+ const url = negotiateUrl.toString();
1666
+ this.#logger.log(LogLevel.Debug, `Sending negotiate request to ${url}.`);
1667
+ const headers = { ...this.#extraHeaders };
1668
+ const factory = this.#getAccessTokenFactory();
1669
+ if (factory) {
1670
+ const token = await factory();
1671
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1672
+ }
1673
+ const res = await this.#http.post(url, { headers });
1674
+ if (res.status !== 200) throw new TransportError(`Unexpected status code ${res.status} from negotiate.`, res.status);
1675
+ if (Buffer.byteLength(res.body, "utf8") > this.#maximumReceiveMessageSize) throw new Error(`Negotiate response exceeds ${this.#maximumReceiveMessageSize} bytes.`);
1676
+ const body = JSON.parse(res.body);
1677
+ if (body.error) throw new Error(`Negotiate returned error: ${body.error}`);
1678
+ return body;
1679
+ }
1680
+ async #selectTransport(url, available) {
1681
+ const ORDERED = [
1682
+ {
1683
+ flag: HttpTransportType.WebSockets,
1684
+ name: "WebSockets"
1685
+ },
1686
+ {
1687
+ flag: HttpTransportType.ServerSentEvents,
1688
+ name: "ServerSentEvents"
1689
+ },
1690
+ {
1691
+ flag: HttpTransportType.LongPolling,
1692
+ name: "LongPolling"
1693
+ }
1694
+ ];
1695
+ /* istanbul ignore next -- both protocol formats are exercised at the protocol boundary */
1696
+ const formatName = this.#protocol.transferFormat === TransferFormat.Binary ? "Binary" : "Text";
1697
+ for (const pref of ORDERED) {
1698
+ if ((this.#requestedTransport & pref.flag) === 0) continue;
1699
+ if (!available.find((candidate) => candidate.transport === pref.name)?.transferFormats.includes(formatName)) continue;
1700
+ this.#logger.log(LogLevel.Debug, `Trying ${pref.name} transport.`);
1701
+ const transport = this.#createTransport(pref.flag);
1702
+ const tUrl = appendParam(url, "id", this.#connectionToken ?? "");
1703
+ try {
1704
+ await transport.connect(tUrl, this.#protocol.transferFormat);
1705
+ this.#transport = transport;
1706
+ return;
1707
+ } catch (err) {
1708
+ this.#logger.log(LogLevel.Warning, `${pref.name} failed: ${err.message}`);
1709
+ }
1710
+ }
1711
+ throw new UnsupportedTransportError("Unable to connect with any of the available transports.", null);
1712
+ }
1713
+ async #connectTransport(url, format) {
1714
+ await this.#transport.connect(url, format);
1715
+ }
1716
+ #createTransport(type) {
1717
+ const factory = this.#getAccessTokenFactory();
1718
+ const headers = this.#extraHeaders;
1719
+ switch (type) {
1720
+ case HttpTransportType.WebSockets: return new WebSocketTransport(factory, this.#logger, headers, this.#dispatcher);
1721
+ case HttpTransportType.ServerSentEvents: return new ServerSentEventsTransport(this.#http, factory, this.#logger, headers);
1722
+ case HttpTransportType.LongPolling: return new LongPollingTransport(this.#http, factory, this.#logger, headers);
1723
+ /* istanbul ignore next */
1724
+ default: throw new UnsupportedTransportError(`Unknown transport type: ${type}`, type);
1725
+ }
1726
+ }
1727
+ #performHandshake() {
1728
+ return new Promise((resolve, reject) => {
1729
+ this.#handshakeResolver = resolve;
1730
+ this.#handshakeRejecter = reject;
1731
+ const request = JSON.stringify({
1732
+ protocol: this.#protocol.name,
1733
+ version: this.#protocol.version
1734
+ }) + "";
1735
+ const fail = (error) => {
1736
+ clearTimeout(this.#handshakeTimer);
1737
+ this.#handshakeResolver = null;
1738
+ this.#handshakeRejecter = null;
1739
+ /* istanbul ignore next -- transport.send rejects with Error */
1740
+ reject(error instanceof Error ? error : new Error(String(error)));
1741
+ };
1742
+ this.#transport.send(request).catch(fail);
1743
+ this.#handshakeTimer = setTimeout(() => {
1744
+ fail(/* @__PURE__ */ new Error("Server timeout: no handshake response received."));
1745
+ }, this.#handshakeTimeout);
1746
+ });
1747
+ }
1748
+ #processIncoming(data) {
1749
+ this.#resetServerTimeoutTimer();
1750
+ /* istanbul ignore next -- binary input is exercised through binary protocol tests */
1751
+ const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
1752
+ if (bytes.byteLength > this.#maximumReceiveMessageSize) {
1753
+ this.#stopWithError(/* @__PURE__ */ new Error(`Incoming message exceeds ${this.#maximumReceiveMessageSize} bytes.`));
1754
+ return;
1755
+ }
1756
+ if (this.#handshakeResolver) {
1757
+ this.#handshakeBuffer = Buffer.concat([this.#handshakeBuffer, bytes]);
1758
+ const separator = this.#handshakeBuffer.indexOf(30);
1759
+ /* istanbul ignore next -- partial handshakes are buffered for the next frame */
1760
+ if (separator === -1) return;
1761
+ try {
1762
+ parseHandshakeResponse(this.#handshakeBuffer.subarray(0, separator + 1).toString("utf8"));
1763
+ const remainder = this.#handshakeBuffer.subarray(separator + 1);
1764
+ this.#handshakeBuffer = Buffer.alloc(0);
1765
+ this.#logger.log(LogLevel.Debug, "Handshake complete.");
1766
+ clearTimeout(this.#handshakeTimer);
1767
+ const r = this.#handshakeResolver;
1768
+ this.#handshakeResolver = null;
1769
+ this.#handshakeRejecter = null;
1770
+ r();
1771
+ /* istanbul ignore next -- servers ordinarily send handshake and hub frames separately */
1772
+ if (remainder.length > 0) this.#processProtocolPayload(remainder);
1773
+ } catch (err) {
1774
+ clearTimeout(this.#handshakeTimer);
1775
+ const r = this.#handshakeRejecter;
1776
+ this.#handshakeResolver = null;
1777
+ this.#handshakeRejecter = null;
1778
+ /* istanbul ignore next -- JSON.parse and protocol validation throw Error objects */
1779
+ r(err instanceof Error ? err : new Error(String(err)));
1780
+ return;
1781
+ }
1782
+ return;
1783
+ }
1784
+ this.#processProtocolPayload(bytes);
1785
+ }
1786
+ #processProtocolPayload(bytes) {
1787
+ /* istanbul ignore next -- transports do not emit empty payloads */
1788
+ if (bytes.length === 0) return;
1789
+ let messages;
1790
+ try {
1791
+ /* istanbul ignore next -- MessagePack parsing is covered directly */
1792
+ const input = this.#protocol.transferFormat === TransferFormat.Text ? Buffer.from(bytes).toString("utf8") : Uint8Array.from(bytes).buffer;
1793
+ messages = this.#protocol.parseMessages(input, this.#logger);
1794
+ } catch (err) {
1795
+ this.#logger.log(LogLevel.Error, `Error parsing messages: ${err.message}`);
1796
+ /* istanbul ignore next -- protocol parsers throw Error objects */
1797
+ this.#stopWithError(err instanceof Error ? err : new Error(String(err)));
1798
+ return;
1799
+ }
1800
+ for (const msg of messages) switch (msg.type) {
1801
+ case MessageType.Invocation:
1802
+ this.#invokeClientMethod(msg);
1803
+ break;
1804
+ case MessageType.StreamItem: {
1805
+ const sc = this.#streamCbs.get(msg.invocationId);
1806
+ if (!sc) break;
1807
+ sc.next(msg.item);
1808
+ break;
1809
+ }
1810
+ case MessageType.Completion: {
1811
+ const cb = this.#callbacks.get(msg.invocationId);
1812
+ if (cb) {
1813
+ this.#callbacks.delete(msg.invocationId);
1814
+ if (msg.error) cb.reject(new HubError(msg.error));
1815
+ else cb.resolve(msg.result ?? null);
1816
+ }
1817
+ const sc = this.#streamCbs.get(msg.invocationId);
1818
+ if (sc) {
1819
+ this.#streamCbs.delete(msg.invocationId);
1820
+ if (msg.error) sc.error(new HubError(msg.error));
1821
+ else sc.complete();
1822
+ }
1823
+ break;
1824
+ }
1825
+ case MessageType.Ping: break;
1826
+ case MessageType.Close: {
1827
+ this.#logger.log(LogLevel.Information, "Server sent Close message.");
1828
+ const closeErr = msg.error ? new HubError(msg.error) : null;
1829
+ if (msg.allowReconnect && this.#reconnectPolicy && !this.#stopping) {
1830
+ this.#clearTimers();
1831
+ /* istanbul ignore next - #transport is always non-null here; null
1832
+ is a defensive guard against theoretical concurrent nulling. */
1833
+ if (this.#transport) this.#transport.onclose = null;
1834
+ const t = this.#transport;
1835
+ this.#transport = null;
1836
+ this.#rejectAllPending(closeErr ?? new AbortError("Connection closed while reconnecting."));
1837
+ t?.stop().catch(
1838
+ /* istanbul ignore next -- shutdown after a server Close is best-effort */
1839
+ () => {}
1840
+ );
1841
+ this.#doReconnect(closeErr);
1842
+ } else this.#stopWithError(closeErr);
1843
+ break;
1844
+ }
1845
+ default: this.#logger.log(LogLevel.Warning, `Unknown message type: ${msg.type}`);
1846
+ }
1847
+ }
1848
+ async #invokeClientMethod(msg) {
1849
+ const handlers = this.#methods.get(msg.target.toLowerCase());
1850
+ if (!handlers || handlers.size === 0) {
1851
+ this.#logger.log(LogLevel.Warning, `No handler for "${msg.target}".`);
1852
+ if (msg.invocationId) await this.#send(this.#encode(JsonHubProtocol.completion(msg.invocationId, void 0, `No client handler registered for '${msg.target}'.`))).catch(
1853
+ /* istanbul ignore next -- completion replies are best-effort */
1854
+ () => {}
1855
+ );
1856
+ return;
1857
+ }
1858
+ let result;
1859
+ let invocationError = null;
1860
+ for (const handler of [...handlers]) try {
1861
+ result = await handler(...msg.arguments);
1862
+ } catch (error) {
1863
+ const err = error instanceof Error ? error : new Error(String(error));
1864
+ invocationError ??= err;
1865
+ this.#logger.log(LogLevel.Error, `Handler for "${msg.target}" threw: ${err.message}`);
1866
+ }
1867
+ if (msg.invocationId) {
1868
+ const completion = invocationError ? {
1869
+ type: MessageType.Completion,
1870
+ invocationId: msg.invocationId,
1871
+ error: invocationError.message
1872
+ } : {
1873
+ type: MessageType.Completion,
1874
+ invocationId: msg.invocationId,
1875
+ ...result !== void 0 && { result }
1876
+ };
1877
+ await this.#send(this.#encode(completion)).catch(
1878
+ /* istanbul ignore next -- completion replies are best-effort */
1879
+ () => {}
1880
+ );
1881
+ }
1882
+ }
1883
+ #onTransportClosed(err) {
1884
+ /* istanbul ignore next - WebSocketTransport.stop() nulls #ws before
1885
+ calling ws.close(), so the close handler never invokes onclose once
1886
+ stop() has set #stopping; this guard is a belt-and-suspenders defence. */
1887
+ if (this.#stopping) return;
1888
+ this.#logger.log(LogLevel.Warning, err ? `Transport closed with error: ${err.message}` : "Transport closed.");
1889
+ this.#clearTimers();
1890
+ this.#rejectAllPending(err ?? new AbortError("Connection closed while reconnecting."));
1891
+ if (this.#reconnectPolicy) this.#doReconnect(err ?? null);
1892
+ else this.#completeClose(err);
1893
+ }
1894
+ #stopWithError(err) {
1895
+ this.#clearTimers();
1896
+ this.#transport?.stop().catch(
1897
+ /* istanbul ignore next -- error shutdown is best-effort */
1898
+ () => {}
1899
+ );
1900
+ this.#transport = null;
1901
+ this.#completeClose(err ?? void 0);
1902
+ }
1903
+ #completeClose(err) {
1904
+ const closeErr = err ?? new AbortError("Connection closed.");
1905
+ this.#rejectAllPending(closeErr);
1906
+ this.#state = HubConnectionState.Disconnected;
1907
+ for (const cb of this.#closedCallbacks) try {
1908
+ cb(err);
1909
+ } catch {}
1910
+ this.#closeOwnedDispatcher();
1911
+ }
1912
+ #rejectAllPending(err) {
1913
+ this.#callbacks.forEach((cb) => cb.reject(err));
1914
+ this.#callbacks.clear();
1915
+ this.#streamCbs.forEach((sc) => sc.error(err));
1916
+ this.#streamCbs.clear();
1917
+ }
1918
+ async #doReconnect(disconnectError) {
1919
+ this.#state = HubConnectionState.Reconnecting;
1920
+ for (const cb of this.#reconnectingCallbacks) try {
1921
+ cb(disconnectError ?? void 0);
1922
+ } catch {}
1923
+ let retryCount = 0;
1924
+ const startTime = Date.now();
1925
+ let lastError = disconnectError;
1926
+ while (!this.#stopping) {
1927
+ const ctx = {
1928
+ previousRetryCount: retryCount,
1929
+ elapsedMilliseconds: Date.now() - startTime,
1930
+ retryReason: lastError
1931
+ };
1932
+ const delay = this.#reconnectPolicy.nextRetryDelayInMilliseconds(ctx);
1933
+ if (delay === null) {
1934
+ this.#logger.log(LogLevel.Debug, "Reconnect policy returned null - giving up.");
1935
+ break;
1936
+ }
1937
+ this.#logger.log(LogLevel.Information, `Reconnecting in ${delay} ms (attempt ${retryCount + 1}).`);
1938
+ await sleep(delay);
1939
+ if (this.#stopping) break;
1940
+ try {
1941
+ await this.#startInternal();
1942
+ this.#state = HubConnectionState.Connected;
1943
+ this.#logger.log(LogLevel.Information, "Reconnected successfully.");
1944
+ for (const cb of this.#reconnectedCallbacks) try {
1945
+ cb(this.#connectionId);
1946
+ } catch {}
1947
+ return;
1948
+ } catch (err) {
1949
+ retryCount++;
1950
+ /* istanbul ignore next - #startInternal() only throws Error instances. */
1951
+ lastError = err instanceof Error ? err : new Error(String(err));
1952
+ this.#logger.log(LogLevel.Warning, `Reconnect attempt ${retryCount} failed: ${lastError.message}`);
1953
+ }
1954
+ }
1955
+ this.#completeClose(lastError ?? void 0);
1956
+ }
1957
+ #resetPingTimer() {
1958
+ clearTimeout(this.#pingTimer);
1959
+ this.#pingTimer = setTimeout(async () => {
1960
+ /* istanbul ignore next - #clearTimers() always cancels this before state
1961
+ leaves Connected; the guard is a defensive fallback. */
1962
+ if (this.#state !== HubConnectionState.Connected) return;
1963
+ try {
1964
+ await this.#send(this.#encode(JsonHubProtocol.ping()));
1965
+ } catch (err) {
1966
+ this.#logger.log(LogLevel.Warning, `Ping failed: ${err.message}`);
1967
+ }
1968
+ this.#resetPingTimer();
1969
+ }, this.#pingInterval);
1970
+ }
1971
+ #resetServerTimeoutTimer() {
1972
+ clearTimeout(this.#serverTimeoutTimer);
1973
+ this.#serverTimeoutTimer = setTimeout(() => {
1974
+ this.#logger.log(LogLevel.Warning, "Server timeout - closing connection.");
1975
+ this.#stopWithError(new AbortError("Server timeout: no message received within the timeout period."));
1976
+ }, this.#serverTimeout);
1977
+ }
1978
+ #clearTimers() {
1979
+ clearTimeout(this.#pingTimer);
1980
+ clearTimeout(this.#serverTimeoutTimer);
1981
+ clearTimeout(this.#handshakeTimer);
1982
+ this.#pingTimer = null;
1983
+ this.#serverTimeoutTimer = null;
1984
+ this.#handshakeTimer = null;
1985
+ }
1986
+ #send(message) {
1987
+ this.#resetPingTimer();
1988
+ return this.#transport.send(message);
1989
+ }
1990
+ #encode(message) {
1991
+ const encoded = this.#protocol.writeMessage(message);
1992
+ return typeof encoded === "string" ? encoded : new Uint8Array(encoded);
1993
+ }
1994
+ #assertConnected(method) {
1995
+ if (this.#state !== HubConnectionState.Connected) throw new Error(`Cannot call "${method}" when connection is not Connected (current: ${this.#state}).`);
1996
+ }
1997
+ #nextId() {
1998
+ return toInvocationId(String(++this.#invocationSeq));
1999
+ }
2000
+ async #closeOwnedDispatcher() {
2001
+ if (!this.#ownsDispatcher || !this.#dispatcher) return;
2002
+ this.#dispatcherClosePromise ??= this.#dispatcher.close().catch(
2003
+ /* istanbul ignore next -- shutdown cleanup is best-effort */
2004
+ () => {}
2005
+ );
2006
+ await this.#dispatcherClosePromise;
2007
+ }
2008
+ };
2009
+ /**
2010
+ * Append a single query-string name/value pair to a URL.
2011
+ *
2012
+ * Uses the WHATWG `URL` API so the operation is always correct regardless of
2013
+ * whether the URL already contains a query string, path segments, or both.
2014
+ * This avoids the classic bug where naive string concatenation appends a path
2015
+ * segment *after* an existing query string, corrupting the URL.
2016
+ *
2017
+ * e.g. appendParam('https://host/hub?token=x', 'id', 'y')
2018
+ * → 'https://host/hub?token=x&id=y' ✓ (not '?token=x&id=y' appended raw)
2019
+ */
2020
+ function appendParam(url, name, value) {
2021
+ const u = new URL(url);
2022
+ u.searchParams.append(name, value);
2023
+ return u.toString();
2024
+ }
2025
+ function sleep(ms) {
2026
+ return new Promise((resolve) => setTimeout(resolve, ms));
2027
+ }
2028
+ //#endregion
2029
+ //#region src/hub-connection-builder.ts
2030
+ /**
2031
+ * hub-connection-builder.ts
2032
+ *
2033
+ * Fluent builder for HubConnection - mirrors the official Microsoft client API.
2034
+ * Uses `const` type parameters (TS 5.0+) and `NoInfer<T>` (TS 5.4+) where
2035
+ * applicable to preserve literal types and prevent unintended widening.
2036
+ *
2037
+ * Usage
2038
+ * ─────
2039
+ * const connection = new HubConnectionBuilder()
2040
+ * .withUrl('https://example.com/hub', {
2041
+ * accessTokenFactory: () => fetchToken(),
2042
+ * transport: HttpTransportType.WebSockets,
2043
+ * })
2044
+ * .configureLogging(LogLevel.Information)
2045
+ * .withAutomaticReconnect()
2046
+ * .build();
2047
+ */
2048
+ /** Default reconnect delay sequence in ms: 0, 2 s, 10 s, 30 s then give up. */
2049
+ const DEFAULT_RETRY_DELAYS = [
2050
+ 0,
2051
+ 2e3,
2052
+ 1e4,
2053
+ 3e4
2054
+ ];
2055
+ var HubConnectionBuilder = class {
2056
+ #url = null;
2057
+ #urlOptions = {};
2058
+ #logger = null;
2059
+ #reconnectPolicy = null;
2060
+ #dispatcher = null;
2061
+ #httpClient = null;
2062
+ #cookieJar = null;
2063
+ #protocol = null;
2064
+ withUrl(url, options) {
2065
+ if (!url || typeof url !== "string") throw new TypeError("withUrl requires a non-empty string URL.");
2066
+ this.#url = url;
2067
+ this.#urlOptions = options ?? {};
2068
+ return this;
2069
+ }
2070
+ /**
2071
+ * Enable automatic cookie handling for the entire connection session using
2072
+ * a `tough-cookie` `CookieJar` and an `@exhumer/undici-cookie-agent` `CookieAgent`.
2073
+ *
2074
+ * When enabled, the `CookieAgent` wraps every outgoing request - including
2075
+ * the `/negotiate` POST, WebSocket upgrade, SSE long-lived GET, and all
2076
+ * long-polling requests - so that:
2077
+ *
2078
+ * 1. `Set-Cookie` headers in the **negotiate response** (and any subsequent
2079
+ * response) are stored in the jar automatically.
2080
+ * 2. All subsequent requests from the same session include the matching
2081
+ * `Cookie` header, exactly as a browser would.
2082
+ *
2083
+ * @param jar
2084
+ * An existing `CookieJar` to use. Pass your own jar when you need to
2085
+ * pre-seed cookies (e.g. an auth session cookie obtained before calling
2086
+ * `start()`) or inspect the jar after the session ends.
2087
+ * When omitted a new, empty jar is created automatically.
2088
+ *
2089
+ * @throws if {@link withDispatcher} has already been called. To combine a
2090
+ * custom dispatcher (e.g. `ProxyAgent`) with cookie support, compose the
2091
+ * `cookie` interceptor onto your dispatcher and pass it to `withDispatcher()`:
2092
+ *
2093
+ * ```ts
2094
+ * import { ProxyAgent } from 'undici';
2095
+ * import { cookie, CookieJar } from '@exhumer/signalr-client';
2096
+ *
2097
+ * const jar = new CookieJar();
2098
+ * const agent = new ProxyAgent('http://proxy:8080').compose(cookie({ jar }));
2099
+ *
2100
+ * const conn = new HubConnectionBuilder()
2101
+ * .withUrl('https://example.com/hub')
2102
+ * .withDispatcher(agent)
2103
+ * .build();
2104
+ * ```
2105
+ *
2106
+ * @example
2107
+ * ```ts
2108
+ * // Automatic jar - no pre-configuration needed
2109
+ * const conn = new HubConnectionBuilder()
2110
+ * .withUrl('https://example.com/hub')
2111
+ * .withCookies()
2112
+ * .build();
2113
+ *
2114
+ * // Pre-seeded jar - inject a session cookie before connecting
2115
+ * import { CookieJar } from '@exhumer/signalr-client';
2116
+ * const jar = new CookieJar();
2117
+ * await jar.setCookie('session=abc123', 'https://example.com');
2118
+ *
2119
+ * const conn = new HubConnectionBuilder()
2120
+ * .withUrl('https://example.com/hub')
2121
+ * .withCookies(jar)
2122
+ * .build();
2123
+ * ```
2124
+ */
2125
+ withCookies(jar) {
2126
+ if (this.#dispatcher !== null) throw new Error("withCookies() cannot be used after withDispatcher(). To combine a custom dispatcher with cookie support, compose the cookie interceptor from @exhumer/undici-cookie-agent onto your dispatcher and pass it to withDispatcher() instead.");
2127
+ this.#cookieJar = jar ?? new CookieJar$1();
2128
+ return this;
2129
+ }
2130
+ /**
2131
+ * Set the undici `Dispatcher` for the entire connection session.
2132
+ *
2133
+ * The same dispatcher is used for:
2134
+ * • the `/negotiate` HTTP POST
2135
+ * • the WebSocket upgrade request
2136
+ * • SSE long-lived GET and send POSTs
2137
+ * • long-polling GETs, POSTs, and DELETE
2138
+ *
2139
+ * Accepts any `Dispatcher` subclass: `Agent`, `Pool`, `Client`,
2140
+ * `ProxyAgent`, `MockAgent`, etc.
2141
+ *
2142
+ * To combine a custom dispatcher with automatic cookie handling, use
2143
+ * `createCookieAgentClass()` to build a cookie-aware variant of your
2144
+ * dispatcher and pass that here. Do **not** chain both
2145
+ * `withDispatcher()` and `withCookies()` - they are mutually exclusive
2146
+ * because `withCookies()` creates its own dispatcher internally.
2147
+ *
2148
+ * @example
2149
+ * ```ts
2150
+ * import { ProxyAgent } from 'undici';
2151
+ *
2152
+ * const conn = new HubConnectionBuilder()
2153
+ * .withUrl('https://example.com/hub')
2154
+ * .withDispatcher(new ProxyAgent('http://proxy:8080'))
2155
+ * .build();
2156
+ * ```
2157
+ */
2158
+ withDispatcher(dispatcher) {
2159
+ if (this.#cookieJar !== null) throw new Error("withDispatcher() cannot be used after withCookies(). To combine a custom dispatcher with cookie support, compose the cookie interceptor from @exhumer/undici-cookie-agent onto your dispatcher and pass it to withDispatcher() instead.");
2160
+ this.#dispatcher = dispatcher;
2161
+ return this;
2162
+ }
2163
+ /**
2164
+ * Provide a custom HTTP client implementation.
2165
+ *
2166
+ * By default `HubConnection` uses `DispatchHttpClient` (undici `dispatch`).
2167
+ * Use this to substitute any of the other undici-backed clients, or
2168
+ * to inject a mock for testing.
2169
+ *
2170
+ * @example
2171
+ * ```ts
2172
+ * import { FetchHttpClient } from '@signalr/client';
2173
+ *
2174
+ * const conn = new HubConnectionBuilder()
2175
+ * .withUrl('https://example.com/hub')
2176
+ * .withHttpClient(new FetchHttpClient())
2177
+ * .build();
2178
+ * ```
2179
+ */
2180
+ withHttpClient(httpClient) {
2181
+ this.#httpClient = httpClient;
2182
+ return this;
2183
+ }
2184
+ /** Select the hub wire protocol (for example, MessagePack). */
2185
+ withHubProtocol(protocol) {
2186
+ this.#protocol = protocol;
2187
+ return this;
2188
+ }
2189
+ /**
2190
+ * Set the minimum log level or provide a custom logger.
2191
+ *
2192
+ * @param logLevelOrLogger
2193
+ * - `LogLevel` number → uses the built-in ConsoleLogger
2194
+ * - `ILogger` object → used as-is
2195
+ */
2196
+ configureLogging(logLevelOrLogger) {
2197
+ this.#logger = resolveLogger(logLevelOrLogger);
2198
+ return this;
2199
+ }
2200
+ withAutomaticReconnect(arg) {
2201
+ if (arg === void 0) this.#reconnectPolicy = new DefaultReconnectPolicy([...DEFAULT_RETRY_DELAYS]);
2202
+ else if (Array.isArray(arg)) this.#reconnectPolicy = new DefaultReconnectPolicy(arg);
2203
+ else if (typeof arg === "object" && typeof arg.nextRetryDelayInMilliseconds === "function") this.#reconnectPolicy = arg;
2204
+ else throw new TypeError("withAutomaticReconnect expects no args, a number[], or an IRetryPolicy.");
2205
+ return this;
2206
+ }
2207
+ build() {
2208
+ if (!this.#url) throw new Error("withUrl() must be called before build().");
2209
+ const effectiveDispatcher = this.#cookieJar !== null ? new CookieAgent$1({ cookies: { jar: this.#cookieJar } }) : this.#dispatcher;
2210
+ const options = {
2211
+ ...this.#logger != null && { logger: this.#logger },
2212
+ ...this.#urlOptions.accessTokenFactory != null && { accessTokenFactory: this.#urlOptions.accessTokenFactory },
2213
+ ...this.#urlOptions.transport != null && { transport: this.#urlOptions.transport },
2214
+ ...this.#urlOptions.headers != null && { headers: this.#urlOptions.headers },
2215
+ ...this.#urlOptions.skipNegotiation != null && { skipNegotiation: this.#urlOptions.skipNegotiation },
2216
+ ...this.#urlOptions.serverTimeoutInMilliseconds != null && { serverTimeoutInMilliseconds: this.#urlOptions.serverTimeoutInMilliseconds },
2217
+ ...this.#urlOptions.keepAliveIntervalInMilliseconds != null && { keepAliveIntervalInMilliseconds: this.#urlOptions.keepAliveIntervalInMilliseconds },
2218
+ ...this.#urlOptions.handshakeTimeoutInMilliseconds != null && { handshakeTimeoutInMilliseconds: this.#urlOptions.handshakeTimeoutInMilliseconds },
2219
+ ...this.#urlOptions.maximumReceiveMessageSize != null && { maximumReceiveMessageSize: this.#urlOptions.maximumReceiveMessageSize },
2220
+ ...this.#reconnectPolicy != null && { reconnectPolicy: this.#reconnectPolicy },
2221
+ ...effectiveDispatcher != null && { dispatcher: effectiveDispatcher },
2222
+ ...this.#httpClient != null && { httpClient: this.#httpClient },
2223
+ ...this.#protocol != null && { protocol: this.#protocol },
2224
+ ...this.#cookieJar != null && { ownsDispatcher: true }
2225
+ };
2226
+ return new HubConnection(this.#url, options);
2227
+ }
2228
+ };
2229
+ var DefaultReconnectPolicy = class {
2230
+ #retryDelays;
2231
+ constructor(retryDelays = [...DEFAULT_RETRY_DELAYS]) {
2232
+ this.#retryDelays = retryDelays;
2233
+ }
2234
+ nextRetryDelayInMilliseconds(context) {
2235
+ return this.#retryDelays[context.previousRetryCount] ?? null;
2236
+ }
2237
+ };
2238
+ //#endregion
2239
+ //#region src/protocols/msgpack-hub-protocol.ts
2240
+ /**
2241
+ * msgpack-hub-protocol.ts
2242
+ *
2243
+ * Implements the SignalR Hub Protocol over MessagePack (version 1).
2244
+ *
2245
+ * Wire format
2246
+ * ───────────
2247
+ * • Handshake: JSON text terminated by ASCII 0x1e - identical to the JSON
2248
+ * protocol, only the "protocol" field changes to "messagepack".
2249
+ * • All subsequent messages: binary MessagePack, each frame prefixed by a
2250
+ * base-128 VarInt carrying the byte-length of the following MessagePack
2251
+ * payload. A single transport payload may contain multiple such frames.
2252
+ *
2253
+ * MessagePack array layout (per message type)
2254
+ * ────────────────────────────────────────────
2255
+ * Invocation [1, headers, invocationId|null, target, args, streamIds]
2256
+ * StreamItem [2, headers, invocationId, item]
2257
+ * Completion [3, headers, invocationId, resultKind, value?]
2258
+ * resultKind: 1 = error string, 2 = void, 3 = non-void result value
2259
+ * StreamInvocation [4, headers, invocationId, target, args, streamIds]
2260
+ * CancelInvocation [5, headers, invocationId]
2261
+ * Ping [6]
2262
+ * Close [7, error|null, allowReconnect]
2263
+ *
2264
+ * Reference:
2265
+ * https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/HubProtocol.md
2266
+ */
2267
+ /**
2268
+ * The JSON-formatted handshake request the client sends once the transport
2269
+ * is open. The server always responds with JSON ({} on success), regardless
2270
+ * of the chosen hub protocol.
2271
+ */
2272
+ const HANDSHAKE_REQUEST$1 = JSON.stringify({
2273
+ protocol: "messagepack",
2274
+ version: 1
2275
+ }) + "";
2276
+ const ResultKind = Object.freeze({
2277
+ Error: 1,
2278
+ Void: 2,
2279
+ NonVoid: 3
2280
+ });
2281
+ /** Number of bytes needed to write `n` as a base-128 VarInt. */
2282
+ function varIntLength(n) {
2283
+ let len = 1;
2284
+ while (n >= 128) {
2285
+ n >>>= 7;
2286
+ len++;
2287
+ }
2288
+ return len;
2289
+ }
2290
+ /**
2291
+ * Write `n` as a base-128 VarInt (little-endian groups of 7 bits) into
2292
+ * `out`, returning the offset just past the last byte written.
2293
+ */
2294
+ function writeVarInt(n, out) {
2295
+ let i = 0;
2296
+ while (n >= 128) {
2297
+ out[i++] = n & 127 | 128;
2298
+ n >>>= 7;
2299
+ }
2300
+ out[i++] = n;
2301
+ return i;
2302
+ }
2303
+ /** Read a VarInt from `data` starting at `offset`. Returns value + bytes consumed. */
2304
+ function readVarInt(data, offset) {
2305
+ let value = 0;
2306
+ let shift = 0;
2307
+ let bytesRead = 0;
2308
+ let hasMore = true;
2309
+ while (hasMore) {
2310
+ if (offset + bytesRead >= data.length) throw new Error("MessagePack: unexpected end of data reading VarInt frame length.");
2311
+ const byte = data[offset + bytesRead];
2312
+ hasMore = (byte & 128) !== 0;
2313
+ if (bytesRead === 4 && hasMore) throw new Error("MessagePack: VarInt frame length prefix is longer than 5 bytes.");
2314
+ if (bytesRead === 4 && byte > 7) throw new Error("MessagePack: VarInt frame length exceeds the 2 GB SignalR limit.");
2315
+ value |= (byte & 127) << shift;
2316
+ shift += 7;
2317
+ bytesRead++;
2318
+ }
2319
+ return {
2320
+ value,
2321
+ bytesRead
2322
+ };
2323
+ }
2324
+ /**
2325
+ * One encoder reused for every frame. `encode()` from @msgpack/msgpack builds
2326
+ * a new Encoder (and a fresh 2 KB buffer) per call, which dominates the cost of
2327
+ * encoding small hub messages. Encoding is synchronous, so sharing is safe.
2328
+ */
2329
+ const encoder = new Encoder();
2330
+ function pack(arr) {
2331
+ const payload = encoder.encodeSharedRef(arr);
2332
+ const frame = new Uint8Array(varIntLength(payload.length) + payload.length);
2333
+ frame.set(payload, writeVarInt(payload.length, frame));
2334
+ return frame.buffer;
2335
+ }
2336
+ var MsgpackHubProtocol = class {
2337
+ name = "messagepack";
2338
+ version = 1;
2339
+ transferFormat = TransferFormat.Binary;
2340
+ parseMessages(input, logger) {
2341
+ const raw = typeof input === "string" ? new TextEncoder().encode(input).buffer : input;
2342
+ const data = new Uint8Array(raw);
2343
+ if (data.length === 0) return [];
2344
+ const messages = [];
2345
+ let offset = 0;
2346
+ while (offset < data.length) {
2347
+ const { value: msgLen, bytesRead } = readVarInt(data, offset);
2348
+ offset += bytesRead;
2349
+ if (offset + msgLen > data.length) throw new Error(`MessagePack hub message: declared length ${msgLen} B exceeds remaining data (${data.length - offset} B).`);
2350
+ const frame = data.subarray(offset, offset + msgLen);
2351
+ offset += msgLen;
2352
+ const rawArr = decode(frame);
2353
+ if (!Array.isArray(rawArr) || rawArr.length === 0) throw new Error("Invalid MessagePack hub message: top-level value must be a non-empty array.");
2354
+ const msg = this.#coerce(rawArr, logger);
2355
+ if (msg !== null) messages.push(msg);
2356
+ }
2357
+ return messages;
2358
+ }
2359
+ writeMessage(message) {
2360
+ switch (message.type) {
2361
+ case MessageType.Invocation: return pack([
2362
+ MessageType.Invocation,
2363
+ message.headers ?? {},
2364
+ message.invocationId ?? null,
2365
+ message.target,
2366
+ message.arguments,
2367
+ message.streamIds ?? []
2368
+ ]);
2369
+ case MessageType.StreamItem: return pack([
2370
+ MessageType.StreamItem,
2371
+ message.headers ?? {},
2372
+ message.invocationId,
2373
+ message.item
2374
+ ]);
2375
+ case MessageType.Completion: {
2376
+ const hasError = message.error != null;
2377
+ const hasResult = Object.prototype.hasOwnProperty.call(message, "result");
2378
+ if (hasError) return pack([
2379
+ MessageType.Completion,
2380
+ message.headers ?? {},
2381
+ message.invocationId,
2382
+ ResultKind.Error,
2383
+ message.error
2384
+ ]);
2385
+ else if (hasResult) return pack([
2386
+ MessageType.Completion,
2387
+ message.headers ?? {},
2388
+ message.invocationId,
2389
+ ResultKind.NonVoid,
2390
+ message.result
2391
+ ]);
2392
+ else return pack([
2393
+ MessageType.Completion,
2394
+ message.headers ?? {},
2395
+ message.invocationId,
2396
+ ResultKind.Void
2397
+ ]);
2398
+ }
2399
+ case MessageType.StreamInvocation: return pack([
2400
+ MessageType.StreamInvocation,
2401
+ message.headers ?? {},
2402
+ message.invocationId,
2403
+ message.target,
2404
+ message.arguments,
2405
+ message.streamIds ?? []
2406
+ ]);
2407
+ case MessageType.CancelInvocation: return pack([
2408
+ MessageType.CancelInvocation,
2409
+ message.headers ?? {},
2410
+ message.invocationId
2411
+ ]);
2412
+ case MessageType.Ping: return pack([MessageType.Ping]);
2413
+ case MessageType.Close: {
2414
+ const arr = [MessageType.Close, message.error ?? null];
2415
+ if (message.allowReconnect !== void 0) arr.push(message.allowReconnect);
2416
+ return pack(arr);
2417
+ }
2418
+ }
2419
+ }
2420
+ /**
2421
+ * Validate and narrow a raw decoded array into a typed `HubMessage`.
2422
+ * Returns `null` for unknown types - they are logged as warnings.
2423
+ */
2424
+ #coerce(arr, logger) {
2425
+ const type = arr[0];
2426
+ if (typeof type !== "number") throw new Error(`Invalid MessagePack hub message: first element (type) must be a number, got ${JSON.stringify(type)}.`);
2427
+ switch (type) {
2428
+ case MessageType.Invocation: return this.#coerceInvocation(arr);
2429
+ case MessageType.StreamItem: return this.#coerceStreamItem(arr);
2430
+ case MessageType.Completion: return this.#coerceCompletion(arr);
2431
+ case MessageType.StreamInvocation: return this.#coerceStreamInvocation(arr);
2432
+ case MessageType.CancelInvocation: return this.#coerceCancelInvocation(arr);
2433
+ case MessageType.Ping: return this.#coercePing();
2434
+ case MessageType.Close: return this.#coerceClose(arr);
2435
+ default:
2436
+ logger.log(3, `Ignoring MessagePack message with unknown type ${String(type)}.`);
2437
+ return null;
2438
+ }
2439
+ }
2440
+ #coerceInvocation(arr) {
2441
+ const target = arr[3];
2442
+ const args = arr[4];
2443
+ if (typeof target !== "string") throw new Error("Invalid Invocation message: index 3 ('target') must be a string.");
2444
+ if (!Array.isArray(args)) throw new Error("Invalid Invocation message: index 4 ('arguments') must be an array.");
2445
+ const rawId = arr[2];
2446
+ const rawSids = arr[5];
2447
+ const rawHdrs = arr[1];
2448
+ const streamIds = rawSids == null ? [] : parseStreamIds(rawSids);
2449
+ return {
2450
+ type: MessageType.Invocation,
2451
+ target,
2452
+ arguments: args,
2453
+ ...rawId != null && rawId !== "" && { invocationId: parseInvocationId(rawId) },
2454
+ ...streamIds.length > 0 && { streamIds },
2455
+ ...isNonEmptyObject(rawHdrs) && { headers: parseHeaders(rawHdrs) }
2456
+ };
2457
+ }
2458
+ #coerceStreamItem(arr) {
2459
+ const rawId = arr[2];
2460
+ if (rawId == null) throw new Error("Invalid StreamItem message: index 2 ('invocationId') is required.");
2461
+ return {
2462
+ type: MessageType.StreamItem,
2463
+ invocationId: parseInvocationId(rawId),
2464
+ item: arr[3],
2465
+ ...isNonEmptyObject(arr[1]) && { headers: parseHeaders(arr[1]) }
2466
+ };
2467
+ }
2468
+ #coerceCompletion(arr) {
2469
+ const rawId = arr[2];
2470
+ if (rawId == null) throw new Error("Invalid Completion message: index 2 ('invocationId') is required.");
2471
+ const invocationId = parseInvocationId(rawId);
2472
+ const resultKind = arr[3];
2473
+ const hdrs = isNonEmptyObject(arr[1]) ? { headers: parseHeaders(arr[1]) } : {};
2474
+ switch (resultKind) {
2475
+ case ResultKind.Error: return {
2476
+ type: MessageType.Completion,
2477
+ invocationId,
2478
+ error: String(arr[4] ?? ""),
2479
+ ...hdrs
2480
+ };
2481
+ case ResultKind.Void: return {
2482
+ type: MessageType.Completion,
2483
+ invocationId,
2484
+ ...hdrs
2485
+ };
2486
+ case ResultKind.NonVoid: return {
2487
+ type: MessageType.Completion,
2488
+ invocationId,
2489
+ result: arr[4],
2490
+ ...hdrs
2491
+ };
2492
+ default: throw new Error(`Invalid Completion message: unknown resultKind ${String(resultKind)}.`);
2493
+ }
2494
+ }
2495
+ #coerceStreamInvocation(arr) {
2496
+ const rawId = arr[2];
2497
+ const target = arr[3];
2498
+ const args = arr[4];
2499
+ if (rawId == null) throw new Error("Invalid StreamInvocation message: index 2 ('invocationId') is required.");
2500
+ if (typeof target !== "string") throw new Error("Invalid StreamInvocation message: index 3 ('target') must be a string.");
2501
+ if (!Array.isArray(args)) throw new Error("Invalid StreamInvocation message: index 4 ('arguments') must be an array.");
2502
+ const rawSids = arr[5];
2503
+ const streamIds = rawSids == null ? [] : parseStreamIds(rawSids);
2504
+ return {
2505
+ type: MessageType.StreamInvocation,
2506
+ invocationId: parseInvocationId(rawId),
2507
+ target,
2508
+ arguments: args,
2509
+ ...streamIds.length > 0 && { streamIds },
2510
+ ...isNonEmptyObject(arr[1]) && { headers: parseHeaders(arr[1]) }
2511
+ };
2512
+ }
2513
+ #coerceCancelInvocation(arr) {
2514
+ const rawId = arr[2];
2515
+ if (rawId == null) throw new Error("Invalid CancelInvocation message: index 2 ('invocationId') is required.");
2516
+ return {
2517
+ type: MessageType.CancelInvocation,
2518
+ invocationId: parseInvocationId(rawId),
2519
+ ...isNonEmptyObject(arr[1]) && { headers: parseHeaders(arr[1]) }
2520
+ };
2521
+ }
2522
+ #coercePing() {
2523
+ return { type: MessageType.Ping };
2524
+ }
2525
+ #coerceClose(arr) {
2526
+ if (arr[2] != null && typeof arr[2] !== "boolean") throw new Error("Invalid Close message: index 2 ('allowReconnect') must be a boolean.");
2527
+ return {
2528
+ type: MessageType.Close,
2529
+ ...arr[1] != null && { error: String(arr[1]) },
2530
+ ...arr[2] != null && { allowReconnect: arr[2] }
2531
+ };
2532
+ }
2533
+ static invocation(invocationId, target, args, streamIds) {
2534
+ return {
2535
+ type: MessageType.Invocation,
2536
+ invocationId,
2537
+ target,
2538
+ arguments: args,
2539
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
2540
+ };
2541
+ }
2542
+ /** Fire-and-forget (no `invocationId`). */
2543
+ static send(target, args, streamIds) {
2544
+ return {
2545
+ type: MessageType.Invocation,
2546
+ target,
2547
+ arguments: args,
2548
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
2549
+ };
2550
+ }
2551
+ static streamInvocation(invocationId, target, args, streamIds) {
2552
+ return {
2553
+ type: MessageType.StreamInvocation,
2554
+ invocationId,
2555
+ target,
2556
+ arguments: args,
2557
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
2558
+ };
2559
+ }
2560
+ static cancelInvocation(invocationId) {
2561
+ return {
2562
+ type: MessageType.CancelInvocation,
2563
+ invocationId
2564
+ };
2565
+ }
2566
+ static streamItem(invocationId, item) {
2567
+ return {
2568
+ type: MessageType.StreamItem,
2569
+ invocationId,
2570
+ item
2571
+ };
2572
+ }
2573
+ static completion(invocationId, result, error) {
2574
+ return {
2575
+ type: MessageType.Completion,
2576
+ invocationId,
2577
+ ...error != null && { error },
2578
+ ...result != null && { result }
2579
+ };
2580
+ }
2581
+ static ping() {
2582
+ return { type: MessageType.Ping };
2583
+ }
2584
+ };
2585
+ function isNonEmptyObject(v) {
2586
+ return v !== null && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length > 0;
2587
+ }
2588
+ function parseInvocationId(value) {
2589
+ if (typeof value !== "string") throw new Error("Invalid MessagePack message: invocationId must be a string.");
2590
+ return toInvocationId(value);
2591
+ }
2592
+ function parseStreamIds(value) {
2593
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) throw new Error("Invalid MessagePack message: streamIds must be an array of strings.");
2594
+ return value;
2595
+ }
2596
+ function parseHeaders(value) {
2597
+ if (!isNonEmptyObject(value) || !Object.values(value).every((entry) => typeof entry === "string")) throw new Error("Invalid MessagePack message: headers must contain string values.");
2598
+ return value;
2599
+ }
2600
+ //#endregion
2601
+ //#region src/ws-client.ts
2602
+ /**
2603
+ * ws-client.ts
2604
+ *
2605
+ * From-scratch WebSocket client (RFC 6455) built on Node's built-in `net`
2606
+ * and `tls` modules. Zero external runtime dependencies.
2607
+ *
2608
+ * Typed EventEmitter via interface declaration merging.
2609
+ *
2610
+ * Emitted events
2611
+ * 'open' - handshake complete, ready to send/receive
2612
+ * 'message' (string) - UTF-8 text frame received
2613
+ * 'binary' (Buffer) - binary frame received
2614
+ * 'ping' (Buffer) - ping frame received (pong sent automatically)
2615
+ * 'pong' (Buffer) - pong frame received
2616
+ * 'close' - socket fully closed
2617
+ * 'error' (Error) - socket-level error
2618
+ */
2619
+ let WebSocketReadyState = /* @__PURE__ */ function(WebSocketReadyState) {
2620
+ WebSocketReadyState[WebSocketReadyState["Connecting"] = 0] = "Connecting";
2621
+ WebSocketReadyState[WebSocketReadyState["Open"] = 1] = "Open";
2622
+ WebSocketReadyState[WebSocketReadyState["Closing"] = 2] = "Closing";
2623
+ WebSocketReadyState[WebSocketReadyState["Closed"] = 3] = "Closed";
2624
+ return WebSocketReadyState;
2625
+ }({});
2626
+ const WS_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
2627
+ var WebSocketClient = class extends EventEmitter {
2628
+ #socket = null;
2629
+ #state = 3;
2630
+ #frameBuf = Buffer.alloc(0);
2631
+ #fragments = [];
2632
+ #fragmentOpcode = null;
2633
+ #handshakeBuf = Buffer.alloc(0);
2634
+ #handshakeResolve = null;
2635
+ #handshakeReject = null;
2636
+ get readyState() {
2637
+ return this.#state;
2638
+ }
2639
+ /**
2640
+ * Open a WebSocket connection.
2641
+ * @param url ws:// or wss:// URL
2642
+ * @param headers Extra HTTP headers for the upgrade request
2643
+ */
2644
+ connect(url, headers = {}) {
2645
+ return new Promise((resolve, reject) => {
2646
+ if (this.#state !== 3) {
2647
+ reject(/* @__PURE__ */ new Error("WebSocket is already open or connecting."));
2648
+ return;
2649
+ }
2650
+ this.#state = 0;
2651
+ this.#handshakeResolve = resolve;
2652
+ this.#handshakeReject = reject;
2653
+ const parsed = new URL(url);
2654
+ const isSecure = parsed.protocol === "wss:";
2655
+ const host = parsed.hostname;
2656
+ const port = Number(parsed.port) || (isSecure ? 443 : 80);
2657
+ /* istanbul ignore next -- URL normalizes an empty pathname to '/' */
2658
+ const resource = (parsed.pathname || "/") + (parsed.search || "");
2659
+ const portSuffix = parsed.port ? `:${parsed.port}` : "";
2660
+ const nonce = crypto.randomBytes(16).toString("base64");
2661
+ const expected = computeAccept(nonce);
2662
+ const request = [
2663
+ `GET ${resource} HTTP/1.1`,
2664
+ `Host: ${host}${portSuffix}`,
2665
+ "Upgrade: websocket",
2666
+ "Connection: Upgrade",
2667
+ `Sec-WebSocket-Key: ${nonce}`,
2668
+ "Sec-WebSocket-Version: 13",
2669
+ ...Object.entries(headers).map(([k, v]) => `${k}: ${v}`),
2670
+ "",
2671
+ ""
2672
+ ].join("\r\n");
2673
+ const onSocketData = (chunk) => {
2674
+ this.#onHandshakeData(chunk, expected);
2675
+ };
2676
+ const socketOpts = {
2677
+ host,
2678
+ port
2679
+ };
2680
+ if (isSecure) this.#socket = tls.connect(
2681
+ {
2682
+ ...socketOpts,
2683
+ servername: host
2684
+ },
2685
+ /* istanbul ignore next -- identical write callback is covered via TCP */
2686
+ () => {
2687
+ this.#socket.write(request);
2688
+ }
2689
+ );
2690
+ else this.#socket = net.connect(socketOpts, () => {
2691
+ this.#socket.write(request);
2692
+ });
2693
+ this.#socket.on("data", onSocketData);
2694
+ this.#socket.on("error", (err) => {
2695
+ if (this.#state === 0) {
2696
+ this.#state = 3;
2697
+ this.#handshakeReject(err);
2698
+ } else if (this.#state !== 2) this.emit("error", err);
2699
+ });
2700
+ this.#socket.on("close", () => this.#onSocketClose());
2701
+ this.#socket.on("end", () => this.#socket?.destroy());
2702
+ });
2703
+ }
2704
+ /**
2705
+ * Send a text or binary message.
2706
+ * @throws if the socket is not open.
2707
+ */
2708
+ send(data) {
2709
+ if (this.#state !== 1) throw new Error("WebSocket is not open.");
2710
+ if (typeof data === "string") this.#writeFrame(1, Buffer.from(data, "utf8"));
2711
+ else this.#writeFrame(2, Buffer.isBuffer(data) ? data : Buffer.from(data));
2712
+ }
2713
+ /**
2714
+ * Initiate a clean close handshake.
2715
+ * @param code WebSocket close code (default 1000 = Normal Closure)
2716
+ * @param reason UTF-8 reason string (max 123 bytes)
2717
+ */
2718
+ close(code = 1e3, reason = "") {
2719
+ if (this.#state !== 1) return;
2720
+ this.#state = 2;
2721
+ const reasonBuf = Buffer.from(reason, "utf8");
2722
+ const payload = Buffer.allocUnsafe(2 + reasonBuf.length);
2723
+ payload.writeUInt16BE(code, 0);
2724
+ reasonBuf.copy(payload, 2);
2725
+ this.#writeFrame(8, payload);
2726
+ }
2727
+ #onHandshakeData(chunk, expectedAccept) {
2728
+ this.#handshakeBuf = Buffer.concat([this.#handshakeBuf, chunk]);
2729
+ const idx = indexOfCRLFCRLF(this.#handshakeBuf);
2730
+ if (idx === -1) return;
2731
+ const headerStr = this.#handshakeBuf.subarray(0, idx).toString("ascii");
2732
+ const remainder = this.#handshakeBuf.subarray(idx + 4);
2733
+ const lines = headerStr.split("\r\n");
2734
+ /* istanbul ignore next -- split() always returns at least one element */
2735
+ const statusLine = lines[0] ?? "";
2736
+ if (!statusLine.includes("101")) {
2737
+ this.#failHandshake(/* @__PURE__ */ new Error(`WebSocket upgrade failed - server replied: ${statusLine}`));
2738
+ return;
2739
+ }
2740
+ let accept = "";
2741
+ for (let i = 1; i < lines.length; i++) {
2742
+ const line = lines[i];
2743
+ const colon = line.indexOf(":");
2744
+ if (colon === -1) continue;
2745
+ if (line.slice(0, colon).trim().toLowerCase() === "sec-websocket-accept") {
2746
+ accept = line.slice(colon + 1).trim();
2747
+ break;
2748
+ }
2749
+ }
2750
+ if (accept !== expectedAccept) {
2751
+ this.#failHandshake(/* @__PURE__ */ new Error(`Invalid Sec-WebSocket-Accept (got "${accept}", expected "${expectedAccept}")`));
2752
+ return;
2753
+ }
2754
+ this.#state = 1;
2755
+ this.#handshakeBuf = Buffer.alloc(0);
2756
+ this.#socket.removeAllListeners("data");
2757
+ this.#socket.on("data", (c) => this.#onFrameData(c));
2758
+ const r = this.#handshakeResolve;
2759
+ this.#handshakeResolve = null;
2760
+ this.#handshakeReject = null;
2761
+ r();
2762
+ this.emit("open");
2763
+ if (remainder.length > 0) this.#onFrameData(remainder);
2764
+ }
2765
+ #failHandshake(err) {
2766
+ this.#state = 3;
2767
+ this.#socket?.destroy();
2768
+ const r = this.#handshakeReject;
2769
+ this.#handshakeResolve = null;
2770
+ this.#handshakeReject = null;
2771
+ r(err);
2772
+ }
2773
+ #onFrameData(chunk) {
2774
+ this.#frameBuf = Buffer.concat([this.#frameBuf, chunk]);
2775
+ this.#drainFrames();
2776
+ }
2777
+ #drainFrames() {
2778
+ for (;;) {
2779
+ const result = tryParseFrame(this.#frameBuf);
2780
+ if (!result) break;
2781
+ this.#frameBuf = this.#frameBuf.subarray(result.consumed);
2782
+ this.#dispatchFrame(result.frame);
2783
+ }
2784
+ }
2785
+ #dispatchFrame({ fin, opcode, payload }) {
2786
+ switch (opcode) {
2787
+ case 1:
2788
+ case 2:
2789
+ if (!fin) {
2790
+ this.#fragmentOpcode = opcode;
2791
+ this.#fragments = [payload];
2792
+ } else this.#emitData(opcode, payload);
2793
+ break;
2794
+ case 0:
2795
+ this.#fragments.push(payload);
2796
+ if (fin) {
2797
+ const full = Buffer.concat(this.#fragments);
2798
+ const op = this.#fragmentOpcode;
2799
+ this.#fragments = [];
2800
+ this.#fragmentOpcode = null;
2801
+ this.#emitData(op, full);
2802
+ }
2803
+ break;
2804
+ case 9:
2805
+ this.#writeFrame(10, payload);
2806
+ this.emit("ping", payload);
2807
+ break;
2808
+ case 10:
2809
+ this.emit("pong", payload);
2810
+ break;
2811
+ case 8:
2812
+ if (this.#state === 1) this.#writeFrame(8, payload);
2813
+ this.#state = 3;
2814
+ this.#socket?.destroy();
2815
+ this.emit("close");
2816
+ }
2817
+ }
2818
+ #emitData(opcode, payload) {
2819
+ if (opcode === 1) this.emit("message", payload.toString("utf8"));
2820
+ else this.emit("binary", payload);
2821
+ }
2822
+ /**
2823
+ * Encode and send one WebSocket frame.
2824
+ * All client→server frames MUST be masked (RFC 6455 §5.3).
2825
+ */
2826
+ #writeFrame(opcode, payload) {
2827
+ /* istanbul ignore next -- public methods validate state before writing */
2828
+ if (!this.#socket || this.#socket.destroyed) return;
2829
+ const len = payload.length;
2830
+ let header;
2831
+ if (len < 126) {
2832
+ header = Buffer.allocUnsafe(2);
2833
+ header[1] = 128 | len;
2834
+ } else if (len < 65536) {
2835
+ header = Buffer.allocUnsafe(4);
2836
+ header[1] = 254;
2837
+ header.writeUInt16BE(len, 2);
2838
+ } else {
2839
+ header = Buffer.allocUnsafe(10);
2840
+ header[1] = 255;
2841
+ header.writeUInt32BE(0, 2);
2842
+ header.writeUInt32BE(len, 6);
2843
+ }
2844
+ header[0] = 128 | opcode;
2845
+ const maskKey = crypto.randomBytes(4);
2846
+ const maskedPayload = Buffer.allocUnsafe(len);
2847
+ for (let i = 0; i < len; i++) maskedPayload[i] = payload[i] ^ maskKey[i % 4];
2848
+ this.#socket.write(Buffer.concat([
2849
+ header,
2850
+ maskKey,
2851
+ maskedPayload
2852
+ ]));
2853
+ }
2854
+ #onSocketClose() {
2855
+ if (this.#state !== 3) {
2856
+ this.#state = 3;
2857
+ this.emit("close");
2858
+ }
2859
+ }
2860
+ };
2861
+ function computeAccept(key) {
2862
+ return crypto.createHash("sha1").update(key + WS_MAGIC).digest("base64");
2863
+ }
2864
+ function indexOfCRLFCRLF(buf) {
2865
+ for (let i = 0; i <= buf.length - 4; i++) if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i;
2866
+ return -1;
2867
+ }
2868
+ function tryParseFrame(buf) {
2869
+ if (buf.length < 2) return null;
2870
+ const b0 = buf[0];
2871
+ const b1 = buf[1];
2872
+ const fin = (b0 & 128) !== 0;
2873
+ const opcode = b0 & 15;
2874
+ const masked = (b1 & 128) !== 0;
2875
+ let payLen = b1 & 127;
2876
+ let offset = 2;
2877
+ if (payLen === 126) {
2878
+ if (buf.length < offset + 2) return null;
2879
+ payLen = buf.readUInt16BE(offset);
2880
+ offset += 2;
2881
+ } else if (payLen === 127) {
2882
+ if (buf.length < offset + 8) return null;
2883
+ /* istanbul ignore next -- allocating a >4 GB test frame is impractical */
2884
+ if (buf.readUInt32BE(offset) !== 0) throw new Error("Received a frame with payload > 4 GB - not supported.");
2885
+ payLen = buf.readUInt32BE(offset + 4);
2886
+ offset += 8;
2887
+ }
2888
+ let maskBytes = null;
2889
+ if (masked) {
2890
+ if (buf.length < offset + 4) return null;
2891
+ maskBytes = buf.subarray(offset, offset + 4);
2892
+ offset += 4;
2893
+ }
2894
+ if (buf.length < offset + payLen) return null;
2895
+ let payload = buf.subarray(offset, offset + payLen);
2896
+ if (masked && maskBytes !== null) {
2897
+ payload = Buffer.from(payload);
2898
+ for (let i = 0; i < payload.length; i++) payload[i] = payload[i] ^ maskBytes[i % 4];
2899
+ }
2900
+ return {
2901
+ frame: {
2902
+ fin,
2903
+ opcode,
2904
+ payload
2905
+ },
2906
+ consumed: offset + payLen
2907
+ };
2908
+ }
2909
+ //#endregion
2910
+ export { AbortError, ConsoleLogger, CookieAgent, CookieJar, DEFAULT_PING_INTERVAL_IN_MS, DEFAULT_SERVER_TIMEOUT_IN_MS, DEFAULT_TIMEOUT_IN_MS, DefaultReconnectPolicy, DispatchHttpClient, DispatchHttpClient as HttpClient, FetchHttpClient, HANDSHAKE_REQUEST, HandshakeError, HttpTransportType, HubConnection, HubConnectionBuilder, HubConnectionState, HubError, JsonHubProtocol, LogLevel, LongPollingTransport, HANDSHAKE_REQUEST$1 as MSGPACK_HANDSHAKE_REQUEST, MessageType, MsgpackHubProtocol, NEGOTIATE_VERSION, NullLogger, PipelineHttpClient, RECORD_SEPARATOR, RequestHttpClient, ServerSentEventsTransport, StreamHttpClient, TransferFormat, TransportError, UnsupportedTransportError, WebSocketClient, WebSocketReadyState, WebSocketTransport, assertNever, cookie, isAbortError, isCancelInvocationMessage, isCloseMessage, isCompletionMessage, isHubError, isInvocationMessage, isPingMessage, isStreamInvocationMessage, isStreamItemMessage, isTransportError, parseHandshakeResponse, toInvocationId };
2911
+
2912
+ //# sourceMappingURL=index.mjs.map