@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/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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"}