@antzsoft/chat-core 1.2.6 → 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 +185 -1
- package/dist/index.cjs +26 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -3
- package/dist/index.d.ts +20 -3
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +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 +177 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1701,6 +1701,173 @@ const config = await appConfigApi.get();
|
|
|
1701
1701
|
|
|
1702
1702
|
---
|
|
1703
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
|
+
|
|
1704
1871
|
## Error Handling
|
|
1705
1872
|
|
|
1706
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.
|
|
@@ -2050,6 +2217,7 @@ Subscribe using `client.socket.on(event, handler)` (headless) or directly on the
|
|
|
2050
2217
|
| `message_deleted` | `MessageDeletedEvent` | A message was deleted for everyone. | **Chat detail screen** — add on mount, remove on unmount. |
|
|
2051
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. |
|
|
2052
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. |
|
|
2053
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. |
|
|
2054
2222
|
| `typing_indicator` | `TypingIndicatorEvent` | A user started or stopped typing. | **Chat detail screen** — add on mount, remove on unmount. |
|
|
2055
2223
|
| `user_online` | `{ userId: string }` | A participant came online — auto-updates `useChatStore.onlineUsers`. | **App root** — drives online indicators everywhere. Keep for full session. |
|
|
@@ -2063,7 +2231,7 @@ Subscribe using `client.socket.on(event, handler)` (headless) or directly on the
|
|
|
2063
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. |
|
|
2064
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. |
|
|
2065
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. |
|
|
2066
|
-
| `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. |
|
|
2067
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. |
|
|
2068
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. |
|
|
2069
2237
|
|
|
@@ -2073,6 +2241,7 @@ import type {
|
|
|
2073
2241
|
MessageUpdatedEvent,
|
|
2074
2242
|
MessageDeletedEvent,
|
|
2075
2243
|
ReactionUpdatedEvent,
|
|
2244
|
+
MessageStarUpdatedEvent,
|
|
2076
2245
|
TypingIndicatorEvent,
|
|
2077
2246
|
UserStatusEvent,
|
|
2078
2247
|
ReadReceiptEvent,
|
|
@@ -2096,6 +2265,11 @@ client.socket.on('reaction_updated', (evt: ReactionUpdatedEvent) => {
|
|
|
2096
2265
|
setMessageReactions(evt.messageId, evt.reactions);
|
|
2097
2266
|
});
|
|
2098
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
|
+
|
|
2099
2273
|
client.socket.on('typing_indicator', (evt: TypingIndicatorEvent) => {
|
|
2100
2274
|
if (evt.isTyping) {
|
|
2101
2275
|
showTyping(evt.conversationId, evt.displayName);
|
|
@@ -2776,6 +2950,16 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
|
|
|
2776
2950
|
|
|
2777
2951
|
## Changelog
|
|
2778
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
|
+
|
|
2779
2963
|
### v1.2.6
|
|
2780
2964
|
|
|
2781
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.
|
package/dist/index.cjs
CHANGED
|
@@ -142,6 +142,7 @@ __export(src_exports, {
|
|
|
142
142
|
setTransitSession: () => setTransitSession,
|
|
143
143
|
socketEmit: () => socketEmit,
|
|
144
144
|
storageApi: () => storageApi,
|
|
145
|
+
syncApi: () => syncApi,
|
|
145
146
|
tryGetSocket: () => tryGetSocket,
|
|
146
147
|
uploadBatch: () => uploadBatch,
|
|
147
148
|
useChatStore: () => useChatStore,
|
|
@@ -840,6 +841,30 @@ var authApi = {
|
|
|
840
841
|
}
|
|
841
842
|
};
|
|
842
843
|
|
|
844
|
+
// src/api/sync.ts
|
|
845
|
+
var syncApi = {
|
|
846
|
+
/**
|
|
847
|
+
* Cross-conversation delta sync — call on every socket reconnect.
|
|
848
|
+
* `since` is the ISO timestamp returned as `syncedAt` from the previous call.
|
|
849
|
+
* Returns `stale: true` (with empty arrays) when the gap exceeds 60 days —
|
|
850
|
+
* the app should mark all conversations `needs_refresh` and lazy-sync on open.
|
|
851
|
+
*/
|
|
852
|
+
pull(since) {
|
|
853
|
+
return getApiClient().get("/sync", { params: { since } }).then((r) => r.data);
|
|
854
|
+
},
|
|
855
|
+
/**
|
|
856
|
+
* Per-conversation delta sync — call when a conversation has `needs_refresh = true`
|
|
857
|
+
* or on fresh install (omit `since` to receive the full message history).
|
|
858
|
+
* Covers messages, deletedForMe, full reaction state, stars, participant changes,
|
|
859
|
+
* and read receipts — everything needed to make local SQLite authoritative.
|
|
860
|
+
*/
|
|
861
|
+
pullConversation(conversationId, since) {
|
|
862
|
+
return getApiClient().get(`/conversations/${conversationId}/sync`, {
|
|
863
|
+
params: since ? { since } : {}
|
|
864
|
+
}).then((r) => r.data);
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
843
868
|
// src/api/app-config.ts
|
|
844
869
|
var appConfigApi = {
|
|
845
870
|
async get() {
|
|
@@ -1974,6 +1999,7 @@ var AntzChatClient = class {
|
|
|
1974
1999
|
setTransitSession,
|
|
1975
2000
|
socketEmit,
|
|
1976
2001
|
storageApi,
|
|
2002
|
+
syncApi,
|
|
1977
2003
|
tryGetSocket,
|
|
1978
2004
|
uploadBatch,
|
|
1979
2005
|
useChatStore,
|