@spooky-sync/client-solid 0.0.1-canary.26 → 0.0.1-canary.261

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,44 @@
1
+ import { createSignal, onCleanup, type Accessor } from 'solid-js';
2
+ import { useDb } from './context';
3
+ import type { StorageHealth, StorageHealthStatus } from '@spooky-sync/core';
4
+
5
+ export interface UseStorageStatus {
6
+ /** Full durability snapshot; updates reactively. */
7
+ health: Accessor<StorageHealth>;
8
+ /** `'unknown'` | `'persistent'` | `'memory'`. */
9
+ status: Accessor<StorageHealthStatus>;
10
+ /** `true` when the local store survives a reload. */
11
+ isPersistent: Accessor<boolean>;
12
+ /**
13
+ * `true` only when durable storage was requested and could NOT be opened, so
14
+ * the dataset is sitting in RAM and local writes die on reload. Drive a
15
+ * warning off this, not off `status`: a store configured as in-memory reports
16
+ * `'memory'` too, and that is a choice rather than a problem.
17
+ */
18
+ isMemoryFallback: Accessor<boolean>;
19
+ }
20
+
21
+ /**
22
+ * Observe how durable the LOCAL cache is, for a "no local storage" warning.
23
+ *
24
+ * Under `localEngine: 'sqlite'` with `store: 'indexeddb'` the durable store is
25
+ * the OPFS SAHPool VFS, and only ONE client per bucket can hold it open: a
26
+ * second tab of the same app cannot get it and runs in memory instead (the
27
+ * engine retries first, so a closing tab's lock is usually waited out). Must be
28
+ * used within a `<Sp00kyProvider>`.
29
+ */
30
+ export function useStorageStatus(): UseStorageStatus {
31
+ const db = useDb();
32
+ // subscribeToStorageHealth fires synchronously with the current snapshot, so
33
+ // the signal is correct from the first read; the initial value avoids a flash.
34
+ const [health, setHealth] = createSignal<StorageHealth>(db.storageHealth);
35
+ const unsub = db.subscribeToStorageHealth(setHealth);
36
+ onCleanup(unsub);
37
+
38
+ return {
39
+ health,
40
+ status: () => health().status,
41
+ isPersistent: () => health().status === 'persistent',
42
+ isMemoryFallback: () => health().fallback,
43
+ };
44
+ }
@@ -0,0 +1,64 @@
1
+ import { createSignal, onCleanup, type Accessor } from 'solid-js';
2
+ import { useDb } from './context';
3
+ import type { ConnectionState, SyncHealth, SyncHealthStatus } from '@spooky-sync/core';
4
+
5
+ export interface UseSyncStatus {
6
+ /** Full health snapshot; updates reactively on every transition. */
7
+ health: Accessor<SyncHealth>;
8
+ /** `'healthy'` | `'degraded'`. */
9
+ status: Accessor<SyncHealthStatus>;
10
+ isHealthy: Accessor<boolean>;
11
+ /** `true` once sync has failed for a sustained run — drive a banner off this. */
12
+ isDegraded: Accessor<boolean>;
13
+ /** `true` once at least one sync round has succeeded this session. */
14
+ everConnected: Accessor<boolean>;
15
+ /**
16
+ * `true` only for a real lost connection: degraded AFTER a first successful
17
+ * sync. Stays `false` during the initial "connecting" phase (degraded but
18
+ * never reached the server yet), so an indicator can show nothing until the
19
+ * app has actually connected once.
20
+ */
21
+ isOffline: Accessor<boolean>;
22
+ /**
23
+ * Transport state of the remote WebSocket. Flips the instant the socket
24
+ * drops, unlike `status`, which only degrades after a sustained run of failed
25
+ * sync rounds — so this is what to drive a "reconnecting…" affordance off.
26
+ */
27
+ connection: Accessor<ConnectionState>;
28
+ /**
29
+ * `true` while the connection is being re-established. Usually still
30
+ * `isHealthy()`: a short reconnect is invisible to sync, and writes made
31
+ * during it are queued locally and pushed once the socket is back.
32
+ */
33
+ isReconnecting: Accessor<boolean>;
34
+ }
35
+
36
+ /**
37
+ * Observe sync health for a "can't reach the server" banner / indicator.
38
+ *
39
+ * Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
40
+ * remote 500 on query registration, a dropped socket) are absorbed by the
41
+ * retry and never flip this; `isDegraded()` only goes true once failures
42
+ * persist for the configured number of consecutive rounds (sp00ky core config
43
+ * `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
44
+ * the next successful round. Must be used within a `<Sp00kyProvider>`.
45
+ */
46
+ export function useSyncStatus(): UseSyncStatus {
47
+ const db = useDb();
48
+ // subscribeToSyncHealth fires synchronously with the current status, so the
49
+ // signal is correct from first read; the initial value just avoids a flash.
50
+ const [health, setHealth] = createSignal<SyncHealth>(db.syncHealth);
51
+ const unsub = db.subscribeToSyncHealth(setHealth);
52
+ onCleanup(unsub);
53
+
54
+ return {
55
+ health,
56
+ status: () => health().status,
57
+ isHealthy: () => health().status === 'healthy',
58
+ isDegraded: () => health().status === 'degraded',
59
+ everConnected: () => health().everConnected,
60
+ isOffline: () => health().status === 'degraded' && health().everConnected,
61
+ connection: () => health().connection,
62
+ isReconnecting: () => health().connection === 'reconnecting',
63
+ };
64
+ }
@@ -1,7 +1,6 @@
1
- import type { Surreal } from 'surrealdb';
2
1
  import type { SyncedDb } from '../index';
