feat: Enhance Electric SQL integration and update notification handling

- Added initialization script for Electric SQL user in Docker setup.
- Updated Electric SQL client to support new PGlite architecture and sync functionality.
- Improved notification fetching and syncing logic in useNotifications hook.
- Refactored ElectricProvider to handle initialization state and errors more gracefully.
- Removed deprecated electric.config.ts file and adjusted package dependencies accordingly.
This commit is contained in:
Anish Sarkar 2026-01-12 14:53:18 +05:30
parent 82c6dd0221
commit f441c7b0ce
10 changed files with 376 additions and 1046 deletions

View file

@ -1,24 +1,56 @@
/**
* Electric SQL client setup
* This initializes the Electric SQL client with local PGlite database (PostgreSQL in browser)
* Electric SQL client setup for ElectricSQL 1.x with PGlite
*
* This uses the new ElectricSQL 1.x architecture:
* - PGlite: In-browser PostgreSQL database (local storage)
* - @electric-sql/pglite-sync: Sync plugin to sync Electric shapes into PGlite
* - @electric-sql/client: HTTP client for subscribing to shapes
*/
import { PGlite } from '@electric-sql/pglite'
import { electrify } from 'electric-sql/pglite'
import { getElectricAuthToken } from './auth'
import { electricSync } from '@electric-sql/pglite-sync'
// We'll generate the schema after running electric:generate
// For now, we'll use a placeholder type
type Electric = any
type Schema = any
// Types
export interface ElectricClient {
db: PGlite
syncShape: <T = Record<string, unknown>>(options: SyncShapeOptions) => Promise<SyncHandle<T>>
}
let electric: Electric | null = null
export interface SyncShapeOptions {
table: string
where?: string
columns?: string[]
primaryKey?: string[]
}
export interface SyncHandle<T = Record<string, unknown>> {
unsubscribe: () => void
isUpToDate: boolean
shape: {
handle?: string
offset?: string
}
}
// Singleton instance
let electricClient: ElectricClient | null = null
let isInitializing = false
let initPromise: Promise<Electric> | null = null
let initPromise: Promise<ElectricClient> | null = null
export async function initElectric(): Promise<Electric> {
if (electric) {
return electric
// Get Electric URL from environment
function getElectricUrl(): string {
if (typeof window !== 'undefined') {
return process.env.NEXT_PUBLIC_ELECTRIC_URL || 'http://localhost:5133'
}
return 'http://localhost:5133'
}
/**
* Initialize the Electric SQL client with PGlite and sync plugin
*/
export async function initElectric(): Promise<ElectricClient> {
if (electricClient) {
return electricClient
}
if (isInitializing && initPromise) {
@ -28,40 +60,75 @@ export async function initElectric(): Promise<Electric> {
isInitializing = true
initPromise = (async () => {
try {
const config = {
auth: {
token: await getElectricAuthToken(),
},
url: process.env.NEXT_PUBLIC_ELECTRIC_URL || 'http://localhost:5133',
}
// Initialize PGlite database (PostgreSQL in browser)
// Use idb:// prefix for IndexedDB storage in browser
// relaxedDurability improves responsiveness by scheduling flush after query returns
const conn = new PGlite('idb://surfsense.db', {
// Create PGlite instance with Electric sync plugin
const db = await PGlite.create('idb://surfsense-notifications', {
relaxedDurability: true,
extensions: {
electric: electricSync(),
},
})
// Import schema (will be generated by electric:generate)
// For now, we'll use a dynamic import that will work after schema generation
let schema: Schema
try {
const schemaModule = await import('./generated/schema')
schema = schemaModule.schema
} catch (error) {
console.warn(
'Electric SQL schema not found. Run "pnpm electric:generate" to generate it.',
error
)
// Return a mock electric client for now
return null as any
// Create the notifications table schema in PGlite
// This matches the backend schema
await db.exec(`
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY,
user_id TEXT NOT NULL,
search_space_id INTEGER,
type TEXT NOT NULL,
title TEXT NOT NULL,
message TEXT NOT NULL,
read BOOLEAN NOT NULL DEFAULT FALSE,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);
CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read);
`)
const electricUrl = getElectricUrl()
// Create the client wrapper
electricClient = {
db,
syncShape: async <T = Record<string, unknown>>(options: SyncShapeOptions): Promise<SyncHandle<T>> => {
const { table, where, columns, primaryKey = ['id'] } = options
// Build params for the shape request
const params: Record<string, string> = { table }
if (where) params.where = where
if (columns) params.columns = columns.join(',')
// Use PGlite's electric sync plugin to sync the shape
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const shape = await (db as any).electric.syncShapeToTable({
shape: {
url: `${electricUrl}/v1/shape`,
params,
},
table,
primaryKey,
})
return {
unsubscribe: () => {
if (shape && typeof shape.unsubscribe === 'function') {
shape.unsubscribe()
}
},
isUpToDate: shape?.isUpToDate ?? false,
shape: {
handle: shape?.handle,
offset: shape?.offset,
},
}
},
}
// Electrify the PGlite database connection
electric = await electrify(conn, schema, config)
console.log('Electric SQL initialized successfully with PGlite')
return electric
return electricClient
} catch (error) {
console.error('Failed to initialize Electric SQL:', error)
throw error
@ -73,14 +140,26 @@ export async function initElectric(): Promise<Electric> {
return initPromise
}
export function getElectric(): Electric {
if (!electric) {
/**
* Get the Electric client (throws if not initialized)
*/
export function getElectric(): ElectricClient {
if (!electricClient) {
throw new Error('Electric not initialized. Call initElectric() first.')
}
return electric
return electricClient
}
/**
* Check if Electric is initialized
*/
export function isElectricInitialized(): boolean {
return electric !== null
return electricClient !== null
}
/**
* Get the PGlite database instance
*/
export function getDb(): PGlite | null {
return electricClient?.db ?? null
}

View file

@ -1,19 +0,0 @@
/**
* Electric SQL configuration
* This file will be used by @electric-sql/cli to generate the schema
*/
export const electricConfig = {
connection: {
host: process.env.ELECTRIC_HOST || 'localhost',
port: parseInt(process.env.ELECTRIC_PORT || '5133', 10),
database: process.env.POSTGRES_DB || 'surfsense',
user: process.env.ELECTRIC_USER || 'electric',
password: process.env.ELECTRIC_PASSWORD || 'electric_password',
},
service: {
host: process.env.ELECTRIC_HOST || 'localhost',
port: parseInt(process.env.ELECTRIC_PORT || '5133', 10),
},
}