@anfenn/zync 0.1.23 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -16
- package/dist/index.cjs +145 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +145 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@anfenn/zync)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Simple, bullet-proof, offline-first sync middleware for Zustand.
|
|
6
|
+
|
|
7
|
+
**_STATUS_**: Actively developed in alpha stage while requirements are being understood. Api may change, requests are welcome.
|
|
6
8
|
|
|
7
9
|
## Benefits
|
|
8
10
|
|
|
9
|
-
-
|
|
11
|
+
- Easy to sync non-nested array state with a backend (i.e. mirror remote database tables locally)
|
|
12
|
+
- **"It just works"** philosophy
|
|
13
|
+
- Batteries optionally included:
|
|
14
|
+
- IndexedDB helper (based on [idb](https://www.npmjs.com/package/idb))
|
|
10
15
|
- Uses the official persist middleware as the local storage (localStorage, IndexedDB, etc.)
|
|
11
16
|
- Zync's persistWithSync() is a drop-in replacement for Zustand's persist()
|
|
12
17
|
- Allows for idiomatic use of Zustand
|
|
13
18
|
- Leaves the api requests up to you (RESTful, GraphQL, etc.), just provide add(), update(), remove() and list()
|
|
14
|
-
- **_Coming soon_**: Customisable conflict resolution. Currently
|
|
19
|
+
- **_Coming soon_**: Customisable conflict resolution. Currently local-wins.
|
|
15
20
|
|
|
16
21
|
## Requirements
|
|
17
22
|
|
|
@@ -30,7 +35,7 @@ npm install @anfenn/zync
|
|
|
30
35
|
### Zustand store creation (store.ts):
|
|
31
36
|
|
|
32
37
|
```ts
|
|
33
|
-
import {
|
|
38
|
+
import { type UseStoreWithSync, persistWithSync } from '@anfenn/zync';
|
|
34
39
|
import { create } from 'zustand';
|
|
35
40
|
import { createJSONStorage } from 'zustand/middleware';
|
|
36
41
|
import { useShallow } from 'zustand/react/shallow';
|
|
@@ -45,32 +50,25 @@ type Store = {
|
|
|
45
50
|
|
|
46
51
|
export const useStore = create<any>()(
|
|
47
52
|
persistWithSync<Store>(
|
|
48
|
-
(set, get,
|
|
49
|
-
// Standard Zustand state and mutation functions with new
|
|
53
|
+
(set, get, setAndSync) => ({
|
|
54
|
+
// Standard Zustand state and mutation functions with new setAndSync()
|
|
50
55
|
|
|
51
56
|
facts: [],
|
|
52
57
|
addFact: (item: Fact) => {
|
|
53
58
|
const updated_at = new Date().toISOString();
|
|
54
59
|
const newItem = { ...item, created_at: updated_at, updated_at };
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
setAndSync((state: Store) => ({
|
|
57
62
|
facts: [...state.facts, newItem],
|
|
58
63
|
}));
|
|
59
|
-
|
|
60
|
-
// Never call queueToSync() inside Zustand set() due to itself calling set(), so may cause lost state changes
|
|
61
|
-
queueToSync(SyncAction.CreateOrUpdate, 'facts', item._localId);
|
|
62
64
|
},
|
|
63
65
|
updateFact: (localId: string, changes: Partial<Fact>) => {
|
|
64
|
-
|
|
66
|
+
setAndSync((state: Store) => ({
|
|
65
67
|
facts: state.facts.map((item) => (item._localId === localId ? { ...item, ...changes } : item)),
|
|
66
68
|
}));
|
|
67
|
-
|
|
68
|
-
queueToSync(SyncAction.CreateOrUpdate, 'facts', localId);
|
|
69
69
|
},
|
|
70
70
|
removeFact: (localId: string) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
set((state: Store) => ({
|
|
71
|
+
setAndSync((state: Store) => ({
|
|
74
72
|
facts: state.facts.filter((item) => item._localId !== localId),
|
|
75
73
|
}));
|
|
76
74
|
},
|
|
@@ -107,6 +105,9 @@ export const useFacts = () =>
|
|
|
107
105
|
);
|
|
108
106
|
```
|
|
109
107
|
|
|
108
|
+
**_NOTE_**: Zync uses an internal timer (setInterval) to sync, so it's advised to just have one store. You could have multiple, with different store names (see Zustand persist options above), but if both stores use Zync, although it would work fine, it wouldn't offer much advantage. If one store becomes large with many state keys and functions, then you could separate them into multiple files and import than with object spreading
|
|
109
|
+
`e.g. {...storeState1, ...storeState2}`
|
|
110
|
+
|
|
110
111
|
### In your component:
|
|
111
112
|
|
|
112
113
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -349,9 +349,42 @@ function findApi(stateKey, syncApi) {
|
|
|
349
349
|
}
|
|
350
350
|
return api;
|
|
351
351
|
}
|
|
352
|
+
function findChanges(current, updated) {
|
|
353
|
+
const currentMap = /* @__PURE__ */ new Map();
|
|
354
|
+
for (const item of current) {
|
|
355
|
+
if (item && item._localId) {
|
|
356
|
+
currentMap.set(item._localId, item);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const changesMap = /* @__PURE__ */ new Map();
|
|
360
|
+
for (const item of updated) {
|
|
361
|
+
if (item && item._localId) {
|
|
362
|
+
const curr = currentMap.get(item._localId);
|
|
363
|
+
if (curr) {
|
|
364
|
+
const diff = {};
|
|
365
|
+
for (const key in curr) {
|
|
366
|
+
if (key !== "_localId" && curr[key] !== item[key]) {
|
|
367
|
+
diff[key] = item[key];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (Object.keys(diff).length > 0) {
|
|
371
|
+
changesMap.set(item._localId, { currentItem: curr, updatedItem: item, changes: diff });
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
changesMap.set(item._localId, { currentItem: null, updatedItem: item, changes: item });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const [localId, curr] of currentMap) {
|
|
379
|
+
if (!updated.some((u) => u && u._localId === localId)) {
|
|
380
|
+
changesMap.set(localId, { currentItem: curr, updatedItem: null, changes: null });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return changesMap;
|
|
384
|
+
}
|
|
352
385
|
|
|
353
386
|
// src/pull.ts
|
|
354
|
-
async function pull(set, get, stateKey, api, logger) {
|
|
387
|
+
async function pull(set, get, stateKey, api, logger, conflictResolutionStrategy) {
|
|
355
388
|
const lastPulled = get().syncState.lastPulled || {};
|
|
356
389
|
const lastPulledAt = new Date(lastPulled[stateKey] || /* @__PURE__ */ new Date(0));
|
|
357
390
|
logger.debug(`[zync] pull:start stateKey=${stateKey} since=${lastPulledAt.toISOString()}`);
|
|
@@ -380,16 +413,53 @@ async function pull(set, get, stateKey, api, logger) {
|
|
|
380
413
|
continue;
|
|
381
414
|
}
|
|
382
415
|
delete remote.deleted;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
416
|
+
if (localItem) {
|
|
417
|
+
const pendingChange = pendingChanges.find((p) => p.stateKey === stateKey && p.localId === localItem._localId);
|
|
418
|
+
if (pendingChange) {
|
|
419
|
+
switch (conflictResolutionStrategy) {
|
|
420
|
+
case "local-wins":
|
|
421
|
+
logger.debug(`[zync] pull:conflict-strategy:${conflictResolutionStrategy} stateKey=${stateKey} id=${remote.id}`);
|
|
422
|
+
break;
|
|
423
|
+
case "remote-wins": {
|
|
424
|
+
const merged = {
|
|
425
|
+
...remote,
|
|
426
|
+
_localId: localItem._localId
|
|
427
|
+
};
|
|
428
|
+
nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
|
|
429
|
+
logger.debug(`[zync] pull:conflict-strategy:${conflictResolutionStrategy} stateKey=${stateKey} id=${remote.id}`);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
// case 'try-shallow-merge':
|
|
433
|
+
// // Try and merge all fields, fail if not possible due to conflicts
|
|
434
|
+
// // throw new ConflictError('Details...');
|
|
435
|
+
// Object.entries(pendingChange.changes || {}).map(([key, value]) => {
|
|
436
|
+
// const localValue = localItem[key];
|
|
437
|
+
// const remoteValue = remote[key];
|
|
438
|
+
// if (localValue !== undefined && localValue !== value) {
|
|
439
|
+
// //throw new ConflictError(`Conflict on ${key}: local=${localValue} remote=${value}`);
|
|
440
|
+
// }
|
|
441
|
+
// });
|
|
442
|
+
// break;
|
|
443
|
+
// case 'custom':
|
|
444
|
+
// // Hook to allow custom userland logic
|
|
445
|
+
// // const error = onConflict(localItem, remote, stateKey, pending);
|
|
446
|
+
// // logger.debug(`[zync] pull:conflict-strategy:${conflictResolutionStrategy} stateKey=${stateKey} id=${remote.id} error=${error}`);
|
|
447
|
+
// // if (error) throw new ConflictError(error);
|
|
448
|
+
// break;
|
|
449
|
+
default:
|
|
450
|
+
logger.error(`[zync] pull:conflict-strategy:unknown stateKey=${stateKey} id=${remote.id} strategy=${conflictResolutionStrategy}`);
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
const merged = {
|
|
455
|
+
...localItem,
|
|
456
|
+
...remote,
|
|
457
|
+
_localId: localItem._localId
|
|
458
|
+
};
|
|
459
|
+
nextItems = nextItems.map((i) => i._localId === localItem._localId ? merged : i);
|
|
460
|
+
logger.debug(`[zync] pull:merge-remote stateKey=${stateKey} id=${remote.id}`);
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
393
463
|
nextItems = [...nextItems, { ...remote, _localId: nextLocalId() }];
|
|
394
464
|
logger.debug(`[zync] pull:add stateKey=${stateKey} id=${remote.id}`);
|
|
395
465
|
}
|
|
@@ -409,7 +479,7 @@ async function pull(set, get, stateKey, api, logger) {
|
|
|
409
479
|
|
|
410
480
|
// src/push.ts
|
|
411
481
|
var SYNC_FIELDS = ["id", "_localId", "updated_at", "deleted"];
|
|
412
|
-
async function pushOne(set, get, change, api, logger,
|
|
482
|
+
async function pushOne(set, get, change, api, logger, setAndQueueToSync, missingStrategy, onMissingRemoteRecordDuringUpdate, onAfterRemoteAdd) {
|
|
413
483
|
logger.debug(`[zync] push:attempt action=${change.action} stateKey=${change.stateKey} localId=${change.localId}`);
|
|
414
484
|
const { action, stateKey, localId, id, version } = change;
|
|
415
485
|
switch (action) {
|
|
@@ -449,26 +519,27 @@ async function pushOne(set, get, change, api, logger, queueToSync, missingStrate
|
|
|
449
519
|
}
|
|
450
520
|
return;
|
|
451
521
|
} else {
|
|
452
|
-
logger.warn("[zync] push:update:missing-remote", {
|
|
453
|
-
stateKey,
|
|
454
|
-
localId,
|
|
455
|
-
id: item.id
|
|
456
|
-
});
|
|
457
522
|
switch (missingStrategy) {
|
|
458
523
|
case "delete-local-record":
|
|
459
524
|
set((s) => ({
|
|
460
525
|
[stateKey]: (s[stateKey] || []).filter((i) => i._localId !== localId)
|
|
461
526
|
}));
|
|
527
|
+
logger.debug(`[zync] push:missing-remote:${missingStrategy} stateKey=${stateKey} id=${item.id}`);
|
|
462
528
|
break;
|
|
463
|
-
case "insert-remote-record":
|
|
529
|
+
case "insert-remote-record":
|
|
464
530
|
omittedItem._localId = crypto.randomUUID();
|
|
465
531
|
omittedItem.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
466
|
-
|
|
532
|
+
setAndQueueToSync((s) => ({
|
|
467
533
|
[stateKey]: (s[stateKey] || []).map((i) => i._localId === localId ? omittedItem : i)
|
|
468
534
|
}));
|
|
469
|
-
|
|
535
|
+
logger.debug(`[zync] push:missing-remote:${missingStrategy} stateKey=${stateKey} id=${item.id}`);
|
|
536
|
+
break;
|
|
537
|
+
case "ignore":
|
|
538
|
+
logger.debug(`[zync] push:missing-remote:${missingStrategy} stateKey=${stateKey} id=${item.id}`);
|
|
539
|
+
break;
|
|
540
|
+
default:
|
|
541
|
+
logger.error(`[zync] push:missing-remote:unknown stateKey=${stateKey} id=${item.id} strategy=${missingStrategy}`);
|
|
470
542
|
break;
|
|
471
|
-
}
|
|
472
543
|
}
|
|
473
544
|
removeFromPendingChanges(set, localId, stateKey);
|
|
474
545
|
onMissingRemoteRecordDuringUpdate?.(missingStrategy, item, omittedItem._localId);
|
|
@@ -488,7 +559,7 @@ async function pushOne(set, get, change, api, logger, queueToSync, missingStrate
|
|
|
488
559
|
if (samePendingVersion(get, stateKey, localId, version)) {
|
|
489
560
|
removeFromPendingChanges(set, localId, stateKey);
|
|
490
561
|
}
|
|
491
|
-
onAfterRemoteAdd?.(set, get,
|
|
562
|
+
onAfterRemoteAdd?.(set, get, setAndQueueToSync, stateKey, {
|
|
492
563
|
...item,
|
|
493
564
|
...result
|
|
494
565
|
});
|
|
@@ -588,6 +659,7 @@ var DEFAULT_SYNC_INTERVAL_MILLIS = 5e3;
|
|
|
588
659
|
var DEFAULT_LOGGER = console;
|
|
589
660
|
var DEFAULT_MIN_LOG_LEVEL = "debug";
|
|
590
661
|
var DEFAULT_MISSING_REMOTE_RECORD_STRATEGY = "ignore";
|
|
662
|
+
var DEFAULT_CONFLICT_RESOLUTION_STRATEGY = "local-wins";
|
|
591
663
|
function createWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
592
664
|
const store = (0, import_zustand.create)(persistWithSync(stateCreator, persistOptions, syncApi, syncOptions));
|
|
593
665
|
return new Promise((resolve) => {
|
|
@@ -599,6 +671,7 @@ function createWithSync(stateCreator, persistOptions, syncApi, syncOptions = {})
|
|
|
599
671
|
function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}) {
|
|
600
672
|
const syncInterval = syncOptions.syncInterval ?? DEFAULT_SYNC_INTERVAL_MILLIS;
|
|
601
673
|
const missingStrategy = syncOptions.missingRemoteRecordDuringUpdateStrategy ?? DEFAULT_MISSING_REMOTE_RECORD_STRATEGY;
|
|
674
|
+
const conflictResolutionStrategy = syncOptions.conflictResolutionStrategy ?? DEFAULT_CONFLICT_RESOLUTION_STRATEGY;
|
|
602
675
|
const logger = newLogger(syncOptions.logger ?? DEFAULT_LOGGER, syncOptions.minLogLevel ?? DEFAULT_MIN_LOG_LEVEL);
|
|
603
676
|
const baseOnRehydrate = persistOptions?.onRehydrateStorage;
|
|
604
677
|
const basePartialize = persistOptions?.partialize;
|
|
@@ -654,15 +727,15 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
654
727
|
for (const stateKey of Object.keys(syncApi)) {
|
|
655
728
|
try {
|
|
656
729
|
const api = findApi(stateKey, syncApi);
|
|
657
|
-
await pull(set, get, stateKey, api, logger);
|
|
730
|
+
await pull(set, get, stateKey, api, logger, conflictResolutionStrategy);
|
|
658
731
|
} catch (err) {
|
|
659
732
|
syncError = syncError ?? err;
|
|
660
733
|
logger.error(`[zync] pull:error stateKey=${stateKey}`, err);
|
|
661
734
|
}
|
|
662
735
|
}
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
for (const change of
|
|
736
|
+
const changesSnapshot = [...get().syncState.pendingChanges || []];
|
|
737
|
+
changesSnapshot.sort((a, b) => orderFor(a.action) - orderFor(b.action));
|
|
738
|
+
for (const change of changesSnapshot) {
|
|
666
739
|
try {
|
|
667
740
|
const api = findApi(change.stateKey, syncApi);
|
|
668
741
|
await pushOne(
|
|
@@ -671,7 +744,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
671
744
|
change,
|
|
672
745
|
api,
|
|
673
746
|
logger,
|
|
674
|
-
|
|
747
|
+
setAndQueueToSync,
|
|
675
748
|
missingStrategy,
|
|
676
749
|
syncOptions.onMissingRemoteRecordDuringUpdate,
|
|
677
750
|
syncOptions.onAfterRemoteAdd
|
|
@@ -755,40 +828,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
755
828
|
}
|
|
756
829
|
}));
|
|
757
830
|
}
|
|
758
|
-
function
|
|
759
|
-
set((state) => {
|
|
760
|
-
const pendingChanges = state.syncState.pendingChanges || [];
|
|
761
|
-
for (const localId of localIds) {
|
|
762
|
-
const item = state[stateKey].find((i) => i._localId === localId);
|
|
763
|
-
if (!item) {
|
|
764
|
-
logger.error(`[zync] queueToSync:no-local-item localId=${localId}`);
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
const queueItem = pendingChanges.find((p) => p.localId === localId && p.stateKey === stateKey);
|
|
768
|
-
if (queueItem) {
|
|
769
|
-
queueItem.version += 1;
|
|
770
|
-
if (queueItem.action === "create-or-update" /* CreateOrUpdate */ && action === "remove" /* Remove */ && item.id) {
|
|
771
|
-
queueItem.action = "remove" /* Remove */;
|
|
772
|
-
queueItem.id = item.id;
|
|
773
|
-
logger.debug(`[zync] queueToSync:changed-to-remove action=${action} localId=${localId} v=${queueItem.version}`);
|
|
774
|
-
} else {
|
|
775
|
-
logger.debug(`[zync] queueToSync:re-queued action=${action} localId=${localId} v=${queueItem.version}`);
|
|
776
|
-
}
|
|
777
|
-
} else {
|
|
778
|
-
pendingChanges.push({ action, stateKey, localId, id: item.id, version: 1 });
|
|
779
|
-
logger.debug(`[zync] queueToSync:added action=${action} localId=${localId}`);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
return {
|
|
783
|
-
syncState: {
|
|
784
|
-
...state.syncState || {},
|
|
785
|
-
pendingChanges
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
});
|
|
789
|
-
syncOnce();
|
|
790
|
-
}
|
|
791
|
-
function setAndSync(partial) {
|
|
831
|
+
function setAndSyncOnce(partial) {
|
|
792
832
|
if (typeof partial === "function") {
|
|
793
833
|
set((state) => ({ ...partial(state) }));
|
|
794
834
|
} else {
|
|
@@ -796,6 +836,49 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
796
836
|
}
|
|
797
837
|
syncOnce();
|
|
798
838
|
}
|
|
839
|
+
function setAndQueueToSync(partial) {
|
|
840
|
+
if (typeof partial === "function") {
|
|
841
|
+
set((state) => newSyncState(state, partial(state)));
|
|
842
|
+
} else {
|
|
843
|
+
set((state) => newSyncState(state, partial));
|
|
844
|
+
}
|
|
845
|
+
syncOnce();
|
|
846
|
+
}
|
|
847
|
+
function newSyncState(state, partial) {
|
|
848
|
+
const pendingChanges = state.syncState.pendingChanges || [];
|
|
849
|
+
Object.keys(partial).map((stateKey) => {
|
|
850
|
+
const current = state[stateKey];
|
|
851
|
+
const updated = partial[stateKey];
|
|
852
|
+
const changes = findChanges(current, updated);
|
|
853
|
+
addToPendingChanges(pendingChanges, stateKey, changes);
|
|
854
|
+
});
|
|
855
|
+
return {
|
|
856
|
+
...partial,
|
|
857
|
+
syncState: {
|
|
858
|
+
...state.syncState || {},
|
|
859
|
+
pendingChanges
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function addToPendingChanges(pendingChanges, stateKey, changes) {
|
|
864
|
+
for (const [localId, change] of changes) {
|
|
865
|
+
const action = change.updatedItem === null ? "remove" /* Remove */ : "create-or-update" /* CreateOrUpdate */;
|
|
866
|
+
const queueItem = pendingChanges.find((p) => p.localId === localId && p.stateKey === stateKey);
|
|
867
|
+
if (queueItem) {
|
|
868
|
+
queueItem.version += 1;
|
|
869
|
+
if (queueItem.action === "create-or-update" /* CreateOrUpdate */ && action === "remove" /* Remove */ && change.currentItem.id) {
|
|
870
|
+
queueItem.action = "remove" /* Remove */;
|
|
871
|
+
queueItem.id = change.currentItem.id;
|
|
872
|
+
logger.debug(`[zync] addToPendingChanges:changed-to-remove action=${action} localId=${localId} v=${queueItem.version}`);
|
|
873
|
+
} else {
|
|
874
|
+
logger.debug(`[zync] addToPendingChanges:re-queued action=${action} localId=${localId} v=${queueItem.version}`);
|
|
875
|
+
}
|
|
876
|
+
} else {
|
|
877
|
+
pendingChanges.push({ action, stateKey, localId, id: change.currentItem?.id, version: 1, changes: change.changes });
|
|
878
|
+
logger.debug(`[zync] addToPendingChanges:added action=${action} localId=${localId}`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
799
882
|
function enable(enabled) {
|
|
800
883
|
set((state) => ({
|
|
801
884
|
syncState: {
|
|
@@ -834,7 +917,7 @@ function persistWithSync(stateCreator, persistOptions, syncApi, syncOptions = {}
|
|
|
834
917
|
enable,
|
|
835
918
|
startFirstLoad
|
|
836
919
|
};
|
|
837
|
-
const userState = stateCreator(
|
|
920
|
+
const userState = stateCreator(setAndSyncOnce, get, setAndQueueToSync);
|
|
838
921
|
return {
|
|
839
922
|
...userState,
|
|
840
923
|
syncState: {
|