diff --git a/CHANGELOG.md b/CHANGELOG.md
index c4b4d163..7492c403 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Change Log
+## 25.0.0
+
+* Breaking: Added `unsubscribe()`, `update()`, and `close()` for Realtime subscription lifecycle.
+* Added: Added `userPhone` to the `Membership` model.
+* Updated: Updated `X-Appwrite-Response-Format` header to `1.9.2`.
+
## 24.2.0
* Added `x` OAuth provider to `OAuthProvider` enum
diff --git a/README.md b/README.md
index 7764e90b..6ed4881e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Appwrite Web SDK

-
+
[](https://travis-ci.com/appwrite/sdk-generator)
[](https://twitter.com/appwrite)
[](https://appwrite.io/discord)
@@ -33,7 +33,7 @@ import { Client, Account } from "appwrite";
To install with a CDN (content delivery network) add the following scripts to the bottom of your
tag, but before you use any Appwrite services:
```html
-
+
```
diff --git a/package-lock.json b/package-lock.json
index 4bca5085..fc146255 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "appwrite",
- "version": "24.2.0",
+ "version": "25.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "appwrite",
- "version": "24.2.0",
+ "version": "25.0.0",
"license": "BSD-3-Clause",
"dependencies": {
"json-bigint": "1.0.0"
diff --git a/package.json b/package.json
index d240cf02..feea2e8e 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "appwrite",
"homepage": "https://appwrite.io/support",
"description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API",
- "version": "24.2.0",
+ "version": "25.0.0",
"license": "BSD-3-Clause",
"main": "dist/cjs/sdk.js",
"exports": {
diff --git a/src/client.ts b/src/client.ts
index ca15ae48..6bd679d2 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -1,6 +1,7 @@
import { Models } from './models';
import { Channel, ActionableChannel, ResolvedChannel } from './channel';
import { Query } from './query';
+import { ID } from './id';
import JSONbigModule from 'json-bigint';
const JSONbigParser = JSONbigModule({ storeAsString: false });
const JSONbigSerializer = JSONbigModule({ useNativeBigInt: true });
@@ -37,7 +38,7 @@ function reviver(_key: string, value: any): any {
return value;
}
-const JSONbig = {
+export const JSONbig = {
parse: (text: string) => JSONbigParser.parse(text, reviver),
stringify: JSONbigSerializer.stringify
};
@@ -76,14 +77,20 @@ type RealtimeResponse = {
*/
type RealtimeRequest = {
/**
- * Type of the request: 'authentication'.
+ * Type of the request: 'authentication' or 'subscribe'.
*/
- type: 'authentication';
+ type: 'authentication' | 'subscribe';
/**
* Data required for authentication.
*/
- data: RealtimeRequestAuthenticate;
+ data: RealtimeRequestAuthenticate | RealtimeRequestSubscribe[];
+}
+
+type RealtimeRequestSubscribe = {
+ subscriptionId: string;
+ channels: string[];
+ queries: string[];
}
/**
@@ -144,11 +151,6 @@ type RealtimeResponseConnected = {
* User object representing the connected user (optional).
*/
user?: object;
-
- /**
- * Map slot index -> subscription ID from backend (optional).
- */
- subscriptions?: Record;
}
/**
@@ -218,33 +220,18 @@ type Realtime = {
channels: Set;
/**
- * Set of query strings the client is subscribed to.
+ * Map of subscriptions keyed by client-generated subscriptionId.
*/
- queries: Set;
-
- /**
- * Map of subscriptions containing channel names and corresponding callback functions.
- */
- subscriptions: Map) => void
}>;
/**
- * Map slot index -> subscription ID (from backend, set on 'connected').
- */
- slotToSubscriptionId: Map;
-
- /**
- * Map subscription ID -> slot index (for O(1) event dispatch).
- */
- subscriptionIdToSlot: Map;
-
- /**
- * Counter for managing subscriptions.
+ * Pending subscribe rows keyed by subscriptionId. Flushed and cleared on each send.
*/
- subscriptionsCounter: number;
+ pendingSubscribes: Map;
/**
* Boolean indicating whether automatic reconnection is enabled.
@@ -276,12 +263,7 @@ type Realtime = {
*/
createHeartbeat: () => void;
- /**
- * Function to clean up resources associated with specified channels.
- *
- * @param {string[]} channels - List of channel names to clean up.
- */
- cleanUp: (channels: string[], queries: string[]) => void;
+ sendPendingSubscribes: () => void;
/**
* Function to handle incoming messages from the WebSocket connection.
@@ -398,8 +380,8 @@ class Client {
'x-sdk-name': 'Web',
'x-sdk-platform': 'client',
'x-sdk-language': 'web',
- 'x-sdk-version': '24.2.0',
- 'X-Appwrite-Response-Format': '1.9.1',
+ 'x-sdk-version': '25.0.0',
+ 'X-Appwrite-Response-Format': '1.9.2',
};
/**
@@ -575,11 +557,8 @@ class Client {
heartbeat: undefined,
url: '',
channels: new Set(),
- queries: new Set(),
subscriptions: new Map(),
- slotToSubscriptionId: new Map(),
- subscriptionIdToSlot: new Map(),
- subscriptionsCounter: 0,
+ pendingSubscribes: new Map(),
reconnect: true,
reconnectAttempts: 0,
lastMessage: undefined,
@@ -620,22 +599,8 @@ class Client {
}
const encodedProject = encodeURIComponent((this.config.project as string) ?? '');
- let queryParams = 'project=' + encodedProject;
-
- this.realtime.channels.forEach(channel => {
- queryParams += '&channels[]=' + encodeURIComponent(channel);
- });
-
- // Per-subscription queries: channel[slot][]=query so server can route events by subscription
- const selectAllQuery = Query.select(['*']).toString();
- this.realtime.subscriptions.forEach((sub, slot) => {
- const queries = sub.queries.length > 0 ? sub.queries : [selectAllQuery];
- sub.channels.forEach(channel => {
- queries.forEach(query => {
- queryParams += '&' + encodeURIComponent(channel) + '[' + slot + '][]=' + encodeURIComponent(query);
- });
- });
- });
+ // URL carries only the project; channels/queries are sent via subscribe message.
+ const queryParams = 'project=' + encodedProject;
const url = this.config.endpointRealtime + '/realtime?' + queryParams;
@@ -679,8 +644,27 @@ class Client {
this.realtime.createSocket();
}, timeout);
})
+ } else if (this.realtime.socket?.readyState === WebSocket.OPEN) {
+ this.realtime.sendPendingSubscribes();
}
},
+ sendPendingSubscribes: () => {
+ if (!this.realtime.socket || this.realtime.socket.readyState !== WebSocket.OPEN) {
+ return;
+ }
+
+ if (this.realtime.pendingSubscribes.size < 1) {
+ return;
+ }
+
+ const rows = Array.from(this.realtime.pendingSubscribes.values());
+ this.realtime.pendingSubscribes.clear();
+
+ this.realtime.socket.send(JSONbig.stringify({
+ type: 'subscribe',
+ data: rows
+ }));
+ },
onMessage: (event) => {
try {
const message: RealtimeResponse = JSONbig.parse(event.data);
@@ -688,17 +672,6 @@ class Client {
switch (message.type) {
case 'connected': {
const messageData = message.data;
- if (messageData?.subscriptions) {
- this.realtime.slotToSubscriptionId.clear();
- this.realtime.subscriptionIdToSlot.clear();
- for (const [slotStr, subscriptionId] of Object.entries(messageData.subscriptions)) {
- const slot = Number(slotStr);
- if (!isNaN(slot) && typeof subscriptionId === 'string') {
- this.realtime.slotToSubscriptionId.set(slot, subscriptionId);
- this.realtime.subscriptionIdToSlot.set(subscriptionId, slot);
- }
- }
- }
let session = this.config.session;
if (!session) {
@@ -713,8 +686,22 @@ class Client {
}
}));
}
+
+ this.realtime.subscriptions.forEach((sub, subscriptionId) => {
+ this.realtime.pendingSubscribes.set(subscriptionId, {
+ subscriptionId,
+ channels: sub.channels,
+ queries: sub.queries ?? []
+ });
+ });
+ this.realtime.sendPendingSubscribes();
break;
}
+ case 'response':
+ // The SDK generates subscriptionIds client-side and sends them on every
+ // subscribe/unsubscribe, so subscribe/unsubscribe acks carry no state
+ // the SDK needs to reconcile.
+ break;
case 'event': {
const data = >message.data;
if (!data?.channels) break;
@@ -722,12 +709,9 @@ class Client {
const eventSubIds = data.subscriptions;
if (eventSubIds && eventSubIds.length > 0) {
for (const subscriptionId of eventSubIds) {
- const slot = this.realtime.subscriptionIdToSlot.get(subscriptionId);
- if (slot !== undefined) {
- const subscription = this.realtime.subscriptions.get(slot);
- if (subscription) {
- setTimeout(() => subscription.callback(data));
- }
+ const subscription = this.realtime.subscriptions.get(subscriptionId);
+ if (subscription) {
+ setTimeout(() => subscription.callback(data));
}
}
} else {
@@ -751,31 +735,6 @@ class Client {
} catch (e) {
console.error(e);
}
- },
- cleanUp: (channels, queries) => {
- this.realtime.channels.forEach(channel => {
- if (channels.includes(channel)) {
- let found = Array.from(this.realtime.subscriptions).some(([_key, subscription] )=> {
- return subscription.channels.includes(channel);
- })
-
- if (!found) {
- this.realtime.channels.delete(channel);
- }
- }
- })
-
- this.realtime.queries.forEach(query => {
- if (queries.includes(query)) {
- let found = Array.from(this.realtime.subscriptions).some(([_key, subscription]) => {
- return subscription.queries?.includes(query);
- });
-
- if (!found) {
- this.realtime.queries.delete(query);
- }
- }
- })
}
}
@@ -835,20 +794,44 @@ class Client {
channelStrings.forEach(channel => this.realtime.channels.add(channel));
const queryStrings = (queries ?? []).map(q => typeof q === 'string' ? q : q.toString());
- queryStrings.forEach(query => this.realtime.queries.add(query));
- const counter = this.realtime.subscriptionsCounter++;
- this.realtime.subscriptions.set(counter, {
+ let subscriptionId = '';
+ const attempts = this.realtime.subscriptions.size + 1;
+ for (let i = 0; i < attempts; i++) {
+ const candidate = ID.unique();
+ if (!this.realtime.subscriptions.has(candidate)) {
+ subscriptionId = candidate;
+ break;
+ }
+ }
+ if (subscriptionId === '') {
+ throw new AppwriteException('Failed to generate unique subscription id');
+ }
+ this.realtime.subscriptions.set(subscriptionId, {
channels: channelStrings,
queries: queryStrings,
callback
});
+ this.realtime.pendingSubscribes.set(subscriptionId, {
+ subscriptionId,
+ channels: channelStrings,
+ queries: queryStrings
+ });
this.realtime.connect();
return () => {
- this.realtime.subscriptions.delete(counter);
- this.realtime.cleanUp(channelStrings, queryStrings);
+ this.realtime.subscriptions.delete(subscriptionId);
+ this.realtime.pendingSubscribes.delete(subscriptionId);
+ const stillUsed = new Set();
+ this.realtime.subscriptions.forEach(sub => {
+ sub.channels.forEach(channel => stillUsed.add(channel));
+ });
+ this.realtime.channels.forEach(channel => {
+ if (!stillUsed.has(channel)) {
+ this.realtime.channels.delete(channel);
+ }
+ });
this.realtime.connect();
}
}
diff --git a/src/models.ts b/src/models.ts
index ef424e14..a22d5b76 100644
--- a/src/models.ts
+++ b/src/models.ts
@@ -987,6 +987,10 @@ export namespace Models {
* User email address. Hide this attribute by toggling membership privacy in the Console.
*/
userEmail: string;
+ /**
+ * User phone number. Hide this attribute by toggling membership privacy in the Console.
+ */
+ userPhone: string;
/**
* Team ID.
*/
diff --git a/src/services/realtime.ts b/src/services/realtime.ts
index a33b45de..a1368b9e 100644
--- a/src/services/realtime.ts
+++ b/src/services/realtime.ts
@@ -1,8 +1,29 @@
-import { AppwriteException, Client } from '../client';
+import { AppwriteException, Client, JSONbig } from '../client';
import { Channel, ActionableChannel, ResolvedChannel } from '../channel';
import { Query } from '../query';
+import { ID } from '../id';
+
+export type RealtimeSubscriptionUpdate = {
+ channels?: (string | Channel | ActionableChannel | ResolvedChannel)[];
+ queries?: (string | Query)[];
+}
export type RealtimeSubscription = {
+ /**
+ * Remove this subscription only. Keeps the WebSocket open so other subscriptions keep receiving events.
+ * Use `Realtime.disconnect()` to close the connection entirely.
+ */
+ unsubscribe: () => Promise;
+
+ /**
+ * Replace the channels and/or queries for this subscription on the server without re-creating it.
+ */
+ update: (changes: RealtimeSubscriptionUpdate) => Promise;
+
+ /**
+ * Alias of `unsubscribe()` plus legacy auto-disconnect when this was the last active subscription.
+ * Prefer `unsubscribe()` for per-subscription teardown and `Realtime.disconnect()` for full shutdown.
+ */
close: () => Promise;
}
@@ -28,14 +49,17 @@ export type RealtimeResponseEvent = {
export type RealtimeResponseConnected = {
channels: string[];
user?: object;
- subscriptions?: { [slot: string]: string }; // Map slot index -> subscriptionId
}
export type RealtimeRequest = {
- type: 'authentication';
- data: {
- session: string;
- };
+ type: 'authentication' | 'subscribe' | 'unsubscribe';
+ data: any;
+}
+
+type RealtimeRequestSubscribeRow = {
+ subscriptionId?: string;
+ channels: string[];
+ queries: string[];
}
export enum RealtimeCode {
@@ -49,22 +73,18 @@ export class Realtime {
private readonly TYPE_EVENT = 'event';
private readonly TYPE_PONG = 'pong';
private readonly TYPE_CONNECTED = 'connected';
+ private readonly TYPE_RESPONSE = 'response';
private readonly DEBOUNCE_MS = 1;
private readonly HEARTBEAT_INTERVAL = 20000; // 20 seconds in milliseconds
private client: Client;
private socket?: WebSocket;
- // Slot-centric state: Map, queries: string[], callback: Function }>
- private activeSubscriptions = new Map>();
- // Map slot index -> subscriptionId (from backend)
- private slotToSubscriptionId = new Map();
- // Inverse map: subscriptionId -> slot index (for O(1) lookup)
- private subscriptionIdToSlot = new Map();
+ private activeSubscriptions = new Map>();
+ private pendingSubscribes = new Map();
private heartbeatTimer?: number;
private subCallDepth = 0;
private reconnectAttempts = 0;
- private subscriptionsCounter = 0;
private connectionId = 0;
private reconnect = true;
@@ -108,16 +128,16 @@ export class Realtime {
private startHeartbeat(): void {
this.stopHeartbeat();
- this.heartbeatTimer = window.setInterval(() => {
+ this.heartbeatTimer = window?.setInterval(() => {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
- this.socket.send(JSON.stringify({ type: 'ping' }));
+ this.socket.send(JSONbig.stringify({ type: 'ping' }));
}
}, this.HEARTBEAT_INTERVAL);
}
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
- window.clearInterval(this.heartbeatTimer);
+ window?.clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined;
}
}
@@ -134,39 +154,8 @@ export class Realtime {
throw new AppwriteException('Missing project ID');
}
- // Collect all unique channels from all slots
- const allChannels = new Set();
- for (const subscription of this.activeSubscriptions.values()) {
- for (const channel of subscription.channels) {
- allChannels.add(channel);
- }
- }
-
- let queryParams = `project=${projectId}`;
- for (const channel of allChannels) {
- queryParams += `&channels[]=${encodeURIComponent(channel)}`;
- }
-
- // Build query string from slots → channels → queries
- // Format: channel[slot][]=query
- // For each slot, repeat its queries under each channel it subscribes to
- // Example: slot 1 → channels [tests, prod], queries [q1, q2]
- // Produces: tests[1][]=q1&tests[1][]=q2&prod[1][]=q1&prod[1][]=q2
- const selectAllQuery = Query.select(['*']).toString();
- for (const [slot, subscription] of this.activeSubscriptions) {
- // queries is string[] - iterate over each query string
- const queries = subscription.queries.length === 0
- ? [selectAllQuery]
- : subscription.queries;
-
- // Repeat this slot's queries under each channel it subscribes to
- // Each query is sent as a separate parameter: channel[slot][]=q1&channel[slot][]=q2
- for (const channel of subscription.channels) {
- for (const query of queries) {
- queryParams += `&${encodeURIComponent(channel)}[${slot}][]=${encodeURIComponent(query)}`;
- }
- }
- }
+ // URL carries only the project; channels/queries are sent via the subscribe message.
+ const queryParams = `project=${projectId}`;
const endpoint =
this.client.config.endpointRealtime !== ''
@@ -206,7 +195,7 @@ export class Realtime {
return;
}
try {
- const message = JSON.parse(event.data) as RealtimeResponse;
+ const message = JSONbig.parse(event.data) as RealtimeResponse;
this.handleMessage(message);
} catch (error) {
console.error('Failed to parse message:', error);
@@ -293,6 +282,72 @@ export class Realtime {
return new Promise(resolve => setTimeout(resolve, ms));
}
+ private sendUnsubscribeMessage(subscriptionIds: string[]): void {
+ const ids = subscriptionIds.filter(id => typeof id === 'string' && id.length > 0);
+ if (ids.length === 0) {
+ return;
+ }
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
+ return;
+ }
+ this.socket.send(JSONbig.stringify({
+ type: 'unsubscribe',
+ data: ids.map(subscriptionId => ({ subscriptionId }))
+ }));
+ }
+
+ private generateUniqueSubscriptionId(): string {
+ const attempts = this.activeSubscriptions.size + 1;
+ for (let i = 0; i < attempts; i++) {
+ const id = ID.unique();
+ if (!this.activeSubscriptions.has(id)) {
+ return id;
+ }
+ }
+ throw new AppwriteException('Failed to generate unique subscription id');
+ }
+
+ private enqueuePendingSubscribe(subscriptionId: string): void {
+ const subscription = this.activeSubscriptions.get(subscriptionId);
+ if (!subscription) {
+ return;
+ }
+ this.pendingSubscribes.set(subscriptionId, {
+ subscriptionId,
+ channels: Array.from(subscription.channels),
+ queries: subscription.queries ?? []
+ });
+ }
+
+ /**
+ * Close the WebSocket connection and drop all active subscriptions client-side.
+ * Use this instead of calling `unsubscribe()` on every subscription when you want to tear everything down.
+ */
+ public async disconnect(): Promise {
+ this.activeSubscriptions.clear();
+ this.pendingSubscribes.clear();
+ this.reconnect = false;
+ await this.closeSocket();
+ }
+
+ private sendPendingSubscribes(): void {
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
+ return;
+ }
+
+ if (this.pendingSubscribes.size < 1) {
+ return;
+ }
+
+ const rows = Array.from(this.pendingSubscribes.values());
+ this.pendingSubscribes.clear();
+
+ this.socket.send(JSONbig.stringify({
+ type: 'subscribe',
+ data: rows
+ }));
+ }
+
/**
* Convert a channel value to a string
*
@@ -390,44 +445,92 @@ export class Realtime {
}
}
- // Allocate a new slot index
- this.subscriptionsCounter++;
- const slot = this.subscriptionsCounter;
+ const subscriptionId = this.generateUniqueSubscriptionId();
- // Store slot-centric data: channels, queries, and callback belong to the slot
- // queries is stored as string[] (array of query strings)
- // No channel mutation occurs here - channels are derived from slots in createSocket()
- this.activeSubscriptions.set(slot, {
+ this.activeSubscriptions.set(subscriptionId, {
channels,
queries: queryStrings,
callback
});
+ this.enqueuePendingSubscribe(subscriptionId);
this.subCallDepth++;
+ try {
+ await this.sleep(this.DEBOUNCE_MS);
+
+ if (this.subCallDepth === 1) {
+ if (!this.socket || this.socket.readyState > WebSocket.OPEN) {
+ await this.createSocket();
+ } else if (this.socket.readyState === WebSocket.OPEN) {
+ this.sendPendingSubscribes();
+ }
+ }
+ } finally {
+ this.subCallDepth--;
+ }
- await this.sleep(this.DEBOUNCE_MS);
+ const unsubscribe = async (): Promise => {
+ if (!this.activeSubscriptions.has(subscriptionId)) {
+ return;
+ }
+ this.activeSubscriptions.delete(subscriptionId);
+ this.pendingSubscribes.delete(subscriptionId);
+ this.sendUnsubscribeMessage([subscriptionId]);
+ };
- if (this.subCallDepth === 1) {
- await this.createSocket();
- }
+ const update = async (changes: RealtimeSubscriptionUpdate): Promise => {
+ const subscription = this.activeSubscriptions.get(subscriptionId);
+ if (!subscription) {
+ return;
+ }
- this.subCallDepth--;
+ if (changes.channels !== undefined) {
+ const nextChannelStrings = changes.channels.map(ch => this.channelToString(ch));
+ subscription.channels = new Set(nextChannelStrings);
+ }
- return {
- close: async () => {
- const subscriptionId = this.slotToSubscriptionId.get(slot);
- this.activeSubscriptions.delete(slot);
- this.slotToSubscriptionId.delete(slot);
- if (subscriptionId) {
- this.subscriptionIdToSlot.delete(subscriptionId);
+ if (changes.queries !== undefined) {
+ const nextQueries: string[] = [];
+ for (const q of changes.queries) {
+ if (Array.isArray(q)) {
+ for (const inner of q) {
+ nextQueries.push(typeof inner === 'string' ? inner : (inner as Query).toString());
+ }
+ } else {
+ nextQueries.push(typeof q === 'string' ? q : q.toString());
+ }
}
- await this.createSocket();
+ subscription.queries = nextQueries;
+ }
+
+ this.enqueuePendingSubscribe(subscriptionId);
+
+ this.subCallDepth++;
+ try {
+ await this.sleep(this.DEBOUNCE_MS);
+
+ if (this.subCallDepth === 1) {
+ if (!this.socket || this.socket.readyState > WebSocket.OPEN) {
+ await this.createSocket();
+ } else if (this.socket.readyState === WebSocket.OPEN) {
+ this.sendPendingSubscribes();
+ }
+ }
+ } finally {
+ this.subCallDepth--;
+ }
+ };
+
+ const close = async (): Promise => {
+ await unsubscribe();
+ if (this.activeSubscriptions.size === 0) {
+ this.reconnect = false;
+ await this.closeSocket();
}
};
- }
- // cleanUp is no longer needed - slots are removed directly in subscribe().close()
- // Channels are automatically rebuilt from remaining slots in createSocket()
+ return { unsubscribe, update, close };
+ }
private handleMessage(message: RealtimeResponse): void {
if (!message.type) {
@@ -447,6 +550,9 @@ export class Realtime {
case this.TYPE_PONG:
// Handle pong response if needed
break;
+ case this.TYPE_RESPONSE:
+ this.handleResponseAction(message);
+ break;
}
}
@@ -457,24 +563,10 @@ export class Realtime {
const messageData = message.data as RealtimeResponseConnected;
- // Store subscription ID mappings from backend
- // Format: { "0": "sub_a1f9", "1": "sub_b83c", ... }
- if (messageData.subscriptions) {
- this.slotToSubscriptionId.clear();
- this.subscriptionIdToSlot.clear();
- for (const [slotStr, subscriptionId] of Object.entries(messageData.subscriptions)) {
- const slot = Number(slotStr);
- if (!isNaN(slot)) {
- this.slotToSubscriptionId.set(slot, subscriptionId);
- this.subscriptionIdToSlot.set(subscriptionId, slot);
- }
- }
- }
-
let session = this.client.config.session;
if (!session) {
try {
- const cookie = JSON.parse(window.localStorage.getItem('cookieFallback') ?? '{}');
+ const cookie = JSONbig.parse(window.localStorage.getItem('cookieFallback') ?? '{}');
session = cookie?.[`a_session_${this.client.config.project}`];
} catch (error) {
console.error('Failed to parse cookie fallback:', error);
@@ -482,13 +574,18 @@ export class Realtime {
}
if (session && !messageData.user) {
- this.socket?.send(JSON.stringify({
+ this.socket?.send(JSONbig.stringify({
type: 'authentication',
data: {
session
}
}));
}
+
+ for (const subscriptionId of this.activeSubscriptions.keys()) {
+ this.enqueuePendingSubscribe(subscriptionId);
+ }
+ this.sendPendingSubscribes();
}
private handleResponseError(message: RealtimeResponse): void {
@@ -515,23 +612,24 @@ export class Realtime {
return;
}
- // Iterate over all matching subscriptionIds and call callback for each
for (const subscriptionId of subscriptions) {
- // O(1) lookup using subscriptionId
- const slot = this.subscriptionIdToSlot.get(subscriptionId);
- if (slot !== undefined) {
- const subscription = this.activeSubscriptions.get(slot);
- if (subscription) {
- const response: RealtimeResponseEvent = {
- events,
- channels,
- timestamp,
- payload,
- subscriptions
- };
- subscription.callback(response);
- }
+ const subscription = this.activeSubscriptions.get(subscriptionId);
+ if (!subscription) {
+ continue;
}
+ subscription.callback({
+ events,
+ channels,
+ timestamp,
+ payload,
+ subscriptions
+ });
}
}
+
+ private handleResponseAction(_message: RealtimeResponse): void {
+ // The SDK generates subscriptionIds client-side and sends them on every
+ // subscribe/unsubscribe, so subscribe/unsubscribe acks carry no state
+ // the SDK needs to reconcile.
+ }
}