@q9labsai/chalk-client 4.1.16 → 4.1.17

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 (64) hide show
  1. package/README.md +44 -3
  2. package/dist/access/grant.d.ts.map +1 -1
  3. package/dist/{chunk-O7WTAXAY.js → chunk-2DZUHV6I.js} +187 -64
  4. package/dist/{chunk-E4OK5K6N.js → chunk-A7BVZEFM.js} +1 -1
  5. package/dist/{chunk-BD7FRQ67.js → chunk-AUQTQOLR.js} +635 -196
  6. package/dist/{chunk-OKIL2ELF.js → chunk-CZUJNAUI.js} +13 -2
  7. package/dist/{chunk-M3S7HNQF.js → chunk-ZRMGKMX4.js} +76 -4
  8. package/dist/client.d.ts +41 -3
  9. package/dist/client.d.ts.map +1 -1
  10. package/dist/connection/dependencies.d.ts +1 -0
  11. package/dist/connection/dependencies.d.ts.map +1 -1
  12. package/dist/connection/diagnostics.d.ts +1 -1
  13. package/dist/connection/diagnostics.d.ts.map +1 -1
  14. package/dist/connection/index.d.ts +1 -0
  15. package/dist/connection/index.d.ts.map +1 -1
  16. package/dist/connection/lifecycle.d.ts +4 -1
  17. package/dist/connection/lifecycle.d.ts.map +1 -1
  18. package/dist/connection/media-devices.d.ts.map +1 -1
  19. package/dist/effect.js +17 -5
  20. package/dist/foreground-deadline.d.ts +9 -0
  21. package/dist/foreground-deadline.d.ts.map +1 -0
  22. package/dist/generated/http-api.d.ts +41 -3
  23. package/dist/generated/http-api.d.ts.map +1 -1
  24. package/dist/generated/schemas.d.ts +88 -6
  25. package/dist/generated/schemas.d.ts.map +1 -1
  26. package/dist/index.js +37 -37
  27. package/dist/media/client.d.ts.map +1 -1
  28. package/dist/media/index.js +2 -2
  29. package/dist/media/rtk.d.ts.map +1 -1
  30. package/dist/public-invites/index.js +2 -2
  31. package/dist/server/access-refresh.d.cts +5 -0
  32. package/dist/server/access-refresh.d.ts +5 -0
  33. package/dist/server/access-refresh.d.ts.map +1 -1
  34. package/dist/server/client.d.ts.map +1 -1
  35. package/dist/server/index.cjs +10 -5
  36. package/dist/server/index.d.cts +1 -1
  37. package/dist/server/index.d.ts +1 -1
  38. package/dist/server/index.d.ts.map +1 -1
  39. package/dist/server/index.js +10 -5
  40. package/dist/server/types.d.cts +2 -1
  41. package/dist/server/types.d.ts +2 -1
  42. package/dist/server/types.d.ts.map +1 -1
  43. package/dist/space-client/core.d.ts.map +1 -1
  44. package/dist/space-client/core.test.helpers.d.ts +1 -1
  45. package/dist/space-client/core.test.helpers.d.ts.map +1 -1
  46. package/dist/space-client/media-controller.d.ts.map +1 -1
  47. package/dist/space-client/terminal.test.helpers.d.ts +115 -0
  48. package/dist/space-client/terminal.test.helpers.d.ts.map +1 -0
  49. package/dist/space-client/types.d.ts +2 -0
  50. package/dist/space-client/types.d.ts.map +1 -1
  51. package/dist/sync/index.js +1 -1
  52. package/dist/sync/v1-client.d.ts +1 -0
  53. package/dist/sync/v1-client.d.ts.map +1 -1
  54. package/dist/webhooks/generated/event-v1.d.cts +10 -2
  55. package/dist/webhooks/generated/event-v1.d.ts +10 -2
  56. package/dist/webhooks/generated/event-v1.d.ts.map +1 -1
  57. package/dist/webhooks/generated/fixtures-v1.d.cts +2 -2
  58. package/dist/webhooks/generated/fixtures-v1.d.ts +2 -2
  59. package/dist/webhooks/index.cjs +10 -2
  60. package/dist/webhooks/index.js +10 -2
  61. package/dist/webhooks/test.cjs +2 -2
  62. package/dist/webhooks/test.js +2 -2
  63. package/dist/whiteboard/v1-client.d.ts.map +1 -1
  64. package/package.json +1 -1
