@anfenn/dync 1.0.25 → 1.0.26
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 +10 -6
- package/dist/{dexie-DRLMKLl5.d.ts → dexie-B1FuZqDB.d.ts} +1 -1
- package/dist/{dexie-BqktVP7s.d.cts → dexie-DtgGGM68.d.cts} +1 -1
- package/dist/dexie.d.cts +1 -1
- package/dist/dexie.d.ts +1 -1
- package/dist/index.cjs +6 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +87 -3
- package/dist/index.d.ts +87 -3
- package/dist/index.js +3831 -10
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +36 -1729
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +65 -31
- package/dist/react/index.d.ts +65 -31
- package/dist/react/index.js +31 -41
- package/dist/react/index.js.map +1 -1
- package/dist/{index.shared-D-fB8Odd.d.ts → types-B1Vn8_DF.d.ts} +4 -85
- package/dist/{index.shared-DHuT0l_t.d.cts → types-BgGGLrE9.d.cts} +4 -85
- package/package.json +1 -1
- package/src/index.shared.ts +7 -1
- package/src/react/index.ts +3 -2
- package/src/react/types.ts +6 -0
- package/src/react/useLiveQuery.ts +97 -0
- package/src/react/useSyncState.ts +45 -0
- package/src/types.ts +2 -1
- package/dist/chunk-QA2TX54K.js +0 -3835
- package/dist/chunk-QA2TX54K.js.map +0 -1
- package/src/react/useDync.ts +0 -156
package/dist/react/index.js
CHANGED
|
@@ -1,50 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
21
|
-
};
|
|
22
|
-
|
|
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
|
-
|
|
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 =
|
|
39
|
-
const queryVersionRef =
|
|
40
|
-
const querierRef =
|
|
41
|
-
const tablesRef =
|
|
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 =
|
|
33
|
+
const runQuery = useCallback2(async () => {
|
|
45
34
|
const currentVersion = ++queryVersionRef.current;
|
|
46
35
|
try {
|
|
47
|
-
const queryResult = await querierRef.current(
|
|
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
|
-
}, [
|
|
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
|
-
|
|
65
|
+
useLiveQuery,
|
|
66
|
+
useSyncState
|
|
77
67
|
};
|
|
78
68
|
//# sourceMappingURL=index.js.map
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/
|
|
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 {
|
|
2
|
-
import { c as SQLiteVersionConfigurator } from './types-CSbIAfu2.js';
|
|
1
|
+
import { b as StorageTable } from './dexie-B1FuZqDB.js';
|
|
3
2
|
|
|
4
3
|
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
|
5
4
|
interface Logger {
|
|
@@ -105,7 +104,8 @@ type FirstLoadProgressCallback = (progress: FirstLoadProgress) => void;
|
|
|
105
104
|
type SyncApi = {
|
|
106
105
|
enable: (enabled: boolean) => Promise<void>;
|
|
107
106
|
startFirstLoad: (onProgress?: FirstLoadProgressCallback) => Promise<void>;
|
|
108
|
-
|
|
107
|
+
/** Current sync state - use useSyncState() hook for reactive updates in React */
|
|
108
|
+
readonly state: SyncState;
|
|
109
109
|
resolveConflict: (localId: string, keepLocal: boolean) => Promise<void>;
|
|
110
110
|
onStateChange: (fn: (state: SyncState) => void) => () => void;
|
|
111
111
|
onMutation: (fn: (event: MutationEvent) => void) => () => void;
|
|
@@ -155,85 +155,4 @@ type TableMap<TStoreMap extends Record<string, unknown>> = {
|
|
|
155
155
|
[K in keyof TStoreMap]: StorageTable<TStoreMap[K]>;
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
-
|
|
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 };
|
|
158
|
+
export { type ApiFunctions as A, type BatchSync as B, type ConflictResolutionStrategy as C, type FirstLoadProgress as F, type MissingRemoteRecordStrategy as M, type SyncOptions as S, type TableMap as T, type SyncStatus as a, type SyncApi as b, SyncAction as c, type AfterRemoteAddCallback as d, type BatchPushPayload as e, type BatchPushResult as f, type BatchFirstLoadResult as g, type FirstLoadProgressCallback as h, type MissingRemoteRecordDuringUpdateCallback as i, type MutationEvent as j, type SyncState as k, type SyncedRecord as l };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as SQLiteVersionConfigurator } from './types-CSbIAfu2.cjs';
|
|
1
|
+
import { b as StorageTable } from './dexie-DtgGGM68.cjs';
|
|
3
2
|
|
|
4
3
|
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
|
|
5
4
|
interface Logger {
|
|
@@ -105,7 +104,8 @@ type FirstLoadProgressCallback = (progress: FirstLoadProgress) => void;
|
|
|
105
104
|
type SyncApi = {
|
|
106
105
|
enable: (enabled: boolean) => Promise<void>;
|
|
107
106
|
startFirstLoad: (onProgress?: FirstLoadProgressCallback) => Promise<void>;
|
|
108
|
-
|
|
107
|
+
/** Current sync state - use useSyncState() hook for reactive updates in React */
|
|
108
|
+
readonly state: SyncState;
|
|
109
109
|
resolveConflict: (localId: string, keepLocal: boolean) => Promise<void>;
|
|
110
110
|
onStateChange: (fn: (state: SyncState) => void) => () => void;
|
|
111
111
|
onMutation: (fn: (event: MutationEvent) => void) => () => void;
|
|
@@ -155,85 +155,4 @@ type TableMap<TStoreMap extends Record<string, unknown>> = {
|
|
|
155
155
|
[K in keyof TStoreMap]: StorageTable<TStoreMap[K]>;
|
|
156
156
|
};
|
|
157
157
|
|
|
158
|
-
|
|
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 };
|
|
158
|
+
export { type ApiFunctions as A, type BatchSync as B, type ConflictResolutionStrategy as C, type FirstLoadProgress as F, type MissingRemoteRecordStrategy as M, type SyncOptions as S, type TableMap as T, type SyncStatus as a, type SyncApi as b, SyncAction as c, type AfterRemoteAddCallback as d, type BatchPushPayload as e, type BatchPushResult as f, type BatchFirstLoadResult as g, type FirstLoadProgressCallback as h, type MissingRemoteRecordDuringUpdateCallback as i, type MutationEvent as j, type SyncState as k, type SyncedRecord as l };
|
package/package.json
CHANGED
package/src/index.shared.ts
CHANGED
|
@@ -107,6 +107,12 @@ class DyncBase<_TStoreMap = Record<string, any>> {
|
|
|
107
107
|
storageAdapter: this.adapter,
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
// Define state getter on sync object (can't use getter syntax in object literal with proper `this` binding)
|
|
111
|
+
Object.defineProperty(this.sync, 'state', {
|
|
112
|
+
get: () => this.getSyncState(),
|
|
113
|
+
enumerable: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
110
116
|
const driverInfo = 'driverType' in this.adapter ? ` (Driver: ${this.adapter.driverType})` : '';
|
|
111
117
|
this.logger.debug(`[dync] Initialized with ${this.adapter.type}${driverInfo}`);
|
|
112
118
|
}
|
|
@@ -577,7 +583,7 @@ class DyncBase<_TStoreMap = Record<string, any>> {
|
|
|
577
583
|
sync: SyncApi = {
|
|
578
584
|
enable: this.enableSync.bind(this),
|
|
579
585
|
startFirstLoad: this.startFirstLoad.bind(this),
|
|
580
|
-
|
|
586
|
+
state: undefined as unknown as SyncState, // getter in constructor
|
|
581
587
|
resolveConflict: this.resolveConflict.bind(this),
|
|
582
588
|
onStateChange: this.onSyncStateChange.bind(this),
|
|
583
589
|
onMutation: this.onMutation.bind(this),
|
package/src/react/index.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export
|
|
1
|
+
export { useSyncState } from './useSyncState';
|
|
2
|
+
export { useLiveQuery } from './useLiveQuery';
|
|
3
|
+
export type { DyncLike } from './types';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { MutationEvent } from '../types';
|
|
3
|
+
import type { DyncLike } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* React hook that observes database queries and automatically re-runs them when data changes.
|
|
7
|
+
*
|
|
8
|
+
* @param db - The Dync database instance
|
|
9
|
+
* @param querier - Function that returns query result (can be async)
|
|
10
|
+
* @param deps - Optional React dependency list for re-running the query
|
|
11
|
+
* @param tables - Optional array of table names to watch for mutations (defaults to all tables)
|
|
12
|
+
* @returns The query result, or undefined if not yet available
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* import { db } from './store';
|
|
17
|
+
* import { useLiveQuery } from '@anfenn/dync/react';
|
|
18
|
+
*
|
|
19
|
+
* function TodoList() {
|
|
20
|
+
* const todos = useLiveQuery(db, () => db.todos.toArray());
|
|
21
|
+
* return <ul>{todos?.map(t => <li key={t._localId}>{t.title}</li>)}</ul>;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // With dependencies
|
|
25
|
+
* function FilteredTodos({ filter }: { filter: string }) {
|
|
26
|
+
* const todos = useLiveQuery(
|
|
27
|
+
* db,
|
|
28
|
+
* () => db.todos.where('status').equals(filter).toArray(),
|
|
29
|
+
* [filter]
|
|
30
|
+
* );
|
|
31
|
+
* return <ul>{todos?.map(t => <li key={t._localId}>{t.title}</li>)}</ul>;
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* // Watch specific tables only
|
|
35
|
+
* function TodoCount() {
|
|
36
|
+
* const count = useLiveQuery(
|
|
37
|
+
* db,
|
|
38
|
+
* () => db.todos.count(),
|
|
39
|
+
* [],
|
|
40
|
+
* ['todos']
|
|
41
|
+
* );
|
|
42
|
+
* return <span>{count ?? 0} todos</span>;
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useLiveQuery<T>(db: DyncLike, querier: () => Promise<T> | T, deps: React.DependencyList = [], tables?: string[]): T | undefined {
|
|
47
|
+
const [result, setResult] = useState<T | undefined>(undefined);
|
|
48
|
+
const [, setError] = useState<Error | null>(null);
|
|
49
|
+
const isMountedRef = useRef(true);
|
|
50
|
+
const queryVersionRef = useRef(0);
|
|
51
|
+
const querierRef = useRef(querier);
|
|
52
|
+
const tablesRef = useRef(tables);
|
|
53
|
+
|
|
54
|
+
// Keep refs up to date
|
|
55
|
+
querierRef.current = querier;
|
|
56
|
+
tablesRef.current = tables;
|
|
57
|
+
|
|
58
|
+
const runQuery = useCallback(async () => {
|
|
59
|
+
const currentVersion = ++queryVersionRef.current;
|
|
60
|
+
try {
|
|
61
|
+
const queryResult = await querierRef.current();
|
|
62
|
+
// Only update if still mounted and this is the latest query
|
|
63
|
+
if (isMountedRef.current && currentVersion === queryVersionRef.current) {
|
|
64
|
+
setResult(queryResult);
|
|
65
|
+
setError(null);
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (isMountedRef.current && currentVersion === queryVersionRef.current) {
|
|
69
|
+
setError(err as Error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
// Re-run query when deps change
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
runQuery();
|
|
77
|
+
}, [...deps, runQuery]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
isMountedRef.current = true;
|
|
81
|
+
|
|
82
|
+
// Subscribe to mutation events
|
|
83
|
+
const unsubscribe = db.sync.onMutation((event: MutationEvent) => {
|
|
84
|
+
// Only re-run if no tables filter specified, or if the mutation affects a watched table
|
|
85
|
+
if (!tablesRef.current || tablesRef.current.includes(event.tableName)) {
|
|
86
|
+
runQuery();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
isMountedRef.current = false;
|
|
92
|
+
unsubscribe();
|
|
93
|
+
};
|
|
94
|
+
}, [db, runQuery]);
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
2
|
+
import type { SyncState } from '../types';
|
|
3
|
+
import type { DyncLike } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* React hook that subscribes to Dync sync state changes.
|
|
7
|
+
* Returns the current SyncState and re-renders when it changes.
|
|
8
|
+
*
|
|
9
|
+
* @param db - The Dync database instance
|
|
10
|
+
* @returns The current SyncState
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { db } from './store';
|
|
15
|
+
* import { useSyncState } from '@anfenn/dync/react';
|
|
16
|
+
*
|
|
17
|
+
* function SyncStatus() {
|
|
18
|
+
* const syncState = useSyncState(db);
|
|
19
|
+
* return <div>Status: {syncState.status}</div>;
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useSyncState(db: DyncLike): SyncState {
|
|
24
|
+
// Use refs to create stable subscribe/getSnapshot functions
|
|
25
|
+
const cacheRef = useRef<SyncState>(db.sync.state);
|
|
26
|
+
|
|
27
|
+
const subscribe = useCallback(
|
|
28
|
+
(listener: () => void) =>
|
|
29
|
+
db.sync.onStateChange((nextState) => {
|
|
30
|
+
cacheRef.current = nextState;
|
|
31
|
+
listener();
|
|
32
|
+
}),
|
|
33
|
+
[db],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const getSnapshot = useCallback(() => {
|
|
37
|
+
const fresh = db.sync.state;
|
|
38
|
+
if (JSON.stringify(fresh) !== JSON.stringify(cacheRef.current)) {
|
|
39
|
+
cacheRef.current = fresh;
|
|
40
|
+
}
|
|
41
|
+
return cacheRef.current;
|
|
42
|
+
}, [db]);
|
|
43
|
+
|
|
44
|
+
return useSyncExternalStore<SyncState>(subscribe, getSnapshot, getSnapshot);
|
|
45
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -134,7 +134,8 @@ export type FirstLoadProgressCallback = (progress: FirstLoadProgress) => void;
|
|
|
134
134
|
export type SyncApi = {
|
|
135
135
|
enable: (enabled: boolean) => Promise<void>;
|
|
136
136
|
startFirstLoad: (onProgress?: FirstLoadProgressCallback) => Promise<void>;
|
|
137
|
-
|
|
137
|
+
/** Current sync state - use useSyncState() hook for reactive updates in React */
|
|
138
|
+
readonly state: SyncState;
|
|
138
139
|
resolveConflict: (localId: string, keepLocal: boolean) => Promise<void>;
|
|
139
140
|
onStateChange: (fn: (state: SyncState) => void) => () => void;
|
|
140
141
|
onMutation: (fn: (event: MutationEvent) => void) => () => void;
|