@furious.luke/argus-js 0.5.4 → 0.5.5
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 +22 -7
- package/dist/index.cjs +163 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -4
- package/dist/index.d.ts +30 -4
- package/dist/index.js +163 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -102,6 +102,12 @@ type SignalMessage = {
|
|
|
102
102
|
turn_username?: string;
|
|
103
103
|
turn_credential?: string;
|
|
104
104
|
read_token?: string;
|
|
105
|
+
} | {
|
|
106
|
+
type: "placement_redirect";
|
|
107
|
+
gateway_url: string;
|
|
108
|
+
} | {
|
|
109
|
+
type: "unavailable";
|
|
110
|
+
retry_after_ms?: number;
|
|
105
111
|
};
|
|
106
112
|
/**
|
|
107
113
|
* Callbacks emitted by the Publisher during its lifecycle.
|
|
@@ -165,7 +171,12 @@ interface GatewayReadyInfo {
|
|
|
165
171
|
* Options for publishing a media stream.
|
|
166
172
|
*/
|
|
167
173
|
interface PublisherOptions {
|
|
168
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Gateway WebSocket URLs returned by POST /api/streams `gateway_urls`. All are
|
|
176
|
+
* opened at once; the region whose `accepted` returns first is selected on
|
|
177
|
+
* network path, and only that region places the stream. The rest are held as
|
|
178
|
+
* standbys the publisher fails over to.
|
|
179
|
+
*/
|
|
169
180
|
gatewayURLs: string[];
|
|
170
181
|
/** The short-lived join token from POST /api/streams. */
|
|
171
182
|
token: string;
|
|
@@ -187,11 +198,26 @@ interface PublisherOptions {
|
|
|
187
198
|
*/
|
|
188
199
|
turnTransportPolicy?: TurnTransportPolicy;
|
|
189
200
|
/**
|
|
190
|
-
* Overall deadline for the initial accepted/proceed/ready gateway race
|
|
191
|
-
*
|
|
192
|
-
*
|
|
201
|
+
* Overall deadline for the initial accepted/proceed/ready gateway race,
|
|
202
|
+
* spanning selection, placement, and any failovers. Sockets that have not sent
|
|
203
|
+
* `accepted` are replaced after three seconds within this deadline. Defaults to
|
|
204
|
+
* 20 seconds.
|
|
193
205
|
*/
|
|
194
206
|
gatewayHandshakeTimeoutMs?: number;
|
|
207
|
+
/**
|
|
208
|
+
* How long the selected region has to return `ready` before the publisher
|
|
209
|
+
* abandons it and fails over to the next-fastest acknowledgement. This is a
|
|
210
|
+
* backstop for a region whose socket stays open but silent (a hung placement);
|
|
211
|
+
* a socket that closes or errors fails over immediately regardless. It is
|
|
212
|
+
* deliberately generous — placement includes the selected region's own
|
|
213
|
+
* control-plane round-trips, and a short deadline would abandon exactly the
|
|
214
|
+
* user-close-but-control-plane-distant regions this race is meant to prefer.
|
|
215
|
+
* Defaults to 8 seconds and is capped at 20 seconds: the gateway reaps a
|
|
216
|
+
* standby socket that has been accepted but not yet told to proceed, so the
|
|
217
|
+
* failover window must stay below that deadline or standbys would disappear
|
|
218
|
+
* before the browser fails over to them.
|
|
219
|
+
*/
|
|
220
|
+
gatewayFailoverTimeoutMs?: number;
|
|
195
221
|
/** Deadline after the initial offer for WebRTC to reach connected. Defaults to 30 seconds. */
|
|
196
222
|
peerConnectionTimeoutMs?: number;
|
|
197
223
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -102,6 +102,12 @@ type SignalMessage = {
|
|
|
102
102
|
turn_username?: string;
|
|
103
103
|
turn_credential?: string;
|
|
104
104
|
read_token?: string;
|
|
105
|
+
} | {
|
|
106
|
+
type: "placement_redirect";
|
|
107
|
+
gateway_url: string;
|
|
108
|
+
} | {
|
|
109
|
+
type: "unavailable";
|
|
110
|
+
retry_after_ms?: number;
|
|
105
111
|
};
|
|
106
112
|
/**
|
|
107
113
|
* Callbacks emitted by the Publisher during its lifecycle.
|
|
@@ -165,7 +171,12 @@ interface GatewayReadyInfo {
|
|
|
165
171
|
* Options for publishing a media stream.
|
|
166
172
|
*/
|
|
167
173
|
interface PublisherOptions {
|
|
168
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Gateway WebSocket URLs returned by POST /api/streams `gateway_urls`. All are
|
|
176
|
+
* opened at once; the region whose `accepted` returns first is selected on
|
|
177
|
+
* network path, and only that region places the stream. The rest are held as
|
|
178
|
+
* standbys the publisher fails over to.
|
|
179
|
+
*/
|
|
169
180
|
gatewayURLs: string[];
|
|
170
181
|
/** The short-lived join token from POST /api/streams. */
|
|
171
182
|
token: string;
|
|
@@ -187,11 +198,26 @@ interface PublisherOptions {
|
|
|
187
198
|
*/
|
|
188
199
|
turnTransportPolicy?: TurnTransportPolicy;
|
|
189
200
|
/**
|
|
190
|
-
* Overall deadline for the initial accepted/proceed/ready gateway race
|
|
191
|
-
*
|
|
192
|
-
*
|
|
201
|
+
* Overall deadline for the initial accepted/proceed/ready gateway race,
|
|
202
|
+
* spanning selection, placement, and any failovers. Sockets that have not sent
|
|
203
|
+
* `accepted` are replaced after three seconds within this deadline. Defaults to
|
|
204
|
+
* 20 seconds.
|
|
193
205
|
*/
|
|
194
206
|
gatewayHandshakeTimeoutMs?: number;
|
|
207
|
+
/**
|
|
208
|
+
* How long the selected region has to return `ready` before the publisher
|
|
209
|
+
* abandons it and fails over to the next-fastest acknowledgement. This is a
|
|
210
|
+
* backstop for a region whose socket stays open but silent (a hung placement);
|
|
211
|
+
* a socket that closes or errors fails over immediately regardless. It is
|
|
212
|
+
* deliberately generous — placement includes the selected region's own
|
|
213
|
+
* control-plane round-trips, and a short deadline would abandon exactly the
|
|
214
|
+
* user-close-but-control-plane-distant regions this race is meant to prefer.
|
|
215
|
+
* Defaults to 8 seconds and is capped at 20 seconds: the gateway reaps a
|
|
216
|
+
* standby socket that has been accepted but not yet told to proceed, so the
|
|
217
|
+
* failover window must stay below that deadline or standbys would disappear
|
|
218
|
+
* before the browser fails over to them.
|
|
219
|
+
*/
|
|
220
|
+
gatewayFailoverTimeoutMs?: number;
|
|
195
221
|
/** Deadline after the initial offer for WebRTC to reach connected. Defaults to 30 seconds. */
|
|
196
222
|
peerConnectionTimeoutMs?: number;
|
|
197
223
|
/**
|
package/dist/index.js
CHANGED
|
@@ -69,6 +69,12 @@ var defaultSignalingReconnectTimeoutMs = 2e4;
|
|
|
69
69
|
var defaultGatewayHandshakeTimeoutMs = 2e4;
|
|
70
70
|
var defaultPeerConnectionTimeoutMs = 3e4;
|
|
71
71
|
var initialGatewayAttemptTimeoutMs = 3e3;
|
|
72
|
+
var defaultGatewayFailoverTimeoutMs = 8e3;
|
|
73
|
+
var maxGatewayFailoverTimeoutMs = 2e4;
|
|
74
|
+
var defaultGatewayRetryBackoffMs = 3e3;
|
|
75
|
+
var minGatewayRetryBackoffMs = 250;
|
|
76
|
+
var maxGatewayRetryBackoffMs = 5e3;
|
|
77
|
+
var maxPlacementRedirects = 2;
|
|
72
78
|
var signalingResumeAttemptTimeoutMs = 3e3;
|
|
73
79
|
var signalingResumeMaxBackoffMs = 3e3;
|
|
74
80
|
var senderRestartPauseMs = 100;
|
|
@@ -522,6 +528,16 @@ var Publisher = class {
|
|
|
522
528
|
// -------------------------------------------------------------------------
|
|
523
529
|
// Private helpers
|
|
524
530
|
// -------------------------------------------------------------------------
|
|
531
|
+
// raceGateways opens every candidate gateway at once, then decides in two
|
|
532
|
+
// separate moments. SELECTION: the first socket to deliver `accepted` (a cheap,
|
|
533
|
+
// control-plane-free acknowledgement) is chosen on network path; the browser
|
|
534
|
+
// sends `proceed` on that one only and keeps the rest as standbys. PLACEMENT:
|
|
535
|
+
// the selected region does its control-plane work and returns `ready`. If the
|
|
536
|
+
// selection dies (socket close/error → immediately) or stalls past the failover
|
|
537
|
+
// deadline (a hung-but-open socket), the browser abandons it — closing the
|
|
538
|
+
// socket cancels that region's placement server-side — and selects the
|
|
539
|
+
// next-fastest standby. A `placement_redirect` points the browser at the region
|
|
540
|
+
// that already holds the stream so a mistimed failover self-heals.
|
|
525
541
|
raceGateways(signal) {
|
|
526
542
|
return new Promise((resolve, reject) => {
|
|
527
543
|
const { gatewayURLs, token } = this.opts;
|
|
@@ -531,43 +547,149 @@ var Publisher = class {
|
|
|
531
547
|
}
|
|
532
548
|
const sockets = [];
|
|
533
549
|
const attemptTimers = /* @__PURE__ */ new Map();
|
|
550
|
+
const reopenTimers = /* @__PURE__ */ new Set();
|
|
551
|
+
const standbys = [];
|
|
552
|
+
let selected = null;
|
|
553
|
+
let failoverTimer = null;
|
|
554
|
+
let redirects = 0;
|
|
534
555
|
let settled = false;
|
|
535
556
|
let timeoutTimer = null;
|
|
557
|
+
const failoverMs = Math.min(
|
|
558
|
+
maxGatewayFailoverTimeoutMs,
|
|
559
|
+
Math.max(0, this.opts.gatewayFailoverTimeoutMs ?? defaultGatewayFailoverTimeoutMs)
|
|
560
|
+
);
|
|
536
561
|
const clearTimeoutTimer = () => {
|
|
537
562
|
if (timeoutTimer !== null) clearTimeout(timeoutTimer);
|
|
538
563
|
timeoutTimer = null;
|
|
539
564
|
};
|
|
565
|
+
const clearFailoverTimer = () => {
|
|
566
|
+
if (failoverTimer !== null) clearTimeout(failoverTimer);
|
|
567
|
+
failoverTimer = null;
|
|
568
|
+
};
|
|
569
|
+
const clearReopenTimers = () => {
|
|
570
|
+
for (const timer of reopenTimers) clearTimeout(timer);
|
|
571
|
+
reopenTimers.clear();
|
|
572
|
+
};
|
|
540
573
|
const clearAttemptTimer = (socket) => {
|
|
541
574
|
const timer = attemptTimers.get(socket);
|
|
542
575
|
if (timer !== void 0) clearTimeout(timer);
|
|
543
576
|
attemptTimers.delete(socket);
|
|
544
577
|
};
|
|
578
|
+
const detach = (socket) => {
|
|
579
|
+
clearAttemptTimer(socket);
|
|
580
|
+
socket.onmessage = null;
|
|
581
|
+
socket.onerror = null;
|
|
582
|
+
socket.onclose = null;
|
|
583
|
+
};
|
|
584
|
+
const dropStandby = (socket) => {
|
|
585
|
+
const i = standbys.indexOf(socket);
|
|
586
|
+
if (i !== -1) standbys.splice(i, 1);
|
|
587
|
+
};
|
|
545
588
|
const closeAll = (except) => {
|
|
546
589
|
for (const s of sockets) {
|
|
547
|
-
clearAttemptTimer(s);
|
|
548
590
|
if (s !== except) {
|
|
549
|
-
s
|
|
550
|
-
s.onerror = null;
|
|
551
|
-
s.onclose = null;
|
|
591
|
+
detach(s);
|
|
552
592
|
s.close();
|
|
553
593
|
}
|
|
554
594
|
}
|
|
555
595
|
};
|
|
556
|
-
const
|
|
557
|
-
|
|
596
|
+
const win = (ws, readyInfo, gatewayURL) => {
|
|
597
|
+
settled = true;
|
|
598
|
+
clearTimeoutTimer();
|
|
599
|
+
clearFailoverTimer();
|
|
600
|
+
clearReopenTimers();
|
|
601
|
+
signal.removeEventListener("abort", abort);
|
|
602
|
+
closeAll(ws);
|
|
603
|
+
resolve({ ws, readyInfo, gatewayURL });
|
|
604
|
+
};
|
|
605
|
+
const fail = (err) => {
|
|
606
|
+
settled = true;
|
|
607
|
+
clearTimeoutTimer();
|
|
608
|
+
clearFailoverTimer();
|
|
609
|
+
clearReopenTimers();
|
|
610
|
+
signal.removeEventListener("abort", abort);
|
|
611
|
+
closeAll();
|
|
612
|
+
reject(err);
|
|
613
|
+
};
|
|
614
|
+
const checkExhausted = () => {
|
|
615
|
+
if (settled || selected !== null || standbys.length > 0 || reopenTimers.size > 0) return;
|
|
558
616
|
if (sockets.every((s) => s.readyState === WebSocket.CLOSED || s.readyState === WebSocket.CLOSING)) {
|
|
559
|
-
|
|
560
|
-
clearTimeoutTimer();
|
|
561
|
-
signal.removeEventListener("abort", abort);
|
|
562
|
-
reject(new Error("all gateways failed to connect"));
|
|
617
|
+
fail(new Error("all gateways failed to connect"));
|
|
563
618
|
}
|
|
564
619
|
};
|
|
565
|
-
const
|
|
620
|
+
const select = (ws) => {
|
|
621
|
+
selected = ws;
|
|
622
|
+
dropStandby(ws);
|
|
623
|
+
try {
|
|
624
|
+
ws.send(JSON.stringify({ type: "proceed" }));
|
|
625
|
+
} catch {
|
|
626
|
+
socketDown(ws);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
clearFailoverTimer();
|
|
630
|
+
failoverTimer = setTimeout(() => failover(ws), failoverMs);
|
|
631
|
+
};
|
|
632
|
+
const failover = (deadSocket) => {
|
|
633
|
+
if (settled || deadSocket !== selected) return;
|
|
634
|
+
clearFailoverTimer();
|
|
635
|
+
detach(deadSocket);
|
|
636
|
+
deadSocket.close();
|
|
637
|
+
selected = null;
|
|
638
|
+
const next = standbys.shift();
|
|
639
|
+
if (next) {
|
|
640
|
+
select(next);
|
|
641
|
+
} else {
|
|
642
|
+
checkExhausted();
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
const redirect = (gatewayURL) => {
|
|
566
646
|
if (settled) return;
|
|
567
|
-
|
|
568
|
-
|
|
647
|
+
if (redirects >= maxPlacementRedirects) {
|
|
648
|
+
fail(new Error("too many placement redirects"));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
redirects++;
|
|
652
|
+
clearFailoverTimer();
|
|
569
653
|
closeAll();
|
|
570
|
-
|
|
654
|
+
standbys.length = 0;
|
|
655
|
+
selected = null;
|
|
656
|
+
try {
|
|
657
|
+
openGateway(gatewayURL);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
const retryUnavailable = (ws, gatewayURL, retryAfterMs) => {
|
|
663
|
+
detach(ws);
|
|
664
|
+
ws.close();
|
|
665
|
+
const delay = Math.min(
|
|
666
|
+
maxGatewayRetryBackoffMs,
|
|
667
|
+
Math.max(minGatewayRetryBackoffMs, retryAfterMs ?? defaultGatewayRetryBackoffMs)
|
|
668
|
+
);
|
|
669
|
+
const timer = setTimeout(() => {
|
|
670
|
+
reopenTimers.delete(timer);
|
|
671
|
+
if (settled) return;
|
|
672
|
+
try {
|
|
673
|
+
openGateway(gatewayURL);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
676
|
+
}
|
|
677
|
+
}, delay);
|
|
678
|
+
reopenTimers.add(timer);
|
|
679
|
+
};
|
|
680
|
+
const socketDown = (ws) => {
|
|
681
|
+
if (settled) return;
|
|
682
|
+
clearAttemptTimer(ws);
|
|
683
|
+
dropStandby(ws);
|
|
684
|
+
if (ws === selected) {
|
|
685
|
+
failover(ws);
|
|
686
|
+
} else {
|
|
687
|
+
checkExhausted();
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const abort = () => {
|
|
691
|
+
if (settled) return;
|
|
692
|
+
fail(new PublisherStoppedError("publisher stopped"));
|
|
571
693
|
};
|
|
572
694
|
if (signal.aborted) {
|
|
573
695
|
abort();
|
|
@@ -580,10 +702,7 @@ var Publisher = class {
|
|
|
580
702
|
);
|
|
581
703
|
timeoutTimer = setTimeout(() => {
|
|
582
704
|
if (settled) return;
|
|
583
|
-
|
|
584
|
-
signal.removeEventListener("abort", abort);
|
|
585
|
-
closeAll();
|
|
586
|
-
reject(new Error(`gateway handshake timed out after ${timeoutMs}ms`));
|
|
705
|
+
fail(new Error(`gateway handshake timed out after ${timeoutMs}ms`));
|
|
587
706
|
}, timeoutMs);
|
|
588
707
|
const openGateway = (gatewayURL) => {
|
|
589
708
|
if (settled) return;
|
|
@@ -595,58 +714,51 @@ var Publisher = class {
|
|
|
595
714
|
const attemptTimer = setTimeout(() => {
|
|
596
715
|
attemptTimers.delete(ws);
|
|
597
716
|
if (settled || accepted) return;
|
|
598
|
-
ws
|
|
599
|
-
ws.onerror = null;
|
|
600
|
-
ws.onclose = null;
|
|
717
|
+
detach(ws);
|
|
601
718
|
ws.close();
|
|
602
719
|
try {
|
|
603
720
|
openGateway(gatewayURL);
|
|
604
721
|
} catch (err) {
|
|
605
|
-
|
|
606
|
-
clearTimeoutTimer();
|
|
607
|
-
signal.removeEventListener("abort", abort);
|
|
608
|
-
closeAll();
|
|
609
|
-
reject(err);
|
|
722
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
610
723
|
}
|
|
611
724
|
}, initialGatewayAttemptTimeoutMs);
|
|
612
725
|
attemptTimers.set(ws, attemptTimer);
|
|
613
726
|
ws.onmessage = (ev) => {
|
|
614
727
|
if (settled) return;
|
|
728
|
+
let msg;
|
|
615
729
|
try {
|
|
616
|
-
|
|
617
|
-
if (!accepted && msg.type === "accepted") {
|
|
618
|
-
accepted = true;
|
|
619
|
-
clearAttemptTimer(ws);
|
|
620
|
-
ws.send(JSON.stringify({ type: "proceed" }));
|
|
621
|
-
} else if (accepted && msg.type === "ready") {
|
|
622
|
-
settled = true;
|
|
623
|
-
clearTimeoutTimer();
|
|
624
|
-
signal.removeEventListener("abort", abort);
|
|
625
|
-
closeAll(ws);
|
|
626
|
-
resolve({ ws, readyInfo: msg, gatewayURL });
|
|
627
|
-
}
|
|
730
|
+
msg = JSON.parse(ev.data);
|
|
628
731
|
} catch {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (!accepted) {
|
|
735
|
+
if (msg.type === "unavailable") {
|
|
736
|
+
retryUnavailable(ws, gatewayURL, msg.retry_after_ms);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (msg.type !== "accepted") return;
|
|
740
|
+
accepted = true;
|
|
741
|
+
clearAttemptTimer(ws);
|
|
742
|
+
if (selected === null) select(ws);
|
|
743
|
+
else standbys.push(ws);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (ws !== selected) return;
|
|
747
|
+
if (msg.type === "ready") {
|
|
748
|
+
win(ws, msg, gatewayURL);
|
|
749
|
+
} else if (msg.type === "placement_redirect" && msg.gateway_url) {
|
|
750
|
+
redirect(msg.gateway_url);
|
|
629
751
|
}
|
|
630
752
|
};
|
|
631
|
-
ws.onerror = () =>
|
|
632
|
-
|
|
633
|
-
checkAllFailed();
|
|
634
|
-
};
|
|
635
|
-
ws.onclose = () => {
|
|
636
|
-
clearAttemptTimer(ws);
|
|
637
|
-
checkAllFailed();
|
|
638
|
-
};
|
|
753
|
+
ws.onerror = () => socketDown(ws);
|
|
754
|
+
ws.onclose = () => socketDown(ws);
|
|
639
755
|
};
|
|
640
756
|
try {
|
|
641
757
|
for (const gatewayURL of gatewayURLs) {
|
|
642
758
|
openGateway(gatewayURL);
|
|
643
759
|
}
|
|
644
760
|
} catch (err) {
|
|
645
|
-
|
|
646
|
-
clearTimeoutTimer();
|
|
647
|
-
signal.removeEventListener("abort", abort);
|
|
648
|
-
closeAll();
|
|
649
|
-
reject(err);
|
|
761
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
650
762
|
}
|
|
651
763
|
});
|
|
652
764
|
}
|