package/README.md CHANGED
@@ -21,14 +21,19 @@ Your backend mints an `AccessGrant` with the server SDK. See the [web quickstart
21
21
  client unchanged: it is an opaque signed envelope, so application code does
22
22
  not construct or inspect it. `getAccess` may resolve with the fetch `Response`
23
23
  that carries the grant or with its decoded JSON; the client validates it and
24
- fails the join on a non-OK response or a malformed body.
24
+ classifies non-OK responses and rejects malformed bodies.
25
25
 
26
26
  ```ts
27
27
  import { createSpaceClient } from "@q9labsai/chalk-client";
28
28
 
29
29
  const client = createSpaceClient({
30
30
  space: "design-review",
31
- getAccess: ({ space, reason }) => fetch(`/api/chalk/spaces/${space}/access?reason=${reason}`),
31
+ getAccess: (request) =>
32
+ fetch("/api/chalk/access", {
33
+ method: "POST",
34
+ headers: { "content-type": "application/json" },
35
+ body: JSON.stringify(request),
36
+ }),
32
37
  });
33
38
 
34
39
  await client.join({
@@ -41,7 +46,13 @@ await client.join({
41
46
  `getAccess` receives `reason: "join" | "refresh" | "retry"`. `Connection`
42
47
  uses it for Entrance freshness, scheduled refresh, wake revalidation, and one
43
48
  refresh-and-retry after an access rejection. Keep the callback available for
44
- the full lifetime of the client.
49
+ the full lifetime of the client. Forward the request unchanged to your backend,
50
+ then pass it as the final `participants.issueAccess` argument alongside the
51
+ server-only state from `getAccessRefreshState`. Chalk selects media replacement
52
+ when recovery needs it and retains healthy media for ordinary refresh and Sync
53
+ recovery. The quickstart shows the endpoint call; no conditional is needed.
54
+
55
+ HTTP 408, 425, 429, and 5xx responses, network failures, and provider timeouts are `access.unavailable` (recoverable). Scheduled refresh retries these failures while retaining the current grant. Other non-OK responses (including 401 and 403) and malformed grants are `access.invalid`. Initial join does not automatically retry unavailable Access: show the failure and let the person check their network and try again. Return the `Response` unchanged so Chalk can classify its status; throwing a generic error for every non-OK response loses that distinction.
45
56
 
46
57
  ## Snapshot store
47
58
 
@@ -184,6 +195,36 @@ Events are `participantJoined`, `participantLeft`, `episodeEnded`,
184
195
  stable codes such as `access.invalid`, `episode.ended`, and
185
196
  `chat.payload_invalid`.
186
197
 
198
+ ### Join capture errors
199
+
200
+ `join()` requests the microphone and camera by default. If both work, capture
201
+ still uses one request. If a requested device fails, Chalk tries the requested
202
+ devices separately and enters with the working devices; the unavailable device
203
+ is off. This also applies to the React entrance, including a device that works
204
+ in preview but fails at entry. A capture warning does not reject `join()`.
205
+
206
+ Listen to `client.on("error", ({ error }) => ...)` (React: `onError`) for one
207
+ recoverable warning, also available as `connection.lastError`. Its message names
208
+ the failing device or devices. React displays a dismissible warning after entry.
209
+
210
+ | Capture problem | Public error code |
211
+ | ----------------------------------- | --------------------------------- |
212
+ | Missing microphone, camera, or both | `media.device_not_found` |
213
+ | Busy microphone or camera | `media.device_busy` |
214
+ | Denied microphone, camera, or both | `media.permission_denied` |
215
+ | Unavailable device constraint | `media.device_constraint_invalid` |
216
+ | Other device capture failure | `media.capture_failed` |
217
+
218
+ When both devices fail for different reasons, the warning uses the microphone's
219
+ code and names both devices. Integrations must allow these codes and must not
220
+ treat a recoverable capture warning as failed entry. Participants can enable a
221
+ device later through `client.media` after resolving the problem.
222
+
223
+ Only an unavailable capture API or an insecure browser context fails capture
224
+ as nonrecoverable `environment.unsupported`. Joining with both devices explicitly
225
+ off does not require capture. Access, Sync, or media transport failures can still
226
+ reject `join()` independently of capture.
227
+
187
228
  ## Journey telemetry
188
229
 
189
230
  Client telemetry is opt-in. Start a `space.join` journey when a Participant
@@ -1 +1 @@
1
- {"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/access/grant.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/access/grant.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,10 @@
1
- import {
2
- journeyHeaders
3
- } from "./chunk-HW5TTZL2.js";
4
1
  import {
5
2
  recordReconnect,
6
3
  traceReconnect
7
4
  } from "./chunk-RCDXIDCN.js";
5
+ import {
6
+ journeyHeaders
7
+ } from "./chunk-HW5TTZL2.js";
8
8
 
9
9
  // src/media/camera-uplink.ts
10
10
  var CameraUplinkPolicy = class {
@@ -230,6 +230,8 @@ var CloudflareSFUClient = class {
230
230
  #replacementAttemptedGeneration = null;
231
231
  #polling = false;
232
232
  #pollAfterCurrent = false;
233
+ #pollFailures = 0;
234
+ #pollFailedPull = false;
233
235
  #pollTimer;
234
236
  #sdpTail = Promise.resolve();
235
237
  #snapshot;
@@ -332,25 +334,29 @@ var CloudflareSFUClient = class {
332
334
  if (!this.#started || this.#stopped || this.#polling) return;
333
335
  this.#polling = true;
334
336
  const generation = this.#generation;
337
+ this.#pollFailedPull = false;
335
338
  try {
336
339
  const transport = this.#requireTransport();
337
340
  const authoritative = await traceReconnect(this.#recordReconnect, "list_publications", () => transport.listPublications());
338
341
  this.#requireGeneration(generation);
339
342
  await this.#reconcileRemotePublications(authoritative, generation);
343
+ this.#requireGeneration(generation);
344
+ if (!this.#remotePullIncomplete) this.#pollFailures = 0;
340
345
  } catch (error) {
341
346
  if (generation === this.#generation && !this.#stopped) this.#reportError(error);
342
347
  throw error;
343
348
  } finally {
344
- if (generation === this.#generation) {
345
- this.#polling = false;
346
- if (this.#pollAfterCurrent) {
347
- this.#pollAfterCurrent = false;
348
- this.#clearPoll();
349
- this.#schedulePoll(0);
350
- }
351
- }
349
+ this.#finishPublicationPoll(generation);
352
350
  }
353
351
  }
352
+ #finishPublicationPoll(generation) {
353
+ if (generation !== this.#generation) return;
354
+ this.#polling = false;
355
+ if (!this.#pollAfterCurrent) return;
356
+ this.#pollAfterCurrent = false;
357
+ this.#clearPoll();
358
+ this.#schedulePoll(0);
359
+ }
354
360
  async setLocalPublicationTarget(target) {
355
361
  const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
356
362
  if (resolved.kind === "result") return resolved.result;
@@ -478,14 +484,12 @@ var CloudflareSFUClient = class {
478
484
  if (options.bootstrap.connectionId !== this.#bootstrap.connectionId) this.#started = false;
479
485
  this.#bootstrap = options.bootstrap;
480
486
  if (options.transport) this.#transport = options.transport;
481
- this.#connection = this.#createPeerConnection(options.bootstrap);
482
- this.#observeConnection(this.#connection, generation, connectionEpoch);
487
+ this.#rebuildPeerConnection(options.bootstrap, generation, connectionEpoch);
483
488
  for (const state of this.#localTracks.values()) {
484
489
  state.transceiver = null;
485
490
  state.enabled = false;
486
491
  }
487
492
  const enabled = [...this.#localTracks.values()].filter((state) => state.desiredEnabled && state.track.readyState !== "ended");
488
- this.#setPhase("recovering", null);
489
493
  await this.#activatePreparedTracks(enabled, generation);
490
494
  }
491
495
  async #activatePreparedTracks(states, generation) {
@@ -740,6 +744,7 @@ var CloudflareSFUClient = class {
740
744
  if (ordering === "stale" || ordering === "same" && !this.#remotePullIncomplete) return;
741
745
  const desired = this.#desiredRemotePublications(authoritative, cursor);
742
746
  const toPull = this.#pendingRemotePulls(desired);
747
+ this.#pollFailedPull = true;
743
748
  const pulled = await this.#pullWithRecovery(toPull, cursor, generation);
744
749
  if (pulled === null) return;
745
750
  this.#requireGeneration(generation);
@@ -1120,16 +1125,14 @@ var CloudflareSFUClient = class {
1120
1125
  this.#clearRemoteTracks();
1121
1126
  this.#cursor = null;
1122
1127
  this.#bootstrap = bootstrap;
1123
- this.#connection = this.#createPeerConnection(bootstrap);
1128
+ this.#rebuildPeerConnection(bootstrap, generation, connectionEpoch);
1124
1129
  this.#retiredLocalConnection = null;
1125
1130
  this.#negotiatedGeneration = null;
1126
- this.#observeConnection(this.#connection, generation, connectionEpoch);
1127
1131
  for (const state of this.#localTracks.values()) {
1128
1132
  state.transceiver = null;
1129
1133
  state.enabled = false;
1130
1134
  state.providerPublicationId = null;
1131
1135
  }
1132
- this.#setPhase("recovering", null);
1133
1136
  this.remotePublicationsChanged();
1134
1137
  }
1135
1138
  async #republishPreparedTracksAfterRecovery(generation) {
@@ -1152,7 +1155,17 @@ var CloudflareSFUClient = class {
1152
1155
  const create = this.#peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
1153
1156
  return create({ iceServers: [{ urls: bootstrap.stunServer }], bundlePolicy: "max-bundle" });
1154
1157
  }
1158
+ #rebuildPeerConnection(bootstrap, generation, connectionEpoch) {
1159
+ try {
1160
+ this.#connection = this.#createPeerConnection(bootstrap);
1161
+ this.#observeConnection(this.#connection, generation, connectionEpoch);
1162
+ } catch (error) {
1163
+ this.#setFailure(error, "media_failed");
1164
+ throw error;
1165
+ }
1166
+ }
1155
1167
  #disposeConnection(stopSenders) {
1168
+ if (!stopSenders) this.#setPhase("recovering", null);
1156
1169
  this.#disposeConnectionObservation?.();
1157
1170
  this.#disposeConnectionObservation = void 0;
1158
1171
  if (stopSenders) {
@@ -1198,15 +1211,27 @@ var CloudflareSFUClient = class {
1198
1211
  this.#schedulePoll();
1199
1212
  return;
1200
1213
  }
1214
+ let nextDelay = this.#pollIntervalMs;
1201
1215
  try {
1202
1216
  await this.refreshRemotePublications();
1217
+ if (this.#remotePullIncomplete) nextDelay = this.#incompletePullDelay();
1203
1218
  } catch {
1219
+ if (this.#pollFailedPull && this.#remotePullIncomplete) nextDelay = this.#incompletePullDelay();
1220
+ else {
1221
+ this.#pollFailures = Math.min(this.#pollFailures + 1, 6);
1222
+ nextDelay = Math.min(this.#pollIntervalMs, (500 + Math.random() * 500) * 2 ** (this.#pollFailures - 1));
1223
+ }
1204
1224
  } finally {
1205
- this.#schedulePoll(this.#remotePullIncomplete ? 750 : this.#pollIntervalMs);
1225
+ this.#schedulePoll(nextDelay);
1206
1226
  }
1207
1227
  }, delayMs);
1208
1228
  }
1229
+ #incompletePullDelay() {
1230
+ this.#pollFailures = Math.min(this.#pollFailures + 1, 6);
1231
+ return Math.min(this.#pollIntervalMs, 750 * 2 ** (this.#pollFailures - 1));
1232
+ }
1209
1233
  #clearPoll() {
1234
+ this.#pollFailures = 0;
1210
1235
  if (this.#pollTimer !== void 0) globalThis.clearTimeout(this.#pollTimer);
1211
1236
  this.#pollTimer = void 0;
1212
1237
  }
@@ -1571,12 +1596,14 @@ var CloudflareRTKClient = class {
1571
1596
  #clientFactory;
1572
1597
  #snapshotListeners = /* @__PURE__ */ new Set();
1573
1598
  #sourceOperations = /* @__PURE__ */ new Map();
1599
+ #generationInvalidators = /* @__PURE__ */ new Set();
1574
1600
  #connection = null;
1575
1601
  #localTracks = /* @__PURE__ */ new Map();
1576
1602
  #connectionUnsubscribers = [];
1577
1603
  #participantUnsubscribers = /* @__PURE__ */ new Map();
1578
1604
  #snapshot;
1579
1605
  #stopped = false;
1606
+ #generation = 0;
1580
1607
  #screenDisableRequested = false;
1581
1608
  #initialAuthToken;
1582
1609
  constructor(options) {
@@ -1613,16 +1640,22 @@ var CloudflareRTKClient = class {
1613
1640
  async clearPreparedLocalTrack(source) {
1614
1641
  const state = this.#localTracks.get(source);
1615
1642
  if (!state) return;
1616
- if (state.enabled) {
1617
- await this.#setSourceEnabled(state, false);
1618
- }
1643
+ const connection = this.#connection;
1619
1644
  if (state.endedListener) state.track.removeEventListener("ended", state.endedListener);
1620
1645
  this.#localTracks.delete(source);
1646
+ const actions = connection ? this.#sourceActions(state, connection) : null;
1647
+ const cleanup = actions?.enabled && state.enabled ? this.#queueSourceOperation(source, () => connection === this.#connection ? actions.disable() : Promise.resolve()) : Promise.resolve();
1621
1648
  this.#publishSnapshot();
1649
+ try {
1650
+ await cleanup;
1651
+ } catch (error) {
1652
+ if (!isStaleGeneration(error)) throw error;
1653
+ }
1622
1654
  }
1623
1655
  async start(localMedia) {
1624
1656
  if (this.#snapshot.connection.phase === "live") return;
1625
1657
  this.#requireStartable();
1658
+ const generation = this.#advanceGeneration();
1626
1659
  this.#setPhase("connecting", null);
1627
1660
  try {
1628
1661
  const tracks = localMedia.getTracks().filter((track) => track.kind === "audio" || track.kind === "video");
@@ -1630,38 +1663,60 @@ var CloudflareRTKClient = class {
1630
1663
  const source = track.kind === "audio" ? "microphone" : "camera";
1631
1664
  if (!this.#localTracks.has(source)) this.#localTracks.set(source, { source, track, enabled: false, endedListener: null });
1632
1665
  }
1633
- await this.#openConnection(this.#initialAuthToken);
1634
- for (const state of this.#localTracks.values()) {
1635
- if (state.source === "microphone" || state.source === "camera") await this.#setSourceEnabled(state, true);
1636
- }
1637
- this.#setPhase("live", null);
1666
+ if (!await this.#openConnection(this.#initialAuthToken, generation)) return;
1667
+ await this.#restoreLocalTracks(generation, true);
1638
1668
  } catch (error) {
1639
- this.#setFailure(error);
1640
- this.#reportError(error);
1669
+ this.#failCurrentGeneration(generation, error);
1641
1670
  throw error;
1642
1671
  }
1643
1672
  }
1644
1673
  async setLocalPublicationTarget(target) {
1645
1674
  const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
1646
1675
  if (resolved.kind === "result") return resolved.result;
1647
- const state = resolved.value;
1648
- const previous = this.#sourceOperations.get(target.source) ?? Promise.resolve();
1649
- const operation = previous.catch(() => void 0).then(async () => {
1650
- if (state.enabled === target.enabled) return "satisfied";
1651
- try {
1652
- await this.#setSourceEnabled(state, target.enabled);
1653
- return "confirmed";
1654
- } catch (error) {
1655
- this.#reportError(error);
1656
- return "retryable_failure";
1657
- }
1676
+ const generation = this.#generation;
1677
+ try {
1678
+ return await this.#queueSourceOperation(target.source, () => this.#applyLocalTarget(target, generation));
1679
+ } catch (error) {
1680
+ return { outcome: "retryable_failure", errorCode: error instanceof CloudflareRTKError ? error.code : "media_failed" };
1681
+ }
1682
+ }
1683
+ async #queueSourceOperation(source, apply) {
1684
+ const previous = this.#sourceOperations.get(source) ?? Promise.resolve();
1685
+ const generation = this.#generation;
1686
+ let invalidate = () => {
1687
+ };
1688
+ const retired = new Promise((_resolve, reject) => {
1689
+ invalidate = () => reject(new CloudflareRTKError("The media operation belongs to a retired connection", "stale_generation"));
1690
+ this.#generationInvalidators.add(invalidate);
1658
1691
  });
1659
- this.#sourceOperations.set(target.source, operation);
1692
+ const operation = Promise.race([
1693
+ previous.catch(() => void 0).then(() => {
1694
+ if (!this.#isCurrentGeneration(generation)) throw new CloudflareRTKError("The queued media operation belongs to a retired connection", "stale_generation");
1695
+ return apply();
1696
+ }),
1697
+ retired
1698
+ ]);
1699
+ this.#sourceOperations.set(source, operation);
1660
1700
  try {
1661
- const outcome = await operation;
1662
- return { outcome, errorCode: outcome === "retryable_failure" ? "media_failed" : null };
1701
+ return await operation;
1663
1702
  } finally {
1664
- if (this.#sourceOperations.get(target.source) === operation) this.#sourceOperations.delete(target.source);
1703
+ this.#generationInvalidators.delete(invalidate);
1704
+ if (this.#sourceOperations.get(source) === operation) this.#sourceOperations.delete(source);
1705
+ }
1706
+ }
1707
+ async #applyLocalTarget(target, generation) {
1708
+ if (!this.#isCurrentGeneration(generation)) return { outcome: "retryable_failure", errorCode: "stale_generation" };
1709
+ const resolved = resolveMediaTarget(this.#participantId, this.#stopped, this.#localTracks, target);
1710
+ if (resolved.kind === "result") return resolved.result;
1711
+ const state = resolved.value;
1712
+ try {
1713
+ if (!this.#connection) throw new CloudflareRTKError("The RealtimeKit connection is recovering", "stale_generation");
1714
+ if (state.enabled === target.enabled) return { outcome: "satisfied", errorCode: null };
1715
+ await this.#setSourceEnabled(state, target.enabled);
1716
+ return { outcome: "confirmed", errorCode: null };
1717
+ } catch (error) {
1718
+ this.#reportError(error);
1719
+ return { outcome: "retryable_failure", errorCode: error instanceof CloudflareRTKError ? error.code : "media_failed" };
1665
1720
  }
1666
1721
  }
1667
1722
  observeLocalPublications(listener) {
@@ -1673,31 +1728,74 @@ var CloudflareRTKClient = class {
1673
1728
  async restart(input) {
1674
1729
  if (!isRTKAccess(input)) throw new CloudflareRTKError("The RealtimeKit adapter requires a Cloudflare RealtimeKit access grant", "invalid_client");
1675
1730
  if (this.#stopped) throw new CloudflareRTKError("The RealtimeKit media client has stopped", "media_stopped");
1731
+ const generation = this.#advanceGeneration();
1676
1732
  this.#setPhase("recovering", null);
1677
1733
  try {
1678
1734
  await this.#closeConnection();
1679
- await this.#openConnection(input.clientPayload.token);
1680
- for (const state of this.#localTracks.values()) if (state.enabled) await this.#setSourceEnabled(state, true);
1681
- this.#setPhase("live", null);
1735
+ if (!this.#isCurrentGeneration(generation)) return;
1736
+ if (!await this.#openConnection(input.clientPayload.token, generation)) return;
1737
+ await this.#restoreLocalTracks(generation, false);
1682
1738
  } catch (error) {
1683
- this.#setFailure(error);
1684
- this.#reportError(error);
1739
+ this.#failCurrentGeneration(generation, error);
1685
1740
  throw error;
1686
1741
  }
1687
1742
  }
1688
1743
  stop() {
1689
1744
  if (this.#stopped) return;
1690
1745
  this.#stopped = true;
1746
+ this.#advanceGeneration();
1691
1747
  const leave = this.#closeConnection();
1692
1748
  void leave.catch((error) => this.#reportError(error));
1693
1749
  for (const state of this.#localTracks.values()) if (state.endedListener) state.track.removeEventListener("ended", state.endedListener);
1694
1750
  this.#localTracks.clear();
1695
1751
  this.#setPhase("stopped", null);
1696
1752
  }
1697
- async #openConnection(authToken) {
1698
- this.#connection = await this.#clientFactory({ authToken, onError: (error) => this.#reportError(error) });
1699
- this.#bindConnection(this.#connection);
1700
- await this.#connection.join();
1753
+ #isCurrentGeneration(generation) {
1754
+ return !this.#stopped && generation === this.#generation;
1755
+ }
1756
+ #advanceGeneration() {
1757
+ const generation = ++this.#generation;
1758
+ this.#sourceOperations.clear();
1759
+ for (const invalidate of this.#generationInvalidators) invalidate();
1760
+ this.#generationInvalidators.clear();
1761
+ return generation;
1762
+ }
1763
+ async #restoreLocalTracks(generation, initial) {
1764
+ for (const state of this.#localTracks.values()) {
1765
+ if (!this.#isCurrentGeneration(generation)) return;
1766
+ const enable = initial ? state.source !== "screen" : state.enabled;
1767
+ if (enable) {
1768
+ await this.#queueSourceOperation(state.source, async () => {
1769
+ if (!this.#isCurrentGeneration(generation) || this.#localTracks.get(state.source) !== state || !initial && !state.enabled) return;
1770
+ await this.#setSourceEnabled(state, true);
1771
+ }).catch((error) => {
1772
+ if (!this.#isCurrentGeneration(generation)) return;
1773
+ if (isStaleGeneration(error) && this.#localTracks.get(state.source) !== state) return;
1774
+ throw error;
1775
+ });
1776
+ }
1777
+ }
1778
+ if (this.#isCurrentGeneration(generation)) this.#setPhase("live", null);
1779
+ }
1780
+ #failCurrentGeneration(generation, error) {
1781
+ if (!this.#isCurrentGeneration(generation)) return;
1782
+ this.#setFailure(error);
1783
+ this.#reportError(error);
1784
+ }
1785
+ async #openConnection(authToken, generation) {
1786
+ const connection = await this.#clientFactory({ authToken, onError: (error) => this.#reportError(error) });
1787
+ if (!this.#isCurrentGeneration(generation)) {
1788
+ await connection.leave();
1789
+ return false;
1790
+ }
1791
+ this.#connection = connection;
1792
+ this.#bindConnection(connection);
1793
+ await connection.join();
1794
+ if (!this.#isCurrentGeneration(generation)) {
1795
+ await connection.leave();
1796
+ return false;
1797
+ }
1798
+ return true;
1701
1799
  }
1702
1800
  async #closeConnection() {
1703
1801
  const connection = this.#connection;
@@ -1749,22 +1847,44 @@ var CloudflareRTKClient = class {
1749
1847
  }
1750
1848
  async #setSourceEnabled(state, enabled) {
1751
1849
  const connection = this.#connection;
1850
+ const generation = this.#generation;
1752
1851
  if (!connection) throw new CloudflareRTKError("The RealtimeKit connection is not active", "media_stopped");
1753
- if (state.source === "microphone") {
1754
- if (enabled) await connection.self.enableAudio(state.track);
1755
- else await connection.self.disableAudio();
1756
- } else if (state.source === "camera") {
1757
- if (enabled) await connection.self.enableVideo(state.track);
1758
- else await connection.self.disableVideo();
1759
- } else {
1760
- this.#screenDisableRequested = !enabled;
1761
- if (enabled) await connection.self.enableScreenShare();
1762
- else await connection.self.disableScreenShare();
1852
+ const actions = this.#sourceActions(state, connection);
1853
+ if (state.source === "screen") this.#screenDisableRequested = !enabled;
1854
+ await actions[enabled ? "enable" : "disable"]();
1855
+ try {
1856
+ this.#requireCurrentSource(state, connection, generation);
1857
+ } catch (error) {
1858
+ if (enabled)
1859
+ await actions.disable().catch((cause) => {
1860
+ this.#failCurrentGeneration(generation, cause);
1861
+ throw cause;
1862
+ });
1863
+ throw error;
1763
1864
  }
1764
1865
  state.enabled = enabled;
1765
1866
  state.track.enabled = enabled;
1766
1867
  this.#publishSnapshot();
1767
1868
  }
1869
+ #sourceActions(state, connection) {
1870
+ return {
1871
+ microphone: { enabled: connection.self.audioEnabled, enable: () => connection.self.enableAudio(state.track), disable: () => connection.self.disableAudio() },
1872
+ camera: { enabled: connection.self.videoEnabled, enable: () => connection.self.enableVideo(state.track), disable: () => connection.self.disableVideo() },
1873
+ screen: {
1874
+ enabled: connection.self.screenShareEnabled,
1875
+ enable: () => connection.self.enableScreenShare(),
1876
+ disable: () => {
1877
+ if (connection === this.#connection) this.#screenDisableRequested = true;
1878
+ return connection.self.disableScreenShare();
1879
+ }
1880
+ }
1881
+ }[state.source];
1882
+ }
1883
+ #requireCurrentSource(state, connection, generation) {
1884
+ if (!this.#isCurrentGeneration(generation) || connection !== this.#connection || this.#localTracks.get(state.source) !== state) {
1885
+ throw new CloudflareRTKError("The local media target belongs to a replaced connection or source", "stale_generation");
1886
+ }
1887
+ }
1768
1888
  #syncLocalState(source) {
1769
1889
  const state = this.#localTracks.get(source);
1770
1890
  const connection = this.#connection;
@@ -1962,6 +2082,9 @@ function toParticipant(participant) {
1962
2082
  }
1963
2083
  };
1964
2084
  }
2085
+ function isStaleGeneration(error) {
2086
+ return error instanceof CloudflareRTKError && error.code === "stale_generation";
2087
+ }
1965
2088
 
1966
2089
  // src/media/capture-error.ts
1967
2090
  function describeMediaCaptureError(cause) {
@@ -1988,6 +2111,7 @@ function describeMediaCaptureError(cause) {
1988
2111
  }
1989
2112
 
1990
2113
  export {
2114
+ describeMediaCaptureError,
1991
2115
  CloudflareSFUError,
1992
2116
  parseCloudflareSFUPublicationID,
1993
2117
  requireDescription,
@@ -1999,6 +2123,5 @@ export {
1999
2123
  CloudflareSFUClient,
2000
2124
  createCloudflareSFUHTTPTransport,
2001
2125
  CloudflareRTKError,
2002
- CloudflareRTKClient,
2003
- describeMediaCaptureError
2126
+ CloudflareRTKClient
2004
2127
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createChalkEffectClient,
3
3
  parseAccessGrant
4
- } from "./chunk-M3S7HNQF.js";
4
+ } from "./chunk-ZRMGKMX4.js";
5
5
 
6
6
  // src/public-invites/client.ts
7
7
  import { Effect } from "effect";