@antzsoft/chat-core 1.4.3 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -747,7 +747,7 @@ import { messagesApi } from '@antzsoft/chat-core';
747
747
  |---|---|---|
748
748
  | `list` | `(conversationId: string, params?: ListMessagesParams) => Promise<CursorPaginatedResponse<Message>>` | Fetch messages with cursor pagination. |
749
749
  | `get` | `(messageId: string) => Promise<Message>` | Fetch a single message. |
750
- | `send` | `(conversationId: string, payload: SendData) => Promise<Message>` | Send a message via REST (use `socketEmit.sendMessage` for real-time delivery). |
750
+ | `send` | `(conversationId: string, payload: SendData) => Promise<Message>` | Send a message via REST (use `socketEmit.sendMessage` for real-time delivery). Pass `payload.tempId` and reuse the SAME value on retry to make a retry-after-timeout safe — see "Retry safety" below. |
751
751
  | `update` | `(messageId: string, text: string) => Promise<Message>` | Edit message text. |
752
752
  | `delete` | `(messageId: string) => Promise<void>` | Delete a message for everyone (own message within window, or admin). |
753
753
  | `deleteForMe` | `(messageId: string) => Promise<void>` | Hide a message for the current user only — other participants are unaffected. |
@@ -779,7 +779,7 @@ interface SendData {
779
779
  text?: string;
780
780
  attachments?: SendMessageAttachment[];
781
781
  replyTo?: string; // messageId of the message being replied to
782
- tempId?: string; // Client-generated ID for optimistic UI
782
+ tempId?: string; // Client-generated idempotency key see "Retry safety" below. NOT auto-generated if omitted.
783
783
  mentions?: string[]; // Mentioned userIds ('all' for @all); derived from @[name](id) tokens in text
784
784
  }
785
785
 
