@legendapp/state 2.2.0-next.8 → 2.2.0-next.80

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