@q9labsai/chalk-client 4.1.11 → 4.1.13

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 (52) hide show
  1. package/dist/{chunk-FJXN6BVS.js → chunk-DV433YIW.js} +51 -22
  2. package/dist/{chunk-JPNE4K2L.js → chunk-EBACU63B.js} +3 -2
  3. package/dist/{chunk-DFLTEYWX.js → chunk-IYRVIFWN.js} +3 -3
  4. package/dist/{chunk-QVHR3RMB.js → chunk-NKOKY2AE.js} +21 -1
  5. package/dist/{chunk-C4BIONCP.js → chunk-S5OUHFGC.js} +174 -13
  6. package/dist/client.d.ts +1 -0
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/connection/dependencies.d.ts +3 -1
  9. package/dist/connection/dependencies.d.ts.map +1 -1
  10. package/dist/connection/index.d.ts +2 -0
  11. package/dist/connection/index.d.ts.map +1 -1
  12. package/dist/connection/lifecycle.d.ts.map +1 -1
  13. package/dist/effect.js +5 -5
  14. package/dist/generated/http-api.d.ts +1 -0
  15. package/dist/generated/http-api.d.ts.map +1 -1
  16. package/dist/generated/schemas.d.ts +2 -0
  17. package/dist/generated/schemas.d.ts.map +1 -1
  18. package/dist/index.js +25 -25
  19. package/dist/media/client.d.ts.map +1 -1
  20. package/dist/media/index.js +1 -1
  21. package/dist/media/transport.d.ts.map +1 -1
  22. package/dist/media/types.d.ts +12 -1
  23. package/dist/media/types.d.ts.map +1 -1
  24. package/dist/public-invites/index.js +3 -3
  25. package/dist/public-invites/types.d.ts +1 -0
  26. package/dist/public-invites/types.d.ts.map +1 -1
  27. package/dist/server/index.cjs +1 -1
  28. package/dist/server/index.js +1 -1
  29. package/dist/server/types.d.cts +1 -1
  30. package/dist/server/types.d.ts +1 -1
  31. package/dist/server/types.d.ts.map +1 -1
  32. package/dist/space-client/core.d.ts +2 -0
  33. package/dist/space-client/core.d.ts.map +1 -1
  34. package/dist/space-client/participants-controller.d.ts.map +1 -1
  35. package/dist/telemetry/attributes.d.ts +1 -1
  36. package/dist/telemetry/attributes.d.ts.map +1 -1
  37. package/dist/telemetry/index.js +1 -1
  38. package/dist/telemetry/rtc.d.ts +6 -0
  39. package/dist/telemetry/rtc.d.ts.map +1 -1
  40. package/dist/whiteboard/types.d.ts +1 -0
  41. package/dist/whiteboard/types.d.ts.map +1 -1
  42. package/dist/whiteboard/v1-client.d.ts +2 -1
  43. package/dist/whiteboard/v1-client.d.ts.map +1 -1
  44. package/dist/whiteboard/v1-codec.d.ts +1 -2
  45. package/dist/whiteboard/v1-codec.d.ts.map +1 -1
  46. package/package.json +2 -2
  47. package/dist/episode-diagnostic-runtime.test.helpers.d.ts +0 -3
  48. package/dist/episode-diagnostic-runtime.test.helpers.d.ts.map +0 -1
  49. package/dist/space-client/controller-parity.test.helpers.d.ts +0 -151
  50. package/dist/space-client/controller-parity.test.helpers.d.ts.map +0 -1
  51. package/dist/space-client/media-controller.test.helpers.d.ts +0 -105
  52. package/dist/space-client/media-controller.test.helpers.d.ts.map +0 -1
@@ -2,21 +2,21 @@ import {
2
2
  CloudflareRTKClient,
3
3
  CloudflareSFUClient,
4
4
  createCloudflareSFUHTTPTransport
5
- } from "./chunk-C4BIONCP.js";
5
+ } from "./chunk-S5OUHFGC.js";
6
6
  import {
7
7
  AccessGrantError,
8
8
  requireParsedAccessGrant,
9
9
  validEpisodeDiagnosticCredential
10
- } from "./chunk-JPNE4K2L.js";
10
+ } from "./chunk-EBACU63B.js";
11
+ import {
12
+ createTraceContext,
13
+ parseTraceparent
14
+ } from "./chunk-NKOKY2AE.js";
11
15
  import {
12
16
  createBrowserSyncLifecycle,
13
17
  createBrowserWebSocketFactory,
14
18
  createV1SyncClient
15
19
  } from "./chunk-5VXGLI3T.js";
16
- import {
17
- createTraceContext,
18
- parseTraceparent
19
- } from "./chunk-QVHR3RMB.js";
20
20
 
21
21
  // src/access/manager.ts
22
22
  import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
