@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,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 const CHAT_WS_REPLAY_LIMIT = 500;
30
+ /** `ChatConsumer.connect()` closes with this when `scope["user"]` is unset. */
31
+ export const CHAT_WS_CLOSE_UNAUTHENTICATED = 4401;
32
+ /** …and with this when the caller is not a participant of the conversation. */
33
+ export const CHAT_WS_CLOSE_NOT_PARTICIPANT = 4403;
34
+ /** The one error code that means "stop replaying, re-read the journal". */
35
+ export const CHAT_WS_RESYNC = "resync";
36
+ // ── parsing (the wire is untrusted structured text) ──────────────────────────
37
+ function isRecord(value) {
38
+ return typeof value === "object" && value !== null;
39
+ }
40
+ function str(value) {
41
+ return typeof value === "string" ? value : null;
42
+ }
43
+ function num(value) {
44
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
45
+ }
46
+ /**
47
+ * Narrow one decoded server frame, or `null` for anything this build does not
48
+ * understand (an unknown `type`, a missing field, a `seq` that is not a
49
+ * number). Unknown is DROPPED, never coerced: a frame this client cannot read
50
+ * must not advance the seq cursor, or the gap it leaves is invisible.
51
+ */
52
+ export function parseServerFrame(value) {
53
+ if (!isRecord(value))
54
+ return null;
55
+ switch (value.type) {
56
+ case "welcome": {
57
+ const conversationId = str(value.conversation_id);
58
+ const serverSeq = num(value.server_seq);
59
+ if (conversationId === null || serverSeq === null)
60
+ return null;
61
+ return {
62
+ type: "welcome",
63
+ conversation_id: conversationId,
64
+ server_seq: serverSeq,
65
+ };
66
+ }
67
+ case "message": {
68
+ const seq = num(value.seq);
69
+ const messageId = str(value.message_id);
70
+ const conversationId = str(value.conversation_id);
71
+ const createdAt = str(value.created_at);
72
+ if (seq === null ||
73
+ messageId === null ||
74
+ conversationId === null ||
75
+ createdAt === null) {
76
+ return null;
77
+ }
78
+ const attachments = Array.isArray(value.attachments)
79
+ ? value.attachments.filter((a) => typeof a === "string")
80
+ : [];
81
+ return {
82
+ type: "message",
83
+ message_id: messageId,
84
+ conversation_id: conversationId,
85
+ sender_id: str(value.sender_id),
86
+ seq,
87
+ kind: str(value.kind) ?? "text",
88
+ body: str(value.body) ?? "",
89
+ reply_to: str(value.reply_to),
90
+ attachments,
91
+ created_at: createdAt,
92
+ };
93
+ }
94
+ case "replay_done": {
95
+ const upTo = num(value.up_to_seq);
96
+ if (upTo === null)
97
+ return null;
98
+ return { type: "replay_done", up_to_seq: upTo };
99
+ }
100
+ case "error": {
101
+ const code = str(value.code);
102
+ if (code === null)
103
+ return null;
104
+ return { type: "error", code, message: str(value.message) ?? "" };
105
+ }
106
+ case "pong":
107
+ return { type: "pong" };
108
+ default:
109
+ return null;
110
+ }
111
+ }
112
+ /** Decode a socket payload (JSON text) into a frame, or `null`. */
113
+ export function decodeServerFrame(data) {
114
+ if (typeof data !== "string")
115
+ return null;
116
+ try {
117
+ return parseServerFrame(JSON.parse(data));
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ //# sourceMappingURL=frames.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/realtime/frames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAExC,+EAA+E;AAC/E,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,CAAC;AAClD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,6BAA6B,GAAG,IAAI,CAAC;AAwGlD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAEvC,gFAAgF;AAEhF,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACrD,CAAC;AAED,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAClD,CAAC;AAED,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACnB,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,cAAc,GAAG,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;YAClD,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACxC,IAAI,cAAc,KAAK,IAAI,IAAI,SAAS,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC;YAC/D,OAAO;gBACL,IAAI,EAAE,SAAS;gBACf,eAAe,EAAE,cAAc;gBAC/B,UAAU,EAAE,SAAS;aACtB,CAAC;QACJ,CAAC;QACD,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC3B,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACxC,MAAM,cAAc,GAAG,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;YAClD,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACxC,IACE,GAAG,KAAK,IAAI;gBACZ,SAAS,KAAK,IAAI;gBAClB,cAAc,KAAK,IAAI;gBACvB,SAAS,KAAK,IAAI,EAClB,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC;gBAClD,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;gBACrE,CAAC,CAAC,EAAE,CAAC;YACP,OAAO;gBACL,IAAI,EAAE,SAAS;gBACf,UAAU,EAAE,SAAS;gBACrB,eAAe,EAAE,cAAc;gBAC/B,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC;gBAC/B,GAAG;gBACH,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM;gBAC/B,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE;gBAC3B,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC;gBAC7B,WAAW;gBACX,UAAU,EAAE,SAAS;aACtB,CAAC;QACJ,CAAC;QACD,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YAClC,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC;YAC/B,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QAClD,CAAC;QACD,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC7B,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC;YAC/B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QACpE,CAAC;QACD,KAAK,MAAM;YACT,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAC1B;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,iBAAiB,CAAC,IAAa;IAC7C,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,CAAC;QACH,OAAO,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * How this pair NAMES the things it can be fresh about (realtime spec §7,
3
+ * "how a pair declares its channels"): typed stream keys plus the one rule
4
+ * that turns a key into a socket URL. Everything that decides *whether* a stream
5
+ * has a socket at all lives here, so the transport seam above can stay a
6
+ * two-line question.
7
+ */
8
+ /** A conversation's own journal — the module mounts a socket per thread. */
9
+ export interface ChatConversationStream {
10
+ readonly kind: "conversation";
11
+ readonly conversationId: string;
12
+ }
13
+ /**
14
+ * The caller's inbox (their conversation list).
15
+ *
16
+ * There is NO socket for this one, and that is a fact about the backend, not
17
+ * a gap in this pair: `stapel_chat.routing` mounts exactly one pattern,
18
+ * `ws/chat/<uuid:conversation_id>`, and the Channels group a message fans out
19
+ * to is `chat.conv.<id>` — per thread. Nothing broadcasts "your list moved".
20
+ * So the inbox is kept fresh by the polling half of the seam, and the seam is
21
+ * the reason no screen has to know that.
22
+ */
23
+ export interface ChatInboxStream {
24
+ readonly kind: "inbox";
25
+ }
26
+ export type ChatStreamKey = ChatConversationStream | ChatInboxStream;
27
+ export declare function chatConversationStream(conversationId: string): ChatConversationStream;
28
+ export declare function chatInboxStream(): ChatInboxStream;
29
+ /** A stable string for a stream key — a React dependency, a log line. */
30
+ export declare function chatStreamId(stream: ChatStreamKey): string;
31
+ /**
32
+ * Derive the socket base from the REST base URL.
33
+ *
34
+ * The mount point is canonical (`stapel_chat/routing.py`, and the fleet
35
+ * convention `/ws/<mod>/...` from the realtime spec §8): the sockets sit at
36
+ * the HOST ROOT, not under the module's API prefix, so only the origin of
37
+ * `baseUrl` is used. `http` → `ws`, `https` → `wss`.
38
+ *
39
+ * Returns `null` when no origin can be resolved — a relative `baseUrl` with
40
+ * no `origin` (server-side rendering, a node test). `null` is not a failure:
41
+ * it means "this build cannot open a socket", and the transport seam polls.
42
+ */
43
+ export declare function deriveChatSocketBase(baseUrl: string, origin?: string | null): string | null;
44
+ /**
45
+ * The socket URL for a stream, or `null` when the stream has none (the
46
+ * inbox) or no base could be derived.
47
+ */
48
+ export declare function chatSocketUrl(base: string | null, stream: ChatStreamKey): string | null;
49
+ //# sourceMappingURL=streams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streams.d.ts","sourceRoot":"","sources":["../../src/realtime/streams.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,aAAa,GAAG,sBAAsB,GAAG,eAAe,CAAC;AAErE,wBAAgB,sBAAsB,CACpC,cAAc,EAAE,MAAM,GACrB,sBAAsB,CAExB;AAED,wBAAgB,eAAe,IAAI,eAAe,CAEjD;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAI1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,MAAM,GAAG,IAAI,CASf;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,MAAM,EAAE,aAAa,GACpB,MAAM,GAAG,IAAI,CAIf"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * How this pair NAMES the things it can be fresh about (realtime spec §7,
3
+ * "how a pair declares its channels"): typed stream keys plus the one rule
4
+ * that turns a key into a socket URL. Everything that decides *whether* a stream
5
+ * has a socket at all lives here, so the transport seam above can stay a
6
+ * two-line question.
7
+ */
8
+ export function chatConversationStream(conversationId) {
9
+ return { kind: "conversation", conversationId };
10
+ }
11
+ export function chatInboxStream() {
12
+ return { kind: "inbox" };
13
+ }
14
+ /** A stable string for a stream key — a React dependency, a log line. */
15
+ export function chatStreamId(stream) {
16
+ return stream.kind === "conversation"
17
+ ? `conversation:${stream.conversationId}`
18
+ : "inbox";
19
+ }
20
+ /**
21
+ * Derive the socket base from the REST base URL.
22
+ *
23
+ * The mount point is canonical (`stapel_chat/routing.py`, and the fleet
24
+ * convention `/ws/<mod>/...` from the realtime spec §8): the sockets sit at
25
+ * the HOST ROOT, not under the module's API prefix, so only the origin of
26
+ * `baseUrl` is used. `http` → `ws`, `https` → `wss`.
27
+ *
28
+ * Returns `null` when no origin can be resolved — a relative `baseUrl` with
29
+ * no `origin` (server-side rendering, a node test). `null` is not a failure:
30
+ * it means "this build cannot open a socket", and the transport seam polls.
31
+ */
32
+ export function deriveChatSocketBase(baseUrl, origin) {
33
+ try {
34
+ const url = new URL(baseUrl, origin ?? undefined);
35
+ const protocol = url.protocol === "https:" || url.protocol === "wss:" ? "wss:" : "ws:";
36
+ return `${protocol}//${url.host}/ws/chat/`;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * The socket URL for a stream, or `null` when the stream has none (the
44
+ * inbox) or no base could be derived.
45
+ */
46
+ export function chatSocketUrl(base, stream) {
47
+ if (base === null || stream.kind !== "conversation")
48
+ return null;
49
+ const prefix = base.endsWith("/") ? base : `${base}/`;
50
+ return `${prefix}${encodeURIComponent(stream.conversationId)}`;
51
+ }
52
+ //# sourceMappingURL=streams.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streams.js","sourceRoot":"","sources":["../../src/realtime/streams.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,MAAM,UAAU,sBAAsB,CACpC,cAAsB;IAEtB,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC3B,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAAC,MAAqB;IAChD,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc;QACnC,CAAC,CAAC,gBAAgB,MAAM,CAAC,cAAc,EAAE;QACzC,CAAC,CAAC,OAAO,CAAC;AACd,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,MAAsB;IAEtB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,MAAM,IAAI,SAAS,CAAC,CAAC;QAClD,MAAM,QAAQ,GACZ,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;QACxE,OAAO,GAAG,QAAQ,KAAK,GAAG,CAAC,IAAI,WAAW,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAmB,EACnB,MAAqB;IAErB,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc;QAAE,OAAO,IAAI,CAAC;IACjE,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC;IACtD,OAAO,GAAG,MAAM,GAAG,kBAAkB,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC;AACjE,CAAC"}
package/llms.txt ADDED
@@ -0,0 +1,89 @@
1
+ # @stapel/chat-react 0.1.0
2
+
3
+ Headless React flow pair for stapel-chat (contract >=0.2 <0.3) — business + state, zero visual opinion.
4
+ Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
5
+ verification-403 interception, i18n engine, analytics facade, TanStack Query layer.
6
+
7
+ ## The one right way (do this, the rest is a review/lint smell)
8
+ - No raw fetch/axios. The client is injected via <ChatProvider>/StapelConfigProvider;
9
+ every hook and flow already carries auth, refresh, and the error envelope.
10
+ - Render errors, never try/catch them: a flow's state carries FlowError{code,params};
11
+ render `t(code, params)` and branch on `explainChatError(code)` remediation.
12
+ - Server state = the use* hooks (query layer); keys come only from chatQueryKeys.
13
+ - User strings = i18n keys (registerChatI18n); never string literals.
14
+ - Sign-in UI = a headless flow component; copy it (shadcn-style) to restyle.
15
+
16
+ ## Layers
17
+ api (typed client) · model (hooks, session) · flows (machines) · headless · i18n
18
+
19
+ ## Documented flows (flows.json — canonical id, steps, endpoints)
20
+
21
+ ## Operations (typed; use the named op, never a path string)
22
+ Request/response schema names are in manifest.json + the generated types.
23
+ Paths are relative to `/chat/api/v1/`.
24
+ - chat_api_v1_conversations_create: POST /conversations
25
+ - chat_api_v1_conversations_list: GET /conversations
26
+ - chat_api_v1_conversations_messages_create: POST /conversations/{conversation_id}/messages
27
+ - chat_api_v1_conversations_messages_list: GET /conversations/{conversation_id}/messages
28
+ - chat_api_v1_conversations_read_create: POST /conversations/{conversation_id}/read
29
+ - chat_api_v1_conversations_retrieve: GET /conversations/{conversation_id}
30
+ - chat_api_v1_support_conversations_assign_create: POST /support/conversations/{conversation_id}/assign
31
+ - chat_api_v1_support_conversations_reopen_create: POST /support/conversations/{conversation_id}/reopen
32
+ - chat_api_v1_support_conversations_resolve_create: POST /support/conversations/{conversation_id}/resolve
33
+ - chat_api_v1_support_queue_list: GET /support/queue
34
+
35
+ ## Query hooks (server state; keys come only from the key factory)
36
+ - useConversation (query) → conversation
37
+ - useLoadOlderMessages (mutation) → messages
38
+ - useMarkRead (mutation) → markRead
39
+ - useSendMessage (mutation) → sendMessage
40
+ - useStartDirectChat (mutation) → createConversation
41
+ - useThread (query) → messages
42
+
43
+ ## Errors (render t(code, params); UX from remediation)
44
+ 54 keys (full catalog: manifest.json §errors). By remediation: fix_input 25 · retry 16 · verify 5 · wait_and_retry 5 · contact_support 2 · reauthenticate 1.
45
+ Param-bearing keys (interpolation slots matter):
46
+ - error.400.field.blank [400] → fix_input {field}
47
+ - error.400.field.does_not_exist [400] → fix_input {field}
48
+ - error.400.field.invalid [400] → fix_input {field}
49
+ - error.400.field.invalid_choice [400] → fix_input {field}
50
+ - error.400.field.max_length [400] → fix_input {field,max_length}
51
+ - error.400.field.max_value [400] → fix_input {field,max_value}
52
+ - error.400.field.min_length [400] → fix_input {field,min_length}
53
+ - error.400.field.min_value [400] → fix_input {field,min_value}
54
+ - error.400.field.null [400] → fix_input {field}
55
+ - error.400.field.required [400] → fix_input {field}
56
+ - error.400.field.unique [400] → fix_input {field}
57
+ - error.429.rate_limit [429] → wait_and_retry {retry_after_minutes}
58
+
59
+ ## Analytics events (typed; defineEvent → events.json, drift-gated)
60
+ - (no app defineEvent() in this pair — its analytic events are the
61
+ auto-instrumented flow funnels below)
62
+
63
+ ## Flow funnels (auto-instrumented: flow.<id>.<step> {phase})
64
+
65
+ ```tsx
66
+ // Typed event + tracked() click (the one right way; §3.1).
67
+ const planSelected = defineEvent({
68
+ name: "pricing.plan.selected",
69
+ description: "User picked a plan",
70
+ props: { plan: prop.oneOf(["free", "pro", "team"], "Plan code") },
71
+ });
72
+ const { tracked } = useTracked();
73
+ <Button onClick={tracked(planSelected, { plan }, startCheckout)} />
74
+ // A click that STEPS a flow machine is already instrumented — mark it
75
+ // data-analytics="flow" instead; tracked() on top double-counts (§3.2).
76
+ ```
77
+
78
+ ## Demos (defineDemo → manifest.demos; compiled, linted, rendered examples)
79
+ - chat.conversations → <ConversationList> [default] demo/ConversationList.demo.tsx
80
+ - chat.start → <StartDirectChat> [default|no-seller|own-listing] demo/StartDirectChat.demo.tsx
81
+ - chat.thread → <ConversationThread> [default] demo/ConversationThread.demo.tsx
82
+ Each source file is the canonical usage snippet (open the default variant).
83
+
84
+ ## Snippets
85
+ ```tsx
86
+ // Error rendering + remediation branch (one pattern for every pair).
87
+ const r = explainChatError(err.code); // 'wait_and_retry' | 'verify' | ...
88
+ return <Alert action={r}>{t(err.code, err.params)}</Alert>;
89
+ ```