Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 02f2f0b

Browse files
filipecabacogrdsdev
andauthoredMay 20, 2025··
fix: Bump up realtime-js (#1418)
* fix: Bump up realtime; add integration tests (#1395) * fix: bump up realtime-js (#1409) * fix: bump up realtime-js; await on token change (#1411) * fix: prevent async calls in _listenForAuthEvents (#1412) * fix: bump up realtime-js (#1413) * bump up realtime-js (#1419) * resolve conflict --------- Co-authored-by: Guilherme Souza <guilherme@supabase.io> Co-authored-by: Guilherme Souza <ogrsouza@gmail.com>
1 parent bc4bce1 commit 02f2f0b

File tree

14 files changed

+8198
-279
lines changed

14 files changed

+8198
-279
lines changed
 

‎.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ jobs:
3636
with:
3737
node-version: ${{ matrix.node }}
3838

39+
- name: Set up Supabase CLI
40+
uses: supabase/setup-cli@v1
41+
with:
42+
version: latest
43+
3944
- name: Run tests
4045
run: |
4146
npm clean-install

‎README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key
8686
})
8787
```
8888

89+
## Testing
90+
91+
### Unit Testing
92+
93+
```bash
94+
pnpm test
95+
```
96+
97+
### Integration Testing
98+
99+
```bash
100+
supabase start
101+
pnpm run test:integration
102+
```
103+
89104
## Badges
90105

91106
[![Coverage Status](https://coveralls.io/repos/github/supabase/supabase-js/badge.svg?branch=master)](https://coveralls.io/github/supabase/supabase-js?branch=master)

‎package-lock.json

Lines changed: 110 additions & 262 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929
"build:umd": "webpack",
3030
"types-generate": "dts-gen -m '@supabase/supabase-js' -s",
3131
"test": "run-s test:types test:run",
32-
"test:run": "jest --runInBand",
33-
"test:coverage": "jest --runInBand --coverage",
32+
"test:run": "jest --runInBand --detectOpenHandles",
33+
"test:integration": "jest --runInBand test/integration.test.ts",
34+
"test:coverage": "jest --runInBand --coverage --testPathIgnorePatterns=\"test/integration.test.ts\"",
3435
"test:db": "cd infra/db && docker-compose down && docker-compose up -d && sleep 5",
3536
"test:watch": "jest --watch --verbose false --silent false",
3637
"test:clean": "cd infra/db && docker-compose down",
@@ -44,7 +45,7 @@
4445
"@supabase/functions-js": "2.4.4",
4546
"@supabase/node-fetch": "2.6.15",
4647
"@supabase/postgrest-js": "1.19.4",
47-
"@supabase/realtime-js": "2.11.2",
48+
"@supabase/realtime-js": "2.11.8",
4849
"@supabase/storage-js": "2.7.1"
4950
},
5051
"devDependencies": {

‎pnpm-lock.yaml

Lines changed: 7861 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/SupabaseClient.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
DEFAULT_REALTIME_OPTIONS,
2020
} from './lib/constants'
2121
import { fetchWithAuth } from './lib/fetch'
22-
import { stripTrailingSlash, applySettingDefaults } from './lib/helpers'
22+
import { cleanUrl, applySettingDefaults } from './lib/helpers'
2323
import { SupabaseAuthClient } from './lib/SupabaseAuthClient'
2424
import { Fetch, GenericSchema, SupabaseClientOptions, SupabaseAuthClientOptions } from './lib/types'
2525

@@ -75,7 +75,7 @@ export default class SupabaseClient<
7575
if (!supabaseUrl) throw new Error('supabaseUrl is required.')
7676
if (!supabaseKey) throw new Error('supabaseKey is required.')
7777

78-
const _supabaseUrl = stripTrailingSlash(supabaseUrl)
78+
const _supabaseUrl = cleanUrl(supabaseUrl)
7979
const baseUrl = new URL(_supabaseUrl)
8080

8181
this.realtimeUrl = new URL('/realtime/v1', baseUrl)
@@ -119,11 +119,13 @@ export default class SupabaseClient<
119119
}
120120

121121
this.fetch = fetchWithAuth(supabaseKey, this._getAccessToken.bind(this), settings.global.fetch)
122+
122123
this.realtime = this._initRealtimeClient({
123124
headers: this.headers,
124125
accessToken: this._getAccessToken.bind(this),
125126
...settings.realtime,
126127
})
128+
127129
this.rest = new PostgrestClient(`${_supabaseUrl}/rest/v1`, {
128130
headers: this.headers,
129131
schema: settings.db.schema,
@@ -321,14 +323,16 @@ export default class SupabaseClient<
321323
})
322324
}
323325

324-
private _listenForAuthEvents() {
325-
let data = this.auth.onAuthStateChange((event, session) => {
326-
this._handleTokenChanged(event, 'CLIENT', session?.access_token)
326+
private async _listenForAuthEvents() {
327+
return await this.auth.onAuthStateChange((event, session) => {
328+
setTimeout(
329+
async () => await this._handleTokenChanged(event, 'CLIENT', session?.access_token),
330+
0
331+
)
327332
})
328-
return data
329333
}
330334

331-
private _handleTokenChanged(
335+
private async _handleTokenChanged(
332336
event: AuthChangeEvent,
333337
source: 'CLIENT' | 'STORAGE',
334338
token?: string
@@ -339,7 +343,7 @@ export default class SupabaseClient<
339343
) {
340344
this.changedAccessToken = token
341345
} else if (event === 'SIGNED_OUT') {
342-
this.realtime.setAuth()
346+
await this.realtime.setAuth()
343347
if (source == 'STORAGE') this.auth.signOut()
344348
this.changedAccessToken = undefined
345349
}

‎src/lib/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ export function uuid() {
99
})
1010
}
1111

12-
export function stripTrailingSlash(url: string): string {
13-
return url.replace(/\/$/, '')
12+
export function cleanUrl(url: string): string {
13+
return url.replace(/[\/\s]*$/, '')
1414
}
1515

1616
export const isBrowser = () => typeof window !== 'undefined'

‎supabase/.branches/_current_branch

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
main

‎supabase/.temp/cli-latest

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v2.22.12
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- Create todos table
2+
CREATE TABLE IF NOT EXISTS public.todos (
3+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4+
task TEXT NOT NULL,
5+
is_complete BOOLEAN NOT NULL DEFAULT FALSE,
6+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
7+
);
8+
9+
-- Set up Row Level Security (RLS)
10+
ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY;
11+
12+
-- Create policies
13+
CREATE POLICY "Allow anonymous access to todos" ON public.todos
14+
FOR ALL
15+
TO anon
16+
USING (true)
17+
WITH CHECK (true);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
create policy "authenticated can read all messages on topic"
2+
on "realtime"."messages"
3+
for select
4+
to authenticated
5+
using ( realtime.topic() like '%channel%' );
6+
7+
create policy "authenticated can insert messages on topic"
8+
on "realtime"."messages"
9+
for insert
10+
to authenticated
11+
with check (realtime.topic() like '%channel%');

‎supabase/seed/seed_todos.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Seed data for todos table
2+
INSERT INTO public.todos (task, is_complete)
3+
VALUES
4+
('Buy groceries', false),
5+
('Complete project report', true),
6+
('Call mom', false),
7+
('Schedule dentist appointment', false),
8+
('Pay bills', true);

‎test/helper.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1-
import { stripTrailingSlash } from '../src/lib/helpers'
1+
import { cleanUrl } from '../src/lib/helpers'
22

33
test('Strip trailing slash from URL', () => {
44
const URL = 'http://localhost:3000/'
55
const expectedURL = URL.slice(0, -1)
6-
expect(stripTrailingSlash(URL)).toBe(expectedURL)
6+
expect(cleanUrl(URL)).toBe(expectedURL)
77
})
88

9-
test('Return the original URL if there is no slash at the end', () => {
9+
test('Return the original URL if there is no slash or white space at the end', () => {
1010
const URL = 'http://localhost:3000'
1111
const expectedURL = URL
12-
expect(stripTrailingSlash(URL)).toBe(expectedURL)
12+
expect(cleanUrl(URL)).toBe(expectedURL)
13+
})
14+
15+
test('Strip trailing white space from URL', () => {
16+
const URL = 'http://localhost:3000 '
17+
const expectedURL = URL.slice(0, -1)
18+
expect(cleanUrl(URL)).toBe(expectedURL)
19+
})
20+
21+
test('Strip trailing slash followed by white space from URL', () => {
22+
const URL = 'http://localhost:3000/ '
23+
const expectedURL = URL.slice(0, -2)
24+
expect(cleanUrl(URL)).toBe(expectedURL)
1325
})

‎test/integration.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { createClient, RealtimeChannel, SupabaseClient } from '../src/index'
2+
3+
// These tests assume that a local Supabase server is already running
4+
// Start a local Supabase instance with 'supabase start' before running these tests
5+
describe('Supabase Integration Tests', () => {
6+
// Default local dev credentials from Supabase CLI
7+
const SUPABASE_URL = 'http://127.0.0.1:54321'
8+
const ANON_KEY =
9+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0'
10+
11+
const supabase = createClient(SUPABASE_URL, ANON_KEY, {
12+
realtime: { heartbeatIntervalMs: 500 },
13+
})
14+
15+
test('should connect to Supabase instance', async () => {
16+
expect(supabase).toBeDefined()
17+
expect(supabase).toBeInstanceOf(SupabaseClient)
18+
})
19+
20+
describe('PostgREST', () => {
21+
test('should query data from public schema', async () => {
22+
const { data, error } = await supabase.from('todos').select('*').limit(5)
23+
24+
// The default schema includes a 'todos' table, but it might be empty
25+
expect(error).toBeNull()
26+
expect(Array.isArray(data)).toBe(true)
27+
})
28+
29+
// Test creating and deleting data
30+
test('should create and delete a todo', async () => {
31+
// Create a new todo
32+
const { data: createdTodo, error: createError } = await supabase
33+
.from('todos')
34+
.insert({ task: 'Integration Test Todo', is_complete: false })
35+
.select()
36+
.single()
37+
38+
expect(createError).toBeNull()
39+
expect(createdTodo).toBeDefined()
40+
expect(createdTodo!.task).toBe('Integration Test Todo')
41+
expect(createdTodo!.is_complete).toBe(false)
42+
43+
// Delete the created todo
44+
const { error: deleteError } = await supabase.from('todos').delete().eq('id', createdTodo!.id)
45+
46+
expect(deleteError).toBeNull()
47+
48+
// Verify the todo was deleted
49+
const { data: fetchedTodo, error: fetchError } = await supabase
50+
.from('todos')
51+
.select('*')
52+
.eq('id', createdTodo!.id)
53+
.single()
54+
55+
expect(fetchError).not.toBeNull()
56+
expect(fetchedTodo).toBeNull()
57+
})
58+
})
59+
60+
describe('Authentication', () => {
61+
afterAll(async () => {
62+
// Clean up by signing out the user
63+
await supabase.auth.signOut()
64+
})
65+
test('should sign up a user', async () => {
66+
const email = `test-${Date.now()}@example.com`
67+
const password = 'password123'
68+
69+
const { data, error } = await supabase.auth.signUp({
70+
email,
71+
password,
72+
})
73+
74+
expect(error).toBeNull()
75+
expect(data.user).toBeDefined()
76+
expect(data.user!.email).toBe(email)
77+
})
78+
})
79+
80+
describe('Realtime', () => {
81+
const channelName = `channel-${crypto.randomUUID()}`
82+
let channel: RealtimeChannel
83+
let email: string
84+
let password: string
85+
86+
beforeEach(async () => {
87+
await supabase.auth.signOut()
88+
email = `test-${Date.now()}@example.com`
89+
password = 'password123'
90+
await supabase.auth.signUp({ email, password })
91+
92+
const config = { broadcast: { self: true }, private: true }
93+
channel = supabase.channel(channelName, { config })
94+
95+
await supabase.realtime.setAuth()
96+
})
97+
98+
afterEach(async () => {
99+
await supabase.removeAllChannels()
100+
})
101+
102+
test('is able to connect and broadcast', async () => {
103+
const testMessage = { message: 'test' }
104+
let receivedMessage: any
105+
let subscribed = false
106+
let attempts = 0
107+
108+
channel
109+
.on('broadcast', { event: '*' }, (payload) => (receivedMessage = payload))
110+
.subscribe((status) => {
111+
if (status == 'SUBSCRIBED') subscribed = true
112+
})
113+
114+
// Wait for subscription
115+
while (!subscribed) {
116+
if (attempts > 50) throw new Error('Timeout waiting for subscription')
117+
await new Promise((resolve) => setTimeout(resolve, 100))
118+
attempts++
119+
}
120+
121+
attempts = 0
122+
123+
channel.send({ type: 'broadcast', event: 'test-event', payload: testMessage })
124+
125+
// Wait on message
126+
while (!receivedMessage) {
127+
if (attempts > 50) throw new Error('Timeout waiting for message')
128+
await new Promise((resolve) => setTimeout(resolve, 100))
129+
attempts++
130+
}
131+
expect(receivedMessage).toBeDefined()
132+
expect(supabase.realtime.getChannels().length).toBe(1)
133+
}, 10000)
134+
})
135+
})

0 commit comments

Comments
 (0)
Please sign in to comment.