@q9labsai/chalk-client 5.2.6 → 5.3.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.
@@ -6,11 +6,11 @@ import {
6
6
  describeMediaCaptureError,
7
7
  grantedMediaFromStream,
8
8
  markMediaFailure
9
- } from "./chunk-YIDKETOX.js";
9
+ } from "./chunk-3TGVTP6Z.js";
10
10
  import {
11
11
  AccessGrantError,
12
12
  requireParsedAccessGrant
13
- } from "./chunk-CJVLTSIO.js";
13
+ } from "./chunk-GOSWSXTH.js";
14
14
  import {
15
15
  ChatAttachmentMimeTypeSchema,
16
16
  ChatReactionSetFrameSchema,
@@ -24,7 +24,7 @@ import {
24
24
  formatSyncConnectDiagnostic,
25
25
  scheduleSyncDialDeadline,
26
26
  syncConnectFailureCode
27
- } from "./chunk-5YPQNMJE.js";
27
+ } from "./chunk-XSB2GDRW.js";
28
28
 
29
29
  // src/access/manager.ts
30
30
  import { Clock, Context, Data, Deferred, Effect, Layer, SynchronizedRef } from "effect";
@@ -2853,7 +2853,7 @@ var ConnectionDiagnostics = class {
2853
2853
  // src/connection/lifecycle.ts
2854
2854
  var MEDIA_RECOVERY_TIMEOUT_MS = 6e4;
2855
2855
  var START_TIMEOUT_MS = 3e4;
2856
- var LEAVE_TIMEOUT_MS = 5e3;
2856
+ var LEAVE_TIMEOUT_MS = 4e4;
2857
2857
  var RECOVERY_BUDGET_MS = MEDIA_RECOVERY_TIMEOUT_MS;
2858
2858
  var MAX_RECOVERY_ATTEMPTS = 3;
2859
2859
  var REFRESH_RETRY_MS = 5e3;
@@ -3291,6 +3291,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
3291
3291
  if (model.failure) return yield* Effect2.fail(lifecycleFailure("leave_unconfirmed", false, model.failure.message));
3292
3292
  return;
3293
3293
  }
3294
+ yield* Effect2.sync(() => {
3295
+ for (const { track } of model.media?.getSnapshot().localTracks ?? []) {
3296
+ track.enabled = false;
3297
+ track.stop();
3298
+ }
3299
+ });
3294
3300
  yield* transition("leaving");
3295
3301
  const confirmed = yield* leaveIsConfirmed();
3296
3302
  yield* stopPorts();
@@ -4877,6 +4883,20 @@ function chatError(message) {
4877
4883
  return new SpaceClientError({ code: "chat.payload_invalid", recoverable: false, message });
4878
4884
  }
4879
4885
 
4886
+ // src/space-client/same-identity.ts
4887
+ function otherDevices(participants, participantId, ownIdentityId) {
4888
+ const identityId = ownIdentityId ?? participants.find((item) => item.participantId === participantId)?.identityId;
4889
+ return identityId ? participants.filter((item) => item.participantId !== participantId && item.identityId === identityId) : [];
4890
+ }
4891
+ function otherDeviceHasMicrophone(snapshot, participants) {
4892
+ if (snapshot.control?.status === "ended") return false;
4893
+ const admitted = snapshot.control ? new Set(snapshot.control.participants.map((item) => item.participantId)) : null;
4894
+ const devices = new Set(
4895
+ otherDevices(participants, snapshot.participantId, snapshot.participantIdentityId).filter((item) => !admitted || admitted.has(item.participantId)).map((item) => item.participantId)
4896
+ );
4897
+ return snapshot.media?.items.some((item) => devices.has(item.participantId) && item.source === "microphone" && item.enabled) ?? false;
4898
+ }
4899
+
4880
4900
  // src/space-client/media-controller.ts
4881
4901
  import { Clock as Clock3, Context as Context6, Deferred as Deferred3, Effect as Effect5, Exit as Exit2, Layer as Layer6, Semaphore } from "effect";
4882
4902
 