@@ -885,6 +885,7 @@ var ChalkWhiteboardV1Client = class {
885
885
  #options;
886
886
  #store;
887
887
  #listeners = /* @__PURE__ */ new Set();
888
+ #summaryListeners = /* @__PURE__ */ new Set();
888
889
  #operations = /* @__PURE__ */ new Map();
889
890
  #reservedOperationIds = /* @__PURE__ */ new Set();
890
891
  #awaitingOperationIds = /* @__PURE__ */ new Set();
@@ -989,6 +990,11 @@ var ChalkWhiteboardV1Client = class {
989
990
  if (this.#snapshots.size === 0 && !this.#waitingForOperations) void this.#requestSnapshot().catch(() => void 0);
990
991
  return () => this.#listeners.delete(listener);
991
992
  }
993
+ subscribeSummary(listener) {
994
+ this.#summaryListeners.add(listener);
995
+ this.#notifySummaryListener(listener, this.#summary());
996
+ return () => this.#summaryListeners.delete(listener);
997
+ }
992
998
  submitUpdate(input) {
993
999
  this.#assertLive("submit_update");
994
1000
  if (!this.#canDraw || !this.#capabilities.includes("drawWhiteboard")) {
@@ -1480,7 +1486,15 @@ var ChalkWhiteboardV1Client = class {
1480
1486
  #publishSummary(status, summaryError) {
1481
1487
  this.#summaryStatus = status;
1482
1488
  this.#summaryError = summaryError;
1483
- const summary = {
1489
+ const summary = this.#summary();
1490
+ try {
1491
+ this.#options.onSummary?.(summary);
1492
+ } catch {
1493
+ }
1494
+ for (const listener of this.#summaryListeners) this.#notifySummaryListener(listener, summary);
1495
+ }
1496
+ #summary() {
1497
+ return {
1484
1498
  status: this.#summaryStatus,
1485
1499
  sceneId: this.#sceneId,
1486
1500
  revision: this.#revision,
@@ -1490,8 +1504,10 @@ var ChalkWhiteboardV1Client = class {
1490
1504
  presenting: this.#presenting,
1491
1505
  error: this.#summaryError
1492
1506
  };
1507
+ }
1508
+ #notifySummaryListener(listener, summary) {
1493
1509
  try {
1494
- this.#options.onSummary?.(summary);
1510
+ listener(summary);
1495
1511
  } catch {
1496
1512
  }
1497
1513
  }
@@ -1739,9 +1755,15 @@ function createMediaClient(apiBaseURL, input) {
1739
1755
  );
1740
1756
  }
1741
1757
  const { subject } = input.access;
1758
+ const replaceMediaConnection = input.replaceMediaConnection;
1742
1759
  const client = new CloudflareSFUClient({
1743
1760
  bootstrap: input.access.media.clientPayload,
1744
1761
  participantId: subject.participantId,
1762
+ replaceMediaConnection: replaceMediaConnection ? async () => {
1763
+ const replacement = await replaceMediaConnection();
1764
+ if (replacement.provider !== "cloudflare_sfu") throw new TypeError("The Cloudflare SFU adapter requires a Cloudflare SFU access grant");
1765
+ return replacement.clientPayload;
1766
+ } : void 0,
1745
1767
  transport: createCloudflareSFUHTTPTransport({
1746
1768
  apiBaseURL,
1747
1769
  credential: input.credential,
@@ -1751,6 +1773,7 @@ function createMediaClient(apiBaseURL, input) {
1751
1773
  participantId: subject.participantId
1752
1774
  }),
1753
1775
  onError: input.onFailure,
1776
+ ...input.recordRtcSummary ? { onRtcSummary: input.recordRtcSummary } : {},
1754
1777
  onScreenEnded: input.onScreenEnded
1755
1778
  });
1756
1779
  return createSFUConnectionMediaClient(client);
@@ -2135,6 +2158,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2135
2158
  try: () => platform.createMediaClient({
2136
2159
  access: grant,
2137
2160
  credential: () => toPromise(access.getMediaToken()),
2161
+ replaceMediaConnection: () => toPromise(access.refresh("media_recovery", true)).then((replacement) => replacement.media),
2162
+ ...options.recordRtcSummary ? { recordRtcSummary: options.recordRtcSummary } : {},
2138
2163
  onFailure: () => enqueueBackground(handleMediaFailure()),
2139
2164
  onScreenEnded: () => enqueueBackground(notifyScreenEnded())
2140
2165
  }),
@@ -3480,7 +3505,8 @@ var MediaControllerRuntime = class {
3480
3505
  if (isAccessInvalid2(input.cause)) return Effect5.fail(input.cause);
3481
3506
  input.restoreIntent?.();
3482
3507
  const rollback = input.track ? this.#discardCaptured(input.source, input.track, input.media, input.prepared) : input.fallback;
3483
- const failure2 = isPermissionDenied(input.cause) ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
3508
+ const captureWasUnavailable = input.action === "startScreenShare" && !input.prepared && input.track === null;
3509
+ const failure2 = isPermissionDenied(input.cause) || captureWasUnavailable ? captureError(input.cause, input.action, input.permissionMessage) : input.cause;
3484
3510
  return rollback.pipe(Effect5.andThen(Effect5.sync(() => this.#publish())), Effect5.andThen(Effect5.fail(failure2)));
3485
3511
  }
3486
3512
  #assertActivePorts(ports, action) {
@@ -3663,7 +3689,7 @@ function sameDeviceList(left, right) {
3663
3689
  }
3664
3690
  function captureError(cause, action, permissionMessage) {
3665
3691
  if (isPermissionDenied(cause)) return new ConnectionError({ code: "permission_denied", action, recoverable: true, message: permissionMessage }, { cause });
3666
- return new ConnectionError({ code: "unsupported_environment", action, recoverable: false, message: "Media capture is unavailable" }, { cause });
3692
+ return new ConnectionError({ code: "unsupported_environment", action, recoverable: false, message: action === "startScreenShare" ? "Screen sharing is unavailable in this browser." : "Media capture is unavailable" }, { cause });
3667
3693
  }
3668
3694
  function isPermissionDenied(cause) {
3669
3695
  return cause instanceof DOMException && (cause.name === "NotAllowedError" || cause.name === "SecurityError");
@@ -3893,16 +3919,24 @@ function participantPresence(participantId, presence) {
3893
3919
  return Object.freeze({ state: item.state, speaking: item.speaking, activeSpeaker: item.activeSpeaker });
3894
3920
  }
3895
3921
  function sameParticipants(left, right) {
3896
- return sameArray(
3897
- left.roster,
3898
- right.roster,
3899
- (current, next) => current.participantId === next.participantId && current.displayName === next.displayName && current.role === next.role && current.handRaised === next.handRaised && sameValues(current.eligibleRoles, next.eligibleRoles) && sameValues(current.capabilities, next.capabilities) && current.media.microphone === next.media.microphone && current.media.camera === next.media.camera && current.media.screenShare === next.media.screenShare && current.presence.state === next.presence.state && current.presence.speaking === next.presence.speaking && current.presence.activeSpeaker === next.presence.activeSpeaker
3900
- ) && sameArray(
3922
+ return sameArray(left.roster, right.roster, sameParticipant) && sameArray(
3901
3923
  left.admissionQueue,
3902
3924
  right.admissionQueue,
3903
3925
  (current, next) => current.requestId === next.requestId && current.participantId === next.participantId && current.displayName === next.displayName && current.initialRole === next.initialRole && current.expiresAt === next.expiresAt && sameValues(current.eligibleRoles, next.eligibleRoles)
3904
3926
  );
3905
3927
  }
3928
+ function sameParticipant(left, right) {
3929
+ return sameParticipantProfile(left, right) && sameParticipantMedia(left, right) && sameParticipantPresence(left, right);
3930
+ }
3931
+ function sameParticipantProfile(left, right) {
3932
+ return left.participantId === right.participantId && left.displayName === right.displayName && left.role === right.role && left.handRaised === right.handRaised && sameValues(left.eligibleRoles, right.eligibleRoles) && sameValues(left.capabilities, right.capabilities);
3933
+ }
3934
+ function sameParticipantMedia(left, right) {
3935
+ return left.media.microphone === right.media.microphone && left.media.camera === right.media.camera && left.media.screenShare === right.media.screenShare;
3936
+ }
3937
+ function sameParticipantPresence(left, right) {
3938
+ return left.presence.state === right.presence.state && left.presence.speaking === right.presence.speaking && left.presence.activeSpeaker === right.presence.activeSpeaker;
3939
+ }
3906
3940
  function sameSelf(left, right) {
3907
3941
  return left.participantId === right.participantId && left.displayName === right.displayName && left.role === right.role && left.handRaised === right.handRaised && sameValues(left.capabilities, right.capabilities);
3908
3942
  }
@@ -4797,7 +4831,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
4797
4831
  now: baseDependencies.clock.now,
4798
4832
  setTimeout: baseDependencies.clock.setTimeout,
4799
4833
  clearTimeout: baseDependencies.clock.clearTimeout,
4800
- release: { id: "chalk-client@4.1.11" }
4834
+ release: { id: "chalk-client@4.1.13" }
4801
4835
  });
4802
4836
  const diagnosticSyncClients = /* @__PURE__ */ new Set();
4803
4837
  const dependencies = {
@@ -4818,6 +4852,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
4818
4852
  initialMicrophoneEnabled: platform.initialMicrophoneEnabled,
4819
4853
  initialCameraEnabled: platform.initialCameraEnabled,
4820
4854
  telemetry: platform.telemetry,
4855
+ recordRtcSummary: platform.recordRtcSummary,
4821
4856
  dependencies,
4822
4857
  diagnostics: {
4823
4858
  onEvent: (event) => {
@@ -4861,7 +4896,6 @@ var SpaceClientCore = class {
4861
4896
  #previous;
4862
4897
  #unsubscribeConnection = null;
4863
4898
  #unsubscribeStore = null;
4864
- #unsupportedReported = false;
4865
4899
  #leaveOperation;
4866
4900
  #participantJoinOperation;
4867
4901
  #syncConnectOperation;
@@ -4998,11 +5032,6 @@ var SpaceClientCore = class {
4998
5032
  #publishConnectionDiagnostics() {
4999
5033
  const snapshot = this.#connection.getSnapshot();
5000
5034
  if (!this.#episodeDiagnostics || snapshot.state !== "live") return;
5001
- if (!this.#unsupportedReported) {
5002
- this.#unsupportedReported = true;
5003
- const unsupported = this.#episodeDiagnostics.startOperation("whiteboard.unsupported");
5004
- unsupported?.succeed({ status: "unsupported" });
5005
- }
5006
5035
  if (!this.#participantJoinOperation) {
5007
5036
  this.#participantJoinOperation = this.#episodeDiagnostics.startOperation("participant.join");
5008
5037
  this.#participantJoinOperation?.succeed({ status: "live" });
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  journeyHeaders
3
- } from "./chunk-QVHR3RMB.js";
3
+ } from "./chunk-NKOKY2AE.js";
4
4
 
5
5
  // src/generated/schemas.ts
6
6
  import { Schema, SchemaGetter } from "effect";
@@ -757,7 +757,8 @@ var RecordingReservationSchema = Schema.Struct({
757
757
  updated_at: DateTimeStringSchema
758
758
  });
759
759
  var RefreshSpacePublicInviteAccessRequestSchema = Schema.Struct({
760
- media_proof: Schema.String.check(Schema.isMinLength(1))
760
+ media_proof: Schema.String.check(Schema.isMinLength(1)),
761
+ replace_media_connection: Schema.Boolean
761
762
  });
762
763
  var RegionsSchema = Schema.Struct({
763
764
  regions: Schema.Array(
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createChalkEffectClient,
3
3
  parseAccessGrant
4
- } from "./chunk-JPNE4K2L.js";
4
+ } from "./chunk-EBACU63B.js";
5
5
 
6
6
  // src/public-invites/client.ts
7
7
  import { Effect } from "effect";
@@ -59,7 +59,7 @@ function createChalkPublicClient(options) {
59
59
  const response = await Effect.runPromise(
60
60
  client.spaces.refreshSpacePublicInviteAccess({
61
61
  headers: { "X-Chalk-Arrival-Handle": normalized.arrivalHandle },
62
- payload: { media_proof: required(normalized.mediaProof, "mediaProof") }
62
+ payload: { media_proof: required(normalized.mediaProof, "mediaProof"), replace_media_connection: normalized.replaceMediaConnection ?? false }
63
63
  })
64
64
  );
65
65
  return parseAccessGrant(response);
@@ -109,7 +109,7 @@ function arrivalReference(reference) {
109
109
  return { arrivalHandle: required(reference.arrivalHandle, "arrivalHandle"), guestCredential: reference.guestCredential };
110
110
  }
111
111
  function refreshInput(input, arrival) {
112
- const values = typeof input === "string" ? { arrivalHandle: arrival?.arrivalHandle, guestCredential: arrival?.guestCredential, mediaProof: input } : { arrivalHandle: input.arrivalHandle ?? arrival?.arrivalHandle, guestCredential: input.guestCredential ?? arrival?.guestCredential, mediaProof: input.mediaProof };
112
+ const values = typeof input === "string" ? { arrivalHandle: arrival?.arrivalHandle, guestCredential: arrival?.guestCredential, mediaProof: input } : { arrivalHandle: input.arrivalHandle ?? arrival?.arrivalHandle, guestCredential: input.guestCredential ?? arrival?.guestCredential, mediaProof: input.mediaProof, replaceMediaConnection: input.replaceMediaConnection };
113
113
  return {
114
114
  ...values,
115
115
  arrivalHandle: required(values.arrivalHandle ?? "", "arrivalHandle")
@@ -22,6 +22,8 @@ var TELEMETRY_BUILT_IN_ATTRIBUTE_KEYS = [
22
22
  "connection_state",
23
23
  "ice_connection_state",
24
24
  "signaling_state",
25
+ "dtls_state",
26
+ "selected_candidate_pair_state",
25
27
  "bytes_received",
26
28
  "bytes_sent",
27
29
  "frames_dropped",
@@ -615,7 +617,9 @@ function summarizeRtcStats(stats) {
615
617
  packetsReceived: 0,
616
618
  packetsSent: 0,
617
619
  roundTripTimeSamples: [],
618
- transportEntries: 0
620
+ transportEntries: 0,
621
+ dtlsStates: [],
622
+ selectedCandidatePairStates: []
619
623
  };
620
624
  for (const stat of stats) {
621
625
  statHandlers[stat.type ?? ""]?.(accumulator, stat);
@@ -634,6 +638,8 @@ function rtcSummaryAttributes(connection, stats) {
634
638
  packets_received: summary.packetsReceived,
635
639
  packets_sent: summary.packetsSent,
636
640
  transport_entries: summary.transportEntries,
641
+ ...summary.dtlsState !== void 0 ? { dtls_state: summary.dtlsState } : {},
642
+ ...summary.selectedCandidatePairState !== void 0 ? { selected_candidate_pair_state: summary.selectedCandidatePairState } : {},
637
643
  ...connection.connectionState !== void 0 ? { connection_state: connection.connectionState } : {},
638
644
  ...connection.iceConnectionState !== void 0 ? { ice_connection_state: connection.iceConnectionState } : {},
639
645
  ...summary.jitterMs !== void 0 ? { jitter_ms: summary.jitterMs } : {},
@@ -663,6 +669,13 @@ function accumulateOutbound(accumulator, stat) {
663
669
  function accumulateTransport(accumulator, stat) {
664
670
  accumulator.transportEntries += 1;
665
671
  appendFinite(accumulator.roundTripTimeSamples, stat.roundTripTime);
672
+ appendRtcState(accumulator.dtlsStates, stat.dtlsState, ["new", "connecting", "connected", "closed", "failed"]);
673
+ if (stat.type === "candidate-pair" && (stat.selected === true || stat.nominated === true)) {
674
+ appendRtcState(accumulator.selectedCandidatePairStates, stat.state, ["frozen", "waiting", "in-progress", "failed", "succeeded"]);
675
+ }
676
+ }
677
+ function appendRtcState(states, value, allowed) {
678
+ if (value !== void 0 && allowed.includes(value) && !states.includes(value)) states.push(value);
666
679
  }
667
680
  function appendFinite(samples, value) {
668
681
  if (isFiniteNumber(value)) samples.push(value);
@@ -670,6 +683,8 @@ function appendFinite(samples, value) {
670
683
  function finalizeAccumulator(accumulator) {
671
684
  const jitterMs = averageMilliseconds(accumulator.jitterSamples);
672
685
  const roundTripTimeMs = averageMilliseconds(accumulator.roundTripTimeSamples);
686
+ const dtlsState = lastValue(accumulator.dtlsStates);
687
+ const selectedCandidatePairState = lastValue(accumulator.selectedCandidatePairStates);
673
688
  return {
674
689
  bytesReceived: accumulator.bytesReceived,
675
690
  bytesSent: accumulator.bytesSent,
@@ -680,10 +695,15 @@ function finalizeAccumulator(accumulator) {
680
695
  packetsReceived: accumulator.packetsReceived,
681
696
  packetsSent: accumulator.packetsSent,
682
697
  transportEntries: accumulator.transportEntries,
698
+ ...dtlsState !== void 0 ? { dtlsState } : {},
699
+ ...selectedCandidatePairState !== void 0 ? { selectedCandidatePairState } : {},
683
700
  ...jitterMs !== void 0 ? { jitterMs } : {},
684
701
  ...roundTripTimeMs !== void 0 ? { roundTripTimeMs } : {}
685
702
  };
686
703
  }
704
+ function lastValue(values) {
705
+ return values.at(-1);
706
+ }
687
707
  function averageMilliseconds(samples) {
688
708
  if (samples.length === 0) return void 0;
689
709
  return Math.round(samples.reduce((total, sample) => total + sample, 0) / samples.length * 1e3);
@@ -25,12 +25,14 @@ function resolveMediaTarget(participantId, stopped, tracks, target) {
25
25
 
26
26
  // src/media/types.ts
27
27
  var CloudflareSFUError = class extends Error {
28
- constructor(message, code) {
28
+ constructor(message, code, options = {}) {
29
29
  super(message);
30
30
  this.code = code;
31
+ this.options = options;
31
32
  this.name = "CloudflareSFUError";
32
33
  }
33
34
  code;
35
+ options;
34
36
  };
35
37
 
36
38
  // src/media/tracks.ts
@@ -138,10 +140,12 @@ var CloudflareSFUClient = class {
138
140
  #localListeners = /* @__PURE__ */ new Set();
139
141
  #onError;
140
142
  #onRemoteTrack;
143
+ #onRtcSummary;
141
144
  #onScreenEnded;
142
145
  #participantId;
143
146
  #peerConnectionFactory;
144
147
  #pollIntervalMs;
148
+ #replaceMediaConnection;
145
149
  #remoteListeners = /* @__PURE__ */ new Set();
146
150
  #snapshotListeners = /* @__PURE__ */ new Set();
147
151
  #localTracks = /* @__PURE__ */ new Map();
@@ -149,8 +153,11 @@ var CloudflareSFUClient = class {
149
153
  #remoteTracks = /* @__PURE__ */ new Map();
150
154
  #bootstrap;
151
155
  #connection;
156
+ #connectionEpoch = 0;
152
157
  #cursor = null;
153
158
  #generation = 0;
159
+ #negotiatedGeneration = null;
160
+ #replacementAttemptedGeneration = null;
154
161
  #polling = false;
155
162
  #pollTimer;
156
163
  #sdpTail = Promise.resolve();
@@ -164,8 +171,10 @@ var CloudflareSFUClient = class {
164
171
  this.#bootstrap = options.bootstrap;
165
172
  this.#transport = options.transport;
166
173
  this.#pollIntervalMs = options.pollIntervalMs ?? 1e3;
174
+ this.#replaceMediaConnection = options.replaceMediaConnection;
167
175
  this.#onError = options.onError;
168
176
  this.#onRemoteTrack = options.onRemoteTrack;
177
+ this.#onRtcSummary = options.onRtcSummary;
169
178
  this.#onScreenEnded = options.onScreenEnded;
170
179
  this.#peerConnectionFactory = options.peerConnectionFactory;
171
180
  this.#connection = this.#createPeerConnection(options.bootstrap);
@@ -176,7 +185,7 @@ var CloudflareSFUClient = class {
176
185
  remoteTracks: EMPTY_REMOTE,
177
186
  failure: null
178
187
  });
179
- this.#observeConnection(this.#connection, this.#generation);
188
+ this.#observeConnection(this.#connection, this.#generation, this.#connectionEpoch);
180
189
  }
181
190
  getSnapshot() {
182
191
  return this.#snapshot;
@@ -276,16 +285,19 @@ var CloudflareSFUClient = class {
276
285
  const options = "connectionId" in input ? { bootstrap: input } : input;
277
286
  validateBootstrap(options.bootstrap);
278
287
  const generation = ++this.#generation;
288
+ const connectionEpoch = ++this.#connectionEpoch;
279
289
  this.#polling = false;
280
290
  this.#clearPoll();
281
291
  this.#disposeConnection(false);
282
292
  this.#reusableLocalTransceivers.clear();
283
293
  this.#clearRemoteTracks();
284
294
  this.#cursor = null;
295
+ this.#negotiatedGeneration = null;
296
+ this.#replacementAttemptedGeneration = null;
285
297
  this.#bootstrap = options.bootstrap;
286
298
  if (options.transport) this.#transport = options.transport;
287
299
  this.#connection = this.#createPeerConnection(options.bootstrap);
288
- this.#observeConnection(this.#connection, generation);
300
+ this.#observeConnection(this.#connection, generation, connectionEpoch);
289
301
  for (const state of this.#localTracks.values()) {
290
302
  state.transceiver = null;
291
303
  state.enabled = false;
@@ -310,6 +322,7 @@ var CloudflareSFUClient = class {
310
322
  if (this.#stopped) return;
311
323
  this.#stopped = true;
312
324
  this.#generation++;
325
+ this.#connectionEpoch++;
313
326
  this.#clearPoll();
314
327
  this.#polling = false;
315
328
  this.#disposeConnection(true);
@@ -367,9 +380,21 @@ var CloudflareSFUClient = class {
367
380
  }
368
381
  async #publishPreparedTracks(states, generation) {
369
382
  if (states.length === 0) return;
370
- const connection = this.#connection;
371
- const bootstrap = this.#bootstrap;
372
- await this.#serializeSDP(() => this.#publishPreparedTracksSerialized(states, generation, connection, bootstrap.connectionId));
383
+ await this.#serializeSDP(async () => {
384
+ await this.#replaceDormantConnectionBeforeNegotiation(generation);
385
+ try {
386
+ await this.#publishPreparedTracksSerialized(states, generation, this.#connection, this.#bootstrap.connectionId);
387
+ } catch (error) {
388
+ if (!this.#replaceMediaConnection || !this.#isRetryableConnectionFailure(error) || this.#replacementAttemptedGeneration === generation) throw error;
389
+ await this.#replaceMediaConnectionForRecovery(generation);
390
+ await this.#publishPreparedTracksSerialized(
391
+ [...this.#localTracks.values()].filter((state) => (state.desiredEnabled || states.includes(state)) && state.track.readyState !== "ended"),
392
+ generation,
393
+ this.#connection,
394
+ this.#bootstrap.connectionId
395
+ );
396
+ }
397
+ });
373
398
  }
374
399
  async #publishPreparedTracksSerialized(states, generation, connection, connectionId) {
375
400
  this.#requireGeneration(generation);
@@ -382,6 +407,7 @@ var CloudflareSFUClient = class {
382
407
  const response = await this.#negotiateLocalPublications(connection, connectionId, publications, generation);
383
408
  if (!wasLive) await this.#waitForConnection(connection, generation);
384
409
  this.#requireGeneration(generation);
410
+ this.#negotiatedGeneration = generation;
385
411
  this.#confirmLocalPublications(publications, response.tracks);
386
412
  this.#publishSnapshot();
387
413
  this.#emitLocal();
@@ -458,6 +484,7 @@ var CloudflareSFUClient = class {
458
484
  const desired = desiredRemotePublications(authoritative.publications, this.#participantId);
459
485
  const toPull = [...desired].filter(([key, publication]) => this.#remoteTracks.get(key)?.publicationId !== publication.publicationId).map(([, publication]) => publication);
460
486
  const pulled = await this.#pull(toPull, generation);
487
+ if (pulled === null) return;
461
488
  this.#requireGeneration(generation);
462
489
  const next = reconcileRemoteTracks(desired, pulled, this.#remoteTracks);
463
490
  stopReplacedRemoteTracks(this.#remoteTracks, next, this.#reportError.bind(this));
@@ -470,9 +497,9 @@ var CloudflareSFUClient = class {
470
497
  }
471
498
  async #pull(publications, generation) {
472
499
  if (publications.length === 0) return [];
473
- const connection = this.#connection;
474
- const connectionId = this.#bootstrap.connectionId;
475
500
  return this.#serializeSDP(async () => {
501
+ const connection = this.#connection;
502
+ const connectionId = this.#bootstrap.connectionId;
476
503
  const requested = publications.map((publication) => {
477
504
  const reference = parseCloudflareSFUPublicationID(publication.publicationId);
478
505
  return { location: "remote", sessionId: reference.connectionId, trackName: reference.trackName };
@@ -489,6 +516,7 @@ var CloudflareSFUClient = class {
489
516
  const responseTracks = response.tracks ?? [];
490
517
  await this.#completeRenegotiation(response, connection, connectionId, generation);
491
518
  await this.#waitForConnection(connection, generation);
519
+ this.#negotiatedGeneration = generation;
492
520
  await waitFor(() => responseTracks.every((track) => track.mid !== void 0 && received.has(track.mid)), 5e3);
493
521
  this.#requireGeneration(generation);
494
522
  return publications.map((publication, index) => {
@@ -532,15 +560,78 @@ var CloudflareSFUClient = class {
532
560
  "Timed out waiting for the Cloudflare SFU peer connection"
533
561
  );
534
562
  }
535
- #observeConnection(connection, generation) {
563
+ #observeConnection(connection, generation, connectionEpoch) {
564
+ let disposed = false;
565
+ const dispose = () => {
566
+ if (disposed) return;
567
+ disposed = true;
568
+ connection.removeEventListener("connectionstatechange", capture);
569
+ connection.removeEventListener("iceconnectionstatechange", capture);
570
+ connection.removeEventListener("signalingstatechange", capture);
571
+ };
536
572
  const observe = () => {
537
- if (generation !== this.#generation || this.#stopped) return;
573
+ if (disposed || generation !== this.#generation || connectionEpoch !== this.#connectionEpoch || this.#stopped) return;
538
574
  const observation = observeConnectionState(connection.connectionState, connection.iceConnectionState, this.#started);
539
575
  if (observation) this.#publishSnapshot(observation.phase, observation.failure);
540
576
  else this.#publishSnapshot();
541
577
  };
542
- connection.addEventListener("connectionstatechange", observe);
543
- connection.addEventListener("iceconnectionstatechange", observe);
578
+ const capture = () => {
579
+ if (disposed || generation !== this.#generation || connectionEpoch !== this.#connectionEpoch || this.#stopped) return;
580
+ observe();
581
+ const recorder = this.#onRtcSummary;
582
+ if (recorder) {
583
+ let stats;
584
+ try {
585
+ stats = Promise.resolve(connection.getStats());
586
+ } catch {
587
+ stats = Promise.reject(new Error("RTC stats are unavailable"));
588
+ }
589
+ void stats.then((report) => {
590
+ if (disposed || generation !== this.#generation || connectionEpoch !== this.#connectionEpoch || this.#stopped) return;
591
+ recorder(rtcConnectionState(connection), rtcStats(report));
592
+ }).catch(() => void 0);
593
+ }
594
+ if (connection.connectionState === "closed") dispose();
595
+ };
596
+ connection.addEventListener("connectionstatechange", capture);
597
+ connection.addEventListener("iceconnectionstatechange", capture);
598
+ connection.addEventListener("signalingstatechange", capture);
599
+ capture();
600
+ }
601
+ async #replaceDormantConnectionBeforeNegotiation(generation) {
602
+ if (!this.#started || this.#negotiatedGeneration === generation || this.#replacementAttemptedGeneration === generation || !this.#replaceMediaConnection) return;
603
+ await this.#replaceMediaConnectionForRecovery(generation);
604
+ }
605
+ async #replaceMediaConnectionForRecovery(generation) {
606
+ this.#requireGeneration(generation);
607
+ if (!this.#replaceMediaConnection || this.#replacementAttemptedGeneration === generation) {
608
+ throw new CloudflareSFUError("A fresh Cloudflare SFU connection is unavailable", "signaling_failed");
609
+ }
610
+ this.#replacementAttemptedGeneration = generation;
611
+ const bootstrap = await this.#replaceMediaConnection();
612
+ this.#requireGeneration(generation);
613
+ validateBootstrap(bootstrap);
614
+ if (bootstrap.connectionId === this.#bootstrap.connectionId) {
615
+ throw new CloudflareSFUError("Participant access did not replace the Cloudflare SFU connection", "signaling_failed");
616
+ }
617
+ const connectionEpoch = ++this.#connectionEpoch;
618
+ this.#disposeConnection(false);
619
+ this.#reusableLocalTransceivers.clear();
620
+ this.#clearRemoteTracks();
621
+ this.#cursor = null;
622
+ this.#bootstrap = bootstrap;
623
+ this.#connection = this.#createPeerConnection(bootstrap);
624
+ this.#negotiatedGeneration = null;
625
+ this.#observeConnection(this.#connection, generation, connectionEpoch);
626
+ for (const state of this.#localTracks.values()) {
627
+ state.transceiver = null;
628
+ state.enabled = false;
629
+ state.providerPublicationId = null;
630
+ }
631
+ this.#setPhase("recovering", null);
632
+ }
633
+ #isRetryableConnectionFailure(error) {
634
+ return error instanceof CloudflareSFUError && error.code === "signaling_failed" && error.options.retryableConnection === true;
544
635
  }
545
636
  #createPeerConnection(bootstrap) {
546
637
  const create = this.#peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
@@ -679,6 +770,71 @@ function validateClientOptions(options) {
679
770
  throw new CloudflareSFUError("Cloudflare SFU polling interval is invalid", "invalid_bootstrap");
680
771
  }
681
772
  }
773
+ function rtcConnectionState(connection) {
774
+ return {
775
+ connectionState: connection.connectionState,
776
+ iceConnectionState: connection.iceConnectionState,
777
+ signalingState: connection.signalingState
778
+ };
779
+ }
780
+ function rtcStats(report) {
781
+ const entries = [];
782
+ report.forEach((entry) => {
783
+ if (typeof entry.type !== "string") return;
784
+ entries.push({ type: entry.type, ...rtcTrafficStats(entry), ...rtcQualityStats(entry), ...rtcTransportStats(entry) });
785
+ });
786
+ return entries;
787
+ }
788
+ function rtcTrafficStats(entry) {
789
+ const bytesReceived = rtcNumber(entry, "bytesReceived");
790
+ const bytesSent = rtcNumber(entry, "bytesSent");
791
+ const packetsReceived = rtcNumber(entry, "packetsReceived");
792
+ const packetsSent = rtcNumber(entry, "packetsSent");
793
+ return {
794
+ ...bytesReceived !== void 0 ? { bytesReceived } : {},
795
+ ...bytesSent !== void 0 ? { bytesSent } : {},
796
+ ...packetsReceived !== void 0 ? { packetsReceived } : {},
797
+ ...packetsSent !== void 0 ? { packetsSent } : {}
798
+ };
799
+ }
800
+ function rtcQualityStats(entry) {
801
+ const kind = rtcString(entry, "kind");
802
+ const framesDropped = rtcNumber(entry, "framesDropped");
803
+ const jitter = rtcNumber(entry, "jitter");
804
+ const packetsLost = rtcNumber(entry, "packetsLost");
805
+ const roundTripTime = rtcNumber(entry, "roundTripTime");
806
+ return {
807
+ ...kind !== void 0 ? { kind } : {},
808
+ ...framesDropped !== void 0 ? { framesDropped } : {},
809
+ ...jitter !== void 0 ? { jitter } : {},
810
+ ...packetsLost !== void 0 ? { packetsLost } : {},
811
+ ...roundTripTime !== void 0 ? { roundTripTime } : {}
812
+ };
813
+ }
814
+ function rtcTransportStats(entry) {
815
+ const state = rtcString(entry, "state");
816
+ const selected = rtcBoolean(entry, "selected");
817
+ const nominated = rtcBoolean(entry, "nominated");
818
+ const dtlsState = rtcString(entry, "dtlsState");
819
+ return {
820
+ ...state !== void 0 ? { state } : {},
821
+ ...selected !== void 0 ? { selected } : {},
822
+ ...nominated !== void 0 ? { nominated } : {},
823
+ ...dtlsState !== void 0 ? { dtlsState } : {}
824
+ };
825
+ }
826
+ function rtcString(value, property) {
827
+ const candidate = Reflect.get(value, property);
828
+ return typeof candidate === "string" ? candidate : void 0;
829
+ }
830
+ function rtcNumber(value, property) {
831
+ const candidate = Reflect.get(value, property);
832
+ return typeof candidate === "number" ? candidate : void 0;
833
+ }
834
+ function rtcBoolean(value, property) {
835
+ const candidate = Reflect.get(value, property);
836
+ return typeof candidate === "boolean" ? candidate : void 0;
837
+ }
682
838
  function validateBootstrap(bootstrap) {
683
839
  if (!bootstrap.connectionId.trim() || !bootstrap.stunServer.trim()) throw new CloudflareSFUError("Cloudflare SFU bootstrap is incomplete", "invalid_bootstrap");
684
840
  }
@@ -771,7 +927,12 @@ function createCloudflareSFUHTTPTransport(options) {
771
927
  ...init,
772
928
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init?.headers }
773
929
  });
774
- if (!response.ok) throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed");
930
+ if (!response.ok) {
931
+ throw new CloudflareSFUError(`Chalk SFU signaling failed with HTTP ${response.status}`, "signaling_failed", {
932
+ status: response.status,
933
+ retryableConnection: response.status === 404 || response.status === 410 || response.status >= 500 && response.status < 600
934
+ });
935
+ }
775
936
  return await response.json();
776
937
  };
777
938
  return {
package/dist/client.d.ts CHANGED
@@ -6560,6 +6560,7 @@ export declare const createChalkEffectClient: (options: ChalkEffectClientOptions
6560
6560
  }>, never, never>>, never, never>, never, never>;
6561
6561
  readonly refreshSpacePublicInviteAccess: HttpApiClient.Client.Method<import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"refreshSpacePublicInviteAccess", "POST", "/v1/public/space-invite-arrival/access-grants", never, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
6562
6562
  readonly media_proof: import("effect/Schema").String;
6563
+ readonly replace_media_connection: import("effect/Schema").Boolean;
6563
6564
  }>>, import("effect/Schema").toCodecStringTree<import("effect/Schema").Struct<{
6564
6565
  readonly "X-Chalk-Arrival-Handle": import("effect/Schema").String;
6565
6566
  }>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{