759 lines
28 KiB
JavaScript
759 lines
28 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const websocket_factory_1 = __importDefault(require("./lib/websocket-factory"));
|
|
const constants_1 = require("./lib/constants");
|
|
const serializer_1 = __importDefault(require("./lib/serializer"));
|
|
const timer_1 = __importDefault(require("./lib/timer"));
|
|
const transformers_1 = require("./lib/transformers");
|
|
const RealtimeChannel_1 = __importDefault(require("./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);
|
|
}
|
|
});`;
|
|
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.
|
|
* @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.
|
|
*/
|
|
constructor(endPoint, options) {
|
|
var _a;
|
|
this.accessTokenValue = null;
|
|
this.apiKey = null;
|
|
this.channels = new Array();
|
|
this.endPoint = '';
|
|
this.httpEndpoint = '';
|
|
/** @deprecated headers cannot be set on websocket connections */
|
|
this.headers = {};
|
|
this.params = {};
|
|
this.timeout = constants_1.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.logger = noop;
|
|
this.conn = null;
|
|
this.sendBuffer = [];
|
|
this.serializer = new serializer_1.default();
|
|
this.stateChangeCallbacks = {
|
|
open: [],
|
|
close: [],
|
|
error: [],
|
|
message: [],
|
|
};
|
|
this.accessToken = null;
|
|
this._connectionState = 'disconnected';
|
|
this._wasManualDisconnect = false;
|
|
this._authPromise = null;
|
|
/**
|
|
* Use either custom fetch, if provided, or default fetch to make HTTP requests
|
|
*
|
|
* @internal
|
|
*/
|
|
this._resolveFetch = (customFetch) => {
|
|
let _fetch;
|
|
if (customFetch) {
|
|
_fetch = customFetch;
|
|
}
|
|
else if (typeof fetch === 'undefined') {
|
|
// Node.js environment without native fetch
|
|
_fetch = (...args) => Promise.resolve(`${'@supabase/node-fetch'}`).then(s => __importStar(require(s))).then(({ default: fetch }) => fetch(...args))
|
|
.catch((error) => {
|
|
throw new Error(`Failed to load @supabase/node-fetch: ${error.message}. ` +
|
|
`This is required for HTTP requests in Node.js environments without native fetch.`);
|
|
});
|
|
}
|
|
else {
|
|
_fetch = fetch;
|
|
}
|
|
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}/${constants_1.TRANSPORTS.websocket}`;
|
|
this.httpEndpoint = (0, transformers_1.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');
|
|
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 = websocket_factory_1.default.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: constants_1.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 (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 constants_1.SOCKET_STATES.connecting:
|
|
return constants_1.CONNECTION_STATE.Connecting;
|
|
case constants_1.SOCKET_STATES.open:
|
|
return constants_1.CONNECTION_STATE.Open;
|
|
case constants_1.SOCKET_STATES.closing:
|
|
return constants_1.CONNECTION_STATE.Closing;
|
|
default:
|
|
return constants_1.CONNECTION_STATE.Closed;
|
|
}
|
|
}
|
|
/**
|
|
* Returns `true` is the connection is open.
|
|
*/
|
|
isConnected() {
|
|
return this.connectionState() === constants_1.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';
|
|
}
|
|
channel(topic, params = { config: {} }) {
|
|
const realtimeTopic = `realtime:${topic}`;
|
|
const exists = this.getChannels().find((c) => c.topic === realtimeTopic);
|
|
if (!exists) {
|
|
const chan = new RealtimeChannel_1.default(`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.
|
|
*
|
|
* @param token A JWT string to override the token set on the client.
|
|
*/
|
|
async setAuth(token = null) {
|
|
this._authPromise = this._performAuth(token);
|
|
try {
|
|
await this._authPromise;
|
|
}
|
|
finally {
|
|
this._authPromise = null;
|
|
}
|
|
}
|
|
/**
|
|
* 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.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(constants_1.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.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');
|
|
}
|
|
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') {
|
|
try {
|
|
this.heartbeatCallback(msg.payload.status === 'ok' ? 'ok' : 'error');
|
|
}
|
|
catch (e) {
|
|
this.log('error', 'error in heartbeat callback', e);
|
|
}
|
|
}
|
|
// Handle pending heartbeat reference cleanup
|
|
if (msg.ref && msg.ref === this.pendingHeartbeatRef) {
|
|
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);
|
|
}
|
|
/**
|
|
* Teardown connection and cleanup resources
|
|
* @internal
|
|
*/
|
|
_teardownConnection() {
|
|
if (this.conn) {
|
|
this.conn.onopen = null;
|
|
this.conn.onerror = null;
|
|
this.conn.onmessage = null;
|
|
this.conn.onclose = null;
|
|
this.conn = null;
|
|
}
|
|
this._clearAllTimers();
|
|
this.channels.forEach((channel) => channel.teardown());
|
|
}
|
|
/** @internal */
|
|
_onConnOpen() {
|
|
this._setConnectionState('connected');
|
|
this.log('transport', `connected to ${this.endpointURL()}`);
|
|
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.workerRef.terminate();
|
|
};
|
|
this.workerRef.onmessage = (event) => {
|
|
if (event.data.event === 'keepAlive') {
|
|
this.sendHeartbeat();
|
|
}
|
|
};
|
|
this.workerRef.postMessage({
|
|
event: 'start',
|
|
interval: this.heartbeatIntervalMs,
|
|
});
|
|
}
|
|
/** @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);
|
|
}
|
|
/** @internal */
|
|
_triggerChanError() {
|
|
this.channels.forEach((channel) => channel._trigger(constants_1.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;
|
|
if (token) {
|
|
tokenToSend = token;
|
|
}
|
|
else if (this.accessToken) {
|
|
// Always call the accessToken callback to get fresh token
|
|
tokenToSend = await this.accessToken();
|
|
}
|
|
else {
|
|
tokenToSend = this.accessTokenValue;
|
|
}
|
|
if (this.accessTokenValue != tokenToSend) {
|
|
this.accessTokenValue = tokenToSend;
|
|
this.channels.forEach((channel) => {
|
|
const payload = {
|
|
access_token: tokenToSend,
|
|
version: constants_1.DEFAULT_VERSION,
|
|
};
|
|
tokenToSend && channel.updateJoinPayload(payload);
|
|
if (channel.joinedOnce && channel._isJoined()) {
|
|
channel._push(constants_1.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') {
|
|
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_1.default(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;
|
|
// 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 : constants_1.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;
|
|
// 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 =
|
|
(_g = options === null || options === void 0 ? void 0 : options.reconnectAfterMs) !== null && _g !== void 0 ? _g : ((tries) => {
|
|
return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK;
|
|
});
|
|
this.encode =
|
|
(_h = options === null || options === void 0 ? void 0 : options.encode) !== null && _h !== void 0 ? _h : ((payload, callback) => {
|
|
return callback(JSON.stringify(payload));
|
|
});
|
|
this.decode =
|
|
(_j = options === null || options === void 0 ? void 0 : options.decode) !== null && _j !== void 0 ? _j : this.serializer.decode.bind(this.serializer);
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
exports.default = RealtimeClient;
|
|
//# sourceMappingURL=RealtimeClient.js.map
|