@@ -5012,6 +5032,7 @@ var MediaControllerRuntime = class {
5012
5032
  #unsubscribePorts = null;
5013
5033
  #microphoneWatch = null;
5014
5034
  #projection;
5035
+ #microphoneMutedByOtherDevice = false;
5015
5036
  constructor(connection, store, selection, gates, now, fork) {
5016
5037
  this.#connection = connection;
5017
5038
  this.#store = store;
@@ -5037,7 +5058,10 @@ var MediaControllerRuntime = class {
5037
5058
  if (intent.microphone !== void 0) this.#recordIntent("microphone", intent.microphone);
5038
5059
  if (intent.camera !== void 0) this.#recordIntent("camera", intent.camera);
5039
5060
  }
5040
- setMicrophoneEnabled = (enabled) => this.#set("microphone", enabled);
5061
+ setMicrophoneEnabled = (enabled) => Effect5.sync(() => {
5062
+ this.#microphoneMutedByOtherDevice = false;
5063
+ this.#publish();
5064
+ }).pipe(Effect5.andThen(this.#set("microphone", enabled)));
5041
5065
  setCameraEnabled = (enabled) => this.#set("camera", enabled);
5042
5066
  captureCameraPreview = () => this.#cameraSource("preview").pipe(Effect5.mapError(normalizeMediaError));
5043
5067
  setScreenShareEnabled = (enabled, options) => enabled ? this.#mediaAction(true, this.#startScreen(options)) : this.#stopScreen();
@@ -5462,19 +5486,36 @@ var MediaControllerRuntime = class {
5462
5486
  }
5463
5487
  }
5464
5488
  startInitial(media, stream) {
5489
+ const waitForSync = Effect5.callback((resume) => {
5490
+ const sync = this.#ports?.sync;
5491
+ if (!sync) return resume(Effect5.void);
5492
+ const check = () => {
5493
+ if (sync.getSnapshot().connection.phase === "live" || media.getSnapshot().connection.phase === "stopped") resume(Effect5.void);
5494
+ };
5495
+ const unsubscribe = sync.subscribe(check);
5496
+ const unsubscribeMedia = media.subscribe(check);
5497
+ check();
5498
+ return Effect5.sync(() => {
5499
+ unsubscribe();
5500
+ unsubscribeMedia();
5501
+ });
5502
+ });
5465
5503
  const publish = foreign2(async () => {
5466
5504
  const revisions = { microphone: this.#sources.microphone.initialRevision, camera: this.#sources.camera.initialRevision };
5467
- const tracks = stream.getTracks().filter((track) => {
5468
- const source = track.kind === "audio" ? "microphone" : "camera";
5469
- const enabled = this.#hasConsent(source);
5470
- track.enabled = enabled;
5471
- if (!enabled) {
5472
- this.#deleteTrack(source);
5473
- if (source === "microphone") track.stop();
5474
- }
5475
- return enabled;
5476
- });
5477
- await media.start(streamFromTracks(tracks));
5505
+ const tracks = this.#consentedTracks(stream.getTracks());
5506
+ await media.start(
5507
+ streamFromTracks(tracks),
5508
+ () => Effect5.runPromise(
5509
+ waitForSync.pipe(
5510
+ Effect5.map(() => {
5511
+ const snapshot = this.#ports?.sync.getSnapshot();
5512
+ this.#microphoneMutedByOtherDevice = Boolean(snapshot && this.#hasConsent("microphone") && otherDeviceHasMicrophone(snapshot, this.#store.getSnapshot().participants.roster));
5513
+ if (this.#microphoneMutedByOtherDevice) this.#recordIntent("microphone", false);
5514
+ return streamFromTracks(this.#consentedTracks(tracks));
5515
+ })
5516
+ )
5517
+ )
5518
+ );
5478
5519
  for (const source of ["microphone", "camera"]) {
5479
5520
  const state = this.#sources[source];
5480
5521
  if (state.desired.revision === revisions[source]) state.result = { revision: revisions[source], status: "confirmed" };
@@ -5482,6 +5523,18 @@ var MediaControllerRuntime = class {
5482
5523
  });
5483
5524
  return this.#serialize("microphone", this.#serialize("camera", publish));
5484
5525
  }
5526
+ #consentedTracks(tracks) {
5527
+ return tracks.filter((track) => {
5528
+ const source = track.kind === "audio" ? "microphone" : "camera";
5529
+ const enabled = this.#hasConsent(source);
5530
+ track.enabled = enabled;
5531
+ if (!enabled) {
5532
+ this.#deleteTrack(source);
5533
+ if (source === "microphone") track.stop();
5534
+ }
5535
+ return enabled;
5536
+ });
5537
+ }
5485
5538
  #send(ports, source, intent, enabled) {
5486
5539
  const command = this.#mediaAction(
5487
5540
  enabled,
@@ -5882,8 +5935,8 @@ var MediaControllerRuntime = class {
5882
5935
  this.#pauseTracks(source);
5883
5936
  if (cause === "capture_cleanup" && source !== "screen" && ports.media.pauseLocalCapture) {
5884
5937
  state.capturePause = "pending";
5885
- this.#retainPause(state, ports.media.pauseLocalCapture(source));
5886
5938
  this.#notifyCapturePauses(ports);
5939
+ this.#retainPause(state, ports.media.pauseLocalCapture(source));
5887
5940
  } else {
5888
5941
  ports.media.setLocalSourceIntent?.(source, false, cause);
5889
5942
  this.#retainPause(state, this.#beginLocalPause(ports, source, cause));
@@ -6101,6 +6154,7 @@ var MediaControllerRuntime = class {
6101
6154
  const remote = Object.freeze((snapshot?.remoteTracks ?? []).map((track) => Object.freeze({ participantId: track.participantId, source: track.source, publicationId: track.publicationId, track: track.track })));
6102
6155
  const media = Object.freeze({
6103
6156
  ...current.media,
6157
+ microphoneMutedByOtherDevice: this.#microphoneMutedByOtherDevice,
6104
6158
  local,
6105
6159
  remote,
6106
6160
  screenShare: {
@@ -6491,6 +6545,12 @@ var ParticipantsControllerRuntime = class {
6491
6545
  #unsubscribePorts = null;
6492
6546
  #unsubscribe = null;
6493
6547
  #unsubscribeMedia = null;
6548
+ #deviceScope = null;
6549
+ // Retain verified seats while the local Sync replica is recovering, and for older servers
6550
+ // whose negotiated hints disappear with sockets. New servers send every admitted seat.
6551
+ #devices = /* @__PURE__ */ new Map();
6552
+ #presenceCursor = null;
6553
+ #invalidatedDevices = /* @__PURE__ */ new Set();
6494
6554
  constructor(connection, store, offline) {
6495
6555
  this.#offline = offline;
6496
6556
  this.#connection = connection;
@@ -6544,6 +6604,8 @@ var ParticipantsControllerRuntime = class {
6544
6604
  return ({ sync }) => foreign3(() => sync.setDisplayName(displayName));
6545
6605
  });
6546
6606
  dispose() {
6607
+ this.#devices.clear();
6608
+ this.#invalidatedDevices.clear();
6547
6609
  this.#unsubscribePorts?.();
6548
6610
  this.#unsubscribe?.();
6549
6611
  this.#unsubscribeMedia?.();
@@ -6563,6 +6625,10 @@ var ParticipantsControllerRuntime = class {
6563
6625
  this.#unsubscribe = null;
6564
6626
  this.#unsubscribeMedia = null;
6565
6627
  if (!ports) {
6628
+ if (["idle", "leaving", "left", "failed"].includes(this.#connection.getSnapshot().state)) {
6629
+ this.#devices.clear();
6630
+ this.#invalidatedDevices.clear();
6631
+ }
6566
6632
  this.#publish(null, {});
6567
6633
  return;
6568
6634
  }
@@ -6571,11 +6637,57 @@ var ParticipantsControllerRuntime = class {
6571
6637
  this.#unsubscribeMedia = ports.media.subscribe(publish);
6572
6638
  publish();
6573
6639
  }
6640
+ #syncDeviceScope() {
6641
+ const subject = this.#connection.getSnapshot().subject;
6642
+ const scope = subject ? JSON.stringify([subject.tenantId, subject.spaceId, subject.episodeId, subject.participantId, subject.participantGeneration]) : null;
6643
+ if (scope !== this.#deviceScope) {
6644
+ this.#devices.clear();
6645
+ this.#invalidatedDevices.clear();
6646
+ }
6647
+ this.#deviceScope = scope;
6648
+ }
6649
+ #rememberDevices(snapshot) {
6650
+ this.#syncDeviceScope();
6651
+ const control = snapshot?.control;
6652
+ if (!control) return;
6653
+ this.#pruneDevices(control, snapshot.presence);
6654
+ const retained = /* @__PURE__ */ new Map();
6655
+ if (control.status !== "ended") {
6656
+ for (const participant of control.participants) {
6657
+ const device = this.#deviceFor(participant, snapshot.presence?.items ?? []);
6658
+ if (device) retained.set(participant.participantId, device);
6659
+ }
6660
+ }
6661
+ this.#devices = retained;
6662
+ }
6663
+ #pruneDevices(control, presence) {
6664
+ const cursor = presence ? JSON.stringify([presence.projectionId, presence.sequence]) : null;
6665
+ if (cursor !== this.#presenceCursor) this.#invalidatedDevices.clear();
6666
+ this.#presenceCursor = cursor;
6667
+ for (const [id, device] of this.#devices) {
6668
+ if (!control.participants.some((participant) => participant.participantId === id && participant.admissionRevision === device.admissionRevision)) this.#invalidatedDevices.add(id);
6669
+ }
6670
+ }
6671
+ #deviceFor(participant, presence) {
6672
+ const previous = this.#devices.get(participant.participantId);
6673
+ const known = previous && previous.admissionRevision === participant.admissionRevision ? previous : {};
6674
+ const hint = this.#hintFor(participant, presence) ?? {};
6675
+ const identityId = hint.identityId ?? known.identityId;
6676
+ return identityId ? { admissionRevision: participant.admissionRevision, identityId, deviceType: hint.deviceType ?? known.deviceType } : void 0;
6677
+ }
6678
+ #hintFor(participant, presence) {
6679
+ return presence.find((item) => {
6680
+ if (item.participantId !== participant.participantId) return false;
6681
+ if (item.admissionRevision !== void 0) return item.admissionRevision === participant.admissionRevision;
6682
+ return !this.#invalidatedDevices.has(participant.participantId);
6683
+ });
6684
+ }
6574
6685
  #publish(snapshot, collaborationCapabilities, media) {
6686
+ this.#rememberDevices(snapshot);
6575
6687
  if (snapshot && !controlFor(snapshot)) return;
6576
6688
  const audioParticipants = new Set(media?.remoteTracks.filter((track) => track.source === "screen_audio" && track.track.readyState === "live").map((track) => track.participantId));
6577
6689
  if (snapshot?.participantId && media?.localTracks.some((track) => track.source === "screen_audio" && track.enabled && track.track.readyState === "live")) audioParticipants.add(snapshot.participantId);
6578
- const projected = participantsFor(snapshot, collaborationCapabilities);
6690
+ const projected = participantsFor(snapshot, collaborationCapabilities, this.#devices);
6579
6691
  const participants = Object.freeze({
6580
6692
  ...projected,
6581
6693
  roster: Object.freeze(projected.roster.map((participant) => Object.freeze({ ...participant, media: Object.freeze({ ...participant.media, screenShareHasAudio: participant.media.screenShare === "active" && audioParticipants.has(participant.participantId) }) })))
@@ -6601,10 +6713,10 @@ function invalid(message) {
6601
6713
  function directed(result) {
6602
6714
  return Object.freeze({ status: result.result, requestId: result.request_id });
6603
6715
  }
6604
- function participantsFor(snapshot, collaborationCapabilities) {
6716
+ function participantsFor(snapshot, collaborationCapabilities, devices) {
6605
6717
  const control = controlFor(snapshot);
6606
- if (!control) return EMPTY_PARTICIPANTS;
6607
- return Object.freeze({ roster: rosterFor(control.participants, collaborationCapabilities, mediaFor(snapshot), presenceFor(snapshot)), admissionQueue: admissionQueueFor(control.admissionRequests) });
6718
+ if (!control || control.status === "ended") return EMPTY_PARTICIPANTS;
6719
+ return Object.freeze({ roster: rosterFor(control.participants, collaborationCapabilities, mediaFor(snapshot), presenceFor(snapshot), devices), admissionQueue: admissionQueueFor(control.admissionRequests) });
6608
6720
  }
6609
6721
  function controlFor(snapshot) {
6610
6722
  if (!snapshot) return null;
@@ -6618,11 +6730,13 @@ function mediaFor(snapshot) {
6618
6730
  function presenceFor(snapshot) {
6619
6731
  return snapshot?.presence ?? null;
6620
6732
  }
6621
- function rosterFor(participants, collaborationCapabilities, media, presence) {
6733
+ function rosterFor(participants, collaborationCapabilities, media, presence, devices) {
6622
6734
  return Object.freeze(
6623
6735
  participants.map(
6624
6736
  (participant) => Object.freeze({
6625
6737
  participantId: participant.participantId,
6738
+ identityId: devices.get(participant.participantId)?.identityId,
6739
+ deviceType: devices.get(participant.participantId)?.deviceType,
6626
6740
  displayName: participant.displayName,
6627
6741
  role: participant.role,
6628
6742
  eligibleRoles: Object.freeze([...participant.eligibleRoles]),
@@ -6644,13 +6758,20 @@ function admissionQueueFor(requests) {
6644
6758
  function selfFor(snapshot, roster) {
6645
6759
  const participantId = snapshot?.participantId ?? null;
6646
6760
  const participant = roster.find((candidate) => candidate.participantId === participantId);
6647
- if (!participant) return emptySelf(participantId);
6761
+ const identityId = selfIdentityFor(snapshot, participant);
6762
+ if (!participant) return emptySelf(participantId, identityId);
6648
6763
  const capabilities = participant.capabilities;
6649
6764
  const allowed = new Set(capabilities);
6650
- return Object.freeze({ participantId, displayName: participant.displayName, role: participant.role, capabilities, handRaised: participant.handRaised, can: (capability) => allowed.has(capability) });
6765
+ return Object.freeze({ participantId, identityId, displayName: participant.displayName, role: participant.role, capabilities, handRaised: participant.handRaised, can: (capability) => allowed.has(capability) });
6766
+ }
6767
+ function selfIdentityFor(snapshot, participant) {
6768
+ if (!snapshot) return void 0;
6769
+ if (snapshot.connection.phase === "terminal") return void 0;
6770
+ if (snapshot.control?.status === "ended") return void 0;
6771
+ return snapshot.participantIdentityId ?? participant?.identityId;
6651
6772
  }
6652
- function emptySelf(participantId) {
6653
- return Object.freeze({ participantId, displayName: null, role: null, capabilities: Object.freeze([]), handRaised: false, can: () => false });
6773
+ function emptySelf(participantId, identityId) {
6774
+ return Object.freeze({ participantId, identityId, displayName: null, role: null, capabilities: Object.freeze([]), handRaised: false, can: () => false });
6654
6775
  }
6655
6776
  function capabilitiesFor(control, collaboration) {
6656
6777
  const capabilities = /* @__PURE__ */ new Set();
@@ -6684,10 +6805,10 @@ function sameParticipantMedia(left, right) {
6684
6805
  return left.media.microphone === right.media.microphone && left.media.camera === right.media.camera && left.media.screenShare === right.media.screenShare && left.media.screenShareHasAudio === right.media.screenShareHasAudio;
6685
6806
  }
6686
6807
  function sameParticipantPresence(left, right) {
6687
- return left.presence.state === right.presence.state && left.presence.speaking === right.presence.speaking && left.presence.activeSpeaker === right.presence.activeSpeaker;
6808
+ return left.identityId === right.identityId && left.deviceType === right.deviceType && left.presence.state === right.presence.state && left.presence.speaking === right.presence.speaking && left.presence.activeSpeaker === right.presence.activeSpeaker;
6688
6809
  }
6689
6810
  function sameSelf(left, right) {
6690
- return left.participantId === right.participantId && left.displayName === right.displayName && left.role === right.role && left.handRaised === right.handRaised && sameValues(left.capabilities, right.capabilities);
6811
+ return left.participantId === right.participantId && left.identityId === right.identityId && left.displayName === right.displayName && left.role === right.role && left.handRaised === right.handRaised && sameValues(left.capabilities, right.capabilities);
6691
6812
  }
6692
6813
  function sameArray(left, right, equal) {
6693
6814
  return left.length === right.length && left.every((value, index) => equal(value, right[index]));
@@ -84,6 +84,7 @@ var AccessGrantSchema = Schema.Struct({
84
84
  });
85
85
  var AdmitEpisodeParticipantRequestSchema = Schema.Struct({
86
86
  admission: Schema.optional(Schema.Literals(["policy", "direct"])),
87
+ identity_external_id: Schema.optional(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256))),
87
88
  identity_id: Schema.optional(UUIDSchema),
88
89
  metadata: Schema.optional(Schema.Unknown),
89
90
  name: Schema.String.check(Schema.isMinLength(1)),
@@ -1125,7 +1126,8 @@ var AddCloudflareSFUTracksQueryParamsSchema = Schema.Struct({
1125
1126
  "local_signaling_failure",
1126
1127
  "remote_signaling_failure",
1127
1128
  "dormant_connection",
1128
- "access_refresh"
1129
+ "access_refresh",
1130
+ "receive_stalled"
1129
1131
  ])
1130
1132
  )
1131
1133
  });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createChalkEffectClient,
3
3
  parseAccessGrant
4
- } from "./chunk-CJVLTSIO.js";
4
+ } from "./chunk-GOSWSXTH.js";
5
5
 
6
6
  // src/public-invites/client.ts
7
7
  import { Effect } from "effect";
@@ -861,7 +861,8 @@ function strictEvent(value) {
861
861
  }
862
862
  function strictProjectionItem(value, stream) {
863
863
  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"]);
864
+ if (stream === "presence")
865
+ 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
866
  return false;
866
867
  }
867
868
  function strictProjection(value) {
@@ -873,7 +874,13 @@ function strictWelcome(value) {
873
874
  const extension = Array.isArray(value.extensions) && value.extensions.length === 1 ? value.extensions[0] : void 0;
874
875
  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
876
  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"] : []];
877
+ const fields = [
878
+ ...base,
879
+ ...extensionValid ? ["extensions"] : [],
880
+ ...Object.hasOwn(value, "entry_confirmation") ? ["entry_confirmation"] : [],
881
+ ...Object.hasOwn(value, "entry_generation") ? ["entry_generation"] : [],
882
+ ...Object.hasOwn(value, "participant_identity_id") ? ["participant_identity_id"] : []
883
+ ];
877
884
  if (!strictCursor(value.head)) return false;
878
885
  if (value.mode === "snapshot") return exactObjectWithCorrelation(value, [...fields, "snapshot"]) && strictSnapshot(value.snapshot);
879
886
  if (value.mode === "terminal") return exactObjectWithCorrelation(value, [...fields, "reason"]);
@@ -1479,6 +1486,7 @@ var WelcomeBase = {
1479
1486
  protocol: Schema.Literal(1),
1480
1487
  participant_id: UuidSchema,
1481
1488
  participant_generation: PositiveIntegerSchema,
1489
+ participant_identity_id: Schema.optionalKey(UuidSchema),
1482
1490
  recovery_id: UuidSchema,
1483
1491
  head: ControlCursorSchema,
1484
1492
  entry_confirmation: Schema.optionalKey(Schema.Literal(true)),
@@ -1527,9 +1535,15 @@ var CommandIdConflictAckFrameSchema = Schema.Struct({ type: Schema.Literal("ack"
1527
1535
  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
1536
  Schema.makeFilter((item) => !item.enabled || item.publication_id !== null ? void 0 : { path: [], issue: "enabled requires a publication_id" })
1529
1537
  );
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
- );
1538
+ var PresenceProjectionItemSchema = Schema.Struct({
1539
+ participant_id: UuidSchema,
1540
+ state: Schema.Union([Schema.Literal("connected"), Schema.Literal("disconnected")]),
1541
+ speaking: Schema.Boolean,
1542
+ active_speaker: Schema.Boolean,
1543
+ identity_id: Schema.optional(UuidSchema),
1544
+ device_type: Schema.optional(Schema.Literals(["phone", "tablet", "computer"])),
1545
+ admission_revision: Schema.optional(PositiveIntegerSchema)
1546
+ }).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
1547
  var MediaProjectionSnapshotFrameSchema = Schema.Struct({
1534
1548
  type: Schema.Literal("projection_snapshot"),
1535
1549
  stream: Schema.Literal("media"),
@@ -3016,6 +3030,7 @@ var V1SyncClient = class {
3016
3030
  #presence = null;
3017
3031
  #participantId = null;
3018
3032
  #participantGeneration = null;
3033
+ #participantIdentityId;
3019
3034
  #entryGeneration = null;
3020
3035
  #recovery = null;
3021
3036
  #snapshotRecoveryRequested = false;
@@ -3136,6 +3151,7 @@ var V1SyncClient = class {
3136
3151
  this.#socket?.close(1e3, "client stopped");
3137
3152
  this.#socket = null;
3138
3153
  this.#phase = { phase: "stopped" };
3154
+ this.#options.mediaPlane?.remotePublicationsChanged?.([], "unavailable");
3139
3155
  this.#rejectEphemeral("client_stopped");
3140
3156
  this.#collaborationState.disconnect("client_stopped");
3141
3157
  this.#localPublications = [];
@@ -3164,6 +3180,7 @@ var V1SyncClient = class {
3164
3180
  },
3165
3181
  participantId: this.#participantId,
3166
3182
  participantGeneration: this.#participantGeneration,
3183
+ participantIdentityId: this.#participantIdentityId,
3167
3184
  control: this.#control,
3168
3185
  optimisticControl,
3169
3186
  media: copyProjection(this.#media),
@@ -3183,6 +3200,7 @@ var V1SyncClient = class {
3183
3200
  return () => this.#mediaPauseListeners.delete(listener);
3184
3201
  }
3185
3202
  #notifyMediaPause(event) {
3203
+ if (event.name === "episode_ended") this.#options.mediaPlane?.remotePublicationsChanged?.([]);
3186
3204
  const source = event.name === "participant_microphone_stopped" ? "microphone" : event.name === "participant_camera_stopped" ? "camera" : event.name === "participant_screen_share_stopped" ? "screen" : null;
3187
3205
  if (source && "participant_id" in event.payload && event.payload.participant_id === this.#participantId) {
3188
3206
  for (const listener of this.#mediaPauseListeners) listener({ source, revision: event.revision });
@@ -3444,6 +3462,7 @@ var V1SyncClient = class {
3444
3462
  const control = this.#requireControl();
3445
3463
  this.#send({ type: "delivery_ack", stream: "control", revision: control.revision, state_digest: control.stateDigest });
3446
3464
  }
3465
+ if (this.#control?.status === "ended" || frame.name === "participant_left" && frame.payload.participant_id === this.#participantId) this.#participantIdentityId = void 0;
3447
3466
  if (frame.name === "participant_left" && frame.payload.participant_id === this.#participantId) {
3448
3467
  this.#phase = { phase: "terminal", terminalReason: frame.payload.reason === "removed" ? "participant_removed" : "participant_inactive" };
3449
3468
  this.#socket?.close(1e3, "participant inactive");
@@ -3534,6 +3553,7 @@ var V1SyncClient = class {
3534
3553
  this.#entryGeneration = frame.entry_generation ?? this.#entryGeneration;
3535
3554
  this.#participantId = frame.participant_id;
3536
3555
  this.#participantGeneration = frame.participant_generation;
3556
+ this.#participantIdentityId = frame.mode === "terminal" ? void 0 : frame.participant_identity_id;
3537
3557
  this.#updateLocalMediaStates();
3538
3558
  if (frame.mode === "terminal") {
3539
3559
  await this.#commandScheduler.settleTerminalRecovery(frame);
@@ -3547,6 +3567,7 @@ var V1SyncClient = class {
3547
3567
  const control = await restoreV1Snapshot(frame.snapshot);
3548
3568
  if (!sameHead(control, frame.head)) throw new V1ReplicaError("snapshot head mismatch");
3549
3569
  this.#control = control;
3570
+ if (control.status === "ended") this.#options.mediaPlane?.remotePublicationsChanged?.([]);
3550
3571
  this.#snapshotRevision = control.revision;
3551
3572
  this.#snapshotRecoveryRequested = false;
3552
3573
  this.#controlHeads.clear();
@@ -3592,7 +3613,7 @@ var V1SyncClient = class {
3592
3613
  previous.find((candidate) => mediaKey(candidate) === mediaKey(item)),
3593
3614
  item
3594
3615
  );
3595
- if (!this.#mediaEnded()) this.#options.mediaPlane?.remotePublicationsChanged?.();
3616
+ if (!this.#mediaEnded()) this.#options.mediaPlane?.remotePublicationsChanged?.(this.#media?.items ?? []);
3596
3617
  } else {
3597
3618
  this.#presence = { projectionId: frame.projection_id, sequence: 0, items: frame.items.map(presenceItem) };
3598
3619
  this.#presenceEventEvidence.clear();
@@ -3611,7 +3632,7 @@ var V1SyncClient = class {
3611
3632
  rememberBoundedEvidence(this.#mediaEventEvidence, frame.sequence, frameSignature(frame), MAX_PROJECTION_EVENT_EVIDENCE);
3612
3633
  this.#notifyRemotePublicationResumed(previous, item);
3613
3634
  if (!this.#mediaEnded() && item.participantId !== this.#participantId && (previous?.enabled !== item.enabled || previous?.publicationId !== item.publicationId)) {
3614
- this.#options.mediaPlane?.remotePublicationsChanged?.();
3635
+ this.#options.mediaPlane?.remotePublicationsChanged?.(this.#media?.items ?? []);
3615
3636
  }
3616
3637
  } else {
3617
3638
  if (this.#acceptProjectionDuplicate(this.#presence, this.#presenceEventEvidence, frame)) return;
@@ -3694,6 +3715,7 @@ var V1SyncClient = class {
3694
3715
  this.#commandScheduler.disconnect();
3695
3716
  this.#media = null;
3696
3717
  this.#presence = null;
3718
+ this.#options.mediaPlane?.remotePublicationsChanged?.([], this.#mediaEnded() ? "available" : "unavailable");
3697
3719
  this.#rejectRequests("disconnected_before_delivery");
3698
3720
  this.#collaborationState.disconnect("disconnected_before_delivery");
3699
3721
  if (!this.#started || this.#phase.phase === "terminal") return;
@@ -3900,7 +3922,7 @@ function mediaItem(item) {
3900
3922
  return { participantId: item.participant_id, source: item.source, enabled: item.enabled, publicationId: item.publication_id };
3901
3923
  }
3902
3924
  function presenceItem(item) {
3903
- return { participantId: item.participant_id, state: item.state, speaking: item.speaking, activeSpeaker: item.active_speaker };
3925
+ 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
3926
  }
3905
3927
  function mediaKey(item) {
3906
3928
  return `${item.participantId}:${item.source}`;
@@ -3928,6 +3950,8 @@ function validMediaPublication(publication) {
3928
3950
  }
3929
3951
  function entryUrl(value, generation, confirmation) {
3930
3952
  const url = new URL(value);
3953
+ url.searchParams.set("participant_devices", "1");
3954
+ url.searchParams.set("participant_device_admissions", "2");
3931
3955
  url.searchParams.set("entry_generation", generation === null ? "new" : String(generation));
3932
3956
  if (confirmation) url.searchParams.set("entry_confirmation", "1");
3933
3957
  return url.toString();
package/dist/client.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const createChalkEffectClient: (options: ChalkEffectClientOptions
23
23
  readonly tenant_id: import("effect/Schema").brand<import("effect/Schema").String, "TenantId">;
24
24
  }>>, import("effect/Schema").toCodecStringTree<import("effect/Schema").Struct<{
25
25
  readonly allow_partial_remote_tracks: import("effect/Schema").optional<import("effect/Schema").Boolean>;
26
- readonly replacement_cause: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["user_pause", "moderation_pause", "sync_recovery_loss", "discovery_absent", "capture_cleanup", "consent_revoked", "publish_failure", "enable_failure", "projection_mismatch", "forced_pause", "local_signaling_failure", "remote_signaling_failure", "dormant_connection", "access_refresh"]>>;
26
+ readonly replacement_cause: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["user_pause", "moderation_pause", "sync_recovery_loss", "discovery_absent", "capture_cleanup", "consent_revoked", "publish_failure", "enable_failure", "projection_mismatch", "forced_pause", "local_signaling_failure", "remote_signaling_failure", "dormant_connection", "access_refresh", "receive_stalled"]>>;
27
27
  }>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
28
28
  readonly connection_id: import("effect/Schema").String;
29
29
  readonly session_description: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Struct<{
@@ -2478,6 +2478,7 @@ export declare const createChalkEffectClient: (options: ChalkEffectClientOptions
2478
2478
  readonly tenant_id: import("effect/Schema").brand<import("effect/Schema").String, "TenantId">;
2479
2479
  }>>, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
2480
2480
  readonly admission: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["policy", "direct"]>>;
2481
+ readonly identity_external_id: import("effect/Schema").optional<import("effect/Schema").String>;
2481
2482
  readonly identity_id: import("effect/Schema").optional<import("effect/Schema").brand<import("effect/Schema").String, "UUID">>;
2482
2483
  readonly metadata: import("effect/Schema").optional<import("effect/Schema").Unknown>;
2483
2484
  readonly name: import("effect/Schema").String;
@@ -68,7 +68,8 @@ export type ConnectionSyncClient = V1CollaborationClient & {
68
68
  readonly requestStartCamera: (participantId: string) => Promise<V1DirectedRequestResult>;
69
69
  };
70
70
  export type ConnectionMediaClient = ClientMediaPlane & {
71
- readonly start: (stream: MediaStream) => Promise<void>;
71
+ /** Starts receiving immediately; resolves the optional initial publication policy before sending. */
72
+ readonly start: (stream: MediaStream, beforePublish?: () => Promise<MediaStream>) => Promise<void>;
72
73
  readonly stop: () => void;
73
74
  readonly restart: (input: CloudflareSFUBootstrap | ParticipantMediaAccess) => Promise<void>;
74
75
  readonly prepareLocalTrack: (source: "microphone" | "camera" | "screen" | "screen_audio", track: MediaStreamTrack) => void;
package/dist/effect.js CHANGED
@@ -17,8 +17,8 @@ import {
17
17
  makeSpaceClientCoreLayer,
18
18
  makeSpaceClientCoreLayerFromServices,
19
19
  makeSpaceStoreLayer
20
- } from "./chunk-I7Y4HR2P.js";
21
- import "./chunk-YIDKETOX.js";
20
+ } from "./chunk-DHUHTIU7.js";
21
+ import "./chunk-3TGVTP6Z.js";
22
22
  import {
23
23
  APIKeyListSchema,
24
24
  APIKeyWithSecretSchema,
@@ -841,8 +841,8 @@ import {
841
841
  WhiteboardStorageUnavailableErrorSchema,
842
842
  WhiteboardStorageUnavailableErrorWireSchema,
843
843
  createChalkEffectClient
844
- } from "./chunk-CJVLTSIO.js";
845
- import "./chunk-5YPQNMJE.js";
844
+ } from "./chunk-GOSWSXTH.js";
845
+ import "./chunk-XSB2GDRW.js";
846
846
 
847
847
  // src/space-client/effect.ts
848
848
  import { Context, Effect, Exit, Layer, Scope } from "effect";
@@ -7,7 +7,7 @@ declare const ChalkApi_base: HttpApi.HttpApi<"ChalkApi", HttpApiGroup.HttpApiGro
7
7
  readonly tenant_id: import("effect/Schema").brand<import("effect/Schema").String, "TenantId">;
8
8
  }>>, import("effect/Schema").toCodecStringTree<import("effect/Schema").Struct<{
9
9
  readonly allow_partial_remote_tracks: import("effect/Schema").optional<import("effect/Schema").Boolean>;
10
- readonly replacement_cause: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["user_pause", "moderation_pause", "sync_recovery_loss", "discovery_absent", "capture_cleanup", "consent_revoked", "publish_failure", "enable_failure", "projection_mismatch", "forced_pause", "local_signaling_failure", "remote_signaling_failure", "dormant_connection", "access_refresh"]>>;
10
+ readonly replacement_cause: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["user_pause", "moderation_pause", "sync_recovery_loss", "discovery_absent", "capture_cleanup", "consent_revoked", "publish_failure", "enable_failure", "projection_mismatch", "forced_pause", "local_signaling_failure", "remote_signaling_failure", "dormant_connection", "access_refresh", "receive_stalled"]>>;
11
11
  }>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
12
12
  readonly connection_id: import("effect/Schema").String;
13
13
  readonly session_description: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Struct<{
@@ -2434,6 +2434,7 @@ declare const ChalkApi_base: HttpApi.HttpApi<"ChalkApi", HttpApiGroup.HttpApiGro
2434
2434
  readonly tenant_id: import("effect/Schema").brand<import("effect/Schema").String, "TenantId">;
2435
2435
  }>>, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
2436
2436
  readonly admission: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["policy", "direct"]>>;
2437
+ readonly identity_external_id: import("effect/Schema").optional<import("effect/Schema").String>;
2437
2438
  readonly identity_id: import("effect/Schema").optional<import("effect/Schema").brand<import("effect/Schema").String, "UUID">>;
2438
2439
  readonly metadata: import("effect/Schema").optional<import("effect/Schema").Unknown>;
2439
2440
  readonly name: import("effect/Schema").String;
@@ -89,6 +89,7 @@ export declare const AccessGrantSchema: Schema.Struct<{
89
89
  export type AccessGrant = typeof AccessGrantSchema.Type;
90
90
  export declare const AdmitEpisodeParticipantRequestSchema: Schema.Struct<{
91
91
  readonly admission: Schema.optional<Schema.Literals<readonly ["policy", "direct"]>>;
92
+ readonly identity_external_id: Schema.optional<Schema.String>;
92
93
  readonly identity_id: Schema.optional<Schema.brand<Schema.String, "UUID">>;
93
94
  readonly metadata: Schema.optional<Schema.Unknown>;
94
95
  readonly name: Schema.String;
@@ -1358,7 +1359,7 @@ export declare const AddCloudflareSFUTracksPathParamsSchema: Schema.Struct<{
1358
1359
  export type AddCloudflareSFUTracksPathParams = typeof AddCloudflareSFUTracksPathParamsSchema.Type;
1359
1360
  export declare const AddCloudflareSFUTracksQueryParamsSchema: Schema.Struct<{
1360
1361
  readonly allow_partial_remote_tracks: Schema.optional<Schema.Boolean>;
1361
- readonly replacement_cause: Schema.optional<Schema.Literals<readonly ["user_pause", "moderation_pause", "sync_recovery_loss", "discovery_absent", "capture_cleanup", "consent_revoked", "publish_failure", "enable_failure", "projection_mismatch", "forced_pause", "local_signaling_failure", "remote_signaling_failure", "dormant_connection", "access_refresh"]>>;
1362
+ readonly replacement_cause: Schema.optional<Schema.Literals<readonly ["user_pause", "moderation_pause", "sync_recovery_loss", "discovery_absent", "capture_cleanup", "consent_revoked", "publish_failure", "enable_failure", "projection_mismatch", "forced_pause", "local_signaling_failure", "remote_signaling_failure", "dormant_connection", "access_refresh", "receive_stalled"]>>;
1362
1363
  }>;
1363
1364
  export type AddCloudflareSFUTracksQueryParams = typeof AddCloudflareSFUTracksQueryParamsSchema.Type;
1364
1365
  export declare const AddCloudflareSFUTracksRequestBodySchema: Schema.Struct<{
@@ -1426,6 +1427,7 @@ export declare const AdmitEpisodeParticipantRequestHeadersSchema: Schema.Struct<
1426
1427
  export type AdmitEpisodeParticipantRequestHeaders = typeof AdmitEpisodeParticipantRequestHeadersSchema.Type;
1427
1428
  export declare const AdmitEpisodeParticipantRequestBodySchema: Schema.Struct<{
1428
1429
  readonly admission: Schema.optional<Schema.Literals<readonly ["policy", "direct"]>>;
1430
+ readonly identity_external_id: Schema.optional<Schema.String>;
1429
1431
  readonly identity_id: Schema.optional<Schema.brand<Schema.String, "UUID">>;
1430
1432
  readonly metadata: Schema.optional<Schema.Unknown>;
1431
1433
  readonly name: Schema.String;