@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.
Files changed (195) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/MODULE.md +118 -0
  3. package/README.md +126 -0
  4. package/dist/api/chatApi.d.ts +49 -0
  5. package/dist/api/chatApi.d.ts.map +1 -0
  6. package/dist/api/chatApi.js +46 -0
  7. package/dist/api/chatApi.js.map +1 -0
  8. package/dist/api/extensions.d.ts +19 -0
  9. package/dist/api/extensions.d.ts.map +1 -0
  10. package/dist/api/extensions.js +19 -0
  11. package/dist/api/extensions.js.map +1 -0
  12. package/dist/api/generated/schema.d.ts +547 -0
  13. package/dist/api/generated/schema.d.ts.map +1 -0
  14. package/dist/api/generated/schema.js +2 -0
  15. package/dist/api/generated/schema.js.map +1 -0
  16. package/dist/api/types.d.ts +75 -0
  17. package/dist/api/types.d.ts.map +1 -0
  18. package/dist/api/types.js +2 -0
  19. package/dist/api/types.js.map +1 -0
  20. package/dist/default/ConversationListPanel.d.ts +22 -0
  21. package/dist/default/ConversationListPanel.d.ts.map +1 -0
  22. package/dist/default/ConversationListPanel.js +49 -0
  23. package/dist/default/ConversationListPanel.js.map +1 -0
  24. package/dist/default/ConversationThreadPanel.d.ts +21 -0
  25. package/dist/default/ConversationThreadPanel.d.ts.map +1 -0
  26. package/dist/default/ConversationThreadPanel.js +51 -0
  27. package/dist/default/ConversationThreadPanel.js.map +1 -0
  28. package/dist/default/ErrorAlert.d.ts +8 -0
  29. package/dist/default/ErrorAlert.d.ts.map +1 -0
  30. package/dist/default/ErrorAlert.js +26 -0
  31. package/dist/default/ErrorAlert.js.map +1 -0
  32. package/dist/default/StartChatButton.d.ts +17 -0
  33. package/dist/default/StartChatButton.d.ts.map +1 -0
  34. package/dist/default/StartChatButton.js +18 -0
  35. package/dist/default/StartChatButton.js.map +1 -0
  36. package/dist/default/index.d.ts +21 -0
  37. package/dist/default/index.d.ts.map +1 -0
  38. package/dist/default/index.js +18 -0
  39. package/dist/default/index.js.map +1 -0
  40. package/dist/flows/errors.d.ts +12 -0
  41. package/dist/flows/errors.d.ts.map +1 -0
  42. package/dist/flows/errors.js +13 -0
  43. package/dist/flows/errors.js.map +1 -0
  44. package/dist/flows/freshness.d.ts +69 -0
  45. package/dist/flows/freshness.d.ts.map +1 -0
  46. package/dist/flows/freshness.js +227 -0
  47. package/dist/flows/freshness.js.map +1 -0
  48. package/dist/flows/registry.d.ts +26 -0
  49. package/dist/flows/registry.d.ts.map +1 -0
  50. package/dist/flows/registry.js +24 -0
  51. package/dist/flows/registry.js.map +1 -0
  52. package/dist/headless/ChatProvider.d.ts +22 -0
  53. package/dist/headless/ChatProvider.d.ts.map +1 -0
  54. package/dist/headless/ChatProvider.js +18 -0
  55. package/dist/headless/ChatProvider.js.map +1 -0
  56. package/dist/headless/ConversationList.d.ts +51 -0
  57. package/dist/headless/ConversationList.d.ts.map +1 -0
  58. package/dist/headless/ConversationList.js +47 -0
  59. package/dist/headless/ConversationList.js.map +1 -0
  60. package/dist/headless/ConversationThread.d.ts +50 -0
  61. package/dist/headless/ConversationThread.d.ts.map +1 -0
  62. package/dist/headless/ConversationThread.js +64 -0
  63. package/dist/headless/ConversationThread.js.map +1 -0
  64. package/dist/headless/MessageComposer.d.ts +39 -0
  65. package/dist/headless/MessageComposer.d.ts.map +1 -0
  66. package/dist/headless/MessageComposer.js +45 -0
  67. package/dist/headless/MessageComposer.js.map +1 -0
  68. package/dist/headless/StartDirectChat.d.ts +40 -0
  69. package/dist/headless/StartDirectChat.d.ts.map +1 -0
  70. package/dist/headless/StartDirectChat.js +46 -0
  71. package/dist/headless/StartDirectChat.js.map +1 -0
  72. package/dist/i18n/errorsMap.d.ts +10 -0
  73. package/dist/i18n/errorsMap.d.ts.map +1 -0
  74. package/dist/i18n/errorsMap.js +20 -0
  75. package/dist/i18n/errorsMap.js.map +1 -0
  76. package/dist/i18n/es.d.ts +23 -0
  77. package/dist/i18n/es.d.ts.map +1 -0
  78. package/dist/i18n/es.js +75 -0
  79. package/dist/i18n/es.js.map +1 -0
  80. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  81. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  82. package/dist/i18n/generated/errors.es.gen.js +58 -0
  83. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  84. package/dist/i18n/generated/errors.gen.d.ts +359 -0
  85. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  86. package/dist/i18n/generated/errors.gen.js +183 -0
  87. package/dist/i18n/generated/errors.gen.js.map +1 -0
  88. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  89. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  90. package/dist/i18n/generated/errors.ru.gen.js +58 -0
  91. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  92. package/dist/i18n/keys.d.ts +78 -0
  93. package/dist/i18n/keys.d.ts.map +1 -0
  94. package/dist/i18n/keys.js +130 -0
  95. package/dist/i18n/keys.js.map +1 -0
  96. package/dist/i18n/ru.d.ts +28 -0
  97. package/dist/i18n/ru.d.ts.map +1 -0
  98. package/dist/i18n/ru.js +80 -0
  99. package/dist/i18n/ru.js.map +1 -0
  100. package/dist/index.d.ts +61 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +55 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/model/context.d.ts +11 -0
  105. package/dist/model/context.d.ts.map +1 -0
  106. package/dist/model/context.js +16 -0
  107. package/dist/model/context.js.map +1 -0
  108. package/dist/model/limits.d.ts +12 -0
  109. package/dist/model/limits.d.ts.map +1 -0
  110. package/dist/model/limits.js +12 -0
  111. package/dist/model/limits.js.map +1 -0
  112. package/dist/model/mutations.d.ts +66 -0
  113. package/dist/model/mutations.d.ts.map +1 -0
  114. package/dist/model/mutations.js +156 -0
  115. package/dist/model/mutations.js.map +1 -0
  116. package/dist/model/queries.d.ts +33 -0
  117. package/dist/model/queries.d.ts.map +1 -0
  118. package/dist/model/queries.js +93 -0
  119. package/dist/model/queries.js.map +1 -0
  120. package/dist/model/queryKeys.d.ts +8 -0
  121. package/dist/model/queryKeys.d.ts.map +1 -0
  122. package/dist/model/queryKeys.js +26 -0
  123. package/dist/model/queryKeys.js.map +1 -0
  124. package/dist/model/readMarker.d.ts +25 -0
  125. package/dist/model/readMarker.d.ts.map +1 -0
  126. package/dist/model/readMarker.js +31 -0
  127. package/dist/model/readMarker.js.map +1 -0
  128. package/dist/model/runtime.d.ts +50 -0
  129. package/dist/model/runtime.d.ts.map +1 -0
  130. package/dist/model/runtime.js +27 -0
  131. package/dist/model/runtime.js.map +1 -0
  132. package/dist/model/threadWindow.d.ts +95 -0
  133. package/dist/model/threadWindow.d.ts.map +1 -0
  134. package/dist/model/threadWindow.js +100 -0
  135. package/dist/model/threadWindow.js.map +1 -0
  136. package/dist/nav/manifest.d.ts +23 -0
  137. package/dist/nav/manifest.d.ts.map +1 -0
  138. package/dist/nav/manifest.js +15 -0
  139. package/dist/nav/manifest.js.map +1 -0
  140. package/dist/realtime/chatSocket.d.ts +93 -0
  141. package/dist/realtime/chatSocket.d.ts.map +1 -0
  142. package/dist/realtime/chatSocket.js +199 -0
  143. package/dist/realtime/chatSocket.js.map +1 -0
  144. package/dist/realtime/frames.d.ts +123 -0
  145. package/dist/realtime/frames.d.ts.map +1 -0
  146. package/dist/realtime/frames.js +123 -0
  147. package/dist/realtime/frames.js.map +1 -0
  148. package/dist/realtime/streams.d.ts +49 -0
  149. package/dist/realtime/streams.d.ts.map +1 -0
  150. package/dist/realtime/streams.js +52 -0
  151. package/dist/realtime/streams.js.map +1 -0
  152. package/llms.txt +89 -0
  153. package/manifest.json +733 -0
  154. package/nav-manifest.json +25 -0
  155. package/package.json +114 -0
  156. package/src/analytics/generated/events.json +7 -0
  157. package/src/api/chatApi.ts +142 -0
  158. package/src/api/extensions.ts +18 -0
  159. package/src/api/generated/schema.ts +549 -0
  160. package/src/api/types.ts +87 -0
  161. package/src/default/ConversationListPanel.tsx +163 -0
  162. package/src/default/ConversationThreadPanel.tsx +245 -0
  163. package/src/default/ErrorAlert.tsx +43 -0
  164. package/src/default/StartChatButton.tsx +78 -0
  165. package/src/default/index.ts +20 -0
  166. package/src/flows/errors.ts +16 -0
  167. package/src/flows/freshness.ts +315 -0
  168. package/src/flows/registry.ts +36 -0
  169. package/src/headless/ChatProvider.tsx +23 -0
  170. package/src/headless/ConversationList.tsx +92 -0
  171. package/src/headless/ConversationThread.tsx +107 -0
  172. package/src/headless/MessageComposer.tsx +86 -0
  173. package/src/headless/StartDirectChat.tsx +75 -0
  174. package/src/i18n/errorsMap.ts +31 -0
  175. package/src/i18n/es.ts +95 -0
  176. package/src/i18n/generated/errors.es.gen.ts +64 -0
  177. package/src/i18n/generated/errors.gen.ts +214 -0
  178. package/src/i18n/generated/errors.json +408 -0
  179. package/src/i18n/generated/errors.ru.gen.ts +64 -0
  180. package/src/i18n/keys.ts +152 -0
  181. package/src/i18n/ru.ts +98 -0
  182. package/src/index.ts +189 -0
  183. package/src/model/context.tsx +25 -0
  184. package/src/model/limits.ts +11 -0
  185. package/src/model/mutations.ts +208 -0
  186. package/src/model/queries.ts +120 -0
  187. package/src/model/queryKeys.ts +32 -0
  188. package/src/model/readMarker.ts +32 -0
  189. package/src/model/runtime.ts +90 -0
  190. package/src/model/threadWindow.ts +173 -0
  191. package/src/nav/manifest.ts +36 -0
  192. package/src/realtime/chatSocket.ts +304 -0
  193. package/src/realtime/frames.ts +229 -0
  194. package/src/realtime/streams.ts +85 -0
  195. 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"}