@ably/ai-transport 0.0.1
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/LICENSE +176 -0
- package/README.md +426 -0
- package/dist/ably-ai-transport.js +1388 -0
- package/dist/ably-ai-transport.js.map +1 -0
- package/dist/ably-ai-transport.umd.cjs +2 -0
- package/dist/ably-ai-transport.umd.cjs.map +1 -0
- package/dist/constants.d.ts +50 -0
- package/dist/core/codec/decoder.d.ts +62 -0
- package/dist/core/codec/encoder.d.ts +56 -0
- package/dist/core/codec/index.d.ts +8 -0
- package/dist/core/codec/lifecycle-tracker.d.ts +74 -0
- package/dist/core/codec/types.d.ts +188 -0
- package/dist/core/transport/client-transport.d.ts +10 -0
- package/dist/core/transport/conversation-tree.d.ts +9 -0
- package/dist/core/transport/decode-history.d.ts +41 -0
- package/dist/core/transport/headers.d.ts +26 -0
- package/dist/core/transport/index.d.ts +4 -0
- package/dist/core/transport/pipe-stream.d.ts +16 -0
- package/dist/core/transport/server-transport.d.ts +7 -0
- package/dist/core/transport/stream-router.d.ts +19 -0
- package/dist/core/transport/turn-manager.d.ts +34 -0
- package/dist/core/transport/types.d.ts +407 -0
- package/dist/errors.d.ts +46 -0
- package/dist/event-emitter.d.ts +65 -0
- package/dist/index.d.ts +11 -0
- package/dist/logger.d.ts +103 -0
- package/dist/react/ably-ai-transport-react.js +823 -0
- package/dist/react/ably-ai-transport-react.js.map +1 -0
- package/dist/react/ably-ai-transport-react.umd.cjs +2 -0
- package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -0
- package/dist/react/index.d.ts +11 -0
- package/dist/react/use-ably-messages.d.ts +18 -0
- package/dist/react/use-active-turns.d.ts +8 -0
- package/dist/react/use-client-transport.d.ts +7 -0
- package/dist/react/use-conversation-tree.d.ts +20 -0
- package/dist/react/use-edit.d.ts +7 -0
- package/dist/react/use-history.d.ts +19 -0
- package/dist/react/use-messages.d.ts +7 -0
- package/dist/react/use-regenerate.d.ts +7 -0
- package/dist/react/use-send.d.ts +7 -0
- package/dist/utils.d.ts +127 -0
- package/dist/vercel/ably-ai-transport-vercel.js +2331 -0
- package/dist/vercel/ably-ai-transport-vercel.js.map +1 -0
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs +2 -0
- package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -0
- package/dist/vercel/codec/accumulator.d.ts +21 -0
- package/dist/vercel/codec/decoder.d.ts +22 -0
- package/dist/vercel/codec/encoder.d.ts +41 -0
- package/dist/vercel/codec/index.d.ts +22 -0
- package/dist/vercel/index.d.ts +3 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +2082 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +2 -0
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -0
- package/dist/vercel/react/index.d.ts +3 -0
- package/dist/vercel/react/use-chat-transport.d.ts +29 -0
- package/dist/vercel/react/use-message-sync.d.ts +19 -0
- package/dist/vercel/transport/chat-transport.d.ts +118 -0
- package/dist/vercel/transport/index.d.ts +36 -0
- package/package.json +123 -0
- package/react/README.md +3 -0
- package/react/index.d.ts +1 -0
- package/react/index.js +1 -0
- package/react/index.umd.cjs +1 -0
- package/src/constants.ts +98 -0
- package/src/core/codec/decoder.ts +402 -0
- package/src/core/codec/encoder.ts +470 -0
- package/src/core/codec/index.ts +28 -0
- package/src/core/codec/lifecycle-tracker.ts +140 -0
- package/src/core/codec/types.ts +249 -0
- package/src/core/transport/client-transport.ts +959 -0
- package/src/core/transport/conversation-tree.ts +434 -0
- package/src/core/transport/decode-history.ts +337 -0
- package/src/core/transport/headers.ts +46 -0
- package/src/core/transport/index.ts +34 -0
- package/src/core/transport/pipe-stream.ts +95 -0
- package/src/core/transport/server-transport.ts +458 -0
- package/src/core/transport/stream-router.ts +118 -0
- package/src/core/transport/turn-manager.ts +147 -0
- package/src/core/transport/types.ts +533 -0
- package/src/errors.ts +58 -0
- package/src/event-emitter.ts +103 -0
- package/src/index.ts +89 -0
- package/src/logger.ts +241 -0
- package/src/react/index.ts +11 -0
- package/src/react/use-ably-messages.ts +37 -0
- package/src/react/use-active-turns.ts +61 -0
- package/src/react/use-client-transport.ts +37 -0
- package/src/react/use-conversation-tree.ts +71 -0
- package/src/react/use-edit.ts +24 -0
- package/src/react/use-history.ts +111 -0
- package/src/react/use-messages.ts +32 -0
- package/src/react/use-regenerate.ts +24 -0
- package/src/react/use-send.ts +25 -0
- package/src/react/vite.config.ts +32 -0
- package/src/tsconfig.json +25 -0
- package/src/utils.ts +230 -0
- package/src/vercel/codec/accumulator.ts +603 -0
- package/src/vercel/codec/decoder.ts +615 -0
- package/src/vercel/codec/encoder.ts +396 -0
- package/src/vercel/codec/index.ts +37 -0
- package/src/vercel/index.ts +12 -0
- package/src/vercel/react/index.ts +4 -0
- package/src/vercel/react/use-chat-transport.ts +60 -0
- package/src/vercel/react/use-message-sync.ts +34 -0
- package/src/vercel/react/vite.config.ts +33 -0
- package/src/vercel/transport/chat-transport.ts +278 -0
- package/src/vercel/transport/index.ts +56 -0
- package/src/vercel/vite.config.ts +33 -0
- package/src/vite.config.ts +31 -0
- package/vercel/README.md +3 -0
- package/vercel/index.d.ts +1 -0
- package/vercel/index.js +1 -0
- package/vercel/index.umd.cjs +1 -0
- package/vercel/react/README.md +3 -0
- package/vercel/react/index.d.ts +1 -0
- package/vercel/react/index.js +1 -0
- package/vercel/react/index.umd.cjs +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useConversationTree — reactive branch navigation for a ClientTransport.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to the transport's "message" notification and provides
|
|
5
|
+
* branch navigation primitives (getSiblings, selectSibling, hasSiblings)
|
|
6
|
+
* backed by the transport's ConversationTree.
|
|
7
|
+
*
|
|
8
|
+
* Branch selection state is local to the hook instance — each component
|
|
9
|
+
* (or tab) can navigate branches independently.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
13
|
+
|
|
14
|
+
import type { ClientTransport } from '../core/transport/types.js';
|
|
15
|
+
|
|
16
|
+
/** Handle for navigating the branching conversation tree. */
|
|
17
|
+
export interface ConversationTreeHandle<TMessage> {
|
|
18
|
+
/** Linear message list for the currently selected branch. */
|
|
19
|
+
messages: TMessage[];
|
|
20
|
+
/** Get all sibling messages at a fork point. */
|
|
21
|
+
getSiblings: (msgId: string) => TMessage[];
|
|
22
|
+
/** Whether a message has siblings (should show navigation arrows). */
|
|
23
|
+
hasSiblings: (msgId: string) => boolean;
|
|
24
|
+
/** Index of the currently selected sibling. */
|
|
25
|
+
getSelectedIndex: (msgId: string) => number;
|
|
26
|
+
/** Navigate to a sibling. Triggers re-render with updated messages. */
|
|
27
|
+
selectSibling: (msgId: string, index: number) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to transport message updates and provide branch navigation primitives.
|
|
32
|
+
* @param transport - The client transport whose conversation tree to navigate.
|
|
33
|
+
* @returns A {@link ConversationTreeHandle} with the current messages and navigation methods.
|
|
34
|
+
*/
|
|
35
|
+
export const useConversationTree = <TEvent, TMessage>(
|
|
36
|
+
transport: ClientTransport<TEvent, TMessage>,
|
|
37
|
+
): ConversationTreeHandle<TMessage> => {
|
|
38
|
+
const [messages, setMessages] = useState<TMessage[]>(() => transport.getMessages());
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
setMessages(transport.getMessages());
|
|
42
|
+
|
|
43
|
+
const unsub = transport.on('message', () => {
|
|
44
|
+
setMessages(transport.getMessages());
|
|
45
|
+
});
|
|
46
|
+
return unsub;
|
|
47
|
+
}, [transport]);
|
|
48
|
+
|
|
49
|
+
const getSiblings = useCallback((msgId: string) => transport.getTree().getSiblings(msgId), [transport]);
|
|
50
|
+
|
|
51
|
+
const hasSiblings = useCallback((msgId: string) => transport.getTree().hasSiblings(msgId), [transport]);
|
|
52
|
+
|
|
53
|
+
const getSelectedIndex = useCallback((msgId: string) => transport.getTree().getSelectedIndex(msgId), [transport]);
|
|
54
|
+
|
|
55
|
+
const selectSibling = useCallback(
|
|
56
|
+
(msgId: string, index: number) => {
|
|
57
|
+
transport.getTree().select(msgId, index);
|
|
58
|
+
// flatten() returns a new array after select(), triggering re-render.
|
|
59
|
+
setMessages(transport.getMessages());
|
|
60
|
+
},
|
|
61
|
+
[transport],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
messages,
|
|
66
|
+
getSiblings,
|
|
67
|
+
hasSiblings,
|
|
68
|
+
getSelectedIndex,
|
|
69
|
+
selectSibling,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEdit — stable callback for editing a user message.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to `transport.edit()`, which automatically computes
|
|
5
|
+
* `forkOf`, `parent`, and history from the conversation tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback } from 'react';
|
|
9
|
+
|
|
10
|
+
import type { ActiveTurn, ClientTransport, SendOptions } from '../core/transport/types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return a stable `edit` callback bound to the given transport.
|
|
14
|
+
* @param transport - The client transport to edit through.
|
|
15
|
+
* @returns A function that edits a user message and returns an {@link ActiveTurn} handle.
|
|
16
|
+
*/
|
|
17
|
+
export const useEdit = <TEvent, TMessage>(
|
|
18
|
+
transport: ClientTransport<TEvent, TMessage>,
|
|
19
|
+
): ((messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>) =>
|
|
20
|
+
useCallback(
|
|
21
|
+
async (messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions): Promise<ActiveTurn<TEvent>> =>
|
|
22
|
+
transport.edit(messageId, newMessages, options),
|
|
23
|
+
[transport],
|
|
24
|
+
);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useHistory — history pagination handle for a ClientTransport.
|
|
3
|
+
*
|
|
4
|
+
* Returns a `HistoryHandle` with `load()`, `next()`, `hasNext`, and
|
|
5
|
+
* `loading` — mirroring the transport's `history()` and
|
|
6
|
+
* `PaginatedMessages` API.
|
|
7
|
+
*
|
|
8
|
+
* The transport's `history()` is branch-aware: `limit` means "keep loading
|
|
9
|
+
* until N new messages appear on the selected branch." Messages on
|
|
10
|
+
* unselected branches are loaded into the tree but not counted toward the
|
|
11
|
+
* limit. The returned `items` contain only the newly visible messages.
|
|
12
|
+
*
|
|
13
|
+
* When `options` are provided, auto-loads the first page on mount
|
|
14
|
+
* (SWR-style: options present = enabled). When omitted or null,
|
|
15
|
+
* no auto-load — call `load()` manually.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* ```tsx
|
|
19
|
+
* // Auto-load on mount
|
|
20
|
+
* const history = useHistory(transport, { limit: 30 });
|
|
21
|
+
*
|
|
22
|
+
* // Manual load (e.g. on button press)
|
|
23
|
+
* const history = useHistory(transport);
|
|
24
|
+
* // ...later: await history.load({ limit: 30 });
|
|
25
|
+
*
|
|
26
|
+
* // Scroll-back
|
|
27
|
+
* if (history.hasNext) await history.next();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
32
|
+
|
|
33
|
+
import type { ClientTransport, LoadHistoryOptions, PaginatedMessages } from '../core/transport/types.js';
|
|
34
|
+
|
|
35
|
+
/** Handle for paginated history loading. */
|
|
36
|
+
export interface HistoryHandle {
|
|
37
|
+
/** Are there older pages available? False until `load()` has been called. */
|
|
38
|
+
hasNext: boolean;
|
|
39
|
+
/** Is a page being fetched? */
|
|
40
|
+
loading: boolean;
|
|
41
|
+
/** Load the first page (or re-load with different options). Inserts into the conversation tree. */
|
|
42
|
+
load: (options?: LoadHistoryOptions) => Promise<void>;
|
|
43
|
+
/** Fetch the next (older) page. No-op if loading or no more pages. Inserts into the conversation tree. */
|
|
44
|
+
next: () => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Paginated history handle for a client transport.
|
|
49
|
+
* @param transport - The client transport to load history from, or null/undefined if not yet available.
|
|
50
|
+
* @param options - When provided, auto-loads the first page on mount. Omit or pass null for manual loading.
|
|
51
|
+
* @returns A {@link HistoryHandle} for loading and paginating through history.
|
|
52
|
+
*/
|
|
53
|
+
export const useHistory = <TEvent, TMessage>(
|
|
54
|
+
transport: ClientTransport<TEvent, TMessage> | null | undefined,
|
|
55
|
+
options?: LoadHistoryOptions | null,
|
|
56
|
+
): HistoryHandle => {
|
|
57
|
+
const [hasNext, setHasNext] = useState(false);
|
|
58
|
+
const [loading, setLoading] = useState(false);
|
|
59
|
+
const loadingRef = useRef(false);
|
|
60
|
+
const pageRef = useRef<PaginatedMessages<TMessage> | null>(null);
|
|
61
|
+
const transportRef = useRef(transport);
|
|
62
|
+
transportRef.current = transport;
|
|
63
|
+
|
|
64
|
+
const load = useCallback(async (loadOptions?: LoadHistoryOptions) => {
|
|
65
|
+
if (!transportRef.current || loadingRef.current) return;
|
|
66
|
+
loadingRef.current = true;
|
|
67
|
+
setLoading(true);
|
|
68
|
+
try {
|
|
69
|
+
const page = await transportRef.current.history(loadOptions);
|
|
70
|
+
pageRef.current = page;
|
|
71
|
+
setHasNext(page.hasNext());
|
|
72
|
+
} finally {
|
|
73
|
+
loadingRef.current = false;
|
|
74
|
+
setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const next = useCallback(async () => {
|
|
79
|
+
const page = pageRef.current;
|
|
80
|
+
if (!page || !page.hasNext() || loadingRef.current || !transportRef.current) return;
|
|
81
|
+
|
|
82
|
+
loadingRef.current = true;
|
|
83
|
+
setLoading(true);
|
|
84
|
+
try {
|
|
85
|
+
const older = await page.next();
|
|
86
|
+
if (older) {
|
|
87
|
+
pageRef.current = older;
|
|
88
|
+
setHasNext(older.hasNext());
|
|
89
|
+
} else {
|
|
90
|
+
setHasNext(false);
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
loadingRef.current = false;
|
|
94
|
+
setLoading(false);
|
|
95
|
+
}
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
// Auto-load first page on mount when options are provided (SWR-style).
|
|
99
|
+
const autoLoad = options !== undefined && options !== null;
|
|
100
|
+
const autoLoadedRef = useRef(false);
|
|
101
|
+
const optionsRef = useRef(options);
|
|
102
|
+
optionsRef.current = options;
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!autoLoad || autoLoadedRef.current || !transportRef.current) return;
|
|
106
|
+
autoLoadedRef.current = true;
|
|
107
|
+
void load(optionsRef.current ?? undefined);
|
|
108
|
+
}, [autoLoad, load]);
|
|
109
|
+
|
|
110
|
+
return { hasNext, loading, load, next };
|
|
111
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMessages — reactive message list from a ClientTransport.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to the transport's "message" notification and returns
|
|
5
|
+
* the current message list as React state. Replaces the manual
|
|
6
|
+
* useState + useEffect + on("message") + getMessages() pattern.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
10
|
+
|
|
11
|
+
import type { ClientTransport } from '../core/transport/types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Subscribe to transport message updates and return the current message list.
|
|
15
|
+
* @param transport - The client transport to observe.
|
|
16
|
+
* @returns The current list of decoded messages.
|
|
17
|
+
*/
|
|
18
|
+
export const useMessages = <TEvent, TMessage>(transport: ClientTransport<TEvent, TMessage>): TMessage[] => {
|
|
19
|
+
const [messages, setMessages] = useState<TMessage[]>(() => transport.getMessages());
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// Sync initial state in case the transport already has messages
|
|
23
|
+
setMessages(transport.getMessages());
|
|
24
|
+
|
|
25
|
+
const unsub = transport.on('message', () => {
|
|
26
|
+
setMessages(transport.getMessages());
|
|
27
|
+
});
|
|
28
|
+
return unsub;
|
|
29
|
+
}, [transport]);
|
|
30
|
+
|
|
31
|
+
return messages;
|
|
32
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRegenerate — stable callback for regenerating an assistant message.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to `transport.regenerate()`, which automatically computes
|
|
5
|
+
* `forkOf`, `parent`, and truncated history from the conversation tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback } from 'react';
|
|
9
|
+
|
|
10
|
+
import type { ActiveTurn, ClientTransport, SendOptions } from '../core/transport/types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return a stable `regenerate` callback bound to the given transport.
|
|
14
|
+
* @param transport - The client transport to regenerate through.
|
|
15
|
+
* @returns A function that regenerates an assistant message and returns an {@link ActiveTurn} handle.
|
|
16
|
+
*/
|
|
17
|
+
export const useRegenerate = <TEvent, TMessage>(
|
|
18
|
+
transport: ClientTransport<TEvent, TMessage>,
|
|
19
|
+
): ((messageId: string, options?: SendOptions) => Promise<ActiveTurn<TEvent>>) =>
|
|
20
|
+
useCallback(
|
|
21
|
+
async (messageId: string, options?: SendOptions): Promise<ActiveTurn<TEvent>> =>
|
|
22
|
+
transport.regenerate(messageId, options),
|
|
23
|
+
[transport],
|
|
24
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSend — stable callback for sending messages through a ClientTransport.
|
|
3
|
+
*
|
|
4
|
+
* Returns a `send` function that sends one or more messages in a single
|
|
5
|
+
* turn via `transport.send()`. Callers construct the domain messages
|
|
6
|
+
* themselves; the hook provides a stable reference suitable for React deps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback } from 'react';
|
|
10
|
+
|
|
11
|
+
import type { ActiveTurn, ClientTransport, SendOptions } from '../core/transport/types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Return a stable `send` callback bound to the given transport.
|
|
15
|
+
* @param transport - The client transport to send through.
|
|
16
|
+
* @returns A function that sends messages and returns an {@link ActiveTurn} handle.
|
|
17
|
+
*/
|
|
18
|
+
export const useSend = <TEvent, TMessage>(
|
|
19
|
+
transport: ClientTransport<TEvent, TMessage>,
|
|
20
|
+
): ((messages: TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>) =>
|
|
21
|
+
useCallback(
|
|
22
|
+
async (messages: TMessage[], options?: SendOptions): Promise<ActiveTurn<TEvent>> =>
|
|
23
|
+
transport.send(messages, options),
|
|
24
|
+
[transport],
|
|
25
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import dts from 'vite-plugin-dts';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
root: resolve(__dirname, '.'),
|
|
7
|
+
plugins: [
|
|
8
|
+
dts({
|
|
9
|
+
entryRoot: resolve(__dirname, '.'),
|
|
10
|
+
insertTypesEntry: true,
|
|
11
|
+
}),
|
|
12
|
+
],
|
|
13
|
+
build: {
|
|
14
|
+
outDir: '../../dist/react',
|
|
15
|
+
lib: {
|
|
16
|
+
entry: resolve(__dirname, 'index.ts'),
|
|
17
|
+
name: 'AblyAiTransportReact',
|
|
18
|
+
fileName: 'ably-ai-transport-react',
|
|
19
|
+
formats: ['es', 'umd'],
|
|
20
|
+
},
|
|
21
|
+
rollupOptions: {
|
|
22
|
+
external: ['ably', 'react'],
|
|
23
|
+
output: {
|
|
24
|
+
globals: {
|
|
25
|
+
ably: 'Ably',
|
|
26
|
+
react: 'React',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
sourcemap: true,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["./**/*.ts", "./**/*.tsx"],
|
|
3
|
+
"exclude": ["**/vite.config.ts"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"rootDir": ".",
|
|
6
|
+
"target": "es6",
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"alwaysStrict": true,
|
|
10
|
+
"noImplicitThis": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"moduleResolution": "nodenext",
|
|
14
|
+
"module": "nodenext",
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"allowJs": true,
|
|
17
|
+
"allowSyntheticDefaultImports": true,
|
|
18
|
+
"resolveJsonModule": true,
|
|
19
|
+
"strictNullChecks": true,
|
|
20
|
+
"noUncheckedIndexedAccess": true,
|
|
21
|
+
"jsx": "react-jsx",
|
|
22
|
+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
23
|
+
"types": ["node"]
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for working with Ably messages.
|
|
3
|
+
*
|
|
4
|
+
* These are general-purpose helpers used by both the codec and transport
|
|
5
|
+
* layers. They live at the top level to avoid either layer depending on
|
|
6
|
+
* the other.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type * as Ably from 'ably';
|
|
10
|
+
|
|
11
|
+
import { DOMAIN_HEADER_PREFIX } from './constants.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract extras.headers from an Ably InboundMessage.
|
|
15
|
+
* @param message - The Ably message to extract headers from.
|
|
16
|
+
* @returns The headers record, or an empty object if absent.
|
|
17
|
+
*/
|
|
18
|
+
export const getHeaders = (message: Ably.InboundMessage): Record<string, string> => {
|
|
19
|
+
// CAST: Ably SDK types `extras` as `any`; runtime checks below guard access.
|
|
20
|
+
const extras = message.extras as unknown;
|
|
21
|
+
if (!extras || typeof extras !== 'object') return {};
|
|
22
|
+
const headers = (extras as { headers?: unknown }).headers;
|
|
23
|
+
if (!headers || typeof headers !== 'object') return {};
|
|
24
|
+
// CAST: Ably wire protocol guarantees headers is Record<string, string>
|
|
25
|
+
// when present, verified by the runtime guards above.
|
|
26
|
+
return headers as Record<string, string>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse a JSON string, returning undefined on failure.
|
|
31
|
+
* @param value - The JSON string to parse.
|
|
32
|
+
* @returns The parsed value, or undefined if parsing fails.
|
|
33
|
+
*/
|
|
34
|
+
export const parseJson = (value: string | undefined): unknown => {
|
|
35
|
+
if (value === undefined) return undefined;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(value) as unknown;
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Set a header value if defined, skipping undefined and null. Strings are set directly,
|
|
45
|
+
* booleans and numbers are stringified, objects are JSON-serialized.
|
|
46
|
+
* @param headers - The headers object to mutate.
|
|
47
|
+
* @param key - The header key.
|
|
48
|
+
* @param value - The value to set.
|
|
49
|
+
*/
|
|
50
|
+
export const setIfPresent = (headers: Record<string, string>, key: string, value: unknown): void => {
|
|
51
|
+
if (value === undefined || value === null) return;
|
|
52
|
+
if (typeof value === 'string') {
|
|
53
|
+
headers[key] = value;
|
|
54
|
+
} else if (typeof value === 'boolean' || typeof value === 'number') {
|
|
55
|
+
headers[key] = String(value);
|
|
56
|
+
} else if (typeof value === 'object') {
|
|
57
|
+
headers[key] = JSON.stringify(value);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
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
|
+
/**
|
|
74
|
+
* Merge two header records into a new object. Later values override earlier ones.
|
|
75
|
+
* Undefined inputs are treated as empty.
|
|
76
|
+
* @param base - Base headers (lower priority).
|
|
77
|
+
* @param overrides - Override headers (higher priority).
|
|
78
|
+
* @returns A new merged headers object.
|
|
79
|
+
*/
|
|
80
|
+
export const mergeHeaders = (
|
|
81
|
+
base: Record<string, string> | undefined,
|
|
82
|
+
overrides: Record<string, string> | undefined,
|
|
83
|
+
): Record<string, string> => ({
|
|
84
|
+
...base,
|
|
85
|
+
...overrides,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse a boolean header ("true"/"false"), returning undefined if absent.
|
|
90
|
+
* @param value - The header string to parse.
|
|
91
|
+
* @returns True if "true", false for any other string, or undefined if absent.
|
|
92
|
+
*/
|
|
93
|
+
export const parseBool = (value: string | undefined): boolean | undefined => {
|
|
94
|
+
if (value === undefined) return undefined;
|
|
95
|
+
return value === 'true';
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build a domain headers record from key-value pairs. Each key is automatically
|
|
100
|
+
* prefixed with {@link DOMAIN_HEADER_PREFIX}. Values that are undefined or null
|
|
101
|
+
* are skipped; strings are set directly; booleans, numbers, and objects are
|
|
102
|
+
* converted using the same rules as {@link setIfPresent}.
|
|
103
|
+
* @param entries - Unprefixed key-value pairs (e.g. `{ toolCallId: 'tc-1' }` becomes `{ 'x-domain-toolCallId': 'tc-1' }`).
|
|
104
|
+
* @returns A new headers record with prefixed keys.
|
|
105
|
+
*/
|
|
106
|
+
export const domainHeaders = (entries: Record<string, unknown>): Record<string, string> => {
|
|
107
|
+
const h: Record<string, string> = {};
|
|
108
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
109
|
+
setIfPresent(h, DOMAIN_HEADER_PREFIX + key, value);
|
|
110
|
+
}
|
|
111
|
+
return h;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Read a domain header value from a headers record.
|
|
116
|
+
* @param headers - The headers record to read from.
|
|
117
|
+
* @param key - The unprefixed domain key (e.g. `'toolCallId'` reads `'x-domain-toolCallId'`).
|
|
118
|
+
* @returns The header value, or undefined if absent.
|
|
119
|
+
*/
|
|
120
|
+
export const getDomainHeader = (headers: Record<string, string>, key: string): string | undefined =>
|
|
121
|
+
headers[DOMAIN_HEADER_PREFIX + key];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Mapped type that converts properties whose type includes `undefined`
|
|
125
|
+
* into optional properties with `undefined` excluded from the value.
|
|
126
|
+
* Properties typed as `unknown` are kept required (since `undefined extends unknown`
|
|
127
|
+
* is always true, but `unknown` fields are intentionally broad, not optional).
|
|
128
|
+
*/
|
|
129
|
+
export type Stripped<T> = {
|
|
130
|
+
[K in keyof T as undefined extends T[K] ? (unknown extends T[K] ? K : never) : K]: T[K];
|
|
131
|
+
} & {
|
|
132
|
+
[K in keyof T as undefined extends T[K] ? (unknown extends T[K] ? never : K) : never]?: Exclude<T[K], undefined>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Remove all keys whose value is `undefined` from a shallow object.
|
|
137
|
+
* Returns a new object — the input is not mutated. Useful for building
|
|
138
|
+
* chunk literals with optional fields without conditional spread noise.
|
|
139
|
+
*
|
|
140
|
+
* The return type converts `{ foo: T | undefined }` to `{ foo?: T }`,
|
|
141
|
+
* matching the optional-field pattern used by the AI SDK chunk types.
|
|
142
|
+
* @param obj - The object to strip undefined values from.
|
|
143
|
+
* @returns A shallow copy with undefined-valued keys removed.
|
|
144
|
+
*/
|
|
145
|
+
export const stripUndefined = <T extends Record<string, unknown>>(obj: T): Stripped<T> => {
|
|
146
|
+
const result = {} as Record<string, unknown>;
|
|
147
|
+
for (const key in obj) {
|
|
148
|
+
if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== undefined) {
|
|
149
|
+
result[key] = obj[key];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// CAST: The runtime strip guarantees the Stripped<T> contract —
|
|
153
|
+
// required keys are always present, optional keys are absent when undefined.
|
|
154
|
+
return result as Stripped<T>;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// DomainHeaderReader — typed accessors for domain headers
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Typed accessor wrapper around a headers record for reading domain headers.
|
|
163
|
+
* Reduces repetitive `getDomainHeader` + `parseBool` / `parseJson` chains.
|
|
164
|
+
*/
|
|
165
|
+
export interface DomainHeaderReader {
|
|
166
|
+
/** Read a domain header as a string, or undefined if absent. */
|
|
167
|
+
str(key: string): string | undefined;
|
|
168
|
+
/** Read a domain header as a string, falling back to a default if absent. */
|
|
169
|
+
strOr(key: string, fallback: string): string;
|
|
170
|
+
/** Read a domain header as a boolean ("true"/"false"), or undefined if absent. */
|
|
171
|
+
bool(key: string): boolean | undefined;
|
|
172
|
+
/** Read a domain header as parsed JSON, or undefined if absent or invalid. */
|
|
173
|
+
json(key: string): unknown;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Create a {@link DomainHeaderReader} over a headers record.
|
|
178
|
+
* @param headers - The raw headers record to read domain headers from.
|
|
179
|
+
* @returns A typed accessor for domain header values.
|
|
180
|
+
*/
|
|
181
|
+
export const headerReader = (headers: Record<string, string>): DomainHeaderReader => ({
|
|
182
|
+
str: (key: string) => getDomainHeader(headers, key),
|
|
183
|
+
strOr: (key: string, fallback: string) => getDomainHeader(headers, key) ?? fallback,
|
|
184
|
+
bool: (key: string) => parseBool(getDomainHeader(headers, key)),
|
|
185
|
+
json: (key: string) => parseJson(getDomainHeader(headers, key)),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// DomainHeaderWriter — typed builder for domain headers
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Fluent builder for constructing domain header records with typed setters.
|
|
194
|
+
* Mirrors {@link DomainHeaderReader} with the same method names for symmetry.
|
|
195
|
+
* Undefined values are silently skipped on all setters.
|
|
196
|
+
*/
|
|
197
|
+
export interface DomainHeaderWriter {
|
|
198
|
+
/** Set a string domain header. Skips if value is undefined. */
|
|
199
|
+
str(key: string, value: string | undefined): DomainHeaderWriter;
|
|
200
|
+
/** Set a boolean domain header (serialized as "true"/"false"). Skips if value is undefined. */
|
|
201
|
+
bool(key: string, value: boolean | undefined): DomainHeaderWriter;
|
|
202
|
+
/** Set a JSON-serialized domain header. Skips if value is undefined or null. */
|
|
203
|
+
json(key: string, value: unknown): DomainHeaderWriter;
|
|
204
|
+
/** Return the accumulated headers record. */
|
|
205
|
+
build(): Record<string, string>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a {@link DomainHeaderWriter} for building a domain headers record.
|
|
210
|
+
* @returns A fluent builder that prefixes each key with the domain header prefix.
|
|
211
|
+
*/
|
|
212
|
+
export const headerWriter = (): DomainHeaderWriter => {
|
|
213
|
+
const h: Record<string, string> = {};
|
|
214
|
+
const writer: DomainHeaderWriter = {
|
|
215
|
+
str: (key: string, value: string | undefined) => {
|
|
216
|
+
if (value !== undefined) h[DOMAIN_HEADER_PREFIX + key] = value;
|
|
217
|
+
return writer;
|
|
218
|
+
},
|
|
219
|
+
bool: (key: string, value: boolean | undefined) => {
|
|
220
|
+
if (value !== undefined) h[DOMAIN_HEADER_PREFIX + key] = String(value);
|
|
221
|
+
return writer;
|
|
222
|
+
},
|
|
223
|
+
json: (key: string, value: unknown) => {
|
|
224
|
+
if (value !== undefined && value !== null) h[DOMAIN_HEADER_PREFIX + key] = JSON.stringify(value);
|
|
225
|
+
return writer;
|
|
226
|
+
},
|
|
227
|
+
build: () => h,
|
|
228
|
+
};
|
|
229
|
+
return writer;
|
|
230
|
+
};
|