@q9labsai/chalk-client 5.3.0 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/grant.d.ts +2 -0
- package/dist/{chunk-DHUHTIU7.js → chunk-2CEMOTZF.js} +187 -39
- package/dist/{chunk-XSB2GDRW.js → chunk-FD7ZSDTM.js} +71 -18
- package/dist/{chunk-GOSWSXTH.js → chunk-MHJ4UPM3.js} +162 -5
- package/dist/chunk-N4IPKRS6.js +76 -0
- package/dist/{chunk-3TGVTP6Z.js → chunk-O5JB5XVX.js} +609 -25
- package/dist/{chunk-O26BHHMI.js → chunk-VAJ6XNAA.js} +1 -1
- package/dist/client.d.ts +180 -0
- package/dist/effect.js +29 -4
- package/dist/generated/http-api.d.ts +178 -0
- package/dist/generated/schemas.d.ts +287 -0
- package/dist/generated/sync.d.ts +15 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +33 -15
- package/dist/media/call-stats.d.ts +27 -0
- package/dist/media/ice-servers.d.ts +3 -0
- package/dist/media/index.d.ts +2 -0
- package/dist/media/index.js +4 -1
- package/dist/media/receive-stall.d.ts +11 -2
- package/dist/media/types.d.ts +19 -0
- package/dist/media/video-render-size.d.ts +7 -0
- package/dist/media/viewer-layer.d.ts +48 -0
- package/dist/public-invites/index.js +3 -2
- package/dist/server/index.cjs +18 -4
- package/dist/server/index.js +18 -4
- package/dist/session/index.d.ts +18 -0
- package/dist/sync/index.js +2 -1
- package/dist/sync/v1-command-scheduler.d.ts +4 -0
- package/package.json +2 -1
package/dist/access/grant.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CloudflareSFUIceServer } from "../media/types";
|
|
1
2
|
declare const syncCredentialBrand: unique symbol;
|
|
2
3
|
declare const mediaCredentialBrand: unique symbol;
|
|
3
4
|
declare const accessGrantBrand: unique symbol;
|
|
@@ -25,6 +26,7 @@ export type ParticipantMediaAccess = {
|
|
|
25
26
|
readonly clientPayload: {
|
|
26
27
|
readonly connectionId: string;
|
|
27
28
|
readonly stunServer: string;
|
|
29
|
+
readonly iceServers?: readonly CloudflareSFUIceServer[];
|
|
28
30
|
};
|
|
29
31
|
};
|
|
30
32
|
export type ParsedAccessGrant = {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccessGrantError,
|
|
3
|
+
requireParsedAccessGrant
|
|
4
|
+
} from "./chunk-MHJ4UPM3.js";
|
|
1
5
|
import {
|
|
2
6
|
CloudflareSFUClient,
|
|
3
7
|
CloudflareSFUError,
|
|
@@ -6,11 +10,7 @@ import {
|
|
|
6
10
|
describeMediaCaptureError,
|
|
7
11
|
grantedMediaFromStream,
|
|
8
12
|
markMediaFailure
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
AccessGrantError,
|
|
12
|
-
requireParsedAccessGrant
|
|
13
|
-
} from "./chunk-GOSWSXTH.js";
|
|
13
|
+
} from "./chunk-O5JB5XVX.js";
|
|
14
14
|
import {
|
|
15
15
|
ChatAttachmentMimeTypeSchema,
|
|
16
16
|
ChatReactionSetFrameSchema,
|
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
formatSyncConnectDiagnostic,
|
|
25
25
|
scheduleSyncDialDeadline,
|
|
26
26
|
syncConnectFailureCode
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-FD7ZSDTM.js";
|
|
28
|
+
import {
|
|
29
|
+
withRequestContext
|
|
30
|
+
} from "./chunk-N4IPKRS6.js";
|
|
28
31
|
|
|
29
32
|
// src/access/manager.ts
|
|
30
33
|
import { Clock, Context, Data, Deferred, Effect, Layer, SynchronizedRef } from "effect";
|
|
@@ -56,31 +59,31 @@ var makeConnectionAccessLayer = (provider, refreshWindowMs = DEFAULT_REFRESH_WIN
|
|
|
56
59
|
});
|
|
57
60
|
});
|
|
58
61
|
const automaticMediaReplacementListeners = /* @__PURE__ */ new Set();
|
|
59
|
-
const
|
|
62
|
+
const fetch = (reason, replaceMediaConnection, force) => SynchronizedRef.modifyEffect(state, (current) => fetchAccessGrant(provider, { current, reason, replaceMediaConnection, force, refreshWindowMs, replacementGate })).pipe(
|
|
60
63
|
Effect.catchTag(
|
|
61
64
|
"MediaReplacementPending",
|
|
62
65
|
({ done }) => Deferred.await(done).pipe(
|
|
63
|
-
Effect.flatMap(() =>
|
|
66
|
+
Effect.flatMap(() => fetch(reason, replaceMediaConnection, force)),
|
|
64
67
|
Effect.map((access) => ({ access, automaticMediaReplacement: false }))
|
|
65
68
|
)
|
|
66
69
|
),
|
|
67
70
|
Effect.tap((result) => Effect.sync(() => notifyAutomaticMediaReplacement(automaticMediaReplacementListeners, result))),
|
|
68
71
|
Effect.map((result) => result.access)
|
|
69
72
|
);
|
|
70
|
-
const initialize = (reason = "join") =>
|
|
73
|
+
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)));
|
|
71
74
|
const millisecondsUntilRefresh = Effect.gen(function* () {
|
|
72
75
|
const access = yield* SynchronizedRef.get(state);
|
|
73
76
|
if (!access.access) return null;
|
|
74
77
|
return millisecondsUntil(access.access, yield* Clock.currentTimeMillis, refreshWindowMs);
|
|
75
78
|
});
|
|
76
|
-
const ensureFresh = (reason = "scheduled_refresh") =>
|
|
79
|
+
const ensureFresh = (reason = "scheduled_refresh") => fetch(reason, false, false);
|
|
77
80
|
return {
|
|
78
81
|
current: SynchronizedRef.get(state).pipe(Effect.map((value) => value.access)),
|
|
79
82
|
currentUnsafe: () => SynchronizedRef.getUnsafe(state).access,
|
|
80
83
|
initialize,
|
|
81
84
|
ensureFresh,
|
|
82
|
-
refresh: (reason, replaceMediaConnection) =>
|
|
83
|
-
refreshAfterRejection: () =>
|
|
85
|
+
refresh: (reason, replaceMediaConnection) => fetch(reason, replaceMediaConnection, true),
|
|
86
|
+
refreshAfterRejection: () => fetch("access_retry", true, true),
|
|
84
87
|
getSyncToken: (reason = "sync_recovery") => ensureFresh(reason).pipe(Effect.map((access) => access.sync.token)),
|
|
85
88
|
getMediaToken: () => ensureFresh("scheduled_refresh").pipe(Effect.map((access) => access.media.token)),
|
|
86
89
|
subscribeAutomaticMediaReplacement: (listener) => Effect.sync(() => {
|
|
@@ -291,8 +294,9 @@ function invalidUpload(message) {
|
|
|
291
294
|
throw chatFileError("initiate_upload", "invalid_payload", false, message);
|
|
292
295
|
}
|
|
293
296
|
function createChatFileRequest(options) {
|
|
294
|
-
const fetchImplementation = options.fetch
|
|
295
|
-
|
|
297
|
+
const fetchImplementation = withRequestContext(options.fetch);
|
|
298
|
+
let baseUrl = options.baseUrl;
|
|
299
|
+
while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
|
|
296
300
|
return async function request(operation, path, init, accepts) {
|
|
297
301
|
const headers = new Headers(init.headers);
|
|
298
302
|
try {
|
|
@@ -786,7 +790,7 @@ var ChalkWhiteboardViewBounds = {
|
|
|
786
790
|
|
|
787
791
|
// src/whiteboard/file-download-transport.ts
|
|
788
792
|
function createChalkWhiteboardV1FileDownloadTransport(options) {
|
|
789
|
-
const fetchImplementation = options.fetch
|
|
793
|
+
const fetchImplementation = withRequestContext(options.fetch);
|
|
790
794
|
let baseUrlEnd = options.baseUrl.length;
|
|
791
795
|
while (baseUrlEnd > 0 && options.baseUrl[baseUrlEnd - 1] === "/") baseUrlEnd -= 1;
|
|
792
796
|
const baseUrl = options.baseUrl.slice(0, baseUrlEnd);
|
|
@@ -2565,6 +2569,7 @@ function screenShareCapture(track) {
|
|
|
2565
2569
|
}
|
|
2566
2570
|
|
|
2567
2571
|
// src/connection/production.ts
|
|
2572
|
+
var apiFetch = withRequestContext();
|
|
2568
2573
|
function createDefaultConnectionDependencies(options) {
|
|
2569
2574
|
return {
|
|
2570
2575
|
clock: {
|
|
@@ -2585,7 +2590,7 @@ function createDefaultConnectionDependencies(options) {
|
|
|
2585
2590
|
return () => globalThis.window?.removeEventListener("online", listener);
|
|
2586
2591
|
},
|
|
2587
2592
|
send: async (grant, report, signal, traceparent) => {
|
|
2588
|
-
const response = await
|
|
2593
|
+
const response = await apiFetch(`${options.apiBaseURL.replace(/\/$/, "")}/v1/tenants/${encodeURIComponent(grant.subject.tenantId)}/connection-reports`, {
|
|
2589
2594
|
method: "POST",
|
|
2590
2595
|
headers: { Authorization: `Bearer ${grant.media.token}`, "Content-Type": "application/json", traceparent },
|
|
2591
2596
|
body: JSON.stringify(report),
|
|
@@ -2601,7 +2606,7 @@ function createDefaultConnectionDependencies(options) {
|
|
|
2601
2606
|
getEpisodeStatus: async (access, signal) => {
|
|
2602
2607
|
const { tenantId, spaceId, episodeId, participantId } = access.subject;
|
|
2603
2608
|
const path = [tenantId, spaceId, episodeId, participantId].map(encodeURIComponent);
|
|
2604
|
-
const response = await
|
|
2609
|
+
const response = await apiFetch(`${options.apiBaseURL.replace(/\/$/, "")}/v1/tenants/${path[0]}/spaces/${path[1]}/episodes/${path[2]}/participants/${path[3]}/episode-status`, {
|
|
2605
2610
|
headers: { Authorization: `Bearer ${access.media.token}` },
|
|
2606
2611
|
signal,
|
|
2607
2612
|
cache: "no-store",
|
|
@@ -2618,7 +2623,7 @@ function createDefaultConnectionDependencies(options) {
|
|
|
2618
2623
|
let baseEnd = options.apiBaseURL.length;
|
|
2619
2624
|
while (baseEnd > 0 && options.apiBaseURL[baseEnd - 1] === "/") baseEnd--;
|
|
2620
2625
|
const baseURL = options.apiBaseURL.slice(0, baseEnd);
|
|
2621
|
-
const response = await
|
|
2626
|
+
const response = await apiFetch(`${baseURL}/v1/tenants/${path[0]}/spaces/${path[1]}/episodes/${path[2]}/participants/${path[3]}/cancel-join`, {
|
|
2622
2627
|
method: "POST",
|
|
2623
2628
|
headers: { Authorization: `Bearer ${access.media.token}` },
|
|
2624
2629
|
signal
|
|
@@ -3777,9 +3782,14 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
3777
3782
|
yield* publish();
|
|
3778
3783
|
});
|
|
3779
3784
|
const handleMediaFailure = (media, cause, binding) => {
|
|
3785
|
+
if (model.state === "leaving") return Effect2.void;
|
|
3780
3786
|
if (media !== model.media || binding !== access.currentUnsafe()?.media.clientPayload.connectionId) return Effect2.void;
|
|
3781
|
-
|
|
3782
|
-
if (sfuAccessRejected(cause)
|
|
3787
|
+
const failure2 = failureFromCause(cause, "media_start_failed", "Media transport needs recovery");
|
|
3788
|
+
if (sfuAccessRejected(cause)) {
|
|
3789
|
+
stopRejectedMedia();
|
|
3790
|
+
return failForSnapshot(failure2);
|
|
3791
|
+
}
|
|
3792
|
+
if (cause instanceof CloudflareSFUError && cause.options.status === 410 || active(model) && connectionSuperseded(cause)) return failForSnapshot(failure2);
|
|
3783
3793
|
return handleMediaSnapshot(media, media.getSnapshot());
|
|
3784
3794
|
};
|
|
3785
3795
|
const applyAutomaticMediaReplacement = (grant) => Effect2.gen(function* () {
|
|
@@ -4461,13 +4471,13 @@ var ChatControllerRuntime = class {
|
|
|
4461
4471
|
#unsubscribeConnection = null;
|
|
4462
4472
|
#unsubscribeEvents = null;
|
|
4463
4473
|
#unsubscribeSnapshot = null;
|
|
4464
|
-
constructor(connection, store, transport, apiBaseUrl,
|
|
4474
|
+
constructor(connection, store, transport, apiBaseUrl, fetch, fork, offline) {
|
|
4465
4475
|
this.#offline = offline;
|
|
4466
4476
|
this.#apiBaseUrl = apiBaseUrl.replace(/\/+$/u, "");
|
|
4467
4477
|
this.#connection = connection;
|
|
4468
4478
|
this.#store = store;
|
|
4469
4479
|
this.#transport = transport;
|
|
4470
|
-
this.#fetch =
|
|
4480
|
+
this.#fetch = fetch;
|
|
4471
4481
|
this.#fork = fork;
|
|
4472
4482
|
this.#unsubscribeConnection = connection.subscribePorts((ports) => this.#bind(ports));
|
|
4473
4483
|
}
|
|
@@ -4545,7 +4555,7 @@ var ChatControllerRuntime = class {
|
|
|
4545
4555
|
});
|
|
4546
4556
|
};
|
|
4547
4557
|
upload = (file) => {
|
|
4548
|
-
const
|
|
4558
|
+
const fetch = this.#fetch;
|
|
4549
4559
|
return Effect4.suspend(() => {
|
|
4550
4560
|
if (!this.#transport) {
|
|
4551
4561
|
return Effect4.fail(new SpaceClientError({ code: "collaboration.unavailable", recoverable: false, message: "Chat file upload is unavailable" }));
|
|
@@ -4561,7 +4571,7 @@ var ChatControllerRuntime = class {
|
|
|
4561
4571
|
(upload) => Effect4.void.pipe(
|
|
4562
4572
|
Effect4.andThen(
|
|
4563
4573
|
Effect4.tryPromise({
|
|
4564
|
-
try: () =>
|
|
4574
|
+
try: () => fetch(upload.uploadUrl, { method: upload.method, headers: upload.headers, body: bytes }),
|
|
4565
4575
|
catch: () => new SpaceClientError({ code: "collaboration.unavailable", recoverable: true, message: CHAT_FILE_REACHABILITY_MESSAGE })
|
|
4566
4576
|
}).pipe(
|
|
4567
4577
|
Effect4.flatMap((response) => {
|
|
@@ -4969,8 +4979,35 @@ var MediaProjectionMonitor = class {
|
|
|
4969
4979
|
// src/space-client/media-controller.ts
|
|
4970
4980
|
var MEDIA_SOURCES = ["microphone", "camera", "screen"];
|
|
4971
4981
|
var MEDIA_READY_TIMEOUT_MS = 1e4;
|
|
4982
|
+
var OFF_RETRY_INITIAL_MS = 1e3;
|
|
4983
|
+
var OFF_RETRY_MAX_MS = 3e4;
|
|
4984
|
+
function locallyOff(state) {
|
|
4985
|
+
return !state.desired.enabled || !state.localEnabled;
|
|
4986
|
+
}
|
|
4987
|
+
function owesRemoteOff(state) {
|
|
4988
|
+
return state.offDebt === "owed" && locallyOff(state);
|
|
4989
|
+
}
|
|
4972
4990
|
function sourceWorker(enabled) {
|
|
4973
|
-
return {
|
|
4991
|
+
return {
|
|
4992
|
+
desired: { enabled, revision: 0, controlRevision: -1, previousEnabled: enabled },
|
|
4993
|
+
result: { revision: 0, status: "confirmed" },
|
|
4994
|
+
initialRevision: 0,
|
|
4995
|
+
localEnabled: enabled,
|
|
4996
|
+
pause: null,
|
|
4997
|
+
offDebt: "none",
|
|
4998
|
+
offRetry: { timer: null, delayMs: OFF_RETRY_INITIAL_MS, running: false },
|
|
4999
|
+
serverSequence: -1,
|
|
5000
|
+
forcedRevision: null,
|
|
5001
|
+
pausedPublicationId: null,
|
|
5002
|
+
capturePause: "none",
|
|
5003
|
+
awaitingLive: null
|
|
5004
|
+
};
|
|
5005
|
+
}
|
|
5006
|
+
function rejectOffline(state, intent) {
|
|
5007
|
+
if (intent.enabled) state.result = { revision: intent.revision, status: "failed", cause: new V1SyncError("Media enable requires a live call", "not_live"), cleanup: !intent.previousEnabled };
|
|
5008
|
+
}
|
|
5009
|
+
function ownProjection(snapshot, source) {
|
|
5010
|
+
return snapshot.media?.items.find((item) => item.participantId === snapshot.participantId && item.source === source);
|
|
4974
5011
|
}
|
|
4975
5012
|
var MediaControllerService = class extends Context6.Service()("@chalk/client/MediaController") {
|
|
4976
5013
|
};
|
|
@@ -5157,6 +5194,8 @@ var MediaControllerRuntime = class {
|
|
|
5157
5194
|
}
|
|
5158
5195
|
dispose() {
|
|
5159
5196
|
this.#disposed = true;
|
|
5197
|
+
this.#cancelOffRetries();
|
|
5198
|
+
for (const source of MEDIA_SOURCES) this.#sources[source].offDebt = "none";
|
|
5160
5199
|
this.#projection.reset();
|
|
5161
5200
|
for (const check of this.#readinessChecks) check(normalizeMediaError(this.#cancelledCaptureError()));
|
|
5162
5201
|
this.#cancelCaptures();
|
|
@@ -5210,8 +5249,27 @@ var MediaControllerRuntime = class {
|
|
|
5210
5249
|
}).pipe(Effect5.ensuring(Effect5.sync(() => cleanup())));
|
|
5211
5250
|
});
|
|
5212
5251
|
}
|
|
5252
|
+
/** While a user enable waits for the call, its revision belongs to that wait; another source worker must not run it. */
|
|
5253
|
+
#awaitLive(source, revision) {
|
|
5254
|
+
const state = this.#sources[source];
|
|
5255
|
+
return Effect5.suspend(() => {
|
|
5256
|
+
state.awaitingLive = revision;
|
|
5257
|
+
return this.#waitForLive(source, revision);
|
|
5258
|
+
}).pipe(
|
|
5259
|
+
Effect5.ensuring(
|
|
5260
|
+
Effect5.sync(() => {
|
|
5261
|
+
if (state.awaitingLive === revision) state.awaitingLive = null;
|
|
5262
|
+
})
|
|
5263
|
+
)
|
|
5264
|
+
);
|
|
5265
|
+
}
|
|
5213
5266
|
#set(source, enabled) {
|
|
5214
|
-
return Effect5.suspend(() => this.#store.getSnapshot().connection.status === "joining" ? this.#act(source, enabled) : this.#mediaAction(enabled, this.#act(source, enabled)));
|
|
5267
|
+
return Effect5.suspend(() => this.#store.getSnapshot().connection.status === "joining" || this.#replacingMediaConnection() ? this.#act(source, enabled) : this.#mediaAction(enabled, this.#act(source, enabled)));
|
|
5268
|
+
}
|
|
5269
|
+
/** Sync is live and only the media connection is being replaced, e.g. while another source moves to a fresh SFU connection. */
|
|
5270
|
+
#replacingMediaConnection() {
|
|
5271
|
+
const { state, connection } = this.#connection.getSnapshot();
|
|
5272
|
+
return (state === "live" || state === "reconnecting") && connection.sync === "healthy" && (connection.media === "connecting" || connection.media === "recovering");
|
|
5215
5273
|
}
|
|
5216
5274
|
#startScreen(options) {
|
|
5217
5275
|
return this.#act("screen", true, options);
|
|
@@ -5258,7 +5316,9 @@ var MediaControllerRuntime = class {
|
|
|
5258
5316
|
return Effect5.suspend(() => {
|
|
5259
5317
|
if (this.#ports?.media.getSnapshot().connection.phase === "stopped") return Effect5.void;
|
|
5260
5318
|
const desired = this.#recordIntent(source, enabled, options);
|
|
5261
|
-
|
|
5319
|
+
if (!enabled) this.#sources[source].offDebt = "owed";
|
|
5320
|
+
else this.#cancelOffRetry(source);
|
|
5321
|
+
const ready = enabled && source !== "screen" && this.#store.getSnapshot().connection.status !== "live" ? this.#awaitLive(source, desired.revision).pipe(
|
|
5262
5322
|
Effect5.tapError(
|
|
5263
5323
|
(cause) => Effect5.sync(() => {
|
|
5264
5324
|
if (this.#sources[source].desired.revision === desired.revision) {
|
|
@@ -5293,12 +5353,12 @@ var MediaControllerRuntime = class {
|
|
|
5293
5353
|
#work(source) {
|
|
5294
5354
|
return Effect5.gen({ self: this }, function* () {
|
|
5295
5355
|
const state = this.#sources[source];
|
|
5296
|
-
while (state.result.revision !== state.desired.revision || state.result.status === "failed" && state.result.cleanup) {
|
|
5356
|
+
while (state.result.revision !== state.desired.revision || state.result.status === "failed" && state.result.cleanup || owesRemoteOff(state)) {
|
|
5297
5357
|
const intent = state.desired;
|
|
5298
5358
|
yield* this.#runRevision(source, intent);
|
|
5299
5359
|
if (state.desired.revision === intent.revision) return;
|
|
5300
5360
|
}
|
|
5301
|
-
});
|
|
5361
|
+
}).pipe(Effect5.ensuring(Effect5.sync(() => this.#scheduleOffRetry(source))));
|
|
5302
5362
|
}
|
|
5303
5363
|
#runRevision(source, intent) {
|
|
5304
5364
|
return Effect5.gen({ self: this }, function* () {
|
|
@@ -5307,11 +5367,12 @@ var MediaControllerRuntime = class {
|
|
|
5307
5367
|
state.pause = null;
|
|
5308
5368
|
const paused = yield* Effect5.exit(pause ? foreign2(() => pause) : Effect5.void);
|
|
5309
5369
|
if (state.desired.revision !== intent.revision) return;
|
|
5370
|
+
if (state.awaitingLive === intent.revision) return yield* paused;
|
|
5310
5371
|
const ports = this.#ports;
|
|
5311
5372
|
const cleanup = source === "screen" && !intent.enabled ? yield* Effect5.exit(this.#stopScreenLocally(ports)) : Exit2.succeed(void 0);
|
|
5312
5373
|
if (state.desired.revision !== intent.revision) return;
|
|
5313
5374
|
if (ports && this.#syncLive) yield* this.#executeRevision(ports, source, intent);
|
|
5314
|
-
else
|
|
5375
|
+
else rejectOffline(state, intent);
|
|
5315
5376
|
yield* paused;
|
|
5316
5377
|
yield* cleanup;
|
|
5317
5378
|
});
|
|
@@ -5319,16 +5380,13 @@ var MediaControllerRuntime = class {
|
|
|
5319
5380
|
#executeRevision(ports, source, intent) {
|
|
5320
5381
|
return Effect5.gen({ self: this }, function* () {
|
|
5321
5382
|
const state = this.#sources[source];
|
|
5322
|
-
if (state.result.revision === intent.revision && state.result.status === "failed")
|
|
5323
|
-
yield* this.#connection.runCommand((p) => this.#send(p, source, intent, false));
|
|
5324
|
-
if (state.result.revision === intent.revision && state.result.status === "failed") state.result.cleanup = false;
|
|
5325
|
-
return;
|
|
5326
|
-
}
|
|
5383
|
+
if (state.result.revision === intent.revision && (state.result.status === "failed" || owesRemoteOff(state))) return yield* this.#sendOwedOff(source, intent);
|
|
5327
5384
|
const operation = source === "screen" ? this.#prepareScreen(ports, intent) : this.#prepareDevice(ports, source, intent);
|
|
5328
5385
|
const result = yield* Effect5.exit(this.#connection.runCommand(() => operation));
|
|
5329
5386
|
if (state.desired.revision !== intent.revision) return;
|
|
5330
5387
|
if (Exit2.isSuccess(result)) {
|
|
5331
5388
|
state.result = { revision: intent.revision, status: "confirmed" };
|
|
5389
|
+
state.offDebt = intent.enabled ? "none" : "sent";
|
|
5332
5390
|
state.localEnabled = state.forcedRevision === intent.revision ? false : intent.enabled;
|
|
5333
5391
|
} else {
|
|
5334
5392
|
const cause = yield* Effect5.flip(result);
|
|
@@ -5336,6 +5394,16 @@ var MediaControllerRuntime = class {
|
|
|
5336
5394
|
}
|
|
5337
5395
|
});
|
|
5338
5396
|
}
|
|
5397
|
+
#sendOwedOff(source, intent) {
|
|
5398
|
+
return Effect5.gen({ self: this }, function* () {
|
|
5399
|
+
const state = this.#sources[source];
|
|
5400
|
+
yield* this.#connection.runCommand((p) => this.#send(p, source, intent, false));
|
|
5401
|
+
state.offDebt = "sent";
|
|
5402
|
+
if (state.result.revision !== intent.revision || state.result.status !== "failed") return;
|
|
5403
|
+
if (intent.enabled) state.result.cleanup = false;
|
|
5404
|
+
else state.result = { revision: intent.revision, status: "confirmed" };
|
|
5405
|
+
});
|
|
5406
|
+
}
|
|
5339
5407
|
#failRevision(ports, source, intent, cause) {
|
|
5340
5408
|
return Effect5.sync(() => {
|
|
5341
5409
|
const state = this.#sources[source];
|
|
@@ -5552,19 +5620,97 @@ var MediaControllerRuntime = class {
|
|
|
5552
5620
|
}
|
|
5553
5621
|
#reconcileIntents() {
|
|
5554
5622
|
const live = this.#connection.getSnapshot().state === "live" && this.#ports?.sync.getSnapshot().connection.phase === "live";
|
|
5555
|
-
if (this.#syncLive && !live)
|
|
5623
|
+
if (this.#syncLive && !live) {
|
|
5624
|
+
this.#rememberRecoveryIntents();
|
|
5625
|
+
this.#cancelOffRetries();
|
|
5626
|
+
}
|
|
5556
5627
|
const resumed = live && !this.#syncLive;
|
|
5557
5628
|
this.#syncLive = live;
|
|
5558
5629
|
if (!resumed) return;
|
|
5559
|
-
if (this.#ports)
|
|
5630
|
+
if (this.#ports) {
|
|
5631
|
+
const snapshot = this.#ports.sync.getSnapshot();
|
|
5632
|
+
this.#pauseRecoveredIntents(this.#ports, snapshot);
|
|
5633
|
+
this.#oweOffsSyncStillShows(snapshot);
|
|
5634
|
+
}
|
|
5560
5635
|
this.#resumePendingActions();
|
|
5561
5636
|
}
|
|
5562
5637
|
#resumePendingActions() {
|
|
5638
|
+
for (const source of MEDIA_SOURCES) {
|
|
5639
|
+
this.#sources[source].offRetry.delayMs = OFF_RETRY_INITIAL_MS;
|
|
5640
|
+
if (source === "screen" && !owesRemoteOff(this.#sources[source])) this.#runWork(source);
|
|
5641
|
+
else this.#resendOwedOff(source);
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
/** After a reconnect, Sync's snapshot decides: a source the device turned off but Sync still shows on, with the publication the device paused, is an off to resend. */
|
|
5645
|
+
#oweOffsSyncStillShows(snapshot) {
|
|
5563
5646
|
for (const source of MEDIA_SOURCES) {
|
|
5564
5647
|
const state = this.#sources[source];
|
|
5565
|
-
if (
|
|
5566
|
-
|
|
5648
|
+
if (state.offDebt !== "sent" || state.pausedPublicationId === null) continue;
|
|
5649
|
+
if (ownProjection(snapshot, source)?.publicationId === state.pausedPublicationId) state.offDebt = "owed";
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
/** Sync needs no off from this device: it shows the source off, or on with a publication this device never paused (another device's). */
|
|
5653
|
+
#syncNeedsNoOff(source) {
|
|
5654
|
+
const snapshot = this.#ports?.sync.getSnapshot();
|
|
5655
|
+
if (!snapshot?.media) return false;
|
|
5656
|
+
const item = ownProjection(snapshot, source);
|
|
5657
|
+
if (!item) return false;
|
|
5658
|
+
return !item.enabled || this.#sources[source].pausedPublicationId !== null && item.publicationId !== this.#sources[source].pausedPublicationId;
|
|
5659
|
+
}
|
|
5660
|
+
/** The one place an owed off is resent after the user's own attempt. At most one is in flight per source, and none while a user action on the source is pending. */
|
|
5661
|
+
#resendOwedOff(source) {
|
|
5662
|
+
const state = this.#sources[source];
|
|
5663
|
+
if (!this.#syncLive || this.#disposed || !owesRemoteOff(state) || state.offRetry.running || this.#pendingSources.has(source)) return;
|
|
5664
|
+
this.#cancelOffRetry(source);
|
|
5665
|
+
if (this.#syncNeedsNoOff(source)) {
|
|
5666
|
+
state.offDebt = "none";
|
|
5667
|
+
state.result = { revision: state.desired.revision, status: "confirmed" };
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5670
|
+
this.#runWork(source);
|
|
5671
|
+
}
|
|
5672
|
+
/** A Sync snapshot repays an owed off now, unless an attempt for that source is already queued or running. */
|
|
5673
|
+
#retryOffs() {
|
|
5674
|
+
for (const source of MEDIA_SOURCES) this.#resendOwedOff(source);
|
|
5675
|
+
}
|
|
5676
|
+
#runWork(source) {
|
|
5677
|
+
const retry = this.#sources[source].offRetry;
|
|
5678
|
+
retry.running = true;
|
|
5679
|
+
this.#fork(
|
|
5680
|
+
this.#serialize(source, this.#work(source)).pipe(
|
|
5681
|
+
Effect5.ignore,
|
|
5682
|
+
Effect5.ensuring(
|
|
5683
|
+
Effect5.sync(() => {
|
|
5684
|
+
retry.running = false;
|
|
5685
|
+
})
|
|
5686
|
+
)
|
|
5687
|
+
)
|
|
5688
|
+
);
|
|
5689
|
+
}
|
|
5690
|
+
/** An off that Sync still has not shown is retried on a timer with doubling backoff, one timer per source, so a quiet healthy connection repairs itself. */
|
|
5691
|
+
#scheduleOffRetry(source) {
|
|
5692
|
+
const state = this.#sources[source];
|
|
5693
|
+
const retry = state.offRetry;
|
|
5694
|
+
if (!owesRemoteOff(state)) {
|
|
5695
|
+
this.#cancelOffRetry(source);
|
|
5696
|
+
retry.delayMs = OFF_RETRY_INITIAL_MS;
|
|
5697
|
+
return;
|
|
5567
5698
|
}
|
|
5699
|
+
if (!this.#syncLive || this.#disposed || retry.timer !== null) return;
|
|
5700
|
+
retry.timer = this.#connection.scheduleUnsafe(() => {
|
|
5701
|
+
retry.timer = null;
|
|
5702
|
+
this.#resendOwedOff(source);
|
|
5703
|
+
}, retry.delayMs);
|
|
5704
|
+
retry.delayMs = Math.min(retry.delayMs * 2, OFF_RETRY_MAX_MS);
|
|
5705
|
+
}
|
|
5706
|
+
#cancelOffRetry(source) {
|
|
5707
|
+
const retry = this.#sources[source].offRetry;
|
|
5708
|
+
if (retry.timer === null) return;
|
|
5709
|
+
this.#connection.cancelScheduleUnsafe(retry.timer);
|
|
5710
|
+
retry.timer = null;
|
|
5711
|
+
}
|
|
5712
|
+
#cancelOffRetries() {
|
|
5713
|
+
for (const source of MEDIA_SOURCES) this.#cancelOffRetry(source);
|
|
5568
5714
|
}
|
|
5569
5715
|
#clearScreenAudio(media) {
|
|
5570
5716
|
return foreign2(() => media.clearPreparedLocalTrack("screen_audio")).pipe(
|
|
@@ -5971,6 +6117,7 @@ var MediaControllerRuntime = class {
|
|
|
5971
6117
|
this.#ports = ports;
|
|
5972
6118
|
this.#projection.reset();
|
|
5973
6119
|
this.#syncLive = false;
|
|
6120
|
+
this.#cancelOffRetries();
|
|
5974
6121
|
this.#bindCamera(ports);
|
|
5975
6122
|
if (!ports) {
|
|
5976
6123
|
this.#resetObservations();
|
|
@@ -6017,6 +6164,7 @@ var MediaControllerRuntime = class {
|
|
|
6017
6164
|
this.#removeRequestsFromMissingParticipants(snapshot);
|
|
6018
6165
|
this.#applyRemoteMediaState(ports, snapshot);
|
|
6019
6166
|
this.#reconcileIntents();
|
|
6167
|
+
this.#retryOffs();
|
|
6020
6168
|
this.#notifyCapturePauses(ports);
|
|
6021
6169
|
this.#publish();
|
|
6022
6170
|
});
|