@q9labsai/chalk-client 4.1.13 → 4.1.15

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.
@@ -38,6 +38,7 @@ export type ParticipantMediaAccess = {
38
38
  };
39
39
  export type ParsedAccessGrant = {
40
40
  readonly subject: AccessSubject;
41
+ readonly episodeStartedAt?: string | null;
41
42
  readonly sync: ParticipantSyncAccess;
42
43
  readonly media: ParticipantMediaAccess;
43
44
  readonly diagnostics?: EpisodeDiagnosticCredential | null;
@@ -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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"}
@@ -10,6 +10,7 @@ export declare class ConnectionAccessFailure extends ConnectionAccessFailure_bas
10
10
  }> {
11
11
  }
12
12
  export type ConnectionAccessEffectProvider = (request: ConnectionAccessRequest) => Effect.Effect<ParsedAccessGrant, ConnectionAccessFailure>;
13
+ export type AutomaticMediaReplacementListener = (access: ParsedAccessGrant) => void;
13
14
  export type ConnectionAccessEffectService = {
14
15
  readonly current: Effect.Effect<ParsedAccessGrant | null>;
15
16
  readonly currentUnsafe: () => ParsedAccessGrant | null;
@@ -19,6 +20,7 @@ export type ConnectionAccessEffectService = {
19
20
  readonly refreshAfterRejection: () => Effect.Effect<ParsedAccessGrant, ConnectionAccessFailure>;
20
21
  readonly getSyncToken: (reason?: ConnectionAccessReason) => Effect.Effect<ParticipantSyncCredential, ConnectionAccessFailure>;
21
22
  readonly getMediaToken: () => Effect.Effect<ParticipantMediaCredential, ConnectionAccessFailure>;
23
+ readonly subscribeAutomaticMediaReplacement: (listener: AutomaticMediaReplacementListener) => Effect.Effect<() => void>;
22
24
  readonly requiresRefresh: Effect.Effect<boolean>;
23
25
  readonly millisecondsUntilRefresh: Effect.Effect<number | null>;
24
26
  readonly clear: Effect.Effect<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/access/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,OAAO,EAAQ,MAAM,EAAE,KAAK,EAAmB,MAAM,QAAQ,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AACxG,OAAO,KAAK,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;;;;AAIlG,qBAAa,uBAAwB,SAAQ,6BAA4C;IACvF,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,oBAAoB,CAAC;IACvD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB,CAAC;CAAG;AAEL,MAAM,MAAM,8BAA8B,GAAG,CAAC,OAAO,EAAE,uBAAuB,KAAK,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;AAE7I,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC1D,QAAQ,CAAC,aAAa,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAC;IACvD,QAAQ,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,cAAc,KAAK,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;IACrH,QAAQ,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC,EAAE,sBAAsB,KAAK,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;IACrH,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,sBAAsB,EAAE,MAAM,CAAC,EAAE,sBAAsB,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;IAClK,QAAQ,CAAC,qBAAqB,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;IAChG,QAAQ,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,EAAE,sBAAsB,KAAK,MAAM,CAAC,MAAM,CAAC,yBAAyB,EAAE,uBAAuB,CAAC,CAAC;IAC9H,QAAQ,CAAC,aAAa,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,0BAA0B,EAAE,uBAAuB,CAAC,CAAC;IACjG,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjD,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC;;AAEF,qBAAa,uBAAwB,SAAQ,4BAA2G;CAAG;AAI3J;;;GAGG;AACH,eAAO,MAAM,yBAAyB,aAAc,8BAA8B,iFAqD/E,CAAC;AAEJ,eAAO,MAAM,6BAA6B,aAvDU,8BAA8B,iFAuDZ,CAAC"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/access/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,OAAO,EAAQ,MAAM,EAAE,KAAK,EAAmB,MAAM,QAAQ,CAAC;AAC9E,OAAO,KAAK,EAAE,iBAAiB,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AACxG,OAAO,KAAK,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;;;;AAIlG,qBAAa,uBAAwB,SAAQ,6BAA4C;IACvF,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,oBAAoB,CAAC;IACvD,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB,CAAC;CAAG;AAEL,MAAM,MAAM,8BAA8B,GAAG,CAAC,OAAO,EAAE,uBAAuB,KAAK,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;AAC7I,MAAM,MAAM,iCAAiC,GAAG,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;AAEpF,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IAC1D,QAAQ,CAAC,aAAa,EAAE,MAAM,iBAAiB,GAAG,IAAI,CAAC;IACvD,QAAQ,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,cAAc,KAAK,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;IACrH,QAAQ,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC,EAAE,sBAAsB,KAAK,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;IACrH,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,sBAAsB,EAAE,MAAM,CAAC,EAAE,sBAAsB,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;IAClK,QAAQ,CAAC,qBAAqB,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;IAChG,QAAQ,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,EAAE,sBAAsB,KAAK,MAAM,CAAC,MAAM,CAAC,yBAAyB,EAAE,uBAAuB,CAAC,CAAC;IAC9H,QAAQ,CAAC,aAAa,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,0BAA0B,EAAE,uBAAuB,CAAC,CAAC;IACjG,QAAQ,CAAC,kCAAkC,EAAE,CAAC,QAAQ,EAAE,iCAAiC,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IACxH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjD,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC;;AAEF,qBAAa,uBAAwB,SAAQ,4BAA2G;CAAG;AAY3J;;;GAGG;AACH,eAAO,MAAM,yBAAyB,aAAc,8BAA8B,iFAuC/E,CAAC;AAEJ,eAAO,MAAM,6BAA6B,aAzCU,8BAA8B,iFAyCZ,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createChalkEffectClient,
3
3
  parseAccessGrant
4
- } from "./chunk-EBACU63B.js";
4
+ } from "./chunk-LSDDADCN.js";
5
5
 
