@ably/ai-transport 0.1.0 → 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 +91 -100
- package/dist/ably-ai-transport.js +1553 -1238
- 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 +116 -42
- 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 +407 -115
- 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 +96 -18
- package/dist/core/transport/index.d.ts +5 -6
- 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 -9
- package/dist/core/transport/run-manager.d.ts +78 -0
- package/dist/core/transport/tree.d.ts +373 -109
- 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 -553
- package/dist/core/transport/view.d.ts +272 -84
- package/dist/errors.d.ts +21 -10
- package/dist/index.d.ts +6 -8
- package/dist/logger.d.ts +12 -0
- package/dist/react/ably-ai-transport-react.js +976 -990
- 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 +12 -12
- package/dist/react/internal/use-resolved-session.d.ts +36 -0
- package/dist/react/use-ably-messages.d.ts +17 -14
- package/dist/react/use-client-session.d.ts +81 -0
- package/dist/react/use-create-view.d.ts +14 -13
- package/dist/react/use-tree.d.ts +30 -15
- package/dist/react/use-view.d.ts +82 -51
- package/dist/utils.d.ts +32 -23
- package/dist/vercel/ably-ai-transport-vercel.js +2573 -2086
- 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 +2 -2
- package/dist/vercel/index.d.ts +4 -5
- package/dist/vercel/react/ably-ai-transport-vercel-react.js +3907 -3266
- package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +33 -8
- package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
- package/dist/vercel/react/contexts/chat-transport-context.d.ts +7 -6
- package/dist/vercel/react/contexts/chat-transport-provider.d.ts +53 -41
- package/dist/vercel/react/index.d.ts +1 -2
- package/dist/vercel/react/use-chat-transport.d.ts +30 -26
- package/dist/vercel/react/use-message-sync.d.ts +17 -30
- package/dist/vercel/run-end-reason.d.ts +29 -0
- package/dist/vercel/transport/chat-transport.d.ts +43 -24
- package/dist/vercel/transport/index.d.ts +25 -21
- package/dist/vercel/transport/run-output-stream.d.ts +56 -0
- package/dist/version.d.ts +2 -0
- package/package.json +30 -23
- package/src/constants.ts +124 -51
- package/src/core/agent.ts +68 -0
- package/src/core/codec/decoder.ts +71 -98
- package/src/core/codec/encoder.ts +113 -65
- package/src/core/codec/index.ts +13 -6
- package/src/core/codec/lifecycle-tracker.ts +10 -9
- package/src/core/codec/types.ts +436 -120
- 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 +181 -22
- package/src/core/transport/index.ts +25 -26
- 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 +54 -39
- package/src/core/transport/run-manager.ts +249 -0
- package/src/core/transport/tree.ts +926 -308
- 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 -706
- package/src/core/transport/view.ts +864 -433
- package/src/errors.ts +22 -9
- package/src/event-emitter.ts +3 -2
- package/src/index.ts +52 -41
- 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 +23 -13
- package/src/react/internal/use-resolved-session.ts +63 -0
- package/src/react/use-ably-messages.ts +32 -22
- package/src/react/use-client-session.ts +201 -0
- package/src/react/use-create-view.ts +33 -29
- package/src/react/use-tree.ts +61 -30
- package/src/react/use-view.ts +139 -97
- package/src/utils.ts +63 -45
- package/src/vercel/codec/decoder.ts +336 -258
- package/src/vercel/codec/encoder.ts +343 -205
- package/src/vercel/codec/events.ts +87 -0
- package/src/vercel/codec/index.ts +60 -13
- package/src/vercel/codec/reducer.ts +977 -0
- package/src/vercel/codec/tool-transitions.ts +2 -2
- package/src/vercel/index.ts +6 -19
- package/src/vercel/react/contexts/chat-transport-context.ts +7 -6
- package/src/vercel/react/contexts/chat-transport-provider.tsx +87 -59
- package/src/vercel/react/index.ts +3 -5
- package/src/vercel/react/use-chat-transport.ts +47 -49
- package/src/vercel/react/use-message-sync.ts +80 -39
- package/src/vercel/run-end-reason.ts +78 -0
- package/src/vercel/transport/chat-transport.ts +392 -98
- package/src/vercel/transport/index.ts +39 -38
- 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/decode-history.d.ts +0 -43
- package/dist/core/transport/server-transport.d.ts +0 -7
- package/dist/core/transport/stream-router.d.ts +0 -29
- package/dist/core/transport/turn-manager.d.ts +0 -37
- package/dist/react/contexts/transport-context.d.ts +0 -31
- package/dist/react/contexts/transport-provider.d.ts +0 -49
- package/dist/react/create-transport-hooks.d.ts +0 -124
- package/dist/react/use-active-turns.d.ts +0 -12
- package/dist/react/use-client-transport.d.ts +0 -80
- package/dist/vercel/codec/accumulator.d.ts +0 -21
- package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +0 -30
- package/dist/vercel/tool-approvals.d.ts +0 -124
- package/dist/vercel/tool-events.d.ts +0 -26
- package/src/core/transport/client-transport.ts +0 -977
- package/src/core/transport/decode-history.ts +0 -485
- package/src/core/transport/server-transport.ts +0 -612
- package/src/core/transport/stream-router.ts +0 -136
- package/src/core/transport/turn-manager.ts +0 -165
- package/src/react/contexts/transport-context.ts +0 -37
- package/src/react/contexts/transport-provider.tsx +0 -164
- package/src/react/create-transport-hooks.ts +0 -144
- package/src/react/use-active-turns.ts +0 -72
- package/src/react/use-client-transport.ts +0 -197
- package/src/vercel/codec/accumulator.ts +0 -588
- package/src/vercel/react/use-staged-add-tool-approval-response.ts +0 -87
- package/src/vercel/tool-approvals.ts +0 -380
- package/src/vercel/tool-events.ts +0 -53
package/src/errors.ts
CHANGED
|
@@ -14,18 +14,24 @@ export enum ErrorCode {
|
|
|
14
14
|
*/
|
|
15
15
|
InvalidArgument = 40003,
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Operation not permitted with the provided capability (Ably 40160).
|
|
19
|
+
* Used when the Ably channel rejects a publish for a capability reason.
|
|
20
|
+
*/
|
|
21
|
+
InsufficientCapability = 40160,
|
|
22
|
+
|
|
17
23
|
// 104000 - 104999 are reserved for AI Transport SDK errors
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
|
-
* Encoder recovery failed
|
|
26
|
+
* Encoder recovery failed during flush — one or more updateMessage calls
|
|
21
27
|
* could not recover a failed append pipeline.
|
|
22
28
|
*/
|
|
23
29
|
EncoderRecoveryFailed = 104000,
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
|
-
* A
|
|
32
|
+
* A session-level channel subscription callback threw unexpectedly.
|
|
27
33
|
*/
|
|
28
|
-
|
|
34
|
+
SessionSubscriptionError = 104001,
|
|
29
35
|
|
|
30
36
|
/**
|
|
31
37
|
* Cancel listener or onCancel hook threw while processing a cancel message.
|
|
@@ -33,19 +39,19 @@ export enum ErrorCode {
|
|
|
33
39
|
CancelListenerError = 104002,
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
|
-
* A publish within a
|
|
42
|
+
* A publish within a run failed (lifecycle event, message, or event).
|
|
37
43
|
*/
|
|
38
|
-
|
|
44
|
+
RunLifecycleError = 104003,
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
|
-
* An operation was attempted on a
|
|
47
|
+
* An operation was attempted on a session that has already been closed.
|
|
42
48
|
*/
|
|
43
|
-
|
|
49
|
+
SessionClosed = 104004,
|
|
44
50
|
|
|
45
51
|
/**
|
|
46
|
-
* The HTTP POST to the
|
|
52
|
+
* The HTTP POST to the agent endpoint failed (network error or non-2xx response).
|
|
47
53
|
*/
|
|
48
|
-
|
|
54
|
+
SessionSendFailed = 104005,
|
|
49
55
|
|
|
50
56
|
/**
|
|
51
57
|
* The Ably channel lost message continuity — the channel entered FAILED,
|
|
@@ -66,6 +72,13 @@ export enum ErrorCode {
|
|
|
66
72
|
* network failure) or an underlying publish failed mid-stream.
|
|
67
73
|
*/
|
|
68
74
|
StreamError = 104008,
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The agent attached to the channel and waited for the input event(s) the
|
|
78
|
+
* invocation points at (rewind + live wait) but `inputEventLookupTimeoutMs`
|
|
79
|
+
* lapsed without seeing them.
|
|
80
|
+
*/
|
|
81
|
+
InputEventNotFound = 104010,
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
/**
|
package/src/event-emitter.ts
CHANGED
|
@@ -76,8 +76,9 @@ const toAblyLogger = (logger: Logger): unknown => ({
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
// CAST: Access Ably's internal EventEmitter constructor. Not publicly exported
|
|
79
|
-
// but available to other Ably SDKs.
|
|
80
|
-
//
|
|
79
|
+
// but available to other Ably SDKs. Ably always catches listener exceptions
|
|
80
|
+
// internally; the logger parameter ensures those caught exceptions are logged
|
|
81
|
+
// rather than silently swallowed.
|
|
81
82
|
const InternalEventEmitter: new <EventsMap>(logger: unknown) => InterfaceEventEmitter<EventsMap> = (
|
|
82
83
|
Ably.Realtime as unknown as { EventEmitter: new <EventsMap>(logger: unknown) => InterfaceEventEmitter<EventsMap> }
|
|
83
84
|
).EventEmitter;
|
package/src/index.ts
CHANGED
|
@@ -1,87 +1,98 @@
|
|
|
1
1
|
// Core transport
|
|
2
2
|
export type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
ActiveRun,
|
|
4
|
+
AgentSession,
|
|
5
|
+
AgentSessionOptions,
|
|
6
|
+
BranchSelection,
|
|
7
7
|
CancelRequest,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
ClientSession,
|
|
9
|
+
ClientSessionOptions,
|
|
10
|
+
ConversationNode,
|
|
11
11
|
EventsNode,
|
|
12
|
+
InputNode,
|
|
13
|
+
InvocationData,
|
|
14
|
+
LoadConversationOptions,
|
|
12
15
|
MessageNode,
|
|
13
|
-
|
|
16
|
+
OutputEvent,
|
|
17
|
+
PipeOptions,
|
|
18
|
+
Run,
|
|
19
|
+
RunEndReason,
|
|
20
|
+
RunInfo,
|
|
21
|
+
RunLifecycleEvent,
|
|
22
|
+
RunNode,
|
|
23
|
+
RunRuntime,
|
|
24
|
+
RunView,
|
|
14
25
|
SendOptions,
|
|
15
|
-
ServerTransport,
|
|
16
|
-
ServerTransportOptions,
|
|
17
|
-
StreamResponseOptions,
|
|
18
26
|
StreamResult,
|
|
19
27
|
Tree,
|
|
20
|
-
Turn,
|
|
21
|
-
TurnEndReason,
|
|
22
|
-
TurnLifecycleEvent,
|
|
23
28
|
View,
|
|
24
29
|
} 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';
|
|
31
|
-
export { buildTransportHeaders, createClientTransport, createServerTransport } from './core/transport/index.js';
|
|
30
|
+
export { buildTransportHeaders, createAgentSession, createClientSession, Invocation } from './core/transport/index.js';
|
|
32
31
|
|
|
33
32
|
// Core codec
|
|
34
33
|
export type {
|
|
35
34
|
ChannelWriter,
|
|
36
35
|
Codec,
|
|
36
|
+
CodecInputEvent,
|
|
37
|
+
CodecMessage,
|
|
38
|
+
CodecOutputEvent,
|
|
39
|
+
DecodedMessage,
|
|
40
|
+
Decoder,
|
|
37
41
|
DecoderCore,
|
|
38
42
|
DecoderCoreHooks,
|
|
39
43
|
DecoderCoreOptions,
|
|
40
|
-
|
|
41
|
-
DiscreteEncoder,
|
|
44
|
+
Encoder,
|
|
42
45
|
EncoderCore,
|
|
43
46
|
EncoderCoreOptions,
|
|
44
47
|
EncoderOptions,
|
|
45
48
|
Extras,
|
|
46
49
|
LifecycleTracker,
|
|
47
|
-
MessageAccumulator,
|
|
48
50
|
MessagePayload,
|
|
49
51
|
PhaseConfig,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
Reducer,
|
|
53
|
+
ReducerMeta,
|
|
54
|
+
Regenerate,
|
|
52
55
|
StreamPayload,
|
|
53
56
|
StreamTrackerState,
|
|
57
|
+
ToolApprovalResponse,
|
|
58
|
+
ToolResult,
|
|
59
|
+
ToolResultError,
|
|
60
|
+
UserMessage,
|
|
54
61
|
WriteOptions,
|
|
55
62
|
} from './core/codec/index.js';
|
|
56
|
-
export { createDecoderCore, createEncoderCore, createLifecycleTracker
|
|
63
|
+
export { createDecoderCore, createEncoderCore, createLifecycleTracker } from './core/codec/index.js';
|
|
57
64
|
|
|
58
65
|
// Constants
|
|
59
66
|
export {
|
|
60
|
-
DOMAIN_HEADER_PREFIX,
|
|
61
|
-
EVENT_ABORT,
|
|
62
67
|
EVENT_CANCEL,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
HEADER_CANCEL_OWN,
|
|
69
|
-
HEADER_CANCEL_TURN_ID,
|
|
68
|
+
EVENT_RUN_END,
|
|
69
|
+
EVENT_RUN_START,
|
|
70
|
+
HEADER_CODEC_MESSAGE_ID,
|
|
71
|
+
HEADER_ERROR_CODE,
|
|
72
|
+
HEADER_ERROR_MESSAGE,
|
|
70
73
|
HEADER_FORK_OF,
|
|
71
|
-
|
|
74
|
+
HEADER_INPUT_CLIENT_ID,
|
|
75
|
+
HEADER_MSG_REGENERATE,
|
|
72
76
|
HEADER_PARENT,
|
|
73
77
|
HEADER_ROLE,
|
|
78
|
+
HEADER_RUN_CLIENT_ID,
|
|
79
|
+
HEADER_RUN_ID,
|
|
80
|
+
HEADER_RUN_REASON,
|
|
74
81
|
HEADER_STATUS,
|
|
75
82
|
HEADER_STREAM,
|
|
76
83
|
HEADER_STREAM_ID,
|
|
77
|
-
HEADER_TURN_CLIENT_ID,
|
|
78
|
-
HEADER_TURN_ID,
|
|
79
|
-
HEADER_TURN_REASON,
|
|
80
84
|
} from './constants.js';
|
|
81
85
|
|
|
82
86
|
// Utilities
|
|
83
87
|
export type { DomainHeaderReader, DomainHeaderWriter, Stripped } from './utils.js';
|
|
84
|
-
export {
|
|
88
|
+
export {
|
|
89
|
+
getCodecHeaders,
|
|
90
|
+
getTransportHeaders,
|
|
91
|
+
headerReader,
|
|
92
|
+
headerWriter,
|
|
93
|
+
mergeHeaders,
|
|
94
|
+
stripUndefined,
|
|
95
|
+
} from './utils.js';
|
|
85
96
|
|
|
86
97
|
// Event emitter
|
|
87
98
|
export { EventEmitter } from './event-emitter.js';
|
package/src/logger.ts
CHANGED
|
@@ -143,10 +143,22 @@ export const consoleLogger = (message: string, level: LogLevel, context?: LogCon
|
|
|
143
143
|
* Options for creating a logger.
|
|
144
144
|
*/
|
|
145
145
|
export interface LoggerOptions {
|
|
146
|
+
/**
|
|
147
|
+
* The handler that receives formatted log messages. Defaults to {@link consoleLogger} when omitted.
|
|
148
|
+
*/
|
|
146
149
|
logHandler?: LogHandler;
|
|
150
|
+
/**
|
|
151
|
+
* The minimum level to emit; messages below this level are suppressed. Must be a valid {@link LogLevel}, otherwise logger creation throws.
|
|
152
|
+
*/
|
|
147
153
|
logLevel: LogLevel;
|
|
148
154
|
}
|
|
149
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Creates a {@link Logger} from the given options.
|
|
158
|
+
* @param options The handler and minimum level for the logger.
|
|
159
|
+
* @returns A logger that filters by level and delegates to the handler.
|
|
160
|
+
* @throws {@link Ably.ErrorInfo} with {@link ErrorCode.InvalidArgument} if `options.logLevel` is not a recognised {@link LogLevel}.
|
|
161
|
+
*/
|
|
150
162
|
export const makeLogger = (options: LoggerOptions): Logger => {
|
|
151
163
|
const logHandler = options.logHandler ?? consoleLogger;
|
|
152
164
|
|
|
@@ -218,7 +230,8 @@ class DefaultLogger implements Logger {
|
|
|
218
230
|
}
|
|
219
231
|
|
|
220
232
|
withContext(context: LogContext): Logger {
|
|
221
|
-
// Get the original log level by finding the key in logLevelNumberMap that matches this._levelNumber
|
|
233
|
+
// Get the original log level by finding the key in logLevelNumberMap that matches this._levelNumber.
|
|
234
|
+
// The Error fallback is defensive and unreachable in practice: _levelNumber always originates from the map.
|
|
222
235
|
const originalLevel =
|
|
223
236
|
[...logLevelNumberMap.entries()].find(([, value]) => value === this._levelNumber)?.[0] ?? LogLevel.Error;
|
|
224
237
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type * as Ably from 'ably';
|
|
2
|
+
import { createContext } from 'react';
|
|
3
|
+
|
|
4
|
+
import type { CodecInputEvent, CodecOutputEvent } from '../../core/codec/types.js';
|
|
5
|
+
import type { ClientSession } from '../../core/transport/types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A single entry in the client-session registry, holding the session and any
|
|
9
|
+
* error that occurred during its construction.
|
|
10
|
+
*
|
|
11
|
+
* `session` is `undefined` when construction failed.
|
|
12
|
+
* `sessionError` is set when `createClientSession` threw during provider render.
|
|
13
|
+
*/
|
|
14
|
+
export interface ClientSessionSlot {
|
|
15
|
+
/** The constructed session, or `undefined` if construction failed. */
|
|
16
|
+
session: ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown> | undefined;
|
|
17
|
+
/** Construction error from `createClientSession`, or `undefined` on success. */
|
|
18
|
+
sessionError?: Ably.ErrorInfo | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The shape of the {@link ClientSessionContext} value.
|
|
23
|
+
*
|
|
24
|
+
* `nearest` is the slot from the innermost enclosing {@link ClientSessionProvider}.
|
|
25
|
+
* `providers` is the full registry of all enclosing providers, keyed by channelName.
|
|
26
|
+
*/
|
|
27
|
+
export interface ClientSessionContextValue {
|
|
28
|
+
/** The innermost {@link ClientSessionProvider}'s slot. `undefined` when no provider is present. */
|
|
29
|
+
nearest: ClientSessionSlot | undefined;
|
|
30
|
+
/** All registered session slots from enclosing providers, keyed by channelName. */
|
|
31
|
+
providers: Readonly<Record<string, ClientSessionSlot>>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Unified client-session context.
|
|
36
|
+
*
|
|
37
|
+
* Holds the nearest client-session slot and the full registry of all registered
|
|
38
|
+
* slots keyed by channelName. Populated by {@link ClientSessionProvider};
|
|
39
|
+
* read by {@link useClientSession} and internal hooks.
|
|
40
|
+
*/
|
|
41
|
+
export const ClientSessionContext = createContext<ClientSessionContextValue>({ nearest: undefined, providers: {} });
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClientSessionProvider: creates a ClientSession and makes it available to
|
|
3
|
+
* descendants via ClientSessionContext.
|
|
4
|
+
*
|
|
5
|
+
* Reads the Ably Realtime client from the surrounding `<AblyProvider>` and
|
|
6
|
+
* forwards it to `createClientSession` along with the supplied `channelName`.
|
|
7
|
+
*
|
|
8
|
+
* The session is created on first render (via useRef) and recreated when
|
|
9
|
+
* `channelName` changes; the previous session is queued for disposal.
|
|
10
|
+
* `connect()` is invoked from a `useEffect` so the session is
|
|
11
|
+
* subscribed/attached before the first descendant operation. If
|
|
12
|
+
* `createClientSession` throws,
|
|
13
|
+
* the error is stored in the ClientSessionSlot (alongside an undefined
|
|
14
|
+
* session) so that useClientSession can surface it as `sessionError`
|
|
15
|
+
* without crashing the component tree.
|
|
16
|
+
*
|
|
17
|
+
* The session is closed when the provider truly unmounts. The close is
|
|
18
|
+
* scheduled as a microtask so that React Strict Mode's synchronous
|
|
19
|
+
* remount cycle (mount → fake-unmount → remount) can cancel it before it
|
|
20
|
+
* fires, avoiding unnecessary session teardown in development.
|
|
21
|
+
*
|
|
22
|
+
* Multiple ClientSessionProviders can be nested using distinct channelNames.
|
|
23
|
+
* Each provider merges its slot into the parent record so descendants
|
|
24
|
+
* can access all registered sessions via useClientSession(channelName).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as Ably from 'ably';
|
|
28
|
+
import { useAbly } from 'ably/react';
|
|
29
|
+
import { type PropsWithChildren, type ReactNode, useContext, useEffect, useMemo, useRef } from 'react';
|
|
30
|
+
|
|
31
|
+
import type { CodecInputEvent, CodecOutputEvent } from '../../core/codec/types.js';
|
|
32
|
+
import { createClientSession } from '../../core/transport/client-session.js';
|
|
33
|
+
import type { ClientSession, ClientSessionOptions } from '../../core/transport/types.js';
|
|
34
|
+
import { ErrorCode } from '../../errors.js';
|
|
35
|
+
import type { ClientSessionSlot } from './client-session-context.js';
|
|
36
|
+
import { ClientSessionContext } from './client-session-context.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Props for {@link ClientSessionProvider}.
|
|
40
|
+
*
|
|
41
|
+
* All {@link ClientSessionOptions} except `client` (read from the surrounding
|
|
42
|
+
* `<AblyProvider>`).
|
|
43
|
+
*/
|
|
44
|
+
export interface ClientSessionProviderProps<
|
|
45
|
+
TInput extends CodecInputEvent,
|
|
46
|
+
TOutput extends CodecOutputEvent,
|
|
47
|
+
TProjection,
|
|
48
|
+
TMessage,
|
|
49
|
+
>
|
|
50
|
+
extends Omit<ClientSessionOptions<TInput, TOutput, TProjection, TMessage>, 'client'>, PropsWithChildren {}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Provide a {@link ClientSession} to descendant components.
|
|
54
|
+
*
|
|
55
|
+
* Reads the Ably Realtime client from the surrounding `<AblyProvider>`,
|
|
56
|
+
* creates a session bound to `channelName`, calls `connect()` on mount,
|
|
57
|
+
* and registers it in `ClientSessionContext` under `channelName`.
|
|
58
|
+
* Descendants call {@link useClientSession} with the same `channelName` to
|
|
59
|
+
* access the session.
|
|
60
|
+
*
|
|
61
|
+
* If `createClientSession` throws during construction, the error is surfaced
|
|
62
|
+
* through `useClientSession` as `sessionError` — the component tree does not
|
|
63
|
+
* crash and children are still rendered.
|
|
64
|
+
*
|
|
65
|
+
* ```tsx
|
|
66
|
+
* <AblyProvider client={ably}>
|
|
67
|
+
* <ClientSessionProvider channelName="ai:demo" codec={UIMessageCodec}>
|
|
68
|
+
* <Chat />
|
|
69
|
+
* </ClientSessionProvider>
|
|
70
|
+
* </AblyProvider>
|
|
71
|
+
*
|
|
72
|
+
* // Inside Chat:
|
|
73
|
+
* const { session, sessionError } = useClientSession({ channelName: 'ai:demo' });
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* For multiple sessions, nest providers with distinct channelNames:
|
|
77
|
+
*
|
|
78
|
+
* ```tsx
|
|
79
|
+
* <ClientSessionProvider channelName="ai:main" codec={UIMessageCodec}>
|
|
80
|
+
* <ClientSessionProvider channelName="ai:aux" codec={UIMessageCodec}>
|
|
81
|
+
* <App />
|
|
82
|
+
* </ClientSessionProvider>
|
|
83
|
+
* </ClientSessionProvider>
|
|
84
|
+
*
|
|
85
|
+
* // Inside App:
|
|
86
|
+
* const { session: main } = useClientSession({ channelName: 'ai:main' });
|
|
87
|
+
* const { session: aux } = useClientSession({ channelName: 'ai:aux' });
|
|
88
|
+
* ```
|
|
89
|
+
* @param props - Provider configuration including `channelName`, `codec`, and all other {@link ClientSessionOptions} except `client`.
|
|
90
|
+
* @param props.children - Descendant components that consume the session via {@link useClientSession}.
|
|
91
|
+
* @returns A React element wrapping children with ClientSessionContext.
|
|
92
|
+
*/
|
|
93
|
+
export const ClientSessionProvider = <
|
|
94
|
+
TInput extends CodecInputEvent,
|
|
95
|
+
TOutput extends CodecOutputEvent,
|
|
96
|
+
TProjection,
|
|
97
|
+
TMessage,
|
|
98
|
+
>({
|
|
99
|
+
children,
|
|
100
|
+
...sessionOptions
|
|
101
|
+
}: ClientSessionProviderProps<TInput, TOutput, TProjection, TMessage>): ReactNode => {
|
|
102
|
+
const client = useAbly();
|
|
103
|
+
const { channelName } = sessionOptions;
|
|
104
|
+
const sessionRef = useRef<ClientSession<TInput, TOutput, TProjection, TMessage> | undefined>(undefined);
|
|
105
|
+
const sessionChannelRef = useRef<string>(channelName);
|
|
106
|
+
const sessionsToDisposeRef = useRef<ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown>[]>([]);
|
|
107
|
+
const pendingCloseRef = useRef(false);
|
|
108
|
+
const constructionErrorRef = useRef<Ably.ErrorInfo | undefined>(undefined);
|
|
109
|
+
|
|
110
|
+
const alreadyCreatedOrFailed = !!sessionRef.current || !!constructionErrorRef.current;
|
|
111
|
+
|
|
112
|
+
if (!alreadyCreatedOrFailed || sessionChannelRef.current !== channelName) {
|
|
113
|
+
sessionChannelRef.current = channelName;
|
|
114
|
+
if (sessionRef.current) sessionsToDisposeRef.current.push(sessionRef.current);
|
|
115
|
+
try {
|
|
116
|
+
sessionRef.current = createClientSession({ ...sessionOptions, client });
|
|
117
|
+
constructionErrorRef.current = undefined;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
sessionRef.current = undefined;
|
|
120
|
+
constructionErrorRef.current =
|
|
121
|
+
error instanceof Ably.ErrorInfo
|
|
122
|
+
? error
|
|
123
|
+
: new Ably.ErrorInfo('Unknown error while creating client session', ErrorCode.BadRequest, 400);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const parentContext = useContext(ClientSessionContext);
|
|
128
|
+
|
|
129
|
+
// Capture ref values as locals so useMemo deps track changes correctly.
|
|
130
|
+
// CAST: ClientSessionContext stores sessions with erased generics.
|
|
131
|
+
// The generic types are fixed at the ClientSessionProvider<TInput, TOutput, TProjection, TMessage> boundary.
|
|
132
|
+
const currentSession = sessionRef.current as
|
|
133
|
+
| ClientSession<CodecInputEvent, CodecOutputEvent, unknown, unknown>
|
|
134
|
+
| undefined;
|
|
135
|
+
const currentError = constructionErrorRef.current;
|
|
136
|
+
|
|
137
|
+
const slot = useMemo<ClientSessionSlot>(
|
|
138
|
+
() => ({ session: currentSession, sessionError: currentError }),
|
|
139
|
+
[currentSession, currentError],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const contextValue = useMemo(
|
|
143
|
+
() => ({ nearest: slot, providers: { ...parentContext.providers, [channelName]: slot } }),
|
|
144
|
+
[channelName, parentContext, slot],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Dispose sessions superseded by a channelName change. When channelName
|
|
148
|
+
// changes, the render path above pushes the now-stale session into
|
|
149
|
+
// sessionsToDisposeRef and creates a replacement. This effect's cleanup —
|
|
150
|
+
// which runs on the next channelName change or on unmount — closes every
|
|
151
|
+
// queued session.
|
|
152
|
+
useEffect(
|
|
153
|
+
() => () => {
|
|
154
|
+
for (const session of sessionsToDisposeRef.current) void session.close();
|
|
155
|
+
},
|
|
156
|
+
[channelName],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Trigger connect() once the session is created. Re-runs when channelName
|
|
160
|
+
// changes so the freshly-recreated session connects too. Any error is
|
|
161
|
+
// stored on the session's emitter and surfaced via on('error');
|
|
162
|
+
// useClientSession doesn't need to await this.
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
void sessionRef.current?.connect();
|
|
165
|
+
}, [channelName]);
|
|
166
|
+
|
|
167
|
+
// Close the session when the component truly unmounts. The close is
|
|
168
|
+
// scheduled as a microtask: in React Strict Mode (dev) the component
|
|
169
|
+
// remounts synchronously before any microtask can drain, so the remount's
|
|
170
|
+
// effect setup resets pendingCloseRef.current = false and cancels the
|
|
171
|
+
// close. On a real unmount no remount follows, the microtask fires, and
|
|
172
|
+
// the session is closed.
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
pendingCloseRef.current = false;
|
|
175
|
+
return () => {
|
|
176
|
+
pendingCloseRef.current = true;
|
|
177
|
+
void Promise.resolve().then(() => {
|
|
178
|
+
if (pendingCloseRef.current) {
|
|
179
|
+
void sessionRef.current?.close();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
}, []);
|
|
184
|
+
|
|
185
|
+
return <ClientSessionContext.Provider value={contextValue}>{children}</ClientSessionContext.Provider>;
|
|
186
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createSessionHooks: factory that captures the codec's type parameters once and
|
|
3
|
+
* returns a bundle of type-safe hooks + ClientSessionProvider. Hook call sites need
|
|
4
|
+
* no type parameters at every use — just call the hooks directly.
|
|
5
|
+
* @example
|
|
6
|
+
* // Once per app (e.g. in a shared session.ts):
|
|
7
|
+
* export const {
|
|
8
|
+
* ClientSessionProvider,
|
|
9
|
+
* useClientSession,
|
|
10
|
+
* useView,
|
|
11
|
+
* } = createSessionHooks<VercelInput, VercelOutput, VercelProjection, UIMessage>();
|
|
12
|
+
*
|
|
13
|
+
* // In page:
|
|
14
|
+
* <ClientSessionProvider channelName="ai:demo" codec={UIMessageCodec}>
|
|
15
|
+
* <Chat />
|
|
16
|
+
* </ClientSessionProvider>
|
|
17
|
+
*
|
|
18
|
+
* // In Chat — no type params needed, session is implicit from nearest provider:
|
|
19
|
+
* const { nodes } = useView({ limit: 30 });
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type * as Ably from 'ably';
|
|
23
|
+
import type { ComponentType } from 'react';
|
|
24
|
+
|
|
25
|
+
import type { CodecInputEvent, CodecOutputEvent } from '../core/codec/types.js';
|
|
26
|
+
import type { ClientSession, View } from '../core/transport/types.js';
|
|
27
|
+
import type { ClientSessionProviderProps } from './contexts/client-session-provider.js';
|
|
28
|
+
import { ClientSessionProvider as _ClientSessionProvider } from './contexts/client-session-provider.js';
|
|
29
|
+
import { useAblyMessages as _useAblyMessages } from './use-ably-messages.js';
|
|
30
|
+
import type { ClientSessionHandle } from './use-client-session.js';
|
|
31
|
+
import { useClientSession as _useClientSession } from './use-client-session.js';
|
|
32
|
+
import { useCreateView as _useCreateView } from './use-create-view.js';
|
|
33
|
+
import type { TreeHandle } from './use-tree.js';
|
|
34
|
+
import { useTree as _useTree } from './use-tree.js';
|
|
35
|
+
import type { ViewHandle } from './use-view.js';
|
|
36
|
+
import { useView as _useView } from './use-view.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Bundle of type-safe hooks and provider returned by {@link createSessionHooks}.
|
|
40
|
+
*
|
|
41
|
+
* The codec's `TInput`, `TOutput`, `TProjection`, and `TMessage` are baked in at
|
|
42
|
+
* factory creation time so no type params are needed at hook call sites.
|
|
43
|
+
*/
|
|
44
|
+
export interface SessionHooks<TInput extends CodecInputEvent, TOutput extends CodecOutputEvent, TProjection, TMessage> {
|
|
45
|
+
/**
|
|
46
|
+
* `ClientSessionProvider` narrowed to the codec's `TInput`/`TOutput`/`TMessage`. No JSX type params needed.
|
|
47
|
+
*/
|
|
48
|
+
ClientSessionProvider: ComponentType<ClientSessionProviderProps<TInput, TOutput, TProjection, TMessage>>;
|
|
49
|
+
/**
|
|
50
|
+
* Read the session from context. No type params needed.
|
|
51
|
+
*
|
|
52
|
+
* Returns `{ session, sessionError }`. When no provider is found (or session
|
|
53
|
+
* construction failed), `sessionError` is set and `session` is a stub that
|
|
54
|
+
* throws on access — the hook never throws during render.
|
|
55
|
+
*
|
|
56
|
+
* Pass `onError` to subscribe to post-construction session errors
|
|
57
|
+
* (e.g. send failures, channel continuity loss) without wiring
|
|
58
|
+
* `session.on('error', …)` manually.
|
|
59
|
+
*/
|
|
60
|
+
useClientSession: (props?: {
|
|
61
|
+
/** Channel name to look up; omit to use the nearest {@link ClientSessionProvider}. */
|
|
62
|
+
channelName?: string;
|
|
63
|
+
/** When `true`, return a stub session that throws on any access. */
|
|
64
|
+
skip?: boolean;
|
|
65
|
+
/** Called whenever the resolved session emits an error event. */
|
|
66
|
+
onError?: (error: Ably.ErrorInfo) => void;
|
|
67
|
+
}) => ClientSessionHandle<TInput, TOutput, TProjection, TMessage>;
|
|
68
|
+
/**
|
|
69
|
+
* Subscribe to the nearest session's view and return the visible message list with pagination.
|
|
70
|
+
* Pass `session` to use a session's default view, `view` to subscribe to a specific view
|
|
71
|
+
* directly. Pass `limit` to auto-load on mount. Pass `skip: true` for an empty handle.
|
|
72
|
+
*/
|
|
73
|
+
useView: (props?: {
|
|
74
|
+
/** Client session whose default view to subscribe to; defaults to the nearest {@link ClientSessionProvider}. */
|
|
75
|
+
session?: ClientSession<TInput, TOutput, TProjection, TMessage> | null;
|
|
76
|
+
/** A specific {@link View} to subscribe to directly. Takes priority over `session`. */
|
|
77
|
+
view?: View<TInput, TMessage> | null;
|
|
78
|
+
/** When provided, auto-loads the first page on mount. */
|
|
79
|
+
limit?: number;
|
|
80
|
+
/** When `true`, skip all subscriptions and return an empty handle. */
|
|
81
|
+
skip?: boolean;
|
|
82
|
+
}) => ViewHandle<TInput, TMessage>;
|
|
83
|
+
/**
|
|
84
|
+
* Navigate conversation branches in the session tree.
|
|
85
|
+
* Pass `session` to override; defaults to the nearest {@link ClientSessionProvider}.
|
|
86
|
+
*/
|
|
87
|
+
useTree: (props?: {
|
|
88
|
+
/** Override session; defaults to the nearest {@link ClientSessionProvider}. */
|
|
89
|
+
session?: ClientSession<TInput, TOutput, TProjection, TMessage>;
|
|
90
|
+
}) => TreeHandle<TProjection>;
|
|
91
|
+
/**
|
|
92
|
+
* Subscribe to raw Ably messages on the session channel.
|
|
93
|
+
* Pass `session` to override; defaults to the nearest {@link ClientSessionProvider}.
|
|
94
|
+
* Pass `skip: true` to return an empty array without subscribing.
|
|
95
|
+
*/
|
|
96
|
+
useAblyMessages: (props?: {
|
|
97
|
+
/** Override session; defaults to the nearest {@link ClientSessionProvider}. */
|
|
98
|
+
session?: ClientSession<TInput, TOutput, TProjection, TMessage>;
|
|
99
|
+
/** When `true`, skip all subscriptions and return an empty array. */
|
|
100
|
+
skip?: boolean;
|
|
101
|
+
}) => Ably.InboundMessage[];
|
|
102
|
+
/**
|
|
103
|
+
* Create an independent view over the same tree.
|
|
104
|
+
* Pass `session` to override; defaults to the nearest {@link ClientSessionProvider}.
|
|
105
|
+
* Pass `skip: true` to return an empty handle without creating a view.
|
|
106
|
+
*/
|
|
107
|
+
useCreateView: (props?: {
|
|
108
|
+
/** Override session; defaults to the nearest {@link ClientSessionProvider}. */
|
|
109
|
+
session?: ClientSession<TInput, TOutput, TProjection, TMessage> | null;
|
|
110
|
+
/** When provided, auto-loads the first page on mount. */
|
|
111
|
+
limit?: number;
|
|
112
|
+
/** When `true`, skip view creation and return an empty handle. */
|
|
113
|
+
skip?: boolean;
|
|
114
|
+
}) => ViewHandle<TInput, TMessage>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a bundle of type-safe hooks and provider for a given codec's
|
|
119
|
+
* `TInput`/`TOutput`/`TProjection`/`TMessage`.
|
|
120
|
+
*
|
|
121
|
+
* These type parameters are captured at factory creation time; hook call sites need
|
|
122
|
+
* no type parameters. The returned hooks are thin wrappers around the standalone hooks
|
|
123
|
+
* with the types resolved.
|
|
124
|
+
* @returns A {@link SessionHooks} bundle.
|
|
125
|
+
*/
|
|
126
|
+
export const createSessionHooks = <
|
|
127
|
+
TInput extends CodecInputEvent,
|
|
128
|
+
TOutput extends CodecOutputEvent,
|
|
129
|
+
TProjection,
|
|
130
|
+
TMessage,
|
|
131
|
+
>(): SessionHooks<TInput, TOutput, TProjection, TMessage> => ({
|
|
132
|
+
// CAST: ClientSessionProvider is generic; factory narrows it to the codec's TInput/TOutput/TProjection/TMessage.
|
|
133
|
+
ClientSessionProvider: _ClientSessionProvider as ComponentType<
|
|
134
|
+
ClientSessionProviderProps<TInput, TOutput, TProjection, TMessage>
|
|
135
|
+
>,
|
|
136
|
+
useClientSession: (props) => _useClientSession<TInput, TOutput, TProjection, TMessage>(props ?? {}),
|
|
137
|
+
useView: (props) => _useView<TInput, TOutput, TProjection, TMessage>(props ?? {}),
|
|
138
|
+
useTree: (props) => _useTree<TInput, TOutput, TProjection, TMessage>(props ?? {}),
|
|
139
|
+
useAblyMessages: (props) => _useAblyMessages<TInput, TOutput, TProjection, TMessage>(props ?? {}),
|
|
140
|
+
useCreateView: (props) => _useCreateView<TInput, TOutput, TProjection, TMessage>(props ?? {}),
|
|
141
|
+
});
|
package/src/react/index.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
export type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
export type { CodecMessage } from '../core/codec/types.js';
|
|
2
|
+
export type {
|
|
3
|
+
ActiveRun,
|
|
4
|
+
BranchSelection,
|
|
5
|
+
ClientSession,
|
|
6
|
+
ConversationNode,
|
|
7
|
+
EventsNode,
|
|
8
|
+
InputNode,
|
|
9
|
+
MessageNode,
|
|
10
|
+
RunInfo,
|
|
11
|
+
RunNode,
|
|
12
|
+
SendOptions,
|
|
13
|
+
} from '../core/transport/types.js';
|
|
14
|
+
export type { ClientSessionSlot } from './contexts/client-session-context.js';
|
|
15
|
+
export type { ClientSessionProviderProps } from './contexts/client-session-provider.js';
|
|
16
|
+
export { ClientSessionProvider } from './contexts/client-session-provider.js';
|
|
17
|
+
export type { SessionHooks } from './create-session-hooks.js';
|
|
18
|
+
export { createSessionHooks } from './create-session-hooks.js';
|
|
19
|
+
export type { UseAblyMessagesOptions } from './use-ably-messages.js';
|
|
10
20
|
export { useAblyMessages } from './use-ably-messages.js';
|
|
11
|
-
export {
|
|
12
|
-
export
|
|
13
|
-
export {
|
|
21
|
+
export type { ClientSessionHandle } from './use-client-session.js';
|
|
22
|
+
export { useClientSession } from './use-client-session.js';
|
|
23
|
+
export type { UseCreateViewOptions } from './use-create-view.js';
|
|
14
24
|
export { useCreateView } from './use-create-view.js';
|
|
15
|
-
export type { TreeHandle } from './use-tree.js';
|
|
25
|
+
export type { TreeHandle, UseTreeOptions } from './use-tree.js';
|
|
16
26
|
export { useTree } from './use-tree.js';
|
|
17
27
|
export type { UseViewOptions, ViewHandle } from './use-view.js';
|
|
18
28
|
export { useView } from './use-view.js';
|