| import { CHANNEL_EVENTS, CHANNEL_STATES } from './lib/constants'; |
| import RealtimePresence from './RealtimePresence'; |
| import * as Transformers from './lib/transformers'; |
| import { httpEndpointURL } from './lib/transformers'; |
| import { normalizeChannelError } from './lib/normalizeChannelError'; |
| import ChannelAdapter from './phoenix/channelAdapter'; |
| import { RealtimePostgresFilterBuilder } from './RealtimePostgresFilterBuilder'; |
| export { RealtimePostgresFilterBuilder, postgresChangesFilter, } from './RealtimePostgresFilterBuilder'; |
| 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; |
| |
| |
| |
| |
| |
| export default class RealtimeChannel { |
| get state() { |
| return this.channelAdapter.state; |
| } |
| set state(state) { |
| this.channelAdapter.state = state; |
| } |
| get joinedOnce() { |
| return this.channelAdapter.joinedOnce; |
| } |
| get timeout() { |
| return this.socket.timeout; |
| } |
| get joinPush() { |
| return this.channelAdapter.joinPush; |
| } |
| get rejoinTimer() { |
| return this.channelAdapter.rejoinTimer; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| constructor( |
| |
| topic, params = { config: {} }, socket) { |
| var _a, _b; |
| this.topic = topic; |
| this.params = params; |
| this.socket = socket; |
| this.bindings = {}; |
| 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.channelAdapter = new ChannelAdapter(this.socket.socketAdapter, topic, this.params); |
| this.presence = new RealtimePresence(this); |
| this._onClose(() => { |
| this.socket._remove(this); |
| }); |
| this._updateFilterTransform(); |
| this.broadcastEndpointURL = httpEndpointURL(this.socket.socketAdapter.endPointURL()); |
| 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 new Error(`tried to use replay on public channel '${this.topic}'. It must be a private channel.`); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| subscribe(callback, timeout = this.timeout) { |
| var _a, _b, _c; |
| if (!this.socket.isConnected()) { |
| this.socket.connect(); |
| } |
| if (this.channelAdapter.isClosed()) { |
| 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((reason) => { |
| callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, normalizeChannelError(reason)); |
| }); |
| this._onClose(() => callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CLOSED)); |
| this.updateJoinPayload(Object.assign({ config }, accessTokenPayload)); |
| this._updateFilterMessage(); |
| this.channelAdapter |
| .subscribe(timeout) |
| .receive('ok', async ({ postgres_changes }) => { |
| |
| if (!this.socket._isManualToken()) { |
| this.socket.setAuth(); |
| } |
| if (postgres_changes === undefined) { |
| callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED); |
| return; |
| } |
| this._updatePostgresBindings(postgres_changes, callback); |
| }) |
| .receive('error', (error) => { |
| this.state = CHANNEL_STATES.errored; |
| const message = Object.values(error).join(', ') || 'error'; |
| callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, new Error(message, { cause: error })); |
| }) |
| .receive('timeout', () => { |
| callback === null || callback === void 0 ? void 0 : callback(REALTIME_SUBSCRIBE_STATES.TIMED_OUT); |
| }); |
| } |
| return this; |
| } |
| _updatePostgresBindings(postgres_changes, callback) { |
| var _a; |
| 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; |
| if (this.state != CHANNEL_STATES.errored && callback) { |
| callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| presenceState() { |
| return this.presence.state; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async track(payload, opts = {}) { |
| return await this.send({ |
| type: 'presence', |
| event: 'track', |
| payload, |
| }, opts); |
| } |
| |
| |
| |
| |
| |
| async untrack(opts = {}) { |
| return await this.send({ |
| type: 'presence', |
| event: 'untrack', |
| }, opts); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| on(type, filter, callback) { |
| const stateCheck = this.channelAdapter.isJoined() || this.channelAdapter.isJoining(); |
| const typeCheck = type === REALTIME_LISTEN_TYPES.PRESENCE || type === REALTIME_LISTEN_TYPES.POSTGRES_CHANGES; |
| if (stateCheck && typeCheck) { |
| this.socket.log('channel', `cannot add \`${type}\` callbacks for ${this.topic} after \`subscribe()\`.`); |
| throw new Error(`cannot add \`${type}\` callbacks for ${this.topic} after \`subscribe()\`.`); |
| } |
| return this._on(type, filter, callback); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async httpSend(event, payload, opts = {}) { |
| var _a; |
| if (payload === undefined || payload === null) { |
| return Promise.reject(new Error('Payload is required for httpSend()')); |
| } |
| const isBinary = payload instanceof ArrayBuffer || ArrayBuffer.isView(payload); |
| const headers = { |
| apikey: this.socket.apiKey ? this.socket.apiKey : '', |
| 'Content-Type': isBinary ? 'application/octet-stream' : 'application/json', |
| }; |
| if (this.socket.accessTokenValue) { |
| headers['Authorization'] = `Bearer ${this.socket.accessTokenValue}`; |
| } |
| const url = new URL(this.broadcastEndpointURL); |
| url.pathname += `/${encodeURIComponent(this.subTopic)}/events/${encodeURIComponent(event)}`; |
| if (this.private) { |
| url.searchParams.set('private', 'true'); |
| } |
| const options = { |
| method: 'POST', |
| headers, |
| body: isBinary ? payload : JSON.stringify(payload), |
| }; |
| const response = await this._fetchWithTimeout(url.toString(), options, (_a = opts.timeout) !== null && _a !== void 0 ? _a : this.timeout); |
| if (response.status === 202) { |
| return { success: true }; |
| } |
| if (response.status === 404) { |
| return Promise.reject(new Error('httpSend() requires Realtime server v2.97.0 or newer; the endpoint returned 404. ' + |
| 'Update your Supabase CLI to a recent version, or upgrade the Realtime server in your self-hosted setup. ' + |
| 'See https://github.com/supabase/supabase-js/blob/master/packages/core/realtime-js/migrations/httpsend-server-version.md')); |
| } |
| let errorMessage = response.statusText; |
| try { |
| const errorBody = await response.json(); |
| errorMessage = errorBody.error || errorBody.message || errorMessage; |
| } |
| catch (_b) { } |
| return Promise.reject(new Error(errorMessage)); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async send(args, opts = {}) { |
| var _a, _b; |
| if (!this.channelAdapter.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 instanceof Error && error.name === 'AbortError') { |
| return 'timed out'; |
| } |
| else { |
| return 'error'; |
| } |
| } |
| } |
| else { |
| return new Promise((resolve) => { |
| var _a, _b, _c; |
| const push = this.channelAdapter.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')); |
| }); |
| } |
| } |
| |
| |
| |
| |
| |
| |
| updateJoinPayload(payload) { |
| this.channelAdapter.updateJoinPayload(payload); |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async unsubscribe(timeout = this.timeout) { |
| return new Promise((resolve) => { |
| this.channelAdapter |
| .unsubscribe(timeout) |
| .receive('ok', () => resolve('ok')) |
| .receive('timeout', () => resolve('timed out')) |
| .receive('error', () => resolve('error')); |
| }); |
| } |
| |
| |
| |
| |
| |
| teardown() { |
| this.channelAdapter.teardown(); |
| } |
| |
| 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; |
| } |
| |
| _on(type, filter, callback) { |
| const typeLower = type.toLocaleLowerCase(); |
| |
| |
| |
| |
| const filterValue = filter === null || filter === void 0 ? void 0 : filter.filter; |
| if (filterValue instanceof RealtimePostgresFilterBuilder || |
| (typeof filterValue === 'object' && |
| filterValue !== null && |
| typeof filterValue.build === 'function')) { |
| filter = Object.assign(Object.assign({}, filter), { filter: filterValue.build() }); |
| } |
| const ref = this.channelAdapter.on(type, callback); |
| const binding = { |
| type: typeLower, |
| filter: filter, |
| callback: callback, |
| ref: ref, |
| }; |
| if (this.bindings[typeLower]) { |
| this.bindings[typeLower].push(binding); |
| } |
| else { |
| this.bindings[typeLower] = [binding]; |
| } |
| this._updateFilterMessage(); |
| return this; |
| } |
| |
| |
| |
| |
| |
| _onClose(callback) { |
| this.channelAdapter.onClose(callback); |
| } |
| |
| |
| |
| |
| |
| _onError(callback) { |
| this.channelAdapter.onError(callback); |
| } |
| |
| _updateFilterMessage() { |
| this.channelAdapter.updateFilterBindings((binding, payload, ref) => { |
| var _a, _b, _c, _d, _e, _f, _g; |
| const typeLower = binding.event.toLocaleLowerCase(); |
| if (this._notThisChannelEvent(typeLower, ref)) { |
| return false; |
| } |
| const bind = (_a = this.bindings[typeLower]) === null || _a === void 0 ? void 0 : _a.find((bind) => bind.ref === binding.ref); |
| if (!bind) { |
| return true; |
| } |
| if (['broadcast', 'presence', 'postgres_changes'].includes(typeLower)) { |
| if ('id' in bind) { |
| const bindId = bind.id; |
| const bindEvent = (_b = bind.filter) === null || _b === void 0 ? void 0 : _b.event; |
| return (bindId && |
| ((_c = payload.ids) === null || _c === void 0 ? void 0 : _c.includes(bindId)) && |
| (bindEvent === '*' || |
| (bindEvent === null || bindEvent === void 0 ? void 0 : bindEvent.toLocaleLowerCase()) === ((_d = payload.data) === null || _d === void 0 ? void 0 : _d.type.toLocaleLowerCase()))); |
| } |
| else { |
| const bindEvent = (_f = (_e = bind === null || bind === void 0 ? void 0 : bind.filter) === null || _e === void 0 ? void 0 : _e.event) === null || _f === void 0 ? void 0 : _f.toLocaleLowerCase(); |
| return bindEvent === '*' || bindEvent === ((_g = payload === null || payload === void 0 ? void 0 : payload.event) === null || _g === void 0 ? void 0 : _g.toLocaleLowerCase()); |
| } |
| } |
| else { |
| return bind.type.toLocaleLowerCase() === typeLower; |
| } |
| }); |
| } |
| |
| _notThisChannelEvent(event, ref) { |
| const { close, error, leave, join } = CHANNEL_EVENTS; |
| const events = [close, error, leave, join]; |
| return ref && events.includes(event) && ref !== this.joinPush.ref; |
| } |
| |
| _updateFilterTransform() { |
| this.channelAdapter.updatePayloadTransform((event, payload, ref) => { |
| if (typeof payload === 'object' && 'ids' in payload) { |
| const postgresChanges = payload.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, |
| }; |
| return Object.assign(Object.assign({}, enrichedPayload), this._getPayloadRecords(postgresChanges)); |
| } |
| return payload; |
| }); |
| } |
| copyBindings(other) { |
| if (this.joinedOnce) { |
| throw new Error('cannot copy bindings into joined channel'); |
| } |
| for (const kind in other.bindings) { |
| for (const binding of other.bindings[kind]) { |
| this._on(binding.type, binding.filter, binding.callback); |
| } |
| } |
| } |
| |
| |
| |
| |
| |
| 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; |
| } |
| |
| _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; |
| } |
| } |
| |