feat: Implement notification system with real-time updates and Electric SQL integration

- Added notifications table to the database schema with replication support for Electric SQL.
- Developed NotificationService to manage indexing notifications, including creation, updates, and status tracking.
- Introduced NotificationButton and NotificationPopup components for displaying notifications in the UI.
- Enhanced useNotifications hook for real-time notification syncing using PGlite live queries.
- Updated package dependencies for Electric SQL and improved error handling in notification processes.
This commit is contained in:
Anish Sarkar 2026-01-12 22:50:15 +05:30
parent f441c7b0ce
commit 93d17b51f5
10 changed files with 1062 additions and 103 deletions

View file

@ -3,6 +3,7 @@
import { Moon, Sun } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { NotificationButton } from "@/components/notifications/NotificationButton";
interface HeaderProps {
breadcrumb?: React.ReactNode;
@ -29,6 +30,9 @@ export function Header({
{/* Right side - Actions */}
<div className="flex items-center gap-2">
{/* Notifications */}
<NotificationButton />
{/* Theme toggle */}
{onToggleTheme && (
<Tooltip>

View file

@ -0,0 +1,53 @@
"use client";
import { Bell } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useNotifications } from "@/hooks/use-notifications";
import { useAtomValue } from "jotai";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { NotificationPopup } from "./NotificationPopup";
import { cn } from "@/lib/utils";
export function NotificationButton() {
const { data: user } = useAtomValue(currentUserAtom);
const userId = user?.id ? String(user.id) : null;
const { notifications, unreadCount, loading, markAsRead, markAllAsRead } = useNotifications(userId);
return (
<Popover>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" className="h-8 w-8 relative">
<Bell className="h-4 w-4" />
{unreadCount > 0 && (
<span
className={cn(
"absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-destructive text-[10px] font-medium text-destructive-foreground",
unreadCount > 9 && "px-1"
)}
>
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
<span className="sr-only">Notifications</span>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent>Notifications</TooltipContent>
</Tooltip>
<PopoverContent align="end" className="w-80 p-0">
<NotificationPopup
notifications={notifications}
unreadCount={unreadCount}
loading={loading}
markAsRead={markAsRead}
markAllAsRead={markAllAsRead}
/>
</PopoverContent>
</Popover>
);
}

View file

@ -0,0 +1,175 @@
"use client";
import { Bell, Check, CheckCheck, Loader2, AlertCircle, CheckCircle2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Badge } from "@/components/ui/badge";
import type { Notification } from "@/hooks/use-notifications";
import { formatDistanceToNow } from "date-fns";
import { cn } from "@/lib/utils";
interface NotificationPopupProps {
notifications: Notification[];
unreadCount: number;
loading: boolean;
markAsRead: (id: number) => Promise<boolean>;
markAllAsRead: () => Promise<boolean>;
}
export function NotificationPopup({
notifications,
unreadCount,
loading,
markAsRead,
markAllAsRead,
}: NotificationPopupProps) {
const handleMarkAsRead = async (id: number) => {
await markAsRead(id);
};
const handleMarkAllAsRead = async () => {
await markAllAsRead();
};
const formatTime = (dateString: string) => {
try {
return formatDistanceToNow(new Date(dateString), { addSuffix: true });
} catch {
return "Recently";
}
};
const getStatusBadge = (notification: Notification) => {
const status = notification.metadata?.status as string | undefined;
if (!status) return null;
switch (status) {
case "in_progress":
return (
<Badge variant="secondary" className="text-xs">
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
In Progress
</Badge>
);
case "completed":
return (
<Badge variant="default" className="text-xs bg-green-600 hover:bg-green-700">
<CheckCircle2 className="h-3 w-3 mr-1" />
Completed
</Badge>
);
case "failed":
return (
<Badge variant="destructive" className="text-xs">
<AlertCircle className="h-3 w-3 mr-1" />
Failed
</Badge>
);
default:
return null;
}
};
return (
<div className="flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b">
<div className="flex items-center gap-2">
<Bell className="h-4 w-4" />
<h3 className="font-semibold text-sm">Notifications</h3>
{unreadCount > 0 && (
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-destructive text-[10px] font-medium text-destructive-foreground">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
</div>
{unreadCount > 0 && (
<Button
variant="ghost"
size="sm"
onClick={handleMarkAllAsRead}
className="h-7 text-xs"
>
<CheckCheck className="h-3.5 w-3.5 mr-1" />
Mark all read
</Button>
)}
</div>
{/* Notifications List */}
<ScrollArea className="h-[400px]">
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : notifications.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 px-4 text-center">
<Bell className="h-8 w-8 text-muted-foreground mb-2" />
<p className="text-sm text-muted-foreground">No notifications</p>
</div>
) : (
<div className="py-2">
{notifications.map((notification, index) => (
<div key={notification.id}>
<button
type="button"
onClick={() => !notification.read && handleMarkAsRead(notification.id)}
className={cn(
"w-full px-4 py-3 text-left hover:bg-accent transition-colors",
!notification.read && "bg-accent/50"
)}
>
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2 mb-1">
<p
className={cn(
"text-sm font-medium truncate",
!notification.read && "font-semibold"
)}
>
{notification.title}
</p>
<div className="flex items-center gap-2 shrink-0">
{getStatusBadge(notification)}
{!notification.read && (
<div className="h-2 w-2 rounded-full bg-primary mt-1.5" />
)}
</div>
</div>
<p className="text-xs text-muted-foreground line-clamp-2">
{notification.message}
</p>
<div className="flex items-center justify-between mt-2">
<span className="text-xs text-muted-foreground">
{formatTime(notification.created_at)}
</span>
{!notification.read && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={(e) => {
e.stopPropagation();
handleMarkAsRead(notification.id);
}}
>
<Check className="h-3 w-3 mr-1" />
Mark read
</Button>
)}
</div>
</div>
</div>
</button>
{index < notifications.length - 1 && <Separator />}
</div>
))}
</div>
)}
</ScrollArea>
</div>
);
}

View file

@ -1,7 +1,7 @@
"use client"
import { useEffect, useState, useCallback, useRef } from 'react'
import { initElectric, getElectric, isElectricInitialized, type ElectricClient, type SyncHandle } from '@/lib/electric/client'
import { initElectric, isElectricInitialized, type ElectricClient, type SyncHandle } from '@/lib/electric/client'
export interface Notification {
id: number
@ -22,9 +22,9 @@ export function useNotifications(userId: string | null) {
const [initialized, setInitialized] = useState(false)
const [error, setError] = useState<Error | null>(null)
const syncHandleRef = useRef<SyncHandle | null>(null)
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const liveQueryRef = useRef<{ unsubscribe: () => void } | null>(null)
// Initialize Electric SQL and start syncing
// Initialize Electric SQL and start syncing with real-time updates
useEffect(() => {
if (!userId || initialized) return
@ -37,12 +37,42 @@ export function useNotifications(userId: string | null) {
setElectric(electricClient)
// Start syncing notifications for this user
const handle = await electricClient.syncShape<Notification>({
// Start syncing notifications for this user via Electric SQL
// Note: user_id is stored as TEXT in PGlite (UUID from backend is converted)
console.log('Starting Electric SQL sync for user:', userId)
// Use string format for WHERE clause (PGlite sync plugin expects this format)
// The user_id is a UUID string, so we need to quote it properly
const handle = await electricClient.syncShape({
table: 'notifications',
where: `user_id = '${userId}'`,
primaryKey: ['id'],
})
console.log('Electric SQL sync started:', {
isUpToDate: handle.isUpToDate,
hasStream: !!handle.stream,
hasInitialSyncPromise: !!handle.initialSyncPromise,
})
// Wait for initial sync to complete if the promise is available
if (handle.initialSyncPromise) {
console.log('Waiting for initial sync to complete...')
try {
await handle.initialSyncPromise
console.log('Initial sync promise resolved, checking status:', {
isUpToDate: handle.isUpToDate,
})
} catch (syncErr) {
console.error('Initial sync failed:', syncErr)
}
}
// Check status after waiting
console.log('Sync status after waiting:', {
isUpToDate: handle.isUpToDate,
hasStream: !!handle.stream,
})
if (!mounted) {
handle.unsubscribe()
@ -53,8 +83,113 @@ export function useNotifications(userId: string | null) {
setInitialized(true)
setError(null)
// Initial fetch
// Fetch notifications after sync is complete (we already waited above)
await fetchNotifications(electricClient.db)
// Set up real-time updates using PGlite live queries
// Electric SQL syncs data to PGlite in real-time via WebSocket/HTTP
// PGlite live queries detect when the synced data changes and trigger callbacks
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const db = electricClient.db as any
// Use PGlite's live query API for real-time updates
// Based on latest PGlite docs: db.live.query(query, params, callback)
if (db.live?.query && typeof db.live.query === 'function') {
const liveQuery = db.live.query(
`SELECT * FROM notifications WHERE user_id = $1 ORDER BY created_at DESC`,
[userId],
(result: { rows: Notification[] }) => {
// This callback fires automatically when Electric SQL syncs changes
if (mounted) {
setNotifications(result.rows)
}
}
)
// Set initial results immediately
if (liveQuery.initialResults) {
setNotifications(liveQuery.initialResults.rows)
}
if (mounted && liveQuery && typeof liveQuery.unsubscribe === 'function') {
liveQueryRef.current = liveQuery
console.log('✅ Real-time notifications enabled via PGlite live queries')
}
} else {
// Fallback: Monitor sync handle for updates
// Electric SQL's syncShape should trigger updates, but we need to detect them
// This is a lightweight approach that only checks when sync indicates changes
console.warn('PGlite live queries not available - using sync-based change detection')
let lastNotificationIds = new Set<number>()
const checkForSyncUpdates = async () => {
if (!mounted) return
try {
const result = await electricClient.db.query<Notification>(
`SELECT * FROM notifications WHERE user_id = $1 ORDER BY created_at DESC`,
[userId]
)
// PGlite query returns { rows: [] } format
const rows = result.rows || []
// Only update if data actually changed
const currentIds = new Set(rows.map(r => r.id))
const currentHash = JSON.stringify(
rows.map(r => ({ id: r.id, read: r.read, updated_at: r.updated_at }))
)
// Check if IDs changed (new/deleted notifications)
const idsChanged =
currentIds.size !== lastNotificationIds.size ||
[...currentIds].some(id => !lastNotificationIds.has(id)) ||
[...lastNotificationIds].some(id => !currentIds.has(id))
if (idsChanged) {
setNotifications(rows)
lastNotificationIds = currentIds
} else {
// Check if any notification properties changed (e.g., read status)
// Compare with current state
setNotifications(prev => {
const prevHash = JSON.stringify(
prev.map(r => ({ id: r.id, read: r.read, updated_at: r.updated_at }))
)
if (prevHash !== currentHash) {
return rows
}
return prev
})
}
} catch (err) {
console.error('Failed to check for notification updates:', err)
}
// Check again after a short delay (Electric SQL syncs are fast)
if (mounted) {
setTimeout(checkForSyncUpdates, 500) // Check every 500ms - Electric SQL syncs are near-instant
}
}
// Start monitoring
checkForSyncUpdates()
liveQueryRef.current = {
unsubscribe: () => {
mounted = false
}
}
}
} catch (liveErr) {
console.warn('Failed to set up real-time updates:', liveErr)
// Minimal fallback - this should rarely be needed
liveQueryRef.current = {
unsubscribe: () => {}
}
}
} catch (err) {
if (!mounted) return
console.error('Failed to initialize Electric SQL:', err)
@ -66,17 +201,29 @@ export function useNotifications(userId: string | null) {
async function fetchNotifications(db: InstanceType<typeof import('@electric-sql/pglite').PGlite>) {
try {
// Debug: Check all notifications first
const allNotifications = await db.query<Notification>(
`SELECT * FROM notifications ORDER BY created_at DESC`
)
console.log('All notifications in PGlite:', allNotifications.rows?.length || 0, allNotifications.rows)
// Use PGlite's query method (not exec for SELECT queries)
const result = await db.query<Notification>(
`SELECT * FROM notifications
WHERE user_id = $1
ORDER BY created_at DESC`,
[userId]
)
console.log(`Notifications for user ${userId}:`, result.rows?.length || 0, result.rows)
if (mounted) {
setNotifications(result.rows)
// PGlite query returns { rows: [] } format
setNotifications(result.rows || [])
}
} catch (err) {
console.error('Failed to fetch notifications:', err)
// Log more details for debugging
console.error('Error details:', err)
}
}
@ -88,38 +235,13 @@ export function useNotifications(userId: string | null) {
syncHandleRef.current.unsubscribe()
syncHandleRef.current = null
}
if (liveQueryRef.current) {
liveQueryRef.current.unsubscribe()
liveQueryRef.current = null
}
}
}, [userId, initialized])
// Poll for updates (PGlite doesn't have live queries like the old electric-sql)
useEffect(() => {
if (!electric || !userId || !initialized) return
const fetchNotifications = async () => {
try {
const result = await electric.db.query<Notification>(
`SELECT * FROM notifications
WHERE user_id = $1
ORDER BY created_at DESC`,
[userId]
)
setNotifications(result.rows)
} catch (err) {
console.error('Failed to fetch notifications:', err)
}
}
// Poll every 2 seconds for updates
pollIntervalRef.current = setInterval(fetchNotifications, 2000)
return () => {
if (pollIntervalRef.current) {
clearInterval(pollIntervalRef.current)
pollIntervalRef.current = null
}
}
}, [electric, userId, initialized])
// Mark notification as read (local only - needs backend sync)
const markAsRead = useCallback(
async (notificationId: number) => {
@ -130,7 +252,7 @@ export function useNotifications(userId: string | null) {
try {
// Update locally in PGlite
await electric.db.exec(
await electric.db.query(
`UPDATE notifications SET read = true, updated_at = NOW() WHERE id = $1`,
[notificationId]
)

View file

@ -9,11 +9,12 @@
import { PGlite } from '@electric-sql/pglite'
import { electricSync } from '@electric-sql/pglite-sync'
import { live } from '@electric-sql/pglite/live'
// Types
export interface ElectricClient {
db: PGlite
syncShape: <T = Record<string, unknown>>(options: SyncShapeOptions) => Promise<SyncHandle<T>>
syncShape: (options: SyncShapeOptions) => Promise<SyncHandle>
}
export interface SyncShapeOptions {
@ -23,13 +24,13 @@ export interface SyncShapeOptions {
primaryKey?: string[]
}
export interface SyncHandle<T = Record<string, unknown>> {
export interface SyncHandle {
unsubscribe: () => void
isUpToDate: boolean
shape: {
handle?: string
offset?: string
}
readonly isUpToDate: boolean
// The stream property contains the ShapeStreamInterface from pglite-sync
stream?: unknown
// Promise that resolves when initial sync is complete
initialSyncPromise?: Promise<void>
}
// Singleton instance
@ -37,6 +38,10 @@ let electricClient: ElectricClient | null = null
let isInitializing = false
let initPromise: Promise<ElectricClient> | null = null
// Version for sync state - increment this to force fresh sync when Electric config changes
// Incremented to v4 to fix sync completion issues
const SYNC_VERSION = 4
// Get Electric URL from environment
function getElectricUrl(): string {
if (typeof window !== 'undefined') {
@ -60,11 +65,15 @@ export async function initElectric(): Promise<ElectricClient> {
isInitializing = true
initPromise = (async () => {
try {
// Create PGlite instance with Electric sync plugin
const db = await PGlite.create('idb://surfsense-notifications', {
// Create PGlite instance with Electric sync plugin and live queries
// Include version in database name to force fresh sync when Electric config changes
const db = await PGlite.create({
dataDir: `idb://surfsense-notifications-v${SYNC_VERSION}`,
relaxedDurability: true,
extensions: {
electric: electricSync(),
// Enable debug mode in electricSync to see detailed sync logs
electric: electricSync({ debug: true }),
live, // Enable live queries for real-time updates
},
})
@ -93,36 +102,185 @@ export async function initElectric(): Promise<ElectricClient> {
// Create the client wrapper
electricClient = {
db,
syncShape: async <T = Record<string, unknown>>(options: SyncShapeOptions): Promise<SyncHandle<T>> => {
syncShape: async (options: SyncShapeOptions): Promise<SyncHandle> => {
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(',')
// Build params for the shape request
// Electric SQL expects params as URL query parameters
const params: Record<string, string> = { table }
// Validate and fix WHERE clause to ensure string literals are properly quoted
let validatedWhere = where
if (where) {
// Check if where uses positional parameters
if (where.includes('$1')) {
// Extract the value from the where clause if it's embedded
// For now, we'll use the where clause as-is and let Electric handle it
params.where = where
validatedWhere = where
} else {
// Validate that string literals are properly quoted
// Count single quotes - should be even (pairs) for properly quoted strings
const singleQuoteCount = (where.match(/'/g) || []).length
if (singleQuoteCount % 2 !== 0) {
// Odd number of quotes means unterminated string literal
console.warn('Where clause has unmatched quotes, fixing:', where)
// Add closing quote at the end
validatedWhere = `${where}'`
params.where = validatedWhere
} else {
// Use the where clause directly (already formatted)
params.where = where
validatedWhere = 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({
console.log('Syncing shape with params:', params)
console.log('Electric URL:', `${electricUrl}/v1/shape`)
console.log('Where clause:', where, 'Validated:', validatedWhere)
try {
// Debug: Test Electric SQL connection directly first
// Use validatedWhere to ensure proper URL encoding
const testUrl = `${electricUrl}/v1/shape?table=${table}&offset=-1${validatedWhere ? `&where=${encodeURIComponent(validatedWhere)}` : ''}`
console.log('Testing Electric SQL directly:', testUrl)
try {
const testResponse = await fetch(testUrl)
const testHeaders = {
handle: testResponse.headers.get('electric-handle'),
offset: testResponse.headers.get('electric-offset'),
upToDate: testResponse.headers.get('electric-up-to-date'),
}
console.log('Direct Electric SQL response headers:', testHeaders)
const testData = await testResponse.json()
console.log('Direct Electric SQL data count:', Array.isArray(testData) ? testData.length : 'not array', testData)
} catch (testErr) {
console.error('Direct Electric SQL test failed:', testErr)
}
// Use PGlite's electric sync plugin to sync the shape
// According to Electric SQL docs, the shape config uses params for table, where, columns
// Note: mapColumns is OPTIONAL per pglite-sync types.ts
// Create a promise that resolves when initial sync is complete
let resolveInitialSync: () => void
let rejectInitialSync: (error: Error) => void
const initialSyncPromise = new Promise<void>((resolve, reject) => {
resolveInitialSync = resolve
rejectInitialSync = reject
// Safety timeout - if sync doesn't complete in 30s, something is wrong
setTimeout(() => {
console.warn(`⚠️ Sync timeout for ${table} - sync did not complete in 30s`)
resolve() // Resolve anyway to not block, but log warning
}, 30000)
})
const shapeConfig = {
shape: {
url: `${electricUrl}/v1/shape`,
params,
params: {
table,
...(validatedWhere ? { where: validatedWhere } : {}),
...(columns ? { columns: columns.join(',') } : {}),
},
},
table,
primaryKey,
shapeKey: `v${SYNC_VERSION}_${table}_${where?.replace(/[^a-zA-Z0-9]/g, '_') || 'all'}`, // Versioned key to force fresh sync when needed
onInitialSync: () => {
console.log(`✅ Initial sync complete for ${table} - data should now be in PGlite`)
resolveInitialSync()
},
onError: (error: Error) => {
console.error(`❌ Shape sync error for ${table}:`, error)
console.error('Error details:', JSON.stringify(error, Object.getOwnPropertyNames(error)))
rejectInitialSync(error)
},
}
console.log('syncShapeToTable config:', JSON.stringify(shapeConfig, null, 2))
// Type assertion to PGlite with electric extension
const pgWithElectric = db as PGlite & { electric: { syncShapeToTable: (config: typeof shapeConfig) => Promise<{ unsubscribe: () => void; isUpToDate: boolean; stream: unknown }> } }
const shape = await pgWithElectric.electric.syncShapeToTable(shapeConfig)
if (!shape) {
throw new Error('syncShapeToTable returned undefined')
}
// Log the actual shape result structure
console.log('Shape sync result (initial):', {
hasUnsubscribe: typeof shape?.unsubscribe === 'function',
isUpToDate: shape?.isUpToDate,
hasStream: !!shape?.stream,
streamType: typeof shape?.stream,
})
// Debug the stream if available
if (shape?.stream) {
const stream = shape.stream as any
console.log('Shape stream details:', {
shapeHandle: stream?.shapeHandle,
lastOffset: stream?.lastOffset,
isUpToDate: stream?.isUpToDate,
error: stream?.error,
hasSubscribe: typeof stream?.subscribe === 'function',
hasUnsubscribe: typeof stream?.unsubscribe === 'function',
})
// Try to subscribe to the stream to see if it's receiving messages
if (typeof stream?.subscribe === 'function') {
console.log('Subscribing to shape stream for debugging...')
stream.subscribe((messages: unknown[]) => {
console.log('🔵 Shape stream received messages:', messages?.length || 0)
if (messages && messages.length > 0) {
console.log('First message:', JSON.stringify(messages[0], null, 2))
}
})
}
}
// Wait briefly to see if sync starts
await new Promise(resolve => setTimeout(resolve, 100))
console.log('Shape sync result (after 100ms):', {
isUpToDate: shape?.isUpToDate,
})
// Return the shape handle - isUpToDate is a getter that reflects current state
return {
unsubscribe: () => {
console.log('unsubscribing')
if (shape && typeof shape.unsubscribe === 'function') {
shape.unsubscribe()
}
},
isUpToDate: shape?.isUpToDate ?? false,
shape: {
handle: shape?.handle,
offset: shape?.offset,
// Use getter to always return current state
get isUpToDate() {
return shape?.isUpToDate ?? false
},
stream: shape?.stream,
initialSyncPromise, // Expose promise so callers can wait for sync
}
} catch (error) {
console.error('Failed to sync shape:', error)
// Check if Electric SQL server is reachable
try {
const response = await fetch(`${electricUrl}/v1/shape?table=${table}&offset=-1`, {
method: 'GET',
})
console.log('Electric SQL server response:', response.status, response.statusText)
if (!response.ok) {
console.error('Electric SQL server error:', await response.text())
}
} catch (fetchError) {
console.error('Cannot reach Electric SQL server:', fetchError)
console.error('Make sure Electric SQL is running at:', electricUrl)
}
throw error
}
},
}

View file

@ -30,7 +30,7 @@
"@blocknote/react": "^0.45.0",
"@blocknote/server-util": "^0.45.0",
"@electric-sql/client": "^1.4.0",
"@electric-sql/pglite": "^0.2.17",
"@electric-sql/pglite": "^0.3.14",
"@electric-sql/pglite-sync": "^0.4.0",
"@electric-sql/react": "^1.0.26",
"@hookform/resolvers": "^5.2.2",

View file

@ -36,11 +36,11 @@ importers:
specifier: ^1.4.0
version: 1.4.0
'@electric-sql/pglite':
specifier: ^0.2.17
version: 0.2.17
specifier: ^0.3.14
version: 0.3.14
'@electric-sql/pglite-sync':
specifier: ^0.4.0
version: 0.4.0(@electric-sql/pglite@0.2.17)
version: 0.4.0(@electric-sql/pglite@0.3.14)
'@electric-sql/react':
specifier: ^1.0.26
version: 1.0.26(react@19.2.3)
@ -157,7 +157,7 @@ importers:
version: 17.2.3
drizzle-orm:
specifier: ^0.44.5
version: 0.44.7(@electric-sql/pglite@0.2.17)(@opentelemetry/api@1.9.0)(@prisma/client@4.8.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.7)
version: 0.44.7(@electric-sql/pglite@0.3.14)(@opentelemetry/api@1.9.0)(@prisma/client@4.8.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.7)
emblor:
specifier: ^1.4.8
version: 1.4.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@ -575,8 +575,8 @@ packages:
peerDependencies:
'@electric-sql/pglite': 0.3.14
'@electric-sql/pglite@0.2.17':
resolution: {integrity: sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw==}
'@electric-sql/pglite@0.3.14':
resolution: {integrity: sha512-3DB258dhqdsArOI1fIt7cb9RpUOgcDg5hXWVgVHAeqVQ/qxtFy605QKs4gx6mFq3jWsSPqDN8TgSEsqC3OfV9Q==}
'@electric-sql/react@1.0.26':
resolution: {integrity: sha512-cCKLQrtGNaAPBzdLZk97bK/Hue3fKkfL0/aA5HAPzoo7U07/TRzzs4EVRy7q+BV6AONEK+YXxxrzH9gEH8YVQA==}
@ -6760,13 +6760,13 @@ snapshots:
optionalDependencies:
'@rollup/rollup-darwin-arm64': 4.55.1
'@electric-sql/pglite-sync@0.4.0(@electric-sql/pglite@0.2.17)':
'@electric-sql/pglite-sync@0.4.0(@electric-sql/pglite@0.3.14)':
dependencies:
'@electric-sql/client': 1.4.0
'@electric-sql/experimental': 1.0.14(@electric-sql/client@1.4.0)
'@electric-sql/pglite': 0.2.17
'@electric-sql/pglite': 0.3.14
'@electric-sql/pglite@0.2.17': {}
'@electric-sql/pglite@0.3.14': {}
'@electric-sql/react@1.0.26(react@19.2.3)':
dependencies:
@ -9701,9 +9701,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
drizzle-orm@0.44.7(@electric-sql/pglite@0.2.17)(@opentelemetry/api@1.9.0)(@prisma/client@4.8.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.7):
drizzle-orm@0.44.7(@electric-sql/pglite@0.3.14)(@opentelemetry/api@1.9.0)(@prisma/client@4.8.1)(@types/pg@8.16.0)(better-sqlite3@11.10.0)(pg@8.16.3)(postgres@3.4.7):
optionalDependencies:
'@electric-sql/pglite': 0.2.17
'@electric-sql/pglite': 0.3.14
'@opentelemetry/api': 1.9.0
'@prisma/client': 4.8.1
'@types/pg': 8.16.0