@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
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* decodeHistory — load conversation history from an Ably channel and
|
|
3
|
-
* return decoded messages as a paginated HistoryPage result.
|
|
4
|
-
*
|
|
5
|
-
* Uses a fresh decoder (not shared with the live subscription) to avoid
|
|
6
|
-
* state conflicts. Per-turn accumulators handle interleaved turns correctly.
|
|
7
|
-
*
|
|
8
|
-
* The `limit` option controls the number of **messages** returned,
|
|
9
|
-
* not the number of Ably wire messages fetched. The implementation pages
|
|
10
|
-
* back through Ably history until `limit` complete messages have
|
|
11
|
-
* been assembled. Partial turns (incomplete at the page boundary) are
|
|
12
|
-
* buffered internally and completed when `next()` fetches more pages.
|
|
13
|
-
*
|
|
14
|
-
* Only completed messages appear in `items`. A message is complete when
|
|
15
|
-
* its terminal event (finish/abort/error) has been received.
|
|
16
|
-
*
|
|
17
|
-
* Because Ably history returns newest-first while the decoder requires
|
|
18
|
-
* chronological order, all collected Ably messages are re-decoded from
|
|
19
|
-
* oldest to newest at the point a result is built. This handles turns
|
|
20
|
-
* that span page boundaries correctly. The fetch loop uses a cheap
|
|
21
|
-
* header-based completion counter to decide when to stop paging, so the
|
|
22
|
-
* full decode runs exactly once per traversal regardless of page count.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import type * as Ably from 'ably';
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
HEADER_AMEND,
|
|
29
|
-
HEADER_DISCRETE,
|
|
30
|
-
HEADER_MSG_ID,
|
|
31
|
-
HEADER_STATUS,
|
|
32
|
-
HEADER_STREAM,
|
|
33
|
-
HEADER_TURN_ID,
|
|
34
|
-
} from '../../constants.js';
|
|
35
|
-
import type { Logger } from '../../logger.js';
|
|
36
|
-
import { getHeaders } from '../../utils.js';
|
|
37
|
-
import type { Codec, DecoderOutput, MessageAccumulator } from '../codec/types.js';
|
|
38
|
-
import type { HistoryPage, LoadHistoryOptions } from './types.js';
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Shared state across pages within one history traversal
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
interface HistoryState<TEvent, TMessage> {
|
|
45
|
-
codec: Codec<TEvent, TMessage>;
|
|
46
|
-
/** All raw Ably messages collected so far, in newest-first order (as received from Ably). */
|
|
47
|
-
rawMessages: Ably.InboundMessage[];
|
|
48
|
-
/** How many completed messages have been returned to the consumer so far. */
|
|
49
|
-
returnedCount: number;
|
|
50
|
-
/** How many raw Ably messages have been returned to the consumer so far. */
|
|
51
|
-
returnedRawCount: number;
|
|
52
|
-
/** The last Ably page cursor for continued pagination. */
|
|
53
|
-
lastAblyPage: Ably.PaginatedResult<Ably.InboundMessage> | undefined;
|
|
54
|
-
/**
|
|
55
|
-
* Cached result of the last {@link decodeAll} call, reused while
|
|
56
|
-
* `rawMessages` is unchanged. Invalidated implicitly by comparing
|
|
57
|
-
* {@link cachedAtRawLength} against `rawMessages.length`; `rawMessages`
|
|
58
|
-
* is append-only within a traversal so length is a sufficient key.
|
|
59
|
-
*/
|
|
60
|
-
cachedDecode: DecodedItem<TMessage>[] | undefined;
|
|
61
|
-
/** `rawMessages.length` at the time {@link cachedDecode} was produced. */
|
|
62
|
-
cachedAtRawLength: number;
|
|
63
|
-
/**
|
|
64
|
-
* `x-ably-msg-id`s for which the decoder has something to produce output
|
|
65
|
-
* from: any `message.create` / `message.update` / `message.append` with
|
|
66
|
-
* `x-ably-stream: "true"` (establishes a tracker via create or
|
|
67
|
-
* first-contact), or a `message.create` carrying `x-ably-discrete` (a
|
|
68
|
-
* discrete message, created and terminated in one wire message).
|
|
69
|
-
*/
|
|
70
|
-
startedMsgIds: Set<string>;
|
|
71
|
-
/**
|
|
72
|
-
* `x-ably-msg-id`s with a terminal wire signal: either `x-ably-discrete`
|
|
73
|
-
* on a `message.create` (discrete message) or `x-ably-status: "finished"`
|
|
74
|
-
* / `"aborted"` on any action (closed stream).
|
|
75
|
-
*/
|
|
76
|
-
terminatedMsgIds: Set<string>;
|
|
77
|
-
/**
|
|
78
|
-
* `x-ably-msg-id`s that are both started AND terminated - ready to appear
|
|
79
|
-
* in the decoded output. The fetch loop reads this set's size to decide
|
|
80
|
-
* when to stop paging, avoiding a full decode per page. Maintained
|
|
81
|
-
* incrementally by {@link countNewCompletions}. Grows monotonically.
|
|
82
|
-
*/
|
|
83
|
-
completedMsgIds: Set<string>;
|
|
84
|
-
logger: Logger;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** A completed message paired with its canonical wire headers and serial. */
|
|
88
|
-
interface DecodedItem<TMessage> {
|
|
89
|
-
message: TMessage;
|
|
90
|
-
headers: Record<string, string>;
|
|
91
|
-
/** Ably serial from the first Ably message for this domain message. */
|
|
92
|
-
serial: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Decode all collected messages from scratch (chronological order)
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Re-decode all collected raw messages into completed domain messages.
|
|
101
|
-
* @param state - The shared history traversal state.
|
|
102
|
-
* @returns Completed messages in newest-first order.
|
|
103
|
-
*/
|
|
104
|
-
const decodeAll = <TEvent, TMessage>(state: HistoryState<TEvent, TMessage>): DecodedItem<TMessage>[] => {
|
|
105
|
-
// Reverse to chronological (oldest first)
|
|
106
|
-
const chronological = [...state.rawMessages].toReversed();
|
|
107
|
-
|
|
108
|
-
// Fresh decoder and per-turn accumulators for each full re-decode.
|
|
109
|
-
const decoder = state.codec.createDecoder();
|
|
110
|
-
const turns = new Map<
|
|
111
|
-
string,
|
|
112
|
-
{
|
|
113
|
-
accumulator: MessageAccumulator<TEvent, TMessage>;
|
|
114
|
-
firstSeen: number;
|
|
115
|
-
/** Headers from the first Ably message per x-ably-msg-id within this turn. */
|
|
116
|
-
msgHeaders: Map<string, Record<string, string>>;
|
|
117
|
-
/** Ably serial from the first Ably message per x-ably-msg-id within this turn. */
|
|
118
|
-
msgSerials: Map<string, string>;
|
|
119
|
-
}
|
|
120
|
-
>();
|
|
121
|
-
const defaultAccumulator = state.codec.createAccumulator();
|
|
122
|
-
let orderCounter = 0;
|
|
123
|
-
|
|
124
|
-
// Headers and serials for non-turn discrete messages, keyed by x-ably-msg-id.
|
|
125
|
-
const discreteHeaders = new Map<string, Record<string, string>>();
|
|
126
|
-
const discreteSerials = new Map<string, string>();
|
|
127
|
-
// Track which msgId produced each non-turn discrete message output (in order).
|
|
128
|
-
const discreteMsgIds: string[] = [];
|
|
129
|
-
|
|
130
|
-
// Cross-turn event targets to complete after all events are processed.
|
|
131
|
-
// Deferred so that finish/abort events that follow the update in serial
|
|
132
|
-
// order can still process on the active message (e.g. applying messageMetadata).
|
|
133
|
-
const deferredCompletions: { accumulator: MessageAccumulator<TEvent, TMessage>; messageId: string }[] = [];
|
|
134
|
-
|
|
135
|
-
for (const msg of chronological) {
|
|
136
|
-
const outputs: DecoderOutput<TEvent, TMessage>[] = decoder.decode(msg);
|
|
137
|
-
const headers = getHeaders(msg);
|
|
138
|
-
const turnId = headers[HEADER_TURN_ID];
|
|
139
|
-
const msgId = headers[HEADER_MSG_ID];
|
|
140
|
-
const serial = msg.serial;
|
|
141
|
-
const amendTarget = headers[HEADER_AMEND];
|
|
142
|
-
|
|
143
|
-
// Cross-turn events target an existing message from a different turn.
|
|
144
|
-
// Route to the owning turn's accumulator via initMessage lifecycle.
|
|
145
|
-
if (amendTarget) {
|
|
146
|
-
for (const turn of turns.values()) {
|
|
147
|
-
if (turn.msgHeaders.has(amendTarget)) {
|
|
148
|
-
const headerKeys = [...turn.msgHeaders.keys()];
|
|
149
|
-
const msgIndex = headerKeys.indexOf(amendTarget);
|
|
150
|
-
const currentMsg = msgIndex === -1 ? undefined : turn.accumulator.messages[msgIndex];
|
|
151
|
-
if (currentMsg) {
|
|
152
|
-
turn.accumulator.initMessage(amendTarget, currentMsg);
|
|
153
|
-
}
|
|
154
|
-
turn.accumulator.processOutputs(outputs);
|
|
155
|
-
deferredCompletions.push({ accumulator: turn.accumulator, messageId: amendTarget });
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (turnId) {
|
|
163
|
-
let turn = turns.get(turnId);
|
|
164
|
-
if (!turn) {
|
|
165
|
-
turn = {
|
|
166
|
-
accumulator: state.codec.createAccumulator(),
|
|
167
|
-
firstSeen: orderCounter++,
|
|
168
|
-
msgHeaders: new Map(),
|
|
169
|
-
msgSerials: new Map(),
|
|
170
|
-
};
|
|
171
|
-
turns.set(turnId, turn);
|
|
172
|
-
}
|
|
173
|
-
// Capture headers per msg-id within this turn. Update on later
|
|
174
|
-
// messages too (e.g. closing append overrides status from
|
|
175
|
-
// "streaming" to "finished"/"aborted"). Only merge when the
|
|
176
|
-
// incoming message has non-empty headers.
|
|
177
|
-
if (msgId) {
|
|
178
|
-
const existing = turn.msgHeaders.get(msgId);
|
|
179
|
-
if (!existing) {
|
|
180
|
-
turn.msgHeaders.set(msgId, { ...headers });
|
|
181
|
-
if (serial) turn.msgSerials.set(msgId, serial);
|
|
182
|
-
} else if (Object.keys(headers).length > 0) {
|
|
183
|
-
Object.assign(existing, headers);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
turn.accumulator.processOutputs(outputs);
|
|
187
|
-
} else {
|
|
188
|
-
defaultAccumulator.processOutputs(outputs);
|
|
189
|
-
|
|
190
|
-
// Capture headers and serial for non-turn discrete messages by x-ably-msg-id.
|
|
191
|
-
for (const output of outputs) {
|
|
192
|
-
if (output.kind === 'message' && msgId) {
|
|
193
|
-
discreteMsgIds.push(msgId);
|
|
194
|
-
const existingDiscrete = discreteHeaders.get(msgId);
|
|
195
|
-
if (!existingDiscrete) {
|
|
196
|
-
discreteHeaders.set(msgId, { ...headers });
|
|
197
|
-
if (serial) discreteSerials.set(msgId, serial);
|
|
198
|
-
} else if (Object.keys(headers).length > 0) {
|
|
199
|
-
Object.assign(existingDiscrete, headers);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Complete any messages that were re-activated for cross-turn updates.
|
|
207
|
-
// Idempotent — if finish already removed the message from active tracking,
|
|
208
|
-
// completeMessage is a no-op.
|
|
209
|
-
for (const { accumulator, messageId } of deferredCompletions) {
|
|
210
|
-
accumulator.completeMessage(messageId);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Collect completed messages in chronological order (oldest first) by turn.
|
|
214
|
-
const completed: DecodedItem<TMessage>[] = [];
|
|
215
|
-
|
|
216
|
-
// Default accumulator messages: pair with their discrete headers by position.
|
|
217
|
-
for (const [i, msg] of defaultAccumulator.completedMessages.entries()) {
|
|
218
|
-
const mid = discreteMsgIds[i];
|
|
219
|
-
completed.push({
|
|
220
|
-
message: msg,
|
|
221
|
-
headers: mid ? (discreteHeaders.get(mid) ?? {}) : {},
|
|
222
|
-
serial: mid ? (discreteSerials.get(mid) ?? '') : '',
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const sorted = [...turns.values()].toSorted((a, b) => a.firstSeen - b.firstSeen);
|
|
227
|
-
for (const turn of sorted) {
|
|
228
|
-
// Assign headers and serials to each completed message in this turn.
|
|
229
|
-
// The turn's msgHeaders map is keyed by x-ably-msg-id and ordered by
|
|
230
|
-
// first-seen. Completed messages are matched positionally.
|
|
231
|
-
const headerEntries = [...turn.msgHeaders.entries()];
|
|
232
|
-
let headerIdx = 0;
|
|
233
|
-
|
|
234
|
-
for (const msg of turn.accumulator.completedMessages) {
|
|
235
|
-
const entry = headerEntries[headerIdx];
|
|
236
|
-
if (entry) {
|
|
237
|
-
const [mid, hdrs] = entry;
|
|
238
|
-
completed.push({
|
|
239
|
-
message: msg,
|
|
240
|
-
headers: hdrs,
|
|
241
|
-
serial: turn.msgSerials.get(mid) ?? '',
|
|
242
|
-
});
|
|
243
|
-
headerIdx++;
|
|
244
|
-
} else {
|
|
245
|
-
completed.push({ message: msg, headers: {}, serial: '' });
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Reverse to newest-first. The consumer slices from the front for the
|
|
251
|
-
// most recent page, and progressively deeper for older pages.
|
|
252
|
-
return completed.toReversed();
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Cached wrapper around {@link decodeAll}. Returns the previous result when
|
|
257
|
-
* `rawMessages` hasn't changed since the last decode; otherwise re-decodes
|
|
258
|
-
* and updates the cache. The cache key is `rawMessages.length` because
|
|
259
|
-
* `rawMessages` is append-only within a traversal.
|
|
260
|
-
* @param state - The shared history traversal state.
|
|
261
|
-
* @returns Completed messages in newest-first order.
|
|
262
|
-
*/
|
|
263
|
-
const decodeAllCached = <TEvent, TMessage>(state: HistoryState<TEvent, TMessage>): DecodedItem<TMessage>[] => {
|
|
264
|
-
if (state.cachedDecode && state.cachedAtRawLength === state.rawMessages.length) {
|
|
265
|
-
return state.cachedDecode;
|
|
266
|
-
}
|
|
267
|
-
const result = decodeAll(state);
|
|
268
|
-
state.cachedDecode = result;
|
|
269
|
-
state.cachedAtRawLength = state.rawMessages.length;
|
|
270
|
-
return result;
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
// ---------------------------------------------------------------------------
|
|
274
|
-
// Incremental completion counting (avoids full decode inside the fetch loop)
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Scan newly-added raw messages and track which `x-ably-msg-id`s have
|
|
279
|
-
* become complete. Used by {@link fetchUntilLimit} to decide when enough
|
|
280
|
-
* completed messages have been collected, without running the decoder.
|
|
281
|
-
*
|
|
282
|
-
* A msg-id is considered complete only when BOTH of these have been seen:
|
|
283
|
-
* - a "start" signal: either `x-ably-discrete` on a `message.create`
|
|
284
|
-
* (discrete messages are created and terminated by the same wire
|
|
285
|
-
* message), OR any `message.create` / `message.update` / `message.append`
|
|
286
|
-
* with `x-ably-stream: "true"` (the decoder establishes a tracker via
|
|
287
|
-
* create or first-contact).
|
|
288
|
-
* - a "terminal" signal: `x-ably-discrete` on the create, or
|
|
289
|
-
* `x-ably-status: "finished"` / `"aborted"` on any later action.
|
|
290
|
-
*
|
|
291
|
-
* Why update and append count as starts: Ably history can compact a live
|
|
292
|
-
* `create + append + ... + append{status:finished}` sequence into a single
|
|
293
|
-
* `message.update` with `STREAM=true` and `STATUS=finished`. The decoder
|
|
294
|
-
* handles that in {@link _decodeUpdate} via first-contact. Counting only
|
|
295
|
-
* `message.create` as a start would cause the fetch loop to page past a
|
|
296
|
-
* compacted turn without ever marking it complete.
|
|
297
|
-
*
|
|
298
|
-
* Requiring both halves matters when a streaming turn spans a page
|
|
299
|
-
* boundary: the terminal arrives in the newer page (fetched first) while
|
|
300
|
-
* the start sits in an older page. Counting the terminal alone would stop
|
|
301
|
-
* the fetch loop prematurely - the decoder would have no stream state to
|
|
302
|
-
* resolve, and the message wouldn't make it into the result.
|
|
303
|
-
*
|
|
304
|
-
* Messages skipped for counting:
|
|
305
|
-
* - Missing `x-ably-msg-id`: lifecycle events not tied to a domain message.
|
|
306
|
-
* - `x-ably-amend` set: amendments target an existing message, not a new
|
|
307
|
-
* completion.
|
|
308
|
-
* - `message.delete`: clears the tracker, doesn't produce output.
|
|
309
|
-
*
|
|
310
|
-
* Known edge case: if Ably history is truncated and a terminal survives
|
|
311
|
-
* while every start signal for its msg-id has rolled off, the counter will
|
|
312
|
-
* never mark that `msg-id` complete. The loop keeps fetching until it runs
|
|
313
|
-
* out of pages, then returns whatever the decoder actually produced.
|
|
314
|
-
* Matches the existing behaviour for the same truncation scenario.
|
|
315
|
-
* @param state - The shared history traversal state.
|
|
316
|
-
* @param newMessages - The Ably messages just pushed onto `state.rawMessages`.
|
|
317
|
-
*/
|
|
318
|
-
const countNewCompletions = <TEvent, TMessage>(
|
|
319
|
-
state: HistoryState<TEvent, TMessage>,
|
|
320
|
-
newMessages: readonly Ably.InboundMessage[],
|
|
321
|
-
): void => {
|
|
322
|
-
for (const msg of newMessages) {
|
|
323
|
-
const headers = getHeaders(msg);
|
|
324
|
-
const msgId = headers[HEADER_MSG_ID];
|
|
325
|
-
if (!msgId) continue;
|
|
326
|
-
// Amendments target an existing message, not a new completion.
|
|
327
|
-
// Defensive: no current encoder path produces an amendment carrying
|
|
328
|
-
// HEADER_STREAM=true, HEADER_STATUS, or HEADER_DISCRETE.
|
|
329
|
-
if (headers[HEADER_AMEND]) continue;
|
|
330
|
-
|
|
331
|
-
const action = msg.action;
|
|
332
|
-
const isDiscreteCreate = action === 'message.create' && HEADER_DISCRETE in headers;
|
|
333
|
-
// Any content-producing action on a streamed serial counts as a start:
|
|
334
|
-
// the decoder uses create or first-contact (update/append) to establish
|
|
335
|
-
// its tracker. Delete clears tracker state and emits nothing, so it
|
|
336
|
-
// never counts as a start.
|
|
337
|
-
const hasStreamContent =
|
|
338
|
-
headers[HEADER_STREAM] === 'true' &&
|
|
339
|
-
(action === 'message.create' || action === 'message.update' || action === 'message.append');
|
|
340
|
-
const status = headers[HEADER_STATUS];
|
|
341
|
-
const isTerminal = status === 'finished' || status === 'aborted';
|
|
342
|
-
|
|
343
|
-
if (isDiscreteCreate || hasStreamContent) state.startedMsgIds.add(msgId);
|
|
344
|
-
if (isDiscreteCreate || isTerminal) state.terminatedMsgIds.add(msgId);
|
|
345
|
-
if (state.startedMsgIds.has(msgId) && state.terminatedMsgIds.has(msgId)) {
|
|
346
|
-
state.completedMsgIds.add(msgId);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// ---------------------------------------------------------------------------
|
|
352
|
-
// Fetch Ably pages until we have enough completed messages
|
|
353
|
-
// ---------------------------------------------------------------------------
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Fetch Ably history pages until we have enough completed messages.
|
|
357
|
-
*
|
|
358
|
-
* The loop uses {@link countNewCompletions} to decide when to stop -
|
|
359
|
-
* a cheap O(new messages) header scan - rather than running the full
|
|
360
|
-
* decoder per page. The decoder runs exactly once later, in
|
|
361
|
-
* {@link buildResult}, against the fully-collected `rawMessages`.
|
|
362
|
-
* @param state - The shared history traversal state.
|
|
363
|
-
* @param ablyPage - The current Ably paginated result to start from.
|
|
364
|
-
* @param limit - Target number of completed messages beyond what has already been returned.
|
|
365
|
-
*/
|
|
366
|
-
const fetchUntilLimit = async <TEvent, TMessage>(
|
|
367
|
-
state: HistoryState<TEvent, TMessage>,
|
|
368
|
-
ablyPage: Ably.PaginatedResult<Ably.InboundMessage>,
|
|
369
|
-
limit: number,
|
|
370
|
-
): Promise<void> => {
|
|
371
|
-
state.rawMessages.push(...ablyPage.items);
|
|
372
|
-
state.lastAblyPage = ablyPage;
|
|
373
|
-
countNewCompletions(state, ablyPage.items);
|
|
374
|
-
|
|
375
|
-
const target = state.returnedCount + limit;
|
|
376
|
-
while (state.completedMsgIds.size < target && ablyPage.hasNext()) {
|
|
377
|
-
state.logger.debug('decodeHistory.fetchUntilLimit(); fetching next page', {
|
|
378
|
-
collected: state.rawMessages.length,
|
|
379
|
-
completed: state.completedMsgIds.size,
|
|
380
|
-
});
|
|
381
|
-
const nextPage = await ablyPage.next();
|
|
382
|
-
if (!nextPage) break;
|
|
383
|
-
ablyPage = nextPage;
|
|
384
|
-
state.rawMessages.push(...nextPage.items);
|
|
385
|
-
state.lastAblyPage = nextPage;
|
|
386
|
-
countNewCompletions(state, nextPage.items);
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
// ---------------------------------------------------------------------------
|
|
391
|
-
// Build HistoryPage result from current state
|
|
392
|
-
// ---------------------------------------------------------------------------
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Build a HistoryPage from the current decode state.
|
|
396
|
-
* @param state - The shared history traversal state.
|
|
397
|
-
* @param limit - Max messages per page.
|
|
398
|
-
* @returns A page of decoded history with a `next()` cursor.
|
|
399
|
-
*/
|
|
400
|
-
const buildResult = <TEvent, TMessage>(state: HistoryState<TEvent, TMessage>, limit: number): HistoryPage<TMessage> => {
|
|
401
|
-
// allCompleted is newest-first. Slice from returnedCount for this page,
|
|
402
|
-
// then reverse to chronological for display.
|
|
403
|
-
const allCompleted = decodeAllCached(state);
|
|
404
|
-
|
|
405
|
-
const pageSlice = allCompleted.slice(state.returnedCount, state.returnedCount + limit);
|
|
406
|
-
const chronSlice = [...pageSlice].toReversed();
|
|
407
|
-
state.returnedCount += pageSlice.length;
|
|
408
|
-
|
|
409
|
-
const moreCompleted = allCompleted.length > state.returnedCount;
|
|
410
|
-
const moreAblyPages = state.lastAblyPage?.hasNext() ?? false;
|
|
411
|
-
|
|
412
|
-
// Raw Ably messages for this page in chronological order.
|
|
413
|
-
const newRawCount = state.rawMessages.length - state.returnedRawCount;
|
|
414
|
-
const rawSlice = newRawCount > 0 ? state.rawMessages.slice(state.returnedRawCount).toReversed() : [];
|
|
415
|
-
state.returnedRawCount = state.rawMessages.length;
|
|
416
|
-
|
|
417
|
-
return {
|
|
418
|
-
items: chronSlice.map((d) => ({ message: d.message, headers: d.headers, serial: d.serial })),
|
|
419
|
-
rawMessages: rawSlice,
|
|
420
|
-
hasNext: () => moreCompleted || moreAblyPages,
|
|
421
|
-
next: async () => {
|
|
422
|
-
if (moreCompleted) {
|
|
423
|
-
return buildResult(state, limit);
|
|
424
|
-
}
|
|
425
|
-
if (!moreAblyPages || !state.lastAblyPage) return;
|
|
426
|
-
const nextAbly = await state.lastAblyPage.next();
|
|
427
|
-
if (!nextAbly) return;
|
|
428
|
-
await fetchUntilLimit(state, nextAbly, limit);
|
|
429
|
-
return buildResult(state, limit);
|
|
430
|
-
},
|
|
431
|
-
};
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
// ---------------------------------------------------------------------------
|
|
435
|
-
// Public API
|
|
436
|
-
// ---------------------------------------------------------------------------
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Load conversation history from a channel and return decoded messages.
|
|
440
|
-
*
|
|
441
|
-
* Attaches the channel if not already attached, then calls
|
|
442
|
-
* `channel.history({ untilAttach: true })` to guarantee no gap between
|
|
443
|
-
* historical and live messages. The attach is idempotent.
|
|
444
|
-
*
|
|
445
|
-
* The `limit` option controls the number of complete messages
|
|
446
|
-
* returned per page, not the number of Ably wire messages fetched.
|
|
447
|
-
* @param channel - The Ably channel to load history from.
|
|
448
|
-
* @param codec - The codec for decoding wire messages into domain messages.
|
|
449
|
-
* @param options - Pagination options.
|
|
450
|
-
* @param logger - Logger for diagnostic output.
|
|
451
|
-
* @returns The first page of decoded history.
|
|
452
|
-
*/
|
|
453
|
-
// Spec: AIT-CT11, AIT-CT11b
|
|
454
|
-
export const decodeHistory = async <TEvent, TMessage>(
|
|
455
|
-
channel: Ably.RealtimeChannel,
|
|
456
|
-
codec: Codec<TEvent, TMessage>,
|
|
457
|
-
options: LoadHistoryOptions | undefined,
|
|
458
|
-
logger: Logger,
|
|
459
|
-
): Promise<HistoryPage<TMessage>> => {
|
|
460
|
-
const limit = options?.limit ?? 100;
|
|
461
|
-
const state: HistoryState<TEvent, TMessage> = {
|
|
462
|
-
codec,
|
|
463
|
-
rawMessages: [],
|
|
464
|
-
returnedCount: 0,
|
|
465
|
-
returnedRawCount: 0,
|
|
466
|
-
lastAblyPage: undefined,
|
|
467
|
-
cachedDecode: undefined,
|
|
468
|
-
cachedAtRawLength: 0,
|
|
469
|
-
startedMsgIds: new Set<string>(),
|
|
470
|
-
terminatedMsgIds: new Set<string>(),
|
|
471
|
-
completedMsgIds: new Set<string>(),
|
|
472
|
-
logger,
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
logger.trace('decodeHistory();', { limit });
|
|
476
|
-
|
|
477
|
-
// Request more Ably messages than the domain limit to account for
|
|
478
|
-
// the many-to-one ratio (multiple wire messages per message).
|
|
479
|
-
const wireLimit = limit * 10;
|
|
480
|
-
|
|
481
|
-
await channel.attach();
|
|
482
|
-
const ablyPage = await channel.history({ untilAttach: true, limit: wireLimit });
|
|
483
|
-
await fetchUntilLimit(state, ablyPage, limit);
|
|
484
|
-
return buildResult(state, limit);
|
|
485
|
-
};
|