6
6
  // src/public-invites/client.ts
7
7
  import { Effect } from "effect";
@@ -2,21 +2,21 @@ import {
2
2
  CloudflareRTKClient,
3
3
  CloudflareSFUClient,
4
4
  createCloudflareSFUHTTPTransport
5
- } from "./chunk-S5OUHFGC.js";
5
+ } from "./chunk-PQNSA7ZN.js";
6
+ import {
7
+ createBrowserSyncLifecycle,
8
+ createBrowserWebSocketFactory,
9
+ createV1SyncClient
10
+ } from "./chunk-PWUU6T3R.js";
6
11
  import {
7
12
  AccessGrantError,
8
13
  requireParsedAccessGrant,
9
14
  validEpisodeDiagnosticCredential
10
- } from "./chunk-EBACU63B.js";
15
+ } from "./chunk-LSDDADCN.js";
11
16
  import {
12
17
  createTraceContext,
13
18
  parseTraceparent
14
19
  } from "./chunk-NKOKY2AE.js";
15
- import {
16
- createBrowserSyncLifecycle,
17
- createBrowserWebSocketFactory,
18
- createV1SyncClient
19
- } from "./chunk-5VXGLI3T.js";
20
20
 
21
21
  // src/access/manager.ts
22
22
  import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
@@ -29,28 +29,10 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
29
29
  ConnectionAccessService,