3
- import { GenericSchema } from '../lib/models';
4
- import type { SpookyConfig } from '@spooky-sync/core';
2
+ import type { GenericSchema } from '../lib/models';
3
+ import type { Sp00kyConfig } from '@spooky-sync/core';
5
4
  import type { SchemaStructure, TableNames, GetTable, TableModel } from '@spooky-sync/query-builder';
6
5
 
7
6
  /**
@@ -44,7 +43,7 @@ export type InferRelationshipsFromConst<S extends SchemaStructure, Schema extend
44
43
  // Prettify helper expands types for better intellisense
45
44
  type Prettify<T> = { [K in keyof T]: T[K] } & {};
46
45
 
47
- export type SyncedDbConfig<S extends SchemaStructure> = Prettify<SpookyConfig<S>>;
46
+ export type SyncedDbConfig<S extends SchemaStructure> = Prettify<Sp00kyConfig<S>>;
48
47
 
49
48
  // export interface LocalDbConfig {
50
49
  // name: string;
@@ -1,55 +0,0 @@
1
- import { createSignal, onMount, createComponent, createMemo, JSX, mergeProps } from 'solid-js';
2
- import type { SchemaStructure } from '@spooky/query-builder';
3
- import type { SyncedDbConfig } from '../types';
4
- import { SyncedDb } from '../index';
5
- import { SpookyContext } from './context';
6
-
7
- export interface SpookyProviderProps<S extends SchemaStructure> {
8
- config: SyncedDbConfig<S>;
9
- fallback?: JSX.Element;
10
- onError?: (error: Error) => void;
11
- onReady?: (db: SyncedDb<S>) => void;
12
- children: JSX.Element;
13
- }
14
-
15
- export function SpookyProvider<S extends SchemaStructure>(
16
- props: SpookyProviderProps<S>
17
- ): JSX.Element {
18
- const merged = mergeProps(
19
- {
20
- fallback: undefined as JSX.Element | undefined,
21
- },
22
- props
23
- );
24
-
25
- const [db, setDb] = createSignal<SyncedDb<S> | undefined>(undefined);
26
-
27
- onMount(async () => {
28
- try {
29
- const instance = new SyncedDb<S>(merged.config);
30
- await instance.init();
31
- setDb(() => instance);
32
- merged.onReady?.(instance);
33
- } catch (e) {
34
- const error = e instanceof Error ? e : new Error(String(e));
35
- if (merged.onError) {
36
- merged.onError(error);
37
- } else {
38
- console.error('SpookyProvider: Failed to initialize database', error);
39
- }
40
- }
41
- });
42
-
43
- const content = createMemo(() => {
44
- const instance = db();
45
- if (!instance) return merged.fallback;
46
- return createComponent(SpookyContext.Provider, {
47
- value: instance,
48
- get children() {
49
- return merged.children;
50
- },
51
- });
52
- });
53
-
54
- return content as unknown as JSX.Element;
55
- }