wip:milestone 0 fixes
Some checks failed
CI/CD Pipeline / unit-tests (push) Failing after 1m16s
CI/CD Pipeline / integration-tests (push) Failing after 2m32s
CI/CD Pipeline / lint (push) Successful in 5m22s
CI/CD Pipeline / e2e-tests (push) Has been skipped
CI/CD Pipeline / build (push) Has been skipped

This commit is contained in:
2026-03-15 12:35:42 +02:00
parent 6708cf28a7
commit cffdf8af86
61266 changed files with 4511646 additions and 1938 deletions

View File

@@ -0,0 +1,370 @@
import { CHANNEL_STATES } from './lib/constants';
import Push from './lib/push';
import type RealtimeClient from './RealtimeClient';
import Timer from './lib/timer';
import RealtimePresence, { REALTIME_PRESENCE_LISTEN_EVENTS } from './RealtimePresence';
import type { RealtimePresenceJoinPayload, RealtimePresenceLeavePayload, RealtimePresenceState } from './RealtimePresence';
type ReplayOption = {
since: number;
limit?: number;
};
export type RealtimeChannelOptions = {
config: {
/**
* self option enables client to receive message it broadcast
* ack option instructs server to acknowledge that broadcast message was received
* replay option instructs server to replay broadcast messages
*/
broadcast?: {
self?: boolean;
ack?: boolean;
replay?: ReplayOption;
};
/**
* key option is used to track presence payload across clients
*/
presence?: {
key?: string;
enabled?: boolean;
};
/**
* defines if the channel is private or not and if RLS policies will be used to check data
*/
private?: boolean;
};
};
type RealtimeChangesPayloadBase = {
schema: string;
table: string;
};
type RealtimeBroadcastChangesPayloadBase = RealtimeChangesPayloadBase & {
id: string;
};
export type RealtimeBroadcastInsertPayload<T extends {
[key: string]: any;
}> = RealtimeBroadcastChangesPayloadBase & {
operation: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT}`;
record: T;
old_record: null;
};
export type RealtimeBroadcastUpdatePayload<T extends {
[key: string]: any;
}> = RealtimeBroadcastChangesPayloadBase & {
operation: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE}`;
record: T;
old_record: T;
};
export type RealtimeBroadcastDeletePayload<T extends {
[key: string]: any;
}> = RealtimeBroadcastChangesPayloadBase & {
operation: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE}`;
record: null;
old_record: T;
};
export type RealtimeBroadcastPayload<T extends {
[key: string]: any;
}> = RealtimeBroadcastInsertPayload<T> | RealtimeBroadcastUpdatePayload<T> | RealtimeBroadcastDeletePayload<T>;
type RealtimePostgresChangesPayloadBase = {
schema: string;
table: string;
commit_timestamp: string;
errors: string[];
};
export type RealtimePostgresInsertPayload<T extends {
[key: string]: any;
}> = RealtimePostgresChangesPayloadBase & {
eventType: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT}`;
new: T;
old: {};
};
export type RealtimePostgresUpdatePayload<T extends {
[key: string]: any;
}> = RealtimePostgresChangesPayloadBase & {
eventType: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE}`;
new: T;
old: Partial<T>;
};
export type RealtimePostgresDeletePayload<T extends {
[key: string]: any;
}> = RealtimePostgresChangesPayloadBase & {
eventType: `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE}`;
new: {};
old: Partial<T>;
};
export type RealtimePostgresChangesPayload<T extends {
[key: string]: any;
}> = RealtimePostgresInsertPayload<T> | RealtimePostgresUpdatePayload<T> | RealtimePostgresDeletePayload<T>;
export type RealtimePostgresChangesFilter<T extends `${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT}`> = {
/**
* The type of database change to listen to.
*/
event: T;
/**
* The database schema to listen to.
*/
schema: string;
/**
* The database table to listen to.
*/
table?: string;
/**
* Receive database changes when filter is matched.
*/
filter?: string;
};
export type RealtimeChannelSendResponse = 'ok' | 'timed out' | 'error';
export declare enum REALTIME_POSTGRES_CHANGES_LISTEN_EVENT {
ALL = "*",
INSERT = "INSERT",
UPDATE = "UPDATE",
DELETE = "DELETE"
}
export declare enum REALTIME_LISTEN_TYPES {
BROADCAST = "broadcast",
PRESENCE = "presence",
POSTGRES_CHANGES = "postgres_changes",
SYSTEM = "system"
}
export declare enum REALTIME_SUBSCRIBE_STATES {
SUBSCRIBED = "SUBSCRIBED",
TIMED_OUT = "TIMED_OUT",
CLOSED = "CLOSED",
CHANNEL_ERROR = "CHANNEL_ERROR"
}
export declare const REALTIME_CHANNEL_STATES: typeof CHANNEL_STATES;
/** A channel is the basic building block of Realtime
* and narrows the scope of data flow to subscribed clients.
* You can think of a channel as a chatroom where participants are able to see who's online
* and send and receive messages.
*/
export default class RealtimeChannel {
/** Topic name can be any string. */
topic: string;
params: RealtimeChannelOptions;
socket: RealtimeClient;
bindings: {
[key: string]: {
type: string;
filter: {
[key: string]: any;
};
callback: Function;
id?: string;
}[];
};
timeout: number;
state: CHANNEL_STATES;
joinedOnce: boolean;
joinPush: Push;
rejoinTimer: Timer;
pushBuffer: Push[];
presence: RealtimePresence;
broadcastEndpointURL: string;
subTopic: string;
private: boolean;
/**
* Creates a channel that can broadcast messages, sync presence, and listen to Postgres changes.
*
* The topic determines which realtime stream you are subscribing to. Config options let you
* enable acknowledgement for broadcasts, presence tracking, or private channels.
*
* @example
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'public-anon-key' },
* })
* const channel = new RealtimeChannel('realtime:public:messages', { config: {} }, client)
* ```
*/
constructor(
/** Topic name can be any string. */
topic: string, params: RealtimeChannelOptions | undefined, socket: RealtimeClient);
/** Subscribe registers your client with the server */
subscribe(callback?: (status: REALTIME_SUBSCRIBE_STATES, err?: Error) => void, timeout?: number): RealtimeChannel;
/**
* Returns the current presence state for this channel.
*
* The shape is a map keyed by presence key (for example a user id) where each entry contains the
* tracked metadata for that user.
*/
presenceState<T extends {
[key: string]: any;
} = {}>(): RealtimePresenceState<T>;
/**
* Sends the supplied payload to the presence tracker so other subscribers can see that this
* client is online. Use `untrack` to stop broadcasting presence for the same key.
*/
track(payload: {
[key: string]: any;
}, opts?: {
[key: string]: any;
}): Promise<RealtimeChannelSendResponse>;
/**
* Removes the current presence state for this client.
*/
untrack(opts?: {
[key: string]: any;
}): Promise<RealtimeChannelSendResponse>;
/**
* Creates an event handler that listens to changes.
*/
on(type: `${REALTIME_LISTEN_TYPES.PRESENCE}`, filter: {
event: `${REALTIME_PRESENCE_LISTEN_EVENTS.SYNC}`;
}, callback: () => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.PRESENCE}`, filter: {
event: `${REALTIME_PRESENCE_LISTEN_EVENTS.JOIN}`;
}, callback: (payload: RealtimePresenceJoinPayload<T>) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.PRESENCE}`, filter: {
event: `${REALTIME_PRESENCE_LISTEN_EVENTS.LEAVE}`;
}, callback: (payload: RealtimePresenceLeavePayload<T>) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`, filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL}`>, callback: (payload: RealtimePostgresChangesPayload<T>) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`, filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT}`>, callback: (payload: RealtimePostgresInsertPayload<T>) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`, filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE}`>, callback: (payload: RealtimePostgresUpdatePayload<T>) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`, filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE}`>, callback: (payload: RealtimePostgresDeletePayload<T>) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.POSTGRES_CHANGES}`, filter: RealtimePostgresChangesFilter<`${REALTIME_POSTGRES_CHANGES_LISTEN_EVENT}`>, callback: (payload: RealtimePostgresChangesPayload<T>) => void): RealtimeChannel;
/**
* The following is placed here to display on supabase.com/docs/reference/javascript/subscribe.
* @param type One of "broadcast", "presence", or "postgres_changes".
* @param filter Custom object specific to the Realtime feature detailing which payloads to receive.
* @param callback Function to be invoked when event handler is triggered.
*/
on(type: `${REALTIME_LISTEN_TYPES.BROADCAST}`, filter: {
event: string;
}, callback: (payload: {
type: `${REALTIME_LISTEN_TYPES.BROADCAST}`;
event: string;
meta?: {
replayed?: boolean;
id: string;
};
[key: string]: any;
}) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.BROADCAST}`, filter: {
event: string;
}, callback: (payload: {
type: `${REALTIME_LISTEN_TYPES.BROADCAST}`;
event: string;
meta?: {
replayed?: boolean;
id: string;
};
payload: T;
}) => void): RealtimeChannel;
on<T extends Record<string, unknown>>(type: `${REALTIME_LISTEN_TYPES.BROADCAST}`, filter: {
event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL;
}, callback: (payload: {
type: `${REALTIME_LISTEN_TYPES.BROADCAST}`;
event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.ALL;
payload: RealtimeBroadcastPayload<T>;
}) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.BROADCAST}`, filter: {
event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT;
}, callback: (payload: {
type: `${REALTIME_LISTEN_TYPES.BROADCAST}`;
event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.INSERT;
payload: RealtimeBroadcastInsertPayload<T>;
}) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.BROADCAST}`, filter: {
event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE;
}, callback: (payload: {
type: `${REALTIME_LISTEN_TYPES.BROADCAST}`;
event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.UPDATE;
payload: RealtimeBroadcastUpdatePayload<T>;
}) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.BROADCAST}`, filter: {
event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE;
}, callback: (payload: {
type: `${REALTIME_LISTEN_TYPES.BROADCAST}`;
event: REALTIME_POSTGRES_CHANGES_LISTEN_EVENT.DELETE;
payload: RealtimeBroadcastDeletePayload<T>;
}) => void): RealtimeChannel;
on<T extends {
[key: string]: any;
}>(type: `${REALTIME_LISTEN_TYPES.SYSTEM}`, filter: {}, callback: (payload: any) => void): RealtimeChannel;
/**
* Sends a broadcast message explicitly via REST API.
*
* This method always uses the REST API endpoint regardless of WebSocket connection state.
* Useful when you want to guarantee REST delivery or when gradually migrating from implicit REST fallback.
*
* @param event The name of the broadcast event
* @param payload Payload to be sent (required)
* @param opts Options including timeout
* @returns Promise resolving to object with success status, and error details if failed
*/
httpSend(event: string, payload: any, opts?: {
timeout?: number;
}): Promise<{
success: true;
} | {
success: false;
status: number;
error: string;
}>;
/**
* Sends a message into the channel.
*
* @param args Arguments to send to channel
* @param args.type The type of event to send
* @param args.event The name of the event being sent
* @param args.payload Payload to be sent
* @param opts Options to be used during the send process
*/
send(args: {
type: 'broadcast' | 'presence' | 'postgres_changes';
event: string;
payload?: any;
[key: string]: any;
}, opts?: {
[key: string]: any;
}): Promise<RealtimeChannelSendResponse>;
/**
* Updates the payload that will be sent the next time the channel joins (reconnects).
* Useful for rotating access tokens or updating config without re-creating the channel.
*/
updateJoinPayload(payload: {
[key: string]: any;
}): void;
/**
* Leaves the channel.
*
* Unsubscribes from server events, and instructs channel to terminate on server.
* Triggers onClose() hooks.
*
* To receive leave acknowledgements, use the a `receive` hook to bind to the server ack, ie:
* channel.unsubscribe().receive("ok", () => alert("left!") )
*/
unsubscribe(timeout?: number): Promise<'ok' | 'timed out' | 'error'>;
/**
* Teardown the channel.
*
* Destroys and stops related timers.
*/
teardown(): void;
}
export {};
//# sourceMappingURL=RealtimeChannel.d.ts.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,639 @@
import { CHANNEL_EVENTS, CHANNEL_STATES, MAX_PUSH_BUFFER_SIZE } from './lib/constants';
import Push from './lib/push';
import Timer from './lib/timer';
import RealtimePresence from './RealtimePresence';
import * as Transformers from './lib/transformers';
import { httpEndpointURL } from './lib/transformers';
export var REALTIME_POSTGRES_CHANGES_LISTEN_EVENT;
(function (REALTIME_POSTGRES_CHANGES_LISTEN_EVENT) {
REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["ALL"] = "*";
REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["INSERT"] = "INSERT";
REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["UPDATE"] = "UPDATE";
REALTIME_POSTGRES_CHANGES_LISTEN_EVENT["DELETE"] = "DELETE";
})(REALTIME_POSTGRES_CHANGES_LISTEN_EVENT || (REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = {}));
export var REALTIME_LISTEN_TYPES;
(function (REALTIME_LISTEN_TYPES) {
REALTIME_LISTEN_TYPES["BROADCAST"] = "broadcast";
REALTIME_LISTEN_TYPES["PRESENCE"] = "presence";
REALTIME_LISTEN_TYPES["POSTGRES_CHANGES"] = "postgres_changes";
REALTIME_LISTEN_TYPES["SYSTEM"] = "system";
})(REALTIME_LISTEN_TYPES || (REALTIME_LISTEN_TYPES = {}));
export var REALTIME_SUBSCRIBE_STATES;
(function (REALTIME_SUBSCRIBE_STATES) {
REALTIME_SUBSCRIBE_STATES["SUBSCRIBED"] = "SUBSCRIBED";
REALTIME_SUBSCRIBE_STATES["TIMED_OUT"] = "TIMED_OUT";
REALTIME_SUBSCRIBE_STATES["CLOSED"] = "CLOSED";
REALTIME_SUBSCRIBE_STATES["CHANNEL_ERROR"] = "CHANNEL_ERROR";
})(REALTIME_SUBSCRIBE_STATES || (REALTIME_SUBSCRIBE_STATES = {}));
export const REALTIME_CHANNEL_STATES = CHANNEL_STATES;
/** A channel is the basic building block of Realtime
* and narrows the scope of data flow to subscribed clients.
* You can think of a channel as a chatroom where participants are able to see who's online
* and send and receive messages.
*/
export default class RealtimeChannel {
/**
* Creates a channel that can broadcast messages, sync presence, and listen to Postgres changes.
*
* The topic determines which realtime stream you are subscribing to. Config options let you
* enable acknowledgement for broadcasts, presence tracking, or private channels.
*
* @example
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'public-anon-key' },
* })
* const channel = new RealtimeChannel('realtime:public:messages', { config: {} }, client)
* ```
*/
constructor(
/** Topic name can be any string. */
topic, params = { config: {} }, socket) {
var _a, _b;
this.topic = topic;
this.params = params;
this.socket = socket;
this.bindings = {};
this.state = CHANNEL_STATES.closed;
this.joinedOnce = false;
this.pushBuffer = [];
this.subTopic = topic.replace(/^realtime:/i, '');
this.params.config = Object.assign({
broadcast: { ack: false, self: false },
presence: { key: '', enabled: false },
private: false,
}, params.config);
this.timeout = this.socket.timeout;
this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout);
this.rejoinTimer = new Timer(() => this._rejoinUntilConnected(), this.socket.reconnectAfterMs);
this.joinPush.receive('ok', () => {
this.state = CHANNEL_STATES.joined;
this.rejoinTimer.reset();
this.pushBuffer.forEach((pushEvent) => pushEvent.send());
this.pushBuffer = [];
});
this._onClose(() => {
this.rejoinTimer.reset();
this.socket.log('channel', `close ${this.topic} ${this._joinRef()}`);
this.state = CHANNEL_STATES.closed;
this.socket._remove(this);
});
this._onError((reason) => {
if (this._isLeaving() || this._isClosed()) {
return;
}
this.socket.log('channel', `error ${this.topic}`, reason);
this.state = CHANNEL_STATES.errored;
this.rejoinTimer.scheduleTimeout();
});
this.joinPush.receive('timeout', () => {
if (!this._isJoining()) {
return;
}
this.socket.log('channel', `timeout ${this.topic}`, this.joinPush.timeout);
this.state = CHANNEL_STATES.errored;
this.rejoinTimer.scheduleTimeout();
});
this.joinPush.receive('error', (reason) => {
if (this._isLeaving() || this._isClosed()) {
return;
}
this.socket.log('channel', `error ${this.topic}`, reason);
this.state = CHANNEL_STATES.errored;
this.rejoinTimer.scheduleTimeout();
});
this._on(CHANNEL_EVENTS.reply, {}, (payload, ref) => {
this._trigger(this._replyEventName(ref), payload);
});
this.presence = new RealtimePresence(this);
this.broadcastEndpointURL = httpEndpointURL(this.socket.endPoint);
this.private = this.params.config.private || false;
if (!this.private && ((_b = (_a = this.params.config) === null || _a === void 0 ? void 0 : _a.broadcast) === null || _b === void 0 ? void 0 : _b.replay)) {
throw `tried to use replay on public channel '${this.topic}'. It must be a private channel.`;
}
}
/** Subscribe registers your client with the server */
subscribe(callback, timeout = this.timeout) {
var _a, _b, _c;
if (!this.socket.isConnected()) {
this.socket.connect();
}
if (this.state == CHANNEL_STATES.closed) {
const { config: { broadcast, presence, private: isPrivate }, } = this.params;
const postgres_changes = (_b = (_a = this.bindings.postgres_changes) === null || _a === void 0 ? void 0 : _a.map((r) => r.filter)) !== null && _b !== void 0 ? _b : [];
const presence_enabled = (!!this.bindings[REALTIME_LISTEN_TYPES.PRESENCE] &&
this.bindings[REALTIME_LISTEN_TYPES.PRESENCE].length > 0) ||
((_c = this.params.config.presence) === null || _c === void 0 ? void 0 : _c.enabled) === true;
const accessTokenPayload = {};
const config = {
broadcast,
presence: Object.assign(Object.assign({}, presence), { enabled: presence_enabled }),
postgres_changes,
private: isPrivate,
};
if (this.socket.accessTokenValue) {
accessTokenPayload.access_token = this.socket.accessTokenValue;
}
this._onError((e) => callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, e));
this._onClose(() => callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CLOSED));
this.updateJoinPayload(Object.assign({ config }, accessTokenPayload));
this.joinedOnce = true;
this._rejoin(timeout);
this.joinPush
.receive('ok', async ({ postgres_changes }) => {
var _a;
// Only refresh auth if using callback-based tokens
if (!this.socket._isManualToken()) {
this.socket.setAuth();
}
if (postgres_changes === undefined) {
callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED);
return;
}
else {
const clientPostgresBindings = this.bindings.postgres_changes;
const bindingsLen = (_a = clientPostgresBindings === null || clientPostgresBindings === void 0 ? void 0 : clientPostgresBindings.length) !== null && _a !== void 0 ? _a : 0;
const newPostgresBindings = [];
for (let i = 0; i < bindingsLen; i++) {
const clientPostgresBinding = clientPostgresBindings[i];
const { filter: { event, schema, table, filter }, } = clientPostgresBinding;
const serverPostgresFilter = postgres_changes && postgres_changes[i];
if (serverPostgresFilter &&
serverPostgresFilter.event === event &&
RealtimeChannel.isFilterValueEqual(serverPostgresFilter.schema, schema) &&
RealtimeChannel.isFilterValueEqual(serverPostgresFilter.table, table) &&
RealtimeChannel.isFilterValueEqual(serverPostgresFilter.filter, filter)) {
newPostgresBindings.push(Object.assign(Object.assign({}, clientPostgresBinding), { id: serverPostgresFilter.id }));
}
else {
this.unsubscribe();
this.state = CHANNEL_STATES.errored;
callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, new Error('mismatch between server and client bindings for postgres changes'));
return;
}
}
this.bindings.postgres_changes = newPostgresBindings;
callback && callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED);
return;
}
})
.receive('error', (error) => {
this.state = CHANNEL_STATES.errored;
callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, new Error(JSON.stringify(Object.values(error).join(', ') || 'error')));
return;
})
.receive('timeout', () => {
callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.TIMED_OUT);
return;
});
}
return this;
}
/**
* Returns the current presence state for this channel.
*
* The shape is a map keyed by presence key (for example a user id) where each entry contains the
* tracked metadata for that user.
*/
presenceState() {
return this.presence.state;
}
/**
* Sends the supplied payload to the presence tracker so other subscribers can see that this
* client is online. Use `untrack` to stop broadcasting presence for the same key.
*/
async track(payload, opts = {}) {
return await this.send({
type: 'presence',
event: 'track',
payload,
}, opts.timeout || this.timeout);
}
/**
* Removes the current presence state for this client.
*/
async untrack(opts = {}) {
return await this.send({
type: 'presence',
event: 'untrack',
}, opts);
}
on(type, filter, callback) {
if (this.state === CHANNEL_STATES.joined && type === REALTIME_LISTEN_TYPES.PRESENCE) {
this.socket.log('channel', `resubscribe to ${this.topic} due to change in presence callbacks on joined channel`);
this.unsubscribe().then(async () => await this.subscribe());
}
return this._on(type, filter, callback);
}
/**
* Sends a broadcast message explicitly via REST API.
*
* This method always uses the REST API endpoint regardless of WebSocket connection state.
* Useful when you want to guarantee REST delivery or when gradually migrating from implicit REST fallback.
*
* @param event The name of the broadcast event
* @param payload Payload to be sent (required)
* @param opts Options including timeout
* @returns Promise resolving to object with success status, and error details if failed
*/
async httpSend(event, payload, opts = {}) {
var _a;
if (payload === undefined || payload === null) {
return Promise.reject('Payload is required for httpSend()');
}
const headers = {
apikey: this.socket.apiKey ? this.socket.apiKey : '',
'Content-Type': 'application/json',
};
if (this.socket.accessTokenValue) {
headers['Authorization'] = `Bearer ${this.socket.accessTokenValue}`;
}
const options = {
method: 'POST',
headers,
body: JSON.stringify({
messages: [
{
topic: this.subTopic,
event,
payload: payload,
private: this.private,
},
],
}),
};
const response = await this._fetchWithTimeout(this.broadcastEndpointURL, options, (_a = opts.timeout) !== null && _a !== void 0 ? _a : this.timeout);
if (response.status === 202) {
return { success: true };
}
let errorMessage = response.statusText;
try {
const errorBody = await response.json();
errorMessage = errorBody.error || errorBody.message || errorMessage;
}
catch (_b) { }
return Promise.reject(new Error(errorMessage));
}
/**
* Sends a message into the channel.
*
* @param args Arguments to send to channel
* @param args.type The type of event to send
* @param args.event The name of the event being sent
* @param args.payload Payload to be sent
* @param opts Options to be used during the send process
*/
async send(args, opts = {}) {
var _a, _b;
if (!this._canPush() && args.type === 'broadcast') {
console.warn('Realtime send() is automatically falling back to REST API. ' +
'This behavior will be deprecated in the future. ' +
'Please use httpSend() explicitly for REST delivery.');
const { event, payload: endpoint_payload } = args;
const headers = {
apikey: this.socket.apiKey ? this.socket.apiKey : '',
'Content-Type': 'application/json',
};
if (this.socket.accessTokenValue) {
headers['Authorization'] = `Bearer ${this.socket.accessTokenValue}`;
}
const options = {
method: 'POST',
headers,
body: JSON.stringify({
messages: [
{
topic: this.subTopic,
event,
payload: endpoint_payload,
private: this.private,
},
],
}),
};
try {
const response = await this._fetchWithTimeout(this.broadcastEndpointURL, options, (_a = opts.timeout) !== null && _a !== void 0 ? _a : this.timeout);
await ((_b = response.body) === null || _b === void 0 ? void 0 : _b.cancel());
return response.ok ? 'ok' : 'error';
}
catch (error) {
if (error.name === 'AbortError') {
return 'timed out';
}
else {
return 'error';
}
}
}
else {
return new Promise((resolve) => {
var _a, _b, _c;
const push = this._push(args.type, args, opts.timeout || this.timeout);
if (args.type === 'broadcast' && !((_c = (_b = (_a = this.params) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.broadcast) === null || _c === void 0 ? void 0 : _c.ack)) {
resolve('ok');
}
push.receive('ok', () => resolve('ok'));
push.receive('error', () => resolve('error'));
push.receive('timeout', () => resolve('timed out'));
});
}
}
/**
* Updates the payload that will be sent the next time the channel joins (reconnects).
* Useful for rotating access tokens or updating config without re-creating the channel.
*/
updateJoinPayload(payload) {
this.joinPush.updatePayload(payload);
}
/**
* Leaves the channel.
*
* Unsubscribes from server events, and instructs channel to terminate on server.
* Triggers onClose() hooks.
*
* To receive leave acknowledgements, use the a `receive` hook to bind to the server ack, ie:
* channel.unsubscribe().receive("ok", () => alert("left!") )
*/
unsubscribe(timeout = this.timeout) {
this.state = CHANNEL_STATES.leaving;
const onClose = () => {
this.socket.log('channel', `leave ${this.topic}`);
this._trigger(CHANNEL_EVENTS.close, 'leave', this._joinRef());
};
this.joinPush.destroy();
let leavePush = null;
return new Promise((resolve) => {
leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);
leavePush
.receive('ok', () => {
onClose();
resolve('ok');
})
.receive('timeout', () => {
onClose();
resolve('timed out');
})
.receive('error', () => {
resolve('error');
});
leavePush.send();
if (!this._canPush()) {
leavePush.trigger('ok', {});
}
}).finally(() => {
leavePush === null || leavePush === void 0 ? void 0 : leavePush.destroy();
});
}
/**
* Teardown the channel.
*
* Destroys and stops related timers.
*/
teardown() {
this.pushBuffer.forEach((push) => push.destroy());
this.pushBuffer = [];
this.rejoinTimer.reset();
this.joinPush.destroy();
this.state = CHANNEL_STATES.closed;
this.bindings = {};
}
/** @internal */
async _fetchWithTimeout(url, options, timeout) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await this.socket.fetch(url, Object.assign(Object.assign({}, options), { signal: controller.signal }));
clearTimeout(id);
return response;
}
/** @internal */
_push(event, payload, timeout = this.timeout) {
if (!this.joinedOnce) {
throw `tried to push '${event}' to '${this.topic}' before joining. Use channel.subscribe() before pushing events`;
}
let pushEvent = new Push(this, event, payload, timeout);
if (this._canPush()) {
pushEvent.send();
}
else {
this._addToPushBuffer(pushEvent);
}
return pushEvent;
}
/** @internal */
_addToPushBuffer(pushEvent) {
pushEvent.startTimeout();
this.pushBuffer.push(pushEvent);
// Enforce buffer size limit
if (this.pushBuffer.length > MAX_PUSH_BUFFER_SIZE) {
const removedPush = this.pushBuffer.shift();
if (removedPush) {
removedPush.destroy();
this.socket.log('channel', `discarded push due to buffer overflow: ${removedPush.event}`, removedPush.payload);
}
}
}
/**
* Overridable message hook
*
* Receives all events for specialized message handling before dispatching to the channel callbacks.
* Must return the payload, modified or unmodified.
*
* @internal
*/
_onMessage(_event, payload, _ref) {
return payload;
}
/** @internal */
_isMember(topic) {
return this.topic === topic;
}
/** @internal */
_joinRef() {
return this.joinPush.ref;
}
/** @internal */
_trigger(type, payload, ref) {
var _a, _b;
const typeLower = type.toLocaleLowerCase();
const { close, error, leave, join } = CHANNEL_EVENTS;
const events = [close, error, leave, join];
if (ref && events.indexOf(typeLower) >= 0 && ref !== this._joinRef()) {
return;
}
let handledPayload = this._onMessage(typeLower, payload, ref);
if (payload && !handledPayload) {
throw 'channel onMessage callbacks must return the payload, modified or unmodified';
}
if (['insert', 'update', 'delete'].includes(typeLower)) {
(_a = this.bindings.postgres_changes) === null || _a === void 0 ? void 0 : _a.filter((bind) => {
var _a, _b, _c;
return ((_a = bind.filter) === null || _a === void 0 ? void 0 : _a.event) === '*' || ((_c = (_b = bind.filter) === null || _b === void 0 ? void 0 : _b.event) === null || _c === void 0 ? void 0 : _c.toLocaleLowerCase()) === typeLower;
}).map((bind) => bind.callback(handledPayload, ref));
}
else {
(_b = this.bindings[typeLower]) === null || _b === void 0 ? void 0 : _b.filter((bind) => {
var _a, _b, _c, _d, _e, _f;
if (['broadcast', 'presence', 'postgres_changes'].includes(typeLower)) {
if ('id' in bind) {
const bindId = bind.id;
const bindEvent = (_a = bind.filter) === null || _a === void 0 ? void 0 : _a.event;
return (bindId &&
((_b = payload.ids) === null || _b === void 0 ? void 0 : _b.includes(bindId)) &&
(bindEvent === '*' ||
(bindEvent === null || bindEvent === void 0 ? void 0 : bindEvent.toLocaleLowerCase()) === ((_c = payload.data) === null || _c === void 0 ? void 0 : _c.type.toLocaleLowerCase())));
}
else {
const bindEvent = (_e = (_d = bind === null || bind === void 0 ? void 0 : bind.filter) === null || _d === void 0 ? void 0 : _d.event) === null || _e === void 0 ? void 0 : _e.toLocaleLowerCase();
return bindEvent === '*' || bindEvent === ((_f = payload === null || payload === void 0 ? void 0 : payload.event) === null || _f === void 0 ? void 0 : _f.toLocaleLowerCase());
}
}
else {
return bind.type.toLocaleLowerCase() === typeLower;
}
}).map((bind) => {
if (typeof handledPayload === 'object' && 'ids' in handledPayload) {
const postgresChanges = handledPayload.data;
const { schema, table, commit_timestamp, type, errors } = postgresChanges;
const enrichedPayload = {
schema: schema,
table: table,
commit_timestamp: commit_timestamp,
eventType: type,
new: {},
old: {},
errors: errors,
};
handledPayload = Object.assign(Object.assign({}, enrichedPayload), this._getPayloadRecords(postgresChanges));
}
bind.callback(handledPayload, ref);
});
}
}
/** @internal */
_isClosed() {
return this.state === CHANNEL_STATES.closed;
}
/** @internal */
_isJoined() {
return this.state === CHANNEL_STATES.joined;
}
/** @internal */
_isJoining() {
return this.state === CHANNEL_STATES.joining;
}
/** @internal */
_isLeaving() {
return this.state === CHANNEL_STATES.leaving;
}
/** @internal */
_replyEventName(ref) {
return `chan_reply_${ref}`;
}
/** @internal */
_on(type, filter, callback) {
const typeLower = type.toLocaleLowerCase();
const binding = {
type: typeLower,
filter: filter,
callback: callback,
};
if (this.bindings[typeLower]) {
this.bindings[typeLower].push(binding);
}
else {
this.bindings[typeLower] = [binding];
}
return this;
}
/** @internal */
_off(type, filter) {
const typeLower = type.toLocaleLowerCase();
if (this.bindings[typeLower]) {
this.bindings[typeLower] = this.bindings[typeLower].filter((bind) => {
var _a;
return !(((_a = bind.type) === null || _a === void 0 ? void 0 : _a.toLocaleLowerCase()) === typeLower &&
RealtimeChannel.isEqual(bind.filter, filter));
});
}
return this;
}
/** @internal */
static isEqual(obj1, obj2) {
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return false;
}
for (const k in obj1) {
if (obj1[k] !== obj2[k]) {
return false;
}
}
return true;
}
/**
* Compares two optional filter values for equality.
* Treats undefined, null, and empty string as equivalent empty values.
* @internal
*/
static isFilterValueEqual(serverValue, clientValue) {
const normalizedServer = serverValue !== null && serverValue !== void 0 ? serverValue : undefined;
const normalizedClient = clientValue !== null && clientValue !== void 0 ? clientValue : undefined;
return normalizedServer === normalizedClient;
}
/** @internal */
_rejoinUntilConnected() {
this.rejoinTimer.scheduleTimeout();
if (this.socket.isConnected()) {
this._rejoin();
}
}
/**
* Registers a callback that will be executed when the channel closes.
*
* @internal
*/
_onClose(callback) {
this._on(CHANNEL_EVENTS.close, {}, callback);
}
/**
* Registers a callback that will be executed when the channel encounteres an error.
*
* @internal
*/
_onError(callback) {
this._on(CHANNEL_EVENTS.error, {}, (reason) => callback(reason));
}
/**
* Returns `true` if the socket is connected and the channel has been joined.
*
* @internal
*/
_canPush() {
return this.socket.isConnected() && this._isJoined();
}
/** @internal */
_rejoin(timeout = this.timeout) {
if (this._isLeaving()) {
return;
}
this.socket._leaveOpenTopic(this.topic);
this.state = CHANNEL_STATES.joining;
this.joinPush.resend(timeout);
}
/** @internal */
_getPayloadRecords(payload) {
const records = {
new: {},
old: {},
};
if (payload.type === 'INSERT' || payload.type === 'UPDATE') {
records.new = Transformers.convertChangeData(payload.columns, payload.record);
}
if (payload.type === 'UPDATE' || payload.type === 'DELETE') {
records.old = Transformers.convertChangeData(payload.columns, payload.old_record);
}
return records;
}
}
//# sourceMappingURL=RealtimeChannel.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,238 @@
import { WebSocketLike } from './lib/websocket-factory';
import { CONNECTION_STATE } from './lib/constants';
import Serializer from './lib/serializer';
import Timer from './lib/timer';
import RealtimeChannel from './RealtimeChannel';
import type { RealtimeChannelOptions } from './RealtimeChannel';
type Fetch = typeof fetch;
export type Channel = {
name: string;
inserted_at: string;
updated_at: string;
id: number;
};
export type LogLevel = 'info' | 'warn' | 'error';
export type RealtimeMessage = {
topic: string;
event: string;
payload: any;
ref: string;
join_ref?: string;
};
export type RealtimeRemoveChannelResponse = 'ok' | 'timed out' | 'error';
export type HeartbeatStatus = 'sent' | 'ok' | 'error' | 'timeout' | 'disconnected';
/**
* Minimal WebSocket constructor interface that RealtimeClient can work with.
* Supply a compatible implementation (native WebSocket, `ws`, etc) when running outside the browser.
*/
export interface WebSocketLikeConstructor {
new (address: string | URL, subprotocols?: string | string[] | undefined): WebSocketLike;
[key: string]: any;
}
export interface WebSocketLikeError {
error: any;
message: string;
type: string;
}
export type RealtimeClientOptions = {
transport?: WebSocketLikeConstructor;
timeout?: number;
heartbeatIntervalMs?: number;
heartbeatCallback?: (status: HeartbeatStatus, latency?: number) => void;
vsn?: string;
logger?: Function;
encode?: Function;
decode?: Function;
reconnectAfterMs?: Function;
headers?: {
[key: string]: string;
};
params?: {
[key: string]: any;
};
log_level?: LogLevel;
logLevel?: LogLevel;
fetch?: Fetch;
worker?: boolean;
workerUrl?: string;
accessToken?: () => Promise<string | null>;
};
export default class RealtimeClient {
accessTokenValue: string | null;
apiKey: string | null;
private _manuallySetToken;
channels: RealtimeChannel[];
endPoint: string;
httpEndpoint: string;
/** @deprecated headers cannot be set on websocket connections */
headers?: {
[key: string]: string;
};
params?: {
[key: string]: string;
};
timeout: number;
transport: WebSocketLikeConstructor | null;
heartbeatIntervalMs: number;
heartbeatTimer: ReturnType<typeof setInterval> | undefined;
pendingHeartbeatRef: string | null;
heartbeatCallback: (status: HeartbeatStatus, latency?: number) => void;
ref: number;
reconnectTimer: Timer | null;
vsn: string;
logger: Function;
logLevel?: LogLevel;
encode: Function;
decode: Function;
reconnectAfterMs: Function;
conn: WebSocketLike | null;
sendBuffer: Function[];
serializer: Serializer;
stateChangeCallbacks: {
open: Function[];
close: Function[];
error: Function[];
message: Function[];
};
fetch: Fetch;
accessToken: (() => Promise<string | null>) | null;
worker?: boolean;
workerUrl?: string;
workerRef?: Worker;
private _connectionState;
private _wasManualDisconnect;
private _authPromise;
private _heartbeatSentAt;
/**
* Initializes the Socket.
*
* @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (inherited host & protocol)
* @param httpEndpoint The string HTTP endpoint, ie, "https://example.com", "/" (inherited host & protocol)
* @param options.transport The Websocket Transport, for example WebSocket. This can be a custom implementation
* @param options.timeout The default timeout in milliseconds to trigger push timeouts.
* @param options.params The optional params to pass when connecting.
* @param options.headers Deprecated: headers cannot be set on websocket connections and this option will be removed in the future.
* @param options.heartbeatIntervalMs The millisec interval to send a heartbeat message.
* @param options.heartbeatCallback The optional function to handle heartbeat status and latency.
* @param options.logger The optional function for specialized logging, ie: logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
* @param options.logLevel Sets the log level for Realtime
* @param options.encode The function to encode outgoing messages. Defaults to JSON: (payload, callback) => callback(JSON.stringify(payload))
* @param options.decode The function to decode incoming messages. Defaults to Serializer's decode.
* @param options.reconnectAfterMs he optional function that returns the millsec reconnect interval. Defaults to stepped backoff off.
* @param options.worker Use Web Worker to set a side flow. Defaults to false.
* @param options.workerUrl The URL of the worker script. Defaults to https://realtime.supabase.com/worker.js that includes a heartbeat event call to keep the connection alive.
* @param options.vsn The protocol version to use when connecting. Supported versions are "1.0.0" and "2.0.0". Defaults to "2.0.0".
* @example
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'public-anon-key' },
* })
* client.connect()
* ```
*/
constructor(endPoint: string, options?: RealtimeClientOptions);
/**
* Connects the socket, unless already connected.
*/
connect(): void;
/**
* Returns the URL of the websocket.
* @returns string The URL of the websocket.
*/
endpointURL(): string;
/**
* Disconnects the socket.
*
* @param code A numeric status code to send on disconnect.
* @param reason A custom reason for the disconnect.
*/
disconnect(code?: number, reason?: string): void;
/**
* Returns all created channels
*/
getChannels(): RealtimeChannel[];
/**
* Unsubscribes and removes a single channel
* @param channel A RealtimeChannel instance
*/
removeChannel(channel: RealtimeChannel): Promise<RealtimeRemoveChannelResponse>;
/**
* Unsubscribes and removes all channels
*/
removeAllChannels(): Promise<RealtimeRemoveChannelResponse[]>;
/**
* Logs the message.
*
* For customized logging, `this.logger` can be overridden.
*/
log(kind: string, msg: string, data?: any): void;
/**
* Returns the current state of the socket.
*/
connectionState(): CONNECTION_STATE;
/**
* Returns `true` is the connection is open.
*/
isConnected(): boolean;
/**
* Returns `true` if the connection is currently connecting.
*/
isConnecting(): boolean;
/**
* Returns `true` if the connection is currently disconnecting.
*/
isDisconnecting(): boolean;
/**
* Creates (or reuses) a {@link RealtimeChannel} for the provided topic.
*
* Topics are automatically prefixed with `realtime:` to match the Realtime service.
* If a channel with the same topic already exists it will be returned instead of creating
* a duplicate connection.
*/
channel(topic: string, params?: RealtimeChannelOptions): RealtimeChannel;
/**
* Push out a message if the socket is connected.
*
* If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established.
*/
push(data: RealtimeMessage): void;
/**
* Sets the JWT access token used for channel subscription authorization and Realtime RLS.
*
* If param is null it will use the `accessToken` callback function or the token set on the client.
*
* On callback used, it will set the value of the token internal to the client.
*
* When a token is explicitly provided, it will be preserved across channel operations
* (including removeChannel and resubscribe). The `accessToken` callback will not be
* invoked until `setAuth()` is called without arguments.
*
* @param token A JWT string to override the token set on the client.
*
* @example
* // Use a manual token (preserved across resubscribes, ignores accessToken callback)
* client.realtime.setAuth('my-custom-jwt')
*
* // Switch back to using the accessToken callback
* client.realtime.setAuth()
*/
setAuth(token?: string | null): Promise<void>;
/**
* Sends a heartbeat message if the socket is connected.
*/
sendHeartbeat(): Promise<void>;
/**
* Sets a callback that receives lifecycle events for internal heartbeat messages.
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
*/
onHeartbeat(callback: (status: HeartbeatStatus, latency?: number) => void): void;
/**
* Flushes send buffer
*/
flushSendBuffer(): void;
private _workerObjectUrl;
}
export {};
//# sourceMappingURL=RealtimeClient.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"RealtimeClient.d.ts","sourceRoot":"","sources":["../../src/RealtimeClient.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,866 @@
import WebSocketFactory from './lib/websocket-factory';
import { CHANNEL_EVENTS, CONNECTION_STATE, DEFAULT_VERSION, DEFAULT_TIMEOUT, SOCKET_STATES, TRANSPORTS, DEFAULT_VSN, VSN_1_0_0, VSN_2_0_0, WS_CLOSE_NORMAL, } from './lib/constants';
import Serializer from './lib/serializer';
import Timer from './lib/timer';
import { httpEndpointURL } from './lib/transformers';
import RealtimeChannel from './RealtimeChannel';
const noop = () => { };
// Connection-related constants
const CONNECTION_TIMEOUTS = {
HEARTBEAT_INTERVAL: 25000,
RECONNECT_DELAY: 10,
HEARTBEAT_TIMEOUT_FALLBACK: 100,
};
const RECONNECT_INTERVALS = [1000, 2000, 5000, 10000];
const DEFAULT_RECONNECT_FALLBACK = 10000;
const WORKER_SCRIPT = `
addEventListener("message", (e) => {
if (e.data.event === "start") {
setInterval(() => postMessage({ event: "keepAlive" }), e.data.interval);
}
});`;
export default class RealtimeClient {
/**
* Initializes the Socket.
*
* @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (inherited host & protocol)
* @param httpEndpoint The string HTTP endpoint, ie, "https://example.com", "/" (inherited host & protocol)
* @param options.transport The Websocket Transport, for example WebSocket. This can be a custom implementation
* @param options.timeout The default timeout in milliseconds to trigger push timeouts.
* @param options.params The optional params to pass when connecting.
* @param options.headers Deprecated: headers cannot be set on websocket connections and this option will be removed in the future.
* @param options.heartbeatIntervalMs The millisec interval to send a heartbeat message.
* @param options.heartbeatCallback The optional function to handle heartbeat status and latency.
* @param options.logger The optional function for specialized logging, ie: logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
* @param options.logLevel Sets the log level for Realtime
* @param options.encode The function to encode outgoing messages. Defaults to JSON: (payload, callback) => callback(JSON.stringify(payload))
* @param options.decode The function to decode incoming messages. Defaults to Serializer's decode.
* @param options.reconnectAfterMs he optional function that returns the millsec reconnect interval. Defaults to stepped backoff off.
* @param options.worker Use Web Worker to set a side flow. Defaults to false.
* @param options.workerUrl The URL of the worker script. Defaults to https://realtime.supabase.com/worker.js that includes a heartbeat event call to keep the connection alive.
* @param options.vsn The protocol version to use when connecting. Supported versions are "1.0.0" and "2.0.0". Defaults to "2.0.0".
* @example
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'public-anon-key' },
* })
* client.connect()
* ```
*/
constructor(endPoint, options) {
var _a;
this.accessTokenValue = null;
this.apiKey = null;
this._manuallySetToken = false;
this.channels = new Array();
this.endPoint = '';
this.httpEndpoint = '';
/** @deprecated headers cannot be set on websocket connections */
this.headers = {};
this.params = {};
this.timeout = DEFAULT_TIMEOUT;
this.transport = null;
this.heartbeatIntervalMs = CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL;
this.heartbeatTimer = undefined;
this.pendingHeartbeatRef = null;
this.heartbeatCallback = noop;
this.ref = 0;
this.reconnectTimer = null;
this.vsn = DEFAULT_VSN;
this.logger = noop;
this.conn = null;
this.sendBuffer = [];
this.serializer = new Serializer();
this.stateChangeCallbacks = {
open: [],
close: [],
error: [],
message: [],
};
this.accessToken = null;
this._connectionState = 'disconnected';
this._wasManualDisconnect = false;
this._authPromise = null;
this._heartbeatSentAt = null;
/**
* Use either custom fetch, if provided, or default fetch to make HTTP requests
*
* @internal
*/
this._resolveFetch = (customFetch) => {
if (customFetch) {
return (...args) => customFetch(...args);
}
return (...args) => fetch(...args);
};
// Validate required parameters
if (!((_a = options === null || options === void 0 ? void 0 : options.params) === null || _a === void 0 ? void 0 : _a.apikey)) {
throw new Error('API key is required to connect to Realtime');
}
this.apiKey = options.params.apikey;
// Initialize endpoint URLs
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`;
this.httpEndpoint = httpEndpointURL(endPoint);
this._initializeOptions(options);
this._setupReconnectionTimer();
this.fetch = this._resolveFetch(options === null || options === void 0 ? void 0 : options.fetch);
}
/**
* Connects the socket, unless already connected.
*/
connect() {
// Skip if already connecting, disconnecting, or connected
if (this.isConnecting() ||
this.isDisconnecting() ||
(this.conn !== null && this.isConnected())) {
return;
}
this._setConnectionState('connecting');
// Trigger auth if needed and not already in progress
// This ensures auth is called for standalone RealtimeClient usage
// while avoiding race conditions with SupabaseClient's immediate setAuth call
if (this.accessToken && !this._authPromise) {
this._setAuthSafely('connect');
}
// Establish WebSocket connection
if (this.transport) {
// Use custom transport if provided
this.conn = new this.transport(this.endpointURL());
}
else {
// Try to use native WebSocket
try {
this.conn = WebSocketFactory.createWebSocket(this.endpointURL());
}
catch (error) {
this._setConnectionState('disconnected');
const errorMessage = error.message;
// Provide helpful error message based on environment
if (errorMessage.includes('Node.js')) {
throw new Error(`${errorMessage}\n\n` +
'To use Realtime in Node.js, you need to provide a WebSocket implementation:\n\n' +
'Option 1: Use Node.js 22+ which has native WebSocket support\n' +
'Option 2: Install and provide the "ws" package:\n\n' +
' npm install ws\n\n' +
' import ws from "ws"\n' +
' const client = new RealtimeClient(url, {\n' +
' ...options,\n' +
' transport: ws\n' +
' })');
}
throw new Error(`WebSocket not available: ${errorMessage}`);
}
}
this._setupConnectionHandlers();
}
/**
* Returns the URL of the websocket.
* @returns string The URL of the websocket.
*/
endpointURL() {
return this._appendParams(this.endPoint, Object.assign({}, this.params, { vsn: this.vsn }));
}
/**
* Disconnects the socket.
*
* @param code A numeric status code to send on disconnect.
* @param reason A custom reason for the disconnect.
*/
disconnect(code, reason) {
if (this.isDisconnecting()) {
return;
}
this._setConnectionState('disconnecting', true);
if (this.conn) {
// Setup fallback timer to prevent hanging in disconnecting state
const fallbackTimer = setTimeout(() => {
this._setConnectionState('disconnected');
}, 100);
this.conn.onclose = () => {
clearTimeout(fallbackTimer);
this._setConnectionState('disconnected');
};
// Close the WebSocket connection if close method exists
if (typeof this.conn.close === 'function') {
if (code) {
this.conn.close(code, reason !== null && reason !== void 0 ? reason : '');
}
else {
this.conn.close();
}
}
this._teardownConnection();
}
else {
this._setConnectionState('disconnected');
}
}
/**
* Returns all created channels
*/
getChannels() {
return this.channels;
}
/**
* Unsubscribes and removes a single channel
* @param channel A RealtimeChannel instance
*/
async removeChannel(channel) {
const status = await channel.unsubscribe();
if (this.channels.length === 0) {
this.disconnect();
}
return status;
}
/**
* Unsubscribes and removes all channels
*/
async removeAllChannels() {
const values_1 = await Promise.all(this.channels.map((channel) => channel.unsubscribe()));
this.channels = [];
this.disconnect();
return values_1;
}
/**
* Logs the message.
*
* For customized logging, `this.logger` can be overridden.
*/
log(kind, msg, data) {
this.logger(kind, msg, data);
}
/**
* Returns the current state of the socket.
*/
connectionState() {
switch (this.conn && this.conn.readyState) {
case SOCKET_STATES.connecting:
return CONNECTION_STATE.Connecting;
case SOCKET_STATES.open:
return CONNECTION_STATE.Open;
case SOCKET_STATES.closing:
return CONNECTION_STATE.Closing;
default:
return CONNECTION_STATE.Closed;
}
}
/**
* Returns `true` is the connection is open.
*/
isConnected() {
return this.connectionState() === CONNECTION_STATE.Open;
}
/**
* Returns `true` if the connection is currently connecting.
*/
isConnecting() {
return this._connectionState === 'connecting';
}
/**
* Returns `true` if the connection is currently disconnecting.
*/
isDisconnecting() {
return this._connectionState === 'disconnecting';
}
/**
* Creates (or reuses) a {@link RealtimeChannel} for the provided topic.
*
* Topics are automatically prefixed with `realtime:` to match the Realtime service.
* If a channel with the same topic already exists it will be returned instead of creating
* a duplicate connection.
*/
channel(topic, params = { config: {} }) {
const realtimeTopic = `realtime:${topic}`;
const exists = this.getChannels().find((c) => c.topic === realtimeTopic);
if (!exists) {
const chan = new RealtimeChannel(`realtime:${topic}`, params, this);
this.channels.push(chan);
return chan;
}
else {
return exists;
}
}
/**
* Push out a message if the socket is connected.
*
* If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established.
*/
push(data) {
const { topic, event, payload, ref } = data;
const callback = () => {
this.encode(data, (result) => {
var _a;
(_a = this.conn) === null || _a === void 0 ? void 0 : _a.send(result);
});
};
this.log('push', `${topic} ${event} (${ref})`, payload);
if (this.isConnected()) {
callback();
}
else {
this.sendBuffer.push(callback);
}
}
/**
* Sets the JWT access token used for channel subscription authorization and Realtime RLS.
*
* If param is null it will use the `accessToken` callback function or the token set on the client.
*
* On callback used, it will set the value of the token internal to the client.
*
* When a token is explicitly provided, it will be preserved across channel operations
* (including removeChannel and resubscribe). The `accessToken` callback will not be
* invoked until `setAuth()` is called without arguments.
*
* @param token A JWT string to override the token set on the client.
*
* @example
* // Use a manual token (preserved across resubscribes, ignores accessToken callback)
* client.realtime.setAuth('my-custom-jwt')
*
* // Switch back to using the accessToken callback
* client.realtime.setAuth()
*/
async setAuth(token = null) {
this._authPromise = this._performAuth(token);
try {
await this._authPromise;
}
finally {
this._authPromise = null;
}
}
/**
* Returns true if the current access token was explicitly set via setAuth(token),
* false if it was obtained via the accessToken callback.
* @internal
*/
_isManualToken() {
return this._manuallySetToken;
}
/**
* Sends a heartbeat message if the socket is connected.
*/
async sendHeartbeat() {
var _a;
if (!this.isConnected()) {
try {
this.heartbeatCallback('disconnected');
}
catch (e) {
this.log('error', 'error in heartbeat callback', e);
}
return;
}
// Handle heartbeat timeout and force reconnection if needed
if (this.pendingHeartbeatRef) {
this.pendingHeartbeatRef = null;
this._heartbeatSentAt = null;
this.log('transport', 'heartbeat timeout. Attempting to re-establish connection');
try {
this.heartbeatCallback('timeout');
}
catch (e) {
this.log('error', 'error in heartbeat callback', e);
}
// Force reconnection after heartbeat timeout
this._wasManualDisconnect = false;
(_a = this.conn) === null || _a === void 0 ? void 0 : _a.close(WS_CLOSE_NORMAL, 'heartbeat timeout');
setTimeout(() => {
var _a;
if (!this.isConnected()) {
(_a = this.reconnectTimer) === null || _a === void 0 ? void 0 : _a.scheduleTimeout();
}
}, CONNECTION_TIMEOUTS.HEARTBEAT_TIMEOUT_FALLBACK);
return;
}
// Send heartbeat message to server
this._heartbeatSentAt = Date.now();
this.pendingHeartbeatRef = this._makeRef();
this.push({
topic: 'phoenix',
event: 'heartbeat',
payload: {},
ref: this.pendingHeartbeatRef,
});
try {
this.heartbeatCallback('sent');
}
catch (e) {
this.log('error', 'error in heartbeat callback', e);
}
this._setAuthSafely('heartbeat');
}
/**
* Sets a callback that receives lifecycle events for internal heartbeat messages.
* Useful for instrumenting connection health (e.g. sent/ok/timeout/disconnected).
*/
onHeartbeat(callback) {
this.heartbeatCallback = callback;
}
/**
* Flushes send buffer
*/
flushSendBuffer() {
if (this.isConnected() && this.sendBuffer.length > 0) {
this.sendBuffer.forEach((callback) => callback());
this.sendBuffer = [];
}
}
/**
* Return the next message ref, accounting for overflows
*
* @internal
*/
_makeRef() {
let newRef = this.ref + 1;
if (newRef === this.ref) {
this.ref = 0;
}
else {
this.ref = newRef;
}
return this.ref.toString();
}
/**
* Unsubscribe from channels with the specified topic.
*
* @internal
*/
_leaveOpenTopic(topic) {
let dupChannel = this.channels.find((c) => c.topic === topic && (c._isJoined() || c._isJoining()));
if (dupChannel) {
this.log('transport', `leaving duplicate topic "${topic}"`);
dupChannel.unsubscribe();
}
}
/**
* Removes a subscription from the socket.
*
* @param channel An open subscription.
*
* @internal
*/
_remove(channel) {
this.channels = this.channels.filter((c) => c.topic !== channel.topic);
}
/** @internal */
_onConnMessage(rawMessage) {
this.decode(rawMessage.data, (msg) => {
// Handle heartbeat responses
if (msg.topic === 'phoenix' &&
msg.event === 'phx_reply' &&
msg.ref &&
msg.ref === this.pendingHeartbeatRef) {
const latency = this._heartbeatSentAt ? Date.now() - this._heartbeatSentAt : undefined;
try {
this.heartbeatCallback(msg.payload.status === 'ok' ? 'ok' : 'error', latency);
}
catch (e) {
this.log('error', 'error in heartbeat callback', e);
}
this._heartbeatSentAt = null;
this.pendingHeartbeatRef = null;
}
// Log incoming message
const { topic, event, payload, ref } = msg;
const refString = ref ? `(${ref})` : '';
const status = payload.status || '';
this.log('receive', `${status} ${topic} ${event} ${refString}`.trim(), payload);
// Route message to appropriate channels
this.channels
.filter((channel) => channel._isMember(topic))
.forEach((channel) => channel._trigger(event, payload, ref));
this._triggerStateCallbacks('message', msg);
});
}
/**
* Clear specific timer
* @internal
*/
_clearTimer(timer) {
var _a;
if (timer === 'heartbeat' && this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined;
}
else if (timer === 'reconnect') {
(_a = this.reconnectTimer) === null || _a === void 0 ? void 0 : _a.reset();
}
}
/**
* Clear all timers
* @internal
*/
_clearAllTimers() {
this._clearTimer('heartbeat');
this._clearTimer('reconnect');
}
/**
* Setup connection handlers for WebSocket events
* @internal
*/
_setupConnectionHandlers() {
if (!this.conn)
return;
// Set binary type if supported (browsers and most WebSocket implementations)
if ('binaryType' in this.conn) {
;
this.conn.binaryType = 'arraybuffer';
}
this.conn.onopen = () => this._onConnOpen();
this.conn.onerror = (error) => this._onConnError(error);
this.conn.onmessage = (event) => this._onConnMessage(event);
this.conn.onclose = (event) => this._onConnClose(event);
if (this.conn.readyState === SOCKET_STATES.open) {
this._onConnOpen();
}
}
/**
* Teardown connection and cleanup resources
* @internal
*/
_teardownConnection() {
if (this.conn) {
if (this.conn.readyState === SOCKET_STATES.open ||
this.conn.readyState === SOCKET_STATES.connecting) {
try {
this.conn.close();
}
catch (e) {
this.log('error', 'Error closing connection', e);
}
}
this.conn.onopen = null;
this.conn.onerror = null;
this.conn.onmessage = null;
this.conn.onclose = null;
this.conn = null;
}
this._clearAllTimers();
this._terminateWorker();
this.channels.forEach((channel) => channel.teardown());
}
/** @internal */
_onConnOpen() {
this._setConnectionState('connected');
this.log('transport', `connected to ${this.endpointURL()}`);
// Wait for any pending auth operations before flushing send buffer
// This ensures channel join messages include the correct access token
const authPromise = this._authPromise ||
(this.accessToken && !this.accessTokenValue ? this.setAuth() : Promise.resolve());
authPromise
.then(() => {
// When subscribe() is called before the accessToken callback has
// resolved (common on React Native / Expo where token storage is
// async), the phx_join payload captured at subscribe()-time will
// have no access_token. By this point auth has settled and
// this.accessTokenValue holds the real JWT.
//
// The stale join messages sitting in sendBuffer captured the old
// (token-less) payload in a closure, so we cannot simply flush
// them. Instead we:
// 1. Patch each channel's joinPush payload with the real token
// 2. Drop the stale buffered messages
// 3. Re-send the join for any channel still in "joining" state
//
// On browsers this is a harmless no-op: accessTokenValue was
// already set synchronously before subscribe() ran, so the join
// payload already had the correct token.
if (this.accessTokenValue) {
this.channels.forEach((channel) => {
channel.updateJoinPayload({ access_token: this.accessTokenValue });
});
this.sendBuffer = [];
this.channels.forEach((channel) => {
if (channel._isJoining()) {
channel.joinPush.sent = false;
channel.joinPush.send();
}
});
}
this.flushSendBuffer();
})
.catch((e) => {
this.log('error', 'error waiting for auth on connect', e);
// Proceed anyway to avoid hanging connections
this.flushSendBuffer();
});
this._clearTimer('reconnect');
if (!this.worker) {
this._startHeartbeat();
}
else {
if (!this.workerRef) {
this._startWorkerHeartbeat();
}
}
this._triggerStateCallbacks('open');
}
/** @internal */
_startHeartbeat() {
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs);
}
/** @internal */
_startWorkerHeartbeat() {
if (this.workerUrl) {
this.log('worker', `starting worker for from ${this.workerUrl}`);
}
else {
this.log('worker', `starting default worker`);
}
const objectUrl = this._workerObjectUrl(this.workerUrl);
this.workerRef = new Worker(objectUrl);
this.workerRef.onerror = (error) => {
this.log('worker', 'worker error', error.message);
this._terminateWorker();
};
this.workerRef.onmessage = (event) => {
if (event.data.event === 'keepAlive') {
this.sendHeartbeat();
}
};
this.workerRef.postMessage({
event: 'start',
interval: this.heartbeatIntervalMs,
});
}
/**
* Terminate the Web Worker and clear the reference
* @internal
*/
_terminateWorker() {
if (this.workerRef) {
this.log('worker', 'terminating worker');
this.workerRef.terminate();
this.workerRef = undefined;
}
}
/** @internal */
_onConnClose(event) {
var _a;
this._setConnectionState('disconnected');
this.log('transport', 'close', event);
this._triggerChanError();
this._clearTimer('heartbeat');
// Only schedule reconnection if it wasn't a manual disconnect
if (!this._wasManualDisconnect) {
(_a = this.reconnectTimer) === null || _a === void 0 ? void 0 : _a.scheduleTimeout();
}
this._triggerStateCallbacks('close', event);
}
/** @internal */
_onConnError(error) {
this._setConnectionState('disconnected');
this.log('transport', `${error}`);
this._triggerChanError();
this._triggerStateCallbacks('error', error);
try {
this.heartbeatCallback('error');
}
catch (e) {
this.log('error', 'error in heartbeat callback', e);
}
}
/** @internal */
_triggerChanError() {
this.channels.forEach((channel) => channel._trigger(CHANNEL_EVENTS.error));
}
/** @internal */
_appendParams(url, params) {
if (Object.keys(params).length === 0) {
return url;
}
const prefix = url.match(/\?/) ? '&' : '?';
const query = new URLSearchParams(params);
return `${url}${prefix}${query}`;
}
_workerObjectUrl(url) {
let result_url;
if (url) {
result_url = url;
}
else {
const blob = new Blob([WORKER_SCRIPT], { type: 'application/javascript' });
result_url = URL.createObjectURL(blob);
}
return result_url;
}
/**
* Set connection state with proper state management
* @internal
*/
_setConnectionState(state, manual = false) {
this._connectionState = state;
if (state === 'connecting') {
this._wasManualDisconnect = false;
}
else if (state === 'disconnecting') {
this._wasManualDisconnect = manual;
}
}
/**
* Perform the actual auth operation
* @internal
*/
async _performAuth(token = null) {
let tokenToSend;
let isManualToken = false;
if (token) {
tokenToSend = token;
// Track if this is a manually-provided token
isManualToken = true;
}
else if (this.accessToken) {
// Call the accessToken callback to get fresh token
try {
tokenToSend = await this.accessToken();
}
catch (e) {
this.log('error', 'Error fetching access token from callback', e);
// Fall back to cached value if callback fails
tokenToSend = this.accessTokenValue;
}
}
else {
tokenToSend = this.accessTokenValue;
}
// Track whether this token was manually set or fetched via callback
if (isManualToken) {
this._manuallySetToken = true;
}
else if (this.accessToken) {
// If we used the callback, clear the manual flag
this._manuallySetToken = false;
}
if (this.accessTokenValue != tokenToSend) {
this.accessTokenValue = tokenToSend;
this.channels.forEach((channel) => {
const payload = {
access_token: tokenToSend,
version: DEFAULT_VERSION,
};
tokenToSend && channel.updateJoinPayload(payload);
if (channel.joinedOnce && channel._isJoined()) {
channel._push(CHANNEL_EVENTS.access_token, {
access_token: tokenToSend,
});
}
});
}
}
/**
* Wait for any in-flight auth operations to complete
* @internal
*/
async _waitForAuthIfNeeded() {
if (this._authPromise) {
await this._authPromise;
}
}
/**
* Safely call setAuth with standardized error handling
* @internal
*/
_setAuthSafely(context = 'general') {
// Only refresh auth if using callback-based tokens
if (!this._isManualToken()) {
this.setAuth().catch((e) => {
this.log('error', `Error setting auth in ${context}`, e);
});
}
}
/**
* Trigger state change callbacks with proper error handling
* @internal
*/
_triggerStateCallbacks(event, data) {
try {
this.stateChangeCallbacks[event].forEach((callback) => {
try {
callback(data);
}
catch (e) {
this.log('error', `error in ${event} callback`, e);
}
});
}
catch (e) {
this.log('error', `error triggering ${event} callbacks`, e);
}
}
/**
* Setup reconnection timer with proper configuration
* @internal
*/
_setupReconnectionTimer() {
this.reconnectTimer = new Timer(async () => {
setTimeout(async () => {
await this._waitForAuthIfNeeded();
if (!this.isConnected()) {
this.connect();
}
}, CONNECTION_TIMEOUTS.RECONNECT_DELAY);
}, this.reconnectAfterMs);
}
/**
* Initialize client options with defaults
* @internal
*/
_initializeOptions(options) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
// Set defaults
this.transport = (_a = options === null || options === void 0 ? void 0 : options.transport) !== null && _a !== void 0 ? _a : null;
this.timeout = (_b = options === null || options === void 0 ? void 0 : options.timeout) !== null && _b !== void 0 ? _b : DEFAULT_TIMEOUT;
this.heartbeatIntervalMs =
(_c = options === null || options === void 0 ? void 0 : options.heartbeatIntervalMs) !== null && _c !== void 0 ? _c : CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL;
this.worker = (_d = options === null || options === void 0 ? void 0 : options.worker) !== null && _d !== void 0 ? _d : false;
this.accessToken = (_e = options === null || options === void 0 ? void 0 : options.accessToken) !== null && _e !== void 0 ? _e : null;
this.heartbeatCallback = (_f = options === null || options === void 0 ? void 0 : options.heartbeatCallback) !== null && _f !== void 0 ? _f : noop;
this.vsn = (_g = options === null || options === void 0 ? void 0 : options.vsn) !== null && _g !== void 0 ? _g : DEFAULT_VSN;
// Handle special cases
if (options === null || options === void 0 ? void 0 : options.params)
this.params = options.params;
if (options === null || options === void 0 ? void 0 : options.logger)
this.logger = options.logger;
if ((options === null || options === void 0 ? void 0 : options.logLevel) || (options === null || options === void 0 ? void 0 : options.log_level)) {
this.logLevel = options.logLevel || options.log_level;
this.params = Object.assign(Object.assign({}, this.params), { log_level: this.logLevel });
}
// Set up functions with defaults
this.reconnectAfterMs =
(_h = options === null || options === void 0 ? void 0 : options.reconnectAfterMs) !== null && _h !== void 0 ? _h : ((tries) => {
return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK;
});
switch (this.vsn) {
case VSN_1_0_0:
this.encode =
(_j = options === null || options === void 0 ? void 0 : options.encode) !== null && _j !== void 0 ? _j : ((payload, callback) => {
return callback(JSON.stringify(payload));
});
this.decode =
(_k = options === null || options === void 0 ? void 0 : options.decode) !== null && _k !== void 0 ? _k : ((payload, callback) => {
return callback(JSON.parse(payload));
});
break;
case VSN_2_0_0:
this.encode = (_l = options === null || options === void 0 ? void 0 : options.encode) !== null && _l !== void 0 ? _l : this.serializer.encode.bind(this.serializer);
this.decode = (_m = options === null || options === void 0 ? void 0 : options.decode) !== null && _m !== void 0 ? _m : this.serializer.decode.bind(this.serializer);
break;
default:
throw new Error(`Unsupported serializer version: ${this.vsn}`);
}
// Handle worker setup
if (this.worker) {
if (typeof window !== 'undefined' && !window.Worker) {
throw new Error('Web Worker is not supported');
}
this.workerUrl = options === null || options === void 0 ? void 0 : options.workerUrl;
}
}
}
//# sourceMappingURL=RealtimeClient.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,76 @@
import type { PresenceOpts, PresenceOnJoinCallback, PresenceOnLeaveCallback } from 'phoenix';
import type RealtimeChannel from './RealtimeChannel';
type Presence<T extends {
[key: string]: any;
} = {}> = {
presence_ref: string;
} & T;
export type RealtimePresenceState<T extends {
[key: string]: any;
} = {}> = {
[key: string]: Presence<T>[];
};
export type RealtimePresenceJoinPayload<T extends {
[key: string]: any;
}> = {
event: `${REALTIME_PRESENCE_LISTEN_EVENTS.JOIN}`;
key: string;
currentPresences: Presence<T>[];
newPresences: Presence<T>[];
};
export type RealtimePresenceLeavePayload<T extends {
[key: string]: any;
}> = {
event: `${REALTIME_PRESENCE_LISTEN_EVENTS.LEAVE}`;
key: string;
currentPresences: Presence<T>[];
leftPresences: Presence<T>[];
};
export declare enum REALTIME_PRESENCE_LISTEN_EVENTS {
SYNC = "sync",
JOIN = "join",
LEAVE = "leave"
}
type RawPresenceState = {
[key: string]: {
metas: {
phx_ref?: string;
phx_ref_prev?: string;
[key: string]: any;
}[];
};
};
type RawPresenceDiff = {
joins: RawPresenceState;
leaves: RawPresenceState;
};
export default class RealtimePresence {
channel: RealtimeChannel;
state: RealtimePresenceState;
pendingDiffs: RawPresenceDiff[];
joinRef: string | null;
enabled: boolean;
caller: {
onJoin: PresenceOnJoinCallback;
onLeave: PresenceOnLeaveCallback;
onSync: () => void;
};
/**
* Creates a Presence helper that keeps the local presence state in sync with the server.
*
* @param channel - The realtime channel to bind to.
* @param opts - Optional custom event names, e.g. `{ events: { state: 'state', diff: 'diff' } }`.
*
* @example
* ```ts
* const presence = new RealtimePresence(channel)
*
* channel.on('presence', ({ event, key }) => {
* console.log(`Presence ${event} on ${key}`)
* })
* ```
*/
constructor(channel: RealtimeChannel, opts?: PresenceOpts);
}
export {};
//# sourceMappingURL=RealtimePresence.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"RealtimePresence.d.ts","sourceRoot":"","sources":["../../src/RealtimePresence.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAC5F,OAAO,KAAK,eAAe,MAAM,mBAAmB,CAAA;AAEpD,KAAK,QAAQ,CAAC,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,GAAG,EAAE,IAAI;IACrD,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,CAAC,CAAA;AAEL,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,GAAG,EAAE,IAAI;IACzE,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,2BAA2B,CAAC,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,IAAI;IAC1E,KAAK,EAAE,GAAG,+BAA+B,CAAC,IAAI,EAAE,CAAA;IAChD,GAAG,EAAE,MAAM,CAAA;IACX,gBAAgB,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IAC/B,YAAY,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,4BAA4B,CAAC,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,IAAI;IAC3E,KAAK,EAAE,GAAG,+BAA+B,CAAC,KAAK,EAAE,CAAA;IACjD,GAAG,EAAE,MAAM,CAAA;IACX,gBAAgB,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IAC/B,aAAa,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;CAC7B,CAAA;AAED,oBAAY,+BAA+B;IACzC,IAAI,SAAS;IACb,IAAI,SAAS;IACb,KAAK,UAAU;CAChB;AAOD,KAAK,gBAAgB,GAAG;IACtB,CAAC,GAAG,EAAE,MAAM,GAAG;QACb,KAAK,EAAE;YACL,OAAO,CAAC,EAAE,MAAM,CAAA;YAChB,YAAY,CAAC,EAAE,MAAM,CAAA;YACrB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;SACnB,EAAE,CAAA;KACJ,CAAA;CACF,CAAA;AAED,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,gBAAgB,CAAA;IACvB,MAAM,EAAE,gBAAgB,CAAA;CACzB,CAAA;AAID,MAAM,CAAC,OAAO,OAAO,gBAAgB;IA+B1B,OAAO,EAAE,eAAe;IA9BjC,KAAK,EAAE,qBAAqB,CAAK;IACjC,YAAY,EAAE,eAAe,EAAE,CAAK;IACpC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAO;IAC7B,OAAO,EAAE,OAAO,CAAQ;IACxB,MAAM,EAAE;QACN,MAAM,EAAE,sBAAsB,CAAA;QAC9B,OAAO,EAAE,uBAAuB,CAAA;QAChC,MAAM,EAAE,MAAM,IAAI,CAAA;KACnB,CAIA;IAED;;;;;;;;;;;;;;OAcG;gBAEM,OAAO,EAAE,eAAe,EAC/B,IAAI,CAAC,EAAE,YAAY;CA+PtB"}

View File

@@ -0,0 +1,233 @@
/*
This file draws heavily from https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/assets/js/phoenix/presence.js
License: https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/LICENSE.md
*/
export var REALTIME_PRESENCE_LISTEN_EVENTS;
(function (REALTIME_PRESENCE_LISTEN_EVENTS) {
REALTIME_PRESENCE_LISTEN_EVENTS["SYNC"] = "sync";
REALTIME_PRESENCE_LISTEN_EVENTS["JOIN"] = "join";
REALTIME_PRESENCE_LISTEN_EVENTS["LEAVE"] = "leave";
})(REALTIME_PRESENCE_LISTEN_EVENTS || (REALTIME_PRESENCE_LISTEN_EVENTS = {}));
export default class RealtimePresence {
/**
* Creates a Presence helper that keeps the local presence state in sync with the server.
*
* @param channel - The realtime channel to bind to.
* @param opts - Optional custom event names, e.g. `{ events: { state: 'state', diff: 'diff' } }`.
*
* @example
* ```ts
* const presence = new RealtimePresence(channel)
*
* channel.on('presence', ({ event, key }) => {
* console.log(`Presence ${event} on ${key}`)
* })
* ```
*/
constructor(channel, opts) {
this.channel = channel;
this.state = {};
this.pendingDiffs = [];
this.joinRef = null;
this.enabled = false;
this.caller = {
onJoin: () => { },
onLeave: () => { },
onSync: () => { },
};
const events = (opts === null || opts === void 0 ? void 0 : opts.events) || {
state: 'presence_state',
diff: 'presence_diff',
};
this.channel._on(events.state, {}, (newState) => {
const { onJoin, onLeave, onSync } = this.caller;
this.joinRef = this.channel._joinRef();
this.state = RealtimePresence.syncState(this.state, newState, onJoin, onLeave);
this.pendingDiffs.forEach((diff) => {
this.state = RealtimePresence.syncDiff(this.state, diff, onJoin, onLeave);
});
this.pendingDiffs = [];
onSync();
});
this.channel._on(events.diff, {}, (diff) => {
const { onJoin, onLeave, onSync } = this.caller;
if (this.inPendingSyncState()) {
this.pendingDiffs.push(diff);
}
else {
this.state = RealtimePresence.syncDiff(this.state, diff, onJoin, onLeave);
onSync();
}
});
this.onJoin((key, currentPresences, newPresences) => {
this.channel._trigger('presence', {
event: 'join',
key,
currentPresences,
newPresences,
});
});
this.onLeave((key, currentPresences, leftPresences) => {
this.channel._trigger('presence', {
event: 'leave',
key,
currentPresences,
leftPresences,
});
});
this.onSync(() => {
this.channel._trigger('presence', { event: 'sync' });
});
}
/**
* Used to sync the list of presences on the server with the
* client's state.
*
* An optional `onJoin` and `onLeave` callback can be provided to
* react to changes in the client's local presences across
* disconnects and reconnects with the server.
*
* @internal
*/
static syncState(currentState, newState, onJoin, onLeave) {
const state = this.cloneDeep(currentState);
const transformedState = this.transformState(newState);
const joins = {};
const leaves = {};
this.map(state, (key, presences) => {
if (!transformedState[key]) {
leaves[key] = presences;
}
});
this.map(transformedState, (key, newPresences) => {
const currentPresences = state[key];
if (currentPresences) {
const newPresenceRefs = newPresences.map((m) => m.presence_ref);
const curPresenceRefs = currentPresences.map((m) => m.presence_ref);
const joinedPresences = newPresences.filter((m) => curPresenceRefs.indexOf(m.presence_ref) < 0);
const leftPresences = currentPresences.filter((m) => newPresenceRefs.indexOf(m.presence_ref) < 0);
if (joinedPresences.length > 0) {
joins[key] = joinedPresences;
}
if (leftPresences.length > 0) {
leaves[key] = leftPresences;
}
}
else {
joins[key] = newPresences;
}
});
return this.syncDiff(state, { joins, leaves }, onJoin, onLeave);
}
/**
* Used to sync a diff of presence join and leave events from the
* server, as they happen.
*
* Like `syncState`, `syncDiff` accepts optional `onJoin` and
* `onLeave` callbacks to react to a user joining or leaving from a
* device.
*
* @internal
*/
static syncDiff(state, diff, onJoin, onLeave) {
const { joins, leaves } = {
joins: this.transformState(diff.joins),
leaves: this.transformState(diff.leaves),
};
if (!onJoin) {
onJoin = () => { };
}
if (!onLeave) {
onLeave = () => { };
}
this.map(joins, (key, newPresences) => {
var _a;
const currentPresences = (_a = state[key]) !== null && _a !== void 0 ? _a : [];
state[key] = this.cloneDeep(newPresences);
if (currentPresences.length > 0) {
const joinedPresenceRefs = state[key].map((m) => m.presence_ref);
const curPresences = currentPresences.filter((m) => joinedPresenceRefs.indexOf(m.presence_ref) < 0);
state[key].unshift(...curPresences);
}
onJoin(key, currentPresences, newPresences);
});
this.map(leaves, (key, leftPresences) => {
let currentPresences = state[key];
if (!currentPresences)
return;
const presenceRefsToRemove = leftPresences.map((m) => m.presence_ref);
currentPresences = currentPresences.filter((m) => presenceRefsToRemove.indexOf(m.presence_ref) < 0);
state[key] = currentPresences;
onLeave(key, currentPresences, leftPresences);
if (currentPresences.length === 0)
delete state[key];
});
return state;
}
/** @internal */
static map(obj, func) {
return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key]));
}
/**
* Remove 'metas' key
* Change 'phx_ref' to 'presence_ref'
* Remove 'phx_ref' and 'phx_ref_prev'
*
* @example
* // returns {
* abc123: [
* { presence_ref: '2', user_id: 1 },
* { presence_ref: '3', user_id: 2 }
* ]
* }
* RealtimePresence.transformState({
* abc123: {
* metas: [
* { phx_ref: '2', phx_ref_prev: '1' user_id: 1 },
* { phx_ref: '3', user_id: 2 }
* ]
* }
* })
*
* @internal
*/
static transformState(state) {
state = this.cloneDeep(state);
return Object.getOwnPropertyNames(state).reduce((newState, key) => {
const presences = state[key];
if ('metas' in presences) {
newState[key] = presences.metas.map((presence) => {
presence['presence_ref'] = presence['phx_ref'];
delete presence['phx_ref'];
delete presence['phx_ref_prev'];
return presence;
});
}
else {
newState[key] = presences;
}
return newState;
}, {});
}
/** @internal */
static cloneDeep(obj) {
return JSON.parse(JSON.stringify(obj));
}
/** @internal */
onJoin(callback) {
this.caller.onJoin = callback;
}
/** @internal */
onLeave(callback) {
this.caller.onLeave = callback;
}
/** @internal */
onSync(callback) {
this.caller.onSync = callback;
}
/** @internal */
inPendingSyncState() {
return !this.joinRef || this.joinRef !== this.channel._joinRef();
}
}
//# sourceMappingURL=RealtimePresence.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
import RealtimeClient, { RealtimeClientOptions, RealtimeMessage, RealtimeRemoveChannelResponse, WebSocketLikeConstructor } from './RealtimeClient';
import RealtimeChannel, { RealtimeChannelOptions, RealtimeChannelSendResponse, RealtimePostgresChangesFilter, RealtimePostgresChangesPayload, RealtimePostgresInsertPayload, RealtimePostgresUpdatePayload, RealtimePostgresDeletePayload, REALTIME_LISTEN_TYPES, REALTIME_POSTGRES_CHANGES_LISTEN_EVENT, REALTIME_SUBSCRIBE_STATES, REALTIME_CHANNEL_STATES } from './RealtimeChannel';
import RealtimePresence, { RealtimePresenceState, RealtimePresenceJoinPayload, RealtimePresenceLeavePayload, REALTIME_PRESENCE_LISTEN_EVENTS } from './RealtimePresence';
import WebSocketFactory, { WebSocketLike } from './lib/websocket-factory';
export { RealtimePresence, RealtimeChannel, RealtimeChannelOptions, RealtimeChannelSendResponse, RealtimeClient, RealtimeClientOptions, RealtimeMessage, RealtimePostgresChangesFilter, RealtimePostgresChangesPayload, RealtimePostgresInsertPayload, RealtimePostgresUpdatePayload, RealtimePostgresDeletePayload, RealtimePresenceJoinPayload, RealtimePresenceLeavePayload, RealtimePresenceState, RealtimeRemoveChannelResponse, REALTIME_LISTEN_TYPES, REALTIME_POSTGRES_CHANGES_LISTEN_EVENT, REALTIME_PRESENCE_LISTEN_EVENTS, REALTIME_SUBSCRIBE_STATES, REALTIME_CHANNEL_STATES, WebSocketFactory, WebSocketLike, WebSocketLikeConstructor, };
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,EAAE,EACrB,qBAAqB,EACrB,eAAe,EACf,6BAA6B,EAC7B,wBAAwB,EACzB,MAAM,kBAAkB,CAAA;AACzB,OAAO,eAAe,EAAE,EACtB,sBAAsB,EACtB,2BAA2B,EAC3B,6BAA6B,EAC7B,8BAA8B,EAC9B,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,qBAAqB,EACrB,sCAAsC,EACtC,yBAAyB,EACzB,uBAAuB,EACxB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,gBAAgB,EAAE,EACvB,qBAAqB,EACrB,2BAA2B,EAC3B,4BAA4B,EAC5B,+BAA+B,EAChC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,gBAAgB,EAAE,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAEzE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,2BAA2B,EAC3B,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,6BAA6B,EAC7B,8BAA8B,EAC9B,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,2BAA2B,EAC3B,4BAA4B,EAC5B,qBAAqB,EACrB,6BAA6B,EAC7B,qBAAqB,EACrB,sCAAsC,EACtC,+BAA+B,EAC/B,yBAAyB,EACzB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,wBAAwB,GACzB,CAAA"}

View File

@@ -0,0 +1,6 @@
import RealtimeClient from './RealtimeClient';
import RealtimeChannel, { REALTIME_LISTEN_TYPES, REALTIME_POSTGRES_CHANGES_LISTEN_EVENT, REALTIME_SUBSCRIBE_STATES, REALTIME_CHANNEL_STATES, } from './RealtimeChannel';
import RealtimePresence, { REALTIME_PRESENCE_LISTEN_EVENTS, } from './RealtimePresence';
import WebSocketFactory from './lib/websocket-factory';
export { RealtimePresence, RealtimeChannel, RealtimeClient, REALTIME_LISTEN_TYPES, REALTIME_POSTGRES_CHANGES_LISTEN_EVENT, REALTIME_PRESENCE_LISTEN_EVENTS, REALTIME_SUBSCRIBE_STATES, REALTIME_CHANNEL_STATES, WebSocketFactory, };
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,cAKN,MAAM,kBAAkB,CAAA;AACzB,OAAO,eAAe,EAAE,EAQtB,qBAAqB,EACrB,sCAAsC,EACtC,yBAAyB,EACzB,uBAAuB,GACxB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,gBAAgB,EAAE,EAIvB,+BAA+B,GAChC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,gBAAmC,MAAM,yBAAyB,CAAA;AAEzE,OAAO,EACL,gBAAgB,EAChB,eAAe,EAGf,cAAc,EAYd,qBAAqB,EACrB,sCAAsC,EACtC,+BAA+B,EAC/B,yBAAyB,EACzB,uBAAuB,EACvB,gBAAgB,GAGjB,CAAA"}

View File

@@ -0,0 +1,39 @@
export declare const DEFAULT_VERSION = "realtime-js/2.99.1";
export declare const VSN_1_0_0: string;
export declare const VSN_2_0_0: string;
export declare const DEFAULT_VSN: string;
export declare const VERSION = "2.99.1";
export declare const DEFAULT_TIMEOUT = 10000;
export declare const WS_CLOSE_NORMAL = 1000;
export declare const MAX_PUSH_BUFFER_SIZE = 100;
export declare enum SOCKET_STATES {
connecting = 0,
open = 1,
closing = 2,
closed = 3
}
export declare enum CHANNEL_STATES {
closed = "closed",
errored = "errored",
joined = "joined",
joining = "joining",
leaving = "leaving"
}
export declare enum CHANNEL_EVENTS {
close = "phx_close",
error = "phx_error",
join = "phx_join",
reply = "phx_reply",
leave = "phx_leave",
access_token = "access_token"
}
export declare enum TRANSPORTS {
websocket = "websocket"
}
export declare enum CONNECTION_STATE {
Connecting = "connecting",
Open = "open",
Closing = "closing",
Closed = "closed"
}
//# sourceMappingURL=constants.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,eAAe,uBAA2B,CAAA;AAEvD,eAAO,MAAM,SAAS,EAAE,MAAgB,CAAA;AACxC,eAAO,MAAM,SAAS,EAAE,MAAgB,CAAA;AACxC,eAAO,MAAM,WAAW,EAAE,MAAkB,CAAA;AAE5C,eAAO,MAAM,OAAO,WAAU,CAAA;AAE9B,eAAO,MAAM,eAAe,QAAQ,CAAA;AAEpC,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC,oBAAY,aAAa;IACvB,UAAU,IAAI;IACd,IAAI,IAAI;IACR,OAAO,IAAI;IACX,MAAM,IAAI;CACX;AAED,oBAAY,cAAc;IACxB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,OAAO,YAAY;CACpB;AAED,oBAAY,cAAc;IACxB,KAAK,cAAc;IACnB,KAAK,cAAc;IACnB,IAAI,aAAa;IACjB,KAAK,cAAc;IACnB,KAAK,cAAc;IACnB,YAAY,iBAAiB;CAC9B;AAED,oBAAY,UAAU;IACpB,SAAS,cAAc;CACxB;AAED,oBAAY,gBAAgB;IAC1B,UAAU,eAAe;IACzB,IAAI,SAAS;IACb,OAAO,YAAY;IACnB,MAAM,WAAW;CAClB"}

View File

@@ -0,0 +1,45 @@
import { version } from './version';
export const DEFAULT_VERSION = `realtime-js/${version}`;
export const VSN_1_0_0 = '1.0.0';
export const VSN_2_0_0 = '2.0.0';
export const DEFAULT_VSN = VSN_2_0_0;
export const VERSION = version;
export const DEFAULT_TIMEOUT = 10000;
export const WS_CLOSE_NORMAL = 1000;
export const MAX_PUSH_BUFFER_SIZE = 100;
export var SOCKET_STATES;
(function (SOCKET_STATES) {
SOCKET_STATES[SOCKET_STATES["connecting"] = 0] = "connecting";
SOCKET_STATES[SOCKET_STATES["open"] = 1] = "open";
SOCKET_STATES[SOCKET_STATES["closing"] = 2] = "closing";
SOCKET_STATES[SOCKET_STATES["closed"] = 3] = "closed";
})(SOCKET_STATES || (SOCKET_STATES = {}));
export var CHANNEL_STATES;
(function (CHANNEL_STATES) {
CHANNEL_STATES["closed"] = "closed";
CHANNEL_STATES["errored"] = "errored";
CHANNEL_STATES["joined"] = "joined";
CHANNEL_STATES["joining"] = "joining";
CHANNEL_STATES["leaving"] = "leaving";
})(CHANNEL_STATES || (CHANNEL_STATES = {}));
export var CHANNEL_EVENTS;
(function (CHANNEL_EVENTS) {
CHANNEL_EVENTS["close"] = "phx_close";
CHANNEL_EVENTS["error"] = "phx_error";
CHANNEL_EVENTS["join"] = "phx_join";
CHANNEL_EVENTS["reply"] = "phx_reply";
CHANNEL_EVENTS["leave"] = "phx_leave";
CHANNEL_EVENTS["access_token"] = "access_token";
})(CHANNEL_EVENTS || (CHANNEL_EVENTS = {}));
export var TRANSPORTS;
(function (TRANSPORTS) {
TRANSPORTS["websocket"] = "websocket";
})(TRANSPORTS || (TRANSPORTS = {}));
export var CONNECTION_STATE;
(function (CONNECTION_STATE) {
CONNECTION_STATE["Connecting"] = "connecting";
CONNECTION_STATE["Open"] = "open";
CONNECTION_STATE["Closing"] = "closing";
CONNECTION_STATE["Closed"] = "closed";
})(CONNECTION_STATE || (CONNECTION_STATE = {}));
//# sourceMappingURL=constants.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,OAAO,EAAE,CAAA;AAEvD,MAAM,CAAC,MAAM,SAAS,GAAW,OAAO,CAAA;AACxC,MAAM,CAAC,MAAM,SAAS,GAAW,OAAO,CAAA;AACxC,MAAM,CAAC,MAAM,WAAW,GAAW,SAAS,CAAA;AAE5C,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAA;AAE9B,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,CAAA;AAEpC,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,CAAA;AACnC,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAA;AAEvC,MAAM,CAAN,IAAY,aAKX;AALD,WAAY,aAAa;IACvB,6DAAc,CAAA;IACd,iDAAQ,CAAA;IACR,uDAAW,CAAA;IACX,qDAAU,CAAA;AACZ,CAAC,EALW,aAAa,KAAb,aAAa,QAKxB;AAED,MAAM,CAAN,IAAY,cAMX;AAND,WAAY,cAAc;IACxB,mCAAiB,CAAA;IACjB,qCAAmB,CAAA;IACnB,mCAAiB,CAAA;IACjB,qCAAmB,CAAA;IACnB,qCAAmB,CAAA;AACrB,CAAC,EANW,cAAc,KAAd,cAAc,QAMzB;AAED,MAAM,CAAN,IAAY,cAOX;AAPD,WAAY,cAAc;IACxB,qCAAmB,CAAA;IACnB,qCAAmB,CAAA;IACnB,mCAAiB,CAAA;IACjB,qCAAmB,CAAA;IACnB,qCAAmB,CAAA;IACnB,+CAA6B,CAAA;AAC/B,CAAC,EAPW,cAAc,KAAd,cAAc,QAOzB;AAED,MAAM,CAAN,IAAY,UAEX;AAFD,WAAY,UAAU;IACpB,qCAAuB,CAAA;AACzB,CAAC,EAFW,UAAU,KAAV,UAAU,QAErB;AAED,MAAM,CAAN,IAAY,gBAKX;AALD,WAAY,gBAAgB;IAC1B,6CAAyB,CAAA;IACzB,iCAAa,CAAA;IACb,uCAAmB,CAAA;IACnB,qCAAiB,CAAA;AACnB,CAAC,EALW,gBAAgB,KAAhB,gBAAgB,QAK3B"}

View File

@@ -0,0 +1,48 @@
import type RealtimeChannel from '../RealtimeChannel';
export default class Push {
channel: RealtimeChannel;
event: string;
payload: {
[key: string]: any;
};
timeout: number;
sent: boolean;
timeoutTimer: number | undefined;
ref: string;
receivedResp: {
status: string;
response: {
[key: string]: any;
};
} | null;
recHooks: {
status: string;
callback: Function;
}[];
refEvent: string | null;
/**
* Initializes the Push
*
* @param channel The Channel
* @param event The event, for example `"phx_join"`
* @param payload The payload, for example `{user_id: 123}`
* @param timeout The push timeout in milliseconds
*/
constructor(channel: RealtimeChannel, event: string, payload?: {
[key: string]: any;
}, timeout?: number);
resend(timeout: number): void;
send(): void;
updatePayload(payload: {
[key: string]: any;
}): void;
receive(status: string, callback: Function): this;
startTimeout(): void;
trigger(status: string, response: any): void;
destroy(): void;
private _cancelRefEvent;
private _cancelTimeout;
private _matchReceive;
private _hasReceived;
}
//# sourceMappingURL=push.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../../src/lib/push.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,eAAe,MAAM,oBAAoB,CAAA;AAErD,MAAM,CAAC,OAAO,OAAO,IAAI;IAuBd,OAAO,EAAE,eAAe;IACxB,KAAK,EAAE,MAAM;IACb,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE;IAC/B,OAAO,EAAE,MAAM;IAzBxB,IAAI,EAAE,OAAO,CAAQ;IACrB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAY;IAC5C,GAAG,EAAE,MAAM,CAAK;IAChB,YAAY,EAAE;QACZ,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE;YAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;SAAE,CAAA;KACjC,GAAG,IAAI,CAAO;IACf,QAAQ,EAAE;QACR,MAAM,EAAE,MAAM,CAAA;QACd,QAAQ,EAAE,QAAQ,CAAA;KACnB,EAAE,CAAK;IACR,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAO;IAE9B;;;;;;;OAOG;gBAEM,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAO,EACpC,OAAO,GAAE,MAAwB;IAG1C,MAAM,CAAC,OAAO,EAAE,MAAM;IAUtB,IAAI;IAeJ,aAAa,CAAC,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,GAAG,IAAI;IAIpD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ;IAS1C,YAAY;IAqBZ,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG;IAIrC,OAAO;IAKP,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,YAAY;CAGrB"}

View File

@@ -0,0 +1,99 @@
import { DEFAULT_TIMEOUT } from '../lib/constants';
export default class Push {
/**
* Initializes the Push
*
* @param channel The Channel
* @param event The event, for example `"phx_join"`
* @param payload The payload, for example `{user_id: 123}`
* @param timeout The push timeout in milliseconds
*/
constructor(channel, event, payload = {}, timeout = DEFAULT_TIMEOUT) {
this.channel = channel;
this.event = event;
this.payload = payload;
this.timeout = timeout;
this.sent = false;
this.timeoutTimer = undefined;
this.ref = '';
this.receivedResp = null;
this.recHooks = [];
this.refEvent = null;
}
resend(timeout) {
this.timeout = timeout;
this._cancelRefEvent();
this.ref = '';
this.refEvent = null;
this.receivedResp = null;
this.sent = false;
this.send();
}
send() {
if (this._hasReceived('timeout')) {
return;
}
this.startTimeout();
this.sent = true;
this.channel.socket.push({
topic: this.channel.topic,
event: this.event,
payload: this.payload,
ref: this.ref,
join_ref: this.channel._joinRef(),
});
}
updatePayload(payload) {
this.payload = Object.assign(Object.assign({}, this.payload), payload);
}
receive(status, callback) {
var _a;
if (this._hasReceived(status)) {
callback((_a = this.receivedResp) === null || _a === void 0 ? void 0 : _a.response);
}
this.recHooks.push({ status, callback });
return this;
}
startTimeout() {
if (this.timeoutTimer) {
return;
}
this.ref = this.channel.socket._makeRef();
this.refEvent = this.channel._replyEventName(this.ref);
const callback = (payload) => {
this._cancelRefEvent();
this._cancelTimeout();
this.receivedResp = payload;
this._matchReceive(payload);
};
this.channel._on(this.refEvent, {}, callback);
this.timeoutTimer = setTimeout(() => {
this.trigger('timeout', {});
}, this.timeout);
}
trigger(status, response) {
if (this.refEvent)
this.channel._trigger(this.refEvent, { status, response });
}
destroy() {
this._cancelRefEvent();
this._cancelTimeout();
}
_cancelRefEvent() {
if (!this.refEvent) {
return;
}
this.channel._off(this.refEvent, {});
}
_cancelTimeout() {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = undefined;
}
_matchReceive({ status, response }) {
this.recHooks.filter((h) => h.status === status).forEach((h) => h.callback(response));
}
_hasReceived(status) {
return this.receivedResp && this.receivedResp.status === status;
}
}
//# sourceMappingURL=push.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"push.js","sourceRoot":"","sources":["../../../src/lib/push.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,33 @@
export type Msg<T> = {
join_ref?: string | null;
ref?: string | null;
topic: string;
event: string;
payload: T;
};
export default class Serializer {
HEADER_LENGTH: number;
USER_BROADCAST_PUSH_META_LENGTH: number;
KINDS: {
userBroadcastPush: number;
userBroadcast: number;
};
BINARY_ENCODING: number;
JSON_ENCODING: number;
BROADCAST_EVENT: string;
allowedMetadataKeys: string[];
constructor(allowedMetadataKeys?: string[] | null);
encode(msg: Msg<{
[key: string]: any;
}>, callback: (result: ArrayBuffer | string) => any): any;
private _binaryEncodeUserBroadcastPush;
private _encodeBinaryUserBroadcastPush;
private _encodeJsonUserBroadcastPush;
private _encodeUserBroadcastPush;
decode(rawPayload: ArrayBuffer | string, callback: Function): any;
private _binaryDecode;
private _decodeUserBroadcast;
private _isArrayBuffer;
private _pick;
}
//# sourceMappingURL=serializer.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../../src/lib/serializer.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI;IACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,CAAC,CAAA;CACX,CAAA;AAED,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,aAAa,SAAI;IACjB,+BAA+B,SAAI;IACnC,KAAK;;;MAA6C;IAClD,eAAe,SAAI;IACnB,aAAa,SAAI;IACjB,eAAe,SAAc;IAE7B,mBAAmB,EAAE,MAAM,EAAE,CAAK;gBAEtB,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI;IAIjD,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,KAAK,GAAG;IAexF,OAAO,CAAC,8BAA8B;IAQtC,OAAO,CAAC,8BAA8B;IAKtC,OAAO,CAAC,4BAA4B;IAOpC,OAAO,CAAC,wBAAwB;IAkEhC,MAAM,CAAC,UAAU,EAAE,WAAW,GAAG,MAAM,EAAE,QAAQ,EAAE,QAAQ;IAe3D,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,oBAAoB;IA0C5B,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,KAAK;CAMd"}

View File

@@ -0,0 +1,152 @@
export default class Serializer {
constructor(allowedMetadataKeys) {
this.HEADER_LENGTH = 1;
this.USER_BROADCAST_PUSH_META_LENGTH = 6;
this.KINDS = { userBroadcastPush: 3, userBroadcast: 4 };
this.BINARY_ENCODING = 0;
this.JSON_ENCODING = 1;
this.BROADCAST_EVENT = 'broadcast';
this.allowedMetadataKeys = [];
this.allowedMetadataKeys = allowedMetadataKeys !== null && allowedMetadataKeys !== void 0 ? allowedMetadataKeys : [];
}
encode(msg, callback) {
if (msg.event === this.BROADCAST_EVENT &&
!(msg.payload instanceof ArrayBuffer) &&
typeof msg.payload.event === 'string') {
return callback(this._binaryEncodeUserBroadcastPush(msg));
}
let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload];
return callback(JSON.stringify(payload));
}
_binaryEncodeUserBroadcastPush(message) {
var _a;
if (this._isArrayBuffer((_a = message.payload) === null || _a === void 0 ? void 0 : _a.payload)) {
return this._encodeBinaryUserBroadcastPush(message);
}
else {
return this._encodeJsonUserBroadcastPush(message);
}
}
_encodeBinaryUserBroadcastPush(message) {
var _a, _b;
const userPayload = (_b = (_a = message.payload) === null || _a === void 0 ? void 0 : _a.payload) !== null && _b !== void 0 ? _b : new ArrayBuffer(0);
return this._encodeUserBroadcastPush(message, this.BINARY_ENCODING, userPayload);
}
_encodeJsonUserBroadcastPush(message) {
var _a, _b;
const userPayload = (_b = (_a = message.payload) === null || _a === void 0 ? void 0 : _a.payload) !== null && _b !== void 0 ? _b : {};
const encoder = new TextEncoder();
const encodedUserPayload = encoder.encode(JSON.stringify(userPayload)).buffer;
return this._encodeUserBroadcastPush(message, this.JSON_ENCODING, encodedUserPayload);
}
_encodeUserBroadcastPush(message, encodingType, encodedPayload) {
var _a, _b;
const topic = message.topic;
const ref = (_a = message.ref) !== null && _a !== void 0 ? _a : '';
const joinRef = (_b = message.join_ref) !== null && _b !== void 0 ? _b : '';
const userEvent = message.payload.event;
// Filter metadata based on allowed keys
const rest = this.allowedMetadataKeys
? this._pick(message.payload, this.allowedMetadataKeys)
: {};
const metadata = Object.keys(rest).length === 0 ? '' : JSON.stringify(rest);
// Validate lengths don't exceed uint8 max value (255)
if (joinRef.length > 255) {
throw new Error(`joinRef length ${joinRef.length} exceeds maximum of 255`);
}
if (ref.length > 255) {
throw new Error(`ref length ${ref.length} exceeds maximum of 255`);
}
if (topic.length > 255) {
throw new Error(`topic length ${topic.length} exceeds maximum of 255`);
}
if (userEvent.length > 255) {
throw new Error(`userEvent length ${userEvent.length} exceeds maximum of 255`);
}
if (metadata.length > 255) {
throw new Error(`metadata length ${metadata.length} exceeds maximum of 255`);
}
const metaLength = this.USER_BROADCAST_PUSH_META_LENGTH +
joinRef.length +
ref.length +
topic.length +
userEvent.length +
metadata.length;
const header = new ArrayBuffer(this.HEADER_LENGTH + metaLength);
let view = new DataView(header);
let offset = 0;
view.setUint8(offset++, this.KINDS.userBroadcastPush); // kind
view.setUint8(offset++, joinRef.length);
view.setUint8(offset++, ref.length);
view.setUint8(offset++, topic.length);
view.setUint8(offset++, userEvent.length);
view.setUint8(offset++, metadata.length);
view.setUint8(offset++, encodingType);
Array.from(joinRef, (char) => view.setUint8(offset++, char.charCodeAt(0)));
Array.from(ref, (char) => view.setUint8(offset++, char.charCodeAt(0)));
Array.from(topic, (char) => view.setUint8(offset++, char.charCodeAt(0)));
Array.from(userEvent, (char) => view.setUint8(offset++, char.charCodeAt(0)));
Array.from(metadata, (char) => view.setUint8(offset++, char.charCodeAt(0)));
var combined = new Uint8Array(header.byteLength + encodedPayload.byteLength);
combined.set(new Uint8Array(header), 0);
combined.set(new Uint8Array(encodedPayload), header.byteLength);
return combined.buffer;
}
decode(rawPayload, callback) {
if (this._isArrayBuffer(rawPayload)) {
let result = this._binaryDecode(rawPayload);
return callback(result);
}
if (typeof rawPayload === 'string') {
const jsonPayload = JSON.parse(rawPayload);
const [join_ref, ref, topic, event, payload] = jsonPayload;
return callback({ join_ref, ref, topic, event, payload });
}
return callback({});
}
_binaryDecode(buffer) {
const view = new DataView(buffer);
const kind = view.getUint8(0);
const decoder = new TextDecoder();
switch (kind) {
case this.KINDS.userBroadcast:
return this._decodeUserBroadcast(buffer, view, decoder);
}
}
_decodeUserBroadcast(buffer, view, decoder) {
const topicSize = view.getUint8(1);
const userEventSize = view.getUint8(2);
const metadataSize = view.getUint8(3);
const payloadEncoding = view.getUint8(4);
let offset = this.HEADER_LENGTH + 4;
const topic = decoder.decode(buffer.slice(offset, offset + topicSize));
offset = offset + topicSize;
const userEvent = decoder.decode(buffer.slice(offset, offset + userEventSize));
offset = offset + userEventSize;
const metadata = decoder.decode(buffer.slice(offset, offset + metadataSize));
offset = offset + metadataSize;
const payload = buffer.slice(offset, buffer.byteLength);
const parsedPayload = payloadEncoding === this.JSON_ENCODING ? JSON.parse(decoder.decode(payload)) : payload;
const data = {
type: this.BROADCAST_EVENT,
event: userEvent,
payload: parsedPayload,
};
// Metadata is optional and always JSON encoded
if (metadataSize > 0) {
data['meta'] = JSON.parse(metadata);
}
return { join_ref: null, ref: null, topic: topic, event: this.BROADCAST_EVENT, payload: data };
}
_isArrayBuffer(buffer) {
var _a;
return buffer instanceof ArrayBuffer || ((_a = buffer === null || buffer === void 0 ? void 0 : buffer.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'ArrayBuffer';
}
_pick(obj, keys) {
if (!obj || typeof obj !== 'object') {
return {};
}
return Object.fromEntries(Object.entries(obj).filter(([key]) => keys.includes(key)));
}
}
//# sourceMappingURL=serializer.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
/**
* Creates a timer that accepts a `timerCalc` function to perform calculated timeout retries, such as exponential backoff.
*
* @example
* let reconnectTimer = new Timer(() => this.connect(), function(tries){
* return [1000, 5000, 10000][tries - 1] || 10000
* })
* reconnectTimer.scheduleTimeout() // fires after 1000
* reconnectTimer.scheduleTimeout() // fires after 5000
* reconnectTimer.reset()
* reconnectTimer.scheduleTimeout() // fires after 1000
*/
export default class Timer {
callback: Function;
timerCalc: Function;
timer: number | undefined;
tries: number;
constructor(callback: Function, timerCalc: Function);
reset(): void;
scheduleTimeout(): void;
}
//# sourceMappingURL=timer.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"timer.d.ts","sourceRoot":"","sources":["../../../src/lib/timer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,OAAO,KAAK;IAKf,QAAQ,EAAE,QAAQ;IAClB,SAAS,EAAE,QAAQ;IAL5B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAY;IACrC,KAAK,EAAE,MAAM,CAAI;gBAGR,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,QAAQ;IAM5B,KAAK;IAOL,eAAe;CAWhB"}

View File

@@ -0,0 +1,36 @@
/**
* Creates a timer that accepts a `timerCalc` function to perform calculated timeout retries, such as exponential backoff.
*
* @example
* let reconnectTimer = new Timer(() => this.connect(), function(tries){
* return [1000, 5000, 10000][tries - 1] || 10000
* })
* reconnectTimer.scheduleTimeout() // fires after 1000
* reconnectTimer.scheduleTimeout() // fires after 5000
* reconnectTimer.reset()
* reconnectTimer.scheduleTimeout() // fires after 1000
*/
export default class Timer {
constructor(callback, timerCalc) {
this.callback = callback;
this.timerCalc = timerCalc;
this.timer = undefined;
this.tries = 0;
this.callback = callback;
this.timerCalc = timerCalc;
}
reset() {
this.tries = 0;
clearTimeout(this.timer);
this.timer = undefined;
}
// Cancels any previous scheduleTimeout and schedules callback
scheduleTimeout() {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.tries = this.tries + 1;
this.callback();
}, this.timerCalc(this.tries + 1));
}
}
//# sourceMappingURL=timer.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"timer.js","sourceRoot":"","sources":["../../../src/lib/timer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,OAAO,KAAK;IAIxB,YACS,QAAkB,EAClB,SAAmB;QADnB,aAAQ,GAAR,QAAQ,CAAU;QAClB,cAAS,GAAT,SAAS,CAAU;QAL5B,UAAK,GAAuB,SAAS,CAAA;QACrC,UAAK,GAAW,CAAC,CAAA;QAMf,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;IAC5B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;QACd,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACxB,IAAI,CAAC,KAAK,GAAG,SAAS,CAAA;IACxB,CAAC;IAED,8DAA8D;IAC9D,eAAe;QACb,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAExB,IAAI,CAAC,KAAK,GAAQ,UAAU,CAC1B,GAAG,EAAE;YACH,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;YAC3B,IAAI,CAAC,QAAQ,EAAE,CAAA;QACjB,CAAC,EACD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAC/B,CAAA;IACH,CAAC;CACF"}

View File

@@ -0,0 +1,109 @@
/**
* Helpers to convert the change Payload into native JS types.
*/
export declare enum PostgresTypes {
abstime = "abstime",
bool = "bool",
date = "date",
daterange = "daterange",
float4 = "float4",
float8 = "float8",
int2 = "int2",
int4 = "int4",
int4range = "int4range",
int8 = "int8",
int8range = "int8range",
json = "json",
jsonb = "jsonb",
money = "money",
numeric = "numeric",
oid = "oid",
reltime = "reltime",
text = "text",
time = "time",
timestamp = "timestamp",
timestamptz = "timestamptz",
timetz = "timetz",
tsrange = "tsrange",
tstzrange = "tstzrange"
}
type Columns = {
name: string;
type: string;
flags?: string[];
type_modifier?: number;
}[];
type BaseValue = null | string | number | boolean;
type RecordValue = BaseValue | BaseValue[];
type Record = {
[key: string]: RecordValue;
};
/**
* Takes an array of columns and an object of string values then converts each string value
* to its mapped type.
*
* @param {{name: String, type: String}[]} columns
* @param {Object} record
* @param {Object} options The map of various options that can be applied to the mapper
* @param {Array} options.skipTypes The array of types that should not be converted
*
* @example convertChangeData([{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age:'33'}, {})
* //=>{ first_name: 'Paul', age: 33 }
*/
export declare const convertChangeData: (columns: Columns, record: Record | null, options?: {
skipTypes?: string[];
}) => Record;
/**
* Converts the value of an individual column.
*
* @param {String} columnName The column that you want to convert
* @param {{name: String, type: String}[]} columns All of the columns
* @param {Object} record The map of string values
* @param {Array} skipTypes An array of types that should not be converted
* @return {object} Useless information
*
* @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, [])
* //=> 33
* @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, ['int4'])
* //=> "33"
*/
export declare const convertColumn: (columnName: string, columns: Columns, record: Record, skipTypes: string[]) => RecordValue;
/**
* If the value of the cell is `null`, returns null.
* Otherwise converts the string value to the correct type.
* @param {String} type A postgres column type
* @param {String} value The cell value
*
* @example convertCell('bool', 't')
* //=> true
* @example convertCell('int8', '10')
* //=> 10
* @example convertCell('_int4', '{1,2,3,4}')
* //=> [1,2,3,4]
*/
export declare const convertCell: (type: string, value: RecordValue) => RecordValue;
export declare const toBoolean: (value: RecordValue) => RecordValue;
export declare const toNumber: (value: RecordValue) => RecordValue;
export declare const toJson: (value: RecordValue) => RecordValue;
/**
* Converts a Postgres Array into a native JS array
*
* @example toArray('{}', 'int4')
* //=> []
* @example toArray('{"[2021-01-01,2021-12-31)","(2021-01-01,2021-12-32]"}', 'daterange')
* //=> ['[2021-01-01,2021-12-31)', '(2021-01-01,2021-12-32]']
* @example toArray([1,2,3,4], 'int4')
* //=> [1,2,3,4]
*/
export declare const toArray: (value: RecordValue, type: string) => RecordValue;
/**
* Fixes timestamp to be ISO-8601. Swaps the space between the date and time for a 'T'
* See https://github.com/supabase/supabase/issues/18
*
* @example toTimestampString('2019-09-10 00:00:00')
* //=> '2019-09-10T00:00:00'
*/
export declare const toTimestampString: (value: RecordValue) => RecordValue;
export declare const httpEndpointURL: (socketUrl: string) => string;
export {};
//# sourceMappingURL=transformers.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"transformers.d.ts","sourceRoot":"","sources":["../../../src/lib/transformers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,oBAAY,aAAa;IACvB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,SAAS,cAAc;IACvB,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,SAAS,cAAc;IACvB,IAAI,SAAS;IACb,SAAS,cAAc;IACvB,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;IACf,OAAO,YAAY;IACnB,GAAG,QAAQ;IACX,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,MAAM,WAAW;IACjB,OAAO,YAAY;IACnB,SAAS,cAAc;CACxB;AAED,KAAK,OAAO,GAAG;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,EAAE,CAAA;AAEH,KAAK,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AACjD,KAAK,WAAW,GAAG,SAAS,GAAG,SAAS,EAAE,CAAA;AAE1C,KAAK,MAAM,GAAG;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAA;CAC3B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,OAAO,EAChB,QAAQ,MAAM,GAAG,IAAI,EACrB,UAAS;IAAE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,KACrC,MAWF,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,GACxB,YAAY,MAAM,EAClB,SAAS,OAAO,EAChB,QAAQ,MAAM,EACd,WAAW,MAAM,EAAE,KAClB,WAUF,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,OAAO,WAAW,KAAG,WA0C9D,CAAA;AAKD,eAAO,MAAM,SAAS,GAAI,OAAO,WAAW,KAAG,WAS9C,CAAA;AACD,eAAO,MAAM,QAAQ,GAAI,OAAO,WAAW,KAAG,WAQ7C,CAAA;AACD,eAAO,MAAM,MAAM,GAAI,OAAO,WAAW,KAAG,WAS3C,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,WAAW,EAAE,MAAM,MAAM,KAAG,WA0B1D,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,WAAW,KAAG,WAMtD,CAAA;AAED,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM,KAAG,MAkBnD,CAAA"}

View File

@@ -0,0 +1,229 @@
/**
* Helpers to convert the change Payload into native JS types.
*/
// Adapted from epgsql (src/epgsql_binary.erl), this module licensed under
// 3-clause BSD found here: https://raw.githubusercontent.com/epgsql/epgsql/devel/LICENSE
export var PostgresTypes;
(function (PostgresTypes) {
PostgresTypes["abstime"] = "abstime";
PostgresTypes["bool"] = "bool";
PostgresTypes["date"] = "date";
PostgresTypes["daterange"] = "daterange";
PostgresTypes["float4"] = "float4";
PostgresTypes["float8"] = "float8";
PostgresTypes["int2"] = "int2";
PostgresTypes["int4"] = "int4";
PostgresTypes["int4range"] = "int4range";
PostgresTypes["int8"] = "int8";
PostgresTypes["int8range"] = "int8range";
PostgresTypes["json"] = "json";
PostgresTypes["jsonb"] = "jsonb";
PostgresTypes["money"] = "money";
PostgresTypes["numeric"] = "numeric";
PostgresTypes["oid"] = "oid";
PostgresTypes["reltime"] = "reltime";
PostgresTypes["text"] = "text";
PostgresTypes["time"] = "time";
PostgresTypes["timestamp"] = "timestamp";
PostgresTypes["timestamptz"] = "timestamptz";
PostgresTypes["timetz"] = "timetz";
PostgresTypes["tsrange"] = "tsrange";
PostgresTypes["tstzrange"] = "tstzrange";
})(PostgresTypes || (PostgresTypes = {}));
/**
* Takes an array of columns and an object of string values then converts each string value
* to its mapped type.
*
* @param {{name: String, type: String}[]} columns
* @param {Object} record
* @param {Object} options The map of various options that can be applied to the mapper
* @param {Array} options.skipTypes The array of types that should not be converted
*
* @example convertChangeData([{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age:'33'}, {})
* //=>{ first_name: 'Paul', age: 33 }
*/
export const convertChangeData = (columns, record, options = {}) => {
var _a;
const skipTypes = (_a = options.skipTypes) !== null && _a !== void 0 ? _a : [];
if (!record) {
return {};
}
return Object.keys(record).reduce((acc, rec_key) => {
acc[rec_key] = convertColumn(rec_key, columns, record, skipTypes);
return acc;
}, {});
};
/**
* Converts the value of an individual column.
*
* @param {String} columnName The column that you want to convert
* @param {{name: String, type: String}[]} columns All of the columns
* @param {Object} record The map of string values
* @param {Array} skipTypes An array of types that should not be converted
* @return {object} Useless information
*
* @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, [])
* //=> 33
* @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, ['int4'])
* //=> "33"
*/
export const convertColumn = (columnName, columns, record, skipTypes) => {
const column = columns.find((x) => x.name === columnName);
const colType = column === null || column === void 0 ? void 0 : column.type;
const value = record[columnName];
if (colType && !skipTypes.includes(colType)) {
return convertCell(colType, value);
}
return noop(value);
};
/**
* If the value of the cell is `null`, returns null.
* Otherwise converts the string value to the correct type.
* @param {String} type A postgres column type
* @param {String} value The cell value
*
* @example convertCell('bool', 't')
* //=> true
* @example convertCell('int8', '10')
* //=> 10
* @example convertCell('_int4', '{1,2,3,4}')
* //=> [1,2,3,4]
*/
export const convertCell = (type, value) => {
// if data type is an array
if (type.charAt(0) === '_') {
const dataType = type.slice(1, type.length);
return toArray(value, dataType);
}
// If not null, convert to correct type.
switch (type) {
case PostgresTypes.bool:
return toBoolean(value);
case PostgresTypes.float4:
case PostgresTypes.float8:
case PostgresTypes.int2:
case PostgresTypes.int4:
case PostgresTypes.int8:
case PostgresTypes.numeric:
case PostgresTypes.oid:
return toNumber(value);
case PostgresTypes.json:
case PostgresTypes.jsonb:
return toJson(value);
case PostgresTypes.timestamp:
return toTimestampString(value); // Format to be consistent with PostgREST
case PostgresTypes.abstime: // To allow users to cast it based on Timezone
case PostgresTypes.date: // To allow users to cast it based on Timezone
case PostgresTypes.daterange:
case PostgresTypes.int4range:
case PostgresTypes.int8range:
case PostgresTypes.money:
case PostgresTypes.reltime: // To allow users to cast it based on Timezone
case PostgresTypes.text:
case PostgresTypes.time: // To allow users to cast it based on Timezone
case PostgresTypes.timestamptz: // To allow users to cast it based on Timezone
case PostgresTypes.timetz: // To allow users to cast it based on Timezone
case PostgresTypes.tsrange:
case PostgresTypes.tstzrange:
return noop(value);
default:
// Return the value for remaining types
return noop(value);
}
};
const noop = (value) => {
return value;
};
export const toBoolean = (value) => {
switch (value) {
case 't':
return true;
case 'f':
return false;
default:
return value;
}
};
export const toNumber = (value) => {
if (typeof value === 'string') {
const parsedValue = parseFloat(value);
if (!Number.isNaN(parsedValue)) {
return parsedValue;
}
}
return value;
};
export const toJson = (value) => {
if (typeof value === 'string') {
try {
return JSON.parse(value);
}
catch (_a) {
return value;
}
}
return value;
};
/**
* Converts a Postgres Array into a native JS array
*
* @example toArray('{}', 'int4')
* //=> []
* @example toArray('{"[2021-01-01,2021-12-31)","(2021-01-01,2021-12-32]"}', 'daterange')
* //=> ['[2021-01-01,2021-12-31)', '(2021-01-01,2021-12-32]']
* @example toArray([1,2,3,4], 'int4')
* //=> [1,2,3,4]
*/
export const toArray = (value, type) => {
if (typeof value !== 'string') {
return value;
}
const lastIdx = value.length - 1;
const closeBrace = value[lastIdx];
const openBrace = value[0];
// Confirm value is a Postgres array by checking curly brackets
if (openBrace === '{' && closeBrace === '}') {
let arr;
const valTrim = value.slice(1, lastIdx);
// TODO: find a better solution to separate Postgres array data
try {
arr = JSON.parse('[' + valTrim + ']');
}
catch (_) {
// WARNING: splitting on comma does not cover all edge cases
arr = valTrim ? valTrim.split(',') : [];
}
return arr.map((val) => convertCell(type, val));
}
return value;
};
/**
* Fixes timestamp to be ISO-8601. Swaps the space between the date and time for a 'T'
* See https://github.com/supabase/supabase/issues/18
*
* @example toTimestampString('2019-09-10 00:00:00')
* //=> '2019-09-10T00:00:00'
*/
export const toTimestampString = (value) => {
if (typeof value === 'string') {
return value.replace(' ', 'T');
}
return value;
};
export const httpEndpointURL = (socketUrl) => {
const wsUrl = new URL(socketUrl);
wsUrl.protocol = wsUrl.protocol.replace(/^ws/i, 'http');
wsUrl.pathname = wsUrl.pathname
.replace(/\/+$/, '') // remove all trailing slashes
.replace(/\/socket\/websocket$/i, '') // remove the socket/websocket path
.replace(/\/socket$/i, '') // remove the socket path
.replace(/\/websocket$/i, ''); // remove the websocket path
if (wsUrl.pathname === '' || wsUrl.pathname === '/') {
wsUrl.pathname = '/api/broadcast';
}
else {
wsUrl.pathname = wsUrl.pathname + '/api/broadcast';
}
return wsUrl.href;
};
//# sourceMappingURL=transformers.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export declare const version = "2.99.1";
//# sourceMappingURL=version.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../../src/lib/version.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,OAAO,WAAW,CAAA"}

View File

@@ -0,0 +1,8 @@
// Generated automatically during releases by scripts/update-version-files.ts
// This file provides runtime access to the package version for:
// - HTTP request headers (e.g., X-Client-Info header for API requests)
// - Debugging and support (identifying which version is running)
// - Telemetry and logging (version reporting in errors/analytics)
// - Ensuring build artifacts match the published package version
export const version = '2.99.1';
//# sourceMappingURL=version.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../../src/lib/version.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,gEAAgE;AAChE,uEAAuE;AACvE,iEAAiE;AACjE,kEAAkE;AAClE,iEAAiE;AACjE,MAAM,CAAC,MAAM,OAAO,GAAG,QAAQ,CAAA"}

View File

@@ -0,0 +1,81 @@
export interface WebSocketLike {
readonly CONNECTING: number;
readonly OPEN: number;
readonly CLOSING: number;
readonly CLOSED: number;
readonly readyState: number;
readonly url: string;
readonly protocol: string;
/**
* Closes the socket, optionally providing a close code and reason.
*/
close(code?: number, reason?: string): void;
/**
* Sends data through the socket using the underlying implementation.
*/
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
onopen: ((this: any, ev: Event) => any) | null;
onmessage: ((this: any, ev: MessageEvent) => any) | null;
onclose: ((this: any, ev: CloseEvent) => any) | null;
onerror: ((this: any, ev: Event) => any) | null;
/**
* Registers an event listener on the socket (compatible with browser WebSocket API).
*/
addEventListener(type: string, listener: EventListener): void;
/**
* Removes a previously registered event listener.
*/
removeEventListener(type: string, listener: EventListener): void;
binaryType?: string;
bufferedAmount?: number;
extensions?: string;
dispatchEvent?: (event: Event) => boolean;
}
export interface WebSocketEnvironment {
type: 'native' | 'ws' | 'cloudflare' | 'unsupported';
constructor?: any;
error?: string;
workaround?: string;
}
/**
* Utilities for creating WebSocket instances across runtimes.
*/
export declare class WebSocketFactory {
/**
* Static-only utility prevent instantiation.
*/
private constructor();
private static detectEnvironment;
/**
* Returns the best available WebSocket constructor for the current runtime.
*
* @example
* ```ts
* const WS = WebSocketFactory.getWebSocketConstructor()
* const socket = new WS('wss://realtime.supabase.co/socket')
* ```
*/
static getWebSocketConstructor(): typeof WebSocket;
/**
* Creates a WebSocket using the detected constructor.
*
* @example
* ```ts
* const socket = WebSocketFactory.createWebSocket('wss://realtime.supabase.co/socket')
* ```
*/
static createWebSocket(url: string | URL, protocols?: string | string[]): WebSocketLike;
/**
* Detects whether the runtime can establish WebSocket connections.
*
* @example
* ```ts
* if (!WebSocketFactory.isWebSocketSupported()) {
* console.warn('Falling back to long polling')
* }
* ```
*/
static isWebSocketSupported(): boolean;
}
export default WebSocketFactory;
//# sourceMappingURL=websocket-factory.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"websocket-factory.d.ts","sourceRoot":"","sources":["../../../src/lib/websocket-factory.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IAEzB;;OAEG;IACH,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3C;;OAEG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,GAAG,eAAe,GAAG,IAAI,CAAA;IAEnE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,KAAK,GAAG,CAAC,GAAG,IAAI,CAAA;IAC9C,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,YAAY,KAAK,GAAG,CAAC,GAAG,IAAI,CAAA;IACxD,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,UAAU,KAAK,GAAG,CAAC,GAAG,IAAI,CAAA;IACpD,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,KAAK,GAAG,CAAC,GAAG,IAAI,CAAA;IAE/C;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC7D;;OAEG;IACH,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAGhE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAA;CAC1C;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,YAAY,GAAG,aAAa,CAAA;IACpD,WAAW,CAAC,EAAE,GAAG,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAC3B;;OAEG;IACH,OAAO;IACP,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAmFhC;;;;;;;;OAQG;WACW,uBAAuB,IAAI,OAAO,SAAS;IAYzD;;;;;;;OAOG;WACW,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa;IAK9F;;;;;;;;;OASG;WACW,oBAAoB,IAAI,OAAO;CAQ9C;AAED,eAAe,gBAAgB,CAAA"}

View File

@@ -0,0 +1,127 @@
/**
* Utilities for creating WebSocket instances across runtimes.
*/
export class WebSocketFactory {
/**
* Static-only utility prevent instantiation.
*/
constructor() { }
static detectEnvironment() {
var _a;
if (typeof WebSocket !== 'undefined') {
return { type: 'native', constructor: WebSocket };
}
if (typeof globalThis !== 'undefined' && typeof globalThis.WebSocket !== 'undefined') {
return { type: 'native', constructor: globalThis.WebSocket };
}
if (typeof global !== 'undefined' && typeof global.WebSocket !== 'undefined') {
return { type: 'native', constructor: global.WebSocket };
}
if (typeof globalThis !== 'undefined' &&
typeof globalThis.WebSocketPair !== 'undefined' &&
typeof globalThis.WebSocket === 'undefined') {
return {
type: 'cloudflare',
error: 'Cloudflare Workers detected. WebSocket clients are not supported in Cloudflare Workers.',
workaround: 'Use Cloudflare Workers WebSocket API for server-side WebSocket handling, or deploy to a different runtime.',
};
}
if ((typeof globalThis !== 'undefined' && globalThis.EdgeRuntime) ||
(typeof navigator !== 'undefined' && ((_a = navigator.userAgent) === null || _a === void 0 ? void 0 : _a.includes('Vercel-Edge')))) {
return {
type: 'unsupported',
error: 'Edge runtime detected (Vercel Edge/Netlify Edge). WebSockets are not supported in edge functions.',
workaround: 'Use serverless functions or a different deployment target for WebSocket functionality.',
};
}
// Use dynamic property access to avoid Next.js Edge Runtime static analysis warnings
const _process = globalThis['process'];
if (_process) {
const processVersions = _process['versions'];
if (processVersions && processVersions['node']) {
// Remove 'v' prefix if present and parse the major version
const versionString = processVersions['node'];
const nodeVersion = parseInt(versionString.replace(/^v/, '').split('.')[0]);
// Node.js 22+ should have native WebSocket
if (nodeVersion >= 22) {
// Check if native WebSocket is available (should be in Node.js 22+)
if (typeof globalThis.WebSocket !== 'undefined') {
return { type: 'native', constructor: globalThis.WebSocket };
}
// If not available, user needs to provide it
return {
type: 'unsupported',
error: `Node.js ${nodeVersion} detected but native WebSocket not found.`,
workaround: 'Provide a WebSocket implementation via the transport option.',
};
}
// Node.js < 22 doesn't have native WebSocket
return {
type: 'unsupported',
error: `Node.js ${nodeVersion} detected without native WebSocket support.`,
workaround: 'For Node.js < 22, install "ws" package and provide it via the transport option:\n' +
'import ws from "ws"\n' +
'new RealtimeClient(url, { transport: ws })',
};
}
}
return {
type: 'unsupported',
error: 'Unknown JavaScript runtime without WebSocket support.',
workaround: "Ensure you're running in a supported environment (browser, Node.js, Deno) or provide a custom WebSocket implementation.",
};
}
/**
* Returns the best available WebSocket constructor for the current runtime.
*
* @example
* ```ts
* const WS = WebSocketFactory.getWebSocketConstructor()
* const socket = new WS('wss://realtime.supabase.co/socket')
* ```
*/
static getWebSocketConstructor() {
const env = this.detectEnvironment();
if (env.constructor) {
return env.constructor;
}
let errorMessage = env.error || 'WebSocket not supported in this environment.';
if (env.workaround) {
errorMessage += `\n\nSuggested solution: ${env.workaround}`;
}
throw new Error(errorMessage);
}
/**
* Creates a WebSocket using the detected constructor.
*
* @example
* ```ts
* const socket = WebSocketFactory.createWebSocket('wss://realtime.supabase.co/socket')
* ```
*/
static createWebSocket(url, protocols) {
const WS = this.getWebSocketConstructor();
return new WS(url, protocols);
}
/**
* Detects whether the runtime can establish WebSocket connections.
*
* @example
* ```ts
* if (!WebSocketFactory.isWebSocketSupported()) {
* console.warn('Falling back to long polling')
* }
* ```
*/
static isWebSocketSupported() {
try {
const env = this.detectEnvironment();
return env.type === 'native' || env.type === 'ws';
}
catch (_a) {
return false;
}
}
}
export default WebSocketFactory;
//# sourceMappingURL=websocket-factory.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"websocket-factory.js","sourceRoot":"","sources":["../../../src/lib/websocket-factory.ts"],"names":[],"mappings":"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"}