@@ -813,13 +813,26 @@ await messagesApi.send('conv-abc', {
813
813
  const results = await messagesApi.search({ query: 'deployment', conversationId: 'conv-abc' });
814
814
  ```
815
815
 
816
+ #### Message Send Retry Safety (v1.4.4+)
817
+
818
+ If `messagesApi.send()`/`socketEmit.sendMessage()` times out client-side, you don't know whether the server actually created the message before the response was lost. Retrying blind can create a duplicate. `tempId` fixes this — the server checks `(conversationId, senderId, tempId)` before creating a message, on both the REST send path and the socket path:
819
+
820
+ - Generate **one** `tempId` per send attempt (a real UUID — `generateUUID` from `'@antzsoft/chat-core/internal'`, or `crypto.randomUUID()`).
821
+ - Reuse the **exact same** `tempId` if you retry that same send. Never mint a new one for a retry — only for a genuinely new message.
822
+ - A retry with a matching `tempId` returns the already-created message instead of creating a duplicate. On the socket path this is fully silent to everyone else — the retrying client still gets its `message_ack` (so its optimistic bubble reconciles), but no second `new_message` is broadcast to the room and no second push notification fires, since the original successful attempt already delivered both.
823
+ - Unlike `forward()`, `send()` does **not** auto-generate a `tempId` if you omit it — omitting it, or generating a fresh one on every retry, gets no dedup protection and reproduces the exact pre-1.4.4 behavior.
824
+ - `socketEmit.sendMessage`'s `SendMessagePayload.tempId` was always a **required** field — every client already sends one on every call. This release is what makes the server actually act on it; no wire-format change.
825
+
826
+ **`useChat().retrySendMessage(failedMessageId)` (both UI SDKs)** does all of this correctly for you: it resends the exact original payload (same `tempId`, same already-uploaded attachment `fileId`s — no re-upload) for a message whose `deliveryStatus` is `'failed'`. The built-in `MessageItem` component renders a tappable "Retry" on the failed-delivery indicator and a "Retry" entry in the message action menu, both wired to this — no extra code needed if you're using the prebuilt UI. If you're driving `messagesApi`/`socketEmit` directly (headless usage), you own remembering the `tempId` per attempt yourself.
827
+
816
828
  #### Forward Message (v1.4.2+)
817
829
 
818
830
  Forwarding creates an **independent copy** of a message in each target conversation — never a shared row. Each copy gets its own ID, sequence number, read receipts, and delete lifecycle; deleting one copy never affects another, and none of them affect the original.
819
831
 
820
832
  | Field / method | Type | Description |
821
833
  |---|---|---|
822
- | `messagesApi.forward(messageId, targetConversationIds, attachmentIds?)` | `Promise<ForwardResult[]>` | Forwards into up to `MAX_FORWARD_TARGETS` (5) conversations in one call — fewer if the source message is highly forwarded, see Limits below. Pass `attachmentIds` to forward only a subset of the source message's attachments (e.g. one image out of a multi-image message); omit it to forward the whole message unchanged. An ID not on the source message is ignored. |
834
+ | `messagesApi.forward(messageId, targetConversationIds, attachmentIds?, tempId?)` | `Promise<ForwardResult[]>` | Forwards into up to `MAX_FORWARD_TARGETS` (5) conversations in one call — fewer if the source message is highly forwarded, see Limits below. Pass `attachmentIds` to forward only a subset of the source message's attachments (e.g. one image out of a multi-image message); omit it to forward the whole message unchanged. An ID not on the source message is ignored. `tempId` makes retries safe — see "Retry safety" below; auto-generated if omitted. Uses the socket transport when connected, REST otherwise — see "Transport" below. |
835
+ | `socketEmit.forwardMessage(payload: ForwardMessagePayload)` | `Promise<ForwardAckPayload>` | Lower-level socket-only entry point that `messagesApi.forward()` uses internally when a socket is connected. Most consumers should call `messagesApi.forward()` instead so REST fallback is automatic. |
823
836
  | `Message.forwardedFrom` | `MessageForwardReference \| undefined` | Present on a message created via forwarding. |
824
837
  | `MAX_FORWARD_TARGETS` | `number` | `5` — matches the server's normal-case cap. |
825
838
  | `HIGHLY_FORWARDED_DEPTH_THRESHOLD` | `number` | `5` — matches the server's `forwardDepth` value that triggers BOTH the "Forwarded many times" label and the reduced fan-out cap. Use this for the label instead of hardcoding a number. |
@@ -870,6 +883,16 @@ await messagesApi.forward(messageId, [convA], [message.content.attachments![1].i
870
883
 
871
884
  **Rendering:** show a "Forwarded" (or "Forwarded many times" at `forwardDepth >= HIGHLY_FORWARDED_DEPTH_THRESHOLD`) label when `message.forwardedFrom` is present — do not render a sender name or content preview for it (unlike `replyTo`), since the message's own `content` already holds what was forwarded. The built-in `MessageItem` component in both `@antzsoft/chat-web-sdk` and `@antzsoft/chat-rn-sdk` already renders this label using the exported constant and exposes a "Forward" action in the message menu — no extra wiring needed if you're using the prebuilt UI.
872
885
 
886
+ **Transport (v1.4.4+).** `forward()` sends over the `forward_message` socket event (acked via the standard socket.io callback — the same request/response ack pattern as `update_message`/`pin_message`/etc., not a separate broadcast event) whenever a socket is connected, and transparently falls back to REST otherwise — you never call the socket path directly; `messagesApi.forward()` picks it for you. Either way it runs through the exact same server-side `MessagesService.forward()` code path, so broadcast/`conversation_updated`/push-notification behavior is identical regardless of which transport was actually used. There is still no built-in *automatic* retry on timeout — if a call times out client-side, you decide whether/when to retry — but unlike a plain REST call, a client that's connected gets the lower-latency socket path without any code change.
887
+
888
+ **Retry safety (v1.4.4+).** The `tempId` parameter is what makes a retry safe, on either transport:
889
+
890
+ - Generate **one** `tempId` per forward *action* (one user tap on "Forward," covering all its target conversations) — not one per target.
891
+ - Reuse the **exact same** `tempId` if you retry that same action (e.g. the user taps "Forward" again after a partial failure). Never mint a new one for a retry — only when the user starts a genuinely new forward.
892
+ - The server checks `(targetConversationId, senderId, tempId)` — scoped per target, since one forward action can create up to 5 messages, one per target — before creating anything. A retry with a matching `tempId` returns the message that already exists for that target instead of creating a duplicate; targets that failed the first time are retried normally.
893
+ - If you omit `tempId`, one is auto-generated per call — meaning a retry without passing the *same* value back gets **no dedup protection** and may create a duplicate for any target that actually succeeded before the failure was reported. The built-in `ForwardPicker` component in both `@antzsoft/chat-web-sdk` and `@antzsoft/chat-rn-sdk` already does this correctly (one `tempId` per picker session, reused across retries) — no extra wiring needed if you're using the prebuilt UI.
894
+ - Regular message sends (`socketEmit.sendMessage` / `messagesApi.send()`) get this same protection — see "Retry safety" above.
895
+
873
896
  #### Jump to first unread message
874
897
 
875
898
  Use `direction: 'after'` with the user's `lastReadMessageId` as the cursor to fetch only the unread messages. This powers a scroll-to-first-unread experience with an "↑ Unread messages" divider.