@guuey/threads 0.31.1 → 0.32.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 +81 -0
- package/dist/closed.d.ts +22 -0
- package/dist/closed.d.ts.map +1 -0
- package/dist/closed.js +28 -0
- package/dist/fold-rows.d.ts +278 -5
- package/dist/fold-rows.d.ts.map +1 -1
- package/dist/fold-rows.js +475 -21
- package/dist/http-read.d.ts +47 -0
- package/dist/http-read.d.ts.map +1 -0
- package/dist/http-read.js +274 -0
- package/dist/http.d.ts +37 -34
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +145 -22
- package/dist/in-memory.d.ts +2 -7
- package/dist/in-memory.d.ts.map +1 -1
- package/dist/in-memory.js +27 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/item-size.d.ts +56 -0
- package/dist/item-size.d.ts.map +1 -0
- package/dist/item-size.js +125 -0
- package/dist/rows.d.ts +122 -1
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +13 -1
- package/dist/seal.d.ts +63 -0
- package/dist/seal.d.ts.map +1 -0
- package/dist/seal.js +120 -0
- package/dist/snapshot-plan.d.ts +61 -0
- package/dist/snapshot-plan.d.ts.map +1 -0
- package/dist/snapshot-plan.js +71 -0
- package/dist/store.d.ts +130 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +274 -66
- package/dist/stored-row.d.ts +17 -3
- package/dist/stored-row.d.ts.map +1 -1
- package/dist/stored-row.js +2 -21
- package/dist/testing/contract-suite.d.ts +37 -1
- package/dist/testing/contract-suite.d.ts.map +1 -1
- package/dist/testing/contract-suite.js +122 -1
- package/package.json +3 -3
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readers for what the hosted thread API answers ({@link HttpThreadPersistence}
|
|
3
|
+
* in `http.ts`). A response body is parsed to `unknown` and read here member by
|
|
4
|
+
* member; nothing is taken as a port type until these readers have proven it.
|
|
5
|
+
*
|
|
6
|
+
* Each reader returns the typed value, or `undefined` when the payload is not
|
|
7
|
+
* one; the binding turns `undefined` into an `HttpThreadStoreError` with code
|
|
8
|
+
* `BAD_RESPONSE`, the error every caller of the binding already handles.
|
|
9
|
+
*
|
|
10
|
+
* What a reader checks follows the port types in `rows.ts`, read across a
|
|
11
|
+
* rolling release, and reads a stored row the way the DynamoDB binding hands
|
|
12
|
+
* the same row to its readers (the two bindings must agree):
|
|
13
|
+
* - a member that IDENTIFIES the row (a thread's own members; a message's
|
|
14
|
+
* `threadId`, integer `seq`, `clientMessageId`, `at`, which the schema
|
|
15
|
+
* requires on every row) is proven by type, and a row without it is refused;
|
|
16
|
+
* - a message's closed vocabularies are read the way every reader of the row
|
|
17
|
+
* reads them: a `kind` this release does not know (a newer writer's, or none
|
|
18
|
+
* on a legacy row) is a `text` row (it is not a card, the one kind a reader
|
|
19
|
+
* branches on), and an `authorRole` it does not know is `agent` (not the
|
|
20
|
+
* user's words: `rowToAgMessage` reads it as the assistant, the managed read
|
|
21
|
+
* plane as `agent`), so one such row never refuses the thread's history;
|
|
22
|
+
* - a message's `userId` (no reader on this path reads it) is left absent when
|
|
23
|
+
* a legacy row has none, and the binding fills in the token's user, the only
|
|
24
|
+
* user whose threads the server answers (`http.ts`);
|
|
25
|
+
* - an OPTIONAL member is kept when it reads as its type and is absent
|
|
26
|
+
* otherwise, the way each one's own reader treats it at use (`readThreadInputKind`,
|
|
27
|
+
* `readTapLabels`, `threadClosure`), so a field an older server omits reads
|
|
28
|
+
* as absent and a value a newer one adds that this release does not know is
|
|
29
|
+
* left out;
|
|
30
|
+
* - a thread's `sealIntervals` is carried as stored, never normalised or
|
|
31
|
+
* dropped: `readSealState` reads it at use, and reads one it cannot read as
|
|
32
|
+
* wholly sealed (fail closed), exactly as over the DynamoDB binding;
|
|
33
|
+
* - a message's `untrustedOrigin` that is not a boolean would read as trusted
|
|
34
|
+
* if it were dropped, so that row is refused instead;
|
|
35
|
+
* - opaque members (`content`, `cardSnapshot`, `aiContext`) are carried as
|
|
36
|
+
* they came, as the port types them (`unknown`).
|
|
37
|
+
*/
|
|
38
|
+
import { JsonValue } from "@silverprotocol/core";
|
|
39
|
+
import { readTapLabels } from "@guuey/mcp-apps-host/narrowing";
|
|
40
|
+
import { splitStoredThreadMemory } from "./fold-rows.js";
|
|
41
|
+
import { readThreadInputKind, } from "./rows.js";
|
|
42
|
+
/** Whether `value` is a JSON object: not `null`, not an array, not a scalar. */
|
|
43
|
+
export function isWireObject(value) {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
function isSeqNumber(value) {
|
|
47
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
48
|
+
}
|
|
49
|
+
/** The member when it is a string, else undefined (absent, null or another type). */
|
|
50
|
+
function optString(obj, key) {
|
|
51
|
+
const value = obj[key];
|
|
52
|
+
return typeof value === "string" ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
const MESSAGE_KINDS = ["text", "card", "event"];
|
|
55
|
+
const MESSAGE_ROLES = ["user", "agent", "system"];
|
|
56
|
+
/** What a message `kind` this release does not know reads as (see the module doc). */
|
|
57
|
+
const UNKNOWN_KIND_READS_AS = "text";
|
|
58
|
+
/** What a message `authorRole` this release does not know reads as (see the module doc). */
|
|
59
|
+
const UNKNOWN_ROLE_READS_AS = "agent";
|
|
60
|
+
const TURN_ORIGINS = ["card_action", "chip", "typed"];
|
|
61
|
+
const CLIENT_CLASSES = ["operator", "bot", "browser"];
|
|
62
|
+
/** `value` when it is one of `allowed`, else undefined. */
|
|
63
|
+
function oneOf(allowed, value) {
|
|
64
|
+
return allowed.find((member) => member === value);
|
|
65
|
+
}
|
|
66
|
+
/** `scope`'s result: the three strings the server binds the token to. */
|
|
67
|
+
export function readThreadScope(value) {
|
|
68
|
+
if (!isWireObject(value))
|
|
69
|
+
return undefined;
|
|
70
|
+
const { appId, userId, region } = value;
|
|
71
|
+
if (typeof appId !== "string" || typeof userId !== "string" || typeof region !== "string")
|
|
72
|
+
return undefined;
|
|
73
|
+
return { appId, userId, region };
|
|
74
|
+
}
|
|
75
|
+
/** `incrementSeq`'s result: the integer seq the server allocated. */
|
|
76
|
+
export function readAllocatedSeq(value) {
|
|
77
|
+
return isSeqNumber(value) ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
/** A `getThread` row. */
|
|
80
|
+
export function readThreadRow(value) {
|
|
81
|
+
if (!isWireObject(value))
|
|
82
|
+
return undefined;
|
|
83
|
+
const { id, userId, appId, servingRegion, title, status, pinned, lastSeq } = value;
|
|
84
|
+
const { lastMessageAt, lastMessagePreview, threadMode, createdAt, updatedAt } = value;
|
|
85
|
+
if (typeof id !== "string" ||
|
|
86
|
+
typeof userId !== "string" ||
|
|
87
|
+
typeof appId !== "string" ||
|
|
88
|
+
typeof servingRegion !== "string" ||
|
|
89
|
+
typeof title !== "string" ||
|
|
90
|
+
typeof status !== "string" ||
|
|
91
|
+
typeof pinned !== "boolean" ||
|
|
92
|
+
!isSeqNumber(lastSeq) ||
|
|
93
|
+
typeof lastMessageAt !== "string" ||
|
|
94
|
+
typeof lastMessagePreview !== "string" ||
|
|
95
|
+
typeof threadMode !== "string" ||
|
|
96
|
+
typeof createdAt !== "string" ||
|
|
97
|
+
typeof updatedAt !== "string") {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
// Carried as stored (`null` is no seal, as `readSealState` reads it): a seal
|
|
101
|
+
// this release cannot read still reaches `readSealState`, which reads it as
|
|
102
|
+
// wholly sealed, so the thread stays usable and closed.
|
|
103
|
+
const sealIntervals = value.sealIntervals;
|
|
104
|
+
const previewSeq = value.lastMessagePreviewSeq;
|
|
105
|
+
const previewDigest = optString(value, "lastMessagePreviewDigest");
|
|
106
|
+
const closedCode = optString(value, "closedCode");
|
|
107
|
+
const closedMessage = optString(value, "closedMessage");
|
|
108
|
+
const closedAt = optString(value, "closedAt");
|
|
109
|
+
return {
|
|
110
|
+
id,
|
|
111
|
+
userId,
|
|
112
|
+
appId,
|
|
113
|
+
servingRegion,
|
|
114
|
+
title,
|
|
115
|
+
status,
|
|
116
|
+
pinned,
|
|
117
|
+
lastSeq,
|
|
118
|
+
lastMessageAt,
|
|
119
|
+
lastMessagePreview,
|
|
120
|
+
...(isSeqNumber(previewSeq) ? { lastMessagePreviewSeq: previewSeq } : {}),
|
|
121
|
+
...(previewDigest !== undefined ? { lastMessagePreviewDigest: previewDigest } : {}),
|
|
122
|
+
...(sealIntervals !== undefined && sealIntervals !== null ? { sealIntervals } : {}),
|
|
123
|
+
...(closedCode !== undefined ? { closedCode: closedCode } : {}),
|
|
124
|
+
...(closedMessage !== undefined ? { closedMessage: closedMessage } : {}),
|
|
125
|
+
...(closedAt !== undefined ? { closedAt: closedAt } : {}),
|
|
126
|
+
threadMode,
|
|
127
|
+
createdAt,
|
|
128
|
+
updatedAt,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** A stored `event` this release reads; another `type`, or a malformed one, is not one ("not mine"). */
|
|
132
|
+
function readThreadMessageEvent(value) {
|
|
133
|
+
if (!isWireObject(value))
|
|
134
|
+
return undefined;
|
|
135
|
+
if (value.type === "handoff") {
|
|
136
|
+
const question = value.question;
|
|
137
|
+
if (typeof question !== "string")
|
|
138
|
+
return undefined;
|
|
139
|
+
const contactEmail = optString(value, "contactEmail");
|
|
140
|
+
const contactName = optString(value, "contactName");
|
|
141
|
+
const summary = optString(value, "summary");
|
|
142
|
+
const event = {
|
|
143
|
+
type: "handoff",
|
|
144
|
+
question,
|
|
145
|
+
...(contactEmail !== undefined ? { contactEmail } : {}),
|
|
146
|
+
...(contactName !== undefined ? { contactName } : {}),
|
|
147
|
+
...(summary !== undefined ? { summary } : {}),
|
|
148
|
+
};
|
|
149
|
+
return event;
|
|
150
|
+
}
|
|
151
|
+
if (value.type === "report") {
|
|
152
|
+
const { summary, reportedBy, wantedHuman } = value;
|
|
153
|
+
if (typeof summary !== "string" || typeof reportedBy !== "string")
|
|
154
|
+
return undefined;
|
|
155
|
+
const contactEmail = optString(value, "contactEmail");
|
|
156
|
+
const contactName = optString(value, "contactName");
|
|
157
|
+
const event = {
|
|
158
|
+
type: "report",
|
|
159
|
+
summary,
|
|
160
|
+
...(typeof wantedHuman === "boolean" ? { wantedHuman } : {}),
|
|
161
|
+
...(contactEmail !== undefined ? { contactEmail } : {}),
|
|
162
|
+
...(contactName !== undefined ? { contactName } : {}),
|
|
163
|
+
reportedBy,
|
|
164
|
+
};
|
|
165
|
+
return event;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
/** A message row (`listRecentMessages`, `findByClientMessageId`). */
|
|
170
|
+
export function readThreadMessageRow(value) {
|
|
171
|
+
if (!isWireObject(value))
|
|
172
|
+
return undefined;
|
|
173
|
+
const { threadId, seq, clientMessageId, at } = value;
|
|
174
|
+
if (typeof threadId !== "string" || !isSeqNumber(seq) || typeof clientMessageId !== "string" || typeof at !== "string") {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const userId = optString(value, "userId");
|
|
178
|
+
const kind = oneOf(MESSAGE_KINDS, value.kind) ?? UNKNOWN_KIND_READS_AS;
|
|
179
|
+
const authorRole = oneOf(MESSAGE_ROLES, value.authorRole) ?? UNKNOWN_ROLE_READS_AS;
|
|
180
|
+
// `untrustedOrigin` marks page-derived content; a stored value that is not a
|
|
181
|
+
// boolean must not read as trusted, so the row is refused.
|
|
182
|
+
const untrustedOrigin = value.untrustedOrigin;
|
|
183
|
+
if (untrustedOrigin !== undefined && typeof untrustedOrigin !== "boolean")
|
|
184
|
+
return undefined;
|
|
185
|
+
const event = readThreadMessageEvent(value.event);
|
|
186
|
+
const turnOrigin = oneOf(TURN_ORIGINS, value.turnOrigin);
|
|
187
|
+
const clientClass = oneOf(CLIENT_CLASSES, value.clientClass);
|
|
188
|
+
const inputKind = readThreadInputKind(value.inputKind);
|
|
189
|
+
const tapLabels = readTapLabels(value.tapLabels);
|
|
190
|
+
const text = optString(value, "text");
|
|
191
|
+
const toolName = optString(value, "toolName");
|
|
192
|
+
const answeredCardSessionId = optString(value, "answeredCardSessionId");
|
|
193
|
+
return {
|
|
194
|
+
threadId,
|
|
195
|
+
seq,
|
|
196
|
+
...(userId !== undefined ? { userId } : {}),
|
|
197
|
+
clientMessageId,
|
|
198
|
+
at,
|
|
199
|
+
kind,
|
|
200
|
+
authorRole,
|
|
201
|
+
...(text !== undefined ? { text } : {}),
|
|
202
|
+
...(value.content !== undefined ? { content: value.content } : {}),
|
|
203
|
+
...(value.cardSnapshot !== undefined ? { cardSnapshot: value.cardSnapshot } : {}),
|
|
204
|
+
...(toolName !== undefined ? { toolName } : {}),
|
|
205
|
+
...(value.aiContext !== undefined ? { aiContext: value.aiContext } : {}),
|
|
206
|
+
...(untrustedOrigin !== undefined ? { untrustedOrigin } : {}),
|
|
207
|
+
...(event !== undefined ? { event } : {}),
|
|
208
|
+
...(turnOrigin !== undefined ? { turnOrigin } : {}),
|
|
209
|
+
...(inputKind !== undefined ? { inputKind } : {}),
|
|
210
|
+
...(answeredCardSessionId !== undefined ? { answeredCardSessionId } : {}),
|
|
211
|
+
...(clientClass !== undefined ? { clientClass } : {}),
|
|
212
|
+
...(tapLabels !== undefined ? { tapLabels } : {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* `listRecentMessages`' result: every row must read, or the list is refused. A
|
|
217
|
+
* row reads unless it lacks a member that identifies it or carries a malformed
|
|
218
|
+
* `untrustedOrigin` (see the module doc); a vocabulary value or a `userId` this
|
|
219
|
+
* release does not know never refuses it.
|
|
220
|
+
*/
|
|
221
|
+
export function readThreadMessageRows(value) {
|
|
222
|
+
if (!Array.isArray(value))
|
|
223
|
+
return undefined;
|
|
224
|
+
const rows = [];
|
|
225
|
+
for (const entry of value) {
|
|
226
|
+
const row = readThreadMessageRow(entry);
|
|
227
|
+
if (row === undefined)
|
|
228
|
+
return undefined;
|
|
229
|
+
rows.push(row);
|
|
230
|
+
}
|
|
231
|
+
return rows;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* A `getSnapshot` row. Its memory arrays are JSON (checked, never assumed); the
|
|
235
|
+
* thread records are re-read through the one stored-memory reader
|
|
236
|
+
* (`splitStoredThreadMemory`), and any element this release cannot read as a
|
|
237
|
+
* thread record joins the carried elements, so it is kept, never dropped.
|
|
238
|
+
*/
|
|
239
|
+
export function readThreadSnapshotRow(value) {
|
|
240
|
+
if (!isWireObject(value))
|
|
241
|
+
return undefined;
|
|
242
|
+
const { threadId, userId, updatedAt } = value;
|
|
243
|
+
if (typeof threadId !== "string" || typeof userId !== "string" || typeof updatedAt !== "string")
|
|
244
|
+
return undefined;
|
|
245
|
+
const memory = JsonValue.array().safeParse(value.threadMemory);
|
|
246
|
+
if (!memory.success)
|
|
247
|
+
return undefined;
|
|
248
|
+
let carried = [];
|
|
249
|
+
if (value.carriedThreadMemory !== undefined) {
|
|
250
|
+
const parsed = JsonValue.array().safeParse(value.carriedThreadMemory);
|
|
251
|
+
if (!parsed.success)
|
|
252
|
+
return undefined;
|
|
253
|
+
carried = parsed.data;
|
|
254
|
+
}
|
|
255
|
+
let workingState;
|
|
256
|
+
if (value.workingState !== undefined) {
|
|
257
|
+
const parsed = JsonValue.safeParse(value.workingState);
|
|
258
|
+
if (!parsed.success)
|
|
259
|
+
return undefined;
|
|
260
|
+
workingState = parsed.data;
|
|
261
|
+
}
|
|
262
|
+
const split = splitStoredThreadMemory(memory.data);
|
|
263
|
+
const carriedThreadMemory = [...carried, ...split.carriedThreadMemory];
|
|
264
|
+
const lastTurnId = optString(value, "lastTurnId");
|
|
265
|
+
return {
|
|
266
|
+
threadId,
|
|
267
|
+
userId,
|
|
268
|
+
...(workingState !== undefined ? { workingState } : {}),
|
|
269
|
+
threadMemory: split.threadMemory,
|
|
270
|
+
...(value.carriedThreadMemory !== undefined || carriedThreadMemory.length > 0 ? { carriedThreadMemory } : {}),
|
|
271
|
+
...(lastTurnId !== undefined ? { lastTurnId } : {}),
|
|
272
|
+
updatedAt,
|
|
273
|
+
};
|
|
274
|
+
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,33 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Hosted HTTP binding — {@link ThreadPersistencePort} over guuey's thread
|
|
3
|
-
* API, `POST <base>/v1/thread-store/<op>` (guuey#208). The "eject the code,
|
|
4
|
-
* keep your memory" leg of the ejected ladder: point a `ThreadStore` at
|
|
5
|
-
* this binding with an end-user token and your self-hosted agent reads and
|
|
6
|
-
* writes the SAME conversation rows guuey's hosted runtime does.
|
|
7
|
-
*
|
|
8
|
-
* Wire protocol (the `@guuey/state` `HttpKv` shape):
|
|
9
|
-
* - Request: JSON body `{ context: { appId }, args: {...} }`, header
|
|
10
|
-
* `Authorization: Bearer <token>`.
|
|
11
|
-
* - Success: `200 { result }`.
|
|
12
|
-
* - Failure: `4xx/5xx { code, message }` → {@link HttpThreadStoreError}.
|
|
13
|
-
*
|
|
14
|
-
* Auth + tenancy: the token is an END-USER token for the app — one the
|
|
15
|
-
* app's configured identity issuer minted (your own IdP, or guuey's per-app
|
|
16
|
-
* widget issuer via `@guuey/widget-auth`). The server verifies it against
|
|
17
|
-
* that issuer, derives the same `byo_…` userId the hosted runtime would, and
|
|
18
|
-
* scopes every op to `(appId, userId)`: rows you write must carry that
|
|
19
|
-
* userId, threads you touch must belong to it. `context` on the wire is
|
|
20
|
-
* advisory — the token is authoritative. Call {@link scope} to learn the
|
|
21
|
-
* derived userId (and the API's region) before `ensureThread`.
|
|
22
|
-
*
|
|
23
|
-
* Retry policy: reads (`getThread`/`listRecentMessages`/
|
|
24
|
-
* `findByClientMessageId`/`getSnapshot`/`scope`) are idempotent and retried
|
|
25
|
-
* once on a network failure or a 5xx. Writes (`createThread`/`incrementSeq`/
|
|
26
|
-
* `putMessage`/`putSnapshot`) are NEVER retried automatically —
|
|
27
|
-
* `incrementSeq` is not idempotent, and a blind retry after a server-side
|
|
28
|
-
* apply would double-allocate a seq.
|
|
29
|
-
*/
|
|
30
|
-
import type { ThreadMessageRow, ThreadPersistencePort, ThreadRow, ThreadSnapshotRow } from "./rows.js";
|
|
1
|
+
import type { RowBudget, ThreadMessageRow, ThreadPersistencePort, ThreadRow, ThreadSnapshotRow } from "./rows.js";
|
|
31
2
|
/** What the token resolves to server-side — the scope every op is bound to. */
|
|
32
3
|
export interface ThreadScope {
|
|
33
4
|
appId: string;
|
|
@@ -50,7 +21,9 @@ export interface HttpThreadPersistenceOptions {
|
|
|
50
21
|
* Thrown for every non-2xx response and every exhausted network failure.
|
|
51
22
|
* `code` is the server's flat error code (`UNAUTHORIZED`, `FORBIDDEN`,
|
|
52
23
|
* `INVALID_ARGUMENT`, `INVALID_CONTEXT`, `THREAD_NOT_FOUND`, `CONFLICT`,
|
|
53
|
-
* `NOT_FOUND`, `TRANSPORT
|
|
24
|
+
* `NOT_FOUND`, `TRANSPORT`, and the client's own `BAD_RESPONSE` for a 2xx body
|
|
25
|
+
* it cannot read; `incrementSeq`'s `THREAD_CLOSED` is thrown as
|
|
26
|
+
* `ThreadClosedError` instead); `status` is the HTTP status (`0` for a network
|
|
54
27
|
* failure that never produced a response).
|
|
55
28
|
*/
|
|
56
29
|
export declare class HttpThreadStoreError extends Error {
|
|
@@ -61,10 +34,17 @@ export declare class HttpThreadStoreError extends Error {
|
|
|
61
34
|
});
|
|
62
35
|
}
|
|
63
36
|
export declare class HttpThreadPersistence implements ThreadPersistencePort {
|
|
37
|
+
/**
|
|
38
|
+
* guuey#1794: the server stores the rows this binding sends in DynamoDB, so
|
|
39
|
+
* the store sizes them by DynamoDB's own item rules before it sends any.
|
|
40
|
+
*/
|
|
41
|
+
readonly rowBudget: RowBudget;
|
|
64
42
|
private readonly baseUrl;
|
|
65
43
|
private readonly appId;
|
|
66
44
|
private readonly token;
|
|
67
45
|
private readonly fetchImpl;
|
|
46
|
+
/** The token's user, read from `scope` the first time a row lacks its `userId`. */
|
|
47
|
+
private ownerRead;
|
|
68
48
|
constructor(opts: HttpThreadPersistenceOptions);
|
|
69
49
|
/** The `(appId, userId, region)` the server bound this token to. */
|
|
70
50
|
scope(): Promise<ThreadScope>;
|
|
@@ -77,10 +57,33 @@ export declare class HttpThreadPersistence implements ThreadPersistencePort {
|
|
|
77
57
|
getSnapshot(threadId: string): Promise<ThreadSnapshotRow | undefined>;
|
|
78
58
|
putSnapshot(row: ThreadSnapshotRow): Promise<void>;
|
|
79
59
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
60
|
+
* A write: never retried, and its body (the server's `{ result: null }`) is
|
|
61
|
+
* never read: a 2xx means it committed, whatever the body holds. The body is
|
|
62
|
+
* cancelled, not left open, so the connection is released.
|
|
63
|
+
*
|
|
64
|
+
* Releasing is best effort and never awaited: a body stream that errors
|
|
65
|
+
* after the 2xx headers (a connection reset) rejects `cancel()`, and that
|
|
66
|
+
* rejection must not turn a committed write into a thrown one, which a
|
|
67
|
+
* caller would retry into `CONFLICT`. The failure is reported on
|
|
68
|
+
* `console.warn` instead: the write stands, and the stream is already dead.
|
|
69
|
+
*/
|
|
70
|
+
private write;
|
|
71
|
+
/** `row` with its `userId`: the stored one, or the token's user for a legacy row without one. */
|
|
72
|
+
private owned;
|
|
73
|
+
/** The token's user, read once; a failed read is not kept, so the next row asks again. */
|
|
74
|
+
private owner;
|
|
75
|
+
/** A point read: `null` (the port's `undefined`) or the row `read` proves. */
|
|
76
|
+
private pointRead;
|
|
77
|
+
/**
|
|
78
|
+
* The op's `{ result }` envelope, its `result` read by `read`. `undefined`
|
|
79
|
+
* from `read` (the result is not this op's answer) is `BAD_RESPONSE`.
|
|
83
80
|
*/
|
|
84
81
|
private call;
|
|
82
|
+
/**
|
|
83
|
+
* One op on the wire: the 2xx response (its body unread), or the typed error. One shared retry budget (`retried`) covers BOTH retry
|
|
84
|
+
* triggers — a network failure and a 5xx response — so a retryable op makes
|
|
85
|
+
* at most 2 total requests, never 3.
|
|
86
|
+
*/
|
|
87
|
+
private send;
|
|
85
88
|
}
|
|
86
89
|
//# sourceMappingURL=http.d.ts.map
|
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,iBAAiB,EAClB,MAAM,WAAW,CAAC;AAEnB,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,4BAA4B;IAC3C,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKhC;AAqCD,qBAAa,qBAAsB,YAAW,qBAAqB;IACjE;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAuB;IAEpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,mFAAmF;IACnF,OAAO,CAAC,SAAS,CAA8B;gBAEnC,IAAI,EAAE,4BAA4B;IAO9C,oEAAoE;IAC9D,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC;IAI7B,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAI3D,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3C,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAatF,UAAU,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIhD,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAKhF,qBAAqB,CACzB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAKlC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIrE,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMxD;;;;;;;;;;OAUG;YACW,KAAK;IAOnB,iGAAiG;YACnF,KAAK;IAInB,0FAA0F;IAC1F,OAAO,CAAC,KAAK;IAWb,8EAA8E;YAChE,SAAS;IAKvB;;;OAGG;YACW,IAAI;IAgBlB;;;;OAIG;YACW,IAAI;CAoCnB"}
|
package/dist/http.js
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosted HTTP binding — {@link ThreadPersistencePort} over guuey's thread
|
|
3
|
+
* API, `POST <base>/v1/thread-store/<op>` (guuey#208). The "eject the code,
|
|
4
|
+
* keep your memory" leg of the ejected ladder: point a `ThreadStore` at
|
|
5
|
+
* this binding with an end-user token and your self-hosted agent reads and
|
|
6
|
+
* writes the SAME conversation rows guuey's hosted runtime does.
|
|
7
|
+
*
|
|
8
|
+
* Wire protocol (the `@guuey/state` `HttpKv` shape):
|
|
9
|
+
* - Request: JSON body `{ context: { appId }, args: {...} }`, header
|
|
10
|
+
* `Authorization: Bearer <token>`.
|
|
11
|
+
* - Success: `200 { result }`.
|
|
12
|
+
* - Failure: `4xx/5xx { code, message }` → {@link HttpThreadStoreError}.
|
|
13
|
+
* - Every read's body is parsed to `unknown` and read through `http-read.ts`;
|
|
14
|
+
* a 2xx whose body is not an envelope, or whose `result` is not what the op
|
|
15
|
+
* answers, is an {@link HttpThreadStoreError} with code `BAD_RESPONSE`. A
|
|
16
|
+
* write's 2xx body is never read: the write has committed, and a body that
|
|
17
|
+
* does not parse must not turn it into a failure a caller would retry (into
|
|
18
|
+
* `CONFLICT`, since `createThread` and `putMessage` are conditional).
|
|
19
|
+
* - A message row an older writer stored without a `userId` gets the token's
|
|
20
|
+
* user (read once from {@link HttpThreadPersistence.scope}): the server
|
|
21
|
+
* answers only threads that user owns, and every row the port writes there
|
|
22
|
+
* carries that user.
|
|
23
|
+
*
|
|
24
|
+
* Auth + tenancy: the token is an END-USER token for the app — one the
|
|
25
|
+
* app's configured identity issuer minted (your own IdP, or guuey's per-app
|
|
26
|
+
* widget issuer via `@guuey/widget-auth`). The server verifies it against
|
|
27
|
+
* that issuer, derives the same `byo_…` userId the hosted runtime would, and
|
|
28
|
+
* scopes every op to `(appId, userId)`: rows you write must carry that
|
|
29
|
+
* userId, threads you touch must belong to it. `context` on the wire is
|
|
30
|
+
* advisory — the token is authoritative. Call {@link scope} to learn the
|
|
31
|
+
* derived userId (and the API's region) before `ensureThread`.
|
|
32
|
+
*
|
|
33
|
+
* Retry policy: reads (`getThread`/`listRecentMessages`/
|
|
34
|
+
* `findByClientMessageId`/`getSnapshot`/`scope`) are idempotent and retried
|
|
35
|
+
* once on a network failure or a 5xx. Writes (`createThread`/`incrementSeq`/
|
|
36
|
+
* `putMessage`/`putSnapshot`) are NEVER retried automatically —
|
|
37
|
+
* `incrementSeq` is not idempotent, and a blind retry after a server-side
|
|
38
|
+
* apply would double-allocate a seq.
|
|
39
|
+
*/
|
|
40
|
+
import { DYNAMODB_ROW_BUDGET } from "./item-size.js";
|
|
41
|
+
import { ThreadClosedError } from "./closed.js";
|
|
42
|
+
import { isWireObject, readAllocatedSeq, readThreadMessageRow, readThreadMessageRows, readThreadRow, readThreadScope, readThreadSnapshotRow, } from "./http-read.js";
|
|
1
43
|
/**
|
|
2
44
|
* Thrown for every non-2xx response and every exhausted network failure.
|
|
3
45
|
* `code` is the server's flat error code (`UNAUTHORIZED`, `FORBIDDEN`,
|
|
4
46
|
* `INVALID_ARGUMENT`, `INVALID_CONTEXT`, `THREAD_NOT_FOUND`, `CONFLICT`,
|
|
5
|
-
* `NOT_FOUND`, `TRANSPORT
|
|
47
|
+
* `NOT_FOUND`, `TRANSPORT`, and the client's own `BAD_RESPONSE` for a 2xx body
|
|
48
|
+
* it cannot read; `incrementSeq`'s `THREAD_CLOSED` is thrown as
|
|
49
|
+
* `ThreadClosedError` instead); `status` is the HTTP status (`0` for a network
|
|
6
50
|
* failure that never produced a response).
|
|
7
51
|
*/
|
|
8
52
|
export class HttpThreadStoreError extends Error {
|
|
@@ -15,21 +59,38 @@ export class HttpThreadStoreError extends Error {
|
|
|
15
59
|
this.name = "HttpThreadStoreError";
|
|
16
60
|
}
|
|
17
61
|
}
|
|
18
|
-
async function
|
|
19
|
-
let body;
|
|
62
|
+
async function parsedBody(res) {
|
|
20
63
|
try {
|
|
21
|
-
|
|
64
|
+
const value = await res.json();
|
|
65
|
+
return { parsed: true, value };
|
|
22
66
|
}
|
|
23
|
-
catch {
|
|
24
|
-
|
|
67
|
+
catch (cause) {
|
|
68
|
+
return { parsed: false, cause };
|
|
25
69
|
}
|
|
26
|
-
|
|
70
|
+
}
|
|
71
|
+
async function toError(res) {
|
|
72
|
+
// `{ code, message }`, each read only when it is a string: a body that is
|
|
73
|
+
// not one (not JSON, a gateway's HTML page) keeps the transport defaults.
|
|
74
|
+
const body = await parsedBody(res);
|
|
75
|
+
const env = body.parsed && isWireObject(body.value) ? body.value : undefined;
|
|
76
|
+
return new HttpThreadStoreError(res.status, typeof env?.code === "string" ? env.code : "TRANSPORT", typeof env?.message === "string" ? env.message : `guuey thread API error (HTTP ${res.status})`, body.parsed ? undefined : { cause: body.cause });
|
|
77
|
+
}
|
|
78
|
+
/** The refusal for a 2xx whose body the op cannot read. */
|
|
79
|
+
function badResponse(res, op, what, cause) {
|
|
80
|
+
return new HttpThreadStoreError(res.status, "BAD_RESPONSE", `guuey thread API ${op}: ${what} (HTTP ${res.status})`, cause !== undefined ? { cause } : undefined);
|
|
27
81
|
}
|
|
28
82
|
export class HttpThreadPersistence {
|
|
83
|
+
/**
|
|
84
|
+
* guuey#1794: the server stores the rows this binding sends in DynamoDB, so
|
|
85
|
+
* the store sizes them by DynamoDB's own item rules before it sends any.
|
|
86
|
+
*/
|
|
87
|
+
rowBudget = DYNAMODB_ROW_BUDGET;
|
|
29
88
|
baseUrl;
|
|
30
89
|
appId;
|
|
31
90
|
token;
|
|
32
91
|
fetchImpl;
|
|
92
|
+
/** The token's user, read from `scope` the first time a row lacks its `userId`. */
|
|
93
|
+
ownerRead;
|
|
33
94
|
constructor(opts) {
|
|
34
95
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
35
96
|
this.appId = opts.appId;
|
|
@@ -38,39 +99,102 @@ export class HttpThreadPersistence {
|
|
|
38
99
|
}
|
|
39
100
|
/** The `(appId, userId, region)` the server bound this token to. */
|
|
40
101
|
async scope() {
|
|
41
|
-
return this.call("scope", {}, true);
|
|
102
|
+
return this.call("scope", {}, true, readThreadScope);
|
|
42
103
|
}
|
|
43
104
|
async getThread(threadId) {
|
|
44
|
-
return
|
|
105
|
+
return this.pointRead("getThread", { threadId }, readThreadRow);
|
|
45
106
|
}
|
|
46
107
|
async createThread(row) {
|
|
47
|
-
await this.
|
|
108
|
+
await this.write("createThread", { row });
|
|
48
109
|
}
|
|
49
110
|
async incrementSeq(threadId, preview, atIso) {
|
|
50
|
-
|
|
111
|
+
try {
|
|
112
|
+
return await this.call("incrementSeq", { threadId, preview, atIso }, false, readAllocatedSeq);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
// The server's word for a closed thread (guuey#2355) becomes the typed
|
|
116
|
+
// refusal every binding gives, so a ThreadStore reads it the same way.
|
|
117
|
+
if (err instanceof HttpThreadStoreError && err.code === "THREAD_CLOSED") {
|
|
118
|
+
throw new ThreadClosedError(threadId);
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
51
122
|
}
|
|
52
123
|
async putMessage(row) {
|
|
53
|
-
await this.
|
|
124
|
+
await this.write("putMessage", { row });
|
|
54
125
|
}
|
|
55
126
|
async listRecentMessages(threadId, limit) {
|
|
56
|
-
|
|
127
|
+
const rows = await this.call("listRecentMessages", { threadId, limit }, true, readThreadMessageRows);
|
|
128
|
+
return Promise.all(rows.map((row) => this.owned(row)));
|
|
57
129
|
}
|
|
58
130
|
async findByClientMessageId(threadId, clientMessageId) {
|
|
59
|
-
|
|
131
|
+
const row = await this.pointRead("findByClientMessageId", { threadId, clientMessageId }, readThreadMessageRow);
|
|
132
|
+
return row === undefined ? undefined : this.owned(row);
|
|
60
133
|
}
|
|
61
134
|
async getSnapshot(threadId) {
|
|
62
|
-
return
|
|
135
|
+
return this.pointRead("getSnapshot", { threadId }, readThreadSnapshotRow);
|
|
63
136
|
}
|
|
64
137
|
async putSnapshot(row) {
|
|
65
|
-
await this.
|
|
138
|
+
await this.write("putSnapshot", { row });
|
|
66
139
|
}
|
|
67
140
|
// ── internals ──────────────────────────────────────────────────────
|
|
68
141
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
142
|
+
* A write: never retried, and its body (the server's `{ result: null }`) is
|
|
143
|
+
* never read: a 2xx means it committed, whatever the body holds. The body is
|
|
144
|
+
* cancelled, not left open, so the connection is released.
|
|
145
|
+
*
|
|
146
|
+
* Releasing is best effort and never awaited: a body stream that errors
|
|
147
|
+
* after the 2xx headers (a connection reset) rejects `cancel()`, and that
|
|
148
|
+
* rejection must not turn a committed write into a thrown one, which a
|
|
149
|
+
* caller would retry into `CONFLICT`. The failure is reported on
|
|
150
|
+
* `console.warn` instead: the write stands, and the stream is already dead.
|
|
151
|
+
*/
|
|
152
|
+
async write(op, args) {
|
|
153
|
+
const res = await this.send(op, args, false);
|
|
154
|
+
void res.body?.cancel().catch((err) => {
|
|
155
|
+
console.warn(`@guuey/threads: ${op} committed, but releasing its response body failed`, err);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** `row` with its `userId`: the stored one, or the token's user for a legacy row without one. */
|
|
159
|
+
async owned(row) {
|
|
160
|
+
return { ...row, userId: row.userId ?? (await this.owner()) };
|
|
161
|
+
}
|
|
162
|
+
/** The token's user, read once; a failed read is not kept, so the next row asks again. */
|
|
163
|
+
owner() {
|
|
164
|
+
this.ownerRead ??= this.scope().then((scope) => scope.userId, (err) => {
|
|
165
|
+
this.ownerRead = undefined;
|
|
166
|
+
throw err;
|
|
167
|
+
});
|
|
168
|
+
return this.ownerRead;
|
|
169
|
+
}
|
|
170
|
+
/** A point read: `null` (the port's `undefined`) or the row `read` proves. */
|
|
171
|
+
async pointRead(op, args, read) {
|
|
172
|
+
const row = await this.call(op, args, true, (value) => (value === null ? null : read(value)));
|
|
173
|
+
return row ?? undefined;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The op's `{ result }` envelope, its `result` read by `read`. `undefined`
|
|
177
|
+
* from `read` (the result is not this op's answer) is `BAD_RESPONSE`.
|
|
178
|
+
*/
|
|
179
|
+
async call(op, args, retryable, read) {
|
|
180
|
+
const res = await this.send(op, args, retryable);
|
|
181
|
+
const parsed = await parsedBody(res);
|
|
182
|
+
if (!parsed.parsed)
|
|
183
|
+
throw badResponse(res, op, "the body is not JSON", parsed.cause);
|
|
184
|
+
const body = parsed.value;
|
|
185
|
+
if (!isWireObject(body) || !("result" in body))
|
|
186
|
+
throw badResponse(res, op, "the body is not a { result } envelope");
|
|
187
|
+
const value = read(body.result);
|
|
188
|
+
if (value === undefined)
|
|
189
|
+
throw badResponse(res, op, "the result does not read as this op's answer");
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* One op on the wire: the 2xx response (its body unread), or the typed error. One shared retry budget (`retried`) covers BOTH retry
|
|
194
|
+
* triggers — a network failure and a 5xx response — so a retryable op makes
|
|
195
|
+
* at most 2 total requests, never 3.
|
|
72
196
|
*/
|
|
73
|
-
async
|
|
197
|
+
async send(op, args, retryable) {
|
|
74
198
|
const doOnce = async () => this.fetchImpl(`${this.baseUrl}/v1/thread-store/${op}`, {
|
|
75
199
|
method: "POST",
|
|
76
200
|
headers: {
|
|
@@ -100,7 +224,6 @@ export class HttpThreadPersistence {
|
|
|
100
224
|
}
|
|
101
225
|
if (!res.ok)
|
|
102
226
|
throw await toError(res);
|
|
103
|
-
|
|
104
|
-
return body.result;
|
|
227
|
+
return res;
|
|
105
228
|
}
|
|
106
229
|
}
|
package/dist/in-memory.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* InMemoryThreadPersistence — the dev/test binding of
|
|
3
|
-
* {@link ThreadPersistencePort}. Same contract as any real binding (run
|
|
4
|
-
* the suite in `@guuey/threads/testing` to prove yours), including the
|
|
5
|
-
* two conditional-write guards and the null-preview no-touch semantics.
|
|
6
|
-
*/
|
|
7
|
-
import type { ThreadMessageRow, ThreadPersistencePort, ThreadRow, ThreadSnapshotRow } from "./rows.js";
|
|
1
|
+
import type { ThreadClosure, ThreadMessageRow, ThreadPersistencePort, ThreadRow, ThreadSnapshotRow } from "./rows.js";
|
|
8
2
|
export declare class InMemoryThreadPersistence implements ThreadPersistencePort {
|
|
9
3
|
readonly threads: Map<string, ThreadRow>;
|
|
10
4
|
readonly messages: ThreadMessageRow[];
|
|
@@ -12,6 +6,7 @@ export declare class InMemoryThreadPersistence implements ThreadPersistencePort
|
|
|
12
6
|
getThread(threadId: string): Promise<ThreadRow | undefined>;
|
|
13
7
|
createThread(row: ThreadRow): Promise<void>;
|
|
14
8
|
incrementSeq(threadId: string, preview: string | null, atIso: string): Promise<number>;
|
|
9
|
+
closeThread(threadId: string, closure: ThreadClosure): Promise<"closed" | "already_closed">;
|
|
15
10
|
putMessage(row: ThreadMessageRow): Promise<void>;
|
|
16
11
|
listRecentMessages(threadId: string, limit: number): Promise<ThreadMessageRow[]>;
|
|
17
12
|
findByClientMessageId(threadId: string, clientMessageId: string): Promise<ThreadMessageRow | undefined>;
|
package/dist/in-memory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-memory.d.ts","sourceRoot":"","sources":["../src/in-memory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"in-memory.d.ts","sourceRoot":"","sources":["../src/in-memory.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,qBAAqB,EACrB,SAAS,EACT,iBAAiB,EAClB,MAAM,WAAW,CAAC;AAEnB,qBAAa,yBAA0B,YAAW,qBAAqB;IACrE,QAAQ,CAAC,OAAO,yBAAgC;IAChD,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,EAAE,CAAM;IAC3C,QAAQ,CAAC,SAAS,iCAAwC;IAEpD,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAI3D,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3C,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBtF,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,GAAG,gBAAgB,CAAC;IAU3F,UAAU,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOhD,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAQhF,qBAAqB,CACzB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAMlC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIrE,WAAW,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;CAGzD"}
|