@athenaintel/react 0.12.2 → 0.12.4
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/chat/AthenaChatErrorBoundary.d.ts +27 -0
- package/dist/chat/StatewireApprovalCard.d.ts +19 -1
- package/dist/chat/StatewireClientToolBridge.d.ts +4 -0
- package/dist/chat/statewire-approval.d.ts +5 -33
- package/dist/collab/client.d.ts +12 -0
- package/dist/collab/mint.d.ts +5 -1
- package/dist/collab/react.d.ts +6 -0
- package/dist/collab.cjs +66 -6
- package/dist/collab.cjs.map +1 -1
- package/dist/collab.js +66 -6
- package/dist/collab.js.map +1 -1
- package/dist/diagnostics/errors.d.ts +2 -0
- package/dist/index.cjs +825 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +849 -317
- package/dist/index.js.map +1 -1
- package/dist/lib/posthog/before-send.d.ts +9 -0
- package/dist/runtime/useAthenaStatewireRuntime.d.ts +5 -5
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
interface AthenaChatErrorBoundaryProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
}
|
|
5
|
+
interface AthenaChatErrorBoundaryState {
|
|
6
|
+
error: Error | null;
|
|
7
|
+
/** Bumped on retry so the children remount with fresh render state. */
|
|
8
|
+
retrySeq: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Error boundary around the SDK chat surface. A render crash anywhere in the
|
|
12
|
+
* thread tree (a malformed message part, a tool UI throwing, a pre-stream
|
|
13
|
+
* state read) otherwise unmounts the integrator's whole app subtree; this
|
|
14
|
+
* contains it to the chat pane, reports it through the SDK diagnostics bus
|
|
15
|
+
* (`sdk.error`, code `chat_render_crash` — forwarded to PostHog / `onError`
|
|
16
|
+
* when configured), and offers a retry that remounts the chat view. The
|
|
17
|
+
* statewire runtime lives above this boundary in `AthenaProvider`, so a retry
|
|
18
|
+
* re-renders against the still-connected transport.
|
|
19
|
+
*/
|
|
20
|
+
export declare class AthenaChatErrorBoundary extends Component<AthenaChatErrorBoundaryProps, AthenaChatErrorBoundaryState> {
|
|
21
|
+
constructor(props: AthenaChatErrorBoundaryProps);
|
|
22
|
+
static getDerivedStateFromError(error: Error): Partial<AthenaChatErrorBoundaryState>;
|
|
23
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
24
|
+
private handleRetry;
|
|
25
|
+
render(): ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
@@ -1,2 +1,20 @@
|
|
|
1
|
+
import type { StatewireThread } from '@assistant-ui/react-statewire';
|
|
1
2
|
import type { FC } from 'react';
|
|
2
|
-
export
|
|
3
|
+
export interface StatewireApprovalCardProps {
|
|
4
|
+
/** Surface-styling overrides merged onto the default classes. */
|
|
5
|
+
classNames?: {
|
|
6
|
+
/** The approval card container. */
|
|
7
|
+
root?: string;
|
|
8
|
+
/** The "approval was cancelled" notice container. */
|
|
9
|
+
abandoned?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Copy shown when an interrupt payload carries no message. */
|
|
12
|
+
fallbackMessage?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Which pending interrupt this card answers. Defaults to
|
|
15
|
+
* `isApprovalCardInterrupt` (client-tool parks excluded — keep it that way
|
|
16
|
+
* unless the surface handles those itself).
|
|
17
|
+
*/
|
|
18
|
+
interruptPredicate?: (request: StatewireThread.InputRequestState) => request is StatewireThread.CustomInputRequest;
|
|
19
|
+
}
|
|
20
|
+
export declare const StatewireApprovalCard: FC<StatewireApprovalCardProps>;
|
|
@@ -7,6 +7,10 @@ import type { StatewireClientTool } from './statewire-client-tools';
|
|
|
7
7
|
* input request. The bridge runs the matching local handler and resumes the
|
|
8
8
|
* run with a per-call result envelope, so a missing tool or a thrown handler
|
|
9
9
|
* still frees the run with an error the model can act on instead of hanging.
|
|
10
|
+
*
|
|
11
|
+
* The claim lifecycle, liveness guards, and wire-safety live in
|
|
12
|
+
* `@athenaintel/agent-runtime`'s `useStatewireClientToolBridge`; this wrapper
|
|
13
|
+
* only adapts the SDK's toolkit-derived tool shape and transport.
|
|
10
14
|
*/
|
|
11
15
|
export declare function StatewireClientToolBridge({ tools, threadId, }: {
|
|
12
16
|
tools: readonly StatewireClientTool[];
|
|
@@ -1,35 +1,7 @@
|
|
|
1
|
-
import type { StatewireThread } from '@assistant-ui/react-statewire';
|
|
2
|
-
/** Human-in-the-loop approval payload the agora deep-agent runtime parks a run
|
|
3
|
-
* on (shaped by `athena_deep_agent/hitl_bridge.py`). */
|
|
4
|
-
export interface HitlApprovalPayload {
|
|
5
|
-
source: string;
|
|
6
|
-
type: string;
|
|
7
|
-
message: string;
|
|
8
|
-
context?: {
|
|
9
|
-
tool_name?: string;
|
|
10
|
-
tool_id?: string;
|
|
11
|
-
tool_args?: Record<string, unknown>;
|
|
12
|
-
pending_action_count?: number;
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
1
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
2
|
+
* Approval-interrupt primitives, re-exported from `@athenaintel/agent-runtime`
|
|
3
|
+
* — the shared headless home for every statewire chat surface. Kept as a
|
|
4
|
+
* module so existing `@athenaintel/react` imports (and the package's public
|
|
5
|
+
* exports) stay stable.
|
|
20
6
|
*/
|
|
21
|
-
export
|
|
22
|
-
/** A parked interrupt that has not been answered yet. */
|
|
23
|
-
export declare function isPendingInterrupt(request: StatewireThread.InputRequestState): request is StatewireThread.CustomInputRequest;
|
|
24
|
-
/**
|
|
25
|
-
* A pending interrupt the approval card may answer. Client-tool parks are
|
|
26
|
-
* excluded — the client-tool bridge owns those, and a generic Continue resume
|
|
27
|
-
* would corrupt the parked tool call. The source check is deliberately loose
|
|
28
|
-
* (any payload stamped with the client-tool source) so a skewed deploy can
|
|
29
|
-
* never route one here.
|
|
30
|
-
*/
|
|
31
|
-
export declare function isApprovalCardInterrupt(request: StatewireThread.InputRequestState): request is StatewireThread.CustomInputRequest;
|
|
32
|
-
/** Narrow an interrupt payload to the Athena HITL approval shape. */
|
|
33
|
-
export declare function asHitlApproval(value: unknown): HitlApprovalPayload | null;
|
|
34
|
-
/** Best-effort human-readable message from any interrupt payload. */
|
|
35
|
-
export declare function readInterruptMessage(value: unknown): string;
|
|
7
|
+
export { asHitlApproval, hasStatewireThreadExtras, type HitlApprovalPayload, isApprovalCardInterrupt, isPendingInterrupt, readInterruptMessage, } from '@athenaintel/agent-runtime';
|
package/dist/collab/client.d.ts
CHANGED
|
@@ -20,12 +20,24 @@ export interface GenericDocHandle {
|
|
|
20
20
|
/** The live shared document. Owned by the handle; destroyed with it. */
|
|
21
21
|
readonly doc: Y.Doc;
|
|
22
22
|
readonly status: GenericDocStatus;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the initial server sync has completed for the current connection.
|
|
25
|
+
* `status === 'connected'` fires before the server state arrives, so a doc
|
|
26
|
+
* read immediately after connect can still be empty — gate first reads on
|
|
27
|
+
* this (or `waitForSync`).
|
|
28
|
+
*/
|
|
29
|
+
readonly synced: boolean;
|
|
23
30
|
/** Granted access from the last successful mint. */
|
|
24
31
|
readonly accessType: 'r' | 'rw' | null;
|
|
25
32
|
/** Terminal error, when status is 'error' or 'closed'. */
|
|
26
33
|
readonly error: Error | null;
|
|
27
34
|
/** Monotonic change counter — bumps on every status/presence notification. */
|
|
28
35
|
readonly version: number;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve once the initial server sync completes; reject on timeout, on a
|
|
38
|
+
* terminal connection state, or if the handle is destroyed first.
|
|
39
|
+
*/
|
|
40
|
+
waitForSync: (timeoutMs?: number) => Promise<void>;
|
|
29
41
|
getPresence: () => PresenceEntry[];
|
|
30
42
|
/** Broadcast this client's presence state (requires a live connection). */
|
|
31
43
|
setPresence: (state: Record<string, unknown> | null) => void;
|
package/dist/collab/mint.d.ts
CHANGED
|
@@ -29,7 +29,11 @@ export declare class CollabTokenError extends Error {
|
|
|
29
29
|
readonly status: number;
|
|
30
30
|
constructor(status: number, message: string);
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Trim a nested Athena endpoint back to the API origin. Accepts the
|
|
34
|
+
* AthenaProvider `backendUrl` (`…/api/assistant-ui`), the sandbox
|
|
35
|
+
* `ATHENA_API_URL` convention (`…/api/v0`), or a bare origin.
|
|
36
|
+
*/
|
|
33
37
|
export declare function getAthenaApiBaseUrl(backendUrl: string): string;
|
|
34
38
|
export declare function mintCollabToken(args: {
|
|
35
39
|
auth: CollabAuth;
|
package/dist/collab/react.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface UseGenericDocResult {
|
|
|
12
12
|
/** Null until the handle is created (first render effect). */
|
|
13
13
|
doc: Y.Doc | null;
|
|
14
14
|
status: GenericDocStatus;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the initial server sync completed for the current connection —
|
|
17
|
+
* `status === 'connected'` fires before the server state arrives, so gate
|
|
18
|
+
* "is this doc really empty?" UI on this.
|
|
19
|
+
*/
|
|
20
|
+
synced: boolean;
|
|
15
21
|
accessType: 'r' | 'rw' | null;
|
|
16
22
|
error: Error | null;
|
|
17
23
|
presence: PresenceEntry[];
|
package/dist/collab.cjs
CHANGED
|
@@ -50,8 +50,7 @@ function getAuthHeaders(auth) {
|
|
|
50
50
|
return {};
|
|
51
51
|
}
|
|
52
52
|
function getAthenaApiBaseUrl(backendUrl) {
|
|
53
|
-
|
|
54
|
-
return stripped.replace(/\/$/, "");
|
|
53
|
+
return backendUrl.replace(/\/api\/assistant-ui\/?$/, "").replace(/\/api\/v0\/?$/, "").replace(/\/$/, "");
|
|
55
54
|
}
|
|
56
55
|
async function mintCollabToken(args) {
|
|
57
56
|
const base = getAthenaApiBaseUrl(args.auth.backendUrl);
|
|
@@ -94,6 +93,7 @@ function connectGenericDoc(args) {
|
|
|
94
93
|
let refreshTimer = null;
|
|
95
94
|
let destroyed = false;
|
|
96
95
|
let status = "connecting";
|
|
96
|
+
let synced = false;
|
|
97
97
|
let accessType = null;
|
|
98
98
|
let error = null;
|
|
99
99
|
let lastPresence = null;
|
|
@@ -117,6 +117,10 @@ function connectGenericDoc(args) {
|
|
|
117
117
|
};
|
|
118
118
|
const teardownProvider = () => {
|
|
119
119
|
clearRefreshTimer();
|
|
120
|
+
if (synced) {
|
|
121
|
+
synced = false;
|
|
122
|
+
notify();
|
|
123
|
+
}
|
|
120
124
|
if (provider) {
|
|
121
125
|
provider.destroy();
|
|
122
126
|
provider = null;
|
|
@@ -163,7 +167,11 @@ function connectGenericDoc(args) {
|
|
|
163
167
|
yauth: grant.token,
|
|
164
168
|
branch: grant.branch,
|
|
165
169
|
gc: "true"
|
|
166
|
-
}
|
|
170
|
+
},
|
|
171
|
+
// BroadcastChannel would sync same-origin clients locally, bypassing the
|
|
172
|
+
// server's permission enforcement (a read-only tab could write into an
|
|
173
|
+
// editor tab's doc). Permissioned docs must round-trip the server.
|
|
174
|
+
disableBc: true
|
|
167
175
|
});
|
|
168
176
|
provider = nextProvider;
|
|
169
177
|
nextProvider.on("status", (event) => {
|
|
@@ -176,6 +184,13 @@ function connectGenericDoc(args) {
|
|
|
176
184
|
nextProvider.awareness.setLocalState(lastPresence);
|
|
177
185
|
}
|
|
178
186
|
});
|
|
187
|
+
nextProvider.on("sync", (state) => {
|
|
188
|
+
if (provider !== nextProvider) return;
|
|
189
|
+
if (synced !== state) {
|
|
190
|
+
synced = state;
|
|
191
|
+
notify();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
179
194
|
nextProvider.on("connection-close", (event) => {
|
|
180
195
|
if (provider !== nextProvider) return;
|
|
181
196
|
const action = classifyClose(event == null ? void 0 : event.code);
|
|
@@ -205,11 +220,53 @@ function connectGenericDoc(args) {
|
|
|
205
220
|
scheduleRefresh(grant);
|
|
206
221
|
};
|
|
207
222
|
void connect();
|
|
223
|
+
const waitForSync = (timeoutMs = 15e3) => new Promise((resolve, reject) => {
|
|
224
|
+
const terminal = () => {
|
|
225
|
+
if (destroyed) return new Error("Handle destroyed before initial sync");
|
|
226
|
+
if (status === "closed" || status === "error") {
|
|
227
|
+
return error ?? new Error(`Connection ${status} before initial sync`);
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
};
|
|
231
|
+
if (synced) {
|
|
232
|
+
resolve();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const immediate = terminal();
|
|
236
|
+
if (immediate) {
|
|
237
|
+
reject(immediate);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const cleanup = () => {
|
|
241
|
+
clearTimeout(timer);
|
|
242
|
+
listeners.delete(listener);
|
|
243
|
+
};
|
|
244
|
+
const timer = setTimeout(() => {
|
|
245
|
+
cleanup();
|
|
246
|
+
reject(new Error(`Timed out after ${timeoutMs}ms waiting for initial sync`));
|
|
247
|
+
}, timeoutMs);
|
|
248
|
+
const listener = () => {
|
|
249
|
+
if (synced) {
|
|
250
|
+
cleanup();
|
|
251
|
+
resolve();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const err = terminal();
|
|
255
|
+
if (err) {
|
|
256
|
+
cleanup();
|
|
257
|
+
reject(err);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
listeners.add(listener);
|
|
261
|
+
});
|
|
208
262
|
return {
|
|
209
263
|
doc,
|
|
210
264
|
get status() {
|
|
211
265
|
return status;
|
|
212
266
|
},
|
|
267
|
+
get synced() {
|
|
268
|
+
return synced;
|
|
269
|
+
},
|
|
213
270
|
get accessType() {
|
|
214
271
|
return accessType;
|
|
215
272
|
},
|
|
@@ -219,6 +276,7 @@ function connectGenericDoc(args) {
|
|
|
219
276
|
get version() {
|
|
220
277
|
return version;
|
|
221
278
|
},
|
|
279
|
+
waitForSync,
|
|
222
280
|
getPresence: () => {
|
|
223
281
|
if (!provider) return [];
|
|
224
282
|
const entries = [];
|
|
@@ -242,9 +300,10 @@ function connectGenericDoc(args) {
|
|
|
242
300
|
if (destroyed) return;
|
|
243
301
|
destroyed = true;
|
|
244
302
|
teardownProvider();
|
|
303
|
+
status = "closed";
|
|
304
|
+
for (const listener of [...listeners]) listener();
|
|
245
305
|
listeners.clear();
|
|
246
306
|
doc.destroy();
|
|
247
|
-
status = "closed";
|
|
248
307
|
}
|
|
249
308
|
};
|
|
250
309
|
}
|
|
@@ -281,13 +340,14 @@ function useGenericDoc(assetId, options) {
|
|
|
281
340
|
subscribe,
|
|
282
341
|
() => {
|
|
283
342
|
if (!handle) return EMPTY_SNAPSHOT;
|
|
284
|
-
return `${handle.status}|${handle.accessType ?? ""}|${handle.version}`;
|
|
343
|
+
return `${handle.status}|${handle.synced}|${handle.accessType ?? ""}|${handle.version}`;
|
|
285
344
|
},
|
|
286
345
|
() => EMPTY_SNAPSHOT
|
|
287
346
|
);
|
|
288
347
|
return {
|
|
289
348
|
doc: (handle == null ? void 0 : handle.doc) ?? null,
|
|
290
349
|
status: (handle == null ? void 0 : handle.status) ?? "connecting",
|
|
350
|
+
synced: (handle == null ? void 0 : handle.synced) ?? false,
|
|
291
351
|
accessType: (handle == null ? void 0 : handle.accessType) ?? null,
|
|
292
352
|
error: (handle == null ? void 0 : handle.error) ?? null,
|
|
293
353
|
presence: (handle == null ? void 0 : handle.getPresence()) ?? [],
|
|
@@ -297,7 +357,7 @@ function useGenericDoc(assetId, options) {
|
|
|
297
357
|
}
|
|
298
358
|
};
|
|
299
359
|
}
|
|
300
|
-
const EMPTY_SNAPSHOT = "init||0";
|
|
360
|
+
const EMPTY_SNAPSHOT = "init|false||0";
|
|
301
361
|
function defineDocShape(shape) {
|
|
302
362
|
const keys = Object.keys(shape);
|
|
303
363
|
return {
|
package/dist/collab.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collab.cjs","sources":["../src/collab/close-policy.ts","../src/collab/mint.ts","../src/collab/client.ts","../src/collab/react.ts","../src/collab/shape.ts"],"sourcesContent":["/**\n * Keryx WebSocket close-code policy.\n *\n * Keryx (@y/hub) encodes retry semantics in close codes: 4400–4499 are\n * permanent application errors — the client must stop reconnecting until the\n * app acts — while everything else is transient and left to the provider's\n * exponential backoff. Two permanent codes carry specific meaning:\n *\n * - 4401 \"permission revoked\": the token was rejected mid-session (access\n * revoked, permissions changed, or the token aged out server-side). The\n * right response is one fresh mint — which re-runs the server-side\n * permission check — and a reconnect only if the mint succeeds.\n * - 4404 \"document deleted\": permanent; the document is not coming back.\n */\n\nexport const WS_CLOSE_AUTH_REVOKED = 4401;\nexport const WS_CLOSE_DOC_DELETED = 4404;\n\nexport type CloseAction = 'remint' | 'stop' | 'retry';\n\nexport function classifyClose(code: number | undefined): CloseAction {\n if (code === undefined) return 'retry';\n if (code === WS_CLOSE_AUTH_REVOKED) return 'remint';\n if (code >= 4400 && code < 4500) return 'stop';\n return 'retry';\n}\n","/**\n * Collab-token mint client for Generic Doc assets.\n *\n * Calls `POST /api/v0/assets/{assetId}/collab-token` — the only public surface\n * that issues Keryx capability tokens, allowlisted server-side to the\n * `generic_doc` asset type and admin-only during the initial rollout. Tokens\n * are room-bound and short-lived; `connectGenericDoc` re-mints automatically\n * before expiry.\n */\n\nexport interface CollabAuth {\n /** Athena API base or any Athena backend URL (e.g. the AthenaProvider `backendUrl`). */\n backendUrl: string;\n /** Personal or sandbox API key. Used when no bearer token is provided. */\n apiKey?: string;\n /** Per-viewer bearer token. Takes precedence over the API key. */\n token?: string | null;\n}\n\nexport interface CollabTokenResponse {\n token: string;\n access_type: 'r' | 'rw';\n expires_at_ms: number;\n ws_url: string;\n rest_url: string;\n org: string;\n doc_id: string;\n branch: string;\n}\n\nexport class CollabTokenError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = 'CollabTokenError';\n this.status = status;\n }\n}\n\nfunction getAuthHeaders(auth: CollabAuth): Record<string, string> {\n if (auth.token) {\n return { Authorization: `Bearer ${auth.token}` };\n }\n if (auth.apiKey) {\n return { 'X-API-KEY': auth.apiKey };\n }\n return {};\n}\n\n/** Trim a nested Athena endpoint (e.g. `/api/assistant-ui`) back to the API origin. */\nexport function getAthenaApiBaseUrl(backendUrl: string): string {\n const stripped = backendUrl.replace(/\\/api\\/assistant-ui\\/?$/, '');\n return stripped.replace(/\\/$/, '');\n}\n\nexport async function mintCollabToken(args: {\n auth: CollabAuth;\n assetId: string;\n access: 'view' | 'edit';\n signal?: AbortSignal;\n}): Promise<CollabTokenResponse> {\n const base = getAthenaApiBaseUrl(args.auth.backendUrl);\n const response = await fetch(\n `${base}/api/v0/assets/${encodeURIComponent(args.assetId)}/collab-token`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getAuthHeaders(args.auth),\n },\n body: JSON.stringify({ access: args.access }),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n let detail = '';\n try {\n const body: unknown = await response.json();\n if (body && typeof body === 'object' && 'detail' in body) {\n detail = String((body as { detail: unknown }).detail);\n }\n } catch {\n // non-JSON error body; status alone tells the story\n }\n throw new CollabTokenError(\n response.status,\n detail || `Collab token mint failed with status ${response.status}`\n );\n }\n return (await response.json()) as CollabTokenResponse;\n}\n","/**\n * Vanilla (framework-agnostic) live client for Generic Doc assets.\n *\n * `connectGenericDoc` owns the full connection lifecycle: mint a room-bound\n * token via the public API, open the Keryx WebSocket with the v14 provider\n * (`@y/websocket` + `@y/y`), re-mint before expiry, apply the close-code\n * policy (4401 → one fresh mint, other 44xx → permanent stop, everything\n * else → provider backoff), and expose presence from awareness. React apps\n * use `useGenericDoc` from `@athenaintel/react/collab` instead of calling\n * this directly.\n */\n\nimport { WebsocketProvider } from '@y/websocket';\nimport * as Y from '@y/y';\nimport { classifyClose } from './close-policy';\nimport {\n type CollabAuth,\n type CollabTokenResponse,\n CollabTokenError,\n mintCollabToken,\n} from './mint';\n\nexport type GenericDocStatus =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'closed'\n | 'error';\n\nexport interface PresenceEntry {\n clientId: number;\n state: Record<string, unknown>;\n}\n\nexport interface GenericDocHandle {\n /** The live shared document. Owned by the handle; destroyed with it. */\n readonly doc: Y.Doc;\n readonly status: GenericDocStatus;\n /** Granted access from the last successful mint. */\n readonly accessType: 'r' | 'rw' | null;\n /** Terminal error, when status is 'error' or 'closed'. */\n readonly error: Error | null;\n /** Monotonic change counter — bumps on every status/presence notification. */\n readonly version: number;\n getPresence: () => PresenceEntry[];\n /** Broadcast this client's presence state (requires a live connection). */\n setPresence: (state: Record<string, unknown> | null) => void;\n /**\n * Force an early token refresh. Session-preserving: the live connection\n * stays up until the fresh token is minted; a failed mint retries on a\n * short timer instead of dropping the session.\n */\n refreshNow: () => Promise<void>;\n onChange: (listener: () => void) => () => void;\n destroy: () => void;\n}\n\n/** Re-mint this long before token expiry (clamped to a floor for short TTLs). */\nconst REFRESH_HEADROOM_MS = 5 * 60 * 1000;\nconst MIN_REFRESH_DELAY_MS = 30 * 1000;\n\n/** Consecutive 4401→mint cycles allowed without a successful connect between. */\nconst MAX_CONSECUTIVE_REMINTS = 2;\n\nexport function connectGenericDoc(args: {\n assetId: string;\n auth: CollabAuth;\n access?: 'view' | 'edit';\n}): GenericDocHandle {\n const access = args.access ?? 'view';\n const doc = new Y.Doc();\n const listeners = new Set<() => void>();\n\n let provider: WebsocketProvider | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let destroyed = false;\n let status: GenericDocStatus = 'connecting';\n let accessType: 'r' | 'rw' | null = null;\n let error: Error | null = null;\n let lastPresence: Record<string, unknown> | null = null;\n let version = 0;\n let consecutiveRemints = 0;\n\n const notify = () => {\n version += 1;\n for (const listener of listeners) listener();\n };\n\n const setStatus = (next: GenericDocStatus, err: Error | null = null) => {\n if (destroyed && next !== 'closed') return;\n status = next;\n error = err;\n notify();\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer !== null) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const teardownProvider = () => {\n clearRefreshTimer();\n if (provider) {\n provider.destroy();\n provider = null;\n }\n };\n\n const scheduleRefresh = (grant: CollabTokenResponse) => {\n clearRefreshTimer();\n const delay = Math.max(\n grant.expires_at_ms - Date.now() - REFRESH_HEADROOM_MS,\n MIN_REFRESH_DELAY_MS\n );\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, delay);\n };\n\n const connect = async (opts?: { scheduledRefresh?: boolean }): Promise<void> => {\n if (destroyed) return;\n const scheduledRefresh = opts?.scheduledRefresh === true;\n // A scheduled refresh mints FIRST and keeps the live session up: the old\n // token has ~5 minutes of headroom, so a transient mint failure retries\n // on a short timer instead of dropping a healthy connection.\n if (!scheduledRefresh) {\n teardownProvider();\n } else {\n clearRefreshTimer();\n }\n let grant: CollabTokenResponse;\n try {\n grant = await mintCollabToken({ auth: args.auth, assetId: args.assetId, access });\n } catch (err) {\n if (scheduledRefresh && !destroyed) {\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, MIN_REFRESH_DELAY_MS);\n return;\n }\n // A denied mint is authoritative (revoked / not shared / not eligible);\n // anything else (network) is worth telling the caller about too — the\n // handle stays usable via a later explicit reconnect-by-recreate.\n setStatus('error', err instanceof Error ? err : new Error(String(err)));\n return;\n }\n if (destroyed) return;\n if (scheduledRefresh) {\n teardownProvider();\n }\n accessType = grant.access_type;\n\n const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {\n params: {\n yauth: grant.token,\n branch: grant.branch,\n gc: 'true',\n },\n });\n provider = nextProvider;\n\n nextProvider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {\n if (provider !== nextProvider) return;\n if (event.status === 'connected') {\n consecutiveRemints = 0;\n }\n setStatus(event.status);\n if (event.status === 'connected' && lastPresence !== null) {\n nextProvider.awareness.setLocalState(lastPresence);\n }\n });\n\n nextProvider.on('connection-close', (event: CloseEvent | null) => {\n if (provider !== nextProvider) return;\n const action = classifyClose(event?.code);\n if (action === 'remint') {\n // Revoked mid-session: a fresh mint re-runs the server-side permission\n // check. If access is truly gone the mint 403s and we land in 'error'.\n // Bounded: repeated 4401s without an intervening successful connect\n // mean the server keeps rejecting freshly minted tokens — stop rather\n // than loop mint→connect→kick.\n consecutiveRemints += 1;\n teardownProvider();\n if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {\n setStatus(\n 'closed',\n new Error('Access repeatedly revoked mid-session (4401); giving up.')\n );\n return;\n }\n void connect();\n } else if (action === 'stop') {\n teardownProvider();\n setStatus(\n 'closed',\n new Error(`Connection closed permanently (code ${event?.code ?? 'unknown'})`)\n );\n }\n // 'retry': the provider's exponential backoff handles it.\n });\n\n nextProvider.awareness.on('change', () => {\n if (provider !== nextProvider) return;\n notify();\n });\n\n scheduleRefresh(grant);\n };\n\n void connect();\n\n return {\n doc,\n get status() {\n return status;\n },\n get accessType() {\n return accessType;\n },\n get error() {\n return error;\n },\n get version() {\n return version;\n },\n getPresence: () => {\n if (!provider) return [];\n const entries: PresenceEntry[] = [];\n provider.awareness.getStates().forEach((state, clientId) => {\n entries.push({ clientId, state: state as Record<string, unknown> });\n });\n return entries;\n },\n setPresence: (state) => {\n lastPresence = state;\n provider?.awareness.setLocalState(state);\n },\n refreshNow: () => connect({ scheduledRefresh: true }),\n onChange: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n teardownProvider();\n listeners.clear();\n doc.destroy();\n status = 'closed';\n },\n };\n}\n\nexport { CollabTokenError };\nexport type { CollabAuth, CollabTokenResponse };\n","/**\n * React bindings for Generic Doc live collaboration.\n *\n * `useGenericDoc(assetId)` opens (and owns) a `connectGenericDoc` handle,\n * resolving auth from the surrounding `AthenaProvider` (per-viewer bearer when\n * present, API key otherwise) and re-rendering on status/presence changes via\n * `useSyncExternalStore`.\n */\n\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type GenericDocHandle,\n type GenericDocStatus,\n type PresenceEntry,\n connectGenericDoc,\n} from './client';\nimport type * as Y from '@y/y';\n\nexport interface UseGenericDocResult {\n /** Null until the handle is created (first render effect). */\n doc: Y.Doc | null;\n status: GenericDocStatus;\n accessType: 'r' | 'rw' | null;\n error: Error | null;\n presence: PresenceEntry[];\n setPresence: (state: Record<string, unknown> | null) => void;\n}\n\nexport function useGenericDoc(\n assetId: string,\n options?: { access?: 'view' | 'edit' }\n): UseGenericDocResult {\n const config = useAthenaConfig();\n const access = options?.access ?? 'view';\n const [handle, setHandle] = useState<GenericDocHandle | null>(null);\n const handleRef = useRef<GenericDocHandle | null>(null);\n\n useEffect(() => {\n const next = connectGenericDoc({\n assetId,\n access,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n },\n });\n handleRef.current = next;\n setHandle(next);\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or credentials change; the handle re-mints on\n // its own schedule otherwise.\n }, [assetId, access, config.backendUrl, config.apiKey, config.token]);\n\n const subscribe = useMemo(() => {\n return (onStoreChange: () => void) => {\n if (!handle) return () => {};\n return handle.onChange(onStoreChange);\n };\n }, [handle]);\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => {\n if (!handle) return EMPTY_SNAPSHOT;\n return `${handle.status}|${handle.accessType ?? ''}|${handle.version}`;\n },\n () => EMPTY_SNAPSHOT\n );\n void snapshot;\n\n return {\n doc: handle?.doc ?? null,\n status: handle?.status ?? 'connecting',\n accessType: handle?.accessType ?? null,\n error: handle?.error ?? null,\n presence: handle?.getPresence() ?? [],\n setPresence: (state) => handleRef.current?.setPresence(state),\n };\n}\n\nconst EMPTY_SNAPSHOT = 'init||0';\n","/**\n * `defineDocShape` — a typed lens over a Generic Doc's root types.\n *\n * A shape is documentation + ergonomics, not a migration system: it names the\n * root keys an app expects and gives JSON-level reads (`toJSON`, `subscribe`)\n * that hide CRDT mechanics for the common case. The raw `Y.Doc` (yjs v14 /\n * `@y/y`) stays available on the handle for full delta-level power.\n */\n\nimport type * as Y from '@y/y';\n\nexport type DocShapeKind = 'map' | 'array' | 'text' | 'xml';\n\nexport interface BoundDocShape<S extends Record<string, DocShapeKind>> {\n /** The live root type for a declared key (v14 unified `YType`). */\n get: <K extends keyof S & string>(key: K) => ReturnType<Y.Doc['get']>;\n /** JSON snapshot of every declared root (v14 node shape: attributes + `children`). */\n toJSON: () => Record<keyof S & string, unknown>;\n /**\n * Subscribe to JSON snapshots. Fires once immediately, then after every\n * change to any declared root (batched per transaction flush).\n */\n subscribe: (listener: (json: Record<keyof S & string, unknown>) => void) => () => void;\n}\n\nexport function defineDocShape<S extends Record<string, DocShapeKind>>(shape: S) {\n const keys = Object.keys(shape) as Array<keyof S & string>;\n return {\n shape,\n bind(doc: Y.Doc): BoundDocShape<S> {\n const toJSON = () => {\n const out: Record<string, unknown> = {};\n for (const key of keys) {\n out[key] = doc.get(key).toJSON();\n }\n return out as Record<keyof S & string, unknown>;\n };\n return {\n get: (key) => doc.get(key),\n toJSON,\n subscribe: (listener) => {\n let scheduled = false;\n const emit = () => {\n scheduled = false;\n listener(toJSON());\n };\n const onDeepChange = () => {\n if (scheduled) return;\n scheduled = true;\n queueMicrotask(emit);\n };\n const roots = keys.map((key) => doc.get(key));\n for (const root of roots) {\n root.observeDeep(onDeepChange);\n }\n listener(toJSON());\n return () => {\n for (const root of roots) {\n root.unobserveDeep(onDeepChange);\n }\n };\n },\n };\n },\n };\n}\n"],"names":["Y","WebsocketProvider","useAthenaConfig","useState","useRef","useEffect","useMemo","useSyncExternalStore"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,MAAM,wBAAwB;AAC9B,MAAM,uBAAuB;AAI7B,SAAS,cAAc,MAAuC;AACnE,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI,SAAS,sBAAuB,QAAO;AAC3C,MAAI,QAAQ,QAAQ,OAAO,KAAM,QAAO;AACxC,SAAO;AACT;ACKO,MAAM,yBAAyB,MAAM;AAAA,EAG1C,YAAY,QAAgB,SAAiB;AAC3C,UAAM,OAAO;AAHN;AAIP,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAEA,SAAS,eAAe,MAA0C;AAChE,MAAI,KAAK,OAAO;AACd,WAAO,EAAE,eAAe,UAAU,KAAK,KAAK,GAAA;AAAA,EAC9C;AACA,MAAI,KAAK,QAAQ;AACf,WAAO,EAAE,aAAa,KAAK,OAAA;AAAA,EAC7B;AACA,SAAO,CAAA;AACT;AAGO,SAAS,oBAAoB,YAA4B;AAC9D,QAAM,WAAW,WAAW,QAAQ,2BAA2B,EAAE;AACjE,SAAO,SAAS,QAAQ,OAAO,EAAE;AACnC;AAEA,eAAsB,gBAAgB,MAKL;AAC/B,QAAM,OAAO,oBAAoB,KAAK,KAAK,UAAU;AACrD,QAAM,WAAW,MAAM;AAAA,IACrB,GAAG,IAAI,kBAAkB,mBAAmB,KAAK,OAAO,CAAC;AAAA,IACzD;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG,eAAe,KAAK,IAAI;AAAA,MAAA;AAAA,MAE7B,MAAM,KAAK,UAAU,EAAE,QAAQ,KAAK,QAAQ;AAAA,MAC5C,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,QAAI,SAAS;AACb,QAAI;AACF,YAAM,OAAgB,MAAM,SAAS,KAAA;AACrC,UAAI,QAAQ,OAAO,SAAS,YAAY,YAAY,MAAM;AACxD,iBAAS,OAAQ,KAA6B,MAAM;AAAA,MACtD;AAAA,IACF,QAAQ;AAAA,IAER;AACA,UAAM,IAAI;AAAA,MACR,SAAS;AAAA,MACT,UAAU,wCAAwC,SAAS,MAAM;AAAA,IAAA;AAAA,EAErE;AACA,SAAQ,MAAM,SAAS,KAAA;AACzB;ACjCA,MAAM,sBAAsB,IAAI,KAAK;AACrC,MAAM,uBAAuB,KAAK;AAGlC,MAAM,0BAA0B;AAEzB,SAAS,kBAAkB,MAIb;AACnB,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,MAAM,IAAIA,aAAE,IAAA;AAClB,QAAM,gCAAgB,IAAA;AAEtB,MAAI,WAAqC;AACzC,MAAI,eAAqD;AACzD,MAAI,YAAY;AAChB,MAAI,SAA2B;AAC/B,MAAI,aAAgC;AACpC,MAAI,QAAsB;AAC1B,MAAI,eAA+C;AACnD,MAAI,UAAU;AACd,MAAI,qBAAqB;AAEzB,QAAM,SAAS,MAAM;AACnB,eAAW;AACX,eAAW,YAAY,UAAW,UAAA;AAAA,EACpC;AAEA,QAAM,YAAY,CAAC,MAAwB,MAAoB,SAAS;AACtE,QAAI,aAAa,SAAS,SAAU;AACpC,aAAS;AACT,YAAQ;AACR,WAAA;AAAA,EACF;AAEA,QAAM,oBAAoB,MAAM;AAC9B,QAAI,iBAAiB,MAAM;AACzB,mBAAa,YAAY;AACzB,qBAAe;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,mBAAmB,MAAM;AAC7B,sBAAA;AACA,QAAI,UAAU;AACZ,eAAS,QAAA;AACT,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,kBAAkB,CAAC,UAA+B;AACtD,sBAAA;AACA,UAAM,QAAQ,KAAK;AAAA,MACjB,MAAM,gBAAgB,KAAK,IAAA,IAAQ;AAAA,MACnC;AAAA,IAAA;AAEF,mBAAe,WAAW,MAAM;AAC9B,WAAK,QAAQ,EAAE,kBAAkB,MAAM;AAAA,IACzC,GAAG,KAAK;AAAA,EACV;AAEA,QAAM,UAAU,OAAO,SAAyD;AAC9E,QAAI,UAAW;AACf,UAAM,oBAAmB,6BAAM,sBAAqB;AAIpD,QAAI,CAAC,kBAAkB;AACrB,uBAAA;AAAA,IACF,OAAO;AACL,wBAAA;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,gBAAgB,EAAE,MAAM,KAAK,MAAM,SAAS,KAAK,SAAS,QAAQ;AAAA,IAClF,SAAS,KAAK;AACZ,UAAI,oBAAoB,CAAC,WAAW;AAClC,uBAAe,WAAW,MAAM;AAC9B,eAAK,QAAQ,EAAE,kBAAkB,MAAM;AAAA,QACzC,GAAG,oBAAoB;AACvB;AAAA,MACF;AAIA,gBAAU,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AACtE;AAAA,IACF;AACA,QAAI,UAAW;AACf,QAAI,kBAAkB;AACpB,uBAAA;AAAA,IACF;AACA,iBAAa,MAAM;AAEnB,UAAM,eAAe,IAAIC,UAAAA,kBAAkB,MAAM,QAAQ,GAAG,MAAM,GAAG,IAAI,MAAM,MAAM,IAAI,KAAK;AAAA,MAC5F,QAAQ;AAAA,QACN,OAAO,MAAM;AAAA,QACb,QAAQ,MAAM;AAAA,QACd,IAAI;AAAA,MAAA;AAAA,IACN,CACD;AACD,eAAW;AAEX,iBAAa,GAAG,UAAU,CAAC,UAAmE;AAC5F,UAAI,aAAa,aAAc;AAC/B,UAAI,MAAM,WAAW,aAAa;AAChC,6BAAqB;AAAA,MACvB;AACA,gBAAU,MAAM,MAAM;AACtB,UAAI,MAAM,WAAW,eAAe,iBAAiB,MAAM;AACzD,qBAAa,UAAU,cAAc,YAAY;AAAA,MACnD;AAAA,IACF,CAAC;AAED,iBAAa,GAAG,oBAAoB,CAAC,UAA6B;AAChE,UAAI,aAAa,aAAc;AAC/B,YAAM,SAAS,cAAc,+BAAO,IAAI;AACxC,UAAI,WAAW,UAAU;AAMvB,8BAAsB;AACtB,yBAAA;AACA,YAAI,qBAAqB,yBAAyB;AAChD;AAAA,YACE;AAAA,YACA,IAAI,MAAM,0DAA0D;AAAA,UAAA;AAEtE;AAAA,QACF;AACA,aAAK,QAAA;AAAA,MACP,WAAW,WAAW,QAAQ;AAC5B,yBAAA;AACA;AAAA,UACE;AAAA,UACA,IAAI,MAAM,wCAAuC,+BAAO,SAAQ,SAAS,GAAG;AAAA,QAAA;AAAA,MAEhF;AAAA,IAEF,CAAC;AAED,iBAAa,UAAU,GAAG,UAAU,MAAM;AACxC,UAAI,aAAa,aAAc;AAC/B,aAAA;AAAA,IACF,CAAC;AAED,oBAAgB,KAAK;AAAA,EACvB;AAEA,OAAK,QAAA;AAEL,SAAO;AAAA,IACL;AAAA,IACA,IAAI,SAAS;AACX,aAAO;AAAA,IACT;AAAA,IACA,IAAI,aAAa;AACf,aAAO;AAAA,IACT;AAAA,IACA,IAAI,QAAQ;AACV,aAAO;AAAA,IACT;AAAA,IACA,IAAI,UAAU;AACZ,aAAO;AAAA,IACT;AAAA,IACA,aAAa,MAAM;AACjB,UAAI,CAAC,SAAU,QAAO,CAAA;AACtB,YAAM,UAA2B,CAAA;AACjC,eAAS,UAAU,UAAA,EAAY,QAAQ,CAAC,OAAO,aAAa;AAC1D,gBAAQ,KAAK,EAAE,UAAU,MAAA,CAAyC;AAAA,MACpE,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IACA,aAAa,CAAC,UAAU;AACtB,qBAAe;AACf,2CAAU,UAAU,cAAc;AAAA,IACpC;AAAA,IACA,YAAY,MAAM,QAAQ,EAAE,kBAAkB,MAAM;AAAA,IACpD,UAAU,CAAC,aAAa;AACtB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM;AACX,kBAAU,OAAO,QAAQ;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,SAAS,MAAM;AACb,UAAI,UAAW;AACf,kBAAY;AACZ,uBAAA;AACA,gBAAU,MAAA;AACV,UAAI,QAAA;AACJ,eAAS;AAAA,IACX;AAAA,EAAA;AAEJ;AClOO,SAAS,cACd,SACA,SACqB;AACrB,QAAM,SAASC,cAAAA,gBAAA;AACf,QAAM,UAAS,mCAAS,WAAU;AAClC,QAAM,CAAC,QAAQ,SAAS,IAAIC,MAAAA,SAAkC,IAAI;AAClE,QAAM,YAAYC,MAAAA,OAAgC,IAAI;AAEtDC,QAAAA,UAAU,MAAM;AACd,UAAM,OAAO,kBAAkB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,MAAM;AAAA,QACJ,YAAY,OAAO;AAAA,QACnB,QAAQ,OAAO;AAAA,QACf,OAAO,OAAO;AAAA,MAAA;AAAA,IAChB,CACD;AACD,cAAU,UAAU;AACpB,cAAU,IAAI;AACd,WAAO,MAAM;AACX,gBAAU,UAAU;AACpB,WAAK,QAAA;AAAA,IACP;AAAA,EAGF,GAAG,CAAC,SAAS,QAAQ,OAAO,YAAY,OAAO,QAAQ,OAAO,KAAK,CAAC;AAEpE,QAAM,YAAYC,MAAAA,QAAQ,MAAM;AAC9B,WAAO,CAAC,kBAA8B;AACpC,UAAI,CAAC,OAAQ,QAAO,MAAM;AAAA,MAAC;AAC3B,aAAO,OAAO,SAAS,aAAa;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEMC,QAAAA;AAAAA,IACf;AAAA,IACA,MAAM;AACJ,UAAI,CAAC,OAAQ,QAAO;AACpB,aAAO,GAAG,OAAO,MAAM,IAAI,OAAO,cAAc,EAAE,IAAI,OAAO,OAAO;AAAA,IACtE;AAAA,IACA,MAAM;AAAA,EAAA;AAIR,SAAO;AAAA,IACL,MAAK,iCAAQ,QAAO;AAAA,IACpB,SAAQ,iCAAQ,WAAU;AAAA,IAC1B,aAAY,iCAAQ,eAAc;AAAA,IAClC,QAAO,iCAAQ,UAAS;AAAA,IACxB,WAAU,iCAAQ,kBAAiB,CAAA;AAAA,IACnC,aAAa,CAAC,UAAA;;AAAU,6BAAU,YAAV,mBAAmB,YAAY;AAAA;AAAA,EAAK;AAEhE;AAEA,MAAM,iBAAiB;AC5DhB,SAAS,eAAuD,OAAU;AAC/E,QAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,SAAO;AAAA,IACL;AAAA,IACA,KAAK,KAA8B;AACjC,YAAM,SAAS,MAAM;AACnB,cAAM,MAA+B,CAAA;AACrC,mBAAW,OAAO,MAAM;AACtB,cAAI,GAAG,IAAI,IAAI,IAAI,GAAG,EAAE,OAAA;AAAA,QAC1B;AACA,eAAO;AAAA,MACT;AACA,aAAO;AAAA,QACL,KAAK,CAAC,QAAQ,IAAI,IAAI,GAAG;AAAA,QACzB;AAAA,QACA,WAAW,CAAC,aAAa;AACvB,cAAI,YAAY;AAChB,gBAAM,OAAO,MAAM;AACjB,wBAAY;AACZ,qBAAS,QAAQ;AAAA,UACnB;AACA,gBAAM,eAAe,MAAM;AACzB,gBAAI,UAAW;AACf,wBAAY;AACZ,2BAAe,IAAI;AAAA,UACrB;AACA,gBAAM,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,GAAG,CAAC;AAC5C,qBAAW,QAAQ,OAAO;AACxB,iBAAK,YAAY,YAAY;AAAA,UAC/B;AACA,mBAAS,QAAQ;AACjB,iBAAO,MAAM;AACX,uBAAW,QAAQ,OAAO;AACxB,mBAAK,cAAc,YAAY;AAAA,YACjC;AAAA,UACF;AAAA,QACF;AAAA,MAAA;AAAA,IAEJ;AAAA,EAAA;AAEJ;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"collab.cjs","sources":["../src/collab/close-policy.ts","../src/collab/mint.ts","../src/collab/client.ts","../src/collab/react.ts","../src/collab/shape.ts"],"sourcesContent":["/**\n * Keryx WebSocket close-code policy.\n *\n * Keryx (@y/hub) encodes retry semantics in close codes: 4400–4499 are\n * permanent application errors — the client must stop reconnecting until the\n * app acts — while everything else is transient and left to the provider's\n * exponential backoff. Two permanent codes carry specific meaning:\n *\n * - 4401 \"permission revoked\": the token was rejected mid-session (access\n * revoked, permissions changed, or the token aged out server-side). The\n * right response is one fresh mint — which re-runs the server-side\n * permission check — and a reconnect only if the mint succeeds.\n * - 4404 \"document deleted\": permanent; the document is not coming back.\n */\n\nexport const WS_CLOSE_AUTH_REVOKED = 4401;\nexport const WS_CLOSE_DOC_DELETED = 4404;\n\nexport type CloseAction = 'remint' | 'stop' | 'retry';\n\nexport function classifyClose(code: number | undefined): CloseAction {\n if (code === undefined) return 'retry';\n if (code === WS_CLOSE_AUTH_REVOKED) return 'remint';\n if (code >= 4400 && code < 4500) return 'stop';\n return 'retry';\n}\n","/**\n * Collab-token mint client for Generic Doc assets.\n *\n * Calls `POST /api/v0/assets/{assetId}/collab-token` — the only public surface\n * that issues Keryx capability tokens, allowlisted server-side to the\n * `generic_doc` asset type and admin-only during the initial rollout. Tokens\n * are room-bound and short-lived; `connectGenericDoc` re-mints automatically\n * before expiry.\n */\n\nexport interface CollabAuth {\n /** Athena API base or any Athena backend URL (e.g. the AthenaProvider `backendUrl`). */\n backendUrl: string;\n /** Personal or sandbox API key. Used when no bearer token is provided. */\n apiKey?: string;\n /** Per-viewer bearer token. Takes precedence over the API key. */\n token?: string | null;\n}\n\nexport interface CollabTokenResponse {\n token: string;\n access_type: 'r' | 'rw';\n expires_at_ms: number;\n ws_url: string;\n rest_url: string;\n org: string;\n doc_id: string;\n branch: string;\n}\n\nexport class CollabTokenError extends Error {\n readonly status: number;\n\n constructor(status: number, message: string) {\n super(message);\n this.name = 'CollabTokenError';\n this.status = status;\n }\n}\n\nfunction getAuthHeaders(auth: CollabAuth): Record<string, string> {\n if (auth.token) {\n return { Authorization: `Bearer ${auth.token}` };\n }\n if (auth.apiKey) {\n return { 'X-API-KEY': auth.apiKey };\n }\n return {};\n}\n\n/**\n * Trim a nested Athena endpoint back to the API origin. Accepts the\n * AthenaProvider `backendUrl` (`…/api/assistant-ui`), the sandbox\n * `ATHENA_API_URL` convention (`…/api/v0`), or a bare origin.\n */\nexport function getAthenaApiBaseUrl(backendUrl: string): string {\n return backendUrl\n .replace(/\\/api\\/assistant-ui\\/?$/, '')\n .replace(/\\/api\\/v0\\/?$/, '')\n .replace(/\\/$/, '');\n}\n\nexport async function mintCollabToken(args: {\n auth: CollabAuth;\n assetId: string;\n access: 'view' | 'edit';\n signal?: AbortSignal;\n}): Promise<CollabTokenResponse> {\n const base = getAthenaApiBaseUrl(args.auth.backendUrl);\n const response = await fetch(\n `${base}/api/v0/assets/${encodeURIComponent(args.assetId)}/collab-token`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...getAuthHeaders(args.auth),\n },\n body: JSON.stringify({ access: args.access }),\n signal: args.signal,\n }\n );\n if (!response.ok) {\n let detail = '';\n try {\n const body: unknown = await response.json();\n if (body && typeof body === 'object' && 'detail' in body) {\n detail = String((body as { detail: unknown }).detail);\n }\n } catch {\n // non-JSON error body; status alone tells the story\n }\n throw new CollabTokenError(\n response.status,\n detail || `Collab token mint failed with status ${response.status}`\n );\n }\n return (await response.json()) as CollabTokenResponse;\n}\n","/**\n * Vanilla (framework-agnostic) live client for Generic Doc assets.\n *\n * `connectGenericDoc` owns the full connection lifecycle: mint a room-bound\n * token via the public API, open the Keryx WebSocket with the v14 provider\n * (`@y/websocket` + `@y/y`), re-mint before expiry, apply the close-code\n * policy (4401 → one fresh mint, other 44xx → permanent stop, everything\n * else → provider backoff), and expose presence from awareness. React apps\n * use `useGenericDoc` from `@athenaintel/react/collab` instead of calling\n * this directly.\n */\n\nimport { WebsocketProvider } from '@y/websocket';\nimport * as Y from '@y/y';\nimport { classifyClose } from './close-policy';\nimport {\n type CollabAuth,\n type CollabTokenResponse,\n CollabTokenError,\n mintCollabToken,\n} from './mint';\n\nexport type GenericDocStatus =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'closed'\n | 'error';\n\nexport interface PresenceEntry {\n clientId: number;\n state: Record<string, unknown>;\n}\n\nexport interface GenericDocHandle {\n /** The live shared document. Owned by the handle; destroyed with it. */\n readonly doc: Y.Doc;\n readonly status: GenericDocStatus;\n /**\n * Whether the initial server sync has completed for the current connection.\n * `status === 'connected'` fires before the server state arrives, so a doc\n * read immediately after connect can still be empty — gate first reads on\n * this (or `waitForSync`).\n */\n readonly synced: boolean;\n /** Granted access from the last successful mint. */\n readonly accessType: 'r' | 'rw' | null;\n /** Terminal error, when status is 'error' or 'closed'. */\n readonly error: Error | null;\n /** Monotonic change counter — bumps on every status/presence notification. */\n readonly version: number;\n /**\n * Resolve once the initial server sync completes; reject on timeout, on a\n * terminal connection state, or if the handle is destroyed first.\n */\n waitForSync: (timeoutMs?: number) => Promise<void>;\n getPresence: () => PresenceEntry[];\n /** Broadcast this client's presence state (requires a live connection). */\n setPresence: (state: Record<string, unknown> | null) => void;\n /**\n * Force an early token refresh. Session-preserving: the live connection\n * stays up until the fresh token is minted; a failed mint retries on a\n * short timer instead of dropping the session.\n */\n refreshNow: () => Promise<void>;\n onChange: (listener: () => void) => () => void;\n destroy: () => void;\n}\n\n/** Re-mint this long before token expiry (clamped to a floor for short TTLs). */\nconst REFRESH_HEADROOM_MS = 5 * 60 * 1000;\nconst MIN_REFRESH_DELAY_MS = 30 * 1000;\n\n/** Consecutive 4401→mint cycles allowed without a successful connect between. */\nconst MAX_CONSECUTIVE_REMINTS = 2;\n\nexport function connectGenericDoc(args: {\n assetId: string;\n auth: CollabAuth;\n access?: 'view' | 'edit';\n}): GenericDocHandle {\n const access = args.access ?? 'view';\n const doc = new Y.Doc();\n const listeners = new Set<() => void>();\n\n let provider: WebsocketProvider | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n let destroyed = false;\n let status: GenericDocStatus = 'connecting';\n let synced = false;\n let accessType: 'r' | 'rw' | null = null;\n let error: Error | null = null;\n let lastPresence: Record<string, unknown> | null = null;\n let version = 0;\n let consecutiveRemints = 0;\n\n const notify = () => {\n version += 1;\n for (const listener of listeners) listener();\n };\n\n const setStatus = (next: GenericDocStatus, err: Error | null = null) => {\n if (destroyed && next !== 'closed') return;\n status = next;\n error = err;\n notify();\n };\n\n const clearRefreshTimer = () => {\n if (refreshTimer !== null) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n const teardownProvider = () => {\n clearRefreshTimer();\n // A future provider must complete its own initial sync; the doc keeps its\n // content either way. Subscribers must observe the flip immediately —\n // waiting for the next provider event would let them act on synced=true\n // across the replacement gap.\n if (synced) {\n synced = false;\n notify();\n }\n if (provider) {\n provider.destroy();\n provider = null;\n }\n };\n\n const scheduleRefresh = (grant: CollabTokenResponse) => {\n clearRefreshTimer();\n const delay = Math.max(\n grant.expires_at_ms - Date.now() - REFRESH_HEADROOM_MS,\n MIN_REFRESH_DELAY_MS\n );\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, delay);\n };\n\n const connect = async (opts?: { scheduledRefresh?: boolean }): Promise<void> => {\n if (destroyed) return;\n const scheduledRefresh = opts?.scheduledRefresh === true;\n // A scheduled refresh mints FIRST and keeps the live session up: the old\n // token has ~5 minutes of headroom, so a transient mint failure retries\n // on a short timer instead of dropping a healthy connection.\n if (!scheduledRefresh) {\n teardownProvider();\n } else {\n clearRefreshTimer();\n }\n let grant: CollabTokenResponse;\n try {\n grant = await mintCollabToken({ auth: args.auth, assetId: args.assetId, access });\n } catch (err) {\n if (scheduledRefresh && !destroyed) {\n refreshTimer = setTimeout(() => {\n void connect({ scheduledRefresh: true });\n }, MIN_REFRESH_DELAY_MS);\n return;\n }\n // A denied mint is authoritative (revoked / not shared / not eligible);\n // anything else (network) is worth telling the caller about too — the\n // handle stays usable via a later explicit reconnect-by-recreate.\n setStatus('error', err instanceof Error ? err : new Error(String(err)));\n return;\n }\n if (destroyed) return;\n if (scheduledRefresh) {\n teardownProvider();\n }\n accessType = grant.access_type;\n\n const nextProvider = new WebsocketProvider(grant.ws_url, `${grant.org}/${grant.doc_id}`, doc, {\n params: {\n yauth: grant.token,\n branch: grant.branch,\n gc: 'true',\n },\n // BroadcastChannel would sync same-origin clients locally, bypassing the\n // server's permission enforcement (a read-only tab could write into an\n // editor tab's doc). Permissioned docs must round-trip the server.\n disableBc: true,\n });\n provider = nextProvider;\n\n nextProvider.on('status', (event: { status: 'connecting' | 'connected' | 'disconnected' }) => {\n if (provider !== nextProvider) return;\n if (event.status === 'connected') {\n consecutiveRemints = 0;\n }\n setStatus(event.status);\n if (event.status === 'connected' && lastPresence !== null) {\n nextProvider.awareness.setLocalState(lastPresence);\n }\n });\n\n nextProvider.on('sync', (state: boolean) => {\n if (provider !== nextProvider) return;\n if (synced !== state) {\n synced = state;\n notify();\n }\n });\n\n nextProvider.on('connection-close', (event: CloseEvent | null) => {\n if (provider !== nextProvider) return;\n const action = classifyClose(event?.code);\n if (action === 'remint') {\n // Revoked mid-session: a fresh mint re-runs the server-side permission\n // check. If access is truly gone the mint 403s and we land in 'error'.\n // Bounded: repeated 4401s without an intervening successful connect\n // mean the server keeps rejecting freshly minted tokens — stop rather\n // than loop mint→connect→kick.\n consecutiveRemints += 1;\n teardownProvider();\n if (consecutiveRemints > MAX_CONSECUTIVE_REMINTS) {\n setStatus(\n 'closed',\n new Error('Access repeatedly revoked mid-session (4401); giving up.')\n );\n return;\n }\n void connect();\n } else if (action === 'stop') {\n teardownProvider();\n setStatus(\n 'closed',\n new Error(`Connection closed permanently (code ${event?.code ?? 'unknown'})`)\n );\n }\n // 'retry': the provider's exponential backoff handles it.\n });\n\n nextProvider.awareness.on('change', () => {\n if (provider !== nextProvider) return;\n notify();\n });\n\n scheduleRefresh(grant);\n };\n\n void connect();\n\n const waitForSync = (timeoutMs = 15_000): Promise<void> =>\n new Promise<void>((resolve, reject) => {\n const terminal = (): Error | null => {\n if (destroyed) return new Error('Handle destroyed before initial sync');\n if (status === 'closed' || status === 'error') {\n return error ?? new Error(`Connection ${status} before initial sync`);\n }\n return null;\n };\n if (synced) {\n resolve();\n return;\n }\n const immediate = terminal();\n if (immediate) {\n reject(immediate);\n return;\n }\n const cleanup = () => {\n clearTimeout(timer);\n listeners.delete(listener);\n };\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`Timed out after ${timeoutMs}ms waiting for initial sync`));\n }, timeoutMs);\n const listener = () => {\n if (synced) {\n cleanup();\n resolve();\n return;\n }\n const err = terminal();\n if (err) {\n cleanup();\n reject(err);\n }\n };\n listeners.add(listener);\n });\n\n return {\n doc,\n get status() {\n return status;\n },\n get synced() {\n return synced;\n },\n get accessType() {\n return accessType;\n },\n get error() {\n return error;\n },\n get version() {\n return version;\n },\n waitForSync,\n getPresence: () => {\n if (!provider) return [];\n const entries: PresenceEntry[] = [];\n provider.awareness.getStates().forEach((state, clientId) => {\n entries.push({ clientId, state: state as Record<string, unknown> });\n });\n return entries;\n },\n setPresence: (state) => {\n lastPresence = state;\n provider?.awareness.setLocalState(state);\n },\n refreshNow: () => connect({ scheduledRefresh: true }),\n onChange: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n teardownProvider();\n status = 'closed';\n // Final notification so pending waitForSync callers reject instead of\n // hanging on listeners that are about to be dropped.\n for (const listener of [...listeners]) listener();\n listeners.clear();\n doc.destroy();\n },\n };\n}\n\nexport { CollabTokenError };\nexport type { CollabAuth, CollabTokenResponse };\n","/**\n * React bindings for Generic Doc live collaboration.\n *\n * `useGenericDoc(assetId)` opens (and owns) a `connectGenericDoc` handle,\n * resolving auth from the surrounding `AthenaProvider` (per-viewer bearer when\n * present, API key otherwise) and re-rendering on status/presence changes via\n * `useSyncExternalStore`.\n */\n\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { useAthenaConfig } from '../provider/AthenaContext';\nimport {\n type GenericDocHandle,\n type GenericDocStatus,\n type PresenceEntry,\n connectGenericDoc,\n} from './client';\nimport type * as Y from '@y/y';\n\nexport interface UseGenericDocResult {\n /** Null until the handle is created (first render effect). */\n doc: Y.Doc | null;\n status: GenericDocStatus;\n /**\n * Whether the initial server sync completed for the current connection —\n * `status === 'connected'` fires before the server state arrives, so gate\n * \"is this doc really empty?\" UI on this.\n */\n synced: boolean;\n accessType: 'r' | 'rw' | null;\n error: Error | null;\n presence: PresenceEntry[];\n setPresence: (state: Record<string, unknown> | null) => void;\n}\n\nexport function useGenericDoc(\n assetId: string,\n options?: { access?: 'view' | 'edit' }\n): UseGenericDocResult {\n const config = useAthenaConfig();\n const access = options?.access ?? 'view';\n const [handle, setHandle] = useState<GenericDocHandle | null>(null);\n const handleRef = useRef<GenericDocHandle | null>(null);\n\n useEffect(() => {\n const next = connectGenericDoc({\n assetId,\n access,\n auth: {\n backendUrl: config.backendUrl,\n apiKey: config.apiKey,\n token: config.token,\n },\n });\n handleRef.current = next;\n setHandle(next);\n return () => {\n handleRef.current = null;\n next.destroy();\n };\n // Recreate when the target or credentials change; the handle re-mints on\n // its own schedule otherwise.\n }, [assetId, access, config.backendUrl, config.apiKey, config.token]);\n\n const subscribe = useMemo(() => {\n return (onStoreChange: () => void) => {\n if (!handle) return () => {};\n return handle.onChange(onStoreChange);\n };\n }, [handle]);\n\n const snapshot = useSyncExternalStore(\n subscribe,\n () => {\n if (!handle) return EMPTY_SNAPSHOT;\n return `${handle.status}|${handle.synced}|${handle.accessType ?? ''}|${handle.version}`;\n },\n () => EMPTY_SNAPSHOT\n );\n void snapshot;\n\n return {\n doc: handle?.doc ?? null,\n status: handle?.status ?? 'connecting',\n synced: handle?.synced ?? false,\n accessType: handle?.accessType ?? null,\n error: handle?.error ?? null,\n presence: handle?.getPresence() ?? [],\n setPresence: (state) => handleRef.current?.setPresence(state),\n };\n}\n\nconst EMPTY_SNAPSHOT = 'init|false||0';\n","/**\n * `defineDocShape` — a typed lens over a Generic Doc's root types.\n *\n * A shape is documentation + ergonomics, not a migration system: it names the\n * root keys an app expects and gives JSON-level reads (`toJSON`, `subscribe`)\n * that hide CRDT mechanics for the common case. The raw `Y.Doc` (yjs v14 /\n * `@y/y`) stays available on the handle for full delta-level power.\n */\n\nimport type * as Y from '@y/y';\n\nexport type DocShapeKind = 'map' | 'array' | 'text' | 'xml';\n\nexport interface BoundDocShape<S extends Record<string, DocShapeKind>> {\n /** The live root type for a declared key (v14 unified `YType`). */\n get: <K extends keyof S & string>(key: K) => ReturnType<Y.Doc['get']>;\n /** JSON snapshot of every declared root (v14 node shape: attributes + `children`). */\n toJSON: () => Record<keyof S & string, unknown>;\n /**\n * Subscribe to JSON snapshots. Fires once immediately, then after every\n * change to any declared root (batched per transaction flush).\n */\n subscribe: (listener: (json: Record<keyof S & string, unknown>) => void) => () => void;\n}\n\nexport function defineDocShape<S extends Record<string, DocShapeKind>>(shape: S) {\n const keys = Object.keys(shape) as Array<keyof S & string>;\n return {\n shape,\n bind(doc: Y.Doc): BoundDocShape<S> {\n const toJSON = () => {\n const out: Record<string, unknown> = {};\n for (const key of keys) {\n out[key] = doc.get(key).toJSON();\n }\n return out as Record<keyof S & string, unknown>;\n };\n return {\n get: (key) => doc.get(key),\n toJSON,\n subscribe: (listener) => {\n let scheduled = false;\n const emit = () => {\n scheduled = false;\n listener(toJSON());\n };\n const onDeepChange = () => {\n if (scheduled) return;\n scheduled = true;\n queueMicrotask(emit);\n };\n const roots = keys.map((key) => doc.get(key));\n for (const root of roots) {\n root.observeDeep(onDeepChange);\n }\n listener(toJSON());\n return () => {\n for (const root of roots) {\n root.unobserveDeep(onDeepChange);\n }\n };\n },\n };\n },\n };\n}\n"],"names":["Y","WebsocketProvider","useAthenaConfig","useState","useRef","useEffect","useMemo","useSyncExternalStore"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,MAAM,wBAAwB;AAC9B,MAAM,uBAAuB;AAI7B,SAAS,cAAc,MAAuC;AACnE,MAAI,SAAS,OAAW,QAAO;AAC/B,MAAI,SAAS,sBAAuB,QAAO;AAC3C,MAAI,QAAQ,QAAQ,OAAO,KAAM,QAAO;AACxC,SAAO;AACT;ACKO,MAAM,yBAAyB,MAAM;AAAA,EAG1C,YAAY,QAAgB,SAAiB;AAC3C,UAAM,OAAO;AAHN;AAIP,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAEA,SAAS,eAAe,MAA0C;AAChE,MAAI,KAAK,OAAO;AACd,WAAO,EAAE,eAAe,UAAU,KAAK,KAAK,GAAA;AAAA,EAC9C;AACA,MAAI,KAAK,QAAQ;AACf,WAAO,EAAE,aAAa,KAAK,OAAA;AAAA,EAC7B;AACA,SAAO,CAAA;AACT;AAOO,SAAS,oBAAoB,YAA4B;AAC9D,SAAO,WACJ,QAAQ,2BAA2B,EAAE,EACrC,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,OAAO,EAAE;AACtB;AAEA,eAAsB,gBAAgB,MAKL;AAC/B,QAAM,OAAO,oBAAoB,KAAK,KAAK,UAAU;AACrD,QAAM,WAAW,MAAM;AAAA,IACrB,GAAG,IAAI,kBAAkB,mBAAmB,KAAK,OAAO,CAAC;AAAA,IACzD;AAAA,MACE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,GAAG,eAAe,KAAK,IAAI;AAAA,MAAA;AAAA,MAE7B,MAAM,KAAK,UAAU,EAAE,QAAQ,KAAK,QAAQ;AAAA,MAC5C,QAAQ,KAAK;AAAA,IAAA;AAAA,EACf;AAEF,MAAI,CAAC,SAAS,IAAI;AAChB,QAAI,SAAS;AACb,QAAI;AACF,YAAM,OAAgB,MAAM,SAAS,KAAA;AACrC,UAAI,QAAQ,OAAO,SAAS,YAAY,YAAY,MAAM;AACxD,iBAAS,OAAQ,KAA6B,MAAM;AAAA,MACtD;AAAA,IACF,QAAQ;AAAA,IAER;AACA,UAAM,IAAI;AAAA,MACR,SAAS;AAAA,MACT,UAAU,wCAAwC,SAAS,MAAM;AAAA,IAAA;AAAA,EAErE;AACA,SAAQ,MAAM,SAAS,KAAA;AACzB;AC3BA,MAAM,sBAAsB,IAAI,KAAK;AACrC,MAAM,uBAAuB,KAAK;AAGlC,MAAM,0BAA0B;AAEzB,SAAS,kBAAkB,MAIb;AACnB,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,MAAM,IAAIA,aAAE,IAAA;AAClB,QAAM,gCAAgB,IAAA;AAEtB,MAAI,WAAqC;AACzC,MAAI,eAAqD;AACzD,MAAI,YAAY;AAChB,MAAI,SAA2B;AAC/B,MAAI,SAAS;AACb,MAAI,aAAgC;AACpC,MAAI,QAAsB;AAC1B,MAAI,eAA+C;AACnD,MAAI,UAAU;AACd,MAAI,qBAAqB;AAEzB,QAAM,SAAS,MAAM;AACnB,eAAW;AACX,eAAW,YAAY,UAAW,UAAA;AAAA,EACpC;AAEA,QAAM,YAAY,CAAC,MAAwB,MAAoB,SAAS;AACtE,QAAI,aAAa,SAAS,SAAU;AACpC,aAAS;AACT,YAAQ;AACR,WAAA;AAAA,EACF;AAEA,QAAM,oBAAoB,MAAM;AAC9B,QAAI,iBAAiB,MAAM;AACzB,mBAAa,YAAY;AACzB,qBAAe;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,mBAAmB,MAAM;AAC7B,sBAAA;AAKA,QAAI,QAAQ;AACV,eAAS;AACT,aAAA;AAAA,IACF;AACA,QAAI,UAAU;AACZ,eAAS,QAAA;AACT,iBAAW;AAAA,IACb;AAAA,EACF;AAEA,QAAM,kBAAkB,CAAC,UAA+B;AACtD,sBAAA;AACA,UAAM,QAAQ,KAAK;AAAA,MACjB,MAAM,gBAAgB,KAAK,IAAA,IAAQ;AAAA,MACnC;AAAA,IAAA;AAEF,mBAAe,WAAW,MAAM;AAC9B,WAAK,QAAQ,EAAE,kBAAkB,MAAM;AAAA,IACzC,GAAG,KAAK;AAAA,EACV;AAEA,QAAM,UAAU,OAAO,SAAyD;AAC9E,QAAI,UAAW;AACf,UAAM,oBAAmB,6BAAM,sBAAqB;AAIpD,QAAI,CAAC,kBAAkB;AACrB,uBAAA;AAAA,IACF,OAAO;AACL,wBAAA;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,gBAAgB,EAAE,MAAM,KAAK,MAAM,SAAS,KAAK,SAAS,QAAQ;AAAA,IAClF,SAAS,KAAK;AACZ,UAAI,oBAAoB,CAAC,WAAW;AAClC,uBAAe,WAAW,MAAM;AAC9B,eAAK,QAAQ,EAAE,kBAAkB,MAAM;AAAA,QACzC,GAAG,oBAAoB;AACvB;AAAA,MACF;AAIA,gBAAU,SAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AACtE;AAAA,IACF;AACA,QAAI,UAAW;AACf,QAAI,kBAAkB;AACpB,uBAAA;AAAA,IACF;AACA,iBAAa,MAAM;AAEnB,UAAM,eAAe,IAAIC,UAAAA,kBAAkB,MAAM,QAAQ,GAAG,MAAM,GAAG,IAAI,MAAM,MAAM,IAAI,KAAK;AAAA,MAC5F,QAAQ;AAAA,QACN,OAAO,MAAM;AAAA,QACb,QAAQ,MAAM;AAAA,QACd,IAAI;AAAA,MAAA;AAAA;AAAA;AAAA;AAAA,MAKN,WAAW;AAAA,IAAA,CACZ;AACD,eAAW;AAEX,iBAAa,GAAG,UAAU,CAAC,UAAmE;AAC5F,UAAI,aAAa,aAAc;AAC/B,UAAI,MAAM,WAAW,aAAa;AAChC,6BAAqB;AAAA,MACvB;AACA,gBAAU,MAAM,MAAM;AACtB,UAAI,MAAM,WAAW,eAAe,iBAAiB,MAAM;AACzD,qBAAa,UAAU,cAAc,YAAY;AAAA,MACnD;AAAA,IACF,CAAC;AAED,iBAAa,GAAG,QAAQ,CAAC,UAAmB;AAC1C,UAAI,aAAa,aAAc;AAC/B,UAAI,WAAW,OAAO;AACpB,iBAAS;AACT,eAAA;AAAA,MACF;AAAA,IACF,CAAC;AAED,iBAAa,GAAG,oBAAoB,CAAC,UAA6B;AAChE,UAAI,aAAa,aAAc;AAC/B,YAAM,SAAS,cAAc,+BAAO,IAAI;AACxC,UAAI,WAAW,UAAU;AAMvB,8BAAsB;AACtB,yBAAA;AACA,YAAI,qBAAqB,yBAAyB;AAChD;AAAA,YACE;AAAA,YACA,IAAI,MAAM,0DAA0D;AAAA,UAAA;AAEtE;AAAA,QACF;AACA,aAAK,QAAA;AAAA,MACP,WAAW,WAAW,QAAQ;AAC5B,yBAAA;AACA;AAAA,UACE;AAAA,UACA,IAAI,MAAM,wCAAuC,+BAAO,SAAQ,SAAS,GAAG;AAAA,QAAA;AAAA,MAEhF;AAAA,IAEF,CAAC;AAED,iBAAa,UAAU,GAAG,UAAU,MAAM;AACxC,UAAI,aAAa,aAAc;AAC/B,aAAA;AAAA,IACF,CAAC;AAED,oBAAgB,KAAK;AAAA,EACvB;AAEA,OAAK,QAAA;AAEL,QAAM,cAAc,CAAC,YAAY,SAC/B,IAAI,QAAc,CAAC,SAAS,WAAW;AACrC,UAAM,WAAW,MAAoB;AACnC,UAAI,UAAW,QAAO,IAAI,MAAM,sCAAsC;AACtE,UAAI,WAAW,YAAY,WAAW,SAAS;AAC7C,eAAO,SAAS,IAAI,MAAM,cAAc,MAAM,sBAAsB;AAAA,MACtE;AACA,aAAO;AAAA,IACT;AACA,QAAI,QAAQ;AACV,cAAA;AACA;AAAA,IACF;AACA,UAAM,YAAY,SAAA;AAClB,QAAI,WAAW;AACb,aAAO,SAAS;AAChB;AAAA,IACF;AACA,UAAM,UAAU,MAAM;AACpB,mBAAa,KAAK;AAClB,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AACA,UAAM,QAAQ,WAAW,MAAM;AAC7B,cAAA;AACA,aAAO,IAAI,MAAM,mBAAmB,SAAS,6BAA6B,CAAC;AAAA,IAC7E,GAAG,SAAS;AACZ,UAAM,WAAW,MAAM;AACrB,UAAI,QAAQ;AACV,gBAAA;AACA,gBAAA;AACA;AAAA,MACF;AACA,YAAM,MAAM,SAAA;AACZ,UAAI,KAAK;AACP,gBAAA;AACA,eAAO,GAAG;AAAA,MACZ;AAAA,IACF;AACA,cAAU,IAAI,QAAQ;AAAA,EACxB,CAAC;AAEH,SAAO;AAAA,IACL;AAAA,IACA,IAAI,SAAS;AACX,aAAO;AAAA,IACT;AAAA,IACA,IAAI,SAAS;AACX,aAAO;AAAA,IACT;AAAA,IACA,IAAI,aAAa;AACf,aAAO;AAAA,IACT;AAAA,IACA,IAAI,QAAQ;AACV,aAAO;AAAA,IACT;AAAA,IACA,IAAI,UAAU;AACZ,aAAO;AAAA,IACT;AAAA,IACA;AAAA,IACA,aAAa,MAAM;AACjB,UAAI,CAAC,SAAU,QAAO,CAAA;AACtB,YAAM,UAA2B,CAAA;AACjC,eAAS,UAAU,UAAA,EAAY,QAAQ,CAAC,OAAO,aAAa;AAC1D,gBAAQ,KAAK,EAAE,UAAU,MAAA,CAAyC;AAAA,MACpE,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IACA,aAAa,CAAC,UAAU;AACtB,qBAAe;AACf,2CAAU,UAAU,cAAc;AAAA,IACpC;AAAA,IACA,YAAY,MAAM,QAAQ,EAAE,kBAAkB,MAAM;AAAA,IACpD,UAAU,CAAC,aAAa;AACtB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM;AACX,kBAAU,OAAO,QAAQ;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,SAAS,MAAM;AACb,UAAI,UAAW;AACf,kBAAY;AACZ,uBAAA;AACA,eAAS;AAGT,iBAAW,YAAY,CAAC,GAAG,SAAS,EAAG,UAAA;AACvC,gBAAU,MAAA;AACV,UAAI,QAAA;AAAA,IACN;AAAA,EAAA;AAEJ;AC7SO,SAAS,cACd,SACA,SACqB;AACrB,QAAM,SAASC,cAAAA,gBAAA;AACf,QAAM,UAAS,mCAAS,WAAU;AAClC,QAAM,CAAC,QAAQ,SAAS,IAAIC,MAAAA,SAAkC,IAAI;AAClE,QAAM,YAAYC,MAAAA,OAAgC,IAAI;AAEtDC,QAAAA,UAAU,MAAM;AACd,UAAM,OAAO,kBAAkB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,MAAM;AAAA,QACJ,YAAY,OAAO;AAAA,QACnB,QAAQ,OAAO;AAAA,QACf,OAAO,OAAO;AAAA,MAAA;AAAA,IAChB,CACD;AACD,cAAU,UAAU;AACpB,cAAU,IAAI;AACd,WAAO,MAAM;AACX,gBAAU,UAAU;AACpB,WAAK,QAAA;AAAA,IACP;AAAA,EAGF,GAAG,CAAC,SAAS,QAAQ,OAAO,YAAY,OAAO,QAAQ,OAAO,KAAK,CAAC;AAEpE,QAAM,YAAYC,MAAAA,QAAQ,MAAM;AAC9B,WAAO,CAAC,kBAA8B;AACpC,UAAI,CAAC,OAAQ,QAAO,MAAM;AAAA,MAAC;AAC3B,aAAO,OAAO,SAAS,aAAa;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEMC,QAAAA;AAAAA,IACf;AAAA,IACA,MAAM;AACJ,UAAI,CAAC,OAAQ,QAAO;AACpB,aAAO,GAAG,OAAO,MAAM,IAAI,OAAO,MAAM,IAAI,OAAO,cAAc,EAAE,IAAI,OAAO,OAAO;AAAA,IACvF;AAAA,IACA,MAAM;AAAA,EAAA;AAIR,SAAO;AAAA,IACL,MAAK,iCAAQ,QAAO;AAAA,IACpB,SAAQ,iCAAQ,WAAU;AAAA,IAC1B,SAAQ,iCAAQ,WAAU;AAAA,IAC1B,aAAY,iCAAQ,eAAc;AAAA,IAClC,QAAO,iCAAQ,UAAS;AAAA,IACxB,WAAU,iCAAQ,kBAAiB,CAAA;AAAA,IACnC,aAAa,CAAC,UAAA;;AAAU,6BAAU,YAAV,mBAAmB,YAAY;AAAA;AAAA,EAAK;AAEhE;AAEA,MAAM,iBAAiB;ACnEhB,SAAS,eAAuD,OAAU;AAC/E,QAAM,OAAO,OAAO,KAAK,KAAK;AAC9B,SAAO;AAAA,IACL;AAAA,IACA,KAAK,KAA8B;AACjC,YAAM,SAAS,MAAM;AACnB,cAAM,MAA+B,CAAA;AACrC,mBAAW,OAAO,MAAM;AACtB,cAAI,GAAG,IAAI,IAAI,IAAI,GAAG,EAAE,OAAA;AAAA,QAC1B;AACA,eAAO;AAAA,MACT;AACA,aAAO;AAAA,QACL,KAAK,CAAC,QAAQ,IAAI,IAAI,GAAG;AAAA,QACzB;AAAA,QACA,WAAW,CAAC,aAAa;AACvB,cAAI,YAAY;AAChB,gBAAM,OAAO,MAAM;AACjB,wBAAY;AACZ,qBAAS,QAAQ;AAAA,UACnB;AACA,gBAAM,eAAe,MAAM;AACzB,gBAAI,UAAW;AACf,wBAAY;AACZ,2BAAe,IAAI;AAAA,UACrB;AACA,gBAAM,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,GAAG,CAAC;AAC5C,qBAAW,QAAQ,OAAO;AACxB,iBAAK,YAAY,YAAY;AAAA,UAC/B;AACA,mBAAS,QAAQ;AACjB,iBAAO,MAAM;AACX,uBAAW,QAAQ,OAAO;AACxB,mBAAK,cAAc,YAAY;AAAA,YACjC;AAAA,UACF;AAAA,QACF;AAAA,MAAA;AAAA,IAEJ;AAAA,EAAA;AAEJ;;;;;;;;;;"}
|
package/dist/collab.js
CHANGED
|
@@ -31,8 +31,7 @@ function getAuthHeaders(auth) {
|
|
|
31
31
|
return {};
|
|
32
32
|
}
|
|
33
33
|
function getAthenaApiBaseUrl(backendUrl) {
|
|
34
|
-
|
|
35
|
-
return stripped.replace(/\/$/, "");
|
|
34
|
+
return backendUrl.replace(/\/api\/assistant-ui\/?$/, "").replace(/\/api\/v0\/?$/, "").replace(/\/$/, "");
|
|
36
35
|
}
|
|
37
36
|
async function mintCollabToken(args) {
|
|
38
37
|
const base = getAthenaApiBaseUrl(args.auth.backendUrl);
|
|
@@ -75,6 +74,7 @@ function connectGenericDoc(args) {
|
|
|
75
74
|
let refreshTimer = null;
|
|
76
75
|
let destroyed = false;
|
|
77
76
|
let status = "connecting";
|
|
77
|
+
let synced = false;
|
|
78
78
|
let accessType = null;
|
|
79
79
|
let error = null;
|
|
80
80
|
let lastPresence = null;
|
|
@@ -98,6 +98,10 @@ function connectGenericDoc(args) {
|
|
|
98
98
|
};
|
|
99
99
|
const teardownProvider = () => {
|
|
100
100
|
clearRefreshTimer();
|
|
101
|
+
if (synced) {
|
|
102
|
+
synced = false;
|
|
103
|
+
notify();
|
|
104
|
+
}
|
|
101
105
|
if (provider) {
|
|
102
106
|
provider.destroy();
|
|
103
107
|
provider = null;
|
|
@@ -144,7 +148,11 @@ function connectGenericDoc(args) {
|
|
|
144
148
|
yauth: grant.token,
|
|
145
149
|
branch: grant.branch,
|
|
146
150
|
gc: "true"
|
|
147
|
-
}
|
|
151
|
+
},
|
|
152
|
+
// BroadcastChannel would sync same-origin clients locally, bypassing the
|
|
153
|
+
// server's permission enforcement (a read-only tab could write into an
|
|
154
|
+
// editor tab's doc). Permissioned docs must round-trip the server.
|
|
155
|
+
disableBc: true
|
|
148
156
|
});
|
|
149
157
|
provider = nextProvider;
|
|
150
158
|
nextProvider.on("status", (event) => {
|
|
@@ -157,6 +165,13 @@ function connectGenericDoc(args) {
|
|
|
157
165
|
nextProvider.awareness.setLocalState(lastPresence);
|
|
158
166
|
}
|
|
159
167
|
});
|
|
168
|
+
nextProvider.on("sync", (state) => {
|
|
169
|
+
if (provider !== nextProvider) return;
|
|
170
|
+
if (synced !== state) {
|
|
171
|
+
synced = state;
|
|
172
|
+
notify();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
160
175
|
nextProvider.on("connection-close", (event) => {
|
|
161
176
|
if (provider !== nextProvider) return;
|
|
162
177
|
const action = classifyClose(event == null ? void 0 : event.code);
|
|
@@ -186,11 +201,53 @@ function connectGenericDoc(args) {
|
|
|
186
201
|
scheduleRefresh(grant);
|
|
187
202
|
};
|
|
188
203
|
void connect();
|
|
204
|
+
const waitForSync = (timeoutMs = 15e3) => new Promise((resolve, reject) => {
|
|
205
|
+
const terminal = () => {
|
|
206
|
+
if (destroyed) return new Error("Handle destroyed before initial sync");
|
|
207
|
+
if (status === "closed" || status === "error") {
|
|
208
|
+
return error ?? new Error(`Connection ${status} before initial sync`);
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
};
|
|
212
|
+
if (synced) {
|
|
213
|
+
resolve();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const immediate = terminal();
|
|
217
|
+
if (immediate) {
|
|
218
|
+
reject(immediate);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const cleanup = () => {
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
listeners.delete(listener);
|
|
224
|
+
};
|
|
225
|
+
const timer = setTimeout(() => {
|
|
226
|
+
cleanup();
|
|
227
|
+
reject(new Error(`Timed out after ${timeoutMs}ms waiting for initial sync`));
|
|
228
|
+
}, timeoutMs);
|
|
229
|
+
const listener = () => {
|
|
230
|
+
if (synced) {
|
|
231
|
+
cleanup();
|
|
232
|
+
resolve();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const err = terminal();
|
|
236
|
+
if (err) {
|
|
237
|
+
cleanup();
|
|
238
|
+
reject(err);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
listeners.add(listener);
|
|
242
|
+
});
|
|
189
243
|
return {
|
|
190
244
|
doc,
|
|
191
245
|
get status() {
|
|
192
246
|
return status;
|
|
193
247
|
},
|
|
248
|
+
get synced() {
|
|
249
|
+
return synced;
|
|
250
|
+
},
|
|
194
251
|
get accessType() {
|
|
195
252
|
return accessType;
|
|
196
253
|
},
|
|
@@ -200,6 +257,7 @@ function connectGenericDoc(args) {
|
|
|
200
257
|
get version() {
|
|
201
258
|
return version;
|
|
202
259
|
},
|
|
260
|
+
waitForSync,
|
|
203
261
|
getPresence: () => {
|
|
204
262
|
if (!provider) return [];
|
|
205
263
|
const entries = [];
|
|
@@ -223,9 +281,10 @@ function connectGenericDoc(args) {
|
|
|
223
281
|
if (destroyed) return;
|
|
224
282
|
destroyed = true;
|
|
225
283
|
teardownProvider();
|
|
284
|
+
status = "closed";
|
|
285
|
+
for (const listener of [...listeners]) listener();
|
|
226
286
|
listeners.clear();
|
|
227
287
|
doc.destroy();
|
|
228
|
-
status = "closed";
|
|
229
288
|
}
|
|
230
289
|
};
|
|
231
290
|
}
|
|
@@ -262,13 +321,14 @@ function useGenericDoc(assetId, options) {
|
|
|
262
321
|
subscribe,
|
|
263
322
|
() => {
|
|
264
323
|
if (!handle) return EMPTY_SNAPSHOT;
|
|
265
|
-
return `${handle.status}|${handle.accessType ?? ""}|${handle.version}`;
|
|
324
|
+
return `${handle.status}|${handle.synced}|${handle.accessType ?? ""}|${handle.version}`;
|
|
266
325
|
},
|
|
267
326
|
() => EMPTY_SNAPSHOT
|
|
268
327
|
);
|
|
269
328
|
return {
|
|
270
329
|
doc: (handle == null ? void 0 : handle.doc) ?? null,
|
|
271
330
|
status: (handle == null ? void 0 : handle.status) ?? "connecting",
|
|
331
|
+
synced: (handle == null ? void 0 : handle.synced) ?? false,
|
|
272
332
|
accessType: (handle == null ? void 0 : handle.accessType) ?? null,
|
|
273
333
|
error: (handle == null ? void 0 : handle.error) ?? null,
|
|
274
334
|
presence: (handle == null ? void 0 : handle.getPresence()) ?? [],
|
|
@@ -278,7 +338,7 @@ function useGenericDoc(assetId, options) {
|
|
|
278
338
|
}
|
|
279
339
|
};
|
|
280
340
|
}
|
|
281
|
-
const EMPTY_SNAPSHOT = "init||0";
|
|
341
|
+
const EMPTY_SNAPSHOT = "init|false||0";
|
|
282
342
|
function defineDocShape(shape) {
|
|
283
343
|
const keys = Object.keys(shape);
|
|
284
344
|
return {
|