browse the protocol like its 2008 ibex.desertthunder.dev
ubuntu atproto svelte
7

Configure Feed

Select the types of activity you want to include in your feed.

feat: pglite cache layer scaffold

Owais Jamil (Jun 9, 2026, 12:25 PM -0500) 77a137b5 58f411e8

+284 -2
+16 -1
eslint.config.js
··· 28 28 parserOptions: { projectService: true, extraFileExtensions: ['.svelte'], parser: ts.parser, svelteConfig } 29 29 } 30 30 }, 31 - { rules: {} } 31 + { 32 + rules: { 33 + '@typescript-eslint/no-unused-vars': [ 34 + 'error', 35 + { 36 + args: 'all', 37 + argsIgnorePattern: '^_', 38 + caughtErrors: 'all', 39 + caughtErrorsIgnorePattern: '^_', 40 + destructuredArrayIgnorePattern: '^_', 41 + varsIgnorePattern: '^_', 42 + ignoreRestSiblings: true 43 + } 44 + ] 45 + } 46 + } 32 47 );
+1
package.json
··· 44 44 "@atcute/bluesky": "^4.0.6", 45 45 "@atcute/client": "^5.0.0", 46 46 "@atcute/lexicons": "^2.0.0", 47 + "@electric-sql/pglite": "^0.5.1", 47 48 "@shikijs/core": "^4.2.0", 48 49 "@shikijs/engine-javascript": "^4.2.0", 49 50 "@shikijs/langs": "^4.2.0",
+8
pnpm-lock.yaml
··· 20 20 '@atcute/lexicons': 21 21 specifier: ^2.0.0 22 22 version: 2.0.0 23 + '@electric-sql/pglite': 24 + specifier: ^0.5.1 25 + version: 0.5.1 23 26 '@shikijs/core': 24 27 specifier: ^4.2.0 25 28 version: 4.2.0 ··· 133 136 134 137 '@blazediff/core@1.9.1': 135 138 resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} 139 + 140 + '@electric-sql/pglite@0.5.1': 141 + resolution: {integrity: sha512-h2Vc+qkQqsEL5kvyN5nBAxn3Vbyvka7QfDW7Io+CdcwU1+X8JbCAN2og+5dI11S3eJuDfroUCxzJaap6k+ezEw==} 136 142 137 143 '@emnapi/core@1.10.0': 138 144 resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} ··· 1448 1454 unicode-segmenter: 0.14.5 1449 1455 1450 1456 '@blazediff/core@1.9.1': {} 1457 + 1458 + '@electric-sql/pglite@0.5.1': {} 1451 1459 1452 1460 '@emnapi/core@1.10.0': 1453 1461 dependencies:
+20
src/lib/db/client.svelte.test.ts
··· 1 + import { afterEach, describe, expect, it } from 'vitest'; 2 + import { closeDatabase, getDatabase, getDatabaseStartupReport } from './client'; 3 + 4 + describe('PGlite client', () => { 5 + afterEach(async () => { 6 + await closeDatabase(); 7 + }); 8 + 9 + it('loads the WASM bundle and answers a query in the browser', async () => { 10 + expect.assertions(3); 11 + 12 + const db = await getDatabase(); 13 + const result = await db.query<{ ok: number }>('select 1::int as ok'); 14 + const report = getDatabaseStartupReport(); 15 + 16 + expect(result.rows[0]?.ok).toBe(1); 17 + expect(report.status).toBe('ready'); 18 + expect(report.wasm).toBe('loaded'); 19 + }); 20 + });
+115
src/lib/db/client.ts
··· 1 + import { PGlite } from '@electric-sql/pglite'; 2 + import { DB_DATA_DIR, SQL } from './schema'; 3 + 4 + export type DbClient = PGlite; 5 + 6 + export type DatabaseStartupReport = { 7 + status: 'idle' | 'loading' | 'ready' | 'error'; 8 + dataDir: string | null; 9 + startedAt: string | null; 10 + finishedAt: string | null; 11 + wasm: 'not_checked' | 'loaded' | 'error'; 12 + indexedDb: 'not_used' | 'available' | 'unavailable'; 13 + persisted: boolean | null; 14 + error: string | null; 15 + }; 16 + 17 + let databasePromise: Promise<DbClient> | null = null; 18 + let database: DbClient | null = null; 19 + let startupReport: DatabaseStartupReport = createIdleReport(); 20 + 21 + export function getDatabaseStartupReport(): DatabaseStartupReport { 22 + return { ...startupReport }; 23 + } 24 + 25 + export async function getDatabase(): Promise<DbClient> { 26 + if (!databasePromise) { 27 + databasePromise = initializeDatabase(); 28 + } 29 + 30 + return databasePromise; 31 + } 32 + 33 + export async function closeDatabase(): Promise<void> { 34 + if (database) { 35 + await database.close(); 36 + } 37 + 38 + database = null; 39 + databasePromise = null; 40 + startupReport = createIdleReport(); 41 + } 42 + 43 + export async function resetDatabaseForTests(): Promise<void> { 44 + await closeDatabase(); 45 + } 46 + 47 + async function initializeDatabase(): Promise<DbClient> { 48 + const dataDir = getDefaultDataDir(); 49 + startupReport = { 50 + status: 'loading', 51 + dataDir: dataDir ?? null, 52 + startedAt: new Date().toISOString(), 53 + finishedAt: null, 54 + wasm: 'not_checked', 55 + indexedDb: getIndexedDbStatus(dataDir), 56 + persisted: null, 57 + error: null 58 + }; 59 + 60 + try { 61 + const client = await PGlite.create(dataDir); 62 + await client.query(SQL.ping); 63 + 64 + database = client; 65 + startupReport = { 66 + ...startupReport, 67 + status: 'ready', 68 + finishedAt: new Date().toISOString(), 69 + wasm: 'loaded', 70 + persisted: dataDir?.startsWith('idb://') === true && startupReport.indexedDb === 'available' 71 + }; 72 + 73 + return client; 74 + } catch (error) { 75 + startupReport = { 76 + ...startupReport, 77 + status: 'error', 78 + finishedAt: new Date().toISOString(), 79 + wasm: 'error', 80 + error: messageFromError(error) 81 + }; 82 + databasePromise = null; 83 + throw error; 84 + } 85 + } 86 + 87 + function getDefaultDataDir() { 88 + return isBrowser() ? DB_DATA_DIR : undefined; 89 + } 90 + 91 + function getIndexedDbStatus(dataDir: string | undefined): DatabaseStartupReport['indexedDb'] { 92 + if (!dataDir?.startsWith('idb://')) return 'not_used'; 93 + return isBrowser() && 'indexedDB' in globalThis ? 'available' : 'unavailable'; 94 + } 95 + 96 + function isBrowser() { 97 + return typeof globalThis.window !== 'undefined'; 98 + } 99 + 100 + function createIdleReport(): DatabaseStartupReport { 101 + return { 102 + status: 'idle', 103 + dataDir: null, 104 + startedAt: null, 105 + finishedAt: null, 106 + wasm: 'not_checked', 107 + indexedDb: 'not_used', 108 + persisted: null, 109 + error: null 110 + }; 111 + } 112 + 113 + function messageFromError(error: unknown) { 114 + return error instanceof Error ? error.message : String(error); 115 + }
+6
src/lib/db/index.ts
··· 1 + export * from './client'; 2 + export * from './migrations'; 3 + export * from './schema'; 4 + export * from './search'; 5 + export * from './sync'; 6 + export * from './repositories/records';
+19
src/lib/db/migrations.ts
··· 1 + import type { DbClient } from './client'; 2 + 3 + export type Migration = { id: string; description: string; up: (db: DbClient) => Promise<void> }; 4 + 5 + export type MigrationStatus = { 6 + pending: readonly Migration[]; 7 + applied: readonly string[]; 8 + currentVersion: string | null; 9 + }; 10 + 11 + export const migrations: readonly Migration[] = []; 12 + 13 + export async function getMigrationStatus(_db: DbClient): Promise<MigrationStatus> { 14 + return { pending: migrations, applied: [], currentVersion: null }; 15 + } 16 + 17 + export async function runMigrations(_db: DbClient): Promise<MigrationStatus> { 18 + return getMigrationStatus(_db); 19 + }
+37
src/lib/db/repositories/records.ts
··· 1 + import type { DbClient } from '../client'; 2 + 3 + export type CachedRecordInput = { 4 + accountDid: string; 5 + repoDid: string; 6 + collection: string; 7 + rkey: string; 8 + uri: string; 9 + cid: string; 10 + value: unknown; 11 + indexedText?: string | null; 12 + createdAt?: string | null; 13 + indexedAt?: string | null; 14 + updatedAt?: string | null; 15 + }; 16 + 17 + export type CachedRecord = CachedRecordInput & { storedAt: string; syncStatus: 'fresh' | 'stale' | 'deleted' }; 18 + 19 + export type ListCachedRecordsOptions = { repoDid: string; collection?: string; limit?: number; offset?: number }; 20 + 21 + export async function upsertCachedRecord(_db: DbClient, _record: CachedRecordInput): Promise<void> { 22 + throw new Error('cached_records schema has not been created yet. Run migrations before writing records.'); 23 + } 24 + 25 + export async function upsertCachedRecords(db: DbClient, records: readonly CachedRecordInput[]): Promise<void> { 26 + for (const record of records) { 27 + await upsertCachedRecord(db, record); 28 + } 29 + } 30 + 31 + export async function listCachedRecords(_db: DbClient, _options: ListCachedRecordsOptions): Promise<CachedRecord[]> { 32 + throw new Error('cached_records schema has not been created yet. Run migrations before reading records.'); 33 + } 34 + 35 + export async function getCachedRecordByUri(_db: DbClient, _uri: string): Promise<CachedRecord | null> { 36 + throw new Error('cached_records schema has not been created yet. Run migrations before reading records.'); 37 + }
+19
src/lib/db/schema.ts
··· 1 + export const DB_DATA_DIR = 'idb://intrepid-ibex-cache-v1'; 2 + 3 + export const TABLES = { 4 + migrations: 'schema_migrations', 5 + accounts: 'accounts', 6 + cachedRecords: 'cached_records', 7 + collectionSyncState: 'collection_sync_state', 8 + recordSearch: 'record_search' 9 + } as const; 10 + 11 + export const INDEXES = { 12 + cachedRecordsCollection: 'cached_records_repo_collection_idx', 13 + cachedRecordsUri: 'cached_records_uri_idx', 14 + cachedRecordsRecent: 'cached_records_recent_idx', 15 + collectionSyncStateAccount: 'collection_sync_state_account_idx', 16 + recordSearchText: 'record_search_text_idx' 17 + } as const; 18 + 19 + export const SQL = { ping: 'select 1::int as ok', currentTimestamp: "timezone('utc', now())" } as const;
+14
src/lib/db/search.ts
··· 1 + import type { DbClient } from './client'; 2 + import type { CachedRecord } from './repositories/records'; 3 + 4 + export type SearchScope = { repoDid: string; collection?: string }; 5 + 6 + export type SearchResult = CachedRecord & { rank: number; snippet: string | null }; 7 + 8 + export function normalizeSearchQuery(query: string): string { 9 + return query.trim().replace(/\s+/g, ' '); 10 + } 11 + 12 + export async function searchCachedRecords(_db: DbClient, _query: string, _scope: SearchScope): Promise<SearchResult[]> { 13 + throw new Error('record_search schema has not been created yet. Run migrations before searching records.'); 14 + }
+20
src/lib/db/sync.ts
··· 1 + import type { DbClient } from './client'; 2 + import { upsertCachedRecords, type CachedRecordInput } from './repositories/records'; 3 + 4 + export type CollectionSyncStateInput = { 5 + accountDid: string; 6 + repoDid: string; 7 + collection: string; 8 + cursor?: string | null; 9 + lastSyncedAt?: string | null; 10 + lastError?: string | null; 11 + }; 12 + 13 + export async function cacheFetchedRecords(db: DbClient, records: readonly CachedRecordInput[]): Promise<void> { 14 + if (records.length === 0) return; 15 + await upsertCachedRecords(db, records); 16 + } 17 + 18 + export async function updateCollectionSyncState(_db: DbClient, _state: CollectionSyncStateInput): Promise<void> { 19 + throw new Error('collection_sync_state schema has not been created yet. Run migrations before storing sync state.'); 20 + }
+9 -1
vite.config.ts
··· 4 4 5 5 export default defineConfig({ 6 6 plugins: [sveltekit()], 7 + assetsInclude: ['**/*.wasm'], 8 + optimizeDeps: { exclude: ['@electric-sql/pglite'] }, 7 9 test: { 8 10 expect: { requireAssertions: true }, 11 + ui: false, 9 12 projects: [ 10 13 { 11 14 extends: './vite.config.ts', 12 15 test: { 13 16 name: 'client', 14 - browser: { enabled: true, provider: playwright(), instances: [{ browser: 'chromium', headless: true }] }, 17 + browser: { 18 + headless: true, 19 + enabled: true, 20 + provider: playwright(), 21 + instances: [{ browser: 'chromium', headless: true }] 22 + }, 15 23 include: ['src/**/*.svelte.{test,spec}.{js,ts}'], 16 24 exclude: ['src/lib/server/**'] 17 25 }