@spooky-sync/client-solid 0.0.0-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +11 -0
- package/QUICK_START.md +415 -0
- package/README.md +330 -0
- package/dist/index.cjs +229 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +164 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
- package/src/cache/index.ts +41 -0
- package/src/cache/surrealdb-wasm-factory.ts +64 -0
- package/src/index.ts +254 -0
- package/src/lib/SpookyProvider.ts +55 -0
- package/src/lib/context.ts +13 -0
- package/src/lib/models.ts +8 -0
- package/src/lib/use-query.ts +165 -0
- package/src/types/index.ts +84 -0
- package/tsconfig.json +27 -0
- package/tsdown.config.ts +19 -0
- package/tsup.config.ts +29 -0
package/QUICK_START.md
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# Quick Start Guide
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add db-solid
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Basic Setup
|
|
10
|
+
|
|
11
|
+
### 1. Configure Database
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
// db.ts
|
|
15
|
+
import { SyncedDb, type SyncedDbConfig } from 'db-solid';
|
|
16
|
+
import { type Schema, SURQL_SCHEMA } from './schema.gen';
|
|
17
|
+
|
|
18
|
+
export const dbConfig: SyncedDbConfig<Schema> = {
|
|
19
|
+
schema: SURQL_SCHEMA,
|
|
20
|
+
localDbName: 'my-app-local',
|
|
21
|
+
internalDbName: 'syncdb-int',
|
|
22
|
+
storageStrategy: 'indexeddb',
|
|
23
|
+
namespace: 'main',
|
|
24
|
+
database: 'my_db',
|
|
25
|
+
remoteUrl: 'http://localhost:8000', // Your SurrealDB server
|
|
26
|
+
tables: ['user', 'thread', 'comment'],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const db = new SyncedDb<Schema>(dbConfig);
|
|
30
|
+
|
|
31
|
+
export async function initDatabase(): Promise<void> {
|
|
32
|
+
await db.init();
|
|
33
|
+
console.log('Database initialized');
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Initialize in Your App
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// App.tsx
|
|
41
|
+
import { initDatabase } from "./db";
|
|
42
|
+
|
|
43
|
+
function App() {
|
|
44
|
+
onMount(async () => {
|
|
45
|
+
await initDatabase();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return <Router>{/* your routes */}</Router>;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage Examples
|
|
53
|
+
|
|
54
|
+
### Querying Data with Live Updates
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { db } from "./db";
|
|
58
|
+
import { createSignal, createEffect, onMount, onCleanup } from "solid-js";
|
|
59
|
+
|
|
60
|
+
function ThreadList() {
|
|
61
|
+
const [threads, setThreads] = createSignal([]);
|
|
62
|
+
|
|
63
|
+
onMount(async () => {
|
|
64
|
+
// Create a live query that automatically updates
|
|
65
|
+
const liveQuery = await db.query.thread
|
|
66
|
+
.find({})
|
|
67
|
+
.orderBy("created_at", "desc")
|
|
68
|
+
.query();
|
|
69
|
+
|
|
70
|
+
// React to changes
|
|
71
|
+
createEffect(() => {
|
|
72
|
+
setThreads([...liveQuery.data]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Cleanup when component unmounts
|
|
76
|
+
onCleanup(() => {
|
|
77
|
+
liveQuery.kill();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<For each={threads()}>
|
|
83
|
+
{(thread) => <ThreadCard thread={thread} />}
|
|
84
|
+
</For>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Filtered Queries
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// Find threads by specific criteria
|
|
93
|
+
const liveQuery = await db.query.thread
|
|
94
|
+
.find({ status: 'active', author: currentUserId })
|
|
95
|
+
.orderBy('created_at', 'desc')
|
|
96
|
+
.limit(20)
|
|
97
|
+
.query();
|
|
98
|
+
|
|
99
|
+
// React to changes
|
|
100
|
+
createEffect(() => {
|
|
101
|
+
setThreads([...liveQuery.data]);
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Creating Records
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Create a new thread on the remote server
|
|
109
|
+
// This will automatically trigger live query updates
|
|
110
|
+
async function createThread(title: string, content: string) {
|
|
111
|
+
const [thread] = await db.query.thread.createRemote({
|
|
112
|
+
title,
|
|
113
|
+
content,
|
|
114
|
+
author: currentUserId,
|
|
115
|
+
created_at: new Date(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return thread;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Updating Records
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Update a record on the remote server
|
|
126
|
+
async function updateThread(threadId: RecordId, updates: Partial<Thread>) {
|
|
127
|
+
await db.query.thread.updateRemote(threadId, updates);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Deleting Records
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Delete a record on the remote server
|
|
135
|
+
async function deleteThread(threadId: RecordId) {
|
|
136
|
+
await db.query.thread.deleteRemote(threadId);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## API Cheat Sheet
|
|
141
|
+
|
|
142
|
+
### Query Builder
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
db.query.tableName
|
|
146
|
+
.find(whereConditions) // Optional: filter records
|
|
147
|
+
.select('field1', 'field2') // Optional: select specific fields
|
|
148
|
+
.orderBy('field', 'asc') // Optional: sort results
|
|
149
|
+
.limit(50) // Optional: limit results
|
|
150
|
+
.offset(10) // Optional: pagination
|
|
151
|
+
.query(); // Execute and return ReactiveQueryResult
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### CRUD Operations
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Create
|
|
158
|
+
await db.query.tableName.createRemote(data);
|
|
159
|
+
await db.query.tableName.createRemote([data1, data2]); // Batch
|
|
160
|
+
|
|
161
|
+
// Read (live)
|
|
162
|
+
const liveQuery = await db.query.tableName.find().query();
|
|
163
|
+
|
|
164
|
+
// Update
|
|
165
|
+
await db.query.tableName.updateRemote(recordId, updates);
|
|
166
|
+
|
|
167
|
+
// Delete
|
|
168
|
+
await db.query.tableName.deleteRemote(recordId);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Cleanup
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Always cleanup live queries when component unmounts
|
|
175
|
+
onCleanup(() => {
|
|
176
|
+
liveQuery.kill();
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## How It Works
|
|
181
|
+
|
|
182
|
+
1. **Query Creation**: When you call `.query()`, a live query is created on the remote server
|
|
183
|
+
2. **Initial Data**: Data is fetched from the local cache and displayed immediately
|
|
184
|
+
3. **Live Updates**: When data changes on the remote server:
|
|
185
|
+
- The syncer receives the change event
|
|
186
|
+
- Local cache is updated automatically
|
|
187
|
+
- Query re-hydrates from local cache
|
|
188
|
+
- UI updates reactively
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
User Action (createRemote)
|
|
192
|
+
↓
|
|
193
|
+
Remote Server Updates
|
|
194
|
+
↓
|
|
195
|
+
Live Query Event
|
|
196
|
+
↓
|
|
197
|
+
Syncer Updates Local Cache
|
|
198
|
+
↓
|
|
199
|
+
Query Re-hydrates
|
|
200
|
+
↓
|
|
201
|
+
UI Updates (via createEffect)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Key Concepts
|
|
205
|
+
|
|
206
|
+
### Remote vs Local Operations
|
|
207
|
+
|
|
208
|
+
- **`createRemote()`**: Creates on remote server, automatically syncs to local cache
|
|
209
|
+
- **`createLocal()`**: Creates only in local cache (rarely used)
|
|
210
|
+
- **Recommendation**: Always use `Remote` methods for real-time sync
|
|
211
|
+
|
|
212
|
+
### Query Deduplication
|
|
213
|
+
|
|
214
|
+
Multiple components with identical queries share the same remote subscription:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
// Component A
|
|
218
|
+
const query1 = await db.query.thread.find({ status: 'active' }).query();
|
|
219
|
+
|
|
220
|
+
// Component B (different location)
|
|
221
|
+
const query2 = await db.query.thread.find({ status: 'active' }).query();
|
|
222
|
+
|
|
223
|
+
// Result: Only ONE remote live query is created!
|
|
224
|
+
// Both components update simultaneously when data changes
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Reactive Updates
|
|
228
|
+
|
|
229
|
+
Use `createEffect` to reactively update when query data changes:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
const liveQuery = await db.query.thread.find().query();
|
|
233
|
+
|
|
234
|
+
// ✅ Good: Reactively updates
|
|
235
|
+
createEffect(() => {
|
|
236
|
+
setThreads([...liveQuery.data]); // Spread to trigger reactivity
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ❌ Bad: Won't update
|
|
240
|
+
const threads = liveQuery.data; // Static reference
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Common Patterns
|
|
244
|
+
|
|
245
|
+
### List View
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
function ListView() {
|
|
249
|
+
const [items, setItems] = createSignal([]);
|
|
250
|
+
|
|
251
|
+
onMount(async () => {
|
|
252
|
+
const liveQuery = await db.query.item
|
|
253
|
+
.find()
|
|
254
|
+
.orderBy("created_at", "desc")
|
|
255
|
+
.query();
|
|
256
|
+
|
|
257
|
+
createEffect(() => setItems([...liveQuery.data]));
|
|
258
|
+
onCleanup(() => liveQuery.kill());
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return <For each={items()}>{(item) => <ItemCard item={item} />}</For>;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Detail View
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
function DetailView(props: { id: string }) {
|
|
269
|
+
const [item, setItem] = createSignal(null);
|
|
270
|
+
|
|
271
|
+
onMount(async () => {
|
|
272
|
+
const liveQuery = await db.query.item
|
|
273
|
+
.find({ id: new RecordId("item", props.id) })
|
|
274
|
+
.query();
|
|
275
|
+
|
|
276
|
+
createEffect(() => setItem(liveQuery.data[0] || null));
|
|
277
|
+
onCleanup(() => liveQuery.kill());
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return <Show when={item()}>{(data) => <ItemDetail item={data()} />}</Show>;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Form Submission
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
async function handleSubmit(formData: FormData) {
|
|
288
|
+
try {
|
|
289
|
+
const [newItem] = await db.query.item.createRemote({
|
|
290
|
+
title: formData.title,
|
|
291
|
+
content: formData.content,
|
|
292
|
+
created_at: new Date(),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Navigate to detail page or show success
|
|
296
|
+
navigate(`/item/${newItem.id}`);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error('Failed to create item:', error);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Troubleshooting
|
|
304
|
+
|
|
305
|
+
### Data Not Updating
|
|
306
|
+
|
|
307
|
+
**Problem**: UI doesn't update when data changes
|
|
308
|
+
|
|
309
|
+
**Solution**: Make sure you're using `createEffect` and spreading the array
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// ✅ Correct
|
|
313
|
+
createEffect(() => {
|
|
314
|
+
setData([...liveQuery.data]); // Spread creates new reference
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ❌ Wrong
|
|
318
|
+
setData(liveQuery.data); // Same reference won't trigger updates
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Memory Leaks
|
|
322
|
+
|
|
323
|
+
**Problem**: Application gets slower over time
|
|
324
|
+
|
|
325
|
+
**Solution**: Always cleanup live queries
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
onMount(async () => {
|
|
329
|
+
const liveQuery = await db.query.item.find().query();
|
|
330
|
+
|
|
331
|
+
onCleanup(() => {
|
|
332
|
+
liveQuery.kill(); // Essential!
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### "No syncer available" Warning
|
|
338
|
+
|
|
339
|
+
**Problem**: Warning in console about missing syncer
|
|
340
|
+
|
|
341
|
+
**Solution**: Ensure `remoteUrl` is configured in your `SyncedDbConfig`
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
export const dbConfig: SyncedDbConfig<Schema> = {
|
|
345
|
+
// ... other config
|
|
346
|
+
remoteUrl: 'http://localhost:8000', // Add this!
|
|
347
|
+
};
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### TypeScript Errors
|
|
351
|
+
|
|
352
|
+
**Problem**: Type errors when accessing query results
|
|
353
|
+
|
|
354
|
+
**Solution**: Make sure your schema types are properly generated
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
# Generate schema types
|
|
358
|
+
pnpm run generate-schema
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Best Practices
|
|
362
|
+
|
|
363
|
+
1. **Always use `createRemote()`** for CRUD operations
|
|
364
|
+
2. **Always cleanup** with `liveQuery.kill()` in `onCleanup()`
|
|
365
|
+
3. **Use `createEffect()`** to reactively update signals
|
|
366
|
+
4. **Spread arrays** when setting state: `[...liveQuery.data]`
|
|
367
|
+
5. **Use specific queries** to benefit from deduplication
|
|
368
|
+
6. **Paginate large lists** with `.limit()` and `.offset()`
|
|
369
|
+
7. **Handle errors** in async operations
|
|
370
|
+
|
|
371
|
+
## Advanced Usage
|
|
372
|
+
|
|
373
|
+
### Custom Queries
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// Use custom SQL for complex queries
|
|
377
|
+
const result = await db.queryRemote(
|
|
378
|
+
`SELECT *, author.* FROM thread WHERE created_at > $timestamp`,
|
|
379
|
+
{ timestamp: new Date('2024-01-01') }
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const [threads] = await result.collect();
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Authentication
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// Authenticate with the database
|
|
389
|
+
await db.authenticate(jwtToken);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Multiple Conditions
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
const liveQuery = await db.query.thread
|
|
396
|
+
.find({ status: 'active' })
|
|
397
|
+
.where({ category: 'tech' }) // Add more conditions
|
|
398
|
+
.orderBy('votes', 'desc')
|
|
399
|
+
.limit(50)
|
|
400
|
+
.query();
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Next Steps
|
|
404
|
+
|
|
405
|
+
- Read the [Full Architecture Documentation](./LIVE_QUERY_ARCHITECTURE.md)
|
|
406
|
+
- Explore the example app in `/example/app-solid`
|
|
407
|
+
- Check out the SurrealDB documentation: https://surrealdb.com/docs
|
|
408
|
+
|
|
409
|
+
## Support
|
|
410
|
+
|
|
411
|
+
For issues or questions:
|
|
412
|
+
|
|
413
|
+
- Open an issue on GitHub
|
|
414
|
+
- Check the architecture documentation for detailed explanations
|
|
415
|
+
- Review the example app for implementation patterns
|