@anfenn/dync 1.0.25 → 1.0.27

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.
@@ -1,50 +1,39 @@
1
- import {
2
- Dync
3
- } from "../chunk-QA2TX54K.js";
4
- import "../chunk-SQB6E7V2.js";
5
-
6
- // src/react/useDync.ts
7
- import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
8
- function makeDync(config) {
9
- const db = "syncApis" in config ? new Dync(config.databaseName, config.syncApis, config.storageAdapter, config.options) : new Dync(config.databaseName, config.batchSync, config.storageAdapter, config.options);
10
- let cachedState = db.sync.getState();
11
- const subscribe = (listener) => db.sync.onStateChange((nextState) => {
12
- cachedState = nextState;
13
- listener();
14
- });
15
- const getSnapshot = () => {
16
- const fresh = db.sync.getState();
17
- if (JSON.stringify(fresh) !== JSON.stringify(cachedState)) {
18
- cachedState = fresh;
1
+ // src/react/useSyncState.ts
2
+ import { useCallback, useRef, useSyncExternalStore } from "react";
3
+ function useSyncState(db) {
4
+ const cacheRef = useRef(db.sync.state);
5
+ const subscribe = useCallback(
6
+ (listener) => db.sync.onStateChange((nextState) => {
7
+ cacheRef.current = nextState;
8
+ listener();
9
+ }),
10
+ [db]
11
+ );
12
+ const getSnapshot = useCallback(() => {
13
+ const fresh = db.sync.state;
14
+ if (JSON.stringify(fresh) !== JSON.stringify(cacheRef.current)) {
15
+ cacheRef.current = fresh;
19
16
  }
20
- return cachedState;
21
- };
22
- const useDync = () => {
23
- const syncState = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
24
- return { db, syncState };
25
- };
26
- const boundUseLiveQuery = (querier, deps = [], tables) => {
27
- return useLiveQueryImpl(db, querier, deps, tables);
28
- };
29
- return {
30
- db,
31
- useDync,
32
- useLiveQuery: boundUseLiveQuery
33
- };
17
+ return cacheRef.current;
18
+ }, [db]);
19
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
34
20
  }
35
- function useLiveQueryImpl(db, querier, deps = [], tables) {
21
+
22
+ // src/react/useLiveQuery.ts
23
+ import { useCallback as useCallback2, useEffect, useRef as useRef2, useState } from "react";
24
+ function useLiveQuery(db, querier, deps = [], tables) {
36
25
  const [result, setResult] = useState(void 0);
37
26
  const [, setError] = useState(null);
38
- const isMountedRef = useRef(true);
39
- const queryVersionRef = useRef(0);
40
- const querierRef = useRef(querier);
41
- const tablesRef = useRef(tables);
27
+ const isMountedRef = useRef2(true);
28
+ const queryVersionRef = useRef2(0);
29
+ const querierRef = useRef2(querier);
30
+ const tablesRef = useRef2(tables);
42
31
  querierRef.current = querier;
43
32
  tablesRef.current = tables;
44
- const runQuery = useCallback(async () => {
33
+ const runQuery = useCallback2(async () => {
45
34
  const currentVersion = ++queryVersionRef.current;
46
35
  try {
47
- const queryResult = await querierRef.current(db);
36
+ const queryResult = await querierRef.current();
48
37
  if (isMountedRef.current && currentVersion === queryVersionRef.current) {
49
38
  setResult(queryResult);
50
39
  setError(null);
@@ -54,7 +43,7 @@ function useLiveQueryImpl(db, querier, deps = [], tables) {
54
43
  setError(err);
55
44
  }
56
45
  }
57
- }, [db]);
46
+ }, []);
58
47
  useEffect(() => {
59
48
  runQuery();
60
49
  }, [...deps, runQuery]);
@@ -73,6 +62,7 @@ function useLiveQueryImpl(db, querier, deps = [], tables) {
73
62
  return result;
74
63
  }
75
64
  export {
76
- makeDync
65
+ useLiveQuery,
66
+ useSyncState
77
67
  };
78
68
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/useDync.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';\nimport { Dync } from '../index';\nimport type { ApiFunctions, BatchSync, MutationEvent, SyncOptions, SyncState } from '../types';\nimport type { StorageAdapter } from '../storage/types';\n\nexport interface MakeDyncConfigPerTable {\n databaseName: string;\n syncApis: Record<string, ApiFunctions>;\n storageAdapter: StorageAdapter;\n options?: SyncOptions;\n}\n\nexport interface MakeDyncConfigBatch {\n databaseName: string;\n batchSync: BatchSync;\n storageAdapter: StorageAdapter;\n options?: SyncOptions;\n}\n\nexport type MakeDyncConfig = MakeDyncConfigPerTable | MakeDyncConfigBatch;\n\nexport interface UseDyncValue<TStoreMap extends Record<string, any>> {\n db: Dync<TStoreMap>;\n syncState: SyncState;\n}\n\n/**\n * A hook returned by makeDync that observes database queries and automatically\n * re-runs them when data changes. The db instance is passed to the querier callback.\n *\n * @param querier - Function that receives db and returns query result\n * @param deps - Optional React dependency list for re-running the query\n * @param tables - Optional array of table names to watch for mutations, otherwise it's all tables\n * @returns The query result, or undefined if not yet available\n */\nexport type BoundUseLiveQuery<TStoreMap extends Record<string, any>> = <T>(\n querier: (db: Dync<TStoreMap>) => Promise<T> | T,\n deps?: React.DependencyList,\n tables?: string[],\n) => T | undefined;\n\nexport interface MakeDyncResult<TStoreMap extends Record<string, any>> {\n // The Dync database instance\n db: Dync<TStoreMap>;\n // Hook to get db and syncState in components\n useDync: () => UseDyncValue<TStoreMap>;\n // Hook for live queries - db is passed to the querier callback\n useLiveQuery: BoundUseLiveQuery<TStoreMap>;\n}\n\nexport function makeDync<TStoreMap extends Record<string, any> = Record<string, unknown>>(config: MakeDyncConfig): MakeDyncResult<TStoreMap> {\n // Determine which mode based on config shape\n const db =\n 'syncApis' in config\n ? new Dync<TStoreMap>(config.databaseName, config.syncApis, config.storageAdapter, config.options)\n : new Dync<TStoreMap>(config.databaseName, config.batchSync, config.storageAdapter, config.options);\n\n // Cache to provide referential stability (React requires stable references)\n // Updated when subscription fires OR when getSnapshot detects value changes\n let cachedState = db.sync.getState();\n\n const subscribe = (listener: () => void) =>\n db.sync.onStateChange((nextState) => {\n // Update cache immediately when notified, before React calls getSnapshot\n cachedState = nextState;\n listener();\n });\n\n // getSnapshot returns cached state, but also checks for changes that happened\n // before subscription was active (e.g., hydration during initial mount)\n const getSnapshot = () => {\n const fresh = db.sync.getState();\n if (JSON.stringify(fresh) !== JSON.stringify(cachedState)) {\n cachedState = fresh;\n }\n return cachedState;\n };\n\n const useDync = () => {\n const syncState = useSyncExternalStore<SyncState>(subscribe, getSnapshot, getSnapshot);\n return { db, syncState } as UseDyncValue<TStoreMap>;\n };\n\n // Create a bound useLiveQuery that passes db to the querier\n const boundUseLiveQuery: BoundUseLiveQuery<TStoreMap> = <T>(\n querier: (db: Dync<TStoreMap>) => Promise<T> | T,\n deps: React.DependencyList = [],\n tables?: string[],\n ): T | undefined => {\n return useLiveQueryImpl(db, querier, deps, tables);\n };\n\n return {\n db,\n useDync,\n useLiveQuery: boundUseLiveQuery,\n };\n}\n\nfunction useLiveQueryImpl<TStoreMap extends Record<string, any>, T>(\n db: Dync<TStoreMap>,\n querier: (db: Dync<TStoreMap>) => Promise<T> | T,\n deps: React.DependencyList = [],\n tables?: string[],\n): T | undefined {\n const [result, setResult] = useState<T | undefined>(undefined);\n const [, setError] = useState<Error | null>(null);\n const isMountedRef = useRef(true);\n const queryVersionRef = useRef(0);\n const querierRef = useRef(querier);\n const tablesRef = useRef(tables);\n\n // Keep refs up to date\n querierRef.current = querier;\n tablesRef.current = tables;\n\n const runQuery = useCallback(async () => {\n const currentVersion = ++queryVersionRef.current;\n try {\n const queryResult = await querierRef.current(db);\n // Only update if still mounted and this is the latest query\n if (isMountedRef.current && currentVersion === queryVersionRef.current) {\n setResult(queryResult);\n setError(null);\n }\n } catch (err) {\n if (isMountedRef.current && currentVersion === queryVersionRef.current) {\n setError(err as Error);\n }\n }\n }, [db]);\n\n // Re-run query when deps change\n useEffect(() => {\n runQuery();\n }, [...deps, runQuery]);\n\n useEffect(() => {\n isMountedRef.current = true;\n\n // Subscribe to mutation events\n const unsubscribe = db.sync.onMutation((event: MutationEvent) => {\n // Only re-run if no tables filter specified, or if the mutation affects a watched table\n if (!tablesRef.current || tablesRef.current.includes(event.tableName)) {\n runQuery();\n }\n });\n\n return () => {\n isMountedRef.current = false;\n unsubscribe();\n };\n }, [db, runQuery]);\n\n return result;\n}\n"],"mappings":";;;;;;AAAA,SAAS,aAAa,WAAW,QAAQ,UAAU,4BAA4B;AAkDxE,SAAS,SAA0E,QAAmD;AAEzI,QAAM,KACF,cAAc,SACR,IAAI,KAAgB,OAAO,cAAc,OAAO,UAAU,OAAO,gBAAgB,OAAO,OAAO,IAC/F,IAAI,KAAgB,OAAO,cAAc,OAAO,WAAW,OAAO,gBAAgB,OAAO,OAAO;AAI1G,MAAI,cAAc,GAAG,KAAK,SAAS;AAEnC,QAAM,YAAY,CAAC,aACf,GAAG,KAAK,cAAc,CAAC,cAAc;AAEjC,kBAAc;AACd,aAAS;AAAA,EACb,CAAC;AAIL,QAAM,cAAc,MAAM;AACtB,UAAM,QAAQ,GAAG,KAAK,SAAS;AAC/B,QAAI,KAAK,UAAU,KAAK,MAAM,KAAK,UAAU,WAAW,GAAG;AACvD,oBAAc;AAAA,IAClB;AACA,WAAO;AAAA,EACX;AAEA,QAAM,UAAU,MAAM;AAClB,UAAM,YAAY,qBAAgC,WAAW,aAAa,WAAW;AACrF,WAAO,EAAE,IAAI,UAAU;AAAA,EAC3B;AAGA,QAAM,oBAAkD,CACpD,SACA,OAA6B,CAAC,GAC9B,WACgB;AAChB,WAAO,iBAAiB,IAAI,SAAS,MAAM,MAAM;AAAA,EACrD;AAEA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA,cAAc;AAAA,EAClB;AACJ;AAEA,SAAS,iBACL,IACA,SACA,OAA6B,CAAC,GAC9B,QACa;AACb,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,MAAS;AAC7D,QAAM,CAAC,EAAE,QAAQ,IAAI,SAAuB,IAAI;AAChD,QAAM,eAAe,OAAO,IAAI;AAChC,QAAM,kBAAkB,OAAO,CAAC;AAChC,QAAM,aAAa,OAAO,OAAO;AACjC,QAAM,YAAY,OAAO,MAAM;AAG/B,aAAW,UAAU;AACrB,YAAU,UAAU;AAEpB,QAAM,WAAW,YAAY,YAAY;AACrC,UAAM,iBAAiB,EAAE,gBAAgB;AACzC,QAAI;AACA,YAAM,cAAc,MAAM,WAAW,QAAQ,EAAE;AAE/C,UAAI,aAAa,WAAW,mBAAmB,gBAAgB,SAAS;AACpE,kBAAU,WAAW;AACrB,iBAAS,IAAI;AAAA,MACjB;AAAA,IACJ,SAAS,KAAK;AACV,UAAI,aAAa,WAAW,mBAAmB,gBAAgB,SAAS;AACpE,iBAAS,GAAY;AAAA,MACzB;AAAA,IACJ;AAAA,EACJ,GAAG,CAAC,EAAE,CAAC;AAGP,YAAU,MAAM;AACZ,aAAS;AAAA,EACb,GAAG,CAAC,GAAG,MAAM,QAAQ,CAAC;AAEtB,YAAU,MAAM;AACZ,iBAAa,UAAU;AAGvB,UAAM,cAAc,GAAG,KAAK,WAAW,CAAC,UAAyB;AAE7D,UAAI,CAAC,UAAU,WAAW,UAAU,QAAQ,SAAS,MAAM,SAAS,GAAG;AACnE,iBAAS;AAAA,MACb;AAAA,IACJ,CAAC;AAED,WAAO,MAAM;AACT,mBAAa,UAAU;AACvB,kBAAY;AAAA,IAChB;AAAA,EACJ,GAAG,CAAC,IAAI,QAAQ,CAAC;AAEjB,SAAO;AACX;","names":[]}
1
+ {"version":3,"sources":["../../src/react/useSyncState.ts","../../src/react/useLiveQuery.ts"],"sourcesContent":["import { useCallback, useRef, useSyncExternalStore } from 'react';\nimport type { SyncState } from '../types';\nimport type { DyncLike } from './types';\n\n/**\n * React hook that subscribes to Dync sync state changes.\n * Returns the current SyncState and re-renders when it changes.\n *\n * @param db - The Dync database instance\n * @returns The current SyncState\n *\n * @example\n * ```tsx\n * import { db } from './store';\n * import { useSyncState } from '@anfenn/dync/react';\n *\n * function SyncStatus() {\n * const syncState = useSyncState(db);\n * return <div>Status: {syncState.status}</div>;\n * }\n * ```\n */\nexport function useSyncState(db: DyncLike): SyncState {\n // Use refs to create stable subscribe/getSnapshot functions\n const cacheRef = useRef<SyncState>(db.sync.state);\n\n const subscribe = useCallback(\n (listener: () => void) =>\n db.sync.onStateChange((nextState) => {\n cacheRef.current = nextState;\n listener();\n }),\n [db],\n );\n\n const getSnapshot = useCallback(() => {\n const fresh = db.sync.state;\n if (JSON.stringify(fresh) !== JSON.stringify(cacheRef.current)) {\n cacheRef.current = fresh;\n }\n return cacheRef.current;\n }, [db]);\n\n return useSyncExternalStore<SyncState>(subscribe, getSnapshot, getSnapshot);\n}\n","import { useCallback, useEffect, useRef, useState } from 'react';\nimport type { MutationEvent } from '../types';\nimport type { DyncLike } from './types';\n\n/**\n * React hook that observes database queries and automatically re-runs them when data changes.\n *\n * @param db - The Dync database instance\n * @param querier - Function that returns query result (can be async)\n * @param deps - Optional React dependency list for re-running the query\n * @param tables - Optional array of table names to watch for mutations (defaults to all tables)\n * @returns The query result, or undefined if not yet available\n *\n * @example\n * ```tsx\n * import { db } from './store';\n * import { useLiveQuery } from '@anfenn/dync/react';\n *\n * function TodoList() {\n * const todos = useLiveQuery(db, () => db.todos.toArray());\n * return <ul>{todos?.map(t => <li key={t._localId}>{t.title}</li>)}</ul>;\n * }\n *\n * // With dependencies\n * function FilteredTodos({ filter }: { filter: string }) {\n * const todos = useLiveQuery(\n * db,\n * () => db.todos.where('status').equals(filter).toArray(),\n * [filter]\n * );\n * return <ul>{todos?.map(t => <li key={t._localId}>{t.title}</li>)}</ul>;\n * }\n *\n * // Watch specific tables only\n * function TodoCount() {\n * const count = useLiveQuery(\n * db,\n * () => db.todos.count(),\n * [],\n * ['todos']\n * );\n * return <span>{count ?? 0} todos</span>;\n * }\n * ```\n */\nexport function useLiveQuery<T>(db: DyncLike, querier: () => Promise<T> | T, deps: React.DependencyList = [], tables?: string[]): T | undefined {\n const [result, setResult] = useState<T | undefined>(undefined);\n const [, setError] = useState<Error | null>(null);\n const isMountedRef = useRef(true);\n const queryVersionRef = useRef(0);\n const querierRef = useRef(querier);\n const tablesRef = useRef(tables);\n\n // Keep refs up to date\n querierRef.current = querier;\n tablesRef.current = tables;\n\n const runQuery = useCallback(async () => {\n const currentVersion = ++queryVersionRef.current;\n try {\n const queryResult = await querierRef.current();\n // Only update if still mounted and this is the latest query\n if (isMountedRef.current && currentVersion === queryVersionRef.current) {\n setResult(queryResult);\n setError(null);\n }\n } catch (err) {\n if (isMountedRef.current && currentVersion === queryVersionRef.current) {\n setError(err as Error);\n }\n }\n }, []);\n\n // Re-run query when deps change\n useEffect(() => {\n runQuery();\n }, [...deps, runQuery]);\n\n useEffect(() => {\n isMountedRef.current = true;\n\n // Subscribe to mutation events\n const unsubscribe = db.sync.onMutation((event: MutationEvent) => {\n // Only re-run if no tables filter specified, or if the mutation affects a watched table\n if (!tablesRef.current || tablesRef.current.includes(event.tableName)) {\n runQuery();\n }\n });\n\n return () => {\n isMountedRef.current = false;\n unsubscribe();\n };\n }, [db, runQuery]);\n\n return result;\n}\n"],"mappings":";AAAA,SAAS,aAAa,QAAQ,4BAA4B;AAsBnD,SAAS,aAAa,IAAyB;AAElD,QAAM,WAAW,OAAkB,GAAG,KAAK,KAAK;AAEhD,QAAM,YAAY;AAAA,IACd,CAAC,aACG,GAAG,KAAK,cAAc,CAAC,cAAc;AACjC,eAAS,UAAU;AACnB,eAAS;AAAA,IACb,CAAC;AAAA,IACL,CAAC,EAAE;AAAA,EACP;AAEA,QAAM,cAAc,YAAY,MAAM;AAClC,UAAM,QAAQ,GAAG,KAAK;AACtB,QAAI,KAAK,UAAU,KAAK,MAAM,KAAK,UAAU,SAAS,OAAO,GAAG;AAC5D,eAAS,UAAU;AAAA,IACvB;AACA,WAAO,SAAS;AAAA,EACpB,GAAG,CAAC,EAAE,CAAC;AAEP,SAAO,qBAAgC,WAAW,aAAa,WAAW;AAC9E;;;AC5CA,SAAS,eAAAA,cAAa,WAAW,UAAAC,SAAQ,gBAAgB;AA6ClD,SAAS,aAAgB,IAAc,SAA+B,OAA6B,CAAC,GAAG,QAAkC;AAC5I,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAwB,MAAS;AAC7D,QAAM,CAAC,EAAE,QAAQ,IAAI,SAAuB,IAAI;AAChD,QAAM,eAAeA,QAAO,IAAI;AAChC,QAAM,kBAAkBA,QAAO,CAAC;AAChC,QAAM,aAAaA,QAAO,OAAO;AACjC,QAAM,YAAYA,QAAO,MAAM;AAG/B,aAAW,UAAU;AACrB,YAAU,UAAU;AAEpB,QAAM,WAAWD,aAAY,YAAY;AACrC,UAAM,iBAAiB,EAAE,gBAAgB;AACzC,QAAI;AACA,YAAM,cAAc,MAAM,WAAW,QAAQ;AAE7C,UAAI,aAAa,WAAW,mBAAmB,gBAAgB,SAAS;AACpE,kBAAU,WAAW;AACrB,iBAAS,IAAI;AAAA,MACjB;AAAA,IACJ,SAAS,KAAK;AACV,UAAI,aAAa,WAAW,mBAAmB,gBAAgB,SAAS;AACpE,iBAAS,GAAY;AAAA,MACzB;AAAA,IACJ;AAAA,EACJ,GAAG,CAAC,CAAC;AAGL,YAAU,MAAM;AACZ,aAAS;AAAA,EACb,GAAG,CAAC,GAAG,MAAM,QAAQ,CAAC;AAEtB,YAAU,MAAM;AACZ,iBAAa,UAAU;AAGvB,UAAM,cAAc,GAAG,KAAK,WAAW,CAAC,UAAyB;AAE7D,UAAI,CAAC,UAAU,WAAW,UAAU,QAAQ,SAAS,MAAM,SAAS,GAAG;AACnE,iBAAS;AAAA,MACb;AAAA,IACJ,CAAC;AAED,WAAO,MAAM;AACT,mBAAa,UAAU;AACvB,kBAAY;AAAA,IAChB;AAAA,EACJ,GAAG,CAAC,IAAI,QAAQ,CAAC;AAEjB,SAAO;AACX;","names":["useCallback","useRef"]}
@@ -1,5 +1,4 @@
1
- import { d as StorageTable, c as StorageAdapter, T as TableSchemaDefinition, D as DexieQueryContext, b as SqliteQueryContext, a as MemoryQueryContext } from './dexie-DRLMKLl5.js';
2
- import { c as SQLiteVersionConfigurator } from './types-CSbIAfu2.js';
1
+ import { d as StorageAdapter, a as StorageTable } from './dexie-BFPA0JU2.js';
3
2
 
4
3
  type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
5
4
  interface Logger {
@@ -95,6 +94,38 @@ interface SyncOptions {
95
94
  onAfterMissingRemoteRecordDuringUpdate?: MissingRemoteRecordDuringUpdateCallback;
96
95
  conflictResolutionStrategy?: ConflictResolutionStrategy;
97
96
  }
97
+ /**
98
+ * Configuration options for creating a Dync instance.
99
+ *
100
+ * @example Per-table sync mode
101
+ * ```ts
102
+ * const db = new Dync<Store>({
103
+ * databaseName: 'my-app',
104
+ * storageAdapter: new SQLiteAdapter(driver),
105
+ * sync: { todos: todoSyncApi },
106
+ * });
107
+ * ```
108
+ *
109
+ * @example Batch sync mode
110
+ * ```ts
111
+ * const db = new Dync<Store>({
112
+ * databaseName: 'my-app',
113
+ * storageAdapter: new SQLiteAdapter(driver),
114
+ * sync: { syncTables: ['todos'], push, pull },
115
+ * });
116
+ * ```
117
+ */
118
+ interface DyncOptions<TStoreMap extends Record<string, any> = Record<string, any>> {
119
+ databaseName: string;
120
+ storageAdapter: StorageAdapter;
121
+ /**
122
+ * Sync configuration - either per-table APIs or batch sync.
123
+ * Per-table: `{ tableName: { add, update, remove, list } }`
124
+ * Batch: `{ syncTables: [...], push, pull }`
125
+ */
126
+ sync: Partial<Record<keyof TStoreMap, ApiFunctions>> | BatchSync;
127
+ options?: SyncOptions;
128
+ }
98
129
  interface FirstLoadProgress {
99
130
  table: string;
100
131
  inserted: number;
@@ -105,7 +136,8 @@ type FirstLoadProgressCallback = (progress: FirstLoadProgress) => void;
105
136
  type SyncApi = {
106
137
  enable: (enabled: boolean) => Promise<void>;
107
138
  startFirstLoad: (onProgress?: FirstLoadProgressCallback) => Promise<void>;
108
- getState: () => SyncState;
139
+ /** Current sync state - use useSyncState() hook for reactive updates in React */
140
+ readonly state: SyncState;
109
141
  resolveConflict: (localId: string, keepLocal: boolean) => Promise<void>;
110
142
  onStateChange: (fn: (state: SyncState) => void) => () => void;
111
143
  onMutation: (fn: (event: MutationEvent) => void) => () => void;
@@ -151,89 +183,8 @@ interface FieldConflict {
151
183
  localValue: any;
152
184
  remoteValue: any;
153
185
  }
154
- type TableMap<TStoreMap extends Record<string, unknown>> = {
186
+ type TableMap<TStoreMap extends Record<string, any>> = {
155
187
  [K in keyof TStoreMap]: StorageTable<TStoreMap[K]>;
156
188
  };
157
189
 
158
- declare class DyncBase<_TStoreMap = Record<string, any>> {
159
- private readonly adapter;
160
- private readonly tableCache;
161
- private readonly mutationWrappedTables;
162
- private readonly syncEnhancedTables;
163
- private readonly mutationListeners;
164
- private visibilitySubscription?;
165
- private openPromise?;
166
- private disableSyncPromise?;
167
- private disableSyncPromiseResolver?;
168
- private sleepAbortController?;
169
- private closing;
170
- private syncApis;
171
- private batchSync?;
172
- private syncedTables;
173
- private syncOptions;
174
- private logger;
175
- private syncTimerStarted;
176
- private mutationsDuringSync;
177
- private state;
178
- readonly name: string;
179
- /**
180
- * Create a new Dync instance.
181
- *
182
- * Mode 1 - Per-table endpoints:
183
- * @param databaseName - Name of the database
184
- * @param syncApis - Map of table names to API functions
185
- * @param storageAdapter - Storage adapter implementation (required)
186
- * @param options - Sync options
187
- *
188
- * Mode 2 - Batch endpoints:
189
- * @param databaseName - Name of the database
190
- * @param batchSync - Batch sync config (syncTables, push, pull, firstLoad)
191
- * @param storageAdapter - Storage adapter implementation (required)
192
- * @param options - Sync options
193
- */
194
- constructor(databaseName: string, syncApis: Record<string, ApiFunctions>, storageAdapter: StorageAdapter, options?: SyncOptions);
195
- constructor(databaseName: string, batchSync: BatchSync, storageAdapter: StorageAdapter, options?: SyncOptions);
196
- version(versionNumber: number): {
197
- stores(schema: Record<string, TableSchemaDefinition>): /*elided*/ any;
198
- sqlite(configure: (builder: SQLiteVersionConfigurator) => void): /*elided*/ any;
199
- };
200
- open(): Promise<void>;
201
- close(): Promise<void>;
202
- delete(): Promise<void>;
203
- query<R>(callback: (ctx: DexieQueryContext | SqliteQueryContext | MemoryQueryContext) => Promise<R>): Promise<R>;
204
- table<K extends keyof _TStoreMap>(name: K): StorageTable<_TStoreMap[K]>;
205
- table<T = any>(name: string): StorageTable<T>;
206
- private withTransaction;
207
- private setupEnhancedTables;
208
- private injectSyncColumns;
209
- private enhanceSyncTable;
210
- private syncOnce;
211
- private pullAll;
212
- private pushAll;
213
- private startSyncTimer;
214
- private tryStart;
215
- private setupVisibilityListener;
216
- private handleVisibilityChange;
217
- private startFirstLoad;
218
- private getSyncState;
219
- private resolveConflict;
220
- private enableSync;
221
- get syncStatus(): SyncStatus;
222
- set syncStatus(status: SyncStatus);
223
- private onSyncStateChange;
224
- private onMutation;
225
- private emitMutation;
226
- sync: SyncApi;
227
- }
228
- type DyncInstance<TStoreMap extends Record<string, unknown> = Record<string, unknown>> = DyncBase<TStoreMap> & TableMap<TStoreMap> & {
229
- table<K extends keyof TStoreMap & string>(name: K): StorageTable<TStoreMap[K]>;
230
- table(name: string): StorageTable<any>;
231
- };
232
- declare const Dync: {
233
- new <TStoreMap extends Record<string, unknown> = Record<string, unknown>>(databaseName: string, syncApis: Record<string, ApiFunctions>, storageAdapter: StorageAdapter, options?: SyncOptions): DyncInstance<TStoreMap>;
234
- new <TStoreMap extends Record<string, unknown> = Record<string, unknown>>(databaseName: string, batchSync: BatchSync, storageAdapter: StorageAdapter, options?: SyncOptions): DyncInstance<TStoreMap>;
235
- prototype: DyncInstance<Record<string, unknown>>;
236
- } & typeof DyncBase;
237
- type Dync<TStoreMap extends Record<string, unknown> = Record<string, unknown>> = DyncInstance<TStoreMap>;
238
-
239
- export { type AfterRemoteAddCallback as A, type BatchSync as B, type ConflictResolutionStrategy as C, Dync as D, type FirstLoadProgress as F, type MissingRemoteRecordStrategy as M, SyncAction as S, type TableMap as T, type ApiFunctions as a, type BatchPushPayload as b, type BatchPushResult as c, type BatchFirstLoadResult as d, type FirstLoadProgressCallback as e, type MissingRemoteRecordDuringUpdateCallback as f, type MutationEvent as g, type SyncOptions as h, type SyncState as i, type SyncedRecord as j };
190
+ export { type AfterRemoteAddCallback as A, type BatchSync as B, type ConflictResolutionStrategy as C, type DyncOptions as D, type FirstLoadProgress as F, type MissingRemoteRecordStrategy as M, type SyncStatus as S, type TableMap as T, type SyncApi as a, SyncAction as b, type ApiFunctions as c, type BatchPushPayload as d, type BatchPushResult as e, type BatchFirstLoadResult as f, type FirstLoadProgressCallback as g, type MissingRemoteRecordDuringUpdateCallback as h, type MutationEvent as i, type SyncOptions as j, type SyncState as k, type SyncedRecord as l };
@@ -1,5 +1,4 @@
1
- import { d as StorageTable, c as StorageAdapter, T as TableSchemaDefinition, D as DexieQueryContext, b as SqliteQueryContext, a as MemoryQueryContext } from './dexie-BqktVP7s.cjs';
2
- import { c as SQLiteVersionConfigurator } from './types-CSbIAfu2.cjs';
1
+ import { d as StorageAdapter, a as StorageTable } from './dexie-T9m1mP1h.cjs';
3
2
 
4
3
  type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
5
4
  interface Logger {
@@ -95,6 +94,38 @@ interface SyncOptions {
95
94
  onAfterMissingRemoteRecordDuringUpdate?: MissingRemoteRecordDuringUpdateCallback;
96
95
  conflictResolutionStrategy?: ConflictResolutionStrategy;
97
96
  }
97
+ /**
98
+ * Configuration options for creating a Dync instance.
99
+ *
100
+ * @example Per-table sync mode
101
+ * ```ts
102
+ * const db = new Dync<Store>({
103
+ * databaseName: 'my-app',
104
+ * storageAdapter: new SQLiteAdapter(driver),
105
+ * sync: { todos: todoSyncApi },
106
+ * });
107
+ * ```
108
+ *
109
+ * @example Batch sync mode
110
+ * ```ts
111
+ * const db = new Dync<Store>({
112
+ * databaseName: 'my-app',
113
+ * storageAdapter: new SQLiteAdapter(driver),
114
+ * sync: { syncTables: ['todos'], push, pull },
115
+ * });
116
+ * ```
117
+ */
118
+ interface DyncOptions<TStoreMap extends Record<string, any> = Record<string, any>> {
119
+ databaseName: string;
120
+ storageAdapter: StorageAdapter;
121
+ /**
122
+ * Sync configuration - either per-table APIs or batch sync.
123
+ * Per-table: `{ tableName: { add, update, remove, list } }`
124
+ * Batch: `{ syncTables: [...], push, pull }`
125
+ */
126
+ sync: Partial<Record<keyof TStoreMap, ApiFunctions>> | BatchSync;
127
+ options?: SyncOptions;
128
+ }
98
129
  interface FirstLoadProgress {
99
130
  table: string;
100
131
  inserted: number;
@@ -105,7 +136,8 @@ type FirstLoadProgressCallback = (progress: FirstLoadProgress) => void;
105
136
  type SyncApi = {
106
137
  enable: (enabled: boolean) => Promise<void>;
107
138
  startFirstLoad: (onProgress?: FirstLoadProgressCallback) => Promise<void>;
108
- getState: () => SyncState;
139
+ /** Current sync state - use useSyncState() hook for reactive updates in React */
140
+ readonly state: SyncState;
109
141
  resolveConflict: (localId: string, keepLocal: boolean) => Promise<void>;
110
142
  onStateChange: (fn: (state: SyncState) => void) => () => void;
111
143
  onMutation: (fn: (event: MutationEvent) => void) => () => void;
@@ -151,89 +183,8 @@ interface FieldConflict {
151
183
  localValue: any;
152
184
  remoteValue: any;
153
185
  }
154
- type TableMap<TStoreMap extends Record<string, unknown>> = {
186
+ type TableMap<TStoreMap extends Record<string, any>> = {
155
187
  [K in keyof TStoreMap]: StorageTable<TStoreMap[K]>;
156
188
  };
157
189
 
158
- declare class DyncBase<_TStoreMap = Record<string, any>> {
159
- private readonly adapter;
160
- private readonly tableCache;
161
- private readonly mutationWrappedTables;
162
- private readonly syncEnhancedTables;
163
- private readonly mutationListeners;
164
- private visibilitySubscription?;
165
- private openPromise?;
166
- private disableSyncPromise?;
167
- private disableSyncPromiseResolver?;
168
- private sleepAbortController?;
169
- private closing;
170
- private syncApis;
171
- private batchSync?;
172
- private syncedTables;
173
- private syncOptions;
174
- private logger;
175
- private syncTimerStarted;
176
- private mutationsDuringSync;
177
- private state;
178
- readonly name: string;
179
- /**
180
- * Create a new Dync instance.
181
- *
182
- * Mode 1 - Per-table endpoints:
183
- * @param databaseName - Name of the database
184
- * @param syncApis - Map of table names to API functions
185
- * @param storageAdapter - Storage adapter implementation (required)
186
- * @param options - Sync options
187
- *
188
- * Mode 2 - Batch endpoints:
189
- * @param databaseName - Name of the database
190
- * @param batchSync - Batch sync config (syncTables, push, pull, firstLoad)
191
- * @param storageAdapter - Storage adapter implementation (required)
192
- * @param options - Sync options
193
- */
194
- constructor(databaseName: string, syncApis: Record<string, ApiFunctions>, storageAdapter: StorageAdapter, options?: SyncOptions);
195
- constructor(databaseName: string, batchSync: BatchSync, storageAdapter: StorageAdapter, options?: SyncOptions);
196
- version(versionNumber: number): {
197
- stores(schema: Record<string, TableSchemaDefinition>): /*elided*/ any;
198
- sqlite(configure: (builder: SQLiteVersionConfigurator) => void): /*elided*/ any;
199
- };
200
- open(): Promise<void>;
201
- close(): Promise<void>;
202
- delete(): Promise<void>;
203
- query<R>(callback: (ctx: DexieQueryContext | SqliteQueryContext | MemoryQueryContext) => Promise<R>): Promise<R>;
204
- table<K extends keyof _TStoreMap>(name: K): StorageTable<_TStoreMap[K]>;
205
- table<T = any>(name: string): StorageTable<T>;
206
- private withTransaction;
207
- private setupEnhancedTables;
208
- private injectSyncColumns;
209
- private enhanceSyncTable;
210
- private syncOnce;
211
- private pullAll;
212
- private pushAll;
213
- private startSyncTimer;
214
- private tryStart;
215
- private setupVisibilityListener;
216
- private handleVisibilityChange;
217
- private startFirstLoad;
218
- private getSyncState;
219
- private resolveConflict;
220
- private enableSync;
221
- get syncStatus(): SyncStatus;
222
- set syncStatus(status: SyncStatus);
223
- private onSyncStateChange;
224
- private onMutation;
225
- private emitMutation;
226
- sync: SyncApi;
227
- }
228
- type DyncInstance<TStoreMap extends Record<string, unknown> = Record<string, unknown>> = DyncBase<TStoreMap> & TableMap<TStoreMap> & {
229
- table<K extends keyof TStoreMap & string>(name: K): StorageTable<TStoreMap[K]>;
230
- table(name: string): StorageTable<any>;
231
- };
232
- declare const Dync: {
233
- new <TStoreMap extends Record<string, unknown> = Record<string, unknown>>(databaseName: string, syncApis: Record<string, ApiFunctions>, storageAdapter: StorageAdapter, options?: SyncOptions): DyncInstance<TStoreMap>;
234
- new <TStoreMap extends Record<string, unknown> = Record<string, unknown>>(databaseName: string, batchSync: BatchSync, storageAdapter: StorageAdapter, options?: SyncOptions): DyncInstance<TStoreMap>;
235
- prototype: DyncInstance<Record<string, unknown>>;
236
- } & typeof DyncBase;
237
- type Dync<TStoreMap extends Record<string, unknown> = Record<string, unknown>> = DyncInstance<TStoreMap>;
238
-
239
- export { type AfterRemoteAddCallback as A, type BatchSync as B, type ConflictResolutionStrategy as C, Dync as D, type FirstLoadProgress as F, type MissingRemoteRecordStrategy as M, SyncAction as S, type TableMap as T, type ApiFunctions as a, type BatchPushPayload as b, type BatchPushResult as c, type BatchFirstLoadResult as d, type FirstLoadProgressCallback as e, type MissingRemoteRecordDuringUpdateCallback as f, type MutationEvent as g, type SyncOptions as h, type SyncState as i, type SyncedRecord as j };
190
+ export { type AfterRemoteAddCallback as A, type BatchSync as B, type ConflictResolutionStrategy as C, type DyncOptions as D, type FirstLoadProgress as F, type MissingRemoteRecordStrategy as M, type SyncStatus as S, type TableMap as T, type SyncApi as a, SyncAction as b, type ApiFunctions as c, type BatchPushPayload as d, type BatchPushResult as e, type BatchFirstLoadResult as f, type FirstLoadProgressCallback as g, type MissingRemoteRecordDuringUpdateCallback as h, type MutationEvent as i, type SyncOptions as j, type SyncState as k, type SyncedRecord as l };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anfenn/dync",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "private": false,
5
5
  "description": "Write once, run IndexedDB & SQLite with sync anywhere - React, React Native, Expo, Capacitor, Electron & Node.js",
6
6
  "keywords": [
@@ -16,6 +16,7 @@ export type {
16
16
  BatchPushResult,
17
17
  BatchFirstLoadResult,
18
18
  ConflictResolutionStrategy,
19
+ DyncOptions,
19
20
  FirstLoadProgress,
20
21
  FirstLoadProgressCallback,
21
22
  MissingRemoteRecordStrategy,
@@ -3,6 +3,7 @@ import { sleep } from './helpers';
3
3
  import {
4
4
  type ApiFunctions,
5
5
  type BatchSync,
6
+ type DyncOptions,
6
7
  type SyncOptions,
7
8
  type SyncState,
8
9
  type SyncedRecord,
@@ -36,7 +37,7 @@ const DEFAULT_MIN_LOG_LEVEL: LogLevel = 'debug';
36
37
  const DEFAULT_MISSING_REMOTE_RECORD_STRATEGY: MissingRemoteRecordStrategy = 'insert-remote-record';
37
38
  const DEFAULT_CONFLICT_RESOLUTION_STRATEGY: ConflictResolutionStrategy = 'try-shallow-merge';
38
39
 
39
- class DyncBase<_TStoreMap = Record<string, any>> {
40
+ class DyncBase<_TStoreMap extends Record<string, any> = Record<string, any>> {
40
41
  private readonly adapter: StorageAdapter;
41
42
  private readonly tableCache = new Map<string, StorageTable<any>>();
42
43
  private readonly mutationWrappedTables = new Set<string>();
@@ -63,31 +64,35 @@ class DyncBase<_TStoreMap = Record<string, any>> {
63
64
  /**
64
65
  * Create a new Dync instance.
65
66
  *
66
- * Mode 1 - Per-table endpoints:
67
- * @param databaseName - Name of the database
68
- * @param syncApis - Map of table names to API functions
69
- * @param storageAdapter - Storage adapter implementation (required)
70
- * @param options - Sync options
67
+ * @example Per-table sync mode
68
+ * ```ts
69
+ * const db = new Dync<Store>({
70
+ * databaseName: 'my-app',
71
+ * storageAdapter: new SQLiteAdapter(driver),
72
+ * sync: { todos: todoSyncApi },
73
+ * });
74
+ * ```
71
75
  *
72
- * Mode 2 - Batch endpoints:
73
- * @param databaseName - Name of the database
74
- * @param batchSync - Batch sync config (syncTables, push, pull, firstLoad)
75
- * @param storageAdapter - Storage adapter implementation (required)
76
- * @param options - Sync options
76
+ * @example Batch sync mode
77
+ * ```ts
78
+ * const db = new Dync<Store>({
79
+ * databaseName: 'my-app',
80
+ * storageAdapter: new SQLiteAdapter(driver),
81
+ * sync: { syncTables: ['todos'], push, pull },
82
+ * });
83
+ * ```
77
84
  */
78
- constructor(databaseName: string, syncApis: Record<string, ApiFunctions>, storageAdapter: StorageAdapter, options?: SyncOptions);
79
- constructor(databaseName: string, batchSync: BatchSync, storageAdapter: StorageAdapter, options?: SyncOptions);
80
- constructor(databaseName: string, syncApisOrBatchSync: Record<string, ApiFunctions> | BatchSync, storageAdapter: StorageAdapter, options?: SyncOptions) {
81
- // Detect mode based on whether the second arg has sync API shape (has 'list' function)
82
- const isBatchMode = typeof (syncApisOrBatchSync as BatchSync).push === 'function';
85
+ constructor(config: DyncOptions<_TStoreMap>) {
86
+ const { databaseName, storageAdapter, sync: syncConfig, options } = config;
87
+
88
+ // Detect mode based on whether sync config has batch sync shape
89
+ const isBatchMode = typeof (syncConfig as BatchSync).push === 'function' && typeof (syncConfig as BatchSync).pull === 'function';
83
90
 
84
91
  if (isBatchMode) {
85
- // Batch mode: (databaseName, batchSync, options?)
86
- this.batchSync = syncApisOrBatchSync as BatchSync;
92
+ this.batchSync = syncConfig as BatchSync;
87
93
  this.syncedTables = new Set(this.batchSync.syncTables);
88
94
  } else {
89
- // Per-table mode: (databaseName, syncApis, options?)
90
- this.syncApis = syncApisOrBatchSync as Record<string, ApiFunctions>;
95
+ this.syncApis = syncConfig as Record<string, ApiFunctions>;
91
96
  this.syncedTables = new Set(Object.keys(this.syncApis));
92
97
  }
93
98
 
@@ -107,6 +112,12 @@ class DyncBase<_TStoreMap = Record<string, any>> {
107
112
  storageAdapter: this.adapter,
108
113
  });
109
114
 
115
+ // Define state getter on sync object (can't use getter syntax in object literal with proper `this` binding)
116
+ Object.defineProperty(this.sync, 'state', {
117
+ get: () => this.getSyncState(),
118
+ enumerable: true,
119
+ });
120
+
110
121
  const driverInfo = 'driverType' in this.adapter ? ` (Driver: ${this.adapter.driverType})` : '';
111
122
  this.logger.debug(`[dync] Initialized with ${this.adapter.type}${driverInfo}`);
112
123
  }
@@ -171,6 +182,19 @@ class DyncBase<_TStoreMap = Record<string, any>> {
171
182
  self.adapter.defineSchema(versionNumber, fullSchema, schemaOptions);
172
183
  self.setupEnhancedTables(Object.keys(schema));
173
184
 
185
+ // Define getters for direct table access (e.g., db.todos)
186
+ for (const tableName of Object.keys(schema)) {
187
+ if (!(tableName in self)) {
188
+ Object.defineProperty(self, tableName, {
189
+ get() {
190
+ return self.table(tableName);
191
+ },
192
+ enumerable: true,
193
+ configurable: false,
194
+ });
195
+ }
196
+ }
197
+
174
198
  return builder;
175
199
  },
176
200
  sqlite(configure: (builder: SQLiteVersionConfigurator) => void) {
@@ -577,34 +601,23 @@ class DyncBase<_TStoreMap = Record<string, any>> {
577
601
  sync: SyncApi = {
578
602
  enable: this.enableSync.bind(this),
579
603
  startFirstLoad: this.startFirstLoad.bind(this),
580
- getState: this.getSyncState.bind(this),
604
+ state: undefined as unknown as SyncState, // getter in constructor
581
605
  resolveConflict: this.resolveConflict.bind(this),
582
606
  onStateChange: this.onSyncStateChange.bind(this),
583
607
  onMutation: this.onMutation.bind(this),
584
608
  };
585
609
  }
586
610
 
587
- type DyncInstance<TStoreMap extends Record<string, unknown> = Record<string, unknown>> = DyncBase<TStoreMap> &
611
+ type DyncInstance<TStoreMap extends Record<string, any> = Record<string, any>> = DyncBase<TStoreMap> &
588
612
  TableMap<TStoreMap> & {
589
613
  table<K extends keyof TStoreMap & string>(name: K): StorageTable<TStoreMap[K]>;
590
614
  table(name: string): StorageTable<any>;
591
615
  };
592
616
 
593
- const DyncConstructor = DyncBase as unknown as {
594
- prototype: DyncInstance<Record<string, unknown>>;
595
- new <TStoreMap extends Record<string, unknown> = Record<string, unknown>>(
596
- databaseName: string,
597
- syncApis: Record<string, ApiFunctions>,
598
- storageAdapter: StorageAdapter,
599
- options?: SyncOptions,
600
- ): DyncInstance<TStoreMap>;
601
- new <TStoreMap extends Record<string, unknown> = Record<string, unknown>>(
602
- databaseName: string,
603
- batchSync: BatchSync,
604
- storageAdapter: StorageAdapter,
605
- options?: SyncOptions,
606
- ): DyncInstance<TStoreMap>;
607
- } & typeof DyncBase;
608
-
609
- export const Dync = DyncConstructor;
610
- export type Dync<TStoreMap extends Record<string, unknown> = Record<string, unknown>> = DyncInstance<TStoreMap>;
617
+ // Export Dync as a class-like constructor with proper typing for direct table access
618
+ export const Dync = DyncBase as unknown as {
619
+ <TStoreMap extends Record<string, any> = Record<string, any>>(config: DyncOptions<TStoreMap>): DyncInstance<TStoreMap>;
620
+ new <TStoreMap extends Record<string, any> = Record<string, any>>(config: DyncOptions<TStoreMap>): DyncInstance<TStoreMap>;
621
+ };
622
+
623
+ export type Dync<TStoreMap extends Record<string, any> = Record<string, any>> = DyncInstance<TStoreMap>;
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export type {
16
16
  BatchPushResult,
17
17
  BatchFirstLoadResult,
18
18
  ConflictResolutionStrategy,
19
+ DyncOptions,
19
20
  FirstLoadProgress,
20
21
  FirstLoadProgressCallback,
21
22
  MissingRemoteRecordStrategy,
@@ -1,2 +1,3 @@
1
- export { makeDync } from './useDync';
2
- export type { BoundUseLiveQuery, MakeDyncConfig, MakeDyncConfigBatch, MakeDyncConfigPerTable, MakeDyncResult, UseDyncValue } from './useDync';
1
+ export { useSyncState } from './useSyncState';
2
+ export { useLiveQuery } from './useLiveQuery';
3
+ export type { DyncLike } from './types';
@@ -0,0 +1,6 @@
1
+ import type { SyncApi } from '../types';
2
+
3
+ /** Minimal database interface for React hooks */
4
+ export interface DyncLike {
5
+ sync: Pick<SyncApi, 'state' | 'onStateChange' | 'onMutation'>;
6
+ }