@spooky-sync/client-solid 0.0.1-canary.16 → 0.0.1-canary.160

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.
@@ -0,0 +1,112 @@
1
+ # File Hooks Reference
2
+
3
+ ## useFileUpload
4
+
5
+ Upload, download, and manage files in a SurrealDB bucket.
6
+
7
+ ### Signatures
8
+
9
+ ```typescript
10
+ // Context-based (inside Sp00kyProvider)
11
+ useFileUpload<S>(bucketName: BucketNames<S>): FileUploadResult;
12
+
13
+ // Explicit db
14
+ useFileUpload<S>(db: SyncedDb<S>, bucketName: BucketNames<S>): FileUploadResult;
15
+ ```
16
+
17
+ ### Return Value
18
+
19
+ ```typescript
20
+ interface FileUploadResult {
21
+ isUploading: () => boolean;
22
+ error: () => Error | null;
23
+ clearError: () => void;
24
+ upload: (path: string, file: File | Blob) => Promise<void>;
25
+ download: (path: string) => Promise<string | null>; // Returns object URL
26
+ remove: (path: string) => Promise<void>;
27
+ exists: (path: string) => Promise<boolean>;
28
+ }
29
+ ```
30
+
31
+ ### Validation
32
+
33
+ If the bucket has `maxSize` or `allowedExtensions` configured in the schema, the hook validates files before upload and sets `error()` on failure.
34
+
35
+ ### Example
36
+
37
+ ```tsx
38
+ function AvatarUpload() {
39
+ const { upload, isUploading, error, clearError } = useFileUpload('avatars');
40
+
41
+ const handleFile = async (e: Event) => {
42
+ const file = (e.target as HTMLInputElement).files?.[0];
43
+ if (file) {
44
+ await upload(`user/${userId()}/avatar.png`, file);
45
+ }
46
+ };
47
+
48
+ return (
49
+ <div>
50
+ <input type="file" onChange={handleFile} disabled={isUploading()} />
51
+ <Show when={error()}>
52
+ <p class="error">{error()!.message}</p>
53
+ <button onClick={clearError}>Dismiss</button>
54
+ </Show>
55
+ </div>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ## useDownloadFile
61
+
62
+ Reactively download a file from a bucket. Re-fetches when the path changes.
63
+
64
+ ### Signatures
65
+
66
+ ```typescript
67
+ // Context-based
68
+ useDownloadFile<S>(
69
+ bucketName: BucketNames<S>,
70
+ path: Accessor<string | null | undefined>,
71
+ options?: { cache?: boolean },
72
+ ): UseDownloadFileResult;
73
+
74
+ // Explicit db
75
+ useDownloadFile<S>(
76
+ db: SyncedDb<S>,
77
+ bucketName: BucketNames<S>,
78
+ path: Accessor<string | null | undefined>,
79
+ options?: { cache?: boolean },
80
+ ): UseDownloadFileResult;
81
+ ```
82
+
83
+ ### Return Value
84
+
85
+ ```typescript
86
+ interface UseDownloadFileResult {
87
+ url: Accessor<string | null>; // Object URL for the file
88
+ isLoading: Accessor<boolean>;
89
+ error: Accessor<Error | null>;
90
+ refetch: () => void; // Force re-download (evicts cache)
91
+ }
92
+ ```
93
+
94
+ ### Caching
95
+
96
+ By default, downloads are cached by `bucket:path` key with reference counting. Object URLs are revoked when no component references them. Set `cache: false` to disable.
97
+
98
+ ### Example
99
+
100
+ ```tsx
101
+ function Avatar(props: { path: string | null }) {
102
+ const { url, isLoading } = useDownloadFile('avatars', () => props.path);
103
+
104
+ return (
105
+ <Show when={!isLoading()} fallback={<Spinner />}>
106
+ <Show when={url()}>
107
+ <img src={url()!} alt="Avatar" />
108
+ </Show>
109
+ </Show>
110
+ );
111
+ }
112
+ ```
@@ -1,6 +1,6 @@
1
1
  export { SurrealDBWasmFactory } from './surrealdb-wasm-factory';
2
2
 
3
- import { Surreal } from 'surrealdb';
3
+ import type { Surreal } from 'surrealdb';
4
4
  import type { CacheStrategy } from '../types';
5
5
 
6
6
  /**
@@ -1,9 +1,11 @@
1
- import { Diagnostic, Surreal, applyDiagnostics } from 'surrealdb';
1
+ import type { Diagnostic} from 'surrealdb';
2
+ import { Surreal, applyDiagnostics } from 'surrealdb';
2
3
  import { createWasmEngines } from '@surrealdb/wasm';
3
4
  import type { CacheStrategy } from '../types';
4
5
 
5
6
  const printDiagnostic = ({ key, type, phase, ...other }: Diagnostic) => {
6
7
  if (phase === 'progress' || phase === 'after') {
8
+ // oxlint-disable-next-line no-console -- intentional diagnostic logging
7
9
  console.log(`[SurrealDB_WASM] [${key}] ${type}:${phase}\n${JSON.stringify(other, null, 2)}`);
8
10
  }
9
11
  };
@@ -11,6 +13,7 @@ const printDiagnostic = ({ key, type, phase, ...other }: Diagnostic) => {
11
13
  /**
12
14
  * SurrealDB WASM client factory for different storage strategies
13
15
  */
16
+ // oxlint-disable-next-line no-extraneous-class -- factory pattern groups related static methods
14
17
  export class SurrealDBWasmFactory {
15
18
  /**
16
19
  * Creates a SurrealDB WASM instance with the specified storage strategy
package/src/index.ts CHANGED
@@ -1,14 +1,18 @@
1
1
  import type { SyncedDbConfig } from './types';
2
2
  import {
3
- SpookyClient,
4
- AuthService,
5
- BucketHandle,
6
- type SpookyQueryResultPromise,
7
- UpdateOptions,
8
- RunOptions,
3
+ Sp00kyClient,
4
+ type Sp00kyQueryResultPromise,
5
+ type AuthService,
6
+ type BucketHandle,
7
+ type UpdateOptions,
8
+ type RunOptions,
9
+ type SyncHealth,
10
+ type StorageHealth,
11
+ type PreloadOptions,
12
+ type PreloadRefresh,
9
13
  } from '@spooky-sync/core';
10
14
 
11
- import {
15
+ import type {
12
16
  GetTable,
13
17
  QueryBuilder,
14
18
  SchemaStructure,
@@ -19,25 +23,49 @@ import {
19
23
  RelationshipFieldsFromSchema,
20
24
  GetRelationship,
21
25
  RelatedFieldMapEntry,
26
+ FinalQuery,
22
27
  InnerQuery,
23
28
  BackendNames,
24
29
  BackendRoutes,
25
30
  RoutePayload,
26
31
  BucketNames,
27
32
  BucketDefinitionSchema,
33
+ QueryModifier,
34
+ QueryModifierBuilder,
35
+ QueryInfo,
36
+ RelationshipsMetadata,
37
+ RelationshipDefinition,
38
+ InferRelatedModelFromMetadata,
39
+ GetCardinality,
28
40
  } from '@spooky-sync/query-builder';
29
41
 
30
- import { RecordId, Uuid, Surreal } from 'surrealdb';
42
+ import { RecordId, Uuid, type Surreal } from 'surrealdb';
31
43
  export { RecordId, Uuid };
32
44
  export type { Model, GenericModel, GenericSchema, ModelPayload } from './lib/models';
33
45
  export { useQuery } from './lib/use-query';
46
+ export { createPreload } from './lib/create-preload';
47
+ export type { PreloadOptions, PreloadRefresh } from '@spooky-sync/core';
48
+ export { useSyncStatus, type UseSyncStatus } from './lib/use-sync-status';
49
+ export type { SyncHealth, SyncHealthStatus, SyncHealthConfig } from '@spooky-sync/core';
50
+ export { useStorageStatus, type UseStorageStatus } from './lib/use-storage-status';
51
+ export type { StorageHealth, StorageHealthStatus } from '@spooky-sync/core';
52
+ export { useCrdtField } from './lib/use-crdt-field';
53
+ export { useFeatureFlag, type UseFeatureFlag } from './lib/use-feature-flag';
54
+ export {
55
+ useAppRelease,
56
+ type UseAppRelease,
57
+ type UseAppReleaseOptions,
58
+ } from './lib/use-app-release';
34
59
  export { useFileUpload, type FileUploadResult } from './lib/use-file-upload';
35
- export { useDownloadFile, type UseDownloadFileOptions, type UseDownloadFileResult } from './lib/use-download-file';
36
- export { SpookyProvider, type SpookyProviderProps } from './lib/SpookyProvider';
60
+ export {
61
+ useDownloadFile,
62
+ type UseDownloadFileOptions,
63
+ type UseDownloadFileResult,
64
+ } from './lib/use-download-file';
65
+ export { Sp00kyProvider, type Sp00kyProviderProps } from './lib/Sp00kyProvider';
37
66
  export { useDb } from './lib/context';
38
67
 
39
68
  // export { AuthEventTypes } from "@spooky-sync/core"; // TODO: Verify if AuthEventTypes exists in core
40
- export type {};
41
69
 
42
70
  // Re-export query builder types for convenience
43
71
  export type {
@@ -52,7 +80,7 @@ export type {
52
80
  TableModel,
53
81
  TableNames,
54
82
  QueryResult,
55
- } from '@spooky-sync/query-builder';
83
+ };
56
84
 
57
85
  export type RelationshipField<
58
86
  Schema extends SchemaStructure,
@@ -94,39 +122,52 @@ export type WithRelatedMany<Field extends string, RelatedFields extends RelatedF
94
122
  };
95
123
 
96
124
  /**
97
- * SyncedDb - A thin wrapper around spooky-ts for Solid.js integration
98
- * Delegates all logic to the underlying spooky-ts instance
125
+ * SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
126
+ * Delegates all logic to the underlying sp00ky-ts instance
99
127
  */
100
128
  export class SyncedDb<S extends SchemaStructure> {
101
129
  private config: SyncedDbConfig<S>;
102
- private spooky: SpookyClient<S> | null = null;
130
+ private sp00ky: Sp00kyClient<S> | null = null;
103
131
  private _initialized = false;
104
132
 
105
133
  constructor(config: SyncedDbConfig<S>) {
106
134
  this.config = config;
107
135
  }
108
136
 
109
- public getSpooky(): SpookyClient<S> {
110
- if (!this.spooky) throw new Error('SyncedDb not initialized');
111
- return this.spooky;
137
+ public getSp00ky(): Sp00kyClient<S> {
138
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
139
+ return this.sp00ky;
112
140
  }
113
141
 
114
142
  /**
115
- * Initialize the spooky-ts instance
143
+ * Initialize the sp00ky-ts instance
116
144
  */
117
145
  async init(): Promise<void> {
118
146
  if (this._initialized) return;
119
- this.spooky = new SpookyClient<S>(this.config);
120
- await this.spooky.init();
147
+ this.sp00ky = new Sp00kyClient<S>(this.config);
148
+ await this.sp00ky.init();
121
149
  this._initialized = true;
122
150
  }
123
151
 
152
+ /**
153
+ * Tear down the client: leaves the tabs broker, closes the local store and
154
+ * remote socket, and frees the wasm circuit. Without this a remounted provider
155
+ * (or an HMR reload) strands a whole client, and the abandoned wasm heaps stay
156
+ * resident because V8 cannot see how much wasm memory a dropped wrapper holds.
157
+ */
158
+ async close(): Promise<void> {
159
+ const instance = this.sp00ky;
160
+ this.sp00ky = null;
161
+ this._initialized = false;
162
+ if (instance) await instance.close();
163
+ }
164
+
124
165
  /**
125
166
  * Create a new record in the database
126
167
  */
127
168
  async create(id: string, payload: Record<string, unknown>): Promise<void> {
128
- if (!this.spooky) throw new Error('SyncedDb not initialized');
129
- await this.spooky.create(id, payload as Record<string, unknown>);
169
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
170
+ await this.sp00ky.create(id, payload as Record<string, unknown>);
130
171
  }
131
172
 
132
173
  /**
@@ -138,8 +179,8 @@ export class SyncedDb<S extends SchemaStructure> {
138
179
  payload: Partial<TableModel<GetTable<S, TName>>>,
139
180
  options?: UpdateOptions
140
181
  ): Promise<void> {
141
- if (!this.spooky) throw new Error('SyncedDb not initialized');
142
- await this.spooky.update(
182
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
183
+ await this.sp00ky.update(
143
184
  tableName as string,
144
185
  recordId,
145
186
  payload as Record<string, unknown>,
@@ -152,12 +193,39 @@ export class SyncedDb<S extends SchemaStructure> {
152
193
  */
153
194
  async delete<TName extends TableNames<S>>(
154
195
  tableName: TName,
155
- selector: string | InnerQuery<GetTable<S, TName>, boolean>
196
+ selector: string | RecordId | InnerQuery<GetTable<S, TName>, boolean>
156
197
  ): Promise<void> {
157
- if (!this.spooky) throw new Error('SyncedDb not initialized');
158
- if (typeof selector !== 'string')
159
- throw new Error('Only string ID selectors are supported currently with core');
160
- await this.spooky.delete(tableName as string, selector);
198
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
199
+ // Accept a `"table:id"` string OR a RecordId — live-query rows carry their
200
+ // `id` as a RecordId, so callers can pass `db.delete('game', row.id)`
201
+ // directly. Build the canonical string from the raw id part (not
202
+ // `RecordId.toString()`, which escapes special chars) so it round-trips
203
+ // through the engine's `parseRecordIdString`. InnerQuery selectors are not
204
+ // supported yet. (cross-package RecordId instances → match by constructor name.)
205
+ const isRecordId =
206
+ selector instanceof RecordId || (selector as any)?.constructor?.name === 'RecordId';
207
+ let id: string;
208
+ if (typeof selector === 'string') {
209
+ id = selector;
210
+ } else if (isRecordId) {
211
+ id = `${tableName as string}:${(selector as RecordId).id}`;
212
+ } else {
213
+ throw new Error('Only string ID or RecordId selectors are supported currently with core');
214
+ }
215
+ await this.sp00ky.delete(tableName as string, id);
216
+ }
217
+
218
+ /**
219
+ * Preload/prewarm a built query into the local cache without registering a
220
+ * live view. Fetches once and stores the rows (+ embedded related children)
221
+ * locally so a later `useQuery` for the same data paints instantly. Best-effort.
222
+ */
223
+ public async preload(
224
+ finalQuery: FinalQuery<S, any, any, any, any, Sp00kyQueryResultPromise>,
225
+ options?: PreloadOptions
226
+ ): Promise<void> {
227
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
228
+ await this.sp00ky.preload(finalQuery, options);
161
229
  }
162
230
 
163
231
  /**
@@ -165,34 +233,31 @@ export class SyncedDb<S extends SchemaStructure> {
165
233
  */
166
234
  public query<TName extends TableNames<S>>(
167
235
  table: TName
168
- ): QueryBuilder<S, TName, SpookyQueryResultPromise, {}, false> {
169
- if (!this.spooky) throw new Error('SyncedDb not initialized');
170
- return this.spooky.query(table, {});
236
+ ): QueryBuilder<S, TName, Sp00kyQueryResultPromise, {}, false> {
237
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
238
+ return this.sp00ky.query(table, {});
171
239
  }
172
240
 
173
241
  /**
174
242
  * Run a backend operation
175
243
  */
176
- public async run<
177
- B extends BackendNames<S>,
178
- R extends BackendRoutes<S, B>,
179
- >(
244
+ public async run<B extends BackendNames<S>, R extends BackendRoutes<S, B>>(
180
245
  backend: B,
181
246
  path: R,
182
247
  payload: RoutePayload<S, B, R>,
183
- options?: RunOptions,
248
+ options?: RunOptions
184
249
  ): Promise<void> {
185
- if (!this.spooky) throw new Error('SyncedDb not initialized');
186
- await this.spooky.run(backend, path, payload, options);
250
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
251
+ await this.sp00ky.run(backend, path, payload, options);
187
252
  }
188
253
 
189
254
  /**
190
255
  * Authenticate with the database
191
256
  */
192
257
  public async authenticate(token: string): Promise<RecordId<string>> {
193
- const result = await this.spooky?.authenticate(token);
194
- // SpookyClient.authenticate returns whatever remote.authenticate returns (boolean or token usually?)
195
- // Wait, checked SpookyClient: return this.remote.getClient().authenticate(token);
258
+ await this.sp00ky?.authenticate(token);
259
+ // Sp00kyClient.authenticate returns whatever remote.authenticate returns (boolean or token usually?)
260
+ // Wait, checked Sp00kyClient: return this.remote.getClient().authenticate(token);
196
261
  // SurrealDB authenticate returns void? or token?
197
262
  // Assuming void or token.
198
263
  return new RecordId('user', 'me'); // Placeholder or actual?
@@ -210,54 +275,92 @@ export class SyncedDb<S extends SchemaStructure> {
210
275
  * Sign out, clear session and local storage
211
276
  */
212
277
  public async signOut(): Promise<void> {
213
- if (!this.spooky) throw new Error('SyncedDb not initialized');
214
- await this.spooky.auth.signOut();
278
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
279
+ await this.sp00ky.auth.signOut();
215
280
  }
216
281
 
217
282
  /**
218
283
  * Execute a function with direct access to the remote database connection
219
284
  */
220
285
  public async useRemote<T>(fn: (db: Surreal) => T | Promise<T>): Promise<T> {
221
- if (!this.spooky) throw new Error('SyncedDb not initialized');
222
- return await this.spooky.useRemote(fn);
286
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
287
+ return await this.sp00ky.useRemote(fn);
223
288
  }
224
289
  /**
225
290
  * Access the remote database service directly
226
291
  */
227
- get remote(): SpookyClient<S>['remoteClient'] {
228
- if (!this.spooky) throw new Error('SyncedDb not initialized');
229
- return this.spooky.remoteClient;
292
+ get remote(): Sp00kyClient<S>['remoteClient'] {
293
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
294
+ return this.sp00ky.remoteClient;
230
295
  }
231
296
 
232
297
  /**
233
298
  * Access the local database service directly
234
299
  */
235
- get local(): SpookyClient<S>['localClient'] {
236
- if (!this.spooky) throw new Error('SyncedDb not initialized');
237
- return this.spooky.localClient;
300
+ get local(): Sp00kyClient<S>['localClient'] {
301
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
302
+ return this.sp00ky.localClient;
238
303
  }
239
304
 
240
305
  /**
241
306
  * Access the auth service
242
307
  */
243
308
  get auth(): AuthService<S> {
244
- if (!this.spooky) throw new Error('SyncedDb not initialized');
245
- return this.spooky.auth;
309
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
310
+ return this.sp00ky.auth;
246
311
  }
247
312
 
248
313
  get pendingMutationCount(): number {
249
- if (!this.spooky) throw new Error('SyncedDb not initialized');
250
- return this.spooky.pendingMutationCount;
314
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
315
+ return this.sp00ky.pendingMutationCount;
316
+ }
317
+
318
+ /** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
319
+ get liveRetryCount(): number {
320
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
321
+ return this.sp00ky.liveRetryCount;
251
322
  }
252
323
 
253
324
  subscribeToPendingMutations(cb: (count: number) => void): () => void {
254
- if (!this.spooky) throw new Error('SyncedDb not initialized');
255
- return this.spooky.subscribeToPendingMutations(cb);
325
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
326
+ return this.sp00ky.subscribeToPendingMutations(cb);
327
+ }
328
+
329
+ /** Current sync-health snapshot. See {@link useSyncStatus}. */
330
+ get syncHealth(): SyncHealth {
331
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
332
+ return this.sp00ky.syncHealth;
333
+ }
334
+
335
+ /**
336
+ * Observe sync health. Fires immediately with the current status and again
337
+ * on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
338
+ * components; this is the imperative escape hatch.
339
+ */
340
+ subscribeToSyncHealth(cb: (health: SyncHealth) => void): () => void {
341
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
342
+ return this.sp00ky.subscribeToSyncHealth(cb);
343
+ }
344
+
345
+ /** Current local-store durability snapshot. See {@link useStorageStatus}. */
346
+ get storageHealth(): StorageHealth {
347
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
348
+ return this.sp00ky.storageHealth;
349
+ }
350
+
351
+ /**
352
+ * Observe local-store durability. Fires immediately with the current snapshot
353
+ * and again on change. Prefer the `useStorageStatus` hook in components; this
354
+ * is the imperative escape hatch.
355
+ */
356
+ subscribeToStorageHealth(cb: (health: StorageHealth) => void): () => void {
357
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
358
+ return this.sp00ky.subscribeToStorageHealth(cb);
256
359
  }
257
360
 
258
361
  bucket<B extends BucketNames<S>>(name: B): BucketHandle {
259
- if (!this.spooky) throw new Error('SyncedDb not initialized');
260
- return this.spooky.bucket(name);
362
+ if (!this.sp00ky) throw new Error('SyncedDb not initialized');
363
+ return this.sp00ky.bucket(name);
261
364
  }
262
365
 
263
366
  getBucketConfig(name: string): BucketDefinitionSchema | undefined {
@@ -0,0 +1,102 @@
1
+ import type { JSX } from 'solid-js';
2
+ import {
3
+ createSignal,
4
+ onMount,
5
+ onCleanup,
6
+ createComponent,
7
+ createMemo,
8
+ mergeProps,
9
+ } from 'solid-js';
10
+ import type { SchemaStructure } from '@spooky/query-builder';
11
+ import type { SyncedDbConfig } from '../types';
12
+ import { SyncedDb } from '../index';
13
+ import { Sp00kyContext } from './context';
14
+
15
+ export interface Sp00kyProviderProps<S extends SchemaStructure> {
16
+ config: SyncedDbConfig<S>;
17
+ fallback?: JSX.Element;
18
+ onError?: (error: Error) => void;
19
+ onReady?: (db: SyncedDb<S>) => void;
20
+ /**
21
+ * Prewarm data into the local cache before revealing the UI. Runs after
22
+ * `init()`; the `fallback` stays visible until it resolves. Use awaitable
23
+ * `db.preload(...)` calls here to gate first-load on essential data (e.g.
24
+ * config). On warm loads preload returns instantly, so there's no perceptible
25
+ * gate after the first run. Best-effort: a rejection is caught and the UI is
26
+ * revealed anyway.
27
+ */
28
+ preload?: (db: SyncedDb<S>) => Promise<void>;
29
+ children: JSX.Element;
30
+ }
31
+
32
+ export function Sp00kyProvider<S extends SchemaStructure>(
33
+ props: Sp00kyProviderProps<S>
34
+ ): JSX.Element {
35
+ const merged = mergeProps(
36
+ {
37
+ fallback: undefined as JSX.Element | undefined,
38
+ },
39
+ props
40
+ );
41
+
42
+ const [db, setDb] = createSignal<SyncedDb<S> | undefined>(undefined);
43
+
44
+ // Set once the provider is disposed. `onMount` is async, so a remount (or a
45
+ // fast unmount) can land mid-init; without this the abandoned instance keeps
46
+ // its SharedWorker leadership, its OPFS worker and its wasm circuit alive for
47
+ // the life of the page.
48
+ let disposed = false;
49
+ let live: SyncedDb<S> | undefined;
50
+
51
+ onCleanup(() => {
52
+ disposed = true;
53
+ const instance = live;
54
+ live = undefined;
55
+ void instance?.close();
56
+ });
57
+
58
+ onMount(async () => {
59
+ try {
60
+ const instance = new SyncedDb<S>(merged.config);
61
+ live = instance;
62
+ await instance.init();
63
+ if (disposed) {
64
+ await instance.close();
65
+ return;
66
+ }
67
+ // Gate first-load UI on prewarmed data. Best-effort: never let a preload
68
+ // failure keep the app stuck on the fallback.
69
+ if (merged.preload) {
70
+ try {
71
+ await merged.preload(instance);
72
+ } catch (e) {
73
+ // oxlint-disable-next-line no-console
74
+ console.error('Sp00kyProvider: preload failed; revealing UI anyway', e);
75
+ }
76
+ }
77
+ setDb(() => instance);
78
+ merged.onReady?.(instance);
79
+ } catch (e) {
80
+ const error = e instanceof Error ? e : new Error(String(e));
81
+ if (merged.onError) {
82
+ merged.onError(error);
83
+ } else {
84
+ // oxlint-disable-next-line no-console
85
+ console.error('Sp00kyProvider: Failed to initialize database', error);
86
+ }
87
+ }
88
+ });
89
+
90
+ const content = createMemo(() => {
91
+ const instance = db();
92
+ if (!instance) return merged.fallback;
93
+ return createComponent(Sp00kyContext.Provider, {
94
+ value: instance,
95
+ get children() {
96
+ return merged.children;
97
+ },
98
+ });
99
+ });
100
+
101
+ return content as unknown as JSX.Element;
102
+ }
@@ -2,12 +2,12 @@ import { createContext, useContext } from 'solid-js';
2
2
  import type { SchemaStructure } from '@spooky/query-builder';
3
3
  import type { SyncedDb } from '../index';
4
4
 
5
- export const SpookyContext = createContext<SyncedDb<any> | undefined>();
5
+ export const Sp00kyContext = createContext<SyncedDb<any> | undefined>();
6
6
 
7
7
  export function useDb<S extends SchemaStructure>(): SyncedDb<S> {
8
- const db = useContext(SpookyContext);
8
+ const db = useContext(Sp00kyContext);
9
9
  if (!db) {
10
- throw new Error('useDb must be used within a <SpookyProvider>. Wrap your app in <SpookyProvider config={...}>.');
10
+ throw new Error('useDb must be used within a <Sp00kyProvider>. Wrap your app in <Sp00kyProvider config={...}>.');
11
11
  }
12
12
  return db as SyncedDb<S>;
13
13
  }