@stapel/chat-react 0.1.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/CHANGELOG.md +1 -0
- package/MODULE.md +118 -0
- package/README.md +126 -0
- package/dist/api/chatApi.d.ts +49 -0
- package/dist/api/chatApi.d.ts.map +1 -0
- package/dist/api/chatApi.js +46 -0
- package/dist/api/chatApi.js.map +1 -0
- package/dist/api/extensions.d.ts +19 -0
- package/dist/api/extensions.d.ts.map +1 -0
- package/dist/api/extensions.js +19 -0
- package/dist/api/extensions.js.map +1 -0
- package/dist/api/generated/schema.d.ts +547 -0
- package/dist/api/generated/schema.d.ts.map +1 -0
- package/dist/api/generated/schema.js +2 -0
- package/dist/api/generated/schema.js.map +1 -0
- package/dist/api/types.d.ts +75 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/default/ConversationListPanel.d.ts +22 -0
- package/dist/default/ConversationListPanel.d.ts.map +1 -0
- package/dist/default/ConversationListPanel.js +49 -0
- package/dist/default/ConversationListPanel.js.map +1 -0
- package/dist/default/ConversationThreadPanel.d.ts +21 -0
- package/dist/default/ConversationThreadPanel.d.ts.map +1 -0
- package/dist/default/ConversationThreadPanel.js +51 -0
- package/dist/default/ConversationThreadPanel.js.map +1 -0
- package/dist/default/ErrorAlert.d.ts +8 -0
- package/dist/default/ErrorAlert.d.ts.map +1 -0
- package/dist/default/ErrorAlert.js +26 -0
- package/dist/default/ErrorAlert.js.map +1 -0
- package/dist/default/StartChatButton.d.ts +17 -0
- package/dist/default/StartChatButton.d.ts.map +1 -0
- package/dist/default/StartChatButton.js +18 -0
- package/dist/default/StartChatButton.js.map +1 -0
- package/dist/default/index.d.ts +21 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +18 -0
- package/dist/default/index.js.map +1 -0
- package/dist/flows/errors.d.ts +12 -0
- package/dist/flows/errors.d.ts.map +1 -0
- package/dist/flows/errors.js +13 -0
- package/dist/flows/errors.js.map +1 -0
- package/dist/flows/freshness.d.ts +69 -0
- package/dist/flows/freshness.d.ts.map +1 -0
- package/dist/flows/freshness.js +227 -0
- package/dist/flows/freshness.js.map +1 -0
- package/dist/flows/registry.d.ts +26 -0
- package/dist/flows/registry.d.ts.map +1 -0
- package/dist/flows/registry.js +24 -0
- package/dist/flows/registry.js.map +1 -0
- package/dist/headless/ChatProvider.d.ts +22 -0
- package/dist/headless/ChatProvider.d.ts.map +1 -0
- package/dist/headless/ChatProvider.js +18 -0
- package/dist/headless/ChatProvider.js.map +1 -0
- package/dist/headless/ConversationList.d.ts +51 -0
- package/dist/headless/ConversationList.d.ts.map +1 -0
- package/dist/headless/ConversationList.js +47 -0
- package/dist/headless/ConversationList.js.map +1 -0
- package/dist/headless/ConversationThread.d.ts +50 -0
- package/dist/headless/ConversationThread.d.ts.map +1 -0
- package/dist/headless/ConversationThread.js +64 -0
- package/dist/headless/ConversationThread.js.map +1 -0
- package/dist/headless/MessageComposer.d.ts +39 -0
- package/dist/headless/MessageComposer.d.ts.map +1 -0
- package/dist/headless/MessageComposer.js +45 -0
- package/dist/headless/MessageComposer.js.map +1 -0
- package/dist/headless/StartDirectChat.d.ts +40 -0
- package/dist/headless/StartDirectChat.d.ts.map +1 -0
- package/dist/headless/StartDirectChat.js +46 -0
- package/dist/headless/StartDirectChat.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +10 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +20 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +23 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +75 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.es.gen.js +58 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +359 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +183 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +58 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +78 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +130 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +28 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +80 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/model/context.d.ts +11 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +16 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/limits.d.ts +12 -0
- package/dist/model/limits.d.ts.map +1 -0
- package/dist/model/limits.js +12 -0
- package/dist/model/limits.js.map +1 -0
- package/dist/model/mutations.d.ts +66 -0
- package/dist/model/mutations.d.ts.map +1 -0
- package/dist/model/mutations.js +156 -0
- package/dist/model/mutations.js.map +1 -0
- package/dist/model/queries.d.ts +33 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +93 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +8 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +26 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/readMarker.d.ts +25 -0
- package/dist/model/readMarker.d.ts.map +1 -0
- package/dist/model/readMarker.js +31 -0
- package/dist/model/readMarker.js.map +1 -0
- package/dist/model/runtime.d.ts +50 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +27 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/model/threadWindow.d.ts +95 -0
- package/dist/model/threadWindow.d.ts.map +1 -0
- package/dist/model/threadWindow.js +100 -0
- package/dist/model/threadWindow.js.map +1 -0
- package/dist/nav/manifest.d.ts +23 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +15 -0
- package/dist/nav/manifest.js.map +1 -0
- package/dist/realtime/chatSocket.d.ts +93 -0
- package/dist/realtime/chatSocket.d.ts.map +1 -0
- package/dist/realtime/chatSocket.js +199 -0
- package/dist/realtime/chatSocket.js.map +1 -0
- package/dist/realtime/frames.d.ts +123 -0
- package/dist/realtime/frames.d.ts.map +1 -0
- package/dist/realtime/frames.js +123 -0
- package/dist/realtime/frames.js.map +1 -0
- package/dist/realtime/streams.d.ts +49 -0
- package/dist/realtime/streams.d.ts.map +1 -0
- package/dist/realtime/streams.js +52 -0
- package/dist/realtime/streams.js.map +1 -0
- package/llms.txt +89 -0
- package/manifest.json +733 -0
- package/nav-manifest.json +25 -0
- package/package.json +114 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/chatApi.ts +142 -0
- package/src/api/extensions.ts +18 -0
- package/src/api/generated/schema.ts +549 -0
- package/src/api/types.ts +87 -0
- package/src/default/ConversationListPanel.tsx +163 -0
- package/src/default/ConversationThreadPanel.tsx +245 -0
- package/src/default/ErrorAlert.tsx +43 -0
- package/src/default/StartChatButton.tsx +78 -0
- package/src/default/index.ts +20 -0
- package/src/flows/errors.ts +16 -0
- package/src/flows/freshness.ts +315 -0
- package/src/flows/registry.ts +36 -0
- package/src/headless/ChatProvider.tsx +23 -0
- package/src/headless/ConversationList.tsx +92 -0
- package/src/headless/ConversationThread.tsx +107 -0
- package/src/headless/MessageComposer.tsx +86 -0
- package/src/headless/StartDirectChat.tsx +75 -0
- package/src/i18n/errorsMap.ts +31 -0
- package/src/i18n/es.ts +95 -0
- package/src/i18n/generated/errors.es.gen.ts +64 -0
- package/src/i18n/generated/errors.gen.ts +214 -0
- package/src/i18n/generated/errors.json +408 -0
- package/src/i18n/generated/errors.ru.gen.ts +64 -0
- package/src/i18n/keys.ts +152 -0
- package/src/i18n/ru.ts +98 -0
- package/src/index.ts +189 -0
- package/src/model/context.tsx +25 -0
- package/src/model/limits.ts +11 -0
- package/src/model/mutations.ts +208 -0
- package/src/model/queries.ts +120 -0
- package/src/model/queryKeys.ts +32 -0
- package/src/model/readMarker.ts +32 -0
- package/src/model/runtime.ts +90 -0
- package/src/model/threadWindow.ts +173 -0
- package/src/nav/manifest.ts +36 -0
- package/src/realtime/chatSocket.ts +304 -0
- package/src/realtime/frames.ts +229 -0
- package/src/realtime/streams.ts +85 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const EMPTY_THREAD_WINDOW = {
|
|
2
|
+
messages: [],
|
|
3
|
+
hasOlder: false,
|
|
4
|
+
olderAnchor: null,
|
|
5
|
+
};
|
|
6
|
+
function ascending(items) {
|
|
7
|
+
return [...items].sort((a, b) => a.seq - b.seq);
|
|
8
|
+
}
|
|
9
|
+
/** The tip: the highest `seq` in the window, or 0 for an empty one. */
|
|
10
|
+
export function threadLastSeq(window) {
|
|
11
|
+
const last = window.messages[window.messages.length - 1];
|
|
12
|
+
return last ? last.seq : 0;
|
|
13
|
+
}
|
|
14
|
+
/** The oldest `seq` in the window, or 0 for an empty one. */
|
|
15
|
+
export function threadFirstSeq(window) {
|
|
16
|
+
const first = window.messages[0];
|
|
17
|
+
return first ? first.seq : 0;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A fresh window from the newest page (`GET …/messages` with no anchor).
|
|
21
|
+
* This is also the resync path: whatever was loaded before is dropped,
|
|
22
|
+
* because a window that cannot be proven contiguous is worse than a short
|
|
23
|
+
* one.
|
|
24
|
+
*/
|
|
25
|
+
export function threadWindowFromPage(page) {
|
|
26
|
+
return {
|
|
27
|
+
messages: ascending(page.items),
|
|
28
|
+
hasOlder: page.has_next,
|
|
29
|
+
olderAnchor: page.next_anchor ?? null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Merge a backfill page (`direction=next` from the window's oldest seq) onto
|
|
34
|
+
* the FRONT. Anything at or above the current oldest is dropped as a
|
|
35
|
+
* duplicate — the anchor is exclusive, so this only fires on a retry.
|
|
36
|
+
*/
|
|
37
|
+
export function mergeOlderPage(window, page) {
|
|
38
|
+
const first = threadFirstSeq(window);
|
|
39
|
+
const older = ascending(page.items).filter((message) => first === 0 || message.seq < first);
|
|
40
|
+
if (older.length === 0) {
|
|
41
|
+
return { ...window, hasOlder: page.has_next, olderAnchor: page.next_anchor ?? null };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
messages: [...older, ...window.messages],
|
|
45
|
+
hasOlder: page.has_next,
|
|
46
|
+
olderAnchor: page.next_anchor ?? null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Merge a tail page (`direction=prev` anchored on the window's tip) onto the
|
|
51
|
+
* END — the poll-by-seq step.
|
|
52
|
+
*
|
|
53
|
+
* Two independent hole detectors, because either one alone can be fooled:
|
|
54
|
+
* - the seq test (`first new seq === tip + 1`), which is only meaningful
|
|
55
|
+
* because seq is gapless;
|
|
56
|
+
* - the paginator's own `has_prev`, which is how a window that was TRUNCATED
|
|
57
|
+
* (more messages above the anchor than `limit`) says so.
|
|
58
|
+
*/
|
|
59
|
+
export function mergeNewerPage(window, page) {
|
|
60
|
+
const tip = threadLastSeq(window);
|
|
61
|
+
const newer = ascending(page.items).filter((message) => message.seq > tip);
|
|
62
|
+
if (newer.length === 0) {
|
|
63
|
+
// Nothing new. `has_prev` cannot be trusted to mean anything here — there
|
|
64
|
+
// is no window to be truncated — so this is simply "still up to date".
|
|
65
|
+
return { window, gap: false };
|
|
66
|
+
}
|
|
67
|
+
const firstNew = newer[0];
|
|
68
|
+
// `tip + 1` also covers the empty window: the first message of a
|
|
69
|
+
// conversation is seq 1, so a page starting at 5 into an empty thread is a
|
|
70
|
+
// hole, not a beginning.
|
|
71
|
+
const contiguous = firstNew !== undefined && firstNew.seq === tip + 1;
|
|
72
|
+
if (!contiguous || page.has_prev) {
|
|
73
|
+
return { window, gap: true };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
window: { ...window, messages: [...window.messages, ...newer] },
|
|
77
|
+
gap: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Merge ONE message onto the end — a live socket frame, or the row a `send`
|
|
82
|
+
* just returned.
|
|
83
|
+
*
|
|
84
|
+
* Already-known seq is dropped (the socket dedupes too, but the sender's own
|
|
85
|
+
* REST answer and the frame it fans out to itself are two paths to the same
|
|
86
|
+
* row, and only this rule makes them idempotent). A seq beyond the tip + 1 is
|
|
87
|
+
* a hole: the window is left untouched and the caller re-hydrates.
|
|
88
|
+
*/
|
|
89
|
+
export function mergeMessage(window, message) {
|
|
90
|
+
const tip = threadLastSeq(window);
|
|
91
|
+
if (message.seq <= tip)
|
|
92
|
+
return { window, gap: false };
|
|
93
|
+
if (message.seq !== tip + 1)
|
|
94
|
+
return { window, gap: true };
|
|
95
|
+
return {
|
|
96
|
+
window: { ...window, messages: [...window.messages, message] },
|
|
97
|
+
gap: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=threadWindow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threadWindow.js","sourceRoot":"","sources":["../../src/model/threadWindow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This pair's contribution to the scripted-fullstack nav contract
|
|
3
|
+
* (`@stapel/core`'s `NavEntry`/`PackageNavManifest`).
|
|
4
|
+
* `scripts/gen-nav-manifest.mjs` reads `navEntries` below, stamps
|
|
5
|
+
* `package`/`version` from THIS package's own `package.json`, and emits
|
|
6
|
+
* `packages/chat-react/nav-manifest.json` plus this package's slice of the
|
|
7
|
+
* root aggregate.
|
|
8
|
+
*
|
|
9
|
+
* ONE ENTRY, MEMBER-ONLY. Chat is a member surface end to end: every endpoint
|
|
10
|
+
* is `IsAuthenticated`, and the socket closes 4401 without a session. The
|
|
11
|
+
* public storefront links INTO it ("message the seller"), but the link lands
|
|
12
|
+
* on a member route — which is why `surface` is stated explicitly here rather
|
|
13
|
+
* than left to be derived: a public container that forgot the audience filter
|
|
14
|
+
* would otherwise mount an inbox for visitors who have none.
|
|
15
|
+
*
|
|
16
|
+
* The thread route (`/…/chat/:id`) is deliberately NOT an entry: it is not a
|
|
17
|
+
* menu destination, and the nav contract addresses routes by static path. A
|
|
18
|
+
* container mounts `<ConversationThreadPanel conversationId={…}/>` under its
|
|
19
|
+
* own `:id` child route.
|
|
20
|
+
*/
|
|
21
|
+
import type { NavEntry } from "@stapel/core";
|
|
22
|
+
export declare const navEntries: readonly NavEntry[];
|
|
23
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/nav/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAazC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const navEntries = [
|
|
2
|
+
{
|
|
3
|
+
id: "chat.conversations",
|
|
4
|
+
labelKey: "chat.nav.conversations",
|
|
5
|
+
icon: "MessageOutlined",
|
|
6
|
+
route: { path: "chat" },
|
|
7
|
+
component: { export: "ConversationListPanel", subpath: "default" },
|
|
8
|
+
placement: { level: "top" },
|
|
9
|
+
menuVisibleDefault: true,
|
|
10
|
+
requiresAuth: true,
|
|
11
|
+
surface: "member",
|
|
12
|
+
order: 25,
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/nav/manifest.ts"],"names":[],"mappings":"AAsBA,MAAM,CAAC,MAAM,UAAU,GAAwB;IAC7C;QACE,EAAE,EAAE,oBAAoB;QACxB,QAAQ,EAAE,wBAAwB;QAClC,IAAI,EAAE,iBAAiB;QACvB,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;QACvB,SAAS,EAAE,EAAE,MAAM,EAAE,uBAAuB,EAAE,OAAO,EAAE,SAAS,EAAE;QAClE,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;QAC3B,kBAAkB,EAAE,IAAI;QACxB,YAAY,EAAE,IAAI;QAClB,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,EAAE;KACV;CACF,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ChatServerFrame } from "./frames.js";
|
|
2
|
+
/**
|
|
3
|
+
* Connection state, named as the realtime spec (§7) names it, so the
|
|
4
|
+
* substrate migration is a rename of nothing.
|
|
5
|
+
*
|
|
6
|
+
* - `connecting` — a socket is being opened (including a reconnect wait),
|
|
7
|
+
* - `open` — connected; frames are arriving,
|
|
8
|
+
* - `degraded` — the socket dropped and a reconnect is pending,
|
|
9
|
+
* - `closed` — it will not come back on its own (deliberate close, a
|
|
10
|
+
* refusal, or the retry budget spent).
|
|
11
|
+
*/
|
|
12
|
+
export type ChatConnectionState = "connecting" | "open" | "degraded" | "closed";
|
|
13
|
+
/** Why the socket will not come back. `undefined` while it still might. */
|
|
14
|
+
export type ChatSocketRefusal = "unauthenticated" | "not_participant" | "unreachable";
|
|
15
|
+
export interface ChatSocketStatus {
|
|
16
|
+
readonly state: ChatConnectionState;
|
|
17
|
+
/** Set only in `closed`, and only when the reason is known. */
|
|
18
|
+
readonly refusal: ChatSocketRefusal | undefined;
|
|
19
|
+
/** Consecutive failed connects since the last `open`. */
|
|
20
|
+
readonly attempt: number;
|
|
21
|
+
}
|
|
22
|
+
/** The callbacks the transport hands back to this client. */
|
|
23
|
+
export interface ChatSocketHandlers {
|
|
24
|
+
readonly onOpen: () => void;
|
|
25
|
+
/** One decoded payload (the `data` of a message event). */
|
|
26
|
+
readonly onData: (data: unknown) => void;
|
|
27
|
+
readonly onClose: (code: number) => void;
|
|
28
|
+
readonly onError: () => void;
|
|
29
|
+
}
|
|
30
|
+
/** What a transport must offer: send text, close. */
|
|
31
|
+
export interface ChatSocketConnection {
|
|
32
|
+
send(payload: string): void;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Opens one socket. Injectable so tests drive the protocol without a network
|
|
37
|
+
* and a host can wrap the socket (instrumentation, a proxy, React Native).
|
|
38
|
+
*/
|
|
39
|
+
export type ChatWebSocketFactory = (url: string, handlers: ChatSocketHandlers) => ChatSocketConnection;
|
|
40
|
+
/** Reconnect backoff knobs (exponential, jittered, capped). */
|
|
41
|
+
export interface ChatReconnectOptions {
|
|
42
|
+
readonly baseDelayMs?: number;
|
|
43
|
+
readonly maxDelayMs?: number;
|
|
44
|
+
/** Consecutive failures after which the socket gives up (and the caller
|
|
45
|
+
* falls back to polling). Default 6. */
|
|
46
|
+
readonly maxAttempts?: number;
|
|
47
|
+
}
|
|
48
|
+
/** Cancel handle for a scheduled callback. */
|
|
49
|
+
export type ChatCancel = () => void;
|
|
50
|
+
export interface ChatSocketOptions {
|
|
51
|
+
/** Fully-qualified socket URL, e.g. `wss://host/ws/chat/<uuid>`. */
|
|
52
|
+
readonly url: string;
|
|
53
|
+
/**
|
|
54
|
+
* The highest seq the CONSUMER holds, read afresh at every connect. A
|
|
55
|
+
* function, not a value, because the consumer keeps advancing while the
|
|
56
|
+
* socket is down — which is the whole point of resuming by seq.
|
|
57
|
+
*/
|
|
58
|
+
readonly lastSeq: () => number;
|
|
59
|
+
readonly onFrame: (frame: ChatServerFrame) => void;
|
|
60
|
+
readonly onStatus: (status: ChatSocketStatus) => void;
|
|
61
|
+
readonly webSocket?: ChatWebSocketFactory;
|
|
62
|
+
readonly reconnect?: ChatReconnectOptions;
|
|
63
|
+
/** Injectable timer (tests). Returns its own cancel. */
|
|
64
|
+
readonly schedule?: (fn: () => void, ms: number) => ChatCancel;
|
|
65
|
+
/** Injectable jitter source (tests). */
|
|
66
|
+
readonly random?: () => number;
|
|
67
|
+
}
|
|
68
|
+
export interface ChatSocket {
|
|
69
|
+
/** Current status (also pushed through `onStatus`). */
|
|
70
|
+
status(): ChatSocketStatus;
|
|
71
|
+
/** Acknowledge delivery up to `seq` (the server tracks it; nothing
|
|
72
|
+
* depends on it — a dropped ack costs nothing). */
|
|
73
|
+
ack(seq: number): void;
|
|
74
|
+
/** Liveness probe. */
|
|
75
|
+
ping(): void;
|
|
76
|
+
/** Close deliberately: no reconnect, no further callbacks. */
|
|
77
|
+
close(): void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The browser transport. Uses `addEventListener` rather than the `on*`
|
|
81
|
+
* properties so the structural contract above stays independent of the DOM
|
|
82
|
+
* `WebSocket` type (and so a host wrapper can be a plain object).
|
|
83
|
+
*/
|
|
84
|
+
export declare function browserWebSocketFactory(url: string, handlers: ChatSocketHandlers): ChatSocketConnection;
|
|
85
|
+
/** True when the environment can open a socket at all (SSR / node cannot). */
|
|
86
|
+
export declare function canOpenWebSocket(): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Open a resumable conversation socket. Connects immediately; returns the
|
|
89
|
+
* handle that closes it. Every state change is pushed through `onStatus`, so
|
|
90
|
+
* a React layer can render the transport without polling this object.
|
|
91
|
+
*/
|
|
92
|
+
export declare function createChatSocket(options: ChatSocketOptions): ChatSocket;
|
|
93
|
+
//# sourceMappingURL=chatSocket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatSocket.d.ts","sourceRoot":"","sources":["../../src/realtime/chatSocket.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A resumable client for stapel-chat's own socket protocol.
|
|
3
|
+
*
|
|
4
|
+
* The module is store-first and transport-thin: the socket never owns state,
|
|
5
|
+
* it relays the durable `seq`-ordered journal (`consumers.py` header). This
|
|
6
|
+
* client is the mirror image of that discipline —
|
|
7
|
+
*
|
|
8
|
+
* - **resume, not subscribe.** Every (re)connect opens with
|
|
9
|
+
* `hello{last_seq}` carrying the seq the CONSUMER already holds, so the
|
|
10
|
+
* server replays exactly what was missed and then goes live. A reconnect
|
|
11
|
+
* after five seconds or five minutes is the same operation.
|
|
12
|
+
* - **idempotent by seq.** The server drops frames it has already sent; this
|
|
13
|
+
* client drops frames whose `seq` it has already surfaced. The
|
|
14
|
+
* replay/live overlap after a resume is therefore invisible to the
|
|
15
|
+
* consumer, on both ends of the wire.
|
|
16
|
+
* - **a gap is not a stall.** Past `REPLAY_LIMIT` the server answers
|
|
17
|
+
* `error{resync}`; this client forwards it verbatim and stops pretending
|
|
18
|
+
* to be up to date. Re-hydration is the consumer's job (it owns the REST
|
|
19
|
+
* history), which is what keeps correctness independent of delivery.
|
|
20
|
+
* - **a refusal is not a retry.** Close codes 4401 (unauthenticated) and
|
|
21
|
+
* 4403 (not a participant) are answers, not faults: reconnecting would
|
|
22
|
+
* hammer the host with a question it already declined. Everything else is
|
|
23
|
+
* a fault and reconnects with exponential backoff + jitter.
|
|
24
|
+
*
|
|
25
|
+
* No React, no `@stapel/core`, no DOM assumptions beyond an injectable socket
|
|
26
|
+
* factory — the future `@stapel/realtime` substrate is meant to replace this
|
|
27
|
+
* file wholesale, so nothing above it may reach into it.
|
|
28
|
+
*/
|
|
29
|
+
import { decodeServerFrame } from "./frames.js";
|
|
30
|
+
import { CHAT_WS_CLOSE_NOT_PARTICIPANT, CHAT_WS_CLOSE_UNAUTHENTICATED, } from "./frames.js";
|
|
31
|
+
const DEFAULT_BASE_DELAY_MS = 500;
|
|
32
|
+
const DEFAULT_MAX_DELAY_MS = 15_000;
|
|
33
|
+
const DEFAULT_MAX_ATTEMPTS = 6;
|
|
34
|
+
function defaultSchedule(fn, ms) {
|
|
35
|
+
const handle = setTimeout(fn, ms);
|
|
36
|
+
return () => {
|
|
37
|
+
clearTimeout(handle);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The browser transport. Uses `addEventListener` rather than the `on*`
|
|
42
|
+
* properties so the structural contract above stays independent of the DOM
|
|
43
|
+
* `WebSocket` type (and so a host wrapper can be a plain object).
|
|
44
|
+
*/
|
|
45
|
+
export function browserWebSocketFactory(url, handlers) {
|
|
46
|
+
const socket = new WebSocket(url);
|
|
47
|
+
socket.addEventListener("open", () => {
|
|
48
|
+
handlers.onOpen();
|
|
49
|
+
});
|
|
50
|
+
socket.addEventListener("message", (event) => {
|
|
51
|
+
handlers.onData(event.data);
|
|
52
|
+
});
|
|
53
|
+
socket.addEventListener("close", (event) => {
|
|
54
|
+
handlers.onClose(event.code);
|
|
55
|
+
});
|
|
56
|
+
socket.addEventListener("error", () => {
|
|
57
|
+
handlers.onError();
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
send: (payload) => {
|
|
61
|
+
socket.send(payload);
|
|
62
|
+
},
|
|
63
|
+
close: () => {
|
|
64
|
+
socket.close();
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** True when the environment can open a socket at all (SSR / node cannot). */
|
|
69
|
+
export function canOpenWebSocket() {
|
|
70
|
+
return typeof WebSocket !== "undefined";
|
|
71
|
+
}
|
|
72
|
+
function refusalFor(code) {
|
|
73
|
+
if (code === CHAT_WS_CLOSE_UNAUTHENTICATED)
|
|
74
|
+
return "unauthenticated";
|
|
75
|
+
if (code === CHAT_WS_CLOSE_NOT_PARTICIPANT)
|
|
76
|
+
return "not_participant";
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Open a resumable conversation socket. Connects immediately; returns the
|
|
81
|
+
* handle that closes it. Every state change is pushed through `onStatus`, so
|
|
82
|
+
* a React layer can render the transport without polling this object.
|
|
83
|
+
*/
|
|
84
|
+
export function createChatSocket(options) {
|
|
85
|
+
const factory = options.webSocket ?? browserWebSocketFactory;
|
|
86
|
+
const schedule = options.schedule ?? defaultSchedule;
|
|
87
|
+
const random = options.random ?? Math.random;
|
|
88
|
+
const baseDelay = options.reconnect?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
89
|
+
const maxDelay = options.reconnect?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
90
|
+
const maxAttempts = options.reconnect?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
91
|
+
let connection = null;
|
|
92
|
+
let cancelRetry = null;
|
|
93
|
+
let disposed = false;
|
|
94
|
+
let attempt = 0;
|
|
95
|
+
/** Highest seq handed to the consumer — the dedup cursor. */
|
|
96
|
+
let maxSeqSeen = 0;
|
|
97
|
+
let status = {
|
|
98
|
+
state: "connecting",
|
|
99
|
+
refusal: undefined,
|
|
100
|
+
attempt: 0,
|
|
101
|
+
};
|
|
102
|
+
function setStatus(state, refusal) {
|
|
103
|
+
status = { state, refusal, attempt };
|
|
104
|
+
options.onStatus(status);
|
|
105
|
+
}
|
|
106
|
+
function send(frame) {
|
|
107
|
+
if (!connection)
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
connection.send(JSON.stringify(frame));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// A send on a socket that just died is not an error worth surfacing:
|
|
114
|
+
// the close handler is already on its way and will reconnect.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function backoffDelay() {
|
|
118
|
+
const exponential = baseDelay * 2 ** Math.max(0, attempt - 1);
|
|
119
|
+
const capped = Math.min(exponential, maxDelay);
|
|
120
|
+
// Full jitter — a fleet of tabs reconnecting after one server blip must
|
|
121
|
+
// not arrive as one wave.
|
|
122
|
+
return Math.round(capped * (0.5 + random() * 0.5));
|
|
123
|
+
}
|
|
124
|
+
function connect() {
|
|
125
|
+
if (disposed)
|
|
126
|
+
return;
|
|
127
|
+
setStatus("connecting");
|
|
128
|
+
// The consumer may have advanced by REST while we were down; never hand
|
|
129
|
+
// it a frame it already has.
|
|
130
|
+
maxSeqSeen = Math.max(maxSeqSeen, options.lastSeq());
|
|
131
|
+
connection = factory(options.url, {
|
|
132
|
+
onOpen: () => {
|
|
133
|
+
if (disposed)
|
|
134
|
+
return;
|
|
135
|
+
attempt = 0;
|
|
136
|
+
send({ type: "hello", last_seq: options.lastSeq() });
|
|
137
|
+
setStatus("open");
|
|
138
|
+
},
|
|
139
|
+
onData: (data) => {
|
|
140
|
+
if (disposed)
|
|
141
|
+
return;
|
|
142
|
+
const frame = decodeServerFrame(data);
|
|
143
|
+
// An unreadable frame must NOT advance the cursor — the gap it would
|
|
144
|
+
// hide is exactly what resume-by-seq exists to close.
|
|
145
|
+
if (!frame)
|
|
146
|
+
return;
|
|
147
|
+
if (frame.type === "message") {
|
|
148
|
+
if (frame.seq <= maxSeqSeen)
|
|
149
|
+
return;
|
|
150
|
+
maxSeqSeen = frame.seq;
|
|
151
|
+
}
|
|
152
|
+
options.onFrame(frame);
|
|
153
|
+
},
|
|
154
|
+
onError: () => {
|
|
155
|
+
// `error` always precedes `close` on a browser socket; the close
|
|
156
|
+
// handler owns the reconnect so the two cannot both schedule one.
|
|
157
|
+
},
|
|
158
|
+
onClose: (code) => {
|
|
159
|
+
if (disposed)
|
|
160
|
+
return;
|
|
161
|
+
connection = null;
|
|
162
|
+
const refusal = refusalFor(code);
|
|
163
|
+
if (refusal) {
|
|
164
|
+
// The host answered the question. Asking again, faster, is the
|
|
165
|
+
// classic way to turn a 4403 into an outage.
|
|
166
|
+
setStatus("closed", refusal);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
attempt += 1;
|
|
170
|
+
if (attempt >= maxAttempts) {
|
|
171
|
+
setStatus("closed", "unreachable");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
setStatus("degraded");
|
|
175
|
+
cancelRetry = schedule(connect, backoffDelay());
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
connect();
|
|
180
|
+
return {
|
|
181
|
+
status: () => status,
|
|
182
|
+
ack: (seq) => {
|
|
183
|
+
send({ type: "ack", seq });
|
|
184
|
+
},
|
|
185
|
+
ping: () => {
|
|
186
|
+
send({ type: "ping" });
|
|
187
|
+
},
|
|
188
|
+
close: () => {
|
|
189
|
+
disposed = true;
|
|
190
|
+
cancelRetry?.();
|
|
191
|
+
cancelRetry = null;
|
|
192
|
+
const open = connection;
|
|
193
|
+
connection = null;
|
|
194
|
+
open?.close();
|
|
195
|
+
status = { state: "closed", refusal: undefined, attempt };
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=chatSocket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatSocket.js","sourceRoot":"","sources":["../../src/realtime/chatSocket.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stapel-chat WebSocket protocol, typed.
|
|
3
|
+
*
|
|
4
|
+
* This is a MIRROR of the module's own contract, not an invention:
|
|
5
|
+
* `stapel_chat/consumers.py` (`ChatConsumer`) and `stapel_chat/realtime.py`
|
|
6
|
+
* (`message_frame`) are the source, and `MODULE.md` §"Realtime protocol"
|
|
7
|
+
* states it in one line:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* client → server: hello{last_seq} / send{body,attachments,reply_to} / ack{seq} / ping
|
|
11
|
+
* server → client: welcome{server_seq} / message{…seq} / replay_done{up_to_seq} / error{code,message} / pong
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* The WS surface is invisible to `schema.json` (OpenAPI describes HTTP), so
|
|
15
|
+
* these types cannot be generated today — the realtime spec (§8) parks the
|
|
16
|
+
* frame contract in the `events.json` family for later. Until then this file
|
|
17
|
+
* is the pair's declared mirror and `test/frames.test.ts` pins it against the
|
|
18
|
+
* shapes the consumer actually sends.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here imports React or `@stapel/core`: it is plain protocol, so the
|
|
21
|
+
* future `@stapel/realtime` substrate can subsume it without dragging the UI.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Widest resume gap the server replays inline (`consumers.REPLAY_LIMIT`).
|
|
25
|
+
* Past it the server answers `error{resync}` instead, and the client
|
|
26
|
+
* re-hydrates from the REST history. Mirrored here so the client can SAY that
|
|
27
|
+
* before the server has to.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CHAT_WS_REPLAY_LIMIT = 500;
|
|
30
|
+
/** `ChatConsumer.connect()` closes with this when `scope["user"]` is unset. */
|
|
31
|
+
export declare const CHAT_WS_CLOSE_UNAUTHENTICATED = 4401;
|
|
32
|
+
/** …and with this when the caller is not a participant of the conversation. */
|
|
33
|
+
export declare const CHAT_WS_CLOSE_NOT_PARTICIPANT = 4403;
|
|
34
|
+
/** Resume: replay everything after `last_seq`, then go live. */
|
|
35
|
+
export interface ChatHelloFrame {
|
|
36
|
+
readonly type: "hello";
|
|
37
|
+
readonly last_seq: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Append a message over the socket.
|
|
41
|
+
*
|
|
42
|
+
* TYPED BUT NEVER SENT BY THIS CLIENT, on purpose. The frame is part of the
|
|
43
|
+
* module's contract, so a mirror that omitted it would be an incomplete
|
|
44
|
+
* mirror; but this pair posts messages over REST (`ChatApi.sendMessage`),
|
|
45
|
+
* where the answer is the persisted row and a refusal is a real error
|
|
46
|
+
* envelope. Over the socket a refusal is an `error` frame with a
|
|
47
|
+
* socket-local code (`empty` / `too_long` / `attachments_disabled` /
|
|
48
|
+
* `invalid_reply`) that is NOT in the module's error registry, so it has no
|
|
49
|
+
* i18n key and no remediation — the UI could not tell the person what went
|
|
50
|
+
* wrong. `createChatSocket` exposes no method that emits this.
|
|
51
|
+
*/
|
|
52
|
+
export interface ChatSendFrame {
|
|
53
|
+
readonly type: "send";
|
|
54
|
+
readonly body?: string;
|
|
55
|
+
readonly attachments?: readonly string[];
|
|
56
|
+
readonly reply_to?: string | null;
|
|
57
|
+
}
|
|
58
|
+
/** Delivery acknowledgement — the server tracks it, nothing depends on it. */
|
|
59
|
+
export interface ChatAckFrame {
|
|
60
|
+
readonly type: "ack";
|
|
61
|
+
readonly seq: number;
|
|
62
|
+
}
|
|
63
|
+
/** Liveness probe; answered with `pong`. */
|
|
64
|
+
export interface ChatPingFrame {
|
|
65
|
+
readonly type: "ping";
|
|
66
|
+
}
|
|
67
|
+
export type ChatClientFrame = ChatHelloFrame | ChatSendFrame | ChatAckFrame | ChatPingFrame;
|
|
68
|
+
/** Answer to `hello`: the conversation's high-water mark right now. */
|
|
69
|
+
export interface ChatWelcomeFrame {
|
|
70
|
+
readonly type: "welcome";
|
|
71
|
+
readonly conversation_id: string;
|
|
72
|
+
readonly server_seq: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A message, replayed or live — the same frame either way (that is what makes
|
|
76
|
+
* the overlap after a resume harmless: it is seq-deduped on both ends).
|
|
77
|
+
* Field-for-field `stapel_chat.realtime.message_frame`.
|
|
78
|
+
*/
|
|
79
|
+
export interface ChatMessageFrame {
|
|
80
|
+
readonly type: "message";
|
|
81
|
+
readonly message_id: string;
|
|
82
|
+
readonly conversation_id: string;
|
|
83
|
+
readonly sender_id: string | null;
|
|
84
|
+
readonly seq: number;
|
|
85
|
+
readonly kind: string;
|
|
86
|
+
readonly body: string;
|
|
87
|
+
readonly reply_to: string | null;
|
|
88
|
+
readonly attachments: readonly string[];
|
|
89
|
+
readonly created_at: string;
|
|
90
|
+
}
|
|
91
|
+
/** End of the replay window — everything up to `up_to_seq` has been sent. */
|
|
92
|
+
export interface ChatReplayDoneFrame {
|
|
93
|
+
readonly type: "replay_done";
|
|
94
|
+
readonly up_to_seq: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A protocol-level refusal. `code` is a SOCKET-LOCAL vocabulary
|
|
98
|
+
* (`resync` / `bad_type` / the send-path codes), NOT a `error.<status>.<code>`
|
|
99
|
+
* key from the module's error registry — which is exactly why the only one
|
|
100
|
+
* this pair acts on is `resync`.
|
|
101
|
+
*/
|
|
102
|
+
export interface ChatErrorFrame {
|
|
103
|
+
readonly type: "error";
|
|
104
|
+
readonly code: string;
|
|
105
|
+
readonly message: string;
|
|
106
|
+
}
|
|
107
|
+
/** Answer to `ping`. */
|
|
108
|
+
export interface ChatPongFrame {
|
|
109
|
+
readonly type: "pong";
|
|
110
|
+
}
|
|
111
|
+
export type ChatServerFrame = ChatWelcomeFrame | ChatMessageFrame | ChatReplayDoneFrame | ChatErrorFrame | ChatPongFrame;
|
|
112
|
+
/** The one error code that means "stop replaying, re-read the journal". */
|
|
113
|
+
export declare const CHAT_WS_RESYNC = "resync";
|
|
114
|
+
/**
|
|
115
|
+
* Narrow one decoded server frame, or `null` for anything this build does not
|
|
116
|
+
* understand (an unknown `type`, a missing field, a `seq` that is not a
|
|
117
|
+
* number). Unknown is DROPPED, never coerced: a frame this client cannot read
|
|
118
|
+
* must not advance the seq cursor, or the gap it leaves is invisible.
|
|
119
|
+
*/
|
|
120
|
+
export declare function parseServerFrame(value: unknown): ChatServerFrame | null;
|
|
121
|
+
/** Decode a socket payload (JSON text) into a frame, or `null`. */
|
|
122
|
+
export declare function decodeServerFrame(data: unknown): ChatServerFrame | null;
|
|
123
|
+
//# sourceMappingURL=frames.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frames.d.ts","sourceRoot":"","sources":["../../src/realtime/frames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,+EAA+E;AAC/E,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAClD,+EAA+E;AAC/E,eAAO,MAAM,6BAA6B,OAAO,CAAC;AAIlD,gEAAgE;AAChE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,aAAa,GACb,YAAY,GACZ,aAAa,CAAC;AAIlB,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAwB;AACxB,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,gBAAgB,GAChB,mBAAmB,GACnB,cAAc,GACd,aAAa,CAAC;AAElB,2EAA2E;AAC3E,eAAO,MAAM,cAAc,WAAW,CAAC;AAgBvC;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAyDvE;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,eAAe,GAAG,IAAI,CAOvE"}
|