@q9labsai/chalk-client 5.2.6 → 5.3.1

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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/access/grant.d.ts +2 -0
  3. package/dist/{chunk-I7Y4HR2P.js → chunk-2CEMOTZF.js} +333 -64
  4. package/dist/{chunk-5YPQNMJE.js → chunk-FD7ZSDTM.js} +103 -26
  5. package/dist/{chunk-CJVLTSIO.js → chunk-MHJ4UPM3.js} +165 -6
  6. package/dist/chunk-N4IPKRS6.js +76 -0
  7. package/dist/{chunk-YIDKETOX.js → chunk-O5JB5XVX.js} +840 -38
  8. package/dist/{chunk-WFRTUBN6.js → chunk-VAJ6XNAA.js} +1 -1
  9. package/dist/client.d.ts +182 -1
  10. package/dist/connection/dependencies.d.ts +2 -1
  11. package/dist/effect.js +29 -4
  12. package/dist/generated/http-api.d.ts +180 -1
  13. package/dist/generated/schemas.d.ts +290 -1
  14. package/dist/generated/sync.d.ts +60 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +33 -15
  17. package/dist/media/call-stats.d.ts +27 -0
  18. package/dist/media/client.d.ts +2 -2
  19. package/dist/media/ice-servers.d.ts +3 -0
  20. package/dist/media/index.d.ts +2 -0
  21. package/dist/media/index.js +4 -1
  22. package/dist/media/plane.d.ts +4 -2
  23. package/dist/media/receive-stall.d.ts +52 -0
  24. package/dist/media/types.d.ts +21 -0
  25. package/dist/media/video-render-size.d.ts +7 -0
  26. package/dist/media/viewer-layer.d.ts +48 -0
  27. package/dist/public-invites/index.js +3 -2
  28. package/dist/server/index.cjs +19 -4
  29. package/dist/server/index.js +19 -4
  30. package/dist/server/types.d.cts +2 -0
  31. package/dist/server/types.d.ts +2 -0
  32. package/dist/session/index.d.ts +18 -0
  33. package/dist/space-client/same-identity.d.ts +6 -0
  34. package/dist/space-client/types.d.ts +6 -0
  35. package/dist/sync/index.js +2 -1
  36. package/dist/sync/v1-command-scheduler.d.ts +4 -0
  37. package/dist/sync/v1-types.d.ts +6 -0
  38. package/package.json +3 -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) {
@@ -405,11 +409,20 @@ var SyncProtocolMetadata = {
405
409
  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
410
  }
407
411
  },
412
+ helloOnlyFields: {
413
+ baggage: {
414
+ kind: "string",
415
+ minBytes: 47,
416
+ maxBytes: 47,
417
+ format: "w3c-baggage-session-id",
418
+ 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}$"
419
+ }
420
+ },
408
421
  acceptedOn: {
409
422
  hello: "client-to-server",
410
423
  serverFrames: "server-to-client"
411
424
  },
412
- upgradeHeaders: ["x-chalk-journey-id", "traceparent", "tracestate"],
425
+ upgradeHeaders: ["x-chalk-journey-id", "traceparent", "tracestate", "baggage"],
413
426
  rule: "propagate_from_first_observed_layer_to_every_downstream_frame"
414
427
  },
