@exhumer/signalr-client 1.0.2 → 2.1.0

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