@exhumer/signalr-client 1.0.2 → 2.0.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,2901 @@
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 { decode, encode } 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 ?? 32 * 1024 * 1024;
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 { statusCode, headers: respHeaders, body } = await request(url, {
171
+ method,
172
+ headers: buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType),
173
+ ...buf !== null && { body: buf },
174
+ headersTimeout: options.timeout ?? this.defaultTimeout,
175
+ dispatcher: this.dispatcher
176
+ });
177
+ return {
178
+ statusCode,
179
+ headers: normaliseHeaders(respHeaders),
180
+ body,
181
+ abort: () => body.destroy()
182
+ };
183
+ }
184
+ };
185
+ /**
186
+ * HTTP client backed by `undici.fetch()`.
187
+ *
188
+ * `undici.fetch` is undici's WHATWG-compatible Fetch implementation.
189
+ * It accepts the same options as the browser `fetch()` API, plus an
190
+ * undici-specific `dispatcher` field for custom transport control.
191
+ *
192
+ * Useful when you need browser API compatibility (e.g., tests that run in
193
+ * both Node and a browser-like environment) or when consuming WHATWG `Response`
194
+ * helpers such as `res.json()` / `res.formData()`.
195
+ *
196
+ * For `stream()` we convert the WHATWG `ReadableStream<Uint8Array>` body to a
197
+ * Node.js `Readable` via `Readable.fromWeb()` (Node 17+).
198
+ */
199
+ var FetchHttpClient = class extends BaseUndiciClient {
200
+ async request(method, url, options = {}) {
201
+ const { buf, inferredContentType } = prepareBody(options.body);
202
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
203
+ const ac = new AbortController();
204
+ const timeout = options.timeout ?? this.defaultTimeout;
205
+ const timer = timeout > 0 ? setTimeout(() => ac.abort(/* @__PURE__ */ new Error(`HTTP request timed out after ${timeout} ms`)), timeout) : null;
206
+ try {
207
+ const res = await fetch(url, {
208
+ method,
209
+ headers,
210
+ ...buf !== null && { body: buf },
211
+ signal: ac.signal,
212
+ dispatcher: this.dispatcher
213
+ });
214
+ const text = await res.text();
215
+ assertBodySize(Buffer.byteLength(text), this.maximumResponseBodySize);
216
+ if (timer !== null) clearTimeout(timer);
217
+ const respHeaders = {};
218
+ res.headers.forEach((value, key) => {
219
+ respHeaders[key] = value;
220
+ });
221
+ return {
222
+ status: res.status,
223
+ headers: respHeaders,
224
+ body: text
225
+ };
226
+ } catch (err) {
227
+ if (timer !== null) clearTimeout(timer);
228
+ throw err;
229
+ }
230
+ }
231
+ async stream(method, url, options = {}) {
232
+ const { buf, inferredContentType } = prepareBody(options.body);
233
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
234
+ const ac = new AbortController();
235
+ const res = await fetch(url, {
236
+ method,
237
+ headers,
238
+ ...buf !== null && { body: buf },
239
+ signal: ac.signal,
240
+ dispatcher: this.dispatcher
241
+ });
242
+ const body = Readable.fromWeb(res.body);
243
+ const respHeaders = {};
244
+ res.headers.forEach((value, key) => {
245
+ respHeaders[key] = value;
246
+ });
247
+ return {
248
+ statusCode: res.status,
249
+ headers: respHeaders,
250
+ body,
251
+ abort: () => {
252
+ ac.abort();
253
+ body.destroy();
254
+ }
255
+ };
256
+ }
257
+ };
258
+ /**
259
+ * HTTP client backed by `undici.stream()`.
260
+ *
261
+ * `undici.stream` uses a *factory callback* pattern: you supply a function
262
+ * that receives `{ statusCode, headers, body }` and must return a
263
+ * `Writable` to which the response body is piped.
264
+ *
265
+ * This makes it ideal when you want to pipe response bytes directly to a
266
+ * destination (e.g., a file, a transform, or a compression stream) without
267
+ * ever holding the entire response in memory.
268
+ *
269
+ * For buffered `request()` we use a `PassThrough` as the sink and collect
270
+ * its output. For streaming `stream()` we expose the PassThrough's
271
+ * readable side as `StreamResult.body`.
272
+ */
273
+ var StreamHttpClient = class extends BaseUndiciClient {
274
+ request(method, url, options = {}) {
275
+ const { buf, inferredContentType } = prepareBody(options.body);
276
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
277
+ const timeout = options.timeout ?? this.defaultTimeout;
278
+ let statusCode = 0;
279
+ let respHeaders = {};
280
+ const chunks = [];
281
+ return stream(url, {
282
+ method,
283
+ headers,
284
+ ...buf !== null && { body: buf },
285
+ headersTimeout: timeout,
286
+ bodyTimeout: timeout,
287
+ dispatcher: this.dispatcher
288
+ }, ({ statusCode: sc, headers: h }) => {
289
+ statusCode = sc;
290
+ respHeaders = h;
291
+ const sink = new PassThrough();
292
+ sink.on("data", (chunk) => chunks.push(chunk));
293
+ return sink;
294
+ }).then(() => ({
295
+ status: statusCode,
296
+ headers: normaliseHeaders(respHeaders),
297
+ body: (() => {
298
+ const body = Buffer.concat(chunks);
299
+ assertBodySize(body.length, this.maximumResponseBodySize);
300
+ return body.toString("utf8");
301
+ })()
302
+ }));
303
+ }
304
+ stream(method, url, options = {}) {
305
+ const { buf, inferredContentType } = prepareBody(options.body);
306
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
307
+ return new Promise((resolve, reject) => {
308
+ const pt = new PassThrough();
309
+ stream(url, {
310
+ method,
311
+ headers,
312
+ ...buf !== null && { body: buf },
313
+ headersTimeout: options.timeout ?? this.defaultTimeout,
314
+ dispatcher: this.dispatcher
315
+ }, ({ statusCode, headers: h }) => {
316
+ resolve({
317
+ statusCode,
318
+ headers: normaliseHeaders(h),
319
+ body: pt,
320
+ abort: () => pt.destroy()
321
+ });
322
+ return pt;
323
+ }).catch((err) => {
324
+ pt.destroy();
325
+ reject(err);
326
+ });
327
+ });
328
+ }
329
+ };
330
+ /**
331
+ * HTTP client backed by `undici.pipeline()`.
332
+ *
333
+ * `undici.pipeline` returns a `stream.Duplex`:
334
+ * - **Writable side**: the request body (write / pipe into it).
335
+ * - **Readable side**: whatever your handler function *returns*.
336
+ *
337
+ * The handler receives `{ statusCode, headers, body }` and must return a
338
+ * `Readable`. Whatever that readable emits becomes the duplex's output.
339
+ *
340
+ * Typical use: response transformation pipelines
341
+ * `pipeline(url, opts, ({ body }) => body.pipe(gunzip()))`
342
+ *
343
+ * For `request()` we return the raw body from the handler, collect the duplex
344
+ * output into a buffer, and resolve with the buffered string.
345
+ *
346
+ * For `stream()` we return the raw body from the handler and give the caller
347
+ * the duplex's readable side directly - zero extra buffering.
348
+ */
349
+ var PipelineHttpClient = class extends BaseUndiciClient {
350
+ request(method, url, options = {}) {
351
+ const { buf, inferredContentType } = prepareBody(options.body);
352
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
353
+ const timeout = options.timeout ?? this.defaultTimeout;
354
+ return new Promise((resolve, reject) => {
355
+ let statusCode = 0;
356
+ let respHeaders = {};
357
+ const chunks = [];
358
+ const duplex = pipeline(url, {
359
+ method,
360
+ headers,
361
+ ...buf !== null && { body: buf },
362
+ headersTimeout: timeout,
363
+ bodyTimeout: timeout,
364
+ dispatcher: this.dispatcher
365
+ }, ({ statusCode: sc, headers: h, body }) => {
366
+ statusCode = sc;
367
+ respHeaders = h;
368
+ return body;
369
+ });
370
+ duplex.on("data", (chunk) => chunks.push(chunk));
371
+ duplex.on("end", () => resolve({
372
+ status: statusCode,
373
+ headers: normaliseHeaders(respHeaders),
374
+ body: (() => {
375
+ const body = Buffer.concat(chunks);
376
+ assertBodySize(body.length, this.maximumResponseBodySize);
377
+ return body.toString("utf8");
378
+ })()
379
+ }));
380
+ duplex.on("error", reject);
381
+ if (buf !== null) duplex.write(buf);
382
+ duplex.end();
383
+ });
384
+ }
385
+ stream(method, url, options = {}) {
386
+ const { buf, inferredContentType } = prepareBody(options.body);
387
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
388
+ return new Promise((resolve, reject) => {
389
+ let resolved = false;
390
+ let duplex;
391
+ duplex = pipeline(url, {
392
+ method,
393
+ headers,
394
+ ...buf !== null && { body: buf },
395
+ headersTimeout: options.timeout ?? this.defaultTimeout,
396
+ dispatcher: this.dispatcher
397
+ }, ({ statusCode, headers: h, body }) => {
398
+ if (!resolved) {
399
+ resolved = true;
400
+ resolve({
401
+ statusCode,
402
+ headers: normaliseHeaders(h),
403
+ body: duplex,
404
+ abort: () => duplex.destroy()
405
+ });
406
+ }
407
+ return body;
408
+ });
409
+ duplex.on("error", (err) => {
410
+ if (!resolved) reject(err);
411
+ });
412
+ if (buf !== null) duplex.write(buf);
413
+ duplex.end();
414
+ });
415
+ }
416
+ };
417
+ /**
418
+ * HTTP client backed by `Dispatcher#dispatch()`.
419
+ *
420
+ * `dispatch` is the *lowest-level* undici API. It exposes the full
421
+ * request/response lifecycle as a handler interface (undici 8+):
422
+ *
423
+ * onRequestStart → request is about to be sent; receives DispatchController
424
+ * onResponseStart → response status + headers received
425
+ * onResponseData → response body chunk
426
+ * onResponseEnd → response fully consumed
427
+ * onResponseError → error at any stage
428
+ *
429
+ * This gives you maximum control: you can abort mid-stream, transform
430
+ * chunks on the fly, or implement custom back-pressure logic.
431
+ *
432
+ * Unlike the other clients, `DispatchHttpClient` *requires* a `Dispatcher`
433
+ * (an `Agent`, `Pool`, or `Client` instance). If none is provided the
434
+ * process-global agent is used.
435
+ *
436
+ * Note: `dispatcher.dispatch()` needs the URL split into `origin` + `path`
437
+ * (for `Agent` / `Pool`) or just `path` (for `Client` whose origin is
438
+ * already bound). We always pass `origin` for maximum compatibility.
439
+ */
440
+ var DispatchHttpClient = class extends BaseUndiciClient {
441
+ request(method, url, options = {}) {
442
+ const { buf, inferredContentType } = prepareBody(options.body);
443
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
444
+ const parsed = new URL(url);
445
+ const timeout = options.timeout ?? this.defaultTimeout;
446
+ return new Promise((resolve, reject) => {
447
+ let statusCode = 0;
448
+ let respHeaders = {};
449
+ const chunks = [];
450
+ let responseSize = 0;
451
+ let controller = null;
452
+ let settled = false;
453
+ let timer = null;
454
+ if (timeout > 0) timer = setTimeout(() => {
455
+ /* istanbul ignore next -- timer is cleared on every settled path */
456
+ if (settled) return;
457
+ settled = true;
458
+ const error = /* @__PURE__ */ new Error(`HTTP request timed out after ${timeout} ms`);
459
+ controller?.abort(error);
460
+ reject(error);
461
+ }, timeout);
462
+ const clearTimer = () => {
463
+ if (timer !== null) {
464
+ clearTimeout(timer);
465
+ timer = null;
466
+ }
467
+ };
468
+ this.dispatcher.dispatch({
469
+ origin: parsed.origin,
470
+ path: parsed.pathname + parsed.search,
471
+ method,
472
+ headers,
473
+ ...buf !== null && { body: buf }
474
+ }, {
475
+ onRequestStart: (ctrl, _context) => {
476
+ controller = ctrl;
477
+ },
478
+ onResponseError: (_controller, err) => {
479
+ clearTimer();
480
+ /* istanbul ignore next -- undici does not end after reporting an error */
481
+ if (settled) return;
482
+ settled = true;
483
+ reject(err);
484
+ },
485
+ onResponseStart: (_controller, sc, headers) => {
486
+ statusCode = sc;
487
+ respHeaders = headers;
488
+ },
489
+ onResponseData: (_controller, chunk) => {
490
+ chunks.push(Buffer.from(chunk));
491
+ responseSize += chunk.length;
492
+ if (responseSize > this.maximumResponseBodySize) _controller.abort(/* @__PURE__ */ new Error(`HTTP response body exceeds the ${this.maximumResponseBodySize} byte limit.`));
493
+ },
494
+ onResponseEnd: (_controller, _trailers) => {
495
+ clearTimer();
496
+ /* istanbul ignore next -- undici does not end after reporting an error */
497
+ if (settled) return;
498
+ settled = true;
499
+ resolve({
500
+ status: statusCode,
501
+ headers: respHeaders,
502
+ body: Buffer.concat(chunks).toString("utf8")
503
+ });
504
+ }
505
+ });
506
+ });
507
+ }
508
+ stream(method, url, options = {}) {
509
+ const { buf, inferredContentType } = prepareBody(options.body);
510
+ const headers = buildHeaders(this.defaultHeaders, options.headers, buf, inferredContentType);
511
+ const parsed = new URL(url);
512
+ return new Promise((resolve, reject) => {
513
+ let resolved = false;
514
+ const pt = new PassThrough();
515
+ let controller = null;
516
+ this.dispatcher.dispatch({
517
+ origin: parsed.origin,
518
+ path: parsed.pathname + parsed.search,
519
+ method,
520
+ headers,
521
+ ...buf !== null && { body: buf },
522
+ headersTimeout: options.timeout ?? this.defaultTimeout
523
+ }, {
524
+ onRequestStart: (ctrl, _context) => {
525
+ controller = ctrl;
526
+ },
527
+ onResponseError: (_ctrl, err) => {
528
+ if (!resolved) {
529
+ resolved = true;
530
+ pt.destroy();
531
+ reject(err);
532
+ } else pt.destroy(err);
533
+ },
534
+ onResponseStart: (_ctrl, statusCode, headers) => {
535
+ if (!resolved) {
536
+ resolved = true;
537
+ resolve({
538
+ statusCode,
539
+ headers,
540
+ body: pt,
541
+ abort: () => {
542
+ controller?.abort(/* @__PURE__ */ new Error("Stream aborted"));
543
+ pt.destroy();
544
+ }
545
+ });
546
+ }
547
+ },
548
+ onResponseData: (_ctrl, chunk) => {
549
+ if (!pt.write(chunk)) {
550
+ controller?.pause();
551
+ pt.once("drain", () => controller?.resume());
552
+ }
553
+ },
554
+ onResponseEnd: (_ctrl, _trailers) => {
555
+ pt.end();
556
+ }
557
+ });
558
+ });
559
+ }
560
+ };
561
+ //#endregion
562
+ //#region src/constants.ts
563
+ /**
564
+ * constants.ts
565
+ *
566
+ * All SignalR protocol constants, enumerations, and timing defaults.
567
+ * Uses `as const satisfies` (TS 4.9+) so values carry their narrow literal
568
+ * types while being verified against a constraint at compile time.
569
+ */
570
+ const MessageType = Object.freeze({
571
+ Invocation: 1,
572
+ StreamItem: 2,
573
+ Completion: 3,
574
+ StreamInvocation: 4,
575
+ CancelInvocation: 5,
576
+ Ping: 6,
577
+ Close: 7
578
+ });
579
+ const HubConnectionState = Object.freeze({
580
+ Disconnected: "Disconnected",
581
+ Connecting: "Connecting",
582
+ Connected: "Connected",
583
+ Disconnecting: "Disconnecting",
584
+ Reconnecting: "Reconnecting"
585
+ });
586
+ const LogLevel = {
587
+ Trace: 0,
588
+ Debug: 1,
589
+ Information: 2,
590
+ Warning: 3,
591
+ Error: 4,
592
+ Critical: 5,
593
+ None: 6
594
+ };
595
+ const HttpTransportType = {
596
+ None: 0,
597
+ WebSockets: 1,
598
+ ServerSentEvents: 2,
599
+ LongPolling: 4
600
+ };
601
+ const TransferFormat = {
602
+ Text: 1,
603
+ Binary: 2
604
+ };
605
+ /** ASCII 30 - the byte that terminates every SignalR JSON message. */
606
+ const RECORD_SEPARATOR = "";
607
+ /** Negotiate version we advertise to the server. */
608
+ const NEGOTIATE_VERSION = 1;
609
+ const DEFAULT_TIMEOUT_IN_MS = 3e4;
610
+ const DEFAULT_PING_INTERVAL_IN_MS = 15e3;
611
+ const DEFAULT_SERVER_TIMEOUT_IN_MS = 3e4;
612
+ //#endregion
613
+ //#region src/messages.ts
614
+ /** Cast a plain string to an InvocationId (only used inside the library). */
615
+ function toInvocationId(s) {
616
+ return s;
617
+ }
618
+ /**
619
+ * Asserts a code path is unreachable at compile time.
620
+ * Use at the `default` branch of a `switch` over `HubMessage['type']`.
621
+ */
622
+ function assertNever(_x, label) {
623
+ throw new Error(`Unexpected value${label ? ` for ${label}` : ""}: ${JSON.stringify(_x)}`);
624
+ }
625
+ function isInvocationMessage(msg) {
626
+ return msg.type === 1;
627
+ }
628
+ function isStreamItemMessage(msg) {
629
+ return msg.type === 2;
630
+ }
631
+ function isCompletionMessage(msg) {
632
+ return msg.type === 3;
633
+ }
634
+ function isStreamInvocationMessage(msg) {
635
+ return msg.type === 4;
636
+ }
637
+ function isCancelInvocationMessage(msg) {
638
+ return msg.type === 5;
639
+ }
640
+ function isPingMessage(msg) {
641
+ return msg.type === 6;
642
+ }
643
+ function isCloseMessage(msg) {
644
+ return msg.type === 7;
645
+ }
646
+ //#endregion
647
+ //#region src/protocols/json-hub-protocol.ts
648
+ /**
649
+ * json-hub-protocol.ts
650
+ *
651
+ * Implements the SignalR Hub Protocol over JSON (version 1).
652
+ *
653
+ * Wire format: every message is JSON followed by ASCII 0x1e (RECORD_SEPARATOR).
654
+ * A single transport payload may contain multiple messages.
655
+ *
656
+ * Reference:
657
+ * https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/HubProtocol.md
658
+ */
659
+ const HANDSHAKE_REQUEST = JSON.stringify({
660
+ protocol: "json",
661
+ version: 1
662
+ }) + "";
663
+ /**
664
+ * Parse the first record from `data` as the server handshake response.
665
+ * @throws {Error} if the record separator is missing or the server returned an error.
666
+ */
667
+ function parseHandshakeResponse(data) {
668
+ const idx = data.indexOf("");
669
+ if (idx === -1) throw new Error("Handshake response is missing the record separator.");
670
+ const raw = data.slice(0, idx);
671
+ const response = JSON.parse(raw);
672
+ if (response.error) throw new Error(`Server rejected the handshake with the error: "${response.error}"`);
673
+ return { remainder: data.slice(idx + 1) };
674
+ }
675
+ var JsonHubProtocol = class {
676
+ name = "json";
677
+ version = 1;
678
+ transferFormat = TransferFormat.Text;
679
+ parseMessages(input, logger) {
680
+ const text = typeof input === "string" ? input : new TextDecoder().decode(input);
681
+ if (!text) return [];
682
+ const messages = [];
683
+ const parts = text.split("");
684
+ for (const part of parts) {
685
+ const trimmed = part.trim();
686
+ if (!trimmed) continue;
687
+ let raw;
688
+ try {
689
+ raw = JSON.parse(trimmed);
690
+ } catch (e) {
691
+ throw new Error(`Error parsing message: ${e.message}. Raw value: "${trimmed}"`);
692
+ }
693
+ const msg = this.#coerce(raw);
694
+ if (msg !== null) messages.push(msg);
695
+ else logger.log(3, `Ignoring message with unknown type ${String(raw.type)}.`);
696
+ }
697
+ return messages;
698
+ }
699
+ writeMessage(message) {
700
+ return JSON.stringify(message) + "";
701
+ }
702
+ /**
703
+ * Validate and narrow a raw parsed object into a typed `HubMessage`.
704
+ * Returns `null` for unknown types (not an error - new protocol versions
705
+ * may add types we don't know about).
706
+ */
707
+ #coerce(raw) {
708
+ const type = raw.type;
709
+ if (typeof type !== "number") throw new Error(`Invalid message: 'type' must be a number, got ${JSON.stringify(type)}.`);
710
+ switch (type) {
711
+ case MessageType.Invocation: return this.#coerceInvocation(raw);
712
+ case MessageType.StreamItem: return this.#coerceStreamItem(raw);
713
+ case MessageType.Completion: return this.#coerceCompletion(raw);
714
+ case MessageType.StreamInvocation: return this.#coerceStreamInvocation(raw);
715
+ case MessageType.CancelInvocation: return this.#coerceCancelInvocation(raw);
716
+ case MessageType.Ping: return this.#coercePing();
717
+ case MessageType.Close: return this.#coerceClose(raw);
718
+ default: return null;
719
+ }
720
+ }
721
+ #coerceInvocation(raw) {
722
+ if (typeof raw.target !== "string") throw new Error("Invalid Invocation message: 'target' must be a string.");
723
+ if (!Array.isArray(raw.arguments)) throw new Error("Invalid Invocation message: 'arguments' must be an array.");
724
+ return {
725
+ type: MessageType.Invocation,
726
+ target: raw.target,
727
+ arguments: raw.arguments,
728
+ ...raw.invocationId != null && { invocationId: parseInvocationId$1(raw.invocationId) },
729
+ ...raw.streamIds != null && { streamIds: parseStreamIds$1(raw.streamIds) },
730
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
731
+ };
732
+ }
733
+ #coerceStreamItem(raw) {
734
+ if (raw.invocationId == null) throw new Error("Invalid StreamItem message: 'invocationId' is required.");
735
+ return {
736
+ type: MessageType.StreamItem,
737
+ invocationId: parseInvocationId$1(raw.invocationId),
738
+ item: raw.item,
739
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
740
+ };
741
+ }
742
+ #coerceCompletion(raw) {
743
+ if (raw.invocationId == null) throw new Error("Invalid Completion message: 'invocationId' is required.");
744
+ const hasResult = Object.prototype.hasOwnProperty.call(raw, "result");
745
+ if (raw.error != null && hasResult) throw new Error("Invalid Completion message: 'error' and 'result' are mutually exclusive.");
746
+ return {
747
+ type: MessageType.Completion,
748
+ invocationId: parseInvocationId$1(raw.invocationId),
749
+ ...raw.error != null && { error: String(raw.error) },
750
+ ...hasResult && { result: raw.result },
751
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
752
+ };
753
+ }
754
+ #coerceStreamInvocation(raw) {
755
+ if (raw.invocationId == null) throw new Error("Invalid StreamInvocation message: 'invocationId' is required.");
756
+ if (typeof raw.target !== "string") throw new Error("Invalid StreamInvocation message: 'target' must be a string.");
757
+ if (!Array.isArray(raw.arguments)) throw new Error("Invalid StreamInvocation message: 'arguments' must be an array.");
758
+ return {
759
+ type: MessageType.StreamInvocation,
760
+ invocationId: parseInvocationId$1(raw.invocationId),
761
+ target: raw.target,
762
+ arguments: raw.arguments,
763
+ ...raw.streamIds != null && { streamIds: parseStreamIds$1(raw.streamIds) },
764
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
765
+ };
766
+ }
767
+ #coerceCancelInvocation(raw) {
768
+ if (raw.invocationId == null) throw new Error("Invalid CancelInvocation message: 'invocationId' is required.");
769
+ return {
770
+ type: MessageType.CancelInvocation,
771
+ invocationId: parseInvocationId$1(raw.invocationId),
772
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
773
+ };
774
+ }
775
+ #coercePing() {
776
+ return { type: MessageType.Ping };
777
+ }
778
+ #coerceClose(raw) {
779
+ if (raw.allowReconnect != null && typeof raw.allowReconnect !== "boolean") throw new Error("Invalid Close message: 'allowReconnect' must be a boolean.");
780
+ return {
781
+ type: MessageType.Close,
782
+ ...raw.error != null && { error: String(raw.error) },
783
+ ...raw.allowReconnect != null && { allowReconnect: raw.allowReconnect },
784
+ ...raw.headers != null && { headers: parseHeaders$1(raw.headers) }
785
+ };
786
+ }
787
+ static invocation(invocationId, target, args, streamIds) {
788
+ return {
789
+ type: MessageType.Invocation,
790
+ invocationId,
791
+ target,
792
+ arguments: args,
793
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
794
+ };
795
+ }
796
+ /**
797
+ * Fire-and-forget (no `invocationId` → server won't send a Completion).
798
+ */
799
+ static send(target, args, streamIds) {
800
+ return {
801
+ type: MessageType.Invocation,
802
+ target,
803
+ arguments: args,
804
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
805
+ };
806
+ }
807
+ static streamInvocation(invocationId, target, args, streamIds) {
808
+ return {
809
+ type: MessageType.StreamInvocation,
810
+ invocationId,
811
+ target,
812
+ arguments: args,
813
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
814
+ };
815
+ }
816
+ static cancelInvocation(invocationId) {
817
+ return {
818
+ type: MessageType.CancelInvocation,
819
+ invocationId
820
+ };
821
+ }
822
+ static streamItem(invocationId, item) {
823
+ return {
824
+ type: MessageType.StreamItem,
825
+ invocationId,
826
+ item
827
+ };
828
+ }
829
+ static completion(invocationId, result, error) {
830
+ return {
831
+ type: MessageType.Completion,
832
+ invocationId,
833
+ ...error != null && { error },
834
+ ...result != null && { result }
835
+ };
836
+ }
837
+ static ping() {
838
+ return { type: MessageType.Ping };
839
+ }
840
+ };
841
+ function parseInvocationId$1(value) {
842
+ if (typeof value !== "string") throw new Error("Invalid message: 'invocationId' must be a string.");
843
+ return toInvocationId(value);
844
+ }
845
+ function parseStreamIds$1(value) {
846
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) throw new Error("Invalid message: 'streamIds' must be an array of strings.");
847
+ return value;
848
+ }
849
+ function parseHeaders$1(value) {
850
+ 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.");
851
+ return value;
852
+ }
853
+ Object.freeze({
854
+ Connecting: 0,
855
+ Open: 1,
856
+ Closing: 2,
857
+ Closed: 3
858
+ });
859
+ var WebSocketTransport = class {
860
+ name = "WebSockets";
861
+ #accessTokenFactory;
862
+ #logger;
863
+ #extraHeaders;
864
+ #dispatcher;
865
+ #ws = null;
866
+ #onreceive = null;
867
+ #pendingMessages = [];
868
+ onclose = null;
869
+ get onreceive() {
870
+ return this.#onreceive;
871
+ }
872
+ set onreceive(handler) {
873
+ this.#onreceive = handler;
874
+ /* istanbul ignore else -- HubConnection installs a receiver after connect */
875
+ if (handler) {
876
+ const pending = this.#pendingMessages;
877
+ this.#pendingMessages = [];
878
+ for (const message of pending)
879
+ /* istanbul ignore next -- requires a server frame in the connect/onreceive microtask gap */
880
+ handler(message);
881
+ }
882
+ }
883
+ /**
884
+ * @param accessTokenFactory Async factory for Bearer tokens, or null.
885
+ * @param logger Logger instance.
886
+ * @param extraHeaders Headers forwarded on every upgrade request.
887
+ * @param dispatcher Optional undici Dispatcher (Agent/Pool/Client/
888
+ * ProxyAgent). When omitted, undici's default
889
+ * global agent is used. Pass the same dispatcher
890
+ * used by your HTTP clients to share the pool.
891
+ */
892
+ constructor(accessTokenFactory, logger, extraHeaders = {}, dispatcher) {
893
+ this.#accessTokenFactory = accessTokenFactory;
894
+ this.#logger = logger;
895
+ this.#extraHeaders = extraHeaders;
896
+ this.#dispatcher = dispatcher;
897
+ }
898
+ async connect(url, _transferFormat) {
899
+ this.#logger.log(LogLevel.Trace, `(WebSockets transport) Connecting to ${url}.`);
900
+ const headers = { ...this.#extraHeaders };
901
+ if (this.#accessTokenFactory) {
902
+ const token = await this.#accessTokenFactory();
903
+ if (token) headers["Authorization"] = `Bearer ${token}`;
904
+ }
905
+ const ws = new WebSocket(url.replace(/^http/, "ws"), {
906
+ headers,
907
+ ...this.#dispatcher !== void 0 && { dispatcher: this.#dispatcher }
908
+ });
909
+ this.#ws = ws;
910
+ ws.binaryType = "arraybuffer";
911
+ ws.addEventListener("message", (event) => {
912
+ const data = event.data;
913
+ const message = typeof data === "string" ? data : Buffer.from(data);
914
+ this.#logger.log(LogLevel.Trace, typeof message === "string" ? `(WebSockets transport) Received text (${message.length} chars).` : `(WebSockets transport) Received binary (${message.length} bytes).`);
915
+ if (this.#onreceive) this.#onreceive(message);
916
+ else this.#pendingMessages.push(message);
917
+ });
918
+ await new Promise((resolve, reject) => {
919
+ const cleanup = () => {
920
+ ws.removeEventListener("open", onOpen);
921
+ ws.removeEventListener("error", onError);
922
+ };
923
+ const onOpen = () => {
924
+ cleanup();
925
+ this.#logger.log(LogLevel.Information, "(WebSockets transport) Connected.");
926
+ resolve();
927
+ };
928
+ const onError = (event) => {
929
+ cleanup();
930
+ const e = event;
931
+ let err;
932
+ 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");
933
+ else err = new Error(String(e.message ?? "WebSocket error"));
934
+ this.#logger.log(LogLevel.Error, `(WebSockets transport) Error during connect: ${err.message}`);
935
+ this.#ws = null;
936
+ ws.close();
937
+ reject(err);
938
+ };
939
+ ws.addEventListener("open", onOpen);
940
+ ws.addEventListener("error", onError);
941
+ });
942
+ ws.addEventListener("close", (event) => {
943
+ const closeEvent = event;
944
+ this.#logger.log(LogLevel.Information, `(WebSockets transport) Socket closed (code=${closeEvent.code}).`);
945
+ if (this.#ws !== null) {
946
+ this.#ws = null;
947
+ this.onclose?.();
948
+ }
949
+ });
950
+ ws.addEventListener("error", (event) => {
951
+ if (ws.readyState !== WebSocket.CONNECTING) {
952
+ const e = event;
953
+ const err = e.error instanceof Error ? e.error : new Error(String(e.message ?? "WebSocket error"));
954
+ this.#logger.log(LogLevel.Error, `(WebSockets transport) Error: ${err.message}`);
955
+ if (this.#ws !== null) {
956
+ this.#ws = null;
957
+ this.onclose?.(err);
958
+ }
959
+ }
960
+ });
961
+ }
962
+ send(data) {
963
+ if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) return Promise.reject(/* @__PURE__ */ new Error("WebSocket is not open."));
964
+ try {
965
+ this.#ws.send(data);
966
+ return Promise.resolve();
967
+ } catch (err) {
968
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
969
+ }
970
+ }
971
+ stop() {
972
+ if (this.#ws) {
973
+ const ws = this.#ws;
974
+ this.#ws = null;
975
+ ws.close(1e3, "Normal closure");
976
+ }
977
+ return Promise.resolve();
978
+ }
979
+ };
980
+ //#endregion
981
+ //#region src/transports/sse-transport.ts
982
+ var ServerSentEventsTransport = class {
983
+ name = "ServerSentEvents";
984
+ #httpClient;
985
+ #accessTokenFactory;
986
+ #logger;
987
+ #extraHeaders;
988
+ #url = null;
989
+ #abort = null;
990
+ #sseBuffer = "";
991
+ onreceive = null;
992
+ onclose = null;
993
+ constructor(httpClient, accessTokenFactory, logger, extraHeaders = {}) {
994
+ this.#httpClient = httpClient;
995
+ this.#accessTokenFactory = accessTokenFactory;
996
+ this.#logger = logger;
997
+ this.#extraHeaders = extraHeaders;
998
+ }
999
+ async connect(url, transferFormat) {
1000
+ if (transferFormat === TransferFormat.Binary) throw new Error("The Server-Sent Events transport only supports the Text transfer format.");
1001
+ this.#logger.log(LogLevel.Trace, `(SSE transport) Connecting to ${url}.`);
1002
+ this.#url = url;
1003
+ const headers = {
1004
+ Accept: "text/event-stream",
1005
+ ...this.#extraHeaders
1006
+ };
1007
+ if (this.#accessTokenFactory) {
1008
+ const token = await this.#accessTokenFactory();
1009
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1010
+ }
1011
+ const result = await this.#httpClient.stream("GET", url, { headers });
1012
+ if (result.statusCode !== 200) {
1013
+ result.body.on("error", () => {});
1014
+ result.abort();
1015
+ throw new Error(`Unexpected status code ${result.statusCode} from SSE endpoint.`);
1016
+ }
1017
+ this.#abort = result.abort;
1018
+ result.body.setEncoding("utf8");
1019
+ result.body.on("data", (chunk) => this.#onData(chunk));
1020
+ result.body.on("end", () => {
1021
+ this.#logger.log(LogLevel.Information, "(SSE transport) EventSource closed.");
1022
+ this.#abort = null;
1023
+ this.onclose?.();
1024
+ });
1025
+ result.body.on("error", (err) => {
1026
+ this.#logger.log(LogLevel.Error, `(SSE transport) Stream error: ${err.message}`);
1027
+ this.#abort = null;
1028
+ this.onclose?.(err);
1029
+ });
1030
+ this.#logger.log(LogLevel.Information, "(SSE transport) Connected.");
1031
+ }
1032
+ async send(data) {
1033
+ if (!this.#url) throw new Error("SSE transport is not connected.");
1034
+ const headers = {
1035
+ "Content-Type": typeof data === "string" ? "text/plain;charset=UTF-8" : "application/octet-stream",
1036
+ ...this.#extraHeaders
1037
+ };
1038
+ if (this.#accessTokenFactory) {
1039
+ const token = await this.#accessTokenFactory();
1040
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1041
+ }
1042
+ const result = await this.#httpClient.post(this.#url, {
1043
+ headers,
1044
+ body: data
1045
+ });
1046
+ if (result.status < 200 || result.status >= 300) throw new Error(`SSE send failed with status ${result.status}.`);
1047
+ }
1048
+ stop() {
1049
+ if (this.#abort) {
1050
+ this.#abort();
1051
+ this.#abort = null;
1052
+ }
1053
+ return Promise.resolve();
1054
+ }
1055
+ #onData(chunk) {
1056
+ this.#sseBuffer += chunk;
1057
+ let boundary;
1058
+ while ((boundary = findEventBoundary(this.#sseBuffer)) !== -1) {
1059
+ const block = this.#sseBuffer.slice(0, boundary);
1060
+ this.#sseBuffer = this.#sseBuffer.slice(boundary).replace(/^[\r\n]+/, "");
1061
+ this.#dispatchEvent(block);
1062
+ }
1063
+ }
1064
+ #dispatchEvent(block) {
1065
+ const lines = block.split(/\r?\n/);
1066
+ const dataParts = [];
1067
+ for (const line of lines) if (line.startsWith("data:")) dataParts.push(line.slice(5).replace(/^ /, ""));
1068
+ if (dataParts.length === 0) return;
1069
+ const message = dataParts.join("\n");
1070
+ this.#logger.log(LogLevel.Trace, `(SSE transport) Received data: ${message}`);
1071
+ this.onreceive?.(message);
1072
+ }
1073
+ };
1074
+ function findEventBoundary(str) {
1075
+ const i1 = str.indexOf("\n\n");
1076
+ const i2 = str.indexOf("\r\n\r\n");
1077
+ if (i1 === -1 && i2 === -1) return -1;
1078
+ if (i1 === -1) return i2 + 4;
1079
+ if (i2 === -1) return i1 + 2;
1080
+ return Math.min(i1 + 2, i2 + 4);
1081
+ }
1082
+ //#endregion
1083
+ //#region src/transports/long-polling-transport.ts
1084
+ /** How long a single poll GET can hang before we issue a new one. */
1085
+ const POLL_TIMEOUT_MS = 1e5;
1086
+ var LongPollingTransport = class {
1087
+ name = "LongPolling";
1088
+ #httpClient;
1089
+ #accessTokenFactory;
1090
+ #logger;
1091
+ #extraHeaders;
1092
+ #url = null;
1093
+ #running = false;
1094
+ #pollImmediate = null;
1095
+ #activeAbort = null;
1096
+ #transferFormat = TransferFormat.Text;
1097
+ onreceive = null;
1098
+ onclose = null;
1099
+ constructor(httpClient, accessTokenFactory, logger, extraHeaders = {}) {
1100
+ this.#httpClient = httpClient;
1101
+ this.#accessTokenFactory = accessTokenFactory;
1102
+ this.#logger = logger;
1103
+ this.#extraHeaders = extraHeaders;
1104
+ }
1105
+ async connect(url, transferFormat) {
1106
+ this.#logger.log(LogLevel.Trace, `(LongPolling transport) Connecting to ${url}.`);
1107
+ this.#url = url;
1108
+ this.#running = true;
1109
+ this.#transferFormat = transferFormat;
1110
+ await this.#poll(url, true);
1111
+ this.#schedulePoll();
1112
+ this.#logger.log(LogLevel.Information, "(LongPolling transport) Connected.");
1113
+ }
1114
+ async send(data) {
1115
+ if (!this.#running || !this.#url) throw new Error("LongPolling transport is not connected.");
1116
+ const headers = await this.#buildHeaders({ "Content-Type": typeof data === "string" ? "text/plain;charset=UTF-8" : "application/octet-stream" });
1117
+ const result = await this.#httpClient.post(this.#url, {
1118
+ headers,
1119
+ body: data
1120
+ });
1121
+ if (result.status < 200 || result.status >= 300) throw new Error(`Long polling send failed with status ${result.status}.`);
1122
+ }
1123
+ async stop() {
1124
+ this.#running = false;
1125
+ if (this.#pollImmediate !== null) {
1126
+ clearImmediate(this.#pollImmediate);
1127
+ this.#pollImmediate = null;
1128
+ }
1129
+ this.#activeAbort?.();
1130
+ this.#activeAbort = null;
1131
+ if (this.#url) try {
1132
+ const headers = await this.#buildHeaders();
1133
+ await this.#httpClient.delete(this.#url, { headers });
1134
+ } catch (e) {
1135
+ this.#logger.log(LogLevel.Warning, `(LongPolling transport) Error sending DELETE: ${e.message}`);
1136
+ }
1137
+ }
1138
+ #schedulePoll() {
1139
+ if (!this.#running) return;
1140
+ this.#pollImmediate = setImmediate(() => void this.#pollLoop());
1141
+ }
1142
+ async #pollLoop() {
1143
+ /* istanbul ignore next - #schedulePoll()'s !#running guard and
1144
+ clearImmediate() in stop() together prevent #pollLoop from ever
1145
+ being called once the transport has stopped; this is a
1146
+ belt-and-suspenders defence. */
1147
+ if (!this.#running || !this.#url) return;
1148
+ try {
1149
+ await this.#poll(this.#url, false);
1150
+ } catch (err) {
1151
+ /* istanbul ignore else - between #poll re-throwing and this catch,
1152
+ no macrotask can run to set #running=false; the false branch is
1153
+ a belt-and-suspenders defence against a theoretical race. */
1154
+ if (this.#running) {
1155
+ this.#logger.log(LogLevel.Error, `(LongPolling transport) Poll error: ${err.message}`);
1156
+ this.#running = false;
1157
+ this.onclose?.(err instanceof Error ? err : new Error(String(err)));
1158
+ return;
1159
+ }
1160
+ }
1161
+ this.#schedulePoll();
1162
+ }
1163
+ async #poll(url, isConnect) {
1164
+ const headers = await this.#buildHeaders();
1165
+ let res;
1166
+ try {
1167
+ res = await this.#httpClient.stream("GET", url, {
1168
+ headers,
1169
+ timeout: POLL_TIMEOUT_MS
1170
+ });
1171
+ this.#activeAbort = res.abort;
1172
+ } catch (err) {
1173
+ if (!this.#running) return;
1174
+ throw err;
1175
+ }
1176
+ if (res.statusCode === 204) {
1177
+ await readAll(res.body);
1178
+ this.#activeAbort = null;
1179
+ this.#logger.log(LogLevel.Information, "(LongPolling transport) Server closed (204).");
1180
+ this.#running = false;
1181
+ this.onclose?.();
1182
+ return;
1183
+ }
1184
+ if (res.statusCode < 200 || res.statusCode >= 300) {
1185
+ await readAll(res.body);
1186
+ this.#activeAbort = null;
1187
+ const err = /* @__PURE__ */ new Error(`Long polling received unexpected status ${res.statusCode}.`);
1188
+ if (!isConnect) {
1189
+ this.#running = false;
1190
+ this.onclose?.(err);
1191
+ } else throw err;
1192
+ return;
1193
+ }
1194
+ let body;
1195
+ try {
1196
+ body = await readAll(res.body);
1197
+ } catch (err) {
1198
+ /* istanbul ignore next -- only an explicit stop aborts the active body */
1199
+ if (!this.#running) return;
1200
+ /* istanbul ignore next -- body errors are propagated to the poll loop */
1201
+ throw err;
1202
+ } finally {
1203
+ this.#activeAbort = null;
1204
+ }
1205
+ if (body.length > 0) {
1206
+ const data = this.#transferFormat === TransferFormat.Binary ? new Uint8Array(body.buffer, body.byteOffset, body.byteLength) : body.toString("utf8");
1207
+ this.#logger.log(LogLevel.Trace, `(LongPolling transport) Received ${body.length} bytes.`);
1208
+ this.onreceive?.(data);
1209
+ }
1210
+ }
1211
+ async #buildHeaders(extra = {}) {
1212
+ const headers = {
1213
+ ...this.#extraHeaders,
1214
+ ...extra
1215
+ };
1216
+ if (this.#accessTokenFactory) {
1217
+ const token = await this.#accessTokenFactory();
1218
+ if (token) headers["Authorization"] = `Bearer ${token}`;
1219
+ }
1220
+ return headers;
1221
+ }
1222
+ };
1223
+ function readAll(stream) {
1224
+ return new Promise((resolve, reject) => {
1225
+ const chunks = [];
1226
+ stream.on("data", (chunk) => {
1227
+ /* istanbul ignore next -- Node HTTP response streams emit Buffer chunks */
1228
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1229
+ });
1230
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
1231
+ stream.on("error", reject);
1232
+ });
1233
+ }
1234
+ //#endregion
1235
+ //#region src/logger.ts
1236
+ /**
1237
+ * logger.ts
1238
+ *
1239
+ * Built-in ILogger implementations.
1240
+ * ConsoleLogger - writes to console with timestamps and severity labels.
1241
+ * NullLogger - discards everything (default when no logger is configured).
1242
+ */
1243
+ var ConsoleLogger = class {
1244
+ #minimumLevel;
1245
+ constructor(minimumLevel = LogLevel.Information) {
1246
+ this.#minimumLevel = minimumLevel;
1247
+ }
1248
+ log(level, message) {
1249
+ if (level < this.#minimumLevel) return;
1250
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${logLevelName(level)}] ${message}`;
1251
+ switch (level) {
1252
+ case LogLevel.Trace:
1253
+ case LogLevel.Debug:
1254
+ console.debug(line);
1255
+ break;
1256
+ case LogLevel.Warning:
1257
+ console.warn(line);
1258
+ break;
1259
+ case LogLevel.Error:
1260
+ case LogLevel.Critical:
1261
+ console.error(line);
1262
+ break;
1263
+ default:
1264
+ console.log(line);
1265
+ break;
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 ?? 32 * 1024 * 1024;
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:
1846
+ this.#logger.log(LogLevel.Warning, `Unknown message type: ${msg.type}`);
1847
+ break;
1848
+ }
1849
+ }
1850
+ async #invokeClientMethod(msg) {
1851
+ const handlers = this.#methods.get(msg.target.toLowerCase());
1852
+ if (!handlers || handlers.size === 0) {
1853
+ this.#logger.log(LogLevel.Warning, `No handler for "${msg.target}".`);
1854
+ if (msg.invocationId) await this.#send(this.#encode(JsonHubProtocol.completion(msg.invocationId, void 0, `No client handler registered for '${msg.target}'.`))).catch(
1855
+ /* istanbul ignore next -- completion replies are best-effort */
1856
+ () => {}
1857
+ );
1858
+ return;
1859
+ }
1860
+ let result;
1861
+ let invocationError = null;
1862
+ for (const handler of [...handlers]) try {
1863
+ result = await handler(...msg.arguments);
1864
+ } catch (error) {
1865
+ const err = error instanceof Error ? error : new Error(String(error));
1866
+ invocationError ??= err;
1867
+ this.#logger.log(LogLevel.Error, `Handler for "${msg.target}" threw: ${err.message}`);
1868
+ }
1869
+ if (msg.invocationId) {
1870
+ const completion = invocationError ? {
1871
+ type: MessageType.Completion,
1872
+ invocationId: msg.invocationId,
1873
+ error: invocationError.message
1874
+ } : {
1875
+ type: MessageType.Completion,
1876
+ invocationId: msg.invocationId,
1877
+ ...result !== void 0 && { result }
1878
+ };
1879
+ await this.#send(this.#encode(completion)).catch(
1880
+ /* istanbul ignore next -- completion replies are best-effort */
1881
+ () => {}
1882
+ );
1883
+ }
1884
+ }
1885
+ #onTransportClosed(err) {
1886
+ /* istanbul ignore next - WebSocketTransport.stop() nulls #ws before
1887
+ calling ws.close(), so the close handler never invokes onclose once
1888
+ stop() has set #stopping; this guard is a belt-and-suspenders defence. */
1889
+ if (this.#stopping) return;
1890
+ this.#logger.log(LogLevel.Warning, err ? `Transport closed with error: ${err.message}` : "Transport closed.");
1891
+ this.#clearTimers();
1892
+ this.#rejectAllPending(err ?? new AbortError("Connection closed while reconnecting."));
1893
+ if (this.#reconnectPolicy) this.#doReconnect(err ?? null);
1894
+ else this.#completeClose(err);
1895
+ }
1896
+ #stopWithError(err) {
1897
+ this.#clearTimers();
1898
+ this.#transport?.stop().catch(
1899
+ /* istanbul ignore next -- error shutdown is best-effort */
1900
+ () => {}
1901
+ );
1902
+ this.#transport = null;
1903
+ this.#completeClose(err ?? void 0);
1904
+ }
1905
+ #completeClose(err) {
1906
+ const closeErr = err ?? new AbortError("Connection closed.");
1907
+ this.#rejectAllPending(closeErr);
1908
+ this.#state = HubConnectionState.Disconnected;
1909
+ for (const cb of this.#closedCallbacks) try {
1910
+ cb(err);
1911
+ } catch {}
1912
+ this.#closeOwnedDispatcher();
1913
+ }
1914
+ #rejectAllPending(err) {
1915
+ this.#callbacks.forEach((cb) => cb.reject(err));
1916
+ this.#callbacks.clear();
1917
+ this.#streamCbs.forEach((sc) => sc.error(err));
1918
+ this.#streamCbs.clear();
1919
+ }
1920
+ async #doReconnect(disconnectError) {
1921
+ this.#state = HubConnectionState.Reconnecting;
1922
+ for (const cb of this.#reconnectingCallbacks) try {
1923
+ cb(disconnectError ?? void 0);
1924
+ } catch {}
1925
+ let retryCount = 0;
1926
+ const startTime = Date.now();
1927
+ let lastError = disconnectError;
1928
+ while (!this.#stopping) {
1929
+ const ctx = {
1930
+ previousRetryCount: retryCount,
1931
+ elapsedMilliseconds: Date.now() - startTime,
1932
+ retryReason: lastError
1933
+ };
1934
+ const delay = this.#reconnectPolicy.nextRetryDelayInMilliseconds(ctx);
1935
+ if (delay === null) {
1936
+ this.#logger.log(LogLevel.Debug, "Reconnect policy returned null - giving up.");
1937
+ break;
1938
+ }
1939
+ this.#logger.log(LogLevel.Information, `Reconnecting in ${delay} ms (attempt ${retryCount + 1}).`);
1940
+ await sleep(delay);
1941
+ if (this.#stopping) break;
1942
+ try {
1943
+ await this.#startInternal();
1944
+ this.#state = HubConnectionState.Connected;
1945
+ this.#logger.log(LogLevel.Information, "Reconnected successfully.");
1946
+ for (const cb of this.#reconnectedCallbacks) try {
1947
+ cb(this.#connectionId);
1948
+ } catch {}
1949
+ return;
1950
+ } catch (err) {
1951
+ retryCount++;
1952
+ /* istanbul ignore next - #startInternal() only throws Error instances. */
1953
+ lastError = err instanceof Error ? err : new Error(String(err));
1954
+ this.#logger.log(LogLevel.Warning, `Reconnect attempt ${retryCount} failed: ${lastError.message}`);
1955
+ }
1956
+ }
1957
+ this.#completeClose(lastError ?? void 0);
1958
+ }
1959
+ #resetPingTimer() {
1960
+ clearTimeout(this.#pingTimer);
1961
+ this.#pingTimer = setTimeout(async () => {
1962
+ /* istanbul ignore next - #clearTimers() always cancels this before state
1963
+ leaves Connected; the guard is a defensive fallback. */
1964
+ if (this.#state !== HubConnectionState.Connected) return;
1965
+ try {
1966
+ await this.#send(this.#encode(JsonHubProtocol.ping()));
1967
+ } catch (err) {
1968
+ this.#logger.log(LogLevel.Warning, `Ping failed: ${err.message}`);
1969
+ }
1970
+ this.#resetPingTimer();
1971
+ }, this.#pingInterval);
1972
+ }
1973
+ #resetServerTimeoutTimer() {
1974
+ clearTimeout(this.#serverTimeoutTimer);
1975
+ this.#serverTimeoutTimer = setTimeout(() => {
1976
+ this.#logger.log(LogLevel.Warning, "Server timeout - closing connection.");
1977
+ this.#stopWithError(new AbortError("Server timeout: no message received within the timeout period."));
1978
+ }, this.#serverTimeout);
1979
+ }
1980
+ #clearTimers() {
1981
+ clearTimeout(this.#pingTimer);
1982
+ clearTimeout(this.#serverTimeoutTimer);
1983
+ clearTimeout(this.#handshakeTimer);
1984
+ this.#pingTimer = null;
1985
+ this.#serverTimeoutTimer = null;
1986
+ this.#handshakeTimer = null;
1987
+ }
1988
+ #send(message) {
1989
+ this.#resetPingTimer();
1990
+ return this.#transport.send(message);
1991
+ }
1992
+ #encode(message) {
1993
+ const encoded = this.#protocol.writeMessage(message);
1994
+ return typeof encoded === "string" ? encoded : new Uint8Array(encoded);
1995
+ }
1996
+ #assertConnected(method) {
1997
+ if (this.#state !== HubConnectionState.Connected) throw new Error(`Cannot call "${method}" when connection is not Connected (current: ${this.#state}).`);
1998
+ }
1999
+ #nextId() {
2000
+ return toInvocationId(String(++this.#invocationSeq));
2001
+ }
2002
+ async #closeOwnedDispatcher() {
2003
+ if (!this.#ownsDispatcher || !this.#dispatcher) return;
2004
+ this.#dispatcherClosePromise ??= this.#dispatcher.close().catch(
2005
+ /* istanbul ignore next -- shutdown cleanup is best-effort */
2006
+ () => {}
2007
+ );
2008
+ await this.#dispatcherClosePromise;
2009
+ }
2010
+ };
2011
+ /**
2012
+ * Append a single query-string name/value pair to a URL.
2013
+ *
2014
+ * Uses the WHATWG `URL` API so the operation is always correct regardless of
2015
+ * whether the URL already contains a query string, path segments, or both.
2016
+ * This avoids the classic bug where naive string concatenation appends a path
2017
+ * segment *after* an existing query string, corrupting the URL.
2018
+ *
2019
+ * e.g. appendParam('https://host/hub?token=x', 'id', 'y')
2020
+ * → 'https://host/hub?token=x&id=y' ✓ (not '?token=x&id=y' appended raw)
2021
+ */
2022
+ function appendParam(url, name, value) {
2023
+ const u = new URL(url);
2024
+ u.searchParams.append(name, value);
2025
+ return u.toString();
2026
+ }
2027
+ function sleep(ms) {
2028
+ return new Promise((resolve) => setTimeout(resolve, ms));
2029
+ }
2030
+ //#endregion
2031
+ //#region src/hub-connection-builder.ts
2032
+ /**
2033
+ * hub-connection-builder.ts
2034
+ *
2035
+ * Fluent builder for HubConnection - mirrors the official Microsoft client API.
2036
+ * Uses `const` type parameters (TS 5.0+) and `NoInfer<T>` (TS 5.4+) where
2037
+ * applicable to preserve literal types and prevent unintended widening.
2038
+ *
2039
+ * Usage
2040
+ * ─────
2041
+ * const connection = new HubConnectionBuilder()
2042
+ * .withUrl('https://example.com/hub', {
2043
+ * accessTokenFactory: () => fetchToken(),
2044
+ * transport: HttpTransportType.WebSockets,
2045
+ * })
2046
+ * .configureLogging(LogLevel.Information)
2047
+ * .withAutomaticReconnect()
2048
+ * .build();
2049
+ */
2050
+ /** Default reconnect delay sequence in ms: 0, 2 s, 10 s, 30 s then give up. */
2051
+ const DEFAULT_RETRY_DELAYS = [
2052
+ 0,
2053
+ 2e3,
2054
+ 1e4,
2055
+ 3e4
2056
+ ];
2057
+ var HubConnectionBuilder = class {
2058
+ #url = null;
2059
+ #urlOptions = {};
2060
+ #logger = null;
2061
+ #reconnectPolicy = null;
2062
+ #dispatcher = null;
2063
+ #httpClient = null;
2064
+ #cookieJar = null;
2065
+ #protocol = null;
2066
+ withUrl(url, options) {
2067
+ if (!url || typeof url !== "string") throw new TypeError("withUrl requires a non-empty string URL.");
2068
+ this.#url = url;
2069
+ this.#urlOptions = options ?? {};
2070
+ return this;
2071
+ }
2072
+ /**
2073
+ * Enable automatic cookie handling for the entire connection session using
2074
+ * a `tough-cookie` `CookieJar` and an `@exhumer/undici-cookie-agent` `CookieAgent`.
2075
+ *
2076
+ * When enabled, the `CookieAgent` wraps every outgoing request - including
2077
+ * the `/negotiate` POST, WebSocket upgrade, SSE long-lived GET, and all
2078
+ * long-polling requests - so that:
2079
+ *
2080
+ * 1. `Set-Cookie` headers in the **negotiate response** (and any subsequent
2081
+ * response) are stored in the jar automatically.
2082
+ * 2. All subsequent requests from the same session include the matching
2083
+ * `Cookie` header, exactly as a browser would.
2084
+ *
2085
+ * @param jar
2086
+ * An existing `CookieJar` to use. Pass your own jar when you need to
2087
+ * pre-seed cookies (e.g. an auth session cookie obtained before calling
2088
+ * `start()`) or inspect the jar after the session ends.
2089
+ * When omitted a new, empty jar is created automatically.
2090
+ *
2091
+ * @throws if {@link withDispatcher} has already been called. To combine a
2092
+ * custom dispatcher (e.g. `ProxyAgent`) with cookie support, compose the
2093
+ * `cookie` interceptor onto your dispatcher and pass it to `withDispatcher()`:
2094
+ *
2095
+ * ```ts
2096
+ * import { ProxyAgent } from 'undici';
2097
+ * import { cookie, CookieJar } from '@exhumer/signalr-client';
2098
+ *
2099
+ * const jar = new CookieJar();
2100
+ * const agent = new ProxyAgent('http://proxy:8080').compose(cookie({ jar }));
2101
+ *
2102
+ * const conn = new HubConnectionBuilder()
2103
+ * .withUrl('https://example.com/hub')
2104
+ * .withDispatcher(agent)
2105
+ * .build();
2106
+ * ```
2107
+ *
2108
+ * @example
2109
+ * ```ts
2110
+ * // Automatic jar - no pre-configuration needed
2111
+ * const conn = new HubConnectionBuilder()
2112
+ * .withUrl('https://example.com/hub')
2113
+ * .withCookies()
2114
+ * .build();
2115
+ *
2116
+ * // Pre-seeded jar - inject a session cookie before connecting
2117
+ * import { CookieJar } from '@exhumer/signalr-client';
2118
+ * const jar = new CookieJar();
2119
+ * await jar.setCookie('session=abc123', 'https://example.com');
2120
+ *
2121
+ * const conn = new HubConnectionBuilder()
2122
+ * .withUrl('https://example.com/hub')
2123
+ * .withCookies(jar)
2124
+ * .build();
2125
+ * ```
2126
+ */
2127
+ withCookies(jar) {
2128
+ 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.");
2129
+ this.#cookieJar = jar ?? new CookieJar$1();
2130
+ return this;
2131
+ }
2132
+ /**
2133
+ * Set the undici `Dispatcher` for the entire connection session.
2134
+ *
2135
+ * The same dispatcher is used for:
2136
+ * • the `/negotiate` HTTP POST
2137
+ * • the WebSocket upgrade request
2138
+ * • SSE long-lived GET and send POSTs
2139
+ * • long-polling GETs, POSTs, and DELETE
2140
+ *
2141
+ * Accepts any `Dispatcher` subclass: `Agent`, `Pool`, `Client`,
2142
+ * `ProxyAgent`, `MockAgent`, etc.
2143
+ *
2144
+ * To combine a custom dispatcher with automatic cookie handling, use
2145
+ * `createCookieAgentClass()` to build a cookie-aware variant of your
2146
+ * dispatcher and pass that here. Do **not** chain both
2147
+ * `withDispatcher()` and `withCookies()` - they are mutually exclusive
2148
+ * because `withCookies()` creates its own dispatcher internally.
2149
+ *
2150
+ * @example
2151
+ * ```ts
2152
+ * import { ProxyAgent } from 'undici';
2153
+ *
2154
+ * const conn = new HubConnectionBuilder()
2155
+ * .withUrl('https://example.com/hub')
2156
+ * .withDispatcher(new ProxyAgent('http://proxy:8080'))
2157
+ * .build();
2158
+ * ```
2159
+ */
2160
+ withDispatcher(dispatcher) {
2161
+ 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.");
2162
+ this.#dispatcher = dispatcher;
2163
+ return this;
2164
+ }
2165
+ /**
2166
+ * Provide a custom HTTP client implementation.
2167
+ *
2168
+ * By default `HubConnection` uses `DispatchHttpClient` (undici `dispatch`).
2169
+ * Use this to substitute any of the other undici-backed clients, or
2170
+ * to inject a mock for testing.
2171
+ *
2172
+ * @example
2173
+ * ```ts
2174
+ * import { FetchHttpClient } from '@signalr/client';
2175
+ *
2176
+ * const conn = new HubConnectionBuilder()
2177
+ * .withUrl('https://example.com/hub')
2178
+ * .withHttpClient(new FetchHttpClient())
2179
+ * .build();
2180
+ * ```
2181
+ */
2182
+ withHttpClient(httpClient) {
2183
+ this.#httpClient = httpClient;
2184
+ return this;
2185
+ }
2186
+ /** Select the hub wire protocol (for example, MessagePack). */
2187
+ withHubProtocol(protocol) {
2188
+ this.#protocol = protocol;
2189
+ return this;
2190
+ }
2191
+ /**
2192
+ * Set the minimum log level or provide a custom logger.
2193
+ *
2194
+ * @param logLevelOrLogger
2195
+ * - `LogLevel` number → uses the built-in ConsoleLogger
2196
+ * - `ILogger` object → used as-is
2197
+ */
2198
+ configureLogging(logLevelOrLogger) {
2199
+ this.#logger = resolveLogger(logLevelOrLogger);
2200
+ return this;
2201
+ }
2202
+ withAutomaticReconnect(arg) {
2203
+ if (arg === void 0) this.#reconnectPolicy = new DefaultReconnectPolicy([...DEFAULT_RETRY_DELAYS]);
2204
+ else if (Array.isArray(arg)) this.#reconnectPolicy = new DefaultReconnectPolicy(arg);
2205
+ else if (typeof arg === "object" && typeof arg.nextRetryDelayInMilliseconds === "function") this.#reconnectPolicy = arg;
2206
+ else throw new TypeError("withAutomaticReconnect expects no args, a number[], or an IRetryPolicy.");
2207
+ return this;
2208
+ }
2209
+ build() {
2210
+ if (!this.#url) throw new Error("withUrl() must be called before build().");
2211
+ const effectiveDispatcher = this.#cookieJar !== null ? new CookieAgent$1({ cookies: { jar: this.#cookieJar } }) : this.#dispatcher;
2212
+ const options = {
2213
+ ...this.#logger != null && { logger: this.#logger },
2214
+ ...this.#urlOptions.accessTokenFactory != null && { accessTokenFactory: this.#urlOptions.accessTokenFactory },
2215
+ ...this.#urlOptions.transport != null && { transport: this.#urlOptions.transport },
2216
+ ...this.#urlOptions.headers != null && { headers: this.#urlOptions.headers },
2217
+ ...this.#urlOptions.skipNegotiation != null && { skipNegotiation: this.#urlOptions.skipNegotiation },
2218
+ ...this.#urlOptions.serverTimeoutInMilliseconds != null && { serverTimeoutInMilliseconds: this.#urlOptions.serverTimeoutInMilliseconds },
2219
+ ...this.#urlOptions.keepAliveIntervalInMilliseconds != null && { keepAliveIntervalInMilliseconds: this.#urlOptions.keepAliveIntervalInMilliseconds },
2220
+ ...this.#urlOptions.handshakeTimeoutInMilliseconds != null && { handshakeTimeoutInMilliseconds: this.#urlOptions.handshakeTimeoutInMilliseconds },
2221
+ ...this.#urlOptions.maximumReceiveMessageSize != null && { maximumReceiveMessageSize: this.#urlOptions.maximumReceiveMessageSize },
2222
+ ...this.#reconnectPolicy != null && { reconnectPolicy: this.#reconnectPolicy },
2223
+ ...effectiveDispatcher != null && { dispatcher: effectiveDispatcher },
2224
+ ...this.#httpClient != null && { httpClient: this.#httpClient },
2225
+ ...this.#protocol != null && { protocol: this.#protocol },
2226
+ ...this.#cookieJar != null && { ownsDispatcher: true }
2227
+ };
2228
+ return new HubConnection(this.#url, options);
2229
+ }
2230
+ };
2231
+ var DefaultReconnectPolicy = class {
2232
+ #retryDelays;
2233
+ constructor(retryDelays = [...DEFAULT_RETRY_DELAYS]) {
2234
+ this.#retryDelays = retryDelays;
2235
+ }
2236
+ nextRetryDelayInMilliseconds(context) {
2237
+ return this.#retryDelays[context.previousRetryCount] ?? null;
2238
+ }
2239
+ };
2240
+ //#endregion
2241
+ //#region src/protocols/msgpack-hub-protocol.ts
2242
+ /**
2243
+ * msgpack-hub-protocol.ts
2244
+ *
2245
+ * Implements the SignalR Hub Protocol over MessagePack (version 1).
2246
+ *
2247
+ * Wire format
2248
+ * ───────────
2249
+ * • Handshake: JSON text terminated by ASCII 0x1e - identical to the JSON
2250
+ * protocol, only the "protocol" field changes to "messagepack".
2251
+ * • All subsequent messages: binary MessagePack, each frame prefixed by a
2252
+ * base-128 VarInt carrying the byte-length of the following MessagePack
2253
+ * payload. A single transport payload may contain multiple such frames.
2254
+ *
2255
+ * MessagePack array layout (per message type)
2256
+ * ────────────────────────────────────────────
2257
+ * Invocation [1, headers, invocationId|null, target, args, streamIds]
2258
+ * StreamItem [2, headers, invocationId, item]
2259
+ * Completion [3, headers, invocationId, resultKind, value?]
2260
+ * resultKind: 1 = error string, 2 = void, 3 = non-void result value
2261
+ * StreamInvocation [4, headers, invocationId, target, args, streamIds]
2262
+ * CancelInvocation [5, headers, invocationId]
2263
+ * Ping [6]
2264
+ * Close [7, error|null, allowReconnect]
2265
+ *
2266
+ * Reference:
2267
+ * https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/HubProtocol.md
2268
+ */
2269
+ /**
2270
+ * The JSON-formatted handshake request the client sends once the transport
2271
+ * is open. The server always responds with JSON ({} on success), regardless
2272
+ * of the chosen hub protocol.
2273
+ */
2274
+ const HANDSHAKE_REQUEST$1 = JSON.stringify({
2275
+ protocol: "messagepack",
2276
+ version: 1
2277
+ }) + "";
2278
+ const ResultKind = Object.freeze({
2279
+ Error: 1,
2280
+ Void: 2,
2281
+ NonVoid: 3
2282
+ });
2283
+ /** Write `n` as a base-128 VarInt (little-endian groups of 7 bits). */
2284
+ function writeVarInt(n) {
2285
+ const bytes = [];
2286
+ do {
2287
+ let b = n & 127;
2288
+ n >>>= 7;
2289
+ if (n > 0) b |= 128;
2290
+ bytes.push(b);
2291
+ } while (n > 0);
2292
+ return new Uint8Array(bytes);
2293
+ }
2294
+ /** Read a VarInt from `data` starting at `offset`. Returns value + bytes consumed. */
2295
+ function readVarInt(data, offset) {
2296
+ let value = 0;
2297
+ let shift = 0;
2298
+ let bytesRead = 0;
2299
+ let hasMore = true;
2300
+ while (hasMore) {
2301
+ if (offset + bytesRead >= data.length) throw new Error("MessagePack: unexpected end of data reading VarInt frame length.");
2302
+ const byte = data[offset + bytesRead];
2303
+ hasMore = (byte & 128) !== 0;
2304
+ if (bytesRead === 4 && hasMore) throw new Error("MessagePack: VarInt frame length prefix is longer than 5 bytes.");
2305
+ if (bytesRead === 4 && byte > 7) throw new Error("MessagePack: VarInt frame length exceeds the 2 GB SignalR limit.");
2306
+ value |= (byte & 127) << shift;
2307
+ shift += 7;
2308
+ bytesRead++;
2309
+ }
2310
+ return {
2311
+ value,
2312
+ bytesRead
2313
+ };
2314
+ }
2315
+ function pack(arr) {
2316
+ const payload = encode(arr);
2317
+ const lenPrefix = writeVarInt(payload.length);
2318
+ const result = new Uint8Array(lenPrefix.length + payload.length);
2319
+ result.set(lenPrefix, 0);
2320
+ result.set(payload, lenPrefix.length);
2321
+ return result.buffer.slice(0, result.byteLength);
2322
+ }
2323
+ var MsgpackHubProtocol = class {
2324
+ name = "messagepack";
2325
+ version = 1;
2326
+ transferFormat = TransferFormat.Binary;
2327
+ parseMessages(input, logger) {
2328
+ const raw = typeof input === "string" ? new TextEncoder().encode(input).buffer : input;
2329
+ const data = new Uint8Array(raw);
2330
+ if (data.length === 0) return [];
2331
+ const messages = [];
2332
+ let offset = 0;
2333
+ while (offset < data.length) {
2334
+ const { value: msgLen, bytesRead } = readVarInt(data, offset);
2335
+ offset += bytesRead;
2336
+ if (offset + msgLen > data.length) throw new Error(`MessagePack hub message: declared length ${msgLen} B exceeds remaining data (${data.length - offset} B).`);
2337
+ const frame = data.subarray(offset, offset + msgLen);
2338
+ offset += msgLen;
2339
+ const rawArr = decode(frame);
2340
+ if (!Array.isArray(rawArr) || rawArr.length === 0) throw new Error("Invalid MessagePack hub message: top-level value must be a non-empty array.");
2341
+ const msg = this.#coerce(rawArr, logger);
2342
+ if (msg !== null) messages.push(msg);
2343
+ }
2344
+ return messages;
2345
+ }
2346
+ writeMessage(message) {
2347
+ switch (message.type) {
2348
+ case MessageType.Invocation: return pack([
2349
+ MessageType.Invocation,
2350
+ message.headers ?? {},
2351
+ message.invocationId ?? null,
2352
+ message.target,
2353
+ message.arguments,
2354
+ message.streamIds ?? []
2355
+ ]);
2356
+ case MessageType.StreamItem: return pack([
2357
+ MessageType.StreamItem,
2358
+ message.headers ?? {},
2359
+ message.invocationId,
2360
+ message.item
2361
+ ]);
2362
+ case MessageType.Completion: {
2363
+ const hasError = message.error != null;
2364
+ const hasResult = Object.prototype.hasOwnProperty.call(message, "result");
2365
+ if (hasError) return pack([
2366
+ MessageType.Completion,
2367
+ message.headers ?? {},
2368
+ message.invocationId,
2369
+ ResultKind.Error,
2370
+ message.error
2371
+ ]);
2372
+ else if (hasResult) return pack([
2373
+ MessageType.Completion,
2374
+ message.headers ?? {},
2375
+ message.invocationId,
2376
+ ResultKind.NonVoid,
2377
+ message.result
2378
+ ]);
2379
+ else return pack([
2380
+ MessageType.Completion,
2381
+ message.headers ?? {},
2382
+ message.invocationId,
2383
+ ResultKind.Void
2384
+ ]);
2385
+ }
2386
+ case MessageType.StreamInvocation: return pack([
2387
+ MessageType.StreamInvocation,
2388
+ message.headers ?? {},
2389
+ message.invocationId,
2390
+ message.target,
2391
+ message.arguments,
2392
+ message.streamIds ?? []
2393
+ ]);
2394
+ case MessageType.CancelInvocation: return pack([
2395
+ MessageType.CancelInvocation,
2396
+ message.headers ?? {},
2397
+ message.invocationId
2398
+ ]);
2399
+ case MessageType.Ping: return pack([MessageType.Ping]);
2400
+ case MessageType.Close: {
2401
+ const arr = [MessageType.Close, message.error ?? null];
2402
+ if (message.allowReconnect !== void 0) arr.push(message.allowReconnect);
2403
+ return pack(arr);
2404
+ }
2405
+ }
2406
+ }
2407
+ /**
2408
+ * Validate and narrow a raw decoded array into a typed `HubMessage`.
2409
+ * Returns `null` for unknown types - they are logged as warnings.
2410
+ */
2411
+ #coerce(arr, logger) {
2412
+ const type = arr[0];
2413
+ if (typeof type !== "number") throw new Error(`Invalid MessagePack hub message: first element (type) must be a number, got ${JSON.stringify(type)}.`);
2414
+ switch (type) {
2415
+ case MessageType.Invocation: return this.#coerceInvocation(arr);
2416
+ case MessageType.StreamItem: return this.#coerceStreamItem(arr);
2417
+ case MessageType.Completion: return this.#coerceCompletion(arr);
2418
+ case MessageType.StreamInvocation: return this.#coerceStreamInvocation(arr);
2419
+ case MessageType.CancelInvocation: return this.#coerceCancelInvocation(arr);
2420
+ case MessageType.Ping: return this.#coercePing();
2421
+ case MessageType.Close: return this.#coerceClose(arr);
2422
+ default:
2423
+ logger.log(3, `Ignoring MessagePack message with unknown type ${String(type)}.`);
2424
+ return null;
2425
+ }
2426
+ }
2427
+ #coerceInvocation(arr) {
2428
+ const target = arr[3];
2429
+ const args = arr[4];
2430
+ if (typeof target !== "string") throw new Error("Invalid Invocation message: index 3 ('target') must be a string.");
2431
+ if (!Array.isArray(args)) throw new Error("Invalid Invocation message: index 4 ('arguments') must be an array.");
2432
+ const rawId = arr[2];
2433
+ const rawSids = arr[5];
2434
+ const rawHdrs = arr[1];
2435
+ const streamIds = rawSids == null ? [] : parseStreamIds(rawSids);
2436
+ return {
2437
+ type: MessageType.Invocation,
2438
+ target,
2439
+ arguments: args,
2440
+ ...rawId != null && rawId !== "" && { invocationId: parseInvocationId(rawId) },
2441
+ ...streamIds.length > 0 && { streamIds },
2442
+ ...isNonEmptyObject(rawHdrs) && { headers: parseHeaders(rawHdrs) }
2443
+ };
2444
+ }
2445
+ #coerceStreamItem(arr) {
2446
+ const rawId = arr[2];
2447
+ if (rawId == null) throw new Error("Invalid StreamItem message: index 2 ('invocationId') is required.");
2448
+ return {
2449
+ type: MessageType.StreamItem,
2450
+ invocationId: parseInvocationId(rawId),
2451
+ item: arr[3],
2452
+ ...isNonEmptyObject(arr[1]) && { headers: parseHeaders(arr[1]) }
2453
+ };
2454
+ }
2455
+ #coerceCompletion(arr) {
2456
+ const rawId = arr[2];
2457
+ if (rawId == null) throw new Error("Invalid Completion message: index 2 ('invocationId') is required.");
2458
+ const invocationId = parseInvocationId(rawId);
2459
+ const resultKind = arr[3];
2460
+ const hdrs = isNonEmptyObject(arr[1]) ? { headers: parseHeaders(arr[1]) } : {};
2461
+ switch (resultKind) {
2462
+ case ResultKind.Error: return {
2463
+ type: MessageType.Completion,
2464
+ invocationId,
2465
+ error: String(arr[4] ?? ""),
2466
+ ...hdrs
2467
+ };
2468
+ case ResultKind.Void: return {
2469
+ type: MessageType.Completion,
2470
+ invocationId,
2471
+ ...hdrs
2472
+ };
2473
+ case ResultKind.NonVoid: return {
2474
+ type: MessageType.Completion,
2475
+ invocationId,
2476
+ result: arr[4],
2477
+ ...hdrs
2478
+ };
2479
+ default: throw new Error(`Invalid Completion message: unknown resultKind ${String(resultKind)}.`);
2480
+ }
2481
+ }
2482
+ #coerceStreamInvocation(arr) {
2483
+ const rawId = arr[2];
2484
+ const target = arr[3];
2485
+ const args = arr[4];
2486
+ if (rawId == null) throw new Error("Invalid StreamInvocation message: index 2 ('invocationId') is required.");
2487
+ if (typeof target !== "string") throw new Error("Invalid StreamInvocation message: index 3 ('target') must be a string.");
2488
+ if (!Array.isArray(args)) throw new Error("Invalid StreamInvocation message: index 4 ('arguments') must be an array.");
2489
+ const rawSids = arr[5];
2490
+ const streamIds = rawSids == null ? [] : parseStreamIds(rawSids);
2491
+ return {
2492
+ type: MessageType.StreamInvocation,
2493
+ invocationId: parseInvocationId(rawId),
2494
+ target,
2495
+ arguments: args,
2496
+ ...streamIds.length > 0 && { streamIds },
2497
+ ...isNonEmptyObject(arr[1]) && { headers: parseHeaders(arr[1]) }
2498
+ };
2499
+ }
2500
+ #coerceCancelInvocation(arr) {
2501
+ const rawId = arr[2];
2502
+ if (rawId == null) throw new Error("Invalid CancelInvocation message: index 2 ('invocationId') is required.");
2503
+ return {
2504
+ type: MessageType.CancelInvocation,
2505
+ invocationId: parseInvocationId(rawId),
2506
+ ...isNonEmptyObject(arr[1]) && { headers: parseHeaders(arr[1]) }
2507
+ };
2508
+ }
2509
+ #coercePing() {
2510
+ return { type: MessageType.Ping };
2511
+ }
2512
+ #coerceClose(arr) {
2513
+ if (arr[2] != null && typeof arr[2] !== "boolean") throw new Error("Invalid Close message: index 2 ('allowReconnect') must be a boolean.");
2514
+ return {
2515
+ type: MessageType.Close,
2516
+ ...arr[1] != null && { error: String(arr[1]) },
2517
+ ...arr[2] != null && { allowReconnect: arr[2] }
2518
+ };
2519
+ }
2520
+ static invocation(invocationId, target, args, streamIds) {
2521
+ return {
2522
+ type: MessageType.Invocation,
2523
+ invocationId,
2524
+ target,
2525
+ arguments: args,
2526
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
2527
+ };
2528
+ }
2529
+ /** Fire-and-forget (no `invocationId`). */
2530
+ static send(target, args, streamIds) {
2531
+ return {
2532
+ type: MessageType.Invocation,
2533
+ target,
2534
+ arguments: args,
2535
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
2536
+ };
2537
+ }
2538
+ static streamInvocation(invocationId, target, args, streamIds) {
2539
+ return {
2540
+ type: MessageType.StreamInvocation,
2541
+ invocationId,
2542
+ target,
2543
+ arguments: args,
2544
+ ...streamIds != null && streamIds.length > 0 ? { streamIds } : {}
2545
+ };
2546
+ }
2547
+ static cancelInvocation(invocationId) {
2548
+ return {
2549
+ type: MessageType.CancelInvocation,
2550
+ invocationId
2551
+ };
2552
+ }
2553
+ static streamItem(invocationId, item) {
2554
+ return {
2555
+ type: MessageType.StreamItem,
2556
+ invocationId,
2557
+ item
2558
+ };
2559
+ }
2560
+ static completion(invocationId, result, error) {
2561
+ return {
2562
+ type: MessageType.Completion,
2563
+ invocationId,
2564
+ ...error != null && { error },
2565
+ ...result != null && { result }
2566
+ };
2567
+ }
2568
+ static ping() {
2569
+ return { type: MessageType.Ping };
2570
+ }
2571
+ };
2572
+ function isNonEmptyObject(v) {
2573
+ return v !== null && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length > 0;
2574
+ }
2575
+ function parseInvocationId(value) {
2576
+ if (typeof value !== "string") throw new Error("Invalid MessagePack message: invocationId must be a string.");
2577
+ return toInvocationId(value);
2578
+ }
2579
+ function parseStreamIds(value) {
2580
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) throw new Error("Invalid MessagePack message: streamIds must be an array of strings.");
2581
+ return value;
2582
+ }
2583
+ function parseHeaders(value) {
2584
+ if (!isNonEmptyObject(value) || !Object.values(value).every((entry) => typeof entry === "string")) throw new Error("Invalid MessagePack message: headers must contain string values.");
2585
+ return value;
2586
+ }
2587
+ //#endregion
2588
+ //#region src/ws-client.ts
2589
+ /**
2590
+ * ws-client.ts
2591
+ *
2592
+ * From-scratch WebSocket client (RFC 6455) built on Node's built-in `net`
2593
+ * and `tls` modules. Zero external runtime dependencies.
2594
+ *
2595
+ * Typed EventEmitter via interface declaration merging.
2596
+ *
2597
+ * Emitted events
2598
+ * 'open' - handshake complete, ready to send/receive
2599
+ * 'message' (string) - UTF-8 text frame received
2600
+ * 'binary' (Buffer) - binary frame received
2601
+ * 'ping' (Buffer) - ping frame received (pong sent automatically)
2602
+ * 'pong' (Buffer) - pong frame received
2603
+ * 'close' - socket fully closed
2604
+ * 'error' (Error) - socket-level error
2605
+ */
2606
+ let WebSocketReadyState = /* @__PURE__ */ function(WebSocketReadyState) {
2607
+ WebSocketReadyState[WebSocketReadyState["Connecting"] = 0] = "Connecting";
2608
+ WebSocketReadyState[WebSocketReadyState["Open"] = 1] = "Open";
2609
+ WebSocketReadyState[WebSocketReadyState["Closing"] = 2] = "Closing";
2610
+ WebSocketReadyState[WebSocketReadyState["Closed"] = 3] = "Closed";
2611
+ return WebSocketReadyState;
2612
+ }({});
2613
+ const WS_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
2614
+ var WebSocketClient = class extends EventEmitter {
2615
+ #socket = null;
2616
+ #state = 3;
2617
+ #frameBuf = Buffer.alloc(0);
2618
+ #fragments = [];
2619
+ #fragmentOpcode = null;
2620
+ #handshakeBuf = Buffer.alloc(0);
2621
+ #handshakeResolve = null;
2622
+ #handshakeReject = null;
2623
+ get readyState() {
2624
+ return this.#state;
2625
+ }
2626
+ /**
2627
+ * Open a WebSocket connection.
2628
+ * @param url ws:// or wss:// URL
2629
+ * @param headers Extra HTTP headers for the upgrade request
2630
+ */
2631
+ connect(url, headers = {}) {
2632
+ return new Promise((resolve, reject) => {
2633
+ if (this.#state !== 3) {
2634
+ reject(/* @__PURE__ */ new Error("WebSocket is already open or connecting."));
2635
+ return;
2636
+ }
2637
+ this.#state = 0;
2638
+ this.#handshakeResolve = resolve;
2639
+ this.#handshakeReject = reject;
2640
+ const parsed = new URL(url);
2641
+ const isSecure = parsed.protocol === "wss:";
2642
+ const host = parsed.hostname;
2643
+ const port = Number(parsed.port) || (isSecure ? 443 : 80);
2644
+ /* istanbul ignore next -- URL normalizes an empty pathname to '/' */
2645
+ const resource = (parsed.pathname || "/") + (parsed.search || "");
2646
+ const portSuffix = parsed.port ? `:${parsed.port}` : "";
2647
+ const nonce = crypto.randomBytes(16).toString("base64");
2648
+ const expected = computeAccept(nonce);
2649
+ const request = [
2650
+ `GET ${resource} HTTP/1.1`,
2651
+ `Host: ${host}${portSuffix}`,
2652
+ "Upgrade: websocket",
2653
+ "Connection: Upgrade",
2654
+ `Sec-WebSocket-Key: ${nonce}`,
2655
+ "Sec-WebSocket-Version: 13",
2656
+ ...Object.entries(headers).map(([k, v]) => `${k}: ${v}`),
2657
+ "",
2658
+ ""
2659
+ ].join("\r\n");
2660
+ const onSocketData = (chunk) => {
2661
+ this.#onHandshakeData(chunk, expected);
2662
+ };
2663
+ const socketOpts = {
2664
+ host,
2665
+ port
2666
+ };
2667
+ if (isSecure) this.#socket = tls.connect(
2668
+ {
2669
+ ...socketOpts,
2670
+ servername: host
2671
+ },
2672
+ /* istanbul ignore next -- identical write callback is covered via TCP */
2673
+ () => {
2674
+ this.#socket.write(request);
2675
+ }
2676
+ );
2677
+ else this.#socket = net.connect(socketOpts, () => {
2678
+ this.#socket.write(request);
2679
+ });
2680
+ this.#socket.on("data", onSocketData);
2681
+ this.#socket.on("error", (err) => {
2682
+ if (this.#state === 0) {
2683
+ this.#state = 3;
2684
+ this.#handshakeReject(err);
2685
+ } else if (this.#state !== 2) this.emit("error", err);
2686
+ });
2687
+ this.#socket.on("close", () => this.#onSocketClose());
2688
+ this.#socket.on("end", () => this.#socket?.destroy());
2689
+ });
2690
+ }
2691
+ /**
2692
+ * Send a text or binary message.
2693
+ * @throws if the socket is not open.
2694
+ */
2695
+ send(data) {
2696
+ if (this.#state !== 1) throw new Error("WebSocket is not open.");
2697
+ if (typeof data === "string") this.#writeFrame(1, Buffer.from(data, "utf8"));
2698
+ else this.#writeFrame(2, Buffer.isBuffer(data) ? data : Buffer.from(data));
2699
+ }
2700
+ /**
2701
+ * Initiate a clean close handshake.
2702
+ * @param code WebSocket close code (default 1000 = Normal Closure)
2703
+ * @param reason UTF-8 reason string (max 123 bytes)
2704
+ */
2705
+ close(code = 1e3, reason = "") {
2706
+ if (this.#state !== 1) return;
2707
+ this.#state = 2;
2708
+ const reasonBuf = Buffer.from(reason, "utf8");
2709
+ const payload = Buffer.allocUnsafe(2 + reasonBuf.length);
2710
+ payload.writeUInt16BE(code, 0);
2711
+ reasonBuf.copy(payload, 2);
2712
+ this.#writeFrame(8, payload);
2713
+ }
2714
+ #onHandshakeData(chunk, expectedAccept) {
2715
+ this.#handshakeBuf = Buffer.concat([this.#handshakeBuf, chunk]);
2716
+ const idx = indexOfCRLFCRLF(this.#handshakeBuf);
2717
+ if (idx === -1) return;
2718
+ const headerStr = this.#handshakeBuf.subarray(0, idx).toString("ascii");
2719
+ const remainder = this.#handshakeBuf.subarray(idx + 4);
2720
+ const lines = headerStr.split("\r\n");
2721
+ /* istanbul ignore next -- split() always returns at least one element */
2722
+ const statusLine = lines[0] ?? "";
2723
+ if (!statusLine.includes("101")) {
2724
+ this.#failHandshake(/* @__PURE__ */ new Error(`WebSocket upgrade failed - server replied: ${statusLine}`));
2725
+ return;
2726
+ }
2727
+ let accept = "";
2728
+ for (let i = 1; i < lines.length; i++) {
2729
+ const line = lines[i];
2730
+ const colon = line.indexOf(":");
2731
+ if (colon === -1) continue;
2732
+ if (line.slice(0, colon).trim().toLowerCase() === "sec-websocket-accept") {
2733
+ accept = line.slice(colon + 1).trim();
2734
+ break;
2735
+ }
2736
+ }
2737
+ if (accept !== expectedAccept) {
2738
+ this.#failHandshake(/* @__PURE__ */ new Error(`Invalid Sec-WebSocket-Accept (got "${accept}", expected "${expectedAccept}")`));
2739
+ return;
2740
+ }
2741
+ this.#state = 1;
2742
+ this.#handshakeBuf = Buffer.alloc(0);
2743
+ this.#socket.removeAllListeners("data");
2744
+ this.#socket.on("data", (c) => this.#onFrameData(c));
2745
+ const r = this.#handshakeResolve;
2746
+ this.#handshakeResolve = null;
2747
+ this.#handshakeReject = null;
2748
+ r();
2749
+ this.emit("open");
2750
+ if (remainder.length > 0) this.#onFrameData(remainder);
2751
+ }
2752
+ #failHandshake(err) {
2753
+ this.#state = 3;
2754
+ this.#socket?.destroy();
2755
+ const r = this.#handshakeReject;
2756
+ this.#handshakeResolve = null;
2757
+ this.#handshakeReject = null;
2758
+ r(err);
2759
+ }
2760
+ #onFrameData(chunk) {
2761
+ this.#frameBuf = Buffer.concat([this.#frameBuf, chunk]);
2762
+ this.#drainFrames();
2763
+ }
2764
+ #drainFrames() {
2765
+ for (;;) {
2766
+ const result = tryParseFrame(this.#frameBuf);
2767
+ if (!result) break;
2768
+ this.#frameBuf = this.#frameBuf.subarray(result.consumed);
2769
+ this.#dispatchFrame(result.frame);
2770
+ }
2771
+ }
2772
+ #dispatchFrame({ fin, opcode, payload }) {
2773
+ switch (opcode) {
2774
+ case 1:
2775
+ case 2:
2776
+ if (!fin) {
2777
+ this.#fragmentOpcode = opcode;
2778
+ this.#fragments = [payload];
2779
+ } else this.#emitData(opcode, payload);
2780
+ break;
2781
+ case 0:
2782
+ this.#fragments.push(payload);
2783
+ if (fin) {
2784
+ const full = Buffer.concat(this.#fragments);
2785
+ const op = this.#fragmentOpcode;
2786
+ this.#fragments = [];
2787
+ this.#fragmentOpcode = null;
2788
+ this.#emitData(op, full);
2789
+ }
2790
+ break;
2791
+ case 9:
2792
+ this.#writeFrame(10, payload);
2793
+ this.emit("ping", payload);
2794
+ break;
2795
+ case 10:
2796
+ this.emit("pong", payload);
2797
+ break;
2798
+ case 8:
2799
+ if (this.#state === 1) this.#writeFrame(8, payload);
2800
+ this.#state = 3;
2801
+ this.#socket?.destroy();
2802
+ this.emit("close");
2803
+ break;
2804
+ default: break;
2805
+ }
2806
+ }
2807
+ #emitData(opcode, payload) {
2808
+ if (opcode === 1) this.emit("message", payload.toString("utf8"));
2809
+ else this.emit("binary", payload);
2810
+ }
2811
+ /**
2812
+ * Encode and send one WebSocket frame.
2813
+ * All client→server frames MUST be masked (RFC 6455 §5.3).
2814
+ */
2815
+ #writeFrame(opcode, payload) {
2816
+ /* istanbul ignore next -- public methods validate state before writing */
2817
+ if (!this.#socket || this.#socket.destroyed) return;
2818
+ const len = payload.length;
2819
+ let header;
2820
+ if (len < 126) {
2821
+ header = Buffer.allocUnsafe(2);
2822
+ header[1] = 128 | len;
2823
+ } else if (len < 65536) {
2824
+ header = Buffer.allocUnsafe(4);
2825
+ header[1] = 254;
2826
+ header.writeUInt16BE(len, 2);
2827
+ } else {
2828
+ header = Buffer.allocUnsafe(10);
2829
+ header[1] = 255;
2830
+ header.writeUInt32BE(0, 2);
2831
+ header.writeUInt32BE(len, 6);
2832
+ }
2833
+ header[0] = 128 | opcode;
2834
+ const maskKey = crypto.randomBytes(4);
2835
+ const maskedPayload = Buffer.allocUnsafe(len);
2836
+ for (let i = 0; i < len; i++) maskedPayload[i] = payload[i] ^ maskKey[i % 4];
2837
+ this.#socket.write(Buffer.concat([
2838
+ header,
2839
+ maskKey,
2840
+ maskedPayload
2841
+ ]));
2842
+ }
2843
+ #onSocketClose() {
2844
+ if (this.#state !== 3) {
2845
+ this.#state = 3;
2846
+ this.emit("close");
2847
+ }
2848
+ }
2849
+ };
2850
+ function computeAccept(key) {
2851
+ return crypto.createHash("sha1").update(key + WS_MAGIC).digest("base64");
2852
+ }
2853
+ function indexOfCRLFCRLF(buf) {
2854
+ 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;
2855
+ return -1;
2856
+ }
2857
+ function tryParseFrame(buf) {
2858
+ if (buf.length < 2) return null;
2859
+ const b0 = buf[0];
2860
+ const b1 = buf[1];
2861
+ const fin = (b0 & 128) !== 0;
2862
+ const opcode = b0 & 15;
2863
+ const masked = (b1 & 128) !== 0;
2864
+ let payLen = b1 & 127;
2865
+ let offset = 2;
2866
+ if (payLen === 126) {
2867
+ if (buf.length < offset + 2) return null;
2868
+ payLen = buf.readUInt16BE(offset);
2869
+ offset += 2;
2870
+ } else if (payLen === 127) {
2871
+ if (buf.length < offset + 8) return null;
2872
+ /* istanbul ignore next -- allocating a >4 GB test frame is impractical */
2873
+ if (buf.readUInt32BE(offset) !== 0) throw new Error("Received a frame with payload > 4 GB - not supported.");
2874
+ payLen = buf.readUInt32BE(offset + 4);
2875
+ offset += 8;
2876
+ }
2877
+ let maskBytes = null;
2878
+ if (masked) {
2879
+ if (buf.length < offset + 4) return null;
2880
+ maskBytes = buf.subarray(offset, offset + 4);
2881
+ offset += 4;
2882
+ }
2883
+ if (buf.length < offset + payLen) return null;
2884
+ let payload = buf.subarray(offset, offset + payLen);
2885
+ if (masked && maskBytes !== null) {
2886
+ payload = Buffer.from(payload);
2887
+ for (let i = 0; i < payload.length; i++) payload[i] = payload[i] ^ maskBytes[i % 4];
2888
+ }
2889
+ return {
2890
+ frame: {
2891
+ fin,
2892
+ opcode,
2893
+ payload
2894
+ },
2895
+ consumed: offset + payLen
2896
+ };
2897
+ }
2898
+ //#endregion
2899
+ 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 };
2900
+
2901
+ //# sourceMappingURL=index.mjs.map