feat: React Native SDK update for version 0.28.0#102
feat: React Native SDK update for version 0.28.0#102ArnabChatterjee20k wants to merge 5 commits intomainfrom
Conversation
Greptile SummaryThis PR refactors the Realtime subsystem to use explicit per-subscription
Confidence Score: 4/5Not safe to merge until the shiftedSlot heuristic in the connected handler is resolved. One P1 logic bug: the src/client.ts — specifically the Important Files Changed
Reviews (4): Last reviewed commit: "chore: restore .github templates and wor..." | Re-trigger Greptile |
|
|
||
| * Added `x` OAuth provider to `OAuthProvider` enum | ||
| * Added `userType` field to `Log` model | ||
| * Updated `X-Appwrite-Response-Format` header to `1.9.1` |
There was a problem hiding this comment.
CHANGELOG contradicts code — response format version mismatch
The changelog entry for 0.28.0 states the X-Appwrite-Response-Format header was updated to 1.9.1, but src/client.ts line 165 sets it to 1.9.0. These two are directly contradictory. Either the CHANGELOG is wrong (and should say 1.9.0) or the code was incorrectly downgraded from 1.9.1 (which was present in the main branch before this PR). Shipping with a CHANGELOG that describes a different version than the one actually sent in requests will mislead consumers about which server API version they're targeting.
| * Updated `X-Appwrite-Response-Format` header to `1.9.1` | |
| * Updated `X-Appwrite-Response-Format` header to `1.9.0` |
| let session = this.config.session; | ||
| if (!session) { | ||
| const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}'); | ||
| session = cookie?.[`a_session_${this.config.project}`]; | ||
| } |
There was a problem hiding this comment.
window.localStorage unavailable in React Native
window.localStorage is a browser-only API and is undefined in React Native's JS runtime. When this.config.session is not set (the common unauthenticated case), this code executes window.localStorage.getItem(...), which throws TypeError: Cannot read properties of undefined (reading 'getItem') and crashes the realtime 'connected' handler entirely, silently swallowed by the surrounding catch.
At minimum, guard with typeof window !== 'undefined' && window.localStorage before accessing it, or use AsyncStorage for the React Native session fallback.
Made-with: Cursor
| const rows: RealtimeRequestSubscribe[] = []; | ||
| this.realtime.pendingSubscribeSlots = []; | ||
|
|
||
| this.realtime.subscriptions.forEach((sub, slot) => { | ||
| const queries = sub.queries ?? []; | ||
|
|
||
| const row: RealtimeRequestSubscribe = { | ||
| channels: sub.channels, | ||
| queries | ||
| }; | ||
| const knownSubscriptionId = this.realtime.slotToSubscriptionId.get(slot); | ||
| if (knownSubscriptionId) { | ||
| row.subscriptionId = knownSubscriptionId; | ||
| } | ||
|
|
||
| rows.push(row); | ||
| this.realtime.pendingSubscribeSlots.push(slot); | ||
| }); | ||
|
|
||
| if (rows.length < 1) { | ||
| return; | ||
| } | ||
|
|
||
| this.realtime.socket.send(JSONbig.stringify(<RealtimeRequest>{ | ||
| type: 'subscribe', | ||
| data: rows | ||
| })); |
There was a problem hiding this comment.
pendingSubscribeSlots can be overwritten while a subscribe response is in-flight
sendSubscribeMessage resets pendingSubscribeSlots on every call. If a subscription is removed while the first subscribe message is awaiting a response, the unsubscribe path calls connect() → createSocket() → sendSubscribeMessage() immediately (because the socket is already OPEN). This overwrites pendingSubscribeSlots with the new slot set before the first 'response' message arrives.
When the first response is then processed in the 'response' handler, pendingSubscribeSlots[index] no longer corresponds to the subscriptions that were actually in the original message — the index-to-slot alignment is off. As a result, the subscriptionIdToSlot map can be written with an incorrect slot, causing events for one subscription to be delivered to a different callback until the second response arrives and overwrites the mapping.
The fix is to snapshot pendingSubscribeSlots alongside each outgoing message (e.g., a queue of [pendingSlots, messageSeq] pairs) so each response is correlated with its own set of pending slots rather than a shared mutable array.
Made-with: Cursor
| const directSlotExists = this.realtime.subscriptions.has(slot); | ||
| const shiftedSlot = slot + 1; | ||
| const shiftedSlotExists = this.realtime.subscriptions.has(shiftedSlot); | ||
| const targetSlot = directSlotExists ? slot : shiftedSlotExists ? shiftedSlot : slot; | ||
| this.realtime.slotToSubscriptionId.set(targetSlot, subscriptionId); | ||
| this.realtime.subscriptionIdToSlot.set(subscriptionId, targetSlot); |
There was a problem hiding this comment.
Fragile
shiftedSlot heuristic can map subscriptionId to the wrong callback
When the server sends a connected message with a subscriptions map (e.g., on reconnect), this code falls back to slot + 1 if the server's slot key doesn't directly match any client slot. If subscriptions.has(slot) is false but subscriptions.has(slot + 1) is true, it maps the server's subscriptionId for slot N to the client subscription at slot N+1. That client subscription is an entirely different subscription — future events routed by subscriptionId will fire the wrong callback with no error.
The fallback to slot + 1 would only be correct if the server and client always differ by exactly 1, which cannot be guaranteed, especially after deletions. If the slot doesn't exist on the client, the safest path is to continue rather than guess the neighbour.
| const directSlotExists = this.realtime.subscriptions.has(slot); | |
| const shiftedSlot = slot + 1; | |
| const shiftedSlotExists = this.realtime.subscriptions.has(shiftedSlot); | |
| const targetSlot = directSlotExists ? slot : shiftedSlotExists ? shiftedSlot : slot; | |
| this.realtime.slotToSubscriptionId.set(targetSlot, subscriptionId); | |
| this.realtime.subscriptionIdToSlot.set(subscriptionId, targetSlot); | |
| if (isNaN(slot) || typeof subscriptionId !== 'string') { | |
| continue; | |
| } | |
| if (!this.realtime.subscriptions.has(slot)) { | |
| continue; | |
| } | |
| this.realtime.slotToSubscriptionId.set(slot, subscriptionId); | |
| this.realtime.subscriptionIdToSlot.set(subscriptionId, slot); |
This PR contains updates to the React Native SDK for version 0.28.0.