@antzsoft/chat-core 1.2.4 → 1.2.6

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
@@ -1104,6 +1104,38 @@ async function onForeground() {
1104
1104
 
1105
1105
  > **Using `@antzsoft/chat-web-sdk` or `@antzsoft/chat-rn-sdk`?** You don't need any of this — `useConversations()` handles socket subscriptions internally. Just sum `conversations.reduce((s, c) => s + (c.unreadCount ?? 0), 0)` and it updates automatically.
1106
1106
 
1107
+ #### Clear / Delete Chat (v1.2.6+)
1108
+
1109
+ `conversationsApi.delete(conversationId)` hides a conversation from the caller's list. Any participant can call it — no admin role required. Other participants are completely unaffected.
1110
+
1111
+ | Scenario | Call | Effect |
1112
+ |---|---|---|
1113
+ | **Delete Chat** (DM) | `conversationsApi.delete(id)` | Hides DM, wipes periods. Re-opens with new history only when other party messages. |
1114
+ | **Exit Group** | `conversationsApi.leave(id)` | Caller inactive, stays in list read-only. Auto-promotes admin if needed. |
1115
+ | **Exit and Delete** | `conversationsApi.leave(id, true)` | Atomic exit + hide. Periods wiped. No race window. |
1116
+ | **Delete Group** (post-exit) | `conversationsApi.delete(id)` | Hides already-exited group entry. Others unaffected. |
1117
+
1118
+ ```typescript
1119
+ // Delete Chat — DM
1120
+ await conversationsApi.delete(dmConversationId);
1121
+
1122
+ // Exit Group
1123
+ await conversationsApi.leave(groupId);
1124
+
1125
+ // Exit and Delete (atomic — one write)
1126
+ await conversationsApi.leave(groupId, true);
1127
+
1128
+ // Listen for confirmation on the caller's own sockets
1129
+ socket.on('conversation_deleted', ({ conversationId }) => {
1130
+ removeFromConversationList(conversationId);
1131
+ if (activeConversationId === conversationId) navigateBackToList();
1132
+ });
1133
+ ```
1134
+
1135
+ **Message history after re-add:** if the user left with `delete()` or `leave(true)`, membership periods are wiped — only messages from the re-add point are visible. Plain `leave()` preserves prior history windows.
1136
+
1137
+ **Web/RN SDK hooks expose named mutations:** `leaveGroup`, `leaveAndDeleteGroup`, `deleteGroup` (web) / `deleteConversation` (RN) — cache updated optimistically, no manual invalidation needed.
1138
+
1107
1139
  ---
1108
1140
 
1109
1141
  ### Storage API (`storageApi` and `uploadBatch`)
@@ -1669,6 +1701,197 @@ const config = await appConfigApi.get();
1669
1701
 
1670
1702
  ---
1671
1703
 
1704
+ ## Error Handling
1705
+
1706
+ All SDK errors — whether from REST calls, socket operations, or internal state — are instances of `AntzChatError` or one of its subclasses. Every error carries a machine-readable `code`, a human-readable `message`, a `retryable` boolean, and an optional `context` object with extra diagnostics.
1707
+
1708
+ ### Error classes
1709
+
1710
+ | Class | When thrown | `retryable` |
1711
+ |---|---|---|
1712
+ | `AntzChatAuthError` | 401 after token refresh also fails, or no refresh token exists | `false` |
1713
+ | `AntzChatValidationError` | 400 / 422 — bad input, validation failure | `false` |
1714
+ | `AntzChatPermissionError` | 403 — insufficient permissions | `false` |
1715
+ | `AntzChatNetworkError` | Network failure, timeout, socket disconnect, queue overflow | `true` |
1716
+ | `AntzChatServerError` | 5xx or unexpected server errors | `true` |
1717
+ | `AntzChatError` (base) | Transit config mismatch, unknown errors | varies |
1718
+
1719
+ All classes extend the native `Error` class, so existing `catch (err)` blocks continue to work unchanged.
1720
+
1721
+ ### Error codes
1722
+
1723
+ Switch on `err.code` for fine-grained handling:
1724
+
1725
+ | Code | Class | Source |
1726
+ |---|---|---|
1727
+ | `SESSION_EXPIRED` | `AntzChatAuthError` | 401, no prior retry |
1728
+ | `AUTH_FAILED` | `AntzChatAuthError` | 401 after token refresh also failed |
1729
+ | `PERMISSION_DENIED` | `AntzChatPermissionError` | 403 |
1730
+ | `VALIDATION_ERROR` | `AntzChatValidationError` | 400 / 422 |
1731
+ | `NOT_FOUND` | `AntzChatServerError` | 404 |
1732
+ | `RATE_LIMITED` | `AntzChatNetworkError` | 429 |
1733
+ | `NETWORK_ERROR` | `AntzChatNetworkError` | No response / connection failure |
1734
+ | `SOCKET_TIMEOUT` | `AntzChatNetworkError` | ACK timeout (5 s) or reconnect timeout (15 s) |
1735
+ | `SOCKET_NOT_CONNECTED` | `AntzChatNetworkError` | Socket not up when an ack-required emit fires |
1736
+ | `SEND_QUEUE_FULL` | `AntzChatNetworkError` | Per-conversation queue > 100 pending messages |
1737
+ | `MESSAGE_DROPPED` | `AntzChatNetworkError` | Message waited > 30 s in queue before socket ready |
1738
+ | `TRANSIT_MISMATCH` | `AntzChatError` | SDK and server transit-encryption config differ |
1739
+ | `SERVER_ERROR` | `AntzChatServerError` | 5xx or unrecognised HTTP error |
1740
+
1741
+ ### Imports
1742
+
1743
+ ```typescript
1744
+ import {
1745
+ AntzChatError,
1746
+ AntzChatAuthError,
1747
+ AntzChatValidationError,
1748
+ AntzChatNetworkError,
1749
+ AntzChatPermissionError,
1750
+ AntzChatServerError,
1751
+ } from '@antzsoft/chat-core';
1752
+ ```
1753
+
1754
+ ### Usage patterns
1755
+
1756
+ #### instanceof branching
1757
+
1758
+ ```typescript
1759
+ import {
1760
+ AntzChatAuthError,
1761
+ AntzChatValidationError,
1762
+ AntzChatNetworkError,
1763
+ AntzChatPermissionError,
1764
+ AntzChatServerError,
1765
+ } from '@antzsoft/chat-core';
1766
+
1767
+ try {
1768
+ await authApi.login({ email, password });
1769
+ } catch (err) {
1770
+ if (err instanceof AntzChatAuthError) showLoginError(err.message);
1771
+ else if (err instanceof AntzChatValidationError) showFieldErrors(err.fields ?? [err.message]);
1772
+ else if (err instanceof AntzChatPermissionError) showPermissionDenied();
1773
+ else if (err instanceof AntzChatNetworkError && err.retryable) scheduleRetry();
1774
+ else if (err instanceof AntzChatServerError) logAndShowGenericError(err);
1775
+ else throw err; // unknown — re-throw
1776
+ }
1777
+ ```
1778
+
1779
+ #### code switching (fine-grained)
1780
+
1781
+ ```typescript
1782
+ import { AntzChatError } from '@antzsoft/chat-core';
1783
+
1784
+ try {
1785
+ await socketEmit.sendMessage(payload);
1786
+ } catch (err) {
1787
+ if (!(err instanceof AntzChatError)) throw err;
1788
+ switch (err.code) {
1789
+ case 'SESSION_EXPIRED': redirectToLogin(); break;
1790
+ case 'SEND_QUEUE_FULL': showToast('Too many messages in flight'); break;
1791
+ case 'MESSAGE_DROPPED': showToast('Message timed out — try again'); break;
1792
+ case 'SOCKET_TIMEOUT':
1793
+ case 'SOCKET_NOT_CONNECTED': showOfflineBanner(); break;
1794
+ case 'PERMISSION_DENIED': showToast('You cannot send here'); break;
1795
+ default: console.error(err.code, err.message, err.context);
1796
+ }
1797
+ }
1798
+ ```
1799
+
1800
+ #### retryable flag
1801
+
1802
+ ```typescript
1803
+ import { AntzChatError } from '@antzsoft/chat-core';
1804
+
1805
+ async function sendWithRetry(payload, maxAttempts = 3) {
1806
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1807
+ try {
1808
+ return await socketEmit.sendMessage(payload);
1809
+ } catch (err) {
1810
+ if (err instanceof AntzChatError && err.retryable && attempt < maxAttempts) {
1811
+ await new Promise(r => setTimeout(r, attempt * 1000)); // back-off
1812
+ continue;
1813
+ }
1814
+ throw err;
1815
+ }
1816
+ }
1817
+ }
1818
+ ```
1819
+
1820
+ #### AntzChatValidationError — field errors
1821
+
1822
+ When the server returns `message` as a string array (e.g. class-validator errors), `AntzChatValidationError.fields` is populated with the individual messages:
1823
+
1824
+ ```typescript
1825
+ import { AntzChatValidationError } from '@antzsoft/chat-core';
1826
+
1827
+ try {
1828
+ await authApi.register(payload);
1829
+ } catch (err) {
1830
+ if (err instanceof AntzChatValidationError) {
1831
+ console.log(err.message); // "email must be a valid email; password is too short"
1832
+ console.log(err.fields); // ["email must be a valid email", "password is too short"]
1833
+ }
1834
+ }
1835
+ ```
1836
+
1837
+ #### Transit encryption mismatch
1838
+
1839
+ If the SDK and server have mismatched `transitEncryption` settings, `connectSocket` throws a non-retryable `AntzChatError` with code `TRANSIT_MISMATCH` before any socket is created:
1840
+
1841
+ ```typescript
1842
+ import { AntzChatError } from '@antzsoft/chat-core';
1843
+
1844
+ try {
1845
+ await client.connect();
1846
+ } catch (err) {
1847
+ if (err instanceof AntzChatError && err.code === 'TRANSIT_MISMATCH') {
1848
+ console.error('Config mismatch:', err.message, err.context);
1849
+ // err.context = { sdkEnabled: true, serverEnabled: false }
1850
+ }
1851
+ }
1852
+ ```
1853
+
1854
+ ### What stays silent (by design)
1855
+
1856
+ These three cases intentionally do not throw:
1857
+
1858
+ | Situation | Reason |
1859
+ |---|---|
1860
+ | `socketEmit.typing()` / `socketEmit.markRead()` — socket not connected | Best-effort only; callers must not be forced to wrap them |
1861
+ | Transit session timeout (5 s) — server event not received | SDK degrades gracefully to unencrypted mode and continues |
1862
+ | `usersApi.getPreferences()` failure | Returns `null`; preferences are non-critical and defaults apply |
1863
+
1864
+ ### Error context
1865
+
1866
+ Every `AntzChatError` carries a `context` object with extra diagnostics. It is safe to log but never needed for control flow:
1867
+
1868
+ ```typescript
1869
+ catch (err) {
1870
+ if (err instanceof AntzChatError) {
1871
+ // context contains: httpStatus, path, serverError, axiosCode, event, decryptionFailed, etc.
1872
+ console.error('[AntzChat]', err.name, err.code, err.message, err.context);
1873
+ }
1874
+ }
1875
+ ```
1876
+
1877
+ If transit decryption fails for an error response body, `context.decryptionFailed = true` is set and the generic HTTP-status-based class is still returned — no unhandled exception.
1878
+
1879
+ ### normalizeAxiosError
1880
+
1881
+ If you make raw axios calls through `getApiClient()` and want the same normalisation:
1882
+
1883
+ ```typescript
1884
+ import { getApiClient, normalizeAxiosError, AntzChatError } from '@antzsoft/chat-core';
1885
+
1886
+ try {
1887
+ const { data } = await getApiClient().get('/some/endpoint');
1888
+ } catch (err) {
1889
+ throw normalizeAxiosError(err); // always returns AntzChatError (or subclass)
1890
+ }
1891
+ ```
1892
+
1893
+ ---
1894
+
1672
1895
  ### Socket
1673
1896
 
1674
1897
  #### Connection management
@@ -2553,6 +2776,103 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
2553
2776
 
2554
2777
  ## Changelog
2555
2778
 
2779
+ ### v1.2.6
2780
+
2781
+ - **New: Clear / Delete Chat — hide any conversation without affecting other participants** — `conversationsApi.delete(conversationId)` is now available to any participant (no admin role required) on both DMs and groups. It hides the conversation from the caller's list only; all other participants are unaffected.
2782
+
2783
+ | Scenario | Call | Effect |
2784
+ |---|---|---|
2785
+ | **Delete Chat** (DM) | `conversationsApi.delete(id)` | Hidden from list, periods wiped. Re-opens with new history only when other party messages. |
2786
+ | **Exit Group** | `conversationsApi.leave(id)` | Caller inactive, stays read-only. Server auto-promotes admin if needed. |
2787
+ | **Exit and Delete** | `conversationsApi.leave(id, true)` | Atomic exit + hide. Periods wiped. |
2788
+ | **Delete Group** (post-exit) | `conversationsApi.delete(id)` | Hides the already-exited entry. Others unaffected. |
2789
+
2790
+ ```typescript
2791
+ // Delete a DM ("Delete Chat")
2792
+ await conversationsApi.delete(dmConversationId);
2793
+
2794
+ // Exit and Delete a group
2795
+ await conversationsApi.leave(groupId, true);
2796
+
2797
+ // Socket event fired to the caller's own sockets
2798
+ socket.on('conversation_deleted', ({ conversationId }) => {
2799
+ removeFromList(conversationId);
2800
+ if (activeConversationId === conversationId) navigateBackToList();
2801
+ });
2802
+ ```
2803
+
2804
+ **Web SDK (`useConversations`):** `leaveGroup` / `leaveAndDeleteGroup` / `deleteGroup` mutations — optimistic cache update, no manual invalidation needed.
2805
+ **RN SDK (`useConversations`):** `leaveGroup` / `leaveAndDeleteGroup` / `deleteConversation` mutations — same behaviour.
2806
+
2807
+ **Message history after re-add:** periods wiped on `delete()` / `leave(true)` — only messages from the re-add point onwards visible on rejoin. Plain `leave()` without delete preserves prior history windows.
2808
+
2809
+ - **New: Inactive & Reactivate user — server-enforced deactivation and fresh-start reactivation** — When a user account is set to `inactive` (self-service builtin, or external user-service sync), the server applies "self-left + delete" atomically across all surfaces:
2810
+
2811
+ | Action | Effect |
2812
+ |---|---|
2813
+ | Refresh tokens | All revoked — no new access tokens |
2814
+ | Device tokens | All disabled — push stops immediately |
2815
+ | Redis caches | `chatuser:`, `shadow:builtin:`, `shadow:<tenant>:` busted atomically |
2816
+ | Groups | `participant_left` to remaining members, admin auto-promoted, periods wiped |
2817
+ | DMs | Hidden silently, periods wiped |
2818
+ | Live socket | Force-disconnected, `user_offline` broadcast fires |
2819
+ | Socket cache-hit (non-builtin) | DB status re-checked on every connect — no 5-min bypass |
2820
+ | `addParticipants` / `createGroup` | Inactive IDs silently filtered before insert |
2821
+ | Blocked users list | Deactivated accounts excluded automatically |
2822
+
2823
+ **On reactivation:** device tokens re-enabled, user cache busted, conversation list intentionally empty (fresh start). DMs reopen when other party messages — new history only.
2824
+
2825
+ **SDK resilience (v1.2.6+):** RN `SocketProvider` retries token refresh up to 5× on `connect_error` auth failures before stopping. Web `SocketProvider` calls `logout()` after 5 retries — shows login screen instead of frozen stale UI.
2826
+
2827
+ **No integration changes required.** Clients receive standard `participant_left` / `user_offline` events. See [Step 6b — Inactive Users](docs/integration-guide.html#step-inactive-user) for the complete client-side handling pattern.
2828
+
2829
+ - **New: Removed/left users see frozen group state — name, icon, description, and member list as of exit time** — Group conversations now serve a historically accurate snapshot to removed or self-exited users instead of live data. The `name`, `description`, `iconUrl`, and `participantCount` fields are frozen at the moment the user left. The `participants` array reflects who was in the group at that exact time (not the current active members). No integration changes required — the response shape is identical; values are simply frozen for inactive participants. Existing code that filters `participants` by `p.isActive !== false` continues to work correctly. Use `conversation.participantCount` (not `participants.length`) for member count display.
2830
+
2831
+ - **New: Structured error layer — `AntzChatError` class hierarchy** — All SDK errors (REST and socket) are now instances of typed error classes instead of raw `AxiosError` objects or plain `Error` strings. Each error carries a machine-readable `code`, `retryable` boolean, and diagnostic `context` object.
2832
+
2833
+ Five subclasses cover every failure category:
2834
+
2835
+ | Class | Codes | Source |
2836
+ |---|---|---|
2837
+ | `AntzChatAuthError` | `SESSION_EXPIRED`, `AUTH_FAILED` | 401 |
2838
+ | `AntzChatValidationError` | `VALIDATION_ERROR` | 400 / 422; `.fields` array for multi-field errors |
2839
+ | `AntzChatPermissionError` | `PERMISSION_DENIED` | 403 |
2840
+ | `AntzChatNetworkError` | `NETWORK_ERROR`, `SOCKET_TIMEOUT`, `SOCKET_NOT_CONNECTED`, `SEND_QUEUE_FULL`, `MESSAGE_DROPPED`, `RATE_LIMITED` | Network, socket, queue |
2841
+ | `AntzChatServerError` | `SERVER_ERROR`, `NOT_FOUND` | 5xx, 404 |
2842
+
2843
+ Transit-encrypted error bodies are decrypted before normalisation. If decryption fails, `context.decryptionFailed = true` is set and the error class is still determined correctly from the HTTP status code.
2844
+
2845
+ **Backward compatible — no integration changes required.** All new classes extend native `Error` so existing `catch (err)` blocks work unchanged. Opt into typed handling by importing the classes:
2846
+
2847
+ ```typescript
2848
+ import { AntzChatAuthError, AntzChatNetworkError } from '@antzsoft/chat-core';
2849
+
2850
+ try {
2851
+ await authApi.login(credentials);
2852
+ } catch (err) {
2853
+ if (err instanceof AntzChatAuthError) redirectToLogin();
2854
+ else if (err instanceof AntzChatNetworkError && err.retryable) retry();
2855
+ }
2856
+ ```
2857
+
2858
+ See [Error Handling](#error-handling) for full reference.
2859
+
2860
+ ### v1.2.5
2861
+
2862
+ - **New: Transit session decoupled from socket — `createRestTransitSession()` exported** — Previously the transit encryption session was established exclusively via the socket ECDH handshake. The session key was unavailable until the socket connected, blocking encrypted HTTP requests that needed to fire before the socket (e.g. notification action replies when the app is killed). A new `POST /crypto/session` REST endpoint performs the full ECDH exchange over HTTP. `createRestTransitSession(apiUrl)` is exported from the core SDK; the axios client uses it automatically when a session is needed before the socket is ready. **No integration changes required** for standard SDK usage.
2863
+
2864
+ ```typescript
2865
+ import { createRestTransitSession } from '@antzsoft/chat-core';
2866
+ // RN / Hermes — uses @noble/curves + @noble/ciphers
2867
+ import { rnCreateRestTransitSession } from '@antzsoft/chat-rn-sdk';
2868
+
2869
+ // In a background notification reply handler (app killed, no socket):
2870
+ const session = await rnCreateRestTransitSession('https://api.yourapp.com/api/v1');
2871
+ // session.sessionId + session.sessionKey ready for encrypting REST calls manually
2872
+ ```
2873
+
2874
+ - **New: `rnCreateRestTransitSession()` exported from `@antzsoft/chat-rn-sdk`** — Hermes-compatible wrapper using `@noble/curves` (X25519) and `@noble/ciphers` (AES-256-GCM). Use in background notification reply handlers when the main app is not running and no socket can be established.
2875
+
2556
2876
  ### v1.2.4
2557
2877
  - **New: Chunked multipart upload for files ≥ 10 MB (S3 and local)** — Files at or above 10 MB on S3 now use the S3 multipart upload protocol (`CreateMultipartUpload` → parallel `UploadPart` per 10 MB chunk → `CompleteMultipartUpload`) instead of a single presigned POST. Benefits: resumable on network failure, parallel part uploads (max 3 concurrent), no single-request timeout risk, supports files up to 5 TB. Files below 10 MB continue to use the existing single-part presigned POST flow unchanged. Local provider also uses chunked multipart for files ≥ 10 MB (parts land in a staging directory, assembled on complete). Azure is unaffected (stays presigned PUT). **No integration changes required for Web and RN SDK users** — `webUploadPartFn` and `rnUploadPartFn` are wired in automatically. Node.js / custom integrators must supply `platformUploadPartFn` to `uploadBatch` to enable chunked uploads; without it the multipart path is skipped and files fail if they exceed the S3 single-PUT limit.
2558
2878
  - **New: `storageApi.completeMultipartUpload(fileId, uploadId, parts)`** — Completes an in-progress S3 multipart upload, assembling all parts and transitioning the file record to active. Called automatically by `uploadBatch` — only needed for custom manual upload flows.