@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
|
@@ -1,434 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ConversationTree — materializes a branching conversation from a flat
|
|
3
|
-
* oplog of Ably messages using serial-first ordering.
|
|
4
|
-
*
|
|
5
|
-
* Serial order (the total order assigned by Ably) is the primary mechanism
|
|
6
|
-
* for linear message sequences. `x-ably-parent` and `x-ably-fork-of` headers
|
|
7
|
-
* are only structurally meaningful at branch points — where the user is
|
|
8
|
-
* interacting with a visible message and the client always has it loaded.
|
|
9
|
-
*
|
|
10
|
-
* `upsert()` is the sole mutation method. Messages can arrive in any order
|
|
11
|
-
* (live subscription, history pages, seed data) and the tree produces the
|
|
12
|
-
* correct `flatten()` output once all messages are present.
|
|
13
|
-
*
|
|
14
|
-
* The tree owns conversation state. `flatten()` returns the linear message
|
|
15
|
-
* list for the currently selected branches — this is what the transport's
|
|
16
|
-
* `getMessages()` delegates to.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { HEADER_FORK_OF, HEADER_PARENT } from '../../constants.js';
|
|
20
|
-
import type { Logger } from '../../logger.js';
|
|
21
|
-
import type { ConversationNode, ConversationTree } from './types.js';
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Internal node type
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
interface InternalNode<TMessage> {
|
|
28
|
-
node: ConversationNode<TMessage>;
|
|
29
|
-
/** Insertion sequence — tiebreaker for null-serial messages. */
|
|
30
|
-
insertSeq: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// Implementation
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
// Spec: AIT-CT13
|
|
38
|
-
class DefaultConversationTree<TMessage> implements ConversationTree<TMessage> {
|
|
39
|
-
/** All nodes indexed by msgId (x-ably-msg-id). */
|
|
40
|
-
private readonly _nodeIndex = new Map<string, InternalNode<TMessage>>();
|
|
41
|
-
|
|
42
|
-
/** Secondary index: codec message key to msgId. Bridges UIMessage.id to x-ably-msg-id. */
|
|
43
|
-
private readonly _codecKeyIndex = new Map<string, string>();
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* All nodes sorted by serial (lexicographic). Null-serial messages
|
|
47
|
-
* (optimistic inserts, seed data) sort after all serial-bearing messages,
|
|
48
|
-
* ordered among themselves by insertion sequence.
|
|
49
|
-
*/
|
|
50
|
-
private readonly _sortedList: InternalNode<TMessage>[] = [];
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Parent index: parentId to set of child msgIds.
|
|
54
|
-
* Nodes with no parent are indexed under the key `null`.
|
|
55
|
-
*/
|
|
56
|
-
private readonly _parentIndex = new Map<string | undefined, Set<string>>();
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Selected sibling index at each fork point, keyed by the msgId of
|
|
60
|
-
* the first sibling in the group (the fork target). Default: last.
|
|
61
|
-
*/
|
|
62
|
-
private readonly _selections = new Map<string, number>();
|
|
63
|
-
|
|
64
|
-
private readonly _getKey: (message: TMessage) => string;
|
|
65
|
-
private readonly _logger: Logger;
|
|
66
|
-
|
|
67
|
-
/** Monotonically increasing counter for insertion sequence. */
|
|
68
|
-
private _seqCounter = 0;
|
|
69
|
-
|
|
70
|
-
constructor(getKey: (message: TMessage) => string, logger: Logger) {
|
|
71
|
-
this._getKey = getKey;
|
|
72
|
-
this._logger = logger;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// -------------------------------------------------------------------------
|
|
76
|
-
// Sorted list maintenance
|
|
77
|
-
// -------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Compare two nodes for sorted list ordering.
|
|
81
|
-
* Serial-bearing nodes sort by serial (lexicographic).
|
|
82
|
-
* Null-serial nodes sort after all serial-bearing nodes.
|
|
83
|
-
* Among null-serial nodes, sort by insertion sequence.
|
|
84
|
-
* @param a - First node to compare.
|
|
85
|
-
* @param b - Second node to compare.
|
|
86
|
-
* @returns Negative if a sorts before b, positive if after, zero if equal.
|
|
87
|
-
*/
|
|
88
|
-
// Spec: AIT-CT13a
|
|
89
|
-
private _compareNodes(a: InternalNode<TMessage>, b: InternalNode<TMessage>): number {
|
|
90
|
-
const sa = a.node.serial;
|
|
91
|
-
const sb = b.node.serial;
|
|
92
|
-
if (sa === undefined && sb === undefined) return a.insertSeq - b.insertSeq;
|
|
93
|
-
if (sa === undefined) return 1; // a sorts after serial-bearing b
|
|
94
|
-
if (sb === undefined) return -1; // b sorts after serial-bearing a
|
|
95
|
-
if (sa < sb) return -1;
|
|
96
|
-
if (sa > sb) return 1;
|
|
97
|
-
return a.insertSeq - b.insertSeq; // same serial: preserve insertion order
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Insert a node into sortedList at the correct position via binary search.
|
|
102
|
-
* @param internal - The node to insert.
|
|
103
|
-
*/
|
|
104
|
-
private _insertSorted(internal: InternalNode<TMessage>): void {
|
|
105
|
-
const serial = internal.node.serial;
|
|
106
|
-
|
|
107
|
-
// Fast path: null-serial always appends to end (among other null-serials)
|
|
108
|
-
if (serial === undefined) {
|
|
109
|
-
this._sortedList.push(internal);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Binary search for insertion point among serial-bearing nodes.
|
|
114
|
-
let lo = 0;
|
|
115
|
-
let hi = this._sortedList.length;
|
|
116
|
-
while (lo < hi) {
|
|
117
|
-
const mid = (lo + hi) >>> 1;
|
|
118
|
-
const midNode = this._sortedList[mid];
|
|
119
|
-
if (!midNode) break; // unreachable: mid is always in bounds
|
|
120
|
-
if (this._compareNodes(midNode, internal) <= 0) {
|
|
121
|
-
lo = mid + 1;
|
|
122
|
-
} else {
|
|
123
|
-
hi = mid;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
this._sortedList.splice(lo, 0, internal);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Remove a node from sortedList.
|
|
131
|
-
* @param internal - The node to remove.
|
|
132
|
-
*/
|
|
133
|
-
private _removeSorted(internal: InternalNode<TMessage>): void {
|
|
134
|
-
const idx = this._sortedList.indexOf(internal);
|
|
135
|
-
if (idx !== -1) this._sortedList.splice(idx, 1);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// -------------------------------------------------------------------------
|
|
139
|
-
// Parent index maintenance
|
|
140
|
-
// -------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
private _addToParentIndex(parentId: string | undefined, msgId: string): void {
|
|
143
|
-
let set = this._parentIndex.get(parentId);
|
|
144
|
-
if (!set) {
|
|
145
|
-
set = new Set();
|
|
146
|
-
this._parentIndex.set(parentId, set);
|
|
147
|
-
}
|
|
148
|
-
set.add(msgId);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private _removeFromParentIndex(parentId: string | undefined, msgId: string): void {
|
|
152
|
-
const set = this._parentIndex.get(parentId);
|
|
153
|
-
if (set) {
|
|
154
|
-
set.delete(msgId);
|
|
155
|
-
if (set.size === 0) this._parentIndex.delete(parentId);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// -------------------------------------------------------------------------
|
|
160
|
-
// Sibling grouping
|
|
161
|
-
// -------------------------------------------------------------------------
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Get the sibling group that `msgId` belongs to.
|
|
165
|
-
*
|
|
166
|
-
* A sibling group is: the original message + all messages whose `forkOf`
|
|
167
|
-
* points to the original (or transitively to a sibling). We find the
|
|
168
|
-
* group root by following `forkOf` chains to the earliest ancestor that
|
|
169
|
-
* has no `forkOf` (or whose `forkOf` target doesn't share the same parent).
|
|
170
|
-
* @param msgId - The msg-id to look up the sibling group for.
|
|
171
|
-
* @returns The ordered list of sibling nodes.
|
|
172
|
-
*/
|
|
173
|
-
// Spec: AIT-CT13b
|
|
174
|
-
private _getSiblingGroup(msgId: string): ConversationNode<TMessage>[] {
|
|
175
|
-
const entry = this._nodeIndex.get(msgId);
|
|
176
|
-
if (!entry) return [];
|
|
177
|
-
|
|
178
|
-
// Find the "original" — the message at the root of the fork chain
|
|
179
|
-
// that shares the same parentId. Guard against cycles in forkOf chains.
|
|
180
|
-
let original = entry.node;
|
|
181
|
-
const visitedGroup = new Set<string>([original.msgId]);
|
|
182
|
-
while (original.forkOf) {
|
|
183
|
-
if (visitedGroup.has(original.forkOf)) break; // cycle guard
|
|
184
|
-
const forkTarget = this._nodeIndex.get(original.forkOf);
|
|
185
|
-
if (!forkTarget || forkTarget.node.parentId !== original.parentId) break;
|
|
186
|
-
original = forkTarget.node;
|
|
187
|
-
visitedGroup.add(original.msgId);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Collect all siblings: nodes with the same parentId that either
|
|
191
|
-
// ARE the original, or have a forkOf chain leading to the original.
|
|
192
|
-
const parentId = original.parentId;
|
|
193
|
-
const originalId = original.msgId;
|
|
194
|
-
const siblings: InternalNode<TMessage>[] = [];
|
|
195
|
-
|
|
196
|
-
const candidateIds = this._parentIndex.get(parentId);
|
|
197
|
-
if (candidateIds) {
|
|
198
|
-
for (const childId of candidateIds) {
|
|
199
|
-
const childEntry = this._nodeIndex.get(childId);
|
|
200
|
-
if (childEntry && this._isSiblingOf(childEntry.node, originalId)) {
|
|
201
|
-
siblings.push(childEntry);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Sort by Ably serial (lexicographic). Messages without a serial
|
|
207
|
-
// (optimistic inserts before server relay) sort after all serial-bearing
|
|
208
|
-
// siblings — they represent the user's most recent action.
|
|
209
|
-
siblings.sort((a, b) => this._compareNodes(a, b));
|
|
210
|
-
return siblings.map((s) => s.node);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Check if `node` belongs to the sibling group rooted at `originalId`.
|
|
215
|
-
* A node is a sibling if it IS the original or its forkOf chain leads
|
|
216
|
-
* to the original (with the same parentId).
|
|
217
|
-
* @param node - The node to check.
|
|
218
|
-
* @param originalId - The group root to match against.
|
|
219
|
-
* @returns True if the node belongs to the sibling group.
|
|
220
|
-
*/
|
|
221
|
-
private _isSiblingOf(node: ConversationNode<TMessage>, originalId: string): boolean {
|
|
222
|
-
if (node.msgId === originalId) return true;
|
|
223
|
-
let current = node;
|
|
224
|
-
const visited = new Set<string>([current.msgId]);
|
|
225
|
-
while (current.forkOf) {
|
|
226
|
-
if (current.forkOf === originalId) return true;
|
|
227
|
-
if (visited.has(current.forkOf)) break; // cycle guard
|
|
228
|
-
const target = this._nodeIndex.get(current.forkOf);
|
|
229
|
-
if (!target) break;
|
|
230
|
-
current = target.node;
|
|
231
|
-
visited.add(current.msgId);
|
|
232
|
-
}
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Get the "group root" msgId for a sibling group — the original message
|
|
238
|
-
* that all forks trace back to.
|
|
239
|
-
* @param msgId - Any msg-id in the sibling group.
|
|
240
|
-
* @returns The msg-id of the group root.
|
|
241
|
-
*/
|
|
242
|
-
private _getGroupRoot(msgId: string): string {
|
|
243
|
-
const entry = this._nodeIndex.get(msgId);
|
|
244
|
-
if (!entry) return msgId;
|
|
245
|
-
|
|
246
|
-
let current = entry.node;
|
|
247
|
-
const visited = new Set<string>([current.msgId]);
|
|
248
|
-
while (current.forkOf) {
|
|
249
|
-
if (visited.has(current.forkOf)) break; // cycle guard
|
|
250
|
-
const forkTarget = this._nodeIndex.get(current.forkOf);
|
|
251
|
-
if (!forkTarget || forkTarget.node.parentId !== current.parentId) break;
|
|
252
|
-
current = forkTarget.node;
|
|
253
|
-
visited.add(current.msgId);
|
|
254
|
-
}
|
|
255
|
-
return current.msgId;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// -------------------------------------------------------------------------
|
|
259
|
-
// Public query methods
|
|
260
|
-
// -------------------------------------------------------------------------
|
|
261
|
-
|
|
262
|
-
flatten(): TMessage[] {
|
|
263
|
-
const result: TMessage[] = [];
|
|
264
|
-
const currentPath = new Set<string>();
|
|
265
|
-
// Track which sibling groups we've already resolved to avoid
|
|
266
|
-
// re-resolving for every member of the group.
|
|
267
|
-
const resolvedGroups = new Map<string, string>(); // groupRootId → selected msgId
|
|
268
|
-
|
|
269
|
-
for (const internal of this._sortedList) {
|
|
270
|
-
const node = internal.node;
|
|
271
|
-
const { msgId, parentId } = node;
|
|
272
|
-
|
|
273
|
-
// Step 1: Check parent reachability.
|
|
274
|
-
if (parentId !== undefined && !currentPath.has(parentId)) {
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Step 2: Check sibling selection.
|
|
279
|
-
const group = this._getSiblingGroup(msgId);
|
|
280
|
-
if (group.length > 1) {
|
|
281
|
-
const groupRootId = this._getGroupRoot(msgId);
|
|
282
|
-
let selectedId = resolvedGroups.get(groupRootId);
|
|
283
|
-
if (selectedId === undefined) {
|
|
284
|
-
const selectedIdx = this._selections.get(groupRootId) ?? group.length - 1;
|
|
285
|
-
const clamped = Math.max(0, Math.min(selectedIdx, group.length - 1));
|
|
286
|
-
const selected = group[clamped];
|
|
287
|
-
if (!selected) break; // unreachable: clamped is always in bounds
|
|
288
|
-
selectedId = selected.msgId;
|
|
289
|
-
resolvedGroups.set(groupRootId, selectedId);
|
|
290
|
-
}
|
|
291
|
-
if (msgId !== selectedId) {
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
currentPath.add(msgId);
|
|
297
|
-
result.push(node.message);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return result;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
getSiblings(msgId: string): TMessage[] {
|
|
304
|
-
return this._getSiblingGroup(msgId).map((n) => n.message);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
hasSiblings(msgId: string): boolean {
|
|
308
|
-
return this._getSiblingGroup(msgId).length > 1;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
getSelectedIndex(msgId: string): number {
|
|
312
|
-
const group = this._getSiblingGroup(msgId);
|
|
313
|
-
if (group.length <= 1) return 0;
|
|
314
|
-
const groupRootId = this._getGroupRoot(msgId);
|
|
315
|
-
const stored = this._selections.get(groupRootId);
|
|
316
|
-
if (stored !== undefined) return Math.max(0, Math.min(stored, group.length - 1));
|
|
317
|
-
return group.length - 1; // default: latest
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Spec: AIT-CT13c
|
|
321
|
-
select(msgId: string, index: number): void {
|
|
322
|
-
this._logger.debug('ConversationTree.select();', { msgId, index });
|
|
323
|
-
const group = this._getSiblingGroup(msgId);
|
|
324
|
-
if (group.length <= 1) return;
|
|
325
|
-
const groupRootId = this._getGroupRoot(msgId);
|
|
326
|
-
this._selections.set(groupRootId, Math.max(0, Math.min(index, group.length - 1)));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
getNode(msgId: string): ConversationNode<TMessage> | undefined {
|
|
330
|
-
return this._nodeIndex.get(msgId)?.node;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
getNodeByKey(key: string): ConversationNode<TMessage> | undefined {
|
|
334
|
-
const msgId = this._codecKeyIndex.get(key);
|
|
335
|
-
if (!msgId) return undefined;
|
|
336
|
-
return this._nodeIndex.get(msgId)?.node;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
getHeaders(msgId: string): Record<string, string> | undefined {
|
|
340
|
-
return this._nodeIndex.get(msgId)?.node.headers;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// -------------------------------------------------------------------------
|
|
344
|
-
// Mutation
|
|
345
|
-
// -------------------------------------------------------------------------
|
|
346
|
-
|
|
347
|
-
upsert(msgId: string, message: TMessage, headers: Record<string, string>, serial?: string): void {
|
|
348
|
-
const parentId = headers[HEADER_PARENT] ?? undefined;
|
|
349
|
-
const forkOf = headers[HEADER_FORK_OF] ?? undefined;
|
|
350
|
-
|
|
351
|
-
// Maintain codec key → msgId secondary index
|
|
352
|
-
this._codecKeyIndex.set(this._getKey(message), msgId);
|
|
353
|
-
|
|
354
|
-
const existing = this._nodeIndex.get(msgId);
|
|
355
|
-
if (existing) {
|
|
356
|
-
// Update in place — message content may have changed (e.g. streaming).
|
|
357
|
-
// Only update headers if the new headers are non-empty (prevents
|
|
358
|
-
// streaming updates from erasing canonical headers).
|
|
359
|
-
existing.node.message = message;
|
|
360
|
-
if (Object.keys(headers).length > 0) {
|
|
361
|
-
existing.node.headers = { ...headers };
|
|
362
|
-
}
|
|
363
|
-
// Spec: AIT-CT13d
|
|
364
|
-
// Promote serial: optimistic (null) → server-assigned on relay.
|
|
365
|
-
if (serial && !existing.node.serial) {
|
|
366
|
-
this._logger.debug('ConversationTree.upsert(); promoting serial', { msgId, serial });
|
|
367
|
-
existing.node.serial = serial;
|
|
368
|
-
// Re-sort: remove from current position, re-insert at correct position.
|
|
369
|
-
this._removeSorted(existing);
|
|
370
|
-
this._insertSorted(existing);
|
|
371
|
-
}
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
this._logger.trace('ConversationTree.upsert(); inserting new node', { msgId, parentId, forkOf });
|
|
376
|
-
|
|
377
|
-
const node: ConversationNode<TMessage> = {
|
|
378
|
-
message,
|
|
379
|
-
msgId,
|
|
380
|
-
parentId,
|
|
381
|
-
forkOf,
|
|
382
|
-
headers: { ...headers },
|
|
383
|
-
serial,
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
const internal: InternalNode<TMessage> = { node, insertSeq: this._seqCounter++ };
|
|
387
|
-
this._nodeIndex.set(msgId, internal);
|
|
388
|
-
this._addToParentIndex(parentId, msgId);
|
|
389
|
-
this._insertSorted(internal);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
delete(msgId: string): void {
|
|
393
|
-
const entry = this._nodeIndex.get(msgId);
|
|
394
|
-
if (!entry) return;
|
|
395
|
-
|
|
396
|
-
this._logger.debug('ConversationTree.delete();', { msgId });
|
|
397
|
-
|
|
398
|
-
const { node } = entry;
|
|
399
|
-
|
|
400
|
-
// Clean up secondary index
|
|
401
|
-
const codecKey = this._getKey(node.message);
|
|
402
|
-
if (this._codecKeyIndex.get(codecKey) === msgId) {
|
|
403
|
-
this._codecKeyIndex.delete(codecKey);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Remove from parent index
|
|
407
|
-
this._removeFromParentIndex(node.parentId, msgId);
|
|
408
|
-
|
|
409
|
-
// Remove from sorted list
|
|
410
|
-
this._removeSorted(entry);
|
|
411
|
-
|
|
412
|
-
// Remove from primary index
|
|
413
|
-
this._nodeIndex.delete(msgId);
|
|
414
|
-
this._selections.delete(msgId);
|
|
415
|
-
|
|
416
|
-
// Children are NOT deleted — they become unreachable in flatten()
|
|
417
|
-
// because their parent is no longer on the active path.
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// ---------------------------------------------------------------------------
|
|
422
|
-
// Factory
|
|
423
|
-
// ---------------------------------------------------------------------------
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Create a ConversationTree that materializes branching history from a flat oplog.
|
|
427
|
-
* @param getKey - Codec function that returns a stable key for a domain message.
|
|
428
|
-
* @param logger - Logger for diagnostic output.
|
|
429
|
-
* @returns A new {@link ConversationTree} instance.
|
|
430
|
-
*/
|
|
431
|
-
export const createConversationTree = <TMessage>(
|
|
432
|
-
getKey: (message: TMessage) => string,
|
|
433
|
-
logger: Logger,
|
|
434
|
-
): ConversationTree<TMessage> => new DefaultConversationTree(getKey, logger);
|