@loadstrike/loadstrike-sdk 1.0.31001 → 1.0.32601
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/README.md +16 -2
- package/dist/cjs/internal/prometheus-remote-write.js +37 -0
- package/dist/cjs/internal/reporting-sink-http-error.js +17 -0
- package/dist/cjs/internal/vendor-metric-payloads.js +390 -0
- package/dist/cjs/iteration-observations.js +24 -8
- package/dist/cjs/local-report-input.js +21 -0
- package/dist/cjs/local.js +48 -63
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-containment.js +242 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +413 -136
- package/dist/cjs/runtime.js +237 -8
- package/dist/cjs/sinks.js +1337 -38
- package/dist/cjs/transports.js +1339 -151
- package/dist/esm/internal/prometheus-remote-write.js +31 -0
- package/dist/esm/internal/reporting-sink-http-error.js +13 -0
- package/dist/esm/internal/vendor-metric-payloads.js +382 -0
- package/dist/esm/iteration-observations.js +24 -8
- package/dist/esm/local-report-input.js +17 -0
- package/dist/esm/local.js +49 -64
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-containment.js +238 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +413 -136
- package/dist/esm/runtime.js +239 -10
- package/dist/esm/sinks.js +1334 -35
- package/dist/esm/transports.js +1335 -151
- package/dist/types/contracts.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/internal/prometheus-remote-write.d.ts +2 -0
- package/dist/types/internal/reporting-sink-http-error.d.ts +6 -0
- package/dist/types/internal/vendor-metric-payloads.d.ts +48 -0
- package/dist/types/local-report-input.d.ts +6 -0
- package/dist/types/local.d.ts +0 -6
- package/dist/types/report-history.d.ts +124 -0
- package/dist/types/reporting-containment.d.ts +2 -0
- package/dist/types/reporting-svg.d.ts +2 -0
- package/dist/types/reporting.d.ts +6 -3
- package/dist/types/runtime.d.ts +24 -0
- package/dist/types/sinks.d.ts +134 -17
- package/dist/types/transports.d.ts +2 -0
- package/package.json +9 -3
- package/dist/cjs/internal-build.js +0 -4
- package/dist/esm/internal-build.js +0 -1
- package/dist/types/internal-build.d.ts +0 -1
package/dist/esm/transports.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import WebSocket from "ws";
|
|
2
3
|
import { TrackingFieldSelector } from "./correlation.js";
|
|
3
4
|
export const LOADSTRIKE_TRACE_ID_HEADER = "loadstrike-trace-id";
|
|
4
5
|
export const LOADSTRIKE_TRACE_ID_TRACKING_FIELD = `header:${LOADSTRIKE_TRACE_ID_HEADER}`;
|
|
6
|
+
const NATIVE_GRPC_EXECUTION_UNAVAILABLE_MESSAGE = "Native gRPC execution is not available in this SDK version. Provide the endpoint Produce/Consume delegate instead.";
|
|
5
7
|
class TrafficEndpointDefinitionModel {
|
|
6
8
|
get JsonSerializerSettings() {
|
|
7
9
|
return this.JsonSettings;
|
|
@@ -421,6 +423,15 @@ class GrpcEndpointDefinitionModel extends TrafficEndpointDefinitionModel {
|
|
|
421
423
|
}
|
|
422
424
|
Validate() {
|
|
423
425
|
super.Validate();
|
|
426
|
+
const hasModeAsyncDelegate = this.Mode === "Produce"
|
|
427
|
+
? typeof this.ProduceAsync === "function"
|
|
428
|
+
: typeof this.ConsumeAsync === "function";
|
|
429
|
+
const hasModeDelegate = this.Mode === "Produce"
|
|
430
|
+
? typeof this.Produce === "function" || hasModeAsyncDelegate
|
|
431
|
+
: typeof this.Consume === "function" || hasModeAsyncDelegate;
|
|
432
|
+
if (this.NativeClient && !hasModeDelegate) {
|
|
433
|
+
throw new Error(NATIVE_GRPC_EXECUTION_UNAVAILABLE_MESSAGE);
|
|
434
|
+
}
|
|
424
435
|
requireNonEmptyString(this.Target, "Target must be provided for gRPC endpoint definitions.");
|
|
425
436
|
requireNonEmptyString(this.ServiceName, "ServiceName must be provided for gRPC endpoint definitions.");
|
|
426
437
|
requireNonEmptyString(this.MethodName, "MethodName must be provided for gRPC endpoint definitions.");
|
|
@@ -428,57 +439,204 @@ class GrpcEndpointDefinitionModel extends TrafficEndpointDefinitionModel {
|
|
|
428
439
|
if (this.DeadlineSeconds <= 0) {
|
|
429
440
|
throw new RangeError("Deadline must be greater than zero.");
|
|
430
441
|
}
|
|
431
|
-
this.NativeClient
|
|
432
|
-
if (this.Mode === "Produce" && typeof this.ProduceAsync !== "function" && !this.NativeClient) {
|
|
442
|
+
if (this.Mode === "Produce" && !hasModeDelegate && !this.NativeClient) {
|
|
433
443
|
throw new Error("ProduceAsync delegate must be provided when endpoint mode is Produce.");
|
|
434
444
|
}
|
|
435
|
-
if (this.Mode === "Consume" &&
|
|
445
|
+
if (this.Mode === "Consume" && !hasModeDelegate && !this.NativeClient) {
|
|
436
446
|
throw new Error("ConsumeAsync delegate must be provided when endpoint mode is Consume.");
|
|
437
447
|
}
|
|
438
448
|
}
|
|
439
449
|
}
|
|
450
|
+
const webSocketReconnectValidationInputs = new WeakMap();
|
|
451
|
+
const webSocketMessageValidationInputs = new WeakMap();
|
|
452
|
+
const webSocketExpectedValidationInputs = new WeakMap();
|
|
453
|
+
const webSocketNativeValidationInputs = new WeakMap();
|
|
454
|
+
const NATIVE_WEBSOCKET_RECONNECT_INPUT_FIELDS = [
|
|
455
|
+
["MaxAttempts", "maxAttempts"],
|
|
456
|
+
["DelayMs", "delayMs"],
|
|
457
|
+
["DelaySeconds", "delaySeconds", "Delay", "delay"]
|
|
458
|
+
];
|
|
459
|
+
const NATIVE_WEBSOCKET_MESSAGE_INPUT_FIELDS = [
|
|
460
|
+
["Kind", "kind"],
|
|
461
|
+
["TextPayload", "textPayload"],
|
|
462
|
+
["BinaryPayload", "binaryPayload"]
|
|
463
|
+
];
|
|
464
|
+
const NATIVE_WEBSOCKET_EXPECTED_INPUT_FIELDS = [
|
|
465
|
+
["ContainsText", "containsText"],
|
|
466
|
+
["ContainsBytes", "containsBytes"],
|
|
467
|
+
["TimeoutMs", "timeoutMs"],
|
|
468
|
+
["TimeoutSeconds", "timeoutSeconds", "Timeout", "timeout"]
|
|
469
|
+
];
|
|
470
|
+
const NATIVE_WEBSOCKET_CLIENT_INPUT_FIELDS = [
|
|
471
|
+
["Headers", "headers"],
|
|
472
|
+
["Cookies", "cookies"],
|
|
473
|
+
["PingIntervalMs", "pingIntervalMs"],
|
|
474
|
+
["PingIntervalSeconds", "pingIntervalSeconds", "PingInterval", "pingInterval"],
|
|
475
|
+
["Reconnect", "reconnect"],
|
|
476
|
+
["Messages", "messages"],
|
|
477
|
+
["ExpectedMessages", "expectedMessages"]
|
|
478
|
+
];
|
|
479
|
+
function captureSelectedNativeWebSocketFields(source, fieldGroups) {
|
|
480
|
+
const captured = {};
|
|
481
|
+
for (const fieldGroup of fieldGroups) {
|
|
482
|
+
const canonicalName = fieldGroup[0];
|
|
483
|
+
for (const fieldName of fieldGroup) {
|
|
484
|
+
if (fieldName in source) {
|
|
485
|
+
captured[canonicalName] = source[fieldName];
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return captured;
|
|
491
|
+
}
|
|
492
|
+
function captureNativeWebSocketRecordEntries(value) {
|
|
493
|
+
if (!isRecord(value)) {
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
const captured = {};
|
|
497
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
498
|
+
captured[key] = entry;
|
|
499
|
+
}
|
|
500
|
+
return captured;
|
|
501
|
+
}
|
|
502
|
+
function captureNativeWebSocketReconnectInput(source) {
|
|
503
|
+
return captureSelectedNativeWebSocketFields(source, NATIVE_WEBSOCKET_RECONNECT_INPUT_FIELDS);
|
|
504
|
+
}
|
|
505
|
+
function captureNativeWebSocketMessageInput(source) {
|
|
506
|
+
const captured = captureSelectedNativeWebSocketFields(source, NATIVE_WEBSOCKET_MESSAGE_INPUT_FIELDS);
|
|
507
|
+
if (Array.isArray(captured.BinaryPayload)) {
|
|
508
|
+
captured.BinaryPayload = Array.from(captured.BinaryPayload);
|
|
509
|
+
}
|
|
510
|
+
return captured;
|
|
511
|
+
}
|
|
512
|
+
function captureNativeWebSocketExpectedInput(source) {
|
|
513
|
+
const captured = captureSelectedNativeWebSocketFields(source, NATIVE_WEBSOCKET_EXPECTED_INPUT_FIELDS);
|
|
514
|
+
if (Array.isArray(captured.ContainsBytes)) {
|
|
515
|
+
captured.ContainsBytes = Array.from(captured.ContainsBytes);
|
|
516
|
+
}
|
|
517
|
+
return captured;
|
|
518
|
+
}
|
|
519
|
+
function captureNativeWebSocketOptionsInput(source) {
|
|
520
|
+
const captured = captureSelectedNativeWebSocketFields(source, NATIVE_WEBSOCKET_CLIENT_INPUT_FIELDS);
|
|
521
|
+
if ("Headers" in captured) {
|
|
522
|
+
captured.Headers = captureNativeWebSocketRecordEntries(captured.Headers);
|
|
523
|
+
}
|
|
524
|
+
if (Array.isArray(captured.Cookies)) {
|
|
525
|
+
captured.Cookies = Array.from(captured.Cookies);
|
|
526
|
+
}
|
|
527
|
+
if (isRecord(captured.Reconnect) && !(captured.Reconnect instanceof WebSocketReconnectPolicy)) {
|
|
528
|
+
captured.Reconnect = captureNativeWebSocketReconnectInput(captured.Reconnect);
|
|
529
|
+
}
|
|
530
|
+
if (Array.isArray(captured.Messages)) {
|
|
531
|
+
captured.Messages = captured.Messages.map((entry) => (entry instanceof WebSocketMessageSpec || !isRecord(entry)
|
|
532
|
+
? entry
|
|
533
|
+
: captureNativeWebSocketMessageInput(entry)));
|
|
534
|
+
}
|
|
535
|
+
if (Array.isArray(captured.ExpectedMessages)) {
|
|
536
|
+
captured.ExpectedMessages = captured.ExpectedMessages.map((entry) => (entry instanceof WebSocketExpectedMessage || !isRecord(entry)
|
|
537
|
+
? entry
|
|
538
|
+
: captureNativeWebSocketExpectedInput(entry)));
|
|
539
|
+
}
|
|
540
|
+
return captured;
|
|
541
|
+
}
|
|
542
|
+
function snapshotNativeWebSocketInput(value, seen = new WeakMap()) {
|
|
543
|
+
if (value == null || typeof value !== "object") {
|
|
544
|
+
return value;
|
|
545
|
+
}
|
|
546
|
+
if (value instanceof WebSocketReconnectPolicy
|
|
547
|
+
|| value instanceof WebSocketMessageSpec
|
|
548
|
+
|| value instanceof WebSocketExpectedMessage) {
|
|
549
|
+
return value;
|
|
550
|
+
}
|
|
551
|
+
const existing = seen.get(value);
|
|
552
|
+
if (existing !== undefined) {
|
|
553
|
+
return existing;
|
|
554
|
+
}
|
|
555
|
+
if (value instanceof Uint8Array) {
|
|
556
|
+
return new Uint8Array(value);
|
|
557
|
+
}
|
|
558
|
+
if (value instanceof ArrayBuffer) {
|
|
559
|
+
return value.slice(0);
|
|
560
|
+
}
|
|
561
|
+
if (Array.isArray(value)) {
|
|
562
|
+
const clone = [];
|
|
563
|
+
seen.set(value, clone);
|
|
564
|
+
for (const entry of value) {
|
|
565
|
+
clone.push(snapshotNativeWebSocketInput(entry, seen));
|
|
566
|
+
}
|
|
567
|
+
return clone;
|
|
568
|
+
}
|
|
569
|
+
const clone = {};
|
|
570
|
+
seen.set(value, clone);
|
|
571
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
572
|
+
clone[key] = snapshotNativeWebSocketInput(entry, seen);
|
|
573
|
+
}
|
|
574
|
+
return clone;
|
|
575
|
+
}
|
|
576
|
+
function defineSynchronizedPublicAlias(target, alias, read, write) {
|
|
577
|
+
Object.defineProperty(target, alias, {
|
|
578
|
+
configurable: true,
|
|
579
|
+
enumerable: true,
|
|
580
|
+
get: read,
|
|
581
|
+
set: write
|
|
582
|
+
});
|
|
583
|
+
}
|
|
440
584
|
export class WebSocketReconnectPolicy {
|
|
441
585
|
constructor(initial = {}) {
|
|
442
586
|
this.MaxAttempts = 0;
|
|
443
587
|
this.maxAttempts = 0;
|
|
444
588
|
this.DelaySeconds = 1;
|
|
445
589
|
this.delaySeconds = 1;
|
|
446
|
-
|
|
447
|
-
|
|
590
|
+
defineSynchronizedPublicAlias(this, "maxAttempts", () => this.MaxAttempts, (value) => {
|
|
591
|
+
this.MaxAttempts = value;
|
|
592
|
+
});
|
|
593
|
+
defineSynchronizedPublicAlias(this, "delaySeconds", () => this.DelaySeconds, (value) => {
|
|
594
|
+
this.DelaySeconds = value;
|
|
595
|
+
});
|
|
596
|
+
const raw = captureNativeWebSocketReconnectInput(asRecordOrEmpty(initial));
|
|
597
|
+
webSocketReconnectValidationInputs.set(this, snapshotNativeWebSocketInput(raw));
|
|
598
|
+
this.MaxAttempts = (pickEndpointValue(raw, "MaxAttempts", "maxAttempts") ?? 0);
|
|
448
599
|
this.maxAttempts = this.MaxAttempts;
|
|
449
|
-
const delayMs =
|
|
600
|
+
const delayMs = pickEndpointValue(raw, "DelayMs", "delayMs");
|
|
601
|
+
const delaySeconds = pickEndpointValue(raw, "DelaySeconds", "delaySeconds", "Delay", "delay");
|
|
450
602
|
this.DelaySeconds = delayMs != null
|
|
451
603
|
? delayMs / 1000
|
|
452
|
-
: (
|
|
604
|
+
: (delaySeconds ?? 1);
|
|
453
605
|
this.delaySeconds = this.DelaySeconds;
|
|
454
606
|
}
|
|
455
607
|
Validate() {
|
|
456
608
|
this.validate();
|
|
457
609
|
}
|
|
458
610
|
validate() {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
if (this.DelaySeconds < 0) {
|
|
463
|
-
throw new RangeError("Delay cannot be negative.");
|
|
464
|
-
}
|
|
611
|
+
validateNativeWebSocketReconnectPolicy(webSocketReconnectValidationInputs.get(this) ?? {});
|
|
612
|
+
validateNativeWebSocketReconnectPolicy(this);
|
|
465
613
|
}
|
|
466
614
|
}
|
|
467
615
|
export class WebSocketMessageSpec {
|
|
468
616
|
constructor(initial = {}) {
|
|
469
617
|
this.Kind = "Text";
|
|
470
618
|
this.kind = "Text";
|
|
471
|
-
|
|
619
|
+
defineSynchronizedPublicAlias(this, "kind", () => this.Kind, (value) => {
|
|
620
|
+
this.Kind = value;
|
|
621
|
+
});
|
|
622
|
+
defineSynchronizedPublicAlias(this, "textPayload", () => this.TextPayload, (value) => {
|
|
623
|
+
this.TextPayload = value;
|
|
624
|
+
});
|
|
625
|
+
defineSynchronizedPublicAlias(this, "binaryPayload", () => this.BinaryPayload, (value) => {
|
|
626
|
+
this.BinaryPayload = value;
|
|
627
|
+
});
|
|
628
|
+
const raw = captureNativeWebSocketMessageInput(asRecordOrEmpty(initial));
|
|
629
|
+
webSocketMessageValidationInputs.set(this, snapshotNativeWebSocketInput(raw));
|
|
472
630
|
this.Kind = pickOptionalEndpointString(raw, "Kind", "kind") ?? "Text";
|
|
473
631
|
this.kind = this.Kind;
|
|
474
|
-
this.TextPayload =
|
|
632
|
+
this.TextPayload = pickOptionalEndpointExactString(raw, "TextPayload", "textPayload");
|
|
475
633
|
this.textPayload = this.TextPayload;
|
|
476
634
|
const binary = pickEndpointValue(raw, "BinaryPayload", "binaryPayload");
|
|
477
635
|
if (binary instanceof Uint8Array) {
|
|
478
636
|
this.BinaryPayload = binary;
|
|
479
637
|
}
|
|
480
638
|
else if (Array.isArray(binary)) {
|
|
481
|
-
this.BinaryPayload = Uint8Array.from(binary.map((value) => Number(value)
|
|
639
|
+
this.BinaryPayload = Uint8Array.from(binary.map((value) => Number(value)));
|
|
482
640
|
}
|
|
483
641
|
this.binaryPayload = this.BinaryPayload;
|
|
484
642
|
}
|
|
@@ -492,42 +650,48 @@ export class WebSocketMessageSpec {
|
|
|
492
650
|
this.validate();
|
|
493
651
|
}
|
|
494
652
|
validate() {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
if (this.Kind.toLowerCase() === "binary" && this.BinaryPayload == null) {
|
|
499
|
-
throw new Error("Binary WebSocket messages require BinaryPayload.");
|
|
500
|
-
}
|
|
653
|
+
validateOriginalNativeWebSocketMessage(webSocketMessageValidationInputs.get(this) ?? {});
|
|
654
|
+
validateNativeWebSocketMessage(this);
|
|
501
655
|
}
|
|
502
656
|
}
|
|
503
657
|
export class WebSocketExpectedMessage {
|
|
504
658
|
constructor(initial = {}) {
|
|
505
659
|
this.TimeoutSeconds = 30;
|
|
506
660
|
this.timeoutSeconds = 30;
|
|
507
|
-
|
|
508
|
-
|
|
661
|
+
defineSynchronizedPublicAlias(this, "containsText", () => this.ContainsText, (value) => {
|
|
662
|
+
this.ContainsText = value;
|
|
663
|
+
});
|
|
664
|
+
defineSynchronizedPublicAlias(this, "containsBytes", () => this.ContainsBytes, (value) => {
|
|
665
|
+
this.ContainsBytes = value;
|
|
666
|
+
});
|
|
667
|
+
defineSynchronizedPublicAlias(this, "timeoutSeconds", () => this.TimeoutSeconds, (value) => {
|
|
668
|
+
this.TimeoutSeconds = value;
|
|
669
|
+
});
|
|
670
|
+
const raw = captureNativeWebSocketExpectedInput(asRecordOrEmpty(initial));
|
|
671
|
+
webSocketExpectedValidationInputs.set(this, snapshotNativeWebSocketInput(raw));
|
|
672
|
+
this.ContainsText = pickOptionalEndpointExactString(raw, "ContainsText", "containsText");
|
|
509
673
|
this.containsText = this.ContainsText;
|
|
510
674
|
const bytes = pickEndpointValue(raw, "ContainsBytes", "containsBytes");
|
|
511
675
|
if (bytes instanceof Uint8Array) {
|
|
512
676
|
this.ContainsBytes = bytes;
|
|
513
677
|
}
|
|
514
678
|
else if (Array.isArray(bytes)) {
|
|
515
|
-
this.ContainsBytes = Uint8Array.from(bytes.map((value) => Number(value)
|
|
679
|
+
this.ContainsBytes = Uint8Array.from(bytes.map((value) => Number(value)));
|
|
516
680
|
}
|
|
517
681
|
this.containsBytes = this.ContainsBytes;
|
|
518
|
-
const timeoutMs =
|
|
682
|
+
const timeoutMs = pickEndpointValue(raw, "TimeoutMs", "timeoutMs");
|
|
683
|
+
const timeoutSeconds = pickEndpointValue(raw, "TimeoutSeconds", "timeoutSeconds", "Timeout", "timeout");
|
|
519
684
|
this.TimeoutSeconds = timeoutMs != null
|
|
520
685
|
? timeoutMs / 1000
|
|
521
|
-
: (
|
|
686
|
+
: (timeoutSeconds ?? 30);
|
|
522
687
|
this.timeoutSeconds = this.TimeoutSeconds;
|
|
523
688
|
}
|
|
524
689
|
Validate() {
|
|
525
690
|
this.validate();
|
|
526
691
|
}
|
|
527
692
|
validate() {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
693
|
+
validateNativeWebSocketExpectedMessage(webSocketExpectedValidationInputs.get(this) ?? {});
|
|
694
|
+
validateNativeWebSocketExpectedMessage(this);
|
|
531
695
|
}
|
|
532
696
|
}
|
|
533
697
|
export class WebSocketNativeClientOptions {
|
|
@@ -542,13 +706,32 @@ export class WebSocketNativeClientOptions {
|
|
|
542
706
|
this.messages = [];
|
|
543
707
|
this.ExpectedMessages = [];
|
|
544
708
|
this.expectedMessages = [];
|
|
545
|
-
|
|
709
|
+
defineSynchronizedPublicAlias(this, "headers", () => this.Headers, (value) => {
|
|
710
|
+
this.Headers = value;
|
|
711
|
+
});
|
|
712
|
+
defineSynchronizedPublicAlias(this, "cookies", () => this.Cookies, (value) => {
|
|
713
|
+
this.Cookies = value;
|
|
714
|
+
});
|
|
715
|
+
defineSynchronizedPublicAlias(this, "pingIntervalSeconds", () => this.PingIntervalSeconds, (value) => {
|
|
716
|
+
this.PingIntervalSeconds = value;
|
|
717
|
+
});
|
|
718
|
+
defineSynchronizedPublicAlias(this, "reconnect", () => this.Reconnect, (value) => {
|
|
719
|
+
this.Reconnect = value;
|
|
720
|
+
});
|
|
721
|
+
defineSynchronizedPublicAlias(this, "messages", () => this.Messages, (value) => {
|
|
722
|
+
this.Messages = value;
|
|
723
|
+
});
|
|
724
|
+
defineSynchronizedPublicAlias(this, "expectedMessages", () => this.ExpectedMessages, (value) => {
|
|
725
|
+
this.ExpectedMessages = value;
|
|
726
|
+
});
|
|
727
|
+
const raw = captureNativeWebSocketOptionsInput(asRecordOrEmpty(initial));
|
|
728
|
+
webSocketNativeValidationInputs.set(this, snapshotNativeWebSocketInput(raw));
|
|
546
729
|
this.Headers = pickEndpointStringRecord(raw, "Headers", "headers");
|
|
547
730
|
this.headers = this.Headers;
|
|
548
731
|
this.Cookies = pickEndpointStringArray(raw, "Cookies", "cookies") ?? [];
|
|
549
732
|
this.cookies = this.Cookies;
|
|
550
|
-
const pingMs =
|
|
551
|
-
const pingSeconds =
|
|
733
|
+
const pingMs = pickEndpointValue(raw, "PingIntervalMs", "pingIntervalMs");
|
|
734
|
+
const pingSeconds = pickEndpointValue(raw, "PingIntervalSeconds", "pingIntervalSeconds", "PingInterval", "pingInterval");
|
|
552
735
|
this.PingIntervalSeconds = pingMs != null ? pingMs / 1000 : pingSeconds;
|
|
553
736
|
this.pingIntervalSeconds = this.PingIntervalSeconds;
|
|
554
737
|
const reconnect = pickEndpointValue(raw, "Reconnect", "reconnect");
|
|
@@ -565,16 +748,8 @@ export class WebSocketNativeClientOptions {
|
|
|
565
748
|
this.validate();
|
|
566
749
|
}
|
|
567
750
|
validate() {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
this.Reconnect.validate();
|
|
572
|
-
for (const message of this.Messages) {
|
|
573
|
-
message.validate();
|
|
574
|
-
}
|
|
575
|
-
for (const expected of this.ExpectedMessages) {
|
|
576
|
-
expected.validate();
|
|
577
|
-
}
|
|
751
|
+
validateNativeWebSocketOptions(webSocketNativeValidationInputs.get(this) ?? {});
|
|
752
|
+
validateNativeWebSocketOptions(this);
|
|
578
753
|
}
|
|
579
754
|
}
|
|
580
755
|
class WebSocketEndpointDefinitionModel extends TrafficEndpointDefinitionModel {
|
|
@@ -590,6 +765,23 @@ class WebSocketEndpointDefinitionModel extends TrafficEndpointDefinitionModel {
|
|
|
590
765
|
}
|
|
591
766
|
Validate() {
|
|
592
767
|
super.Validate();
|
|
768
|
+
const hasModeAsyncDelegate = this.Mode === "Produce"
|
|
769
|
+
? typeof this.ProduceAsync === "function"
|
|
770
|
+
: typeof this.ConsumeAsync === "function";
|
|
771
|
+
const hasModeDelegate = this.Mode === "Produce"
|
|
772
|
+
? typeof this.Produce === "function" || hasModeAsyncDelegate
|
|
773
|
+
: typeof this.Consume === "function" || hasModeAsyncDelegate;
|
|
774
|
+
if (this.NativeClient != null && !hasModeDelegate) {
|
|
775
|
+
if (this.Mode === "Consume" && this.TrackingField.trim().toLowerCase().startsWith("header:")) {
|
|
776
|
+
throw new Error("Native WebSocket Consume cannot use a header TrackingField because WebSocket messages do not contain HTTP response headers.");
|
|
777
|
+
}
|
|
778
|
+
if (this.Mode === "Consume" && this.GatherByField?.trim().toLowerCase().startsWith("header:")) {
|
|
779
|
+
throw new Error("Native WebSocket Consume cannot use a header GatherByField because WebSocket messages do not contain HTTP response headers.");
|
|
780
|
+
}
|
|
781
|
+
validateNativeWebSocketSubprotocols(this.Subprotocols);
|
|
782
|
+
validateNativeWebSocketHeaderLayer(this.ConnectionMetadata, "ConnectionMetadata");
|
|
783
|
+
validateNativeWebSocketOptionsWithInstance(this.NativeClient);
|
|
784
|
+
}
|
|
593
785
|
const url = requireNonEmptyString(this.Url, "Url must be provided for WebSocket endpoint definitions.");
|
|
594
786
|
let parsed;
|
|
595
787
|
try {
|
|
@@ -598,20 +790,17 @@ class WebSocketEndpointDefinitionModel extends TrafficEndpointDefinitionModel {
|
|
|
598
790
|
catch {
|
|
599
791
|
throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
|
|
600
792
|
}
|
|
601
|
-
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:")
|
|
793
|
+
if ((parsed.protocol !== "ws:" && parsed.protocol !== "wss:")
|
|
794
|
+
|| !parsed.hostname
|
|
795
|
+
|| Boolean(parsed.hash)) {
|
|
602
796
|
throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
|
|
603
797
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
if (this.CloseTimeoutSeconds <= 0) {
|
|
608
|
-
throw new RangeError("CloseTimeout must be greater than zero.");
|
|
609
|
-
}
|
|
610
|
-
this.NativeClient?.validate();
|
|
611
|
-
if (this.Mode === "Produce" && typeof this.ProduceAsync !== "function" && !this.NativeClient) {
|
|
798
|
+
validateNativeWebSocketTimerDelay(undefined, this.ConnectTimeoutSeconds, "ConnectTimeout");
|
|
799
|
+
validateNativeWebSocketTimerDelay(undefined, this.CloseTimeoutSeconds, "CloseTimeout");
|
|
800
|
+
if (this.Mode === "Produce" && !hasModeDelegate && this.NativeClient == null) {
|
|
612
801
|
throw new Error("ProduceAsync delegate must be provided when endpoint mode is Produce.");
|
|
613
802
|
}
|
|
614
|
-
if (this.Mode === "Consume" &&
|
|
803
|
+
if (this.Mode === "Consume" && !hasModeDelegate && this.NativeClient == null) {
|
|
615
804
|
throw new Error("ConsumeAsync delegate must be provided when endpoint mode is Consume.");
|
|
616
805
|
}
|
|
617
806
|
}
|
|
@@ -1138,25 +1327,31 @@ function initializeWebSocketEndpointDefinitionModel(target, initial) {
|
|
|
1138
1327
|
if (url) {
|
|
1139
1328
|
target.Url = url;
|
|
1140
1329
|
}
|
|
1141
|
-
const subprotocols =
|
|
1142
|
-
if (subprotocols) {
|
|
1143
|
-
target.Subprotocols = subprotocols;
|
|
1330
|
+
const subprotocols = pickEndpointValue(raw, "Subprotocols", "subprotocols");
|
|
1331
|
+
if (subprotocols != null) {
|
|
1332
|
+
target.Subprotocols = normalizeWebSocketEndpointStringArrayInput(subprotocols);
|
|
1144
1333
|
}
|
|
1145
|
-
const connectMs =
|
|
1146
|
-
const connectSeconds =
|
|
1334
|
+
const connectMs = pickEndpointValue(raw, "ConnectTimeoutMs", "connectTimeoutMs");
|
|
1335
|
+
const connectSeconds = pickEndpointValue(raw, "ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeout", "connectTimeout");
|
|
1147
1336
|
if (connectMs != null) {
|
|
1148
|
-
|
|
1337
|
+
const normalized = normalizeWebSocketEndpointNumberInput(connectMs);
|
|
1338
|
+
target.ConnectTimeoutSeconds = typeof normalized === "number"
|
|
1339
|
+
? normalized / 1000
|
|
1340
|
+
: normalized;
|
|
1149
1341
|
}
|
|
1150
1342
|
else if (connectSeconds != null) {
|
|
1151
|
-
target.ConnectTimeoutSeconds = connectSeconds;
|
|
1343
|
+
target.ConnectTimeoutSeconds = normalizeWebSocketEndpointNumberInput(connectSeconds);
|
|
1152
1344
|
}
|
|
1153
|
-
const closeMs =
|
|
1154
|
-
const closeSeconds =
|
|
1345
|
+
const closeMs = pickEndpointValue(raw, "CloseTimeoutMs", "closeTimeoutMs");
|
|
1346
|
+
const closeSeconds = pickEndpointValue(raw, "CloseTimeoutSeconds", "closeTimeoutSeconds", "CloseTimeout", "closeTimeout");
|
|
1155
1347
|
if (closeMs != null) {
|
|
1156
|
-
|
|
1348
|
+
const normalized = normalizeWebSocketEndpointNumberInput(closeMs);
|
|
1349
|
+
target.CloseTimeoutSeconds = typeof normalized === "number"
|
|
1350
|
+
? normalized / 1000
|
|
1351
|
+
: normalized;
|
|
1157
1352
|
}
|
|
1158
1353
|
else if (closeSeconds != null) {
|
|
1159
|
-
target.CloseTimeoutSeconds = closeSeconds;
|
|
1354
|
+
target.CloseTimeoutSeconds = normalizeWebSocketEndpointNumberInput(closeSeconds);
|
|
1160
1355
|
}
|
|
1161
1356
|
const produce = pickEndpointFunction(raw, "Produce", "produce");
|
|
1162
1357
|
if (produce) {
|
|
@@ -1175,15 +1370,45 @@ function initializeWebSocketEndpointDefinitionModel(target, initial) {
|
|
|
1175
1370
|
target.ConsumeAsync = consumeAsync;
|
|
1176
1371
|
}
|
|
1177
1372
|
if (hasAnyEndpointField(raw, ["ConnectionMetadata", "connectionMetadata"])) {
|
|
1178
|
-
target.ConnectionMetadata =
|
|
1373
|
+
target.ConnectionMetadata = normalizeWebSocketEndpointMetadataInput(pickEndpointValue(raw, "ConnectionMetadata", "connectionMetadata"));
|
|
1179
1374
|
}
|
|
1180
1375
|
if (hasAnyEndpointField(raw, ["NativeClient", "nativeClient"])) {
|
|
1181
1376
|
const nativeClient = pickEndpointValue(raw, "NativeClient", "nativeClient");
|
|
1182
1377
|
target.NativeClient = nativeClient instanceof WebSocketNativeClientOptions
|
|
1183
1378
|
? nativeClient
|
|
1184
|
-
:
|
|
1379
|
+
: isRecord(nativeClient)
|
|
1380
|
+
? new WebSocketNativeClientOptions(nativeClient)
|
|
1381
|
+
: nativeClient;
|
|
1185
1382
|
}
|
|
1186
1383
|
}
|
|
1384
|
+
function normalizeWebSocketEndpointNumberInput(value) {
|
|
1385
|
+
if (typeof value === "number") {
|
|
1386
|
+
return value;
|
|
1387
|
+
}
|
|
1388
|
+
if (typeof value === "string" && value.trim()) {
|
|
1389
|
+
const parsed = Number(value);
|
|
1390
|
+
if (Number.isFinite(parsed)) {
|
|
1391
|
+
return parsed;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return value;
|
|
1395
|
+
}
|
|
1396
|
+
function normalizeWebSocketEndpointStringArrayInput(value) {
|
|
1397
|
+
if (!Array.isArray(value)) {
|
|
1398
|
+
return value;
|
|
1399
|
+
}
|
|
1400
|
+
return value.map((entry) => typeof entry === "string" ? entry.trim() : entry);
|
|
1401
|
+
}
|
|
1402
|
+
function normalizeWebSocketEndpointMetadataInput(value) {
|
|
1403
|
+
if (!isRecord(value)) {
|
|
1404
|
+
return value;
|
|
1405
|
+
}
|
|
1406
|
+
const normalized = {};
|
|
1407
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1408
|
+
normalized[key] = entry;
|
|
1409
|
+
}
|
|
1410
|
+
return normalized;
|
|
1411
|
+
}
|
|
1187
1412
|
function validateTrafficEndpointDefinitionModel(target) {
|
|
1188
1413
|
requireNonEmptyString(target.Name, "Endpoint name must be provided.");
|
|
1189
1414
|
if (target.Mode !== "Produce" && target.Mode !== "Consume") {
|
|
@@ -1517,13 +1742,12 @@ class CallbackAdapter {
|
|
|
1517
1742
|
return null;
|
|
1518
1743
|
}
|
|
1519
1744
|
const resolved = prepareProducedPayload(this.endpoint, payload);
|
|
1520
|
-
const
|
|
1521
|
-
if (
|
|
1522
|
-
return await invokeStructuredProduceDelegate(this.endpoint, resolved,
|
|
1745
|
+
const callbacks = selectProduceCallbackGroup(this.endpoint);
|
|
1746
|
+
if (callbacks.asyncDelegate) {
|
|
1747
|
+
return await invokeStructuredProduceDelegate(this.endpoint, resolved, callbacks.asyncDelegate);
|
|
1523
1748
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
const next = await delegate(clonePayload(resolved));
|
|
1749
|
+
if (callbacks.syncDelegate) {
|
|
1750
|
+
const next = await callbacks.syncDelegate(clonePayload(resolved));
|
|
1527
1751
|
return next == null ? null : normalizeTrackingPayload(next);
|
|
1528
1752
|
}
|
|
1529
1753
|
return clonePayload(resolved);
|
|
@@ -1535,19 +1759,17 @@ class CallbackAdapter {
|
|
|
1535
1759
|
if (this.consumeQueue.length > 0) {
|
|
1536
1760
|
return this.consumeQueue.shift() ?? null;
|
|
1537
1761
|
}
|
|
1538
|
-
const
|
|
1539
|
-
if (
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
this.ensureConsumeStreamStarted(streamDelegate);
|
|
1762
|
+
const callbacks = selectConsumeCallbackGroup(this.endpoint);
|
|
1763
|
+
if (callbacks.asyncDelegate) {
|
|
1764
|
+
if (callbacks.asyncDelegate.length === 0) {
|
|
1765
|
+
const next = await callbacks.asyncDelegate();
|
|
1766
|
+
return normalizeConsumedDelegatePayload(next);
|
|
1767
|
+
}
|
|
1768
|
+
this.ensureConsumeStreamStarted(callbacks.asyncDelegate);
|
|
1546
1769
|
return this.consumeQueue.shift() ?? null;
|
|
1547
1770
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
const next = await delegate();
|
|
1771
|
+
if (callbacks.syncDelegate) {
|
|
1772
|
+
const next = await callbacks.syncDelegate();
|
|
1551
1773
|
return next == null ? null : normalizeTrackingPayload(next);
|
|
1552
1774
|
}
|
|
1553
1775
|
return this.defaultPayload();
|
|
@@ -2313,7 +2535,610 @@ class DelegateStreamEndpointAdapter extends CallbackAdapter {
|
|
|
2313
2535
|
}
|
|
2314
2536
|
class GrpcEndpointAdapter extends CallbackAdapter {
|
|
2315
2537
|
}
|
|
2538
|
+
const NATIVE_WEBSOCKET_FRAME_QUEUE_CAPACITY = 64;
|
|
2539
|
+
const NATIVE_WEBSOCKET_FRAME_QUEUE_MAX_BYTES = 1024 * 1024;
|
|
2540
|
+
const NATIVE_WEBSOCKET_MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
2541
|
+
class NativeWebSocketAbortError extends Error {
|
|
2542
|
+
constructor() {
|
|
2543
|
+
super("WebSocket native client was interrupted.");
|
|
2544
|
+
this.name = "AbortError";
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
class NativeWebSocketPeerClosedError extends Error {
|
|
2548
|
+
constructor(code, reason) {
|
|
2549
|
+
super(code == null
|
|
2550
|
+
? "WebSocket peer closed before the operation completed."
|
|
2551
|
+
: `WebSocket peer closed before the operation completed (code ${code}${reason ? `: ${reason}` : ""}).`);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
class NativeWebSocketReceiveTimeoutError extends Error {
|
|
2555
|
+
constructor() {
|
|
2556
|
+
super("WebSocket receive timed out before the expected message arrived.");
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
class NativeWebSocketSendTimeoutError extends Error {
|
|
2560
|
+
constructor() {
|
|
2561
|
+
super("WebSocket send timed out before the frame was written.");
|
|
2562
|
+
this.code = "ETIMEDOUT";
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
class NativeWebSocketQueueOverflowError extends Error {
|
|
2566
|
+
constructor() {
|
|
2567
|
+
super("WebSocket receive queue exceeded its bounded frame limit.");
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
class NativeWebSocketFrameQueue {
|
|
2571
|
+
constructor(socket) {
|
|
2572
|
+
this.socket = socket;
|
|
2573
|
+
this.frames = [];
|
|
2574
|
+
this.queuedBytes = 0;
|
|
2575
|
+
this.terminalError = null;
|
|
2576
|
+
this.wakeResolver = null;
|
|
2577
|
+
this.onMessage = (data, isBinary) => {
|
|
2578
|
+
const copy = copyNativeWebSocketRawData(data);
|
|
2579
|
+
if (this.frames.length >= NATIVE_WEBSOCKET_FRAME_QUEUE_CAPACITY
|
|
2580
|
+
|| this.queuedBytes + copy.byteLength > NATIVE_WEBSOCKET_FRAME_QUEUE_MAX_BYTES) {
|
|
2581
|
+
this.fail(new NativeWebSocketQueueOverflowError());
|
|
2582
|
+
this.socket.terminate();
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
this.frames.push({ data: copy, isBinary });
|
|
2586
|
+
this.queuedBytes += copy.byteLength;
|
|
2587
|
+
this.wake();
|
|
2588
|
+
};
|
|
2589
|
+
this.onError = (error) => {
|
|
2590
|
+
this.fail(error);
|
|
2591
|
+
};
|
|
2592
|
+
this.onClose = (code, reason) => {
|
|
2593
|
+
this.fail(new NativeWebSocketPeerClosedError(code, reason.toString("utf8")));
|
|
2594
|
+
};
|
|
2595
|
+
socket.on("message", this.onMessage);
|
|
2596
|
+
socket.on("error", this.onError);
|
|
2597
|
+
socket.on("close", this.onClose);
|
|
2598
|
+
}
|
|
2599
|
+
async next(deadlineMs, signal) {
|
|
2600
|
+
while (true) {
|
|
2601
|
+
if (signal.aborted) {
|
|
2602
|
+
throw new NativeWebSocketAbortError();
|
|
2603
|
+
}
|
|
2604
|
+
if (isNativeWebSocketOverflowError(this.terminalError)) {
|
|
2605
|
+
throw this.terminalError;
|
|
2606
|
+
}
|
|
2607
|
+
const frame = this.frames.shift();
|
|
2608
|
+
if (frame) {
|
|
2609
|
+
this.queuedBytes -= frame.data.byteLength;
|
|
2610
|
+
return frame;
|
|
2611
|
+
}
|
|
2612
|
+
if (this.terminalError) {
|
|
2613
|
+
throw this.terminalError;
|
|
2614
|
+
}
|
|
2615
|
+
const remainingMs = deadlineMs - Date.now();
|
|
2616
|
+
if (remainingMs <= 0) {
|
|
2617
|
+
throw new NativeWebSocketReceiveTimeoutError();
|
|
2618
|
+
}
|
|
2619
|
+
await this.waitForFrame(remainingMs, signal);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
dispose() {
|
|
2623
|
+
this.socket.off("message", this.onMessage);
|
|
2624
|
+
this.socket.off("error", this.onError);
|
|
2625
|
+
this.socket.off("close", this.onClose);
|
|
2626
|
+
this.wake();
|
|
2627
|
+
}
|
|
2628
|
+
waitForFrame(timeoutMs, signal) {
|
|
2629
|
+
return new Promise((resolve, reject) => {
|
|
2630
|
+
let settled = false;
|
|
2631
|
+
const finish = (error) => {
|
|
2632
|
+
if (settled) {
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
settled = true;
|
|
2636
|
+
clearTimeout(timeout);
|
|
2637
|
+
signal.removeEventListener("abort", onAbort);
|
|
2638
|
+
if (this.wakeResolver === onWake) {
|
|
2639
|
+
this.wakeResolver = null;
|
|
2640
|
+
}
|
|
2641
|
+
if (error) {
|
|
2642
|
+
reject(error);
|
|
2643
|
+
}
|
|
2644
|
+
else {
|
|
2645
|
+
resolve();
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
const onWake = () => finish();
|
|
2649
|
+
const onAbort = () => finish(new NativeWebSocketAbortError());
|
|
2650
|
+
const timeout = setTimeout(() => finish(new NativeWebSocketReceiveTimeoutError()), timeoutMs);
|
|
2651
|
+
this.wakeResolver = onWake;
|
|
2652
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2653
|
+
if (signal.aborted) {
|
|
2654
|
+
onAbort();
|
|
2655
|
+
}
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
fail(error) {
|
|
2659
|
+
this.terminalError ?? (this.terminalError = error);
|
|
2660
|
+
this.wake();
|
|
2661
|
+
}
|
|
2662
|
+
wake() {
|
|
2663
|
+
const resolve = this.wakeResolver;
|
|
2664
|
+
this.wakeResolver = null;
|
|
2665
|
+
resolve?.();
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
function isNativeWebSocketOverflowError(error) {
|
|
2669
|
+
return error instanceof NativeWebSocketQueueOverflowError
|
|
2670
|
+
|| String(error?.code ?? "") === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH";
|
|
2671
|
+
}
|
|
2672
|
+
function copyNativeWebSocketRawData(data) {
|
|
2673
|
+
if (Array.isArray(data)) {
|
|
2674
|
+
return new Uint8Array(Buffer.concat(data));
|
|
2675
|
+
}
|
|
2676
|
+
if (data instanceof ArrayBuffer) {
|
|
2677
|
+
return new Uint8Array(data.slice(0));
|
|
2678
|
+
}
|
|
2679
|
+
return new Uint8Array(Buffer.from(data));
|
|
2680
|
+
}
|
|
2316
2681
|
class WebSocketEndpointAdapter extends CallbackAdapter {
|
|
2682
|
+
constructor() {
|
|
2683
|
+
super(...arguments);
|
|
2684
|
+
this.nativeAbort = new AbortController();
|
|
2685
|
+
this.activeSockets = new Set();
|
|
2686
|
+
}
|
|
2687
|
+
async produce(payload) {
|
|
2688
|
+
if (this.endpoint.mode !== "Produce") {
|
|
2689
|
+
return null;
|
|
2690
|
+
}
|
|
2691
|
+
const delegates = selectProduceCallbackGroup(this.endpoint);
|
|
2692
|
+
if (delegates.asyncDelegate || delegates.syncDelegate) {
|
|
2693
|
+
return super.produce(payload);
|
|
2694
|
+
}
|
|
2695
|
+
const resolved = prepareProducedPayload(this.endpoint, payload);
|
|
2696
|
+
const configuration = normalizeNativeWebSocketConfiguration(this.endpoint, resolved);
|
|
2697
|
+
await this.executeNative(configuration);
|
|
2698
|
+
resolved.producedUtc ?? (resolved.producedUtc = new Date().toISOString());
|
|
2699
|
+
return resolved;
|
|
2700
|
+
}
|
|
2701
|
+
async consume() {
|
|
2702
|
+
if (this.endpoint.mode !== "Consume") {
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
const delegates = selectConsumeCallbackGroup(this.endpoint);
|
|
2706
|
+
if (delegates.asyncDelegate || delegates.syncDelegate) {
|
|
2707
|
+
return super.consume();
|
|
2708
|
+
}
|
|
2709
|
+
const configuration = normalizeNativeWebSocketConfiguration(this.endpoint);
|
|
2710
|
+
return this.executeNative(configuration);
|
|
2711
|
+
}
|
|
2712
|
+
interrupt() {
|
|
2713
|
+
if (!this.nativeAbort.signal.aborted) {
|
|
2714
|
+
this.nativeAbort.abort();
|
|
2715
|
+
}
|
|
2716
|
+
for (const socket of this.activeSockets) {
|
|
2717
|
+
socket.terminate();
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
async dispose() {
|
|
2721
|
+
this.interrupt();
|
|
2722
|
+
}
|
|
2723
|
+
async executeNative(configuration) {
|
|
2724
|
+
const totalAttempts = configuration.maxAttempts + 1;
|
|
2725
|
+
let lastError;
|
|
2726
|
+
for (let attempt = 1; attempt <= totalAttempts; attempt += 1) {
|
|
2727
|
+
if (this.nativeAbort.signal.aborted) {
|
|
2728
|
+
throw new NativeWebSocketAbortError();
|
|
2729
|
+
}
|
|
2730
|
+
try {
|
|
2731
|
+
return await this.executeNativeOnce(configuration);
|
|
2732
|
+
}
|
|
2733
|
+
catch (error) {
|
|
2734
|
+
lastError = error;
|
|
2735
|
+
if (!isReconnectableNativeWebSocketError(error) || attempt >= totalAttempts) {
|
|
2736
|
+
throw error;
|
|
2737
|
+
}
|
|
2738
|
+
await waitForNativeWebSocketRetry(configuration.reconnectDelayMs, this.nativeAbort.signal);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
throw lastError;
|
|
2742
|
+
}
|
|
2743
|
+
async executeNativeOnce(configuration) {
|
|
2744
|
+
const options = {
|
|
2745
|
+
headers: configuration.headers,
|
|
2746
|
+
handshakeTimeout: configuration.connectTimeoutMs,
|
|
2747
|
+
followRedirects: false,
|
|
2748
|
+
maxPayload: NATIVE_WEBSOCKET_MAX_PAYLOAD_BYTES
|
|
2749
|
+
};
|
|
2750
|
+
const socket = configuration.subprotocols.length > 0
|
|
2751
|
+
? new WebSocket(configuration.url, configuration.subprotocols, options)
|
|
2752
|
+
: new WebSocket(configuration.url, options);
|
|
2753
|
+
this.activeSockets.add(socket);
|
|
2754
|
+
const frames = new NativeWebSocketFrameQueue(socket);
|
|
2755
|
+
let pingTimer;
|
|
2756
|
+
try {
|
|
2757
|
+
await waitForNativeWebSocketOpen(socket, configuration.connectTimeoutMs, this.nativeAbort.signal);
|
|
2758
|
+
if (configuration.pingIntervalMs != null) {
|
|
2759
|
+
pingTimer = setInterval(() => {
|
|
2760
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
2761
|
+
socket.ping();
|
|
2762
|
+
}
|
|
2763
|
+
}, configuration.pingIntervalMs);
|
|
2764
|
+
pingTimer.unref?.();
|
|
2765
|
+
}
|
|
2766
|
+
for (const message of configuration.messages) {
|
|
2767
|
+
await sendNativeWebSocketFrame(socket, message, configuration.connectTimeoutMs, this.nativeAbort.signal);
|
|
2768
|
+
}
|
|
2769
|
+
if (this.endpoint.mode === "Produce") {
|
|
2770
|
+
for (const expected of configuration.expectedMessages) {
|
|
2771
|
+
await receiveExpectedNativeWebSocketFrame(frames, expected, this.nativeAbort.signal);
|
|
2772
|
+
}
|
|
2773
|
+
return null;
|
|
2774
|
+
}
|
|
2775
|
+
if (configuration.expectedMessages.length > 0) {
|
|
2776
|
+
let payload = null;
|
|
2777
|
+
for (const expected of configuration.expectedMessages) {
|
|
2778
|
+
const frame = await receiveExpectedNativeWebSocketFrame(frames, expected, this.nativeAbort.signal);
|
|
2779
|
+
payload = nativeWebSocketFrameToTrackingPayload(frame);
|
|
2780
|
+
}
|
|
2781
|
+
return payload;
|
|
2782
|
+
}
|
|
2783
|
+
const frame = await frames.next(Date.now() + configuration.connectTimeoutMs, this.nativeAbort.signal);
|
|
2784
|
+
return nativeWebSocketFrameToTrackingPayload(frame);
|
|
2785
|
+
}
|
|
2786
|
+
finally {
|
|
2787
|
+
if (pingTimer) {
|
|
2788
|
+
clearInterval(pingTimer);
|
|
2789
|
+
}
|
|
2790
|
+
frames.dispose();
|
|
2791
|
+
await closeNativeWebSocket(socket, configuration.closeTimeoutMs);
|
|
2792
|
+
this.activeSockets.delete(socket);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
function normalizeNativeWebSocketConfiguration(endpoint, payload) {
|
|
2797
|
+
const options = asRecordOrEmpty(endpoint.webSocket);
|
|
2798
|
+
const native = asRecordOrEmpty(pickEndpointValue(options, "NativeClient", "nativeClient"));
|
|
2799
|
+
const reconnect = asRecordOrEmpty(pickEndpointValue(native, "Reconnect", "reconnect"));
|
|
2800
|
+
if (payload) {
|
|
2801
|
+
validateNativeWebSocketHeaderLayer(payload.headers ?? {}, "payload headers");
|
|
2802
|
+
}
|
|
2803
|
+
const headers = mergeNativeWebSocketHeaders(toStringRecord(pickEndpointValue(native, "Headers", "headers")), resolveConnectionMetadata(endpoint), payload?.headers ?? {});
|
|
2804
|
+
const configuredCookies = pickEndpointValue(native, "Cookies", "cookies");
|
|
2805
|
+
const cookies = Array.isArray(configuredCookies)
|
|
2806
|
+
? configuredCookies.map((value) => String(value).trim())
|
|
2807
|
+
: [];
|
|
2808
|
+
if (cookies.length > 0) {
|
|
2809
|
+
const cookieName = Object.keys(headers).find((name) => name.toLowerCase() === "cookie");
|
|
2810
|
+
const existing = cookieName ? headers[cookieName] : "";
|
|
2811
|
+
if (cookieName) {
|
|
2812
|
+
delete headers[cookieName];
|
|
2813
|
+
}
|
|
2814
|
+
headers.Cookie = [existing, ...cookies].filter(Boolean).join("; ");
|
|
2815
|
+
}
|
|
2816
|
+
const rawMessages = pickEndpointValue(native, "Messages", "messages");
|
|
2817
|
+
const messages = Array.isArray(rawMessages)
|
|
2818
|
+
? rawMessages.map(normalizeNativeWebSocketMessage)
|
|
2819
|
+
: [];
|
|
2820
|
+
if (messages.length === 0 && payload) {
|
|
2821
|
+
const projected = projectNativeWebSocketPayload(payload.body);
|
|
2822
|
+
if (projected) {
|
|
2823
|
+
messages.push(projected);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
if (endpoint.mode === "Produce" && messages.length === 0) {
|
|
2827
|
+
throw new Error("Native WebSocket Produce requires at least one configured message or a projected payload body.");
|
|
2828
|
+
}
|
|
2829
|
+
const rawExpected = pickEndpointValue(native, "ExpectedMessages", "expectedMessages");
|
|
2830
|
+
const expectedMessages = Array.isArray(rawExpected)
|
|
2831
|
+
? rawExpected.map(normalizeNativeWebSocketExpectedMessage)
|
|
2832
|
+
: [];
|
|
2833
|
+
const connectTimeoutMs = nativeWebSocketTimeoutMs(options, 30000, ["ConnectTimeoutMs", "connectTimeoutMs"], ["ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeout", "connectTimeout"]);
|
|
2834
|
+
const closeTimeoutMs = nativeWebSocketTimeoutMs(options, 5000, ["CloseTimeoutMs", "closeTimeoutMs"], ["CloseTimeoutSeconds", "closeTimeoutSeconds", "CloseTimeout", "closeTimeout"]);
|
|
2835
|
+
const pingMsValue = pickEndpointValue(native, "PingIntervalMs", "pingIntervalMs");
|
|
2836
|
+
const pingSecondsValue = pickEndpointValue(native, "PingIntervalSeconds", "pingIntervalSeconds", "PingInterval", "pingInterval");
|
|
2837
|
+
const pingIntervalMs = pingMsValue != null
|
|
2838
|
+
? Number(pingMsValue)
|
|
2839
|
+
: (pingSecondsValue != null ? Number(pingSecondsValue) * 1000 : undefined);
|
|
2840
|
+
const delayMsValue = pickEndpointValue(reconnect, "DelayMs", "delayMs");
|
|
2841
|
+
const delaySecondsValue = pickEndpointValue(reconnect, "DelaySeconds", "delaySeconds", "Delay", "delay");
|
|
2842
|
+
return {
|
|
2843
|
+
url: optionString(options, "Url", "url"),
|
|
2844
|
+
subprotocols: nativeWebSocketStringArray(pickEndpointValue(options, "Subprotocols", "subprotocols")),
|
|
2845
|
+
connectTimeoutMs,
|
|
2846
|
+
closeTimeoutMs,
|
|
2847
|
+
headers,
|
|
2848
|
+
pingIntervalMs,
|
|
2849
|
+
maxAttempts: Number(pickEndpointValue(reconnect, "MaxAttempts", "maxAttempts") ?? 0),
|
|
2850
|
+
reconnectDelayMs: delayMsValue != null
|
|
2851
|
+
? Number(delayMsValue)
|
|
2852
|
+
: Number(delaySecondsValue ?? 1) * 1000,
|
|
2853
|
+
messages,
|
|
2854
|
+
expectedMessages
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
function nativeWebSocketTimeoutMs(options, fallbackMs, millisecondKeys, secondKeys) {
|
|
2858
|
+
const millisecondValue = pickEndpointValue(options, ...millisecondKeys);
|
|
2859
|
+
if (millisecondValue != null) {
|
|
2860
|
+
return Number(millisecondValue);
|
|
2861
|
+
}
|
|
2862
|
+
const secondValue = pickEndpointValue(options, ...secondKeys);
|
|
2863
|
+
return secondValue == null ? fallbackMs : Number(secondValue) * 1000;
|
|
2864
|
+
}
|
|
2865
|
+
function nativeWebSocketStringArray(value) {
|
|
2866
|
+
return Array.isArray(value) ? value.map((entry) => String(entry).trim()) : [];
|
|
2867
|
+
}
|
|
2868
|
+
function normalizeNativeWebSocketMessage(value) {
|
|
2869
|
+
const options = asRecordOrEmpty(value);
|
|
2870
|
+
const kind = optionString(options, "Kind", "kind").toLowerCase();
|
|
2871
|
+
if (kind === "binary") {
|
|
2872
|
+
const bytes = pickEndpointValue(options, "BinaryPayload", "binaryPayload");
|
|
2873
|
+
return {
|
|
2874
|
+
data: bytes instanceof Uint8Array ? new Uint8Array(bytes) : Uint8Array.from(bytes),
|
|
2875
|
+
isBinary: true
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
return {
|
|
2879
|
+
data: String(pickEndpointValue(options, "TextPayload", "textPayload") ?? ""),
|
|
2880
|
+
isBinary: false
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2883
|
+
function normalizeNativeWebSocketExpectedMessage(value) {
|
|
2884
|
+
const options = asRecordOrEmpty(value);
|
|
2885
|
+
const bytes = pickEndpointValue(options, "ContainsBytes", "containsBytes");
|
|
2886
|
+
return {
|
|
2887
|
+
containsText: pickEndpointValue(options, "ContainsText", "containsText") == null
|
|
2888
|
+
? undefined
|
|
2889
|
+
: String(pickEndpointValue(options, "ContainsText", "containsText")),
|
|
2890
|
+
containsBytes: bytes == null
|
|
2891
|
+
? undefined
|
|
2892
|
+
: (bytes instanceof Uint8Array ? new Uint8Array(bytes) : Uint8Array.from(bytes)),
|
|
2893
|
+
timeoutMs: nativeWebSocketTimeoutMs(options, 30000, ["TimeoutMs", "timeoutMs"], ["TimeoutSeconds", "timeoutSeconds", "Timeout", "timeout"])
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
function projectNativeWebSocketPayload(value) {
|
|
2897
|
+
if (value == null) {
|
|
2898
|
+
return null;
|
|
2899
|
+
}
|
|
2900
|
+
if (value instanceof Uint8Array) {
|
|
2901
|
+
return { data: new Uint8Array(value), isBinary: true };
|
|
2902
|
+
}
|
|
2903
|
+
if (value instanceof ArrayBuffer) {
|
|
2904
|
+
return { data: new Uint8Array(value), isBinary: true };
|
|
2905
|
+
}
|
|
2906
|
+
if (typeof value === "string") {
|
|
2907
|
+
return { data: value, isBinary: false };
|
|
2908
|
+
}
|
|
2909
|
+
const serialized = JSON.stringify(value);
|
|
2910
|
+
if (serialized === undefined) {
|
|
2911
|
+
throw new TypeError("Native WebSocket payload cannot be serialized as JSON.");
|
|
2912
|
+
}
|
|
2913
|
+
return {
|
|
2914
|
+
data: serialized,
|
|
2915
|
+
isBinary: false
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
function mergeNativeWebSocketHeaders(...layers) {
|
|
2919
|
+
const merged = new Map();
|
|
2920
|
+
for (const layer of layers) {
|
|
2921
|
+
for (const [name, value] of Object.entries(layer)) {
|
|
2922
|
+
merged.set(name.toLowerCase(), { name, value: String(value) });
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
return Object.fromEntries([...merged.values()].map(({ name, value }) => [name, value]));
|
|
2926
|
+
}
|
|
2927
|
+
function waitForNativeWebSocketOpen(socket, timeoutMs, signal) {
|
|
2928
|
+
return new Promise((resolve, reject) => {
|
|
2929
|
+
let settled = false;
|
|
2930
|
+
const finish = (error) => {
|
|
2931
|
+
if (settled) {
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
settled = true;
|
|
2935
|
+
clearTimeout(timeout);
|
|
2936
|
+
socket.off("open", onOpen);
|
|
2937
|
+
socket.off("error", onError);
|
|
2938
|
+
socket.off("close", onClose);
|
|
2939
|
+
socket.off("unexpected-response", onUnexpectedResponse);
|
|
2940
|
+
signal.removeEventListener("abort", onAbort);
|
|
2941
|
+
if (error) {
|
|
2942
|
+
reject(error);
|
|
2943
|
+
}
|
|
2944
|
+
else {
|
|
2945
|
+
resolve();
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
const onOpen = () => finish();
|
|
2949
|
+
const onError = (error) => finish(error);
|
|
2950
|
+
const onClose = (code, reason) => {
|
|
2951
|
+
finish(new NativeWebSocketPeerClosedError(code, reason.toString("utf8")));
|
|
2952
|
+
};
|
|
2953
|
+
const onUnexpectedResponse = (_request, response) => {
|
|
2954
|
+
response.resume();
|
|
2955
|
+
finish(new Error(`WebSocket opening handshake was rejected with HTTP status ${response.statusCode ?? "unknown"}.`));
|
|
2956
|
+
socket.terminate();
|
|
2957
|
+
};
|
|
2958
|
+
const onAbort = () => {
|
|
2959
|
+
finish(new NativeWebSocketAbortError());
|
|
2960
|
+
socket.terminate();
|
|
2961
|
+
};
|
|
2962
|
+
const timeout = setTimeout(() => {
|
|
2963
|
+
const error = new Error("WebSocket connection timed out.");
|
|
2964
|
+
error.code = "ETIMEDOUT";
|
|
2965
|
+
finish(error);
|
|
2966
|
+
socket.terminate();
|
|
2967
|
+
}, timeoutMs);
|
|
2968
|
+
socket.once("open", onOpen);
|
|
2969
|
+
socket.once("error", onError);
|
|
2970
|
+
socket.once("close", onClose);
|
|
2971
|
+
socket.once("unexpected-response", onUnexpectedResponse);
|
|
2972
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2973
|
+
if (signal.aborted) {
|
|
2974
|
+
onAbort();
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
function sendNativeWebSocketFrame(socket, message, timeoutMs, signal) {
|
|
2979
|
+
if (signal.aborted) {
|
|
2980
|
+
return Promise.reject(new NativeWebSocketAbortError());
|
|
2981
|
+
}
|
|
2982
|
+
return new Promise((resolve, reject) => {
|
|
2983
|
+
let settled = false;
|
|
2984
|
+
let timeout;
|
|
2985
|
+
const finish = (error) => {
|
|
2986
|
+
if (settled) {
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
settled = true;
|
|
2990
|
+
if (timeout) {
|
|
2991
|
+
clearTimeout(timeout);
|
|
2992
|
+
}
|
|
2993
|
+
signal.removeEventListener("abort", onAbort);
|
|
2994
|
+
error ? reject(error) : resolve();
|
|
2995
|
+
};
|
|
2996
|
+
const onAbort = () => {
|
|
2997
|
+
finish(new NativeWebSocketAbortError());
|
|
2998
|
+
socket.terminate();
|
|
2999
|
+
};
|
|
3000
|
+
const onTimeout = () => {
|
|
3001
|
+
finish(new NativeWebSocketSendTimeoutError());
|
|
3002
|
+
socket.terminate();
|
|
3003
|
+
};
|
|
3004
|
+
timeout = setTimeout(onTimeout, timeoutMs);
|
|
3005
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3006
|
+
try {
|
|
3007
|
+
socket.send(message.data, { binary: message.isBinary }, (error) => {
|
|
3008
|
+
finish(error ? normalizeNativeWebSocketSendError(socket, error) : undefined);
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
catch (error) {
|
|
3012
|
+
finish(normalizeNativeWebSocketSendError(socket, error));
|
|
3013
|
+
}
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
function normalizeNativeWebSocketSendError(socket, error) {
|
|
3017
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
3018
|
+
const code = String(normalized.code ?? "");
|
|
3019
|
+
const isClosingState = socket.readyState === WebSocket.CLOSING
|
|
3020
|
+
|| socket.readyState === WebSocket.CLOSED;
|
|
3021
|
+
const isWsNotOpenStateError = /^WebSocket is not open: readyState [23] \((?:CLOSING|CLOSED)\)$/.test(normalized.message);
|
|
3022
|
+
return !code && isClosingState && isWsNotOpenStateError
|
|
3023
|
+
? new NativeWebSocketPeerClosedError()
|
|
3024
|
+
: normalized;
|
|
3025
|
+
}
|
|
3026
|
+
async function receiveExpectedNativeWebSocketFrame(frames, expected, signal) {
|
|
3027
|
+
const deadline = Date.now() + expected.timeoutMs;
|
|
3028
|
+
while (true) {
|
|
3029
|
+
const frame = await frames.next(deadline, signal);
|
|
3030
|
+
if (nativeWebSocketFrameMatches(frame, expected)) {
|
|
3031
|
+
return frame;
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
function nativeWebSocketFrameMatches(frame, expected) {
|
|
3036
|
+
if (expected.containsBytes) {
|
|
3037
|
+
return bufferContains(frame.data, expected.containsBytes);
|
|
3038
|
+
}
|
|
3039
|
+
if (expected.containsText != null) {
|
|
3040
|
+
return !frame.isBinary && Buffer.from(frame.data).toString("utf8").includes(expected.containsText);
|
|
3041
|
+
}
|
|
3042
|
+
return frame.data.byteLength > 0;
|
|
3043
|
+
}
|
|
3044
|
+
function bufferContains(value, expected) {
|
|
3045
|
+
if (expected.byteLength === 0) {
|
|
3046
|
+
return true;
|
|
3047
|
+
}
|
|
3048
|
+
return Buffer.from(value).indexOf(Buffer.from(expected)) >= 0;
|
|
3049
|
+
}
|
|
3050
|
+
function nativeWebSocketFrameToTrackingPayload(frame) {
|
|
3051
|
+
if (frame.isBinary) {
|
|
3052
|
+
return attachPayloadHelpers({
|
|
3053
|
+
headers: {},
|
|
3054
|
+
body: new Uint8Array(frame.data),
|
|
3055
|
+
contentType: "application/octet-stream"
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
const text = Buffer.from(frame.data).toString("utf8");
|
|
3059
|
+
let body = text;
|
|
3060
|
+
let contentType = "text/plain";
|
|
3061
|
+
try {
|
|
3062
|
+
body = JSON.parse(text);
|
|
3063
|
+
contentType = "application/json";
|
|
3064
|
+
}
|
|
3065
|
+
catch {
|
|
3066
|
+
// Non-JSON text is returned verbatim.
|
|
3067
|
+
}
|
|
3068
|
+
return attachPayloadHelpers({ headers: {}, body, contentType });
|
|
3069
|
+
}
|
|
3070
|
+
function isReconnectableNativeWebSocketError(error) {
|
|
3071
|
+
if (error instanceof NativeWebSocketPeerClosedError) {
|
|
3072
|
+
return true;
|
|
3073
|
+
}
|
|
3074
|
+
if (!error || typeof error !== "object") {
|
|
3075
|
+
return false;
|
|
3076
|
+
}
|
|
3077
|
+
const code = String(error.code ?? "");
|
|
3078
|
+
return new Set([
|
|
3079
|
+
"ECONNABORTED",
|
|
3080
|
+
"ECONNREFUSED",
|
|
3081
|
+
"ECONNRESET",
|
|
3082
|
+
"EHOSTUNREACH",
|
|
3083
|
+
"ENETUNREACH",
|
|
3084
|
+
"ENOTFOUND",
|
|
3085
|
+
"EAI_AGAIN",
|
|
3086
|
+
"EPIPE",
|
|
3087
|
+
"ETIMEDOUT"
|
|
3088
|
+
]).has(code);
|
|
3089
|
+
}
|
|
3090
|
+
function waitForNativeWebSocketRetry(delayMs, signal) {
|
|
3091
|
+
if (signal.aborted) {
|
|
3092
|
+
return Promise.reject(new NativeWebSocketAbortError());
|
|
3093
|
+
}
|
|
3094
|
+
return new Promise((resolve, reject) => {
|
|
3095
|
+
const onAbort = () => {
|
|
3096
|
+
clearTimeout(timer);
|
|
3097
|
+
reject(new NativeWebSocketAbortError());
|
|
3098
|
+
};
|
|
3099
|
+
const timer = setTimeout(() => {
|
|
3100
|
+
signal.removeEventListener("abort", onAbort);
|
|
3101
|
+
resolve();
|
|
3102
|
+
}, delayMs);
|
|
3103
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
function closeNativeWebSocket(socket, timeoutMs) {
|
|
3107
|
+
if (socket.readyState === WebSocket.CLOSED) {
|
|
3108
|
+
return Promise.resolve();
|
|
3109
|
+
}
|
|
3110
|
+
if (socket.readyState === WebSocket.CONNECTING) {
|
|
3111
|
+
socket.terminate();
|
|
3112
|
+
return Promise.resolve();
|
|
3113
|
+
}
|
|
3114
|
+
return new Promise((resolve) => {
|
|
3115
|
+
let settled = false;
|
|
3116
|
+
const finish = () => {
|
|
3117
|
+
if (settled) {
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
settled = true;
|
|
3121
|
+
clearTimeout(timeout);
|
|
3122
|
+
socket.off("close", onClose);
|
|
3123
|
+
socket.off("error", onError);
|
|
3124
|
+
resolve();
|
|
3125
|
+
};
|
|
3126
|
+
const onClose = () => finish();
|
|
3127
|
+
const onError = () => finish();
|
|
3128
|
+
const timeout = setTimeout(() => {
|
|
3129
|
+
socket.terminate();
|
|
3130
|
+
finish();
|
|
3131
|
+
}, timeoutMs);
|
|
3132
|
+
socket.once("close", onClose);
|
|
3133
|
+
socket.once("error", onError);
|
|
3134
|
+
try {
|
|
3135
|
+
socket.close(1000);
|
|
3136
|
+
}
|
|
3137
|
+
catch {
|
|
3138
|
+
socket.terminate();
|
|
3139
|
+
finish();
|
|
3140
|
+
}
|
|
3141
|
+
});
|
|
2317
3142
|
}
|
|
2318
3143
|
export class EndpointAdapterFactory {
|
|
2319
3144
|
static create(endpoint) {
|
|
@@ -2322,6 +3147,7 @@ export class EndpointAdapterFactory {
|
|
|
2322
3147
|
}
|
|
2323
3148
|
const normalized = normalizeEndpointDefinition(endpoint);
|
|
2324
3149
|
validateEndpointDefinition(normalized);
|
|
3150
|
+
validateRawNativeWebSocketHeaderInputs(endpoint, normalized);
|
|
2325
3151
|
switch (normalized.kind) {
|
|
2326
3152
|
case "Http":
|
|
2327
3153
|
return new HttpEndpointAdapter(normalized);
|
|
@@ -2350,6 +3176,56 @@ export class EndpointAdapterFactory {
|
|
|
2350
3176
|
}
|
|
2351
3177
|
}
|
|
2352
3178
|
}
|
|
3179
|
+
function validateRawNativeWebSocketHeaderInputs(input, endpoint) {
|
|
3180
|
+
if (endpoint.kind !== "WebSocket") {
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
const callbacks = endpoint.mode === "Produce"
|
|
3184
|
+
? selectProduceCallbackGroup(endpoint)
|
|
3185
|
+
: selectConsumeCallbackGroup(endpoint);
|
|
3186
|
+
if (callbacks.asyncDelegate || callbacks.syncDelegate) {
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
const raw = asRecordOrEmpty(input);
|
|
3190
|
+
const options = pickEndpointTransportRecord(raw, "WebSocket", "WebSocket", ["webSocket", "WebSocket"], WEB_SOCKET_ENDPOINT_FLAT_KEYS);
|
|
3191
|
+
if (pickEndpointValue(options, "NativeClient", "nativeClient") == null) {
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
const protocolMetadata = pickEndpointValue(options, "ConnectionMetadata", "connectionMetadata");
|
|
3195
|
+
if (protocolMetadata != null) {
|
|
3196
|
+
validateNativeWebSocketHeaderLayer(protocolMetadata, "ConnectionMetadata");
|
|
3197
|
+
}
|
|
3198
|
+
const rootMetadata = pickEndpointValue(raw, "ConnectionMetadata", "connectionMetadata");
|
|
3199
|
+
if (rootMetadata != null && rootMetadata !== protocolMetadata) {
|
|
3200
|
+
validateNativeWebSocketHeaderLayer(rootMetadata, "ConnectionMetadata");
|
|
3201
|
+
}
|
|
3202
|
+
const delegate = pickEndpointValue(raw, "Delegate", "delegate", "DelegateStream");
|
|
3203
|
+
if (isRecord(delegate)) {
|
|
3204
|
+
const delegateMetadata = pickEndpointValue(delegate, "ConnectionMetadata", "connectionMetadata");
|
|
3205
|
+
if (delegateMetadata != null) {
|
|
3206
|
+
validateNativeWebSocketHeaderLayer(delegateMetadata, "ConnectionMetadata");
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
export function validateNativeEndpointExecutionSupport(endpoint) {
|
|
3211
|
+
const normalized = normalizeEndpointDefinition(endpoint);
|
|
3212
|
+
if (!normalized || typeof normalized !== "object") {
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
validateRawNativeWebSocketHeaderInputs(endpoint, normalized);
|
|
3216
|
+
const kind = String(normalized.kind ?? "").trim().toLowerCase();
|
|
3217
|
+
const modeToken = String(normalized.mode ?? "").trim().toLowerCase();
|
|
3218
|
+
if (modeToken !== "produce" && modeToken !== "consume") {
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
const mode = modeToken === "produce" ? "Produce" : "Consume";
|
|
3222
|
+
if (kind === "grpc") {
|
|
3223
|
+
validateGrpcEndpoint(normalized, mode);
|
|
3224
|
+
}
|
|
3225
|
+
else if (kind === "websocket") {
|
|
3226
|
+
validateWebSocketEndpoint(normalized, mode);
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
2353
3229
|
const HTTP_ENDPOINT_FLAT_KEYS = [
|
|
2354
3230
|
"Url",
|
|
2355
3231
|
"url",
|
|
@@ -2576,7 +3452,9 @@ const WEB_SOCKET_ENDPOINT_FLAT_KEYS = [
|
|
|
2576
3452
|
"ConsumeAsync",
|
|
2577
3453
|
"consumeAsync",
|
|
2578
3454
|
"ConnectionMetadata",
|
|
2579
|
-
"connectionMetadata"
|
|
3455
|
+
"connectionMetadata",
|
|
3456
|
+
"NativeClient",
|
|
3457
|
+
"nativeClient"
|
|
2580
3458
|
];
|
|
2581
3459
|
function normalizeEndpointDefinition(endpoint) {
|
|
2582
3460
|
if (!endpoint || typeof endpoint !== "object") {
|
|
@@ -2882,6 +3760,10 @@ function pickOptionalEndpointString(record, ...keys) {
|
|
|
2882
3760
|
const value = optionString(record, ...keys).trim();
|
|
2883
3761
|
return value ? value : undefined;
|
|
2884
3762
|
}
|
|
3763
|
+
function pickOptionalEndpointExactString(record, ...keys) {
|
|
3764
|
+
const value = pickEndpointValue(record, ...keys);
|
|
3765
|
+
return typeof value === "string" ? value : undefined;
|
|
3766
|
+
}
|
|
2885
3767
|
function pickOptionalEndpointStringAllowEmpty(record, ...keys) {
|
|
2886
3768
|
const value = pickEndpointValue(record, ...keys);
|
|
2887
3769
|
return typeof value === "string" ? value.trim() : undefined;
|
|
@@ -2985,8 +3867,10 @@ function validateEndpointDefinition(endpoint) {
|
|
|
2985
3867
|
if (mode !== "Produce" && mode !== "Consume") {
|
|
2986
3868
|
throw new Error(`Unsupported endpoint mode: ${mode}.`);
|
|
2987
3869
|
}
|
|
2988
|
-
const
|
|
2989
|
-
const
|
|
3870
|
+
const produceCallbacks = selectProduceCallbackGroup(endpoint);
|
|
3871
|
+
const consumeCallbacks = selectConsumeCallbackGroup(endpoint);
|
|
3872
|
+
const hasProduceDelegate = Boolean(produceCallbacks.asyncDelegate || produceCallbacks.syncDelegate);
|
|
3873
|
+
const hasConsumeDelegate = Boolean(consumeCallbacks.asyncDelegate || consumeCallbacks.syncDelegate);
|
|
2990
3874
|
const hasModeDelegate = mode === "Produce" ? hasProduceDelegate : hasConsumeDelegate;
|
|
2991
3875
|
switch (kind) {
|
|
2992
3876
|
case "Http":
|
|
@@ -3098,7 +3982,14 @@ function validateDelegateStreamEndpoint(endpoint, mode) {
|
|
|
3098
3982
|
}
|
|
3099
3983
|
}
|
|
3100
3984
|
function validateGrpcEndpoint(endpoint, mode) {
|
|
3985
|
+
const callbacks = mode === "Produce"
|
|
3986
|
+
? selectProduceCallbackGroup(endpoint)
|
|
3987
|
+
: selectConsumeCallbackGroup(endpoint);
|
|
3101
3988
|
const options = asRecordOrEmpty(endpoint.grpc);
|
|
3989
|
+
const nativeClient = pickEndpointValue(options, "NativeClient", "nativeClient");
|
|
3990
|
+
if (nativeClient != null && !callbacks.asyncDelegate && !callbacks.syncDelegate) {
|
|
3991
|
+
throw new Error(NATIVE_GRPC_EXECUTION_UNAVAILABLE_MESSAGE);
|
|
3992
|
+
}
|
|
3102
3993
|
requireNonEmptyString(optionString(options, "Target", "target"), "Target must be provided for gRPC endpoint definitions.");
|
|
3103
3994
|
requireNonEmptyString(optionString(options, "ServiceName", "serviceName"), "ServiceName must be provided for gRPC endpoint definitions.");
|
|
3104
3995
|
requireNonEmptyString(optionString(options, "MethodName", "methodName"), "MethodName must be provided for gRPC endpoint definitions.");
|
|
@@ -3109,18 +4000,17 @@ function validateGrpcEndpoint(endpoint, mode) {
|
|
|
3109
4000
|
|| (hasOptionValue(options, "DeadlineSeconds", "deadlineSeconds", "Deadline", "deadline") && deadlineSeconds <= 0)) {
|
|
3110
4001
|
throw new RangeError("Deadline must be greater than zero.");
|
|
3111
4002
|
}
|
|
3112
|
-
|
|
3113
|
-
if (nativeClient != null) {
|
|
3114
|
-
new GrpcNativeClientOptions(asRecordOrEmpty(nativeClient)).validate();
|
|
3115
|
-
}
|
|
3116
|
-
if (mode === "Produce" && !resolveStructuredProduceDelegate(endpoint) && !resolveProduceDelegate(endpoint) && nativeClient == null) {
|
|
4003
|
+
if (mode === "Produce" && !callbacks.asyncDelegate && !callbacks.syncDelegate && nativeClient == null) {
|
|
3117
4004
|
throw new Error("ProduceAsync delegate must be provided when endpoint mode is Produce.");
|
|
3118
4005
|
}
|
|
3119
|
-
if (mode === "Consume" && !
|
|
4006
|
+
if (mode === "Consume" && !callbacks.asyncDelegate && !callbacks.syncDelegate && nativeClient == null) {
|
|
3120
4007
|
throw new Error("ConsumeAsync delegate must be provided when endpoint mode is Consume.");
|
|
3121
4008
|
}
|
|
3122
4009
|
}
|
|
3123
4010
|
function validateWebSocketEndpoint(endpoint, mode) {
|
|
4011
|
+
const callbacks = mode === "Produce"
|
|
4012
|
+
? selectProduceCallbackGroup(endpoint)
|
|
4013
|
+
: selectConsumeCallbackGroup(endpoint);
|
|
3124
4014
|
const options = asRecordOrEmpty(endpoint.webSocket);
|
|
3125
4015
|
const url = requireNonEmptyString(optionString(options, "Url", "url"), "Url must be provided for WebSocket endpoint definitions.");
|
|
3126
4016
|
let parsed;
|
|
@@ -3130,32 +4020,277 @@ function validateWebSocketEndpoint(endpoint, mode) {
|
|
|
3130
4020
|
catch {
|
|
3131
4021
|
throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
|
|
3132
4022
|
}
|
|
3133
|
-
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:")
|
|
4023
|
+
if ((parsed.protocol !== "ws:" && parsed.protocol !== "wss:")
|
|
4024
|
+
|| !parsed.hostname
|
|
4025
|
+
|| Boolean(parsed.hash)) {
|
|
3134
4026
|
throw new Error("Url must be an absolute WebSocket URI using the ws or wss scheme.");
|
|
3135
4027
|
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
throw new RangeError("CloseTimeout must be greater than zero.");
|
|
3147
|
-
}
|
|
4028
|
+
validateNativeWebSocketTimerDelay(hasOptionValue(options, "ConnectTimeoutMs", "connectTimeoutMs")
|
|
4029
|
+
? optionNumber(options, "ConnectTimeoutMs", "connectTimeoutMs")
|
|
4030
|
+
: undefined, hasOptionValue(options, "ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeout", "connectTimeout")
|
|
4031
|
+
? optionNumber(options, "ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeout", "connectTimeout")
|
|
4032
|
+
: undefined, "ConnectTimeout");
|
|
4033
|
+
validateNativeWebSocketTimerDelay(hasOptionValue(options, "CloseTimeoutMs", "closeTimeoutMs")
|
|
4034
|
+
? optionNumber(options, "CloseTimeoutMs", "closeTimeoutMs")
|
|
4035
|
+
: undefined, hasOptionValue(options, "CloseTimeoutSeconds", "closeTimeoutSeconds", "CloseTimeout", "closeTimeout")
|
|
4036
|
+
? optionNumber(options, "CloseTimeoutSeconds", "closeTimeoutSeconds", "CloseTimeout", "closeTimeout")
|
|
4037
|
+
: undefined, "CloseTimeout");
|
|
3148
4038
|
const nativeClient = pickEndpointValue(options, "NativeClient", "nativeClient");
|
|
3149
|
-
|
|
3150
|
-
|
|
4039
|
+
const hasSelectedDelegate = Boolean(callbacks.asyncDelegate || callbacks.syncDelegate);
|
|
4040
|
+
if (nativeClient != null && !hasSelectedDelegate) {
|
|
4041
|
+
if (mode === "Consume" && endpoint.trackingField.trim().toLowerCase().startsWith("header:")) {
|
|
4042
|
+
throw new Error("Native WebSocket Consume cannot use a header TrackingField because WebSocket messages do not contain HTTP response headers.");
|
|
4043
|
+
}
|
|
4044
|
+
if (mode === "Consume" && endpoint.gatherByField?.trim().toLowerCase().startsWith("header:")) {
|
|
4045
|
+
throw new Error("Native WebSocket Consume cannot use a header GatherByField because WebSocket messages do not contain HTTP response headers.");
|
|
4046
|
+
}
|
|
4047
|
+
validateNativeWebSocketSubprotocols(pickEndpointValue(options, "Subprotocols", "subprotocols"));
|
|
4048
|
+
validateNativeWebSocketOptionsWithInstance(nativeClient);
|
|
4049
|
+
validateNativeWebSocketHeaderLayer(resolveConnectionMetadata(endpoint), "ConnectionMetadata");
|
|
3151
4050
|
}
|
|
3152
|
-
if (mode === "Produce" && !
|
|
4051
|
+
if (mode === "Produce" && !callbacks.asyncDelegate && !callbacks.syncDelegate && nativeClient == null) {
|
|
3153
4052
|
throw new Error("ProduceAsync delegate must be provided when endpoint mode is Produce.");
|
|
3154
4053
|
}
|
|
3155
|
-
if (mode === "Consume" && !
|
|
4054
|
+
if (mode === "Consume" && !callbacks.asyncDelegate && !callbacks.syncDelegate && nativeClient == null) {
|
|
3156
4055
|
throw new Error("ConsumeAsync delegate must be provided when endpoint mode is Consume.");
|
|
3157
4056
|
}
|
|
3158
4057
|
}
|
|
4058
|
+
const NATIVE_WEBSOCKET_MANAGED_HEADERS = new Set([
|
|
4059
|
+
"connection",
|
|
4060
|
+
"content-length",
|
|
4061
|
+
"expect",
|
|
4062
|
+
"host",
|
|
4063
|
+
"sec-websocket-accept",
|
|
4064
|
+
"sec-websocket-extensions",
|
|
4065
|
+
"sec-websocket-key",
|
|
4066
|
+
"sec-websocket-protocol",
|
|
4067
|
+
"sec-websocket-version",
|
|
4068
|
+
"upgrade"
|
|
4069
|
+
]);
|
|
4070
|
+
const NATIVE_WEBSOCKET_HTTP_TOKEN_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
4071
|
+
const NATIVE_WEBSOCKET_MAX_RECONNECT_ATTEMPTS = 2147483646;
|
|
4072
|
+
const NATIVE_WEBSOCKET_MAX_TIMER_DELAY_MS = 2147483647;
|
|
4073
|
+
function validateNativeWebSocketReconnectPolicy(value) {
|
|
4074
|
+
if (!isRecord(value)) {
|
|
4075
|
+
throw new TypeError("WebSocket NativeClient.Reconnect must be an object.");
|
|
4076
|
+
}
|
|
4077
|
+
const maxAttempts = pickEndpointValue(value, "MaxAttempts", "maxAttempts");
|
|
4078
|
+
if (maxAttempts != null) {
|
|
4079
|
+
const number = nativeWebSocketStrictNumber(maxAttempts, "MaxAttempts");
|
|
4080
|
+
if (!Number.isInteger(number) || number < 0 || number > NATIVE_WEBSOCKET_MAX_RECONNECT_ATTEMPTS) {
|
|
4081
|
+
throw new RangeError("MaxAttempts must be a finite nonnegative whole number.");
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
validateNativeWebSocketTimerDelay(pickEndpointValue(value, "DelayMs", "delayMs"), pickEndpointValue(value, "DelaySeconds", "delaySeconds", "Delay", "delay"), "Delay", true);
|
|
4085
|
+
}
|
|
4086
|
+
function validateNativeWebSocketOptions(value) {
|
|
4087
|
+
if (!isRecord(value)) {
|
|
4088
|
+
throw new TypeError("WebSocket NativeClient must be an object.");
|
|
4089
|
+
}
|
|
4090
|
+
const native = value;
|
|
4091
|
+
const headers = pickEndpointValue(native, "Headers", "headers");
|
|
4092
|
+
if (headers != null) {
|
|
4093
|
+
validateNativeWebSocketHeaderLayer(headers, "NativeClient.Headers");
|
|
4094
|
+
}
|
|
4095
|
+
const cookies = pickEndpointValue(native, "Cookies", "cookies");
|
|
4096
|
+
if (cookies != null && !Array.isArray(cookies)) {
|
|
4097
|
+
throw new TypeError("WebSocket NativeClient.Cookies must be an array.");
|
|
4098
|
+
}
|
|
4099
|
+
if (Array.isArray(cookies)) {
|
|
4100
|
+
cookies.forEach((cookie, index) => {
|
|
4101
|
+
if (typeof cookie !== "string") {
|
|
4102
|
+
throw new TypeError(`WebSocket NativeClient.Cookies[${index}] must be a string.`);
|
|
4103
|
+
}
|
|
4104
|
+
if (!cookie.trim()) {
|
|
4105
|
+
throw new Error(`WebSocket NativeClient.Cookies[${index}] must not be blank.`);
|
|
4106
|
+
}
|
|
4107
|
+
if (/\r|\n/.test(cookie)) {
|
|
4108
|
+
throw new Error(`WebSocket NativeClient.Cookies[${index}] must not contain CR or LF characters.`);
|
|
4109
|
+
}
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
4112
|
+
const pingMs = pickEndpointValue(native, "PingIntervalMs", "pingIntervalMs");
|
|
4113
|
+
const pingSeconds = pickEndpointValue(native, "PingIntervalSeconds", "pingIntervalSeconds", "PingInterval", "pingInterval");
|
|
4114
|
+
validateNativeWebSocketTimerDelay(pingMs, pingSeconds, "PingInterval");
|
|
4115
|
+
const reconnectValue = pickEndpointValue(native, "Reconnect", "reconnect");
|
|
4116
|
+
if (reconnectValue != null) {
|
|
4117
|
+
validateNativeWebSocketReconnectPolicy(reconnectValue);
|
|
4118
|
+
if (reconnectValue instanceof WebSocketReconnectPolicy) {
|
|
4119
|
+
reconnectValue.validate();
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
const messages = pickEndpointValue(native, "Messages", "messages");
|
|
4123
|
+
if (messages != null && !Array.isArray(messages)) {
|
|
4124
|
+
throw new TypeError("WebSocket NativeClient.Messages must be an array.");
|
|
4125
|
+
}
|
|
4126
|
+
if (Array.isArray(messages)) {
|
|
4127
|
+
for (const message of messages) {
|
|
4128
|
+
if (message instanceof WebSocketMessageSpec) {
|
|
4129
|
+
message.validate();
|
|
4130
|
+
}
|
|
4131
|
+
else {
|
|
4132
|
+
validateNativeWebSocketMessage(message);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
const expectedMessages = pickEndpointValue(native, "ExpectedMessages", "expectedMessages");
|
|
4137
|
+
if (expectedMessages != null && !Array.isArray(expectedMessages)) {
|
|
4138
|
+
throw new TypeError("WebSocket NativeClient.ExpectedMessages must be an array.");
|
|
4139
|
+
}
|
|
4140
|
+
if (Array.isArray(expectedMessages)) {
|
|
4141
|
+
for (const expected of expectedMessages) {
|
|
4142
|
+
if (expected instanceof WebSocketExpectedMessage) {
|
|
4143
|
+
expected.validate();
|
|
4144
|
+
}
|
|
4145
|
+
else {
|
|
4146
|
+
validateNativeWebSocketExpectedMessage(expected);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
function validateNativeWebSocketOptionsWithInstance(value) {
|
|
4152
|
+
validateNativeWebSocketOptions(value);
|
|
4153
|
+
if (value instanceof WebSocketNativeClientOptions) {
|
|
4154
|
+
value.validate();
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
function validateNativeWebSocketHeaderLayer(value, layerName) {
|
|
4158
|
+
if (!isRecord(value)) {
|
|
4159
|
+
throw new TypeError(`WebSocket ${layerName} must be an object.`);
|
|
4160
|
+
}
|
|
4161
|
+
for (const [name, headerValue] of Object.entries(value)) {
|
|
4162
|
+
const normalizedName = name.trim();
|
|
4163
|
+
if (!normalizedName) {
|
|
4164
|
+
throw new Error(`WebSocket ${layerName} contains an empty header name.`);
|
|
4165
|
+
}
|
|
4166
|
+
if (name !== normalizedName) {
|
|
4167
|
+
throw new Error(`WebSocket ${layerName} header names must not contain surrounding whitespace.`);
|
|
4168
|
+
}
|
|
4169
|
+
if (/\r|\n/.test(name) || /\r|\n/.test(String(headerValue ?? ""))) {
|
|
4170
|
+
throw new Error(`WebSocket ${layerName} must not contain CR or LF characters.`);
|
|
4171
|
+
}
|
|
4172
|
+
if (!NATIVE_WEBSOCKET_HTTP_TOKEN_PATTERN.test(normalizedName)) {
|
|
4173
|
+
throw new Error(`WebSocket ${layerName} contains an invalid HTTP header name.`);
|
|
4174
|
+
}
|
|
4175
|
+
if (typeof headerValue !== "string") {
|
|
4176
|
+
throw new TypeError(`WebSocket ${layerName} header values must be strings.`);
|
|
4177
|
+
}
|
|
4178
|
+
if (NATIVE_WEBSOCKET_MANAGED_HEADERS.has(normalizedName.toLowerCase())) {
|
|
4179
|
+
throw new Error(`WebSocket ${layerName} must not configure ws-managed header '${normalizedName}'.`);
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
function validateNativeWebSocketSubprotocols(value) {
|
|
4184
|
+
if (value == null) {
|
|
4185
|
+
return;
|
|
4186
|
+
}
|
|
4187
|
+
if (!Array.isArray(value)) {
|
|
4188
|
+
throw new TypeError("WebSocket Subprotocols must be an array.");
|
|
4189
|
+
}
|
|
4190
|
+
const seen = new Set();
|
|
4191
|
+
value.forEach((protocol, index) => {
|
|
4192
|
+
if (typeof protocol !== "string" || !protocol.trim()) {
|
|
4193
|
+
throw new TypeError(`WebSocket Subprotocols[${index}] must be a nonblank string.`);
|
|
4194
|
+
}
|
|
4195
|
+
const normalized = protocol.trim();
|
|
4196
|
+
if (!NATIVE_WEBSOCKET_HTTP_TOKEN_PATTERN.test(normalized)) {
|
|
4197
|
+
throw new Error(`WebSocket Subprotocols[${index}] must be a valid RFC 6455 protocol token.`);
|
|
4198
|
+
}
|
|
4199
|
+
if (seen.has(normalized)) {
|
|
4200
|
+
throw new Error("WebSocket Subprotocols must not contain duplicate values.");
|
|
4201
|
+
}
|
|
4202
|
+
seen.add(normalized);
|
|
4203
|
+
});
|
|
4204
|
+
}
|
|
4205
|
+
function validateOriginalNativeWebSocketMessage(value) {
|
|
4206
|
+
validateNativeWebSocketMessage({
|
|
4207
|
+
Kind: pickEndpointValue(value, "Kind", "kind") ?? "Text",
|
|
4208
|
+
TextPayload: pickEndpointValue(value, "TextPayload", "textPayload"),
|
|
4209
|
+
BinaryPayload: pickEndpointValue(value, "BinaryPayload", "binaryPayload")
|
|
4210
|
+
});
|
|
4211
|
+
}
|
|
4212
|
+
function validateNativeWebSocketMessage(value) {
|
|
4213
|
+
if (!isRecord(value)) {
|
|
4214
|
+
throw new TypeError("WebSocket NativeClient.Messages entries must be message objects.");
|
|
4215
|
+
}
|
|
4216
|
+
const kind = optionString(value, "Kind", "kind").toLowerCase();
|
|
4217
|
+
if (kind !== "text" && kind !== "binary") {
|
|
4218
|
+
throw new Error("WebSocket Message kind must be Text or Binary.");
|
|
4219
|
+
}
|
|
4220
|
+
if (kind === "text") {
|
|
4221
|
+
if (typeof pickEndpointValue(value, "TextPayload", "textPayload") !== "string") {
|
|
4222
|
+
throw new TypeError("Text WebSocket messages require TextPayload.");
|
|
4223
|
+
}
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
validateNativeWebSocketBytes(pickEndpointValue(value, "BinaryPayload", "binaryPayload"), "WebSocket binary payload");
|
|
4227
|
+
}
|
|
4228
|
+
function validateNativeWebSocketExpectedMessage(value) {
|
|
4229
|
+
if (!isRecord(value)) {
|
|
4230
|
+
throw new TypeError("WebSocket NativeClient.ExpectedMessages entries must be expected-message objects.");
|
|
4231
|
+
}
|
|
4232
|
+
const containsText = pickEndpointValue(value, "ContainsText", "containsText");
|
|
4233
|
+
if (containsText != null && typeof containsText !== "string") {
|
|
4234
|
+
throw new TypeError("WebSocket expected ContainsText must be a string.");
|
|
4235
|
+
}
|
|
4236
|
+
const containsBytes = pickEndpointValue(value, "ContainsBytes", "containsBytes");
|
|
4237
|
+
if (containsBytes != null) {
|
|
4238
|
+
validateNativeWebSocketBytes(containsBytes, "WebSocket expected ContainsBytes");
|
|
4239
|
+
}
|
|
4240
|
+
const timeoutMs = pickEndpointValue(value, "TimeoutMs", "timeoutMs");
|
|
4241
|
+
const timeoutSeconds = pickEndpointValue(value, "TimeoutSeconds", "timeoutSeconds", "Timeout", "timeout");
|
|
4242
|
+
validateNativeWebSocketTimerDelay(timeoutMs, timeoutSeconds, "Timeout");
|
|
4243
|
+
}
|
|
4244
|
+
function validateNativeWebSocketBytes(value, fieldName) {
|
|
4245
|
+
if (value instanceof Uint8Array) {
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
if (!Array.isArray(value) || value.some((byte) => (typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255))) {
|
|
4249
|
+
throw new TypeError(`${fieldName} must contain only byte values from 0 through 255.`);
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
function validateNativeWebSocketTimerDelay(millisecondValue, secondValue, fieldName, allowZero = false) {
|
|
4253
|
+
const validateValue = (value, multiplier) => {
|
|
4254
|
+
const number = nativeWebSocketStrictNumber(value, fieldName);
|
|
4255
|
+
if (allowZero ? number < 0 : number <= 0) {
|
|
4256
|
+
throw new RangeError(allowZero
|
|
4257
|
+
? `${fieldName} must be zero or greater and finite.`
|
|
4258
|
+
: `${fieldName} must be greater than zero and finite.`);
|
|
4259
|
+
}
|
|
4260
|
+
if (number > NATIVE_WEBSOCKET_MAX_TIMER_DELAY_MS / multiplier) {
|
|
4261
|
+
throw new RangeError(`${fieldName} must not exceed 2,147,483,647 milliseconds.`);
|
|
4262
|
+
}
|
|
4263
|
+
return number * multiplier;
|
|
4264
|
+
};
|
|
4265
|
+
const milliseconds = millisecondValue == null
|
|
4266
|
+
? undefined
|
|
4267
|
+
: validateValue(millisecondValue, 1);
|
|
4268
|
+
const seconds = secondValue == null
|
|
4269
|
+
? undefined
|
|
4270
|
+
: validateValue(secondValue, 1000);
|
|
4271
|
+
return milliseconds ?? seconds;
|
|
4272
|
+
}
|
|
4273
|
+
function nativeWebSocketStrictNumber(value, fieldName) {
|
|
4274
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
4275
|
+
throw new TypeError(`${fieldName} must be a finite number.`);
|
|
4276
|
+
}
|
|
4277
|
+
return value;
|
|
4278
|
+
}
|
|
4279
|
+
function validateUnsupportedNativeEndpointMode(endpoint, protocol, mode, hasSelectedDelegate) {
|
|
4280
|
+
const options = protocol === "gRPC"
|
|
4281
|
+
? asRecordOrEmpty(endpoint.grpc)
|
|
4282
|
+
: asRecordOrEmpty(endpoint.webSocket);
|
|
4283
|
+
const nativeClient = pickEndpointValue(options, "NativeClient", "nativeClient");
|
|
4284
|
+
if (nativeClient != null && !hasSelectedDelegate) {
|
|
4285
|
+
throw unsupportedNativeEndpointModeError(protocol, mode);
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
function unsupportedNativeEndpointModeError(protocol, mode) {
|
|
4289
|
+
const callbackName = mode === "Produce" ? "Produce" : "Consume";
|
|
4290
|
+
const asyncCallbackName = `${callbackName}Async`;
|
|
4291
|
+
return new Error(`Native ${protocol} ${mode} mode is not supported by the TypeScript SDK. `
|
|
4292
|
+
+ `Provide a ${callbackName} or ${asyncCallbackName} callback instead.`);
|
|
4293
|
+
}
|
|
3159
4294
|
function validateHttpAuthOptions(options) {
|
|
3160
4295
|
const auth = options.auth;
|
|
3161
4296
|
if (!auth) {
|
|
@@ -3330,13 +4465,13 @@ function validatePushDiffusionEndpoint(endpoint, mode) {
|
|
|
3330
4465
|
}
|
|
3331
4466
|
requireNonEmptyString(optionString(options, "ServerUrl", "serverUrl"), "ServerUrl must be provided for Push Diffusion endpoint.");
|
|
3332
4467
|
requireNonEmptyString(optionString(options, "TopicPath", "topicPath"), "TopicPath must be provided for Push Diffusion endpoint.");
|
|
3333
|
-
|
|
4468
|
+
const callbacks = mode === "Produce"
|
|
4469
|
+
? selectProduceCallbackGroup(endpoint)
|
|
4470
|
+
: selectConsumeCallbackGroup(endpoint);
|
|
4471
|
+
if (mode === "Produce" && !callbacks.asyncDelegate && !callbacks.syncDelegate) {
|
|
3334
4472
|
throw new Error("PublishAsync delegate must be provided for Push Diffusion producer endpoint.");
|
|
3335
4473
|
}
|
|
3336
|
-
if (mode === "Consume"
|
|
3337
|
-
&& !resolveStructuredConsumeDelegate(endpoint)
|
|
3338
|
-
&& !resolveStructuredConsumeStreamDelegate(endpoint)
|
|
3339
|
-
&& !resolveConsumeDelegate(endpoint)) {
|
|
4474
|
+
if (mode === "Consume" && !callbacks.asyncDelegate && !callbacks.syncDelegate) {
|
|
3340
4475
|
throw new Error("SubscribeAsync delegate must be provided for Push Diffusion consumer endpoint.");
|
|
3341
4476
|
}
|
|
3342
4477
|
}
|
|
@@ -4077,60 +5212,109 @@ function parseBodyObject(body) {
|
|
|
4077
5212
|
return null;
|
|
4078
5213
|
}
|
|
4079
5214
|
}
|
|
5215
|
+
function selectProduceCallbackGroup(endpoint) {
|
|
5216
|
+
const genericAsync = endpoint.delegate?.produceAsync;
|
|
5217
|
+
const genericSync = endpoint.delegate?.produce;
|
|
5218
|
+
if (typeof genericAsync === "function" || typeof genericSync === "function") {
|
|
5219
|
+
return {
|
|
5220
|
+
asyncDelegate: typeof genericAsync === "function" ? genericAsync : undefined,
|
|
5221
|
+
syncDelegate: typeof genericSync === "function" ? genericSync : undefined
|
|
5222
|
+
};
|
|
5223
|
+
}
|
|
5224
|
+
let activeAsync;
|
|
5225
|
+
let activeSync;
|
|
5226
|
+
if (endpoint.kind === "PushDiffusion") {
|
|
5227
|
+
activeAsync = endpoint.pushDiffusion?.PublishAsync
|
|
5228
|
+
?? endpoint.pushDiffusion?.publishAsync;
|
|
5229
|
+
}
|
|
5230
|
+
else if (endpoint.kind === "Grpc") {
|
|
5231
|
+
activeAsync = endpoint.grpc?.ProduceAsync
|
|
5232
|
+
?? endpoint.grpc?.produceAsync;
|
|
5233
|
+
activeSync = endpoint.grpc?.Produce
|
|
5234
|
+
?? endpoint.grpc?.produce;
|
|
5235
|
+
}
|
|
5236
|
+
else if (endpoint.kind === "WebSocket") {
|
|
5237
|
+
activeAsync = endpoint.webSocket?.ProduceAsync
|
|
5238
|
+
?? endpoint.webSocket?.produceAsync;
|
|
5239
|
+
activeSync = endpoint.webSocket?.Produce
|
|
5240
|
+
?? endpoint.webSocket?.produce;
|
|
5241
|
+
}
|
|
5242
|
+
return {
|
|
5243
|
+
asyncDelegate: typeof activeAsync === "function"
|
|
5244
|
+
? activeAsync
|
|
5245
|
+
: undefined,
|
|
5246
|
+
syncDelegate: typeof activeSync === "function"
|
|
5247
|
+
? activeSync
|
|
5248
|
+
: undefined
|
|
5249
|
+
};
|
|
5250
|
+
}
|
|
5251
|
+
function selectConsumeCallbackGroup(endpoint) {
|
|
5252
|
+
const genericAsync = endpoint.delegate?.consumeAsync;
|
|
5253
|
+
const genericSync = endpoint.delegate?.consume;
|
|
5254
|
+
if (typeof genericAsync === "function" || typeof genericSync === "function") {
|
|
5255
|
+
return {
|
|
5256
|
+
asyncDelegate: typeof genericAsync === "function" ? genericAsync : undefined,
|
|
5257
|
+
syncDelegate: typeof genericSync === "function" ? genericSync : undefined
|
|
5258
|
+
};
|
|
5259
|
+
}
|
|
5260
|
+
let activeAsync;
|
|
5261
|
+
let activeSync;
|
|
5262
|
+
if (endpoint.kind === "PushDiffusion") {
|
|
5263
|
+
activeAsync = endpoint.pushDiffusion?.SubscribeAsync
|
|
5264
|
+
?? endpoint.pushDiffusion?.subscribeAsync;
|
|
5265
|
+
}
|
|
5266
|
+
else if (endpoint.kind === "Grpc") {
|
|
5267
|
+
activeAsync = endpoint.grpc?.ConsumeAsync
|
|
5268
|
+
?? endpoint.grpc?.consumeAsync;
|
|
5269
|
+
activeSync = endpoint.grpc?.Consume
|
|
5270
|
+
?? endpoint.grpc?.consume;
|
|
5271
|
+
}
|
|
5272
|
+
else if (endpoint.kind === "WebSocket") {
|
|
5273
|
+
activeAsync = endpoint.webSocket?.ConsumeAsync
|
|
5274
|
+
?? endpoint.webSocket?.consumeAsync;
|
|
5275
|
+
activeSync = endpoint.webSocket?.Consume
|
|
5276
|
+
?? endpoint.webSocket?.consume;
|
|
5277
|
+
}
|
|
5278
|
+
return {
|
|
5279
|
+
asyncDelegate: typeof activeAsync === "function" ? activeAsync : undefined,
|
|
5280
|
+
syncDelegate: typeof activeSync === "function"
|
|
5281
|
+
? activeSync
|
|
5282
|
+
: undefined
|
|
5283
|
+
};
|
|
5284
|
+
}
|
|
4080
5285
|
function resolveStructuredProduceDelegate(endpoint) {
|
|
4081
|
-
return endpoint.
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
?? endpoint.grpc?.produceAsync
|
|
4086
|
-
?? endpoint.webSocket?.ProduceAsync
|
|
4087
|
-
?? endpoint.webSocket?.produceAsync;
|
|
5286
|
+
return selectProduceCallbackGroup(endpoint).asyncDelegate;
|
|
5287
|
+
}
|
|
5288
|
+
function resolveProduceDelegate(endpoint) {
|
|
5289
|
+
return selectProduceCallbackGroup(endpoint).syncDelegate;
|
|
4088
5290
|
}
|
|
4089
5291
|
function resolveStructuredConsumeDelegate(endpoint) {
|
|
4090
|
-
const delegate = endpoint.
|
|
4091
|
-
?? endpoint.pushDiffusion?.SubscribeAsync
|
|
4092
|
-
?? endpoint.pushDiffusion?.subscribeAsync
|
|
4093
|
-
?? endpoint.grpc?.ConsumeAsync
|
|
4094
|
-
?? endpoint.grpc?.consumeAsync
|
|
4095
|
-
?? endpoint.webSocket?.ConsumeAsync
|
|
4096
|
-
?? endpoint.webSocket?.consumeAsync;
|
|
5292
|
+
const delegate = selectConsumeCallbackGroup(endpoint).asyncDelegate;
|
|
4097
5293
|
return typeof delegate === "function" && delegate.length === 0
|
|
4098
5294
|
? delegate
|
|
4099
5295
|
: undefined;
|
|
4100
5296
|
}
|
|
4101
5297
|
function resolveStructuredConsumeStreamDelegate(endpoint) {
|
|
4102
|
-
const delegate = endpoint.
|
|
4103
|
-
?? endpoint.pushDiffusion?.SubscribeAsync
|
|
4104
|
-
?? endpoint.pushDiffusion?.subscribeAsync
|
|
4105
|
-
?? endpoint.grpc?.ConsumeAsync
|
|
4106
|
-
?? endpoint.grpc?.consumeAsync
|
|
4107
|
-
?? endpoint.webSocket?.ConsumeAsync
|
|
4108
|
-
?? endpoint.webSocket?.consumeAsync;
|
|
5298
|
+
const delegate = selectConsumeCallbackGroup(endpoint).asyncDelegate;
|
|
4109
5299
|
return typeof delegate === "function" && delegate.length > 0
|
|
4110
5300
|
? delegate
|
|
4111
5301
|
: undefined;
|
|
4112
5302
|
}
|
|
4113
|
-
function resolveProduceDelegate(endpoint) {
|
|
4114
|
-
return endpoint.delegate?.produce
|
|
4115
|
-
?? endpoint.grpc?.Produce
|
|
4116
|
-
?? endpoint.grpc?.produce
|
|
4117
|
-
?? endpoint.webSocket?.Produce
|
|
4118
|
-
?? endpoint.webSocket?.produce;
|
|
4119
|
-
}
|
|
4120
5303
|
function resolveConsumeDelegate(endpoint) {
|
|
4121
|
-
return endpoint.
|
|
4122
|
-
?? endpoint.grpc?.Consume
|
|
4123
|
-
?? endpoint.grpc?.consume
|
|
4124
|
-
?? endpoint.webSocket?.Consume
|
|
4125
|
-
?? endpoint.webSocket?.consume;
|
|
5304
|
+
return selectConsumeCallbackGroup(endpoint).syncDelegate;
|
|
4126
5305
|
}
|
|
4127
5306
|
function resolveConnectionMetadata(endpoint) {
|
|
5307
|
+
const activeProtocolMetadata = endpoint.kind === "PushDiffusion"
|
|
5308
|
+
? toStringRecord(endpoint.pushDiffusion?.ConnectionProperties ?? endpoint.pushDiffusion?.connectionProperties)
|
|
5309
|
+
: endpoint.kind === "Grpc"
|
|
5310
|
+
? toStringRecord(endpoint.grpc?.ConnectionMetadata ?? endpoint.grpc?.connectionMetadata)
|
|
5311
|
+
: endpoint.kind === "WebSocket"
|
|
5312
|
+
? toStringRecord(endpoint.webSocket?.ConnectionMetadata ?? endpoint.webSocket?.connectionMetadata)
|
|
5313
|
+
: {};
|
|
4128
5314
|
return {
|
|
5315
|
+
...activeProtocolMetadata,
|
|
4129
5316
|
...(endpoint.connectionMetadata ?? {}),
|
|
4130
|
-
...(endpoint.delegate?.connectionMetadata ?? {})
|
|
4131
|
-
...toStringRecord(endpoint.pushDiffusion?.ConnectionProperties ?? endpoint.pushDiffusion?.connectionProperties),
|
|
4132
|
-
...toStringRecord(endpoint.grpc?.ConnectionMetadata ?? endpoint.grpc?.connectionMetadata),
|
|
4133
|
-
...toStringRecord(endpoint.webSocket?.ConnectionMetadata ?? endpoint.webSocket?.connectionMetadata)
|
|
5317
|
+
...(endpoint.delegate?.connectionMetadata ?? {})
|
|
4134
5318
|
};
|
|
4135
5319
|
}
|
|
4136
5320
|
function isStructuredProduceResult(value) {
|