@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/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# db-solid
|
|
2
|
+
|
|
3
|
+
A SurrealDB client for Solid.js with automatic cache synchronization and live query support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Live Queries**: Real-time data synchronization from remote SurrealDB server
|
|
8
|
+
- **Local Cache**: Fast reads from local WASM SurrealDB instance
|
|
9
|
+
- **Automatic Sync**: Changes from remote automatically update local cache
|
|
10
|
+
- **Query Deduplication**: Multiple components share the same remote subscriptions
|
|
11
|
+
- **Type-Safe**: Full TypeScript support with generated schema types
|
|
12
|
+
- **Reactive**: Seamless integration with Solid.js reactivity
|
|
13
|
+
- **Offline Support**: Local cache works even when remote is temporarily unavailable
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add db-solid
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Basic Setup
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// db.ts
|
|
27
|
+
import { SyncedDb, type SyncedDbConfig } from 'db-solid';
|
|
28
|
+
import { type Schema, SURQL_SCHEMA } from './schema.gen';
|
|
29
|
+
|
|
30
|
+
export const dbConfig: SyncedDbConfig<Schema> = {
|
|
31
|
+
schema: SURQL_SCHEMA,
|
|
32
|
+
localDbName: 'my-app-local',
|
|
33
|
+
internalDbName: 'syncdb-int',
|
|
34
|
+
storageStrategy: 'indexeddb',
|
|
35
|
+
namespace: 'main',
|
|
36
|
+
database: 'my_db',
|
|
37
|
+
remoteUrl: 'http://localhost:8000',
|
|
38
|
+
tables: ['user', 'thread', 'comment'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const db = new SyncedDb<Schema>(dbConfig);
|
|
42
|
+
|
|
43
|
+
export async function initDatabase() {
|
|
44
|
+
await db.init();
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Usage in Components
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { db } from "./db";
|
|
52
|
+
import { createSignal, createEffect, onMount, onCleanup, For } from "solid-js";
|
|
53
|
+
|
|
54
|
+
function ThreadList() {
|
|
55
|
+
const [threads, setThreads] = createSignal([]);
|
|
56
|
+
|
|
57
|
+
onMount(async () => {
|
|
58
|
+
// Create live query
|
|
59
|
+
const liveQuery = await db.query.thread
|
|
60
|
+
.find({})
|
|
61
|
+
.orderBy("created_at", "desc")
|
|
62
|
+
.query();
|
|
63
|
+
|
|
64
|
+
// React to changes
|
|
65
|
+
createEffect(() => {
|
|
66
|
+
setThreads([...liveQuery.data]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Cleanup on unmount
|
|
70
|
+
onCleanup(() => {
|
|
71
|
+
liveQuery.kill();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return <For each={threads()}>{(thread) => <div>{thread.title}</div>}</For>;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Creating Records
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Creates on remote server, automatically syncs to local cache and updates UI
|
|
83
|
+
await db.query.thread.createRemote({
|
|
84
|
+
title: 'New Thread',
|
|
85
|
+
content: 'Thread content',
|
|
86
|
+
author: userId,
|
|
87
|
+
created_at: new Date(),
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How It Works
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
95
|
+
│ Your Application │
|
|
96
|
+
│ Multiple components can query the same data │
|
|
97
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
98
|
+
│
|
|
99
|
+
▼
|
|
100
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
101
|
+
│ Query Deduplication │
|
|
102
|
+
│ Identical queries share a single remote subscription │
|
|
103
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
104
|
+
│
|
|
105
|
+
▼
|
|
106
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
107
|
+
│ Remote SurrealDB Server │
|
|
108
|
+
│ Live queries watch for data changes │
|
|
109
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
110
|
+
│
|
|
111
|
+
▼
|
|
112
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
113
|
+
│ Syncer (Cache Manager) │
|
|
114
|
+
│ Receives changes and updates local cache │
|
|
115
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
116
|
+
│
|
|
117
|
+
▼
|
|
118
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
119
|
+
│ Local Cache (WASM) │
|
|
120
|
+
│ Fast reads, automatic synchronization │
|
|
121
|
+
└────────────────────────┬─────────────────────────────────────┘
|
|
122
|
+
│
|
|
123
|
+
▼
|
|
124
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
125
|
+
│ UI Updates (Solid.js) │
|
|
126
|
+
│ Components re-render with new data │
|
|
127
|
+
└─────────────────────────────────────────────────────────────┘
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## API Overview
|
|
131
|
+
|
|
132
|
+
### Query Builder
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
db.query.tableName
|
|
136
|
+
.find({ status: 'active' }) // Filter records
|
|
137
|
+
.select('field1', 'field2') // Select fields (optional)
|
|
138
|
+
.orderBy('created_at', 'desc') // Sort results
|
|
139
|
+
.limit(50) // Limit results
|
|
140
|
+
.offset(10) // Pagination
|
|
141
|
+
.query(); // Execute and return ReactiveQueryResult
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### CRUD Operations
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Create
|
|
148
|
+
await db.query.thread.createRemote({ title: 'Hello', content: 'World' });
|
|
149
|
+
|
|
150
|
+
// Read (with live updates)
|
|
151
|
+
const liveQuery = await db.query.thread.find().query();
|
|
152
|
+
|
|
153
|
+
// Update
|
|
154
|
+
await db.query.thread.updateRemote(recordId, { title: 'Updated' });
|
|
155
|
+
|
|
156
|
+
// Delete
|
|
157
|
+
await db.query.thread.deleteRemote(recordId);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Reactive Updates
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const liveQuery = await db.query.thread.find().query();
|
|
164
|
+
|
|
165
|
+
createEffect(() => {
|
|
166
|
+
// Spread to create new reference and trigger reactivity
|
|
167
|
+
setThreads([...liveQuery.data]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
onCleanup(() => {
|
|
171
|
+
liveQuery.kill(); // Always cleanup!
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Key Features
|
|
176
|
+
|
|
177
|
+
### Query Deduplication
|
|
178
|
+
|
|
179
|
+
Multiple components with identical queries automatically share a single remote subscription:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// Component A
|
|
183
|
+
const query1 = await db.query.thread.find({ status: 'active' }).query();
|
|
184
|
+
|
|
185
|
+
// Component B (elsewhere in your app)
|
|
186
|
+
const query2 = await db.query.thread.find({ status: 'active' }).query();
|
|
187
|
+
|
|
188
|
+
// ✨ Only ONE remote subscription is created!
|
|
189
|
+
// Both components update simultaneously when data changes
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Automatic Cache Synchronization
|
|
193
|
+
|
|
194
|
+
When you create, update, or delete records on the remote server:
|
|
195
|
+
|
|
196
|
+
1. Change is made on remote SurrealDB
|
|
197
|
+
2. Live query detects the change
|
|
198
|
+
3. Syncer updates local cache automatically
|
|
199
|
+
4. All affected queries re-hydrate from cache
|
|
200
|
+
5. UI updates reactively
|
|
201
|
+
|
|
202
|
+
### Type Safety
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// Full TypeScript support
|
|
206
|
+
const [thread] = await db.query.thread.createRemote({
|
|
207
|
+
title: 'Hello', // ✅ Type-checked
|
|
208
|
+
content: 'World', // ✅ Type-checked
|
|
209
|
+
invalidField: 'oops', // ❌ TypeScript error!
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Autocomplete works everywhere
|
|
213
|
+
const liveQuery = await db.query.thread
|
|
214
|
+
.find({ status: 'active' }) // ✅ Status field is type-checked
|
|
215
|
+
.orderBy('created_at', 'desc'); // ✅ Field names autocompleted
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Documentation
|
|
219
|
+
|
|
220
|
+
- **[Quick Start Guide](./QUICK_START.md)**: Get up and running quickly
|
|
221
|
+
- **[Architecture Documentation](./LIVE_QUERY_ARCHITECTURE.md)**: Deep dive into how it works
|
|
222
|
+
- **[Example App](../../example/app-solid)**: Full example application
|
|
223
|
+
|
|
224
|
+
## Example
|
|
225
|
+
|
|
226
|
+
See the complete example application in [`/example/app-solid`](../../example/app-solid) demonstrating:
|
|
227
|
+
|
|
228
|
+
- User authentication
|
|
229
|
+
- Thread creation and listing
|
|
230
|
+
- Comments with live updates
|
|
231
|
+
- Real-time synchronization
|
|
232
|
+
- Proper cleanup and error handling
|
|
233
|
+
|
|
234
|
+
## Performance
|
|
235
|
+
|
|
236
|
+
### Query Deduplication Benefits
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
Without Deduplication:
|
|
240
|
+
- 100 components with same query = 100 WebSocket subscriptions
|
|
241
|
+
- Each update processed 100 times
|
|
242
|
+
|
|
243
|
+
With Deduplication:
|
|
244
|
+
- 100 components with same query = 1 WebSocket subscription
|
|
245
|
+
- Each update processed once
|
|
246
|
+
- Savings: 99% reduction in network traffic and CPU usage
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Memory Usage
|
|
250
|
+
|
|
251
|
+
Very efficient memory footprint:
|
|
252
|
+
|
|
253
|
+
- Per unique query: ~1-2 KB
|
|
254
|
+
- Local cache: Shared across all queries
|
|
255
|
+
- Typical app (10 unique queries): ~20-30 KB
|
|
256
|
+
|
|
257
|
+
## Best Practices
|
|
258
|
+
|
|
259
|
+
1. **Always cleanup**: Call `liveQuery.kill()` in `onCleanup()`
|
|
260
|
+
2. **Use `createRemote()`**: For CRUD operations to ensure sync
|
|
261
|
+
3. **Spread arrays**: `[...liveQuery.data]` to trigger reactivity
|
|
262
|
+
4. **Use `createEffect()`**: To reactively update signals
|
|
263
|
+
5. **Paginate large lists**: Use `.limit()` and `.offset()`
|
|
264
|
+
6. **Handle errors**: Wrap async operations in try-catch
|
|
265
|
+
|
|
266
|
+
## Troubleshooting
|
|
267
|
+
|
|
268
|
+
### Data Not Updating
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// ❌ Wrong - won't trigger updates
|
|
272
|
+
const threads = liveQuery.data;
|
|
273
|
+
|
|
274
|
+
// ✅ Correct - creates new reference
|
|
275
|
+
createEffect(() => {
|
|
276
|
+
setThreads([...liveQuery.data]);
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Memory Leaks
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// ❌ Wrong - memory leak!
|
|
284
|
+
onMount(async () => {
|
|
285
|
+
const liveQuery = await db.query.thread.find().query();
|
|
286
|
+
// Missing cleanup
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ✅ Correct - properly cleaned up
|
|
290
|
+
onMount(async () => {
|
|
291
|
+
const liveQuery = await db.query.thread.find().query();
|
|
292
|
+
|
|
293
|
+
onCleanup(() => {
|
|
294
|
+
liveQuery.kill(); // Essential!
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### "No syncer available" Warning
|
|
300
|
+
|
|
301
|
+
Make sure you have `remoteUrl` in your config:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
export const dbConfig: SyncedDbConfig<Schema> = {
|
|
305
|
+
// ... other config
|
|
306
|
+
remoteUrl: 'http://localhost:8000', // Don't forget this!
|
|
307
|
+
};
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Requirements
|
|
311
|
+
|
|
312
|
+
- Solid.js 1.8+
|
|
313
|
+
- SurrealDB 2.0+
|
|
314
|
+
- Modern browser with IndexedDB support
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT
|
|
319
|
+
|
|
320
|
+
## Contributing
|
|
321
|
+
|
|
322
|
+
Contributions are welcome! Please read the architecture documentation to understand how the system works before making changes.
|
|
323
|
+
|
|
324
|
+
## Acknowledgments
|
|
325
|
+
|
|
326
|
+
Built with:
|
|
327
|
+
|
|
328
|
+
- [SurrealDB](https://surrealdb.com/) - The ultimate database
|
|
329
|
+
- [Solid.js](https://www.solidjs.com/) - Simple and performant reactivity
|
|
330
|
+
- [Valtio](https://github.com/pmndrs/valtio) - Proxy-based state management
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let _spooky_sync_core = require("@spooky-sync/core");
|
|
3
|
+
let surrealdb = require("surrealdb");
|
|
4
|
+
let solid_js = require("solid-js");
|
|
5
|
+
|
|
6
|
+
//#region src/lib/context.ts
|
|
7
|
+
const SpookyContext = (0, solid_js.createContext)();
|
|
8
|
+
function useDb() {
|
|
9
|
+
const db = (0, solid_js.useContext)(SpookyContext);
|
|
10
|
+
if (!db) throw new Error("useDb must be used within a <SpookyProvider>. Wrap your app in <SpookyProvider config={...}>.");
|
|
11
|
+
return db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/lib/use-query.ts
|
|
16
|
+
function useQuery(dbOrQuery, queryOrOptions, maybeOptions) {
|
|
17
|
+
let db;
|
|
18
|
+
let finalQuery;
|
|
19
|
+
let options;
|
|
20
|
+
if (dbOrQuery instanceof SyncedDb) {
|
|
21
|
+
db = dbOrQuery;
|
|
22
|
+
finalQuery = queryOrOptions;
|
|
23
|
+
options = maybeOptions;
|
|
24
|
+
} else {
|
|
25
|
+
const contextDb = (0, solid_js.useContext)(SpookyContext);
|
|
26
|
+
if (!contextDb) throw new Error("useQuery: No db argument provided and no SpookyContext found. Either pass a SyncedDb instance or wrap your app in <SpookyProvider>.");
|
|
27
|
+
db = contextDb;
|
|
28
|
+
finalQuery = dbOrQuery;
|
|
29
|
+
options = queryOrOptions;
|
|
30
|
+
}
|
|
31
|
+
const [data, setData] = (0, solid_js.createSignal)(void 0);
|
|
32
|
+
const [error, setError] = (0, solid_js.createSignal)(void 0);
|
|
33
|
+
const [isFetched, setIsFetched] = (0, solid_js.createSignal)(false);
|
|
34
|
+
const [unsubscribe, setUnsubscribe] = (0, solid_js.createSignal)(void 0);
|
|
35
|
+
let prevQueryString;
|
|
36
|
+
const spooky = db.getSpooky();
|
|
37
|
+
const initQuery = async (query) => {
|
|
38
|
+
const { hash } = await query.run();
|
|
39
|
+
setError(void 0);
|
|
40
|
+
const unsub = await spooky.subscribe(hash, (e) => {
|
|
41
|
+
const data = query.isOne ? e[0] : e;
|
|
42
|
+
setData(() => data);
|
|
43
|
+
setIsFetched(true);
|
|
44
|
+
}, { immediate: true });
|
|
45
|
+
setUnsubscribe(() => unsub);
|
|
46
|
+
};
|
|
47
|
+
(0, solid_js.createEffect)(() => {
|
|
48
|
+
if (!(options?.enabled?.() ?? true)) {
|
|
49
|
+
setError(void 0);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const query = typeof finalQuery === "function" ? finalQuery() : finalQuery;
|
|
53
|
+
if (!query) return;
|
|
54
|
+
const queryString = JSON.stringify(query);
|
|
55
|
+
if (queryString === prevQueryString) return;
|
|
56
|
+
prevQueryString = queryString;
|
|
57
|
+
setIsFetched(false);
|
|
58
|
+
initQuery(query);
|
|
59
|
+
(0, solid_js.onCleanup)(() => {
|
|
60
|
+
unsubscribe?.();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
const isLoading = () => {
|
|
64
|
+
return !isFetched() && error() === void 0;
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
data,
|
|
68
|
+
error,
|
|
69
|
+
isLoading
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/lib/SpookyProvider.ts
|
|
75
|
+
function SpookyProvider(props) {
|
|
76
|
+
const merged = (0, solid_js.mergeProps)({ fallback: void 0 }, props);
|
|
77
|
+
const [db, setDb] = (0, solid_js.createSignal)(void 0);
|
|
78
|
+
(0, solid_js.onMount)(async () => {
|
|
79
|
+
try {
|
|
80
|
+
const instance = new SyncedDb(merged.config);
|
|
81
|
+
await instance.init();
|
|
82
|
+
setDb(() => instance);
|
|
83
|
+
merged.onReady?.(instance);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
86
|
+
if (merged.onError) merged.onError(error);
|
|
87
|
+
else console.error("SpookyProvider: Failed to initialize database", error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return (0, solid_js.createMemo)(() => {
|
|
91
|
+
const instance = db();
|
|
92
|
+
if (!instance) return merged.fallback;
|
|
93
|
+
return (0, solid_js.createComponent)(SpookyContext.Provider, {
|
|
94
|
+
value: instance,
|
|
95
|
+
get children() {
|
|
96
|
+
return merged.children;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/index.ts
|
|
104
|
+
/**
|
|
105
|
+
* SyncedDb - A thin wrapper around spooky-ts for Solid.js integration
|
|
106
|
+
* Delegates all logic to the underlying spooky-ts instance
|
|
107
|
+
*/
|
|
108
|
+
var SyncedDb = class {
|
|
109
|
+
constructor(config) {
|
|
110
|
+
this.spooky = null;
|
|
111
|
+
this._initialized = false;
|
|
112
|
+
this.config = config;
|
|
113
|
+
}
|
|
114
|
+
getSpooky() {
|
|
115
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
116
|
+
return this.spooky;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Initialize the spooky-ts instance
|
|
120
|
+
*/
|
|
121
|
+
async init() {
|
|
122
|
+
if (this._initialized) return;
|
|
123
|
+
this.spooky = new _spooky_sync_core.SpookyClient(this.config);
|
|
124
|
+
await this.spooky.init();
|
|
125
|
+
this._initialized = true;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Create a new record in the database
|
|
129
|
+
*/
|
|
130
|
+
async create(id, payload) {
|
|
131
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
132
|
+
await this.spooky.create(id, payload);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Update an existing record in the database
|
|
136
|
+
*/
|
|
137
|
+
async update(tableName, recordId, payload, options) {
|
|
138
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
139
|
+
await this.spooky.update(tableName, recordId, payload, options);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Delete an existing record in the database
|
|
143
|
+
*/
|
|
144
|
+
async delete(tableName, selector) {
|
|
145
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
146
|
+
if (typeof selector !== "string") throw new Error("Only string ID selectors are supported currently with core");
|
|
147
|
+
await this.spooky.delete(tableName, selector);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Query data from the database
|
|
151
|
+
*/
|
|
152
|
+
query(table) {
|
|
153
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
154
|
+
return this.spooky.query(table, {});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Run a backend operation
|
|
158
|
+
*/
|
|
159
|
+
async run(backend, path, payload, options) {
|
|
160
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
161
|
+
await this.spooky.run(backend, path, payload, options);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Authenticate with the database
|
|
165
|
+
*/
|
|
166
|
+
async authenticate(token) {
|
|
167
|
+
await this.spooky?.authenticate(token);
|
|
168
|
+
return new surrealdb.RecordId("user", "me");
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Deauthenticate from the database
|
|
172
|
+
* @deprecated Use signOut() instead
|
|
173
|
+
*/
|
|
174
|
+
async deauthenticate() {
|
|
175
|
+
await this.signOut();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Sign out, clear session and local storage
|
|
179
|
+
*/
|
|
180
|
+
async signOut() {
|
|
181
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
182
|
+
await this.spooky.auth.signOut();
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Execute a function with direct access to the remote database connection
|
|
186
|
+
*/
|
|
187
|
+
async useRemote(fn) {
|
|
188
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
189
|
+
return await this.spooky.useRemote(fn);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Access the remote database service directly
|
|
193
|
+
*/
|
|
194
|
+
get remote() {
|
|
195
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
196
|
+
return this.spooky.remoteClient;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Access the local database service directly
|
|
200
|
+
*/
|
|
201
|
+
get local() {
|
|
202
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
203
|
+
return this.spooky.localClient;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Access the auth service
|
|
207
|
+
*/
|
|
208
|
+
get auth() {
|
|
209
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
210
|
+
return this.spooky.auth;
|
|
211
|
+
}
|
|
212
|
+
get pendingMutationCount() {
|
|
213
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
214
|
+
return this.spooky.pendingMutationCount;
|
|
215
|
+
}
|
|
216
|
+
subscribeToPendingMutations(cb) {
|
|
217
|
+
if (!this.spooky) throw new Error("SyncedDb not initialized");
|
|
218
|
+
return this.spooky.subscribeToPendingMutations(cb);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
223
|
+
exports.RecordId = surrealdb.RecordId;
|
|
224
|
+
exports.SpookyProvider = SpookyProvider;
|
|
225
|
+
exports.SyncedDb = SyncedDb;
|
|
226
|
+
exports.Uuid = surrealdb.Uuid;
|
|
227
|
+
exports.useDb = useDb;
|
|
228
|
+
exports.useQuery = useQuery;
|
|
229
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["SpookyClient","RecordId"],"sources":["../src/lib/context.ts","../src/lib/use-query.ts","../src/lib/SpookyProvider.ts","../src/index.ts"],"sourcesContent":["import { createContext, useContext } from 'solid-js';\nimport type { SchemaStructure } from '@spooky/query-builder';\nimport type { SyncedDb } from '../index';\n\nexport const SpookyContext = createContext<SyncedDb<any> | undefined>();\n\nexport function useDb<S extends SchemaStructure>(): SyncedDb<S> {\n const db = useContext(SpookyContext);\n if (!db) {\n throw new Error('useDb must be used within a <SpookyProvider>. Wrap your app in <SpookyProvider config={...}>.');\n }\n return db as SyncedDb<S>;\n}\n","import {\n ColumnSchema,\n FinalQuery,\n SchemaStructure,\n TableNames,\n QueryResult,\n} from '@spooky-sync/query-builder';\nimport { createEffect, createSignal, onCleanup, useContext } from 'solid-js';\nimport { SyncedDb } from '..';\nimport { SpookyQueryResultPromise } from '@spooky-sync/core';\nimport { SpookyContext } from './context';\n\ntype QueryArg<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n> =\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, SpookyQueryResultPromise>\n | (() =>\n | FinalQuery<S, TableName, T, RelatedFields, IsOne, SpookyQueryResultPromise>\n | null\n | undefined);\n\ntype QueryOptions = { enabled?: () => boolean };\n\n// Overload: context-based (no explicit db)\nexport function useQuery<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,\n>(\n finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>,\n options?: QueryOptions,\n): { data: () => TData | undefined; error: () => Error | undefined; isLoading: () => boolean };\n\n// Overload: explicit db (backward-compatible)\nexport function useQuery<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends { columns: Record<string, ColumnSchema> },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,\n>(\n db: SyncedDb<S>,\n finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>,\n options?: QueryOptions,\n): { data: () => TData | undefined; error: () => Error | undefined; isLoading: () => boolean };\n\n// Implementation\nexport function useQuery<\n S extends SchemaStructure,\n TableName extends TableNames<S>,\n T extends {\n columns: Record<string, ColumnSchema>;\n },\n RelatedFields extends Record<string, any>,\n IsOne extends boolean,\n TData = QueryResult<S, TableName, RelatedFields, IsOne> | null,\n>(\n dbOrQuery:\n | SyncedDb<S>\n | QueryArg<S, TableName, T, RelatedFields, IsOne>,\n queryOrOptions?:\n | QueryArg<S, TableName, T, RelatedFields, IsOne>\n | QueryOptions,\n maybeOptions?: QueryOptions,\n) {\n let db: SyncedDb<S>;\n let finalQuery: QueryArg<S, TableName, T, RelatedFields, IsOne>;\n let options: QueryOptions | undefined;\n\n if (dbOrQuery instanceof SyncedDb) {\n // Explicit db overload: useQuery(db, query, options?)\n db = dbOrQuery;\n finalQuery = queryOrOptions as QueryArg<S, TableName, T, RelatedFields, IsOne>;\n options = maybeOptions;\n } else {\n // Context-based overload: useQuery(query, options?)\n const contextDb = useContext(SpookyContext);\n if (!contextDb) {\n throw new Error(\n 'useQuery: No db argument provided and no SpookyContext found. ' +\n 'Either pass a SyncedDb instance or wrap your app in <SpookyProvider>.'\n );\n }\n db = contextDb as SyncedDb<S>;\n finalQuery = dbOrQuery;\n options = queryOrOptions as QueryOptions | undefined;\n }\n\n const [data, setData] = createSignal<TData | undefined>(undefined);\n const [error, setError] = createSignal<Error | undefined>(undefined);\n const [isFetched, setIsFetched] = createSignal(false);\n const [unsubscribe, setUnsubscribe] = createSignal<(() => void) | undefined>(undefined);\n let prevQueryString: string | undefined;\n\n const spooky = db.getSpooky();\n\n const initQuery = async (\n query: FinalQuery<S, TableName, T, RelatedFields, IsOne, SpookyQueryResultPromise>\n ) => {\n const { hash } = await query.run();\n setError(undefined);\n\n const unsub = await spooky.subscribe(\n hash,\n (e) => {\n const data = (query.isOne ? e[0] : e) as TData;\n setData(() => data);\n setIsFetched(true);\n },\n { immediate: true }\n );\n\n setUnsubscribe(() => unsub);\n };\n\n createEffect(() => {\n const enabled = options?.enabled?.() ?? true;\n\n // If disabled, clear error and don't run query\n if (!enabled) {\n setError(undefined);\n return;\n }\n\n // Init Query\n const query = typeof finalQuery === 'function' ? finalQuery() : finalQuery;\n if (!query) {\n return;\n }\n\n // Prevent re-running if query hasn't changed\n const queryString = JSON.stringify(query);\n if (queryString === prevQueryString) {\n return;\n }\n prevQueryString = queryString;\n\n // Reset fetched state when query changes\n setIsFetched(false);\n initQuery(query);\n\n // Cleanup\n onCleanup(() => {\n unsubscribe?.();\n });\n });\n\n const isLoading = () => {\n return !isFetched() && error() === undefined;\n };\n\n return {\n data,\n error,\n isLoading,\n };\n}\n","import { createSignal, onMount, createComponent, createMemo, JSX, mergeProps } from 'solid-js';\nimport type { SchemaStructure } from '@spooky/query-builder';\nimport type { SyncedDbConfig } from '../types';\nimport { SyncedDb } from '../index';\nimport { SpookyContext } from './context';\n\nexport interface SpookyProviderProps<S extends SchemaStructure> {\n config: SyncedDbConfig<S>;\n fallback?: JSX.Element;\n onError?: (error: Error) => void;\n onReady?: (db: SyncedDb<S>) => void;\n children: JSX.Element;\n}\n\nexport function SpookyProvider<S extends SchemaStructure>(\n props: SpookyProviderProps<S>\n): JSX.Element {\n const merged = mergeProps(\n {\n fallback: undefined as JSX.Element | undefined,\n },\n props\n );\n\n const [db, setDb] = createSignal<SyncedDb<S> | undefined>(undefined);\n\n onMount(async () => {\n try {\n const instance = new SyncedDb<S>(merged.config);\n await instance.init();\n setDb(() => instance);\n merged.onReady?.(instance);\n } catch (e) {\n const error = e instanceof Error ? e : new Error(String(e));\n if (merged.onError) {\n merged.onError(error);\n } else {\n console.error('SpookyProvider: Failed to initialize database', error);\n }\n }\n });\n\n const content = createMemo(() => {\n const instance = db();\n if (!instance) return merged.fallback;\n return createComponent(SpookyContext.Provider, {\n value: instance,\n get children() {\n return merged.children;\n },\n });\n });\n\n return content as unknown as JSX.Element;\n}\n","import type { SyncedDbConfig } from './types';\nimport {\n SpookyClient,\n AuthService,\n type SpookyQueryResultPromise,\n UpdateOptions,\n RunOptions,\n} from '@spooky-sync/core';\n\nimport {\n GetTable,\n QueryBuilder,\n SchemaStructure,\n TableModel,\n TableNames,\n QueryResult,\n RelatedFieldsMap,\n RelationshipFieldsFromSchema,\n GetRelationship,\n RelatedFieldMapEntry,\n InnerQuery,\n BackendNames,\n BackendRoutes,\n RoutePayload,\n} from '@spooky-sync/query-builder';\n\nimport { RecordId, Uuid, Surreal } from 'surrealdb';\nexport { RecordId, Uuid };\nexport type { Model, GenericModel, GenericSchema, ModelPayload } from './lib/models';\nexport { useQuery } from './lib/use-query';\nexport { SpookyProvider, type SpookyProviderProps } from './lib/SpookyProvider';\nexport { useDb } from './lib/context';\n\n// export { AuthEventTypes } from \"@spooky-sync/core\"; // TODO: Verify if AuthEventTypes exists in core\nexport type {};\n\n// Re-export query builder types for convenience\nexport type {\n QueryModifier,\n QueryModifierBuilder,\n QueryInfo,\n RelationshipsMetadata,\n RelationshipDefinition,\n InferRelatedModelFromMetadata,\n GetCardinality,\n GetTable,\n TableModel,\n TableNames,\n QueryResult,\n} from '@spooky-sync/query-builder';\n\nexport type RelationshipField<\n Schema extends SchemaStructure,\n TableName extends TableNames<Schema>,\n Field extends RelationshipFieldsFromSchema<Schema, TableName>,\n> = GetRelationship<Schema, TableName, Field>;\n\nexport type RelatedFieldsTableScoped<\n Schema extends SchemaStructure,\n TableName extends TableNames<Schema>,\n RelatedFields extends RelationshipFieldsFromSchema<Schema, TableName> =\n RelationshipFieldsFromSchema<Schema, TableName>,\n> = {\n [K in RelatedFields]: {\n to: RelationshipField<Schema, TableName, K>['to'];\n relatedFields: RelatedFieldsMap;\n cardinality: RelationshipField<Schema, TableName, K>['cardinality'];\n };\n};\n\nexport type InferModel<\n Schema extends SchemaStructure,\n TableName extends TableNames<Schema>,\n RelatedFields extends RelatedFieldsTableScoped<Schema, TableName>,\n> = QueryResult<Schema, TableName, RelatedFields, true>;\n\nexport type WithRelated<Field extends string, RelatedFields extends RelatedFieldsMap = {}> = {\n [K in Field]: Omit<RelatedFieldMapEntry, 'relatedFields'> & {\n relatedFields: RelatedFields;\n };\n};\n\nexport type WithRelatedMany<Field extends string, RelatedFields extends RelatedFieldsMap = {}> = {\n [K in Field]: {\n to: Field;\n relatedFields: RelatedFields;\n cardinality: 'many';\n };\n};\n\n/**\n * SyncedDb - A thin wrapper around spooky-ts for Solid.js integration\n * Delegates all logic to the underlying spooky-ts instance\n */\nexport class SyncedDb<S extends SchemaStructure> {\n private config: SyncedDbConfig<S>;\n private spooky: SpookyClient<S> | null = null;\n private _initialized = false;\n\n constructor(config: SyncedDbConfig<S>) {\n this.config = config;\n }\n\n public getSpooky(): SpookyClient<S> {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n return this.spooky;\n }\n\n /**\n * Initialize the spooky-ts instance\n */\n async init(): Promise<void> {\n if (this._initialized) return;\n this.spooky = new SpookyClient<S>(this.config);\n await this.spooky.init();\n this._initialized = true;\n }\n\n /**\n * Create a new record in the database\n */\n async create(id: string, payload: Record<string, unknown>): Promise<void> {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n await this.spooky.create(id, payload as Record<string, unknown>);\n }\n\n /**\n * Update an existing record in the database\n */\n async update<TName extends TableNames<S>>(\n tableName: TName,\n recordId: string,\n payload: Partial<TableModel<GetTable<S, TName>>>,\n options?: UpdateOptions\n ): Promise<void> {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n await this.spooky.update(\n tableName as string,\n recordId,\n payload as Record<string, unknown>,\n options\n );\n }\n\n /**\n * Delete an existing record in the database\n */\n async delete<TName extends TableNames<S>>(\n tableName: TName,\n selector: string | InnerQuery<GetTable<S, TName>, boolean>\n ): Promise<void> {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n if (typeof selector !== 'string')\n throw new Error('Only string ID selectors are supported currently with core');\n await this.spooky.delete(tableName as string, selector);\n }\n\n /**\n * Query data from the database\n */\n public query<TName extends TableNames<S>>(\n table: TName\n ): QueryBuilder<S, TName, SpookyQueryResultPromise, {}, false> {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n return this.spooky.query(table, {});\n }\n\n /**\n * Run a backend operation\n */\n public async run<\n B extends BackendNames<S>,\n R extends BackendRoutes<S, B>,\n >(\n backend: B,\n path: R,\n payload: RoutePayload<S, B, R>,\n options?: RunOptions,\n ): Promise<void> {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n await this.spooky.run(backend, path, payload, options);\n }\n\n /**\n * Authenticate with the database\n */\n public async authenticate(token: string): Promise<RecordId<string>> {\n const result = await this.spooky?.authenticate(token);\n // SpookyClient.authenticate returns whatever remote.authenticate returns (boolean or token usually?)\n // Wait, checked SpookyClient: return this.remote.getClient().authenticate(token);\n // SurrealDB authenticate returns void? or token?\n // Assuming void or token.\n return new RecordId('user', 'me'); // Placeholder or actual?\n }\n\n /**\n * Deauthenticate from the database\n * @deprecated Use signOut() instead\n */\n public async deauthenticate(): Promise<void> {\n await this.signOut();\n }\n\n /**\n * Sign out, clear session and local storage\n */\n public async signOut(): Promise<void> {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n await this.spooky.auth.signOut();\n }\n\n /**\n * Execute a function with direct access to the remote database connection\n */\n public async useRemote<T>(fn: (db: Surreal) => T | Promise<T>): Promise<T> {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n return await this.spooky.useRemote(fn);\n }\n /**\n * Access the remote database service directly\n */\n get remote(): SpookyClient<S>['remoteClient'] {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n return this.spooky.remoteClient;\n }\n\n /**\n * Access the local database service directly\n */\n get local(): SpookyClient<S>['localClient'] {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n return this.spooky.localClient;\n }\n\n /**\n * Access the auth service\n */\n get auth(): AuthService<S> {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n return this.spooky.auth;\n }\n\n get pendingMutationCount(): number {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n return this.spooky.pendingMutationCount;\n }\n\n subscribeToPendingMutations(cb: (count: number) => void): () => void {\n if (!this.spooky) throw new Error('SyncedDb not initialized');\n return this.spooky.subscribeToPendingMutations(cb);\n }\n}\n\nexport * from './types';\n"],"mappings":"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"}
|