30
30
  Effect.gen(function* () {
31
31
  const state = yield* SynchronizedRef.make({ access: null });
32
- const fetch = (reason, replaceMediaConnection, force) => SynchronizedRef.modifyEffect(
33
- state,
34
- (current) => Effect.gen(function* () {
35
- const now = yield* Clock.currentTimeMillis;
36
- if (!force && current.access && millisecondsUntil(current.access, now, refreshWindowMs) > 0) return [current.access, current];
37
- const request = Object.freeze({
38
- reason,
39
- replaceMediaConnection,
40
- ...current.access ? {
41
- currentMediaToken: current.access.media.token,
42
- expectedParticipantGeneration: current.access.subject.participantGeneration
43
- } : {}
44
- });
45
- const next = yield* provider(request);
46
- try {
47
- validateExpiration(next, now);
48
- if (current.access) validateRefresh(current.access, next, replaceMediaConnection);
49
- } catch (cause) {
50
- return yield* Effect.fail(new ConnectionAccessFailure({ code: "access.invalid", cause }));
51
- }
52
- return [next, { access: next }];
53
- })
32
+ const automaticMediaReplacementListeners = /* @__PURE__ */ new Set();
33
+ const fetch = (reason, replaceMediaConnection, force) => SynchronizedRef.modifyEffect(state, (current) => fetchAccessGrant(provider, { current, reason, replaceMediaConnection, force, refreshWindowMs })).pipe(
34
+ Effect.tap((result) => Effect.sync(() => notifyAutomaticMediaReplacement(automaticMediaReplacementListeners, result))),
35
+ Effect.map((result) => result.access)
54
36
  );
55
37
  const initialize = (reason = "join") => fetch(reason, false, false).pipe(Effect.catchTag("ConnectionAccessFailure", (failure2) => failure2.code === "access.invalid" && SynchronizedRef.getUnsafe(state).access === null ? fetch(reason, false, true) : Effect.fail(failure2)));
56
38
  const millisecondsUntilRefresh = Effect.gen(function* () {
@@ -68,6 +50,10 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
68
50
  refreshAfterRejection: () => fetch("access_retry", true, true),
69
51
  getSyncToken: (reason = "sync_recovery") => ensureFresh(reason).pipe(Effect.map((access) => access.sync.token)),
70
52
  getMediaToken: () => ensureFresh("scheduled_refresh").pipe(Effect.map((access) => access.media.token)),
53
+ subscribeAutomaticMediaReplacement: (listener) => Effect.sync(() => {
54
+ automaticMediaReplacementListeners.add(listener);
55
+ return () => automaticMediaReplacementListeners.delete(listener);
56
+ }),
71
57
  requiresRefresh: millisecondsUntilRefresh.pipe(Effect.map((milliseconds) => milliseconds === null || milliseconds === 0)),
72
58
  millisecondsUntilRefresh,
73
59
  clear: SynchronizedRef.set(state, { access: null })
@@ -75,6 +61,46 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
75
61
  })
76
62
  );
77
63
  var makeFakeConnectionAccessLayer = makeConnectionAccessLayer;
64
+ function fetchAccessGrant(provider, input) {
65
+ return Effect.gen(function* () {
66
+ const now = yield* Clock.currentTimeMillis;
67
+ const currentAccess = input.current.access;
68
+ if (!input.force && currentAccess && millisecondsUntil(currentAccess, now, input.refreshWindowMs) > 0) {
69
+ return [{ access: currentAccess, automaticMediaReplacement: false }, input.current];
70
+ }
71
+ const mediaExpired = currentAccess !== null && expiresAt(currentAccess.media.expiresAt) <= now;
72
+ const shouldReplaceMediaConnection = input.replaceMediaConnection || mediaExpired;
73
+ const next = yield* provider(connectionAccessRequest(input, shouldReplaceMediaConnection));
74
+ yield* Effect.try({
75
+ try: () => validateFetchedAccess(currentAccess, next, now, shouldReplaceMediaConnection),
76
+ catch: (cause) => new ConnectionAccessFailure({ code: "access.invalid", cause })
77
+ });
78
+ return [{ access: next, automaticMediaReplacement: mediaExpired && !input.replaceMediaConnection }, { access: next }];
79
+ });
80
+ }
81
+ function connectionAccessRequest(input, replaceMediaConnection) {
82
+ return Object.freeze({
83
+ reason: input.reason,
84
+ replaceMediaConnection,
85
+ ...input.current.access ? {
86
+ currentMediaToken: input.current.access.media.token,
87
+ expectedParticipantGeneration: input.current.access.subject.participantGeneration
88
+ } : {}
89
+ });
90
+ }
91
+ function validateFetchedAccess(previous, next, now, replaceMediaConnection) {
92
+ validateExpiration(next, now);
93
+ if (previous) validateRefresh(previous, next, replaceMediaConnection);
94
+ }
95
+ function notifyAutomaticMediaReplacement(listeners, result) {
96
+ if (!result.automaticMediaReplacement) return;
97
+ for (const listener of listeners) {
98
+ try {
99
+ listener(result.access);
100
+ } catch {
101
+ }
102
+ }
103
+ }
78
104
  function millisecondsUntil(access, now, refreshWindowMs) {
79
105
  return Math.max(0, Math.min(expiresAt(access.sync.expiresAt), expiresAt(access.media.expiresAt)) - now - refreshWindowMs);
80
106
  }
@@ -82,12 +108,15 @@ function validateExpiration(access, now) {
82
108
  if (expiresAt(access.sync.expiresAt) <= now || expiresAt(access.media.expiresAt) <= now) throw new TypeError("Access grant is expired");
83
109
  }
84
110
  function validateRefresh(previous, next, replaceMediaConnection) {
111
+ if (!hasSameSubject(previous, next)) throw new TypeError("Access grant refresh changed its subject");
112
+ const mediaConnectionChanged = previous.media.provider !== next.media.provider || mediaBinding(previous) !== mediaBinding(next);
113
+ if (replaceMediaConnection === mediaConnectionChanged) return;
114
+ throw new TypeError(replaceMediaConnection ? "Access grant replacement did not change its media binding" : "Access grant refresh unexpectedly replaced its media connection");
115
+ }
116
+ function hasSameSubject(previous, next) {
85
117
  const left = previous.subject;
86
118
  const right = next.subject;
87
- if (left.tenantId !== right.tenantId || left.spaceId !== right.spaceId || left.episodeId !== right.episodeId || left.participantId !== right.participantId || left.participantGeneration !== right.participantGeneration) {
88
- throw new TypeError("Access grant refresh changed its subject");
89
- }
90
- if (!replaceMediaConnection && (previous.media.provider !== next.media.provider || mediaBinding(previous) !== mediaBinding(next))) throw new TypeError("Access grant refresh unexpectedly replaced its media connection");
119
+ return left.tenantId === right.tenantId && left.spaceId === right.spaceId && left.episodeId === right.episodeId && left.participantId === right.participantId && left.participantGeneration === right.participantGeneration;
91
120
  }
92
121
  function mediaBinding(access) {
93
122
  switch (access.media.provider) {
@@ -879,6 +908,8 @@ var CLIENT_RESTART_CLOSE_CODE = 4e3;
879
908
  var DEPENDENCY_UNAVAILABLE_CLOSE_CODE = 1012;
880
909
  var HEARTBEAT_INTERVAL_MS = 2e4;
881
910
  var MAX_MISSED_HEARTBEATS = 2;
911
+ var DEFAULT_RECONNECT_DELAY_MS = 250;
912
+ var MAX_RECONNECT_DELAY_MS = 5e3;
882
913
  var encoder2 = new TextEncoder();
883
914
  var ChalkWhiteboardV1Client = class {
884
915
  files;
@@ -914,6 +945,7 @@ var ChalkWhiteboardV1Client = class {
914
945
  #stopPromise = null;
915
946
  #waitingForOperations = false;
916
947
  #reconnectTimer;
948
+ #reconnectAttempt = 0;
917
949
  #heartbeatTimer;
918
950
  #missedHeartbeats = 0;
919
951
  #lastCursorAt = Number.NEGATIVE_INFINITY;
@@ -1119,7 +1151,7 @@ var ChalkWhiteboardV1Client = class {
1119
1151
  const cursor = this.#sceneId && this.#revision ? { scene_id: this.#sceneId, revision: this.#revision } : null;
1120
1152
  this.#send(this.#useLegacyHello ? { type: "hello", protocol: "whiteboard-v1", token, cursor } : { type: "hello", protocol: "whiteboard-v1", token, cursor, extensions: [{ name: "presentation_v1" }] });
1121
1153
  } catch {
1122
- socket.close(1008, "whiteboard authentication failed");
1154
+ socket.close(CLIENT_RESTART_CLOSE_CODE, "whiteboard authentication failed");
1123
1155
  }
1124
1156
  }
1125
1157
  async #receive(socket, data) {
@@ -1231,6 +1263,7 @@ var ChalkWhiteboardV1Client = class {
1231
1263
  #welcome(frame) {
1232
1264
  if (this.#phase !== "authenticating") throw new Error("unexpected whiteboard welcome");
1233
1265
  this.#phase = "live";
1266
+ this.#reconnectAttempt = 0;
1234
1267
  this.#participantId = frame.participant_id;
1235
1268
  this.#sceneId = frame.scene_id;
1236
1269
  this.#revision = frame.revision;
@@ -1443,10 +1476,13 @@ var ChalkWhiteboardV1Client = class {
1443
1476
  }
1444
1477
  #scheduleReconnect() {
1445
1478
  this.#clearReconnect();
1479
+ const configuredDelay = this.#options.reconnectDelayMs;
1480
+ const delay = configuredDelay === 0 ? 0 : Math.min(MAX_RECONNECT_DELAY_MS, (configuredDelay ?? DEFAULT_RECONNECT_DELAY_MS) * 2 ** Math.min(this.#reconnectAttempt, 5));
1481
+ this.#reconnectAttempt += 1;
1446
1482
  this.#reconnectTimer = this.#clock().setTimeout(() => {
1447
1483
  this.#reconnectTimer = void 0;
1448
1484
  this.#connect();
1449
- }, this.#options.reconnectDelayMs ?? 250);
1485
+ }, delay);
1450
1486
  }
1451
1487
  #handleLifecycle(event) {
1452
1488
  if (event === "online" || event === "offline") this.#online = event === "online";
@@ -1989,7 +2025,7 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
1989
2025
  };
1990
2026
  const foreign6 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => lifecycleFailure(code, true, message, cause) });
1991
2027
  const toPromise = (effect) => Effect2.runPromiseWith(context)(effect);
1992
- const publish = () => Effect2.sync(() => snapshotFor(model, access.currentUnsafe()?.subject ?? null)).pipe(
2028
+ const publish = () => Effect2.sync(() => snapshotFor(model, access.currentUnsafe())).pipe(
1993
2029
  Effect2.tap((snapshot) => SubscriptionRef.set(snapshotRef, snapshot)),
1994
2030
  Effect2.asVoid,
1995
2031
  Effect2.tap(
@@ -2352,6 +2388,18 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2352
2388
  yield* publish();
2353
2389
  });
2354
2390
  const handleMediaFailure = () => model.media ? handleMediaSnapshot(model.media, model.media.getSnapshot()) : Effect2.void;
2391
+ const applyAutomaticMediaReplacement = (grant) => Effect2.gen(function* () {
2392
+ const media = model.media;
2393
+ if (!media || !active(model)) return;
2394
+ const restartInput = grant.media.provider === "cloudflare_sfu" ? grant.media.clientPayload : grant.media;
2395
+ const restarted = yield* Effect2.exit(foreign6(() => media.restart(restartInput), "media_start_failed", "The media client could not apply a replacement access grant"));
2396
+ if (restarted._tag === "Failure") {
2397
+ yield* recover("media");
2398
+ return;
2399
+ }
2400
+ model.mediaSnapshot = media.getSnapshot();
2401
+ yield* publish();
2402
+ });
2355
2403
  const notifyScreenEnded = () => Effect2.sync(() => {
2356
2404
  if (!active(model)) return;
2357
2405
  for (const listener of screenEndedListeners) {
@@ -2370,10 +2418,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2370
2418
  yield* Deferred.done(work.deferred, exit);
2371
2419
  }) : Deferred.complete(work.deferred, work.effect).pipe(Effect2.asVoid);
2372
2420
  yield* Effect2.forkScoped(Queue.take(queue).pipe(Effect2.flatMap(process), Effect2.forever));
2421
+ const accessReplacementUnsubscribe = yield* access.subscribeAutomaticMediaReplacement((grant) => enqueueBackground(applyAutomaticMediaReplacement(grant)));
2373
2422
  const foregroundUnsubscribe = platform.subscribeForeground?.(() => enqueueBackground(refreshAccess())) ?? null;
2374
2423
  yield* Effect2.addFinalizer(
2375
2424
  () => Effect2.gen(function* () {
2376
2425
  model.closed = true;
2426
+ accessReplacementUnsubscribe();
2377
2427
  foregroundUnsubscribe?.();
2378
2428
  if (activeJoin) yield* Fiber.interrupt(activeJoin);
2379
2429
  yield* performLeave().pipe(Effect2.ignore);
@@ -2446,16 +2496,28 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
2446
2496
  function idleSnapshot() {
2447
2497
  return Object.freeze({ state: "idle", subject: null, episode: null, connection: Object.freeze({ sync: "idle", media: "idle" }), failure: null });
2448
2498
  }
2449
- function snapshotFor(model, subject) {
2499
+ function snapshotFor(model, access) {
2450
2500
  const control = model.syncSnapshot?.optimisticControl ?? model.syncSnapshot?.control;
2451
2501
  return Object.freeze({
2452
2502
  state: model.state,
2453
- subject: subject ? Object.freeze({ ...subject }) : null,
2454
- episode: subject ? Object.freeze({ id: subject.episodeId, startedAt: null, deadline: control ? new Date(control.deadlineAtMs).toISOString() : null }) : null,
2503
+ subject: subjectFor(access),
2504
+ episode: episodeFor(access, control),
2455
2505
  connection: Object.freeze({ sync: syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
2456
2506
  failure: model.failure ? Object.freeze({ ...model.failure }) : null
2457
2507
  });
2458
2508
  }
2509
+ function subjectFor(access) {
2510
+ if (!access) return null;
2511
+ return Object.freeze({ ...access.subject });
2512
+ }
2513
+ function episodeFor(access, control) {
2514
+ if (!access) return null;
2515
+ return Object.freeze({
2516
+ id: access.subject.episodeId,
2517
+ startedAt: access.episodeStartedAt ?? null,
2518
+ deadline: control ? new Date(control.deadlineAtMs).toISOString() : null
2519
+ });
2520
+ }
2459
2521
  function portsFor(model) {
2460
2522
  return model.sync && model.media ? { sync: model.sync, media: model.media } : null;
2461
2523
  }
@@ -4726,8 +4788,15 @@ function stripTrailingUrlSlash(value) {
4726
4788
  }
4727
4789
  function intakeResponse(value) {
4728
4790
  if (!isRecord3(value) || typeof value.diagnosticReference !== "string" || !Number.isSafeInteger(value.committedCursor)) return null;
4729
- if (!Array.isArray(value.accepted) || !Array.isArray(value.duplicates) || !Array.isArray(value.conflicts)) return null;
4730
- return { accepted: value.accepted, duplicates: value.duplicates, conflicts: value.conflicts };
4791
+ const accepted = legacyEmptyAcknowledgement(value.accepted);
4792
+ const duplicates = legacyEmptyAcknowledgement(value.duplicates);
4793
+ const conflicts = legacyEmptyAcknowledgement(value.conflicts);
4794
+ if (accepted === null || duplicates === null || conflicts === null) return null;
4795
+ return { accepted, duplicates, conflicts };
4796
+ }
4797
+ function legacyEmptyAcknowledgement(value) {
4798
+ if (value === null) return [];
4799
+ return Array.isArray(value) ? value : null;
4731
4800
  }
4732
4801
  function completeDelivery(delivery, expected) {
4733
4802
  const all = [...delivery.accepted, ...delivery.duplicates, ...delivery.conflicts];
@@ -4831,7 +4900,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
4831
4900
  now: baseDependencies.clock.now,
4832
4901
  setTimeout: baseDependencies.clock.setTimeout,
4833
4902
  clearTimeout: baseDependencies.clock.clearTimeout,
4834
- release: { id: "chalk-client@4.1.13" }
4903
+ release: { id: "chalk-client@4.1.15" }
4835
4904
  });
4836
4905
  const diagnosticSyncClients = /* @__PURE__ */ new Set();
4837
4906
  const dependencies = {
@@ -5102,7 +5171,7 @@ async function resolveAccessGrant(input) {
5102
5171
  const { operationName: operationName2, reason } = diagnosticAccessOperation(input.request);
5103
5172
  const operationBeforeRotation = input.diagnostics.activeContext() ? input.diagnostics.startOperation(operationName2, { reason }) : void 0;
5104
5173
  try {
5105
- const grant = await requireParsedAccessGrant(await fetchAccessGrant(input));
5174
+ const grant = await requireParsedAccessGrant(await fetchAccessGrant2(input));
5106
5175
  input.diagnostics.rotateCredential(grant.diagnostics ?? null);
5107
5176
  (operationBeforeRotation ?? input.diagnostics.startOperation(operationName2, { reason }))?.succeed({ result: "bound" });
5108
5177
  return grant;
@@ -5115,7 +5184,7 @@ function diagnosticAccessOperation(request) {
5115
5184
  const reason = request?.reason ?? "join";
5116
5185
  return { operationName: reason === "join" ? "access.request" : "access.refresh", reason };
5117
5186
  }
5118
- async function fetchAccessGrant(input) {
5187
+ async function fetchAccessGrant2(input) {
5119
5188
  if (input.platform.connectionAccess) return input.platform.connectionAccess(input.request);
5120
5189
  return input.options.getAccess({ space: input.options.space.trim(), reason: accessReason(input.request?.reason) });
5121
5190
  }
@@ -76,6 +76,7 @@ var AccessGrantSchema = Schema.Struct({
76
76
  })
77
77
  )
78
78
  ),
79
+ episode_started_at: Schema.optional(Schema.NullOr(DateTimeStringSchema)),
79
80
  media: Schema.Struct({
80
81
  client_payload: Schema.Record(Schema.String, Schema.Unknown),
81
82
  expires_at: DateTimeStringSchema,
@@ -2150,6 +2151,44 @@ var AccessInvalidPasswordErrorSchema = AccessInvalidPasswordErrorWireSchema.pipe
2150
2151
  encode: SchemaGetter.transform((error) => ({ error: error.error }))
2151
2152
  })
2152
2153
  );
2154
+ var AccessMediaProofExpiredError = class extends Schema.TaggedErrorClass()("AccessMediaProofExpiredError", {
2155
+ error: Schema.Struct({
2156
+ code: Schema.Literal("access.media_proof_expired"),
2157
+ message: Schema.String
2158
+ })
2159
+ }) {
2160
+ };
2161
+ var AccessMediaProofExpiredErrorWireSchema = Schema.Struct({
2162
+ error: Schema.Struct({
2163
+ code: Schema.Literal("access.media_proof_expired"),
2164
+ message: Schema.String
2165
+ })
2166
+ });
2167
+ var AccessMediaProofExpiredErrorSchema = AccessMediaProofExpiredErrorWireSchema.pipe(
2168
+ Schema.decodeTo(AccessMediaProofExpiredError, {
2169
+ decode: SchemaGetter.transform((wire) => ({ _tag: "AccessMediaProofExpiredError", ...wire })),
2170
+ encode: SchemaGetter.transform((error) => ({ error: error.error }))
2171
+ })
2172
+ );
2173
+ var AccessMediaProofRejectedError = class extends Schema.TaggedErrorClass()("AccessMediaProofRejectedError", {
2174
+ error: Schema.Struct({
2175
+ code: Schema.Literal("access.media_proof_rejected"),
2176
+ message: Schema.String
2177
+ })
2178
+ }) {
2179
+ };
2180
+ var AccessMediaProofRejectedErrorWireSchema = Schema.Struct({
2181
+ error: Schema.Struct({
2182
+ code: Schema.Literal("access.media_proof_rejected"),
2183
+ message: Schema.String
2184
+ })
2185
+ });
2186
+ var AccessMediaProofRejectedErrorSchema = AccessMediaProofRejectedErrorWireSchema.pipe(
2187
+ Schema.decodeTo(AccessMediaProofRejectedError, {
2188
+ decode: SchemaGetter.transform((wire) => ({ _tag: "AccessMediaProofRejectedError", ...wire })),
2189
+ encode: SchemaGetter.transform((error) => ({ error: error.error }))
2190
+ })
2191
+ );
2153
2192
  var AccessRecentAuthRequiredError = class extends Schema.TaggedErrorClass()("AccessRecentAuthRequiredError", {
2154
2193
  error: Schema.Struct({
2155
2194
  code: Schema.Literal("access.recent_auth_required"),
@@ -5368,6 +5407,8 @@ var RefreshIntegrationConnectionErrorSchema = Schema.Union([
5368
5407
  TenantInvalidIdErrorSchema
5369
5408
  ]);
5370
5409
  var RefreshSpacePublicInviteAccessErrorSchema = Schema.Union([
5410
+ AccessMediaProofExpiredErrorSchema,
5411
+ AccessMediaProofRejectedErrorSchema,
5371
5412
  ArrivalInvalidHandleErrorSchema,
5372
5413
  ArrivalUnavailableErrorSchema,
5373
5414
  EpisodeCapacityExceededErrorSchema,
@@ -7375,8 +7416,10 @@ var spacesGroup = HttpApiGroup.make("spaces").add(
7375
7416
  ArrivalInvalidHandleErrorSchema.pipe(HttpApiSchema.status(400)),
7376
7417
  RequestInvalidErrorSchema.pipe(HttpApiSchema.status(400)),
7377
7418
  RequestInvalidIdempotencyKeyErrorSchema.pipe(HttpApiSchema.status(400)),
7419
+ AccessMediaProofRejectedErrorSchema.pipe(HttpApiSchema.status(401)),
7378
7420
  ArrivalUnavailableErrorSchema.pipe(HttpApiSchema.status(404)),
7379
7421
  SpacePublicInviteUnavailableErrorSchema.pipe(HttpApiSchema.status(404)),
7422
+ AccessMediaProofExpiredErrorSchema.pipe(HttpApiSchema.status(409)),
7380
7423
  EpisodeCapacityExceededErrorSchema.pipe(HttpApiSchema.status(409)),
7381
7424
  RequestIdempotencyConflictErrorSchema.pipe(HttpApiSchema.status(409)),
7382
7425
  RequestPayloadTooLargeErrorSchema.pipe(HttpApiSchema.status(413)),
@@ -7801,9 +7844,11 @@ var AccessGrantError = class extends TypeError {
7801
7844
  function parseParsedAccessGrant(value) {
7802
7845
  if (!isRecord2(value)) throw new AccessGrantError();
7803
7846
  const subject = parseSubject(value.subject);
7847
+ const episodeStartedAt = parseOptionalDateTime(value.episode_started_at);
7804
7848
  const diagnostics = parseEpisodeDiagnosticCredential(value.diagnostics);
7805
7849
  return {
7806
7850
  subject,
7851
+ episodeStartedAt,
7807
7852
  sync: parseSyncAccess(value.sync),
7808
7853
  media: parseMediaAccess(value.media),
7809
7854
  diagnostics: diagnostics?.generation === subject.participantGeneration ? diagnostics : null
@@ -7821,6 +7866,7 @@ function accessGrantFromParsed(value) {
7821
7866
  participant_id: value.subject.participantId,
7822
7867
  participant_generation: value.subject.participantGeneration
7823
7868
  },
7869
+ ...value.episodeStartedAt ? { episode_started_at: value.episodeStartedAt } : {},
7824
7870
  sync: { token: value.sync.token, expires_at: value.sync.expiresAt },
7825
7871
  media: {
7826
7872
  token: value.media.token,
@@ -7929,6 +7975,10 @@ function requireDateTime(value) {
7929
7975
  if (!Number.isFinite(Date.parse(dateTime))) throw new AccessGrantError();
7930
7976
  return dateTime;
7931
7977
  }
7978
+ function parseOptionalDateTime(value) {
7979
+ if (typeof value !== "string" || value.trim() === "" || !Number.isFinite(Date.parse(value))) return null;
7980
+ return value;
7981
+ }
7932
7982
  function requireNonEmptyString(value) {
7933
7983
  if (typeof value !== "string" || value.trim().length === 0) throw new AccessGrantError();
7934
7984
  return value;
@@ -8422,6 +8472,12 @@ export {
8422
8472
  AccessInvalidPasswordError,
8423
8473
  AccessInvalidPasswordErrorWireSchema,
8424
8474
  AccessInvalidPasswordErrorSchema,
8475
+ AccessMediaProofExpiredError,
8476
+ AccessMediaProofExpiredErrorWireSchema,
8477
+ AccessMediaProofExpiredErrorSchema,
8478
+ AccessMediaProofRejectedError,
8479
+ AccessMediaProofRejectedErrorWireSchema,
8480
+ AccessMediaProofRejectedErrorSchema,
8425
8481
  AccessRecentAuthRequiredError,
8426
8482
  AccessRecentAuthRequiredErrorWireSchema,
8427
8483
  AccessRecentAuthRequiredErrorSchema,
@@ -135,6 +135,7 @@ async function waitFor(predicate, timeoutMs, timeoutMessage = "Timed out waiting
135
135
  // src/media/client.ts
136
136
  var EMPTY_LOCAL = Object.freeze([]);
137
137
  var EMPTY_REMOTE = Object.freeze([]);
138
+ var INVALID_PUBLICATION_SIGNATURE = "\0invalid";
138
139
  var CONNECTION_TIMEOUT_MS = 8e3;
139
140
  var CloudflareSFUClient = class {
140
141
  #localListeners = /* @__PURE__ */ new Set();
@@ -478,12 +479,13 @@ var CloudflareSFUClient = class {
478
479
  }
479
480
  }
480
481
  async #reconcileRemotePublications(authoritative, generation) {
481
- const cursor = validatePublicationSnapshot(authoritative);
482
+ const cursor = this.#validatedRemotePublicationCursor(authoritative);
483
+ if (cursor === null) return;
482
484
  const ordering = comparePublicationCursor(this.#cursor, cursor);
483
485
  if (ordering !== "newer") return;
484
- const desired = desiredRemotePublications(authoritative.publications, this.#participantId);
486
+ const desired = this.#desiredRemotePublications(authoritative, cursor);
485
487
  const toPull = [...desired].filter(([key, publication]) => this.#remoteTracks.get(key)?.publicationId !== publication.publicationId).map(([, publication]) => publication);
486
- const pulled = await this.#pull(toPull, generation);
488
+ const pulled = await this.#pullWithRecovery(toPull, cursor, generation);
487
489
  if (pulled === null) return;
488
490
  this.#requireGeneration(generation);
489
491
  const next = reconcileRemoteTracks(desired, pulled, this.#remoteTracks);
@@ -495,6 +497,52 @@ var CloudflareSFUClient = class {
495
497
  this.#publishSnapshot();
496
498
  this.#emitRemote();
497
499
  }
500
+ #validatedRemotePublicationCursor(authoritative) {
501
+ if (this.#cursor?.signature === INVALID_PUBLICATION_SIGNATURE && this.#cursor.incarnation === authoritative.incarnation && this.#cursor.sequence === authoritative.sequence) return null;
502
+ try {
503
+ return validatePublicationSnapshot(authoritative);
504
+ } catch (error) {
505
+ this.#quarantineInvalidRemotePublicationCursor(authoritative);
506
+ throw error;
507
+ }
508
+ }
509
+ #desiredRemotePublications(authoritative, cursor) {
510
+ try {
511
+ return desiredRemotePublications(authoritative.publications, this.#participantId);
512
+ } catch (error) {
513
+ this.#observeRemotePublicationCursor(cursor);
514
+ throw error;
515
+ }
516
+ }
517
+ async #pullWithRecovery(publications, cursor, generation) {
518
+ try {
519
+ return await this.#pull(publications, generation);
520
+ } catch (error) {
521
+ if (generation !== this.#generation || this.#stopped) throw error;
522
+ if (!this.#canReplaceConnectionAfterRemotePull(error, generation)) {
523
+ this.#observeRemotePublicationCursor(cursor);
524
+ throw error;
525
+ }
526
+ return this.#retryRemotePullOnReplacement(publications, cursor, generation);
527
+ }
528
+ }
529
+ #canReplaceConnectionAfterRemotePull(error, generation) {
530
+ return error instanceof CloudflareSFUError && error.code === "signaling_failed" && !this.#hasLiveRemoteTracks() && this.#replaceMediaConnection !== void 0 && this.#replacementAttemptedGeneration !== generation;
531
+ }
532
+ #hasLiveRemoteTracks() {
533
+ return [...this.#remoteTracks.values()].some((publication) => publication.track.readyState === "live");
534
+ }
535
+ async #retryRemotePullOnReplacement(publications, cursor, generation) {
536
+ try {
537
+ await this.#replaceMediaConnectionForRecovery(generation);
538
+ await this.#republishPreparedTracksAfterRecovery(generation);
539
+ return await this.#pull(publications, generation);
540
+ } catch (error) {
541
+ if (generation !== this.#generation || this.#stopped) throw error;
542
+ this.#observeRemotePublicationCursor(cursor);
543
+ throw error;
544
+ }
545
+ }
498
546
  async #pull(publications, generation) {
499
547
  if (publications.length === 0) return [];
500
548
  return this.#serializeSDP(async () => {
@@ -630,6 +678,18 @@ var CloudflareSFUClient = class {
630
678
  }
631
679
  this.#setPhase("recovering", null);
632
680
  }
681
+ async #republishPreparedTracksAfterRecovery(generation) {
682
+ const enabled = [...this.#localTracks.values()].filter((state) => state.desiredEnabled && state.track.readyState !== "ended");
683
+ await this.#publishPreparedTracks(enabled, generation);
684
+ }
685
+ #observeRemotePublicationCursor(cursor) {
686
+ this.#cursor = cursor;
687
+ this.#publishSnapshot();
688
+ }
689
+ #quarantineInvalidRemotePublicationCursor(authoritative) {
690
+ if (!Number.isSafeInteger(authoritative.incarnation) || authoritative.incarnation < 0 || !Number.isSafeInteger(authoritative.sequence) || authoritative.sequence < 0) return;
691
+ this.#observeRemotePublicationCursor({ incarnation: authoritative.incarnation, sequence: authoritative.sequence, signature: INVALID_PUBLICATION_SIGNATURE });
692
+ }
633
693
  #isRetryableConnectionFailure(error) {
634
694
  return error instanceof CloudflareSFUError && error.code === "signaling_failed" && error.options.retryableConnection === true;
635
695
  }
@@ -1916,6 +1916,8 @@ var MAX_LIVE_TARGET_RETRY_DELAY_MS = 1e3;
1916
1916
  var MAX_PROJECTION_EVENT_EVIDENCE = 256;
1917
1917
  var OPERATION_PENDING_POLL_INTERVAL_MS = 1e3;
1918
1918
  var CLIENT_RESTART_CLOSE_CODE = 4e3;
1919
+ var DEFAULT_RECONNECT_DELAY_MS = 250;
1920
+ var MAX_RECONNECT_DELAY_MS = 5e3;
1919
1921
  var MAX_COLLABORATION_REQUESTS_IN_FLIGHT = 64;
1920
1922
  var V1SyncError = class extends Error {
1921
1923
  constructor(message, code) {
@@ -1962,6 +1964,7 @@ var V1SyncClient = class {
1962
1964
  #startupGeneration = 0;
1963
1965
  #connectionGeneration = 0;
1964
1966
  #reconnectTimer;
1967
+ #reconnectAttempt = 0;
1965
1968
  #heartbeatTimer;
1966
1969
  #missedHeartbeats = 0;
1967
1970
  #unsubscribeLifecycle;
@@ -2855,6 +2858,7 @@ var V1SyncClient = class {
2855
2858
  if (!this.#recovery?.controlComplete || !this.#media || !this.#presence) return;
2856
2859
  this.#recovery = null;
2857
2860
  this.#phase = { phase: "live" };
2861
+ this.#reconnectAttempt = 0;
2858
2862
  this.#missedHeartbeats = 0;
2859
2863
  for (const pending of this.#commands.values()) {
2860
2864
  pending.retries = 0;
@@ -2936,10 +2940,13 @@ var V1SyncClient = class {
2936
2940
  this.#phase = { phase: "connecting" };
2937
2941
  this.#emit();
2938
2942
  this.#clearReconnect();
2943
+ const configuredDelay = this.#options.reconnectDelayMs;
2944
+ const delay = configuredDelay === 0 ? 0 : Math.min(MAX_RECONNECT_DELAY_MS, (configuredDelay ?? DEFAULT_RECONNECT_DELAY_MS) * 2 ** Math.min(this.#reconnectAttempt, 5));
2945
+ this.#reconnectAttempt += 1;
2939
2946
  this.#reconnectTimer = this.#clock().setTimeout(() => {
2940
2947
  this.#reconnectTimer = void 0;
2941
2948
  this.#connect();
2942
- }, this.#options.reconnectDelayMs ?? 250);
2949
+ }, delay);
2943
2950
  }
2944
2951
  #recover(reason) {
2945
2952
  const socket = this.#socket;