@q9labsai/chalk-client 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/grant.d.ts +2 -0
- package/dist/{chunk-GOSWSXTH.js → chunk-MHJ4UPM3.js} +162 -5
- package/dist/chunk-N4IPKRS6.js +76 -0
- package/dist/{chunk-DHUHTIU7.js → chunk-TA2BJXI7.js} +243 -45
- package/dist/{chunk-3TGVTP6Z.js → chunk-UVWRTXNO.js} +894 -72
- package/dist/{chunk-O26BHHMI.js → chunk-VAJ6XNAA.js} +1 -1
- package/dist/{chunk-XSB2GDRW.js → chunk-XF567GPH.js} +198 -26
- package/dist/client.d.ts +180 -0
- package/dist/connection/dependencies.d.ts +5 -0
- package/dist/connection/production.d.ts +1 -1
- package/dist/effect.js +29 -4
- package/dist/generated/http-api.d.ts +178 -0
- package/dist/generated/schemas.d.ts +287 -0
- package/dist/generated/sync.d.ts +53 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +33 -15
- package/dist/media/call-stats.d.ts +27 -0
- package/dist/media/client.d.ts +1 -0
- package/dist/media/ice-servers.d.ts +3 -0
- package/dist/media/index.d.ts +2 -0
- package/dist/media/index.js +4 -1
- package/dist/media/plane.d.ts +1 -0
- package/dist/media/receive-stall.d.ts +11 -2
- package/dist/media/types.d.ts +19 -0
- package/dist/media/video-render-size.d.ts +7 -0
- package/dist/media/viewer-layer.d.ts +48 -0
- package/dist/public-invites/index.js +3 -2
- package/dist/server/index.cjs +18 -4
- package/dist/server/index.js +18 -4
- package/dist/session/index.d.ts +18 -0
- package/dist/sync/index.js +2 -1
- package/dist/sync/link-watch.d.ts +67 -0
- package/dist/sync/v1-client.d.ts +5 -0
- package/dist/sync/v1-command-scheduler.d.ts +4 -0
- package/dist/sync/v1-types.d.ts +12 -1
- package/package.json +2 -1
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sessionBaggage
|
|
3
|
+
} from "./chunk-N4IPKRS6.js";
|
|
4
|
+
|
|
1
5
|
// src/sync/canonical.ts
|
|
2
6
|
var encoder = new TextEncoder();
|
|
3
7
|
function canonicalJson(value) {
|
|
@@ -297,6 +301,73 @@ function scheduleSyncDialDeadline(clock, onTimeout, timeoutMs = SYNC_DIAL_TIMEOU
|
|
|
297
301
|
return () => clock.clearTimeout(timer);
|
|
298
302
|
}
|
|
299
303
|
|
|
304
|
+
// src/sync/link-watch.ts
|
|
305
|
+
var LONG_DROP_MS = 6e4;
|
|
306
|
+
var ROUND_TRIP_MS = 5e3;
|
|
307
|
+
var UNANSWERED_LIMIT_MS = 6e4;
|
|
308
|
+
var LinkWatch = class {
|
|
309
|
+
#clock;
|
|
310
|
+
#page;
|
|
311
|
+
#suspends = 0;
|
|
312
|
+
#pings = [];
|
|
313
|
+
#proof = null;
|
|
314
|
+
#suspended = () => {
|
|
315
|
+
this.#suspends += 1;
|
|
316
|
+
};
|
|
317
|
+
constructor(clock, page) {
|
|
318
|
+
this.#clock = clock;
|
|
319
|
+
this.#page = page;
|
|
320
|
+
}
|
|
321
|
+
/** Listens for page suspension. Idempotent, and paired with `close`. */
|
|
322
|
+
open() {
|
|
323
|
+
this.close();
|
|
324
|
+
this.#page?.addEventListener("freeze", this.#suspended);
|
|
325
|
+
this.#page?.addEventListener("resume", this.#suspended);
|
|
326
|
+
}
|
|
327
|
+
close() {
|
|
328
|
+
this.#page?.removeEventListener("freeze", this.#suspended);
|
|
329
|
+
this.#page?.removeEventListener("resume", this.#suspended);
|
|
330
|
+
}
|
|
331
|
+
/** Call when a connection goes live: the previous socket's pings will not be answered. */
|
|
332
|
+
restart() {
|
|
333
|
+
this.#pings = [];
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Call as a ping goes out, in the same order as the pings. False means the oldest ping has gone unanswered for over
|
|
337
|
+
* `UNANSWERED_LIMIT_MS`: the socket is dead and this ping must not be sent on it.
|
|
338
|
+
*/
|
|
339
|
+
pinged() {
|
|
340
|
+
const now = this.#read();
|
|
341
|
+
const oldest = this.#pings[0];
|
|
342
|
+
if (oldest !== void 0 && (now.wall - oldest.wall > UNANSWERED_LIMIT_MS || now.mono - oldest.mono > UNANSWERED_LIMIT_MS)) return false;
|
|
343
|
+
this.#pings.push(now);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
/** Call as a pong is received. It answers the oldest ping in flight, and proves contact only if the round trip was short. */
|
|
347
|
+
ponged() {
|
|
348
|
+
const sent = this.#pings.shift();
|
|
349
|
+
if (sent === void 0) return;
|
|
350
|
+
const now = this.#read();
|
|
351
|
+
const wall = now.wall - sent.wall;
|
|
352
|
+
const mono = now.mono - sent.mono;
|
|
353
|
+
if (wall < 0 || wall > ROUND_TRIP_MS || mono < 0 || mono > ROUND_TRIP_MS || now.suspends !== sent.suspends) return;
|
|
354
|
+
this.#proof = sent;
|
|
355
|
+
}
|
|
356
|
+
proof() {
|
|
357
|
+
return this.#proof;
|
|
358
|
+
}
|
|
359
|
+
longSince(proof) {
|
|
360
|
+
if (proof === null) return true;
|
|
361
|
+
const now = this.#read();
|
|
362
|
+
const wall = now.wall - proof.wall;
|
|
363
|
+
const mono = now.mono - proof.mono;
|
|
364
|
+
return wall < 0 || mono < 0 || wall >= LONG_DROP_MS || mono >= LONG_DROP_MS || now.suspends !== proof.suspends;
|
|
365
|
+
}
|
|
366
|
+
#read() {
|
|
367
|
+
return { wall: this.#clock.wall(), mono: this.#clock.mono(), suspends: this.#suspends + (this.#page?.wasDiscarded === true ? 1 : 0) };
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
300
371
|
// src/generated/sync.ts
|
|
301
372
|
import { Schema } from "effect";
|
|
302
373
|
var SyncProtocolMetadata = {
|
|
@@ -405,11 +476,20 @@ var SyncProtocolMetadata = {
|
|
|
405
476
|
pattern: "^[a-z][a-z0-9_*\\-/]{0,255}=[\\x21-\\x2b\\x2d-\\x3c\\x3e-\\x7e]{1,256}(,[a-z][a-z0-9_*\\-/]{0,255}=[\\x21-\\x2b\\x2d-\\x3c\\x3e-\\x7e]{1,256})*$"
|
|
406
477
|
}
|
|
407
478
|
},
|
|
479
|
+
helloOnlyFields: {
|
|
480
|
+
baggage: {
|
|
481
|
+
kind: "string",
|
|
482
|
+
minBytes: 47,
|
|
483
|
+
maxBytes: 47,
|
|
484
|
+
format: "w3c-baggage-session-id",
|
|
485
|
+
pattern: "^session\\.id=[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
486
|
+
}
|
|
487
|
+
},
|
|
408
488
|
acceptedOn: {
|
|
409
489
|
hello: "client-to-server",
|
|
410
490
|
serverFrames: "server-to-client"
|
|
411
491
|
},
|
|
412
|
-
upgradeHeaders: ["x-chalk-journey-id", "traceparent", "tracestate"],
|
|
492
|
+
upgradeHeaders: ["x-chalk-journey-id", "traceparent", "tracestate", "baggage"],
|
|
413
493
|
rule: "propagate_from_first_observed_layer_to_every_downstream_frame"
|
|
414
494
|
},
|
|
415
495
|
externalIntents: [
|
|
@@ -712,7 +792,9 @@ function strictCommand(value) {
|
|
|
712
792
|
function strictCollaborationsHelloExtension(value) {
|
|
713
793
|
return exactObject(value, ["name", "chat_cursor"]) && ["collaboration_v1", "collaboration_v2"].includes(String(value.name)) && exactObject(value.chat_cursor, ["after_sequence", "retained_floor_sequence"]);
|
|
714
794
|
}
|
|
715
|
-
function strictHello(
|
|
795
|
+
function strictHello(hello) {
|
|
796
|
+
const { baggage, ...value } = hello;
|
|
797
|
+
if (baggage !== void 0 && !(typeof baggage === "string" && boundedCorrelationString(baggage, 47, 47) && /^session\.id=[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(baggage))) return false;
|
|
716
798
|
const base = exactObjectWithCorrelation(value, ["type", "protocol", "token", "streams"]);
|
|
717
799
|
const extended = exactObjectWithCorrelation(value, ["type", "protocol", "token", "streams", "extensions"]) && Array.isArray(value.extensions) && value.extensions.length === 1 && strictCollaborationsHelloExtension(value.extensions[0]);
|
|
718
800
|
if (!base && !extended || !exactObject(value.streams, ["control", "media", "presence", "requests"])) return false;
|
|
@@ -859,8 +941,13 @@ function strictEvent(value) {
|
|
|
859
941
|
return false;
|
|
860
942
|
}
|
|
861
943
|
}
|
|
944
|
+
function validMediaOffReason(item) {
|
|
945
|
+
if (!Object.hasOwn(item, "off_reason")) return !Object.hasOwn(item, "off_revision");
|
|
946
|
+
if (item.off_reason === "host") return Number.isInteger(item.off_revision) && item.off_revision > 0;
|
|
947
|
+
return item.enabled === false && (item.off_reason === "self" || item.off_reason === "dead") && !Object.hasOwn(item, "off_revision");
|
|
948
|
+
}
|
|
862
949
|
function strictProjectionItem(value, stream) {
|
|
863
|
-
if (stream === "media") return exactObject(value, ["participant_id", "source", "enabled", "publication_id"]);
|
|
950
|
+
if (stream === "media") return isObject(value) && exactObject(value, [...["participant_id", "source", "enabled", "publication_id"], ...Object.hasOwn(value, "off_reason") ? ["off_reason"] : [], ...Object.hasOwn(value, "off_revision") ? ["off_revision"] : []]) && validMediaOffReason(value);
|
|
864
951
|
if (stream === "presence")
|
|
865
952
|
return isObject(value) && exactObject(value, [...["participant_id", "state", "speaking", "active_speaker"], ...Object.hasOwn(value, "identity_id") ? ["identity_id"] : [], ...Object.hasOwn(value, "device_type") ? ["device_type"] : [], ...Object.hasOwn(value, "admission_revision") ? ["admission_revision"] : []]);
|
|
866
953
|
return false;
|
|
@@ -874,10 +961,12 @@ function strictWelcome(value) {
|
|
|
874
961
|
const extension = Array.isArray(value.extensions) && value.extensions.length === 1 ? value.extensions[0] : void 0;
|
|
875
962
|
const extensionValid = isObject(extension) && ["collaboration_v1", "collaboration_v2"].includes(String(extension.name)) && exactObject(extension, ["name", "capabilities", "participant_capabilities", "chat_head_sequence", "retained_floor_sequence", "read_receipts"]) && Array.isArray(extension.read_receipts);
|
|
876
963
|
if (Object.hasOwn(value, "entry_confirmation") && value.entry_confirmation !== true) return false;
|
|
964
|
+
if (Object.hasOwn(value, "media_off_reason") && value.media_off_reason !== true) return false;
|
|
877
965
|
const fields = [
|
|
878
966
|
...base,
|
|
879
967
|
...extensionValid ? ["extensions"] : [],
|
|
880
968
|
...Object.hasOwn(value, "entry_confirmation") ? ["entry_confirmation"] : [],
|
|
969
|
+
...Object.hasOwn(value, "media_off_reason") ? ["media_off_reason"] : [],
|
|
881
970
|
...Object.hasOwn(value, "entry_generation") ? ["entry_generation"] : [],
|
|
882
971
|
...Object.hasOwn(value, "participant_identity_id") ? ["participant_identity_id"] : []
|
|
883
972
|
];
|
|
@@ -957,9 +1046,10 @@ var TraceparentSchema = boundedUtf8String(55, 55).check(
|
|
|
957
1046
|
var TracestateSchema = boundedUtf8String(512, 1).check(
|
|
958
1047
|
Schema.makeFilter((value) => new RegExp("^[a-z][a-z0-9_*\\-/]{0,255}=[\\x21-\\x2b\\x2d-\\x3c\\x3e-\\x7e]{1,256}(,[a-z][a-z0-9_*\\-/]{0,255}=[\\x21-\\x2b\\x2d-\\x3c\\x3e-\\x7e]{1,256})*$", "u").test(value) ? void 0 : { path: [], issue: "must be a valid bounded W3C tracestate" })
|
|
959
1048
|
);
|
|
1049
|
+
var BaggageSchema = boundedUtf8String(47, 47).check(Schema.makeFilter((value) => /^session\.id=[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value) ? void 0 : { path: [], issue: "must be the single W3C baggage member session.id=<lowercase UUID>" }));
|
|
960
1050
|
var SyncCorrelationFieldsSchema = Schema.Struct({ journey_id: Schema.optional(JourneyIdSchema), traceparent: Schema.optional(TraceparentSchema), tracestate: Schema.optional(TracestateSchema) });
|
|
961
|
-
var
|
|
962
|
-
var
|
|
1051
|
+
var ServerCorrelationFields = { journey_id: Schema.optional(JourneyIdSchema), traceparent: Schema.optional(TraceparentSchema), tracestate: Schema.optional(TracestateSchema) };
|
|
1052
|
+
var HelloCorrelationFields = { ...ServerCorrelationFields, baggage: Schema.optional(BaggageSchema) };
|
|
963
1053
|
var StateDigestSchema = Schema.String.check(Schema.makeFilter((value) => /^[0-9a-f]{64}$/u.test(value) ? void 0 : { path: [], issue: "must be a lowercase hexadecimal SHA-256 digest" }));
|
|
964
1054
|
var UnsignedDecimalSchema = Schema.String.check(Schema.makeFilter((value) => /^(?:0|[1-9][0-9]*)$/u.test(value) ? void 0 : { path: [], issue: "must be an unsigned decimal integer" }));
|
|
965
1055
|
var CommandIdSchema = boundedUtf8String(64).check(Schema.makeFilter((value) => textEncoder.encode(value).byteLength >= 16 ? void 0 : { path: [], issue: "must meet the command ID minimum byte length" }));
|
|
@@ -1490,6 +1580,7 @@ var WelcomeBase = {
|
|
|
1490
1580
|
recovery_id: UuidSchema,
|
|
1491
1581
|
head: ControlCursorSchema,
|
|
1492
1582
|
entry_confirmation: Schema.optionalKey(Schema.Literal(true)),
|
|
1583
|
+
media_off_reason: Schema.optionalKey(Schema.Literal(true)),
|
|
1493
1584
|
entry_generation: Schema.optionalKey(PositiveIntegerSchema.check(Schema.isLessThanOrEqualTo(9007199254740991)))
|
|
1494
1585
|
};
|
|
1495
1586
|
var ExtendedWelcomeBase = { ...WelcomeBase, extensions: Schema.Tuple([CollaborationWelcomeExtensionSchema]) };
|
|
@@ -1532,9 +1623,14 @@ var CommittedAckFrameSchema = Schema.Struct({ type: Schema.Literal("ack"), comma
|
|
|
1532
1623
|
var SatisfiedAckFrameSchema = Schema.Struct({ type: Schema.Literal("ack"), command_id: CommandIdSchema, delivery: AckDeliverySchema, outcome: Schema.Literal("satisfied"), revision: NonNegativeIntegerSchema, state_digest: StateDigestSchema, ...ServerCorrelationFields });
|
|
1533
1624
|
var RejectedAckFrameSchema = Schema.Struct({ type: Schema.Literal("ack"), command_id: CommandIdSchema, delivery: AckDeliverySchema, outcome: Schema.Literal("rejected"), reason: RejectionReasonSchema, ...ServerCorrelationFields });
|
|
1534
1625
|
var CommandIdConflictAckFrameSchema = Schema.Struct({ type: Schema.Literal("ack"), command_id: CommandIdSchema, delivery: AckDeliverySchema, outcome: Schema.Literal("command_id_conflict"), reason: Schema.Literal("command_id_conflict"), ...ServerCorrelationFields });
|
|
1535
|
-
var MediaProjectionItemSchema = Schema.Struct({
|
|
1536
|
-
|
|
1537
|
-
)
|
|
1626
|
+
var MediaProjectionItemSchema = Schema.Struct({
|
|
1627
|
+
participant_id: UuidSchema,
|
|
1628
|
+
source: Schema.Union([Schema.Literal("microphone"), Schema.Literal("camera"), Schema.Literal("screen")]),
|
|
1629
|
+
enabled: Schema.Boolean,
|
|
1630
|
+
publication_id: Schema.NullOr(boundedUtf8String(256, 1)),
|
|
1631
|
+
off_reason: Schema.optional(Schema.Literals(["host", "self", "dead"])),
|
|
1632
|
+
off_revision: Schema.optional(PositiveIntegerSchema)
|
|
1633
|
+
}).check(Schema.makeFilter((item) => !item.enabled || item.publication_id !== null ? void 0 : { path: [], issue: "enabled requires a publication_id" }));
|
|
1538
1634
|
var PresenceProjectionItemSchema = Schema.Struct({
|
|
1539
1635
|
participant_id: UuidSchema,
|
|
1540
1636
|
state: Schema.Union([Schema.Literal("connected"), Schema.Literal("disconnected")]),
|
|
@@ -2528,6 +2624,11 @@ var V1CommandScheduler = class {
|
|
|
2528
2624
|
async acknowledge(frame) {
|
|
2529
2625
|
const deferred = this.#commands.get(frame.command_id);
|
|
2530
2626
|
if (!deferred) return;
|
|
2627
|
+
if (isSatisfiedByInactiveParticipant(frame, deferred.frame)) {
|
|
2628
|
+
const head = this.#options.controlHead();
|
|
2629
|
+
await this.#finishCommand(frame.command_id, { type: "ack", command_id: frame.command_id, delivery: "duplicate", outcome: "satisfied", revision: head.revision, state_digest: head.state_digest });
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2531
2632
|
if (frame.outcome === "rejected" || frame.outcome === "command_id_conflict") {
|
|
2532
2633
|
await this.#finishCommand(frame.command_id, frame);
|
|
2533
2634
|
return;
|
|
@@ -2566,15 +2667,12 @@ var V1CommandScheduler = class {
|
|
|
2566
2667
|
}
|
|
2567
2668
|
}
|
|
2568
2669
|
async settleTerminalRecovery(frame) {
|
|
2569
|
-
const
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
} else if (frame.reason === "participant_inactive") {
|
|
2574
|
-
satisfiedOperations.add("participant_leave");
|
|
2575
|
-
}
|
|
2670
|
+
const satisfies = (operation) => {
|
|
2671
|
+
if (frame.reason === "episode_ended") return operation === "end_episode" || operation === "participant_leave";
|
|
2672
|
+
return frame.reason === "participant_inactive" && inactiveParticipantSatisfies(operation);
|
|
2673
|
+
};
|
|
2576
2674
|
for (const [commandId, deferred] of this.#commands) {
|
|
2577
|
-
if (deferred.frame.type !== "operation" || !
|
|
2675
|
+
if (deferred.frame.type !== "operation" || !satisfies(deferred.frame.name)) continue;
|
|
2578
2676
|
await this.#finishCommand(commandId, {
|
|
2579
2677
|
type: "ack",
|
|
2580
2678
|
command_id: commandId,
|
|
@@ -2744,6 +2842,12 @@ function targetFrame(commandId, command) {
|
|
|
2744
2842
|
return { type: "command", command_id: commandId, name: command.name, payload: command.payload };
|
|
2745
2843
|
}
|
|
2746
2844
|
}
|
|
2845
|
+
function inactiveParticipantSatisfies(operation) {
|
|
2846
|
+
return operation === "participant_leave";
|
|
2847
|
+
}
|
|
2848
|
+
function isSatisfiedByInactiveParticipant(result, command) {
|
|
2849
|
+
return result.outcome === "rejected" && result.reason === "participant_inactive" && command.type === "operation" && inactiveParticipantSatisfies(command.name);
|
|
2850
|
+
}
|
|
2747
2851
|
function isTerminalLifecycleOperation(frame) {
|
|
2748
2852
|
return frame.type === "operation" && (frame.name === "participant_leave" || frame.name === "end_episode");
|
|
2749
2853
|
}
|
|
@@ -3002,6 +3106,7 @@ var MAX_REPLAY_EVENTS = SyncProtocolLimits.completeReplayMaxEvents;
|
|
|
3002
3106
|
var MAX_REPLAY_BYTES = SyncProtocolLimits.completeReplayEncodedBytes;
|
|
3003
3107
|
var MAX_PROJECTION_EVENT_EVIDENCE = 256;
|
|
3004
3108
|
var CLIENT_RESTART_CLOSE_CODE = 4e3;
|
|
3109
|
+
var legacyHelloServers = /* @__PURE__ */ new Set();
|
|
3005
3110
|
var DEFAULT_RECONNECT_DELAY_MS = 250;
|
|
3006
3111
|
var MAX_RECONNECT_DELAY_MS = 5e3;
|
|
3007
3112
|
var FAST_RECONNECT_ATTEMPTS = 8;
|
|
@@ -3046,12 +3151,19 @@ var V1SyncClient = class {
|
|
|
3046
3151
|
#socketCloses = [];
|
|
3047
3152
|
#omittedSocketCloses = 0;
|
|
3048
3153
|
#lastHeartbeatReplyAt = null;
|
|
3154
|
+
/** Whether this connection's welcome advertised `media_off_reason`; reset for each connection. */
|
|
3155
|
+
#offReasonNegotiated = false;
|
|
3156
|
+
#watch;
|
|
3157
|
+
/** Tells a recovery whether the drop was proven short. */
|
|
3158
|
+
link;
|
|
3049
3159
|
#opened = false;
|
|
3050
3160
|
#connectFailure;
|
|
3051
3161
|
#connectDiagnostic() {
|
|
3052
3162
|
return { stage: this.#opened ? "hello" : "upgrade", opened: this.#opened, closeCode: null, closeReason: null, durationMs: Math.max(0, this.#clock().now() - (this.#connectStartedAt ?? this.#clock().now())), attempt: this.#connectAttemptCount };
|
|
3053
3163
|
}
|
|
3054
3164
|
#reconnectAttempt = 0;
|
|
3165
|
+
/** The socket whose hello carried baggage and has not been answered by a welcome yet. */
|
|
3166
|
+
#baggageHelloSocket = null;
|
|
3055
3167
|
#episodeEndTimer;
|
|
3056
3168
|
#heartbeatTimer;
|
|
3057
3169
|
#missedHeartbeats = 0;
|
|
@@ -3070,6 +3182,8 @@ var V1SyncClient = class {
|
|
|
3070
3182
|
constructor(options) {
|
|
3071
3183
|
assertV1Url(options.url);
|
|
3072
3184
|
this.#options = options;
|
|
3185
|
+
this.#watch = new LinkWatch({ wall: () => this.#now(), mono: options.monotonic ?? (() => performance.now()) }, options.page === void 0 ? typeof document === "object" ? document : null : options.page);
|
|
3186
|
+
this.link = this.#watch;
|
|
3073
3187
|
this.#commandScheduler = new V1CommandScheduler({
|
|
3074
3188
|
store: options.pendingStore ?? new InMemoryV1PendingTargetStore(),
|
|
3075
3189
|
ids: options.ids,
|
|
@@ -3082,7 +3196,11 @@ var V1SyncClient = class {
|
|
|
3082
3196
|
isStarted: () => this.#started,
|
|
3083
3197
|
sendIfLive: (frame) => this.#sendIfLive(frame),
|
|
3084
3198
|
stateChanged: () => this.#emit(),
|
|
3085
|
-
ackHeadIsProven: (ack) => this.#ackHeadIsProven(ack)
|
|
3199
|
+
ackHeadIsProven: (ack) => this.#ackHeadIsProven(ack),
|
|
3200
|
+
controlHead: () => {
|
|
3201
|
+
const control = this.#requireControl();
|
|
3202
|
+
return { revision: control.revision, state_digest: control.stateDigest };
|
|
3203
|
+
}
|
|
3086
3204
|
});
|
|
3087
3205
|
this.#liveTargets = new V1LiveTargetCoordinator({
|
|
3088
3206
|
mediaPlane: options.mediaPlane,
|
|
@@ -3112,6 +3230,7 @@ var V1SyncClient = class {
|
|
|
3112
3230
|
async start() {
|
|
3113
3231
|
if (this.#started) return;
|
|
3114
3232
|
this.#started = true;
|
|
3233
|
+
this.#watch.open();
|
|
3115
3234
|
this.#connectAttemptCount = 0;
|
|
3116
3235
|
this.#socketCloses = [];
|
|
3117
3236
|
this.#omittedSocketCloses = 0;
|
|
@@ -3124,6 +3243,7 @@ var V1SyncClient = class {
|
|
|
3124
3243
|
} catch (error) {
|
|
3125
3244
|
if (startupGeneration === this.#startupGeneration) {
|
|
3126
3245
|
this.#started = false;
|
|
3246
|
+
this.#watch.close();
|
|
3127
3247
|
}
|
|
3128
3248
|
throw error;
|
|
3129
3249
|
}
|
|
@@ -3135,6 +3255,7 @@ var V1SyncClient = class {
|
|
|
3135
3255
|
}
|
|
3136
3256
|
stop() {
|
|
3137
3257
|
this.#started = false;
|
|
3258
|
+
this.#watch.close();
|
|
3138
3259
|
this.#entryPending = false;
|
|
3139
3260
|
this.#entryGeneration = null;
|
|
3140
3261
|
this.#startupGeneration += 1;
|
|
@@ -3166,6 +3287,10 @@ var V1SyncClient = class {
|
|
|
3166
3287
|
if (!this.#entryNegotiated) return;
|
|
3167
3288
|
this.#send({ type: "entered" });
|
|
3168
3289
|
}
|
|
3290
|
+
/** True when the connected Sync advertised `media_off_reason` in its welcome, so media items may carry `off_reason`. */
|
|
3291
|
+
offReasonNegotiated() {
|
|
3292
|
+
return this.#offReasonNegotiated;
|
|
3293
|
+
}
|
|
3169
3294
|
getSnapshot() {
|
|
3170
3295
|
const pendingCommands = this.#commandScheduler.pendingCommands;
|
|
3171
3296
|
const optimisticControl = this.#control && this.#participantId ? optimisticV1Control(this.#control, this.#participantId, pendingCommands) : this.#control;
|
|
@@ -3326,6 +3451,7 @@ var V1SyncClient = class {
|
|
|
3326
3451
|
if (this.#connectFailure?.opened) this.#connectFailure = void 0;
|
|
3327
3452
|
this.#emit();
|
|
3328
3453
|
this.#entryNegotiated = false;
|
|
3454
|
+
this.#offReasonNegotiated = false;
|
|
3329
3455
|
let socket;
|
|
3330
3456
|
try {
|
|
3331
3457
|
socket = this.#options.webSocket.connect(entryUrl(this.#options.url, this.#entryGeneration, this.#options.entryConfirmation === true));
|
|
@@ -3373,11 +3499,12 @@ var V1SyncClient = class {
|
|
|
3373
3499
|
this.#clock().setTimeout(() => {
|
|
3374
3500
|
if (connectionGeneration !== this.#connectionGeneration) return;
|
|
3375
3501
|
this.#inbound = this.#inbound.then(() => {
|
|
3502
|
+
const legacyHello = event.code === 1009 && this.#legacyHelloRejected(socket);
|
|
3376
3503
|
if (socket === this.#socket && event.code === 1008 && (event.reason === "" || event.reason === "invalid token" || event.reason === "policy violation" || event.reason === "entry timeout")) {
|
|
3377
3504
|
this.#phase = { phase: "terminal", terminalReason: event.reason === "entry timeout" ? "entry_timeout" : "access_rejected" };
|
|
3378
3505
|
this.#emit();
|
|
3379
3506
|
}
|
|
3380
|
-
this.#disconnected(socket);
|
|
3507
|
+
this.#disconnected(socket, legacyHello);
|
|
3381
3508
|
});
|
|
3382
3509
|
}, 0);
|
|
3383
3510
|
};
|
|
@@ -3394,10 +3521,12 @@ var V1SyncClient = class {
|
|
|
3394
3521
|
const token = await this.#options.token();
|
|
3395
3522
|
if (socket !== this.#socket) return;
|
|
3396
3523
|
this.#phase = { phase: "recovering" };
|
|
3524
|
+
const baggage = legacyHelloServers.has(this.#options.url) ? void 0 : sessionBaggage();
|
|
3397
3525
|
const hello = {
|
|
3398
3526
|
type: "hello",
|
|
3399
3527
|
protocol: 1,
|
|
3400
3528
|
...this.#options.traceparent ? { traceparent: this.#options.traceparent } : {},
|
|
3529
|
+
...baggage ? { baggage } : {},
|
|
3401
3530
|
token,
|
|
3402
3531
|
streams: {
|
|
3403
3532
|
control: { cursor: this.#control && { revision: this.#control.revision, state_schema_version: this.#control.stateSchemaVersion, state_digest: this.#control.stateDigest } },
|
|
@@ -3407,6 +3536,7 @@ var V1SyncClient = class {
|
|
|
3407
3536
|
}
|
|
3408
3537
|
};
|
|
3409
3538
|
const collaborationExtension = this.#collaborationState.helloExtension;
|
|
3539
|
+
this.#baggageHelloSocket = baggage ? socket : null;
|
|
3410
3540
|
if (!collaborationExtension) {
|
|
3411
3541
|
this.#send(hello);
|
|
3412
3542
|
} else {
|
|
@@ -3426,7 +3556,7 @@ var V1SyncClient = class {
|
|
|
3426
3556
|
} catch (error) {
|
|
3427
3557
|
if (this.#snapshotRecoveryRequested) this.#failDigestRecovery();
|
|
3428
3558
|
else if (error instanceof V1StateDigestMismatchError) this.#recoverDigestMismatch();
|
|
3429
|
-
else this.#recover("invalid_frame");
|
|
3559
|
+
else this.#recover("invalid_frame", error instanceof V1ReplicaError && error.message === "invalid_frame" && this.#legacyHelloRejected(socket));
|
|
3430
3560
|
}
|
|
3431
3561
|
}
|
|
3432
3562
|
async #handleFrame(frame) {
|
|
@@ -3537,6 +3667,7 @@ var V1SyncClient = class {
|
|
|
3537
3667
|
return;
|
|
3538
3668
|
case "pong": {
|
|
3539
3669
|
this.#requireLive();
|
|
3670
|
+
this.#watch.ponged();
|
|
3540
3671
|
this.#missedHeartbeats = 0;
|
|
3541
3672
|
this.#lastHeartbeatReplyAt = this.#now();
|
|
3542
3673
|
const previous = this.#health.health;
|
|
@@ -3548,8 +3679,10 @@ var V1SyncClient = class {
|
|
|
3548
3679
|
}
|
|
3549
3680
|
async #welcome(frame) {
|
|
3550
3681
|
if (this.#phase.phase !== "recovering" || this.#recovery) throw new V1ReplicaError("unexpected welcome");
|
|
3682
|
+
this.#baggageHelloSocket = null;
|
|
3551
3683
|
this.#collaborationState.acceptWelcome(frame);
|
|
3552
3684
|
this.#entryNegotiated = frame.entry_confirmation === true;
|
|
3685
|
+
this.#offReasonNegotiated = frame.media_off_reason === true;
|
|
3553
3686
|
this.#entryGeneration = frame.entry_generation ?? this.#entryGeneration;
|
|
3554
3687
|
this.#participantId = frame.participant_id;
|
|
3555
3688
|
this.#participantGeneration = frame.participant_generation;
|
|
@@ -3631,6 +3764,9 @@ var V1SyncClient = class {
|
|
|
3631
3764
|
this.#media = updateProjection(this.#media, frame.projection_id, frame.sequence, item, mediaKey);
|
|
3632
3765
|
rememberBoundedEvidence(this.#mediaEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
|
|
3633
3766
|
this.#notifyRemotePublicationResumed(previous, item);
|
|
3767
|
+
if (!this.#mediaEnded() && item.participantId === this.#participantId && previous?.publicationId && previous.publicationId !== item.publicationId) {
|
|
3768
|
+
this.#options.mediaPlane?.localPublicationWithdrawn?.(previous.publicationId);
|
|
3769
|
+
}
|
|
3634
3770
|
if (!this.#mediaEnded() && item.participantId !== this.#participantId && (previous?.enabled !== item.enabled || previous?.publicationId !== item.publicationId)) {
|
|
3635
3771
|
this.#options.mediaPlane?.remotePublicationsChanged?.(this.#media?.items ?? []);
|
|
3636
3772
|
}
|
|
@@ -3704,7 +3840,7 @@ var V1SyncClient = class {
|
|
|
3704
3840
|
if (evidence.get(frame.sequence) !== frameSignature(frame)) throw new V1ReplicaError("conflicting or unprovable duplicate projection event");
|
|
3705
3841
|
return true;
|
|
3706
3842
|
}
|
|
3707
|
-
#disconnected(socket) {
|
|
3843
|
+
#disconnected(socket, retryNow = false) {
|
|
3708
3844
|
if (socket !== this.#socket) return;
|
|
3709
3845
|
this.#socket = null;
|
|
3710
3846
|
this.#clearDial();
|
|
@@ -3723,6 +3859,13 @@ var V1SyncClient = class {
|
|
|
3723
3859
|
this.#emit();
|
|
3724
3860
|
if (!this.#transportAvailable) return;
|
|
3725
3861
|
this.#clearReconnect();
|
|
3862
|
+
if (retryNow) {
|
|
3863
|
+
this.#reconnectTimer = this.#clock().setTimeout(() => {
|
|
3864
|
+
this.#reconnectTimer = void 0;
|
|
3865
|
+
this.#connect();
|
|
3866
|
+
}, 0);
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3726
3869
|
const configuredDelay = this.#options.reconnectDelayMs;
|
|
3727
3870
|
const exponentialAttempt = configuredDelay === void 0 ? Math.max(0, this.#reconnectAttempt - FAST_RECONNECT_ATTEMPTS + 1) : this.#reconnectAttempt;
|
|
3728
3871
|
const baseDelay = configuredDelay === 0 ? 0 : Math.min(MAX_RECONNECT_DELAY_MS, (configuredDelay ?? DEFAULT_RECONNECT_DELAY_MS) * 2 ** Math.min(exponentialAttempt, 5));
|
|
@@ -3733,11 +3876,21 @@ var V1SyncClient = class {
|
|
|
3733
3876
|
this.#connect();
|
|
3734
3877
|
}, delay);
|
|
3735
3878
|
}
|
|
3736
|
-
#recover(reason) {
|
|
3879
|
+
#recover(reason, retryNow = false) {
|
|
3737
3880
|
const socket = this.#socket;
|
|
3738
3881
|
if (!socket) return;
|
|
3739
3882
|
socket.close(CLIENT_RESTART_CLOSE_CODE, reason);
|
|
3740
|
-
this.#disconnected(socket);
|
|
3883
|
+
this.#disconnected(socket, retryNow);
|
|
3884
|
+
}
|
|
3885
|
+
/**
|
|
3886
|
+
* A Sync that predates the session ID rejects any hello with an unknown `baggage` field before welcome. The first
|
|
3887
|
+
* rejection marks that server as legacy, so the retry and every later reconnect send the hello without it.
|
|
3888
|
+
*/
|
|
3889
|
+
#legacyHelloRejected(socket) {
|
|
3890
|
+
if (this.#baggageHelloSocket !== socket) return false;
|
|
3891
|
+
this.#baggageHelloSocket = null;
|
|
3892
|
+
legacyHelloServers.add(this.#options.url);
|
|
3893
|
+
return true;
|
|
3741
3894
|
}
|
|
3742
3895
|
#recoverDigestMismatch() {
|
|
3743
3896
|
const now = this.#clock().now();
|
|
@@ -3872,7 +4025,7 @@ var V1SyncClient = class {
|
|
|
3872
4025
|
this.#socket?.close(CLIENT_RESTART_CLOSE_CODE, "heartbeat timeout");
|
|
3873
4026
|
return;
|
|
3874
4027
|
}
|
|
3875
|
-
this.#
|
|
4028
|
+
if (!this.#sendPing()) return;
|
|
3876
4029
|
this.#startHeartbeat();
|
|
3877
4030
|
}, 2e4);
|
|
3878
4031
|
}
|
|
@@ -3883,7 +4036,8 @@ var V1SyncClient = class {
|
|
|
3883
4036
|
}
|
|
3884
4037
|
#startNoticeProbe() {
|
|
3885
4038
|
this.#clearNoticeProbe();
|
|
3886
|
-
this.#
|
|
4039
|
+
this.#watch.restart();
|
|
4040
|
+
this.#sendPing();
|
|
3887
4041
|
this.#health = new V1SyncHealth(this.#now());
|
|
3888
4042
|
const tick = () => {
|
|
3889
4043
|
this.#noticeProbeTimer = void 0;
|
|
@@ -3891,7 +4045,7 @@ var V1SyncClient = class {
|
|
|
3891
4045
|
const previous = this.#health.health;
|
|
3892
4046
|
this.#health.check(this.#now());
|
|
3893
4047
|
if (previous !== this.#health.health) this.#emit();
|
|
3894
|
-
this.#
|
|
4048
|
+
if (!this.#sendPing()) return;
|
|
3895
4049
|
this.#noticeProbeTimer = this.#clock().setTimeout(tick, SYNC_HEALTH_BUDGET.probeIntervalMs);
|
|
3896
4050
|
};
|
|
3897
4051
|
this.#noticeProbeTimer = this.#clock().setTimeout(tick, SYNC_HEALTH_BUDGET.probeIntervalMs);
|
|
@@ -3901,6 +4055,16 @@ var V1SyncClient = class {
|
|
|
3901
4055
|
this.#emit();
|
|
3902
4056
|
}, SYNC_HEALTH_BUDGET.degradedAfterMs);
|
|
3903
4057
|
}
|
|
4058
|
+
// The link watch times each ping, so every ping goes through here. When the oldest ping has gone unanswered for over 60 s, the
|
|
4059
|
+
// old heartbeat timeout, the socket is dead: it is replaced through the normal reconnect and this ping is not sent.
|
|
4060
|
+
#sendPing() {
|
|
4061
|
+
if (!this.#watch.pinged()) {
|
|
4062
|
+
this.#recover("ping unanswered for 60 s");
|
|
4063
|
+
return false;
|
|
4064
|
+
}
|
|
4065
|
+
this.#send({ type: "ping" });
|
|
4066
|
+
return true;
|
|
4067
|
+
}
|
|
3904
4068
|
#clearNoticeProbe() {
|
|
3905
4069
|
if (this.#noticeTimer !== void 0) this.#clock().clearTimeout(this.#noticeTimer);
|
|
3906
4070
|
if (this.#noticeProbeTimer !== void 0) this.#clock().clearTimeout(this.#noticeProbeTimer);
|
|
@@ -3919,7 +4083,14 @@ function updateProjection(projection, projectionId, sequence, item, key) {
|
|
|
3919
4083
|
return { projectionId, sequence, items: [...projection.items.filter((candidate) => key(candidate) !== itemKey), item] };
|
|
3920
4084
|
}
|
|
3921
4085
|
function mediaItem(item) {
|
|
3922
|
-
return {
|
|
4086
|
+
return {
|
|
4087
|
+
participantId: item.participant_id,
|
|
4088
|
+
source: item.source,
|
|
4089
|
+
enabled: item.enabled,
|
|
4090
|
+
publicationId: item.publication_id,
|
|
4091
|
+
...item.off_reason === void 0 ? {} : { offReason: item.off_reason },
|
|
4092
|
+
...item.off_revision === void 0 ? {} : { offRevision: item.off_revision }
|
|
4093
|
+
};
|
|
3923
4094
|
}
|
|
3924
4095
|
function presenceItem(item) {
|
|
3925
4096
|
return { participantId: item.participant_id, admissionRevision: item.admission_revision, identityId: item.identity_id, deviceType: item.device_type, state: item.state, speaking: item.speaking, activeSpeaker: item.active_speaker };
|
|
@@ -3952,6 +4123,7 @@ function entryUrl(value, generation, confirmation) {
|
|
|
3952
4123
|
const url = new URL(value);
|
|
3953
4124
|
url.searchParams.set("participant_devices", "1");
|
|
3954
4125
|
url.searchParams.set("participant_device_admissions", "2");
|
|
4126
|
+
url.searchParams.set("media_off_reason", "1");
|
|
3955
4127
|
url.searchParams.set("entry_generation", generation === null ? "new" : String(generation));
|
|
3956
4128
|
if (confirmation) url.searchParams.set("entry_confirmation", "1");
|
|
3957
4129
|
return url.toString();
|