Skip to content

feat: React Native SDK update for version 0.28.0#102

Open
ArnabChatterjee20k wants to merge 5 commits intomainfrom
dev
Open

feat: React Native SDK update for version 0.28.0#102
ArnabChatterjee20k wants to merge 5 commits intomainfrom
dev

Conversation

@ArnabChatterjee20k
Copy link
Copy Markdown
Member

This PR contains updates to the React Native SDK for version 0.28.0.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 20, 2026

Greptile Summary

This PR refactors the Realtime subsystem to use explicit per-subscription subscribe messages and slot-based ID tracking instead of global channel/query sets, and bumps the package version to 0.29.0 (note: the PR title says 0.28.0).

  • P1 — wrong callback on reconnect: The connected handler's shiftedSlot fallback (slot + 1) can map a server subscriptionId to the wrong client subscription, causing future events to fire the wrong callback silently (lines 545–550 of src/client.ts).

Confidence Score: 4/5

Not safe to merge until the shiftedSlot heuristic in the connected handler is resolved.

One P1 logic bug: the slot + 1 fallback in the connected message handler can silently route events to the wrong callback after a reconnect. All other open concerns are P2 (README wording, PR title mismatch) or were already flagged in prior review rounds.

src/client.ts — specifically the connected case in onMessage (lines 545–550)

Important Files Changed

Filename Overview
src/client.ts Major realtime refactor: replaced global channel/query sets with per-subscription slot tracking and explicit subscribe messages; introduces a fragile shiftedSlot heuristic in the connected handler that can map subscription IDs to wrong callbacks, plus the pre-existing window.localStorage crash on RN still present.
CHANGELOG.md Adds 0.29.0 changelog entry; PR title references 0.28.0 which is inconsistent with the actual version bump.
README.md Changes server compatibility string from 1.9.x to latest — removes concrete version information for consumers.
package.json Version bumped from 0.28.0 to 0.29.0.
package-lock.json Lockfile version field updated to match package.json 0.29.0 bump.

Reviews (4): Last reviewed commit: "chore: restore .github templates and wor..." | Re-trigger Greptile

Comment thread CHANGELOG.md

* Added `x` OAuth provider to `OAuthProvider` enum
* Added `userType` field to `Log` model
* Updated `X-Appwrite-Response-Format` header to `1.9.1`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
* Updated `X-Appwrite-Response-Format` header to `1.9.1`
* Updated `X-Appwrite-Response-Format` header to `1.9.0`

Comment thread README.md
Comment thread src/client.ts
Comment on lines +554 to +558
let session = this.config.session;
if (!session) {
const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}');
session = cookie?.[`a_session_${this.config.project}`];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment thread src/client.ts
Comment on lines +503 to +529
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
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment thread src/client.ts
Comment on lines +545 to +550
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants