Package Exports
- @spooky-sync/client-solid
Readme
db-solid
A SurrealDB client for Solid.js with automatic cache synchronization and live query support.
Features
- Live Queries: Real-time data synchronization from remote SurrealDB server
- Local Cache: Fast reads from local WASM SurrealDB instance
- Automatic Sync: Changes from remote automatically update local cache
- Query Deduplication: Multiple components share the same remote subscriptions
- Type-Safe: Full TypeScript support with generated schema types
- Reactive: Seamless integration with Solid.js reactivity
- Offline Support: Local cache works even when remote is temporarily unavailable
Quick Start
Installation
pnpm add db-solidBasic Setup
// db.ts
import { SyncedDb, type SyncedDbConfig } from 'db-solid';
import { type Schema, SURQL_SCHEMA } from './schema.gen';
export const dbConfig: SyncedDbConfig<Schema> = {
schema: SURQL_SCHEMA,
localDbName: 'my-app-local',
internalDbName: 'syncdb-int',
storageStrategy: 'indexeddb',
namespace: 'main',
database: 'my_db',
remoteUrl: 'http://localhost:8000',
tables: ['user', 'thread', 'comment'],
};
export const db = new SyncedDb<Schema>(dbConfig);
export async function initDatabase() {
await db.init();
}Usage in Components
import { db } from "./db";
import { createSignal, createEffect, onMount, onCleanup, For } from "solid-js";
function ThreadList() {
const [threads, setThreads] = createSignal([]);
onMount(async () => {
// Create live query
const liveQuery = await db.query.thread
.find({})
.orderBy("created_at", "desc")
.query();
// React to changes
createEffect(() => {
setThreads([...liveQuery.data]);
});
// Cleanup on unmount
onCleanup(() => {
liveQuery.kill();
});
});
return <For each={threads()}>{(thread) => <div>{thread.title}</div>}</For>;
}Creating Records
// Creates on remote server, automatically syncs to local cache and updates UI
await db.query.thread.createRemote({
title: 'New Thread',
content: 'Thread content',
author: userId,
created_at: new Date(),
});How It Works
┌─────────────────────────────────────────────────────────────┐
│ Your Application │
│ Multiple components can query the same data │
└────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Query Deduplication │
│ Identical queries share a single remote subscription │
└────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Remote SurrealDB Server │
│ Live queries watch for data changes │
└────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Syncer (Cache Manager) │
│ Receives changes and updates local cache │
└────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Local Cache (WASM) │
│ Fast reads, automatic synchronization │
└────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ UI Updates (Solid.js) │
│ Components re-render with new data │
└─────────────────────────────────────────────────────────────┘API Overview
Query Builder
db.query.tableName
.find({ status: 'active' }) // Filter records
.select('field1', 'field2') // Select fields (optional)
.orderBy('created_at', 'desc') // Sort results
.limit(50) // Limit results
.offset(10) // Pagination
.query(); // Execute and return ReactiveQueryResultCRUD Operations
// Create
await db.query.thread.createRemote({ title: 'Hello', content: 'World' });
// Read (with live updates)
const liveQuery = await db.query.thread.find().query();
// Update
await db.query.thread.updateRemote(recordId, { title: 'Updated' });
// Delete
await db.query.thread.deleteRemote(recordId);Reactive Updates
const liveQuery = await db.query.thread.find().query();
createEffect(() => {
// Spread to create new reference and trigger reactivity
setThreads([...liveQuery.data]);
});
onCleanup(() => {
liveQuery.kill(); // Always cleanup!
});Key Features
Query Deduplication
Multiple components with identical queries automatically share a single remote subscription:
// Component A
const query1 = await db.query.thread.find({ status: 'active' }).query();
// Component B (elsewhere in your app)
const query2 = await db.query.thread.find({ status: 'active' }).query();
// ✨ Only ONE remote subscription is created!
// Both components update simultaneously when data changesAutomatic Cache Synchronization
When you create, update, or delete records on the remote server:
- Change is made on remote SurrealDB
- Live query detects the change
- Syncer updates local cache automatically
- All affected queries re-hydrate from cache
- UI updates reactively
Type Safety
// Full TypeScript support
const [thread] = await db.query.thread.createRemote({
title: 'Hello', // ✅ Type-checked
content: 'World', // ✅ Type-checked
invalidField: 'oops', // ❌ TypeScript error!
});
// Autocomplete works everywhere
const liveQuery = await db.query.thread
.find({ status: 'active' }) // ✅ Status field is type-checked
.orderBy('created_at', 'desc'); // ✅ Field names autocompletedDocumentation
- Quick Start Guide: Get up and running quickly
- Architecture Documentation: Deep dive into how it works
- Example App: Full example application
Example
See the complete example application in /example/app-solid demonstrating:
- User authentication
- Thread creation and listing
- Comments with live updates
- Real-time synchronization
- Proper cleanup and error handling
Performance
Query Deduplication Benefits
Without Deduplication:
- 100 components with same query = 100 WebSocket subscriptions
- Each update processed 100 times
With Deduplication:
- 100 components with same query = 1 WebSocket subscription
- Each update processed once
- Savings: 99% reduction in network traffic and CPU usageMemory Usage
Very efficient memory footprint:
- Per unique query: ~1-2 KB
- Local cache: Shared across all queries
- Typical app (10 unique queries): ~20-30 KB
Best Practices
- Always cleanup: Call
liveQuery.kill()inonCleanup() - Use
createRemote(): For CRUD operations to ensure sync - Spread arrays:
[...liveQuery.data]to trigger reactivity - Use
createEffect(): To reactively update signals - Paginate large lists: Use
.limit()and.offset() - Handle errors: Wrap async operations in try-catch
Troubleshooting
Data Not Updating
// ❌ Wrong - won't trigger updates
const threads = liveQuery.data;
// ✅ Correct - creates new reference
createEffect(() => {
setThreads([...liveQuery.data]);
});Memory Leaks
// ❌ Wrong - memory leak!
onMount(async () => {
const liveQuery = await db.query.thread.find().query();
// Missing cleanup
});
// ✅ Correct - properly cleaned up
onMount(async () => {
const liveQuery = await db.query.thread.find().query();
onCleanup(() => {
liveQuery.kill(); // Essential!
});
});"No syncer available" Warning
Make sure you have remoteUrl in your config:
export const dbConfig: SyncedDbConfig<Schema> = {
// ... other config
remoteUrl: 'http://localhost:8000', // Don't forget this!
};Requirements
- Solid.js 1.8+
- SurrealDB 2.0+
- Modern browser with IndexedDB support
License
MIT
Contributing
Contributions are welcome! Please read the architecture documentation to understand how the system works before making changes.
Acknowledgments
Built with: