@ably/ai-transport 0.0.1 → 0.2.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/README.md +114 -116
- package/dist/ably-ai-transport.js +1743 -961
- package/dist/ably-ai-transport.js.map +1 -1
- package/dist/ably-ai-transport.umd.cjs +1 -1
- package/dist/ably-ai-transport.umd.cjs.map +1 -1
- package/dist/constants.d.ts +117 -39
- package/dist/core/agent.d.ts +29 -0
- package/dist/core/codec/decoder.d.ts +20 -23
- package/dist/core/codec/encoder.d.ts +11 -8
- package/dist/core/codec/index.d.ts +1 -2
- package/dist/core/codec/lifecycle-tracker.d.ts +10 -9
- package/dist/core/codec/types.d.ts +410 -101
- package/dist/core/transport/agent-session.d.ts +10 -0
- package/dist/core/transport/branch-chain.d.ts +43 -0
- package/dist/core/transport/client-session.d.ts +13 -0
- package/dist/core/transport/decode-fold.d.ts +47 -0
- package/dist/core/transport/headers.d.ts +97 -17
- package/dist/core/transport/index.d.ts +5 -3
- package/dist/core/transport/internal/bounded-map.d.ts +20 -0
- package/dist/core/transport/invocation.d.ts +74 -0
- package/dist/core/transport/load-conversation.d.ts +128 -0
- package/dist/core/transport/load-history.d.ts +39 -0
- package/dist/core/transport/pipe-stream.d.ts +9 -8
- package/dist/core/transport/run-manager.d.ts +78 -0
- package/dist/core/transport/tree.d.ts +435 -0
- package/dist/core/transport/types/agent.d.ts +353 -0
- package/dist/core/transport/types/client.d.ts +168 -0
- package/dist/core/transport/types/shared.d.ts +24 -0
- package/dist/core/transport/types/tree.d.ts +315 -0
- package/dist/core/transport/types/view.d.ts +222 -0
- package/dist/core/transport/types.d.ts +13 -402
- package/dist/core/transport/view.d.ts +354 -0
- package/dist/errors.d.ts +37 -9
- package/dist/index.d.ts +6 -6
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +1164 -645
- package/dist/react/ably-ai-transport-react.js.map +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
- package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
- package/dist/react/contexts/client-session-context.d.ts +36 -0
- package/dist/react/contexts/client-session-provider.d.ts +53 -0
- package/dist/react/create-session-hooks.d.ts +116 -0
- package/dist/react/index.d.ts +16 -10
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +20 -11
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +23 -0
- package/dist/react/use-tree.d.ts +35 -0
- package/dist/react/use-view.d.ts +110 -0
- package/dist/utils.d.ts +32 -23
- package/dist/vercel/ably-ai-transport-vercel.js +2748 -1625
- package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
- package/dist/vercel/codec/decoder.d.ts +5 -18
- package/dist/vercel/codec/encoder.d.ts +6 -36
- package/dist/vercel/codec/events.d.ts +51 -0
- package/dist/vercel/codec/index.d.ts +24 -12
- package/dist/vercel/codec/reducer.d.ts +144 -0
- package/dist/vercel/codec/tool-transitions.d.ts +50 -0
- package/dist/vercel/index.d.ts +4 -2
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +10298 -1410
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +70 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +33 -0
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +96 -0
- package/dist/vercel/react/index.d.ts +4 -0
- package/dist/vercel/react/use-chat-transport.d.ts +66 -21
- package/dist/vercel/react/use-message-sync.d.ts +31 -12
- package/dist/vercel/run-end-reason.d.ts +29 -0
- package/dist/vercel/transport/chat-transport.d.ts +71 -30
- package/dist/vercel/transport/index.d.ts +25 -18
- package/dist/vercel/transport/run-output-stream.d.ts +56 -0
- package/dist/version.d.ts +2 -0
- package/package.json +47 -34
- package/src/constants.ts +126 -47
- package/src/core/agent.ts +68 -0
- package/src/core/codec/decoder.ts +71 -98
- package/src/core/codec/encoder.ts +115 -58
- package/src/core/codec/index.ts +13 -6
- package/src/core/codec/lifecycle-tracker.ts +10 -9
- package/src/core/codec/types.ts +438 -106
- package/src/core/transport/agent-session.ts +1344 -0
- package/src/core/transport/branch-chain.ts +58 -0
- package/src/core/transport/client-session.ts +775 -0
- package/src/core/transport/decode-fold.ts +91 -0
- package/src/core/transport/headers.ts +182 -19
- package/src/core/transport/index.ts +29 -22
- package/src/core/transport/internal/bounded-map.ts +27 -0
- package/src/core/transport/invocation.ts +98 -0
- package/src/core/transport/load-conversation.ts +355 -0
- package/src/core/transport/load-history.ts +269 -0
- package/src/core/transport/pipe-stream.ts +58 -40
- package/src/core/transport/run-manager.ts +249 -0
- package/src/core/transport/tree.ts +1167 -0
- package/src/core/transport/types/agent.ts +407 -0
- package/src/core/transport/types/client.ts +211 -0
- package/src/core/transport/types/shared.ts +27 -0
- package/src/core/transport/types/tree.ts +344 -0
- package/src/core/transport/types/view.ts +259 -0
- package/src/core/transport/types.ts +13 -527
- package/src/core/transport/view.ts +1271 -0
- package/src/errors.ts +42 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +55 -39
- package/src/logger.ts +14 -1
- package/src/react/contexts/client-session-context.ts +41 -0
- package/src/react/contexts/client-session-provider.tsx +186 -0
- package/src/react/create-session-hooks.ts +141 -0
- package/src/react/index.ts +27 -10
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +47 -19
- package/src/react/use-client-session.ts +201 -0
- package/src/react/use-create-view.ts +72 -0
- package/src/react/use-tree.ts +84 -0
- package/src/react/use-view.ts +275 -0
- package/src/react/vite.config.ts +4 -1
- package/src/utils.ts +63 -45
- package/src/vercel/codec/decoder.ts +336 -255
- package/src/vercel/codec/encoder.ts +348 -196
- package/src/vercel/codec/events.ts +87 -0
- package/src/vercel/codec/index.ts +59 -14
- package/src/vercel/codec/reducer.ts +977 -0
- package/src/vercel/codec/tool-transitions.ts +122 -0
- package/src/vercel/index.ts +7 -3
- package/src/vercel/react/contexts/chat-transport-context.ts +41 -0
- package/src/vercel/react/contexts/chat-transport-provider.tsx +150 -0
- package/src/vercel/react/index.ts +13 -1
- package/src/vercel/react/use-chat-transport.ts +162 -42
- package/src/vercel/react/use-message-sync.ts +121 -22
- package/src/vercel/react/vite.config.ts +4 -2
- package/src/vercel/run-end-reason.ts +78 -0
- package/src/vercel/transport/chat-transport.ts +553 -113
- package/src/vercel/transport/index.ts +40 -28
- package/src/vercel/transport/run-output-stream.ts +170 -0
- package/src/version.ts +2 -0
- package/dist/core/transport/client-transport.d.ts +0 -10
- package/dist/core/transport/conversation-tree.d.ts +0 -9
- package/dist/core/transport/decode-history.d.ts +0 -41
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -19
- package/dist/core/transport/turn-manager.d.ts +0 -34
- package/dist/react/use-active-turns.d.ts +0 -8
- package/dist/react/use-client-transport.d.ts +0 -7
- package/dist/react/use-conversation-tree.d.ts +0 -20
- package/dist/react/use-edit.d.ts +0 -7
- package/dist/react/use-history.d.ts +0 -19
- package/dist/react/use-messages.d.ts +0 -7
- package/dist/react/use-regenerate.d.ts +0 -7
- package/dist/react/use-send.d.ts +0 -7
- package/dist/vercel/codec/accumulator.d.ts +0 -21
- package/src/core/transport/client-transport.ts +0 -959
- package/src/core/transport/conversation-tree.ts +0 -434
- package/src/core/transport/decode-history.ts +0 -337
- package/src/core/transport/server-transport.ts +0 -458
- package/src/core/transport/stream-router.ts +0 -118
- package/src/core/transport/turn-manager.ts +0 -147
- package/src/react/use-active-turns.ts +0 -61
- package/src/react/use-client-transport.ts +0 -37
- package/src/react/use-conversation-tree.ts +0 -71
- package/src/react/use-edit.ts +0 -24
- package/src/react/use-history.ts +0 -111
- package/src/react/use-messages.ts +0 -32
- package/src/react/use-regenerate.ts +0 -24
- package/src/react/use-send.ts +0 -25
- package/src/vercel/codec/accumulator.ts +0 -603
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useView — reactive paginated view of the conversation.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to view updates and exposes the visible messages, msg-anchored
|
|
5
|
+
* branch navigation, write operations, pagination state, and a `loadOlder`
|
|
6
|
+
* callback. Pass `session` to use a session's default view, or `view` to
|
|
7
|
+
* subscribe to a specific {@link View} directly. When both are omitted,
|
|
8
|
+
* defaults to the nearest {@link ClientSessionProvider}'s session via context.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as Ably from 'ably';
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
13
|
+
|
|
14
|
+
import type { CodecInputEvent, CodecMessage, CodecOutputEvent } from '../core/codec/types.js';
|
|
15
|
+
import type { ActiveRun, BranchSelection, RunInfo, SendOptions, View } from '../core/transport/types.js';
|
|
16
|
+
import { ErrorCode } from '../errors.js';
|
|
17
|
+
import type { BaseSessionOption } from './internal/use-resolved-session.js';
|
|
18
|
+
import { useResolvedSession } from './internal/use-resolved-session.js';
|
|
19
|
+
|
|
20
|
+
/** Options for {@link useView}. */
|
|
21
|
+
export interface UseViewOptions<
|
|
22
|
+
TInput extends CodecInputEvent,
|
|
23
|
+
TOutput extends CodecOutputEvent,
|
|
24
|
+
TProjection,
|
|
25
|
+
TMessage,
|
|
26
|
+
> extends BaseSessionOption<TInput, TOutput, TProjection, TMessage> {
|
|
27
|
+
/** A specific {@link View} to subscribe to directly. Takes priority over `session`. */
|
|
28
|
+
view?: View<TInput, TMessage> | null;
|
|
29
|
+
/** Maximum number of older Runs to reveal per page (the pagination unit is the Run, not the message). When provided, auto-loads the first page on mount. */
|
|
30
|
+
limit?: number;
|
|
31
|
+
/** When `true`, skip all subscriptions and return an empty handle immediately. */
|
|
32
|
+
skip?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Handle for the paginated, branch-aware conversation view. */
|
|
36
|
+
export interface ViewHandle<TInput extends CodecInputEvent, TMessage> {
|
|
37
|
+
/**
|
|
38
|
+
* The visible messages along the selected branch, concatenated across all
|
|
39
|
+
* visible Runs, each paired with its codec-message-id (see
|
|
40
|
+
* {@link CodecMessage}). Read the domain object from each entry's
|
|
41
|
+
* `message` field.
|
|
42
|
+
*
|
|
43
|
+
* Correlate a rendered message back to the View — `runOf`,
|
|
44
|
+
* `branchSelection`, `selectSibling`, `regenerate`, or `edit` — via its
|
|
45
|
+
* `codecMessageId`, which the SDK assigns and tracks independently of any
|
|
46
|
+
* identity the domain `message` may carry. See {@link View.getMessages}.
|
|
47
|
+
*/
|
|
48
|
+
messages: CodecMessage<TMessage>[];
|
|
49
|
+
/** Whether there are older Runs that can be revealed via `loadOlder`. */
|
|
50
|
+
hasOlder: boolean;
|
|
51
|
+
/** Whether a page load is currently in progress. */
|
|
52
|
+
loading: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Set when the most recent `loadOlder` call failed.
|
|
55
|
+
* Cleared automatically on the next successful load.
|
|
56
|
+
* `undefined` when no error has occurred or when `skip` is `true`.
|
|
57
|
+
*/
|
|
58
|
+
loadError: Ably.ErrorInfo | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Load older messages into the view. No-op if already loading.
|
|
61
|
+
* On failure, `loadError` is set; on success, `loadError` is cleared.
|
|
62
|
+
*/
|
|
63
|
+
loadOlder: () => Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Look up the {@link RunInfo} for the Run that owns `codecMessageId`.
|
|
66
|
+
* Returns `undefined` when the codec-message-id hasn't been observed.
|
|
67
|
+
* See {@link View.runOf}.
|
|
68
|
+
*/
|
|
69
|
+
runOf: (codecMessageId: string) => RunInfo | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Direct lookup by runId. Returns `undefined` when the Run hasn't been
|
|
72
|
+
* observed. See {@link View.run}.
|
|
73
|
+
*/
|
|
74
|
+
run: (runId: string) => RunInfo | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Snapshot of the visible Runs along the selected branch, in
|
|
77
|
+
* chronological order. Returns `[]` when the view isn't resolved.
|
|
78
|
+
* See {@link View.runs}.
|
|
79
|
+
*/
|
|
80
|
+
runs: () => RunInfo[];
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the {@link BranchSelection} bundle anchored at
|
|
83
|
+
* `codecMessageId`. Always returns a safe object — see
|
|
84
|
+
* {@link BranchSelection}. See {@link View.branchSelection}.
|
|
85
|
+
*/
|
|
86
|
+
branchSelection: (codecMessageId: string) => BranchSelection<TMessage>;
|
|
87
|
+
/**
|
|
88
|
+
* Select a sibling at the branch point anchored at `codecMessageId`.
|
|
89
|
+
* `index` is clamped to `[0, siblings.length - 1]`. Silent no-op when
|
|
90
|
+
* `codecMessageId` isn't a branch anchor. See {@link View.selectSibling}.
|
|
91
|
+
*/
|
|
92
|
+
selectSibling: (codecMessageId: string, index: number) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Send one or more TInputs on the channel and fire a POST. See {@link View.send}.
|
|
95
|
+
* @throws Ably.ErrorInfo with code {@link ErrorCode.InvalidArgument} when no view is resolved (before the session is available, or when `skip` is `true`).
|
|
96
|
+
*/
|
|
97
|
+
send: (events: TInput | TInput[], options?: SendOptions) => Promise<ActiveRun>;
|
|
98
|
+
/**
|
|
99
|
+
* Regenerate an assistant message, using this view's branch for history.
|
|
100
|
+
* @throws Ably.ErrorInfo with code {@link ErrorCode.InvalidArgument} when no view is resolved (before the session is available, or when `skip` is `true`).
|
|
101
|
+
*/
|
|
102
|
+
regenerate: (messageId: string, options?: SendOptions) => Promise<ActiveRun>;
|
|
103
|
+
/**
|
|
104
|
+
* Edit a user message, forking from this view's branch.
|
|
105
|
+
* Rejects with an `Ably.ErrorInfo` (code {@link ErrorCode.InvalidArgument}) if no view is resolved — e.g. before the session is available, or when `skip` is `true`.
|
|
106
|
+
*/
|
|
107
|
+
edit: (messageId: string, inputs: TInput | TInput[], options?: SendOptions) => Promise<ActiveRun>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fallback returned by `branchSelection` when the view isn't resolved.
|
|
112
|
+
* Same shape the view returns for an unknown codec-message-id, so callers
|
|
113
|
+
* can destructure uniformly.
|
|
114
|
+
*/
|
|
115
|
+
const EMPTY_BRANCH_SELECTION: BranchSelection<never> = {
|
|
116
|
+
hasSiblings: false,
|
|
117
|
+
siblings: [],
|
|
118
|
+
index: 0,
|
|
119
|
+
selected: undefined,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Subscribe to a view and return the visible messages with pagination, navigation, and write operations.
|
|
124
|
+
*
|
|
125
|
+
* `view` takes priority over `session`. When neither is provided, the nearest
|
|
126
|
+
* {@link ClientSessionProvider}'s session is used. When `limit` is provided, auto-loads
|
|
127
|
+
* the first page on mount (SWR-style).
|
|
128
|
+
* @param props - Options for selecting the view source and configuring auto-load.
|
|
129
|
+
* @param props.session - Client session whose default view to subscribe to; defaults to the nearest provider.
|
|
130
|
+
* @param props.view - A specific {@link View} to subscribe to directly. Takes priority over `session`.
|
|
131
|
+
* @param props.limit - Max older Runs to reveal per page; when provided, auto-loads the first page on mount.
|
|
132
|
+
* @param props.skip - When `true`, skip all subscriptions and return an empty handle.
|
|
133
|
+
* @returns A {@link ViewHandle} with messages, pagination state, navigation, write operations, and loadOlder.
|
|
134
|
+
*/
|
|
135
|
+
export const useView = <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage>({
|
|
136
|
+
session,
|
|
137
|
+
view,
|
|
138
|
+
limit,
|
|
139
|
+
skip,
|
|
140
|
+
}: UseViewOptions<TInput, TOutput, TProjection, TMessage> = {}): ViewHandle<TInput, TMessage> => {
|
|
141
|
+
const resolvedSession = useResolvedSession({ session, skip });
|
|
142
|
+
const resolvedView = skip ? undefined : (view ?? resolvedSession?.view);
|
|
143
|
+
|
|
144
|
+
const [messages, setMessages] = useState<CodecMessage<TMessage>[]>(() => resolvedView?.getMessages() ?? []);
|
|
145
|
+
const [hasOlder, setHasOlder] = useState(() => resolvedView?.hasOlder() ?? false);
|
|
146
|
+
const [loading, setLoading] = useState(false);
|
|
147
|
+
const [loadError, setLoadError] = useState<Ably.ErrorInfo | undefined>();
|
|
148
|
+
const loadingRef = useRef(false);
|
|
149
|
+
|
|
150
|
+
// Auto-load first page on mount when limit is provided (SWR-style).
|
|
151
|
+
// Fires once per view instance — subsequent changes to limit
|
|
152
|
+
// only affect manual loadOlder() calls, not the initial auto-load.
|
|
153
|
+
const autoLoad = limit !== undefined;
|
|
154
|
+
const autoLoadedRef = useRef(false);
|
|
155
|
+
|
|
156
|
+
// Subscribe to view updates
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!resolvedView) {
|
|
159
|
+
setMessages([]);
|
|
160
|
+
setHasOlder(false);
|
|
161
|
+
setLoadError(undefined);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Reset auto-load flag so the new view gets its first page loaded
|
|
166
|
+
autoLoadedRef.current = false;
|
|
167
|
+
|
|
168
|
+
// Sync initial state
|
|
169
|
+
setMessages(resolvedView.getMessages());
|
|
170
|
+
setHasOlder(resolvedView.hasOlder());
|
|
171
|
+
setLoadError(undefined);
|
|
172
|
+
|
|
173
|
+
const unsub = resolvedView.on('update', () => {
|
|
174
|
+
setMessages(resolvedView.getMessages());
|
|
175
|
+
setHasOlder(resolvedView.hasOlder());
|
|
176
|
+
});
|
|
177
|
+
return unsub;
|
|
178
|
+
}, [resolvedView]);
|
|
179
|
+
|
|
180
|
+
const loadOlder = useCallback(async () => {
|
|
181
|
+
if (!resolvedView || loadingRef.current) return;
|
|
182
|
+
loadingRef.current = true;
|
|
183
|
+
setLoading(true);
|
|
184
|
+
try {
|
|
185
|
+
await resolvedView.loadOlder(limit);
|
|
186
|
+
setLoadError(undefined);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error instanceof Ably.ErrorInfo) {
|
|
189
|
+
setLoadError(error);
|
|
190
|
+
} else {
|
|
191
|
+
setLoadError(new Ably.ErrorInfo('Unknown error loading older messages', ErrorCode.BadRequest, 400));
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
loadingRef.current = false;
|
|
195
|
+
setLoading(false);
|
|
196
|
+
}
|
|
197
|
+
}, [resolvedView, limit]);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!autoLoad || autoLoadedRef.current || !resolvedView) return;
|
|
201
|
+
autoLoadedRef.current = true;
|
|
202
|
+
void loadOlder();
|
|
203
|
+
}, [autoLoad, resolvedView, loadOlder]);
|
|
204
|
+
|
|
205
|
+
// Run lookups
|
|
206
|
+
const runOf = useCallback(
|
|
207
|
+
(codecMessageId: string): RunInfo | undefined => resolvedView?.runOf(codecMessageId),
|
|
208
|
+
[resolvedView],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const run = useCallback((runId: string): RunInfo | undefined => resolvedView?.run(runId), [resolvedView]);
|
|
212
|
+
|
|
213
|
+
const runs = useCallback((): RunInfo[] => resolvedView?.runs() ?? [], [resolvedView]);
|
|
214
|
+
|
|
215
|
+
// Branch navigation
|
|
216
|
+
const branchSelection = useCallback(
|
|
217
|
+
(codecMessageId: string): BranchSelection<TMessage> =>
|
|
218
|
+
// CAST: `EMPTY_BRANCH_SELECTION` is typed `BranchSelection<never>`; `never` is
|
|
219
|
+
// assignable to any `TMessage`, so the empty bundle is a valid fallback for
|
|
220
|
+
// the not-yet-resolved view case.
|
|
221
|
+
resolvedView?.branchSelection(codecMessageId) ?? (EMPTY_BRANCH_SELECTION as BranchSelection<TMessage>),
|
|
222
|
+
[resolvedView],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const selectSibling = useCallback(
|
|
226
|
+
(codecMessageId: string, index: number) => {
|
|
227
|
+
resolvedView?.selectSibling(codecMessageId, index);
|
|
228
|
+
},
|
|
229
|
+
[resolvedView],
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Write operation callbacks
|
|
233
|
+
const send = useCallback(
|
|
234
|
+
async (events: TInput | TInput[], opts?: SendOptions) => {
|
|
235
|
+
if (!resolvedView)
|
|
236
|
+
throw new Ably.ErrorInfo('unable to send; view is not available', ErrorCode.InvalidArgument, 400);
|
|
237
|
+
return resolvedView.send(events, opts);
|
|
238
|
+
},
|
|
239
|
+
[resolvedView],
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const regenerate = useCallback(
|
|
243
|
+
async (messageId: string, opts?: SendOptions) => {
|
|
244
|
+
if (!resolvedView)
|
|
245
|
+
throw new Ably.ErrorInfo('unable to regenerate; view is not available', ErrorCode.InvalidArgument, 400);
|
|
246
|
+
return resolvedView.regenerate(messageId, opts);
|
|
247
|
+
},
|
|
248
|
+
[resolvedView],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const edit = useCallback(
|
|
252
|
+
async (messageId: string, inputs: TInput | TInput[], opts?: SendOptions) => {
|
|
253
|
+
if (!resolvedView)
|
|
254
|
+
throw new Ably.ErrorInfo('unable to edit; view is not available', ErrorCode.InvalidArgument, 400);
|
|
255
|
+
return resolvedView.edit(messageId, inputs, opts);
|
|
256
|
+
},
|
|
257
|
+
[resolvedView],
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
messages,
|
|
262
|
+
hasOlder,
|
|
263
|
+
loading,
|
|
264
|
+
loadError,
|
|
265
|
+
loadOlder,
|
|
266
|
+
runOf,
|
|
267
|
+
run,
|
|
268
|
+
runs,
|
|
269
|
+
branchSelection,
|
|
270
|
+
selectSibling,
|
|
271
|
+
send,
|
|
272
|
+
regenerate,
|
|
273
|
+
edit,
|
|
274
|
+
};
|
|
275
|
+
};
|
package/src/react/vite.config.ts
CHANGED
|
@@ -19,11 +19,14 @@ export default defineConfig({
|
|
|
19
19
|
formats: ['es', 'umd'],
|
|
20
20
|
},
|
|
21
21
|
rollupOptions: {
|
|
22
|
-
external: ['ably', 'react'],
|
|
22
|
+
external: ['ably', 'ably/react', 'react', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
|
|
23
23
|
output: {
|
|
24
24
|
globals: {
|
|
25
25
|
ably: 'Ably',
|
|
26
|
+
'ably/react': 'AblyReact',
|
|
26
27
|
react: 'React',
|
|
28
|
+
'react/jsx-runtime': 'ReactJsxRuntime',
|
|
29
|
+
'react/jsx-dev-runtime': 'ReactJsxDevRuntime',
|
|
27
30
|
},
|
|
28
31
|
},
|
|
29
32
|
},
|
package/src/utils.ts
CHANGED
|
@@ -8,24 +8,48 @@
|
|
|
8
8
|
|
|
9
9
|
import type * as Ably from 'ably';
|
|
10
10
|
|
|
11
|
-
import { DOMAIN_HEADER_PREFIX } from './constants.js';
|
|
12
|
-
|
|
13
11
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Read one tier of the SDK's `extras.ai` namespace from an Ably message.
|
|
13
|
+
* `extras.ai` is the SDK's reserved corner of the message envelope, split into
|
|
14
|
+
* a `transport` tier (generic transport headers) and a `codec` tier (codec
|
|
15
|
+
* headers). The application's own `extras.headers` is deliberately left
|
|
16
|
+
* untouched.
|
|
17
|
+
* @param message - The Ably message to read from.
|
|
18
|
+
* @param tier - Which `extras.ai` sub-namespace to read.
|
|
19
|
+
* @returns The tier's headers record, or an empty object if absent.
|
|
17
20
|
*/
|
|
18
|
-
|
|
21
|
+
const getAiTier = (message: Ably.InboundMessage, tier: 'transport' | 'codec'): Record<string, string> => {
|
|
19
22
|
// CAST: Ably SDK types `extras` as `any`; runtime checks below guard access.
|
|
20
23
|
const extras = message.extras as unknown;
|
|
21
24
|
if (!extras || typeof extras !== 'object') return {};
|
|
22
|
-
const
|
|
23
|
-
if (!
|
|
24
|
-
|
|
25
|
+
const ai = (extras as { ai?: unknown }).ai;
|
|
26
|
+
if (!ai || typeof ai !== 'object') return {};
|
|
27
|
+
const sub = (ai as Record<string, unknown>)[tier];
|
|
28
|
+
if (!sub || typeof sub !== 'object') return {};
|
|
29
|
+
// CAST: Ably wire protocol guarantees the tier is Record<string, string>
|
|
25
30
|
// when present, verified by the runtime guards above.
|
|
26
|
-
return
|
|
31
|
+
return sub as Record<string, string>;
|
|
27
32
|
};
|
|
28
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Extract the transport-tier headers (`extras.ai.transport`) from an Ably
|
|
36
|
+
* InboundMessage. These are the generic transport headers (run/stream/identity/
|
|
37
|
+
* branching), set and read by the transport layer.
|
|
38
|
+
* @param message - The Ably message to extract headers from.
|
|
39
|
+
* @returns The transport headers record, or an empty object if absent.
|
|
40
|
+
*/
|
|
41
|
+
export const getTransportHeaders = (message: Ably.InboundMessage): Record<string, string> =>
|
|
42
|
+
getAiTier(message, 'transport');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract the codec-tier headers (`extras.ai.codec`) from an Ably
|
|
46
|
+
* InboundMessage. These are the codec's own headers, with no prefix — the
|
|
47
|
+
* tier isolates them from transport headers.
|
|
48
|
+
* @param message - The Ably message to extract headers from.
|
|
49
|
+
* @returns The codec headers record, or an empty object if absent.
|
|
50
|
+
*/
|
|
51
|
+
export const getCodecHeaders = (message: Ably.InboundMessage): Record<string, string> => getAiTier(message, 'codec');
|
|
52
|
+
|
|
29
53
|
/**
|
|
30
54
|
* Parse a JSON string, returning undefined on failure.
|
|
31
55
|
* @param value - The JSON string to parse.
|
|
@@ -58,18 +82,6 @@ export const setIfPresent = (headers: Record<string, string>, key: string, value
|
|
|
58
82
|
}
|
|
59
83
|
};
|
|
60
84
|
|
|
61
|
-
/**
|
|
62
|
-
* Set multiple headers at once, skipping entries whose values are undefined or null.
|
|
63
|
-
* Each value is converted using the same rules as {@link setIfPresent}.
|
|
64
|
-
* @param headers - The headers object to mutate.
|
|
65
|
-
* @param entries - Key-value pairs to set.
|
|
66
|
-
*/
|
|
67
|
-
export const setHeadersIfPresent = (headers: Record<string, string>, entries: Record<string, unknown>): void => {
|
|
68
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
69
|
-
setIfPresent(headers, key, value);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
85
|
/**
|
|
74
86
|
* Merge two header records into a new object. Later values override earlier ones.
|
|
75
87
|
* Undefined inputs are treated as empty.
|
|
@@ -95,30 +107,36 @@ export const parseBool = (value: string | undefined): boolean | undefined => {
|
|
|
95
107
|
return value === 'true';
|
|
96
108
|
};
|
|
97
109
|
|
|
110
|
+
/** A record carrying an optional Ably `serial`, orderable by {@link compareBySerial}. */
|
|
111
|
+
interface HasSerial {
|
|
112
|
+
/** Ably serial, or undefined if the server has not yet assigned one. */
|
|
113
|
+
readonly serial?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* @param
|
|
104
|
-
* @returns
|
|
117
|
+
* Comparator that orders records by their Ably `serial` ascending
|
|
118
|
+
* (chronological). Serials are lexicographically comparable; records whose
|
|
119
|
+
* serial is undefined sort last. Pass directly to `Array.prototype.sort`.
|
|
120
|
+
* @param a - First record to compare.
|
|
121
|
+
* @param b - Second record to compare.
|
|
122
|
+
* @returns Negative if `a` precedes `b`, positive if `a` follows `b`, 0 if equal.
|
|
105
123
|
*/
|
|
106
|
-
export const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return
|
|
124
|
+
export const compareBySerial = (a: HasSerial, b: HasSerial): number => {
|
|
125
|
+
if (a.serial === undefined && b.serial === undefined) return 0;
|
|
126
|
+
if (a.serial === undefined) return 1;
|
|
127
|
+
if (b.serial === undefined) return -1;
|
|
128
|
+
if (a.serial < b.serial) return -1;
|
|
129
|
+
if (a.serial > b.serial) return 1;
|
|
130
|
+
return 0;
|
|
112
131
|
};
|
|
113
132
|
|
|
114
133
|
/**
|
|
115
|
-
* Read a domain header value from a headers record.
|
|
116
|
-
* @param headers - The headers record to read from.
|
|
117
|
-
* @param key - The
|
|
134
|
+
* Read a domain header value from a codec-tier headers record.
|
|
135
|
+
* @param headers - The codec headers record to read from.
|
|
136
|
+
* @param key - The domain key (e.g. `'toolCallId'`).
|
|
118
137
|
* @returns The header value, or undefined if absent.
|
|
119
138
|
*/
|
|
120
|
-
export const getDomainHeader = (headers: Record<string, string>, key: string): string | undefined =>
|
|
121
|
-
headers[DOMAIN_HEADER_PREFIX + key];
|
|
139
|
+
export const getDomainHeader = (headers: Record<string, string>, key: string): string | undefined => headers[key];
|
|
122
140
|
|
|
123
141
|
/**
|
|
124
142
|
* Mapped type that converts properties whose type includes `undefined`
|
|
@@ -167,7 +185,7 @@ export interface DomainHeaderReader {
|
|
|
167
185
|
str(key: string): string | undefined;
|
|
168
186
|
/** Read a domain header as a string, falling back to a default if absent. */
|
|
169
187
|
strOr(key: string, fallback: string): string;
|
|
170
|
-
/** Read a domain header as a boolean
|
|
188
|
+
/** Read a domain header as a boolean: `true` only for the exact string "true", `false` for any other present value, or undefined if absent. */
|
|
171
189
|
bool(key: string): boolean | undefined;
|
|
172
190
|
/** Read a domain header as parsed JSON, or undefined if absent or invalid. */
|
|
173
191
|
json(key: string): unknown;
|
|
@@ -206,22 +224,22 @@ export interface DomainHeaderWriter {
|
|
|
206
224
|
}
|
|
207
225
|
|
|
208
226
|
/**
|
|
209
|
-
* Create a {@link DomainHeaderWriter} for building a
|
|
210
|
-
* @returns A fluent builder that
|
|
227
|
+
* Create a {@link DomainHeaderWriter} for building a codec-tier headers record.
|
|
228
|
+
* @returns A fluent builder that accumulates codec headers under their bare keys.
|
|
211
229
|
*/
|
|
212
230
|
export const headerWriter = (): DomainHeaderWriter => {
|
|
213
231
|
const h: Record<string, string> = {};
|
|
214
232
|
const writer: DomainHeaderWriter = {
|
|
215
233
|
str: (key: string, value: string | undefined) => {
|
|
216
|
-
if (value !== undefined) h[
|
|
234
|
+
if (value !== undefined) h[key] = value;
|
|
217
235
|
return writer;
|
|
218
236
|
},
|
|
219
237
|
bool: (key: string, value: boolean | undefined) => {
|
|
220
|
-
if (value !== undefined) h[
|
|
238
|
+
if (value !== undefined) h[key] = String(value);
|
|
221
239
|
return writer;
|
|
222
240
|
},
|
|
223
241
|
json: (key: string, value: unknown) => {
|
|
224
|
-
if (value !== undefined && value !== null) h[
|
|
242
|
+
if (value !== undefined && value !== null) h[key] = JSON.stringify(value);
|
|
225
243
|
return writer;
|
|
226
244
|
},
|
|
227
245
|
build: () => h,
|