Skip to content

Instantly share code, notes, and snippets.

@smontlouis
Created December 5, 2024 15:29
Show Gist options
  • Save smontlouis/a2b911c73885b154b99afb134f0e68f3 to your computer and use it in GitHub Desktop.
Save smontlouis/a2b911c73885b154b99afb134f0e68f3 to your computer and use it in GitHub Desktop.
PowerSync React-native
import React, { useContext, createContext, useState, useEffect } from 'react'
import { Session, User } from '@supabase/supabase-js'
import { useSystem } from './system'
interface AuthProviderProps {
children: React.ReactNode
}
type AuthContextType = {
session: Session | null
user: User | null
}
const AuthContext = createContext<AuthContextType>({
session: null,
user: null,
})
const AuthProvider = (props: AuthProviderProps) => {
const [user, setUser] = useState<User | null>(null)
const [session, setSession] = useState<Session | null>(null)
const [loading, setLoading] = useState<boolean>(true)
const system = useSystem()
useEffect(() => {
system.init()
}, [])
useEffect(() => {
const { data } = system.supabaseConnector.client.auth.onAuthStateChange(
(_event, session) => {
console.log('supabase.auth.onAuthStateChange', _event, session)
if (session?.user) {
console.log('User found:', session.user)
setSession(session)
setUser(session?.user || null)
system.powersync.connect(system.supabaseConnector)
}
setLoading(false)
}
)
return () => {
data?.subscription.unsubscribe()
}
}, [])
return (
<AuthContext.Provider
value={{
session,
user,
}}
>
{props.children}
</AuthContext.Provider>
)
}
export const useAuth = () => {
return useContext(AuthContext)
}
export default AuthProvider
import {
AbstractPowerSyncDatabase,
CrudEntry,
PowerSyncBackendConnector,
UpdateType,
} from '@powersync/react-native'
import { SupabaseClient, createClient } from '@supabase/supabase-js'
import { System } from './system'
import { AppConfig } from './AppConfig'
/// Postgres Response codes that we cannot recover from by retrying.
const FATAL_RESPONSE_CODES = [
// Class 22 — Data Exception
// Examples include data type mismatch.
new RegExp('^22...$'),
// Class 23 — Integrity Constraint Violation.
// Examples include NOT NULL, FOREIGN KEY and UNIQUE violations.
new RegExp('^23...$'),
// INSUFFICIENT PRIVILEGE - typically a row-level security violation
new RegExp('^42501$'),
]
export class SupabaseConnector implements PowerSyncBackendConnector {
client: SupabaseClient
constructor(protected system: System) {
this.client = createClient(
AppConfig.supabaseUrl,
AppConfig.supabaseAnonKey,
{
auth: {
persistSession: true,
storage: this.system.kvStorage,
},
}
)
}
async login(username: string, password: string) {
const { error } = await this.client.auth.signInWithPassword({
email: username,
password: password,
})
if (error) {
throw error
}
}
async fetchCredentials() {
try {
const {
data: { session },
error,
} = await this.client.auth.getSession()
if (error) {
throw error
}
if (session) {
return {
endpoint: AppConfig.powersyncUrl,
token: session.access_token ?? '',
expiresAt: session.expires_at
? new Date(session.expires_at * 1000)
: undefined,
userID: session.user.id,
}
}
const authResponse = await this.client.functions.invoke<{
token: string
}>('powersync-auth-anonymous')
const { data } = authResponse
return {
endpoint: AppConfig.powersyncUrl,
token: data?.token ?? '',
}
} catch (error) {
console.error('Could not fetch Supabase credentials:', error)
throw error
}
}
async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {
const transaction = await database.getNextCrudTransaction()
if (!transaction) {
return
}
let lastOp: CrudEntry | null = null
try {
// Note: If transactional consistency is important, use database functions
// or edge functions to process the entire transaction in a single call.
for (const op of transaction.crud) {
lastOp = op
const table = this.client.from(op.table)
let result: any = null
switch (op.op) {
case UpdateType.PUT:
// eslint-disable-next-line no-case-declarations
const record = { ...op.opData, id: op.id }
result = await table.upsert(record)
break
case UpdateType.PATCH:
result = await table.update(op.opData).eq('id', op.id)
break
case UpdateType.DELETE:
result = await table.delete().eq('id', op.id)
break
}
if (result.error) {
console.error(result.error)
result.error.message = `Could not ${op.op} data to Supabase error: ${JSON.stringify(result)}`
throw result.error
}
}
await transaction.complete()
} catch (ex: any) {
console.debug(ex)
if (
typeof ex.code == 'string' &&
FATAL_RESPONSE_CODES.some((regex) => regex.test(ex.code))
) {
/**
* Instead of blocking the queue with these errors,
* discard the (rest of the) transaction.
*
* Note that these errors typically indicate a bug in the application.
* If protecting against data loss is important, save the failing records
* elsewhere instead of discarding, and/or notify the user.
*/
console.error('Data upload error - discarding:', lastOp, ex)
await transaction.complete()
} else {
// Error may be retryable - e.g. network error or temporary server error.
// Throwing an error here causes this call to be retried after a delay.
throw ex
}
}
}
}
import '@azure/core-asynciterator-polyfill'
import { Kysely, wrapPowerSyncWithKysely } from '@powersync/kysely-driver'
import {
AbstractPowerSyncDatabase,
PowerSyncDatabase,
SyncStreamConnectionMethod,
} from '@powersync/react-native'
import { createContext, useContext } from 'react'
import { AppSchema, Database } from './AppSchema'
import { KVStorage } from './KVStorage'
import { SupabaseConnector } from './SupabaseConnector'
// Log messages will be written to the window's console.
// Logger.useDefaults()
// Logger.setLevel(Logger.DEBUG)
export class System {
kvStorage: KVStorage
supabaseConnector: SupabaseConnector
powersync: AbstractPowerSyncDatabase
db: Kysely<Database>
constructor() {
this.kvStorage = new KVStorage()
this.supabaseConnector = new SupabaseConnector(this)
this.powersync = new PowerSyncDatabase({
schema: AppSchema,
database: {
dbFilename: 'app.sqlite',
},
})
this.db = wrapPowerSyncWithKysely(this.powersync)
}
async init() {
await this.powersync.init()
await this.powersync.connect(this.supabaseConnector, {
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
})
}
}
export const system = new System()
export const SystemContext = createContext(system)
export const useSystem = () => useContext(SystemContext)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment