Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 3x 2x 2x 10x 4x 4x 2x 2x 2x 2x 6x 5x 3x 3x 3x 6x 6x 6x 6x 6x 5x 5x 1x 1x 1x 5x | import type { PowerSyncBackendConnector, AbstractPowerSyncDatabase } from '@powersync/web';
import type { SupabaseClient } from '@supabase/supabase-js';
import { PUBLIC_POWERSYNC_URL } from '$env/static/public';
// PostgreSQL error class prefixes that indicate a permanent data problem.
// Operations with these errors are discarded rather than retried.
function isDiscardableError(code: string): boolean {
if (code === '42501') return true; // insufficient privilege (RLS violation)
const prefix = code.slice(0, 2);
return prefix === '22' || prefix === '23'; // data exception / integrity constraint violation
}
export class SupabaseConnector implements PowerSyncBackendConnector {
constructor(private readonly supabase: SupabaseClient) {}
async fetchCredentials() {
const {
data: { session },
error,
} = await this.supabase.auth.getSession();
if (!session || error) {
throw new Error('No active session');
}
const endpoint = PUBLIC_POWERSYNC_URL;
Iif (!endpoint) {
throw new Error('PUBLIC_POWERSYNC_URL is not configured');
}
return {
endpoint,
token: session.access_token,
expiresAt: session.expires_at ? new Date(session.expires_at * 1000) : undefined,
};
}
/**
* Execute a single Supabase CRUD operation and handle errors uniformly.
* Discardable errors (RLS, constraint, data exceptions) are logged and swallowed.
* All other errors propagate so PowerSync can retry the batch.
*/
private async executeOp(
table: string,
opType: string,
id: string,
fn: () => PromiseLike<{ error: { code: string; message: string } | null }>,
): Promise<void> {
const { error } = await fn();
if (!error) return;
Eif (isDiscardableError(error.code)) {
console.warn(
`[PowerSync] Discarding ${opType} on ${table}/${id} — permanent error ${error.code}: ${error.message}`,
);
return; // fall through; batch.complete() will be called by uploadData
}
throw new Error(
`[PowerSync] Upload failed for ${opType} on ${table}/${id}: ${error.message} (${error.code})`,
);
}
async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {
const batch = await database.getCrudBatch(200);
Iif (!batch) return;
for (const op of batch.crud) {
const { table, op: opType, id, opData: record } = op;
if (opType === 'PUT' || opType === 'PATCH') {
await this.executeOp(table, opType, id, () =>
this.supabase.from(table).upsert({ id, ...record }),
);
} else if (opType === 'DELETE') {
await this.executeOp(table, 'DELETE', id, () =>
this.supabase.from(table).delete().eq('id', id),
);
} else E{
console.warn(`[PowerSync] Unknown operation type: ${opType} — skipping`);
}
}
// Network errors and unexpected failures propagate before reaching here,
// so PowerSync retries the batch. Only completed batches reach this point.
await batch.complete();
}
}
|