@q9labsai/chalk-client 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/grant.d.ts +2 -0
- package/dist/{chunk-GOSWSXTH.js → chunk-MHJ4UPM3.js} +162 -5
- package/dist/chunk-N4IPKRS6.js +76 -0
- package/dist/{chunk-DHUHTIU7.js → chunk-TA2BJXI7.js} +243 -45
- package/dist/{chunk-3TGVTP6Z.js → chunk-UVWRTXNO.js} +894 -72
- package/dist/{chunk-O26BHHMI.js → chunk-VAJ6XNAA.js} +1 -1
- package/dist/{chunk-XSB2GDRW.js → chunk-XF567GPH.js} +198 -26
- package/dist/client.d.ts +180 -0
- package/dist/connection/dependencies.d.ts +5 -0
- package/dist/connection/production.d.ts +1 -1
- package/dist/effect.js +29 -4
- package/dist/generated/http-api.d.ts +178 -0
- package/dist/generated/schemas.d.ts +287 -0
- package/dist/generated/sync.d.ts +53 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +33 -15
- package/dist/media/call-stats.d.ts +27 -0
- package/dist/media/client.d.ts +1 -0
- package/dist/media/ice-servers.d.ts +3 -0
- package/dist/media/index.d.ts +2 -0
- package/dist/media/index.js +4 -1
- package/dist/media/plane.d.ts +1 -0
- package/dist/media/receive-stall.d.ts +11 -2
- package/dist/media/types.d.ts +19 -0
- package/dist/media/video-render-size.d.ts +7 -0
- package/dist/media/viewer-layer.d.ts +48 -0
- package/dist/public-invites/index.js +3 -2
- package/dist/server/index.cjs +18 -4
- package/dist/server/index.js +18 -4
- package/dist/session/index.d.ts +18 -0
- package/dist/sync/index.js +2 -1
- package/dist/sync/link-watch.d.ts +67 -0
- package/dist/sync/v1-client.d.ts +5 -0
- package/dist/sync/v1-command-scheduler.d.ts +4 -0
- package/dist/sync/v1-types.d.ts +12 -1
- package/package.json +2 -1
package/dist/server/index.cjs
CHANGED
|
@@ -115,17 +115,31 @@ function parseMediaAccess(value) {
|
|
|
115
115
|
provider: value.provider,
|
|
116
116
|
clientPayload: {
|
|
117
117
|
connectionId: requireNonEmptyString(payload.connectionId),
|
|
118
|
-
stunServer: requireNonEmptyString(payload.stunServer)
|
|
118
|
+
stunServer: requireNonEmptyString(payload.stunServer),
|
|
119
|
+
...payload.iceServers === void 0 || payload.iceServers === null ? {} : { iceServers: parseIceServers(payload.iceServers) }
|
|
119
120
|
}
|
|
120
121
|
};
|
|
121
122
|
default:
|
|
122
123
|
throw new AccessGrantError();
|
|
123
124
|
}
|
|
124
125
|
}
|
|
126
|
+
function parseIceServers(value) {
|
|
127
|
+
if (!Array.isArray(value)) throw new AccessGrantError();
|
|
128
|
+
return value.map((entry) => {
|
|
129
|
+
if (!isRecord(entry) || !Array.isArray(entry.urls) || entry.urls.length === 0) throw new AccessGrantError();
|
|
130
|
+
const urls = entry.urls.map(requireNonEmptyString);
|
|
131
|
+
if (entry.username === void 0 && entry.credential === void 0) return { urls };
|
|
132
|
+
return { urls, username: requireNonEmptyString(entry.username), credential: requireNonEmptyString(entry.credential) };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
125
135
|
function mediaClientPayload(value) {
|
|
126
136
|
switch (value.provider) {
|
|
127
137
|
case "cloudflare_sfu":
|
|
128
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
connectionId: value.clientPayload.connectionId,
|
|
140
|
+
stunServer: value.clientPayload.stunServer,
|
|
141
|
+
...value.clientPayload.iceServers ? { iceServers: value.clientPayload.iceServers } : {}
|
|
142
|
+
};
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
145
|
function requireCredential(value, audience) {
|
|
@@ -700,9 +714,9 @@ function accessGrant(value) {
|
|
|
700
714
|
function mediaClientPayload2(media) {
|
|
701
715
|
switch (media.provider) {
|
|
702
716
|
case "cloudflare_sfu": {
|
|
703
|
-
const { connectionId, stunServer } = media.client_payload;
|
|
717
|
+
const { connectionId, stunServer, iceServers } = media.client_payload;
|
|
704
718
|
if (typeof connectionId !== "string" || typeof stunServer !== "string") throw invalidResponse(201);
|
|
705
|
-
return { connectionId, stunServer };
|
|
719
|
+
return { connectionId, stunServer, ...iceServers === void 0 ? {} : { iceServers } };
|
|
706
720
|
}
|
|
707
721
|
default:
|
|
708
722
|
throw invalidResponse(201);
|
package/dist/server/index.js
CHANGED
|
@@ -93,17 +93,31 @@ function parseMediaAccess(value) {
|
|
|
93
93
|
provider: value.provider,
|
|
94
94
|
clientPayload: {
|
|
95
95
|
connectionId: requireNonEmptyString(payload.connectionId),
|
|
96
|
-
stunServer: requireNonEmptyString(payload.stunServer)
|
|
96
|
+
stunServer: requireNonEmptyString(payload.stunServer),
|
|
97
|
+
...payload.iceServers === void 0 || payload.iceServers === null ? {} : { iceServers: parseIceServers(payload.iceServers) }
|
|
97
98
|
}
|
|
98
99
|
};
|
|
99
100
|
default:
|
|
100
101
|
throw new AccessGrantError();
|
|
101
102
|
}
|
|
102
103
|
}
|
|
104
|
+
function parseIceServers(value) {
|
|
105
|
+
if (!Array.isArray(value)) throw new AccessGrantError();
|
|
106
|
+
return value.map((entry) => {
|
|
107
|
+
if (!isRecord(entry) || !Array.isArray(entry.urls) || entry.urls.length === 0) throw new AccessGrantError();
|
|
108
|
+
const urls = entry.urls.map(requireNonEmptyString);
|
|
109
|
+
if (entry.username === void 0 && entry.credential === void 0) return { urls };
|
|
110
|
+
return { urls, username: requireNonEmptyString(entry.username), credential: requireNonEmptyString(entry.credential) };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
103
113
|
function mediaClientPayload(value) {
|
|
104
114
|
switch (value.provider) {
|
|
105
115
|
case "cloudflare_sfu":
|
|
106
|
-
return {
|
|
116
|
+
return {
|
|
117
|
+
connectionId: value.clientPayload.connectionId,
|
|
118
|
+
stunServer: value.clientPayload.stunServer,
|
|
119
|
+
...value.clientPayload.iceServers ? { iceServers: value.clientPayload.iceServers } : {}
|
|
120
|
+
};
|
|
107
121
|
}
|
|
108
122
|
}
|
|
109
123
|
function requireCredential(value, audience) {
|
|
@@ -602,9 +616,9 @@ function accessGrant(value) {
|
|
|
602
616
|
function mediaClientPayload2(media) {
|
|
603
617
|
switch (media.provider) {
|
|
604
618
|
case "cloudflare_sfu": {
|
|
605
|
-
const { connectionId, stunServer } = media.client_payload;
|
|
619
|
+
const { connectionId, stunServer, iceServers } = media.client_payload;
|
|
606
620
|
if (typeof connectionId !== "string" || typeof stunServer !== "string") throw invalidResponse(201);
|
|
607
|
-
return { connectionId, stunServer };
|
|
621
|
+
return { connectionId, stunServer, ...iceServers === void 0 ? {} : { iceServers } };
|
|
608
622
|
}
|
|
609
623
|
default:
|
|
610
624
|
throw invalidResponse(201);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session is one run of a Chalk app on one device: a browser tab, or an app launch on mobile. Its ID is the
|
|
3
|
+
* OpenTelemetry attribute `session.id` and travels with the trace context as W3C baggage on every Chalk API request and
|
|
4
|
+
* Sync connection. It is unrelated to an Episode and to Cloudflare track or dashboard login `sessionId`s.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SESSION_ID_KEY = "session.id";
|
|
7
|
+
export type SessionSource = () => string | undefined;
|
|
8
|
+
/** Platforms with their own session rules, such as React Native, install their source once at startup. */
|
|
9
|
+
export declare function setSessionSource(next: SessionSource): void;
|
|
10
|
+
/** The current session ID, or undefined where no app session exists, such as a Node server. */
|
|
11
|
+
export declare function currentSessionId(): string | undefined;
|
|
12
|
+
export declare function newSessionId(): string;
|
|
13
|
+
/** The W3C baggage value that carries the session, or undefined where no app session exists. */
|
|
14
|
+
export declare function sessionBaggage(): string | undefined;
|
|
15
|
+
/** `baggage` and a fresh `traceparent` for one Chalk API request; empty where there is no app session. */
|
|
16
|
+
export declare function requestContextHeaders(): Record<string, string>;
|
|
17
|
+
/** Wraps a fetch so every request carries the session; headers the caller already set win. Use only for Chalk API origins. */
|
|
18
|
+
export declare function withRequestContext(fetchImplementation?: typeof globalThis.fetch): typeof globalThis.fetch;
|
package/dist/sync/index.js
CHANGED
|
@@ -22,7 +22,8 @@ import {
|
|
|
22
22
|
encodeV1ClientFrame,
|
|
23
23
|
optimisticV1Control,
|
|
24
24
|
restoreV1Snapshot
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-XF567GPH.js";
|
|
26
|
+
import "../chunk-N4IPKRS6.js";
|
|
26
27
|
export {
|
|
27
28
|
AsyncStorageV1PendingTargetStore,
|
|
28
29
|
InMemoryV1PendingTargetStore,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decides whether a Sync drop is proven short. Contact is proven only by a ping and the pong that answers it: the pong proves
|
|
3
|
+
* contact at the moment the ping was SENT, and only when the round trip took 0 to `ROUND_TRIP_MS` on both the wall clock and the
|
|
4
|
+
* monotonic clock, with no page freeze, resume or discard in between. A pong that was buffered through a suspend shows a long round
|
|
5
|
+
* trip, a clock step back shows a negative one, and neither proves anything. No other frame proves contact, because a frame says
|
|
6
|
+
* when the page processed it, not when Sync sent it.
|
|
7
|
+
*
|
|
8
|
+
* A pong answers the oldest ping in flight, and no ping is ever evicted. A ping or pong that is lost therefore only makes later
|
|
9
|
+
* pongs pair with an older ping, which records an earlier contact and a longer round trip: it fails closed, and proof stops once the
|
|
10
|
+
* pairing skew passes `ROUND_TRIP_MS`. The one assumption is that Sync sends exactly one pong per ping; a duplicate pong is the only
|
|
11
|
+
* way to pair with a newer ping, and a ping sequence number would remove it. A late pong proves nothing and changes nothing else.
|
|
12
|
+
* When the oldest ping has gone unanswered for over `UNANSWERED_LIMIT_MS`, the old heartbeat timeout, `pinged()` returns false and
|
|
13
|
+
* the client replaces the socket; `restart()` empties the queue on the next one.
|
|
14
|
+
*
|
|
15
|
+
* A drop is long when any of these holds, and nothing else makes it long (a hidden tab, throttled timers and a slow network do not):
|
|
16
|
+
* - no contact was ever proven;
|
|
17
|
+
* - either clock went backwards since the last proven contact;
|
|
18
|
+
* - either clock advanced `LONG_DROP_MS` or more since the last proven contact;
|
|
19
|
+
* - the page was frozen, resumed or discarded since then.
|
|
20
|
+
*
|
|
21
|
+
* Known gap: a tab hidden for over 5 minutes with no live WebRTC connection runs its timers once a minute, so it sends one ping a
|
|
22
|
+
* minute and its last proof can be nearly 60 s old when the link drops. A short drop then reads long and the source turns off.
|
|
23
|
+
* That errs toward off, and Chrome exempts a page that holds a live WebRTC connection.
|
|
24
|
+
*
|
|
25
|
+
* On a quiet call the last ping can be up to 20 s older than the loss, so down time can read up to 20 s too long and the
|
|
26
|
+
* effective cutoff is 40 to 60 s. That errs toward turning the source off.
|
|
27
|
+
*/
|
|
28
|
+
export declare const LONG_DROP_MS = 60000;
|
|
29
|
+
export type LinkReading = {
|
|
30
|
+
readonly wall: number;
|
|
31
|
+
readonly mono: number;
|
|
32
|
+
readonly suspends: number;
|
|
33
|
+
};
|
|
34
|
+
export type LinkJudge = {
|
|
35
|
+
/** The last proof of contact, or null before the first answered ping. */
|
|
36
|
+
readonly proof: () => LinkReading | null;
|
|
37
|
+
/** True unless the time since `proof` is proven short; a missing proof is long. */
|
|
38
|
+
readonly longSince: (proof: LinkReading | null) => boolean;
|
|
39
|
+
};
|
|
40
|
+
export type LinkClock = {
|
|
41
|
+
readonly wall: () => number;
|
|
42
|
+
readonly mono: () => number;
|
|
43
|
+
};
|
|
44
|
+
/** The browser document, or any target that fires the Page Lifecycle `freeze` and `resume` events. */
|
|
45
|
+
export type PageLifecycle = {
|
|
46
|
+
readonly addEventListener: (type: "freeze" | "resume", listener: () => void) => void;
|
|
47
|
+
readonly removeEventListener: (type: "freeze" | "resume", listener: () => void) => void;
|
|
48
|
+
readonly wasDiscarded?: boolean;
|
|
49
|
+
};
|
|
50
|
+
export declare class LinkWatch implements LinkJudge {
|
|
51
|
+
#private;
|
|
52
|
+
constructor(clock: LinkClock, page: PageLifecycle | null);
|
|
53
|
+
/** Listens for page suspension. Idempotent, and paired with `close`. */
|
|
54
|
+
open(): void;
|
|
55
|
+
close(): void;
|
|
56
|
+
/** Call when a connection goes live: the previous socket's pings will not be answered. */
|
|
57
|
+
restart(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Call as a ping goes out, in the same order as the pings. False means the oldest ping has gone unanswered for over
|
|
60
|
+
* `UNANSWERED_LIMIT_MS`: the socket is dead and this ping must not be sent on it.
|
|
61
|
+
*/
|
|
62
|
+
pinged(): boolean;
|
|
63
|
+
/** Call as a pong is received. It answers the oldest ping in flight, and proves contact only if the round trip was short. */
|
|
64
|
+
ponged(): void;
|
|
65
|
+
proof(): LinkReading | null;
|
|
66
|
+
longSince(proof: LinkReading | null): boolean;
|
|
67
|
+
}
|
package/dist/sync/v1-client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LinkJudge } from "./link-watch";
|
|
1
2
|
import type { ChalkChatReactionInput, ChalkChatReactions, ChalkChatMessage, ChalkChatPageResult, ChalkChatReadReceipt, ChalkReaction, ChalkReactionEvent, ChalkSendChatMessageInput, ChalkSyncV1CollaborationCapability } from "../collaboration/types";
|
|
2
3
|
import type { V1AdmissionTarget, V1AssignableRole, V1CommandResult, V1DirectedRequest, V1DirectedRequestResult, V1MediaPause, V1CollaborationEvent, V1CollaborationClient, V1CollaborationExtensionState, V1EpisodeSnapshot, V1SelfMediaTargetResult, V1SyncClientOptions } from "./v1-types";
|
|
3
4
|
type CommandOptions = {
|
|
@@ -9,12 +10,16 @@ type RequestOptions = {
|
|
|
9
10
|
export { V1SyncError } from "./v1-error";
|
|
10
11
|
export declare class V1SyncClient implements V1CollaborationClient {
|
|
11
12
|
#private;
|
|
13
|
+
/** Tells a recovery whether the drop was proven short. */
|
|
14
|
+
readonly link: LinkJudge;
|
|
12
15
|
constructor(options: V1SyncClientOptions);
|
|
13
16
|
restartTransport(): void;
|
|
14
17
|
start(): Promise<void>;
|
|
15
18
|
stop(): void;
|
|
16
19
|
/** Announces completed startup; transport recovery resends until acknowledged. */
|
|
17
20
|
confirmEntered(): Promise<void>;
|
|
21
|
+
/** True when the connected Sync advertised `media_off_reason` in its welcome, so media items may carry `off_reason`. */
|
|
22
|
+
offReasonNegotiated(): boolean;
|
|
18
23
|
getSnapshot(): V1EpisodeSnapshot;
|
|
19
24
|
subscribe(listener: (snapshot: V1EpisodeSnapshot) => void): () => void;
|
|
20
25
|
onMediaPause(listener: (pause: V1MediaPause) => void): () => void;
|
|
@@ -22,6 +22,10 @@ type V1CommandSchedulerOptions = {
|
|
|
22
22
|
readonly ackHeadIsProven: (ack: Extract<V1CommandResult, {
|
|
23
23
|
readonly outcome: "committed" | "satisfied";
|
|
24
24
|
}>) => boolean;
|
|
25
|
+
readonly controlHead: () => {
|
|
26
|
+
readonly revision: number;
|
|
27
|
+
readonly state_digest: string;
|
|
28
|
+
};
|
|
25
29
|
};
|
|
26
30
|
export declare class V1CommandScheduler {
|
|
27
31
|
#private;
|
package/dist/sync/v1-types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConnectionReportTransportEvent } from "../transport-diagnostics";
|
|
2
|
+
import type { PageLifecycle } from "./link-watch";
|
|
2
3
|
import type { SetAdmissionPolicyCommandFrame, SyncV1ServerFrame } from "../generated/sync";
|
|
3
4
|
import type { ClientMediaPlane, MediaPlaneOutcome, MediaPlaneResult, MediaPlaneTarget, MediaPublication, MediaSource } from "../media/plane";
|
|
4
5
|
import type { ChalkChatReactionInput, ChalkChatReactions, ChalkChatMessage, ChalkChatPageResult, ChalkChatReadReceipt, ChalkReaction, ChalkReactionEvent, ChalkSendChatMessageInput, ChalkSyncV1CollaborationCapability } from "../collaboration/types";
|
|
@@ -50,7 +51,13 @@ export type V1ControlState = {
|
|
|
50
51
|
readonly participants: readonly V1Participant[];
|
|
51
52
|
readonly admissionRequests: readonly V1AdmissionRequest[];
|
|
52
53
|
};
|
|
53
|
-
|
|
54
|
+
/** Why Sync shows a media item as off. A missing reason is no evidence, never "off". */
|
|
55
|
+
export type V1MediaOffReason = "host" | "self" | "dead";
|
|
56
|
+
export type V1MediaPublication = MediaPublication & {
|
|
57
|
+
readonly offReason?: V1MediaOffReason;
|
|
58
|
+
/** The control revision of the host stop; only with `offReason: "host"`. */
|
|
59
|
+
readonly offRevision?: number;
|
|
60
|
+
};
|
|
54
61
|
export type V1Presence = {
|
|
55
62
|
readonly admissionRevision?: number;
|
|
56
63
|
/** Stable person identity, independent of the per-device participant ID. */
|
|
@@ -206,6 +213,10 @@ export type V1SyncClientOptions = {
|
|
|
206
213
|
readonly ids?: SyncIdGenerator;
|
|
207
214
|
readonly requestIds?: SyncIdGenerator;
|
|
208
215
|
readonly clock?: SyncClock;
|
|
216
|
+
/** Monotonic milliseconds, `performance.now()` by default. */
|
|
217
|
+
readonly monotonic?: () => number;
|
|
218
|
+
/** The page whose `freeze` and `resume` events show a suspend; the browser document by default, null for none. */
|
|
219
|
+
readonly page?: PageLifecycle | null;
|
|
209
220
|
readonly lifecycle?: SyncLifecycle;
|
|
210
221
|
readonly reconnectDelayMs?: number;
|
|
211
222
|
/** Per-attempt WebSocket opening allowance, clamped to 10–60 seconds; defaults to 10 seconds. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@q9labsai/chalk-client",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -222,6 +222,7 @@
|
|
|
222
222
|
"test:webhooks-package": "pnpm run build && node ./scripts/check-webhook-package.mjs",
|
|
223
223
|
"test:server-package": "pnpm run build && node ./scripts/check-server-package.mjs",
|
|
224
224
|
"test:receive-stall-sfu": "node ./scripts/real-sfu-receive-stall.mjs",
|
|
225
|
+
"test:viewer-layer-sfu": "node ./scripts/real-sfu-viewer-layer.mjs",
|
|
225
226
|
"test:sync-browser": "pnpm run build && node ./scripts/real-browser-sync.mjs",
|
|
226
227
|
"test:sync-node-restart": "pnpm run build && node ./scripts/real-node-restart-sync.mjs",
|
|
227
228
|
"check-types": "tsc-rs --project tsconfig.check-types.json --noEmit",
|