@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,63 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { CodecInputEvent, CodecOutputEvent } from '../../core/codec/types.js';
|
|
4
|
+
import type { ClientSession } from '../../core/transport/types.js';
|
|
5
|
+
import { ClientSessionContext } from '../contexts/client-session-context.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared base for hook options that accept an explicit session override.
|
|
9
|
+
* Extend this interface for any hook whose `session` option defaults to the
|
|
10
|
+
* nearest {@link ClientSessionProvider} when omitted. Pass `null` to defer
|
|
11
|
+
* resolution (e.g. when a split pane is collapsed) — the helper returns
|
|
12
|
+
* `undefined` rather than falling back to the nearest provider.
|
|
13
|
+
*/
|
|
14
|
+
export interface BaseSessionOption<
|
|
15
|
+
TInput extends CodecInputEvent,
|
|
16
|
+
TOutput extends CodecOutputEvent,
|
|
17
|
+
TProjection,
|
|
18
|
+
TMessage,
|
|
19
|
+
> {
|
|
20
|
+
/**
|
|
21
|
+
* Session to operate on; defaults to the nearest {@link ClientSessionProvider}.
|
|
22
|
+
* Pass `null` to defer (returns undefined; nearest provider is not used).
|
|
23
|
+
*/
|
|
24
|
+
session?: ClientSession<TInput, TOutput, TProjection, TMessage> | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the active `ClientSession` for a hook.
|
|
29
|
+
*
|
|
30
|
+
* Reads `ClientSessionContext` and applies the standard three-way
|
|
31
|
+
* priority: explicit `session` argument → nearest provider → `undefined`.
|
|
32
|
+
* When `skip` is `true`, returns `undefined` regardless of context.
|
|
33
|
+
* When `session` is `null`, returns `undefined` (caller is deferring).
|
|
34
|
+
*
|
|
35
|
+
* Internal — not part of the public API.
|
|
36
|
+
* @param root0 - Options.
|
|
37
|
+
* @param root0.session - Explicit session; takes priority over the nearest provider. `null` to defer.
|
|
38
|
+
* @param root0.skip - When `true`, returns `undefined` immediately; context is still read but its value is ignored.
|
|
39
|
+
* @returns The resolved session, or `undefined` if none is available or `skip` is `true`.
|
|
40
|
+
*/
|
|
41
|
+
export const useResolvedSession = <
|
|
42
|
+
TInput extends CodecInputEvent,
|
|
43
|
+
TOutput extends CodecOutputEvent,
|
|
44
|
+
TProjection,
|
|
45
|
+
TMessage,
|
|
46
|
+
>({
|
|
47
|
+
session,
|
|
48
|
+
skip,
|
|
49
|
+
}: {
|
|
50
|
+
/** Explicit session; takes priority over the nearest provider. `null` to defer. */
|
|
51
|
+
session?: ClientSession<TInput, TOutput, TProjection, TMessage> | null;
|
|
52
|
+
/** When `true`, return `undefined` immediately (context is still read, but ignored). */
|
|
53
|
+
skip?: boolean;
|
|
54
|
+
} = {}): ClientSession<TInput, TOutput, TProjection, TMessage> | undefined => {
|
|
55
|
+
const { nearest } = useContext(ClientSessionContext);
|
|
56
|
+
// CAST: ClientSessionContext stores session with erased generics; types fixed at call site.
|
|
57
|
+
const nearestSession = nearest?.session as unknown as
|
|
58
|
+
| ClientSession<TInput, TOutput, TProjection, TMessage>
|
|
59
|
+
| undefined;
|
|
60
|
+
if (skip) return undefined;
|
|
61
|
+
if (session === null) return undefined;
|
|
62
|
+
return session ?? nearestSession;
|
|
63
|
+
};
|
|
@@ -1,37 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useAblyMessages — reactive raw Ably message log from a
|
|
2
|
+
* useAblyMessages — reactive raw Ably message log from a ClientSession.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* history-loaded messages (from transport.history() calls).
|
|
4
|
+
* Accumulates raw Ably InboundMessages from the session's tree
|
|
5
|
+
* 'ably-message' event. Messages are appended in arrival order.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* When `session` is omitted, defaults to the nearest
|
|
8
|
+
* {@link ClientSessionProvider}'s session 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 { useEffect, useRef, useState } from 'react';
|
|
14
14
|
|
|
15
|
-
import type {
|
|
15
|
+
import type { CodecInputEvent, CodecOutputEvent } from '../core/codec/types.js';
|
|
16
|
+
import type { BaseSessionOption } from './internal/use-resolved-session.js';
|
|
17
|
+
import { useResolvedSession } from './internal/use-resolved-session.js';
|
|
18
|
+
|
|
19
|
+
/** Options for {@link useAblyMessages}. */
|
|
20
|
+
export interface UseAblyMessagesOptions<
|
|
21
|
+
TInput extends CodecInputEvent,
|
|
22
|
+
TOutput extends CodecOutputEvent,
|
|
23
|
+
TProjection,
|
|
24
|
+
TMessage,
|
|
25
|
+
> extends BaseSessionOption<TInput, TOutput, TProjection, TMessage> {
|
|
26
|
+
/** When `true`, skip all subscriptions and return an empty array. */
|
|
27
|
+
skip?: boolean;
|
|
28
|
+
}
|
|
16
29
|
|
|
17
30
|
/**
|
|
18
|
-
* Subscribe to raw Ably message updates from a client
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
31
|
+
* Subscribe to raw Ably message updates from a client session's tree.
|
|
32
|
+
* When `session` is omitted, uses the nearest {@link ClientSessionProvider}'s session via context.
|
|
33
|
+
* @param props - Options including optional `session` and `skip`.
|
|
34
|
+
* @param props.session - Session to subscribe to; defaults to the nearest provider.
|
|
35
|
+
* @param props.skip - When `true`, skip all subscriptions and return an empty array.
|
|
36
|
+
* @returns The accumulated raw Ably messages in event-arrival order — older history messages loaded later are appended after the live messages already present, so this is not strictly chronological.
|
|
21
37
|
*/
|
|
22
|
-
export const useAblyMessages = <
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
export const useAblyMessages = <
|
|
39
|
+
TInput extends CodecInputEvent,
|
|
40
|
+
TOutput extends CodecOutputEvent,
|
|
41
|
+
TProjection,
|
|
42
|
+
TMessage,
|
|
43
|
+
>({ session, skip }: UseAblyMessagesOptions<TInput, TOutput, TProjection, TMessage> = {}): Ably.InboundMessage[] => {
|
|
44
|
+
const resolved = useResolvedSession({ session, skip });
|
|
45
|
+
|
|
46
|
+
const [messages, setMessages] = useState<Ably.InboundMessage[]>([]);
|
|
47
|
+
const messagesRef = useRef<Ably.InboundMessage[]>([]);
|
|
26
48
|
|
|
27
49
|
useEffect(() => {
|
|
28
|
-
|
|
50
|
+
// Reset on session change
|
|
51
|
+
messagesRef.current = [];
|
|
52
|
+
setMessages([]);
|
|
53
|
+
|
|
54
|
+
if (!resolved) return;
|
|
29
55
|
|
|
30
|
-
const unsub =
|
|
31
|
-
|
|
56
|
+
const unsub = resolved.tree.on('ably-message', (msg: Ably.InboundMessage) => {
|
|
57
|
+
const next = [...messagesRef.current, msg];
|
|
58
|
+
messagesRef.current = next;
|
|
59
|
+
setMessages(next);
|
|
32
60
|
});
|
|
33
61
|
return unsub;
|
|
34
|
-
}, [
|
|
62
|
+
}, [resolved]);
|
|
35
63
|
|
|
36
64
|
return messages;
|
|
37
65
|
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useClientSession — read a ClientSession from the nearest ClientSessionProvider.
|
|
3
|
+
*
|
|
4
|
+
* The session is created by {@link ClientSessionProvider}, which reads the Ably
|
|
5
|
+
* Realtime client from the surrounding `<AblyProvider>`. This hook is a thin
|
|
6
|
+
* context reader — it does not create or manage session state.
|
|
7
|
+
*
|
|
8
|
+
* **Provider lookup**
|
|
9
|
+
* - Omit `channelName` to use the innermost `ClientSessionProvider` in the tree.
|
|
10
|
+
* - Pass `channelName` to look up a specific provider by name.
|
|
11
|
+
* - Pass `skip: true` to receive a stub session that throws on any access —
|
|
12
|
+
* safe to hold in state before auth or other conditions are ready.
|
|
13
|
+
*
|
|
14
|
+
* **Error handling**
|
|
15
|
+
* - When no matching provider is found, or when the provider's `createClientSession`
|
|
16
|
+
* call threw, `sessionError` is set on the returned object instead of throwing.
|
|
17
|
+
* The component can render an error state without an error boundary.
|
|
18
|
+
* - Pass `onError` to receive post-construction session errors (e.g. send failures,
|
|
19
|
+
* channel continuity loss) without wiring `session.on('error', ...)` manually.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as Ably from 'ably';
|
|
23
|
+
import { useContext, useEffect, useRef } from 'react';
|
|
24
|
+
|
|
25
|
+
import type { CodecInputEvent, CodecOutputEvent } from '../core/codec/types.js';
|
|
26
|
+
import type { ClientSession, Tree, View } from '../core/transport/types.js';
|
|
27
|
+
import { ErrorCode } from '../errors.js';
|
|
28
|
+
import { ClientSessionContext } from './contexts/client-session-context.js';
|
|
29
|
+
|
|
30
|
+
const SKIPPED_SESSION: ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown> = {
|
|
31
|
+
get tree(): Tree<CodecOutputEvent, unknown> {
|
|
32
|
+
throw new Ably.ErrorInfo('unable to access tree; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
33
|
+
},
|
|
34
|
+
get view(): View<CodecInputEvent, unknown> {
|
|
35
|
+
throw new Ably.ErrorInfo('unable to access view; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
36
|
+
},
|
|
37
|
+
connect: () => {
|
|
38
|
+
throw new Ably.ErrorInfo('unable to connect; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
39
|
+
},
|
|
40
|
+
createView: (): View<CodecInputEvent, unknown> => {
|
|
41
|
+
throw new Ably.ErrorInfo('unable to create view; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
42
|
+
},
|
|
43
|
+
cancel: () => {
|
|
44
|
+
throw new Ably.ErrorInfo('unable to cancel; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
45
|
+
},
|
|
46
|
+
on: () => {
|
|
47
|
+
throw new Ably.ErrorInfo('unable to subscribe; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
48
|
+
},
|
|
49
|
+
close: () => {
|
|
50
|
+
throw new Ably.ErrorInfo('unable to close; hook is skipped', ErrorCode.InvalidArgument, 400);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Return value of {@link useClientSession}.
|
|
56
|
+
*
|
|
57
|
+
* `session` is always a valid object. When `skip` is `true`, when no provider was
|
|
58
|
+
* found, or when the provider's session construction failed, `session` is a stub
|
|
59
|
+
* that throws {@link Ably.ErrorInfo} on every access.
|
|
60
|
+
* Check `sessionError` before using `session` to avoid those throws.
|
|
61
|
+
*/
|
|
62
|
+
export interface ClientSessionHandle<
|
|
63
|
+
TInput extends CodecInputEvent,
|
|
64
|
+
TOutput extends CodecOutputEvent,
|
|
65
|
+
TProjection,
|
|
66
|
+
TMessage,
|
|
67
|
+
> {
|
|
68
|
+
/**
|
|
69
|
+
* The resolved session.
|
|
70
|
+
*
|
|
71
|
+
* A throwing stub when `skip` is `true`, when no matching {@link ClientSessionProvider}
|
|
72
|
+
* was found in the tree, or when session construction failed.
|
|
73
|
+
*/
|
|
74
|
+
session: ClientSession<TInput, TOutput, TProjection, TMessage>;
|
|
75
|
+
/**
|
|
76
|
+
* Set when no matching {@link ClientSessionProvider} was found, when session
|
|
77
|
+
* construction failed, and `skip` is `false`.
|
|
78
|
+
* `undefined` when the session resolved successfully or when `skip` is `true`.
|
|
79
|
+
*/
|
|
80
|
+
sessionError?: Ably.ErrorInfo | undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read a {@link ClientSession} from the nearest {@link ClientSessionProvider}.
|
|
85
|
+
*
|
|
86
|
+
* Returns `{ session, sessionError }`. When no provider is found or session
|
|
87
|
+
* construction failed, `sessionError` is set and `session` is a stub that throws
|
|
88
|
+
* on access — the hook never throws during render.
|
|
89
|
+
*
|
|
90
|
+
* Pass `onError` to subscribe to post-construction session errors
|
|
91
|
+
* (e.g. {@link ErrorCode.SessionSendFailed}, {@link ErrorCode.ChannelContinuityLost})
|
|
92
|
+
* without calling `session.on('error', …)` manually. The subscription is
|
|
93
|
+
* created when the session resolves and removed on unmount.
|
|
94
|
+
* @param props - Hook options.
|
|
95
|
+
* @param props.channelName - Look up a specific provider by channel name; omit for the nearest.
|
|
96
|
+
* @param props.skip - When `true`, return the stub session immediately without resolving a provider slot.
|
|
97
|
+
* @param props.onError - Called whenever the resolved session emits an error event.
|
|
98
|
+
* @returns `{ session, sessionError }`.
|
|
99
|
+
*/
|
|
100
|
+
export const useClientSession = <
|
|
101
|
+
TInput extends CodecInputEvent,
|
|
102
|
+
TOutput extends CodecOutputEvent,
|
|
103
|
+
TProjection,
|
|
104
|
+
TMessage,
|
|
105
|
+
>({
|
|
106
|
+
channelName,
|
|
107
|
+
skip,
|
|
108
|
+
onError,
|
|
109
|
+
}: {
|
|
110
|
+
/**
|
|
111
|
+
* Channel name passed to the enclosing {@link ClientSessionProvider}.
|
|
112
|
+
* Omit to use the nearest provider in the tree.
|
|
113
|
+
*/
|
|
114
|
+
channelName?: string;
|
|
115
|
+
/**
|
|
116
|
+
* When `true`, skip context lookup and return a stub session that throws on
|
|
117
|
+
* any access. Use when a condition (auth, feature flag) is not yet resolved.
|
|
118
|
+
*/
|
|
119
|
+
skip?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Called whenever the resolved session emits an error event.
|
|
122
|
+
* The subscription is established once the session resolves and
|
|
123
|
+
* automatically removed on unmount or when the session changes.
|
|
124
|
+
*/
|
|
125
|
+
onError?: (error: Ably.ErrorInfo) => void;
|
|
126
|
+
} = {}): ClientSessionHandle<TInput, TOutput, TProjection, TMessage> => {
|
|
127
|
+
const { nearest: nearestSlot, providers } = useContext(ClientSessionContext);
|
|
128
|
+
const errorCallbackRef = useRef(onError);
|
|
129
|
+
errorCallbackRef.current = onError;
|
|
130
|
+
|
|
131
|
+
// Compute the session for the onError subscription *before* any conditional
|
|
132
|
+
// returns to satisfy React's rules of hooks (no hooks in branches).
|
|
133
|
+
// Erased generics — this local is only used in the useEffect below.
|
|
134
|
+
const resolvedForEffect: ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown> | undefined = skip
|
|
135
|
+
? undefined
|
|
136
|
+
: channelName === undefined
|
|
137
|
+
? nearestSlot?.session
|
|
138
|
+
: providers[channelName]?.session;
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!resolvedForEffect) return;
|
|
142
|
+
return resolvedForEffect.on('error', (errorInfo: Ably.ErrorInfo) => {
|
|
143
|
+
errorCallbackRef.current?.(errorInfo);
|
|
144
|
+
});
|
|
145
|
+
}, [resolvedForEffect]);
|
|
146
|
+
|
|
147
|
+
if (skip) {
|
|
148
|
+
return {
|
|
149
|
+
session: SKIPPED_SESSION as unknown as ClientSession<TInput, TOutput, TProjection, TMessage>,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (channelName !== undefined) {
|
|
154
|
+
const slot = providers[channelName];
|
|
155
|
+
if (slot) {
|
|
156
|
+
if (slot.session) {
|
|
157
|
+
// CAST: ClientSessionContext stores sessions with erased generics.
|
|
158
|
+
// The caller is responsible for using type parameters matching those of the ClientSessionProvider.
|
|
159
|
+
return {
|
|
160
|
+
session: slot.session as unknown as ClientSession<TInput, TOutput, TProjection, TMessage>,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Provider exists but construction failed.
|
|
164
|
+
return {
|
|
165
|
+
session: SKIPPED_SESSION as unknown as ClientSession<TInput, TOutput, TProjection, TMessage>,
|
|
166
|
+
sessionError: slot.sessionError,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
session: SKIPPED_SESSION as unknown as ClientSession<TInput, TOutput, TProjection, TMessage>,
|
|
171
|
+
sessionError: new Ably.ErrorInfo(
|
|
172
|
+
`unable to use session; no ClientSessionProvider found for channelName "${channelName}"`,
|
|
173
|
+
ErrorCode.BadRequest,
|
|
174
|
+
400,
|
|
175
|
+
),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (nearestSlot) {
|
|
180
|
+
if (nearestSlot.session) {
|
|
181
|
+
// CAST: ClientSessionContext stores session with erased generics; types fixed at call site.
|
|
182
|
+
return {
|
|
183
|
+
session: nearestSlot.session as unknown as ClientSession<TInput, TOutput, TProjection, TMessage>,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// Nearest provider exists but construction failed.
|
|
187
|
+
return {
|
|
188
|
+
session: SKIPPED_SESSION as unknown as ClientSession<TInput, TOutput, TProjection, TMessage>,
|
|
189
|
+
sessionError: nearestSlot.sessionError,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
session: SKIPPED_SESSION as unknown as ClientSession<TInput, TOutput, TProjection, TMessage>,
|
|
195
|
+
sessionError: new Ably.ErrorInfo(
|
|
196
|
+
'unable to use session; no ClientSessionProvider found in the tree',
|
|
197
|
+
ErrorCode.BadRequest,
|
|
198
|
+
400,
|
|
199
|
+
),
|
|
200
|
+
};
|
|
201
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCreateView — create an independent view with the same API as useView.
|
|
3
|
+
*
|
|
4
|
+
* Calls {@link ClientSession.createView} to create an independent view over
|
|
5
|
+
* the same conversation tree, then subscribes to it exactly like
|
|
6
|
+
* {@link useView}. The view is closed automatically on unmount or when the
|
|
7
|
+
* session reference changes.
|
|
8
|
+
*
|
|
9
|
+
* Pass `null` or omit `session` to defer creation (e.g. when a split pane is
|
|
10
|
+
* collapsed). The returned handle has empty state until a session is provided.
|
|
11
|
+
* When `session` is omitted entirely, defaults to the nearest
|
|
12
|
+
* {@link ClientSessionProvider}'s session via context.
|
|
13
|
+
* Pass `skip: true` to bypass all context reads and view creation entirely.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useEffect, useState } from 'react';
|
|
17
|
+
|
|
18
|
+
import type { CodecInputEvent, CodecOutputEvent } from '../core/codec/types.js';
|
|
19
|
+
import type { View } from '../core/transport/types.js';
|
|
20
|
+
import type { BaseSessionOption } from './internal/use-resolved-session.js';
|
|
21
|
+
import { useResolvedSession } from './internal/use-resolved-session.js';
|
|
22
|
+
import type { ViewHandle } from './use-view.js';
|
|
23
|
+
import { useView } from './use-view.js';
|
|
24
|
+
|
|
25
|
+
/** Options for {@link useCreateView}. */
|
|
26
|
+
export interface UseCreateViewOptions<
|
|
27
|
+
TInput extends CodecInputEvent,
|
|
28
|
+
TOutput extends CodecOutputEvent,
|
|
29
|
+
TProjection,
|
|
30
|
+
TMessage,
|
|
31
|
+
> extends BaseSessionOption<TInput, TOutput, TProjection, TMessage> {
|
|
32
|
+
/** When provided, auto-loads the first page on mount. Omit for manual load. */
|
|
33
|
+
limit?: number;
|
|
34
|
+
/** When `true`, skip view creation and return an empty handle immediately. */
|
|
35
|
+
skip?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create an independent {@link View} and subscribe to it.
|
|
40
|
+
* Returns the same {@link ViewHandle} as {@link useView}, but backed by a
|
|
41
|
+
* newly created view with its own branch selections and pagination state.
|
|
42
|
+
* The view is closed on unmount or when the session changes.
|
|
43
|
+
* When `session` is omitted, uses the nearest {@link ClientSessionProvider}'s session via context.
|
|
44
|
+
* @param props - Options including optional `session`, `limit` for auto-load, and `skip`.
|
|
45
|
+
* @param props.session - Session to create a view from; defaults to the nearest provider.
|
|
46
|
+
* @param props.limit - Max older messages per page; when provided, auto-loads on mount.
|
|
47
|
+
* @param props.skip - When `true`, skip view creation and return an empty handle.
|
|
48
|
+
* @returns A {@link ViewHandle} with nodes, pagination, navigation, and write operations.
|
|
49
|
+
*/
|
|
50
|
+
export const useCreateView = <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage>({
|
|
51
|
+
session,
|
|
52
|
+
limit,
|
|
53
|
+
skip,
|
|
54
|
+
}: UseCreateViewOptions<TInput, TOutput, TProjection, TMessage> = {}): ViewHandle<TInput, TMessage> => {
|
|
55
|
+
const resolved = useResolvedSession({ session, skip });
|
|
56
|
+
|
|
57
|
+
const [view, setView] = useState<View<TInput, TMessage> | undefined>();
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!resolved) {
|
|
61
|
+
setView(undefined);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const v = resolved.createView();
|
|
65
|
+
setView(v);
|
|
66
|
+
return () => {
|
|
67
|
+
v.close();
|
|
68
|
+
};
|
|
69
|
+
}, [resolved]);
|
|
70
|
+
|
|
71
|
+
return useView({ view, limit, skip });
|
|
72
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTree — stable structural query callbacks for a ClientSession's tree.
|
|
3
|
+
*
|
|
4
|
+
* Returns a {@link TreeHandle} with methods to inspect the tree structure.
|
|
5
|
+
* These are thin `useCallback` wrappers around `session.tree` — no local
|
|
6
|
+
* state or subscriptions. Branch navigation (select, getSelectedIndex) is
|
|
7
|
+
* on {@link ViewHandle} from {@link useView}.
|
|
8
|
+
*
|
|
9
|
+
* When `session` is omitted, defaults to the nearest
|
|
10
|
+
* {@link ClientSessionProvider}'s session via context.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback } from 'react';
|
|
14
|
+
|
|
15
|
+
import type { CodecInputEvent, CodecOutputEvent } from '../core/codec/types.js';
|
|
16
|
+
import type { ConversationNode, RunNode } from '../core/transport/types.js';
|
|
17
|
+
import type { BaseSessionOption } from './internal/use-resolved-session.js';
|
|
18
|
+
import { useResolvedSession } from './internal/use-resolved-session.js';
|
|
19
|
+
|
|
20
|
+
/** Handle for querying the conversation tree structure. */
|
|
21
|
+
export interface TreeHandle<TProjection> {
|
|
22
|
+
/**
|
|
23
|
+
* Get the RunNode for `runId`, or undefined if no node is keyed by `runId`
|
|
24
|
+
* (or the keyed node is an input node, not a reply run).
|
|
25
|
+
*/
|
|
26
|
+
getRunNode: (runId: string) => RunNode<TProjection> | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Get the node that owns a given codec-message-id, or undefined if not
|
|
29
|
+
* observed. Returns a {@link ConversationNode} union — narrow on `kind`
|
|
30
|
+
* (`'input'` vs `'run'`) before reading kind-specific fields.
|
|
31
|
+
*/
|
|
32
|
+
getNodeByCodecMessageId: (codecMessageId: string) => ConversationNode<TProjection> | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Get the sibling group (both kinds) the node keyed by `key` belongs to —
|
|
35
|
+
* edit versions for an input node, regenerate runs for a reply run — ordered
|
|
36
|
+
* oldest-first. A single-element array when the node has no siblings; empty
|
|
37
|
+
* when `key` is unknown. `key` is a {@link RunNode.runId} or an
|
|
38
|
+
* {@link InputNode.codecMessageId}.
|
|
39
|
+
*/
|
|
40
|
+
getSiblingNodes: (key: string) => ConversationNode<TProjection>[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Options for {@link useTree}. */
|
|
44
|
+
export type UseTreeOptions<
|
|
45
|
+
TInput extends CodecInputEvent,
|
|
46
|
+
TOutput extends CodecOutputEvent,
|
|
47
|
+
TProjection,
|
|
48
|
+
TMessage,
|
|
49
|
+
> = BaseSessionOption<TInput, TOutput, TProjection, TMessage>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Provide stable structural query callbacks backed by the session's tree.
|
|
53
|
+
* When `session` is omitted, uses the nearest {@link ClientSessionProvider}'s session via context.
|
|
54
|
+
* @param props - Options including optional `session`.
|
|
55
|
+
* @param props.session - Session to read tree structure from; defaults to the nearest provider.
|
|
56
|
+
* @returns A {@link TreeHandle} with structural query methods.
|
|
57
|
+
*/
|
|
58
|
+
export const useTree = <TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage>({
|
|
59
|
+
session,
|
|
60
|
+
}: UseTreeOptions<TInput, TOutput, TProjection, TMessage> = {}): TreeHandle<TProjection> => {
|
|
61
|
+
const resolved = useResolvedSession({ session });
|
|
62
|
+
|
|
63
|
+
const getRunNode = useCallback(
|
|
64
|
+
(runId: string): RunNode<TProjection> | undefined => resolved?.tree.getRunNode(runId),
|
|
65
|
+
[resolved],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const getNodeByCodecMessageId = useCallback(
|
|
69
|
+
(codecMessageId: string): ConversationNode<TProjection> | undefined =>
|
|
70
|
+
resolved?.tree.getNodeByCodecMessageId(codecMessageId),
|
|
71
|
+
[resolved],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const getSiblingNodes = useCallback(
|
|
75
|
+
(key: string): ConversationNode<TProjection>[] => resolved?.tree.getSiblingNodes(key) ?? [],
|
|
76
|
+
[resolved],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
getRunNode,
|
|
81
|
+
getNodeByCodecMessageId,
|
|
82
|
+
getSiblingNodes,
|
|
83
|
+
};
|
|
84
|
+
};
|