| import { CHANNEL_EVENTS, CHANNEL_STATES } from './lib/constants' |
| import type { ChannelState } from './lib/constants' |
| import type RealtimeClient from './RealtimeClient' |
| import RealtimePresence, { REALTIME_PRESENCE_LISTEN_EVENTS } from './RealtimePresence' |
| import type { |
| RealtimePresenceJoinPayload, |
| RealtimePresenceLeavePayload, |
| RealtimePresenceState, |
| } from './RealtimePresence' |
| import * as Transformers from './lib/transformers' |
| import { httpEndpointURL } from './lib/transformers' |
| import { normalizeChannelError } from './lib/normalizeChannelError' |
| import ChannelAdapter from './phoenix/channelAdapter' |
| import { ChannelBindingCallback, ChannelOnErrorCallback } from './phoenix/types' |
| import type { Timer } from './phoenix/types' |
| import { RealtimePostgresFilterBuilder } from './RealtimePostgresFilterBuilder' |
| import type { RealtimePostgresChangesFilterOperator } from './RealtimePostgresFilterBuilder' |
|
|
| export type { RealtimePostgresChangesFilterOperator } from './RealtimePostgresFilterBuilder' |
| export { |
| RealtimePostgresFilterBuilder, |
| postgresChangesFilter, |
| } from './RealtimePostgresFilterBuilder' |
|
|
| type ReplayOption = { |
| since: number |
| limit?: number |
| } |
|
|
| export type RealtimeChannelOptions = { |
| config: { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| broadcast?: { |
| self?: boolean |
| ack?: boolean |
| replay?: ReplayOption |
| replication_ready?: boolean |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| presence?: { key?: string; enabled?: boolean } |
| |
| |
| |
| 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}`> = { |
| |
| |
| |
| event: T |
| |
| |
| |
| schema: string |
| |
| |
| |
| table?: string |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| filter?: string | RealtimePostgresFilterBuilder |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| select?: string[] |
| } |
|
|
| export type RealtimeChannelSendResponse = 'ok' | 'timed out' | 'error' | (string & {}) |
|
|
| |
| |
| |
| |
| |
| |
| |
| export type RealtimeSystemPayload = { |
| |
| extension: 'system' | 'postgres_changes' | (string & {}) |
| |
| status: 'ok' | 'error' | (string & {}) |
| |
| message: string |
| |
| channel: string |
| } |
|
|
| export enum REALTIME_POSTGRES_CHANGES_LISTEN_EVENT { |
| ALL = '*', |
| INSERT = 'INSERT', |
| UPDATE = 'UPDATE', |
| DELETE = 'DELETE', |
| } |
|
|
| export enum REALTIME_LISTEN_TYPES { |
| BROADCAST = 'broadcast', |
| PRESENCE = 'presence', |
| POSTGRES_CHANGES = 'postgres_changes', |
| SYSTEM = 'system', |
| } |
|
|
| export enum REALTIME_SUBSCRIBE_STATES { |
| SUBSCRIBED = 'SUBSCRIBED', |
| TIMED_OUT = 'TIMED_OUT', |
| CLOSED = 'CLOSED', |
| CHANNEL_ERROR = 'CHANNEL_ERROR', |
| } |
|
|
| export const REALTIME_CHANNEL_STATES = CHANNEL_STATES |
|
|
| type PostgresChangesFilters = { |
| postgres_changes: { |
| id: string |
| event: string |
| schema?: string |
| table?: string |
| filter?: string |
| select?: string[] |
| }[] |
| } |
|
|
| type Binding = { |
| type: string |
| filter: { [key: string]: any } |
| callback: ChannelBindingCallback |
| ref: number |
| id?: string |
| } |
|
|
| |
| |
| |
| |
| |
| export default class RealtimeChannel { |
| bindings: Record<string, Binding[]> = {} |
| subTopic: string |
| broadcastEndpointURL: string |
| private: boolean |
| presence: RealtimePresence |
| |
| channelAdapter: ChannelAdapter |
|
|
| get state() { |
| return this.channelAdapter.state |
| } |
|
|
| set state(state: ChannelState) { |
| this.channelAdapter.state = state |
| } |
|
|
| get joinedOnce() { |
| return this.channelAdapter.joinedOnce |
| } |
|
|
| get timeout() { |
| return this.socket.timeout |
| } |
|
|
| get joinPush() { |
| return this.channelAdapter.joinPush |
| } |
|
|
| get rejoinTimer(): Timer { |
| return this.channelAdapter.rejoinTimer |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| constructor( |
| |
| public topic: string, |
| public params: RealtimeChannelOptions = { config: {} }, |
| public socket: RealtimeClient |
| ) { |
| this.subTopic = topic.replace(/^realtime:/i, '') |
| this.params.config = { |
| ...{ |
| 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 && this.params.config?.broadcast?.replay) { |
| throw new Error( |
| `tried to use replay on public channel '${this.topic}'. It must be a private channel.` |
| ) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| subscribe( |
| callback?: (status: REALTIME_SUBSCRIBE_STATES, err?: Error) => void, |
| timeout = this.timeout |
| ): RealtimeChannel { |
| if (!this.socket.isConnected()) { |
| this.socket.connect() |
| } |
| if (this.channelAdapter.isClosed()) { |
| const { |
| config: { broadcast, presence, private: isPrivate }, |
| } = this.params |
|
|
| const postgres_changes = this.bindings.postgres_changes?.map((r) => r.filter) ?? [] |
|
|
| const presence_enabled = |
| (!!this.bindings[REALTIME_LISTEN_TYPES.PRESENCE] && |
| this.bindings[REALTIME_LISTEN_TYPES.PRESENCE].length > 0) || |
| this.params.config.presence?.enabled === true |
| const accessTokenPayload: { access_token?: string } = {} |
| const config = { |
| broadcast, |
| presence: { ...presence, enabled: presence_enabled }, |
| postgres_changes, |
| private: isPrivate, |
| } |
|
|
| if (this.socket.accessTokenValue) { |
| accessTokenPayload.access_token = this.socket.accessTokenValue |
| } |
|
|
| this._onError((reason: unknown) => { |
| callback?.(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, normalizeChannelError(reason)) |
| }) |
|
|
| this._onClose(() => callback?.(REALTIME_SUBSCRIBE_STATES.CLOSED)) |
|
|
| this.updateJoinPayload({ ...{ config }, ...accessTokenPayload }) |
|
|
| this._updateFilterMessage() |
|
|
| this.channelAdapter |
| .subscribe(timeout) |
| .receive('ok', async ({ postgres_changes }: PostgresChangesFilters) => { |
| |
| if (!this.socket._isManualToken()) { |
| this.socket.setAuth() |
| } |
| if (postgres_changes === undefined) { |
| callback?.(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) |
| return |
| } |
|
|
| this._updatePostgresBindings(postgres_changes, callback) |
| }) |
| .receive('error', (error: { [key: string]: any }) => { |
| this.state = CHANNEL_STATES.errored |
| const message = Object.values(error).join(', ') || 'error' |
| callback?.(REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR, new Error(message, { cause: error })) |
| }) |
| .receive('timeout', () => { |
| callback?.(REALTIME_SUBSCRIBE_STATES.TIMED_OUT) |
| }) |
| } |
| return this |
| } |
|
|
| private _updatePostgresBindings( |
| postgres_changes: PostgresChangesFilters['postgres_changes'], |
| callback?: (status: REALTIME_SUBSCRIBE_STATES, err?: Error) => void |
| ) { |
| const clientPostgresBindings = this.bindings.postgres_changes |
| const bindingsLen = clientPostgresBindings?.length ?? 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({ |
| ...clientPostgresBinding, |
| id: serverPostgresFilter.id, |
| }) |
| } else { |
| this.unsubscribe() |
| this.state = CHANNEL_STATES.errored |
|
|
| 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<T extends { [key: string]: any } = {}>(): RealtimePresenceState<T> { |
| return this.presence.state as RealtimePresenceState<T> |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async track( |
| payload: { [key: string]: any }, |
| opts: { [key: string]: any } = {} |
| ): Promise<RealtimeChannelSendResponse> { |
| return await this.send( |
| { |
| type: 'presence', |
| event: 'track', |
| payload, |
| }, |
| opts |
| ) |
| } |
|
|
| |
| |
| |
| |
| |
| async untrack(opts: { [key: string]: any } = {}): Promise<RealtimeChannelSendResponse> { |
| return await this.send( |
| { |
| type: 'presence', |
| event: 'untrack', |
| }, |
| opts |
| ) |
| } |
|
|
| |
| |
| |
| |
| 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.PRESENCE}`, |
| filter: { event: '*' }, |
| callback: (payload?: RealtimePresenceJoinPayload<T> | 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 |
| |
| |
| |
| |
| |
| |
| |
| 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 |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| on( |
| type: `${REALTIME_LISTEN_TYPES}`, |
| filter: { event: string; [key: string]: any }, |
| callback: (payload: any) => void |
| ): RealtimeChannel { |
| 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: string, |
| payload: any, |
| opts: { timeout?: number } = {} |
| ): Promise<{ success: true } | { success: false; status: number; error: string }> { |
| 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: Record<string, string> = { |
| 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 as ArrayBuffer | ArrayBufferView) : JSON.stringify(payload), |
| } |
|
|
| const response = await this._fetchWithTimeout( |
| url.toString(), |
| options, |
| opts.timeout ?? 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 {} |
|
|
| return Promise.reject(new Error(errorMessage)) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async send( |
| args: { |
| type: 'broadcast' | 'presence' | 'postgres_changes' |
| event: string |
| payload?: any |
| [key: string]: any |
| }, |
| opts: { [key: string]: any } = {} |
| ): Promise<RealtimeChannelSendResponse> { |
| 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: Record<string, string> = { |
| 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, |
| opts.timeout ?? this.timeout |
| ) |
|
|
| await response.body?.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) => { |
| const push = this.channelAdapter.push(args.type, args, opts.timeout || this.timeout) |
|
|
| if (args.type === 'broadcast' && !this.params?.config?.broadcast?.ack) { |
| resolve('ok') |
| } |
|
|
| push.receive('ok', () => resolve('ok')) |
| push.receive('error', () => resolve('error')) |
| push.receive('timeout', () => resolve('timed out')) |
| }) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| updateJoinPayload(payload: Record<string, any>) { |
| this.channelAdapter.updateJoinPayload(payload) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async unsubscribe(timeout = this.timeout) { |
| return new Promise<RealtimeChannelSendResponse>((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: string, options: { [key: string]: any }, timeout: number) { |
| const controller = new AbortController() |
| const id = setTimeout(() => controller.abort(), timeout) |
|
|
| const response = await this.socket.fetch(url, { |
| ...options, |
| signal: controller.signal, |
| }) |
|
|
| clearTimeout(id) |
|
|
| return response |
| } |
|
|
| |
| _on(type: string, filter: { [key: string]: any }, callback: ChannelBindingCallback) { |
| const typeLower = type.toLocaleLowerCase() |
|
|
| |
| |
| |
| |
| const filterValue = filter?.filter |
| if ( |
| filterValue instanceof RealtimePostgresFilterBuilder || |
| (typeof filterValue === 'object' && |
| filterValue !== null && |
| typeof (filterValue as { build?: unknown }).build === 'function') |
| ) { |
| filter = { ...filter, filter: (filterValue as RealtimePostgresFilterBuilder).build() } |
| } |
|
|
| const ref = this.channelAdapter.on(type, callback) |
|
|
| const binding: 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 |
| } |
|
|
| |
| |
| |
| |
| |
| private _onClose(callback: ChannelBindingCallback) { |
| this.channelAdapter.onClose(callback) |
| } |
|
|
| |
| |
| |
| |
| |
| private _onError(callback: ChannelOnErrorCallback) { |
| this.channelAdapter.onError(callback) |
| } |
|
|
| |
| private _updateFilterMessage() { |
| this.channelAdapter.updateFilterBindings((binding, payload: any, ref) => { |
| const typeLower = binding.event.toLocaleLowerCase() |
|
|
| if (this._notThisChannelEvent(typeLower, ref)) { |
| return false |
| } |
|
|
| const bind = this.bindings[typeLower]?.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 = bind.filter?.event |
| return ( |
| bindId && |
| payload.ids?.includes(bindId) && |
| (bindEvent === '*' || |
| bindEvent?.toLocaleLowerCase() === payload.data?.type.toLocaleLowerCase()) |
| ) |
| } else { |
| const bindEvent = bind?.filter?.event?.toLocaleLowerCase() |
| return bindEvent === '*' || bindEvent === payload?.event?.toLocaleLowerCase() |
| } |
| } else { |
| return bind.type.toLocaleLowerCase() === typeLower |
| } |
| }) |
| } |
|
|
| |
| private _notThisChannelEvent(event: string, ref?: string | null) { |
| const { close, error, leave, join } = CHANNEL_EVENTS |
| const events: string[] = [close, error, leave, join] |
| return ref && events.includes(event) && ref !== this.joinPush.ref |
| } |
|
|
| |
| private _updateFilterTransform() { |
| this.channelAdapter.updatePayloadTransform((event, payload: any, 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 { |
| ...enrichedPayload, |
| ...this._getPayloadRecords(postgresChanges), |
| } |
| } |
|
|
| return payload |
| }) |
| } |
|
|
| copyBindings(other: RealtimeChannel) { |
| 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) |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| private static isFilterValueEqual( |
| serverValue: string | undefined | null, |
| clientValue: string | undefined |
| ): boolean { |
| const normalizedServer = serverValue ?? undefined |
| const normalizedClient = clientValue ?? undefined |
| return normalizedServer === normalizedClient |
| } |
|
|
| |
| private _getPayloadRecords(payload: any) { |
| 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 |
| } |
| } |
|
|