415
428
  externalIntents: [
@@ -712,7 +725,9 @@ function strictCommand(value) {
712
725
  function strictCollaborationsHelloExtension(value) {
713
726
  return exactObject(value, ["name", "chat_cursor"]) && ["collaboration_v1", "collaboration_v2"].includes(String(value.name)) && exactObject(value.chat_cursor, ["after_sequence", "retained_floor_sequence"]);
714
727
  }
715
- function strictHello(value) {
728
+ function strictHello(hello) {
729
+ const { baggage, ...value } = hello;
730
+ 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
731
  const base = exactObjectWithCorrelation(value, ["type", "protocol", "token", "streams"]);
717
732
  const extended = exactObjectWithCorrelation(value, ["type", "protocol", "token", "streams", "extensions"]) && Array.isArray(value.extensions) && value.extensions.length === 1 && strictCollaborationsHelloExtension(value.extensions[0]);
718
733
  if (!base && !extended || !exactObject(value.streams, ["control", "media", "presence", "requests"])) return false;
@@ -861,7 +876,8 @@ function strictEvent(value) {
861
876
  }
862
877
  function strictProjectionItem(value, stream) {
863
878
  if (stream === "media") return exactObject(value, ["participant_id", "source", "enabled", "publication_id"]);
864
- if (stream === "presence") return exactObject(value, ["participant_id", "state", "speaking", "active_speaker"]);
879
+ if (stream === "presence")
880
+ 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"] : []]);
865
881
  return false;
866
882
  }
867
883
  function strictProjection(value) {
@@ -873,7 +889,13 @@ function strictWelcome(value) {
873
889
  const extension = Array.isArray(value.extensions) && value.extensions.length === 1 ? value.extensions[0] : void 0;
874
890
  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);
875
891
  if (Object.hasOwn(value, "entry_confirmation") && value.entry_confirmation !== true) return false;
876
- const fields = [...base, ...extensionValid ? ["extensions"] : [], ...Object.hasOwn(value, "entry_confirmation") ? ["entry_confirmation"] : [], ...Object.hasOwn(value, "entry_generation") ? ["entry_generation"] : []];
892
+ const fields = [
893
+ ...base,
894
+ ...extensionValid ? ["extensions"] : [],
895
+ ...Object.hasOwn(value, "entry_confirmation") ? ["entry_confirmation"] : [],
896
+ ...Object.hasOwn(value, "entry_generation") ? ["entry_generation"] : [],
897
+ ...Object.hasOwn(value, "participant_identity_id") ? ["participant_identity_id"] : []
898
+ ];
877
899
  if (!strictCursor(value.head)) return false;
878
900
  if (value.mode === "snapshot") return exactObjectWithCorrelation(value, [...fields, "snapshot"]) && strictSnapshot(value.snapshot);
879
901
  if (value.mode === "terminal") return exactObjectWithCorrelation(value, [...fields, "reason"]);
@@ -950,9 +972,10 @@ var TraceparentSchema = boundedUtf8String(55, 55).check(
950
972
  var TracestateSchema = boundedUtf8String(512, 1).check(
951
973
  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" })
952
974
  );
975
+ 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>" }));
953
976
  var SyncCorrelationFieldsSchema = Schema.Struct({ journey_id: Schema.optional(JourneyIdSchema), traceparent: Schema.optional(TraceparentSchema), tracestate: Schema.optional(TracestateSchema) });
954
- var HelloCorrelationFields = { journey_id: Schema.optional(JourneyIdSchema), traceparent: Schema.optional(TraceparentSchema), tracestate: Schema.optional(TracestateSchema) };
955
- var ServerCorrelationFields = { ...HelloCorrelationFields };
977
+ var ServerCorrelationFields = { journey_id: Schema.optional(JourneyIdSchema), traceparent: Schema.optional(TraceparentSchema), tracestate: Schema.optional(TracestateSchema) };
978
+ var HelloCorrelationFields = { ...ServerCorrelationFields, baggage: Schema.optional(BaggageSchema) };
956
979
  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" }));
957
980
  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" }));
958
981
  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" }));
@@ -1479,6 +1502,7 @@ var WelcomeBase = {
1479
1502
  protocol: Schema.Literal(1),
1480
1503
  participant_id: UuidSchema,
1481
1504
  participant_generation: PositiveIntegerSchema,
1505
+ participant_identity_id: Schema.optionalKey(UuidSchema),
1482
1506
  recovery_id: UuidSchema,
1483
1507
  head: ControlCursorSchema,
1484
1508
  entry_confirmation: Schema.optionalKey(Schema.Literal(true)),
@@ -1527,9 +1551,15 @@ var CommandIdConflictAckFrameSchema = Schema.Struct({ type: Schema.Literal("ack"
1527
1551
  var MediaProjectionItemSchema = Schema.Struct({ participant_id: UuidSchema, source: Schema.Union([Schema.Literal("microphone"), Schema.Literal("camera"), Schema.Literal("screen")]), enabled: Schema.Boolean, publication_id: Schema.NullOr(boundedUtf8String(256, 1)) }).check(
1528
1552
  Schema.makeFilter((item) => !item.enabled || item.publication_id !== null ? void 0 : { path: [], issue: "enabled requires a publication_id" })
1529
1553
  );
1530
- var PresenceProjectionItemSchema = Schema.Struct({ participant_id: UuidSchema, state: Schema.Union([Schema.Literal("connected"), Schema.Literal("disconnected")]), speaking: Schema.Boolean, active_speaker: Schema.Boolean }).check(
1531
- Schema.makeFilter((item) => item.state !== "disconnected" || !item.speaking && !item.active_speaker ? void 0 : { path: [], issue: "disconnected presence cannot be speaking or active speaker" })
1532
- );
1554
+ var PresenceProjectionItemSchema = Schema.Struct({
1555
+ participant_id: UuidSchema,
1556
+ state: Schema.Union([Schema.Literal("connected"), Schema.Literal("disconnected")]),
1557
+ speaking: Schema.Boolean,
1558
+ active_speaker: Schema.Boolean,
1559
+ identity_id: Schema.optional(UuidSchema),
1560
+ device_type: Schema.optional(Schema.Literals(["phone", "tablet", "computer"])),
1561
+ admission_revision: Schema.optional(PositiveIntegerSchema)
1562
+ }).check(Schema.makeFilter((item) => item.state !== "disconnected" || !item.speaking && !item.active_speaker ? void 0 : { path: [], issue: "disconnected presence cannot be speaking or active speaker" }));
1533
1563
  var MediaProjectionSnapshotFrameSchema = Schema.Struct({
1534
1564
  type: Schema.Literal("projection_snapshot"),
1535
1565
  stream: Schema.Literal("media"),
@@ -2514,6 +2544,11 @@ var V1CommandScheduler = class {
2514
2544
  async acknowledge(frame) {
2515
2545
  const deferred = this.#commands.get(frame.command_id);
2516
2546
  if (!deferred) return;
2547
+ if (isSatisfiedByInactiveParticipant(frame, deferred.frame)) {
2548
+ const head = this.#options.controlHead();
2549
+ 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 });
2550
+ return;
2551
+ }
2517
2552
  if (frame.outcome === "rejected" || frame.outcome === "command_id_conflict") {
2518
2553
  await this.#finishCommand(frame.command_id, frame);
2519
2554
  return;
@@ -2552,15 +2587,12 @@ var V1CommandScheduler = class {
2552
2587
  }
2553
2588
  }
2554
2589
  async settleTerminalRecovery(frame) {
2555
- const satisfiedOperations = /* @__PURE__ */ new Set();
2556
- if (frame.reason === "episode_ended") {
2557
- satisfiedOperations.add("end_episode");
2558
- satisfiedOperations.add("participant_leave");
2559
- } else if (frame.reason === "participant_inactive") {
2560
- satisfiedOperations.add("participant_leave");
2561
- }
2590
+ const satisfies = (operation) => {
2591
+ if (frame.reason === "episode_ended") return operation === "end_episode" || operation === "participant_leave";
2592
+ return frame.reason === "participant_inactive" && inactiveParticipantSatisfies(operation);
2593
+ };
2562
2594
  for (const [commandId, deferred] of this.#commands) {
2563
- if (deferred.frame.type !== "operation" || !satisfiedOperations.has(deferred.frame.name)) continue;
2595
+ if (deferred.frame.type !== "operation" || !satisfies(deferred.frame.name)) continue;
2564
2596
  await this.#finishCommand(commandId, {
2565
2597
  type: "ack",
2566
2598
  command_id: commandId,
@@ -2730,6 +2762,12 @@ function targetFrame(commandId, command) {
2730
2762
  return { type: "command", command_id: commandId, name: command.name, payload: command.payload };
2731
2763
  }
2732
2764
  }
2765
+ function inactiveParticipantSatisfies(operation) {
2766
+ return operation === "participant_leave";
2767
+ }
2768
+ function isSatisfiedByInactiveParticipant(result, command) {
2769
+ return result.outcome === "rejected" && result.reason === "participant_inactive" && command.type === "operation" && inactiveParticipantSatisfies(command.name);
2770
+ }
2733
2771
  function isTerminalLifecycleOperation(frame) {
2734
2772
  return frame.type === "operation" && (frame.name === "participant_leave" || frame.name === "end_episode");
2735
2773
  }
@@ -2988,6 +3026,7 @@ var MAX_REPLAY_EVENTS = SyncProtocolLimits.completeReplayMaxEvents;
2988
3026
  var MAX_REPLAY_BYTES = SyncProtocolLimits.completeReplayEncodedBytes;
2989
3027
  var MAX_PROJECTION_EVENT_EVIDENCE = 256;
2990
3028
  var CLIENT_RESTART_CLOSE_CODE = 4e3;
3029
+ var legacyHelloServers = /* @__PURE__ */ new Set();
2991
3030
  var DEFAULT_RECONNECT_DELAY_MS = 250;
2992
3031
  var MAX_RECONNECT_DELAY_MS = 5e3;
2993
3032
  var FAST_RECONNECT_ATTEMPTS = 8;
@@ -3016,6 +3055,7 @@ var V1SyncClient = class {
3016
3055
  #presence = null;
3017
3056
  #participantId = null;
3018
3057
  #participantGeneration = null;
3058
+ #participantIdentityId;
3019
3059
  #entryGeneration = null;
3020
3060
  #recovery = null;
3021
3061
  #snapshotRecoveryRequested = false;
@@ -3037,6 +3077,8 @@ var V1SyncClient = class {
3037
3077
  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 };
3038
3078
  }
3039
3079
  #reconnectAttempt = 0;
3080
+ /** The socket whose hello carried baggage and has not been answered by a welcome yet. */
3081
+ #baggageHelloSocket = null;
3040
3082
  #episodeEndTimer;
3041
3083
  #heartbeatTimer;
3042
3084
  #missedHeartbeats = 0;
@@ -3067,7 +3109,11 @@ var V1SyncClient = class {
3067
3109
  isStarted: () => this.#started,
3068
3110
  sendIfLive: (frame) => this.#sendIfLive(frame),
3069
3111
  stateChanged: () => this.#emit(),
3070
- ackHeadIsProven: (ack) => this.#ackHeadIsProven(ack)
3112
+ ackHeadIsProven: (ack) => this.#ackHeadIsProven(ack),
3113
+ controlHead: () => {
3114
+ const control = this.#requireControl();
3115
+ return { revision: control.revision, state_digest: control.stateDigest };
3116
+ }
3071
3117
  });
3072
3118
  this.#liveTargets = new V1LiveTargetCoordinator({
3073
3119
  mediaPlane: options.mediaPlane,
@@ -3136,6 +3182,7 @@ var V1SyncClient = class {
3136
3182
  this.#socket?.close(1e3, "client stopped");
3137
3183
  this.#socket = null;
3138
3184
  this.#phase = { phase: "stopped" };
3185
+ this.#options.mediaPlane?.remotePublicationsChanged?.([], "unavailable");
3139
3186
  this.#rejectEphemeral("client_stopped");
3140
3187
  this.#collaborationState.disconnect("client_stopped");
3141
3188
  this.#localPublications = [];
@@ -3164,6 +3211,7 @@ var V1SyncClient = class {
3164
3211
  },
3165
3212
  participantId: this.#participantId,
3166
3213
  participantGeneration: this.#participantGeneration,
3214
+ participantIdentityId: this.#participantIdentityId,
3167
3215
  control: this.#control,
3168
3216
  optimisticControl,
3169
3217
  media: copyProjection(this.#media),
@@ -3183,6 +3231,7 @@ var V1SyncClient = class {
3183
3231
  return () => this.#mediaPauseListeners.delete(listener);
3184
3232
  }
3185
3233
  #notifyMediaPause(event) {
3234
+ if (event.name === "episode_ended") this.#options.mediaPlane?.remotePublicationsChanged?.([]);
3186
3235
  const source = event.name === "participant_microphone_stopped" ? "microphone" : event.name === "participant_camera_stopped" ? "camera" : event.name === "participant_screen_share_stopped" ? "screen" : null;
3187
3236
  if (source && "participant_id" in event.payload && event.payload.participant_id === this.#participantId) {
3188
3237
  for (const listener of this.#mediaPauseListeners) listener({ source, revision: event.revision });
@@ -3355,11 +3404,12 @@ var V1SyncClient = class {
3355
3404
  this.#clock().setTimeout(() => {
3356
3405
  if (connectionGeneration !== this.#connectionGeneration) return;
3357
3406
  this.#inbound = this.#inbound.then(() => {
3407
+ const legacyHello = event.code === 1009 && this.#legacyHelloRejected(socket);
3358
3408
  if (socket === this.#socket && event.code === 1008 && (event.reason === "" || event.reason === "invalid token" || event.reason === "policy violation" || event.reason === "entry timeout")) {
3359
3409
  this.#phase = { phase: "terminal", terminalReason: event.reason === "entry timeout" ? "entry_timeout" : "access_rejected" };
3360
3410
  this.#emit();
3361
3411
  }
3362
- this.#disconnected(socket);
3412
+ this.#disconnected(socket, legacyHello);
3363
3413
  });
3364
3414
  }, 0);
3365
3415
  };
@@ -3376,10 +3426,12 @@ var V1SyncClient = class {
3376
3426
  const token = await this.#options.token();
3377
3427
  if (socket !== this.#socket) return;
3378
3428
  this.#phase = { phase: "recovering" };
3429
+ const baggage = legacyHelloServers.has(this.#options.url) ? void 0 : sessionBaggage();
3379
3430
  const hello = {
3380
3431
  type: "hello",
3381
3432
  protocol: 1,
3382
3433
  ...this.#options.traceparent ? { traceparent: this.#options.traceparent } : {},
3434
+ ...baggage ? { baggage } : {},
3383
3435
  token,
3384
3436
  streams: {
3385
3437
  control: { cursor: this.#control && { revision: this.#control.revision, state_schema_version: this.#control.stateSchemaVersion, state_digest: this.#control.stateDigest } },
@@ -3389,6 +3441,7 @@ var V1SyncClient = class {
3389
3441
  }
3390
3442
  };
3391
3443
  const collaborationExtension = this.#collaborationState.helloExtension;
3444
+ this.#baggageHelloSocket = baggage ? socket : null;
3392
3445
  if (!collaborationExtension) {
3393
3446
  this.#send(hello);
3394
3447
  } else {
@@ -3408,7 +3461,7 @@ var V1SyncClient = class {
3408
3461
  } catch (error) {
3409
3462
  if (this.#snapshotRecoveryRequested) this.#failDigestRecovery();
3410
3463
  else if (error instanceof V1StateDigestMismatchError) this.#recoverDigestMismatch();
3411
- else this.#recover("invalid_frame");
3464
+ else this.#recover("invalid_frame", error instanceof V1ReplicaError && error.message === "invalid_frame" && this.#legacyHelloRejected(socket));
3412
3465
  }
3413
3466
  }
3414
3467
  async #handleFrame(frame) {
@@ -3444,6 +3497,7 @@ var V1SyncClient = class {
3444
3497
  const control = this.#requireControl();
3445
3498
  this.#send({ type: "delivery_ack", stream: "control", revision: control.revision, state_digest: control.stateDigest });
3446
3499
  }
3500
+ if (this.#control?.status === "ended" || frame.name === "participant_left" && frame.payload.participant_id === this.#participantId) this.#participantIdentityId = void 0;
3447
3501
  if (frame.name === "participant_left" && frame.payload.participant_id === this.#participantId) {
3448
3502
  this.#phase = { phase: "terminal", terminalReason: frame.payload.reason === "removed" ? "participant_removed" : "participant_inactive" };
3449
3503
  this.#socket?.close(1e3, "participant inactive");
@@ -3529,11 +3583,13 @@ var V1SyncClient = class {
3529
3583
  }
3530
3584
  async #welcome(frame) {
3531
3585
  if (this.#phase.phase !== "recovering" || this.#recovery) throw new V1ReplicaError("unexpected welcome");
3586
+ this.#baggageHelloSocket = null;
3532
3587
  this.#collaborationState.acceptWelcome(frame);
3533
3588
  this.#entryNegotiated = frame.entry_confirmation === true;
3534
3589
  this.#entryGeneration = frame.entry_generation ?? this.#entryGeneration;
3535
3590
  this.#participantId = frame.participant_id;
3536
3591
  this.#participantGeneration = frame.participant_generation;
3592
+ this.#participantIdentityId = frame.mode === "terminal" ? void 0 : frame.participant_identity_id;
3537
3593
  this.#updateLocalMediaStates();
3538
3594
  if (frame.mode === "terminal") {
3539
3595
  await this.#commandScheduler.settleTerminalRecovery(frame);
@@ -3547,6 +3603,7 @@ var V1SyncClient = class {
3547
3603
  const control = await restoreV1Snapshot(frame.snapshot);
3548
3604
  if (!sameHead(control, frame.head)) throw new V1ReplicaError("snapshot head mismatch");
3549
3605
  this.#control = control;
3606
+ if (control.status === "ended") this.#options.mediaPlane?.remotePublicationsChanged?.([]);
3550
3607
  this.#snapshotRevision = control.revision;
3551
3608
  this.#snapshotRecoveryRequested = false;
3552
3609
  this.#controlHeads.clear();
@@ -3592,7 +3649,7 @@ var V1SyncClient = class {
3592
3649
  previous.find((candidate) => mediaKey(candidate) === mediaKey(item)),
3593
3650
  item
3594
3651
  );
3595
- if (!this.#mediaEnded()) this.#options.mediaPlane?.remotePublicationsChanged?.();
3652
+ if (!this.#mediaEnded()) this.#options.mediaPlane?.remotePublicationsChanged?.(this.#media?.items ?? []);
3596
3653
  } else {
3597
3654
  this.#presence = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(presenceItem) };
3598
3655
  this.#presenceEventEvidence.clear();
@@ -3611,7 +3668,7 @@ var V1SyncClient = class {
3611
3668
  rememberBoundedEvidence(this.#mediaEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
3612
3669
  this.#notifyRemotePublicationResumed(previous, item);
3613
3670
  if (!this.#mediaEnded() && item.participantId !== this.#participantId && (previous?.enabled !== item.enabled || previous?.publicationId !== item.publicationId)) {
3614
- this.#options.mediaPlane?.remotePublicationsChanged?.();
3671
+ this.#options.mediaPlane?.remotePublicationsChanged?.(this.#media?.items ?? []);
3615
3672
  }
3616
3673
  } else {
3617
3674
  if (this.#acceptProjectionDuplicate(this.#presence, this.#presenceEventEvidence, frame)) return;
@@ -3683,7 +3740,7 @@ var V1SyncClient = class {
3683
3740
  if (evidence.get(frame.sequence) !== frameSignature(frame)) throw new V1ReplicaError("conflicting or unprovable duplicate projection event");
3684
3741
  return true;
3685
3742
  }
3686
- #disconnected(socket) {
3743
+ #disconnected(socket, retryNow = false) {
3687
3744
  if (socket !== this.#socket) return;
3688
3745
  this.#socket = null;
3689
3746
  this.#clearDial();
@@ -3694,6 +3751,7 @@ var V1SyncClient = class {
3694
3751
  this.#commandScheduler.disconnect();
3695
3752
  this.#media = null;
3696
3753
  this.#presence = null;
3754
+ this.#options.mediaPlane?.remotePublicationsChanged?.([], this.#mediaEnded() ? "available" : "unavailable");
3697
3755
  this.#rejectRequests("disconnected_before_delivery");
3698
3756
  this.#collaborationState.disconnect("disconnected_before_delivery");
3699
3757
  if (!this.#started || this.#phase.phase === "terminal") return;
@@ -3701,6 +3759,13 @@ var V1SyncClient = class {
3701
3759
  this.#emit();
3702
3760
  if (!this.#transportAvailable) return;
3703
3761
  this.#clearReconnect();
3762
+ if (retryNow) {
3763
+ this.#reconnectTimer = this.#clock().setTimeout(() => {
3764
+ this.#reconnectTimer = void 0;
3765
+ this.#connect();
3766
+ }, 0);
3767
+ return;
3768
+ }
3704
3769
  const configuredDelay = this.#options.reconnectDelayMs;
3705
3770
  const exponentialAttempt = configuredDelay === void 0 ? Math.max(0, this.#reconnectAttempt - FAST_RECONNECT_ATTEMPTS + 1) : this.#reconnectAttempt;
3706
3771
  const baseDelay = configuredDelay === 0 ? 0 : Math.min(MAX_RECONNECT_DELAY_MS, (configuredDelay ?? DEFAULT_RECONNECT_DELAY_MS) * 2 ** Math.min(exponentialAttempt, 5));
@@ -3711,11 +3776,21 @@ var V1SyncClient = class {
3711
3776
  this.#connect();
3712
3777
  }, delay);
3713
3778
  }
3714
- #recover(reason) {
3779
+ #recover(reason, retryNow = false) {
3715
3780
  const socket = this.#socket;
3716
3781
  if (!socket) return;
3717
3782
  socket.close(CLIENT_RESTART_CLOSE_CODE, reason);
3718
- this.#disconnected(socket);
3783
+ this.#disconnected(socket, retryNow);
3784
+ }
3785
+ /**
3786
+ * A Sync that predates the session ID rejects any hello with an unknown `baggage` field before welcome. The first
3787
+ * rejection marks that server as legacy, so the retry and every later reconnect send the hello without it.
3788
+ */
3789
+ #legacyHelloRejected(socket) {
3790
+ if (this.#baggageHelloSocket !== socket) return false;
3791
+ this.#baggageHelloSocket = null;
3792
+ legacyHelloServers.add(this.#options.url);
3793
+ return true;
3719
3794
  }
3720
3795
  #recoverDigestMismatch() {
3721
3796
  const now = this.#clock().now();
@@ -3900,7 +3975,7 @@ function mediaItem(item) {
3900
3975
  return { participantId: item.participant_id, source: item.source, enabled: item.enabled, publicationId: item.publication_id };
3901
3976
  }
3902
3977
  function presenceItem(item) {
3903
- return { participantId: item.participant_id, state: item.state, speaking: item.speaking, activeSpeaker: item.active_speaker };
3978
+ 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 };
3904
3979
  }
3905
3980
  function mediaKey(item) {
3906
3981
  return `${item.participantId}:${item.source}`;
@@ -3928,6 +4003,8 @@ function validMediaPublication(publication) {
3928
4003
  }
3929
4004
  function entryUrl(value, generation, confirmation) {
3930
4005
  const url = new URL(value);
4006
+ url.searchParams.set("participant_devices", "1");
4007
+ url.searchParams.set("participant_device_admissions", "2");
3931
4008
  url.searchParams.set("entry_generation", generation === null ? "new" : String(generation));
3932
4009
  if (confirmation) url.searchParams.set("entry_confirmation", "1");
3933
4010
  return url.toString();