@antzsoft/chat-core 1.2.5 → 1.2.7
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 +505 -1
- package/dist/{chunk-ZNA6B2R5.js → chunk-XTQYF5HU.js} +99 -4
- package/dist/chunk-XTQYF5HU.js.map +1 -0
- package/dist/index.cjs +158 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -3
- package/dist/index.d.ts +62 -3
- package/dist/index.js +64 -13
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +6 -1
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/{storage-CctLCOnZ.d.cts → storage-CW70b4vi.d.cts} +53 -1
- package/dist/{storage-CctLCOnZ.d.ts → storage-CW70b4vi.d.ts} +53 -1
- package/docs/integration-guide.html +988 -114
- package/package.json +1 -1
- package/dist/chunk-ZNA6B2R5.js.map +0 -1
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,364 @@ const config = await appConfigApi.get();
|
|
|
1669
1701
|
|
|
1670
1702
|
---
|
|
1671
1703
|
|
|
1704
|
+
### Sync API (`syncApi`)
|
|
1705
|
+
|
|
1706
|
+
> **v1.2.7+** — Mobile (React Native) only. Web SDK uses React Query cache invalidation on reconnect — no sync calls needed.
|
|
1707
|
+
|
|
1708
|
+
```typescript
|
|
1709
|
+
import { syncApi } from '@antzsoft/chat-core';
|
|
1710
|
+
import type {
|
|
1711
|
+
CrossConversationSyncResponse,
|
|
1712
|
+
ConversationSyncResponse,
|
|
1713
|
+
} from '@antzsoft/chat-core';
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1716
|
+
| Method | Signature | Description |
|
|
1717
|
+
|---|---|---|
|
|
1718
|
+
| `pull` | `(since: string) => Promise<CrossConversationSyncResponse>` | Cross-conversation delta sync. Call on every socket reconnect. `since` is the `syncedAt` value returned by the previous call (server clock — avoids client clock skew). |
|
|
1719
|
+
| `pullConversation` | `(conversationId: string, since?: string) => Promise<ConversationSyncResponse>` | Per-conversation delta sync. Call when a conversation has `needs_refresh = true`, or omit `since` on fresh install to receive full history. |
|
|
1720
|
+
|
|
1721
|
+
#### When to call each
|
|
1722
|
+
|
|
1723
|
+
| Situation | Call |
|
|
1724
|
+
|---|---|
|
|
1725
|
+
| Socket reconnects (within stale threshold) | `syncApi.pull(lastSyncedAt)` — returns all cross-conversation changes |
|
|
1726
|
+
| Socket reconnects (gap > stale threshold) | `pull()` returns `stale: true` — mark all conversations `needs_refresh`, lazy-sync on open |
|
|
1727
|
+
| User opens a stale conversation | `syncApi.pullConversation(convId, lastSyncedAt)` — returns full delta for that conversation |
|
|
1728
|
+
| Fresh install / first open | `syncApi.pullConversation(convId)` — omit `since` to receive full history |
|
|
1729
|
+
|
|
1730
|
+
#### `CrossConversationSyncResponse`
|
|
1731
|
+
|
|
1732
|
+
Returned by `syncApi.pull(since)`. Does **not** include reactions or stars — those are per-conversation and returned by `pullConversation`.
|
|
1733
|
+
|
|
1734
|
+
```typescript
|
|
1735
|
+
interface CrossConversationSyncResponse {
|
|
1736
|
+
syncedAt: string; // server-stamped ISO timestamp — store as new lastSyncedAt
|
|
1737
|
+
stale: boolean; // true when gap > CHAT_SYNC_STALE_DAYS (default 30) — app should lazy-sync on open
|
|
1738
|
+
messages: Message[]; // edits, deletes, pins, unpins — any message with updatedAt > since
|
|
1739
|
+
deletedForMe: SyncDeletedForMe[];
|
|
1740
|
+
participantChanges: SyncParticipantChange[];
|
|
1741
|
+
readReceipts: SyncReadReceipt[];
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
interface SyncDeletedForMe {
|
|
1745
|
+
messageId: string;
|
|
1746
|
+
conversationId: string;
|
|
1747
|
+
deletedAt: string | null;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
interface SyncParticipantChange {
|
|
1751
|
+
conversationId: string;
|
|
1752
|
+
userId: string;
|
|
1753
|
+
role: string;
|
|
1754
|
+
isActive: boolean; // false = removed
|
|
1755
|
+
isMuted: boolean;
|
|
1756
|
+
mutedUntil: string | null;
|
|
1757
|
+
updatedAt: string | null;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
interface SyncReadReceipt {
|
|
1761
|
+
messageId: string;
|
|
1762
|
+
conversationId: string;
|
|
1763
|
+
userId: string;
|
|
1764
|
+
readAt: string | null;
|
|
1765
|
+
}
|
|
1766
|
+
```
|
|
1767
|
+
|
|
1768
|
+
#### `ConversationSyncResponse`
|
|
1769
|
+
|
|
1770
|
+
Returned by `syncApi.pullConversation(convId, since?)`. Includes everything needed to make local SQLite authoritative for that conversation.
|
|
1771
|
+
|
|
1772
|
+
```typescript
|
|
1773
|
+
interface ConversationSyncResponse {
|
|
1774
|
+
syncedAt: string;
|
|
1775
|
+
messages: Message[]; // updatedAt > since — edits, deletes, pins
|
|
1776
|
+
deletedForMe: SyncDeletedForMe[];
|
|
1777
|
+
reactions: SyncReactions; // full current state for messages with reactedAt > since
|
|
1778
|
+
stars: SyncStarEntry[]; // updatedAt > since; isActive: false = unstarred
|
|
1779
|
+
participantChanges: SyncParticipantChange[];
|
|
1780
|
+
readReceipts: SyncReadReceipt[];
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// keyed by messageId — full current emoji counts (not delta)
|
|
1784
|
+
type SyncReactions = Record<string, MessageReaction[]>;
|
|
1785
|
+
|
|
1786
|
+
interface SyncStarEntry {
|
|
1787
|
+
messageId: string;
|
|
1788
|
+
conversationId: string;
|
|
1789
|
+
isActive: boolean; // true = starred, false = unstarred (soft-deleted)
|
|
1790
|
+
updatedAt: string | null;
|
|
1791
|
+
}
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
#### What each field covers
|
|
1795
|
+
|
|
1796
|
+
| Field | What changed events it captures |
|
|
1797
|
+
|---|---|
|
|
1798
|
+
| `messages` | Edits (`isEdited: true`), deletes (`status: "deleted"`), pins / unpins (`isPinned`) |
|
|
1799
|
+
| `deletedForMe` | Messages this user hid for themselves only — remove from local DB |
|
|
1800
|
+
| `reactions` | Full current emoji counts for any message that had reaction activity. Upsert as ground truth — never merge |
|
|
1801
|
+
| `stars` | `isActive: true` = starred, `isActive: false` = unstarred — delete local star record when false |
|
|
1802
|
+
| `participantChanges` | Role changes, mute/unmute, removals (`isActive: false`) |
|
|
1803
|
+
| `readReceipts` | Read receipts received since `since` |
|
|
1804
|
+
|
|
1805
|
+
#### Stale handling
|
|
1806
|
+
|
|
1807
|
+
When `pull()` returns `stale: true`, the server returns no data — it would be too large to return everything. The stale threshold defaults to **30 days** and is controlled by the `CHAT_SYNC_STALE_DAYS` server environment variable. The correct pattern:
|
|
1808
|
+
|
|
1809
|
+
```typescript
|
|
1810
|
+
const result = await syncApi.pull(lastSyncedAt);
|
|
1811
|
+
|
|
1812
|
+
if (result.stale) {
|
|
1813
|
+
// Store new cursor so next reconnect uses the server's timestamp, not the old one
|
|
1814
|
+
await AsyncStorage.setItem('@antz_last_synced_at', result.syncedAt);
|
|
1815
|
+
// Mark every conversation needs_refresh in your local SQLite
|
|
1816
|
+
await db.execute('UPDATE conversations SET needs_refresh = 1');
|
|
1817
|
+
// Lazy-sync — call pullConversation when the user actually opens each conversation
|
|
1818
|
+
} else {
|
|
1819
|
+
await AsyncStorage.setItem('@antz_last_synced_at', result.syncedAt);
|
|
1820
|
+
await mergeSyncDeltaIntoSQLite(result);
|
|
1821
|
+
}
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
#### RN SDK — built-in hooks
|
|
1825
|
+
|
|
1826
|
+
When using `@antzsoft/chat-rn-sdk`, use the pre-built hooks instead of calling `syncApi` directly:
|
|
1827
|
+
|
|
1828
|
+
```typescript
|
|
1829
|
+
import { useSync, useConversationSync } from '@antzsoft/chat-rn-sdk';
|
|
1830
|
+
|
|
1831
|
+
// In your root provider — auto-triggers on every socket reconnect
|
|
1832
|
+
useSync({
|
|
1833
|
+
onSync: async (data) => {
|
|
1834
|
+
// data is CrossConversationSyncResponse
|
|
1835
|
+
await mergeSyncDeltaIntoSQLite(data);
|
|
1836
|
+
},
|
|
1837
|
+
onStale: async () => {
|
|
1838
|
+
// Gap > stale threshold — mark all conversations needs_refresh
|
|
1839
|
+
await db.execute('UPDATE conversations SET needs_refresh = 1');
|
|
1840
|
+
},
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
// In your conversation screen — call when needs_refresh is true
|
|
1844
|
+
const { isSyncing, sync } = useConversationSync(conversationId);
|
|
1845
|
+
|
|
1846
|
+
useEffect(() => {
|
|
1847
|
+
if (needsRefresh) {
|
|
1848
|
+
sync().then((data) => {
|
|
1849
|
+
if (data) upsertConversationDeltaToSQLite(data);
|
|
1850
|
+
clearNeedsRefresh(conversationId);
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}, [conversationId, needsRefresh]);
|
|
1854
|
+
```
|
|
1855
|
+
|
|
1856
|
+
#### Full event coverage
|
|
1857
|
+
|
|
1858
|
+
| Event | `pull()` | `pullConversation()` |
|
|
1859
|
+
|---|---|---|
|
|
1860
|
+
| Message edited | ✅ | ✅ |
|
|
1861
|
+
| Message deleted (everyone) | ✅ | ✅ |
|
|
1862
|
+
| Message deleted for me | ✅ | ✅ |
|
|
1863
|
+
| Message pinned / unpinned | ✅ | ✅ |
|
|
1864
|
+
| Reaction added / removed | — | ✅ full current state |
|
|
1865
|
+
| Star / unstar | — | ✅ with `isActive` |
|
|
1866
|
+
| Read receipt | ✅ | ✅ |
|
|
1867
|
+
| Role change / mute / removal | ✅ | ✅ |
|
|
1868
|
+
|
|
1869
|
+
---
|
|
1870
|
+
|
|
1871
|
+
## Error Handling
|
|
1872
|
+
|
|
1873
|
+
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.
|
|
1874
|
+
|
|
1875
|
+
### Error classes
|
|
1876
|
+
|
|
1877
|
+
| Class | When thrown | `retryable` |
|
|
1878
|
+
|---|---|---|
|
|
1879
|
+
| `AntzChatAuthError` | 401 after token refresh also fails, or no refresh token exists | `false` |
|
|
1880
|
+
| `AntzChatValidationError` | 400 / 422 — bad input, validation failure | `false` |
|
|
1881
|
+
| `AntzChatPermissionError` | 403 — insufficient permissions | `false` |
|
|
1882
|
+
| `AntzChatNetworkError` | Network failure, timeout, socket disconnect, queue overflow | `true` |
|
|
1883
|
+
| `AntzChatServerError` | 5xx or unexpected server errors | `true` |
|
|
1884
|
+
| `AntzChatError` (base) | Transit config mismatch, unknown errors | varies |
|
|
1885
|
+
|
|
1886
|
+
All classes extend the native `Error` class, so existing `catch (err)` blocks continue to work unchanged.
|
|
1887
|
+
|
|
1888
|
+
### Error codes
|
|
1889
|
+
|
|
1890
|
+
Switch on `err.code` for fine-grained handling:
|
|
1891
|
+
|
|
1892
|
+
| Code | Class | Source |
|
|
1893
|
+
|---|---|---|
|
|
1894
|
+
| `SESSION_EXPIRED` | `AntzChatAuthError` | 401, no prior retry |
|
|
1895
|
+
| `AUTH_FAILED` | `AntzChatAuthError` | 401 after token refresh also failed |
|
|
1896
|
+
| `PERMISSION_DENIED` | `AntzChatPermissionError` | 403 |
|
|
1897
|
+
| `VALIDATION_ERROR` | `AntzChatValidationError` | 400 / 422 |
|
|
1898
|
+
| `NOT_FOUND` | `AntzChatServerError` | 404 |
|
|
1899
|
+
| `RATE_LIMITED` | `AntzChatNetworkError` | 429 |
|
|
1900
|
+
| `NETWORK_ERROR` | `AntzChatNetworkError` | No response / connection failure |
|
|
1901
|
+
| `SOCKET_TIMEOUT` | `AntzChatNetworkError` | ACK timeout (5 s) or reconnect timeout (15 s) |
|
|
1902
|
+
| `SOCKET_NOT_CONNECTED` | `AntzChatNetworkError` | Socket not up when an ack-required emit fires |
|
|
1903
|
+
| `SEND_QUEUE_FULL` | `AntzChatNetworkError` | Per-conversation queue > 100 pending messages |
|
|
1904
|
+
| `MESSAGE_DROPPED` | `AntzChatNetworkError` | Message waited > 30 s in queue before socket ready |
|
|
1905
|
+
| `TRANSIT_MISMATCH` | `AntzChatError` | SDK and server transit-encryption config differ |
|
|
1906
|
+
| `SERVER_ERROR` | `AntzChatServerError` | 5xx or unrecognised HTTP error |
|
|
1907
|
+
|
|
1908
|
+
### Imports
|
|
1909
|
+
|
|
1910
|
+
```typescript
|
|
1911
|
+
import {
|
|
1912
|
+
AntzChatError,
|
|
1913
|
+
AntzChatAuthError,
|
|
1914
|
+
AntzChatValidationError,
|
|
1915
|
+
AntzChatNetworkError,
|
|
1916
|
+
AntzChatPermissionError,
|
|
1917
|
+
AntzChatServerError,
|
|
1918
|
+
} from '@antzsoft/chat-core';
|
|
1919
|
+
```
|
|
1920
|
+
|
|
1921
|
+
### Usage patterns
|
|
1922
|
+
|
|
1923
|
+
#### instanceof branching
|
|
1924
|
+
|
|
1925
|
+
```typescript
|
|
1926
|
+
import {
|
|
1927
|
+
AntzChatAuthError,
|
|
1928
|
+
AntzChatValidationError,
|
|
1929
|
+
AntzChatNetworkError,
|
|
1930
|
+
AntzChatPermissionError,
|
|
1931
|
+
AntzChatServerError,
|
|
1932
|
+
} from '@antzsoft/chat-core';
|
|
1933
|
+
|
|
1934
|
+
try {
|
|
1935
|
+
await authApi.login({ email, password });
|
|
1936
|
+
} catch (err) {
|
|
1937
|
+
if (err instanceof AntzChatAuthError) showLoginError(err.message);
|
|
1938
|
+
else if (err instanceof AntzChatValidationError) showFieldErrors(err.fields ?? [err.message]);
|
|
1939
|
+
else if (err instanceof AntzChatPermissionError) showPermissionDenied();
|
|
1940
|
+
else if (err instanceof AntzChatNetworkError && err.retryable) scheduleRetry();
|
|
1941
|
+
else if (err instanceof AntzChatServerError) logAndShowGenericError(err);
|
|
1942
|
+
else throw err; // unknown — re-throw
|
|
1943
|
+
}
|
|
1944
|
+
```
|
|
1945
|
+
|
|
1946
|
+
#### code switching (fine-grained)
|
|
1947
|
+
|
|
1948
|
+
```typescript
|
|
1949
|
+
import { AntzChatError } from '@antzsoft/chat-core';
|
|
1950
|
+
|
|
1951
|
+
try {
|
|
1952
|
+
await socketEmit.sendMessage(payload);
|
|
1953
|
+
} catch (err) {
|
|
1954
|
+
if (!(err instanceof AntzChatError)) throw err;
|
|
1955
|
+
switch (err.code) {
|
|
1956
|
+
case 'SESSION_EXPIRED': redirectToLogin(); break;
|
|
1957
|
+
case 'SEND_QUEUE_FULL': showToast('Too many messages in flight'); break;
|
|
1958
|
+
case 'MESSAGE_DROPPED': showToast('Message timed out — try again'); break;
|
|
1959
|
+
case 'SOCKET_TIMEOUT':
|
|
1960
|
+
case 'SOCKET_NOT_CONNECTED': showOfflineBanner(); break;
|
|
1961
|
+
case 'PERMISSION_DENIED': showToast('You cannot send here'); break;
|
|
1962
|
+
default: console.error(err.code, err.message, err.context);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
```
|
|
1966
|
+
|
|
1967
|
+
#### retryable flag
|
|
1968
|
+
|
|
1969
|
+
```typescript
|
|
1970
|
+
import { AntzChatError } from '@antzsoft/chat-core';
|
|
1971
|
+
|
|
1972
|
+
async function sendWithRetry(payload, maxAttempts = 3) {
|
|
1973
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1974
|
+
try {
|
|
1975
|
+
return await socketEmit.sendMessage(payload);
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
if (err instanceof AntzChatError && err.retryable && attempt < maxAttempts) {
|
|
1978
|
+
await new Promise(r => setTimeout(r, attempt * 1000)); // back-off
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
throw err;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
```
|
|
1986
|
+
|
|
1987
|
+
#### AntzChatValidationError — field errors
|
|
1988
|
+
|
|
1989
|
+
When the server returns `message` as a string array (e.g. class-validator errors), `AntzChatValidationError.fields` is populated with the individual messages:
|
|
1990
|
+
|
|
1991
|
+
```typescript
|
|
1992
|
+
import { AntzChatValidationError } from '@antzsoft/chat-core';
|
|
1993
|
+
|
|
1994
|
+
try {
|
|
1995
|
+
await authApi.register(payload);
|
|
1996
|
+
} catch (err) {
|
|
1997
|
+
if (err instanceof AntzChatValidationError) {
|
|
1998
|
+
console.log(err.message); // "email must be a valid email; password is too short"
|
|
1999
|
+
console.log(err.fields); // ["email must be a valid email", "password is too short"]
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
```
|
|
2003
|
+
|
|
2004
|
+
#### Transit encryption mismatch
|
|
2005
|
+
|
|
2006
|
+
If the SDK and server have mismatched `transitEncryption` settings, `connectSocket` throws a non-retryable `AntzChatError` with code `TRANSIT_MISMATCH` before any socket is created:
|
|
2007
|
+
|
|
2008
|
+
```typescript
|
|
2009
|
+
import { AntzChatError } from '@antzsoft/chat-core';
|
|
2010
|
+
|
|
2011
|
+
try {
|
|
2012
|
+
await client.connect();
|
|
2013
|
+
} catch (err) {
|
|
2014
|
+
if (err instanceof AntzChatError && err.code === 'TRANSIT_MISMATCH') {
|
|
2015
|
+
console.error('Config mismatch:', err.message, err.context);
|
|
2016
|
+
// err.context = { sdkEnabled: true, serverEnabled: false }
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
```
|
|
2020
|
+
|
|
2021
|
+
### What stays silent (by design)
|
|
2022
|
+
|
|
2023
|
+
These three cases intentionally do not throw:
|
|
2024
|
+
|
|
2025
|
+
| Situation | Reason |
|
|
2026
|
+
|---|---|
|
|
2027
|
+
| `socketEmit.typing()` / `socketEmit.markRead()` — socket not connected | Best-effort only; callers must not be forced to wrap them |
|
|
2028
|
+
| Transit session timeout (5 s) — server event not received | SDK degrades gracefully to unencrypted mode and continues |
|
|
2029
|
+
| `usersApi.getPreferences()` failure | Returns `null`; preferences are non-critical and defaults apply |
|
|
2030
|
+
|
|
2031
|
+
### Error context
|
|
2032
|
+
|
|
2033
|
+
Every `AntzChatError` carries a `context` object with extra diagnostics. It is safe to log but never needed for control flow:
|
|
2034
|
+
|
|
2035
|
+
```typescript
|
|
2036
|
+
catch (err) {
|
|
2037
|
+
if (err instanceof AntzChatError) {
|
|
2038
|
+
// context contains: httpStatus, path, serverError, axiosCode, event, decryptionFailed, etc.
|
|
2039
|
+
console.error('[AntzChat]', err.name, err.code, err.message, err.context);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
```
|
|
2043
|
+
|
|
2044
|
+
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.
|
|
2045
|
+
|
|
2046
|
+
### normalizeAxiosError
|
|
2047
|
+
|
|
2048
|
+
If you make raw axios calls through `getApiClient()` and want the same normalisation:
|
|
2049
|
+
|
|
2050
|
+
```typescript
|
|
2051
|
+
import { getApiClient, normalizeAxiosError, AntzChatError } from '@antzsoft/chat-core';
|
|
2052
|
+
|
|
2053
|
+
try {
|
|
2054
|
+
const { data } = await getApiClient().get('/some/endpoint');
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
throw normalizeAxiosError(err); // always returns AntzChatError (or subclass)
|
|
2057
|
+
}
|
|
2058
|
+
```
|
|
2059
|
+
|
|
2060
|
+
---
|
|
2061
|
+
|
|
1672
2062
|
### Socket
|
|
1673
2063
|
|
|
1674
2064
|
#### Connection management
|
|
@@ -1827,6 +2217,7 @@ Subscribe using `client.socket.on(event, handler)` (headless) or directly on the
|
|
|
1827
2217
|
| `message_deleted` | `MessageDeletedEvent` | A message was deleted for everyone. | **Chat detail screen** — add on mount, remove on unmount. |
|
|
1828
2218
|
| `message_deleted_for_me` | `{ messageId: string; conversationId: string }` | A message was hidden for the current user only (fired only to that user's socket). | **Chat detail screen** — add on mount, remove on unmount. |
|
|
1829
2219
|
| `reaction_updated` | `ReactionUpdatedEvent` | Reactions on a message changed (full reaction array). | **Chat detail screen** — add on mount, remove on unmount. |
|
|
2220
|
+
| `message_star_updated` | `MessageStarUpdatedEvent` | A message was starred or unstarred by the current user on another device. Fired only to the acting user's personal room — other participants never receive it. `isStarred: true` = starred, `isStarred: false` = unstarred. | **App root** (to sync star state across all open chat screens and the starred messages list). Keep for full session. |
|
|
1830
2221
|
| `message_pin_updated` | `{ messageId: string; conversationId: string; isPinned: boolean; pinnedBy?: string; pinnedAt?: string }` | A message was pinned or unpinned. | **Chat detail screen** — add on mount, remove on unmount. |
|
|
1831
2222
|
| `typing_indicator` | `TypingIndicatorEvent` | A user started or stopped typing. | **Chat detail screen** — add on mount, remove on unmount. |
|
|
1832
2223
|
| `user_online` | `{ userId: string }` | A participant came online — auto-updates `useChatStore.onlineUsers`. | **App root** — drives online indicators everywhere. Keep for full session. |
|
|
@@ -1840,7 +2231,7 @@ Subscribe using `client.socket.on(event, handler)` (headless) or directly on the
|
|
|
1840
2231
|
| `messages_delivered` | `MessagesDeliveredEvent` | Batch delivery catch-up — fired to the sender when an offline recipient reconnects and all pending undelivered messages are marked delivered in bulk. Payload: `{ conversationId, messageIds[], deliveredTo: string, deliveredAt }` | **Chat detail screen** — add on mount, remove on unmount. |
|
|
1841
2232
|
| `conversation_created` | `Conversation` | A new conversation was created (or you were added to one). | **App root** — call `joinRoom` here for the new conversation. Keep for full session. |
|
|
1842
2233
|
| `conversation_updated` | `Conversation` | A conversation's last message or metadata changed — use this to update the conversation list. | **App root** — keep for full session. The server emits this for every message across all conversations; a global listener keeps the in-memory conversation list and unread badge always in sync. |
|
|
1843
|
-
| `conversation_deleted` | `{ conversationId: string }` |
|
|
2234
|
+
| `conversation_deleted` | `{ conversationId: string }` | Emitted **only to the acting user's own sockets** when they: (1) self-delete/hide a conversation via `delete()`, (2) exit+delete a group via `leave(id, true)`, or (3) hide an already-exited group via `delete()`. Other participants never receive this event. | **App root / conversation list screen** — remove from state and navigate away if it was open. |
|
|
1844
2235
|
| `participant_joined` | `{ conversationId: string; userId: string; displayName: string; addedBy: string }` | A new participant was added to a group conversation. | **Chat detail screen** — add on mount, remove on unmount. |
|
|
1845
2236
|
| `participant_left` | `{ conversationId: string; userId: string; displayName: string; removedBy?: string }` | A participant left or was removed from a group conversation. | **Chat detail screen** — add on mount, remove on unmount. |
|
|
1846
2237
|
|
|
@@ -1850,6 +2241,7 @@ import type {
|
|
|
1850
2241
|
MessageUpdatedEvent,
|
|
1851
2242
|
MessageDeletedEvent,
|
|
1852
2243
|
ReactionUpdatedEvent,
|
|
2244
|
+
MessageStarUpdatedEvent,
|
|
1853
2245
|
TypingIndicatorEvent,
|
|
1854
2246
|
UserStatusEvent,
|
|
1855
2247
|
ReadReceiptEvent,
|
|
@@ -1873,6 +2265,11 @@ client.socket.on('reaction_updated', (evt: ReactionUpdatedEvent) => {
|
|
|
1873
2265
|
setMessageReactions(evt.messageId, evt.reactions);
|
|
1874
2266
|
});
|
|
1875
2267
|
|
|
2268
|
+
client.socket.on('message_star_updated', (evt: MessageStarUpdatedEvent) => {
|
|
2269
|
+
// Fires on your other devices when you star/unstar on one device
|
|
2270
|
+
updateMessageStarState(evt.messageId, evt.isStarred);
|
|
2271
|
+
});
|
|
2272
|
+
|
|
1876
2273
|
client.socket.on('typing_indicator', (evt: TypingIndicatorEvent) => {
|
|
1877
2274
|
if (evt.isTyping) {
|
|
1878
2275
|
showTyping(evt.conversationId, evt.displayName);
|
|
@@ -2553,6 +2950,113 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
|
|
|
2553
2950
|
|
|
2554
2951
|
## Changelog
|
|
2555
2952
|
|
|
2953
|
+
### v1.2.7
|
|
2954
|
+
- **New: `syncApi` — offline delta sync for mobile** — Two new REST endpoints let the mobile app pull all changes since a given timestamp without a full refetch. Call `syncApi.pull(since)` on every socket reconnect to get cross-conversation deltas; call `syncApi.pullConversation(convId, since?)` when opening a stale conversation (or on fresh install with no `since`). When the gap exceeds the stale threshold (default 30 days, env: `CHAT_SYNC_STALE_DAYS`) the server sets `stale: true` and returns no data — the app lazy-syncs each conversation as the user opens it. See [`syncApi`](#sync-api-syncapi) for full reference.
|
|
2955
|
+
- **New: `message_star_updated` socket event — real-time star sync across devices** — When the current user stars or unstars a message, the server now emits `message_star_updated` to their personal socket room. All other connected devices of the same user receive the event and update their local message cache automatically — no refetch needed. Payload: `{ messageId: string; conversationId: string; isStarred: boolean }`. New exported type: `MessageStarUpdatedEvent`. Web and RN `SocketProvider` handle this event internally.
|
|
2956
|
+
- **New: Soft-delete for unstar — offline sync support** — Unstar is no longer a hard delete on `chat_message_stars`. The record is now soft-deleted (`isActive: false`, `unstarredAt` timestamp set) so delta sync queries (`updatedAt > since`) can detect both star and unstar events. All `getStarredMessages` queries filter `isActive: true` — no visible behaviour change.
|
|
2957
|
+
- **New: `updatedAt` on `chat_conversation_participants`** — Participant documents now carry `updatedAt` (Mongoose timestamps). Enables delta sync queries to detect mute, pin, role, and removal changes since a given timestamp.
|
|
2958
|
+
- **New: Sync indexes** — Five new MongoDB indexes added: `{ conversationId, updatedAt }` on `chat_messages`, `{ userId, tenantId, updatedAt }` on `chat_conversation_participants`, `{ userId, tenantId, updatedAt }` on `chat_message_stars`, `{ conversationId, reactedAt }` on `chat_message_reactions`, `{ conversationId, readAt }` on `chat_message_reads`. Added idempotently at startup — no manual migration needed.
|
|
2959
|
+
- **Migrations 003 & 004** — `003_backfill_participant_updatedAt`: sets `updatedAt = joinedAt` on all existing participant documents. `004_backfill_star_isActive`: sets `isActive: true` on all existing star records. Both run automatically on first deploy and are idempotent.
|
|
2960
|
+
- **New exported types:** `CrossConversationSyncResponse`, `ConversationSyncResponse`, `SyncReadReceipt`, `SyncDeletedForMe`, `SyncParticipantChange`, `SyncStarEntry`, `SyncReactions`.
|
|
2961
|
+
- **No breaking changes** — all additions are purely additive. Existing API contracts, socket events, and type shapes are unchanged.
|
|
2962
|
+
|
|
2963
|
+
### v1.2.6
|
|
2964
|
+
|
|
2965
|
+
- **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.
|
|
2966
|
+
|
|
2967
|
+
| Scenario | Call | Effect |
|
|
2968
|
+
|---|---|---|
|
|
2969
|
+
| **Delete Chat** (DM) | `conversationsApi.delete(id)` | Hidden from list, periods wiped. Re-opens with new history only when other party messages. |
|
|
2970
|
+
| **Exit Group** | `conversationsApi.leave(id)` | Caller inactive, stays read-only. Server auto-promotes admin if needed. |
|
|
2971
|
+
| **Exit and Delete** | `conversationsApi.leave(id, true)` | Atomic exit + hide. Periods wiped. |
|
|
2972
|
+
| **Delete Group** (post-exit) | `conversationsApi.delete(id)` | Hides the already-exited entry. Others unaffected. |
|
|
2973
|
+
|
|
2974
|
+
```typescript
|
|
2975
|
+
// Delete a DM ("Delete Chat")
|
|
2976
|
+
await conversationsApi.delete(dmConversationId);
|
|
2977
|
+
|
|
2978
|
+
// Exit and Delete a group
|
|
2979
|
+
await conversationsApi.leave(groupId, true);
|
|
2980
|
+
|
|
2981
|
+
// Socket event fired to the caller's own sockets
|
|
2982
|
+
socket.on('conversation_deleted', ({ conversationId }) => {
|
|
2983
|
+
removeFromList(conversationId);
|
|
2984
|
+
if (activeConversationId === conversationId) navigateBackToList();
|
|
2985
|
+
});
|
|
2986
|
+
```
|
|
2987
|
+
|
|
2988
|
+
**Web SDK (`useConversations`):** `leaveGroup` / `leaveAndDeleteGroup` / `deleteGroup` mutations — optimistic cache update, no manual invalidation needed.
|
|
2989
|
+
**RN SDK (`useConversations`):** `leaveGroup` / `leaveAndDeleteGroup` / `deleteConversation` mutations — same behaviour.
|
|
2990
|
+
|
|
2991
|
+
**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.
|
|
2992
|
+
|
|
2993
|
+
- **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:
|
|
2994
|
+
|
|
2995
|
+
| Action | Effect |
|
|
2996
|
+
|---|---|
|
|
2997
|
+
| Refresh tokens | All revoked — no new access tokens |
|
|
2998
|
+
| Device tokens | All disabled — push stops immediately |
|
|
2999
|
+
| Redis caches | `chatuser:`, `shadow:builtin:`, `shadow:<tenant>:` busted atomically |
|
|
3000
|
+
| Groups | `participant_left` to remaining members, admin auto-promoted, periods wiped |
|
|
3001
|
+
| DMs | Hidden silently, periods wiped |
|
|
3002
|
+
| Live socket | Force-disconnected, `user_offline` broadcast fires |
|
|
3003
|
+
| Socket cache-hit (non-builtin) | DB status re-checked on every connect — no 5-min bypass |
|
|
3004
|
+
| `addParticipants` / `createGroup` | Inactive IDs silently filtered before insert |
|
|
3005
|
+
| Blocked users list | Deactivated accounts excluded automatically |
|
|
3006
|
+
|
|
3007
|
+
**On reactivation:** device tokens re-enabled, user cache busted, conversation list intentionally empty (fresh start). DMs reopen when other party messages — new history only.
|
|
3008
|
+
|
|
3009
|
+
**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.
|
|
3010
|
+
|
|
3011
|
+
**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.
|
|
3012
|
+
|
|
3013
|
+
- **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.
|
|
3014
|
+
|
|
3015
|
+
- **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.
|
|
3016
|
+
|
|
3017
|
+
Five subclasses cover every failure category:
|
|
3018
|
+
|
|
3019
|
+
| Class | Codes | Source |
|
|
3020
|
+
|---|---|---|
|
|
3021
|
+
| `AntzChatAuthError` | `SESSION_EXPIRED`, `AUTH_FAILED` | 401 |
|
|
3022
|
+
| `AntzChatValidationError` | `VALIDATION_ERROR` | 400 / 422; `.fields` array for multi-field errors |
|
|
3023
|
+
| `AntzChatPermissionError` | `PERMISSION_DENIED` | 403 |
|
|
3024
|
+
| `AntzChatNetworkError` | `NETWORK_ERROR`, `SOCKET_TIMEOUT`, `SOCKET_NOT_CONNECTED`, `SEND_QUEUE_FULL`, `MESSAGE_DROPPED`, `RATE_LIMITED` | Network, socket, queue |
|
|
3025
|
+
| `AntzChatServerError` | `SERVER_ERROR`, `NOT_FOUND` | 5xx, 404 |
|
|
3026
|
+
|
|
3027
|
+
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.
|
|
3028
|
+
|
|
3029
|
+
**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:
|
|
3030
|
+
|
|
3031
|
+
```typescript
|
|
3032
|
+
import { AntzChatAuthError, AntzChatNetworkError } from '@antzsoft/chat-core';
|
|
3033
|
+
|
|
3034
|
+
try {
|
|
3035
|
+
await authApi.login(credentials);
|
|
3036
|
+
} catch (err) {
|
|
3037
|
+
if (err instanceof AntzChatAuthError) redirectToLogin();
|
|
3038
|
+
else if (err instanceof AntzChatNetworkError && err.retryable) retry();
|
|
3039
|
+
}
|
|
3040
|
+
```
|
|
3041
|
+
|
|
3042
|
+
See [Error Handling](#error-handling) for full reference.
|
|
3043
|
+
|
|
3044
|
+
### v1.2.5
|
|
3045
|
+
|
|
3046
|
+
- **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.
|
|
3047
|
+
|
|
3048
|
+
```typescript
|
|
3049
|
+
import { createRestTransitSession } from '@antzsoft/chat-core';
|
|
3050
|
+
// RN / Hermes — uses @noble/curves + @noble/ciphers
|
|
3051
|
+
import { rnCreateRestTransitSession } from '@antzsoft/chat-rn-sdk';
|
|
3052
|
+
|
|
3053
|
+
// In a background notification reply handler (app killed, no socket):
|
|
3054
|
+
const session = await rnCreateRestTransitSession('https://api.yourapp.com/api/v1');
|
|
3055
|
+
// session.sessionId + session.sessionKey ready for encrypting REST calls manually
|
|
3056
|
+
```
|
|
3057
|
+
|
|
3058
|
+
- **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.
|
|
3059
|
+
|
|
2556
3060
|
### v1.2.4
|
|
2557
3061
|
- **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
3062
|
- **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.
|