This commit is contained in:
MarcWieland
2026-07-26 23:57:23 +02:00
commit 8b613ec0bc
6300 changed files with 1257646 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Agent instructions for `@supabase/realtime-js`
Listen to realtime updates from a Supabase PostgreSQL database. Handles channels, presence, broadcast, and Postgres CDC subscriptions.
When helping a user work with this package:
- **Usage and quick start**: [`README.md`](./README.md)
- **Full API reference**: [`src/`](./src/) — every public method and type has TSDoc with `@example` blocks. Read the source for the canonical, version-pinned answer.
- **Migration notes**: [`migrations/`](./migrations/) — per-theme markdown files for changes that need caller action.
For broader Supabase guidance (docs site, MCP tools, schema and project workflows), use the centralized [Supabase agent skills](https://github.com/supabase/agent-skills): `npx skills add supabase/agent-skills`. Or pair with the Supabase MCP server via the [Supabase Plugin for AI Coding Agents](https://supabase.com/docs/guides/getting-started/plugins).
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Supabase
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+447
View File
@@ -0,0 +1,447 @@
<br />
<p align="center">
<a href="https://supabase.io">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/supabase/supabase/master/packages/common/assets/images/supabase-logo-wordmark--dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/supabase/supabase/master/packages/common/assets/images/supabase-logo-wordmark--light.svg">
<img alt="Supabase Logo" width="300" src="https://raw.githubusercontent.com/supabase/supabase/master/packages/common/assets/images/logo-preview.jpg">
</picture>
</a>
<h1 align="center">Supabase Realtime JS SDK</h1>
<h3 align="center">Send ephemeral messages with <b>Broadcast</b>, track and synchronize state with <b>Presence</b>, and listen to database changes with <b>Postgres Change Data Capture (CDC)</b>.</h3>
<p align="center">
<a href="https://supabase.com/docs/guides/realtime">Guides</a>
·
<a href="https://supabase.com/docs/reference/javascript">Reference Docs</a>
·
<a href="https://multiplayer.dev">Multiplayer Demo</a>
</p>
</p>
<div align="center">
[![Build](https://github.com/supabase/supabase-js/workflows/CI/badge.svg)](https://github.com/supabase/supabase-js/actions?query=branch%3Amaster)
[![Package](https://img.shields.io/npm/v/@supabase/realtime-js)](https://www.npmjs.com/package/@supabase/realtime-js)
[![License: MIT](https://img.shields.io/npm/l/@supabase/supabase-js)](#license)
[![pkg.pr.new](https://pkg.pr.new/badge/supabase/realtime-js)](https://pkg.pr.new/~/supabase/realtime-js)
</div>
# Overview
This SDK enables you to use the following Supabase Realtime's features:
- **Broadcast**: send ephemeral messages from client to clients with minimal latency. Use cases include sharing cursor positions between users.
- **Presence**: track and synchronize shared state across clients with the help of CRDTs. Use cases include tracking which users are currently viewing a specific webpage.
- **Postgres Change Data Capture (CDC)**: listen for changes in your PostgreSQL database and send them to clients.
# Usage
## Installing the Package
```bash
npm install @supabase/realtime-js
```
## Creating a Channel
```js
import { RealtimeClient } from '@supabase/realtime-js'
const client = new RealtimeClient(REALTIME_URL, {
params: {
apikey: API_KEY,
},
})
const channel = client.channel('test-channel', {})
channel.subscribe((status, err) => {
if (status === 'SUBSCRIBED') {
console.log('Connected!')
}
if (status === 'CHANNEL_ERROR') {
console.log(`There was an error subscribing to channel: ${err.message}`)
}
if (status === 'TIMED_OUT') {
console.log('Realtime server did not respond in time.')
}
if (status === 'CLOSED') {
console.log('Realtime channel was unexpectedly closed.')
}
})
```
### Notes:
- `REALTIME_URL` is `'ws://localhost:4000/socket'` when developing locally and `'wss://<project_ref>.supabase.co/realtime/v1'` when connecting to your Supabase project.
- `API_KEY` is a JWT whose claims must contain `exp` and `role` (existing database role).
- Channel name can be any `string`.
- Setting `private` to `true` means that the client will use RLS to determine if the user can connect or not to a given channel.
## Broadcast
Your client can send and receive messages based on the `event`.
```js
// Setup...
const channel = client.channel('broadcast-test', { broadcast: { ack: false, self: false } })
channel.on('broadcast', { event: 'some-event' }, (payload) => console.log(payload))
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Send message to other clients listening to 'broadcast-test' channel
await channel.send({
type: 'broadcast',
event: 'some-event',
payload: { hello: 'world' },
})
}
})
```
### Notes:
- Setting `ack` to `true` means that the `channel.send` promise will resolve once server replies with acknowledgment that it received the broadcast message request.
- Setting `self` to `true` means that the client will receive the broadcast message it sent out.
### Broadcast Replay
Broadcast Replay enables **private** channels to access messages that were sent earlier. Only messages published via [Broadcast From the Database](https://supabase.com/docs/guides/realtime/broadcast#trigger-broadcast-messages-from-your-database) are available for replay.
You can configure replay with the following options:
- **`since`** (Required): The epoch timestamp in milliseconds, specifying the earliest point from which messages should be retrieved.
- **`limit`** (Optional): The number of messages to return. This must be a positive integer, with a maximum value of 25.
Example:
```typescript
const twelveHours = 12 * 60 * 60 * 1000
const twelveHoursAgo = Date.now() - twelveHours
const config = { private: true, broadcast: { replay: { since: twelveHoursAgo, limit: 10 } } }
supabase
.channel('main:room', { config })
.on('broadcast', { event: 'my_event' }, (payload) => {
if (payload?.meta?.replayed) {
console.log('This message was sent earlier:', payload)
} else {
console.log('This is a new message', payload)
}
// ...
})
.subscribe()
```
## Presence
Your client can track and sync state that's stored in the channel.
```js
// Setup...
const channel = client.channel('presence-test', {
config: {
presence: {
key: '',
},
},
})
channel.on('presence', { event: 'sync' }, () => {
console.log('Online users: ', channel.presenceState())
})
channel.on('presence', { event: 'join' }, ({ newPresences }) => {
console.log('New users have joined: ', newPresences)
})
channel.on('presence', { event: 'leave' }, ({ leftPresences }) => {
console.log('Users have left: ', leftPresences)
})
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
const status = await channel.track({ user_id: 1 })
console.log(status)
}
})
```
> `config.presence.enabled` (set automatically if you add an `.on('presence', ...)` listener)
> controls whether _this_ client receives presence state and updates from other clients —
> without it, `presenceState()` stays empty for you. It does not affect whether other clients
> see you: calling `track()` always makes you visible to subscribers that do have presence
> enabled. On RLS-protected channels, receiving presence updates additionally requires the
> `presence.read` policy to authorize this client.
## Postgres CDC
Receive database changes on the client.
```js
// Setup...
const channel = client.channel('db-changes')
channel.on('postgres_changes', { event: '*', schema: 'public' }, (payload) => {
console.log('All changes in public schema: ', payload)
})
channel.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages' },
(payload) => {
console.log('All inserts in messages table: ', payload)
}
)
channel.on(
'postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'users', filter: 'username=eq.Realtime' },
(payload) => {
console.log('All updates on users table when username is Realtime: ', payload)
}
)
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
console.log('Ready to receive database changes!')
}
})
```
### Filters
The `filter` option accepts **either** a raw string **or** a
`postgresChangesFilter()` builder — both produce the exact same wire format, so
you can mix and match and existing string filters keep working unchanged:
```js
// Raw string — always supported, fully backward compatible
{ event: 'UPDATE', schema: 'public', table: 'users', filter: 'id=eq.1' }
// Builder — type-checked, ergonomic; the SDK serializes it for you
{ event: 'UPDATE', schema: 'public', table: 'users', filter: postgresChangesFilter().eq('id', 1) }
```
A filter is a `column=operator.value` expression evaluated server-side. The
following operators are supported:
| Operator | String form | Builder | Meaning |
| --------------------- | ---------------------------- | -------------------------------------- | --------------------------------- |
| `eq` | `id=eq.1` | `.eq('id', 1)` | equal |
| `neq` | `id=neq.1` | `.neq('id', 1)` | not equal |
| `lt` `lte` `gt` `gte` | `age=gte.18` | `.gte('age', 18)` | comparison |
| `in` | `status=in.(active,pending)` | `.in('status', ['active', 'pending'])` | in list |
| `like` `ilike` | `title=like.%foo%` | `.like('title', '%foo%')` | pattern match (case in/sensitive) |
| `is` | `deleted_at=is.null` | `.is('deleted_at', null)` | `IS null/true/false/unknown` |
| `match` `imatch` | `title=match.^foo` | `.match('title', '^foo')` | POSIX regex match (`~` / `~*`) |
| `isdistinct` | `value=isdistinct.1` | `.isDistinct('value', 1)` | NULL-safe inequality |
**Negation** — prefix any operator with `not.` (string) or use
`.not(column, operator, value)` (builder):
```js
// String
{ event: '*', schema: 'public', table: 'posts', filter: 'status=not.in.(draft,archived)' }
// Builder
{
event: '*',
schema: 'public',
table: 'posts',
filter: postgresChangesFilter().not('status', 'in', ['draft', 'archived']),
}
```
**AND composition** — multiple conditions are combined with commas and applied
as an `AND`. With the builder you just chain calls:
```js
// String
{ event: 'UPDATE', schema: 'public', table: 'orders', filter: 'amount=gt.100,status=in.(open,pending)' }
// Builder — equivalent, chained
{
event: 'UPDATE',
schema: 'public',
table: 'orders',
filter: postgresChangesFilter().gt('amount', 100).in('status', ['open', 'pending']),
}
```
#### Building filters with `postgresChangesFilter()`
The builder (modeled on the `postgrest-js` filter methods) is the recommended,
type-checked way to compose filters — but it is entirely optional; raw strings
remain fully supported.
```js
import { postgresChangesFilter } from '@supabase/realtime-js'
channel.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'orders',
// → 'amount=gt.100,status=not.in.(draft,archived)'
filter: postgresChangesFilter().gt('amount', 100).not('status', 'in', ['draft', 'archived']),
},
(payload) => console.log(payload)
)
```
The builder exposes `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `like`,
`ilike`, `match`, `imatch`, `is`, `isDistinct` and `not`. Call `.build()` if you
need the raw string yourself (e.g. to log it or store it).
**Values are sent verbatim** — the server has no quoting/escaping, so spaces and
quotes are preserved as-is. The server separates conditions by commas outside
parentheses, so a literal comma in a scalar value can't be expressed (commas
inside `in.(…)` are fine); the builder throws on such values rather than
silently producing a broken filter.
> **Note for PostgREST users:** Realtime evaluates filters server-side over a
> single table's WAL — there is no resource embedding (`!inner`, embedded
> filters) and no `or()` grouping. Use `%` (not `*`) for `like`/`ilike`
> wildcards, since filters travel in the WebSocket payload rather than a URL.
### Selecting columns
Use `select` to receive only a subset of columns instead of the full row. This
reduces payload size (helpful for large `bytea`/`jsonb` columns). The selected
columns must be selectable by the subscribing role:
```js
channel.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'users', select: ['id', 'first_name'] },
(payload) => {
// payload.new only contains { id, first_name }
console.log(payload)
}
)
```
## Get All Channels
You can see all the channels that your client has instantiatied.
```js
// Setup...
client.getChannels()
```
## Cleanup
It is highly recommended that you clean up your channels after you're done with them.
- Remove a single channel
```js
// Setup...
const channel = client.channel('some-channel-to-remove')
channel.unsubscribe()
client.removeChannel(channel)
```
- Remove all channels and close the connection
```js
// Setup...
client.removeAllChannels()
client.disconnect()
```
## Development
This package is part of the [Supabase JavaScript monorepo](https://github.com/supabase/supabase-js). To work on this package:
### Building
```bash
# Complete build (from monorepo root)
pnpm nx build realtime-js
# Build with watch mode for development
pnpm nx build realtime-js --watch
# Individual build targets
pnpm nx build:main realtime-js # CommonJS build (dist/main/)
pnpm nx build:module realtime-js # ES Modules build (dist/module/)
# Other useful commands
pnpm nx clean realtime-js # Clean build artifacts
pnpm nx lint realtime-js # Run ESLint
pnpm nx typecheck realtime-js # TypeScript type checking
```
#### Build Outputs
- **CommonJS (`dist/main/`)** - For Node.js environments
- **ES Modules (`dist/module/`)** - For modern bundlers (Webpack, Vite, Rollup)
- **TypeScript definitions (`dist/module/index.d.ts`)** - Type definitions for TypeScript projects
Note: Unlike some other packages, realtime-js doesn't include a UMD build since it's primarily used in Node.js or bundled applications.
#### Validating Package Exports
```bash
# Check if package exports are correctly configured
pnpm nx check-exports realtime-js
```
This command uses ["Are the types wrong?"](https://github.com/arethetypeswrong/arethetypeswrong.github.io) to verify that the package exports work correctly in different environments. Run this before publishing to ensure your package can be imported correctly by all consumers.
### Testing
**No Docker or Supabase instance required!** The realtime-js tests use mocked WebSocket connections, so they're completely self-contained.
```bash
# Run unit tests (from monorepo root)
pnpm nx test realtime-js
# Run tests with coverage report
pnpm nx test:coverage realtime-js
# Run tests in watch mode during development
pnpm nx test:watch realtime-js
```
#### Test Scripts Explained
- **test** - Runs all unit tests once using Vitest
- **test:coverage** - Runs tests and generates coverage report with terminal output
- **test:watch** - Runs tests in interactive watch mode for development
The tests mock WebSocket connections using `mock-socket`, so you can run them anytime without any external dependencies.
### Contributing
We welcome contributions! Please see our [Contributing Guide](../../../CONTRIBUTING.md) for details on how to get started.
For major changes or if you're unsure about something, please open an issue first to discuss your proposed changes.
## Credits
This repo draws heavily from [phoenix-js](https://github.com/phoenixframework/phoenix/tree/master/assets/js/phoenix).
## License
MIT.
+575
View File
@@ -0,0 +1,575 @@
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 { ChannelBindingCallback } from './phoenix/types';
import type { Timer } from './phoenix/types';
import { RealtimePostgresFilterBuilder } from './RealtimePostgresFilterBuilder';
export type { RealtimePostgresChangesFilterOperator } from './RealtimePostgresFilterBuilder';
export { RealtimePostgresFilterBuilder, postgresChangesFilter, } from './RealtimePostgresFilterBuilder';
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
* replication_ready option instructs the server to emit a `system` event once the
* Postgres replication connection backing this channel is established and ready to
* stream changes. Listen for it with `channel.on('system', {}, (payload) => ...)`;
* the payload's `status` is `'ok'` (`message: 'Replication connection established'`)
* on success or `'error'` if the connection is not ready in time.
*/
broadcast?: {
self?: boolean;
ack?: boolean;
replay?: ReplayOption;
replication_ready?: boolean;
};
/**
* key option is used to track presence payload across clients
*
* enabled controls whether this client receives presence state and updates from other
* clients — set it to true (or add an `.on('presence', ...)` listener, which enables it
* automatically) if you want to see who else is present. Without it, this client's
* `presenceState()` stays empty and no `presence` events fire for you, because the
* underlying presence state machine buffers incoming updates until it has received an
* initial snapshot, which is only requested when this flag is set.
*
* It does not gate the other direction: calling `track()` always makes this client
* visible to other subscribers that have presence enabled, regardless of this client's
* own `enabled` setting. On RLS-protected (private) channels, receiving presence updates
* additionally requires the `presence.read` policy to authorize this client.
*/
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 only when the filter is matched.
*
* A filter is a `column=operator.value` expression, e.g. `id=eq.1` or
* `title=like.%foo%`. See {@link RealtimePostgresChangesFilterOperator} for
* the available operators.
*
* Multiple filters can be combined with commas; they are applied as an `AND`
* condition: `filter: 'id=gt.0,id=lt.100'`.
*
* Any operator can be negated with the `not.` prefix: `filter: 'status=not.in.(draft,archived)'`.
*
* The server splits conditions on commas outside quotes/parentheses. To
* include a reserved character (`,`, `(`, `)`) in a value, wrap it in double
* quotes PostgREST-style: `name=eq."a,b"`. The {@link RealtimePostgresFilterBuilder}
* does this quoting for you.
*
* Instead of a raw string you can pass a {@link RealtimePostgresFilterBuilder}
* (via `postgresChangesFilter()`) for a type-checked, ergonomic way to compose filters; the
* SDK serializes it to a string automatically.
*/
filter?: string | RealtimePostgresFilterBuilder;
/**
* Restrict the change payload to a subset of columns instead of receiving the
* full row. Reduces payload size (helpful for large `bytea`/`jsonb` columns)
* and the data transferred per event.
*
* The listed columns must be selectable by the subscribing role.
*
* @example
* channel.on('postgres_changes', {
* event: '*',
* schema: 'public',
* table: 'users',
* select: ['id', 'first_name'],
* }, (payload) => {
* // payload.new only contains { id, first_name }
* })
*/
select?: string[];
};
export type RealtimeChannelSendResponse = 'ok' | 'timed out' | 'error' | (string & {});
/**
* Payload of a `system` event emitted by the server.
*
* Most notably, when a channel is created with `config.broadcast.replication_ready: true`,
* the server sends one of these once the Postgres replication connection is ready
* (`status: 'ok'`) or fails to become ready in time (`status: 'error'`).
*/
export type RealtimeSystemPayload = {
/** The extension that produced the message, e.g. `'system'` or `'postgres_changes'`. */
extension: 'system' | 'postgres_changes' | (string & {});
/** `'ok'` on success, `'error'` on failure. */
status: 'ok' | 'error' | (string & {});
/** Human-readable description, e.g. `'Replication connection established'`. */
message: string;
/** The channel (sub)topic the message refers to. */
channel: string;
};
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: {
readonly closed: "closed";
readonly errored: "errored";
readonly joined: "joined";
readonly joining: "joining";
readonly leaving: "leaving";
};
type Binding = {
type: string;
filter: {
[key: string]: any;
};
callback: ChannelBindingCallback;
ref: number;
id?: string;
};
/** 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: Record<string, Binding[]>;
subTopic: string;
broadcastEndpointURL: string;
private: boolean;
presence: RealtimePresence;
get state(): ChannelState;
set state(state: ChannelState);
get joinedOnce(): boolean;
get timeout(): number;
get joinPush(): import("@supabase/phoenix").Push;
get rejoinTimer(): Timer;
/**
* 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.
*
* @category Realtime
*
* @example Using supabase-js (recommended)
* ```ts
* import { createClient } from '@supabase/supabase-js'
*
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
* const channel = supabase.channel('room1')
* channel
* .on('broadcast', { event: 'cursor-pos' }, (payload) => console.log(payload))
* .subscribe()
* ```
*
* @example Standalone import for bundle-sensitive environments
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'your-publishable-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.
*
* The optional `callback` receives a `status` and, on failure, an `err` argument.
* Log the full `err` so its `cause`, `name`, and any structured fields aren't hidden
* behind `err.message`.
*
* @category Realtime
*
* @example Handling errors
* ```js
* supabase.channel('room1').subscribe((status, err) => {
* if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
* // Log the full error: its `cause` often holds the underlying reason.
* console.error(status, err)
* }
* })
* ```
*/
subscribe(callback?: (status: REALTIME_SUBSCRIBE_STATES, err?: Error) => void, timeout?: number): RealtimeChannel;
private _updatePostgresBindings;
/**
* 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.
*
* @category Realtime
*/
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.
*
* Tracking makes this client visible to other subscribers immediately, regardless of this
* channel's `config.presence.enabled` setting or whether it has a `presence` listener — that
* flag only affects whether *this* client receives presence updates from others (and, on
* RLS-protected channels, whether it's authorized to do so).
*
* @category Realtime
*/
track(payload: {
[key: string]: any;
}, opts?: {
[key: string]: any;
}): Promise<RealtimeChannelSendResponse>;
/**
* Removes the current presence state for this client.
*
* @category Realtime
*/
untrack(opts?: {
[key: string]: any;
}): Promise<RealtimeChannelSendResponse>;
/**
* Listen for presence events on this channel — when peers join, leave, or
* sync presence state.
*/
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;
/**
* Listen for Postgres database changes (insert / update / delete) streamed
* over this channel.
*/
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;
/**
* Listen for broadcast messages sent on this channel.
*
* @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;
/**
* Listen for `system` events on this channel.
*
* The payload follows the {@link RealtimeSystemPayload} shape. Opt in to the replication-ready
* notification with `config.broadcast.replication_ready: true` when creating the channel, then
* watch for `payload.status === 'ok'` to know the Postgres replication connection is ready.
*
* @example Know when the replication connection is ready
* ```js
* const channel = supabase.channel('room1', {
* config: { broadcast: { replication_ready: true } },
* })
*
* channel
* .on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, (payload) => {
* console.log('Change received!', payload)
* })
* .on('system', {}, (payload) => {
* if (payload.extension === 'system' && payload.status === 'ok') {
* console.log('Replication connection is ready:', payload.message)
* }
* })
* .subscribe()
* ```
*/
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.
*
* Payloads that are `ArrayBuffer` or `ArrayBufferView` (e.g. `Uint8Array`) are sent as
* `application/octet-stream`; all other payloads are JSON-encoded.
*
* @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
*
* @category Realtime
*/
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
*
* @category Realtime
*
* @remarks
* - When using REST you don't need to subscribe to the channel
* - REST calls are only available from 2.37.0 onwards
* - If you create a channel only to send a REST broadcast, remove it from
* the client when the send completes
*
* @example Send a message via websocket
* ```js
* const channel = supabase.channel('room1')
*
* channel.subscribe((status) => {
* if (status === 'SUBSCRIBED') {
* channel.send({
* type: 'broadcast',
* event: 'cursor-pos',
* payload: { x: Math.random(), y: Math.random() },
* })
* }
* })
* ```
*
* @exampleResponse Send a message via websocket
* ```js
* ok | timed out | error
* ```
*
* @example Send a message via REST
* ```js
* const channel = supabase.channel('room1')
*
* try {
* await channel.httpSend('cursor-pos', { x: Math.random(), y: Math.random() })
* } finally {
* await supabase.removeChannel(channel)
* }
* ```
*/
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.
*
* @category Realtime
*/
updateJoinPayload(payload: Record<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!") )
*
* @category Realtime
*/
unsubscribe(timeout?: number): Promise<RealtimeChannelSendResponse>;
/**
* Destroys and stops related timers.
*
* @category Realtime
*/
teardown(): void;
copyBindings(other: RealtimeChannel): void;
}
//# sourceMappingURL=RealtimeChannel.d.ts.map
File diff suppressed because one or more lines are too long
+765
View File
@@ -0,0 +1,765 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.REALTIME_CHANNEL_STATES = exports.REALTIME_SUBSCRIBE_STATES = exports.REALTIME_LISTEN_TYPES = exports.REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = exports.postgresChangesFilter = exports.RealtimePostgresFilterBuilder = void 0;
const tslib_1 = require("tslib");
const constants_1 = require("./lib/constants");
const RealtimePresence_1 = tslib_1.__importDefault(require("./RealtimePresence"));
const Transformers = tslib_1.__importStar(require("./lib/transformers"));
const transformers_1 = require("./lib/transformers");
const normalizeChannelError_1 = require("./lib/normalizeChannelError");
const channelAdapter_1 = tslib_1.__importDefault(require("./phoenix/channelAdapter"));
const RealtimePostgresFilterBuilder_1 = require("./RealtimePostgresFilterBuilder");
var RealtimePostgresFilterBuilder_2 = require("./RealtimePostgresFilterBuilder");
Object.defineProperty(exports, "RealtimePostgresFilterBuilder", { enumerable: true, get: function () { return RealtimePostgresFilterBuilder_2.RealtimePostgresFilterBuilder; } });
Object.defineProperty(exports, "postgresChangesFilter", { enumerable: true, get: function () { return RealtimePostgresFilterBuilder_2.postgresChangesFilter; } });
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 || (exports.REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = {}));
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 || (exports.REALTIME_LISTEN_TYPES = REALTIME_LISTEN_TYPES = {}));
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 || (exports.REALTIME_SUBSCRIBE_STATES = REALTIME_SUBSCRIBE_STATES = {}));
exports.REALTIME_CHANNEL_STATES = constants_1.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.
*/
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;
}
/**
* 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.
*
* @category Realtime
*
* @example Using supabase-js (recommended)
* ```ts
* import { createClient } from '@supabase/supabase-js'
*
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
* const channel = supabase.channel('room1')
* channel
* .on('broadcast', { event: 'cursor-pos' }, (payload) => console.log(payload))
* .subscribe()
* ```
*
* @example Standalone import for bundle-sensitive environments
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'your-publishable-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.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_1.default(this.socket.socketAdapter, topic, this.params);
this.presence = new RealtimePresence_1.default(this);
this._onClose(() => {
this.socket._remove(this);
});
this._updateFilterTransform();
this.broadcastEndpointURL = (0, transformers_1.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 registers your client with the server.
*
* The optional `callback` receives a `status` and, on failure, an `err` argument.
* Log the full `err` so its `cause`, `name`, and any structured fields aren't hidden
* behind `err.message`.
*
* @category Realtime
*
* @example Handling errors
* ```js
* supabase.channel('room1').subscribe((status, err) => {
* if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
* // Log the full error: its `cause` often holds the underlying reason.
* console.error(status, err)
* }
* })
* ```
*/
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, (0, normalizeChannelError_1.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 }) => {
// 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;
}
this._updatePostgresBindings(postgres_changes, callback);
})
.receive('error', (error) => {
this.state = constants_1.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 = constants_1.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 != constants_1.CHANNEL_STATES.errored && callback) {
callback(REALTIME_SUBSCRIBE_STATES.SUBSCRIBED);
}
}
/**
* 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.
*
* @category Realtime
*/
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.
*
* Tracking makes this client visible to other subscribers immediately, regardless of this
* channel's `config.presence.enabled` setting or whether it has a `presence` listener — that
* flag only affects whether *this* client receives presence updates from others (and, on
* RLS-protected channels, whether it's authorized to do so).
*
* @category Realtime
*/
async track(payload, opts = {}) {
return await this.send({
type: 'presence',
event: 'track',
payload,
}, opts);
}
/**
* Removes the current presence state for this client.
*
* @category Realtime
*/
async untrack(opts = {}) {
return await this.send({
type: 'presence',
event: 'untrack',
}, opts);
}
/**
* Listen to realtime events on this channel.
* @category Realtime
*
* @remarks
* - By default, Broadcast and Presence are enabled for all projects.
* - By default, listening to database changes is disabled for new projects due to database performance and security concerns. You can turn it on by managing Realtime's [replication](/docs/guides/api#realtime-api-overview).
* - You can receive the "previous" data for updates and deletes by setting the table's `REPLICA IDENTITY` to `FULL` (e.g., `ALTER TABLE your_table REPLICA IDENTITY FULL;`).
* - Row level security is not applied to delete statements. When RLS is enabled and replica identity is set to full, only the primary key is sent to clients.
*
* @example Listen to broadcast messages
* ```js
* const channel = supabase.channel("room1")
*
* channel.on("broadcast", { event: "cursor-pos" }, (payload) => {
* console.log("Cursor position received!", payload);
* }).subscribe((status) => {
* if (status === "SUBSCRIBED") {
* channel.send({
* type: "broadcast",
* event: "cursor-pos",
* payload: { x: Math.random(), y: Math.random() },
* });
* }
* });
* ```
*
* @example Listen to presence sync
* ```js
* const channel = supabase.channel('room1')
* channel
* .on('presence', { event: 'sync' }, () => {
* console.log('Synced presence state: ', channel.presenceState())
* })
* .subscribe(async (status) => {
* if (status === 'SUBSCRIBED') {
* await channel.track({ online_at: new Date().toISOString() })
* }
* })
* ```
*
* @example Listen to presence join
* ```js
* const channel = supabase.channel('room1')
* channel
* .on('presence', { event: 'join' }, ({ newPresences }) => {
* console.log('Newly joined presences: ', newPresences)
* })
* .subscribe(async (status) => {
* if (status === 'SUBSCRIBED') {
* await channel.track({ online_at: new Date().toISOString() })
* }
* })
* ```
*
* @example Listen to presence leave
* ```js
* const channel = supabase.channel('room1')
* channel
* .on('presence', { event: 'leave' }, ({ leftPresences }) => {
* console.log('Newly left presences: ', leftPresences)
* })
* .subscribe(async (status) => {
* if (status === 'SUBSCRIBED') {
* await channel.track({ online_at: new Date().toISOString() })
* await channel.untrack()
* }
* })
* ```
*
* @example Listen to all database changes
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: '*', schema: '*' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @example Listen to a specific table
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: '*', schema: 'public', table: 'countries' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @example Listen to inserts
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'countries' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @exampleDescription Listen to updates
* By default, Supabase will send only the updated record. If you want to receive the previous values as well you can
* enable full replication for the table you are listening to:
*
* ```sql
* alter table "your_table" replica identity full;
* ```
*
* @example Listen to updates
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'countries' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @exampleDescription Listen to deletes
* By default, Supabase does not send deleted records. If you want to receive the deleted record you can
* enable full replication for the table you are listening to:
*
* ```sql
* alter table "your_table" replica identity full;
* ```
*
* @example Listen to deletes
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'countries' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @exampleDescription Listen to multiple events
* You can chain listeners if you want to listen to multiple events for each table.
*
* @example Listen to multiple events
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'countries' }, handleRecordInserted)
* .on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'countries' }, handleRecordDeleted)
* .subscribe()
* ```
*
* @exampleDescription Listen to row level changes
* You can listen to individual rows using the format `{table}:{col}=eq.{val}` - where `{col}` is the column name, and `{val}` is the value which you want to match.
*
* @example Listen to row level changes
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'countries', filter: 'id=eq.200' }, handleRecordUpdated)
* .subscribe()
* ```
*/
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);
}
/**
* 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.
*
* Payloads that are `ArrayBuffer` or `ArrayBufferView` (e.g. `Uint8Array`) are sent as
* `application/octet-stream`; all other payloads are JSON-encoded.
*
* @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
*
* @category Realtime
*/
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));
}
/**
* 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
*
* @category Realtime
*
* @remarks
* - When using REST you don't need to subscribe to the channel
* - REST calls are only available from 2.37.0 onwards
* - If you create a channel only to send a REST broadcast, remove it from
* the client when the send completes
*
* @example Send a message via websocket
* ```js
* const channel = supabase.channel('room1')
*
* channel.subscribe((status) => {
* if (status === 'SUBSCRIBED') {
* channel.send({
* type: 'broadcast',
* event: 'cursor-pos',
* payload: { x: Math.random(), y: Math.random() },
* })
* }
* })
* ```
*
* @exampleResponse Send a message via websocket
* ```js
* ok | timed out | error
* ```
*
* @example Send a message via REST
* ```js
* const channel = supabase.channel('room1')
*
* try {
* await channel.httpSend('cursor-pos', { x: Math.random(), y: Math.random() })
* } finally {
* await supabase.removeChannel(channel)
* }
* ```
*/
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'));
});
}
}
/**
* 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.
*
* @category Realtime
*/
updateJoinPayload(payload) {
this.channelAdapter.updateJoinPayload(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!") )
*
* @category Realtime
*/
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'));
});
}
/**
* Destroys and stops related timers.
*
* @category Realtime
*/
teardown() {
this.channelAdapter.teardown();
}
/** @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 */
_on(type, filter, callback) {
const typeLower = type.toLocaleLowerCase();
// Serialize a postgres_changes filter builder into its string form so the
// rest of the pipeline (join payload, server binding match) sees a string.
// Duck-type `build()` in addition to `instanceof` so a builder constructed
// against a duplicate copy of the package (separate module realm) still works.
const filterValue = filter === null || filter === void 0 ? void 0 : filter.filter;
if (filterValue instanceof RealtimePostgresFilterBuilder_1.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;
}
/**
* Registers a callback that will be executed when the channel closes.
*
* @internal
*/
_onClose(callback) {
this.channelAdapter.onClose(callback);
}
/**
* Registers a callback that will be executed when the channel encounteres an error.
*
* @internal
*/
_onError(callback) {
this.channelAdapter.onError(callback);
}
/** @internal */
_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;
}
});
}
/** @internal */
_notThisChannelEvent(event, ref) {
const { close, error, leave, join } = constants_1.CHANNEL_EVENTS;
const events = [close, error, leave, join];
return ref && events.includes(event) && ref !== this.joinPush.ref;
}
/** @internal */
_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);
}
}
}
/**
* 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 */
_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;
}
}
exports.default = RealtimeChannel;
//# sourceMappingURL=RealtimeChannel.js.map
File diff suppressed because one or more lines are too long
+274
View File
@@ -0,0 +1,274 @@
import { WebSocketLike } from './lib/websocket-factory';
import Serializer from './lib/serializer';
import RealtimeChannel from './RealtimeChannel';
import type { RealtimeChannelOptions } from './RealtimeChannel';
import type { HeartbeatCallback, Encode, Decode, Timer, Vsn } from './phoenix/types';
type Fetch = typeof fetch;
export type LogLevel = 'info' | 'warn' | 'error' | (string & {});
export type RealtimeMessage = {
topic: string;
event: string;
payload: any;
ref: string;
join_ref?: string;
};
export type RealtimeRemoveChannelResponse = 'ok' | 'timed out' | 'error' | (string & {});
export type HeartbeatStatus = 'sent' | 'ok' | 'error' | 'timeout' | 'disconnected' | (string & {});
export type HeartbeatTimer = ReturnType<typeof setTimeout> | undefined;
/**
* 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 type RealtimeClientOptions = {
transport?: WebSocketLikeConstructor;
timeout?: number;
heartbeatIntervalMs?: number;
heartbeatCallback?: (status: HeartbeatStatus, latency?: number) => void;
vsn?: string;
logger?: (kind: string, msg: string, data?: any) => void;
encode?: Encode<void>;
decode?: Decode<void>;
reconnectAfterMs?: (tries: number) => number;
headers?: {
[key: string]: string;
};
params?: {
[key: string]: any;
};
log_level?: LogLevel;
logLevel?: LogLevel;
fetch?: Fetch;
worker?: boolean;
workerUrl?: string;
accessToken?: () => Promise<string | null>;
disconnectOnEmptyChannelsAfterMs?: number;
/**
* Storage compatible object used by the underlying socket for longpoll fallback history.
* Provide a custom implementation in environments where reading `globalThis.sessionStorage`
* throws (sandboxed iframes, in-app webviews, "block third-party storage" privacy modes).
* Defaults to `globalThis.sessionStorage` when accessible, otherwise an in-memory store.
*/
sessionStorage?: Storage;
};
export default class RealtimeClient {
channels: RealtimeChannel[];
accessTokenValue: string | null;
accessToken: (() => Promise<string | null>) | null;
apiKey: string | null;
httpEndpoint: string;
/** @deprecated headers cannot be set on websocket connections */
headers?: {
[key: string]: string;
};
params?: {
[key: string]: string;
};
ref: number;
logLevel?: LogLevel;
fetch: Fetch;
worker?: boolean;
workerUrl?: string;
workerRef?: Worker;
serializer: Serializer;
get endPoint(): string;
get timeout(): number;
get transport(): WebSocketLikeConstructor;
get heartbeatCallback(): HeartbeatCallback;
get heartbeatIntervalMs(): number;
get heartbeatTimer(): HeartbeatTimer;
get pendingHeartbeatRef(): string | null;
get reconnectTimer(): Timer;
get vsn(): Vsn;
get encode(): Encode<void>;
get decode(): Decode<void>;
get reconnectAfterMs(): (tries: number) => number;
get sendBuffer(): (() => void)[];
get stateChangeCallbacks(): {
open: [string, Function][];
close: [string, Function][];
error: [string, Function][];
message: [string, Function][];
};
private _manuallySetToken;
private _authPromise;
private _workerHeartbeatTimer;
private _pendingWorkerHeartbeatRef;
private _pendingDisconnectTimer;
private _disconnectOnEmptyChannelsAfterMs;
/**
* Initializes the Socket.
*
* @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (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".
*
* @category Realtime
*
* @example Using supabase-js (recommended)
* ```ts
* import { createClient } from '@supabase/supabase-js'
*
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
* const channel = supabase.channel('room1')
* channel
* .on('broadcast', { event: 'cursor-pos' }, (payload) => console.log(payload))
* .subscribe()
* ```
*
* @example Standalone import for bundle-sensitive environments
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'your-publishable-key' },
* })
* client.connect()
* ```
*/
constructor(endPoint: string, options?: RealtimeClientOptions);
/**
* Connects the socket, unless already connected.
*
* @category Realtime
*/
connect(): void;
/**
* Returns the URL of the websocket.
* @returns string The URL of the websocket.
*
* @category Realtime
*/
endpointURL(): string;
/**
* Disconnects the socket.
*
* @param code A numeric status code to send on disconnect.
* @param reason A custom reason for the disconnect.
*
* @category Realtime
*/
disconnect(code?: number, reason?: string): Promise<"ok" | "timeout">;
/**
* Returns all created channels
*
* @category Realtime
*/
getChannels(): RealtimeChannel[];
/**
* Unsubscribes, removes and tears down a single channel
* @param channel A RealtimeChannel instance
*
* @category Realtime
*/
removeChannel(channel: RealtimeChannel): Promise<RealtimeRemoveChannelResponse>;
/**
* Unsubscribes, removes and tears down all channels
*
* @category Realtime
*/
removeAllChannels(): Promise<RealtimeRemoveChannelResponse[]>;
/**
* Logs the message.
*
* For customized logging, `this.logger` can be overridden in Client constructor.
*
* @category Realtime
*/
log(kind: string, msg: string, data?: any): void;
/**
* Returns the current state of the socket.
*
* @category Realtime
*/
connectionState(): import("./lib/constants").ConnectionState;
/**
* Returns `true` is the connection is open.
*
* @category Realtime
*/
isConnected(): boolean;
/**
* Returns `true` if the connection is currently connecting.
*
* @category Realtime
*/
isConnecting(): boolean;
/**
* Returns `true` if the connection is currently disconnecting.
*
* @category Realtime
*/
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.
*
* @category Realtime
*/
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.
*
* @category Realtime
*/
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 Setting the authorization header
* // 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()
*
* @category Realtime
*/
setAuth(token?: string | null): Promise<void>;
/**
* Sends a heartbeat message if the socket is connected.
*
* @category Realtime
*/
sendHeartbeat(): Promise<void>;
/**
* Sets a callback that receives lifecycle events for internal heartbeat messages.
* Useful for instrumenting connection health (e.g. sent/ok/timeout).
*
* @category Realtime
*/
onHeartbeat(callback: HeartbeatCallback): void;
}
export {};
//# sourceMappingURL=RealtimeClient.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"RealtimeClient.d.ts","sourceRoot":"","sources":["../../src/RealtimeClient.ts"],"names":[],"mappings":"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"}
+685
View File
@@ -0,0 +1,685 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const websocket_factory_1 = tslib_1.__importDefault(require("./lib/websocket-factory"));
const constants_1 = require("./lib/constants");
const serializer_1 = tslib_1.__importDefault(require("./lib/serializer"));
const transformers_1 = require("./lib/transformers");
const RealtimeChannel_1 = tslib_1.__importDefault(require("./RealtimeChannel"));
const socketAdapter_1 = tslib_1.__importDefault(require("./phoenix/socketAdapter"));
// 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;
function createMemorySessionStorage() {
const store = new Map();
return {
get length() {
return store.size;
},
clear() {
store.clear();
},
getItem(key) {
return store.has(key) ? store.get(key) : null;
},
key(index) {
var _a;
return (_a = Array.from(store.keys())[index]) !== null && _a !== void 0 ? _a : null;
},
removeItem(key) {
store.delete(key);
},
setItem(key, value) {
store.set(key, String(value));
},
};
}
function resolveSessionStorage() {
try {
if (typeof globalThis !== 'undefined' && globalThis.sessionStorage) {
return globalThis.sessionStorage;
}
}
catch (_a) {
// Property access on `sessionStorage` itself throws in restricted-storage browsers.
}
return createMemorySessionStorage();
}
const WORKER_SCRIPT = `
addEventListener("message", (e) => {
if (e.data.event === "start") {
setInterval(() => postMessage({ event: "keepAlive" }), e.data.interval);
}
});`;
class RealtimeClient {
get endPoint() {
return this.socketAdapter.endPoint;
}
get timeout() {
return this.socketAdapter.timeout;
}
get transport() {
return this.socketAdapter.transport;
}
get heartbeatCallback() {
return this.socketAdapter.heartbeatCallback;
}
get heartbeatIntervalMs() {
return this.socketAdapter.heartbeatIntervalMs;
}
get heartbeatTimer() {
if (this.worker) {
return this._workerHeartbeatTimer;
}
return this.socketAdapter.heartbeatTimer;
}
get pendingHeartbeatRef() {
if (this.worker) {
return this._pendingWorkerHeartbeatRef;
}
return this.socketAdapter.pendingHeartbeatRef;
}
get reconnectTimer() {
return this.socketAdapter.reconnectTimer;
}
get vsn() {
return this.socketAdapter.vsn;
}
get encode() {
return this.socketAdapter.encode;
}
get decode() {
return this.socketAdapter.decode;
}
get reconnectAfterMs() {
return this.socketAdapter.reconnectAfterMs;
}
get sendBuffer() {
return this.socketAdapter.sendBuffer;
}
get stateChangeCallbacks() {
return this.socketAdapter.stateChangeCallbacks;
}
/**
* Initializes the Socket.
*
* @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (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".
*
* @category Realtime
*
* @example Using supabase-js (recommended)
* ```ts
* import { createClient } from '@supabase/supabase-js'
*
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
* const channel = supabase.channel('room1')
* channel
* .on('broadcast', { event: 'cursor-pos' }, (payload) => console.log(payload))
* .subscribe()
* ```
*
* @example Standalone import for bundle-sensitive environments
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'your-publishable-key' },
* })
* client.connect()
* ```
*/
constructor(endPoint, options) {
var _a;
this.channels = new Array();
this.accessTokenValue = null;
this.accessToken = null;
this.apiKey = null;
this.httpEndpoint = '';
/** @deprecated headers cannot be set on websocket connections */
this.headers = {};
this.params = {};
this.ref = 0;
this.serializer = new serializer_1.default();
this._manuallySetToken = false;
this._authPromise = null;
this._workerHeartbeatTimer = undefined;
this._pendingWorkerHeartbeatRef = null;
this._pendingDisconnectTimer = null;
this._disconnectOnEmptyChannelsAfterMs = 0;
/**
* 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;
const socketAdapterOptions = this._initializeOptions(options);
this.socketAdapter = new socketAdapter_1.default(endPoint, socketAdapterOptions);
this.httpEndpoint = (0, transformers_1.httpEndpointURL)(endPoint);
this.fetch = this._resolveFetch(options === null || options === void 0 ? void 0 : options.fetch);
}
/**
* Connects the socket, unless already connected.
*
* @category Realtime
*/
connect() {
// Skip if already connecting, disconnecting, or connected
if (this.isConnecting() || this.isDisconnecting() || this.isConnected()) {
return;
}
// 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');
}
this._setupConnectionHandlers();
try {
this.socketAdapter.connect();
}
catch (error) {
const errorMessage = error.message;
throw new Error(`WebSocket not available: ${errorMessage}`);
}
this._handleNodeJsRaceCondition();
}
/**
* Returns the URL of the websocket.
* @returns string The URL of the websocket.
*
* @category Realtime
*/
endpointURL() {
return this.socketAdapter.endPointURL();
}
/**
* Disconnects the socket.
*
* @param code A numeric status code to send on disconnect.
* @param reason A custom reason for the disconnect.
*
* @category Realtime
*/
async disconnect(code, reason) {
this._cancelPendingDisconnect();
if (this.isDisconnecting()) {
return 'ok';
}
return await this.socketAdapter.disconnect(() => {
clearInterval(this._workerHeartbeatTimer);
this._terminateWorker();
}, code, reason);
}
/**
* Returns all created channels
*
* @category Realtime
*/
getChannels() {
return this.channels;
}
/**
* Unsubscribes, removes and tears down a single channel
* @param channel A RealtimeChannel instance
*
* @category Realtime
*/
async removeChannel(channel) {
const status = await channel.unsubscribe();
if (status === 'ok') {
channel.teardown();
}
return status;
}
/**
* Unsubscribes, removes and tears down all channels
*
* @category Realtime
*/
async removeAllChannels() {
const promises = this.channels.map(async (channel) => {
const result = await channel.unsubscribe();
channel.teardown();
return result;
});
const result = await Promise.all(promises);
await this.disconnect();
return result;
}
/**
* Logs the message.
*
* For customized logging, `this.logger` can be overridden in Client constructor.
*
* @category Realtime
*/
log(kind, msg, data) {
this.socketAdapter.log(kind, msg, data);
}
/**
* Returns the current state of the socket.
*
* @category Realtime
*/
connectionState() {
return this.socketAdapter.connectionState() || constants_1.CONNECTION_STATE.closed;
}
/**
* Returns `true` is the connection is open.
*
* @category Realtime
*/
isConnected() {
return this.socketAdapter.isConnected();
}
/**
* Returns `true` if the connection is currently connecting.
*
* @category Realtime
*/
isConnecting() {
return this.socketAdapter.isConnecting();
}
/**
* Returns `true` if the connection is currently disconnecting.
*
* @category Realtime
*/
isDisconnecting() {
return this.socketAdapter.isDisconnecting();
}
/**
* 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.
*
* @category Realtime
*/
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._cancelPendingDisconnect();
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.
*
* @category Realtime
*/
push(data) {
this.socketAdapter.push(data);
}
/**
* 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 Setting the authorization header
* // 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()
*
* @category Realtime
*/
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.
*
* @category Realtime
*/
async sendHeartbeat() {
this.socketAdapter.sendHeartbeat();
}
/**
* Sets a callback that receives lifecycle events for internal heartbeat messages.
* Useful for instrumenting connection health (e.g. sent/ok/timeout).
*
* @category Realtime
*/
onHeartbeat(callback) {
this.socketAdapter.heartbeatCallback = this._wrapHeartbeatCallback(callback);
}
/**
* Return the next message ref, accounting for overflows
*
* @internal
*/
_makeRef() {
return this.socketAdapter.makeRef();
}
/**
* Removes a channel from RealtimeClient
*
* @param channel An open subscription.
*
* @internal
*/
_remove(channel) {
this.channels = this.channels.filter((c) => c.topic !== channel.topic);
if (this.channels.length === 0) {
this.log('transport', 'no channels remaining, scheduling disconnect');
this._schedulePendingDisconnect();
}
}
/** @internal */
_schedulePendingDisconnect() {
this._cancelPendingDisconnect();
if (this._disconnectOnEmptyChannelsAfterMs === 0) {
this.log('transport', 'disconnecting immediately - no channels');
this.disconnect();
return;
}
this._pendingDisconnectTimer = setTimeout(() => {
this._pendingDisconnectTimer = null;
if (this.channels.length === 0) {
this.log('transport', 'deferred disconnect fired - no channels, disconnecting');
this.disconnect();
}
}, this._disconnectOnEmptyChannelsAfterMs);
this.log('transport', `deferred disconnect scheduled in ${this._disconnectOnEmptyChannelsAfterMs}ms`);
}
/** @internal */
_cancelPendingDisconnect() {
if (this._pendingDisconnectTimer !== null) {
this.log('transport', 'pending disconnect cancelled - channel activity detected');
clearTimeout(this._pendingDisconnectTimer);
this._pendingDisconnectTimer = null;
}
}
/**
* 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: constants_1.DEFAULT_VERSION,
};
tokenToSend && channel.updateJoinPayload(payload);
if (channel.joinedOnce && channel.channelAdapter.isJoined()) {
channel.channelAdapter.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') {
// Only refresh auth if using callback-based tokens
if (!this._isManualToken()) {
this.setAuth().catch((e) => {
this.log('error', `Error setting auth in ${context}`, e);
});
}
}
/** @internal */
_setupConnectionHandlers() {
this.socketAdapter.onOpen(() => {
const authPromise = this._authPromise ||
(this.accessToken && !this.accessTokenValue ? this.setAuth() : Promise.resolve());
authPromise.catch((e) => {
this.log('error', 'error waiting for auth on connect', e);
});
if (this.worker && !this.workerRef) {
this._startWorkerHeartbeat();
}
});
this.socketAdapter.onClose(() => {
if (this.worker && this.workerRef) {
this._terminateWorker();
}
});
this.socketAdapter.onMessage((message) => {
if (message.ref && message.ref === this._pendingWorkerHeartbeatRef) {
this._pendingWorkerHeartbeatRef = null;
}
});
}
/** @internal */
_handleNodeJsRaceCondition() {
if (this.socketAdapter.isConnected()) {
// hack: ensure onConnOpen is called
this.socketAdapter.getSocket().onConnOpen();
}
}
/** @internal */
_wrapHeartbeatCallback(heartbeatCallback) {
return (status, latency) => {
if (status === 'disconnected')
return;
if (status == 'sent')
this._setAuthSafely();
if (heartbeatCallback)
heartbeatCallback(status, latency);
};
}
/** @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.disconnect();
};
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 */
_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;
}
/**
* Initialize socket options with defaults
* @internal
*/
_initializeOptions(options) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
this.worker = (_a = options === null || options === void 0 ? void 0 : options.worker) !== null && _a !== void 0 ? _a : false;
this.accessToken = (_b = options === null || options === void 0 ? void 0 : options.accessToken) !== null && _b !== void 0 ? _b : null;
const result = {};
result.timeout = (_c = options === null || options === void 0 ? void 0 : options.timeout) !== null && _c !== void 0 ? _c : constants_1.DEFAULT_TIMEOUT;
result.heartbeatIntervalMs =
(_d = options === null || options === void 0 ? void 0 : options.heartbeatIntervalMs) !== null && _d !== void 0 ? _d : CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL;
this._disconnectOnEmptyChannelsAfterMs =
(_e = options === null || options === void 0 ? void 0 : options.disconnectOnEmptyChannelsAfterMs) !== null && _e !== void 0 ? _e : 2 * ((_f = options === null || options === void 0 ? void 0 : options.heartbeatIntervalMs) !== null && _f !== void 0 ? _f : CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL);
// @ts-ignore - mismatch between phoenix and supabase
result.transport = (_g = options === null || options === void 0 ? void 0 : options.transport) !== null && _g !== void 0 ? _g : websocket_factory_1.default.getWebSocketConstructor();
result.params = options === null || options === void 0 ? void 0 : options.params;
result.logger = options === null || options === void 0 ? void 0 : options.logger;
result.heartbeatCallback = this._wrapHeartbeatCallback(options === null || options === void 0 ? void 0 : options.heartbeatCallback);
result.sessionStorage = (_h = options === null || options === void 0 ? void 0 : options.sessionStorage) !== null && _h !== void 0 ? _h : resolveSessionStorage();
result.reconnectAfterMs =
(_j = options === null || options === void 0 ? void 0 : options.reconnectAfterMs) !== null && _j !== void 0 ? _j : ((tries) => {
return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK;
});
let defaultEncode;
let defaultDecode;
const vsn = (_k = options === null || options === void 0 ? void 0 : options.vsn) !== null && _k !== void 0 ? _k : constants_1.DEFAULT_VSN;
switch (vsn) {
case constants_1.VSN_1_0_0:
defaultEncode = (payload, callback) => {
return callback(JSON.stringify(payload));
};
defaultDecode = (payload, callback) => {
return callback(JSON.parse(payload));
};
break;
case constants_1.VSN_2_0_0:
defaultEncode = this.serializer.encode.bind(this.serializer);
defaultDecode = this.serializer.decode.bind(this.serializer);
break;
default:
throw new Error(`Unsupported serializer version: ${result.vsn}`);
}
result.vsn = vsn;
result.encode = (_l = options === null || options === void 0 ? void 0 : options.encode) !== null && _l !== void 0 ? _l : defaultEncode;
result.decode = (_m = options === null || options === void 0 ? void 0 : options.decode) !== null && _m !== void 0 ? _m : defaultDecode;
result.beforeReconnect = this._reconnectAuth.bind(this);
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;
result.params = Object.assign(Object.assign({}, result.params), { log_level: this.logLevel });
}
// 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;
result.autoSendHeartbeat = !this.worker;
}
return result;
}
/** @internal */
async _reconnectAuth() {
await this._waitForAuthIfNeeded();
if (!this.isConnected()) {
this.connect();
}
}
}
exports.default = RealtimeClient;
//# sourceMappingURL=RealtimeClient.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,132 @@
/**
* Comparison operators accepted in a Postgres Changes `filter` string.
*
* These mirror the PostgREST operator surface and are evaluated server-side.
* Any operator can be negated via {@link RealtimePostgresFilterBuilder.not}
* (the `not.` prefix).
*
* This is the subset of PostgREST's `FilterOperator` (see `postgrest-js`) that
* Realtime Postgres Changes supports. Containment, range and full-text search
* operators (`cs`, `cd`, `ov`, range ops, `fts`, …) are intentionally not
* included because the Realtime server does not evaluate them.
*
* - `eq`, `neq`, `lt`, `lte`, `gt`, `gte` — comparison
* - `in` — membership: `status=in.(active,pending)`
* - `like`, `ilike` — pattern match (case-sensitive / insensitive): `title=like.%foo%`
* - `is` — `IS` check against `null` / `true` / `false` / `unknown`: `deleted_at=is.null`
* - `match`, `imatch` — POSIX regex match (`~` / `~*`)
* - `isdistinct` — NULL-safe inequality (`IS DISTINCT FROM`)
*/
export type RealtimePostgresChangesFilterOperator = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'in' | 'like' | 'ilike' | 'is' | 'match' | 'imatch' | 'isdistinct';
/** Scalar value accepted by a single filter operator. */
export type RealtimeFilterValue = string | number | boolean | null;
/** Value accepted by the `is` operator. */
export type RealtimeIsFilterValue = null | boolean | 'null' | 'true' | 'false' | 'unknown';
/**
* Fluent builder for Postgres Changes `filter` strings.
*
* Each method appends a single `column=operator.value` condition. Multiple
* conditions are combined with commas, which the Realtime server applies as an
* `AND`. Pass an instance straight to `channel.on('postgres_changes', …)` — the
* SDK serializes it to a string automatically — or call {@link build} to obtain
* the string yourself.
*
* The builder mirrors the `postgrest-js` filter API (`eq`, `neq`, `in`, `like`,
* `not`, …) for the operators that Realtime supports. Values containing reserved
* characters (`,`, `(`, `)`, `"`, `\`) — or surrounding whitespace — are
* automatically double-quoted and escaped the same way PostgREST does, so they
* survive the server's filter parser; all other values are sent verbatim.
*
* The filter is snapshotted when passed to `channel.on(...)`; mutating the
* builder afterwards does not affect an existing subscription. An empty builder
* serializes to `''`, which the server treats as "no filter".
*
* @example
* channel.on('postgres_changes', {
* event: '*',
* schema: 'public',
* table: 'users',
* filter: postgresChangesFilter().eq('id', 1).lt('age', 30), // → 'id=eq.1,age=lt.30'
* }, (payload) => { ... })
*/
export declare class RealtimePostgresFilterBuilder {
private readonly filters;
private add;
/** Match rows where `column` equals `value` (`column=eq.value`). */
eq(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` does not equal `value` (`column=neq.value`). */
neq(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` is greater than `value` (`column=gt.value`). */
gt(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` is greater than or equal to `value` (`column=gte.value`). */
gte(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` is less than `value` (`column=lt.value`). */
lt(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` is less than or equal to `value` (`column=lte.value`). */
lte(column: string, value: RealtimeFilterValue): this;
/**
* Match rows where `column` is one of `values` (`column=in.(a,b,c)`).
* Requires at least one value; duplicates are removed. An element containing a
* reserved character is double-quoted (`in.("a,b",c)`), so commas inside an
* element are preserved. `null` is intentionally not accepted (`IN (null)`
* never matches in SQL) — use `is`/`not('col','is',null)` for null checks.
*/
in(column: string, values: ReadonlyArray<string | number | boolean>): this;
/** Match rows where `column` matches the case-sensitive `pattern` (`column=like.pattern`). */
like(column: string, pattern: string): this;
/** Match rows where `column` matches the case-insensitive `pattern` (`column=ilike.pattern`). */
ilike(column: string, pattern: string): this;
/** Match rows where `column` matches the POSIX regex `pattern` (`column=match.pattern`). */
match(column: string, pattern: string): this;
/** Match rows where `column` matches the case-insensitive POSIX regex `pattern` (`column=imatch.pattern`). */
imatch(column: string, pattern: string): this;
/**
* Match rows where `column` `IS` the given value (`column=is.null`).
* Accepts `null`, a boolean, or the keywords `'null' | 'true' | 'false' | 'unknown'`.
*/
is(column: string, value: RealtimeIsFilterValue): this;
/** Match rows where `column` is distinct from `value` (`column=isdistinct.value`). NULL-safe inequality. */
isDistinct(column: string, value: RealtimeFilterValue): this;
/**
* Negate any operator with the `not.` prefix (`column=not.operator.value`).
* `in` takes an array, `is` takes an `IS` keyword/boolean/null, and every
* other operator takes a scalar value.
*
* @example
* postgresChangesFilter().not('status', 'in', ['draft', 'archived'])
* // → status=not.in.(draft,archived)
* postgresChangesFilter().not('deleted_at', 'is', null)
* // → deleted_at=not.is.null
*/
not(column: string, operator: 'in', value: ReadonlyArray<string | number | boolean>): this;
not(column: string, operator: 'is', value: RealtimeIsFilterValue): this;
not(column: string, operator: Exclude<RealtimePostgresChangesFilterOperator, 'in' | 'is'>, value: RealtimeFilterValue): this;
/**
* Serialize all conditions into the comma-separated (AND) filter string.
*
* Conditions are joined by commas, which the server applies as `AND`. A scalar
* value (or single `in` element) that contains a reserved character — `,`,
* `(`, `)`, `"`, `\` — or surrounding whitespace is double-quoted and escaped
* the way PostgREST does, so commas inside a value are preserved rather than
* read as a condition boundary.
*/
build(): string;
/** Alias for {@link build}; lets the builder be used wherever a string is expected. */
toString(): string;
}
/**
* Create a {@link RealtimePostgresFilterBuilder} for composing a Postgres
* Changes `filter`. Conditions are combined with `AND`.
*
* @example
* import { postgresChangesFilter } from '@supabase/realtime-js'
*
* channel.on('postgres_changes', {
* event: 'UPDATE',
* schema: 'public',
* table: 'orders',
* filter: postgresChangesFilter().gt('amount', 100).eq('status', 'open'),
* }, (payload) => { ... })
*/
export declare const postgresChangesFilter: () => RealtimePostgresFilterBuilder;
//# sourceMappingURL=RealtimePostgresFilterBuilder.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"RealtimePostgresFilterBuilder.d.ts","sourceRoot":"","sources":["../../src/RealtimePostgresFilterBuilder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,qCAAqC,GAC7C,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,MAAM,GACN,OAAO,GACP,IAAI,GACJ,OAAO,GACP,QAAQ,GACR,YAAY,CAAA;AAEhB,yDAAyD;AACzD,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AAElE,2CAA2C;AAC3C,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;AAuC1F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,6BAA6B;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,OAAO,CAAC,GAAG;IAWX,oEAAoE;IACpE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIpD,6EAA6E;IAC7E,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIrD,6EAA6E;IAC7E,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIpD,0FAA0F;IAC1F,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIrD,0EAA0E;IAC1E,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIpD,uFAAuF;IACvF,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIrD;;;;;;OAMG;IACH,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI;IAI1E,8FAA8F;IAC9F,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3C,iGAAiG;IACjG,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5C,4FAA4F;IAC5F,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5C,8GAA8G;IAC9G,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAI7C;;;OAGG;IACH,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,IAAI;IAItD,4GAA4G;IAC5G,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAI5D;;;;;;;;;;OAUG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI;IAC1F,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,qBAAqB,GAAG,IAAI;IACvE,GAAG,CACD,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,CAAC,qCAAqC,EAAE,IAAI,GAAG,IAAI,CAAC,EACrE,KAAK,EAAE,mBAAmB,GACzB,IAAI;IASP;;;;;;;;OAQG;IACH,KAAK,IAAI,MAAM;IAIf,uFAAuF;IACvF,QAAQ,IAAI,MAAM;CAGnB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,QAAO,6BACJ,CAAA"}
@@ -0,0 +1,165 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.postgresChangesFilter = exports.RealtimePostgresFilterBuilder = void 0;
// Reserved characters that force PostgREST-style quoting: `[,()]` (which the
// server reads as condition/list delimiters) plus `"`/`\` (escaped inside quotes).
const PostgrestReservedCharsRegexp = /[,()"\\]/;
const needsQuoting = (value) => PostgrestReservedCharsRegexp.test(value) || value !== value.trim();
const quote = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
const serializeScalar = (value) => {
const serialized = value === null ? 'null' : String(value);
return needsQuoting(serialized) ? quote(serialized) : serialized;
};
const serializeIsValue = (value) => value === null ? 'null' : String(value);
// Builds the `operator.value` portion of a filter (everything after `column=`).
const serialize = (operator, value) => {
if (operator === 'in') {
const values = Array.isArray(value) ? value : [value];
if (values.length === 0) {
throw new Error('Realtime `in` filter requires at least one value.');
}
const items = Array.from(new Set(values))
.map((v) => serializeScalar(v))
.join(',');
return `in.(${items})`;
}
if (operator === 'is') {
return `is.${serializeIsValue(value)}`;
}
return `${operator}.${serializeScalar(value)}`;
};
/**
* Fluent builder for Postgres Changes `filter` strings.
*
* Each method appends a single `column=operator.value` condition. Multiple
* conditions are combined with commas, which the Realtime server applies as an
* `AND`. Pass an instance straight to `channel.on('postgres_changes', …)` — the
* SDK serializes it to a string automatically — or call {@link build} to obtain
* the string yourself.
*
* The builder mirrors the `postgrest-js` filter API (`eq`, `neq`, `in`, `like`,
* `not`, …) for the operators that Realtime supports. Values containing reserved
* characters (`,`, `(`, `)`, `"`, `\`) — or surrounding whitespace — are
* automatically double-quoted and escaped the same way PostgREST does, so they
* survive the server's filter parser; all other values are sent verbatim.
*
* The filter is snapshotted when passed to `channel.on(...)`; mutating the
* builder afterwards does not affect an existing subscription. An empty builder
* serializes to `''`, which the server treats as "no filter".
*
* @example
* channel.on('postgres_changes', {
* event: '*',
* schema: 'public',
* table: 'users',
* filter: postgresChangesFilter().eq('id', 1).lt('age', 30), // → 'id=eq.1,age=lt.30'
* }, (payload) => { ... })
*/
class RealtimePostgresFilterBuilder {
constructor() {
this.filters = [];
}
add(column, operator, value, negate = false) {
const prefix = negate ? 'not.' : '';
this.filters.push(`${column}=${prefix}${serialize(operator, value)}`);
return this;
}
/** Match rows where `column` equals `value` (`column=eq.value`). */
eq(column, value) {
return this.add(column, 'eq', value);
}
/** Match rows where `column` does not equal `value` (`column=neq.value`). */
neq(column, value) {
return this.add(column, 'neq', value);
}
/** Match rows where `column` is greater than `value` (`column=gt.value`). */
gt(column, value) {
return this.add(column, 'gt', value);
}
/** Match rows where `column` is greater than or equal to `value` (`column=gte.value`). */
gte(column, value) {
return this.add(column, 'gte', value);
}
/** Match rows where `column` is less than `value` (`column=lt.value`). */
lt(column, value) {
return this.add(column, 'lt', value);
}
/** Match rows where `column` is less than or equal to `value` (`column=lte.value`). */
lte(column, value) {
return this.add(column, 'lte', value);
}
/**
* Match rows where `column` is one of `values` (`column=in.(a,b,c)`).
* Requires at least one value; duplicates are removed. An element containing a
* reserved character is double-quoted (`in.("a,b",c)`), so commas inside an
* element are preserved. `null` is intentionally not accepted (`IN (null)`
* never matches in SQL) — use `is`/`not('col','is',null)` for null checks.
*/
in(column, values) {
return this.add(column, 'in', values);
}
/** Match rows where `column` matches the case-sensitive `pattern` (`column=like.pattern`). */
like(column, pattern) {
return this.add(column, 'like', pattern);
}
/** Match rows where `column` matches the case-insensitive `pattern` (`column=ilike.pattern`). */
ilike(column, pattern) {
return this.add(column, 'ilike', pattern);
}
/** Match rows where `column` matches the POSIX regex `pattern` (`column=match.pattern`). */
match(column, pattern) {
return this.add(column, 'match', pattern);
}
/** Match rows where `column` matches the case-insensitive POSIX regex `pattern` (`column=imatch.pattern`). */
imatch(column, pattern) {
return this.add(column, 'imatch', pattern);
}
/**
* Match rows where `column` `IS` the given value (`column=is.null`).
* Accepts `null`, a boolean, or the keywords `'null' | 'true' | 'false' | 'unknown'`.
*/
is(column, value) {
return this.add(column, 'is', value);
}
/** Match rows where `column` is distinct from `value` (`column=isdistinct.value`). NULL-safe inequality. */
isDistinct(column, value) {
return this.add(column, 'isdistinct', value);
}
not(column, operator, value) {
return this.add(column, operator, value, true);
}
/**
* Serialize all conditions into the comma-separated (AND) filter string.
*
* Conditions are joined by commas, which the server applies as `AND`. A scalar
* value (or single `in` element) that contains a reserved character — `,`,
* `(`, `)`, `"`, `\` — or surrounding whitespace is double-quoted and escaped
* the way PostgREST does, so commas inside a value are preserved rather than
* read as a condition boundary.
*/
build() {
return this.filters.join(',');
}
/** Alias for {@link build}; lets the builder be used wherever a string is expected. */
toString() {
return this.build();
}
}
exports.RealtimePostgresFilterBuilder = RealtimePostgresFilterBuilder;
/**
* Create a {@link RealtimePostgresFilterBuilder} for composing a Postgres
* Changes `filter`. Conditions are combined with `AND`.
*
* @example
* import { postgresChangesFilter } from '@supabase/realtime-js'
*
* channel.on('postgres_changes', {
* event: 'UPDATE',
* schema: 'public',
* table: 'orders',
* filter: postgresChangesFilter().gt('amount', 100).eq('status', 'open'),
* }, (payload) => { ... })
*/
const postgresChangesFilter = () => new RealtimePostgresFilterBuilder();
exports.postgresChangesFilter = postgresChangesFilter;
//# sourceMappingURL=RealtimePostgresFilterBuilder.js.map
@@ -0,0 +1 @@
{"version":3,"file":"RealtimePostgresFilterBuilder.js","sourceRoot":"","sources":["../../src/RealtimePostgresFilterBuilder.ts"],"names":[],"mappings":"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"}
+62
View File
@@ -0,0 +1,62 @@
import type RealtimeChannel from './RealtimeChannel';
export 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"
}
export type RealtimePresenceOptions = {
events?: {
state: string;
diff: string;
};
};
export default class RealtimePresence {
channel: RealtimeChannel;
get state(): RealtimePresenceState;
private presenceAdapter;
/**
* 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' } }`.
*
* @category Realtime
*
* @example Example for a presence channel
* ```ts
* const presence = new RealtimePresence(channel)
*
* channel.on('presence', ({ event, key }) => {
* console.log(`Presence ${event} on ${key}`)
* })
* ```
*/
constructor(channel: RealtimeChannel, opts?: RealtimePresenceOptions);
}
//# sourceMappingURL=RealtimePresence.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"RealtimePresence.d.ts","sourceRoot":"","sources":["../../src/RealtimePresence.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,eAAe,MAAM,mBAAmB,CAAA;AAGpD,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,GAAG,EAAE,IAAI;IAC5D,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;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACzC,CAAA;AAED,MAAM,CAAC,OAAO,OAAO,gBAAgB;IAyB1B,OAAO,EAAE,eAAe;IAxBjC,IAAI,KAAK,0BAER;IAED,OAAO,CAAC,eAAe,CAAiB;IAExC;;;;;;;;;;;;;;;;OAgBG;gBAEM,OAAO,EAAE,eAAe,EAC/B,IAAI,CAAC,EAAE,uBAAuB;CAIjC"}
+43
View File
@@ -0,0 +1,43 @@
"use strict";
/*
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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.REALTIME_PRESENCE_LISTEN_EVENTS = void 0;
const tslib_1 = require("tslib");
const presenceAdapter_1 = tslib_1.__importDefault(require("./phoenix/presenceAdapter"));
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 || (exports.REALTIME_PRESENCE_LISTEN_EVENTS = REALTIME_PRESENCE_LISTEN_EVENTS = {}));
class RealtimePresence {
get state() {
return this.presenceAdapter.state;
}
/**
* 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' } }`.
*
* @category Realtime
*
* @example Example for a presence channel
* ```ts
* const presence = new RealtimePresence(channel)
*
* channel.on('presence', ({ event, key }) => {
* console.log(`Presence ${event} on ${key}`)
* })
* ```
*/
constructor(channel, opts) {
this.channel = channel;
this.presenceAdapter = new presenceAdapter_1.default(this.channel.channelAdapter, opts);
}
}
exports.default = RealtimePresence;
//# sourceMappingURL=RealtimePresence.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"RealtimePresence.js","sourceRoot":"","sources":["../../src/RealtimePresence.ts"],"names":[],"mappings":";AAAA;;;EAGE;;;;AAGF,wFAAuD;AAwBvD,IAAY,+BAIX;AAJD,WAAY,+BAA+B;IACzC,gDAAa,CAAA;IACb,gDAAa,CAAA;IACb,kDAAe,CAAA;AACjB,CAAC,EAJW,+BAA+B,+CAA/B,+BAA+B,QAI1C;AAMD,MAAqB,gBAAgB;IACnC,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,CAAA;IACnC,CAAC;IAID;;;;;;;;;;;;;;;;OAgBG;IACH,YACS,OAAwB,EAC/B,IAA8B;QADvB,YAAO,GAAP,OAAO,CAAiB;QAG/B,IAAI,CAAC,eAAe,GAAG,IAAI,yBAAe,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC,CAAA;IAC/E,CAAC;CACF;AA9BD,mCA8BC"}
+6
View File
@@ -0,0 +1,6 @@
import RealtimeClient, { RealtimeClientOptions, RealtimeMessage, RealtimeRemoveChannelResponse, WebSocketLikeConstructor } from './RealtimeClient';
import RealtimeChannel, { RealtimeChannelOptions, RealtimeChannelSendResponse, RealtimePostgresChangesFilter, RealtimePostgresChangesFilterOperator, RealtimePostgresFilterBuilder, postgresChangesFilter, RealtimePostgresChangesPayload, RealtimePostgresInsertPayload, RealtimePostgresUpdatePayload, RealtimePostgresDeletePayload, RealtimeSystemPayload, 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, RealtimePostgresChangesFilterOperator, RealtimePostgresFilterBuilder, postgresChangesFilter, RealtimePostgresChangesPayload, RealtimePostgresInsertPayload, RealtimePostgresUpdatePayload, RealtimePostgresDeletePayload, RealtimeSystemPayload, 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
+1
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,qCAAqC,EACrC,6BAA6B,EAC7B,qBAAqB,EACrB,8BAA8B,EAC9B,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,qBAAqB,EACrB,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,qCAAqC,EACrC,6BAA6B,EAC7B,qBAAqB,EACrB,8BAA8B,EAC9B,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,qBAAqB,EACrB,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"}
+20
View File
@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebSocketFactory = exports.REALTIME_CHANNEL_STATES = exports.REALTIME_SUBSCRIBE_STATES = exports.REALTIME_PRESENCE_LISTEN_EVENTS = exports.REALTIME_POSTGRES_CHANGES_LISTEN_EVENT = exports.REALTIME_LISTEN_TYPES = exports.postgresChangesFilter = exports.RealtimePostgresFilterBuilder = exports.RealtimeClient = exports.RealtimeChannel = exports.RealtimePresence = void 0;
const tslib_1 = require("tslib");
const RealtimeClient_1 = tslib_1.__importDefault(require("./RealtimeClient"));
exports.RealtimeClient = RealtimeClient_1.default;
const RealtimeChannel_1 = tslib_1.__importStar(require("./RealtimeChannel"));
exports.RealtimeChannel = RealtimeChannel_1.default;
Object.defineProperty(exports, "RealtimePostgresFilterBuilder", { enumerable: true, get: function () { return RealtimeChannel_1.RealtimePostgresFilterBuilder; } });
Object.defineProperty(exports, "postgresChangesFilter", { enumerable: true, get: function () { return RealtimeChannel_1.postgresChangesFilter; } });
Object.defineProperty(exports, "REALTIME_LISTEN_TYPES", { enumerable: true, get: function () { return RealtimeChannel_1.REALTIME_LISTEN_TYPES; } });
Object.defineProperty(exports, "REALTIME_POSTGRES_CHANGES_LISTEN_EVENT", { enumerable: true, get: function () { return RealtimeChannel_1.REALTIME_POSTGRES_CHANGES_LISTEN_EVENT; } });
Object.defineProperty(exports, "REALTIME_SUBSCRIBE_STATES", { enumerable: true, get: function () { return RealtimeChannel_1.REALTIME_SUBSCRIBE_STATES; } });
Object.defineProperty(exports, "REALTIME_CHANNEL_STATES", { enumerable: true, get: function () { return RealtimeChannel_1.REALTIME_CHANNEL_STATES; } });
const RealtimePresence_1 = tslib_1.__importStar(require("./RealtimePresence"));
exports.RealtimePresence = RealtimePresence_1.default;
Object.defineProperty(exports, "REALTIME_PRESENCE_LISTEN_EVENTS", { enumerable: true, get: function () { return RealtimePresence_1.REALTIME_PRESENCE_LISTEN_EVENTS; } });
const websocket_factory_1 = tslib_1.__importDefault(require("./lib/websocket-factory"));
exports.WebSocketFactory = websocket_factory_1.default;
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;AAAA,8EAKyB;AA+BvB,yBApCK,wBAAc,CAoCL;AA9BhB,6EAgB0B;AAWxB,0BA3BK,yBAAe,CA2BL;AAQf,8GA9BA,+CAA6B,OA8BA;AAC7B,sGA9BA,uCAAqB,OA8BA;AAUrB,sGAlCA,uCAAqB,OAkCA;AACrB,uHAlCA,wDAAsC,OAkCA;AAEtC,0GAnCA,2CAAyB,OAmCA;AACzB,wGAnCA,yCAAuB,OAmCA;AAjCzB,+EAK2B;AAIzB,2BATK,0BAAgB,CASL;AAsBhB,gHA3BA,kDAA+B,OA2BA;AAzBjC,wFAAyE;AA4BvE,2BA5BK,2BAAgB,CA4BL"}
+43
View File
@@ -0,0 +1,43 @@
import type { SocketState, ChannelState, ChannelEvent as PhoenixChannelEvent, Transport, Vsn } from '../phoenix/types';
export type { SocketState, ChannelState, Transport };
export declare const DEFAULT_VERSION = "realtime-js/2.110.8";
export declare const VSN_1_0_0: Vsn;
export declare const VSN_2_0_0: Vsn;
export declare const DEFAULT_VSN: Vsn;
export declare const VERSION = "2.110.8";
export declare const DEFAULT_TIMEOUT = 10000;
export declare const WS_CLOSE_NORMAL = 1000;
export declare const MAX_PUSH_BUFFER_SIZE = 100;
export declare const SOCKET_STATES: {
readonly connecting: 0;
readonly open: 1;
readonly closing: 2;
readonly closed: 3;
};
export declare const CHANNEL_STATES: {
readonly closed: "closed";
readonly errored: "errored";
readonly joined: "joined";
readonly joining: "joining";
readonly leaving: "leaving";
};
export type ChannelEvent = PhoenixChannelEvent | 'access_token';
export declare const CHANNEL_EVENTS: {
readonly close: "phx_close";
readonly error: "phx_error";
readonly join: "phx_join";
readonly reply: "phx_reply";
readonly leave: "phx_leave";
readonly access_token: "access_token";
};
export declare const TRANSPORTS: {
readonly websocket: "websocket";
};
export type ConnectionState = 'connecting' | 'open' | 'closing' | 'closed' | (string & {});
export declare const CONNECTION_STATE: {
readonly connecting: "connecting";
readonly open: "open";
readonly closing: "closing";
readonly closed: "closed";
};
//# sourceMappingURL=constants.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,YAAY,IAAI,mBAAmB,EACnC,SAAS,EACT,GAAG,EACJ,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,CAAA;AAEpD,eAAO,MAAM,eAAe,wBAA2B,CAAA;AAEvD,eAAO,MAAM,SAAS,EAAE,GAAa,CAAA;AACrC,eAAO,MAAM,SAAS,EAAE,GAAa,CAAA;AACrC,eAAO,MAAM,WAAW,EAAE,GAAe,CAAA;AAEzC,eAAO,MAAM,OAAO,YAAU,CAAA;AAE9B,eAAO,MAAM,eAAe,QAAQ,CAAA;AAEpC,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC,eAAO,MAAM,aAAa;;;;;CAKhB,CAAA;AAEV,eAAO,MAAM,cAAc;;;;;;CAMjB,CAAA;AAEV,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,cAAc,CAAA;AAE/D,eAAO,MAAM,cAAc;;;;;;;CAOjB,CAAA;AAEV,eAAO,MAAM,UAAU;;CAEb,CAAA;AAEV,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAE1F,eAAO,MAAM,gBAAgB;;;;;CAKnB,CAAA"}
+43
View File
@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONNECTION_STATE = exports.TRANSPORTS = exports.CHANNEL_EVENTS = exports.CHANNEL_STATES = exports.SOCKET_STATES = exports.MAX_PUSH_BUFFER_SIZE = exports.WS_CLOSE_NORMAL = exports.DEFAULT_TIMEOUT = exports.VERSION = exports.DEFAULT_VSN = exports.VSN_2_0_0 = exports.VSN_1_0_0 = exports.DEFAULT_VERSION = void 0;
const version_1 = require("./version");
exports.DEFAULT_VERSION = `realtime-js/${version_1.version}`;
exports.VSN_1_0_0 = '1.0.0';
exports.VSN_2_0_0 = '2.0.0';
exports.DEFAULT_VSN = exports.VSN_2_0_0;
exports.VERSION = version_1.version;
exports.DEFAULT_TIMEOUT = 10000;
exports.WS_CLOSE_NORMAL = 1000;
exports.MAX_PUSH_BUFFER_SIZE = 100;
exports.SOCKET_STATES = {
connecting: 0,
open: 1,
closing: 2,
closed: 3,
};
exports.CHANNEL_STATES = {
closed: 'closed',
errored: 'errored',
joined: 'joined',
joining: 'joining',
leaving: 'leaving',
};
exports.CHANNEL_EVENTS = {
close: 'phx_close',
error: 'phx_error',
join: 'phx_join',
reply: 'phx_reply',
leave: 'phx_leave',
access_token: 'access_token',
};
exports.TRANSPORTS = {
websocket: 'websocket',
};
exports.CONNECTION_STATE = {
connecting: 'connecting',
open: 'open',
closing: 'closing',
closed: 'closed',
};
//# sourceMappingURL=constants.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":";;;AAAA,uCAAmC;AAWtB,QAAA,eAAe,GAAG,eAAe,iBAAO,EAAE,CAAA;AAE1C,QAAA,SAAS,GAAQ,OAAO,CAAA;AACxB,QAAA,SAAS,GAAQ,OAAO,CAAA;AACxB,QAAA,WAAW,GAAQ,iBAAS,CAAA;AAE5B,QAAA,OAAO,GAAG,iBAAO,CAAA;AAEjB,QAAA,eAAe,GAAG,KAAK,CAAA;AAEvB,QAAA,eAAe,GAAG,IAAI,CAAA;AACtB,QAAA,oBAAoB,GAAG,GAAG,CAAA;AAE1B,QAAA,aAAa,GAAG;IAC3B,UAAU,EAAE,CAAC;IACb,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;CACD,CAAA;AAEG,QAAA,cAAc,GAAG;IAC5B,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;CACV,CAAA;AAIG,QAAA,cAAc,GAAG;IAC5B,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,WAAW;IAClB,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,WAAW;IAClB,YAAY,EAAE,cAAc;CACpB,CAAA;AAEG,QAAA,UAAU,GAAG;IACxB,SAAS,EAAE,WAAW;CACd,CAAA;AAIG,QAAA,gBAAgB,GAAG;IAC9B,UAAU,EAAE,YAAY;IACxB,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,QAAQ;CACR,CAAA"}
@@ -0,0 +1,10 @@
/**
* Normalize the various shapes a channel error reason can take into a real `Error`.
*
* Transport-level channel errors arrive as a `CloseEvent`, a transport `Event`, an `Error`,
* a string, or `undefined` depending on which path in the underlying socket fired. Server-reply
* errors arrive as a payload object. This helper produces a consistent `Error` for every case
* and preserves the original via `cause` so callers can still inspect the raw event.
*/
export declare function normalizeChannelError(reason: unknown): Error;
//# sourceMappingURL=normalizeChannelError.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"normalizeChannelError.d.ts","sourceRoot":"","sources":["../../../src/lib/normalizeChannelError.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,KAAK,CAqB5D"}
@@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeChannelError = normalizeChannelError;
/**
* Normalize the various shapes a channel error reason can take into a real `Error`.
*
* Transport-level channel errors arrive as a `CloseEvent`, a transport `Event`, an `Error`,
* a string, or `undefined` depending on which path in the underlying socket fired. Server-reply
* errors arrive as a payload object. This helper produces a consistent `Error` for every case
* and preserves the original via `cause` so callers can still inspect the raw event.
*/
function normalizeChannelError(reason) {
if (reason instanceof Error) {
return reason;
}
if (typeof reason === 'string') {
return new Error(reason);
}
if (reason && typeof reason === 'object') {
const obj = reason;
if (typeof obj.code === 'number') {
const detail = typeof obj.reason === 'string' && obj.reason ? ` (${obj.reason})` : '';
return new Error(`socket closed: ${obj.code}${detail}`, { cause: reason });
}
return new Error('channel error: transport failure', { cause: reason });
}
return new Error('channel error: connection lost');
}
//# sourceMappingURL=normalizeChannelError.js.map
@@ -0,0 +1 @@
{"version":3,"file":"normalizeChannelError.js","sourceRoot":"","sources":["../../../src/lib/normalizeChannelError.ts"],"names":[],"mappings":";;AAQA,sDAqBC;AA7BD;;;;;;;GAOG;AACH,SAAgB,qBAAqB,CAAC,MAAe;IACnD,IAAI,MAAM,YAAY,KAAK,EAAE,CAAC;QAC5B,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,CAAA;IAC1B,CAAC;IAED,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,MAAiC,CAAA;QAE7C,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;YACrF,OAAO,IAAI,KAAK,CAAC,kBAAkB,GAAG,CAAC,IAAI,GAAG,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;QAC5E,CAAC;QAED,OAAO,IAAI,KAAK,CAAC,kCAAkC,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;IACzE,CAAC;IAED,OAAO,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;AACpD,CAAC"}
+33
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
+1
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;IA6EhC,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"}
+166
View File
@@ -0,0 +1,166 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
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;
// Encode each header field as UTF-8. The length prefixes are byte counts and
// the decode side uses TextDecoder (UTF-8), so measuring with String.length
// and writing with charCodeAt would corrupt any multi-byte character (e.g.
// accents or emoji) and desynchronize the buffer.
const encoder = new TextEncoder();
const topic = encoder.encode(message.topic);
const ref = encoder.encode((_a = message.ref) !== null && _a !== void 0 ? _a : '');
const joinRef = encoder.encode((_b = message.join_ref) !== null && _b !== void 0 ? _b : '');
const userEvent = encoder.encode(message.payload.event);
// Filter metadata based on allowed keys
const rest = this.allowedMetadataKeys
? this._pick(message.payload, this.allowedMetadataKeys)
: {};
const metadata = encoder.encode(Object.keys(rest).length === 0 ? '' : JSON.stringify(rest));
// Validate byte 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);
const view = new DataView(header);
const bytes = new Uint8Array(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);
bytes.set(joinRef, offset);
offset += joinRef.length;
bytes.set(ref, offset);
offset += ref.length;
bytes.set(topic, offset);
offset += topic.length;
bytes.set(userEvent, offset);
offset += userEvent.length;
bytes.set(metadata, offset);
offset += metadata.length;
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)));
}
}
exports.default = Serializer;
//# sourceMappingURL=serializer.js.map
File diff suppressed because one or more lines are too long
+109
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
@@ -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"}
+241
View File
@@ -0,0 +1,241 @@
"use strict";
/**
* Helpers to convert the change Payload into native JS types.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.httpEndpointURL = exports.toTimestampString = exports.toArray = exports.toJson = exports.toNumber = exports.toBoolean = exports.convertCell = exports.convertColumn = exports.convertChangeData = exports.PostgresTypes = void 0;
// Adapted from epgsql (src/epgsql_binary.erl), this module licensed under
// 3-clause BSD found here: https://raw.githubusercontent.com/epgsql/epgsql/devel/LICENSE
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 || (exports.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 }
*/
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] = (0, exports.convertColumn)(rec_key, columns, record, skipTypes);
return acc;
}, {});
};
exports.convertChangeData = convertChangeData;
/**
* 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"
*/
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 (0, exports.convertCell)(colType, value);
}
return noop(value);
};
exports.convertColumn = convertColumn;
/**
* 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]
*/
const convertCell = (type, value) => {
// if data type is an array
if (type.charAt(0) === '_') {
const dataType = type.slice(1, type.length);
return (0, exports.toArray)(value, dataType);
}
// If not null, convert to correct type.
switch (type) {
case PostgresTypes.bool:
return (0, exports.toBoolean)(value);
case PostgresTypes.float4:
case PostgresTypes.float8:
case PostgresTypes.int2:
case PostgresTypes.int4:
case PostgresTypes.int8:
case PostgresTypes.numeric:
case PostgresTypes.oid:
return (0, exports.toNumber)(value);
case PostgresTypes.json:
case PostgresTypes.jsonb:
return (0, exports.toJson)(value);
case PostgresTypes.timestamp:
return (0, exports.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);
}
};
exports.convertCell = convertCell;
const noop = (value) => {
return value;
};
const toBoolean = (value) => {
switch (value) {
case 't':
return true;
case 'f':
return false;
default:
return value;
}
};
exports.toBoolean = toBoolean;
const toNumber = (value) => {
if (typeof value === 'string') {
const parsedValue = parseFloat(value);
if (!Number.isNaN(parsedValue)) {
return parsedValue;
}
}
return value;
};
exports.toNumber = toNumber;
const toJson = (value) => {
if (typeof value === 'string') {
try {
return JSON.parse(value);
}
catch (_a) {
return value;
}
}
return value;
};
exports.toJson = toJson;
/**
* 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]
*/
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) => (0, exports.convertCell)(type, val));
}
return value;
};
exports.toArray = toArray;
/**
* 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'
*/
const toTimestampString = (value) => {
if (typeof value === 'string') {
return value.replace(' ', 'T');
}
return value;
};
exports.toTimestampString = toTimestampString;
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;
};
exports.httpEndpointURL = httpEndpointURL;
//# sourceMappingURL=transformers.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
export declare const version = "2.110.8";
//# sourceMappingURL=version.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../../src/lib/version.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,OAAO,YAAY,CAAA"}
+11
View File
@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.version = void 0;
// 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
exports.version = '2.110.8';
//# sourceMappingURL=version.js.map
+1
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;AACpD,QAAA,OAAO,GAAG,SAAS,CAAA"}
@@ -0,0 +1,82 @@
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' | 'cloudflare' | 'unsupported';
/** WebSocket constructor for this environment, if available. */
wsConstructor?: typeof WebSocket;
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.
*
* @category Realtime
*
* @example Example with error handling
* ```ts
* try {
* const WS = WebSocketFactory.getWebSocketConstructor()
* const socket = new WS('wss://example.com/socket')
* } catch (error) {
* console.error('WebSocket not available in this environment.', error)
* }
* ```
*/
static getWebSocketConstructor(): typeof WebSocket;
/**
* Detects whether the runtime can establish WebSocket connections.
*
* @category Realtime
*
* @example Example in a Node.js script
* ```ts
* if (!WebSocketFactory.isWebSocketSupported()) {
* console.error('WebSockets are required for this script.')
* process.exitCode = 1
* }
* ```
*/
static isWebSocketSupported(): boolean;
}
export default WebSocketFactory;
//# sourceMappingURL=websocket-factory.d.ts.map
@@ -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,YAAY,GAAG,aAAa,CAAA;IAC7C,gEAAgE;IAChE,aAAa,CAAC,EAAE,OAAO,SAAS,CAAA;IAChC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAYD;;GAEG;AACH,qBAAa,gBAAgB;IAC3B;;OAEG;IACH,OAAO;IACP,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAqEhC;;;;;;;;;;;;;;OAcG;WACW,uBAAuB,IAAI,OAAO,SAAS;IAYzD;;;;;;;;;;;;OAYG;WACW,oBAAoB,IAAI,OAAO;CAQ9C;AAED,eAAe,gBAAgB,CAAA"}
+113
View File
@@ -0,0 +1,113 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebSocketFactory = void 0;
/**
* Utilities for creating WebSocket instances across runtimes.
*/
class WebSocketFactory {
/**
* Static-only utility prevent instantiation.
*/
constructor() { }
static detectEnvironment() {
var _a;
if (typeof WebSocket !== 'undefined') {
return { type: 'native', wsConstructor: WebSocket };
}
const gt = globalThis;
if (typeof globalThis !== 'undefined' && typeof gt.WebSocket !== 'undefined') {
return { type: 'native', wsConstructor: gt.WebSocket };
}
const gl = typeof global !== 'undefined' ? global : undefined;
if (gl && typeof gl.WebSocket !== 'undefined') {
return { type: 'native', wsConstructor: gl.WebSocket };
}
if (typeof globalThis !== 'undefined' &&
typeof gt.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' && gt.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']) {
// Reaching here means an earlier check did not find a native WebSocket,
// so this Node.js process is missing the global WebSocket (Node.js 22+).
return {
type: 'unsupported',
error: 'Node.js detected but native WebSocket not found.',
workaround: 'Ensure you are running Node.js 22+ or provide a WebSocket implementation via the transport option.',
};
}
}
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.
*
* @category Realtime
*
* @example Example with error handling
* ```ts
* try {
* const WS = WebSocketFactory.getWebSocketConstructor()
* const socket = new WS('wss://example.com/socket')
* } catch (error) {
* console.error('WebSocket not available in this environment.', error)
* }
* ```
*/
static getWebSocketConstructor() {
const env = this.detectEnvironment();
if (env.wsConstructor) {
return env.wsConstructor;
}
let errorMessage = env.error || 'WebSocket not supported in this environment.';
if (env.workaround) {
errorMessage += `\n\nSuggested solution: ${env.workaround}`;
}
throw new Error(errorMessage);
}
/**
* Detects whether the runtime can establish WebSocket connections.
*
* @category Realtime
*
* @example Example in a Node.js script
* ```ts
* if (!WebSocketFactory.isWebSocketSupported()) {
* console.error('WebSockets are required for this script.')
* process.exitCode = 1
* }
* ```
*/
static isWebSocketSupported() {
try {
const env = this.detectEnvironment();
return env.type === 'native';
}
catch (_a) {
return false;
}
}
}
exports.WebSocketFactory = WebSocketFactory;
exports.default = WebSocketFactory;
//# sourceMappingURL=websocket-factory.js.map
@@ -0,0 +1 @@
{"version":3,"file":"websocket-factory.js","sourceRoot":"","sources":["../../../src/lib/websocket-factory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
import type { RealtimeChannelOptions } from '../RealtimeChannel';
import SocketAdapter from './socketAdapter';
import type { ChannelBindingCallback, ChannelOnMessage, ChannelOnErrorCallback, ChannelFilterBindings, ChannelState, Push, Timer } from './types';
export default class ChannelAdapter {
private channel;
private socket;
constructor(socket: SocketAdapter, topic: string, params: RealtimeChannelOptions);
get state(): ChannelState;
set state(state: ChannelState);
get joinedOnce(): boolean;
get joinPush(): Push;
get rejoinTimer(): Timer;
on(event: string, callback: ChannelBindingCallback): number;
off(event: string, refNumber?: number): void;
subscribe(timeout?: number): Push;
unsubscribe(timeout?: number): Push;
teardown(): void;
onClose(callback: ChannelBindingCallback): void;
onError(callback: ChannelOnErrorCallback): number;
push(event: string, payload: {
[key: string]: any;
}, timeout?: number): Push;
updateJoinPayload(payload: Record<string, any>): void;
canPush(): boolean;
isJoined(): boolean;
isJoining(): boolean;
isClosed(): boolean;
isLeaving(): boolean;
updateFilterBindings(filterBindings: ChannelFilterBindings): void;
updatePayloadTransform(callback: ChannelOnMessage): void;
}
//# sourceMappingURL=channelAdapter.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"channelAdapter.d.ts","sourceRoot":"","sources":["../../../src/phoenix/channelAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAChE,OAAO,aAAa,MAAM,iBAAiB,CAAA;AAC3C,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,EAErB,YAAY,EACZ,IAAI,EACJ,KAAK,EACN,MAAM,SAAS,CAAA;AAEhB,MAAM,CAAC,OAAO,OAAO,cAAc;IACjC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB;IAMhF,IAAI,KAAK,IAAI,YAAY,CAExB;IAED,IAAI,KAAK,CAAC,KAAK,EAAE,YAAY,EAE5B;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,IAAI,QAAQ,IAAI,IAAI,CAEnB;IAED,IAAI,WAAW,IAAI,KAAK,CAEvB;IAED,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG,MAAM;IAI3D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM;IAIrC,SAAS,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAIjC,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAInC,QAAQ;IAIR,OAAO,CAAC,QAAQ,EAAE,sBAAsB;IAIxC,OAAO,CAAC,QAAQ,EAAE,sBAAsB,GAAG,MAAM;IAIjD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAuB5E,iBAAiB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAK9C,OAAO;IAIP,QAAQ;IAIR,SAAS;IAIT,QAAQ;IAIR,SAAS;IAIT,oBAAoB,CAAC,cAAc,EAAE,qBAAqB;IAI1D,sBAAsB,CAAC,QAAQ,EAAE,gBAAgB;CAUlD"}
+103
View File
@@ -0,0 +1,103 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../lib/constants");
class ChannelAdapter {
constructor(socket, topic, params) {
const phoenixParams = phoenixChannelParams(params);
this.channel = socket.getSocket().channel(topic, phoenixParams);
this.socket = socket;
}
get state() {
return this.channel.state;
}
set state(state) {
this.channel.state = state;
}
get joinedOnce() {
return this.channel.joinedOnce;
}
get joinPush() {
return this.channel.joinPush;
}
get rejoinTimer() {
return this.channel.rejoinTimer;
}
on(event, callback) {
return this.channel.on(event, callback);
}
off(event, refNumber) {
this.channel.off(event, refNumber);
}
subscribe(timeout) {
return this.channel.join(timeout);
}
unsubscribe(timeout) {
return this.channel.leave(timeout);
}
teardown() {
this.channel.teardown();
}
onClose(callback) {
this.channel.onClose(callback);
}
onError(callback) {
return this.channel.onError(callback);
}
push(event, payload, timeout) {
let push;
try {
push = this.channel.push(event, payload, timeout);
}
catch (error) {
throw new Error(`tried to push '${event}' to '${this.channel.topic}' before joining. Use channel.subscribe() before pushing events`);
}
if (this.channel.pushBuffer.length > constants_1.MAX_PUSH_BUFFER_SIZE) {
const removedPush = this.channel.pushBuffer.shift();
removedPush.cancelTimeout();
this.socket.log('channel', `discarded push due to buffer overflow: ${removedPush.event}`, removedPush.payload());
}
return push;
}
updateJoinPayload(payload) {
const oldPayload = this.channel.joinPush.payload();
this.channel.joinPush.payload = () => (Object.assign(Object.assign({}, oldPayload), payload));
}
canPush() {
return this.socket.isConnected() && this.state === constants_1.CHANNEL_STATES.joined;
}
isJoined() {
return this.state === constants_1.CHANNEL_STATES.joined;
}
isJoining() {
return this.state === constants_1.CHANNEL_STATES.joining;
}
isClosed() {
return this.state === constants_1.CHANNEL_STATES.closed;
}
isLeaving() {
return this.state === constants_1.CHANNEL_STATES.leaving;
}
updateFilterBindings(filterBindings) {
this.channel.filterBindings = filterBindings;
}
updatePayloadTransform(callback) {
this.channel.onMessage = callback;
}
/**
* @internal
*/
getChannel() {
return this.channel;
}
}
exports.default = ChannelAdapter;
function phoenixChannelParams(options) {
return {
config: Object.assign({
broadcast: { ack: false, self: false },
presence: { key: '', enabled: false },
private: false,
}, options.config),
};
}
//# sourceMappingURL=channelAdapter.js.map
@@ -0,0 +1 @@
{"version":3,"file":"channelAdapter.js","sourceRoot":"","sources":["../../../src/phoenix/channelAdapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
import type { PresenceState, PresenceStates } from './types';
import type { RealtimePresenceOptions, RealtimePresenceState } from '../RealtimePresence';
import ChannelAdapter from './channelAdapter';
export default class PresenceAdapter {
private presence;
constructor(channel: ChannelAdapter, opts?: RealtimePresenceOptions);
get state(): RealtimePresenceState;
/**
* @private
* Remove 'metas' key
* Change 'phx_ref' to 'presence_ref'
* Remove 'phx_ref' and 'phx_ref_prev'
*
* @example Transform state
* // 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 }
* ]
* }
* })
*
*/
static transformState(state: PresenceStates): RealtimePresenceState;
static onJoinPayload(key: string, currentPresence: PresenceState, newPresence: PresenceState): {
event: string;
key: string;
currentPresences: {
presence_ref: string;
}[];
newPresences: {
presence_ref: string;
}[];
};
static onLeavePayload(key: string, currentPresence: PresenceState, leftPresence: PresenceState): {
event: string;
key: string;
currentPresences: {
presence_ref: string;
}[];
leftPresences: {
presence_ref: string;
}[];
};
}
//# sourceMappingURL=presenceAdapter.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"presenceAdapter.d.ts","sourceRoot":"","sources":["../../../src/phoenix/presenceAdapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC5D,OAAO,KAAK,EACV,uBAAuB,EACvB,qBAAqB,EAEtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,cAAc,MAAM,kBAAkB,CAAA;AAE7C,MAAM,CAAC,OAAO,OAAO,eAAe;IAClC,OAAO,CAAC,QAAQ,CAAU;gBAEd,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,uBAAuB;IAmBnE,IAAI,KAAK,0BAER;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,qBAAqB;IAWnE,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa;;;;;;;;;;IAY5F,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa;;;;;;;;;;CAW/F"}
@@ -0,0 +1,93 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const phoenix_1 = require("@supabase/phoenix");
class PresenceAdapter {
constructor(channel, opts) {
const phoenixOptions = phoenixPresenceOptions(opts);
this.presence = new phoenix_1.Presence(channel.getChannel(), phoenixOptions);
this.presence.onJoin((key, currentPresence, newPresence) => {
const onJoinPayload = PresenceAdapter.onJoinPayload(key, currentPresence, newPresence);
channel.getChannel().trigger('presence', onJoinPayload);
});
this.presence.onLeave((key, currentPresence, leftPresence) => {
const onLeavePayload = PresenceAdapter.onLeavePayload(key, currentPresence, leftPresence);
channel.getChannel().trigger('presence', onLeavePayload);
});
this.presence.onSync(() => {
channel.getChannel().trigger('presence', { event: 'sync' });
});
}
get state() {
return PresenceAdapter.transformState(this.presence.state);
}
/**
* @private
* Remove 'metas' key
* Change 'phx_ref' to 'presence_ref'
* Remove 'phx_ref' and 'phx_ref_prev'
*
* @example Transform state
* // 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 }
* ]
* }
* })
*
*/
static transformState(state) {
state = cloneState(state);
return Object.getOwnPropertyNames(state).reduce((newState, key) => {
const presences = state[key];
newState[key] = transformState(presences);
return newState;
}, {});
}
static onJoinPayload(key, currentPresence, newPresence) {
const currentPresences = parseCurrentPresences(currentPresence);
const newPresences = transformState(newPresence);
return {
event: 'join',
key,
currentPresences,
newPresences,
};
}
static onLeavePayload(key, currentPresence, leftPresence) {
const currentPresences = parseCurrentPresences(currentPresence);
const leftPresences = transformState(leftPresence);
return {
event: 'leave',
key,
currentPresences,
leftPresences,
};
}
}
exports.default = PresenceAdapter;
function transformState(presences) {
return presences.metas.map((presence) => {
presence['presence_ref'] = presence['phx_ref'];
delete presence['phx_ref'];
delete presence['phx_ref_prev'];
return presence;
});
}
function cloneState(state) {
return JSON.parse(JSON.stringify(state));
}
function phoenixPresenceOptions(opts) {
return (opts === null || opts === void 0 ? void 0 : opts.events) && { events: opts.events };
}
function parseCurrentPresences(currentPresences) {
return (currentPresences === null || currentPresences === void 0 ? void 0 : currentPresences.metas) ? transformState(currentPresences) : [];
}
//# sourceMappingURL=presenceAdapter.js.map
@@ -0,0 +1 @@
{"version":3,"file":"presenceAdapter.js","sourceRoot":"","sources":["../../../src/phoenix/presenceAdapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
import type { Message, SocketOnClose, SocketOnMessage, SocketOnOpen, SocketOnError, SocketOptions, SocketStateChangeCallbacks, Vsn, Encode, Decode, HeartbeatCallback, Timer } from './types';
import { ConnectionState } from '../lib/constants';
import type { HeartbeatTimer, WebSocketLikeConstructor } from '../RealtimeClient';
export default class SocketAdapter {
private socket;
constructor(endPoint: string, options: SocketOptions);
get timeout(): number;
get endPoint(): string;
get transport(): WebSocketLikeConstructor;
get heartbeatIntervalMs(): number;
get heartbeatCallback(): HeartbeatCallback;
set heartbeatCallback(callback: HeartbeatCallback);
get heartbeatTimer(): HeartbeatTimer;
get pendingHeartbeatRef(): string | null;
get reconnectTimer(): Timer;
get vsn(): Vsn;
get encode(): Encode<void>;
get decode(): Decode<void>;
get reconnectAfterMs(): (tries: number) => number;
get sendBuffer(): (() => void)[];
get stateChangeCallbacks(): SocketStateChangeCallbacks;
connect(): void;
disconnect(callback: () => void, code?: number, reason?: string, timeout?: number): Promise<'ok' | 'timeout'>;
push(data: Message<Record<string, unknown>>): void;
log(kind: string, msg: string, data?: any): void;
makeRef(): string;
onOpen(callback: SocketOnOpen): void;
onClose(callback: SocketOnClose): void;
onError(callback: SocketOnError): void;
onMessage(callback: SocketOnMessage): void;
isConnected(): boolean;
isConnecting(): boolean;
isDisconnecting(): boolean;
connectionState(): ConnectionState;
endPointURL(): string;
sendHeartbeat(): void;
}
//# sourceMappingURL=socketAdapter.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"socketAdapter.d.ts","sourceRoot":"","sources":["../../../src/phoenix/socketAdapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EACb,eAAe,EACf,YAAY,EACZ,aAAa,EACb,aAAa,EACb,0BAA0B,EAC1B,GAAG,EACH,MAAM,EACN,MAAM,EACN,iBAAiB,EACjB,KAAK,EACN,MAAM,SAAS,CAAA;AAChB,OAAO,EAAoB,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AAEjF,MAAM,CAAC,OAAO,OAAO,aAAa;IAChC,OAAO,CAAC,MAAM,CAAQ;gBAEV,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa;IAIpD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,SAAS,IAAI,wBAAwB,CAExC;IAED,IAAI,mBAAmB,IAAI,MAAM,CAEhC;IAED,IAAI,iBAAiB,IAAI,iBAAiB,CAEzC;IAED,IAAI,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,EAEhD;IAED,IAAI,cAAc,IAAI,cAAc,CAEnC;IAED,IAAI,mBAAmB,IAAI,MAAM,GAAG,IAAI,CAEvC;IAED,IAAI,cAAc,IAAI,KAAK,CAE1B;IAED,IAAI,GAAG,IAAI,GAAG,CAEb;IAED,IAAI,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,CAEzB;IAED,IAAI,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,CAEzB;IAED,IAAI,gBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAEhD;IAED,IAAI,UAAU,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,CAE/B;IAED,IAAI,oBAAoB,IAAI,0BAA0B,CAErD;IAED,OAAO;IAIP,UAAU,CACR,QAAQ,EAAE,MAAM,IAAI,EACpB,IAAI,CAAC,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,GAAE,MAAc,GACtB,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC;IAc5B,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAI3C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG;IAIzC,OAAO,IAAI,MAAM;IAIjB,MAAM,CAAC,QAAQ,EAAE,YAAY;IAI7B,OAAO,CAAC,QAAQ,EAAE,aAAa;IAI/B,OAAO,CAAC,QAAQ,EAAE,aAAa;IAI/B,SAAS,CAAC,QAAQ,EAAE,eAAe;IAInC,WAAW;IAIX,YAAY;IAIZ,eAAe;IAIf,eAAe,IAAI,eAAe;IAKlC,WAAW,IAAI,MAAM;IAIrB,aAAa;CAUd"}
+114
View File
@@ -0,0 +1,114 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const phoenix_1 = require("@supabase/phoenix");
const constants_1 = require("../lib/constants");
class SocketAdapter {
constructor(endPoint, options) {
this.socket = new phoenix_1.Socket(endPoint, options);
}
get timeout() {
return this.socket.timeout;
}
get endPoint() {
return this.socket.endPoint;
}
get transport() {
return this.socket.transport;
}
get heartbeatIntervalMs() {
return this.socket.heartbeatIntervalMs;
}
get heartbeatCallback() {
return this.socket.heartbeatCallback;
}
set heartbeatCallback(callback) {
this.socket.heartbeatCallback = callback;
}
get heartbeatTimer() {
return this.socket.heartbeatTimer;
}
get pendingHeartbeatRef() {
return this.socket.pendingHeartbeatRef;
}
get reconnectTimer() {
return this.socket.reconnectTimer;
}
get vsn() {
return this.socket.vsn;
}
get encode() {
return this.socket.encode;
}
get decode() {
return this.socket.decode;
}
get reconnectAfterMs() {
return this.socket.reconnectAfterMs;
}
get sendBuffer() {
return this.socket.sendBuffer;
}
get stateChangeCallbacks() {
return this.socket.stateChangeCallbacks;
}
connect() {
this.socket.connect();
}
disconnect(callback, code, reason, timeout = 10000) {
return new Promise((resolve) => {
setTimeout(() => resolve('timeout'), timeout);
this.socket.disconnect(() => {
callback();
resolve('ok');
}, code, reason);
});
}
push(data) {
this.socket.push(data);
}
log(kind, msg, data) {
this.socket.log(kind, msg, data);
}
makeRef() {
return this.socket.makeRef();
}
onOpen(callback) {
this.socket.onOpen(callback);
}
onClose(callback) {
this.socket.onClose(callback);
}
onError(callback) {
this.socket.onError(callback);
}
onMessage(callback) {
this.socket.onMessage(callback);
}
isConnected() {
return this.socket.isConnected();
}
isConnecting() {
return this.socket.connectionState() == constants_1.CONNECTION_STATE.connecting;
}
isDisconnecting() {
return this.socket.connectionState() == constants_1.CONNECTION_STATE.closing;
}
connectionState() {
// @ts-ignore - requires better typing and exposing type in phoenix
return this.socket.connectionState();
}
endPointURL() {
return this.socket.endPointURL();
}
sendHeartbeat() {
this.socket.sendHeartbeat();
}
/**
* @internal
*/
getSocket() {
return this.socket;
}
}
exports.default = SocketAdapter;
//# sourceMappingURL=socketAdapter.js.map
@@ -0,0 +1 @@
{"version":3,"file":"socketAdapter.js","sourceRoot":"","sources":["../../../src/phoenix/socketAdapter.ts"],"names":[],"mappings":"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"}
+5
View File
@@ -0,0 +1,5 @@
export type { Socket, SocketOptions, SocketState, SocketOnOpen, SocketOnError, SocketOnMessage, SocketOnClose, SocketStateChangeCallbacks, Channel, ChannelState, ChannelEvent, ChannelBindingCallback, ChannelFilterBindings, ChannelOnMessage, ChannelOnErrorCallback, PresenceState, Message, Params, Transport, Timer, Vsn, Encode, Decode, HeartbeatCallback, HeartbeatStatus, } from '@supabase/phoenix';
import type { Channel, PresenceState } from '@supabase/phoenix';
export type Push = ReturnType<Channel['push']>;
export type PresenceStates = Record<string, PresenceState>;
//# sourceMappingURL=types.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/phoenix/types.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,MAAM,EACN,aAAa,EACb,WAAW,EACX,YAAY,EACZ,aAAa,EACb,eAAe,EACf,aAAa,EACb,0BAA0B,EAC1B,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,EACrB,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,OAAO,EACP,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,MAAM,EACN,MAAM,EACN,iBAAiB,EACjB,eAAe,GAChB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAE/D,MAAM,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;AAC9C,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA"}
+3
View File
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/phoenix/types.ts"],"names":[],"mappings":""}
+575
View File
@@ -0,0 +1,575 @@
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 { ChannelBindingCallback } from './phoenix/types';
import type { Timer } from './phoenix/types';
import { RealtimePostgresFilterBuilder } from './RealtimePostgresFilterBuilder';
export type { RealtimePostgresChangesFilterOperator } from './RealtimePostgresFilterBuilder';
export { RealtimePostgresFilterBuilder, postgresChangesFilter, } from './RealtimePostgresFilterBuilder';
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
* replication_ready option instructs the server to emit a `system` event once the
* Postgres replication connection backing this channel is established and ready to
* stream changes. Listen for it with `channel.on('system', {}, (payload) => ...)`;
* the payload's `status` is `'ok'` (`message: 'Replication connection established'`)
* on success or `'error'` if the connection is not ready in time.
*/
broadcast?: {
self?: boolean;
ack?: boolean;
replay?: ReplayOption;
replication_ready?: boolean;
};
/**
* key option is used to track presence payload across clients
*
* enabled controls whether this client receives presence state and updates from other
* clients — set it to true (or add an `.on('presence', ...)` listener, which enables it
* automatically) if you want to see who else is present. Without it, this client's
* `presenceState()` stays empty and no `presence` events fire for you, because the
* underlying presence state machine buffers incoming updates until it has received an
* initial snapshot, which is only requested when this flag is set.
*
* It does not gate the other direction: calling `track()` always makes this client
* visible to other subscribers that have presence enabled, regardless of this client's
* own `enabled` setting. On RLS-protected (private) channels, receiving presence updates
* additionally requires the `presence.read` policy to authorize this client.
*/
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 only when the filter is matched.
*
* A filter is a `column=operator.value` expression, e.g. `id=eq.1` or
* `title=like.%foo%`. See {@link RealtimePostgresChangesFilterOperator} for
* the available operators.
*
* Multiple filters can be combined with commas; they are applied as an `AND`
* condition: `filter: 'id=gt.0,id=lt.100'`.
*
* Any operator can be negated with the `not.` prefix: `filter: 'status=not.in.(draft,archived)'`.
*
* The server splits conditions on commas outside quotes/parentheses. To
* include a reserved character (`,`, `(`, `)`) in a value, wrap it in double
* quotes PostgREST-style: `name=eq."a,b"`. The {@link RealtimePostgresFilterBuilder}
* does this quoting for you.
*
* Instead of a raw string you can pass a {@link RealtimePostgresFilterBuilder}
* (via `postgresChangesFilter()`) for a type-checked, ergonomic way to compose filters; the
* SDK serializes it to a string automatically.
*/
filter?: string | RealtimePostgresFilterBuilder;
/**
* Restrict the change payload to a subset of columns instead of receiving the
* full row. Reduces payload size (helpful for large `bytea`/`jsonb` columns)
* and the data transferred per event.
*
* The listed columns must be selectable by the subscribing role.
*
* @example
* channel.on('postgres_changes', {
* event: '*',
* schema: 'public',
* table: 'users',
* select: ['id', 'first_name'],
* }, (payload) => {
* // payload.new only contains { id, first_name }
* })
*/
select?: string[];
};
export type RealtimeChannelSendResponse = 'ok' | 'timed out' | 'error' | (string & {});
/**
* Payload of a `system` event emitted by the server.
*
* Most notably, when a channel is created with `config.broadcast.replication_ready: true`,
* the server sends one of these once the Postgres replication connection is ready
* (`status: 'ok'`) or fails to become ready in time (`status: 'error'`).
*/
export type RealtimeSystemPayload = {
/** The extension that produced the message, e.g. `'system'` or `'postgres_changes'`. */
extension: 'system' | 'postgres_changes' | (string & {});
/** `'ok'` on success, `'error'` on failure. */
status: 'ok' | 'error' | (string & {});
/** Human-readable description, e.g. `'Replication connection established'`. */
message: string;
/** The channel (sub)topic the message refers to. */
channel: string;
};
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: {
readonly closed: "closed";
readonly errored: "errored";
readonly joined: "joined";
readonly joining: "joining";
readonly leaving: "leaving";
};
type Binding = {
type: string;
filter: {
[key: string]: any;
};
callback: ChannelBindingCallback;
ref: number;
id?: string;
};
/** 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: Record<string, Binding[]>;
subTopic: string;
broadcastEndpointURL: string;
private: boolean;
presence: RealtimePresence;
get state(): ChannelState;
set state(state: ChannelState);
get joinedOnce(): boolean;
get timeout(): number;
get joinPush(): import("@supabase/phoenix").Push;
get rejoinTimer(): Timer;
/**
* 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.
*
* @category Realtime
*
* @example Using supabase-js (recommended)
* ```ts
* import { createClient } from '@supabase/supabase-js'
*
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
* const channel = supabase.channel('room1')
* channel
* .on('broadcast', { event: 'cursor-pos' }, (payload) => console.log(payload))
* .subscribe()
* ```
*
* @example Standalone import for bundle-sensitive environments
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'your-publishable-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.
*
* The optional `callback` receives a `status` and, on failure, an `err` argument.
* Log the full `err` so its `cause`, `name`, and any structured fields aren't hidden
* behind `err.message`.
*
* @category Realtime
*
* @example Handling errors
* ```js
* supabase.channel('room1').subscribe((status, err) => {
* if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
* // Log the full error: its `cause` often holds the underlying reason.
* console.error(status, err)
* }
* })
* ```
*/
subscribe(callback?: (status: REALTIME_SUBSCRIBE_STATES, err?: Error) => void, timeout?: number): RealtimeChannel;
private _updatePostgresBindings;
/**
* 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.
*
* @category Realtime
*/
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.
*
* Tracking makes this client visible to other subscribers immediately, regardless of this
* channel's `config.presence.enabled` setting or whether it has a `presence` listener — that
* flag only affects whether *this* client receives presence updates from others (and, on
* RLS-protected channels, whether it's authorized to do so).
*
* @category Realtime
*/
track(payload: {
[key: string]: any;
}, opts?: {
[key: string]: any;
}): Promise<RealtimeChannelSendResponse>;
/**
* Removes the current presence state for this client.
*
* @category Realtime
*/
untrack(opts?: {
[key: string]: any;
}): Promise<RealtimeChannelSendResponse>;
/**
* Listen for presence events on this channel — when peers join, leave, or
* sync presence state.
*/
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;
/**
* Listen for Postgres database changes (insert / update / delete) streamed
* over this channel.
*/
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;
/**
* Listen for broadcast messages sent on this channel.
*
* @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;
/**
* Listen for `system` events on this channel.
*
* The payload follows the {@link RealtimeSystemPayload} shape. Opt in to the replication-ready
* notification with `config.broadcast.replication_ready: true` when creating the channel, then
* watch for `payload.status === 'ok'` to know the Postgres replication connection is ready.
*
* @example Know when the replication connection is ready
* ```js
* const channel = supabase.channel('room1', {
* config: { broadcast: { replication_ready: true } },
* })
*
* channel
* .on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, (payload) => {
* console.log('Change received!', payload)
* })
* .on('system', {}, (payload) => {
* if (payload.extension === 'system' && payload.status === 'ok') {
* console.log('Replication connection is ready:', payload.message)
* }
* })
* .subscribe()
* ```
*/
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.
*
* Payloads that are `ArrayBuffer` or `ArrayBufferView` (e.g. `Uint8Array`) are sent as
* `application/octet-stream`; all other payloads are JSON-encoded.
*
* @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
*
* @category Realtime
*/
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
*
* @category Realtime
*
* @remarks
* - When using REST you don't need to subscribe to the channel
* - REST calls are only available from 2.37.0 onwards
* - If you create a channel only to send a REST broadcast, remove it from
* the client when the send completes
*
* @example Send a message via websocket
* ```js
* const channel = supabase.channel('room1')
*
* channel.subscribe((status) => {
* if (status === 'SUBSCRIBED') {
* channel.send({
* type: 'broadcast',
* event: 'cursor-pos',
* payload: { x: Math.random(), y: Math.random() },
* })
* }
* })
* ```
*
* @exampleResponse Send a message via websocket
* ```js
* ok | timed out | error
* ```
*
* @example Send a message via REST
* ```js
* const channel = supabase.channel('room1')
*
* try {
* await channel.httpSend('cursor-pos', { x: Math.random(), y: Math.random() })
* } finally {
* await supabase.removeChannel(channel)
* }
* ```
*/
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.
*
* @category Realtime
*/
updateJoinPayload(payload: Record<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!") )
*
* @category Realtime
*/
unsubscribe(timeout?: number): Promise<RealtimeChannelSendResponse>;
/**
* Destroys and stops related timers.
*
* @category Realtime
*/
teardown(): void;
copyBindings(other: RealtimeChannel): void;
}
//# sourceMappingURL=RealtimeChannel.d.ts.map
File diff suppressed because one or more lines are too long
+758
View File
@@ -0,0 +1,758 @@
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;
/** 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 {
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;
}
/**
* 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.
*
* @category Realtime
*
* @example Using supabase-js (recommended)
* ```ts
* import { createClient } from '@supabase/supabase-js'
*
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
* const channel = supabase.channel('room1')
* channel
* .on('broadcast', { event: 'cursor-pos' }, (payload) => console.log(payload))
* .subscribe()
* ```
*
* @example Standalone import for bundle-sensitive environments
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'your-publishable-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.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 registers your client with the server.
*
* The optional `callback` receives a `status` and, on failure, an `err` argument.
* Log the full `err` so its `cause`, `name`, and any structured fields aren't hidden
* behind `err.message`.
*
* @category Realtime
*
* @example Handling errors
* ```js
* supabase.channel('room1').subscribe((status, err) => {
* if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
* // Log the full error: its `cause` often holds the underlying reason.
* console.error(status, err)
* }
* })
* ```
*/
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 }) => {
// 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;
}
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);
}
}
/**
* 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.
*
* @category Realtime
*/
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.
*
* Tracking makes this client visible to other subscribers immediately, regardless of this
* channel's `config.presence.enabled` setting or whether it has a `presence` listener — that
* flag only affects whether *this* client receives presence updates from others (and, on
* RLS-protected channels, whether it's authorized to do so).
*
* @category Realtime
*/
async track(payload, opts = {}) {
return await this.send({
type: 'presence',
event: 'track',
payload,
}, opts);
}
/**
* Removes the current presence state for this client.
*
* @category Realtime
*/
async untrack(opts = {}) {
return await this.send({
type: 'presence',
event: 'untrack',
}, opts);
}
/**
* Listen to realtime events on this channel.
* @category Realtime
*
* @remarks
* - By default, Broadcast and Presence are enabled for all projects.
* - By default, listening to database changes is disabled for new projects due to database performance and security concerns. You can turn it on by managing Realtime's [replication](/docs/guides/api#realtime-api-overview).
* - You can receive the "previous" data for updates and deletes by setting the table's `REPLICA IDENTITY` to `FULL` (e.g., `ALTER TABLE your_table REPLICA IDENTITY FULL;`).
* - Row level security is not applied to delete statements. When RLS is enabled and replica identity is set to full, only the primary key is sent to clients.
*
* @example Listen to broadcast messages
* ```js
* const channel = supabase.channel("room1")
*
* channel.on("broadcast", { event: "cursor-pos" }, (payload) => {
* console.log("Cursor position received!", payload);
* }).subscribe((status) => {
* if (status === "SUBSCRIBED") {
* channel.send({
* type: "broadcast",
* event: "cursor-pos",
* payload: { x: Math.random(), y: Math.random() },
* });
* }
* });
* ```
*
* @example Listen to presence sync
* ```js
* const channel = supabase.channel('room1')
* channel
* .on('presence', { event: 'sync' }, () => {
* console.log('Synced presence state: ', channel.presenceState())
* })
* .subscribe(async (status) => {
* if (status === 'SUBSCRIBED') {
* await channel.track({ online_at: new Date().toISOString() })
* }
* })
* ```
*
* @example Listen to presence join
* ```js
* const channel = supabase.channel('room1')
* channel
* .on('presence', { event: 'join' }, ({ newPresences }) => {
* console.log('Newly joined presences: ', newPresences)
* })
* .subscribe(async (status) => {
* if (status === 'SUBSCRIBED') {
* await channel.track({ online_at: new Date().toISOString() })
* }
* })
* ```
*
* @example Listen to presence leave
* ```js
* const channel = supabase.channel('room1')
* channel
* .on('presence', { event: 'leave' }, ({ leftPresences }) => {
* console.log('Newly left presences: ', leftPresences)
* })
* .subscribe(async (status) => {
* if (status === 'SUBSCRIBED') {
* await channel.track({ online_at: new Date().toISOString() })
* await channel.untrack()
* }
* })
* ```
*
* @example Listen to all database changes
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: '*', schema: '*' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @example Listen to a specific table
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: '*', schema: 'public', table: 'countries' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @example Listen to inserts
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'countries' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @exampleDescription Listen to updates
* By default, Supabase will send only the updated record. If you want to receive the previous values as well you can
* enable full replication for the table you are listening to:
*
* ```sql
* alter table "your_table" replica identity full;
* ```
*
* @example Listen to updates
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'countries' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @exampleDescription Listen to deletes
* By default, Supabase does not send deleted records. If you want to receive the deleted record you can
* enable full replication for the table you are listening to:
*
* ```sql
* alter table "your_table" replica identity full;
* ```
*
* @example Listen to deletes
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'countries' }, payload => {
* console.log('Change received!', payload)
* })
* .subscribe()
* ```
*
* @exampleDescription Listen to multiple events
* You can chain listeners if you want to listen to multiple events for each table.
*
* @example Listen to multiple events
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'countries' }, handleRecordInserted)
* .on('postgres_changes', { event: 'DELETE', schema: 'public', table: 'countries' }, handleRecordDeleted)
* .subscribe()
* ```
*
* @exampleDescription Listen to row level changes
* You can listen to individual rows using the format `{table}:{col}=eq.{val}` - where `{col}` is the column name, and `{val}` is the value which you want to match.
*
* @example Listen to row level changes
* ```js
* supabase
* .channel('room1')
* .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'countries', filter: 'id=eq.200' }, handleRecordUpdated)
* .subscribe()
* ```
*/
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);
}
/**
* 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.
*
* Payloads that are `ArrayBuffer` or `ArrayBufferView` (e.g. `Uint8Array`) are sent as
* `application/octet-stream`; all other payloads are JSON-encoded.
*
* @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
*
* @category Realtime
*/
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));
}
/**
* 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
*
* @category Realtime
*
* @remarks
* - When using REST you don't need to subscribe to the channel
* - REST calls are only available from 2.37.0 onwards
* - If you create a channel only to send a REST broadcast, remove it from
* the client when the send completes
*
* @example Send a message via websocket
* ```js
* const channel = supabase.channel('room1')
*
* channel.subscribe((status) => {
* if (status === 'SUBSCRIBED') {
* channel.send({
* type: 'broadcast',
* event: 'cursor-pos',
* payload: { x: Math.random(), y: Math.random() },
* })
* }
* })
* ```
*
* @exampleResponse Send a message via websocket
* ```js
* ok | timed out | error
* ```
*
* @example Send a message via REST
* ```js
* const channel = supabase.channel('room1')
*
* try {
* await channel.httpSend('cursor-pos', { x: Math.random(), y: Math.random() })
* } finally {
* await supabase.removeChannel(channel)
* }
* ```
*/
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'));
});
}
}
/**
* 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.
*
* @category Realtime
*/
updateJoinPayload(payload) {
this.channelAdapter.updateJoinPayload(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!") )
*
* @category Realtime
*/
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'));
});
}
/**
* Destroys and stops related timers.
*
* @category Realtime
*/
teardown() {
this.channelAdapter.teardown();
}
/** @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 */
_on(type, filter, callback) {
const typeLower = type.toLocaleLowerCase();
// Serialize a postgres_changes filter builder into its string form so the
// rest of the pipeline (join payload, server binding match) sees a string.
// Duck-type `build()` in addition to `instanceof` so a builder constructed
// against a duplicate copy of the package (separate module realm) still works.
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;
}
/**
* Registers a callback that will be executed when the channel closes.
*
* @internal
*/
_onClose(callback) {
this.channelAdapter.onClose(callback);
}
/**
* Registers a callback that will be executed when the channel encounteres an error.
*
* @internal
*/
_onError(callback) {
this.channelAdapter.onError(callback);
}
/** @internal */
_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;
}
});
}
/** @internal */
_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;
}
/** @internal */
_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);
}
}
}
/**
* 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 */
_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
+274
View File
@@ -0,0 +1,274 @@
import { WebSocketLike } from './lib/websocket-factory';
import Serializer from './lib/serializer';
import RealtimeChannel from './RealtimeChannel';
import type { RealtimeChannelOptions } from './RealtimeChannel';
import type { HeartbeatCallback, Encode, Decode, Timer, Vsn } from './phoenix/types';
type Fetch = typeof fetch;
export type LogLevel = 'info' | 'warn' | 'error' | (string & {});
export type RealtimeMessage = {
topic: string;
event: string;
payload: any;
ref: string;
join_ref?: string;
};
export type RealtimeRemoveChannelResponse = 'ok' | 'timed out' | 'error' | (string & {});
export type HeartbeatStatus = 'sent' | 'ok' | 'error' | 'timeout' | 'disconnected' | (string & {});
export type HeartbeatTimer = ReturnType<typeof setTimeout> | undefined;
/**
* 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 type RealtimeClientOptions = {
transport?: WebSocketLikeConstructor;
timeout?: number;
heartbeatIntervalMs?: number;
heartbeatCallback?: (status: HeartbeatStatus, latency?: number) => void;
vsn?: string;
logger?: (kind: string, msg: string, data?: any) => void;
encode?: Encode<void>;
decode?: Decode<void>;
reconnectAfterMs?: (tries: number) => number;
headers?: {
[key: string]: string;
};
params?: {
[key: string]: any;
};
log_level?: LogLevel;
logLevel?: LogLevel;
fetch?: Fetch;
worker?: boolean;
workerUrl?: string;
accessToken?: () => Promise<string | null>;
disconnectOnEmptyChannelsAfterMs?: number;
/**
* Storage compatible object used by the underlying socket for longpoll fallback history.
* Provide a custom implementation in environments where reading `globalThis.sessionStorage`
* throws (sandboxed iframes, in-app webviews, "block third-party storage" privacy modes).
* Defaults to `globalThis.sessionStorage` when accessible, otherwise an in-memory store.
*/
sessionStorage?: Storage;
};
export default class RealtimeClient {
channels: RealtimeChannel[];
accessTokenValue: string | null;
accessToken: (() => Promise<string | null>) | null;
apiKey: string | null;
httpEndpoint: string;
/** @deprecated headers cannot be set on websocket connections */
headers?: {
[key: string]: string;
};
params?: {
[key: string]: string;
};
ref: number;
logLevel?: LogLevel;
fetch: Fetch;
worker?: boolean;
workerUrl?: string;
workerRef?: Worker;
serializer: Serializer;
get endPoint(): string;
get timeout(): number;
get transport(): WebSocketLikeConstructor;
get heartbeatCallback(): HeartbeatCallback;
get heartbeatIntervalMs(): number;
get heartbeatTimer(): HeartbeatTimer;
get pendingHeartbeatRef(): string | null;
get reconnectTimer(): Timer;
get vsn(): Vsn;
get encode(): Encode<void>;
get decode(): Decode<void>;
get reconnectAfterMs(): (tries: number) => number;
get sendBuffer(): (() => void)[];
get stateChangeCallbacks(): {
open: [string, Function][];
close: [string, Function][];
error: [string, Function][];
message: [string, Function][];
};
private _manuallySetToken;
private _authPromise;
private _workerHeartbeatTimer;
private _pendingWorkerHeartbeatRef;
private _pendingDisconnectTimer;
private _disconnectOnEmptyChannelsAfterMs;
/**
* Initializes the Socket.
*
* @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (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".
*
* @category Realtime
*
* @example Using supabase-js (recommended)
* ```ts
* import { createClient } from '@supabase/supabase-js'
*
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
* const channel = supabase.channel('room1')
* channel
* .on('broadcast', { event: 'cursor-pos' }, (payload) => console.log(payload))
* .subscribe()
* ```
*
* @example Standalone import for bundle-sensitive environments
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'your-publishable-key' },
* })
* client.connect()
* ```
*/
constructor(endPoint: string, options?: RealtimeClientOptions);
/**
* Connects the socket, unless already connected.
*
* @category Realtime
*/
connect(): void;
/**
* Returns the URL of the websocket.
* @returns string The URL of the websocket.
*
* @category Realtime
*/
endpointURL(): string;
/**
* Disconnects the socket.
*
* @param code A numeric status code to send on disconnect.
* @param reason A custom reason for the disconnect.
*
* @category Realtime
*/
disconnect(code?: number, reason?: string): Promise<"ok" | "timeout">;
/**
* Returns all created channels
*
* @category Realtime
*/
getChannels(): RealtimeChannel[];
/**
* Unsubscribes, removes and tears down a single channel
* @param channel A RealtimeChannel instance
*
* @category Realtime
*/
removeChannel(channel: RealtimeChannel): Promise<RealtimeRemoveChannelResponse>;
/**
* Unsubscribes, removes and tears down all channels
*
* @category Realtime
*/
removeAllChannels(): Promise<RealtimeRemoveChannelResponse[]>;
/**
* Logs the message.
*
* For customized logging, `this.logger` can be overridden in Client constructor.
*
* @category Realtime
*/
log(kind: string, msg: string, data?: any): void;
/**
* Returns the current state of the socket.
*
* @category Realtime
*/
connectionState(): import("./lib/constants").ConnectionState;
/**
* Returns `true` is the connection is open.
*
* @category Realtime
*/
isConnected(): boolean;
/**
* Returns `true` if the connection is currently connecting.
*
* @category Realtime
*/
isConnecting(): boolean;
/**
* Returns `true` if the connection is currently disconnecting.
*
* @category Realtime
*/
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.
*
* @category Realtime
*/
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.
*
* @category Realtime
*/
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 Setting the authorization header
* // 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()
*
* @category Realtime
*/
setAuth(token?: string | null): Promise<void>;
/**
* Sends a heartbeat message if the socket is connected.
*
* @category Realtime
*/
sendHeartbeat(): Promise<void>;
/**
* Sets a callback that receives lifecycle events for internal heartbeat messages.
* Useful for instrumenting connection health (e.g. sent/ok/timeout).
*
* @category Realtime
*/
onHeartbeat(callback: HeartbeatCallback): void;
}
export {};
//# sourceMappingURL=RealtimeClient.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"RealtimeClient.d.ts","sourceRoot":"","sources":["../../src/RealtimeClient.ts"],"names":[],"mappings":"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"}
+681
View File
@@ -0,0 +1,681 @@
import WebSocketFactory from './lib/websocket-factory';
import { CHANNEL_EVENTS, CONNECTION_STATE, DEFAULT_VERSION, DEFAULT_TIMEOUT, DEFAULT_VSN, VSN_1_0_0, VSN_2_0_0, } from './lib/constants';
import Serializer from './lib/serializer';
import { httpEndpointURL } from './lib/transformers';
import RealtimeChannel from './RealtimeChannel';
import SocketAdapter from './phoenix/socketAdapter';
// 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;
function createMemorySessionStorage() {
const store = new Map();
return {
get length() {
return store.size;
},
clear() {
store.clear();
},
getItem(key) {
return store.has(key) ? store.get(key) : null;
},
key(index) {
var _a;
return (_a = Array.from(store.keys())[index]) !== null && _a !== void 0 ? _a : null;
},
removeItem(key) {
store.delete(key);
},
setItem(key, value) {
store.set(key, String(value));
},
};
}
function resolveSessionStorage() {
try {
if (typeof globalThis !== 'undefined' && globalThis.sessionStorage) {
return globalThis.sessionStorage;
}
}
catch (_a) {
// Property access on `sessionStorage` itself throws in restricted-storage browsers.
}
return createMemorySessionStorage();
}
const WORKER_SCRIPT = `
addEventListener("message", (e) => {
if (e.data.event === "start") {
setInterval(() => postMessage({ event: "keepAlive" }), e.data.interval);
}
});`;
export default class RealtimeClient {
get endPoint() {
return this.socketAdapter.endPoint;
}
get timeout() {
return this.socketAdapter.timeout;
}
get transport() {
return this.socketAdapter.transport;
}
get heartbeatCallback() {
return this.socketAdapter.heartbeatCallback;
}
get heartbeatIntervalMs() {
return this.socketAdapter.heartbeatIntervalMs;
}
get heartbeatTimer() {
if (this.worker) {
return this._workerHeartbeatTimer;
}
return this.socketAdapter.heartbeatTimer;
}
get pendingHeartbeatRef() {
if (this.worker) {
return this._pendingWorkerHeartbeatRef;
}
return this.socketAdapter.pendingHeartbeatRef;
}
get reconnectTimer() {
return this.socketAdapter.reconnectTimer;
}
get vsn() {
return this.socketAdapter.vsn;
}
get encode() {
return this.socketAdapter.encode;
}
get decode() {
return this.socketAdapter.decode;
}
get reconnectAfterMs() {
return this.socketAdapter.reconnectAfterMs;
}
get sendBuffer() {
return this.socketAdapter.sendBuffer;
}
get stateChangeCallbacks() {
return this.socketAdapter.stateChangeCallbacks;
}
/**
* Initializes the Socket.
*
* @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (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".
*
* @category Realtime
*
* @example Using supabase-js (recommended)
* ```ts
* import { createClient } from '@supabase/supabase-js'
*
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
* const channel = supabase.channel('room1')
* channel
* .on('broadcast', { event: 'cursor-pos' }, (payload) => console.log(payload))
* .subscribe()
* ```
*
* @example Standalone import for bundle-sensitive environments
* ```ts
* import RealtimeClient from '@supabase/realtime-js'
*
* const client = new RealtimeClient('https://xyzcompany.supabase.co/realtime/v1', {
* params: { apikey: 'your-publishable-key' },
* })
* client.connect()
* ```
*/
constructor(endPoint, options) {
var _a;
this.channels = new Array();
this.accessTokenValue = null;
this.accessToken = null;
this.apiKey = null;
this.httpEndpoint = '';
/** @deprecated headers cannot be set on websocket connections */
this.headers = {};
this.params = {};
this.ref = 0;
this.serializer = new Serializer();
this._manuallySetToken = false;
this._authPromise = null;
this._workerHeartbeatTimer = undefined;
this._pendingWorkerHeartbeatRef = null;
this._pendingDisconnectTimer = null;
this._disconnectOnEmptyChannelsAfterMs = 0;
/**
* 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;
const socketAdapterOptions = this._initializeOptions(options);
this.socketAdapter = new SocketAdapter(endPoint, socketAdapterOptions);
this.httpEndpoint = httpEndpointURL(endPoint);
this.fetch = this._resolveFetch(options === null || options === void 0 ? void 0 : options.fetch);
}
/**
* Connects the socket, unless already connected.
*
* @category Realtime
*/
connect() {
// Skip if already connecting, disconnecting, or connected
if (this.isConnecting() || this.isDisconnecting() || this.isConnected()) {
return;
}
// 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');
}
this._setupConnectionHandlers();
try {
this.socketAdapter.connect();
}
catch (error) {
const errorMessage = error.message;
throw new Error(`WebSocket not available: ${errorMessage}`);
}
this._handleNodeJsRaceCondition();
}
/**
* Returns the URL of the websocket.
* @returns string The URL of the websocket.
*
* @category Realtime
*/
endpointURL() {
return this.socketAdapter.endPointURL();
}
/**
* Disconnects the socket.
*
* @param code A numeric status code to send on disconnect.
* @param reason A custom reason for the disconnect.
*
* @category Realtime
*/
async disconnect(code, reason) {
this._cancelPendingDisconnect();
if (this.isDisconnecting()) {
return 'ok';
}
return await this.socketAdapter.disconnect(() => {
clearInterval(this._workerHeartbeatTimer);
this._terminateWorker();
}, code, reason);
}
/**
* Returns all created channels
*
* @category Realtime
*/
getChannels() {
return this.channels;
}
/**
* Unsubscribes, removes and tears down a single channel
* @param channel A RealtimeChannel instance
*
* @category Realtime
*/
async removeChannel(channel) {
const status = await channel.unsubscribe();
if (status === 'ok') {
channel.teardown();
}
return status;
}
/**
* Unsubscribes, removes and tears down all channels
*
* @category Realtime
*/
async removeAllChannels() {
const promises = this.channels.map(async (channel) => {
const result = await channel.unsubscribe();
channel.teardown();
return result;
});
const result = await Promise.all(promises);
await this.disconnect();
return result;
}
/**
* Logs the message.
*
* For customized logging, `this.logger` can be overridden in Client constructor.
*
* @category Realtime
*/
log(kind, msg, data) {
this.socketAdapter.log(kind, msg, data);
}
/**
* Returns the current state of the socket.
*
* @category Realtime
*/
connectionState() {
return this.socketAdapter.connectionState() || CONNECTION_STATE.closed;
}
/**
* Returns `true` is the connection is open.
*
* @category Realtime
*/
isConnected() {
return this.socketAdapter.isConnected();
}
/**
* Returns `true` if the connection is currently connecting.
*
* @category Realtime
*/
isConnecting() {
return this.socketAdapter.isConnecting();
}
/**
* Returns `true` if the connection is currently disconnecting.
*
* @category Realtime
*/
isDisconnecting() {
return this.socketAdapter.isDisconnecting();
}
/**
* 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.
*
* @category Realtime
*/
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._cancelPendingDisconnect();
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.
*
* @category Realtime
*/
push(data) {
this.socketAdapter.push(data);
}
/**
* 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 Setting the authorization header
* // 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()
*
* @category Realtime
*/
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.
*
* @category Realtime
*/
async sendHeartbeat() {
this.socketAdapter.sendHeartbeat();
}
/**
* Sets a callback that receives lifecycle events for internal heartbeat messages.
* Useful for instrumenting connection health (e.g. sent/ok/timeout).
*
* @category Realtime
*/
onHeartbeat(callback) {
this.socketAdapter.heartbeatCallback = this._wrapHeartbeatCallback(callback);
}
/**
* Return the next message ref, accounting for overflows
*
* @internal
*/
_makeRef() {
return this.socketAdapter.makeRef();
}
/**
* Removes a channel from RealtimeClient
*
* @param channel An open subscription.
*
* @internal
*/
_remove(channel) {
this.channels = this.channels.filter((c) => c.topic !== channel.topic);
if (this.channels.length === 0) {
this.log('transport', 'no channels remaining, scheduling disconnect');
this._schedulePendingDisconnect();
}
}
/** @internal */
_schedulePendingDisconnect() {
this._cancelPendingDisconnect();
if (this._disconnectOnEmptyChannelsAfterMs === 0) {
this.log('transport', 'disconnecting immediately - no channels');
this.disconnect();
return;
}
this._pendingDisconnectTimer = setTimeout(() => {
this._pendingDisconnectTimer = null;
if (this.channels.length === 0) {
this.log('transport', 'deferred disconnect fired - no channels, disconnecting');
this.disconnect();
}
}, this._disconnectOnEmptyChannelsAfterMs);
this.log('transport', `deferred disconnect scheduled in ${this._disconnectOnEmptyChannelsAfterMs}ms`);
}
/** @internal */
_cancelPendingDisconnect() {
if (this._pendingDisconnectTimer !== null) {
this.log('transport', 'pending disconnect cancelled - channel activity detected');
clearTimeout(this._pendingDisconnectTimer);
this._pendingDisconnectTimer = null;
}
}
/**
* 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.channelAdapter.isJoined()) {
channel.channelAdapter.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);
});
}
}
/** @internal */
_setupConnectionHandlers() {
this.socketAdapter.onOpen(() => {
const authPromise = this._authPromise ||
(this.accessToken && !this.accessTokenValue ? this.setAuth() : Promise.resolve());
authPromise.catch((e) => {
this.log('error', 'error waiting for auth on connect', e);
});
if (this.worker && !this.workerRef) {
this._startWorkerHeartbeat();
}
});
this.socketAdapter.onClose(() => {
if (this.worker && this.workerRef) {
this._terminateWorker();
}
});
this.socketAdapter.onMessage((message) => {
if (message.ref && message.ref === this._pendingWorkerHeartbeatRef) {
this._pendingWorkerHeartbeatRef = null;
}
});
}
/** @internal */
_handleNodeJsRaceCondition() {
if (this.socketAdapter.isConnected()) {
// hack: ensure onConnOpen is called
this.socketAdapter.getSocket().onConnOpen();
}
}
/** @internal */
_wrapHeartbeatCallback(heartbeatCallback) {
return (status, latency) => {
if (status === 'disconnected')
return;
if (status == 'sent')
this._setAuthSafely();
if (heartbeatCallback)
heartbeatCallback(status, latency);
};
}
/** @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.disconnect();
};
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 */
_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;
}
/**
* Initialize socket options with defaults
* @internal
*/
_initializeOptions(options) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
this.worker = (_a = options === null || options === void 0 ? void 0 : options.worker) !== null && _a !== void 0 ? _a : false;
this.accessToken = (_b = options === null || options === void 0 ? void 0 : options.accessToken) !== null && _b !== void 0 ? _b : null;
const result = {};
result.timeout = (_c = options === null || options === void 0 ? void 0 : options.timeout) !== null && _c !== void 0 ? _c : DEFAULT_TIMEOUT;
result.heartbeatIntervalMs =
(_d = options === null || options === void 0 ? void 0 : options.heartbeatIntervalMs) !== null && _d !== void 0 ? _d : CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL;
this._disconnectOnEmptyChannelsAfterMs =
(_e = options === null || options === void 0 ? void 0 : options.disconnectOnEmptyChannelsAfterMs) !== null && _e !== void 0 ? _e : 2 * ((_f = options === null || options === void 0 ? void 0 : options.heartbeatIntervalMs) !== null && _f !== void 0 ? _f : CONNECTION_TIMEOUTS.HEARTBEAT_INTERVAL);
// @ts-ignore - mismatch between phoenix and supabase
result.transport = (_g = options === null || options === void 0 ? void 0 : options.transport) !== null && _g !== void 0 ? _g : WebSocketFactory.getWebSocketConstructor();
result.params = options === null || options === void 0 ? void 0 : options.params;
result.logger = options === null || options === void 0 ? void 0 : options.logger;
result.heartbeatCallback = this._wrapHeartbeatCallback(options === null || options === void 0 ? void 0 : options.heartbeatCallback);
result.sessionStorage = (_h = options === null || options === void 0 ? void 0 : options.sessionStorage) !== null && _h !== void 0 ? _h : resolveSessionStorage();
result.reconnectAfterMs =
(_j = options === null || options === void 0 ? void 0 : options.reconnectAfterMs) !== null && _j !== void 0 ? _j : ((tries) => {
return RECONNECT_INTERVALS[tries - 1] || DEFAULT_RECONNECT_FALLBACK;
});
let defaultEncode;
let defaultDecode;
const vsn = (_k = options === null || options === void 0 ? void 0 : options.vsn) !== null && _k !== void 0 ? _k : DEFAULT_VSN;
switch (vsn) {
case VSN_1_0_0:
defaultEncode = (payload, callback) => {
return callback(JSON.stringify(payload));
};
defaultDecode = (payload, callback) => {
return callback(JSON.parse(payload));
};
break;
case VSN_2_0_0:
defaultEncode = this.serializer.encode.bind(this.serializer);
defaultDecode = this.serializer.decode.bind(this.serializer);
break;
default:
throw new Error(`Unsupported serializer version: ${result.vsn}`);
}
result.vsn = vsn;
result.encode = (_l = options === null || options === void 0 ? void 0 : options.encode) !== null && _l !== void 0 ? _l : defaultEncode;
result.decode = (_m = options === null || options === void 0 ? void 0 : options.decode) !== null && _m !== void 0 ? _m : defaultDecode;
result.beforeReconnect = this._reconnectAuth.bind(this);
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;
result.params = Object.assign(Object.assign({}, result.params), { log_level: this.logLevel });
}
// 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;
result.autoSendHeartbeat = !this.worker;
}
return result;
}
/** @internal */
async _reconnectAuth() {
await this._waitForAuthIfNeeded();
if (!this.isConnected()) {
this.connect();
}
}
}
//# sourceMappingURL=RealtimeClient.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,132 @@
/**
* Comparison operators accepted in a Postgres Changes `filter` string.
*
* These mirror the PostgREST operator surface and are evaluated server-side.
* Any operator can be negated via {@link RealtimePostgresFilterBuilder.not}
* (the `not.` prefix).
*
* This is the subset of PostgREST's `FilterOperator` (see `postgrest-js`) that
* Realtime Postgres Changes supports. Containment, range and full-text search
* operators (`cs`, `cd`, `ov`, range ops, `fts`, …) are intentionally not
* included because the Realtime server does not evaluate them.
*
* - `eq`, `neq`, `lt`, `lte`, `gt`, `gte` — comparison
* - `in` — membership: `status=in.(active,pending)`
* - `like`, `ilike` — pattern match (case-sensitive / insensitive): `title=like.%foo%`
* - `is` — `IS` check against `null` / `true` / `false` / `unknown`: `deleted_at=is.null`
* - `match`, `imatch` — POSIX regex match (`~` / `~*`)
* - `isdistinct` — NULL-safe inequality (`IS DISTINCT FROM`)
*/
export type RealtimePostgresChangesFilterOperator = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'in' | 'like' | 'ilike' | 'is' | 'match' | 'imatch' | 'isdistinct';
/** Scalar value accepted by a single filter operator. */
export type RealtimeFilterValue = string | number | boolean | null;
/** Value accepted by the `is` operator. */
export type RealtimeIsFilterValue = null | boolean | 'null' | 'true' | 'false' | 'unknown';
/**
* Fluent builder for Postgres Changes `filter` strings.
*
* Each method appends a single `column=operator.value` condition. Multiple
* conditions are combined with commas, which the Realtime server applies as an
* `AND`. Pass an instance straight to `channel.on('postgres_changes', …)` — the
* SDK serializes it to a string automatically — or call {@link build} to obtain
* the string yourself.
*
* The builder mirrors the `postgrest-js` filter API (`eq`, `neq`, `in`, `like`,
* `not`, …) for the operators that Realtime supports. Values containing reserved
* characters (`,`, `(`, `)`, `"`, `\`) — or surrounding whitespace — are
* automatically double-quoted and escaped the same way PostgREST does, so they
* survive the server's filter parser; all other values are sent verbatim.
*
* The filter is snapshotted when passed to `channel.on(...)`; mutating the
* builder afterwards does not affect an existing subscription. An empty builder
* serializes to `''`, which the server treats as "no filter".
*
* @example
* channel.on('postgres_changes', {
* event: '*',
* schema: 'public',
* table: 'users',
* filter: postgresChangesFilter().eq('id', 1).lt('age', 30), // → 'id=eq.1,age=lt.30'
* }, (payload) => { ... })
*/
export declare class RealtimePostgresFilterBuilder {
private readonly filters;
private add;
/** Match rows where `column` equals `value` (`column=eq.value`). */
eq(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` does not equal `value` (`column=neq.value`). */
neq(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` is greater than `value` (`column=gt.value`). */
gt(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` is greater than or equal to `value` (`column=gte.value`). */
gte(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` is less than `value` (`column=lt.value`). */
lt(column: string, value: RealtimeFilterValue): this;
/** Match rows where `column` is less than or equal to `value` (`column=lte.value`). */
lte(column: string, value: RealtimeFilterValue): this;
/**
* Match rows where `column` is one of `values` (`column=in.(a,b,c)`).
* Requires at least one value; duplicates are removed. An element containing a
* reserved character is double-quoted (`in.("a,b",c)`), so commas inside an
* element are preserved. `null` is intentionally not accepted (`IN (null)`
* never matches in SQL) — use `is`/`not('col','is',null)` for null checks.
*/
in(column: string, values: ReadonlyArray<string | number | boolean>): this;
/** Match rows where `column` matches the case-sensitive `pattern` (`column=like.pattern`). */
like(column: string, pattern: string): this;
/** Match rows where `column` matches the case-insensitive `pattern` (`column=ilike.pattern`). */
ilike(column: string, pattern: string): this;
/** Match rows where `column` matches the POSIX regex `pattern` (`column=match.pattern`). */
match(column: string, pattern: string): this;
/** Match rows where `column` matches the case-insensitive POSIX regex `pattern` (`column=imatch.pattern`). */
imatch(column: string, pattern: string): this;
/**
* Match rows where `column` `IS` the given value (`column=is.null`).
* Accepts `null`, a boolean, or the keywords `'null' | 'true' | 'false' | 'unknown'`.
*/
is(column: string, value: RealtimeIsFilterValue): this;
/** Match rows where `column` is distinct from `value` (`column=isdistinct.value`). NULL-safe inequality. */
isDistinct(column: string, value: RealtimeFilterValue): this;
/**
* Negate any operator with the `not.` prefix (`column=not.operator.value`).
* `in` takes an array, `is` takes an `IS` keyword/boolean/null, and every
* other operator takes a scalar value.
*
* @example
* postgresChangesFilter().not('status', 'in', ['draft', 'archived'])
* // → status=not.in.(draft,archived)
* postgresChangesFilter().not('deleted_at', 'is', null)
* // → deleted_at=not.is.null
*/
not(column: string, operator: 'in', value: ReadonlyArray<string | number | boolean>): this;
not(column: string, operator: 'is', value: RealtimeIsFilterValue): this;
not(column: string, operator: Exclude<RealtimePostgresChangesFilterOperator, 'in' | 'is'>, value: RealtimeFilterValue): this;
/**
* Serialize all conditions into the comma-separated (AND) filter string.
*
* Conditions are joined by commas, which the server applies as `AND`. A scalar
* value (or single `in` element) that contains a reserved character — `,`,
* `(`, `)`, `"`, `\` — or surrounding whitespace is double-quoted and escaped
* the way PostgREST does, so commas inside a value are preserved rather than
* read as a condition boundary.
*/
build(): string;
/** Alias for {@link build}; lets the builder be used wherever a string is expected. */
toString(): string;
}
/**
* Create a {@link RealtimePostgresFilterBuilder} for composing a Postgres
* Changes `filter`. Conditions are combined with `AND`.
*
* @example
* import { postgresChangesFilter } from '@supabase/realtime-js'
*
* channel.on('postgres_changes', {
* event: 'UPDATE',
* schema: 'public',
* table: 'orders',
* filter: postgresChangesFilter().gt('amount', 100).eq('status', 'open'),
* }, (payload) => { ... })
*/
export declare const postgresChangesFilter: () => RealtimePostgresFilterBuilder;
//# sourceMappingURL=RealtimePostgresFilterBuilder.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"RealtimePostgresFilterBuilder.d.ts","sourceRoot":"","sources":["../../src/RealtimePostgresFilterBuilder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,qCAAqC,GAC7C,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,MAAM,GACN,OAAO,GACP,IAAI,GACJ,OAAO,GACP,QAAQ,GACR,YAAY,CAAA;AAEhB,yDAAyD;AACzD,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AAElE,2CAA2C;AAC3C,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAA;AAuC1F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,6BAA6B;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,OAAO,CAAC,GAAG;IAWX,oEAAoE;IACpE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIpD,6EAA6E;IAC7E,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIrD,6EAA6E;IAC7E,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIpD,0FAA0F;IAC1F,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIrD,0EAA0E;IAC1E,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIpD,uFAAuF;IACvF,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAIrD;;;;;;OAMG;IACH,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI;IAI1E,8FAA8F;IAC9F,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3C,iGAAiG;IACjG,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5C,4FAA4F;IAC5F,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5C,8GAA8G;IAC9G,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAI7C;;;OAGG;IACH,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,qBAAqB,GAAG,IAAI;IAItD,4GAA4G;IAC5G,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,GAAG,IAAI;IAI5D;;;;;;;;;;OAUG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI;IAC1F,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,qBAAqB,GAAG,IAAI;IACvE,GAAG,CACD,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,OAAO,CAAC,qCAAqC,EAAE,IAAI,GAAG,IAAI,CAAC,EACrE,KAAK,EAAE,mBAAmB,GACzB,IAAI;IASP;;;;;;;;OAQG;IACH,KAAK,IAAI,MAAM;IAIf,uFAAuF;IACvF,QAAQ,IAAI,MAAM;CAGnB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,QAAO,6BACJ,CAAA"}
@@ -0,0 +1,160 @@
// Reserved characters that force PostgREST-style quoting: `[,()]` (which the
// server reads as condition/list delimiters) plus `"`/`\` (escaped inside quotes).
const PostgrestReservedCharsRegexp = /[,()"\\]/;
const needsQuoting = (value) => PostgrestReservedCharsRegexp.test(value) || value !== value.trim();
const quote = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
const serializeScalar = (value) => {
const serialized = value === null ? 'null' : String(value);
return needsQuoting(serialized) ? quote(serialized) : serialized;
};
const serializeIsValue = (value) => value === null ? 'null' : String(value);
// Builds the `operator.value` portion of a filter (everything after `column=`).
const serialize = (operator, value) => {
if (operator === 'in') {
const values = Array.isArray(value) ? value : [value];
if (values.length === 0) {
throw new Error('Realtime `in` filter requires at least one value.');
}
const items = Array.from(new Set(values))
.map((v) => serializeScalar(v))
.join(',');
return `in.(${items})`;
}
if (operator === 'is') {
return `is.${serializeIsValue(value)}`;
}
return `${operator}.${serializeScalar(value)}`;
};
/**
* Fluent builder for Postgres Changes `filter` strings.
*
* Each method appends a single `column=operator.value` condition. Multiple
* conditions are combined with commas, which the Realtime server applies as an
* `AND`. Pass an instance straight to `channel.on('postgres_changes', …)` — the
* SDK serializes it to a string automatically — or call {@link build} to obtain
* the string yourself.
*
* The builder mirrors the `postgrest-js` filter API (`eq`, `neq`, `in`, `like`,
* `not`, …) for the operators that Realtime supports. Values containing reserved
* characters (`,`, `(`, `)`, `"`, `\`) — or surrounding whitespace — are
* automatically double-quoted and escaped the same way PostgREST does, so they
* survive the server's filter parser; all other values are sent verbatim.
*
* The filter is snapshotted when passed to `channel.on(...)`; mutating the
* builder afterwards does not affect an existing subscription. An empty builder
* serializes to `''`, which the server treats as "no filter".
*
* @example
* channel.on('postgres_changes', {
* event: '*',
* schema: 'public',
* table: 'users',
* filter: postgresChangesFilter().eq('id', 1).lt('age', 30), // → 'id=eq.1,age=lt.30'
* }, (payload) => { ... })
*/
export class RealtimePostgresFilterBuilder {
constructor() {
this.filters = [];
}
add(column, operator, value, negate = false) {
const prefix = negate ? 'not.' : '';
this.filters.push(`${column}=${prefix}${serialize(operator, value)}`);
return this;
}
/** Match rows where `column` equals `value` (`column=eq.value`). */
eq(column, value) {
return this.add(column, 'eq', value);
}
/** Match rows where `column` does not equal `value` (`column=neq.value`). */
neq(column, value) {
return this.add(column, 'neq', value);
}
/** Match rows where `column` is greater than `value` (`column=gt.value`). */
gt(column, value) {
return this.add(column, 'gt', value);
}
/** Match rows where `column` is greater than or equal to `value` (`column=gte.value`). */
gte(column, value) {
return this.add(column, 'gte', value);
}
/** Match rows where `column` is less than `value` (`column=lt.value`). */
lt(column, value) {
return this.add(column, 'lt', value);
}
/** Match rows where `column` is less than or equal to `value` (`column=lte.value`). */
lte(column, value) {
return this.add(column, 'lte', value);
}
/**
* Match rows where `column` is one of `values` (`column=in.(a,b,c)`).
* Requires at least one value; duplicates are removed. An element containing a
* reserved character is double-quoted (`in.("a,b",c)`), so commas inside an
* element are preserved. `null` is intentionally not accepted (`IN (null)`
* never matches in SQL) — use `is`/`not('col','is',null)` for null checks.
*/
in(column, values) {
return this.add(column, 'in', values);
}
/** Match rows where `column` matches the case-sensitive `pattern` (`column=like.pattern`). */
like(column, pattern) {
return this.add(column, 'like', pattern);
}
/** Match rows where `column` matches the case-insensitive `pattern` (`column=ilike.pattern`). */
ilike(column, pattern) {
return this.add(column, 'ilike', pattern);
}
/** Match rows where `column` matches the POSIX regex `pattern` (`column=match.pattern`). */
match(column, pattern) {
return this.add(column, 'match', pattern);
}
/** Match rows where `column` matches the case-insensitive POSIX regex `pattern` (`column=imatch.pattern`). */
imatch(column, pattern) {
return this.add(column, 'imatch', pattern);
}
/**
* Match rows where `column` `IS` the given value (`column=is.null`).
* Accepts `null`, a boolean, or the keywords `'null' | 'true' | 'false' | 'unknown'`.
*/
is(column, value) {
return this.add(column, 'is', value);
}
/** Match rows where `column` is distinct from `value` (`column=isdistinct.value`). NULL-safe inequality. */
isDistinct(column, value) {
return this.add(column, 'isdistinct', value);
}
not(column, operator, value) {
return this.add(column, operator, value, true);
}
/**
* Serialize all conditions into the comma-separated (AND) filter string.
*
* Conditions are joined by commas, which the server applies as `AND`. A scalar
* value (or single `in` element) that contains a reserved character — `,`,
* `(`, `)`, `"`, `\` — or surrounding whitespace is double-quoted and escaped
* the way PostgREST does, so commas inside a value are preserved rather than
* read as a condition boundary.
*/
build() {
return this.filters.join(',');
}
/** Alias for {@link build}; lets the builder be used wherever a string is expected. */
toString() {
return this.build();
}
}
/**
* Create a {@link RealtimePostgresFilterBuilder} for composing a Postgres
* Changes `filter`. Conditions are combined with `AND`.
*
* @example
* import { postgresChangesFilter } from '@supabase/realtime-js'
*
* channel.on('postgres_changes', {
* event: 'UPDATE',
* schema: 'public',
* table: 'orders',
* filter: postgresChangesFilter().gt('amount', 100).eq('status', 'open'),
* }, (payload) => { ... })
*/
export const postgresChangesFilter = () => new RealtimePostgresFilterBuilder();
//# sourceMappingURL=RealtimePostgresFilterBuilder.js.map
@@ -0,0 +1 @@
{"version":3,"file":"RealtimePostgresFilterBuilder.js","sourceRoot":"","sources":["../../src/RealtimePostgresFilterBuilder.ts"],"names":[],"mappings":"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"}
+62
View File
@@ -0,0 +1,62 @@
import type RealtimeChannel from './RealtimeChannel';
export 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"
}
export type RealtimePresenceOptions = {
events?: {
state: string;
diff: string;
};
};
export default class RealtimePresence {
channel: RealtimeChannel;
get state(): RealtimePresenceState;
private presenceAdapter;
/**
* 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' } }`.
*
* @category Realtime
*
* @example Example for a presence channel
* ```ts
* const presence = new RealtimePresence(channel)
*
* channel.on('presence', ({ event, key }) => {
* console.log(`Presence ${event} on ${key}`)
* })
* ```
*/
constructor(channel: RealtimeChannel, opts?: RealtimePresenceOptions);
}
//# sourceMappingURL=RealtimePresence.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"RealtimePresence.d.ts","sourceRoot":"","sources":["../../src/RealtimePresence.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,eAAe,MAAM,mBAAmB,CAAA;AAGpD,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,GAAG,EAAE,IAAI;IAC5D,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;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CACzC,CAAA;AAED,MAAM,CAAC,OAAO,OAAO,gBAAgB;IAyB1B,OAAO,EAAE,eAAe;IAxBjC,IAAI,KAAK,0BAER;IAED,OAAO,CAAC,eAAe,CAAiB;IAExC;;;;;;;;;;;;;;;;OAgBG;gBAEM,OAAO,EAAE,eAAe,EAC/B,IAAI,CAAC,EAAE,uBAAuB;CAIjC"}
+38
View File
@@ -0,0 +1,38 @@
/*
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
*/
import PresenceAdapter from './phoenix/presenceAdapter';
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 {
get state() {
return this.presenceAdapter.state;
}
/**
* 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' } }`.
*
* @category Realtime
*
* @example Example for a presence channel
* ```ts
* const presence = new RealtimePresence(channel)
*
* channel.on('presence', ({ event, key }) => {
* console.log(`Presence ${event} on ${key}`)
* })
* ```
*/
constructor(channel, opts) {
this.channel = channel;
this.presenceAdapter = new PresenceAdapter(this.channel.channelAdapter, opts);
}
}
//# sourceMappingURL=RealtimePresence.js.map
@@ -0,0 +1 @@
{"version":3,"file":"RealtimePresence.js","sourceRoot":"","sources":["../../src/RealtimePresence.ts"],"names":[],"mappings":"AAAA;;;EAGE;AAGF,OAAO,eAAe,MAAM,2BAA2B,CAAA;AAwBvD,MAAM,CAAN,IAAY,+BAIX;AAJD,WAAY,+BAA+B;IACzC,gDAAa,CAAA;IACb,gDAAa,CAAA;IACb,kDAAe,CAAA;AACjB,CAAC,EAJW,+BAA+B,KAA/B,+BAA+B,QAI1C;AAMD,MAAM,CAAC,OAAO,OAAO,gBAAgB;IACnC,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,CAAA;IACnC,CAAC;IAID;;;;;;;;;;;;;;;;OAgBG;IACH,YACS,OAAwB,EAC/B,IAA8B;QADvB,YAAO,GAAP,OAAO,CAAiB;QAG/B,IAAI,CAAC,eAAe,GAAG,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC,CAAA;IAC/E,CAAC;CACF"}
+6
View File
@@ -0,0 +1,6 @@
import RealtimeClient, { RealtimeClientOptions, RealtimeMessage, RealtimeRemoveChannelResponse, WebSocketLikeConstructor } from './RealtimeClient';
import RealtimeChannel, { RealtimeChannelOptions, RealtimeChannelSendResponse, RealtimePostgresChangesFilter, RealtimePostgresChangesFilterOperator, RealtimePostgresFilterBuilder, postgresChangesFilter, RealtimePostgresChangesPayload, RealtimePostgresInsertPayload, RealtimePostgresUpdatePayload, RealtimePostgresDeletePayload, RealtimeSystemPayload, 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, RealtimePostgresChangesFilterOperator, RealtimePostgresFilterBuilder, postgresChangesFilter, RealtimePostgresChangesPayload, RealtimePostgresInsertPayload, RealtimePostgresUpdatePayload, RealtimePostgresDeletePayload, RealtimeSystemPayload, 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
+1
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,qCAAqC,EACrC,6BAA6B,EAC7B,qBAAqB,EACrB,8BAA8B,EAC9B,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,qBAAqB,EACrB,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,qCAAqC,EACrC,6BAA6B,EAC7B,qBAAqB,EACrB,8BAA8B,EAC9B,6BAA6B,EAC7B,6BAA6B,EAC7B,6BAA6B,EAC7B,qBAAqB,EACrB,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"}
+6
View File
@@ -0,0 +1,6 @@
import RealtimeClient from './RealtimeClient';
import RealtimeChannel, { RealtimePostgresFilterBuilder, postgresChangesFilter, 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, RealtimePostgresFilterBuilder, postgresChangesFilter, REALTIME_LISTEN_TYPES, REALTIME_POSTGRES_CHANGES_LISTEN_EVENT, REALTIME_PRESENCE_LISTEN_EVENTS, REALTIME_SUBSCRIBE_STATES, REALTIME_CHANNEL_STATES, WebSocketFactory, };
//# sourceMappingURL=index.js.map
+1
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,EAKtB,6BAA6B,EAC7B,qBAAqB,EAMrB,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,EAKd,6BAA6B,EAC7B,qBAAqB,EAUrB,qBAAqB,EACrB,sCAAsC,EACtC,+BAA+B,EAC/B,yBAAyB,EACzB,uBAAuB,EACvB,gBAAgB,GAGjB,CAAA"}
+43
View File
@@ -0,0 +1,43 @@
import type { SocketState, ChannelState, ChannelEvent as PhoenixChannelEvent, Transport, Vsn } from '../phoenix/types';
export type { SocketState, ChannelState, Transport };
export declare const DEFAULT_VERSION = "realtime-js/2.110.8";
export declare const VSN_1_0_0: Vsn;
export declare const VSN_2_0_0: Vsn;
export declare const DEFAULT_VSN: Vsn;
export declare const VERSION = "2.110.8";
export declare const DEFAULT_TIMEOUT = 10000;
export declare const WS_CLOSE_NORMAL = 1000;
export declare const MAX_PUSH_BUFFER_SIZE = 100;
export declare const SOCKET_STATES: {
readonly connecting: 0;
readonly open: 1;
readonly closing: 2;
readonly closed: 3;
};
export declare const CHANNEL_STATES: {
readonly closed: "closed";
readonly errored: "errored";
readonly joined: "joined";
readonly joining: "joining";
readonly leaving: "leaving";
};
export type ChannelEvent = PhoenixChannelEvent | 'access_token';
export declare const CHANNEL_EVENTS: {
readonly close: "phx_close";
readonly error: "phx_error";
readonly join: "phx_join";
readonly reply: "phx_reply";
readonly leave: "phx_leave";
readonly access_token: "access_token";
};
export declare const TRANSPORTS: {
readonly websocket: "websocket";
};
export type ConnectionState = 'connecting' | 'open' | 'closing' | 'closed' | (string & {});
export declare const CONNECTION_STATE: {
readonly connecting: "connecting";
readonly open: "open";
readonly closing: "closing";
readonly closed: "closed";
};
//# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/constants.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,YAAY,IAAI,mBAAmB,EACnC,SAAS,EACT,GAAG,EACJ,MAAM,kBAAkB,CAAA;AAEzB,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,CAAA;AAEpD,eAAO,MAAM,eAAe,wBAA2B,CAAA;AAEvD,eAAO,MAAM,SAAS,EAAE,GAAa,CAAA;AACrC,eAAO,MAAM,SAAS,EAAE,GAAa,CAAA;AACrC,eAAO,MAAM,WAAW,EAAE,GAAe,CAAA;AAEzC,eAAO,MAAM,OAAO,YAAU,CAAA;AAE9B,eAAO,MAAM,eAAe,QAAQ,CAAA;AAEpC,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,oBAAoB,MAAM,CAAA;AAEvC,eAAO,MAAM,aAAa;;;;;CAKhB,CAAA;AAEV,eAAO,MAAM,cAAc;;;;;;CAMjB,CAAA;AAEV,MAAM,MAAM,YAAY,GAAG,mBAAmB,GAAG,cAAc,CAAA;AAE/D,eAAO,MAAM,cAAc;;;;;;;CAOjB,CAAA;AAEV,eAAO,MAAM,UAAU;;CAEb,CAAA;AAEV,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAE1F,eAAO,MAAM,gBAAgB;;;;;CAKnB,CAAA"}
+40
View File
@@ -0,0 +1,40 @@
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 const SOCKET_STATES = {
connecting: 0,
open: 1,
closing: 2,
closed: 3,
};
export const CHANNEL_STATES = {
closed: 'closed',
errored: 'errored',
joined: 'joined',
joining: 'joining',
leaving: 'leaving',
};
export const CHANNEL_EVENTS = {
close: 'phx_close',
error: 'phx_error',
join: 'phx_join',
reply: 'phx_reply',
leave: 'phx_leave',
access_token: 'access_token',
};
export const TRANSPORTS = {
websocket: 'websocket',
};
export const CONNECTION_STATE = {
connecting: 'connecting',
open: 'open',
closing: 'closing',
closed: 'closed',
};
//# sourceMappingURL=constants.js.map
+1
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;AAWnC,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,OAAO,EAAE,CAAA;AAEvD,MAAM,CAAC,MAAM,SAAS,GAAQ,OAAO,CAAA;AACrC,MAAM,CAAC,MAAM,SAAS,GAAQ,OAAO,CAAA;AACrC,MAAM,CAAC,MAAM,WAAW,GAAQ,SAAS,CAAA;AAEzC,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,CAAC,MAAM,aAAa,GAAG;IAC3B,UAAU,EAAE,CAAC;IACb,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;CACD,CAAA;AAEV,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,SAAS;CACV,CAAA;AAIV,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,WAAW;IAClB,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,WAAW;IAClB,KAAK,EAAE,WAAW;IAClB,YAAY,EAAE,cAAc;CACpB,CAAA;AAEV,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,SAAS,EAAE,WAAW;CACd,CAAA;AAIV,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,UAAU,EAAE,YAAY;IACxB,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,QAAQ;CACR,CAAA"}
@@ -0,0 +1,10 @@
/**
* Normalize the various shapes a channel error reason can take into a real `Error`.
*
* Transport-level channel errors arrive as a `CloseEvent`, a transport `Event`, an `Error`,
* a string, or `undefined` depending on which path in the underlying socket fired. Server-reply
* errors arrive as a payload object. This helper produces a consistent `Error` for every case
* and preserves the original via `cause` so callers can still inspect the raw event.
*/
export declare function normalizeChannelError(reason: unknown): Error;
//# sourceMappingURL=normalizeChannelError.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"normalizeChannelError.d.ts","sourceRoot":"","sources":["../../../src/lib/normalizeChannelError.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,KAAK,CAqB5D"}
@@ -0,0 +1,26 @@
/**
* Normalize the various shapes a channel error reason can take into a real `Error`.
*
* Transport-level channel errors arrive as a `CloseEvent`, a transport `Event`, an `Error`,
* a string, or `undefined` depending on which path in the underlying socket fired. Server-reply
* errors arrive as a payload object. This helper produces a consistent `Error` for every case
* and preserves the original via `cause` so callers can still inspect the raw event.
*/
export function normalizeChannelError(reason) {
if (reason instanceof Error) {
return reason;
}
if (typeof reason === 'string') {
return new Error(reason);
}
if (reason && typeof reason === 'object') {
const obj = reason;
if (typeof obj.code === 'number') {
const detail = typeof obj.reason === 'string' && obj.reason ? ` (${obj.reason})` : '';
return new Error(`socket closed: ${obj.code}${detail}`, { cause: reason });
}
return new Error('channel error: transport failure', { cause: reason });
}
return new Error('channel error: connection lost');
}
//# sourceMappingURL=normalizeChannelError.js.map
@@ -0,0 +1 @@
{"version":3,"file":"normalizeChannelError.js","sourceRoot":"","sources":["../../../src/lib/normalizeChannelError.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAe;IACnD,IAAI,MAAM,YAAY,KAAK,EAAE,CAAC;QAC5B,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,CAAA;IAC1B,CAAC;IAED,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,MAAiC,CAAA;QAE7C,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;YACrF,OAAO,IAAI,KAAK,CAAC,kBAAkB,GAAG,CAAC,IAAI,GAAG,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;QAC5E,CAAC;QAED,OAAO,IAAI,KAAK,CAAC,kCAAkC,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;IACzE,CAAC;IAED,OAAO,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;AACpD,CAAC"}
+33
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
@@ -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;IA6EhC,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"}
+163
View File
@@ -0,0 +1,163 @@
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;
// Encode each header field as UTF-8. The length prefixes are byte counts and
// the decode side uses TextDecoder (UTF-8), so measuring with String.length
// and writing with charCodeAt would corrupt any multi-byte character (e.g.
// accents or emoji) and desynchronize the buffer.
const encoder = new TextEncoder();
const topic = encoder.encode(message.topic);
const ref = encoder.encode((_a = message.ref) !== null && _a !== void 0 ? _a : '');
const joinRef = encoder.encode((_b = message.join_ref) !== null && _b !== void 0 ? _b : '');
const userEvent = encoder.encode(message.payload.event);
// Filter metadata based on allowed keys
const rest = this.allowedMetadataKeys
? this._pick(message.payload, this.allowedMetadataKeys)
: {};
const metadata = encoder.encode(Object.keys(rest).length === 0 ? '' : JSON.stringify(rest));
// Validate byte 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);
const view = new DataView(header);
const bytes = new Uint8Array(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);
bytes.set(joinRef, offset);
offset += joinRef.length;
bytes.set(ref, offset);
offset += ref.length;
bytes.set(topic, offset);
offset += topic.length;
bytes.set(userEvent, offset);
offset += userEvent.length;
bytes.set(metadata, offset);
offset += metadata.length;
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
+109
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
@@ -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"}
+229
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
+2
View File
@@ -0,0 +1,2 @@
export declare const version = "2.110.8";
//# sourceMappingURL=version.d.ts.map

Some files were not shown because too many files have changed in this diff Show More