@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 @@
|
|
|
1
|
+
{"version":3,"file":"mutations.d.ts","sourceRoot":"","sources":["../../src/model/mutations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAMjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AA4B1D,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,cAAc,EAAE,MAAM,GACrB,iBAAiB,CAAC,WAAW,EAAE,cAAc,EAAE,oBAAoB,CAAC,CAuBtE;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,cAAc,EAAE,MAAM,GACrB,iBAAiB,CAAC,MAAM,GAAG,IAAI,EAAE,cAAc,EAAE,MAAM,CAAC,CAsB1D;AAED,gDAAgD;AAChD,MAAM,WAAW,wBAAwB;IACvC,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,IAAI,iBAAiB,CACrD,YAAY,EACZ,cAAc,EACd,wBAAwB,CACzB,CAoBA;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,MAAM,EACtB,KAAK,GAAE,MAAoB,GAC1B,iBAAiB,CAAC,gBAAgB,GAAG,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,CAyBlE"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { useChatApi } from "./context.js";
|
|
3
|
+
import { chatQueryKeys } from "./queryKeys.js";
|
|
4
|
+
import { nextReadMarker } from "./readMarker.js";
|
|
5
|
+
import { THREAD_PAGE } from "./queries.js";
|
|
6
|
+
import { mergeMessage, mergeOlderPage } from "./threadWindow.js";
|
|
7
|
+
/**
|
|
8
|
+
* Write hooks (frontend-standard §2 — mutations invalidate on success).
|
|
9
|
+
*
|
|
10
|
+
* Sending is REST, always: the socket's `send` frame refuses with a
|
|
11
|
+
* socket-local code that has no i18n key and no remediation, while the HTTP
|
|
12
|
+
* path answers with the persisted row and a real error envelope. See
|
|
13
|
+
* `realtime/frames.ts`.
|
|
14
|
+
*/
|
|
15
|
+
/** Fold a freshly-known message into the cached window, or re-read on a hole. */
|
|
16
|
+
function absorbMessage(queryClient, conversationId, message) {
|
|
17
|
+
const key = chatQueryKeys.thread(conversationId);
|
|
18
|
+
const window = queryClient.getQueryData(key);
|
|
19
|
+
if (!window)
|
|
20
|
+
return;
|
|
21
|
+
const merged = mergeMessage(window, message);
|
|
22
|
+
if (merged.gap) {
|
|
23
|
+
void queryClient.invalidateQueries({ queryKey: key });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
queryClient.setQueryData(key, merged.window);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Append a message to a thread. On success the persisted row (with its `seq`)
|
|
30
|
+
* is folded straight into the window, so the sender sees their own line
|
|
31
|
+
* without waiting for a poll — and the socket's fan-out copy of the same row
|
|
32
|
+
* is then a duplicate that `mergeMessage` drops.
|
|
33
|
+
*
|
|
34
|
+
* Attachments are NOT wired in this version (spec §4.5): the field exists on
|
|
35
|
+
* the wire, but shipping it would mean shipping CDN upload rights into chat,
|
|
36
|
+
* and a control that is visible but does nothing is worse than one that is
|
|
37
|
+
* absent.
|
|
38
|
+
*/
|
|
39
|
+
export function useSendMessage(conversationId) {
|
|
40
|
+
const api = useChatApi();
|
|
41
|
+
const queryClient = useQueryClient();
|
|
42
|
+
const options = {
|
|
43
|
+
mutationFn: (vars) => api.sendMessage(conversationId, {
|
|
44
|
+
body: vars.body,
|
|
45
|
+
...(vars.replyTo !== undefined ? { reply_to: vars.replyTo } : {}),
|
|
46
|
+
}),
|
|
47
|
+
onSuccess: (message) => {
|
|
48
|
+
absorbMessage(queryClient, conversationId, message);
|
|
49
|
+
// The list orders by `updated_at` and carries the unread badge; both
|
|
50
|
+
// just moved.
|
|
51
|
+
void queryClient.invalidateQueries({
|
|
52
|
+
queryKey: chatQueryKeys.conversations(),
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
return useMutation(options);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Advance the caller's read marker to `upto_seq`.
|
|
60
|
+
*
|
|
61
|
+
* Monotonic on the client as well as on the server: a candidate at or below
|
|
62
|
+
* what this client already reported resolves without a request
|
|
63
|
+
* (`nextReadMarker`). The reported value is remembered in the query cache —
|
|
64
|
+
* see `readMarker.ts` for why it cannot be read back off the wire.
|
|
65
|
+
*/
|
|
66
|
+
export function useMarkRead(conversationId) {
|
|
67
|
+
const api = useChatApi();
|
|
68
|
+
const queryClient = useQueryClient();
|
|
69
|
+
const markerKey = chatQueryKeys.readMarker(conversationId);
|
|
70
|
+
const options = {
|
|
71
|
+
mutationFn: async (candidate) => {
|
|
72
|
+
const known = queryClient.getQueryData(markerKey);
|
|
73
|
+
const seq = nextReadMarker(known, candidate);
|
|
74
|
+
if (seq === null)
|
|
75
|
+
return null;
|
|
76
|
+
await api.markRead(conversationId, seq);
|
|
77
|
+
return seq;
|
|
78
|
+
},
|
|
79
|
+
onSuccess: (seq) => {
|
|
80
|
+
if (seq === null)
|
|
81
|
+
return;
|
|
82
|
+
queryClient.setQueryData(markerKey, seq);
|
|
83
|
+
// The badge is server-computed; re-read it rather than guessing.
|
|
84
|
+
void queryClient.invalidateQueries({
|
|
85
|
+
queryKey: chatQueryKeys.conversations(),
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
return useMutation(options);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Open the direct thread with someone, creating it only if it does not exist.
|
|
93
|
+
*
|
|
94
|
+
* The idempotency is the module's, not ours: a direct thread is keyed by an
|
|
95
|
+
* order-independent `direct_key` over the participant pair (namespaced by
|
|
96
|
+
* scope) under a unique constraint, and the create race is resolved by the
|
|
97
|
+
* constraint — the loser gets the winner's row (`MODULE.md`, "Direct
|
|
98
|
+
* idempotency"). So "message the seller" is safe to press twice, from two
|
|
99
|
+
* tabs, on two listings.
|
|
100
|
+
*
|
|
101
|
+
* TWO PEOPLE, ONE THREAD — INCLUDING ACROSS LISTINGS. The key is the pair,
|
|
102
|
+
* not the listing: a buyer who writes to the same seller about a second item
|
|
103
|
+
* lands in the same conversation. `CreateConversationRequest.scope_key` does
|
|
104
|
+
* not change that (the server ignores the field and resolves the scope
|
|
105
|
+
* itself — `api/extensions.ts`), so a host that wants the listing named must
|
|
106
|
+
* name it in the first message.
|
|
107
|
+
*/
|
|
108
|
+
export function useStartDirectChat() {
|
|
109
|
+
const api = useChatApi();
|
|
110
|
+
const queryClient = useQueryClient();
|
|
111
|
+
const options = {
|
|
112
|
+
mutationFn: (vars) => api.createConversation("direct", [vars.userId]),
|
|
113
|
+
onSuccess: (conversation) => {
|
|
114
|
+
queryClient.setQueryData(chatQueryKeys.conversation(conversation.id), conversation);
|
|
115
|
+
void queryClient.invalidateQueries({
|
|
116
|
+
queryKey: chatQueryKeys.conversations(),
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
return useMutation(options);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Backfill one page of older history onto the front of the window
|
|
124
|
+
* (`direction=next` from the window's oldest seq — "next" is OLDER here,
|
|
125
|
+
* because the history is ordered `-seq`).
|
|
126
|
+
*
|
|
127
|
+
* A mutation rather than a second query: it is an action a person takes
|
|
128
|
+
* ("show me more"), it must not re-run on focus, and its result belongs in
|
|
129
|
+
* the ONE thread cache entry rather than in a second one that would then have
|
|
130
|
+
* to be reconciled with it.
|
|
131
|
+
*/
|
|
132
|
+
export function useLoadOlderMessages(conversationId, limit = THREAD_PAGE) {
|
|
133
|
+
const api = useChatApi();
|
|
134
|
+
const queryClient = useQueryClient();
|
|
135
|
+
const key = chatQueryKeys.thread(conversationId);
|
|
136
|
+
const options = {
|
|
137
|
+
mutationFn: async () => {
|
|
138
|
+
const window = queryClient.getQueryData(key);
|
|
139
|
+
if (!window || !window.hasOlder || window.olderAnchor === null)
|
|
140
|
+
return null;
|
|
141
|
+
const page = await api.messages(conversationId, {
|
|
142
|
+
direction: "next",
|
|
143
|
+
anchor: window.olderAnchor,
|
|
144
|
+
limit,
|
|
145
|
+
});
|
|
146
|
+
return mergeOlderPage(window, page);
|
|
147
|
+
},
|
|
148
|
+
onSuccess: (window) => {
|
|
149
|
+
if (window === null)
|
|
150
|
+
return;
|
|
151
|
+
queryClient.setQueryData(key, window);
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
return useMutation(options);
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=mutations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutations.js","sourceRoot":"","sources":["../../src/model/mutations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { InfiniteData, UseInfiniteQueryResult, UseQueryResult } from "@tanstack/react-query";
|
|
2
|
+
import type { StapelApiError } from "@stapel/core";
|
|
3
|
+
import type { Conversation, ConversationPage } from "../api/types.js";
|
|
4
|
+
import type { ChatThreadWindow } from "./threadWindow.js";
|
|
5
|
+
/** Default window size for a thread — one screenful of history and change. */
|
|
6
|
+
export declare const THREAD_PAGE = 50;
|
|
7
|
+
/** One conversation (participant-only; 403 otherwise). */
|
|
8
|
+
export declare function useConversation(conversationId: string): UseQueryResult<Conversation, StapelApiError>;
|
|
9
|
+
/**
|
|
10
|
+
* The caller's conversations as an infinite (load-more) list, anchored on
|
|
11
|
+
* `updated_at`. `unread_count` is computed server-side per conversation, so
|
|
12
|
+
* the badge needs no second request.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useConversations(limit?: number): UseInfiniteQueryResult<InfiniteData<ConversationPage, string | undefined>, StapelApiError>;
|
|
15
|
+
/**
|
|
16
|
+
* The thread — one cache entry holding a contiguous, seq-ordered window
|
|
17
|
+
* (`threadWindow.ts`).
|
|
18
|
+
*
|
|
19
|
+
* ITS REFETCH IS THE POLL. The query function reads the window already in the
|
|
20
|
+
* cache and asks only for what comes after it
|
|
21
|
+
* (`direction=prev&anchor=<tip>`), so `refetch()` means "advance by seq", not
|
|
22
|
+
* "read the newest page again". That is what lets the transport seam stay a
|
|
23
|
+
* signal → refetch bridge: a socket frame and a polling tick take the exact
|
|
24
|
+
* same code path, and the UI cannot tell which arrived.
|
|
25
|
+
*
|
|
26
|
+
* A tail that does not touch the tip (more messages than one page holds, or
|
|
27
|
+
* the paginator's own truncation flag) is a hole. Rather than render a thread
|
|
28
|
+
* with a hole, the window is rebuilt from the newest page — the REST twin of
|
|
29
|
+
* the socket's `error{resync}`, and the reason correctness never depends on
|
|
30
|
+
* delivery.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useThread(conversationId: string, limit?: number): UseQueryResult<ChatThreadWindow, StapelApiError>;
|
|
33
|
+
//# sourceMappingURL=queries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/model/queries.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,sBAAsB,EACtB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAQtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAe1D,8EAA8E;AAC9E,eAAO,MAAM,WAAW,KAAK,CAAC;AAE9B,0DAA0D;AAC1D,wBAAgB,eAAe,CAC7B,cAAc,EAAE,MAAM,GACrB,cAAc,CAAC,YAAY,EAAE,cAAc,CAAC,CAQ9C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,GAAE,MAA2B,GACjC,sBAAsB,CACvB,YAAY,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC,EAClD,cAAc,CACf,CAgBA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CACvB,cAAc,EAAE,MAAM,EACtB,KAAK,GAAE,MAAoB,GAC1B,cAAc,CAAC,gBAAgB,EAAE,cAAc,CAAC,CAwBlD"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { useActiveSessionReady } from "@stapel/core";
|
|
3
|
+
import { useChatApi } from "./context.js";
|
|
4
|
+
import { chatQueryKeys } from "./queryKeys.js";
|
|
5
|
+
import { mergeNewerPage, threadLastSeq, threadWindowFromPage, } from "./threadWindow.js";
|
|
6
|
+
/**
|
|
7
|
+
* Read hooks over the chat API. Staleness follows core's query defaults;
|
|
8
|
+
* freshness is driven by the transport seam (`flows/freshness.ts`), never by
|
|
9
|
+
* a `refetchInterval` sprinkled on a call site.
|
|
10
|
+
*
|
|
11
|
+
* Every hook is gated on {@link useActiveSessionReady}: chat is a member
|
|
12
|
+
* surface with no natural `enabled` condition of its own, and a top-level
|
|
13
|
+
* read that races a still-bootstrapping session reports "loaded, nothing
|
|
14
|
+
* here" for the length of the bootstrap.
|
|
15
|
+
*/
|
|
16
|
+
/** Default page size for the conversation list. */
|
|
17
|
+
const CONVERSATIONS_PAGE = 20;
|
|
18
|
+
/** Default window size for a thread — one screenful of history and change. */
|
|
19
|
+
export const THREAD_PAGE = 50;
|
|
20
|
+
/** One conversation (participant-only; 403 otherwise). */
|
|
21
|
+
export function useConversation(conversationId) {
|
|
22
|
+
const api = useChatApi();
|
|
23
|
+
const sessionReady = useActiveSessionReady();
|
|
24
|
+
return useQuery({
|
|
25
|
+
queryKey: chatQueryKeys.conversation(conversationId),
|
|
26
|
+
queryFn: () => api.conversation(conversationId),
|
|
27
|
+
enabled: sessionReady && conversationId.length > 0,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The caller's conversations as an infinite (load-more) list, anchored on
|
|
32
|
+
* `updated_at`. `unread_count` is computed server-side per conversation, so
|
|
33
|
+
* the badge needs no second request.
|
|
34
|
+
*/
|
|
35
|
+
export function useConversations(limit = CONVERSATIONS_PAGE) {
|
|
36
|
+
const api = useChatApi();
|
|
37
|
+
const sessionReady = useActiveSessionReady();
|
|
38
|
+
return useInfiniteQuery({
|
|
39
|
+
queryKey: chatQueryKeys.conversations(),
|
|
40
|
+
queryFn: ({ pageParam }) => api.conversations({
|
|
41
|
+
direction: "next",
|
|
42
|
+
limit,
|
|
43
|
+
...(pageParam !== undefined ? { anchor: pageParam } : {}),
|
|
44
|
+
}),
|
|
45
|
+
initialPageParam: undefined,
|
|
46
|
+
getNextPageParam: (last) => last.has_next ? (last.next_anchor ?? undefined) : undefined,
|
|
47
|
+
enabled: sessionReady,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The thread — one cache entry holding a contiguous, seq-ordered window
|
|
52
|
+
* (`threadWindow.ts`).
|
|
53
|
+
*
|
|
54
|
+
* ITS REFETCH IS THE POLL. The query function reads the window already in the
|
|
55
|
+
* cache and asks only for what comes after it
|
|
56
|
+
* (`direction=prev&anchor=<tip>`), so `refetch()` means "advance by seq", not
|
|
57
|
+
* "read the newest page again". That is what lets the transport seam stay a
|
|
58
|
+
* signal → refetch bridge: a socket frame and a polling tick take the exact
|
|
59
|
+
* same code path, and the UI cannot tell which arrived.
|
|
60
|
+
*
|
|
61
|
+
* A tail that does not touch the tip (more messages than one page holds, or
|
|
62
|
+
* the paginator's own truncation flag) is a hole. Rather than render a thread
|
|
63
|
+
* with a hole, the window is rebuilt from the newest page — the REST twin of
|
|
64
|
+
* the socket's `error{resync}`, and the reason correctness never depends on
|
|
65
|
+
* delivery.
|
|
66
|
+
*/
|
|
67
|
+
export function useThread(conversationId, limit = THREAD_PAGE) {
|
|
68
|
+
const api = useChatApi();
|
|
69
|
+
const queryClient = useQueryClient();
|
|
70
|
+
const sessionReady = useActiveSessionReady();
|
|
71
|
+
const queryKey = chatQueryKeys.thread(conversationId);
|
|
72
|
+
return useQuery({
|
|
73
|
+
queryKey,
|
|
74
|
+
queryFn: async () => {
|
|
75
|
+
const previous = queryClient.getQueryData(queryKey);
|
|
76
|
+
const tip = previous ? threadLastSeq(previous) : 0;
|
|
77
|
+
if (!previous || tip === 0) {
|
|
78
|
+
return threadWindowFromPage(await api.messages(conversationId, { limit }));
|
|
79
|
+
}
|
|
80
|
+
const tail = await api.messages(conversationId, {
|
|
81
|
+
direction: "prev",
|
|
82
|
+
anchor: String(tip),
|
|
83
|
+
limit,
|
|
84
|
+
});
|
|
85
|
+
const merged = mergeNewerPage(previous, tail);
|
|
86
|
+
if (!merged.gap)
|
|
87
|
+
return merged.window;
|
|
88
|
+
return threadWindowFromPage(await api.messages(conversationId, { limit }));
|
|
89
|
+
},
|
|
90
|
+
enabled: sessionReady && conversationId.length > 0,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=queries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queries.js","sourceRoot":"","sources":["../../src/model/queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const chatQueryKeys: {
|
|
2
|
+
readonly all: readonly ["chat"];
|
|
3
|
+
conversations(): readonly ["chat", "conversations"];
|
|
4
|
+
conversation(conversationId: string): readonly ["chat", "conversation", string];
|
|
5
|
+
thread(conversationId: string): readonly ["chat", "thread", string];
|
|
6
|
+
readMarker(conversationId: string): readonly ["chat", "read-marker", string];
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=queryKeys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryKeys.d.ts","sourceRoot":"","sources":["../../src/model/queryKeys.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,aAAa,EAAE;IAC1B,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;IAChC,aAAa,IAAI,SAAS,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACpD,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC;IAChF,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACpE,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC;CAiB9E,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespaced TanStack Query keys (frontend-standard §2 — namespaced keys).
|
|
3
|
+
* Everything under the `"chat"` root so a host can invalidate the whole module
|
|
4
|
+
* or match a single thread. Persist scope is per-user via core's query runtime
|
|
5
|
+
* (`setPersistUser`). Explicit tuple return types satisfy
|
|
6
|
+
* `--isolatedDeclarations`.
|
|
7
|
+
*/
|
|
8
|
+
const ROOT = "chat";
|
|
9
|
+
export const chatQueryKeys = {
|
|
10
|
+
all: [ROOT],
|
|
11
|
+
// The infinite list shares one root key across pages (its pages live under
|
|
12
|
+
// a single cache entry).
|
|
13
|
+
conversations: () => [ROOT, "conversations"],
|
|
14
|
+
conversation: (conversationId) => [ROOT, "conversation", conversationId],
|
|
15
|
+
// The thread is ONE cache entry holding a merged, seq-ordered window — not
|
|
16
|
+
// a page list. Its query function reads this entry to decide what to ask
|
|
17
|
+
// for next (a tail by seq, not a blind re-read), which is what makes a
|
|
18
|
+
// plain `refetch()` mean "poll by seq" and lets the transport seam stay a
|
|
19
|
+
// signal → refetch bridge.
|
|
20
|
+
thread: (conversationId) => [ROOT, "thread", conversationId],
|
|
21
|
+
// Not a server read: the highest `upto_seq` this client has already
|
|
22
|
+
// reported. Lives in the query cache so it survives a remount and is wiped
|
|
23
|
+
// at logout with everything else (frontend-core-architecture-v2 §43.3).
|
|
24
|
+
readMarker: (conversationId) => [ROOT, "read-marker", conversationId],
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=queryKeys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryKeys.js","sourceRoot":"","sources":["../../src/model/queryKeys.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,IAAI,GAAG,MAAe,CAAC;AAE7B,MAAM,CAAC,MAAM,aAAa,GAMtB;IACF,GAAG,EAAE,CAAC,IAAI,CAAC;IACX,2EAA2E;IAC3E,yBAAyB;IACzB,aAAa,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,eAAe,CAAC;IAC5C,YAAY,EAAE,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,cAAc,EAAE,cAAc,CAAC;IACxE,2EAA2E;IAC3E,yEAAyE;IACzE,uEAAuE;IACvE,0EAA0E;IAC1E,2BAA2B;IAC3B,MAAM,EAAE,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,cAAc,CAAC;IAC5D,oEAAoE;IACpE,2EAA2E;IAC3E,wEAAwE;IACxE,UAAU,EAAE,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,aAAa,EAAE,cAAc,CAAC;CACtE,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read marker, and the one rule that governs it: it only ever moves
|
|
3
|
+
* forward.
|
|
4
|
+
*
|
|
5
|
+
* The server already enforces that (`services.mark_read` never lowers
|
|
6
|
+
* `last_read_seq`), so a client that sends a smaller value is not corrupting
|
|
7
|
+
* anything — it is spending a request to be ignored, and, worse, telling the
|
|
8
|
+
* UI a story that the next list refresh will contradict. The guard is here so
|
|
9
|
+
* the client's own belief matches the server's.
|
|
10
|
+
*
|
|
11
|
+
* WHY THE CLIENT HAS TO REMEMBER AT ALL. `ConversationResponse.participants[]`
|
|
12
|
+
* carries `last_read_seq` — for every participant, with no marker for which
|
|
13
|
+
* one is the caller (the response has no "me"). `unread_count` is the only
|
|
14
|
+
* caller-relative number the endpoint returns. So the highest marker THIS
|
|
15
|
+
* client has reported cannot be read back off the wire; it is remembered in
|
|
16
|
+
* the query cache (`chatQueryKeys.readMarker`), which the session layer wipes
|
|
17
|
+
* at logout along with everything else.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The value to send, or `null` for "already reported — send nothing".
|
|
21
|
+
*
|
|
22
|
+
* `candidate` of 0 (an empty thread) is never sent: there is no such seq.
|
|
23
|
+
*/
|
|
24
|
+
export declare function nextReadMarker(known: number | undefined, candidate: number): number | null;
|
|
25
|
+
//# sourceMappingURL=readMarker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readMarker.d.ts","sourceRoot":"","sources":["../../src/model/readMarker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAIf"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read marker, and the one rule that governs it: it only ever moves
|
|
3
|
+
* forward.
|
|
4
|
+
*
|
|
5
|
+
* The server already enforces that (`services.mark_read` never lowers
|
|
6
|
+
* `last_read_seq`), so a client that sends a smaller value is not corrupting
|
|
7
|
+
* anything — it is spending a request to be ignored, and, worse, telling the
|
|
8
|
+
* UI a story that the next list refresh will contradict. The guard is here so
|
|
9
|
+
* the client's own belief matches the server's.
|
|
10
|
+
*
|
|
11
|
+
* WHY THE CLIENT HAS TO REMEMBER AT ALL. `ConversationResponse.participants[]`
|
|
12
|
+
* carries `last_read_seq` — for every participant, with no marker for which
|
|
13
|
+
* one is the caller (the response has no "me"). `unread_count` is the only
|
|
14
|
+
* caller-relative number the endpoint returns. So the highest marker THIS
|
|
15
|
+
* client has reported cannot be read back off the wire; it is remembered in
|
|
16
|
+
* the query cache (`chatQueryKeys.readMarker`), which the session layer wipes
|
|
17
|
+
* at logout along with everything else.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The value to send, or `null` for "already reported — send nothing".
|
|
21
|
+
*
|
|
22
|
+
* `candidate` of 0 (an empty thread) is never sent: there is no such seq.
|
|
23
|
+
*/
|
|
24
|
+
export function nextReadMarker(known, candidate) {
|
|
25
|
+
if (!Number.isFinite(candidate) || candidate <= 0)
|
|
26
|
+
return null;
|
|
27
|
+
if (known !== undefined && candidate <= known)
|
|
28
|
+
return null;
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=readMarker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readMarker.js","sourceRoot":"","sources":["../../src/model/readMarker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAyB,EACzB,SAAiB;IAEjB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/D,IAAI,KAAK,KAAK,SAAS,IAAI,SAAS,IAAI,KAAK;QAAE,OAAO,IAAI,CAAC;IAC3D,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CreateModuleRuntimeOptions, ModuleRuntime } from "@stapel/core";
|
|
2
|
+
import type { ChatApi } from "../api/chatApi.js";
|
|
3
|
+
import type { ChatReconnectOptions, ChatWebSocketFactory } from "../realtime/chatSocket.js";
|
|
4
|
+
/**
|
|
5
|
+
* Host-side realtime configuration. Every field is optional and every default
|
|
6
|
+
* degrades to polling rather than to a broken socket — a host that says
|
|
7
|
+
* nothing gets the right behaviour on a WSGI deployment (no sockets at all)
|
|
8
|
+
* and on an ASGI one (sockets, if the origin can be resolved).
|
|
9
|
+
*/
|
|
10
|
+
export interface ChatRealtimeOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Where this deployment mounts `stapel_chat.routing.websocket_urlpatterns`,
|
|
13
|
+
* e.g. `wss://darom.example/ws/chat/`. Omit to derive it from `baseUrl`
|
|
14
|
+
* (the canonical `/ws/chat/` at the API's own origin). Pass `null` to turn
|
|
15
|
+
* the socket transport OFF explicitly — a host that knows its backend runs
|
|
16
|
+
* under WSGI says so here instead of letting every tab fail a handshake
|
|
17
|
+
* six times first.
|
|
18
|
+
*/
|
|
19
|
+
readonly socketUrl?: string | null;
|
|
20
|
+
/** Injectable socket transport (tests, instrumentation, React Native). */
|
|
21
|
+
readonly webSocket?: ChatWebSocketFactory;
|
|
22
|
+
/** Reconnect backoff knobs. */
|
|
23
|
+
readonly reconnect?: ChatReconnectOptions;
|
|
24
|
+
/**
|
|
25
|
+
* Origin used to resolve a relative `baseUrl`. Defaults to the browser's
|
|
26
|
+
* own; irrelevant when `baseUrl` is absolute.
|
|
27
|
+
*/
|
|
28
|
+
readonly origin?: string | null;
|
|
29
|
+
}
|
|
30
|
+
/** The realtime configuration as the transport seam reads it. */
|
|
31
|
+
export interface ChatRealtimeConfig {
|
|
32
|
+
/** `null` — this build will not open sockets; the seam polls. */
|
|
33
|
+
readonly socketBase: string | null;
|
|
34
|
+
readonly webSocket: ChatWebSocketFactory | undefined;
|
|
35
|
+
readonly reconnect: ChatReconnectOptions | undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The wired chat runtime — core's `ModuleRuntime` bound to this pair's API,
|
|
39
|
+
* PLUS the one thing chat has and a plain REST pair does not: where its
|
|
40
|
+
* socket lives. The socket URL is runtime configuration, not module state, so
|
|
41
|
+
* it rides here rather than through a second provider.
|
|
42
|
+
*/
|
|
43
|
+
export type ChatRuntime = ModuleRuntime<ChatApi> & {
|
|
44
|
+
readonly realtime: ChatRealtimeConfig;
|
|
45
|
+
};
|
|
46
|
+
export interface CreateChatRuntimeOptions extends CreateModuleRuntimeOptions {
|
|
47
|
+
readonly realtime?: ChatRealtimeOptions;
|
|
48
|
+
}
|
|
49
|
+
export declare function createChatRuntime(options: CreateChatRuntimeOptions): ChatRuntime;
|
|
50
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/model/runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE9E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,2BAA2B,CAAC;AAGnC;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,oBAAoB,CAAC;IAC1C,+BAA+B;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,oBAAoB,CAAC;IAC1C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,iEAAiE;AACjE,MAAM,WAAW,kBAAkB;IACjC,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,SAAS,EAAE,oBAAoB,GAAG,SAAS,CAAC;CACtD;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG;IACjD,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;CACvC,CAAC;AAEF,MAAM,WAAW,wBAAyB,SAAQ,0BAA0B;IAC1E,QAAQ,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC;CACzC;AA0BD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,wBAAwB,GAChC,WAAW,CAGb"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createModuleRuntime } from "@stapel/core";
|
|
2
|
+
import { createChatApi } from "../api/chatApi.js";
|
|
3
|
+
import { canOpenWebSocket } from "../realtime/chatSocket.js";
|
|
4
|
+
import { deriveChatSocketBase } from "../realtime/streams.js";
|
|
5
|
+
function currentOrigin() {
|
|
6
|
+
return typeof location !== "undefined" ? location.origin : null;
|
|
7
|
+
}
|
|
8
|
+
function resolveRealtime(baseUrl, options) {
|
|
9
|
+
const explicit = options?.socketUrl;
|
|
10
|
+
const socketBase = explicit === null
|
|
11
|
+
? null
|
|
12
|
+
: explicit !== undefined
|
|
13
|
+
? explicit
|
|
14
|
+
: canOpenWebSocket()
|
|
15
|
+
? deriveChatSocketBase(baseUrl, options?.origin ?? currentOrigin())
|
|
16
|
+
: null;
|
|
17
|
+
return {
|
|
18
|
+
socketBase,
|
|
19
|
+
webSocket: options?.webSocket,
|
|
20
|
+
reconnect: options?.reconnect,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createChatRuntime(options) {
|
|
24
|
+
const runtime = createModuleRuntime(createChatApi, options);
|
|
25
|
+
return { ...runtime, realtime: resolveRealtime(options.baseUrl, options.realtime) };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/model/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAK7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAmD9D,SAAS,aAAa;IACpB,OAAO,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAClE,CAAC;AAED,SAAS,eAAe,CACtB,OAAe,EACf,OAAwC;IAExC,MAAM,QAAQ,GAAG,OAAO,EAAE,SAAS,CAAC;IACpC,MAAM,UAAU,GACd,QAAQ,KAAK,IAAI;QACf,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,QAAQ,KAAK,SAAS;YACtB,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,gBAAgB,EAAE;gBAClB,CAAC,CAAC,oBAAoB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,aAAa,EAAE,CAAC;gBACnE,CAAC,CAAC,IAAI,CAAC;IACf,OAAO;QACL,UAAU;QACV,SAAS,EAAE,OAAO,EAAE,SAAS;QAC7B,SAAS,EAAE,OAAO,EAAE,SAAS;KAC9B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,OAAiC;IAEjC,MAAM,OAAO,GAAG,mBAAmB,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;IAC5D,OAAO,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,eAAe,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;AACtF,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thread store: a merged, `seq`-ordered window over a conversation, and
|
|
3
|
+
* the four pure functions that move it.
|
|
4
|
+
*
|
|
5
|
+
* WHY A WINDOW AND NOT A PAGE LIST. A chat thread grows at BOTH ends: older
|
|
6
|
+
* history is paged backwards on scroll, new messages arrive at the tip while
|
|
7
|
+
* the reader is looking at it. TanStack's bidirectional infinite query cannot
|
|
8
|
+
* express the second half — `getPreviousPageParam` is answered from the page
|
|
9
|
+
* the server sent, and at the tip the server correctly says "no previous
|
|
10
|
+
* page", which would freeze the tail forever. So the thread is ONE cache
|
|
11
|
+
* entry holding a contiguous run of messages, and the two directions are two
|
|
12
|
+
* merges into it.
|
|
13
|
+
*
|
|
14
|
+
* THE ORDERING RULE IS THE BACKEND'S, NOT OURS. `seq` is a gapless, total
|
|
15
|
+
* per-conversation order (`stapel-chat/MODULE.md`: "seq is gapless and total
|
|
16
|
+
* — the canonical anchor for history and the resume cursor for realtime"), and
|
|
17
|
+
* ordering by timestamp is an explicit anti-pattern there: two messages in the
|
|
18
|
+
* same millisecond still have a definite order. Everything below therefore
|
|
19
|
+
* sorts, dedupes and detects gaps by `seq` alone.
|
|
20
|
+
*
|
|
21
|
+
* ONE MORE THING THE PAGINATOR MAKES US DO. Item order depends on the
|
|
22
|
+
* DIRECTION asked for: core's `AnchorPagination` returns newest-first for
|
|
23
|
+
* `next` and reverses to oldest-first for `prev` (`pagination.py`,
|
|
24
|
+
* `items[::-1]`). Rather than remember which call produced which order, every
|
|
25
|
+
* merge sorts by `seq` — the total order is cheaper to apply than to
|
|
26
|
+
* remember.
|
|
27
|
+
*/
|
|
28
|
+
import type { ChatMessage, MessagePage } from "../api/types.js";
|
|
29
|
+
/**
|
|
30
|
+
* A contiguous run of messages, ascending by `seq`.
|
|
31
|
+
*
|
|
32
|
+
* "Contiguous" is load-bearing: because `seq` is gapless, a window whose seqs
|
|
33
|
+
* are consecutive integers provably has no hole in it. Every merge below
|
|
34
|
+
* either preserves that or refuses the merge and reports a gap — it never
|
|
35
|
+
* quietly stitches two runs with a hole between them, which is the defect
|
|
36
|
+
* that makes a chat show yesterday's message under today's.
|
|
37
|
+
*/
|
|
38
|
+
export interface ChatThreadWindow {
|
|
39
|
+
/** Ascending by `seq`, no duplicates, no holes. */
|
|
40
|
+
readonly messages: readonly ChatMessage[];
|
|
41
|
+
/** Older history exists before `messages[0]`. */
|
|
42
|
+
readonly hasOlder: boolean;
|
|
43
|
+
/** Anchor for the next backfill page (`direction=next`), if any. */
|
|
44
|
+
readonly olderAnchor: string | null;
|
|
45
|
+
}
|
|
46
|
+
/** A merge that either advanced the window, or found a hole and refused. */
|
|
47
|
+
export interface ThreadMergeResult {
|
|
48
|
+
readonly window: ChatThreadWindow;
|
|
49
|
+
/**
|
|
50
|
+
* `true` when the incoming messages do not touch the window's tip: more
|
|
51
|
+
* arrived than one page holds. The caller re-hydrates (the REST twin of the
|
|
52
|
+
* socket's `error{resync}`) rather than rendering a thread with a hole.
|
|
53
|
+
*/
|
|
54
|
+
readonly gap: boolean;
|
|
55
|
+
}
|
|
56
|
+
export declare const EMPTY_THREAD_WINDOW: ChatThreadWindow;
|
|
57
|
+
/** The tip: the highest `seq` in the window, or 0 for an empty one. */
|
|
58
|
+
export declare function threadLastSeq(window: ChatThreadWindow): number;
|
|
59
|
+
/** The oldest `seq` in the window, or 0 for an empty one. */
|
|
60
|
+
export declare function threadFirstSeq(window: ChatThreadWindow): number;
|
|
61
|
+
/**
|
|
62
|
+
* A fresh window from the newest page (`GET …/messages` with no anchor).
|
|
63
|
+
* This is also the resync path: whatever was loaded before is dropped,
|
|
64
|
+
* because a window that cannot be proven contiguous is worse than a short
|
|
65
|
+
* one.
|
|
66
|
+
*/
|
|
67
|
+
export declare function threadWindowFromPage(page: MessagePage): ChatThreadWindow;
|
|
68
|
+
/**
|
|
69
|
+
* Merge a backfill page (`direction=next` from the window's oldest seq) onto
|
|
70
|
+
* the FRONT. Anything at or above the current oldest is dropped as a
|
|
71
|
+
* duplicate — the anchor is exclusive, so this only fires on a retry.
|
|
72
|
+
*/
|
|
73
|
+
export declare function mergeOlderPage(window: ChatThreadWindow, page: MessagePage): ChatThreadWindow;
|
|
74
|
+
/**
|
|
75
|
+
* Merge a tail page (`direction=prev` anchored on the window's tip) onto the
|
|
76
|
+
* END — the poll-by-seq step.
|
|
77
|
+
*
|
|
78
|
+
* Two independent hole detectors, because either one alone can be fooled:
|
|
79
|
+
* - the seq test (`first new seq === tip + 1`), which is only meaningful
|
|
80
|
+
* because seq is gapless;
|
|
81
|
+
* - the paginator's own `has_prev`, which is how a window that was TRUNCATED
|
|
82
|
+
* (more messages above the anchor than `limit`) says so.
|
|
83
|
+
*/
|
|
84
|
+
export declare function mergeNewerPage(window: ChatThreadWindow, page: MessagePage): ThreadMergeResult;
|
|
85
|
+
/**
|
|
86
|
+
* Merge ONE message onto the end — a live socket frame, or the row a `send`
|
|
87
|
+
* just returned.
|
|
88
|
+
*
|
|
89
|
+
* Already-known seq is dropped (the socket dedupes too, but the sender's own
|
|
90
|
+
* REST answer and the frame it fans out to itself are two paths to the same
|
|
91
|
+
* row, and only this rule makes them idempotent). A seq beyond the tip + 1 is
|
|
92
|
+
* a hole: the window is left untouched and the caller re-hydrates.
|
|
93
|
+
*/
|
|
94
|
+
export declare function mergeMessage(window: ChatThreadWindow, message: ChatMessage): ThreadMergeResult;
|
|
95
|
+
//# sourceMappingURL=threadWindow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threadWindow.d.ts","sourceRoot":"","sources":["../../src/model/threadWindow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;CACvB;AAED,eAAO,MAAM,mBAAmB,EAAE,gBAIjC,CAAC;AAMF,uEAAuE;AACvE,wBAAgB,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAG9D;AAED,6DAA6D;AAC7D,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAG/D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,GAAG,gBAAgB,CAMxE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,WAAW,GAChB,gBAAgB,CAalB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,WAAW,GAChB,iBAAiB,CAoBnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,WAAW,GACnB,iBAAiB,CAQnB"}
|