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