Skip to content

Commit e6d2106

Browse files
committed
react: update hooks to support context-based transport fallback
- Updated hooks (`useView`, `useTree`, `useCreateView`, `useAblyMessages`, `useActiveTurns`, etc.) to use nearest transport for transport fallback when no explicit transport is provided. - Modified `TransportProvider` - Adjusted all affected hooks and types to align with the updated transport resolution mechanism via context.
1 parent 2d47e48 commit e6d2106

16 files changed

Lines changed: 314 additions & 154 deletions

demo/vercel/react/use-chat/src/app/chat.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ export function Chat({ chatId, clientId, historyLimit }: { chatId: string; clien
3333

3434
useMessageSync(transport, setMessages);
3535

36-
const activeTurns = useActiveTurns(transport);
36+
const activeTurns = useActiveTurns({ transport });
3737
const hasAnyTurns = activeTurns.size > 0;
3838

39-
// Auto-loads first page on mount (options provided = enabled)
40-
const { nodes, hasOlder, loading, loadOlder } = useView(transport, { limit: historyLimit ?? 30 });
39+
// Auto-loads first page on mount
40+
const { nodes, hasOlder, loading, loadOlder } = useView({ source: transport, limit: historyLimit ?? 30 });
4141

4242
useClientTools(chatMessages, addToolResult, nodes, clientId);
4343

44-
const ablyMessages = useAblyMessages(transport);
44+
const ablyMessages = useAblyMessages({ transport });
4545

4646
return (
4747
<div className="flex h-dvh">

demo/vercel/react/use-client-transport/src/app/components/chat.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ export function Chat({ chatId, clientId, historyLimit }: ChatProps) {
3434
const [split, setSplit] = useState(false);
3535

3636
const limit = historyLimit ?? 30;
37-
const view = useView(transport, { limit });
38-
const splitView = useCreateView(split ? transport : undefined, { limit });
37+
const view = useView({ source: transport, limit });
38+
const splitView = useCreateView({ transport: split ? transport : undefined, limit });
3939

4040
useClientTools(view, clientId);
4141

42-
const activeTurns = useActiveTurns(transport);
43-
const ablyMessages = useAblyMessages(transport);
42+
const activeTurns = useActiveTurns({ transport });
43+
const ablyMessages = useAblyMessages({ transport });
4444
const queue = useMessageQueue(transport, view.send);
4545

4646
const handleToolApproved = useCallback(

src/react/contexts/transport-context.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ export type TransportContextValue = Readonly<Record<string, ClientTransport<unkn
1010
* Populated by {@link TransportProvider}; read by {@link useClientTransport}.
1111
*/
1212
export const TransportContext = createContext<TransportContextValue>({});
13+
14+
/**
15+
* Context that holds the nearest (innermost) registered {@link ClientTransport}.
16+
* Each {@link TransportProvider} sets this to its own transport, so descendants
17+
* can access the nearest transport without knowing its channel name.
18+
* Read by hooks whose `transport` argument is omitted.
19+
*/
20+
export const NearestTransportContext = createContext<ClientTransport<unknown, unknown> | undefined>(undefined);

src/react/contexts/transport-provider.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { type PropsWithChildren, type ReactNode, useContext, useEffect, useMemo,
2424

2525
import { createClientTransport } from '../../core/transport/client-transport.js';
2626
import type { ClientTransport, ClientTransportOptions } from '../../core/transport/types.js';
27-
import { TransportContext } from '../contexts/transport-context.js';
27+
import { NearestTransportContext, TransportContext } from '../contexts/transport-context.js';
2828

2929
/**
3030
* Props for {@link TransportProvider}.
@@ -81,7 +81,13 @@ const TransportProviderInner = <TEvent, TMessage>({
8181
[],
8282
);
8383

84-
return <TransportContext.Provider value={contextValue}>{children}</TransportContext.Provider>;
84+
return (
85+
<TransportContext.Provider value={contextValue}>
86+
<NearestTransportContext.Provider value={transportRef.current as ClientTransport<unknown, unknown>}>
87+
{children}
88+
</NearestTransportContext.Provider>
89+
</TransportContext.Provider>
90+
);
8591
};
8692

8793
/**

src/react/create-transport-hooks.ts

Lines changed: 43 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
* TransportProvider,
99
* useClientTransport,
1010
* useView,
11-
* useSend,
1211
* useActiveTurns,
1312
* } = createTransportHooks<UIMessageChunk, UIMessage>();
1413
*
@@ -17,27 +16,24 @@
1716
* <Chat />
1817
* </TransportProvider>
1918
*
20-
* // In Chat — no type params needed:
21-
* const transport = useClientTransport('ai:demo');
22-
* const { nodes } = useView(transport, { limit: 30 });
23-
* const send = useSend(transport);
19+
* // In Chat — no type params needed, transport is implicit from nearest provider:
20+
* const { nodes } = useView({ limit: 30 });
21+
* const turns = useActiveTurns();
2422
*/
2523

2624
import type * as Ably from 'ably';
2725
import type { ComponentType } from 'react';
2826

29-
import type { ActiveTurn, ClientTransport, SendOptions } from '../core/transport/types.js';
27+
import type { ClientTransport, View } from '../core/transport/types.js';
3028
import type { TransportProviderProps } from './contexts/transport-provider.js';
3129
import { TransportProvider as _TransportProvider } from './contexts/transport-provider.js';
3230
import { useAblyMessages as _useAblyMessages } from './use-ably-messages.js';
3331
import { useActiveTurns as _useActiveTurns } from './use-active-turns.js';
3432
import { useClientTransport as _useClientTransport } from './use-client-transport.js';
35-
import { useEdit as _useEdit } from './use-edit.js';
36-
import { useRegenerate as _useRegenerate } from './use-regenerate.js';
37-
import { useSend as _useSend } from './use-send.js';
33+
import { useCreateView as _useCreateView } from './use-create-view.js';
3834
import type { TreeHandle } from './use-tree.js';
3935
import { useTree as _useTree } from './use-tree.js';
40-
import type { ViewHandle, ViewOptions } from './use-view.js';
36+
import type { ViewHandle } from './use-view.js';
4137
import { useView as _useView } from './use-view.js';
4238

4339
/**
@@ -58,53 +54,49 @@ export interface TransportHooks<TEvent, TMessage> {
5854
*/
5955
useClientTransport: (channelName: string) => ClientTransport<TEvent, TMessage>;
6056
/**
61-
* Subscribe to the transport's view and return the visible node list with pagination.
62-
* @param transport - The transport to read from.
63-
* @param options - When provided, auto-loads the first page on mount.
57+
* Subscribe to the nearest transport's view and return the visible node list with pagination.
58+
* Pass `source` to override the transport/view. Pass `limit` to auto-load on mount.
6459
*/
65-
useView: (
66-
transport: ClientTransport<TEvent, TMessage> | null | undefined,
67-
options?: ViewOptions | null,
68-
) => ViewHandle<TMessage>;
69-
/**
70-
* Return a stable `send` callback.
71-
* The returned function sends messages and returns an {@link ActiveTurn} handle.
72-
* @param transport - The transport to send through.
73-
*/
74-
useSend: (
75-
transport: ClientTransport<TEvent, TMessage>,
76-
) => (messages: TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
60+
useView: (props?: {
61+
/** Override transport or view; defaults to the nearest {@link TransportProvider}. */
62+
source?: ClientTransport<TEvent, TMessage> | View<TEvent, TMessage> | null;
63+
/** When provided, auto-loads the first page on mount. */
64+
limit?: number;
65+
}) => ViewHandle<TEvent, TMessage>;
7766
/**
7867
* Track active turns across all clients on the channel.
79-
* @param transport - The transport to observe.
68+
* Pass `transport` to override; defaults to the nearest {@link TransportProvider}.
8069
*/
81-
useActiveTurns: (transport: ClientTransport<TEvent, TMessage> | null | undefined) => Map<string, Set<string>>;
70+
useActiveTurns: (props?: {
71+
/** Override transport; defaults to the nearest {@link TransportProvider}. */
72+
transport?: ClientTransport<TEvent, TMessage> | null;
73+
}) => Map<string, Set<string>>;
8274
/**
8375
* Navigate conversation branches in the transport tree.
84-
* @param transport - The transport to read from.
85-
*/
86-
useTree: (transport: ClientTransport<TEvent, TMessage>) => TreeHandle<TMessage>;
87-
/**
88-
* Return a stable `regenerate` callback.
89-
* The returned function regenerates the given message and returns an {@link ActiveTurn} handle.
90-
* @param transport - The transport to send through.
76+
* Pass `transport` to override; defaults to the nearest {@link TransportProvider}.
9177
*/
92-
useRegenerate: (
93-
transport: ClientTransport<TEvent, TMessage>,
94-
) => (messageId: string, options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
78+
useTree: (props?: {
79+
/** Override transport; defaults to the nearest {@link TransportProvider}. */
80+
transport?: ClientTransport<TEvent, TMessage>;
81+
}) => TreeHandle<TMessage>;
9582
/**
96-
* Return a stable `edit` callback.
97-
* The returned function edits the given message and returns an {@link ActiveTurn} handle.
98-
* @param transport - The transport to send through.
83+
* Subscribe to raw Ably messages on the transport channel.
84+
* Pass `transport` to override; defaults to the nearest {@link TransportProvider}.
9985
*/
100-
useEdit: (
101-
transport: ClientTransport<TEvent, TMessage>,
102-
) => (messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
86+
useAblyMessages: (props?: {
87+
/** Override transport; defaults to the nearest {@link TransportProvider}. */
88+
transport?: ClientTransport<TEvent, TMessage>;
89+
}) => Ably.InboundMessage[];
10390
/**
104-
* Subscribe to raw Ably messages on the transport channel.
105-
* @param transport - The transport to observe.
91+
* Create an independent view over the same tree.
92+
* Pass `transport` to override; defaults to the nearest {@link TransportProvider}.
10693
*/
107-
useAblyMessages: (transport: ClientTransport<TEvent, TMessage>) => Ably.InboundMessage[];
94+
useCreateView: (props?: {
95+
/** Override transport; defaults to the nearest {@link TransportProvider}. */
96+
transport?: ClientTransport<TEvent, TMessage> | null;
97+
/** When provided, auto-loads the first page on mount. */
98+
limit?: number;
99+
}) => ViewHandle<TEvent, TMessage>;
108100
}
109101

110102
/**
@@ -119,11 +111,9 @@ export const createTransportHooks = <TEvent, TMessage>(): TransportHooks<TEvent,
119111
// CAST: TransportProvider is generic; factory narrows it to TEvent/TMessage.
120112
TransportProvider: _TransportProvider as ComponentType<TransportProviderProps<TEvent, TMessage>>,
121113
useClientTransport: (channelName: string) => _useClientTransport<TEvent, TMessage>(channelName),
122-
useView: (transport, options) => _useView(transport, options),
123-
useSend: (transport) => _useSend(transport),
124-
useActiveTurns: (transport) => _useActiveTurns(transport),
125-
useTree: (transport) => _useTree(transport),
126-
useRegenerate: (transport) => _useRegenerate(transport),
127-
useEdit: (transport) => _useEdit(transport),
128-
useAblyMessages: (transport) => _useAblyMessages(transport),
114+
useView: (props) => _useView<TEvent, TMessage>(props ?? {}),
115+
useActiveTurns: (props) => _useActiveTurns<TEvent, TMessage>(props ?? {}),
116+
useTree: (props) => _useTree<TEvent, TMessage>(props ?? {}),
117+
useAblyMessages: (props) => _useAblyMessages<TEvent, TMessage>(props ?? {}),
118+
useCreateView: (props) => _useCreateView<TEvent, TMessage>(props ?? {}),
129119
});

src/react/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
export type { EventsNode, MessageNode } from '../core/transport/types.js';
2+
export { NearestTransportContext } from './contexts/transport-context.js';
13
export type { TransportProviderProps } from './contexts/transport-provider.js';
24
export { TransportProvider } from './contexts/transport-provider.js';
35
export type { TransportHooks } from './create-transport-hooks.js';
46
export { createTransportHooks } from './create-transport-hooks.js';
5-
export type { EventsNode, MessageNode } from '../core/transport/types.js';
67
// eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional re-export for backwards compatibility
78
export type { EventNode, TreeNode } from '../core/transport/types.js';
89
export { useAblyMessages } from './use-ably-messages.js';

src/react/use-ably-messages.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,31 @@
33
*
44
* Accumulates raw Ably InboundMessages from the transport's tree
55
* 'ably-message' event. Messages are appended in arrival order.
6+
*
7+
* When `transport` is omitted, defaults to the nearest
8+
* {@link TransportProvider}'s transport via context.
69
*/
710

811
import type * as Ably from 'ably';
9-
import { useEffect, useRef, useState } from 'react';
12+
import { useContext, useEffect, useRef, useState } from 'react';
1013

1114
import type { ClientTransport } from '../core/transport/types.js';
15+
import { NearestTransportContext } from './contexts/transport-context.js';
1216

1317
/**
1418
* Subscribe to raw Ably message updates from a client transport's tree.
15-
* @param transport - The client transport to observe.
19+
* When `transport` is omitted, uses the nearest {@link TransportProvider}'s transport via context.
20+
* @param props - Options including optional `transport`.
21+
* @param props.transport - Transport to subscribe to; defaults to the nearest provider.
1622
* @returns The accumulated raw Ably messages in chronological order.
1723
*/
18-
export const useAblyMessages = <TEvent, TMessage>(
19-
transport: ClientTransport<TEvent, TMessage>,
20-
): Ably.InboundMessage[] => {
24+
export const useAblyMessages = <TEvent, TMessage>({
25+
transport,
26+
}: { transport?: ClientTransport<TEvent, TMessage> } = {}): Ably.InboundMessage[] => {
27+
const nearestTransport = useContext(NearestTransportContext);
28+
// CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
29+
const resolved = (transport ?? nearestTransport) as ClientTransport<TEvent, TMessage> | undefined;
30+
2131
const [messages, setMessages] = useState<Ably.InboundMessage[]>([]);
2232
const messagesRef = useRef<Ably.InboundMessage[]>([]);
2333

@@ -26,13 +36,15 @@ export const useAblyMessages = <TEvent, TMessage>(
2636
messagesRef.current = [];
2737
setMessages([]);
2838

29-
const unsub = transport.tree.on('ably-message', (msg: Ably.InboundMessage) => {
39+
if (!resolved) return;
40+
41+
const unsub = resolved.tree.on('ably-message', (msg: Ably.InboundMessage) => {
3042
const next = [...messagesRef.current, msg];
3143
messagesRef.current = next;
3244
setMessages(next);
3345
});
3446
return unsub;
35-
}, [transport]);
47+
}, [resolved]);
3648

3749
return messages;
3850
};

src/react/use-active-turns.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,36 @@
1111
* Generic — works with any codec, not tied to Vercel types.
1212
*/
1313

14-
import { useEffect, useState } from 'react';
14+
import { useContext, useEffect, useState } from 'react';
1515

1616
import { EVENT_TURN_START } from '../constants.js';
1717
import type { ClientTransport, TurnLifecycleEvent } from '../core/transport/types.js';
18+
import { NearestTransportContext } from './contexts/transport-context.js';
1819

1920
/**
2021
* Returns a reactive Map of all active turns on the channel, keyed by clientId.
21-
* Updates when turns start or end.
22-
* @param transport - The client transport to observe, or null/undefined if not yet available.
22+
* Updates when turns start or end. When `transport` is omitted, uses the nearest
23+
* {@link TransportProvider}'s transport via context.
24+
* @param props - Options including optional `transport`.
25+
* @param props.transport - Transport to track turns for; defaults to the nearest provider.
2326
* @returns A Map where keys are clientIds and values are Sets of active turnIds.
2427
*/
25-
export const useActiveTurns = <TEvent, TMessage>(
26-
transport: ClientTransport<TEvent, TMessage> | null | undefined,
27-
): Map<string, Set<string>> => {
28+
export const useActiveTurns = <TEvent, TMessage>({
29+
transport,
30+
}: { transport?: ClientTransport<TEvent, TMessage> | null } = {}): Map<string, Set<string>> => {
31+
const nearestTransport = useContext(NearestTransportContext);
32+
// CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
33+
const resolved = (transport ?? nearestTransport) as ClientTransport<TEvent, TMessage> | undefined;
34+
2835
const [turns, setTurns] = useState<Map<string, Set<string>>>(() => new Map());
2936

3037
useEffect(() => {
31-
if (!transport) return;
38+
if (!resolved) return;
3239

3340
// Initialize from current state
34-
setTurns(transport.tree.getActiveTurnIds());
41+
setTurns(resolved.tree.getActiveTurnIds());
3542

36-
const unsubscribe = transport.tree.on('turn', (event: TurnLifecycleEvent) => {
43+
const unsubscribe = resolved.tree.on('turn', (event: TurnLifecycleEvent) => {
3744
setTurns((prev) => {
3845
const next = new Map(prev);
3946

@@ -42,9 +49,9 @@ export const useActiveTurns = <TEvent, TMessage>(
4249
set.add(event.turnId);
4350
next.set(event.clientId, set);
4451
} else {
45-
const prev = next.get(event.clientId);
46-
if (prev) {
47-
const updated = new Set(prev);
52+
const existing = next.get(event.clientId);
53+
if (existing) {
54+
const updated = new Set(existing);
4855
updated.delete(event.turnId);
4956
if (updated.size === 0) {
5057
next.delete(event.clientId);
@@ -59,7 +66,7 @@ export const useActiveTurns = <TEvent, TMessage>(
5966
});
6067

6168
return unsubscribe;
62-
}, [transport]);
69+
}, [resolved]);
6370

6471
return turns;
6572
};

0 commit comments

Comments
 (0)