Created
December 5, 2024 15:29
-
-
Save smontlouis/a2b911c73885b154b99afb134f0e68f3 to your computer and use it in GitHub Desktop.
PowerSync React-native
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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