@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.
- package/README.md +44 -3
- package/dist/access/grant.d.ts.map +1 -1
- package/dist/{chunk-O7WTAXAY.js → chunk-2DZUHV6I.js} +187 -64
- package/dist/{chunk-E4OK5K6N.js → chunk-A7BVZEFM.js} +1 -1
- package/dist/{chunk-BD7FRQ67.js → chunk-AUQTQOLR.js} +635 -196
- package/dist/{chunk-OKIL2ELF.js → chunk-CZUJNAUI.js} +13 -2
- package/dist/{chunk-M3S7HNQF.js → chunk-ZRMGKMX4.js} +76 -4
- package/dist/client.d.ts +41 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/connection/dependencies.d.ts +1 -0
- package/dist/connection/dependencies.d.ts.map +1 -1
- package/dist/connection/diagnostics.d.ts +1 -1
- package/dist/connection/diagnostics.d.ts.map +1 -1
- package/dist/connection/index.d.ts +1 -0
- package/dist/connection/index.d.ts.map +1 -1
- package/dist/connection/lifecycle.d.ts +4 -1
- package/dist/connection/lifecycle.d.ts.map +1 -1
- package/dist/connection/media-devices.d.ts.map +1 -1
- package/dist/effect.js +17 -5
- package/dist/foreground-deadline.d.ts +9 -0
- package/dist/foreground-deadline.d.ts.map +1 -0
- package/dist/generated/http-api.d.ts +41 -3
- package/dist/generated/http-api.d.ts.map +1 -1
- package/dist/generated/schemas.d.ts +88 -6
- package/dist/generated/schemas.d.ts.map +1 -1
- package/dist/index.js +37 -37
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/index.js +2 -2
- package/dist/media/rtk.d.ts.map +1 -1
- package/dist/public-invites/index.js +2 -2
- package/dist/server/access-refresh.d.cts +5 -0
- package/dist/server/access-refresh.d.ts +5 -0
- package/dist/server/access-refresh.d.ts.map +1 -1
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/index.cjs +10 -5
- package/dist/server/index.d.cts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +10 -5
- package/dist/server/types.d.cts +2 -1
- package/dist/server/types.d.ts +2 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/space-client/core.d.ts.map +1 -1
- package/dist/space-client/core.test.helpers.d.ts +1 -1
- package/dist/space-client/core.test.helpers.d.ts.map +1 -1
- package/dist/space-client/media-controller.d.ts.map +1 -1
- package/dist/space-client/terminal.test.helpers.d.ts +115 -0
- package/dist/space-client/terminal.test.helpers.d.ts.map +1 -0
- package/dist/space-client/types.d.ts +2 -0
- package/dist/space-client/types.d.ts.map +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/v1-client.d.ts +1 -0
- package/dist/sync/v1-client.d.ts.map +1 -1
- package/dist/webhooks/generated/event-v1.d.cts +10 -2
- package/dist/webhooks/generated/event-v1.d.ts +10 -2
- package/dist/webhooks/generated/event-v1.d.ts.map +1 -1
- package/dist/webhooks/generated/fixtures-v1.d.cts +2 -2
- package/dist/webhooks/generated/fixtures-v1.d.ts +2 -2
- package/dist/webhooks/index.cjs +10 -2
- package/dist/webhooks/index.js +10 -2
- package/dist/webhooks/test.cjs +2 -2
- package/dist/webhooks/test.js +2 -2
- package/dist/whiteboard/v1-client.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CloudflareRTKClient,
|
|
3
|
-
CloudflareSFUClient,
|
|
4
|
-
createCloudflareSFUHTTPTransport,
|
|
5
|
-
describeMediaCaptureError
|
|
6
|
-
} from "./chunk-O7WTAXAY.js";
|
|
7
1
|
import {
|
|
8
2
|
AccessGrantError,
|
|
9
3
|
requireParsedAccessGrant,
|
|
10
4
|
validEpisodeDiagnosticCredential
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import {
|
|
13
|
-
normalizeTelemetryAttributes
|
|
14
|
-
} from "./chunk-EWTITNJ3.js";
|
|
5
|
+
} from "./chunk-ZRMGKMX4.js";
|
|
15
6
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
CloudflareRTKClient,
|
|
8
|
+
CloudflareSFUClient,
|
|
9
|
+
createCloudflareSFUHTTPTransport,
|
|
10
|
+
describeMediaCaptureError
|
|
11
|
+
} from "./chunk-2DZUHV6I.js";
|
|
20
12
|
import {
|
|
21
13
|
ChatAttachmentMimeTypeSchema,
|
|
22
14
|
V1SyncError,
|
|
23
15
|
createBrowserSyncLifecycle,
|
|
24
16
|
createBrowserWebSocketFactory,
|
|
25
17
|
createV1SyncClient
|
|
26
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-CZUJNAUI.js";
|
|
27
19
|
import {
|
|
28
20
|
recordInitialConnection,
|
|
29
21
|
recordReconnect
|
|
30
22
|
} from "./chunk-RCDXIDCN.js";
|
|
23
|
+
import {
|
|
24
|
+
normalizeTelemetryAttributes
|
|
25
|
+
} from "./chunk-EWTITNJ3.js";
|
|
26
|
+
import {
|
|
27
|
+
createTraceContext,
|
|
28
|
+
journeyHeaders,
|
|
29
|
+
parseTraceparent
|
|
30
|
+
} from "./chunk-HW5TTZL2.js";
|
|
31
31
|
|
|
32
32
|
// src/access/manager.ts
|
|
33
33
|
import { Clock, Context, Data, Effect, Layer, SynchronizedRef } from "effect";
|
|
@@ -154,6 +154,20 @@ var ConnectionPlatformService = class extends Context2.Service()("@chalk/client/
|
|
|
154
154
|
var makeConnectionPlatformLayer = (dependencies) => Layer2.succeed(ConnectionPlatformService, dependencies);
|
|
155
155
|
var makeFakeConnectionPlatformLayer = makeConnectionPlatformLayer;
|
|
156
156
|
|
|
157
|
+
// src/whiteboard/types.ts
|
|
158
|
+
var ChalkWhiteboardV1Error = class extends Error {
|
|
159
|
+
operation;
|
|
160
|
+
code;
|
|
161
|
+
recoverable;
|
|
162
|
+
constructor(failure3, options) {
|
|
163
|
+
super(failure3.message, options);
|
|
164
|
+
this.name = "ChalkWhiteboardV1Error";
|
|
165
|
+
this.operation = failure3.operation;
|
|
166
|
+
this.code = failure3.code;
|
|
167
|
+
this.recoverable = failure3.recoverable;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
157
171
|
// src/collaboration/types.ts
|
|
158
172
|
var CHALK_CHAT_ATTACHMENT_LIMITS = {
|
|
159
173
|
maximumPerMessage: 5,
|
|
@@ -301,20 +315,6 @@ function isAllowedMimeType(value) {
|
|
|
301
315
|
return typeof value === "string" && CHALK_CHAT_ATTACHMENT_MIME_TYPES.includes(value);
|
|
302
316
|
}
|
|
303
317
|
|
|
304
|
-
// src/whiteboard/types.ts
|
|
305
|
-
var ChalkWhiteboardV1Error = class extends Error {
|
|
306
|
-
operation;
|
|
307
|
-
code;
|
|
308
|
-
recoverable;
|
|
309
|
-
constructor(failure3, options) {
|
|
310
|
-
super(failure3.message, options);
|
|
311
|
-
this.name = "ChalkWhiteboardV1Error";
|
|
312
|
-
this.operation = failure3.operation;
|
|
313
|
-
this.code = failure3.code;
|
|
314
|
-
this.recoverable = failure3.recoverable;
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
|
|
318
318
|
// src/whiteboard/file-http-transport.ts
|
|
319
319
|
function createChalkWhiteboardV1FileHttpTransport(options) {
|
|
320
320
|
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
@@ -1304,6 +1304,8 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1304
1304
|
void this.#requestSnapshot().catch(() => void 0);
|
|
1305
1305
|
}
|
|
1306
1306
|
#requestSnapshot() {
|
|
1307
|
+
const active2 = this.#snapshots.values().next().value;
|
|
1308
|
+
if (active2) return active2.promise;
|
|
1307
1309
|
const requestId = this.#nextId();
|
|
1308
1310
|
const frame = { type: "request_snapshot", request_id: requestId };
|
|
1309
1311
|
encodeWhiteboardV1ClientFrame(frame);
|
|
@@ -1311,7 +1313,7 @@ var ChalkWhiteboardV1Client = class {
|
|
|
1311
1313
|
const promise = new Promise((resolve, reject) => {
|
|
1312
1314
|
deferred = { resolve, reject, settled: false };
|
|
1313
1315
|
});
|
|
1314
|
-
this.#snapshots.set(requestId, { requestId, deferred, pages: /* @__PURE__ */ new Map() });
|
|
1316
|
+
this.#snapshots.set(requestId, { requestId, deferred, promise, pages: /* @__PURE__ */ new Map() });
|
|
1315
1317
|
this.#send(frame);
|
|
1316
1318
|
return promise;
|
|
1317
1319
|
}
|
|
@@ -1725,11 +1727,25 @@ function createChalkWhiteboardV1Client(options) {
|
|
|
1725
1727
|
});
|
|
1726
1728
|
}
|
|
1727
1729
|
|
|
1730
|
+
// src/connection/types.ts
|
|
1731
|
+
var ConnectionError = class extends Error {
|
|
1732
|
+
code;
|
|
1733
|
+
action;
|
|
1734
|
+
recoverable;
|
|
1735
|
+
constructor(failure3, options) {
|
|
1736
|
+
super(failure3.message, options);
|
|
1737
|
+
this.name = "ConnectionError";
|
|
1738
|
+
this.code = failure3.code;
|
|
1739
|
+
this.action = failure3.action;
|
|
1740
|
+
this.recoverable = failure3.recoverable;
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1728
1744
|
// src/connection/media-devices.ts
|
|
1729
1745
|
function createBrowserMediaDevices(mediaDevices = globalThis.navigator?.mediaDevices) {
|
|
1730
1746
|
return {
|
|
1731
1747
|
async getUserMedia(constraints) {
|
|
1732
|
-
if (!mediaDevices?.getUserMedia) throw new
|
|
1748
|
+
if (!mediaDevices?.getUserMedia || globalThis.isSecureContext === false) throw new ConnectionError({ code: "unsupported_environment", action: null, recoverable: false, message: "Browser media capture is unavailable. Use a supported browser over HTTPS." });
|
|
1733
1749
|
return mediaDevices.getUserMedia(constraints);
|
|
1734
1750
|
},
|
|
1735
1751
|
async getDisplayMedia(constraints) {
|
|
@@ -1911,6 +1927,26 @@ function whiteboardURL(syncURL) {
|
|
|
1911
1927
|
return url.toString();
|
|
1912
1928
|
}
|
|
1913
1929
|
|
|
1930
|
+
// src/foreground-deadline.ts
|
|
1931
|
+
function scheduleForegroundDeadline(clock, milliseconds, expired) {
|
|
1932
|
+
let timer;
|
|
1933
|
+
let cancelled = false;
|
|
1934
|
+
const arm = () => {
|
|
1935
|
+
const started = clock.now();
|
|
1936
|
+
timer = clock.setTimeout(() => {
|
|
1937
|
+
if (cancelled) return;
|
|
1938
|
+
const elapsed = clock.now() - started;
|
|
1939
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden" || elapsed > milliseconds + 2e3 || elapsed < 0) arm();
|
|
1940
|
+
else expired();
|
|
1941
|
+
}, milliseconds);
|
|
1942
|
+
};
|
|
1943
|
+
arm();
|
|
1944
|
+
return () => {
|
|
1945
|
+
cancelled = true;
|
|
1946
|
+
clock.clearTimeout(timer);
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1914
1950
|
// src/connection/lifecycle.ts
|
|
1915
1951
|
import { Clock as Clock2, Context as Context3, Data as Data2, Deferred, Duration, Effect as Effect2, Exit, Fiber, Layer as Layer3, Queue, Scope, SubscriptionRef } from "effect";
|
|
1916
1952
|
|
|
@@ -1967,26 +2003,15 @@ var ConnectionDiagnostics = class {
|
|
|
1967
2003
|
}
|
|
1968
2004
|
};
|
|
1969
2005
|
|
|
1970
|
-
// src/connection/types.ts
|
|
1971
|
-
var ConnectionError = class extends Error {
|
|
1972
|
-
code;
|
|
1973
|
-
action;
|
|
1974
|
-
recoverable;
|
|
1975
|
-
constructor(failure3, options) {
|
|
1976
|
-
super(failure3.message, options);
|
|
1977
|
-
this.name = "ConnectionError";
|
|
1978
|
-
this.code = failure3.code;
|
|
1979
|
-
this.action = failure3.action;
|
|
1980
|
-
this.recoverable = failure3.recoverable;
|
|
1981
|
-
}
|
|
1982
|
-
};
|
|
1983
|
-
|
|
1984
2006
|
// src/connection/lifecycle.ts
|
|
2007
|
+
var MEDIA_RECOVERY_TIMEOUT_MS = 6e4;
|
|
1985
2008
|
var START_TIMEOUT_MS = 1e4;
|
|
1986
2009
|
var LEAVE_TIMEOUT_MS = 5e3;
|
|
1987
2010
|
var RECOVERY_BUDGET_MS = 1e4;
|
|
1988
2011
|
var MAX_RECOVERY_ATTEMPTS = 3;
|
|
1989
2012
|
var REFRESH_RETRY_MS = 5e3;
|
|
2013
|
+
var RECONNECT_GRACE_MS = 250;
|
|
2014
|
+
var BACKGROUND_RETRY_MAX_MS = 6e4;
|
|
1990
2015
|
var ConnectionLifecycleFailure = class extends Data2.TaggedError("ConnectionLifecycleFailure") {
|
|
1991
2016
|
};
|
|
1992
2017
|
var ConnectionLifecycleService = class extends Context3.Service()("@chalk/client/ConnectionLifecycle") {
|
|
@@ -1997,7 +2022,7 @@ var makeConnectionLifecycleLayer = (options) => {
|
|
|
1997
2022
|
const access = makeConnectionAccessLayer(
|
|
1998
2023
|
(request) => Effect2.tryPromise({
|
|
1999
2024
|
try: (signal) => Promise.resolve(options.access(request ? { ...request, signal } : void 0)),
|
|
2000
|
-
catch: (cause) => new ConnectionAccessFailure({ code: "access.unavailable", cause })
|
|
2025
|
+
catch: (cause) => cause instanceof ConnectionAccessFailure ? cause : new ConnectionAccessFailure({ code: accessRejected(cause) ? "access.invalid" : "access.unavailable", cause })
|
|
2001
2026
|
}),
|
|
2002
2027
|
options.accessRefreshWindowMs
|
|
2003
2028
|
);
|
|
@@ -2032,6 +2057,8 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2032
2057
|
let joinDeferred = null;
|
|
2033
2058
|
let activeJoin = null;
|
|
2034
2059
|
let fallbackIdentifier = 0;
|
|
2060
|
+
let emittedPorts = null;
|
|
2061
|
+
let foregroundRecoveryDeadline = 0;
|
|
2035
2062
|
const model = {
|
|
2036
2063
|
state: "idle",
|
|
2037
2064
|
sync: null,
|
|
@@ -2044,21 +2071,51 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2044
2071
|
try: () => intent.microphone || intent.camera ? platform.mediaDevices.getUserMedia({ audio: intent.microphone, video: intent.camera }) : Promise.resolve(streamFromTracks([])),
|
|
2045
2072
|
catch: (cause) => cause
|
|
2046
2073
|
}),
|
|
2074
|
+
initialMediaIntent: null,
|
|
2047
2075
|
intent: { microphone: options.initialMicrophoneEnabled ?? true, camera: options.initialCameraEnabled ?? true },
|
|
2048
2076
|
episodeEndConfirmed: false,
|
|
2049
2077
|
activeScope: null,
|
|
2050
2078
|
syncBindingCleanup: null,
|
|
2051
2079
|
mediaBindingCleanup: null,
|
|
2052
2080
|
refreshFiber: null,
|
|
2081
|
+
refreshFailures: 0,
|
|
2082
|
+
refreshInFlight: false,
|
|
2053
2083
|
recoveryQueued: null,
|
|
2054
2084
|
joinCancelled: false,
|
|
2055
2085
|
closed: false,
|
|
2056
2086
|
epoch: 0
|
|
2057
2087
|
};
|
|
2088
|
+
const mediaAttempt = { expiresAt: null };
|
|
2058
2089
|
const foreign7 = (operation, code = "internal_error", message = "A browser transport operation failed") => Effect2.tryPromise({ try: operation, catch: (cause) => lifecycleFailure(code, true, message, cause) });
|
|
2090
|
+
const withinForegroundBudget = (effect, milliseconds, failure3) => Effect2.raceFirst(
|
|
2091
|
+
effect,
|
|
2092
|
+
Effect2.callback((resume) => {
|
|
2093
|
+
const cancel = scheduleForegroundDeadline(platform.clock, milliseconds, () => resume(Effect2.fail(failure3)));
|
|
2094
|
+
return Effect2.sync(cancel);
|
|
2095
|
+
})
|
|
2096
|
+
);
|
|
2059
2097
|
const toPromise = (effect) => Effect2.runPromiseWith(context)(effect);
|
|
2060
|
-
const publish = () => Effect2.sync(() => snapshotFor(model, access.currentUnsafe())).pipe(
|
|
2061
|
-
Effect2.tap((snapshot) => SubscriptionRef.set(snapshotRef, snapshot)),
|
|
2098
|
+
const publish = () => Effect2.sync(() => ({ previous: SubscriptionRef.getUnsafe(snapshotRef), snapshot: snapshotFor(model, access.currentUnsafe()) })).pipe(
|
|
2099
|
+
Effect2.tap(({ snapshot }) => SubscriptionRef.set(snapshotRef, snapshot)),
|
|
2100
|
+
Effect2.tap(
|
|
2101
|
+
({ previous, snapshot }) => Effect2.sync(() => {
|
|
2102
|
+
const ports = portsFor(model);
|
|
2103
|
+
if (![
|
|
2104
|
+
ports?.sync === emittedPorts?.sync,
|
|
2105
|
+
ports?.media === emittedPorts?.media,
|
|
2106
|
+
["healthy", "unresponsive"].includes(previous.connection.sync) === ["healthy", "unresponsive"].includes(snapshot.connection.sync),
|
|
2107
|
+
previous.connection.media === "healthy" === (snapshot.connection.media === "healthy")
|
|
2108
|
+
].every(Boolean)) {
|
|
2109
|
+
emittedPorts = ports;
|
|
2110
|
+
for (const listener of portListeners) {
|
|
2111
|
+
try {
|
|
2112
|
+
listener(ports);
|
|
2113
|
+
} catch {
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
})
|
|
2118
|
+
),
|
|
2062
2119
|
Effect2.asVoid,
|
|
2063
2120
|
Effect2.tap(
|
|
2064
2121
|
() => Effect2.sync(() => {
|
|
@@ -2071,15 +2128,6 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2071
2128
|
})
|
|
2072
2129
|
)
|
|
2073
2130
|
);
|
|
2074
|
-
const emitPorts = () => Effect2.sync(() => {
|
|
2075
|
-
const ports = portsFor(model);
|
|
2076
|
-
for (const listener of portListeners) {
|
|
2077
|
-
try {
|
|
2078
|
-
listener(ports);
|
|
2079
|
-
} catch {
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
});
|
|
2083
2131
|
const transition = (state) => Effect2.gen(function* () {
|
|
2084
2132
|
if (model.state === state) return;
|
|
2085
2133
|
model.state = state;
|
|
@@ -2109,28 +2157,51 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2109
2157
|
model.refreshFiber = yield* Effect2.forkIn(Effect2.sleep(Math.max(0, milliseconds)).pipe(Effect2.andThen(enqueue(refreshAccess())), Effect2.ignore), scope);
|
|
2110
2158
|
});
|
|
2111
2159
|
const refreshAccess = () => Effect2.gen(function* () {
|
|
2112
|
-
|
|
2160
|
+
const scope = model.activeScope;
|
|
2161
|
+
if (!active(model) || !scope || model.refreshInFlight) return;
|
|
2113
2162
|
const epoch = model.epoch;
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2163
|
+
model.refreshInFlight = true;
|
|
2164
|
+
yield* Effect2.forkIn(
|
|
2165
|
+
access.ensureFresh("scheduled_refresh").pipe(
|
|
2166
|
+
Effect2.match({ onSuccess: () => null, onFailure: (cause) => cause }),
|
|
2167
|
+
Effect2.flatMap(
|
|
2168
|
+
(failure3) => enqueue(
|
|
2169
|
+
Effect2.gen(function* () {
|
|
2170
|
+
if (model.epoch !== epoch || !active(model)) return;
|
|
2171
|
+
model.refreshInFlight = false;
|
|
2172
|
+
if (failure3) {
|
|
2173
|
+
diagnostics.record({ event: "access_refresh_failed", state: model.state, epoch, code: failure3.code === "access.invalid" ? "invalid_access" : "access_unavailable" });
|
|
2174
|
+
if (failure3.code === "access.invalid") return yield* failForSnapshot(accessFailure(failure3));
|
|
2175
|
+
model.refreshFailures = Math.min(model.refreshFailures + 1, 5);
|
|
2176
|
+
yield* scheduleRefresh(Math.min(BACKGROUND_RETRY_MAX_MS, REFRESH_RETRY_MS * 2 ** (model.refreshFailures - 1)));
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
model.refreshFailures = 0;
|
|
2180
|
+
diagnostics.record({ event: "access_refreshed", state: model.state, epoch });
|
|
2181
|
+
yield* publish();
|
|
2182
|
+
yield* scheduleRefresh();
|
|
2183
|
+
})
|
|
2184
|
+
)
|
|
2185
|
+
),
|
|
2186
|
+
Effect2.ignore
|
|
2187
|
+
),
|
|
2188
|
+
scope
|
|
2189
|
+
);
|
|
2123
2190
|
});
|
|
2124
2191
|
const stopPorts = () => Effect2.gen(function* () {
|
|
2125
2192
|
yield* stopRefresh();
|
|
2126
2193
|
const scope = model.activeScope;
|
|
2127
2194
|
model.activeScope = null;
|
|
2195
|
+
model.refreshFailures = 0;
|
|
2196
|
+
model.refreshInFlight = false;
|
|
2197
|
+
mediaAttempt.expiresAt = null;
|
|
2128
2198
|
model.sync = null;
|
|
2129
2199
|
model.media = null;
|
|
2130
2200
|
model.syncSnapshot = null;
|
|
2131
2201
|
model.syncNoticeUnresponsive = false;
|
|
2132
2202
|
model.mediaSnapshot = null;
|
|
2133
|
-
|
|
2203
|
+
model.recoveryQueued = null;
|
|
2204
|
+
yield* publish();
|
|
2134
2205
|
if (scope) yield* Scope.close(scope, Exit.void);
|
|
2135
2206
|
yield* access.clear;
|
|
2136
2207
|
yield* publish();
|
|
@@ -2193,19 +2264,16 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2193
2264
|
yield* handleMediaSnapshot(media, media.getSnapshot());
|
|
2194
2265
|
});
|
|
2195
2266
|
const waitForSyncLive = (sync, timeoutMs) => Effect2.callback((resume) => {
|
|
2196
|
-
|
|
2197
|
-
resume(Effect2.
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2267
|
+
const observe = (snapshot) => {
|
|
2268
|
+
if (episodeEnded(snapshot)) resume(Effect2.fail(lifecycleFailure("episode_ended", false, "The Episode has ended")));
|
|
2269
|
+
else if (snapshot.connection.phase === "live") resume(Effect2.void);
|
|
2270
|
+
else if (snapshot.connection.phase === "terminal") resume(Effect2.fail(syncTerminalFailure(snapshot)));
|
|
2271
|
+
else if (snapshot.connection.phase === "stopped") resume(Effect2.fail(lifecycleFailure("sync_start_failed", true, "Sync stopped before becoming live")));
|
|
2272
|
+
};
|
|
2273
|
+
const unsubscribe = sync.subscribe(observe);
|
|
2274
|
+
observe(sync.getSnapshot());
|
|
2204
2275
|
return Effect2.sync(unsubscribe);
|
|
2205
|
-
}).pipe(
|
|
2206
|
-
Effect2.timeout(timeoutMs),
|
|
2207
|
-
Effect2.catchTag("TimeoutError", () => Effect2.fail(lifecycleFailure("sync_start_failed", true, "Sync did not become live before the startup deadline")))
|
|
2208
|
-
);
|
|
2276
|
+
}).pipe((effect) => withinForegroundBudget(effect, timeoutMs, lifecycleFailure("sync_start_failed", true, "Sync did not become live before the startup deadline")));
|
|
2209
2277
|
const trace = (step, effect) => {
|
|
2210
2278
|
const span = diagnostics.startSpan({ step, state: model.state, epoch: model.epoch });
|
|
2211
2279
|
return traceRecovery(step, effect, recordInitialConnection).pipe(
|
|
@@ -2224,9 +2292,12 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2224
2292
|
const scope = yield* Scope.make("sequential");
|
|
2225
2293
|
model.activeScope = scope;
|
|
2226
2294
|
let stream = null;
|
|
2295
|
+
let captureWarning = null;
|
|
2227
2296
|
yield* Effect2.gen(function* () {
|
|
2228
2297
|
if (model.joinCancelled) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
2229
|
-
|
|
2298
|
+
const captured = yield* trace("acquire_initial_media", acquireInitialMedia(model.initialMedia, model.initialMediaIntent?.() ?? model.intent));
|
|
2299
|
+
stream = captured.stream;
|
|
2300
|
+
captureWarning = captured.failure;
|
|
2230
2301
|
if (model.joinCancelled) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
2231
2302
|
const grant = yield* trace("access_initialize", access.initialize(reason));
|
|
2232
2303
|
const media = yield* trace(
|
|
@@ -2235,7 +2306,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2235
2306
|
try: () => platform.createMediaClient({
|
|
2236
2307
|
access: grant,
|
|
2237
2308
|
credential: () => toPromise(access.getMediaToken()),
|
|
2238
|
-
replaceMediaConnection: () => toPromise(access.refresh("media_recovery", true)).then((replacement) =>
|
|
2309
|
+
replaceMediaConnection: () => toPromise(access.refresh("media_recovery", true)).then((replacement) => {
|
|
2310
|
+
if (media === model.media) mediaAttempt.expiresAt = platform.clock.now() + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2311
|
+
return replacement.media;
|
|
2312
|
+
}),
|
|
2239
2313
|
recordReconnect: options.recordReconnect,
|
|
2240
2314
|
telemetry: options.telemetry,
|
|
2241
2315
|
...options.recordRtcSummary ? { recordRtcSummary: options.recordRtcSummary } : {},
|
|
@@ -2275,17 +2349,20 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2275
2349
|
span.end({ state: model.state, epoch: model.epoch, outcome: "cancelled", code: "invalid_state" });
|
|
2276
2350
|
return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Join was cancelled by Leave"));
|
|
2277
2351
|
}
|
|
2352
|
+
span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure3.code });
|
|
2353
|
+
const retry = !retried && failure3.code === "invalid_access";
|
|
2354
|
+
if (!retry) yield* recordFailure(failure3);
|
|
2278
2355
|
yield* stopPorts();
|
|
2279
|
-
if (
|
|
2356
|
+
if (retry) return yield* performJoin("access_retry", true);
|
|
2280
2357
|
yield* transition("failed");
|
|
2281
|
-
span.end({ state: model.state, epoch: model.epoch, outcome: "failed", code: failure3.code });
|
|
2282
2358
|
return yield* Effect2.fail(failure3);
|
|
2283
2359
|
}),
|
|
2284
2360
|
onSuccess: () => Effect2.gen(function* () {
|
|
2285
2361
|
model.syncSnapshot = model.sync?.getSnapshot() ?? null;
|
|
2286
2362
|
model.mediaSnapshot = model.media?.getSnapshot() ?? null;
|
|
2363
|
+
model.failure = captureWarning ? toFailure(captureWarning) : null;
|
|
2287
2364
|
yield* transition("live");
|
|
2288
|
-
yield*
|
|
2365
|
+
if (model.mediaSnapshot?.connection.phase !== "live") yield* recover("media");
|
|
2289
2366
|
yield* scheduleRefresh();
|
|
2290
2367
|
span.end({ state: model.state, epoch: model.epoch, outcome: "succeeded" });
|
|
2291
2368
|
})
|
|
@@ -2308,26 +2385,36 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2308
2385
|
const leave = foreign7(() => sync.leave()).pipe(Effect2.timeout(LEAVE_TIMEOUT_MS));
|
|
2309
2386
|
return (yield* Effect2.exit(leave))._tag === "Success";
|
|
2310
2387
|
});
|
|
2388
|
+
const publishFreshAccess = () => enqueue(
|
|
2389
|
+
Effect2.gen(function* () {
|
|
2390
|
+
if (!active(model)) return;
|
|
2391
|
+
diagnostics.record({ event: "access_refreshed", state: model.state, epoch: model.epoch });
|
|
2392
|
+
yield* publish();
|
|
2393
|
+
yield* scheduleRefresh();
|
|
2394
|
+
})
|
|
2395
|
+
);
|
|
2396
|
+
const requireOpen = Effect2.suspend(() => model.closed ? Effect2.fail(lifecycleFailure("invalid_state", false, "The Connection scope is closed")) : Effect2.void);
|
|
2311
2397
|
const withFreshAccess = (operation) => Effect2.gen(function* () {
|
|
2398
|
+
yield* requireOpen;
|
|
2312
2399
|
if (yield* access.requiresRefresh) {
|
|
2400
|
+
yield* requireOpen;
|
|
2313
2401
|
yield* access.ensureFresh("scheduled_refresh").pipe(Effect2.mapError(accessFailure));
|
|
2314
|
-
|
|
2315
|
-
yield* scheduleRefresh();
|
|
2402
|
+
yield* publishFreshAccess();
|
|
2316
2403
|
}
|
|
2317
|
-
|
|
2404
|
+
const run = requireOpen.pipe(
|
|
2405
|
+
Effect2.andThen(Effect2.suspend(operation)),
|
|
2406
|
+
Effect2.flatMap((value) => requireOpen.pipe(Effect2.as(value)))
|
|
2407
|
+
);
|
|
2408
|
+
return yield* run.pipe(
|
|
2318
2409
|
Effect2.matchEffect({
|
|
2319
2410
|
onSuccess: Effect2.succeed,
|
|
2320
2411
|
onFailure: (cause) => {
|
|
2321
2412
|
if (!accessRejected(cause)) return Effect2.fail(cause);
|
|
2322
|
-
return
|
|
2323
|
-
Effect2.
|
|
2324
|
-
Effect2.
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
})
|
|
2328
|
-
),
|
|
2329
|
-
Effect2.andThen(scheduleRefresh()),
|
|
2330
|
-
Effect2.andThen(operation())
|
|
2413
|
+
return requireOpen.pipe(
|
|
2414
|
+
Effect2.andThen(access.refreshAfterRejection()),
|
|
2415
|
+
Effect2.mapError((failure3) => failure3 instanceof ConnectionAccessFailure ? accessFailure(failure3) : failure3),
|
|
2416
|
+
Effect2.andThen(publishFreshAccess()),
|
|
2417
|
+
Effect2.andThen(run)
|
|
2331
2418
|
);
|
|
2332
2419
|
}
|
|
2333
2420
|
})
|
|
@@ -2346,15 +2433,67 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2346
2433
|
)
|
|
2347
2434
|
);
|
|
2348
2435
|
});
|
|
2349
|
-
const recover = (kind) => Effect2.gen(function*
|
|
2350
|
-
|
|
2351
|
-
if (model
|
|
2436
|
+
const recover = (kind) => Effect2.gen(function* () {
|
|
2437
|
+
const scope = model.activeScope;
|
|
2438
|
+
if (!active(model) || !scope) return;
|
|
2439
|
+
if (model.recoveryQueued !== null) {
|
|
2440
|
+
model.recoveryQueued = kind;
|
|
2441
|
+
yield* publish();
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2352
2444
|
model.recoveryQueued = kind;
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2445
|
+
const phase = kind === "sync" ? model.sync?.getSnapshot().connection.phase : model.media?.getSnapshot().connection.phase;
|
|
2446
|
+
if (phase === "terminal" || phase === "failed") yield* transition("reconnecting");
|
|
2447
|
+
const epoch = model.epoch;
|
|
2448
|
+
yield* Effect2.forkIn(
|
|
2449
|
+
Effect2.gen(function* () {
|
|
2450
|
+
yield* Effect2.sleep(RECONNECT_GRACE_MS);
|
|
2451
|
+
const phase2 = kind === "sync" ? model.sync?.getSnapshot().connection.phase : model.media?.getSnapshot().connection.phase;
|
|
2452
|
+
if (phase2 === "live") {
|
|
2453
|
+
enqueueBackground(finishRecovery(0, epoch));
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
yield* enqueue(
|
|
2457
|
+
Effect2.gen(function* () {
|
|
2458
|
+
if (model.epoch === epoch && active(model)) yield* transition("reconnecting");
|
|
2459
|
+
})
|
|
2460
|
+
);
|
|
2461
|
+
let cycle = 0;
|
|
2462
|
+
const syncAttempt = { client: null, expiresAt: null, started: true };
|
|
2463
|
+
while (model.epoch === epoch && active(model)) {
|
|
2464
|
+
const plan = yield* recoveryPlan(model.recoveryQueued ?? kind);
|
|
2465
|
+
const outcome = yield* attemptRecovery(plan, mediaAttempt, syncAttempt).pipe(
|
|
2466
|
+
Effect2.match({
|
|
2467
|
+
onSuccess: (attempt) => ({ attempt, failure: null }),
|
|
2468
|
+
onFailure: (failure3) => ({ attempt: null, failure: failure3 })
|
|
2469
|
+
})
|
|
2470
|
+
);
|
|
2471
|
+
if (outcome.failure) {
|
|
2472
|
+
const failure3 = outcome.failure;
|
|
2473
|
+
enqueueBackground(Effect2.suspend(() => model.epoch === epoch && active(model) ? failForSnapshot(failure3) : Effect2.void));
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
if (outcome.attempt !== null) {
|
|
2477
|
+
enqueueBackground(finishRecovery(outcome.attempt, epoch));
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
yield* enqueue(
|
|
2481
|
+
Effect2.gen(function* () {
|
|
2482
|
+
if (model.epoch !== epoch || !active(model)) return;
|
|
2483
|
+
const code = plan.kind === "media" ? "media_recovery_exhausted" : "sync_recovery_exhausted";
|
|
2484
|
+
model.failure = toFailure(lifecycleFailure(code, true, "Connection interrupted. Reconnecting in the background."));
|
|
2485
|
+
diagnostics.record({ event: "recovery_exhausted", state: model.state, epoch, code });
|
|
2486
|
+
yield* publish();
|
|
2487
|
+
})
|
|
2488
|
+
);
|
|
2489
|
+
if (model.recoveryQueued !== plan.kind) continue;
|
|
2490
|
+
const delay = Math.min(BACKGROUND_RETRY_MAX_MS, 250 * 2 ** cycle);
|
|
2491
|
+
cycle = Math.min(cycle + 1, 8);
|
|
2492
|
+
yield* Effect2.race(Effect2.sleep(delay * (0.5 + Math.random() * 0.5)), waitForTransportChange(plan.kind));
|
|
2493
|
+
}
|
|
2494
|
+
}),
|
|
2495
|
+
scope
|
|
2496
|
+
);
|
|
2358
2497
|
});
|
|
2359
2498
|
const recoveryPlan = (kind) => Clock2.currentTimeMillis.pipe(
|
|
2360
2499
|
Effect2.map((now) => ({
|
|
@@ -2364,80 +2503,216 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2364
2503
|
delays: options.recovery?.backoffMs?.length ? options.recovery.backoffMs : [100, 250, 500]
|
|
2365
2504
|
}))
|
|
2366
2505
|
);
|
|
2367
|
-
const attemptRecovery = (plan) => Effect2.gen(function*
|
|
2506
|
+
const attemptRecovery = (plan, mediaAttempt2, syncAttempt) => Effect2.gen(function* () {
|
|
2368
2507
|
for (let attempt = 1; attempt <= plan.attempts; attempt += 1) {
|
|
2369
2508
|
const remaining = plan.deadline - (yield* Clock2.currentTimeMillis);
|
|
2370
2509
|
if (remaining <= 0) return null;
|
|
2371
2510
|
recordReconnect(options.recordReconnect, "attempt", { recovery_kind: plan.kind, attempt });
|
|
2372
2511
|
diagnostics.record({ event: "recovery_attempt", state: model.state, epoch: model.epoch, attempt });
|
|
2373
|
-
const
|
|
2374
|
-
|
|
2375
|
-
|
|
2512
|
+
const failure3 = yield* recoveryOperation(mediaAttempt2, syncAttempt).pipe(
|
|
2513
|
+
Effect2.timeout(remaining),
|
|
2514
|
+
Effect2.catchTag("TimeoutError", () => Effect2.fail(lifecycleFailure("sync_start_failed", true, "Connection recovery is still waiting for the transport"))),
|
|
2515
|
+
Effect2.match({ onSuccess: () => null, onFailure: (cause) => cause })
|
|
2516
|
+
);
|
|
2517
|
+
if (failure3 === null) return attempt;
|
|
2518
|
+
if (!failure3.recoverable) return yield* Effect2.fail(failure3);
|
|
2519
|
+
if (attempt < plan.attempts) {
|
|
2520
|
+
const remaining2 = Math.max(0, plan.deadline - (yield* Clock2.currentTimeMillis));
|
|
2521
|
+
yield* traceRecovery("lifecycle_backoff", Effect2.sleep(Math.min(recoveryDelay(plan, attempt), remaining2)));
|
|
2522
|
+
}
|
|
2376
2523
|
}
|
|
2377
2524
|
return null;
|
|
2378
2525
|
});
|
|
2379
|
-
const recoveryOperation = (
|
|
2380
|
-
|
|
2381
|
-
|
|
2526
|
+
const recoveryOperation = (mediaAttempt2, syncAttempt) => Effect2.gen(function* () {
|
|
2527
|
+
const results = yield* Effect2.all(
|
|
2528
|
+
[recoverSync(syncAttempt), recoverMedia(mediaAttempt2)].map((effect) => effect.pipe(Effect2.matchEffect({ onSuccess: () => Effect2.succeed(null), onFailure: (failure4) => failure4.recoverable ? Effect2.succeed(failure4) : Effect2.fail(failure4) }))),
|
|
2529
|
+
{ concurrency: "unbounded" }
|
|
2530
|
+
);
|
|
2531
|
+
const failure3 = results.find((failure4) => failure4 !== null);
|
|
2532
|
+
if (failure3) return yield* Effect2.fail(failure3);
|
|
2533
|
+
});
|
|
2534
|
+
const recoveryDelay = (plan, attempt) => {
|
|
2535
|
+
const delay = plan.delays[Math.min(attempt - 1, plan.delays.length - 1)] ?? 0;
|
|
2536
|
+
return Number.isFinite(delay) ? Math.max(0, delay) : 0;
|
|
2537
|
+
};
|
|
2538
|
+
const finishRecovery = (attempt, epoch) => Effect2.gen(function* () {
|
|
2539
|
+
if (model.epoch !== epoch || !active(model)) return;
|
|
2382
2540
|
model.recoveryQueued = null;
|
|
2383
|
-
|
|
2541
|
+
model.syncSnapshot = model.sync?.getSnapshot() ?? null;
|
|
2542
|
+
model.mediaSnapshot = model.media?.getSnapshot() ?? null;
|
|
2543
|
+
model.failure = null;
|
|
2544
|
+
if (model.syncSnapshot?.connection.phase !== "live") return yield* recover("sync");
|
|
2545
|
+
if (model.mediaSnapshot?.connection.phase !== "live") return yield* recover("media");
|
|
2546
|
+
mediaAttempt.expiresAt = null;
|
|
2547
|
+
diagnostics.record({ event: "recovery_succeeded", state: model.state, epoch, attempt });
|
|
2384
2548
|
yield* transition("live");
|
|
2549
|
+
yield* publish();
|
|
2385
2550
|
yield* scheduleRefresh();
|
|
2386
2551
|
});
|
|
2387
|
-
const
|
|
2388
|
-
const
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2552
|
+
const waitForTransportChange = (kind) => Effect2.callback((resume) => {
|
|
2553
|
+
const sync = model.sync;
|
|
2554
|
+
const media = model.media;
|
|
2555
|
+
const syncPhase2 = sync?.getSnapshot().connection.phase;
|
|
2556
|
+
const mediaPhase2 = media?.getSnapshot().connection.phase;
|
|
2557
|
+
const observe = () => {
|
|
2558
|
+
if (model.recoveryQueued !== kind || model.sync !== sync || model.media !== media || model.sync?.getSnapshot().connection.phase !== syncPhase2 || model.media?.getSnapshot().connection.phase !== mediaPhase2 || syncPhase2 === "live" && mediaPhase2 === "live") resume(Effect2.void);
|
|
2559
|
+
};
|
|
2560
|
+
listeners.add(observe);
|
|
2561
|
+
const unsubscribeSync = sync?.subscribe(observe);
|
|
2562
|
+
const unsubscribeMedia = media?.subscribe(observe);
|
|
2563
|
+
observe();
|
|
2564
|
+
return Effect2.sync(() => {
|
|
2565
|
+
listeners.delete(observe);
|
|
2566
|
+
unsubscribeSync?.();
|
|
2567
|
+
unsubscribeMedia?.();
|
|
2568
|
+
});
|
|
2569
|
+
});
|
|
2570
|
+
const waitForMediaLive = (media) => Effect2.callback((resume) => {
|
|
2571
|
+
const observe = () => {
|
|
2572
|
+
const snapshot = media.getSnapshot();
|
|
2573
|
+
if (snapshot.connection.phase === "live") resume(Effect2.void);
|
|
2574
|
+
else if (snapshot.connection.phase === "failed") resume(Effect2.fail(lifecycleFailure("media_start_failed", snapshot.failure?.recoverable ?? true, "Media transport needs recovery")));
|
|
2575
|
+
};
|
|
2576
|
+
const unsubscribe = media.subscribe(observe);
|
|
2577
|
+
observe();
|
|
2578
|
+
return Effect2.sync(unsubscribe);
|
|
2394
2579
|
});
|
|
2395
|
-
const recoverMedia = () => Effect2.gen(function* () {
|
|
2580
|
+
const recoverMedia = (attempt) => Effect2.gen(function* () {
|
|
2396
2581
|
const media = model.media;
|
|
2397
|
-
|
|
2398
|
-
if (
|
|
2582
|
+
if (!media) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Media recovery requires active ports"));
|
|
2583
|
+
if (media.getSnapshot().connection.phase === "live") {
|
|
2584
|
+
attempt.expiresAt = null;
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
if (media.getSnapshot().connection.phase === "recovering") {
|
|
2588
|
+
const now = yield* Clock2.currentTimeMillis;
|
|
2589
|
+
attempt.expiresAt ??= now + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2590
|
+
attempt.expiresAt = Math.max(attempt.expiresAt, foregroundRecoveryDeadline);
|
|
2591
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") attempt.expiresAt = now + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2592
|
+
if (now < attempt.expiresAt) return yield* waitForMediaLive(media);
|
|
2593
|
+
}
|
|
2399
2594
|
recordReconnect(options.recordReconnect, "media_decision", { strategy: "full_rebuild" });
|
|
2400
2595
|
const grant = yield* traceRecovery("access_refresh", access.refresh("media_recovery", true).pipe(Effect2.mapError(accessFailure)));
|
|
2401
2596
|
const restartInput = grant.media.provider === "cloudflare_sfu" ? grant.media.clientPayload : grant.media;
|
|
2597
|
+
attempt.expiresAt = (yield* Clock2.currentTimeMillis) + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2402
2598
|
yield* traceRecovery(
|
|
2403
2599
|
"media_rebuild",
|
|
2404
|
-
foreign7(() => media.restart(restartInput))
|
|
2600
|
+
foreign7(() => media.restart(restartInput), "media_start_failed", "Media transport could not restart")
|
|
2405
2601
|
);
|
|
2406
|
-
|
|
2602
|
+
yield* waitForMediaLive(media);
|
|
2603
|
+
});
|
|
2604
|
+
const syncAttemptExpired = (attempt, sync, now) => {
|
|
2605
|
+
if (attempt.client !== sync) {
|
|
2606
|
+
attempt.client = sync;
|
|
2607
|
+
attempt.expiresAt = now + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2608
|
+
attempt.started = true;
|
|
2609
|
+
}
|
|
2610
|
+
attempt.expiresAt = Math.max(attempt.expiresAt ?? 0, foregroundRecoveryDeadline);
|
|
2611
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") attempt.expiresAt = now + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2612
|
+
return now >= attempt.expiresAt && sync.getSnapshot().connection.phase !== "live";
|
|
2613
|
+
};
|
|
2614
|
+
const syncNeedsReplacement = (snapshot) => {
|
|
2615
|
+
if (snapshot.connection.phase === "idle" || snapshot.connection.phase === "stopped") return Effect2.succeed(true);
|
|
2616
|
+
if (snapshot.connection.phase !== "terminal") return Effect2.succeed(false);
|
|
2617
|
+
const failure3 = syncTerminalFailure(snapshot);
|
|
2618
|
+
return failure3.recoverable ? Effect2.succeed(true) : Effect2.fail(failure3);
|
|
2619
|
+
};
|
|
2620
|
+
const recoverSync = (attempt) => Effect2.gen(function* () {
|
|
2621
|
+
const sync = model.sync;
|
|
2622
|
+
if (!sync) return yield* restartSync(attempt);
|
|
2623
|
+
const snapshot = sync.getSnapshot();
|
|
2624
|
+
const now = yield* Clock2.currentTimeMillis;
|
|
2625
|
+
if (syncAttemptExpired(attempt, sync, now)) {
|
|
2626
|
+
if (!attempt.started || !sync.restartTransport) return yield* restartSync(attempt);
|
|
2627
|
+
sync.restartTransport();
|
|
2628
|
+
attempt.expiresAt = now + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2629
|
+
}
|
|
2630
|
+
if ((attempt.started || snapshot.connection.phase !== "idle") && (yield* syncNeedsReplacement(snapshot))) return yield* restartSync(attempt);
|
|
2407
2631
|
yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
|
|
2408
2632
|
});
|
|
2409
|
-
const
|
|
2410
|
-
const
|
|
2411
|
-
const scope = model.activeScope;
|
|
2412
|
-
const grant = access.currentUnsafe();
|
|
2413
|
-
if (!media || !scope || !grant) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Sync recovery requires active ports"));
|
|
2633
|
+
const restartSync = (attempt) => Effect2.gen(function* () {
|
|
2634
|
+
const epoch = model.epoch;
|
|
2414
2635
|
yield* access.getSyncToken("sync_recovery").pipe(Effect2.mapError(accessFailure));
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2636
|
+
const sync = yield* enqueue(
|
|
2637
|
+
Effect2.gen(function* () {
|
|
2638
|
+
const media = model.media;
|
|
2639
|
+
const scope = model.activeScope;
|
|
2640
|
+
const grant = access.currentUnsafe();
|
|
2641
|
+
if (model.epoch !== epoch || !active(model) || !media || !scope || !grant) return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "Sync recovery requires active ports"));
|
|
2642
|
+
model.syncBindingCleanup?.();
|
|
2643
|
+
model.sync = null;
|
|
2644
|
+
const sync2 = yield* Effect2.try({
|
|
2645
|
+
try: () => platform.createSyncClient({ access: grant, token: () => toPromise(access.getSyncToken()), media, telemetry: options.telemetry, recordReconnect: options.recordReconnect }),
|
|
2646
|
+
catch: (cause) => lifecycleFailure("sync_start_failed", true, "The sync layer could not start", cause)
|
|
2647
|
+
});
|
|
2648
|
+
model.sync = sync2;
|
|
2649
|
+
yield* bindSync(scope, sync2);
|
|
2650
|
+
return sync2;
|
|
2651
|
+
})
|
|
2652
|
+
);
|
|
2653
|
+
attempt.client = sync;
|
|
2654
|
+
attempt.started = false;
|
|
2655
|
+
attempt.expiresAt = (yield* Clock2.currentTimeMillis) + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2656
|
+
yield* foreign7(
|
|
2657
|
+
() => sync.start().then(() => {
|
|
2658
|
+
if (sync !== model.sync || model.epoch !== epoch) sync.stop();
|
|
2659
|
+
else if (attempt.client === sync) attempt.started = true;
|
|
2660
|
+
})
|
|
2661
|
+
).pipe(
|
|
2662
|
+
Effect2.tapError(
|
|
2663
|
+
() => enqueue(
|
|
2664
|
+
Effect2.gen(function* () {
|
|
2665
|
+
if (sync !== model.sync) return;
|
|
2666
|
+
model.syncBindingCleanup?.();
|
|
2667
|
+
model.sync = null;
|
|
2668
|
+
model.syncSnapshot = null;
|
|
2669
|
+
yield* publish();
|
|
2670
|
+
})
|
|
2671
|
+
)
|
|
2672
|
+
)
|
|
2673
|
+
);
|
|
2424
2674
|
yield* waitForSyncLive(sync, boundedInteger(options.recovery?.budgetMs, RECOVERY_BUDGET_MS, 1, 6e4));
|
|
2425
|
-
yield* emitPorts();
|
|
2426
2675
|
});
|
|
2427
2676
|
const handleSyncSnapshot = (sync, snapshot) => Effect2.gen(function* handleSyncSnapshotEffect() {
|
|
2428
2677
|
if (!isCurrentSyncSnapshot(sync, snapshot)) return;
|
|
2429
2678
|
model.syncSnapshot = snapshot;
|
|
2430
2679
|
if (syncSubjectMismatched(access.currentUnsafe()?.subject ?? null, snapshot)) return yield* failForSnapshot(lifecycleFailure("invalid_access", false, "Sync authenticated a different participant subject"));
|
|
2431
|
-
if (
|
|
2680
|
+
if (yield* settleTerminalSnapshot(snapshot)) return;
|
|
2432
2681
|
if (syncNeedsRecovery(snapshot)) yield* recover("sync");
|
|
2433
2682
|
yield* publish();
|
|
2434
2683
|
});
|
|
2684
|
+
const settleTerminalSnapshot = (snapshot) => Effect2.gen(function* () {
|
|
2685
|
+
const ended = episodeEnded(snapshot);
|
|
2686
|
+
if (ended || snapshot.connection.terminalReason === "participant_inactive") {
|
|
2687
|
+
if (ended) model.episodeEndConfirmed = true;
|
|
2688
|
+
yield* finishTerminalSnapshot();
|
|
2689
|
+
return true;
|
|
2690
|
+
}
|
|
2691
|
+
if (snapshot.connection.phase !== "terminal") return false;
|
|
2692
|
+
const failure3 = syncTerminalFailure(snapshot);
|
|
2693
|
+
if (failure3.recoverable) return false;
|
|
2694
|
+
yield* failForSnapshot(failure3);
|
|
2695
|
+
return true;
|
|
2696
|
+
});
|
|
2435
2697
|
const isCurrentSyncSnapshot = (sync, snapshot) => sync === model.sync && snapshot !== model.syncSnapshot;
|
|
2436
2698
|
const syncSubjectMismatched = (subject, snapshot) => subject !== null && snapshot.participantId !== null && (subject.participantId !== snapshot.participantId || subject.participantGeneration !== snapshot.participantGeneration);
|
|
2437
|
-
const episodeEnded = (snapshot) => snapshot.control?.status === "ended" || snapshot.
|
|
2438
|
-
const
|
|
2699
|
+
const episodeEnded = (snapshot) => snapshot.control?.status === "ended" || snapshot.connection.terminalReason === "episode_ended";
|
|
2700
|
+
const finishTerminalSnapshot = () => Effect2.gen(function* () {
|
|
2701
|
+
model.failure = null;
|
|
2702
|
+
yield* transition("leaving");
|
|
2703
|
+
yield* stopPorts();
|
|
2704
|
+
yield* transition("left");
|
|
2705
|
+
});
|
|
2706
|
+
const syncNeedsRecovery = (snapshot) => active(model) && (snapshot.connection.phase === "connecting" || snapshot.connection.phase === "recovering" || snapshot.connection.phase === "terminal");
|
|
2707
|
+
const recordFailure = (failure3) => Effect2.sync(() => {
|
|
2708
|
+
const reason = model.sync?.getSnapshot().connection.terminalReason;
|
|
2709
|
+
const terminalReason = reason === "participant_inactive" || reason === "stale_participant_generation" || reason === "episode_ended" ? reason : failure3.code;
|
|
2710
|
+
diagnostics.record({ event: "connection_failed", state: model.state, epoch: model.epoch, code: failure3.code });
|
|
2711
|
+
recordReconnect(options.recordReconnect, "terminal_failure", { code: failure3.code, terminal_reason: terminalReason }, "failed");
|
|
2712
|
+
});
|
|
2439
2713
|
const failForSnapshot = (failure3) => Effect2.gen(function* failForSnapshotEffect() {
|
|
2440
2714
|
model.failure = toFailure(failure3);
|
|
2715
|
+
yield* recordFailure(failure3);
|
|
2441
2716
|
yield* stopPorts();
|
|
2442
2717
|
yield* transition("failed");
|
|
2443
2718
|
});
|
|
@@ -2445,21 +2720,43 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2445
2720
|
if (media !== model.media || media.getSnapshot() !== snapshot) return;
|
|
2446
2721
|
if (snapshot === model.mediaSnapshot) return;
|
|
2447
2722
|
model.mediaSnapshot = snapshot;
|
|
2448
|
-
if (active(model) && snapshot.connection.phase === "failed" && snapshot.failure?.recoverable) yield* recover("media");
|
|
2723
|
+
if (active(model) && (snapshot.connection.phase === "recovering" || snapshot.connection.phase === "failed" && snapshot.failure?.recoverable)) yield* recover("media");
|
|
2449
2724
|
yield* publish();
|
|
2450
2725
|
});
|
|
2451
2726
|
const handleMediaFailure = () => model.media ? handleMediaSnapshot(model.media, model.media.getSnapshot()) : Effect2.void;
|
|
2452
2727
|
const applyAutomaticMediaReplacement = (grant) => Effect2.gen(function* () {
|
|
2453
2728
|
const media = model.media;
|
|
2454
|
-
|
|
2729
|
+
const scope = model.activeScope;
|
|
2730
|
+
const epoch = model.epoch;
|
|
2731
|
+
if (!media || !scope || !active(model)) return;
|
|
2455
2732
|
const restartInput = grant.media.provider === "cloudflare_sfu" ? grant.media.clientPayload : grant.media;
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2733
|
+
mediaAttempt.expiresAt = (yield* Clock2.currentTimeMillis) + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2734
|
+
yield* Effect2.forkIn(
|
|
2735
|
+
foreign7(
|
|
2736
|
+
() => media.restart(restartInput).then(() => {
|
|
2737
|
+
if (media !== model.media || model.epoch !== epoch) media.stop();
|
|
2738
|
+
}),
|
|
2739
|
+
"media_start_failed",
|
|
2740
|
+
"The media client could not apply a replacement access grant"
|
|
2741
|
+
).pipe(
|
|
2742
|
+
(effect) => withinForegroundBudget(effect, MEDIA_RECOVERY_TIMEOUT_MS, lifecycleFailure("media_start_failed", true, "Automatic media replacement did not finish")),
|
|
2743
|
+
Effect2.match({ onSuccess: () => true, onFailure: () => false }),
|
|
2744
|
+
Effect2.flatMap(
|
|
2745
|
+
(restarted) => enqueue(
|
|
2746
|
+
Effect2.gen(function* () {
|
|
2747
|
+
if (model.epoch !== epoch || media !== model.media || !active(model)) return;
|
|
2748
|
+
if (!restarted) {
|
|
2749
|
+
yield* recover("media");
|
|
2750
|
+
return;
|
|
2751
|
+
}
|
|
2752
|
+
model.mediaSnapshot = media.getSnapshot();
|
|
2753
|
+
yield* publish();
|
|
2754
|
+
})
|
|
2755
|
+
)
|
|
2756
|
+
)
|
|
2757
|
+
),
|
|
2758
|
+
scope
|
|
2759
|
+
);
|
|
2463
2760
|
});
|
|
2464
2761
|
const notifyScreenEnded = () => Effect2.sync(() => {
|
|
2465
2762
|
if (!active(model)) return;
|
|
@@ -2480,7 +2777,10 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2480
2777
|
}) : Deferred.complete(work.deferred, work.effect).pipe(Effect2.asVoid);
|
|
2481
2778
|
yield* Effect2.forkScoped(Queue.take(queue).pipe(Effect2.flatMap(process), Effect2.forever));
|
|
2482
2779
|
const accessReplacementUnsubscribe = yield* access.subscribeAutomaticMediaReplacement((grant) => enqueueBackground(applyAutomaticMediaReplacement(grant)));
|
|
2483
|
-
const foregroundUnsubscribe = platform.subscribeForeground?.(() =>
|
|
2780
|
+
const foregroundUnsubscribe = platform.subscribeForeground?.(() => {
|
|
2781
|
+
foregroundRecoveryDeadline = platform.clock.now() + MEDIA_RECOVERY_TIMEOUT_MS;
|
|
2782
|
+
enqueueBackground(refreshAccess());
|
|
2783
|
+
}) ?? null;
|
|
2484
2784
|
yield* Effect2.addFinalizer(
|
|
2485
2785
|
() => Effect2.gen(function* () {
|
|
2486
2786
|
model.closed = true;
|
|
@@ -2509,8 +2809,9 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2509
2809
|
},
|
|
2510
2810
|
getDiagnostics: () => diagnostics.snapshot(),
|
|
2511
2811
|
getJoinTrace: () => diagnostics.joinTrace(),
|
|
2512
|
-
setInitialMedia: (provider) => Effect2.sync(() => {
|
|
2812
|
+
setInitialMedia: (provider, intent) => Effect2.sync(() => {
|
|
2513
2813
|
model.initialMedia = provider;
|
|
2814
|
+
model.initialMediaIntent = intent ?? null;
|
|
2514
2815
|
}),
|
|
2515
2816
|
configureJoin: (intent) => Effect2.sync(() => {
|
|
2516
2817
|
if (model.state !== "idle" && model.state !== "left" && model.state !== "failed") throw lifecycleFailure("invalid_state", false, `Cannot configure Join while ${model.state}`);
|
|
@@ -2540,13 +2841,20 @@ var makeConnectionLifecycleLayerFromServices = (options) => Layer3.effect(
|
|
|
2540
2841
|
runCommand: (operation) => enqueue(
|
|
2541
2842
|
Effect2.gen(function* () {
|
|
2542
2843
|
const ports = portsFor(model);
|
|
2543
|
-
if (model.
|
|
2544
|
-
|
|
2545
|
-
if (model.state !== "live") return yield* Effect2.fail(lifecycleFailure("invalid_state", false, "The command belongs to an inactive Connection"));
|
|
2546
|
-
return value;
|
|
2844
|
+
if (!active(model) || !ports || ports.sync.getSnapshot().connection.phase !== "live") return yield* Effect2.fail(lifecycleFailure("invalid_state", true, "Connection interrupted. Try again when reconnected."));
|
|
2845
|
+
return ports;
|
|
2547
2846
|
})
|
|
2847
|
+
).pipe(
|
|
2848
|
+
Effect2.flatMap(
|
|
2849
|
+
(ports) => withFreshAccess(
|
|
2850
|
+
() => Effect2.suspend(() => {
|
|
2851
|
+
if (!active(model) || ports.sync !== model.sync) return Effect2.fail(lifecycleFailure("invalid_state", true, "Connection interrupted. Try again when reconnected."));
|
|
2852
|
+
return operation(ports).pipe(Effect2.flatMap((value) => active(model) && ports.sync === model.sync ? Effect2.succeed(value) : Effect2.fail(lifecycleFailure("invalid_state", true, "The command belongs to an inactive Connection"))));
|
|
2853
|
+
})
|
|
2854
|
+
)
|
|
2855
|
+
)
|
|
2548
2856
|
),
|
|
2549
|
-
runPortCommand: (operation) =>
|
|
2857
|
+
runPortCommand: (operation) => withFreshAccess(operation),
|
|
2550
2858
|
nowUnsafe: () => clock.currentTimeMillisUnsafe(),
|
|
2551
2859
|
scheduleUnsafe: (callback, milliseconds) => platform.clock.setTimeout(callback, milliseconds),
|
|
2552
2860
|
cancelScheduleUnsafe: (handle) => platform.clock.clearTimeout(handle),
|
|
@@ -2564,7 +2872,8 @@ function snapshotFor(model, access) {
|
|
|
2564
2872
|
subject: subjectFor(access),
|
|
2565
2873
|
episode: episodeFor(access, control),
|
|
2566
2874
|
connection: Object.freeze({ sync: model.syncSnapshot?.connection.phase === "live" && model.syncNoticeUnresponsive ? "unresponsive" : syncPhase(model.syncSnapshot?.connection.phase), media: mediaPhase(model.mediaSnapshot?.connection.phase) }),
|
|
2567
|
-
failure: model.failure ? Object.freeze({ ...model.failure }) : null
|
|
2875
|
+
failure: model.failure ? Object.freeze({ ...model.failure }) : null,
|
|
2876
|
+
episodeEnded: model.episodeEndConfirmed
|
|
2568
2877
|
});
|
|
2569
2878
|
}
|
|
2570
2879
|
function subjectFor(access) {
|
|
@@ -2583,7 +2892,7 @@ function portsFor(model) {
|
|
|
2583
2892
|
return model.sync && model.media ? { sync: model.sync, media: model.media } : null;
|
|
2584
2893
|
}
|
|
2585
2894
|
function active(model) {
|
|
2586
|
-
return model.state === "live" || model.state === "reconnecting";
|
|
2895
|
+
return !model.closed && (model.state === "live" || model.state === "reconnecting");
|
|
2587
2896
|
}
|
|
2588
2897
|
function lifecycleFailure(code, recoverable, message, cause) {
|
|
2589
2898
|
return new ConnectionLifecycleFailure({ code, recoverable, message, ...cause === void 0 ? {} : { cause } });
|
|
@@ -2592,11 +2901,60 @@ function toFailure(value) {
|
|
|
2592
2901
|
return Object.freeze({ code: value.code, action: null, recoverable: value.recoverable, message: value.message });
|
|
2593
2902
|
}
|
|
2594
2903
|
function accessFailure(value) {
|
|
2595
|
-
return lifecycleFailure(value.code === "access.invalid" ? "invalid_access" : "access_unavailable", value.code === "access.unavailable", "Access was rejected", value.cause);
|
|
2904
|
+
return lifecycleFailure(value.code === "access.invalid" ? "invalid_access" : "access_unavailable", value.code === "access.unavailable", value.code === "access.invalid" ? "Access was rejected" : "Could not reach the Space. Check your network and try again.", value.cause);
|
|
2596
2905
|
}
|
|
2597
2906
|
function captureFailure(cause) {
|
|
2598
|
-
if (cause instanceof
|
|
2599
|
-
|
|
2907
|
+
if (cause instanceof ConnectionError && cause.cause === void 0) return lifecycleFailure(cause.code, cause.recoverable, cause.message, cause);
|
|
2908
|
+
const failure3 = describeMediaCaptureError(cause instanceof ConnectionError ? cause.cause : cause);
|
|
2909
|
+
if (failure3.code === "unsupported_environment") return lifecycleFailure("media_capture_failed", true, "Media capture failed. Try again or enter with devices off.", cause);
|
|
2910
|
+
return lifecycleFailure(failure3.code, failure3.recoverable, failure3.message, cause);
|
|
2911
|
+
}
|
|
2912
|
+
function acquireInitialMedia(provider, intent) {
|
|
2913
|
+
return provider(intent).pipe(
|
|
2914
|
+
Effect2.map((stream) => ({ stream, failure: null })),
|
|
2915
|
+
Effect2.catch((cause) => {
|
|
2916
|
+
const initialFailure = captureFailure(cause);
|
|
2917
|
+
if (!initialFailure.recoverable) return Effect2.fail(initialFailure);
|
|
2918
|
+
const tracks = /* @__PURE__ */ new Set();
|
|
2919
|
+
return Effect2.gen(function* () {
|
|
2920
|
+
const failures = [];
|
|
2921
|
+
for (const source of ["microphone", "camera"]) {
|
|
2922
|
+
if (!intent[source]) continue;
|
|
2923
|
+
if (!intent.microphone || !intent.camera) {
|
|
2924
|
+
failures.push({ source, failure: initialFailure });
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
yield* provider({ microphone: source === "microphone", camera: source === "camera" }).pipe(
|
|
2928
|
+
Effect2.tap(
|
|
2929
|
+
(stream) => Effect2.sync(() => {
|
|
2930
|
+
for (const track of stream.getTracks()) tracks.add(track);
|
|
2931
|
+
})
|
|
2932
|
+
),
|
|
2933
|
+
Effect2.catch(
|
|
2934
|
+
(cause2) => Effect2.sync(() => {
|
|
2935
|
+
failures.push({ source, failure: captureFailure(cause2) });
|
|
2936
|
+
})
|
|
2937
|
+
)
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
if (tracks.size === 0) yield* provider({ microphone: false, camera: false }).pipe(Effect2.mapError(captureFailure));
|
|
2941
|
+
return { stream: streamFromTracks([...tracks]), failure: initialCaptureWarning(failures) };
|
|
2942
|
+
}).pipe(
|
|
2943
|
+
Effect2.onInterrupt(
|
|
2944
|
+
() => Effect2.sync(() => {
|
|
2945
|
+
for (const track of tracks) track.stop();
|
|
2946
|
+
})
|
|
2947
|
+
)
|
|
2948
|
+
);
|
|
2949
|
+
})
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
function initialCaptureWarning(failures) {
|
|
2953
|
+
const first = failures[0];
|
|
2954
|
+
if (!first) return null;
|
|
2955
|
+
const names = failures.length === 2 ? "Microphone and camera" : first.source === "microphone" ? "Microphone" : "Camera";
|
|
2956
|
+
const message = failures.length === 2 && first.failure.code !== failures[1]?.failure.code ? failures.map(({ source, failure: failure3 }) => `${source === "microphone" ? "Microphone" : "Camera"}: ${failure3.message}`).join(" ") : `${names}: ${first.failure.message}`;
|
|
2957
|
+
return lifecycleFailure(first.failure.code, true, message, first.failure.cause);
|
|
2600
2958
|
}
|
|
2601
2959
|
function failureFromCause(cause, code, message) {
|
|
2602
2960
|
if (cause instanceof ConnectionLifecycleFailure) return cause;
|
|
@@ -2630,6 +2988,12 @@ function mediaPhase(phase) {
|
|
|
2630
2988
|
if (phase === "connecting") return "connecting";
|
|
2631
2989
|
return "idle";
|
|
2632
2990
|
}
|
|
2991
|
+
function syncTerminalFailure(snapshot) {
|
|
2992
|
+
const reason = snapshot.connection.terminalReason;
|
|
2993
|
+
if (reason === "episode_ended") return lifecycleFailure("episode_ended", false, "The Episode has ended");
|
|
2994
|
+
if (reason === "participant_inactive" || reason === "stale_participant_generation") return lifecycleFailure("invalid_access", false, "Participant access is no longer valid");
|
|
2995
|
+
return lifecycleFailure("sync_start_failed", true, "The Sync transport needs recovery");
|
|
2996
|
+
}
|
|
2633
2997
|
|
|
2634
2998
|
// src/space-client/errors.ts
|
|
2635
2999
|
import { Data as Data3 } from "effect";
|
|
@@ -2742,10 +3106,10 @@ var SpaceStore = class _SpaceStore {
|
|
|
2742
3106
|
};
|
|
2743
3107
|
updateConnection(snapshot) {
|
|
2744
3108
|
const current = this.getSnapshot().connection;
|
|
2745
|
-
const error2 = snapshot.failure ? empty({ code:
|
|
3109
|
+
const error2 = snapshot.failure ? empty({ code: publicCode(snapshot.failure.code), recoverable: snapshot.failure.recoverable, message: snapshot.failure.message }) : null;
|
|
2746
3110
|
const status = snapshot.state === "live" && (INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.sync) || INTERRUPTED_CONNECTION_PHASES.has(snapshot.connection.media)) ? "reconnecting" : snapshot.state;
|
|
2747
3111
|
if (current.status === status && sameEpisode(current.episode, snapshot.episode) && sameError(current.lastError, error2)) return;
|
|
2748
|
-
this.#replace("connection", empty({ status, episode: snapshot.episode, lastError: error2 }));
|
|
3112
|
+
this.#replace("connection", empty({ status, episode: snapshot.episode, lastError: sameError(current.lastError, error2) ? current.lastError : error2 }));
|
|
2749
3113
|
}
|
|
2750
3114
|
updateSelf(value) {
|
|
2751
3115
|
this.#replace("self", value);
|
|
@@ -2793,7 +3157,16 @@ var SpaceStoreService = class extends Context4.Service()("@chalk/client/SpaceSto
|
|
|
2793
3157
|
var makeSpaceStoreLayer = Layer4.effect(SpaceStoreService, SpaceStore.make());
|
|
2794
3158
|
var makeFakeSpaceStoreLayer = (store = new SpaceStore()) => Layer4.succeed(SpaceStoreService, store);
|
|
2795
3159
|
function mapCode(code) {
|
|
2796
|
-
return {
|
|
3160
|
+
return {
|
|
3161
|
+
invalid_access: "access.invalid",
|
|
3162
|
+
access_unavailable: "access.unavailable",
|
|
3163
|
+
episode_ended: "episode.ended",
|
|
3164
|
+
invalid_payload: "chat.payload_invalid",
|
|
3165
|
+
permission_denied: "media.permission_denied",
|
|
3166
|
+
unsupported_environment: "environment.unsupported",
|
|
3167
|
+
rate_limited: "command.rate_limited",
|
|
3168
|
+
command_rejected: "command.rejected"
|
|
3169
|
+
}[code] ?? "client.internal_error";
|
|
2797
3170
|
}
|
|
2798
3171
|
function sameEpisode(left, right) {
|
|
2799
3172
|
if (left === right) return true;
|
|
@@ -2805,6 +3178,20 @@ function sameError(left, right) {
|
|
|
2805
3178
|
if (!left || !right) return false;
|
|
2806
3179
|
return left.code === right.code && left.recoverable === right.recoverable && left.message === right.message;
|
|
2807
3180
|
}
|
|
3181
|
+
function publicCode(code) {
|
|
3182
|
+
switch (code) {
|
|
3183
|
+
case "device_not_found":
|
|
3184
|
+
return "media.device_not_found";
|
|
3185
|
+
case "device_busy":
|
|
3186
|
+
return "media.device_busy";
|
|
3187
|
+
case "device_constraint_invalid":
|
|
3188
|
+
return "media.device_constraint_invalid";
|
|
3189
|
+
case "media_capture_failed":
|
|
3190
|
+
return "media.capture_failed";
|
|
3191
|
+
default:
|
|
3192
|
+
return mapCode(code);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
2808
3195
|
|
|
2809
3196
|
// src/feedback/types.ts
|
|
2810
3197
|
var FEEDBACK_EVIDENCE_SCHEMA_VERSION = "FeedbackEvidence/v1";
|
|
@@ -3834,6 +4221,7 @@ var ChatControllerRuntime = class {
|
|
|
3834
4221
|
#offline;
|
|
3835
4222
|
#catchUpRunning = false;
|
|
3836
4223
|
#catchUpRequested = false;
|
|
4224
|
+
#contiguousSequence = null;
|
|
3837
4225
|
#hasOlder = false;
|
|
3838
4226
|
#historyTruncated = false;
|
|
3839
4227
|
#initialCatchUpComplete = true;
|
|
@@ -4027,7 +4415,8 @@ var ChatControllerRuntime = class {
|
|
|
4027
4415
|
this.#ports = ports;
|
|
4028
4416
|
if (!ports) return this.#reset();
|
|
4029
4417
|
this.#status = "ready";
|
|
4030
|
-
this.#
|
|
4418
|
+
if (this.#contiguousSequence === null && ports.sync.getCollaborationExtensionState().chatHeadSequence === null) this.#contiguousSequence = "0";
|
|
4419
|
+
this.#initialCatchUpComplete = this.#contiguousSequence !== null;
|
|
4031
4420
|
this.#unsubscribeEvents = ports.sync.subscribeCollaboration((event) => {
|
|
4032
4421
|
if (event.type === "chat_message") this.#observeMessage(chatMessageFor(event.message), this.#initialCatchUpComplete);
|
|
4033
4422
|
else if (event.type === "chat_read_receipt") this.#mergeReceipt(chatReceiptFor(event.receipt));
|
|
@@ -4039,7 +4428,12 @@ var ChatControllerRuntime = class {
|
|
|
4039
4428
|
}
|
|
4040
4429
|
#scheduleCatchUp() {
|
|
4041
4430
|
const ports = this.#ports;
|
|
4042
|
-
|
|
4431
|
+
const floor = ports?.sync.getCollaborationExtensionState().retainedFloorSequence;
|
|
4432
|
+
if (this.#contiguousSequence === "0" && floor) {
|
|
4433
|
+
this.#contiguousSequence = String(BigInt(floor) - 1n);
|
|
4434
|
+
this.#advanceContiguousSequence();
|
|
4435
|
+
}
|
|
4436
|
+
if (!ports || ports.sync.getSnapshot().connection.phase !== "live" || !catchUpRequest(this.#contiguousSequence, this.#messages.at(-1)?.sequence ?? null, ports)) return;
|
|
4043
4437
|
if (this.#catchUpRunning) {
|
|
4044
4438
|
this.#catchUpRequested = true;
|
|
4045
4439
|
return;
|
|
@@ -4059,7 +4453,6 @@ var ChatControllerRuntime = class {
|
|
|
4059
4453
|
Effect4.ensuring(
|
|
4060
4454
|
Effect4.sync(() => {
|
|
4061
4455
|
this.#catchUpRunning = false;
|
|
4062
|
-
this.#initialCatchUpComplete = true;
|
|
4063
4456
|
if (this.#catchUpRequested) {
|
|
4064
4457
|
this.#catchUpRequested = false;
|
|
4065
4458
|
this.#scheduleCatchUp();
|
|
@@ -4072,17 +4465,19 @@ var ChatControllerRuntime = class {
|
|
|
4072
4465
|
#catchUp(ports) {
|
|
4073
4466
|
return Effect4.suspend(() => {
|
|
4074
4467
|
if (this.#ports !== ports || ports.sync.getSnapshot().connection.phase !== "live") return Effect4.void;
|
|
4075
|
-
const request = catchUpRequest(this.#messages.at(-1)?.sequence ?? null, ports);
|
|
4468
|
+
const request = catchUpRequest(this.#contiguousSequence, this.#messages.at(-1)?.sequence ?? null, ports);
|
|
4076
4469
|
if (!request) return Effect4.void;
|
|
4077
4470
|
return foreign(() => ports.sync.readChatPage(request.input)).pipe(
|
|
4078
4471
|
Effect4.flatMap((result) => {
|
|
4079
4472
|
if (this.#ports !== ports) return Effect4.void;
|
|
4080
4473
|
if (result.status === "cursor_reset") return Effect4.sync(() => this.#applyCursorReset(result.retainedFloorSequence)).pipe(Effect4.andThen(this.#reloadAfterCursorReset(ports)));
|
|
4081
4474
|
if (request.kind === "initial") {
|
|
4475
|
+
this.#completeInitialPage(ports);
|
|
4082
4476
|
this.#hasOlder = result.hasOlder;
|
|
4083
4477
|
this.#publish();
|
|
4084
4478
|
}
|
|
4085
|
-
|
|
4479
|
+
const progressed = request.kind === "initial" || this.#contiguousSequence !== request.input.afterSequence;
|
|
4480
|
+
return progressed ? this.#catchUp(ports) : Effect4.void;
|
|
4086
4481
|
})
|
|
4087
4482
|
);
|
|
4088
4483
|
});
|
|
@@ -4092,11 +4487,12 @@ var ChatControllerRuntime = class {
|
|
|
4092
4487
|
Effect4.tap(
|
|
4093
4488
|
(result) => Effect4.sync(() => {
|
|
4094
4489
|
if (this.#ports !== ports || result.status === "cursor_reset") return;
|
|
4490
|
+
this.#completeInitialPage(ports);
|
|
4095
4491
|
this.#hasOlder = result.hasOlder;
|
|
4096
4492
|
this.#publish();
|
|
4097
4493
|
})
|
|
4098
4494
|
),
|
|
4099
|
-
Effect4.
|
|
4495
|
+
Effect4.andThen(this.#catchUp(ports))
|
|
4100
4496
|
);
|
|
4101
4497
|
}
|
|
4102
4498
|
#applyPage(result) {
|
|
@@ -4112,8 +4508,32 @@ var ChatControllerRuntime = class {
|
|
|
4112
4508
|
this.#hasOlder = false;
|
|
4113
4509
|
this.#historyTruncated = true;
|
|
4114
4510
|
this.#retainedFloorSequence = retainedFloorSequence;
|
|
4511
|
+
this.#messages = Object.freeze(this.#messages.filter((message) => compareChatSequence(message.sequence, retainedFloorSequence) >= 0));
|
|
4512
|
+
this.#contiguousSequence = null;
|
|
4513
|
+
this.#initialCatchUpComplete = false;
|
|
4115
4514
|
this.#publish();
|
|
4116
4515
|
}
|
|
4516
|
+
#completeInitialPage(ports) {
|
|
4517
|
+
const floor = this.#messages[0]?.sequence ?? ports.sync.getCollaborationExtensionState().retainedFloorSequence;
|
|
4518
|
+
this.#contiguousSequence = floor === null ? "0" : String(BigInt(floor) - 1n);
|
|
4519
|
+
this.#advanceContiguousSequence();
|
|
4520
|
+
this.#initialCatchUpComplete = true;
|
|
4521
|
+
}
|
|
4522
|
+
#advanceContiguousSequence(message) {
|
|
4523
|
+
if (this.#contiguousSequence === null) return;
|
|
4524
|
+
let sequence = BigInt(this.#contiguousSequence);
|
|
4525
|
+
if (message) {
|
|
4526
|
+
if (BigInt(message.sequence) !== sequence + 1n) return;
|
|
4527
|
+
sequence += 1n;
|
|
4528
|
+
}
|
|
4529
|
+
for (const message2 of this.#messages) {
|
|
4530
|
+
const candidate = BigInt(message2.sequence);
|
|
4531
|
+
if (candidate <= sequence) continue;
|
|
4532
|
+
if (candidate !== sequence + 1n) break;
|
|
4533
|
+
sequence = candidate;
|
|
4534
|
+
}
|
|
4535
|
+
this.#contiguousSequence = String(sequence);
|
|
4536
|
+
}
|
|
4117
4537
|
#observeMessage(message, countUnread) {
|
|
4118
4538
|
const existing = this.#messages.find((candidate) => candidate.messageId === message.messageId || candidate.sequence === message.sequence);
|
|
4119
4539
|
const next = this.#messagesWith(message, existing);
|
|
@@ -4121,11 +4541,13 @@ var ChatControllerRuntime = class {
|
|
|
4121
4541
|
const unread = this.#unreadCountFor(message, existing, countUnread);
|
|
4122
4542
|
if (this.#messageStateMatches(next, pendingSends, unread)) return existing ?? message;
|
|
4123
4543
|
this.#messages = next;
|
|
4544
|
+
this.#advanceContiguousSequence(message);
|
|
4124
4545
|
this.#pendingSends = Object.freeze(pendingSends);
|
|
4125
4546
|
this.#unreadCount = unread;
|
|
4126
4547
|
this.#status = "ready";
|
|
4127
4548
|
this.#lastError = null;
|
|
4128
4549
|
this.#publish();
|
|
4550
|
+
if (!this.#catchUpRunning && countUnread) this.#scheduleCatchUp();
|
|
4129
4551
|
return message;
|
|
4130
4552
|
}
|
|
4131
4553
|
#messagesWith(message, existing) {
|
|
@@ -4170,6 +4592,7 @@ var ChatControllerRuntime = class {
|
|
|
4170
4592
|
}
|
|
4171
4593
|
#reset() {
|
|
4172
4594
|
this.#catchUpRequested = false;
|
|
4595
|
+
this.#contiguousSequence = null;
|
|
4173
4596
|
this.#hasOlder = false;
|
|
4174
4597
|
this.#historyTruncated = false;
|
|
4175
4598
|
this.#initialCatchUpComplete = true;
|
|
@@ -4213,11 +4636,11 @@ function validateUpload(file, bytes, clientAttachmentId) {
|
|
|
4213
4636
|
function bytesFor(file) {
|
|
4214
4637
|
return "fileName" in file ? Effect4.succeed(file.bytes) : foreign(() => file.arrayBuffer());
|
|
4215
4638
|
}
|
|
4216
|
-
function catchUpRequest(latestSequence, ports) {
|
|
4639
|
+
function catchUpRequest(contiguousSequence, latestSequence, ports) {
|
|
4217
4640
|
const extension = ports.sync.getCollaborationExtensionState();
|
|
4218
|
-
const head = extension.chatHeadSequence;
|
|
4219
|
-
if (!extension.negotiated || head === null ||
|
|
4220
|
-
return
|
|
4641
|
+
const head = latestSequence !== null && (extension.chatHeadSequence === null || compareChatSequence(latestSequence, extension.chatHeadSequence) > 0) ? latestSequence : extension.chatHeadSequence;
|
|
4642
|
+
if (!extension.negotiated || head === null || contiguousSequence !== null && compareChatSequence(contiguousSequence, head) >= 0) return null;
|
|
4643
|
+
return contiguousSequence === null ? { kind: "initial", input: { limit: MAX_CHAT_PAGE_SIZE } } : { kind: "newer", input: { afterSequence: contiguousSequence, limit: MAX_CHAT_PAGE_SIZE } };
|
|
4221
4644
|
}
|
|
4222
4645
|
function chatError(message) {
|
|
4223
4646
|
return new SpaceClientError({ code: "chat.payload_invalid", recoverable: false, message });
|
|
@@ -4249,7 +4672,10 @@ var makeMediaController = (connection, store, selection, diagnostics) => Effect5
|
|
|
4249
4672
|
fork,
|
|
4250
4673
|
diagnostics
|
|
4251
4674
|
);
|
|
4252
|
-
yield* connection.setInitialMedia(
|
|
4675
|
+
yield* connection.setInitialMedia(
|
|
4676
|
+
(intent) => controller.captureInitial(intent),
|
|
4677
|
+
() => controller.initialIntent()
|
|
4678
|
+
);
|
|
4253
4679
|
yield* Effect5.addFinalizer(() => Effect5.sync(() => controller.dispose()));
|
|
4254
4680
|
controller.refreshDevicesInBackground();
|
|
4255
4681
|
return controller;
|
|
@@ -4344,14 +4770,22 @@ var MediaControllerRuntime = class {
|
|
|
4344
4770
|
Effect5.tapError(() => Effect5.sync(() => operation?.fail("decline_failed")))
|
|
4345
4771
|
);
|
|
4346
4772
|
};
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4773
|
+
initialIntent() {
|
|
4774
|
+
return { ...this.#intent };
|
|
4775
|
+
}
|
|
4776
|
+
captureInitial(intent) {
|
|
4777
|
+
if (!intent.microphone && !intent.camera) {
|
|
4778
|
+
this.#intent = { microphone: this.#tracks.has("microphone"), camera: this.#tracks.has("camera") };
|
|
4779
|
+
this.#publish();
|
|
4780
|
+
return Effect5.succeed(streamFromTracks([...this.#tracks.values()]));
|
|
4781
|
+
}
|
|
4782
|
+
return foreign2(() => this.#selection.getUserMedia({ audio: intent.microphone, video: intent.camera })).pipe(
|
|
4350
4783
|
Effect5.flatMap(
|
|
4351
|
-
(stream) => Effect5.try({ try: () => selectInitialTracks(stream,
|
|
4784
|
+
(stream) => Effect5.try({ try: () => selectInitialTracks(stream, intent), catch: (cause) => cause }).pipe(
|
|
4352
4785
|
Effect5.tap(
|
|
4353
4786
|
(tracks) => Effect5.sync(() => {
|
|
4354
4787
|
for (const [source, track] of tracks) this.#tracks.set(source, track);
|
|
4788
|
+
this.#intent = { microphone: this.#tracks.has("microphone"), camera: this.#tracks.has("camera") };
|
|
4355
4789
|
this.#publish();
|
|
4356
4790
|
})
|
|
4357
4791
|
),
|
|
@@ -4744,7 +5178,7 @@ var MediaControllerRuntime = class {
|
|
|
4744
5178
|
}
|
|
4745
5179
|
}
|
|
4746
5180
|
#applyForcedMediaState(ports, snapshot) {
|
|
4747
|
-
if (!snapshot
|
|
5181
|
+
if (!canApplyForcedMediaState(snapshot)) return;
|
|
4748
5182
|
for (const source of MEDIA_SOURCES) {
|
|
4749
5183
|
const projected = snapshot.media.items.find((item) => item.participantId === snapshot.participantId && item.source === source);
|
|
4750
5184
|
if (projected?.publicationId) {
|
|
@@ -4953,6 +5387,9 @@ function isAccessInvalid2(cause) {
|
|
|
4953
5387
|
function isSyncRejection(cause) {
|
|
4954
5388
|
return isAccessInvalid2(cause) || cause instanceof V1SyncError && cause.code === "terminal_failure";
|
|
4955
5389
|
}
|
|
5390
|
+
function canApplyForcedMediaState(snapshot) {
|
|
5391
|
+
return !!snapshot.media && !!snapshot.participantId && snapshot.control?.status !== "ended" && snapshot.connection.phase !== "terminal";
|
|
5392
|
+
}
|
|
4956
5393
|
|
|
4957
5394
|
// src/space-client/offline-actions.ts
|
|
4958
5395
|
import { Context as Context7, Effect as Effect6, Layer as Layer7 } from "effect";
|
|
@@ -6563,7 +7000,7 @@ var makeSpaceClientCoreLayer = (options, platform = {}) => {
|
|
|
6563
7000
|
now: baseDependencies.clock.now,
|
|
6564
7001
|
setTimeout: baseDependencies.clock.setTimeout,
|
|
6565
7002
|
clearTimeout: baseDependencies.clock.clearTimeout,
|
|
6566
|
-
release: { id: "chalk-client@4.1.
|
|
7003
|
+
release: { id: "chalk-client@4.1.17" }
|
|
6567
7004
|
});
|
|
6568
7005
|
const diagnosticSyncClients = /* @__PURE__ */ new Set();
|
|
6569
7006
|
const dependencies = {
|
|
@@ -6645,6 +7082,7 @@ var SpaceClientCore = class {
|
|
|
6645
7082
|
#episodeDiagnosticTracks = /* @__PURE__ */ new Map();
|
|
6646
7083
|
#handlers = { participantJoined: /* @__PURE__ */ new Set(), participantLeft: /* @__PURE__ */ new Set(), episodeEnded: /* @__PURE__ */ new Set(), screenShareStarted: /* @__PURE__ */ new Set(), screenShareStopped: /* @__PURE__ */ new Set(), error: /* @__PURE__ */ new Set() };
|
|
6647
7084
|
#disposed = false;
|
|
7085
|
+
#episodeEnded = false;
|
|
6648
7086
|
#previous;
|
|
6649
7087
|
#unsubscribeConnection = null;
|
|
6650
7088
|
#unsubscribeStore = null;
|
|
@@ -6678,7 +7116,12 @@ var SpaceClientCore = class {
|
|
|
6678
7116
|
this.#previous = this.#store.getSnapshot();
|
|
6679
7117
|
this.#unsubscribeStore = this.#store.subscribe(() => this.#publishStore());
|
|
6680
7118
|
this.#unsubscribeConnection = connection.subscribe(() => {
|
|
6681
|
-
|
|
7119
|
+
const snapshot = connection.getSnapshot();
|
|
7120
|
+
const episode = snapshot.episode ?? this.#store.getSnapshot().connection.episode;
|
|
7121
|
+
this.#store.updateConnection(snapshot);
|
|
7122
|
+
const newlyEnded = snapshot.episodeEnded && !this.#episodeEnded;
|
|
7123
|
+
this.#episodeEnded = snapshot.episodeEnded ?? false;
|
|
7124
|
+
if (newlyEnded) this.#emit("episodeEnded", { episode });
|
|
6682
7125
|
this.#publishConnectionDiagnostics();
|
|
6683
7126
|
});
|
|
6684
7127
|
this.#publishConnectionDiagnostics();
|
|
@@ -6708,6 +7151,7 @@ var SpaceClientCore = class {
|
|
|
6708
7151
|
this.#syncConnectOperation?.fail("connect_failed");
|
|
6709
7152
|
})
|
|
6710
7153
|
),
|
|
7154
|
+
Effect12.catchTag("ConnectionLifecycleFailure", (failure3) => Effect12.fail(failure3.code === "episode_ended" ? new SpaceClientError({ code: "episode.ended", recoverable: failure3.recoverable, message: failure3.message }) : failure3)),
|
|
6711
7155
|
Effect12.mapError(normalizeClientError)
|
|
6712
7156
|
);
|
|
6713
7157
|
}
|
|
@@ -6720,11 +7164,10 @@ var SpaceClientCore = class {
|
|
|
6720
7164
|
);
|
|
6721
7165
|
}
|
|
6722
7166
|
endEpisode() {
|
|
6723
|
-
return trackedEffect(
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
).pipe(Effect12.asVoid, Effect12.mapError(normalizeClientError));
|
|
7167
|
+
return trackedEffect(this.#episodeDiagnostics, "episode.end.authorized", this.#connection.runCommand(({ sync }) => foreign6(() => sync.endEpisode()).pipe(Effect12.tap(() => this.#connection.confirmEpisodeEnded))).pipe(Effect12.tap(() => this.#connection.leave()))).pipe(
|
|
7168
|
+
Effect12.asVoid,
|
|
7169
|
+
Effect12.mapError(normalizeClientError)
|
|
7170
|
+
);
|
|
6728
7171
|
}
|
|
6729
7172
|
extendEpisode(minutes) {
|
|
6730
7173
|
return trackedEffect(
|
|
@@ -6812,7 +7255,6 @@ var SpaceClientCore = class {
|
|
|
6812
7255
|
}
|
|
6813
7256
|
#emitChanges(previous, next) {
|
|
6814
7257
|
this.#emitParticipantChanges(previous, next);
|
|
6815
|
-
this.#emitEpisodeEnd(previous, next);
|
|
6816
7258
|
this.#emitScreenShareChanges(previous, next);
|
|
6817
7259
|
this.#emitErrorChange(previous, next);
|
|
6818
7260
|
}
|
|
@@ -6822,9 +7264,6 @@ var SpaceClientCore = class {
|
|
|
6822
7264
|
for (const participant of after.values()) if (!before.has(participant.participantId)) this.#emit("participantJoined", { participant });
|
|
6823
7265
|
for (const participant of before.values()) if (!after.has(participant.participantId)) this.#emit("participantLeft", { participant });
|
|
6824
7266
|
}
|
|
6825
|
-
#emitEpisodeEnd(previous, next) {
|
|
6826
|
-
if (previous.connection.lastError?.code !== "episode.ended" && next.connection.lastError?.code === "episode.ended") this.#emit("episodeEnded", { episode: previous.connection.episode ?? next.connection.episode });
|
|
6827
|
-
}
|
|
6828
7267
|
#emitScreenShareChanges(previous, next) {
|
|
6829
7268
|
const previousShares = screenShares(previous);
|
|
6830
7269
|
const nextShares = screenShares(next);
|
|
@@ -6886,7 +7325,7 @@ function diagnosticAccessOperation(request) {
|
|
|
6886
7325
|
}
|
|
6887
7326
|
async function fetchAccessGrant2(input) {
|
|
6888
7327
|
if (input.platform.connectionAccess) return input.platform.connectionAccess(input.request);
|
|
6889
|
-
return input.options.getAccess({ space: input.options.space.trim(), reason: accessReason(input.request?.reason), signal: input.request?.signal });
|
|
7328
|
+
return input.options.getAccess({ space: input.options.space.trim(), reason: accessReason(input.request?.reason), replaceMediaConnection: input.request?.replaceMediaConnection ?? false, signal: input.request?.signal });
|
|
6890
7329
|
}
|
|
6891
7330
|
function accessReason(reason) {
|
|
6892
7331
|
if (reason === void 0 || reason === "join") return "join";
|
|
@@ -6999,11 +7438,11 @@ export {
|
|
|
6999
7438
|
ConnectionPlatformService,
|
|
7000
7439
|
makeConnectionPlatformLayer,
|
|
7001
7440
|
makeFakeConnectionPlatformLayer,
|
|
7441
|
+
ChalkWhiteboardV1Error,
|
|
7002
7442
|
CHALK_CHAT_ATTACHMENT_LIMITS,
|
|
7003
7443
|
CHALK_CHAT_ATTACHMENT_MIME_TYPES,
|
|
7004
7444
|
ChalkChatFileError,
|
|
7005
7445
|
createChalkChatFileHttpTransport,
|
|
7006
|
-
ChalkWhiteboardV1Error,
|
|
7007
7446
|
createChalkWhiteboardV1FileHttpTransport,
|
|
7008
7447
|
encodeWhiteboardV1ClientFrame,
|
|
7009
7448
|
decodeWhiteboardV1ServerFrame,
|