@ably/ai-transport 0.0.1 → 0.1.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.
Files changed (110) hide show
  1. package/README.md +54 -47
  2. package/dist/ably-ai-transport.js +1006 -539
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +4 -0
  7. package/dist/core/codec/types.d.ts +19 -2
  8. package/dist/core/transport/decode-history.d.ts +8 -6
  9. package/dist/core/transport/headers.d.ts +4 -2
  10. package/dist/core/transport/index.d.ts +4 -1
  11. package/dist/core/transport/pipe-stream.d.ts +3 -2
  12. package/dist/core/transport/stream-router.d.ts +11 -1
  13. package/dist/core/transport/tree.d.ts +171 -0
  14. package/dist/core/transport/turn-manager.d.ts +4 -1
  15. package/dist/core/transport/types.d.ts +270 -119
  16. package/dist/core/transport/view.d.ts +166 -0
  17. package/dist/errors.d.ts +19 -2
  18. package/dist/index.d.ts +3 -1
  19. package/dist/react/ably-ai-transport-react.js +1019 -486
  20. package/dist/react/ably-ai-transport-react.js.map +1 -1
  21. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  22. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  23. package/dist/react/contexts/transport-context.d.ts +31 -0
  24. package/dist/react/contexts/transport-provider.d.ts +49 -0
  25. package/dist/react/create-transport-hooks.d.ts +124 -0
  26. package/dist/react/index.d.ts +14 -8
  27. package/dist/react/use-ably-messages.d.ts +14 -8
  28. package/dist/react/use-active-turns.d.ts +7 -3
  29. package/dist/react/use-client-transport.d.ts +78 -5
  30. package/dist/react/use-create-view.d.ts +22 -0
  31. package/dist/react/use-tree.d.ts +20 -0
  32. package/dist/react/use-view.d.ts +79 -0
  33. package/dist/vercel/ably-ai-transport-vercel.js +1478 -842
  34. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  35. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  36. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  37. package/dist/vercel/codec/tool-transitions.d.ts +50 -0
  38. package/dist/vercel/index.d.ts +3 -0
  39. package/dist/vercel/react/ably-ai-transport-vercel-react.js +9099 -852
  40. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  41. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +45 -1
  42. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  43. package/dist/vercel/react/contexts/chat-transport-context.d.ts +32 -0
  44. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +84 -0
  45. package/dist/vercel/react/index.d.ts +5 -0
  46. package/dist/vercel/react/use-chat-transport.d.ts +61 -20
  47. package/dist/vercel/react/use-message-sync.d.ts +41 -9
  48. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +30 -0
  49. package/dist/vercel/tool-approvals.d.ts +124 -0
  50. package/dist/vercel/tool-events.d.ts +26 -0
  51. package/dist/vercel/transport/chat-transport.d.ts +33 -11
  52. package/dist/vercel/transport/index.d.ts +5 -2
  53. package/package.json +23 -17
  54. package/src/constants.ts +6 -0
  55. package/src/core/codec/encoder.ts +10 -1
  56. package/src/core/codec/types.ts +19 -3
  57. package/src/core/transport/client-transport.ts +382 -364
  58. package/src/core/transport/decode-history.ts +229 -81
  59. package/src/core/transport/headers.ts +6 -2
  60. package/src/core/transport/index.ts +13 -5
  61. package/src/core/transport/pipe-stream.ts +8 -5
  62. package/src/core/transport/server-transport.ts +212 -58
  63. package/src/core/transport/stream-router.ts +21 -3
  64. package/src/core/transport/{conversation-tree.ts → tree.ts} +192 -77
  65. package/src/core/transport/turn-manager.ts +28 -10
  66. package/src/core/transport/types.ts +318 -139
  67. package/src/core/transport/view.ts +840 -0
  68. package/src/errors.ts +21 -1
  69. package/src/index.ts +10 -5
  70. package/src/react/contexts/transport-context.ts +37 -0
  71. package/src/react/contexts/transport-provider.tsx +164 -0
  72. package/src/react/create-transport-hooks.ts +144 -0
  73. package/src/react/index.ts +15 -8
  74. package/src/react/use-ably-messages.ts +34 -16
  75. package/src/react/use-active-turns.ts +28 -17
  76. package/src/react/use-client-transport.ts +184 -24
  77. package/src/react/use-create-view.ts +68 -0
  78. package/src/react/use-tree.ts +53 -0
  79. package/src/react/use-view.ts +233 -0
  80. package/src/react/vite.config.ts +4 -1
  81. package/src/vercel/codec/accumulator.ts +64 -79
  82. package/src/vercel/codec/decoder.ts +11 -8
  83. package/src/vercel/codec/encoder.ts +68 -54
  84. package/src/vercel/codec/index.ts +0 -2
  85. package/src/vercel/codec/tool-transitions.ts +122 -0
  86. package/src/vercel/index.ts +17 -0
  87. package/src/vercel/react/contexts/chat-transport-context.ts +40 -0
  88. package/src/vercel/react/contexts/chat-transport-provider.tsx +122 -0
  89. package/src/vercel/react/index.ts +14 -0
  90. package/src/vercel/react/use-chat-transport.ts +164 -42
  91. package/src/vercel/react/use-message-sync.ts +77 -19
  92. package/src/vercel/react/use-staged-add-tool-approval-response.ts +87 -0
  93. package/src/vercel/react/vite.config.ts +4 -2
  94. package/src/vercel/tool-approvals.ts +380 -0
  95. package/src/vercel/tool-events.ts +53 -0
  96. package/src/vercel/transport/chat-transport.ts +225 -79
  97. package/src/vercel/transport/index.ts +14 -3
  98. package/dist/core/transport/conversation-tree.d.ts +0 -9
  99. package/dist/react/use-conversation-tree.d.ts +0 -20
  100. package/dist/react/use-edit.d.ts +0 -7
  101. package/dist/react/use-history.d.ts +0 -19
  102. package/dist/react/use-messages.d.ts +0 -7
  103. package/dist/react/use-regenerate.d.ts +0 -7
  104. package/dist/react/use-send.d.ts +0 -7
  105. package/src/react/use-conversation-tree.ts +0 -71
  106. package/src/react/use-edit.ts +0 -24
  107. package/src/react/use-history.ts +0 -111
  108. package/src/react/use-messages.ts +0 -32
  109. package/src/react/use-regenerate.ts +0 -24
  110. package/src/react/use-send.ts +0 -25
