@legendapp/state 3.0.0-alpha.1 → 3.0.0-alpha.3
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/.DS_Store +0 -0
- package/CHANGELOG.md +1 -831
- package/LICENSE +1 -21
- package/README.md +1 -141
- package/as/arrayAsRecord.d.mts +5 -0
- package/as/arrayAsRecord.d.ts +5 -0
- package/as/arrayAsRecord.js +28 -0
- package/as/arrayAsRecord.mjs +26 -0
- package/as/arrayAsSet.d.mts +5 -0
- package/as/arrayAsSet.d.ts +5 -0
- package/as/arrayAsSet.js +13 -0
- package/as/arrayAsSet.mjs +11 -0
- package/as/arrayAsString.d.mts +5 -0
- package/as/arrayAsString.d.ts +5 -0
- package/as/arrayAsString.js +13 -0
- package/as/arrayAsString.mjs +11 -0
- package/as/numberAsString.d.mts +5 -0
- package/as/numberAsString.d.ts +5 -0
- package/as/numberAsString.js +13 -0
- package/as/numberAsString.mjs +11 -0
- package/as/recordAsArray.d.mts +5 -0
- package/as/recordAsArray.d.ts +5 -0
- package/as/recordAsArray.js +25 -0
- package/as/recordAsArray.mjs +23 -0
- package/as/recordAsString.d.mts +5 -0
- package/as/recordAsString.d.ts +5 -0
- package/as/recordAsString.js +13 -0
- package/as/recordAsString.mjs +11 -0
- package/as/setAsArray.d.mts +5 -0
- package/as/setAsArray.d.ts +5 -0
- package/as/setAsArray.js +13 -0
- package/as/setAsArray.mjs +11 -0
- package/as/setAsString.d.mts +5 -0
- package/as/setAsString.d.ts +5 -0
- package/as/setAsString.js +13 -0
- package/as/setAsString.mjs +11 -0
- package/as/stringAsArray.d.mts +5 -0
- package/as/stringAsArray.d.ts +5 -0
- package/as/stringAsArray.js +13 -0
- package/as/stringAsArray.mjs +11 -0
- package/as/stringAsNumber.d.mts +5 -0
- package/as/stringAsNumber.d.ts +5 -0
- package/as/stringAsNumber.js +16 -0
- package/as/stringAsNumber.mjs +14 -0
- package/as/stringAsRecord.d.mts +5 -0
- package/as/stringAsRecord.d.ts +5 -0
- package/as/stringAsRecord.js +15 -0
- package/as/stringAsRecord.mjs +13 -0
- package/as/stringAsSet.d.mts +5 -0
- package/as/stringAsSet.d.ts +5 -0
- package/as/stringAsSet.js +13 -0
- package/as/stringAsSet.mjs +11 -0
- package/babel.d.mts +21 -0
- package/babel.d.ts +21 -2
- package/babel.js +57 -53
- package/babel.mjs +65 -0
- package/config/enable$GetSet.js +13 -14
- package/config/enable$GetSet.mjs +13 -14
- package/config/enableReactComponents.d.mts +9 -0
- package/config/enableReactComponents.d.ts +4 -2
- package/config/enableReactComponents.js +13 -10
- package/config/enableReactComponents.mjs +13 -10
- package/config/enableReactNativeComponents.d.mts +22 -0
- package/config/enableReactNativeComponents.d.ts +6 -4
- package/config/enableReactNativeComponents.js +43 -47
- package/config/enableReactNativeComponents.mjs +43 -47
- package/config/enableReactTracking.d.mts +7 -0
- package/config/enableReactTracking.d.ts +3 -2
- package/config/enableReactTracking.js +33 -38
- package/config/enableReactTracking.mjs +33 -38
- package/config/enableReactUse.d.mts +10 -0
- package/config/enableReactUse.d.ts +4 -1
- package/config/enableReactUse.js +15 -14
- package/config/enableReactUse.mjs +15 -14
- package/config/{enable$GetSet.d.ts → enable_GetSet.d.mts} +4 -2
- package/config/enable_GetSet.d.ts +10 -0
- package/config/enable_PeekAssign.d.mts +10 -0
- package/config/enable_PeekAssign.d.ts +4 -2
- package/config/enable_PeekAssign.js +13 -14
- package/config/enable_PeekAssign.mjs +13 -14
- package/helpers/pageHash.d.mts +9 -0
- package/helpers/pageHash.d.ts +2 -0
- package/helpers/pageHash.js +25 -30
- package/helpers/pageHash.mjs +25 -30
- package/helpers/pageHashParams.d.mts +9 -0
- package/helpers/pageHashParams.d.ts +2 -0
- package/helpers/pageHashParams.js +34 -37
- package/helpers/pageHashParams.mjs +34 -37
- package/helpers/time.d.mts +6 -0
- package/helpers/time.d.ts +6 -3
- package/helpers/time.js +17 -17
- package/helpers/time.mjs +17 -17
- package/helpers/trackHistory.d.mts +6 -0
- package/helpers/trackHistory.d.ts +4 -2
- package/helpers/trackHistory.js +13 -16
- package/helpers/trackHistory.mjs +13 -16
- package/helpers/undoRedo.d.mts +37 -0
- package/helpers/undoRedo.d.ts +5 -3
- package/helpers/undoRedo.js +59 -94
- package/helpers/undoRedo.mjs +59 -94
- package/index.d.mts +404 -0
- package/index.d.ts +371 -28
- package/index.js +2015 -2166
- package/index.mjs +2015 -2166
- package/package.json +254 -195
- package/persist-plugins/async-storage.d.mts +18 -0
- package/persist-plugins/async-storage.d.ts +6 -3
- package/persist-plugins/async-storage.js +79 -86
- package/persist-plugins/async-storage.mjs +79 -86
- package/persist-plugins/indexeddb.d.mts +29 -0
- package/persist-plugins/indexeddb.d.ts +6 -3
- package/persist-plugins/indexeddb.js +331 -352
- package/persist-plugins/indexeddb.mjs +331 -352
- package/persist-plugins/local-storage.d.mts +23 -0
- package/persist-plugins/local-storage.d.ts +8 -5
- package/persist-plugins/local-storage.js +74 -76
- package/persist-plugins/local-storage.mjs +74 -76
- package/persist-plugins/mmkv.d.mts +18 -0
- package/persist-plugins/mmkv.d.ts +6 -3
- package/persist-plugins/mmkv.js +82 -86
- package/persist-plugins/mmkv.mjs +82 -86
- package/react-hooks/createObservableHook.d.mts +5 -0
- package/react-hooks/createObservableHook.d.ts +4 -1
- package/react-hooks/createObservableHook.js +29 -30
- package/react-hooks/createObservableHook.mjs +25 -30
- package/react-hooks/useHover.d.mts +5 -0
- package/react-hooks/useHover.d.ts +5 -3
- package/react-hooks/useHover.js +29 -29
- package/react-hooks/useHover.mjs +29 -29
- package/react-hooks/useMeasure.d.mts +9 -0
- package/react-hooks/useMeasure.d.ts +5 -2
- package/react-hooks/useMeasure.js +30 -32
- package/react-hooks/useMeasure.mjs +30 -32
- package/react-hooks/useObservableNextRouter.d.mts +35 -0
- package/react-hooks/useObservableNextRouter.d.ts +9 -7
- package/react-hooks/useObservableNextRouter.js +64 -77
- package/react-hooks/useObservableNextRouter.mjs +60 -77
- package/react.d.mts +157 -0
- package/react.d.ts +157 -21
- package/react.js +458 -749
- package/react.mjs +457 -752
- package/sync-plugins/crud.d.mts +54 -0
- package/sync-plugins/crud.d.ts +12 -10
- package/sync-plugins/crud.js +253 -270
- package/sync-plugins/crud.mjs +253 -270
- package/sync-plugins/fetch.d.mts +21 -0
- package/sync-plugins/fetch.d.ts +7 -4
- package/sync-plugins/fetch.js +50 -37
- package/sync-plugins/fetch.mjs +50 -37
- package/sync-plugins/keel.d.mts +108 -0
- package/sync-plugins/keel.d.ts +17 -15
- package/sync-plugins/keel.js +229 -462
- package/sync-plugins/keel.mjs +227 -464
- package/sync-plugins/supabase.d.mts +39 -0
- package/sync-plugins/supabase.d.ts +16 -14
- package/sync-plugins/supabase.js +128 -128
- package/sync-plugins/supabase.mjs +128 -128
- package/sync-plugins/tanstack-query.d.mts +14 -0
- package/sync-plugins/tanstack-query.d.ts +7 -4
- package/sync-plugins/tanstack-query.js +51 -57
- package/sync-plugins/tanstack-query.mjs +51 -57
- package/sync-plugins/tanstack-react-query.d.mts +8 -0
- package/sync-plugins/tanstack-react-query.d.ts +6 -1
- package/sync-plugins/tanstack-react-query.js +2 -2
- package/sync-plugins/tanstack-react-query.mjs +2 -2
- package/sync.d.mts +351 -0
- package/sync.d.ts +349 -9
- package/sync.js +910 -964
- package/sync.mjs +920 -974
- package/trace.d.mts +9 -0
- package/trace.d.ts +9 -4
- package/trace.js +72 -62
- package/trace.mjs +72 -62
- package/types/babel.d.ts +1 -12
- package/babel.js.map +0 -1
- package/config/enable$GetSet.js.map +0 -1
- package/config/enable$GetSet.mjs.map +0 -1
- package/config/enableReactComponents.js.map +0 -1
- package/config/enableReactComponents.mjs.map +0 -1
- package/config/enableReactNativeComponents.js.map +0 -1
- package/config/enableReactNativeComponents.mjs.map +0 -1
- package/config/enableReactTracking.js.map +0 -1
- package/config/enableReactTracking.mjs.map +0 -1
- package/config/enableReactUse.js.map +0 -1
- package/config/enableReactUse.mjs.map +0 -1
- package/config/enable_PeekAssign.js.map +0 -1
- package/config/enable_PeekAssign.mjs.map +0 -1
- package/helpers/pageHash.js.map +0 -1
- package/helpers/pageHash.mjs.map +0 -1
- package/helpers/pageHashParams.js.map +0 -1
- package/helpers/pageHashParams.mjs.map +0 -1
- package/helpers/time.js.map +0 -1
- package/helpers/time.mjs.map +0 -1
- package/helpers/trackHistory.js.map +0 -1
- package/helpers/trackHistory.mjs.map +0 -1
- package/helpers/undoRedo.js.map +0 -1
- package/helpers/undoRedo.mjs.map +0 -1
- package/history.d.ts +0 -1
- package/history.js +0 -24
- package/history.js.map +0 -1
- package/history.mjs +0 -22
- package/history.mjs.map +0 -1
- package/index.js.map +0 -1
- package/index.mjs.map +0 -1
- package/persist-plugins/async-storage.js.map +0 -1
- package/persist-plugins/async-storage.mjs.map +0 -1
- package/persist-plugins/indexeddb.js.map +0 -1
- package/persist-plugins/indexeddb.mjs.map +0 -1
- package/persist-plugins/local-storage.js.map +0 -1
- package/persist-plugins/local-storage.mjs.map +0 -1
- package/persist-plugins/mmkv.js.map +0 -1
- package/persist-plugins/mmkv.mjs.map +0 -1
- package/react-hooks/createObservableHook.js.map +0 -1
- package/react-hooks/createObservableHook.mjs.map +0 -1
- package/react-hooks/useHover.js.map +0 -1
- package/react-hooks/useHover.mjs.map +0 -1
- package/react-hooks/useMeasure.js.map +0 -1
- package/react-hooks/useMeasure.mjs.map +0 -1
- package/react-hooks/useObservableNextRouter.js.map +0 -1
- package/react-hooks/useObservableNextRouter.mjs.map +0 -1
- package/react.js.map +0 -1
- package/react.mjs.map +0 -1
- package/src/ObservableObject.ts +0 -1350
- package/src/ObservablePrimitive.ts +0 -62
- package/src/babel/index.ts +0 -83
- package/src/batching.ts +0 -357
- package/src/computed.ts +0 -18
- package/src/config/enable$GetSet.ts +0 -30
- package/src/config/enableReactComponents.ts +0 -26
- package/src/config/enableReactNativeComponents.ts +0 -102
- package/src/config/enableReactTracking.ts +0 -62
- package/src/config/enableReactUse.ts +0 -32
- package/src/config/enable_PeekAssign.ts +0 -31
- package/src/config.ts +0 -47
- package/src/createObservable.ts +0 -47
- package/src/event.ts +0 -26
- package/src/globals.ts +0 -235
- package/src/helpers/pageHash.ts +0 -41
- package/src/helpers/pageHashParams.ts +0 -55
- package/src/helpers/time.ts +0 -30
- package/src/helpers/trackHistory.ts +0 -29
- package/src/helpers/undoRedo.ts +0 -111
- package/src/helpers.ts +0 -231
- package/src/is.ts +0 -63
- package/src/linked.ts +0 -17
- package/src/observable.ts +0 -32
- package/src/observableInterfaces.ts +0 -151
- package/src/observableTypes.ts +0 -232
- package/src/observe.ts +0 -89
- package/src/old-plugins/firebase.ts +0 -1053
- package/src/onChange.ts +0 -146
- package/src/persist/configureObservablePersistence.ts +0 -7
- package/src/persist/fieldTransformer.ts +0 -149
- package/src/persist/observablePersistRemoteFunctionsAdapter.ts +0 -39
- package/src/persist/persistObservable.ts +0 -1034
- package/src/persist-plugins/async-storage.ts +0 -99
- package/src/persist-plugins/indexeddb.ts +0 -439
- package/src/persist-plugins/local-storage.ts +0 -86
- package/src/persist-plugins/mmkv.ts +0 -91
- package/src/proxy.ts +0 -28
- package/src/react/Computed.tsx +0 -8
- package/src/react/For.tsx +0 -116
- package/src/react/Memo.tsx +0 -4
- package/src/react/Reactive.tsx +0 -53
- package/src/react/Show.tsx +0 -33
- package/src/react/Switch.tsx +0 -43
- package/src/react/react-globals.ts +0 -3
- package/src/react/reactInterfaces.ts +0 -32
- package/src/react/reactive-observer.tsx +0 -210
- package/src/react/useComputed.ts +0 -36
- package/src/react/useEffectOnce.ts +0 -41
- package/src/react/useIsMounted.ts +0 -16
- package/src/react/useMount.ts +0 -15
- package/src/react/useObservable.ts +0 -24
- package/src/react/useObservableReducer.ts +0 -52
- package/src/react/useObservableState.ts +0 -30
- package/src/react/useObserve.ts +0 -54
- package/src/react/useObserveEffect.ts +0 -40
- package/src/react/usePauseProvider.tsx +0 -16
- package/src/react/useSelector.ts +0 -167
- package/src/react/useUnmount.ts +0 -8
- package/src/react/useWhen.ts +0 -9
- package/src/react-hooks/createObservableHook.ts +0 -53
- package/src/react-hooks/useHover.ts +0 -40
- package/src/react-hooks/useMeasure.ts +0 -48
- package/src/react-hooks/useObservableNextRouter.ts +0 -137
- package/src/retry.ts +0 -71
- package/src/setupTracking.ts +0 -26
- package/src/sync/activateSyncedNode.ts +0 -128
- package/src/sync/configureObservableSync.ts +0 -7
- package/src/sync/persistTypes.ts +0 -216
- package/src/sync/syncHelpers.ts +0 -180
- package/src/sync/syncObservable.ts +0 -1056
- package/src/sync/syncObservableAdapter.ts +0 -31
- package/src/sync/syncTypes.ts +0 -189
- package/src/sync/synced.ts +0 -21
- package/src/sync-plugins/crud.ts +0 -412
- package/src/sync-plugins/fetch.ts +0 -80
- package/src/sync-plugins/keel.ts +0 -495
- package/src/sync-plugins/supabase.ts +0 -249
- package/src/sync-plugins/tanstack-query.ts +0 -113
- package/src/sync-plugins/tanstack-react-query.ts +0 -12
- package/src/trace/traceHelpers.ts +0 -11
- package/src/trace/useTraceListeners.ts +0 -34
- package/src/trace/useTraceUpdates.ts +0 -24
- package/src/trace/useVerifyNotTracking.ts +0 -33
- package/src/trace/useVerifyOneRender.ts +0 -10
- package/src/trackSelector.ts +0 -52
- package/src/tracking.ts +0 -43
- package/src/types/babel.d.ts +0 -12
- package/src/when.ts +0 -75
- package/sync-plugins/crud.js.map +0 -1
- package/sync-plugins/crud.mjs.map +0 -1
- package/sync-plugins/fetch.js.map +0 -1
- package/sync-plugins/fetch.mjs.map +0 -1
- package/sync-plugins/keel.js.map +0 -1
- package/sync-plugins/keel.mjs.map +0 -1
- package/sync-plugins/supabase.js.map +0 -1
- package/sync-plugins/supabase.mjs.map +0 -1
- package/sync-plugins/tanstack-query.js.map +0 -1
- package/sync-plugins/tanstack-query.mjs.map +0 -1
- package/sync-plugins/tanstack-react-query.js.map +0 -1
- package/sync-plugins/tanstack-react-query.mjs.map +0 -1
- package/sync.js.map +0 -1
- package/sync.mjs.map +0 -1
- package/trace.js.map +0 -1
- package/trace.mjs.map +0 -1
|
@@ -1,1056 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Change,
|
|
3
|
-
ClassConstructor,
|
|
4
|
-
ListenerParams,
|
|
5
|
-
NodeValue,
|
|
6
|
-
Observable,
|
|
7
|
-
ObservableObject,
|
|
8
|
-
ObservableParam,
|
|
9
|
-
TypeAtPath,
|
|
10
|
-
UpdateFnParams,
|
|
11
|
-
} from '@legendapp/state';
|
|
12
|
-
import {
|
|
13
|
-
beginBatch,
|
|
14
|
-
constructObjectWithPath,
|
|
15
|
-
deconstructObjectWithPath,
|
|
16
|
-
endBatch,
|
|
17
|
-
internal,
|
|
18
|
-
isArray,
|
|
19
|
-
isEmpty,
|
|
20
|
-
isFunction,
|
|
21
|
-
isObject,
|
|
22
|
-
isPromise,
|
|
23
|
-
isString,
|
|
24
|
-
mergeIntoObservable,
|
|
25
|
-
observable,
|
|
26
|
-
setAtPath,
|
|
27
|
-
shouldIgnoreUnobserved,
|
|
28
|
-
when,
|
|
29
|
-
} from '@legendapp/state';
|
|
30
|
-
import { observableSyncConfiguration } from './configureObservableSync';
|
|
31
|
-
import type { ObservableOnChangeParams } from './persistTypes';
|
|
32
|
-
import { removeNullUndefined } from './syncHelpers';
|
|
33
|
-
import { syncObservableAdapter } from './syncObservableAdapter';
|
|
34
|
-
import type {
|
|
35
|
-
ObservablePersistPlugin,
|
|
36
|
-
ObservableSyncClass,
|
|
37
|
-
ObservableSyncState,
|
|
38
|
-
PersistMetadata,
|
|
39
|
-
PersistOptions,
|
|
40
|
-
SyncTransform,
|
|
41
|
-
SyncTransformMethod,
|
|
42
|
-
Synced,
|
|
43
|
-
SyncedOptions,
|
|
44
|
-
} from './syncTypes';
|
|
45
|
-
|
|
46
|
-
const { createPreviousHandler, clone, getValueAtPath, globalState, symbolLinked, getNode, getNodeValue } = internal;
|
|
47
|
-
|
|
48
|
-
export const mapSyncPlugins: WeakMap<
|
|
49
|
-
ClassConstructor<ObservablePersistPlugin | ObservableSyncClass>,
|
|
50
|
-
{
|
|
51
|
-
plugin: ObservablePersistPlugin | ObservableSyncClass;
|
|
52
|
-
initialized: Observable<boolean>;
|
|
53
|
-
}
|
|
54
|
-
> = new WeakMap();
|
|
55
|
-
|
|
56
|
-
const metadatas = new WeakMap<ObservableParam<any>, PersistMetadata>();
|
|
57
|
-
const promisesLocalSaves = new Set<Promise<void>>();
|
|
58
|
-
|
|
59
|
-
interface LocalState {
|
|
60
|
-
pluginPersist?: ObservablePersistPlugin;
|
|
61
|
-
pluginSync?: ObservableSyncClass;
|
|
62
|
-
pendingChanges?: Record<string, { p: any; v?: any; t: TypeAtPath[] }>;
|
|
63
|
-
numSavesOutstanding?: number;
|
|
64
|
-
pendingSaveResults?: object[];
|
|
65
|
-
isApplyingPending?: boolean;
|
|
66
|
-
timeoutSaveMetadata?: any;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface PreppedChangeLocal {
|
|
70
|
-
queuedChange: QueuedChange;
|
|
71
|
-
changesLocal: ChangeWithPathStr[];
|
|
72
|
-
saveRemote: boolean;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
interface PreppedChangeRemote {
|
|
76
|
-
queuedChange: QueuedChange;
|
|
77
|
-
changesRemote: ChangeWithPathStr[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
type ChangeWithPathStr = Change & { pathStr: string };
|
|
81
|
-
type ChangeWithPathStrAndPrevious = ChangeWithPathStr & { valuePrevious: any };
|
|
82
|
-
|
|
83
|
-
function parseLocalConfig(config: string | PersistOptions | undefined): {
|
|
84
|
-
table: string;
|
|
85
|
-
config: PersistOptions;
|
|
86
|
-
} {
|
|
87
|
-
return config
|
|
88
|
-
? isString(config)
|
|
89
|
-
? { table: config, config: { name: config } }
|
|
90
|
-
: { table: config.name, config }
|
|
91
|
-
: ({} as { table: string; config: PersistOptions });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function doInOrder<T>(arg1: T | Promise<T>, arg2: (value: T) => void): any {
|
|
95
|
-
return isPromise(arg1) ? arg1.then(arg2) : arg2(arg1);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function onChangeRemote(cb: () => void) {
|
|
99
|
-
endBatch(true);
|
|
100
|
-
// Remote changes should only update local state
|
|
101
|
-
globalState.isLoadingRemote = true;
|
|
102
|
-
|
|
103
|
-
beginBatch();
|
|
104
|
-
cb();
|
|
105
|
-
// Reset isLoadingRemote before ending the batch so it doesn't
|
|
106
|
-
// apply to any side effects
|
|
107
|
-
globalState.isLoadingRemote = false;
|
|
108
|
-
endBatch(true);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function transformSaveData(
|
|
112
|
-
value: any,
|
|
113
|
-
path: string[],
|
|
114
|
-
pathTypes: TypeAtPath[],
|
|
115
|
-
{ transform }: { transform?: SyncTransform },
|
|
116
|
-
): Promise<any> | any {
|
|
117
|
-
if (transform?.save) {
|
|
118
|
-
const constructed = constructObjectWithPath(path, pathTypes, value);
|
|
119
|
-
const saved = transform.save(constructed);
|
|
120
|
-
value = deconstructObjectWithPath(path, pathTypes, saved);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return value;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function transformLoadData(
|
|
127
|
-
value: any,
|
|
128
|
-
{ transform }: { transform?: SyncTransform },
|
|
129
|
-
doUserTransform: boolean,
|
|
130
|
-
method: SyncTransformMethod,
|
|
131
|
-
): Promise<any> | any {
|
|
132
|
-
if (doUserTransform && transform?.load) {
|
|
133
|
-
value = transform.load(value, method);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return value;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function updateMetadataImmediate<T>(
|
|
140
|
-
value$: ObservableParam<any>,
|
|
141
|
-
localState: LocalState,
|
|
142
|
-
syncState: Observable<ObservableSyncState>,
|
|
143
|
-
syncOptions: SyncedOptions<T>,
|
|
144
|
-
newMetadata: PersistMetadata,
|
|
145
|
-
) {
|
|
146
|
-
const saves = Array.from(promisesLocalSaves);
|
|
147
|
-
if (saves.length > 0) {
|
|
148
|
-
await Promise.all(saves);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const { pluginPersist } = localState;
|
|
152
|
-
const { table, config } = parseLocalConfig(syncOptions?.persist);
|
|
153
|
-
|
|
154
|
-
// Save metadata
|
|
155
|
-
const oldMetadata: PersistMetadata | undefined = metadatas.get(value$);
|
|
156
|
-
|
|
157
|
-
const { lastSync, pending } = newMetadata;
|
|
158
|
-
|
|
159
|
-
const needsUpdate = pending || (lastSync && (!oldMetadata || lastSync !== oldMetadata.lastSync));
|
|
160
|
-
|
|
161
|
-
if (needsUpdate) {
|
|
162
|
-
const metadata = Object.assign({}, oldMetadata, newMetadata);
|
|
163
|
-
metadatas.set(value$, metadata);
|
|
164
|
-
if (pluginPersist) {
|
|
165
|
-
await pluginPersist!.setMetadata(table, metadata, config);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (lastSync) {
|
|
169
|
-
syncState.assign({
|
|
170
|
-
lastSync: lastSync,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function updateMetadata<T>(
|
|
177
|
-
value$: ObservableParam<any>,
|
|
178
|
-
localState: LocalState,
|
|
179
|
-
syncState: ObservableObject<ObservableSyncState>,
|
|
180
|
-
syncOptions: SyncedOptions<T>,
|
|
181
|
-
newMetadata: PersistMetadata,
|
|
182
|
-
) {
|
|
183
|
-
if (localState.timeoutSaveMetadata) {
|
|
184
|
-
clearTimeout(localState.timeoutSaveMetadata);
|
|
185
|
-
}
|
|
186
|
-
localState.timeoutSaveMetadata = setTimeout(
|
|
187
|
-
() => updateMetadataImmediate(value$, localState, syncState, syncOptions as SyncedOptions<T>, newMetadata),
|
|
188
|
-
0,
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
interface QueuedChange<T = any> {
|
|
193
|
-
inRemoteChange: boolean;
|
|
194
|
-
isApplyingPending: boolean;
|
|
195
|
-
value$: Observable<T>;
|
|
196
|
-
syncState: ObservableObject<ObservableSyncState>;
|
|
197
|
-
localState: LocalState;
|
|
198
|
-
syncOptions: SyncedOptions<T>;
|
|
199
|
-
changes: ListenerParams['changes'];
|
|
200
|
-
valuePrevious?: T;
|
|
201
|
-
getPrevious: () => T;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
let _queuedChanges: QueuedChange[] = [];
|
|
205
|
-
const _queuedRemoteChanges: Map<SyncedOptions, QueuedChange[]> = new Map();
|
|
206
|
-
const _queuedRemoteChangesTimeouts: Map<SyncedOptions, number> = new Map();
|
|
207
|
-
|
|
208
|
-
function mergeChanges(changes: Change[]) {
|
|
209
|
-
const changesByPath = new Map<string, Change>();
|
|
210
|
-
const changesOut: Change[] = [];
|
|
211
|
-
// TODO: This could be even more robust by going deeper into paths like the firebase plugin's _updatePendingSave
|
|
212
|
-
for (let i = 0; i < changes.length; i++) {
|
|
213
|
-
const change = changes[i];
|
|
214
|
-
const pathStr = change.path.join('/');
|
|
215
|
-
const existing = changesByPath.get(pathStr);
|
|
216
|
-
if (existing) {
|
|
217
|
-
// If setting a value back to what it was, no need to save it
|
|
218
|
-
if (change.valueAtPath === existing.prevAtPath) {
|
|
219
|
-
changesOut.splice(changesOut.indexOf(change), 1);
|
|
220
|
-
} else {
|
|
221
|
-
existing.valueAtPath = change.valueAtPath;
|
|
222
|
-
}
|
|
223
|
-
} else {
|
|
224
|
-
changesByPath.set(pathStr, change);
|
|
225
|
-
changesOut.push(change);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return changesOut;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function mergeQueuedChanges(allChanges: QueuedChange[]) {
|
|
232
|
-
const changesByObsRemote = new Map<Observable, Change[]>();
|
|
233
|
-
const changesByObsLocal = new Map<Observable, Change[]>();
|
|
234
|
-
const previousByObs = new Map<Observable, any>();
|
|
235
|
-
const outRemote: Map<Observable, QueuedChange> = new Map();
|
|
236
|
-
const outLocal: Map<Observable, QueuedChange> = new Map();
|
|
237
|
-
|
|
238
|
-
for (let i = 0; i < allChanges.length; i++) {
|
|
239
|
-
const value = allChanges[i];
|
|
240
|
-
const { value$: obs, changes, inRemoteChange, getPrevious } = value;
|
|
241
|
-
const targetMap = inRemoteChange ? outRemote : outLocal;
|
|
242
|
-
const changesMap = inRemoteChange ? changesByObsRemote : changesByObsLocal;
|
|
243
|
-
const existing = changesMap.get(obs);
|
|
244
|
-
const newChanges = existing ? [...existing, ...changes] : changes;
|
|
245
|
-
const merged = mergeChanges(newChanges);
|
|
246
|
-
changesMap.set(obs, merged);
|
|
247
|
-
value.changes = merged;
|
|
248
|
-
if (!previousByObs.has(obs)) {
|
|
249
|
-
previousByObs.set(obs, getPrevious());
|
|
250
|
-
}
|
|
251
|
-
value.valuePrevious = previousByObs.get(obs);
|
|
252
|
-
targetMap.set(obs, value);
|
|
253
|
-
}
|
|
254
|
-
return Array.from(outRemote.values()).concat(Array.from(outLocal.values()));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function processQueuedChanges() {
|
|
258
|
-
// Get a local copy of the queued changes and clear the global queue
|
|
259
|
-
const queuedChanges = mergeQueuedChanges(_queuedChanges);
|
|
260
|
-
_queuedChanges = [];
|
|
261
|
-
|
|
262
|
-
const pendingSyncOptions = new Set<SyncedOptions>();
|
|
263
|
-
for (let i = 0; i < queuedChanges.length; i++) {
|
|
264
|
-
const change = queuedChanges[i];
|
|
265
|
-
if (!change.inRemoteChange) {
|
|
266
|
-
if (!_queuedRemoteChanges.has(change.syncOptions)) {
|
|
267
|
-
_queuedRemoteChanges.set(change.syncOptions, []);
|
|
268
|
-
}
|
|
269
|
-
pendingSyncOptions.add(change.syncOptions);
|
|
270
|
-
_queuedRemoteChanges.get(change.syncOptions)!.push(change);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Note: Summary of the order of operations these functions:
|
|
275
|
-
// 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
|
|
276
|
-
// We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
|
|
277
|
-
// since some may take longer to transformSaveData than others.
|
|
278
|
-
// 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
|
|
279
|
-
// the current value again on next load, which isn't too bad.
|
|
280
|
-
// 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
|
|
281
|
-
// and attempted again.
|
|
282
|
-
// 4. Wait for remote load or error if allowed
|
|
283
|
-
// 5. Save to remote
|
|
284
|
-
// 6. On successful save, merge changes (if any) back into observable
|
|
285
|
-
// 7. Lastly, update metadata to clear pending and update lastSync. Doing this earlier could potentially cause
|
|
286
|
-
// sync inconsistences so it's very important that this is last.
|
|
287
|
-
|
|
288
|
-
const preppedChangesLocal = await Promise.all(queuedChanges.map(prepChangeLocal));
|
|
289
|
-
|
|
290
|
-
// TODO Clean this up: We only need to prep this now in order to save pending changes, don't need any of the other stuff. Should split that up?
|
|
291
|
-
await Promise.all(queuedChanges.map(prepChangeRemote));
|
|
292
|
-
|
|
293
|
-
await Promise.all(preppedChangesLocal.map(doChangeLocal));
|
|
294
|
-
|
|
295
|
-
for (const options of pendingSyncOptions) {
|
|
296
|
-
const timeout = options.debounceSet ?? observableSyncConfiguration?.debounceSet;
|
|
297
|
-
const timeoutSaveRemote = _queuedRemoteChangesTimeouts.get(options);
|
|
298
|
-
const run = () => processQueuedRemoteChanges(options);
|
|
299
|
-
if (timeout) {
|
|
300
|
-
if (timeoutSaveRemote) {
|
|
301
|
-
clearTimeout(timeoutSaveRemote);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
_queuedRemoteChangesTimeouts.set(options, setTimeout(run, timeout) as any);
|
|
305
|
-
} else {
|
|
306
|
-
run();
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
async function processQueuedRemoteChanges(syncOptions: SyncedOptions) {
|
|
312
|
-
const arr = _queuedRemoteChanges.get(syncOptions);
|
|
313
|
-
if (arr?.length) {
|
|
314
|
-
const queuedRemoteChanges = mergeQueuedChanges(arr);
|
|
315
|
-
_queuedRemoteChanges.set(syncOptions, []);
|
|
316
|
-
|
|
317
|
-
const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote));
|
|
318
|
-
|
|
319
|
-
preppedChangesRemote.forEach(doChangeRemote);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
async function prepChangeLocal(queuedChange: QueuedChange): Promise<PreppedChangeLocal | undefined> {
|
|
324
|
-
const { syncState, changes, localState, syncOptions, inRemoteChange, isApplyingPending } = queuedChange;
|
|
325
|
-
|
|
326
|
-
const persist = syncOptions.persist;
|
|
327
|
-
const { pluginSync } = localState;
|
|
328
|
-
const { config: configLocal } = parseLocalConfig(persist);
|
|
329
|
-
const configRemote = syncOptions;
|
|
330
|
-
const saveLocal = persist?.name && !configLocal.readonly && !isApplyingPending && syncState.isPersistEnabled.peek();
|
|
331
|
-
const saveRemote = !!(
|
|
332
|
-
!inRemoteChange &&
|
|
333
|
-
pluginSync?.set &&
|
|
334
|
-
configRemote?.enableSync !== false &&
|
|
335
|
-
syncState.isSyncEnabled.peek()
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
if (saveLocal || saveRemote) {
|
|
339
|
-
if (saveLocal && !syncState.isPersistLoaded.peek()) {
|
|
340
|
-
console.error(
|
|
341
|
-
'[legend-state] WARNING: An observable was changed before being loaded from persist',
|
|
342
|
-
persist,
|
|
343
|
-
);
|
|
344
|
-
return undefined;
|
|
345
|
-
}
|
|
346
|
-
const changesLocal: ChangeWithPathStr[] = [];
|
|
347
|
-
const changesPaths = new Set<string>();
|
|
348
|
-
let promisesTransform: (void | Promise<any>)[] = [];
|
|
349
|
-
|
|
350
|
-
// Reverse order
|
|
351
|
-
for (let i = changes.length - 1; i >= 0; i--) {
|
|
352
|
-
const { path } = changes[i];
|
|
353
|
-
|
|
354
|
-
let found = false;
|
|
355
|
-
|
|
356
|
-
// Optimization to only save the latest update at each path. We might have multiple changes at the same path
|
|
357
|
-
// and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
|
|
358
|
-
// already processed. If a later change modifies a parent of an earlier change (which happens on delete()
|
|
359
|
-
// it should be ignored as it's superseded by the parent modification.
|
|
360
|
-
if (changesPaths.size > 0) {
|
|
361
|
-
for (let u = 0; u < path.length; u++) {
|
|
362
|
-
if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
|
|
363
|
-
found = true;
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!found) {
|
|
370
|
-
const pathStr = path.join('/');
|
|
371
|
-
changesPaths.add(pathStr);
|
|
372
|
-
|
|
373
|
-
const { prevAtPath, valueAtPath, pathTypes } = changes[i];
|
|
374
|
-
if (saveLocal) {
|
|
375
|
-
const promiseTransformLocal = transformSaveData(
|
|
376
|
-
valueAtPath,
|
|
377
|
-
path as string[],
|
|
378
|
-
pathTypes,
|
|
379
|
-
configLocal,
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
promisesTransform.push(
|
|
383
|
-
doInOrder(promiseTransformLocal, (valueTransformed) => {
|
|
384
|
-
// Prepare the local change with the transformed path/value
|
|
385
|
-
changesLocal.push({
|
|
386
|
-
path,
|
|
387
|
-
pathTypes,
|
|
388
|
-
prevAtPath,
|
|
389
|
-
valueAtPath: valueTransformed,
|
|
390
|
-
pathStr,
|
|
391
|
-
});
|
|
392
|
-
}),
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// If there's any transform promises, wait for them before saving
|
|
399
|
-
promisesTransform = promisesTransform.filter(Boolean);
|
|
400
|
-
if (promisesTransform.length > 0) {
|
|
401
|
-
await Promise.all(promisesTransform);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return { queuedChange, changesLocal, saveRemote };
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
async function prepChangeRemote(queuedChange: QueuedChange): Promise<PreppedChangeRemote | undefined> {
|
|
408
|
-
const {
|
|
409
|
-
syncState,
|
|
410
|
-
changes,
|
|
411
|
-
localState,
|
|
412
|
-
syncOptions: syncOptions,
|
|
413
|
-
inRemoteChange,
|
|
414
|
-
isApplyingPending,
|
|
415
|
-
valuePrevious,
|
|
416
|
-
} = queuedChange;
|
|
417
|
-
|
|
418
|
-
const persist = syncOptions.persist;
|
|
419
|
-
const { pluginSync } = localState;
|
|
420
|
-
const { config: configLocal } = parseLocalConfig(persist!);
|
|
421
|
-
const configRemote = syncOptions;
|
|
422
|
-
const saveLocal = persist && !configLocal.readonly && !isApplyingPending && syncState.isPersistEnabled.peek();
|
|
423
|
-
const saveRemote =
|
|
424
|
-
!inRemoteChange && pluginSync?.set && configRemote?.enableSync !== false && syncState.isSyncEnabled.peek();
|
|
425
|
-
|
|
426
|
-
if (saveLocal || saveRemote) {
|
|
427
|
-
if (saveLocal && !syncState.isPersistLoaded.peek()) {
|
|
428
|
-
console.error(
|
|
429
|
-
'[legend-state] WARNING: An observable was changed before being loaded from persist',
|
|
430
|
-
persist,
|
|
431
|
-
);
|
|
432
|
-
return undefined;
|
|
433
|
-
}
|
|
434
|
-
const changesRemote: ChangeWithPathStrAndPrevious[] = [];
|
|
435
|
-
const changesPaths = new Set<string>();
|
|
436
|
-
let promisesTransform: (void | Promise<any>)[] = [];
|
|
437
|
-
|
|
438
|
-
// Reverse order
|
|
439
|
-
for (let i = changes.length - 1; i >= 0; i--) {
|
|
440
|
-
const { path } = changes[i];
|
|
441
|
-
|
|
442
|
-
let found = false;
|
|
443
|
-
|
|
444
|
-
// Optimization to only save the latest update at each path. We might have multiple changes at the same path
|
|
445
|
-
// and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
|
|
446
|
-
// already processed. If a later change modifies a parent of an earlier change (which happens on delete()
|
|
447
|
-
// it should be ignored as it's superseded by the parent modification.
|
|
448
|
-
if (changesPaths.size > 0) {
|
|
449
|
-
for (let u = 0; u < path.length; u++) {
|
|
450
|
-
if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
|
|
451
|
-
found = true;
|
|
452
|
-
break;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (!found) {
|
|
458
|
-
const pathStr = path.join('/');
|
|
459
|
-
changesPaths.add(pathStr);
|
|
460
|
-
|
|
461
|
-
const { prevAtPath, valueAtPath, pathTypes } = changes[i];
|
|
462
|
-
|
|
463
|
-
if (saveRemote) {
|
|
464
|
-
const promiseTransformRemote = transformSaveData(
|
|
465
|
-
valueAtPath,
|
|
466
|
-
path as string[],
|
|
467
|
-
pathTypes,
|
|
468
|
-
configRemote || {},
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
promisesTransform.push(
|
|
472
|
-
doInOrder(promiseTransformRemote, (valueTransformed) => {
|
|
473
|
-
// Prepare pending changes
|
|
474
|
-
if (!localState.pendingChanges) {
|
|
475
|
-
localState.pendingChanges = {};
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// First look for existing pending changes at a higher level than this change
|
|
479
|
-
// If they exist then merge this change into it
|
|
480
|
-
let found = false;
|
|
481
|
-
for (let i = 0; !found && i < path.length - 1; i++) {
|
|
482
|
-
const pathParent = path.slice(0, i + 1).join('/');
|
|
483
|
-
if (localState.pendingChanges[pathParent]?.v) {
|
|
484
|
-
found = true;
|
|
485
|
-
const pathChild = path.slice(i + 1);
|
|
486
|
-
const pathTypesChild = pathTypes.slice(i + 1);
|
|
487
|
-
setAtPath(
|
|
488
|
-
localState.pendingChanges[pathParent].v,
|
|
489
|
-
pathChild,
|
|
490
|
-
pathTypesChild,
|
|
491
|
-
valueAtPath,
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
if (!found) {
|
|
496
|
-
// If an existing pending change is deeper than this change, just delete it
|
|
497
|
-
// in favor of this wider change
|
|
498
|
-
for (const key in localState.pendingChanges) {
|
|
499
|
-
if (key !== pathStr && key.startsWith(pathStr)) {
|
|
500
|
-
delete localState.pendingChanges[key];
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
// The "p" saved in pending should be the previous state before changes,
|
|
504
|
-
// so don't overwrite it if it already exists
|
|
505
|
-
if (!localState.pendingChanges[pathStr]) {
|
|
506
|
-
localState.pendingChanges[pathStr] = { p: prevAtPath ?? null, t: pathTypes };
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Pending value is the untransformed value because it gets loaded without transformment
|
|
510
|
-
// and forwarded through to onObsChange where it gets transformed before save
|
|
511
|
-
localState.pendingChanges[pathStr].v = valueAtPath;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Prepare the remote change with the transformed path/value
|
|
515
|
-
changesRemote.push({
|
|
516
|
-
path,
|
|
517
|
-
pathTypes,
|
|
518
|
-
prevAtPath,
|
|
519
|
-
valueAtPath: valueTransformed,
|
|
520
|
-
pathStr,
|
|
521
|
-
valuePrevious,
|
|
522
|
-
});
|
|
523
|
-
}),
|
|
524
|
-
);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// If there's any transform promises, wait for them before saving
|
|
530
|
-
promisesTransform = promisesTransform.filter(Boolean);
|
|
531
|
-
if (promisesTransform.length > 0) {
|
|
532
|
-
await Promise.all(promisesTransform);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return { queuedChange, changesRemote };
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
async function doChangeLocal(changeInfo: PreppedChangeLocal | undefined) {
|
|
540
|
-
if (!changeInfo) return;
|
|
541
|
-
|
|
542
|
-
const { queuedChange, changesLocal, saveRemote } = changeInfo;
|
|
543
|
-
const { value$: obs, syncState, localState, syncOptions: syncOptions } = queuedChange;
|
|
544
|
-
const { pluginPersist } = localState;
|
|
545
|
-
|
|
546
|
-
const persist = syncOptions.persist;
|
|
547
|
-
const { table, config: configLocal } = parseLocalConfig(persist!);
|
|
548
|
-
const shouldSaveMetadata = persist?.retrySync;
|
|
549
|
-
|
|
550
|
-
if (saveRemote && shouldSaveMetadata) {
|
|
551
|
-
// First save pending changes before saving local or remote
|
|
552
|
-
await updateMetadataImmediate(obs, localState, syncState, syncOptions, {
|
|
553
|
-
pending: localState.pendingChanges,
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
if (changesLocal.length > 0) {
|
|
558
|
-
// Save the changes to local cache before saving to remote. They are already marked as pending so
|
|
559
|
-
// if remote sync fails or the app is closed before remote sync, it will attempt to sync them on the next load.
|
|
560
|
-
let promiseSet = pluginPersist!.set(table, changesLocal, configLocal);
|
|
561
|
-
|
|
562
|
-
if (promiseSet) {
|
|
563
|
-
promiseSet = promiseSet.then(() => {
|
|
564
|
-
promisesLocalSaves.delete(promiseSet as Promise<any>);
|
|
565
|
-
});
|
|
566
|
-
// Keep track of local save promises so that updateMetadata runs only after everything is saved
|
|
567
|
-
promisesLocalSaves.add(promiseSet);
|
|
568
|
-
|
|
569
|
-
// await the local save before proceeding to save remotely
|
|
570
|
-
await promiseSet;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) {
|
|
575
|
-
if (!changeInfo) return;
|
|
576
|
-
|
|
577
|
-
const { queuedChange, changesRemote } = changeInfo;
|
|
578
|
-
const { value$: obs, syncState, localState, syncOptions, valuePrevious: previous } = queuedChange;
|
|
579
|
-
const { pluginPersist, pluginSync } = localState;
|
|
580
|
-
|
|
581
|
-
const persist = syncOptions.persist;
|
|
582
|
-
const { table, config: configLocal } = parseLocalConfig(persist!);
|
|
583
|
-
const { allowSetIfGetError, onBeforeSet, onSetError, waitForSet, onAfterSet } =
|
|
584
|
-
syncOptions || ({} as SyncedOptions);
|
|
585
|
-
const shouldSaveMetadata = persist?.retrySync;
|
|
586
|
-
|
|
587
|
-
if (changesRemote.length > 0) {
|
|
588
|
-
// Wait for remote to be ready before saving
|
|
589
|
-
await when(() => syncState.isLoaded.get() || (allowSetIfGetError && syncState.error.get()));
|
|
590
|
-
|
|
591
|
-
if (waitForSet) {
|
|
592
|
-
const waitFor = isFunction(waitForSet)
|
|
593
|
-
? waitForSet({ changes: changesRemote, value: obs.peek() })
|
|
594
|
-
: waitForSet;
|
|
595
|
-
if (waitFor) {
|
|
596
|
-
await when(waitFor);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
let value = obs.peek();
|
|
601
|
-
const transformSave = syncOptions?.transform?.save;
|
|
602
|
-
if (transformSave) {
|
|
603
|
-
// Clone value before transforming to ensure it doesn't change observable value
|
|
604
|
-
value = transformSave(clone(value));
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
onBeforeSet?.();
|
|
608
|
-
|
|
609
|
-
localState.numSavesOutstanding = (localState.numSavesOutstanding || 0) + 1;
|
|
610
|
-
|
|
611
|
-
let savedPromise = pluginSync!.set!({
|
|
612
|
-
value$: obs,
|
|
613
|
-
syncState: syncState,
|
|
614
|
-
options: syncOptions,
|
|
615
|
-
changes: changesRemote,
|
|
616
|
-
value,
|
|
617
|
-
valuePrevious: previous,
|
|
618
|
-
});
|
|
619
|
-
if (isPromise(savedPromise)) {
|
|
620
|
-
savedPromise = savedPromise.catch((err) => onSetError?.(err));
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const saved = await savedPromise;
|
|
624
|
-
|
|
625
|
-
localState.numSavesOutstanding--;
|
|
626
|
-
|
|
627
|
-
// If this remote save changed anything then update cache and metadata
|
|
628
|
-
// Because save happens after a timeout and they're batched together, some calls to save will
|
|
629
|
-
// return saved data and others won't, so those can be ignored.
|
|
630
|
-
if (saved !== undefined) {
|
|
631
|
-
const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
|
|
632
|
-
const { changes, lastSync } = saved;
|
|
633
|
-
if (pathStrs.length > 0) {
|
|
634
|
-
let transformedChanges: object | undefined = undefined;
|
|
635
|
-
const metadata: PersistMetadata = {};
|
|
636
|
-
if (persist) {
|
|
637
|
-
const pendingMetadata = pluginPersist!.getMetadata(table, configLocal)?.pending;
|
|
638
|
-
const pending = localState.pendingChanges;
|
|
639
|
-
|
|
640
|
-
for (let i = 0; i < pathStrs.length; i++) {
|
|
641
|
-
const pathStr = pathStrs[i];
|
|
642
|
-
// Clear pending for this path
|
|
643
|
-
if (pendingMetadata?.[pathStr]) {
|
|
644
|
-
// Remove pending from persisted medata state
|
|
645
|
-
delete pendingMetadata[pathStr];
|
|
646
|
-
metadata.pending = pendingMetadata;
|
|
647
|
-
}
|
|
648
|
-
// Clear pending for this path if not already removed by above
|
|
649
|
-
// pendingMetadata === pending sometimes
|
|
650
|
-
if (pending?.[pathStr]) {
|
|
651
|
-
// Remove pending from local state
|
|
652
|
-
delete pending[pathStr];
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (lastSync) {
|
|
657
|
-
metadata.lastSync = lastSync;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Remote can optionally have data that needs to be merged back into the observable,
|
|
662
|
-
// for example Firebase may update dateModified with the server timestamp
|
|
663
|
-
if (changes && !isEmpty(changes)) {
|
|
664
|
-
transformedChanges = transformLoadData(changes, syncOptions, false, 'set');
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (localState.numSavesOutstanding > 0) {
|
|
668
|
-
if (transformedChanges) {
|
|
669
|
-
if (!localState.pendingSaveResults) {
|
|
670
|
-
localState.pendingSaveResults = [];
|
|
671
|
-
}
|
|
672
|
-
localState.pendingSaveResults.push(transformedChanges);
|
|
673
|
-
}
|
|
674
|
-
} else {
|
|
675
|
-
let allChanges = [...(localState.pendingSaveResults || []), transformedChanges].filter(
|
|
676
|
-
(v) => v !== undefined,
|
|
677
|
-
);
|
|
678
|
-
if (allChanges.length > 0) {
|
|
679
|
-
if (allChanges.some((change) => isPromise(change))) {
|
|
680
|
-
allChanges = await Promise.all(allChanges);
|
|
681
|
-
}
|
|
682
|
-
onChangeRemote(() => mergeIntoObservable(obs, ...allChanges));
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
if (persist) {
|
|
686
|
-
if (shouldSaveMetadata && !isEmpty(metadata)) {
|
|
687
|
-
updateMetadata(obs, localState, syncState, syncOptions, metadata);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
localState.pendingSaveResults = [];
|
|
692
|
-
}
|
|
693
|
-
onAfterSet?.();
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function onObsChange<T>(
|
|
700
|
-
value$: ObservableParam<T>,
|
|
701
|
-
syncState: ObservableObject<ObservableSyncState>,
|
|
702
|
-
localState: LocalState,
|
|
703
|
-
syncOptions: SyncedOptions<T>,
|
|
704
|
-
{ changes, loading, remote, getPrevious }: ListenerParams,
|
|
705
|
-
) {
|
|
706
|
-
if (!loading) {
|
|
707
|
-
const inRemoteChange = remote;
|
|
708
|
-
const isApplyingPending = localState.isApplyingPending;
|
|
709
|
-
// Queue changes in a microtask so that multiple changes within a frame get run together
|
|
710
|
-
_queuedChanges.push({
|
|
711
|
-
value$: value$ as Observable<any>,
|
|
712
|
-
syncState,
|
|
713
|
-
localState,
|
|
714
|
-
syncOptions,
|
|
715
|
-
changes,
|
|
716
|
-
inRemoteChange,
|
|
717
|
-
isApplyingPending: isApplyingPending!,
|
|
718
|
-
getPrevious,
|
|
719
|
-
});
|
|
720
|
-
if (_queuedChanges.length === 1) {
|
|
721
|
-
queueMicrotask(processQueuedChanges);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
async function loadLocal<T>(
|
|
727
|
-
value$: ObservableParam<T>,
|
|
728
|
-
syncOptions: SyncedOptions<any>,
|
|
729
|
-
syncState: ObservableObject<ObservableSyncState>,
|
|
730
|
-
localState: LocalState,
|
|
731
|
-
) {
|
|
732
|
-
const { persist } = syncOptions;
|
|
733
|
-
|
|
734
|
-
if (persist) {
|
|
735
|
-
const PersistPlugin: ClassConstructor<ObservablePersistPlugin> =
|
|
736
|
-
persist.plugin! || observableSyncConfiguration.persist?.plugin;
|
|
737
|
-
const { table, config } = parseLocalConfig(persist);
|
|
738
|
-
const node = getNode(value$);
|
|
739
|
-
|
|
740
|
-
if (!PersistPlugin) {
|
|
741
|
-
throw new Error('Local persist is not configured');
|
|
742
|
-
}
|
|
743
|
-
// Ensure there's only one instance of the cache plugin
|
|
744
|
-
if (!mapSyncPlugins.has(PersistPlugin)) {
|
|
745
|
-
const persistPlugin = new PersistPlugin();
|
|
746
|
-
const mapValue = { plugin: persistPlugin, initialized: observable(false) };
|
|
747
|
-
mapSyncPlugins.set(PersistPlugin, mapValue);
|
|
748
|
-
if (persistPlugin.initialize) {
|
|
749
|
-
const initializePromise = persistPlugin.initialize?.(observableSyncConfiguration?.persist || {});
|
|
750
|
-
if (isPromise(initializePromise)) {
|
|
751
|
-
await initializePromise;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
mapValue.initialized.set(true);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const { plugin, initialized: initialized$ } = mapSyncPlugins.get(PersistPlugin)!;
|
|
758
|
-
const persistPlugin = plugin as ObservablePersistPlugin;
|
|
759
|
-
|
|
760
|
-
localState.pluginPersist = persistPlugin as ObservablePersistPlugin;
|
|
761
|
-
|
|
762
|
-
if (!initialized$.peek()) {
|
|
763
|
-
await when(initialized$);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// If cache has an asynchronous load, wait for it
|
|
767
|
-
if (persistPlugin.loadTable) {
|
|
768
|
-
const promise = persistPlugin.loadTable(table, config);
|
|
769
|
-
if (promise) {
|
|
770
|
-
await promise;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Get current value for init
|
|
775
|
-
const prevValue = getNodeValue(node) as object;
|
|
776
|
-
|
|
777
|
-
// Get the value from state
|
|
778
|
-
let value = persistPlugin.getTable(table, prevValue, config);
|
|
779
|
-
const metadata = persistPlugin.getMetadata(table, config);
|
|
780
|
-
|
|
781
|
-
if (metadata) {
|
|
782
|
-
metadatas.set(value$, metadata);
|
|
783
|
-
localState.pendingChanges = metadata.pending;
|
|
784
|
-
syncState.assign({
|
|
785
|
-
lastSync: metadata.lastSync,
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Merge the data from local cache into the default state
|
|
790
|
-
if (value !== undefined) {
|
|
791
|
-
const { transform } = config;
|
|
792
|
-
|
|
793
|
-
value = transformLoadData(value, { transform }, true, 'get');
|
|
794
|
-
|
|
795
|
-
if (isPromise(value)) {
|
|
796
|
-
value = await value;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// isLoadingLocal prevents saving remotely when two different caches
|
|
800
|
-
// are set on the same observable
|
|
801
|
-
internal.globalState.isLoadingLocal = true;
|
|
802
|
-
|
|
803
|
-
// We want to merge the local data on top of any initial state the object is created with
|
|
804
|
-
if (value === null && (!prevValue || (prevValue as any)[symbolLinked])) {
|
|
805
|
-
value$.set(value);
|
|
806
|
-
} else {
|
|
807
|
-
mergeIntoObservable(value$, value);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
internal.globalState.isLoadingLocal = false;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
getNodeValue(getNode(node.state!)).clearPersist = () =>
|
|
814
|
-
Promise.all([
|
|
815
|
-
persistPlugin.deleteTable(table, config),
|
|
816
|
-
persistPlugin.deleteMetadata(table, config),
|
|
817
|
-
]) as unknown as Promise<void>;
|
|
818
|
-
}
|
|
819
|
-
syncState.isPersistLoaded.set(true);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
export function syncObservable<T>(
|
|
823
|
-
obs$: ObservableParam<T>,
|
|
824
|
-
syncOptions: SyncedOptions<T>,
|
|
825
|
-
): Observable<ObservableSyncState>;
|
|
826
|
-
export function syncObservable<T>(obs$: ObservableParam<T>, syncOptions: Synced<T>): Observable<ObservableSyncState>;
|
|
827
|
-
export function syncObservable<T>(
|
|
828
|
-
obs$: ObservableParam<T>,
|
|
829
|
-
syncOptionsOrSynced: SyncedOptions<T> | Synced<T>,
|
|
830
|
-
): Observable<ObservableSyncState> {
|
|
831
|
-
let syncOptions = syncOptionsOrSynced as SyncedOptions<T>;
|
|
832
|
-
// If it's a synced then get the SyncOptions from it
|
|
833
|
-
if (isFunction(syncOptions)) {
|
|
834
|
-
syncOptions = syncOptions()[symbolLinked];
|
|
835
|
-
}
|
|
836
|
-
const node = getNode(obs$);
|
|
837
|
-
|
|
838
|
-
// Merge remote sync options with global options
|
|
839
|
-
syncOptions = {
|
|
840
|
-
syncMode: 'auto',
|
|
841
|
-
...observableSyncConfiguration,
|
|
842
|
-
...removeNullUndefined(syncOptions || {}),
|
|
843
|
-
} as any;
|
|
844
|
-
const localState: LocalState = {};
|
|
845
|
-
let sync: () => Promise<void>;
|
|
846
|
-
|
|
847
|
-
const syncState = (node.state = observable<ObservableSyncState>({
|
|
848
|
-
isPersistLoaded: false,
|
|
849
|
-
isLoaded: !syncOptions.get,
|
|
850
|
-
isPersistEnabled: true,
|
|
851
|
-
isSyncEnabled: true,
|
|
852
|
-
clearPersist: undefined as unknown as () => Promise<void>,
|
|
853
|
-
sync: () => Promise.resolve(),
|
|
854
|
-
getPendingChanges: () => localState.pendingChanges,
|
|
855
|
-
}));
|
|
856
|
-
|
|
857
|
-
loadLocal(obs$, syncOptions, syncState, localState);
|
|
858
|
-
|
|
859
|
-
localState.pluginSync = syncObservableAdapter(syncOptions);
|
|
860
|
-
|
|
861
|
-
if (syncOptions.get) {
|
|
862
|
-
let isSynced = false;
|
|
863
|
-
let isSubscribed = false;
|
|
864
|
-
let unsubscribe: void | (() => void) = undefined;
|
|
865
|
-
sync = async () => {
|
|
866
|
-
if (isSynced && shouldIgnoreUnobserved(node, sync)) {
|
|
867
|
-
if (unsubscribe) {
|
|
868
|
-
isSubscribed = false;
|
|
869
|
-
unsubscribe();
|
|
870
|
-
unsubscribe = undefined;
|
|
871
|
-
}
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
const lastSync = metadatas.get(obs$)?.lastSync;
|
|
875
|
-
const pending = localState.pendingChanges;
|
|
876
|
-
const get = localState.pluginSync!.get?.bind(localState.pluginSync);
|
|
877
|
-
|
|
878
|
-
if (get) {
|
|
879
|
-
const runGet = () => {
|
|
880
|
-
const onChange = async ({ value, mode, lastSync }: UpdateFnParams) => {
|
|
881
|
-
mode = mode || syncOptions.mode || 'set';
|
|
882
|
-
if (value !== undefined) {
|
|
883
|
-
value = transformLoadData(value, syncOptions, true, 'get');
|
|
884
|
-
if (isPromise(value)) {
|
|
885
|
-
value = await (value as Promise<T>);
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const pending = localState.pendingChanges;
|
|
889
|
-
const currentValue = obs$.peek();
|
|
890
|
-
if (pending) {
|
|
891
|
-
let didChangeMetadata = false;
|
|
892
|
-
Object.keys(pending).forEach((key) => {
|
|
893
|
-
const p = key.split('/').filter((p) => p !== '');
|
|
894
|
-
const { v, t } = pending[key];
|
|
895
|
-
|
|
896
|
-
if (t.length === 0 || !value) {
|
|
897
|
-
if (isObject(value) && isObject(v)) {
|
|
898
|
-
Object.assign(value, v);
|
|
899
|
-
} else {
|
|
900
|
-
value = v;
|
|
901
|
-
}
|
|
902
|
-
} else if ((value as any)[p[0]] !== undefined) {
|
|
903
|
-
const curValue = getValueAtPath(currentValue as object, p);
|
|
904
|
-
const newValue = getValueAtPath(value as object, p);
|
|
905
|
-
if (JSON.stringify(curValue) === JSON.stringify(newValue)) {
|
|
906
|
-
delete pending[key];
|
|
907
|
-
didChangeMetadata = true;
|
|
908
|
-
} else {
|
|
909
|
-
(value as any) = setAtPath(
|
|
910
|
-
value as any,
|
|
911
|
-
p,
|
|
912
|
-
t,
|
|
913
|
-
v,
|
|
914
|
-
'merge',
|
|
915
|
-
obs$.peek(),
|
|
916
|
-
(path: string[], value: any) => {
|
|
917
|
-
delete pending[key];
|
|
918
|
-
pending[path.join('/')] = {
|
|
919
|
-
p: null,
|
|
920
|
-
v: value,
|
|
921
|
-
t: t.slice(0, path.length),
|
|
922
|
-
};
|
|
923
|
-
},
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
if (didChangeMetadata) {
|
|
930
|
-
updateMetadata(obs$, localState, syncState, syncOptions, {
|
|
931
|
-
pending,
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
onChangeRemote(() => {
|
|
937
|
-
if (mode === 'assign' && isObject(value)) {
|
|
938
|
-
(obs$ as unknown as Observable<object>).assign(value);
|
|
939
|
-
} else if (mode === 'append' && isArray(value)) {
|
|
940
|
-
(obs$ as unknown as Observable<any[]>).push(...value);
|
|
941
|
-
} else if (mode === 'prepend' && isArray(value)) {
|
|
942
|
-
(obs$ as unknown as Observable<any[]>).splice(0, 0, ...value);
|
|
943
|
-
} else if (mode === 'merge') {
|
|
944
|
-
mergeIntoObservable(obs$, value);
|
|
945
|
-
} else {
|
|
946
|
-
obs$.set(value);
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
if (lastSync && syncOptions.persist) {
|
|
951
|
-
updateMetadata(obs$, localState, syncState, syncOptions, {
|
|
952
|
-
lastSync,
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
};
|
|
956
|
-
get({
|
|
957
|
-
state: syncState,
|
|
958
|
-
value$: obs$,
|
|
959
|
-
options: syncOptions,
|
|
960
|
-
lastSync,
|
|
961
|
-
dateModified: lastSync,
|
|
962
|
-
onError: (error: Error) => {
|
|
963
|
-
syncOptions.onGetError?.(error);
|
|
964
|
-
},
|
|
965
|
-
onGet: () => {
|
|
966
|
-
node.state!.assign({
|
|
967
|
-
isLoaded: true,
|
|
968
|
-
error: undefined,
|
|
969
|
-
});
|
|
970
|
-
},
|
|
971
|
-
onChange,
|
|
972
|
-
});
|
|
973
|
-
if (!isSubscribed && syncOptions.subscribe) {
|
|
974
|
-
isSubscribed = true;
|
|
975
|
-
unsubscribe = syncOptions.subscribe({
|
|
976
|
-
node,
|
|
977
|
-
value$: obs$,
|
|
978
|
-
update: (params: ObservableOnChangeParams) => {
|
|
979
|
-
when(node.state!.isLoaded, () => {
|
|
980
|
-
params.mode ||= syncOptions.mode || 'merge';
|
|
981
|
-
onChange(params);
|
|
982
|
-
});
|
|
983
|
-
},
|
|
984
|
-
refresh: () => when(node.state!.isLoaded, sync),
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
};
|
|
988
|
-
runGet();
|
|
989
|
-
} else {
|
|
990
|
-
node.state!.assign({
|
|
991
|
-
isLoaded: true,
|
|
992
|
-
error: undefined,
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
if (!isSynced) {
|
|
996
|
-
isSynced = true;
|
|
997
|
-
// Wait for remote to be ready before saving pending
|
|
998
|
-
await when(() => syncState.isLoaded.get() || (syncOptions.allowSetIfGetError && syncState.error.get()));
|
|
999
|
-
|
|
1000
|
-
if (pending && !isEmpty(pending)) {
|
|
1001
|
-
localState.isApplyingPending = true;
|
|
1002
|
-
const keys = Object.keys(pending);
|
|
1003
|
-
|
|
1004
|
-
// Bundle up all the changes from pending
|
|
1005
|
-
const changes: Change[] = [];
|
|
1006
|
-
for (let i = 0; i < keys.length; i++) {
|
|
1007
|
-
const key = keys[i];
|
|
1008
|
-
const path = key.split('/').filter((p) => p !== '');
|
|
1009
|
-
const { p, v, t } = pending[key];
|
|
1010
|
-
changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// Send the changes into onObsChange so that they get synced remotely
|
|
1014
|
-
const value = getNodeValue(node);
|
|
1015
|
-
onObsChange(obs$, syncState, localState, syncOptions, {
|
|
1016
|
-
value,
|
|
1017
|
-
loading: false,
|
|
1018
|
-
remote: false,
|
|
1019
|
-
getPrevious: createPreviousHandler(value, changes),
|
|
1020
|
-
changes,
|
|
1021
|
-
});
|
|
1022
|
-
localState.isApplyingPending = false;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
};
|
|
1026
|
-
|
|
1027
|
-
syncState.assign({ sync });
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Wait for this node and all parent nodes up the hierarchy to be loaded
|
|
1031
|
-
const onAllPersistLoaded = () => {
|
|
1032
|
-
let parentNode: NodeValue | undefined = node;
|
|
1033
|
-
while (parentNode) {
|
|
1034
|
-
if (parentNode.state?.isPersistLoaded?.get() === false) {
|
|
1035
|
-
return false;
|
|
1036
|
-
}
|
|
1037
|
-
parentNode = parentNode.parent;
|
|
1038
|
-
}
|
|
1039
|
-
return true;
|
|
1040
|
-
};
|
|
1041
|
-
// When all is loaded locally we can start syncing and listening for changes
|
|
1042
|
-
when(onAllPersistLoaded, function (this: any) {
|
|
1043
|
-
// If remote is not manual, then sync() is called automatically
|
|
1044
|
-
if (syncOptions.get && syncOptions.syncMode === 'auto') {
|
|
1045
|
-
sync();
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
if (syncOptions?.set || syncOptions?.persist) {
|
|
1049
|
-
obs$.onChange(
|
|
1050
|
-
onObsChange.bind(this, obs$ as any, syncState, localState, syncOptions as SyncedOptions<any>),
|
|
1051
|
-
);
|
|
1052
|
-
}
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
return syncState;
|
|
1056
|
-
}
|