@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.
@@ -155,7 +155,7 @@ section.sec>h2:hover{color:#fff}
155
155
 
156
156
  <div class="section-label">What's New</div>
157
157
  <ul>
158
- <li><a href="#whats-new">v1.2.4 Release Notes</a></li>
158
+ <li><a href="#whats-new">v1.2.7 Release Notes</a></li>
159
159
  </ul>
160
160
 
161
161
  <div class="section-label">Getting Started</div>
@@ -182,6 +182,8 @@ section.sec>h2:hover{color:#fff}
182
182
  <div class="section-label">Conversations</div>
183
183
  <ul>
184
184
  <li><a href="#step-convs">6. Conversations</a></li>
185
+ <li><a href="#step-inactive-user">6b. Inactive Users</a></li>
186
+ <li><a href="#step-clear-chat">6c. Clear / Delete Chat</a></li>
185
187
  <li><a href="#step-rooms">7. Join &amp; Leave Room</a></li>
186
188
  </ul>
187
189
 
@@ -207,6 +209,11 @@ section.sec>h2:hover{color:#fff}
207
209
  <li><a href="#step-example">18. Full Example</a></li>
208
210
  <li><a href="#step-unread">19. Unread Counts</a></li>
209
211
  </ul>
212
+
213
+ <div class="section-label">Error Handling</div>
214
+ <ul>
215
+ <li><a href="#step-errors">20. Error Handling</a></li>
216
+ </ul>
210
217
  </nav>
211
218
 
212
219
  <!-- ══ MAIN ═════════════════════════════════════════════════════════════ -->
@@ -243,189 +250,308 @@ section.sec>h2:hover{color:#fff}
243
250
  <h2>What's New</h2>
244
251
  <p style="color:var(--muted);font-size:13px;margin-bottom:20px">Version history and release notes. Click a version to expand.</p>
245
252
 
246
- <!-- ── v1.2.4 (current) ── -->
247
- <div class="wn-version open" id="wn-124">
248
- <div class="wn-header" onclick="toggleVersion('wn-124')">
253
+ <!-- ── v1.2.7 (current) ── -->
254
+ <div class="wn-version open" id="wn-127">
255
+ <div class="wn-header" onclick="toggleVersion('wn-127')">
249
256
  <div class="wn-title">
250
- <span class="wn-ver">v1.2.4</span>
257
+ <span class="wn-ver">v1.2.7</span>
251
258
  <span class="wn-badge current">Current</span>
252
- <span class="wn-date">May 2026</span>
259
+ <span class="wn-date">June 2026</span>
253
260
  </div>
254
261
  <span class="wn-chevron">▲</span>
255
262
  </div>
256
263
  <div class="wn-body">
257
264
 
258
- <div class="wn-item" id="wn-124-mp1">
259
- <div class="wn-item-header" onclick="toggleItem('wn-124-mp1')">
265
+ <div class="wn-item" id="wn-127-starsynce">
266
+ <div class="wn-item-header" onclick="toggleItem('wn-127-starsynce')">
260
267
  <span class="wn-tag new">New</span>
261
- <span class="wn-item-title">Chunked multipart upload for files 10 MB (S3 and local)</span>
268
+ <span class="wn-item-title">Real-time star sync across devices <code>message_star_updated</code> socket event</span>
262
269
  <span class="wn-chevron-sm">▾</span>
263
270
  </div>
264
271
  <div class="wn-item-body">
265
- <p>Files at or above <strong>10 MB</strong> on S3 and local storage now use chunked multipart upload instead of a single presigned POST. The SDK splits the file into 10 MB chunks, uploads up to 3 parts in parallel, then completes the upload server-side.</p>
266
- <p><strong>What changes per provider:</strong></p>
272
+ <p>When the current user stars or unstars a message on one device, the server now emits <code>message_star_updated</code> to their personal socket room. All other connected devices of the same user receive the event and update their message cache automatically no refetch, no polling.</p>
267
273
  <table>
268
- <thead><tr><th>Provider</th><th>File &lt; 10 MB</th><th>File ≥ 10 MB</th></tr></thead>
274
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
269
275
  <tbody>
270
- <tr><td>AWS S3</td><td>Presigned POST (unchanged)</td><td><strong>New:</strong> chunked multipart N presigned PUT URLs (one per part) → <code>CompleteMultipartUpload</code></td></tr>
271
- <tr><td>Local (same-server / mount)</td><td>Presigned POST (unchanged)</td><td><strong>New:</strong> chunked multipart — N signed PUT URLs (one per part) → <code>POST /storage/multipart/complete/:fileId</code> assembles parts on disk</td></tr>
272
- <tr><td>Azure Blob Storage</td><td>Presigned PUT (unchanged)</td><td>Presigned PUT (unchanged)</td></tr>
276
+ <tr><td><code>messageId</code></td><td><code>string</code></td><td>ID of the starred/unstarred message</td></tr>
277
+ <tr><td><code>conversationId</code></td><td><code>string</code></td><td>Conversation the message belongs to</td></tr>
278
+ <tr><td><code>isStarred</code></td><td><code>boolean</code></td><td><code>true</code> = starred, <code>false</code> = unstarred</td></tr>
273
279
  </tbody>
274
280
  </table>
275
- <p><strong>Benefits at 50 MB+:</strong> resumes from last completed part on network failure, parallel throughput, no single-request timeout, supports files up to 5 TB.</p>
276
- <p><strong>No integration changes required</strong> for Web SDK (<code>@antzsoft/chat-web-sdk</code>) and RN SDK (<code>@antzsoft/chat-rn-sdk</code>) users — <code>webUploadPartFn</code> and <code>rnUploadPartFn</code> are wired in automatically. Node.js / custom integrators must pass <code>platformUploadPartFn</code> to <code>uploadBatch</code> to enable chunked uploads.</p>
277
- <p class="wn-ref">→ <a href="#step-upload">Step 16 — Upload Files</a></p>
281
+ <p>The event is emitted <strong>only to the acting user's own sockets</strong> other participants never see it. Stars are personal bookmarks, not shared state.</p>
282
+ <pre><code><span class="kw">import</span> { tryGetSocket } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
283
+ <span class="kw">import type</span> { MessageStarUpdatedEvent } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
284
+
285
+ <span class="cm">// Register at app root — fires when you star/unstar on any other device</span>
286
+ <span class="kw">const</span> socket = tryGetSocket();
287
+ socket?.<span class="fn">on</span>(<span class="str">'message_star_updated'</span>, (evt: MessageStarUpdatedEvent) =&gt; {
288
+ <span class="cm">// Update the open chat view for the affected conversation</span>
289
+ updateMessageStarState(evt.messageId, evt.isStarred);
290
+
291
+ <span class="cm">// Optionally invalidate the starred messages list if it's open</span>
292
+ <span class="kw">if</span> (!evt.isStarred) removeFromStarredList(evt.messageId);
293
+ });</code></pre>
294
+ <p><strong>Web and RN SDK users:</strong> both <code>SocketProvider</code> implementations handle this event internally and patch the React Query cache automatically. No code changes needed in the host app.</p>
295
+ <p><strong>No integration changes required</strong> for SDK users. Core SDK direct users should add the listener above at app root level.</p>
278
296
  </div>
279
297
  </div>
280
298
 
281
- <div class="wn-item" id="wn-124-mp2">
282
- <div class="wn-item-header" onclick="toggleItem('wn-124-mp2')">
299
+ <div class="wn-item" id="wn-127-softustar">
300
+ <div class="wn-item-header" onclick="toggleItem('wn-127-softustar')">
283
301
  <span class="wn-tag new">New</span>
284
- <span class="wn-item-title"><code>storageApi.completeMultipartUpload()</code> · <code>PlatformUploadPartFn</code> type · <code>CompletedPart</code> / <code>MultipartUploadInfo</code> types</span>
302
+ <span class="wn-item-title">Soft-delete unstar + sync indexes foundation for offline delta sync</span>
285
303
  <span class="wn-chevron-sm">▾</span>
286
304
  </div>
287
305
  <div class="wn-item-body">
288
- <p><code>storageApi.completeMultipartUpload(fileId, uploadId, parts)</code> is called automatically by <code>uploadBatch</code> only needed in custom manual upload flows. It assembles S3 parts and transitions the file record to active in one round-trip (replaces the separate <code>confirmUpload</code> call for the multipart path).</p>
289
- <p><code>PlatformUploadPartFn</code> is the new platform adapter type: <code>(uploadUrl: string, blob: Blob | ArrayBuffer, onProgress?) =&gt; Promise&lt;string&gt;</code> — returns the ETag S3 sends back for the part. Exported from <code>@antzsoft/chat-core</code>.</p>
290
- <p>New exported types: <code>MultipartUploadInfo</code>, <code>MultipartPartUrl</code>, <code>CompletedPart</code>.</p>
306
+ <p>Three server-side changes lay the groundwork for efficient offline delta sync in the mobile app:</p>
307
+ <ul>
308
+ <li><strong>Unstar is now a soft delete</strong> — <code>chat_message_stars</code> records are no longer hard-deleted when a user unstars. The record is updated to <code>isActive: false</code> with an <code>unstarredAt</code> timestamp. A sync query using <code>updatedAt &gt; since</code> can now detect both star and unstar events from a single collection scan. <code>getStarredMessages</code> filters <code>isActive: true</code> so visible behaviour is unchanged.</li>
309
+ <li><strong><code>updatedAt</code> on participant documents</strong> — <code>chat_conversation_participants</code> now carries <code>updatedAt</code> (Mongoose timestamps). Mute, pin, role change, and removal all bump this field, making the full participant state syncable via timestamp-based delta queries.</li>
310
+ <li><strong>New sync indexes</strong> — Three compound indexes added at startup (idempotent, no manual step needed):
311
+ <ul>
312
+ <li><code>{ conversationId, updatedAt }</code> on <code>chat_messages</code> — fetch all messages in a conversation mutated after a timestamp</li>
313
+ <li><code>{ userId, tenantId, updatedAt }</code> on <code>chat_conversation_participants</code> — fetch all participant records for a user mutated after a timestamp</li>
314
+ <li><code>{ userId, tenantId, updatedAt }</code> on <code>chat_message_stars</code> — fetch all star/unstar records for a user mutated after a timestamp</li>
315
+ </ul>
316
+ </li>
317
+ </ul>
318
+ <p><strong>Migrations 003 &amp; 004</strong> run automatically on first deploy:</p>
319
+ <ul>
320
+ <li><code>003_backfill_participant_updatedAt</code> — sets <code>updatedAt = joinedAt</code> on all existing participant documents that predate the schema change</li>
321
+ <li><code>004_backfill_star_isActive</code> — sets <code>isActive: true</code> on all existing star records (all surviving records represent active stars — hard-deleted unstar records no longer exist in the DB)</li>
322
+ </ul>
323
+ <p><strong>No integration changes required.</strong> All changes are server-side. The <code>getStarredMessages</code> API contract is unchanged.</p>
291
324
  </div>
292
325
  </div>
293
326
 
294
-
295
- <div class="wn-item" id="wn-124-1">
296
- <div class="wn-item-header" onclick="toggleItem('wn-124-1')">
297
- <span class="wn-tag improvement">Improvement</span>
298
- <span class="wn-item-title">S3 and local storage uploads now use presigned POST multipart — file bytes never touch the chat server</span>
327
+ <div class="wn-item" id="wn-127-crossconv-sync">
328
+ <div class="wn-item-header" onclick="toggleItem('wn-127-crossconv-sync')">
329
+ <span class="wn-tag new">New</span>
330
+ <span class="wn-item-title">Offline delta sync — <code>GET /sync</code> cross-conversation reconnect sync</span>
299
331
  <span class="wn-chevron-sm">▾</span>
300
332
  </div>
301
333
  <div class="wn-item-body">
302
- <p>File uploads to S3 and the local storage provider now use <strong>presigned POST</strong> (AWS POST policy / HMAC-signed form fields) instead of presigned PUT. The client sends a <code>multipart/form-data</code> request directly to the storage endpoint — S3 or the chat server's own <code>/storage/files/upload</code> endpoint with the signed fields embedded in the form body. No file bytes pass through the chat server.</p>
303
- <p><strong>What changed per provider:</strong></p>
334
+ <p>Mobile apps can now catch up on all missed changes in a single HTTP request after a socket reconnect. <code>GET /sync?since=&lt;ISO timestamp&gt;</code> returns every cross-conversation delta since the cursoredits, deletes, pins, participant changes, read receipts, and messages deleted-for-me across all conversations the user belongs to.</p>
304
335
  <table>
305
- <thead><tr><th>Provider</th><th>Upload method</th><th>Change</th></tr></thead>
336
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
306
337
  <tbody>
307
- <tr><td>AWS S3</td><td>Presigned POST (multipart/form-data)</td><td>Changed from presigned PUT POST. AWS signs a policy; client POSTs a FormData with all fields + file.</td></tr>
308
- <tr><td>Local (same-server / mount)</td><td>Presigned POST (multipart/form-data)</td><td>HMAC-signed fields now go in the form body (not query string) — WAF-friendly.</td></tr>
309
- <tr><td>Azure Blob Storage</td><td>Presigned PUT (SAS token)</td><td>UnchangedAzure has no form-based upload API.</td></tr>
338
+ <tr><td><code>syncedAt</code></td><td><code>string</code></td><td>Server-stamped ISO timestamp store as the new <code>since</code> cursor. Always use server clock to avoid client clock skew.</td></tr>
339
+ <tr><td><code>stale</code></td><td><code>boolean</code></td><td><code>true</code> when gap exceeds the stale threshold (default 30 days, configurable via <code>CHAT_SYNC_STALE_DAYS</code> env var). No data returned mark all conversations <code>needs_refresh</code> and lazy-sync on open.</td></tr>
340
+ <tr><td><code>messages</code></td><td><code>Message[]</code></td><td>Messages with <code>updatedAt &gt; since</code> covers edits, deletes, pins, unpins. Excludes messages deleted-for-me (those are in <code>deletedForMe</code>).</td></tr>
341
+ <tr><td><code>deletedForMe</code></td><td><code>SyncDeletedForMe[]</code></td><td>Messages this user hid for themselves — remove from local DB.</td></tr>
342
+ <tr><td><code>participantChanges</code></td><td><code>SyncParticipantChange[]</code></td><td>Role changes, mute/unmute, removals (<code>isActive: false</code>) across all conversations.</td></tr>
343
+ <tr><td><code>readReceipts</code></td><td><code>SyncReadReceipt[]</code></td><td>Read receipts received since <code>since</code>.</td></tr>
310
344
  </tbody>
311
345
  </table>
312
- <p><strong>How the SDK handles both paths:</strong> The <code>PresignedUrlResponse</code> carries <code>method: 'POST'</code> and a <code>fields</code> object for S3/local. Your <code>platformUploadFn</code> should branch on this:</p>
313
- <pre><code><span class="cm">// S3 / local multipart FormData (method: 'POST', fields present)</span>
314
- <span class="cm">// Azure → raw PUT body (method: 'PUT', no fields)</span>
315
- <span class="kw">if</span> (presigned.method === <span class="str">'POST'</span> &amp;&amp; presigned.fields) {
316
- <span class="kw">const</span> fd = <span class="kw">new</span> <span class="fn">FormData</span>();
317
- Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k, v]) => fd.<span class="fn">append</span>(k, v));
318
- fd.<span class="fn">append</span>(<span class="str">'file'</span>, blob, file.name); <span class="cm">// file must be last</span>
319
- xhr.<span class="fn">send</span>(fd);
320
- } <span class="kw">else</span> {
321
- xhr.<span class="fn">send</span>(blob); <span class="cm">// Azure PUT</span>
322
- }</code></pre>
323
- <p><strong>No integration changes required.</strong> Both the web SDK and RN SDK already implement this branching. If you are using the recommended <code>platformUploadFn</code> from Step 1 of this guide, it already handles both paths correctly. Only custom <code>platformUploadFn</code> implementations that unconditionally used <code>presigned.method</code> as PUT need updating — see the updated examples in <a href="#step1">Step 1</a>.</p>
324
- <p class="wn-ref">→ <a href="#step1">Step 1 platformUploadFn examples</a> · <a href="#step-upload">Step 16 Upload Files</a></p>
346
+ <p><strong>Stale handling</strong> when <code>stale: true</code> the server still returns the new <code>syncedAt</code> cursor. Save it and mark every conversation <code>needs_refresh = true</code> in local SQLite; open <code>GET /conversations/:id/sync</code> lazily when the user navigates to each conversation.</p>
347
+ <pre><code><span class="kw">import</span> { syncApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
348
+
349
+ <span class="cm">// Using @antzsoft/chat-rn-sdk built-in hook (recommended for RN)</span>
350
+ <span class="kw">import</span> { useSync } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
351
+
352
+ useSync({
353
+ onSync: <span class="kw">async</span> (data) =&gt; {
354
+ <span class="cm">// data: CrossConversationSyncResponse</span>
355
+ <span class="kw">await</span> mergeSyncDeltaIntoSQLite(data);
356
+ },
357
+ onStale: <span class="kw">async</span> () =&gt; {
358
+ <span class="cm">// Gap > stale threshold (default 30 days) lazy-sync per conversation on open</span>
359
+ <span class="kw">await</span> db.execute(<span class="str">'UPDATE conversations SET needs_refresh = 1'</span>);
360
+ },
361
+ });
362
+
363
+ <span class="cm">// Or call syncApi directly (custom integration)</span>
364
+ <span class="kw">const</span> result = <span class="kw">await</span> syncApi.<span class="fn">pull</span>(lastSyncedAt);
365
+ <span class="kw">await</span> AsyncStorage.<span class="fn">setItem</span>(<span class="str">'@antz_last_synced_at'</span>, result.syncedAt);
366
+ <span class="kw">if</span> (result.stale) markAllNeedsRefresh();
367
+ <span class="kw">else</span> mergeSyncDeltaIntoSQLite(result);</code></pre>
368
+ <p><strong>Does not include reactions or stars</strong> — these are per-conversation and returned by <code>GET /conversations/:id/sync</code>.</p>
369
+ <p><strong>Web SDK users:</strong> the web SDK uses React Query cache invalidation on reconnect — no sync call needed. <code>syncApi</code> is intended for mobile (React Native) only.</p>
325
370
  </div>
326
371
  </div>
327
372
 
328
- <div class="wn-item" id="wn-124-2">
329
- <div class="wn-item-header" onclick="toggleItem('wn-124-2')">
330
- <span class="wn-tag fix">Fix</span>
331
- <span class="wn-item-title"><code>POST /storage/files/upload</code> returns <code>403</code> when transit encryption is enabled</span>
373
+ <div class="wn-item" id="wn-127-conv-sync">
374
+ <div class="wn-item-header" onclick="toggleItem('wn-127-conv-sync')">
375
+ <span class="wn-tag new">New</span>
376
+ <span class="wn-item-title">Offline delta sync <code>GET /conversations/:id/sync</code> per-conversation full delta</span>
332
377
  <span class="wn-chevron-sm">▾</span>
333
378
  </div>
334
379
  <div class="wn-item-body">
335
- <p>The local-storage direct upload endpoint (<code>POST /storage/files/upload</code>) was missing the <code>@PreTransit()</code> decorator. With <code>transitEncryption: true</code>, the <code>TransitEnforceGuard</code> rejected every upload request with <code>403 Transit encryption required</code>.</p>
336
- <p><strong>Why this endpoint is pre-transit:</strong> The client uploads directly via XHR/fetch — not through the Axios client — so no <code>x-transit-session</code> header is ever present on this request. Auth is the HMAC signature embedded in the <code>multipart/form-data</code> fields (<code>token</code>, <code>ts</code>) which the server validates independently.</p>
337
- <p><strong>No integration changes required.</strong> This fix unblocks uploads on deployments with <code>transitEncryption: true</code>. No client-side changes needed.</p>
380
+ <p>Returns the complete delta for a single conversation, including reactions and stars. Call this when a conversation is marked <code>needs_refresh</code> (gap exceeded the stale threshold) or when a fresh-install user opens a conversation for the first time (omit <code>since</code> to receive the full history).</p>
381
+ <table>
382
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
383
+ <tbody>
384
+ <tr><td><code>syncedAt</code></td><td><code>string</code></td><td>Server-stamped cursor — update <code>lastSyncedAt</code> after upsert.</td></tr>
385
+ <tr><td><code>messages</code></td><td><code>Message[]</code></td><td>All messages with <code>updatedAt &gt; since</code>. On fresh install (<code>since</code> omitted), returns all messages.</td></tr>
386
+ <tr><td><code>deletedForMe</code></td><td><code>SyncDeletedForMe[]</code></td><td>Messages deleted for this user — remove from local DB.</td></tr>
387
+ <tr><td><code>reactions</code></td><td><code>SyncReactions</code></td><td>Full current emoji counts for every message that had reaction activity since <code>since</code>. <strong>Always upsert as ground truth</strong> — reactions are hard-deleted so delta is not safe.</td></tr>
388
+ <tr><td><code>stars</code></td><td><code>SyncStarEntry[]</code></td><td><code>isActive: true</code> = starred, <code>isActive: false</code> = unstarred (soft-delete). Delete local star record when <code>false</code>.</td></tr>
389
+ <tr><td><code>participantChanges</code></td><td><code>SyncParticipantChange[]</code></td><td>Role changes, mutes, removals for this conversation.</td></tr>
390
+ <tr><td><code>readReceipts</code></td><td><code>SyncReadReceipt[]</code></td><td>Read receipts received since <code>since</code>.</td></tr>
391
+ </tbody>
392
+ </table>
393
+ <p><strong>Reactions are always returned as full current state</strong> for the affected messages — not as a delta. Because reaction removes are hard-deleted (no timestamp trace), a delta is impossible. Upsert the reaction counts returned and discard any locally cached state for those message IDs.</p>
394
+ <pre><code><span class="kw">import</span> { useConversationSync } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
395
+
396
+ <span class="cm">// In your conversation screen</span>
397
+ <span class="kw">const</span> { isSyncing, sync } = <span class="fn">useConversationSync</span>(conversationId);
398
+
399
+ <span class="cm">// Trigger when conversation is marked stale</span>
400
+ <span class="fn">useEffect</span>(() =&gt; {
401
+ <span class="kw">if</span> (needsRefresh) {
402
+ <span class="fn">sync</span>().then((data) =&gt; {
403
+ <span class="kw">if</span> (data) upsertConversationDeltaToSQLite(data);
404
+ clearNeedsRefresh(conversationId);
405
+ });
406
+ }
407
+ }, [conversationId, needsRefresh]);
408
+
409
+ <span class="cm">// Fresh install — omit since to get full history</span>
410
+ <span class="kw">const</span> { sync: fullSync } = <span class="fn">useConversationSync</span>(conversationId, { skipSince: <span class="kw">true</span> });
411
+
412
+ <span class="cm">// Or call syncApi directly</span>
413
+ <span class="kw">const</span> result = <span class="kw">await</span> syncApi.<span class="fn">pullConversation</span>(conversationId, lastSyncedAt);
414
+ <span class="kw">await</span> upsertConversationDeltaToSQLite(result);</code></pre>
415
+ <p><strong>Authorization</strong> — server verifies the caller is an active participant. Non-participants receive <code>403 Forbidden</code>.</p>
416
+ <p><strong>No integration changes required</strong> for web or existing RN users. RN apps that want offline sync should add <code>useSync</code> at the root provider level and <code>useConversationSync</code> in each conversation screen. See the <a href="#syncApi">Sync API reference</a> in the README for type signatures and full usage examples.</p>
338
417
  </div>
339
418
  </div>
340
419
 
341
420
  </div>
342
- </div>
421
+ </div><!-- /.wn-version -->
343
422
 
344
- <!-- ── v1.2.3 ── -->
345
- <div class="wn-version" id="wn-123">
346
- <div class="wn-header" onclick="toggleVersion('wn-123')">
423
+ <!-- ── v1.2.6 ── -->
424
+ <div class="wn-version" id="wn-126">
425
+ <div class="wn-header" onclick="toggleVersion('wn-126')">
347
426
  <div class="wn-title">
348
- <span class="wn-ver">v1.2.3</span>
349
- <span class="wn-date">May 2026</span>
427
+ <span class="wn-ver">v1.2.6</span>
428
+ <span class="wn-date">June 2026</span>
350
429
  </div>
351
430
  <span class="wn-chevron">▼</span>
352
431
  </div>
353
432
  <div class="wn-body">
354
433
 
355
- <div class="wn-item" id="wn-123-1">
356
- <div class="wn-item-header" onclick="toggleItem('wn-123-1')">
434
+ <div class="wn-item" id="wn-126-clearchat">
435
+ <div class="wn-item-header" onclick="toggleItem('wn-126-clearchat')">
357
436
  <span class="wn-tag new">New</span>
358
- <span class="wn-item-title"><code>messagesApi.getReceipts(messageId)</code>per-user read &amp; delivery receipt detail</span>
437
+ <span class="wn-item-title">Clear / Delete Chat hide any conversation from your list without affecting other participants</span>
359
438
  <span class="wn-chevron-sm">▾</span>
360
439
  </div>
361
440
  <div class="wn-item-body">
362
- <p>New endpoint <code>GET /messages/:id/receipts</code> returns the full read and delivery receipt list for a single message, with user profiles (name, avatar) resolved server-side. Designed as the initial load for a "Read by / Delivered to" message info screen.</p>
363
- <p>Response shape:</p>
364
- <pre><code>{
365
- messageId: string,
366
- readBy: [{ userId, displayName, avatarUrl, readAt }],
367
- deliveredTo: [{ userId, displayName, avatarUrl, deliveredAt }]
368
- }</code></pre>
369
- <p>No secondary user lookup needed — profiles are included. The "Sent to" bucket (not yet delivered) is derived client-side by subtracting <code>readBy</code> and <code>deliveredTo</code> user IDs from the conversation participant list.</p>
370
- <p class="wn-ref">→ <a href="#step-message-info">Step 12b — Message Info Screen</a> for the complete pattern including live updates.</p>
441
+ <p><code>conversationsApi.delete(conversationId)</code> is now available to <strong>any participant</strong> on both DMs and groups no admin role required. It hides the conversation from the caller's list only; all other participants are completely unaffected.</p>
442
+ <table>
443
+ <thead><tr><th>Scenario</th><th>What to call</th><th>Effect</th></tr></thead>
444
+ <tbody>
445
+ <tr><td><strong>Delete Chat</strong> (DM)</td><td><code>conversationsApi.delete(id)</code></td><td>Conversation hidden from caller's list. Membership periods wiped — if the other person messages again, the chat reappears showing only messages from the reappearance point onwards.</td></tr>
446
+ <tr><td><strong>Exit Group</strong></td><td><code>conversationsApi.leave(id)</code></td><td>Caller becomes inactive participant, stays in list as read-only. Server auto-promotes another admin if caller was the only admin.</td></tr>
447
+ <tr><td><strong>Exit and Delete Group</strong></td><td><code>conversationsApi.leave(id, true)</code></td><td>Atomic: exit the group + hide from list in one write. Membership periods wiped. No race window between the two operations.</td></tr>
448
+ <tr><td><strong>Delete Group</strong> (after exit)</td><td><code>conversationsApi.delete(id)</code></td><td>Hides the already-exited group from caller's list. Other participants unaffected.</td></tr>
449
+ </tbody>
450
+ </table>
451
+ <p><strong>Message history after re-add / reappear:</strong> Membership periods are wiped on "Delete Chat" and "Exit and Delete". When the caller is re-added to a group or the other party messages in a DM, only messages from that point onwards are visible — no prior history leaks through.</p>
452
+ <p><strong>Socket event:</strong> <code>conversation_deleted</code> is emitted <strong>only to the acting user's own sockets</strong> in three cases: (1) <code>delete()</code> — self-delete/hide a DM or group, (2) <code>leave(id, true)</code> — exit and delete a group atomically, (3) <code>delete()</code> called on an already-exited group. In all cases, remove the conversation from local state and navigate away if it was open. Other participants never receive this event.</p>
453
+ <pre><code><span class="kw">import</span> { conversationsApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
454
+
455
+ <span class="cm">// ── DM: "Delete Chat" ─────────────────────────────────────────────────</span>
456
+ <span class="kw">await</span> conversationsApi.<span class="fn">delete</span>(dmConversationId);
457
+ <span class="cm">// → Caller's list: conversation hidden</span>
458
+ <span class="cm">// → Other participant: unaffected</span>
459
+ <span class="cm">// → Re-open trigger: other party sends a message → reappears with new history only</span>
460
+
461
+ <span class="cm">// ── Group: "Exit Group" (stay in list read-only) ──────────────────────</span>
462
+ <span class="kw">await</span> conversationsApi.<span class="fn">leave</span>(groupId);
463
+
464
+ <span class="cm">// ── Group: "Exit and Delete" (leave + hide atomically) ───────────────</span>
465
+ <span class="kw">await</span> conversationsApi.<span class="fn">leave</span>(groupId, <span class="kw">true</span>);
466
+
467
+ <span class="cm">// ── Group: "Delete Group" (already exited, now hiding the entry) ──────</span>
468
+ <span class="kw">await</span> conversationsApi.<span class="fn">delete</span>(groupId);</code></pre>
469
+ <p><strong>Web / RN SDK hooks:</strong> <code>useConversations()</code> exposes these as named mutations — <code>leaveGroup</code>, <code>leaveAndDeleteGroup</code>, and <code>deleteConversation</code> (RN) / <code>deleteGroup</code> (web). The cache is updated optimistically; no manual invalidation needed.</p>
470
+ <p class="wn-ref">→ <a href="#step-clear-chat">Step 6c — Clear / Delete Chat</a> for the full pattern with socket handling and Web/RN SDK examples.</p>
371
471
  </div>
372
472
  </div>
373
473
 
374
- <div class="wn-item" id="wn-123-2">
375
- <div class="wn-item-header" onclick="toggleItem('wn-123-2')">
376
- <span class="wn-tag fix">Fix</span>
377
- <span class="wn-item-title"><code>message_delivered</code> now fires per recipient, not all-or-nothing</span>
474
+ <div class="wn-item" id="wn-126-inactive">
475
+ <div class="wn-item-header" onclick="toggleItem('wn-126-inactive')">
476
+ <span class="wn-tag new">New</span>
477
+ <span class="wn-item-title">Inactive &amp; reactivate user server-enforced deactivation and fresh-start reactivation</span>
378
478
  <span class="wn-chevron-sm">▾</span>
379
479
  </div>
380
480
  <div class="wn-item-body">
381
- <p>Previously <code>message_delivered</code> only fired to the sender when <em>all</em> recipients were online at the moment of sending if even one recipient was offline, the sender received no delivery event at all until every recipient came online simultaneously (which often never happens in large groups).</p>
382
- <p>The server now fires <code>message_delivered</code> once per online recipient at send time. The payload's <code>deliveredTo</code> field is now a single object <code>{ userId, deliveredAt }</code> (previously an array) — one event per person, matching how <code>read_receipt</code> works.</p>
383
- <p>This means the delivery section of a message info screen builds up live one entry at a time, exactly as WhatsApp does.</p>
384
- <p><strong>Backward compatible</strong> — existing <code>message_delivered</code> handlers in both the web and RN SDKs only read <code>event.messageId</code> to update the tick mark and are unaffected by the <code>deliveredTo</code> shape change. The event fires more frequently (good — more accurate delivery feedback) but the existing handler logic is identical.</p>
385
- <p class="wn-ref">→ <a href="#step-message-info">Step 12bMessage Info Screen</a> · <a href="#step-realtime">Step 10 — Real-time Events</a></p>
481
+ <p>When a user account is set to <code>inactive</code> (self-service in builtin mode, or via external user-service sync in non-builtin modes), the server applies full <strong>"self-left + delete"</strong> semantics atomically:</p>
482
+ <table>
483
+ <thead><tr><th>Action</th><th>Effect</th></tr></thead>
484
+ <tbody>
485
+ <tr><td>Refresh tokens</td><td>All revokedno new access tokens can be issued</td></tr>
486
+ <tr><td>Device tokens</td><td>All disabled — push notifications stop immediately</td></tr>
487
+ <tr><td>Redis caches</td><td><code>chatuser:</code>, <code>shadow:builtin:</code>, <code>shadow:&lt;tenant&gt;:</code> busted atomically — no 5-min window</td></tr>
488
+ <tr><td>Group conversations</td><td><code>participant_left</code> fires to remaining members, admin auto-promoted, periods wiped</td></tr>
489
+ <tr><td>DM conversations</td><td>Hidden silently — periods wiped, no event emitted</td></tr>
490
+ <tr><td>Live socket</td><td>Force-disconnected — <code>user_offline</code> broadcast fires automatically</td></tr>
491
+ <tr><td>Socket auth cache-hit</td><td>Non-builtin shadow cache hit now re-checks DB status — no 5-min bypass window</td></tr>
492
+ <tr><td>addParticipants / createGroup</td><td>Inactive user IDs silently filtered before insert</td></tr>
493
+ <tr><td>Blocked users list</td><td>Deactivated users excluded automatically</td></tr>
494
+ </tbody>
495
+ </table>
496
+ <p><strong>On reactivation:</strong> device tokens re-enabled, user cache busted, conversation list intentionally empty — fresh start. DMs reopen when the other party messages, showing only messages from that point.</p>
497
+ <p><strong>SDK resilience:</strong> RN <code>SocketProvider</code> retries token refresh up to 5× on <code>connect_error</code> auth failures. Web <code>SocketProvider</code> calls <code>logout()</code> after 5 failed retries instead of silently freezing.</p>
498
+ <p><strong>No integration changes required</strong> — server-side only. Clients receive standard <code>participant_left</code> / <code>user_offline</code> events.</p>
499
+ <p class="wn-ref">→ <a href="#step-inactive-user">Step 6b — Inactive Users</a> for client-side handling: UI states, 401 enforcement, socket reconnect, and reactivation behaviour.</p>
386
500
  </div>
387
501
  </div>
388
502
 
389
- <div class="wn-item" id="wn-123-3">
390
- <div class="wn-item-header" onclick="toggleItem('wn-123-3')">
503
+ <div class="wn-item" id="wn-126-frozengroup">
504
+ <div class="wn-item-header" onclick="toggleItem('wn-126-frozengroup')">
391
505
  <span class="wn-tag new">New</span>
392
- <span class="wn-item-title">Viewer-aware system message text SDK resolves "You", "you", or full names automatically</span>
506
+ <span class="wn-item-title">Removed/left users see frozen group state name, icon, description, and member list as of exit time</span>
393
507
  <span class="wn-chevron-sm">▾</span>
394
508
  </div>
395
509
  <div class="wn-item-body">
396
- <p>System messages (member added/removed, admin promoted/demoted, group created/updated) now render differently depending on who is reading them matching WhatsApp behaviour.</p>
510
+ <p>Group conversations now serve a historically accurate snapshot to removed or self-exited users instead of live data. Every field a removed user sees is frozen at the moment they left:</p>
397
511
  <table>
398
- <thead><tr><th>Viewer</th><th>Rendered text</th></tr></thead>
512
+ <thead><tr><th>Field</th><th>Active member</th><th>Removed / left member</th></tr></thead>
399
513
  <tbody>
400
- <tr><td>Actor (Anil removed Ajay)</td><td>"You removed Ajay Antony"</td></tr>
401
- <tr><td>Target (Ajay was removed)</td><td>"Anil Rathod removed you"</td></tr>
402
- <tr><td>Bystander (Saket)</td><td>"Anil Rathod removed Ajay Antony"</td></tr>
514
+ <tr><td><code>name</code></td><td>Live group name</td><td>Frozen at exit time</td></tr>
515
+ <tr><td><code>description</code></td><td>Live description</td><td>Frozen at exit time</td></tr>
516
+ <tr><td><code>iconUrl</code></td><td>Live icon</td><td>Frozen at exit time</td></tr>
517
+ <tr><td><code>participantCount</code></td><td>Current active count</td><td>Count at exit time</td></tr>
518
+ <tr><td><code>participants</code> array</td><td>Current active members</td><td>Members present at exit time</td></tr>
519
+ <tr><td><code>lastMessage</code></td><td>Live last message</td><td>Frozen at exit time (unchanged)</td></tr>
403
520
  </tbody>
404
521
  </table>
405
- <p><strong>How it works:</strong> The server stores the neutral third-person form in <code>content.text</code> (used for push notifications and exports) and sends structured <code>actorUserId</code>, <code>actorUserName</code>, <code>targetUserId</code>, <code>targetUserName</code> fields in the message <code>metadata</code>. The SDK's <code>messagesApi.list()</code> applies <code>resolveSystemMessageText()</code> in-memory before returning messages — <code>content.text</code> is already correct by the time it reaches your UI. No frontend logic changes needed.</p>
406
- <p><strong>Socket path:</strong> For raw <code>new_message</code> socket events handled outside the SDK pipeline (e.g. <code>AntzChatClient</code> headless usage), call <code>resolveSystemMessageText(message, currentUserId)</code> manually:</p>
407
- <pre><code>import { resolveSystemMessageText, getAuthStore } from '@antzsoft/chat-core';
408
-
409
- client.socket.on('new_message', (event) => {
410
- const message = event.message;
411
- if (message.content.type === 'system') {
412
- const uid = getAuthStore().useAuthStore.getState().user?.id ?? '';
413
- message.content.text = resolveSystemMessageText(message, uid);
414
- }
415
- appendMessageToView(message);
416
- });</code></pre>
417
- <p><strong>Backward compatible:</strong></p>
418
- <ul>
419
- <li>Old SDK + new server: <code>actorUserId</code> fields ignored, raw <code>content.text</code> displayed — degrades gracefully.</li>
420
- <li>New SDK + old server: <code>actorUserId</code> absent, guard fires, raw <code>content.text</code> returned unchanged.</li>
421
- </ul>
422
- <p>Applies to: <code>group_created</code>, <code>user_added</code>, <code>user_removed</code>, <code>user_left</code>, <code>admin_promoted</code>, <code>admin_demoted</code>, <code>group_updated</code>.</p>
423
- <p><strong>Also fixed in this release:</strong></p>
522
+ <p><strong>No integration changes required.</strong> The response shape is identical field names and types are unchanged. Values are simply frozen for inactive participants. Existing code that filters participants by <code>p.isActive !== false</code> continues to work correctly since all members in a removed user's snapshot have <code>isActive: true</code> (they were active at that time).</p>
523
+ <p><strong>One recommendation:</strong> use <code>conversation.participantCount</code> (the server-provided field) for member count display rather than <code>conversation.participants.length</code> the two are kept in sync by the server and <code>participantCount</code> is always the accurate value for both active and removed users.</p>
524
+ <p><strong>On re-add:</strong> all frozen snapshots are cleared automatically. The re-added user immediately sees live group data — current name, icon, description, and active member list.</p>
525
+ </div>
526
+ </div>
527
+
528
+ <div class="wn-item" id="wn-126-errors">
529
+ <div class="wn-item-header" onclick="toggleItem('wn-126-errors')">
530
+ <span class="wn-tag new">New</span>
531
+ <span class="wn-item-title">Structured error layer — typed error classes with codes, retryable flag, and context</span>
532
+ <span class="wn-chevron-sm">▾</span>
533
+ </div>
534
+ <div class="wn-item-body">
535
+ <p>All SDK errors — REST failures, socket timeouts, queue overflows, and transit mismatches — are now instances of typed error classes instead of raw <code>AxiosError</code> objects or plain <code>Error</code> strings. Every error carries:</p>
424
536
  <ul>
425
- <li><code>group_updated</code> system message text now includes the actor name ("Anil changed the group name to 'X'" instead of the previous "Group name changed to 'X'").</li>
426
- <li>Push notification body for <code>admin_demoted</code> corrected from "made you member" to "removed you as admin".</li>
427
- <li>Removed users who are offline at the time of removal now receive a push notification ("Anil Rathod removed you from the group") since the socket event cannot reach them.</li>
537
+ <li><code>code</code> machine-readable string (e.g. <code>SESSION_EXPIRED</code>, <code>SEND_QUEUE_FULL</code>)</li>
538
+ <li><code>retryable</code> <code>true</code> when the operation is safe to retry (network errors, 5xx, rate limiting)</li>
539
+ <li><code>context</code> diagnostic object (HTTP status, server path, axios code, <code>decryptionFailed</code> flag, etc.)</li>
428
540
  </ul>
541
+ <table>
542
+ <thead><tr><th>Class</th><th>Codes</th><th>Source</th></tr></thead>
543
+ <tbody>
544
+ <tr><td><code>AntzChatAuthError</code></td><td><code>SESSION_EXPIRED</code>, <code>AUTH_FAILED</code></td><td>HTTP 401</td></tr>
545
+ <tr><td><code>AntzChatValidationError</code></td><td><code>VALIDATION_ERROR</code></td><td>HTTP 400 / 422. <code>.fields</code> array for multi-field errors</td></tr>
546
+ <tr><td><code>AntzChatPermissionError</code></td><td><code>PERMISSION_DENIED</code></td><td>HTTP 403</td></tr>
547
+ <tr><td><code>AntzChatNetworkError</code></td><td><code>NETWORK_ERROR</code>, <code>RATE_LIMITED</code>, <code>SOCKET_TIMEOUT</code>, <code>SOCKET_NOT_CONNECTED</code>, <code>SEND_QUEUE_FULL</code>, <code>MESSAGE_DROPPED</code></td><td>Network, socket, queue</td></tr>
548
+ <tr><td><code>AntzChatServerError</code></td><td><code>SERVER_ERROR</code>, <code>NOT_FOUND</code></td><td>HTTP 5xx, 404</td></tr>
549
+ <tr><td><code>AntzChatError</code> (base)</td><td><code>TRANSIT_MISMATCH</code></td><td>SDK/server encryption config mismatch</td></tr>
550
+ </tbody>
551
+ </table>
552
+ <p>Transit-encrypted error bodies are decrypted before normalisation. If decryption itself fails, <code>context.decryptionFailed = true</code> is set and the class is still determined correctly from the HTTP status code.</p>
553
+ <p><strong>Backward compatible — no integration changes required.</strong> All classes extend native <code>Error</code> so existing <code>catch (err)</code> blocks work unchanged.</p>
554
+ <p class="wn-ref">→ <a href="#step-errors">Step 20 — Error Handling</a> for full usage patterns, code reference, and platform-specific examples.</p>
429
555
  </div>
430
556
  </div>
431
557
 
@@ -436,7 +562,7 @@ client.socket.on('new_message', (event) => {
436
562
  <button class="wn-history-btn" onclick="openVersionHistory()">
437
563
  <span class="wn-history-icon">⏱</span>
438
564
  View version history
439
- <span class="wn-history-count">15 older versions</span>
565
+ <span class="wn-history-count">20 older versions</span>
440
566
  </button>
441
567
 
442
568
  </section>
@@ -453,6 +579,137 @@ client.socket.on('new_message', (event) => {
453
579
  </div>
454
580
  <div class="vh-body">
455
581
 
582
+ <!-- ── v1.2.5 ── -->
583
+ <div class="wn-version" id="wn-125">
584
+ <div class="wn-header" onclick="toggleVersion('wn-125')">
585
+ <div class="wn-title">
586
+ <span class="wn-ver">v1.2.5</span>
587
+ <span class="wn-date">May 2026</span>
588
+ </div>
589
+ <span class="wn-chevron">▾</span>
590
+ </div>
591
+ <div class="wn-body">
592
+
593
+ <div class="wn-item" id="wn-125-transit">
594
+ <div class="wn-item-header" onclick="toggleItem('wn-125-transit')">
595
+ <span class="wn-tag new">New</span>
596
+ <span class="wn-item-title">Transit session decoupled from socket — <code>createRestTransitSession()</code> and <code>rnCreateRestTransitSession()</code></span>
597
+ <span class="wn-chevron-sm">▾</span>
598
+ </div>
599
+ <div class="wn-item-body">
600
+ <p>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 — for example, notification reply handlers when the app is killed.</p>
601
+ <p>A new <code>POST /crypto/session</code> REST endpoint establishes a full ECDH session over HTTP. <code>createRestTransitSession(apiUrl)</code> performs the exchange and returns <code>{ sessionId, sessionKey }</code> so any REST call can be encrypted without a socket connection. The axios client uses this path automatically when a transit session is needed before the socket is ready.</p>
602
+ <p><code>rnCreateRestTransitSession(apiUrl)</code> is exported from <code>@antzsoft/chat-rn-sdk</code> — a Hermes-compatible wrapper using <code>@noble/curves</code> (X25519) and <code>@noble/ciphers</code> (AES-256-GCM). Use it in background notification reply handlers when the main app is not running:</p>
603
+ <pre><code><span class="kw">import</span> { createRestTransitSession } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
604
+ <span class="kw">import</span> { rnCreateRestTransitSession } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>; <span class="cm">// RN / Hermes</span>
605
+
606
+ <span class="cm">// In a background notification reply handler (app killed, no socket):</span>
607
+ <span class="kw">const</span> session = <span class="kw">await</span> <span class="fn">rnCreateRestTransitSession</span>(<span class="str">'https://api.yourapp.com/api/v1'</span>);
608
+ <span class="cm">// Use session.sessionId + session.sessionKey to encrypt REST calls manually.</span>
609
+ <span class="cm">// Normal SDK usage: fully automatic — no code changes needed.</span></code></pre>
610
+ <p><strong>No integration changes required</strong> for standard SDK usage.</p>
611
+ </div>
612
+ </div>
613
+
614
+ </div>
615
+ </div><!-- /.wn-version -->
616
+
617
+ <!-- ── v1.2.4 ── -->
618
+ <div class="wn-version" id="wn-124">
619
+ <div class="wn-header" onclick="toggleVersion('wn-124')">
620
+ <div class="wn-title">
621
+ <span class="wn-ver">v1.2.4</span>
622
+ <span class="wn-date">May 2026</span>
623
+ </div>
624
+ <span class="wn-chevron">▾</span>
625
+ </div>
626
+ <div class="wn-body">
627
+
628
+ <div class="wn-item" id="wn-124h-mp1">
629
+ <div class="wn-item-header" onclick="toggleItem('wn-124h-mp1')">
630
+ <span class="wn-tag new">New</span>
631
+ <span class="wn-item-title">Chunked multipart upload for files ≥ 10 MB (S3 and local)</span>
632
+ <span class="wn-chevron-sm">▾</span>
633
+ </div>
634
+ <div class="wn-item-body">
635
+ <p>Files ≥ 10 MB on S3 and local storage now use S3 multipart upload (10 MB parts, up to 3 parallel, <code>CompleteMultipartUpload</code>). Benefits: resumable, parallel throughput, no single-request timeout, up to 5 TB. Files below 10 MB and Azure are unchanged.</p>
636
+ <p><strong>No integration changes required</strong> for Web and RN SDK users — <code>webUploadPartFn</code> / <code>rnUploadPartFn</code> wired automatically. Node.js integrators must supply <code>platformUploadPartFn</code> to <code>uploadBatch</code>.</p>
637
+ </div>
638
+ </div>
639
+
640
+ <div class="wn-item" id="wn-124h-1">
641
+ <div class="wn-item-header" onclick="toggleItem('wn-124h-1')">
642
+ <span class="wn-tag improvement">Improvement</span>
643
+ <span class="wn-item-title">S3 and local uploads now use presigned POST — file bytes never touch the chat server</span>
644
+ <span class="wn-chevron-sm">▾</span>
645
+ </div>
646
+ <div class="wn-item-body">
647
+ <p>S3 and local uploads changed from presigned PUT to presigned POST (AWS policy / HMAC-signed fields). Bytes upload directly client-to-storage. Azure unchanged. <code>platformUploadFn</code> already branches on <code>method === 'POST' &amp;&amp; presigned.fields</code> — no changes needed.</p>
648
+ </div>
649
+ </div>
650
+
651
+ <div class="wn-item" id="wn-124h-2">
652
+ <div class="wn-item-header" onclick="toggleItem('wn-124h-2')">
653
+ <span class="wn-tag fix">Fix</span>
654
+ <span class="wn-item-title"><code>POST /storage/files/upload</code> returns 403 when transit encryption is enabled</span>
655
+ <span class="wn-chevron-sm">▾</span>
656
+ </div>
657
+ <div class="wn-item-body">
658
+ <p>The local-storage direct upload endpoint was missing <code>@PreTransit()</code>. XHR/fetch uploads never carry <code>x-transit-session</code> — the decorator marks the endpoint reachable without a transit session. Fixed. <strong>No integration changes required.</strong></p>
659
+ </div>
660
+ </div>
661
+
662
+ </div>
663
+ </div><!-- /.wn-version -->
664
+
665
+ <!-- ── v1.2.3 ── -->
666
+ <div class="wn-version" id="wn-123">
667
+ <div class="wn-header" onclick="toggleVersion('wn-123')">
668
+ <div class="wn-title">
669
+ <span class="wn-ver">v1.2.3</span>
670
+ <span class="wn-date">May 2026</span>
671
+ </div>
672
+ <span class="wn-chevron">▾</span>
673
+ </div>
674
+ <div class="wn-body">
675
+
676
+ <div class="wn-item" id="wn-123h-1">
677
+ <div class="wn-item-header" onclick="toggleItem('wn-123h-1')">
678
+ <span class="wn-tag new">New</span>
679
+ <span class="wn-item-title"><code>messagesApi.getReceipts(messageId)</code> — per-user read &amp; delivery receipt detail</span>
680
+ <span class="wn-chevron-sm">▾</span>
681
+ </div>
682
+ <div class="wn-item-body">
683
+ <p>New <code>GET /messages/:id/receipts</code> returns the full read/delivery receipt list with resolved user profiles. Use as the initial load for a "Read by / Delivered to" message info screen — no secondary user lookup needed.</p>
684
+ <p class="wn-ref">→ <a href="#step-message-info">Step 12b — Message Info Screen</a></p>
685
+ </div>
686
+ </div>
687
+
688
+ <div class="wn-item" id="wn-123h-2">
689
+ <div class="wn-item-header" onclick="toggleItem('wn-123h-2')">
690
+ <span class="wn-tag fix">Fix</span>
691
+ <span class="wn-item-title"><code>message_delivered</code> now fires per recipient, not all-or-nothing</span>
692
+ <span class="wn-chevron-sm">▾</span>
693
+ </div>
694
+ <div class="wn-item-body">
695
+ <p>Server now fires once per online recipient. <code>deliveredTo</code> is a single <code>{ userId, deliveredAt }</code> object (was an array). Backward compatible — existing handlers only read <code>event.messageId</code>.</p>
696
+ </div>
697
+ </div>
698
+
699
+ <div class="wn-item" id="wn-123h-3">
700
+ <div class="wn-item-header" onclick="toggleItem('wn-123h-3')">
701
+ <span class="wn-tag new">New</span>
702
+ <span class="wn-item-title">Viewer-aware system message text — SDK resolves "You" / full names automatically</span>
703
+ <span class="wn-chevron-sm">▾</span>
704
+ </div>
705
+ <div class="wn-item-body">
706
+ <p>System messages render differently per viewer (actor / target / bystander) — matching WhatsApp behaviour. <code>content.text</code> resolved by <code>messagesApi.list()</code> before reaching your UI. Raw socket events: call <code>resolveSystemMessageText(message, currentUserId)</code> manually.</p>
707
+ </div>
708
+ </div>
709
+
710
+ </div>
711
+ </div><!-- /.wn-version -->
712
+
456
713
  <!-- ── v1.2.0 ── -->
457
714
  <div class="wn-version" id="wn-120">
458
715
  <div class="wn-header" onclick="toggleVersion('wn-120')">
@@ -2564,6 +2821,176 @@ chatClient.<span class="fn">disconnect</span>();
2564
2821
  </div>
2565
2822
  </section>
2566
2823
 
2824
+ <!-- ─── STEP 6b: INACTIVE USERS ────────────────────────────────────────── -->
2825
+ <section id="step-inactive-user">
2826
+ <h2><span class="step">STEP 6b</span> Inactive Users — Handling Deactivated Accounts</h2>
2827
+
2828
+ <div class="callout info"><strong>New in v1.2.6</strong> — When a user's account is deactivated server-side, the server applies full "self-left + delete" semantics automatically. This section explains what your client receives and how to handle each case.</div>
2829
+
2830
+ <h3>What the server does on deactivation (automatic)</h3>
2831
+ <p>No client action triggers this — it is driven entirely server-side when <code>user.status</code> changes to <code>inactive</code>:</p>
2832
+ <table>
2833
+ <thead><tr><th>Step</th><th>What happens</th><th>Client impact</th></tr></thead>
2834
+ <tbody>
2835
+ <tr><td>1</td><td>All refresh tokens revoked</td><td>Next <code>/auth/refresh</code> call → <code>401 Unauthorized</code></td></tr>
2836
+ <tr><td>2</td><td>All device tokens disabled</td><td>Push notifications stop immediately — no action needed in app</td></tr>
2837
+ <tr><td>3</td><td>All Redis auth caches busted</td><td>Next HTTP request or socket connect → <code>401 Unauthorized</code> immediately (no 5-min cache window)</td></tr>
2838
+ <tr><td>4</td><td>All group conversations: <code>leave(hideAfterLeave=true)</code></td><td><code>participant_left</code> event fires to remaining members — update your member list</td></tr>
2839
+ <tr><td>5</td><td>All DM conversations: hidden + periods wiped silently</td><td>No event — the conversation disappears from the list on next fetch</td></tr>
2840
+ <tr><td>6</td><td>Live socket force-disconnected</td><td><code>disconnect</code> event fires on the deactivated user's socket; <code>user_offline</code> broadcast fires to all parties</td></tr>
2841
+ </tbody>
2842
+ </table>
2843
+
2844
+ <h3>What your app receives when a user in a conversation is deactivated</h3>
2845
+ <p>When someone <em>else</em> in a conversation is deactivated, your app receives standard events — no special handling needed:</p>
2846
+ <pre><code><span class="cm">// Standard events you already handle — no changes needed</span>
2847
+ socket.<span class="fn">on</span>(<span class="str">'participant_left'</span>, (event) => {
2848
+ <span class="cm">// event.userId = deactivated user's ID</span>
2849
+ <span class="cm">// Remove them from the group member list, update participant count</span>
2850
+ <span class="fn">removeParticipant</span>(event.conversationId, event.userId);
2851
+ });
2852
+
2853
+ socket.<span class="fn">on</span>(<span class="str">'user_offline'</span>, (event) => {
2854
+ <span class="cm">// Standard presence update — deactivated users are offline</span>
2855
+ useChatStore.<span class="fn">getState</span>().<span class="fn">setUserOffline</span>(event.userId);
2856
+ });</code></pre>
2857
+
2858
+ <h3>What your app receives when the CURRENT USER is deactivated</h3>
2859
+ <p>The deactivated user's socket is force-disconnected. On RN, the <code>connect_error</code> handler (new in v1.2.6) retries up to 5 times; each attempt gets a <code>401 Unauthorized</code>. After all retries fail, the socket stays disconnected. On Web, after 5 retries the SDK calls <code>logout()</code> automatically and shows the login screen.</p>
2860
+
2861
+ <pre><code><span class="cm">// RN — the connect_error handler is built into SocketProvider (v1.2.6+)</span>
2862
+ <span class="cm">// No changes needed. After 5 retries, socket stays disconnected.</span>
2863
+ <span class="cm">// The conversation list goes empty on the next authenticated fetch (returns 401).</span>
2864
+
2865
+ <span class="cm">// Web — SocketProvider calls logout() after 5 retries (v1.2.6+)</span>
2866
+ <span class="cm">// builtInAuth: true → login screen shown automatically</span>
2867
+ <span class="cm">// builtInAuth: false → handle via your own auth flow:</span>
2868
+
2869
+ <span class="kw">import</span> { getAuthStore } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
2870
+
2871
+ <span class="kw">const</span> { useAuthStore } = <span class="fn">getAuthStore</span>();
2872
+ useAuthStore.<span class="fn">subscribe</span>((state) => {
2873
+ <span class="kw">if</span> (!state.isAuthenticated) {
2874
+ <span class="cm">// User was logged out — navigate to your own login screen</span>
2875
+ router.<span class="fn">push</span>(<span class="str">'/login'</span>);
2876
+ }
2877
+ });</code></pre>
2878
+
2879
+ <h3>Handling 401 on HTTP calls after deactivation</h3>
2880
+ <p>The axios interceptor in <code>@antzsoft/chat-core</code> already handles this — on <code>401</code> it attempts a token refresh. If the refresh also returns <code>401</code> (because refresh tokens are all revoked), <code>clearTokens()</code> is called and the error is re-thrown. Handle it at the call site:</p>
2881
+ <pre><code><span class="kw">try</span> {
2882
+ <span class="kw">const</span> { data } = <span class="kw">await</span> conversationsApi.<span class="fn">list</span>();
2883
+ } <span class="kw">catch</span> (err: <span class="kw">any</span>) {
2884
+ <span class="kw">if</span> (err?.response?.status === <span class="num">401</span>) {
2885
+ <span class="cm">// Session permanently invalid — navigate to login</span>
2886
+ router.<span class="fn">push</span>(<span class="str">'/login'</span>);
2887
+ }
2888
+ }</code></pre>
2889
+
2890
+ <h3>Conversation list after deactivation</h3>
2891
+ <p>All participant records are set to <code>isHidden: true</code> with membership periods wiped. <code>conversationsApi.list()</code> returns an empty array — your UI renders an empty state automatically with no special handling.</p>
2892
+
2893
+ <h3>On reactivation — fresh start</h3>
2894
+ <p>The reactivated user starts with an empty conversation list. They must log in again (refresh tokens were revoked). Once logged in:</p>
2895
+ <ul>
2896
+ <li>Device tokens are re-enabled automatically — push notifications resume</li>
2897
+ <li>Conversation list is empty — groups must be re-added by an admin; DMs reopen automatically when the other person sends a new message (only messages from the reopen point are visible)</li>
2898
+ <li>User search and listing immediately shows the reactivated user (cache busted on reactivation)</li>
2899
+ </ul>
2900
+
2901
+ <div class="callout tip"><strong>No client code changes required</strong> — all behaviour described here is handled by existing SDK event handlers and the HTTP interceptor. This step documents what to expect so you can test and verify each state in your app.</div>
2902
+ </section>
2903
+
2904
+ <!-- ─── STEP 6c: CLEAR / DELETE CHAT ─────────────────────────────────────── -->
2905
+ <section id="step-clear-chat">
2906
+ <h2><span class="step">STEP 6c</span> Clear / Delete Chat</h2>
2907
+
2908
+ <div class="callout info"><strong>New in v1.2.6</strong> — <code>conversationsApi.delete()</code> is now available to <strong>any participant</strong> on both DMs and group conversations, with no admin role required. It hides the conversation from the caller's list only — all other participants are completely unaffected.</div>
2909
+
2910
+ <h3>Conversation lifecycle actions</h3>
2911
+
2912
+ <table>
2913
+ <thead><tr><th>Action</th><th>API call</th><th>Caller result</th><th>Others</th></tr></thead>
2914
+ <tbody>
2915
+ <tr><td><strong>Delete Chat</strong> (DM)</td><td><code>conversationsApi.delete(id)</code></td><td>Conversation hidden, periods wiped</td><td>Unaffected</td></tr>
2916
+ <tr><td><strong>Exit Group</strong></td><td><code>conversationsApi.leave(id)</code></td><td>Inactive, stays read-only</td><td>Auto-promote if last admin</td></tr>
2917
+ <tr><td><strong>Exit and Delete</strong></td><td><code>conversationsApi.leave(id, true)</code></td><td>Left + hidden atomically, periods wiped</td><td>Auto-promote if last admin</td></tr>
2918
+ <tr><td><strong>Delete Group</strong> (post-exit)</td><td><code>conversationsApi.delete(id)</code></td><td>Already-exited group hidden</td><td>Unaffected</td></tr>
2919
+ </tbody>
2920
+ </table>
2921
+
2922
+ <h3>Clear Chat (DM)</h3>
2923
+
2924
+ <p>Calling <code>delete()</code> on a DM hides it from the caller's list and wipes their membership periods. The other participant's view is unchanged.</p>
2925
+
2926
+ <p><strong>Re-open behaviour:</strong> when the other participant sends a new message after the DM was deleted, the conversation reappears in the caller's list with only messages from the re-open point visible — no prior history leaks through (membership periods were wiped at delete time).</p>
2927
+
2928
+ <pre><code><span class="kw">import</span> { conversationsApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
2929
+
2930
+ <span class="cm">// Delete a DM from the caller's list</span>
2931
+ <span class="kw">await</span> conversationsApi.<span class="fn">delete</span>(dmConversationId);
2932
+
2933
+ <span class="cm">// Listen for the server confirmation on the caller's own sockets</span>
2934
+ socket.<span class="fn">on</span>(<span class="str">'conversation_deleted'</span>, ({ conversationId }) => {
2935
+ removeFromList(conversationId); <span class="cm">// remove from local state</span>
2936
+ <span class="kw">if</span> (activeConversationId === conversationId) {
2937
+ navigateBackToList(); <span class="cm">// navigate away if currently open</span>
2938
+ }
2939
+ });</code></pre>
2940
+
2941
+ <h3>Exit Group / Exit and Delete</h3>
2942
+
2943
+ <p><code>leave(id)</code> exits the group but keeps it in the conversation list as read-only — the caller can still read history but cannot send messages. <code>leave(id, true)</code> exits and hides it atomically in a single server write.</p>
2944
+
2945
+ <p>When the caller is the <strong>only admin</strong>, the server automatically promotes the longest-standing active member to admin before completing the exit — no client action required.</p>
2946
+
2947
+ <pre><code><span class="cm">// Exit Group — stay in list (read-only)</span>
2948
+ <span class="kw">await</span> conversationsApi.<span class="fn">leave</span>(groupId);
2949
+
2950
+ <span class="cm">// Exit and Delete — leave + hide atomically (one write, no race)</span>
2951
+ <span class="kw">await</span> conversationsApi.<span class="fn">leave</span>(groupId, <span class="kw">true</span>);
2952
+
2953
+ <span class="cm">// Delete Group — hide an already-exited group entry from the list</span>
2954
+ <span class="kw">await</span> conversationsApi.<span class="fn">delete</span>(groupId);</code></pre>
2955
+
2956
+ <h3>Message history after re-add</h3>
2957
+ <table>
2958
+ <thead><tr><th>How the user left</th><th>Re-add behaviour</th></tr></thead>
2959
+ <tbody>
2960
+ <tr><td>Plain <code>leave()</code> — was not hidden</td><td>Prior history visible from the old membership periods. New period appended.</td></tr>
2961
+ <tr><td><code>leave(true)</code> or <code>delete()</code> — periods wiped</td><td>Only messages from the re-add point onwards visible — fresh window.</td></tr>
2962
+ </tbody>
2963
+ </table>
2964
+
2965
+ <h3>Web SDK — <code>useConversations()</code> mutations</h3>
2966
+
2967
+ <pre><code><span class="kw">import</span> { useConversations } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
2968
+
2969
+ <span class="kw">const</span> {
2970
+ leaveGroup, <span class="cm">// leave(id) — exit, stay in list</span>
2971
+ leaveAndDeleteGroup, <span class="cm">// leave(id, true) — exit + hide atomically</span>
2972
+ deleteGroup, <span class="cm">// delete(id) — hide DM or already-exited group</span>
2973
+ } = <span class="fn">useConversations</span>();
2974
+
2975
+ <span class="cm">// Optimistic cache update — no manual invalidation needed</span>
2976
+ <span class="kw">await</span> leaveAndDeleteGroup.<span class="fn">mutateAsync</span>(groupId);</code></pre>
2977
+
2978
+ <h3>RN SDK — <code>useConversations()</code> mutations</h3>
2979
+
2980
+ <pre><code><span class="kw">import</span> { useConversations } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
2981
+
2982
+ <span class="kw">const</span> {
2983
+ leaveGroup, <span class="cm">// leave(id) — exit, stay in list</span>
2984
+ leaveAndDeleteGroup, <span class="cm">// leave(id, true) — exit + hide atomically</span>
2985
+ deleteConversation, <span class="cm">// delete(id) — hide DM or already-exited group</span>
2986
+ } = <span class="fn">useConversations</span>();
2987
+
2988
+ <span class="cm">// Long-press action in conversation list:</span>
2989
+ <span class="kw">await</span> deleteConversation.<span class="fn">mutateAsync</span>(dmConversationId);</code></pre>
2990
+
2991
+ <div class="callout tip"><strong>Headless / custom UI</strong> — use <code>conversationsApi.leave()</code> and <code>conversationsApi.delete()</code> directly and update your local conversation list state on the <code>conversation_deleted</code> socket event.</div>
2992
+ </section>
2993
+
2567
2994
  <!-- ─── STEP 7: ROOMS ──────────────────────────────────────────────────── -->
2568
2995
  <section id="step-rooms">
2569
2996
  <h2><span class="step">STEP 7</span> Rooms — Auto-join, <code>joinRoom</code>, and <code>new_message</code></h2>
@@ -2908,7 +3335,7 @@ socket.<span class="fn">on</span>(<span class="str">'message_deleted'</span>, (e
2908
3335
  </tr>
2909
3336
  <tr>
2910
3337
  <td><code>'conversation_deleted'</code></td>
2911
- <td>A conversation was deleted</td>
3338
+ <td>Emitted only to the acting user's own sockets on: self-delete (<code>delete()</code>), exit+delete (<code>leave(id, true)</code>), or hide-after-exit (<code>delete()</code> on an already-exited group). Other participants never receive this event.</td>
2912
3339
  <td><strong>App root / conversation list screen</strong> — remove the conversation from state and navigate away if it was open.</td>
2913
3340
  </tr>
2914
3341
  <tr>
@@ -4342,6 +4769,453 @@ console.<span class="fn">log</span>(<span class="str">`${summary.totalUnread} un
4342
4769
  </div>
4343
4770
  </section>
4344
4771
 
4772
+ <!-- ─── STEP 20: ERROR HANDLING ─────────────────────────────────────────── -->
4773
+ <section id="step-errors">
4774
+ <h2>20. Error Handling</h2>
4775
+ <p>Every error thrown by the SDK — REST calls, socket operations, transit mismatches, and queue overflows — is an instance of a typed <code>AntzChatError</code> subclass. Each carries a <strong>machine-readable <code>code</code></strong>, a <strong><code>retryable</code> boolean</strong>, and a diagnostic <strong><code>context</code> object</strong>.</p>
4776
+
4777
+ <h3>Error classes</h3>
4778
+ <table>
4779
+ <thead><tr><th>Class</th><th>Codes</th><th><code>retryable</code></th><th>Typical cause</th></tr></thead>
4780
+ <tbody>
4781
+ <tr><td><code>AntzChatAuthError</code></td><td><code>SESSION_EXPIRED</code>, <code>AUTH_FAILED</code></td><td>false</td><td>HTTP 401 — expired/missing token; refresh also failed</td></tr>
4782
+ <tr><td><code>AntzChatValidationError</code></td><td><code>VALIDATION_ERROR</code></td><td>false</td><td>HTTP 400/422 — bad input. <code>.fields</code> array for multi-field server errors</td></tr>
4783
+ <tr><td><code>AntzChatPermissionError</code></td><td><code>PERMISSION_DENIED</code></td><td>false</td><td>HTTP 403 — insufficient role</td></tr>
4784
+ <tr><td><code>AntzChatNetworkError</code></td><td><code>NETWORK_ERROR</code>, <code>RATE_LIMITED</code>, <code>SOCKET_TIMEOUT</code>, <code>SOCKET_NOT_CONNECTED</code>, <code>SEND_QUEUE_FULL</code>, <code>MESSAGE_DROPPED</code></td><td>true</td><td>Network failure, socket disconnect, queue overflow</td></tr>
4785
+ <tr><td><code>AntzChatServerError</code></td><td><code>SERVER_ERROR</code>, <code>NOT_FOUND</code></td><td>true</td><td>HTTP 5xx, 404</td></tr>
4786
+ <tr><td><code>AntzChatError</code> (base)</td><td><code>TRANSIT_MISMATCH</code>, <code>UNKNOWN_ERROR</code></td><td>false</td><td>Transit config mismatch, unknown errors</td></tr>
4787
+ </tbody>
4788
+ </table>
4789
+
4790
+ <h3>Error code reference</h3>
4791
+ <table>
4792
+ <thead><tr><th>Code</th><th>Class</th><th>When</th></tr></thead>
4793
+ <tbody>
4794
+ <tr><td><code>SESSION_EXPIRED</code></td><td><code>AntzChatAuthError</code></td><td>401 with no prior refresh attempt</td></tr>
4795
+ <tr><td><code>AUTH_FAILED</code></td><td><code>AntzChatAuthError</code></td><td>401 after token refresh also failed</td></tr>
4796
+ <tr><td><code>PERMISSION_DENIED</code></td><td><code>AntzChatPermissionError</code></td><td>403</td></tr>
4797
+ <tr><td><code>VALIDATION_ERROR</code></td><td><code>AntzChatValidationError</code></td><td>400 / 422</td></tr>
4798
+ <tr><td><code>NOT_FOUND</code></td><td><code>AntzChatServerError</code></td><td>404</td></tr>
4799
+ <tr><td><code>RATE_LIMITED</code></td><td><code>AntzChatNetworkError</code></td><td>429</td></tr>
4800
+ <tr><td><code>NETWORK_ERROR</code></td><td><code>AntzChatNetworkError</code></td><td>No response / connection failure</td></tr>
4801
+ <tr><td><code>SOCKET_TIMEOUT</code></td><td><code>AntzChatNetworkError</code></td><td>Socket ACK not received within 5 s; reconnect not achieved within 15 s</td></tr>
4802
+ <tr><td><code>SOCKET_NOT_CONNECTED</code></td><td><code>AntzChatNetworkError</code></td><td>Ack-required emit called when socket is down</td></tr>
4803
+ <tr><td><code>SEND_QUEUE_FULL</code></td><td><code>AntzChatNetworkError</code></td><td>Per-conversation queue exceeds 100 pending messages</td></tr>
4804
+ <tr><td><code>MESSAGE_DROPPED</code></td><td><code>AntzChatNetworkError</code></td><td>Message waited &gt; 30 s in queue before the socket became ready</td></tr>
4805
+ <tr><td><code>TRANSIT_MISMATCH</code></td><td><code>AntzChatError</code></td><td>SDK has <code>transitEncryption</code> set opposite to the server</td></tr>
4806
+ <tr><td><code>SERVER_ERROR</code></td><td><code>AntzChatServerError</code></td><td>5xx or any other unrecognised HTTP status</td></tr>
4807
+ </tbody>
4808
+ </table>
4809
+
4810
+ <!-- ── Import ── -->
4811
+ <h3>Import</h3>
4812
+
4813
+ <div data-p="rn">
4814
+ <pre><code><span class="kw">import</span> {
4815
+ AntzChatError,
4816
+ AntzChatAuthError,
4817
+ AntzChatValidationError,
4818
+ AntzChatNetworkError,
4819
+ AntzChatPermissionError,
4820
+ AntzChatServerError,
4821
+ } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>; <span class="cm">// re-exports everything from @antzsoft/chat-core</span></code></pre>
4822
+ </div>
4823
+
4824
+ <div data-p="web">
4825
+ <pre><code><span class="kw">import</span> {
4826
+ AntzChatError,
4827
+ AntzChatAuthError,
4828
+ AntzChatValidationError,
4829
+ AntzChatNetworkError,
4830
+ AntzChatPermissionError,
4831
+ AntzChatServerError,
4832
+ } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>; <span class="cm">// re-exports everything from @antzsoft/chat-core</span></code></pre>
4833
+ </div>
4834
+
4835
+ <div data-p="node">
4836
+ <pre><code><span class="kw">import</span> {
4837
+ AntzChatError,
4838
+ AntzChatAuthError,
4839
+ AntzChatValidationError,
4840
+ AntzChatNetworkError,
4841
+ AntzChatPermissionError,
4842
+ AntzChatServerError,
4843
+ } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;</code></pre>
4844
+ </div>
4845
+
4846
+ <!-- ── Pattern 1: instanceof ── -->
4847
+ <h3>Pattern 1 — instanceof branching</h3>
4848
+ <p>Use when each error category needs a different UI response.</p>
4849
+
4850
+ <div data-p="rn">
4851
+ <pre><code><span class="kw">import</span> { Alert } <span class="kw">from</span> <span class="str">'react-native'</span>;
4852
+ <span class="kw">import</span> {
4853
+ AntzChatAuthError, AntzChatValidationError,
4854
+ AntzChatPermissionError, AntzChatNetworkError,
4855
+ } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
4856
+
4857
+ <span class="kw">try</span> {
4858
+ <span class="kw">await</span> authApi.<span class="fn">login</span>({ email, password });
4859
+ } <span class="kw">catch</span> (err) {
4860
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatAuthError)
4861
+ Alert.<span class="fn">alert</span>(<span class="str">'Login failed'</span>, err.message);
4862
+ <span class="kw">else if</span> (err <span class="kw">instanceof</span> AntzChatValidationError)
4863
+ Alert.<span class="fn">alert</span>(<span class="str">'Invalid input'</span>, err.fields?.<span class="fn">join</span>(<span class="str">'\n'</span>) ?? err.message);
4864
+ <span class="kw">else if</span> (err <span class="kw">instanceof</span> AntzChatPermissionError)
4865
+ Alert.<span class="fn">alert</span>(<span class="str">'Not allowed'</span>, err.message);
4866
+ <span class="kw">else if</span> (err <span class="kw">instanceof</span> AntzChatNetworkError && err.retryable)
4867
+ <span class="fn">scheduleRetry</span>();
4868
+ <span class="kw">else throw</span> err;
4869
+ }</code></pre>
4870
+ </div>
4871
+
4872
+ <div data-p="web">
4873
+ <pre><code><span class="kw">import</span> {
4874
+ AntzChatAuthError, AntzChatValidationError,
4875
+ AntzChatPermissionError, AntzChatNetworkError,
4876
+ } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
4877
+
4878
+ <span class="kw">try</span> {
4879
+ <span class="kw">await</span> authApi.<span class="fn">login</span>({ email, password });
4880
+ } <span class="kw">catch</span> (err) {
4881
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatAuthError)
4882
+ setLoginError(err.message);
4883
+ <span class="kw">else if</span> (err <span class="kw">instanceof</span> AntzChatValidationError)
4884
+ err.fields?.<span class="fn">forEach</span>(msg => <span class="fn">addFormError</span>(msg));
4885
+ <span class="kw">else if</span> (err <span class="kw">instanceof</span> AntzChatPermissionError)
4886
+ toast.<span class="fn">error</span>(err.message);
4887
+ <span class="kw">else if</span> (err <span class="kw">instanceof</span> AntzChatNetworkError && err.retryable)
4888
+ <span class="fn">showOfflineBanner</span>();
4889
+ <span class="kw">else throw</span> err;
4890
+ }</code></pre>
4891
+ </div>
4892
+
4893
+ <div data-p="node">
4894
+ <pre><code><span class="kw">import</span> {
4895
+ AntzChatAuthError, AntzChatValidationError,
4896
+ AntzChatNetworkError, AntzChatServerError,
4897
+ } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
4898
+
4899
+ <span class="kw">try</span> {
4900
+ <span class="kw">await</span> client.auth.<span class="fn">login</span>({ email, password });
4901
+ } <span class="kw">catch</span> (err) {
4902
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatAuthError) {
4903
+ console.<span class="fn">error</span>(<span class="str">'Auth failed:'</span>, err.message); process.<span class="fn">exit</span>(<span class="num">1</span>);
4904
+ } <span class="kw">else if</span> (err <span class="kw">instanceof</span> AntzChatValidationError) {
4905
+ console.<span class="fn">error</span>(<span class="str">'Bad input:'</span>, err.fields ?? err.message);
4906
+ } <span class="kw">else if</span> (err <span class="kw">instanceof</span> AntzChatNetworkError && err.retryable) {
4907
+ <span class="fn">scheduleRetry</span>();
4908
+ } <span class="kw">else if</span> (err <span class="kw">instanceof</span> AntzChatServerError) {
4909
+ console.<span class="fn">error</span>(<span class="str">`Server error ${err.httpStatus}:`</span>, err.message);
4910
+ } <span class="kw">else throw</span> err;
4911
+ }</code></pre>
4912
+ </div>
4913
+
4914
+ <!-- ── Pattern 2: code switch ── -->
4915
+ <h3>Pattern 2 — code switch (fine-grained)</h3>
4916
+ <p>Use when different codes within the same class need different handling.</p>
4917
+
4918
+ <div data-p="rn">
4919
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
4920
+
4921
+ <span class="kw">try</span> {
4922
+ <span class="kw">await</span> socketEmit.<span class="fn">sendMessage</span>(payload);
4923
+ } <span class="kw">catch</span> (err) {
4924
+ <span class="kw">if</span> (!(err <span class="kw">instanceof</span> AntzChatError)) <span class="kw">throw</span> err;
4925
+ <span class="kw">switch</span> (err.code) {
4926
+ <span class="kw">case</span> <span class="str">'SESSION_EXPIRED'</span>: navigation.<span class="fn">navigate</span>(<span class="str">'Auth'</span>); <span class="kw">break</span>;
4927
+ <span class="kw">case</span> <span class="str">'SEND_QUEUE_FULL'</span>: <span class="fn">showToast</span>(<span class="str">'Too many messages in flight'</span>); <span class="kw">break</span>;
4928
+ <span class="kw">case</span> <span class="str">'MESSAGE_DROPPED'</span>: <span class="fn">showToast</span>(<span class="str">'Message timed out — try again'</span>); <span class="kw">break</span>;
4929
+ <span class="kw">case</span> <span class="str">'SOCKET_TIMEOUT'</span>:
4930
+ <span class="kw">case</span> <span class="str">'SOCKET_NOT_CONNECTED'</span>: <span class="fn">showOfflineBanner</span>(); <span class="kw">break</span>;
4931
+ <span class="kw">case</span> <span class="str">'PERMISSION_DENIED'</span>: <span class="fn">showToast</span>(<span class="str">'You cannot send here'</span>); <span class="kw">break</span>;
4932
+ <span class="kw">default</span>: console.<span class="fn">error</span>(err.code, err.message, err.context);
4933
+ }
4934
+ }</code></pre>
4935
+ </div>
4936
+
4937
+ <div data-p="web">
4938
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
4939
+
4940
+ <span class="kw">try</span> {
4941
+ <span class="kw">await</span> socketEmit.<span class="fn">sendMessage</span>(payload);
4942
+ } <span class="kw">catch</span> (err) {
4943
+ <span class="kw">if</span> (!(err <span class="kw">instanceof</span> AntzChatError)) <span class="kw">throw</span> err;
4944
+ <span class="kw">switch</span> (err.code) {
4945
+ <span class="kw">case</span> <span class="str">'SESSION_EXPIRED'</span>: router.<span class="fn">push</span>(<span class="str">'/login'</span>); <span class="kw">break</span>;
4946
+ <span class="kw">case</span> <span class="str">'SEND_QUEUE_FULL'</span>: toast.<span class="fn">warning</span>(<span class="str">'Too many messages in flight'</span>); <span class="kw">break</span>;
4947
+ <span class="kw">case</span> <span class="str">'MESSAGE_DROPPED'</span>: toast.<span class="fn">warning</span>(<span class="str">'Message timed out — try again'</span>); <span class="kw">break</span>;
4948
+ <span class="kw">case</span> <span class="str">'SOCKET_TIMEOUT'</span>:
4949
+ <span class="kw">case</span> <span class="str">'SOCKET_NOT_CONNECTED'</span>: <span class="fn">showOfflineBanner</span>(); <span class="kw">break</span>;
4950
+ <span class="kw">case</span> <span class="str">'PERMISSION_DENIED'</span>: toast.<span class="fn">error</span>(<span class="str">'You cannot send here'</span>); <span class="kw">break</span>;
4951
+ <span class="kw">default</span>: console.<span class="fn">error</span>(err.code, err.message, err.context);
4952
+ }
4953
+ }</code></pre>
4954
+ </div>
4955
+
4956
+ <div data-p="node">
4957
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
4958
+
4959
+ <span class="kw">try</span> {
4960
+ <span class="kw">await</span> client.socket.emit.<span class="fn">sendMessage</span>(payload);
4961
+ } <span class="kw">catch</span> (err) {
4962
+ <span class="kw">if</span> (!(err <span class="kw">instanceof</span> AntzChatError)) <span class="kw">throw</span> err;
4963
+ <span class="kw">switch</span> (err.code) {
4964
+ <span class="kw">case</span> <span class="str">'SESSION_EXPIRED'</span>: console.<span class="fn">error</span>(<span class="str">'Token expired — re-authenticate'</span>); <span class="kw">break</span>;
4965
+ <span class="kw">case</span> <span class="str">'SEND_QUEUE_FULL'</span>: console.<span class="fn">warn</span>(<span class="str">'Queue full — slow down sends'</span>); <span class="kw">break</span>;
4966
+ <span class="kw">case</span> <span class="str">'MESSAGE_DROPPED'</span>: console.<span class="fn">warn</span>(<span class="str">'Message dropped — socket was down'</span>); <span class="kw">break</span>;
4967
+ <span class="kw">case</span> <span class="str">'SOCKET_TIMEOUT'</span>:
4968
+ <span class="kw">case</span> <span class="str">'SOCKET_NOT_CONNECTED'</span>: console.<span class="fn">warn</span>(<span class="str">'Socket offline'</span>, err.context); <span class="kw">break</span>;
4969
+ <span class="kw">default</span>: console.<span class="fn">error</span>(err.code, err.message, err.context);
4970
+ }
4971
+ }</code></pre>
4972
+ </div>
4973
+
4974
+ <!-- ── Pattern 3: retryable ── -->
4975
+ <h3>Pattern 3 — retryable flag with back-off</h3>
4976
+ <p><code>retryable === true</code> on <code>AntzChatNetworkError</code> and <code>AntzChatServerError</code> — safe to retry automatically.</p>
4977
+
4978
+ <div data-p="rn">
4979
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
4980
+
4981
+ <span class="kw">async function</span> <span class="fn">sendWithRetry</span>(payload, maxAttempts = <span class="num">3</span>) {
4982
+ <span class="kw">for</span> (<span class="kw">let</span> attempt = <span class="num">1</span>; attempt &lt;= maxAttempts; attempt++) {
4983
+ <span class="kw">try</span> {
4984
+ <span class="kw">return await</span> socketEmit.<span class="fn">sendMessage</span>(payload);
4985
+ } <span class="kw">catch</span> (err) {
4986
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatError && err.retryable && attempt &lt; maxAttempts) {
4987
+ <span class="kw">await new</span> <span class="fn">Promise</span>(r => <span class="fn">setTimeout</span>(r, attempt * <span class="num">1500</span>));
4988
+ <span class="kw">continue</span>;
4989
+ }
4990
+ <span class="kw">throw</span> err;
4991
+ }
4992
+ }
4993
+ }</code></pre>
4994
+ </div>
4995
+
4996
+ <div data-p="web">
4997
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
4998
+
4999
+ <span class="kw">async function</span> <span class="fn">fetchWithRetry</span>&lt;T&gt;(fn: () =&gt; Promise&lt;T&gt;, maxAttempts = <span class="num">3</span>): Promise&lt;T&gt; {
5000
+ <span class="kw">for</span> (<span class="kw">let</span> i = <span class="num">1</span>; i &lt;= maxAttempts; i++) {
5001
+ <span class="kw">try</span> {
5002
+ <span class="kw">return await</span> <span class="fn">fn</span>();
5003
+ } <span class="kw">catch</span> (err) {
5004
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatError && err.retryable && i &lt; maxAttempts) {
5005
+ <span class="kw">await new</span> <span class="fn">Promise</span>(r => <span class="fn">setTimeout</span>(r, i * <span class="num">1000</span>));
5006
+ <span class="kw">continue</span>;
5007
+ }
5008
+ <span class="kw">throw</span> err;
5009
+ }
5010
+ }
5011
+ <span class="kw">throw new</span> <span class="fn">Error</span>(<span class="str">'unreachable'</span>);
5012
+ }
5013
+
5014
+ <span class="cm">// Usage</span>
5015
+ <span class="kw">const</span> messages = <span class="kw">await</span> <span class="fn">fetchWithRetry</span>(() => messagesApi.<span class="fn">list</span>(conversationId));</code></pre>
5016
+ </div>
5017
+
5018
+ <div data-p="node">
5019
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
5020
+
5021
+ <span class="kw">async function</span> <span class="fn">withRetry</span>&lt;T&gt;(fn: () =&gt; Promise&lt;T&gt;, maxAttempts = <span class="num">3</span>): Promise&lt;T&gt; {
5022
+ <span class="kw">for</span> (<span class="kw">let</span> i = <span class="num">1</span>; i &lt;= maxAttempts; i++) {
5023
+ <span class="kw">try</span> {
5024
+ <span class="kw">return await</span> <span class="fn">fn</span>();
5025
+ } <span class="kw">catch</span> (err) {
5026
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatError && err.retryable && i &lt; maxAttempts) {
5027
+ <span class="kw">await new</span> <span class="fn">Promise</span>(r => <span class="fn">setTimeout</span>(r, i * <span class="num">1000</span>));
5028
+ <span class="kw">continue</span>;
5029
+ }
5030
+ <span class="kw">throw</span> err;
5031
+ }
5032
+ }
5033
+ <span class="kw">throw new</span> <span class="fn">Error</span>(<span class="str">'unreachable'</span>);
5034
+ }
5035
+
5036
+ <span class="cm">// Usage — bot polling messages with automatic retry</span>
5037
+ <span class="kw">const</span> history = <span class="kw">await</span> <span class="fn">withRetry</span>(() => client.messages.<span class="fn">list</span>(conversationId));</code></pre>
5038
+ </div>
5039
+
5040
+ <!-- ── Pattern 4: validation fields ── -->
5041
+ <h3>Pattern 4 — <code>AntzChatValidationError</code> field errors</h3>
5042
+ <p>When the server returns a <code>string[]</code> message (class-validator errors), <code>.fields</code> is populated:</p>
5043
+
5044
+ <div data-p="rn">
5045
+ <pre><code><span class="kw">import</span> { AntzChatValidationError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
5046
+
5047
+ <span class="kw">try</span> {
5048
+ <span class="kw">await</span> authApi.<span class="fn">register</span>(payload);
5049
+ } <span class="kw">catch</span> (err) {
5050
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatValidationError) {
5051
+ <span class="cm">// err.message → "email must be a valid email; password is too short"</span>
5052
+ <span class="cm">// err.fields → ["email must be a valid email", "password is too short"]</span>
5053
+ Alert.<span class="fn">alert</span>(<span class="str">'Registration failed'</span>, err.fields?.<span class="fn">join</span>(<span class="str">'\n'</span>) ?? err.message);
5054
+ }
5055
+ }</code></pre>
5056
+ </div>
5057
+
5058
+ <div data-p="web">
5059
+ <pre><code><span class="kw">import</span> { AntzChatValidationError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
5060
+
5061
+ <span class="kw">try</span> {
5062
+ <span class="kw">await</span> authApi.<span class="fn">register</span>(payload);
5063
+ } <span class="kw">catch</span> (err) {
5064
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatValidationError) {
5065
+ <span class="cm">// err.fields → ["email must be a valid email", "password is too short"]</span>
5066
+ err.fields?.<span class="fn">forEach</span>(msg => <span class="fn">addFormError</span>(msg));
5067
+ <span class="cm">// or show a single joined message:</span>
5068
+ <span class="fn">setError</span>(err.message); <span class="cm">// "email must be a valid email; password is too short"</span>
5069
+ }
5070
+ }</code></pre>
5071
+ </div>
5072
+
5073
+ <div data-p="node">
5074
+ <pre><code><span class="kw">import</span> { AntzChatValidationError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
5075
+
5076
+ <span class="kw">try</span> {
5077
+ <span class="kw">await</span> client.auth.<span class="fn">register</span>(payload);
5078
+ } <span class="kw">catch</span> (err) {
5079
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatValidationError) {
5080
+ console.<span class="fn">error</span>(<span class="str">'Validation errors:'</span>, err.fields ?? err.message);
5081
+ <span class="cm">// err.fields → ["email must be a valid email", "password is too short"]</span>
5082
+ }
5083
+ }</code></pre>
5084
+ </div>
5085
+
5086
+ <!-- ── Pattern 5: transit mismatch ── -->
5087
+ <h3>Pattern 5 — transit mismatch on connect</h3>
5088
+ <p>Thrown before the socket is created when the SDK and server have mismatched <code>transitEncryption</code> settings:</p>
5089
+
5090
+ <div data-p="rn">
5091
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
5092
+
5093
+ <span class="kw">try</span> {
5094
+ <span class="kw">await</span> connectSocket(config, getToken);
5095
+ } <span class="kw">catch</span> (err) {
5096
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatError && err.code === <span class="str">'TRANSIT_MISMATCH'</span>) {
5097
+ <span class="cm">// err.context = { sdkEnabled: true, serverEnabled: false }</span>
5098
+ console.<span class="fn">error</span>(<span class="str">'Transit config mismatch:'</span>, err.message, err.context);
5099
+ Alert.<span class="fn">alert</span>(<span class="str">'Config error'</span>, err.message);
5100
+ }
5101
+ }</code></pre>
5102
+ </div>
5103
+
5104
+ <div data-p="web">
5105
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
5106
+
5107
+ <span class="kw">try</span> {
5108
+ <span class="kw">await</span> connectSocket(config, getToken);
5109
+ } <span class="kw">catch</span> (err) {
5110
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatError && err.code === <span class="str">'TRANSIT_MISMATCH'</span>) {
5111
+ <span class="cm">// err.context = { sdkEnabled: false, serverEnabled: true }</span>
5112
+ console.<span class="fn">error</span>(<span class="str">'Transit config mismatch:'</span>, err.message, err.context);
5113
+ }
5114
+ }</code></pre>
5115
+ </div>
5116
+
5117
+ <div data-p="node">
5118
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
5119
+
5120
+ <span class="kw">try</span> {
5121
+ <span class="kw">await</span> client.<span class="fn">connect</span>();
5122
+ } <span class="kw">catch</span> (err) {
5123
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatError && err.code === <span class="str">'TRANSIT_MISMATCH'</span>) {
5124
+ <span class="cm">// err.context = { sdkEnabled, serverEnabled }</span>
5125
+ console.<span class="fn">error</span>(<span class="str">'Transit config mismatch:'</span>, err.message, err.context);
5126
+ process.<span class="fn">exit</span>(<span class="num">1</span>);
5127
+ }
5128
+ }</code></pre>
5129
+ </div>
5130
+
5131
+ <!-- ── Logging with context ── -->
5132
+ <h3>Logging with context</h3>
5133
+ <p><code>context</code> is safe to log but never needed for control flow. When transit decryption fails for an error body, <code>context.decryptionFailed = true</code> is set and the HTTP-status-based class is still returned correctly:</p>
5134
+
5135
+ <div data-p="rn">
5136
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
5137
+
5138
+ <span class="kw">catch</span> (err) {
5139
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatError) {
5140
+ <span class="cm">// context may contain: httpStatus, path, serverError, axiosCode,</span>
5141
+ <span class="cm">// event, conversationId, decryptionFailed, sdkEnabled, serverEnabled</span>
5142
+ console.<span class="fn">error</span>(<span class="str">'[AntzChat]'</span>, err.name, err.code, err.message, err.context);
5143
+ }
5144
+ }</code></pre>
5145
+ </div>
5146
+
5147
+ <div data-p="web">
5148
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
5149
+
5150
+ <span class="kw">catch</span> (err) {
5151
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatError) {
5152
+ <span class="cm">// context may contain: httpStatus, path, serverError, axiosCode,</span>
5153
+ <span class="cm">// event, conversationId, decryptionFailed, sdkEnabled, serverEnabled</span>
5154
+ console.<span class="fn">error</span>(<span class="str">'[AntzChat]'</span>, err.name, err.code, err.message, err.context);
5155
+ }
5156
+ }</code></pre>
5157
+ </div>
5158
+
5159
+ <div data-p="node">
5160
+ <pre><code><span class="kw">import</span> { AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
5161
+
5162
+ <span class="kw">catch</span> (err) {
5163
+ <span class="kw">if</span> (err <span class="kw">instanceof</span> AntzChatError) {
5164
+ <span class="cm">// context may contain: httpStatus, path, serverError, axiosCode,</span>
5165
+ <span class="cm">// event, conversationId, decryptionFailed, sdkEnabled, serverEnabled</span>
5166
+ console.<span class="fn">error</span>(<span class="str">'[AntzChat]'</span>, err.name, err.code, err.message, err.context);
5167
+ }
5168
+ }</code></pre>
5169
+ </div>
5170
+
5171
+ <!-- ── What stays silent ── -->
5172
+ <h3>What stays silent — by design</h3>
5173
+ <table>
5174
+ <thead><tr><th>Call</th><th>Behaviour</th><th>Reason</th></tr></thead>
5175
+ <tbody>
5176
+ <tr><td><code>socketEmit.typing()</code></td><td>Silently no-ops when socket is down</td><td>Best-effort — callers must never be forced to wrap typing events</td></tr>
5177
+ <tr><td><code>socketEmit.markRead()</code></td><td>Silently no-ops when socket is down</td><td>Best-effort — read marking is not critical-path</td></tr>
5178
+ <tr><td>Transit session timeout (5 s)</td><td>SDK degrades to unencrypted mode and continues</td><td>Graceful degradation — hard-failing would break auth entirely</td></tr>
5179
+ <tr><td><code>usersApi.getPreferences()</code></td><td>Returns <code>null</code> on any error</td><td>Preferences are non-critical — defaults apply when absent</td></tr>
5180
+ </tbody>
5181
+ </table>
5182
+
5183
+ <!-- ── normalizeAxiosError ── -->
5184
+ <h3><code>normalizeAxiosError</code></h3>
5185
+ <p>If you make raw axios calls through <code>getApiClient()</code> and want the same normalisation pipeline:</p>
5186
+
5187
+ <div data-p="rn">
5188
+ <pre><code><span class="kw">import</span> { getApiClient, normalizeAxiosError, AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
5189
+
5190
+ <span class="kw">try</span> {
5191
+ <span class="kw">const</span> { data } = <span class="kw">await</span> <span class="fn">getApiClient</span>().<span class="fn">get</span>(<span class="str">'/some/endpoint'</span>);
5192
+ } <span class="kw">catch</span> (err) {
5193
+ <span class="kw">throw</span> <span class="fn">normalizeAxiosError</span>(err); <span class="cm">// always returns AntzChatError or subclass</span>
5194
+ }</code></pre>
5195
+ </div>
5196
+
5197
+ <div data-p="web">
5198
+ <pre><code><span class="kw">import</span> { getApiClient, normalizeAxiosError, AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
5199
+
5200
+ <span class="kw">try</span> {
5201
+ <span class="kw">const</span> { data } = <span class="kw">await</span> <span class="fn">getApiClient</span>().<span class="fn">get</span>(<span class="str">'/some/endpoint'</span>);
5202
+ } <span class="kw">catch</span> (err) {
5203
+ <span class="kw">throw</span> <span class="fn">normalizeAxiosError</span>(err); <span class="cm">// always returns AntzChatError or subclass</span>
5204
+ }</code></pre>
5205
+ </div>
5206
+
5207
+ <div data-p="node">
5208
+ <pre><code><span class="kw">import</span> { getApiClient, normalizeAxiosError, AntzChatError } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
5209
+
5210
+ <span class="kw">try</span> {
5211
+ <span class="kw">const</span> { data } = <span class="kw">await</span> <span class="fn">getApiClient</span>().<span class="fn">get</span>(<span class="str">'/some/endpoint'</span>);
5212
+ } <span class="kw">catch</span> (err) {
5213
+ <span class="kw">throw</span> <span class="fn">normalizeAxiosError</span>(err); <span class="cm">// always returns AntzChatError or subclass</span>
5214
+ }</code></pre>
5215
+ </div>
5216
+
5217
+ </section>
5218
+
4345
5219
  </main>
4346
5220
 
4347
5221
  <!-- ── Go-to-top FAB ── -->