package/src/errors.ts CHANGED
@@ -33,7 +33,7 @@ export enum ErrorCode {
33
33
  CancelListenerError = 104002,
34
34
 
35
35
  /**
36
- * A turn lifecycle event (turn-start or turn-end) failed to publish.
36
+ * A publish within a turn failed (lifecycle event, message, or event).
37
37
  */
38
38
  TurnLifecycleError = 104003,
39
39
 
@@ -46,6 +46,26 @@ export enum ErrorCode {
46
46
  * The HTTP POST to the server endpoint failed (network error or non-2xx response).
47
47
  */
48
48
  TransportSendFailed = 104005,
49
+
50
+ /**
51
+ * The Ably channel lost message continuity — the channel entered FAILED,
52
+ * SUSPENDED, or DETACHED, or re-attached with `resumed: false`. Active
53
+ * streams can no longer be guaranteed to receive all events.
54
+ */
55
+ ChannelContinuityLost = 104006,
56
+
57
+ /**
58
+ * An operation was attempted but the channel is not in a usable state
59
+ * (not ATTACHED or ATTACHING).
60
+ */
61
+ ChannelNotReady = 104007,
62
+
63
+ /**
64
+ * An error occurred while piping a response stream to the channel — either
65
+ * the source event stream threw (e.g. LLM provider rate limit, model error,
66
+ * network failure) or an underlying publish failed mid-stream.
67
+ */
68
+ StreamError = 104008,
49
69
  }
50
70
 
51
71
  /**
package/src/index.ts CHANGED
@@ -8,21 +8,26 @@ export type {
8
8
  ClientTransport,
9
9
  ClientTransportOptions,
10
10
  CloseOptions,
11
- ConversationNode,
12
- ConversationTree,
13
- LoadHistoryOptions,
14
- MessageWithHeaders,
11
+ EventsNode,
12
+ MessageNode,
15
13
  NewTurnOptions,
16
- PaginatedMessages,
17
14
  SendOptions,
18
15
  ServerTransport,
19
16
  ServerTransportOptions,
20
17
  StreamResponseOptions,
21
18
  StreamResult,
19
+ Tree,
22
20
  Turn,
23
21
  TurnEndReason,
24
22
  TurnLifecycleEvent,
23
+ View,
25
24
  } from './core/transport/index.js';
25
+
26
+ // Deprecated aliases — intentional re-export of deprecated types for backwards compatibility.
27
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
28
+ export type { EventNode } from './core/transport/index.js';
29
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
30
+ export type { TreeNode } from './core/transport/index.js';
26
31
  export { buildTransportHeaders, createClientTransport, createServerTransport } from './core/transport/index.js';
27
32
 
28
33
  // Core codec
@@ -0,0 +1,37 @@
1
+ import type * as Ably from 'ably';
2
+ import { createContext } from 'react';
3
+
4
+ import type { ClientTransport } from '../../core/transport/types.js';
5
+
6
+ /**
7
+ * A single entry in the transport registry, holding the transport and any
8
+ * error that occurred during its construction.
9
+ *
10
+ * `transport` is `undefined` when construction failed.
11
+ * `error` is set when `createClientTransport` threw during provider render.
12
+ */
13
+ export interface TransportSlot {
14
+ /** The constructed transport, or `undefined` if construction failed. */
15
+ transport: ClientTransport<unknown, unknown> | undefined;
16
+ /** Construction error from `createClientTransport`, or `undefined` on success. */
17
+ error: Ably.ErrorInfo | undefined;
18
+ }
19
+
20
+ /** The shape of the TransportContext value — a record of channelName → slot. */
21
+ export type TransportContextValue = Readonly<Record<string, TransportSlot>>;
22
+
23
+ /**
24
+ * Context that holds the registered {@link ClientTransport} slots, keyed by channelName.
25
+ * Each slot contains the transport (or `undefined` on construction failure) and any error.
26
+ * Populated by {@link TransportProvider}; read by {@link useClientTransport}.
27
+ */
28
+ export const TransportContext = createContext<TransportContextValue>({});
29
+
30
+ /**
31
+ * Context that holds the nearest (innermost) transport slot.
32
+ * Each {@link TransportProvider} sets this to its own slot, so descendants
33
+ * can access the nearest transport without knowing its channel name.
34
+ * `undefined` when no provider is present.
35
+ * Read by hooks whose `transport` argument is omitted.
36
+ */
37
+ export const NearestTransportContext = createContext<TransportSlot | undefined>(undefined);
@@ -0,0 +1,164 @@
1
+ /**
2
+ * TransportProvider: creates a ClientTransport and makes it available to
3
+ * descendants via TransportContext.
4
+ *
5
+ * Wraps children with Ably's ChannelProvider so the underlying channel
6
+ * lifecycle is managed in one place. An inner component calls useChannel
7
+ * to get the stable channel reference and creates the transport once on
8
+ * first render (via useRef).
9
+ *
10
+ * If createClientTransport throws, the error is stored in the TransportSlot
11
+ * (alongside an undefined transport) so that useClientTransport can surface it
12
+ * as transportError without crashing the component tree.
13
+ *
14
+ * The transport is closed when the provider truly unmounts. The close is
15
+ * scheduled as a microtask so that React Strict Mode's synchronous
16
+ * remount cycle (mount → fake-unmount → remount) can cancel it before it
17
+ * fires, avoiding unnecessary transport teardown in development.
18
+ *
19
+ * Multiple TransportProviders can be nested using distinct channelNames.
20
+ * Each provider merges its slot into the parent record so descendants
21
+ * can access all registered transports via useClientTransport(channelName).
22
+ */
23
+
24
+ import * as Ably from 'ably';
25
+ import { ChannelProvider, useChannel } from 'ably/react';
26
+ import { type PropsWithChildren, type ReactNode, useContext, useEffect, useMemo, useRef } from 'react';
27
+
28
+ import { createClientTransport } from '../../core/transport/client-transport.js';
29
+ import type { ClientTransport, ClientTransportOptions } from '../../core/transport/types.js';
30
+ import { ErrorCode } from '../../errors.js';
31
+ import type { TransportSlot } from '../contexts/transport-context.js';
32
+ import { NearestTransportContext, TransportContext } from '../contexts/transport-context.js';
33
+
34
+ /**
35
+ * Props for {@link TransportProvider}.
36
+ *
37
+ * All {@link ClientTransportOptions} except `channel` (managed internally) plus `channelName`.
38
+ */
39
+ export interface TransportProviderProps<TEvent, TMessage>
40
+ extends Omit<ClientTransportOptions<TEvent, TMessage>, 'channel'>, PropsWithChildren {
41
+ /** The Ably channel name to subscribe to. Also used as the context registry key. */
42
+ channelName: string;
43
+ }
44
+
45
+ // Inner component: rendered inside ChannelProvider so useChannel resolves to
46
+ // the channel created by the outer wrapper.
47
+ const TransportProviderInner = <TEvent, TMessage>({
48
+ channelName,
49
+ children,
50
+ ...transportOptions
51
+ }: TransportProviderProps<TEvent, TMessage>) => {
52
+ const { channel } = useChannel({ channelName });
53
+ const transportRef = useRef<ClientTransport<TEvent, TMessage> | undefined>(undefined);
54
+ const transportChannelRef = useRef<string>(channelName);
55
+ const transportsToDisposeRef = useRef<ClientTransport<unknown, unknown>[]>([]);
56
+ const pendingCloseRef = useRef(false);
57
+ const constructionErrorRef = useRef<Ably.ErrorInfo | undefined>(undefined);
58
+
59
+ const alreadyCreatedOrFailed = !!transportRef.current || !!constructionErrorRef.current;
60
+
61
+ if (!alreadyCreatedOrFailed || transportChannelRef.current !== channelName) {
62
+ transportChannelRef.current = channelName;
63
+ if (transportRef.current) transportsToDisposeRef.current.push(transportRef.current);
64
+ try {
65
+ transportRef.current = createClientTransport({ ...transportOptions, channel });
66
+ constructionErrorRef.current = undefined;
67
+ } catch (error) {
68
+ transportRef.current = undefined;
69
+ constructionErrorRef.current =
70
+ error instanceof Ably.ErrorInfo
71
+ ? error
72
+ : new Ably.ErrorInfo('Unknown error while creating transport', ErrorCode.BadRequest, 400);
73
+ }
74
+ }
75
+
76
+ const parentMap = useContext(TransportContext);
77
+
78
+ // Capture ref values as locals so useMemo deps track changes correctly.
79
+ // CAST: TransportContext stores transports with erased generics.
80
+ // The generic types are fixed at the TransportProvider<TEvent, TMessage> boundary.
81
+ const currentTransport = transportRef.current as ClientTransport<unknown, unknown> | undefined;
82
+ const currentError = constructionErrorRef.current;
83
+
84
+ const slot = useMemo<TransportSlot>(
85
+ () => ({ transport: currentTransport, error: currentError }),
86
+ [currentTransport, currentError],
87
+ );
88
+
89
+ const contextValue = useMemo(() => ({ ...parentMap, [channelName]: slot }), [channelName, parentMap, slot]);
90
+
91
+ useEffect(
92
+ () => () => {
93
+ for (const transport of transportsToDisposeRef.current) void transport.close();
94
+ },
95
+ [channelName],
96
+ );
97
+
98
+ // Close the transport when the component truly unmounts. The close is
99
+ // scheduled as a microtask: in React Strict Mode (dev) the component
100
+ // remounts synchronously before any microtask can drain, so the remount's
101
+ // effect setup resets pendingCloseRef.current = false and cancels the
102
+ // close. On a real unmount no remount follows, the microtask fires, and
103
+ // the transport is closed.
104
+ useEffect(() => {
105
+ pendingCloseRef.current = false;
106
+ return () => {
107
+ pendingCloseRef.current = true;
108
+ void Promise.resolve().then(() => {
109
+ if (pendingCloseRef.current) {
110
+ void transportRef.current?.close();
111
+ }
112
+ });
113
+ };
114
+ }, []);
115
+
116
+ return (
117
+ <TransportContext.Provider value={contextValue}>
118
+ <NearestTransportContext.Provider value={slot}>{children}</NearestTransportContext.Provider>
119
+ </TransportContext.Provider>
120
+ );
121
+ };
122
+
123
+ /**
124
+ * Provide a {@link ClientTransport} to descendant components.
125
+ *
126
+ * Wraps children with Ably's `ChannelProvider` using `channelName`, creates a
127
+ * transport from the resolved channel and the remaining options, and registers it
128
+ * in `TransportContext` under `channelName`. Descendants call
129
+ * {@link useClientTransport} with the same `channelName` to access the transport.
130
+ *
131
+ * If `createClientTransport` throws during construction, the error is surfaced
132
+ * through `useClientTransport` as `transportError` — the component tree does not
133
+ * crash and children are still rendered.
134
+ *
135
+ * ```tsx
136
+ * <TransportProvider channelName="ai:demo" codec={UIMessageCodec}>
137
+ * <Chat />
138
+ * </TransportProvider>
139
+ *
140
+ * // Inside Chat:
141
+ * const { transport, transportError } = useClientTransport({ channelName: 'ai:demo' });
142
+ * ```
143
+ *
144
+ * For multiple transports, nest providers with distinct channelNames:
145
+ *
146
+ * ```tsx
147
+ * <TransportProvider channelName="ai:main" codec={UIMessageCodec}>
148
+ * <TransportProvider channelName="ai:aux" codec={UIMessageCodec}>
149
+ * <App />
150
+ * </TransportProvider>
151
+ * </TransportProvider>
152
+ *
153
+ * // Inside App:
154
+ * const { transport: main } = useClientTransport({ channelName: 'ai:main' });
155
+ * const { transport: aux } = useClientTransport({ channelName: 'ai:aux' });
156
+ * ```
157
+ * @param props - Provider configuration including `channelName`, `codec`, and all other {@link ClientTransportOptions}.
158
+ * @returns A React element wrapping children with ChannelProvider and TransportContext.
159
+ */
160
+ export const TransportProvider = <TEvent, TMessage>(props: TransportProviderProps<TEvent, TMessage>): ReactNode => (
161
+ <ChannelProvider channelName={props.channelName}>
162
+ <TransportProviderInner {...props} />
163
+ </ChannelProvider>
164
+ );
@@ -0,0 +1,144 @@
1
+ /**
2
+ * createTransportHooks: factory that captures TEvent and TMessage once and returns
3
+ * a bundle of type-safe hooks + TransportProvider. Hook call sites need no type
4
+ * parameters at every use — just call the hooks directly.
5
+ * @example
6
+ * // Once per app (e.g. in a shared transport.ts):
7
+ * export const {
8
+ * TransportProvider,
9
+ * useClientTransport,
10
+ * useView,
11
+ * useActiveTurns,
12
+ * } = createTransportHooks<UIMessageChunk, UIMessage>();
13
+ *
14
+ * // In page:
15
+ * <TransportProvider channelName="ai:demo" codec={UIMessageCodec}>
16
+ * <Chat />
17
+ * </TransportProvider>
18
+ *
19
+ * // In Chat — no type params needed, transport is implicit from nearest provider:
20
+ * const { nodes } = useView({ limit: 30 });
21
+ * const turns = useActiveTurns();
22
+ */
23
+
24
+ import type * as Ably from 'ably';
25
+ import type { ComponentType } from 'react';
26
+
27
+ import type { ClientTransport, View } from '../core/transport/types.js';
28
+ import type { TransportProviderProps } from './contexts/transport-provider.js';
29
+ import { TransportProvider as _TransportProvider } from './contexts/transport-provider.js';
30
+ import { useAblyMessages as _useAblyMessages } from './use-ably-messages.js';
31
+ import { useActiveTurns as _useActiveTurns } from './use-active-turns.js';
32
+ import type { ClientTransportHandle } from './use-client-transport.js';
33
+ import { useClientTransport as _useClientTransport } from './use-client-transport.js';
34
+ import { useCreateView as _useCreateView } from './use-create-view.js';
35
+ import type { TreeHandle } from './use-tree.js';
36
+ import { useTree as _useTree } from './use-tree.js';
37
+ import type { ViewHandle } from './use-view.js';
38
+ import { useView as _useView } from './use-view.js';
39
+
40
+ /**
41
+ * Bundle of type-safe hooks and provider returned by {@link createTransportHooks}.
42
+ *
43
+ * `TEvent` and `TMessage` are baked in at factory creation time so no type params
44
+ * are needed at hook call sites.
45
+ */
46
+ export interface TransportHooks<TEvent, TMessage> {
47
+ /**
48
+ * `TransportProvider` narrowed to `TEvent`/`TMessage`. No JSX type params needed.
49
+ */
50
+ TransportProvider: ComponentType<TransportProviderProps<TEvent, TMessage>>;
51
+ /**
52
+ * Read the transport from context. No type params needed.
53
+ *
54
+ * Returns `{ transport, transportError }`. When no provider is found,
55
+ * `transportError` is set and `transport` is a stub that throws on access —
56
+ * the hook never throws during render.
57
+ *
58
+ * Pass `onError` to subscribe to post-construction transport errors
59
+ * (e.g. send failures, channel continuity loss) without wiring
60
+ * `transport.on('error', …)` manually.
61
+ */
62
+ useClientTransport: (props?: {
63
+ /** Channel name to look up; omit to use the nearest {@link TransportProvider}. */
64
+ channelName?: string;
65
+ /** When `true`, return a stub transport that throws on any access. */
66
+ skip?: boolean;
67
+ /** Called whenever the resolved transport emits an error event. */
68
+ onError?: (error: Ably.ErrorInfo) => void;
69
+ }) => ClientTransportHandle<TEvent, TMessage>;
70
+ /**
71
+ * Subscribe to the nearest transport's view and return the visible node list with pagination.
72
+ * Pass `transport` to use a transport's default view, `view` to subscribe to a specific view
73
+ * directly. Pass `limit` to auto-load on mount. Pass `skip: true` for an empty handle.
74
+ */
75
+ useView: (props?: {
76
+ /** Client transport whose default view to subscribe to; defaults to the nearest {@link TransportProvider}. */
77
+ transport?: ClientTransport<TEvent, TMessage> | null;
78
+ /** A specific {@link View} to subscribe to directly. Takes priority over `transport`. */
79
+ view?: View<TEvent, TMessage> | null;
80
+ /** When provided, auto-loads the first page on mount. */
81
+ limit?: number;
82
+ /** When `true`, skip all subscriptions and return an empty handle. */
83
+ skip?: boolean;
84
+ }) => ViewHandle<TEvent, TMessage>;
85
+ /**
86
+ * Track active turns across all clients on the channel.
87
+ * Pass `transport` to override; defaults to the nearest {@link TransportProvider}.
88
+ */
89
+ useActiveTurns: (props?: {
90
+ /** Override transport; defaults to the nearest {@link TransportProvider}. */
91
+ transport?: ClientTransport<TEvent, TMessage> | null;
92
+ }) => Map<string, Set<string>>;
93
+ /**
94
+ * Navigate conversation branches in the transport tree.
95
+ * Pass `transport` to override; defaults to the nearest {@link TransportProvider}.
96
+ */
97
+ useTree: (props?: {
98
+ /** Override transport; defaults to the nearest {@link TransportProvider}. */
99
+ transport?: ClientTransport<TEvent, TMessage>;
100
+ }) => TreeHandle<TMessage>;
101
+ /**
102
+ * Subscribe to raw Ably messages on the transport channel.
103
+ * Pass `transport` to override; defaults to the nearest {@link TransportProvider}.
104
+ * Pass `skip: true` to return an empty array without subscribing.
105
+ */
106
+ useAblyMessages: (props?: {
107
+ /** Override transport; defaults to the nearest {@link TransportProvider}. */
108
+ transport?: ClientTransport<TEvent, TMessage>;
109
+ /** When `true`, skip all subscriptions and return an empty array. */
110
+ skip?: boolean;
111
+ }) => Ably.InboundMessage[];
112
+ /**
113
+ * Create an independent view over the same tree.
114
+ * Pass `transport` to override; defaults to the nearest {@link TransportProvider}.
115
+ * Pass `skip: true` to return an empty handle without creating a view.
116
+ */
117
+ useCreateView: (props?: {
118
+ /** Override transport; defaults to the nearest {@link TransportProvider}. */
119
+ transport?: ClientTransport<TEvent, TMessage> | null;
120
+ /** When provided, auto-loads the first page on mount. */
121
+ limit?: number;
122
+ /** When `true`, skip view creation and return an empty handle. */
123
+ skip?: boolean;
124
+ }) => ViewHandle<TEvent, TMessage>;
125
+ }
126
+
127
+ /**
128
+ * Create a bundle of type-safe hooks and provider for a given `TEvent`/`TMessage` pair.
129
+ *
130
+ * `TEvent` and `TMessage` are captured at factory creation time; hook call sites need
131
+ * no type parameters. The returned hooks are thin wrappers around the standalone hooks
132
+ * with the types resolved.
133
+ * @returns A {@link TransportHooks} bundle.
134
+ */
135
+ export const createTransportHooks = <TEvent, TMessage>(): TransportHooks<TEvent, TMessage> => ({
136
+ // CAST: TransportProvider is generic; factory narrows it to TEvent/TMessage.
137
+ TransportProvider: _TransportProvider as ComponentType<TransportProviderProps<TEvent, TMessage>>,
138
+ useClientTransport: (props) => _useClientTransport<TEvent, TMessage>(props ?? {}),
139
+ useView: (props) => _useView<TEvent, TMessage>(props ?? {}),
140
+ useActiveTurns: (props) => _useActiveTurns<TEvent, TMessage>(props ?? {}),
141
+ useTree: (props) => _useTree<TEvent, TMessage>(props ?? {}),
142
+ useAblyMessages: (props) => _useAblyMessages<TEvent, TMessage>(props ?? {}),
143
+ useCreateView: (props) => _useCreateView<TEvent, TMessage>(props ?? {}),
144
+ });
@@ -1,11 +1,18 @@
1
+ export type { EventsNode, MessageNode } from '../core/transport/types.js';
2
+ export type { TransportSlot } from './contexts/transport-context.js';
3
+ export { NearestTransportContext } from './contexts/transport-context.js';
4
+ export type { TransportProviderProps } from './contexts/transport-provider.js';
5
+ export { TransportProvider } from './contexts/transport-provider.js';
6
+ export type { TransportHooks } from './create-transport-hooks.js';
7
+ export { createTransportHooks } from './create-transport-hooks.js';
8
+ // eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional re-export for backwards compatibility
9
+ export type { EventNode, TreeNode } from '../core/transport/types.js';
1
10
  export { useAblyMessages } from './use-ably-messages.js';
2
11
  export { useActiveTurns } from './use-active-turns.js';
12
+ export type { ClientTransportHandle } from './use-client-transport.js';
3
13
  export { useClientTransport } from './use-client-transport.js';
4
- export type { ConversationTreeHandle } from './use-conversation-tree.js';
5
- export { useConversationTree } from './use-conversation-tree.js';
6
- export { useEdit } from './use-edit.js';
7
- export type { HistoryHandle } from './use-history.js';
8
- export { useHistory } from './use-history.js';
9
- export { useMessages } from './use-messages.js';
10
- export { useRegenerate } from './use-regenerate.js';
11
- export { useSend } from './use-send.js';
14
+ export { useCreateView } from './use-create-view.js';
15
+ export type { TreeHandle } from './use-tree.js';
16
+ export { useTree } from './use-tree.js';
17
+ export type { UseViewOptions, ViewHandle } from './use-view.js';
18
+ export { useView } from './use-view.js';
@@ -1,37 +1,55 @@
1
1
  /**
2
2
  * useAblyMessages — reactive raw Ably message log from a ClientTransport.
3
3
  *
4
- * Returns the accumulated raw Ably InboundMessages in chronological order,
5
- * including both live messages (from the channel subscription) and
6
- * history-loaded messages (from transport.history() calls).
4
+ * Accumulates raw Ably InboundMessages from the transport's tree
5
+ * 'ably-message' event. Messages are appended in arrival order.
7
6
  *
8
- * Subscribes to the transport's "ably-message" event and re-reads the
9
- * list on each update.
7
+ * When `transport` is omitted, defaults to the nearest
8
+ * {@link TransportProvider}'s transport via context.
9
+ * Pass `skip: true` to bypass all subscriptions and return an empty array.
10
10
  */
11
11
 
12
12
  import type * as Ably from 'ably';
13
- import { useEffect, useState } from 'react';
13
+ import { useContext, useEffect, useRef, useState } from 'react';
14
14
 
15
15
  import type { ClientTransport } from '../core/transport/types.js';
16
+ import { NearestTransportContext } from './contexts/transport-context.js';
16
17
 
17
18
  /**
18
- * Subscribe to raw Ably message updates from a client transport.
19
- * @param transport - The client transport to observe.
19
+ * Subscribe to raw Ably message updates from a client transport's tree.
20
+ * When `transport` is omitted, uses the nearest {@link TransportProvider}'s transport via context.
21
+ * @param props - Options including optional `transport` and `skip`.
22
+ * @param props.transport - Transport to subscribe to; defaults to the nearest provider.
23
+ * @param props.skip - When `true`, skip all subscriptions and return an empty array.
20
24
  * @returns The accumulated raw Ably messages in chronological order.
21
25
  */
22
- export const useAblyMessages = <TEvent, TMessage>(
23
- transport: ClientTransport<TEvent, TMessage>,
24
- ): Ably.InboundMessage[] => {
25
- const [messages, setMessages] = useState<Ably.InboundMessage[]>(() => transport.getAblyMessages());
26
+ export const useAblyMessages = <TEvent, TMessage>({
27
+ transport,
28
+ skip,
29
+ }: { transport?: ClientTransport<TEvent, TMessage>; skip?: boolean } = {}): Ably.InboundMessage[] => {
30
+ const nearestSlot = useContext(NearestTransportContext);
31
+ // CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
32
+ const resolved = skip
33
+ ? undefined
34
+ : ((transport ?? nearestSlot?.transport) as ClientTransport<TEvent, TMessage> | undefined);
35
+
36
+ const [messages, setMessages] = useState<Ably.InboundMessage[]>([]);
37
+ const messagesRef = useRef<Ably.InboundMessage[]>([]);
26
38
 
27
39
  useEffect(() => {
28
- setMessages(transport.getAblyMessages());
40
+ // Reset on transport change
41
+ messagesRef.current = [];
42
+ setMessages([]);
43
+
44
+ if (!resolved) return;
29
45
 
30
- const unsub = transport.on('ably-message', () => {
31
- setMessages(transport.getAblyMessages());
46
+ const unsub = resolved.tree.on('ably-message', (msg: Ably.InboundMessage) => {
47
+ const next = [...messagesRef.current, msg];
48
+ messagesRef.current = next;
49
+ setMessages(next);
32
50
  });
33
51
  return unsub;
34
- }, [transport]);
52
+ }, [resolved]);
35
53
 
36
54
  return messages;
37
55
  };
@@ -2,50 +2,61 @@
2
2
  * useActiveTurns: reactive view of active turns on the channel,
3
3
  * keyed by clientId.
4
4
  *
5
- * Subscribes to transport turn lifecycle events and maintains a
5
+ * Subscribes to transport tree turn lifecycle events and maintains a
6
6
  * Map<clientId, Set<turnId>> that updates on every turn start/end.
7
7
  *
8
+ * Uses tree (not view) so that all turns are tracked — including remote
9
+ * turns whose messages haven't arrived yet.
10
+ *
8
11
  * Generic — works with any codec, not tied to Vercel types.
9
12
  */
10
13
 
11
- import { useEffect, useState } from 'react';
14
+ import { useContext, useEffect, useState } from 'react';
12
15
 
13
16
  import { EVENT_TURN_START } from '../constants.js';
14
17
  import type { ClientTransport, TurnLifecycleEvent } from '../core/transport/types.js';
18
+ import { NearestTransportContext } from './contexts/transport-context.js';
15
19
 
16
20
  /**
17
21
  * Returns a reactive Map of all active turns on the channel, keyed by clientId.
18
- * Updates when turns start or end.
19
- * @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.
20
26
  * @returns A Map where keys are clientIds and values are Sets of active turnIds.
21
27
  */
22
- export const useActiveTurns = <TEvent, TMessage>(
23
- transport: ClientTransport<TEvent, TMessage> | null | undefined,
24
- ): Map<string, Set<string>> => {
28
+ export const useActiveTurns = <TEvent, TMessage>({
29
+ transport,
30
+ }: { transport?: ClientTransport<TEvent, TMessage> | null } = {}): Map<string, Set<string>> => {
31
+ const nearestSlot = useContext(NearestTransportContext);
32
+ // CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
33
+ const resolved = (transport ?? nearestSlot?.transport) as ClientTransport<TEvent, TMessage> | undefined;
34
+
25
35
  const [turns, setTurns] = useState<Map<string, Set<string>>>(() => new Map());
26
36
 
27
37
  useEffect(() => {
28
- if (!transport) return;
38
+ if (!resolved) return;
29
39
 
30
40
  // Initialize from current state
31
- setTurns(transport.getActiveTurnIds());
41
+ setTurns(resolved.tree.getActiveTurnIds());
32
42
 
33
- const unsubscribe = transport.on('turn', (event: TurnLifecycleEvent) => {
43
+ const unsubscribe = resolved.tree.on('turn', (event: TurnLifecycleEvent) => {
34
44
  setTurns((prev) => {
35
45
  const next = new Map(prev);
36
46
 
37
47
  if (event.type === EVENT_TURN_START) {
38
- const set = new Set(next.get(event.clientId) ?? []);
48
+ const set = new Set(next.get(event.clientId));
39
49
  set.add(event.turnId);
40
50
  next.set(event.clientId, set);
41
51
  } else {
42
- const set = next.get(event.clientId);
43
- if (set) {
44
- set.delete(event.turnId);
45
- if (set.size === 0) {
52
+ const existing = next.get(event.clientId);
53
+ if (existing) {
54
+ const updated = new Set(existing);
55
+ updated.delete(event.turnId);
56
+ if (updated.size === 0) {
46
57
  next.delete(event.clientId);
47
58
  } else {
48
- next.set(event.clientId, new Set(set));
59
+ next.set(event.clientId, updated);
49
60
  }
50
61
  }
51
62
  }
@@ -55,7 +66,7 @@ export const useActiveTurns = <TEvent, TMessage>(
55
66
  });
56
67
 
57
68
  return unsubscribe;
58
- }, [transport]);
69
+ }, [resolved]);
59
70
 
60
71
  return turns;
61
72
  };