@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
package/persist.js CHANGED
@@ -7,12 +7,13 @@ function configureObservablePersistence(options) {
7
7
  Object.assign(observablePersistConfiguration, options);
8
8
  }
9
9
 
10
+ const { initializePathType, symbolDelete } = state.internal;
10
11
  let validateMap;
11
12
  function transformPath(path, pathTypes, map) {
12
13
  const data = {};
13
14
  let d = data;
14
15
  for (let i = 0; i < path.length; i++) {
15
- d = d[path[i]] = i === path.length - 1 ? null : pathTypes[i] === 'array' ? [] : {};
16
+ d = d[path[i]] = i === path.length - 1 ? null : initializePathType(pathTypes[i]);
16
17
  }
17
18
  let value = transformObject(data, map);
18
19
  const pathOut = [];
@@ -29,7 +30,7 @@ function transformObject(dataIn, map) {
29
30
  }
30
31
  let ret = dataIn;
31
32
  if (dataIn) {
32
- if (dataIn === state.symbolDelete)
33
+ if (dataIn === symbolDelete)
33
34
  return dataIn;
34
35
  if (state.isString(dataIn)) {
35
36
  return map[dataIn];
@@ -135,13 +136,31 @@ if (process.env.NODE_ENV === 'development') {
135
136
  };
136
137
  }
137
138
 
139
+ function removeNullUndefined(a, recursive) {
140
+ const out = {};
141
+ Object.keys(a).forEach((key) => {
142
+ if (a[key] !== null && a[key] !== undefined) {
143
+ out[key] = recursive && state.isObject(a[key]) ? removeNullUndefined(a[key]) : a[key];
144
+ }
145
+ });
146
+ return out;
147
+ }
148
+
138
149
  function observablePersistRemoteFunctionsAdapter({ get, set, }) {
139
150
  const ret = {};
140
151
  if (get) {
141
152
  ret.get = (async (params) => {
142
153
  try {
143
- const value = (await get(params));
144
- params.onChange({ value, dateModified: params.dateModified });
154
+ let value = get(params);
155
+ if (state.isPromise(value)) {
156
+ value = await value;
157
+ }
158
+ params.onChange({
159
+ value,
160
+ dateModified: params.dateModified,
161
+ lastSync: params.lastSync,
162
+ mode: params.mode,
163
+ });
145
164
  params.onGet();
146
165
  // eslint-disable-next-line no-empty
147
166
  }
@@ -154,31 +173,29 @@ function observablePersistRemoteFunctionsAdapter({ get, set, }) {
154
173
  return ret;
155
174
  }
156
175
 
157
- const { globalState: globalState$1 } = state.internal;
176
+ const { globalState: globalState$2, symbolLinked: symbolLinked$3, getNode: getNode$1 } = state.internal;
158
177
  const mapPersistences = new WeakMap();
159
- const metadatas = new WeakMap();
160
- const promisesLocalSaves = new Set();
161
- function parseLocalConfig(config) {
178
+ const metadatas$1 = new WeakMap();
179
+ const promisesLocalSaves$1 = new Set();
180
+ function parseLocalConfig$1(config) {
162
181
  return config
163
182
  ? state.isString(config)
164
183
  ? { table: config, config: { name: config } }
165
184
  : { table: config.name, config }
166
185
  : {};
167
186
  }
168
- function doInOrder(arg1, arg2) {
187
+ function doInOrder$1(arg1, arg2) {
169
188
  return state.isPromise(arg1) ? arg1.then(arg2) : arg2(arg1);
170
189
  }
171
- function onChangeRemote(cb) {
172
- state.when(() => !globalState$1.isLoadingRemote$.get(), () => {
173
- // Remote changes should only update local state
174
- globalState$1.isLoadingRemote$.set(true);
175
- state.batch(cb, () => {
176
- globalState$1.isLoadingRemote$.set(false);
177
- });
178
- });
190
+ function onChangeRemote$1(cb) {
191
+ state.endBatch(true);
192
+ // Remote changes should only update local state
193
+ globalState$2.isLoadingRemote = true;
194
+ state.batch(cb);
195
+ globalState$2.isLoadingRemote = false;
179
196
  }
180
197
  function transformOutData(value, path, pathTypes, { transform, fieldTransforms }) {
181
- if (fieldTransforms || (transform === null || transform === void 0 ? void 0 : transform.out)) {
198
+ if (fieldTransforms || (transform === null || transform === void 0 ? void 0 : transform.save)) {
182
199
  const transformFn = () => {
183
200
  if (fieldTransforms) {
184
201
  const { obj, path: pathTransformed } = transformObjectWithPath(value, path, pathTypes, fieldTransforms);
@@ -187,69 +204,111 @@ function transformOutData(value, path, pathTypes, { transform, fieldTransforms }
187
204
  }
188
205
  return { value, path };
189
206
  };
190
- if (transform === null || transform === void 0 ? void 0 : transform.out) {
207
+ if (transform === null || transform === void 0 ? void 0 : transform.save) {
191
208
  const constructed = state.constructObjectWithPath(path, pathTypes, value);
192
- const saved = transform.out(constructed);
209
+ const saved = transform.save(constructed);
193
210
  const deconstruct = (toDeconstruct) => {
194
211
  value = state.deconstructObjectWithPath(path, pathTypes, toDeconstruct);
195
212
  return transformFn();
196
213
  };
197
- return doInOrder(saved, deconstruct);
214
+ return doInOrder$1(saved, deconstruct);
198
215
  }
199
216
  return transformFn();
200
217
  }
201
218
  return { value, path };
202
219
  }
203
- function transformLoadData(value, { transform, fieldTransforms }, doUserTransform) {
220
+ function transformLoadData$1(value, { transform, fieldTransforms }, doUserTransform) {
204
221
  if (fieldTransforms) {
205
222
  const inverted = invertFieldMap(fieldTransforms);
206
223
  value = transformObject(value, inverted);
207
224
  }
208
- if (doUserTransform && (transform === null || transform === void 0 ? void 0 : transform.in)) {
209
- value = transform.in(value);
225
+ if (doUserTransform && (transform === null || transform === void 0 ? void 0 : transform.load)) {
226
+ value = transform.load(value);
210
227
  }
211
228
  return value;
212
229
  }
213
- async function updateMetadataImmediate(obs, localState, syncState, persistOptions, newMetadata) {
214
- const saves = Array.from(promisesLocalSaves);
230
+ async function updateMetadataImmediate$1(obs, localState, syncState, persistOptions, newMetadata) {
231
+ const saves = Array.from(promisesLocalSaves$1);
215
232
  if (saves.length > 0) {
216
233
  await Promise.all(saves);
217
234
  }
218
235
  const { persistenceLocal } = localState;
219
236
  const local = persistOptions.local;
220
- const { table, config } = parseLocalConfig(local);
237
+ const { table, config } = parseLocalConfig$1(local);
221
238
  // Save metadata
222
- const oldMetadata = metadatas.get(obs);
223
- const { modified, pending } = newMetadata;
224
- const needsUpdate = pending || (modified && (!oldMetadata || modified !== oldMetadata.modified));
239
+ const oldMetadata = metadatas$1.get(obs);
240
+ const { lastSync, pending } = newMetadata;
241
+ const needsUpdate = pending || (lastSync && (!oldMetadata || lastSync !== oldMetadata.lastSync));
225
242
  if (needsUpdate) {
226
243
  const metadata = Object.assign({}, oldMetadata, newMetadata);
227
- metadatas.set(obs, metadata);
244
+ metadatas$1.set(obs, metadata);
228
245
  if (persistenceLocal) {
229
246
  await persistenceLocal.setMetadata(table, metadata, config);
230
247
  }
231
- if (modified) {
232
- syncState.dateModified.set(modified);
248
+ if (lastSync) {
249
+ syncState.assign({
250
+ lastSync: lastSync,
251
+ dateModified: lastSync,
252
+ });
233
253
  }
234
254
  }
235
255
  }
236
- function updateMetadata(obs, localState, syncState, persistOptions, newMetadata) {
256
+ function updateMetadata$1(obs, localState, syncState, persistOptions, newMetadata) {
237
257
  var _a;
238
258
  if (localState.timeoutSaveMetadata) {
239
259
  clearTimeout(localState.timeoutSaveMetadata);
240
260
  }
241
- localState.timeoutSaveMetadata = setTimeout(() => updateMetadataImmediate(obs, localState, syncState, persistOptions, newMetadata), ((_a = persistOptions === null || persistOptions === void 0 ? void 0 : persistOptions.remote) === null || _a === void 0 ? void 0 : _a.metadataTimeout) || 0);
261
+ localState.timeoutSaveMetadata = setTimeout(() => updateMetadataImmediate$1(obs, localState, syncState, persistOptions, newMetadata), ((_a = persistOptions === null || persistOptions === void 0 ? void 0 : persistOptions.remote) === null || _a === void 0 ? void 0 : _a.metadataTimeout) || 0);
242
262
  }
243
- let _queuedChanges = [];
244
- async function processQueuedChanges() {
263
+ let _queuedChanges$1 = [];
264
+ let _queuedRemoteChanges$1 = [];
265
+ let timeoutSaveRemote = undefined;
266
+ function mergeChanges$1(changes) {
267
+ const changesByPath = new Map();
268
+ const changesOut = [];
269
+ // TODO: This could be even more robust by going deeper into paths like the firebase plugin's _updatePendingSave
270
+ for (let i = 0; i < changes.length; i++) {
271
+ const change = changes[i];
272
+ const pathStr = change.path.join('/');
273
+ const existing = changesByPath.get(pathStr);
274
+ if (existing) {
275
+ existing.valueAtPath = change.valueAtPath;
276
+ }
277
+ else {
278
+ changesByPath.set(pathStr, change);
279
+ changesOut.push(change);
280
+ }
281
+ }
282
+ return changesOut;
283
+ }
284
+ function mergeQueuedChanges$1(allChanges) {
285
+ const changesByObsRemote = new Map();
286
+ const changesByObsLocal = new Map();
287
+ const outRemote = new Map();
288
+ const outLocal = new Map();
289
+ for (let i = 0; i < allChanges.length; i++) {
290
+ const value = allChanges[i];
291
+ const { obs, changes, inRemoteChange } = value;
292
+ const changesMap = inRemoteChange ? changesByObsRemote : changesByObsLocal;
293
+ const existing = changesMap.get(obs);
294
+ const newChanges = existing ? [...existing, ...changes] : changes;
295
+ const merged = mergeChanges$1(newChanges);
296
+ changesMap.set(obs, merged);
297
+ value.changes = merged;
298
+ (inRemoteChange ? outRemote : outLocal).set(obs, value);
299
+ }
300
+ return Array.from(outRemote.values()).concat(Array.from(outLocal.values()));
301
+ }
302
+ async function processQueuedChanges$1() {
303
+ var _a;
245
304
  // Get a local copy of the queued changes and clear the global queue
246
- const queuedChanges = _queuedChanges;
247
- _queuedChanges = [];
305
+ const queuedChanges = mergeQueuedChanges$1(_queuedChanges$1);
306
+ _queuedChanges$1 = [];
307
+ _queuedRemoteChanges$1.push(...queuedChanges.filter((c) => !c.inRemoteChange));
248
308
  // Note: Summary of the order of operations these functions:
249
309
  // 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
250
310
  // We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
251
311
  // since some may take longer to transformSaveData than others.
252
- const changes = await Promise.all(queuedChanges.map(prepChange));
253
312
  // 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
254
313
  // the current value again on next load, which isn't too bad.
255
314
  // 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
@@ -257,25 +316,46 @@ async function processQueuedChanges() {
257
316
  // 4. Wait for remote load or error if allowed
258
317
  // 5. Save to remote
259
318
  // 6. On successful save, merge changes (if any) back into observable
260
- // 7. Lastly, update metadata to clear pending and update dateModified. Doing this earlier could potentially cause
319
+ // 7. Lastly, update metadata to clear pending and update lastSync. Doing this earlier could potentially cause
261
320
  // sync inconsistences so it's very important that this is last.
262
- changes.forEach(doChange);
321
+ const preppedChangesLocal = await Promise.all(queuedChanges.map(prepChangeLocal$1));
322
+ // 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?
323
+ await Promise.all(queuedChanges.map(prepChangeRemote$1));
324
+ await Promise.all(preppedChangesLocal.map(doChangeLocal$1));
325
+ const timeout = (_a = observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.remoteOptions) === null || _a === void 0 ? void 0 : _a.debounceSet;
326
+ if (timeout) {
327
+ if (timeoutSaveRemote) {
328
+ clearTimeout(timeoutSaveRemote);
329
+ }
330
+ timeoutSaveRemote = setTimeout(processQueuedRemoteChanges$1, timeout);
331
+ }
332
+ else {
333
+ processQueuedRemoteChanges$1();
334
+ }
335
+ }
336
+ async function processQueuedRemoteChanges$1() {
337
+ const queuedRemoteChanges = mergeQueuedChanges$1(_queuedRemoteChanges$1);
338
+ _queuedRemoteChanges$1 = [];
339
+ const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote$1));
340
+ preppedChangesRemote.forEach(doChangeRemote$1);
263
341
  }
264
- async function prepChange(queuedChange) {
342
+ async function prepChangeLocal$1(queuedChange) {
265
343
  const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
266
344
  const local = persistOptions.local;
267
345
  const { persistenceRemote } = localState;
268
- const { config: configLocal } = parseLocalConfig(local);
346
+ const { config: configLocal } = parseLocalConfig$1(local);
269
347
  const configRemote = persistOptions.remote;
270
348
  const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
271
- const saveRemote = !inRemoteChange && (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
349
+ const saveRemote = !!(!inRemoteChange &&
350
+ (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) &&
351
+ !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) &&
352
+ syncState.isEnabledRemote.peek());
272
353
  if (saveLocal || saveRemote) {
273
354
  if (saveLocal && !syncState.isLoadedLocal.peek()) {
274
355
  console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
275
- return;
356
+ return undefined;
276
357
  }
277
358
  const changesLocal = [];
278
- const changesRemote = [];
279
359
  const changesPaths = new Set();
280
360
  let promisesTransform = [];
281
361
  // Reverse order
@@ -300,7 +380,7 @@ async function prepChange(queuedChange) {
300
380
  const { prevAtPath, valueAtPath, pathTypes } = changes[i];
301
381
  if (saveLocal) {
302
382
  const promiseTransformLocal = transformOutData(valueAtPath, path, pathTypes, configLocal);
303
- promisesTransform.push(doInOrder(promiseTransformLocal, ({ path: pathTransformed, value: valueTransformed }) => {
383
+ promisesTransform.push(doInOrder$1(promiseTransformLocal, ({ path: pathTransformed, value: valueTransformed }) => {
304
384
  // If path includes undefined there was a null in fieldTransforms so don't need to save it
305
385
  if (!pathTransformed.includes(undefined)) {
306
386
  // Prepare the local change with the transformed path/value
@@ -314,9 +394,55 @@ async function prepChange(queuedChange) {
314
394
  }
315
395
  }));
316
396
  }
397
+ }
398
+ }
399
+ // If there's any transform promises, wait for them before saving
400
+ promisesTransform = promisesTransform.filter(Boolean);
401
+ if (promisesTransform.length > 0) {
402
+ await Promise.all(promisesTransform);
403
+ }
404
+ return { queuedChange, changesLocal, saveRemote };
405
+ }
406
+ }
407
+ async function prepChangeRemote$1(queuedChange) {
408
+ const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
409
+ const local = persistOptions.local;
410
+ const { persistenceRemote } = localState;
411
+ const { config: configLocal } = parseLocalConfig$1(local);
412
+ const configRemote = persistOptions.remote;
413
+ const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
414
+ const saveRemote = !inRemoteChange && (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
415
+ if (saveLocal || saveRemote) {
416
+ if (saveLocal && !syncState.isLoadedLocal.peek()) {
417
+ console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
418
+ return undefined;
419
+ }
420
+ const changesRemote = [];
421
+ const changesPaths = new Set();
422
+ let promisesTransform = [];
423
+ // Reverse order
424
+ for (let i = changes.length - 1; i >= 0; i--) {
425
+ const { path } = changes[i];
426
+ let found = false;
427
+ // Optimization to only save the latest update at each path. We might have multiple changes at the same path
428
+ // and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
429
+ // already processed. If a later change modifies a parent of an earlier change (which happens on delete()
430
+ // it should be ignored as it's superseded by the parent modification.
431
+ if (changesPaths.size > 0) {
432
+ for (let u = 0; u < path.length; u++) {
433
+ if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
434
+ found = true;
435
+ break;
436
+ }
437
+ }
438
+ }
439
+ if (!found) {
440
+ const pathStr = path.join('/');
441
+ changesPaths.add(pathStr);
442
+ const { prevAtPath, valueAtPath, pathTypes } = changes[i];
317
443
  if (saveRemote) {
318
444
  const promiseTransformRemote = transformOutData(valueAtPath, path, pathTypes, configRemote || {});
319
- promisesTransform.push(doInOrder(promiseTransformRemote, ({ path: pathTransformed, value: valueTransformed }) => {
445
+ promisesTransform.push(doInOrder$1(promiseTransformRemote, ({ path: pathTransformed, value: valueTransformed }) => {
320
446
  var _a;
321
447
  // If path includes undefined there was a null in fieldTransforms so don't need to save it
322
448
  if (!pathTransformed.includes(undefined)) {
@@ -371,23 +497,22 @@ async function prepChange(queuedChange) {
371
497
  if (promisesTransform.length > 0) {
372
498
  await Promise.all(promisesTransform);
373
499
  }
374
- return { queuedChange, changesLocal, changesRemote };
500
+ return { queuedChange, changesRemote };
375
501
  }
376
502
  }
377
- async function doChange(changeInfo) {
378
- var _a, _b, _c, _d;
503
+ async function doChangeLocal$1(changeInfo) {
379
504
  if (!changeInfo)
380
505
  return;
381
- const { queuedChange, changesLocal, changesRemote } = changeInfo;
506
+ const { queuedChange, changesLocal, saveRemote } = changeInfo;
382
507
  const { obs, syncState, localState, persistOptions } = queuedChange;
383
- const { persistenceLocal, persistenceRemote } = localState;
508
+ const { persistenceLocal } = localState;
384
509
  const local = persistOptions.local;
385
- const { table, config: configLocal } = parseLocalConfig(local);
510
+ const { table, config: configLocal } = parseLocalConfig$1(local);
386
511
  const configRemote = persistOptions.remote;
387
512
  const shouldSaveMetadata = local && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.offlineBehavior) === 'retry';
388
- if (changesRemote.length > 0 && shouldSaveMetadata) {
513
+ if (saveRemote && shouldSaveMetadata) {
389
514
  // First save pending changes before saving local or remote
390
- await updateMetadataImmediate(obs, localState, syncState, persistOptions, {
515
+ await updateMetadataImmediate$1(obs, localState, syncState, persistOptions, {
391
516
  pending: localState.pendingChanges,
392
517
  });
393
518
  }
@@ -397,37 +522,63 @@ async function doChange(changeInfo) {
397
522
  let promiseSet = persistenceLocal.set(table, changesLocal, configLocal);
398
523
  if (promiseSet) {
399
524
  promiseSet = promiseSet.then(() => {
400
- promisesLocalSaves.delete(promiseSet);
525
+ promisesLocalSaves$1.delete(promiseSet);
401
526
  });
402
527
  // Keep track of local save promises so that updateMetadata runs only after everything is saved
403
- promisesLocalSaves.add(promiseSet);
528
+ promisesLocalSaves$1.add(promiseSet);
404
529
  // await the local save before proceeding to save remotely
405
530
  await promiseSet;
406
531
  }
407
532
  }
533
+ }
534
+ async function doChangeRemote$1(changeInfo) {
535
+ var _a;
536
+ if (!changeInfo)
537
+ return;
538
+ const { queuedChange, changesRemote } = changeInfo;
539
+ const { obs, syncState, localState, persistOptions } = queuedChange;
540
+ const { persistenceLocal, persistenceRemote } = localState;
541
+ const local = persistOptions.local;
542
+ const { table, config: configLocal } = parseLocalConfig$1(local);
543
+ const { offlineBehavior, allowSetIfError, onBeforeSet, onSetError, waitForSet, onAfterSet } = persistOptions.remote || {};
544
+ const shouldSaveMetadata = local && offlineBehavior === 'retry';
408
545
  if (changesRemote.length > 0) {
409
546
  // Wait for remote to be ready before saving
410
- await state.when(() => syncState.isLoaded.get() || ((configRemote === null || configRemote === void 0 ? void 0 : configRemote.allowSetIfError) && syncState.error.get()));
547
+ await state.when(() => syncState.isLoaded.get() || (allowSetIfError && syncState.error.get()));
548
+ if (waitForSet) {
549
+ const waitFor = state.isFunction(waitForSet)
550
+ ? waitForSet({ changes: changesRemote, value: obs.peek() })
551
+ : waitForSet;
552
+ if (waitFor) {
553
+ await state.when(waitFor);
554
+ }
555
+ }
411
556
  const value = obs.peek();
412
- (_a = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onBeforeSet) === null || _a === void 0 ? void 0 : _a.call(configRemote);
413
- const saved = await ((_b = persistenceRemote.set({
557
+ onBeforeSet === null || onBeforeSet === void 0 ? void 0 : onBeforeSet();
558
+ localState.numSavesOutstanding = (localState.numSavesOutstanding || 0) + 1;
559
+ let savedPromise = persistenceRemote.set({
414
560
  obs,
415
561
  syncState: syncState,
416
562
  options: persistOptions,
417
563
  changes: changesRemote,
418
564
  value,
419
- })) === null || _b === void 0 ? void 0 : _b.catch((err) => { var _a; return (_a = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onSetError) === null || _a === void 0 ? void 0 : _a.call(configRemote, err); }));
565
+ });
566
+ if (state.isPromise(savedPromise)) {
567
+ savedPromise = savedPromise.catch((err) => onSetError === null || onSetError === void 0 ? void 0 : onSetError(err));
568
+ }
569
+ const saved = await savedPromise;
570
+ localState.numSavesOutstanding--;
420
571
  // If this remote save changed anything then update persistence and metadata
421
572
  // Because save happens after a timeout and they're batched together, some calls to save will
422
573
  // return saved data and others won't, so those can be ignored.
423
574
  if (saved) {
424
575
  const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
425
- const { changes, dateModified } = saved;
576
+ const { changes, lastSync } = saved;
426
577
  if (pathStrs.length > 0) {
427
578
  if (local) {
428
579
  const metadata = {};
429
- const pending = (_c = persistenceLocal.getMetadata(table, configLocal)) === null || _c === void 0 ? void 0 : _c.pending;
430
- let transformedChanges = [];
580
+ const pending = (_a = persistenceLocal.getMetadata(table, configLocal)) === null || _a === void 0 ? void 0 : _a.pending;
581
+ let transformedChanges = undefined;
431
582
  for (let i = 0; i < pathStrs.length; i++) {
432
583
  const pathStr = pathStrs[i];
433
584
  // Clear pending for this path
@@ -437,35 +588,47 @@ async function doChange(changeInfo) {
437
588
  metadata.pending = pending;
438
589
  }
439
590
  }
440
- if (dateModified) {
441
- metadata.modified = dateModified;
591
+ if (lastSync) {
592
+ metadata.lastSync = lastSync;
442
593
  }
443
594
  // Remote can optionally have data that needs to be merged back into the observable,
444
595
  // for example Firebase may update dateModified with the server timestamp
445
596
  if (changes && !state.isEmpty(changes)) {
446
- transformedChanges.push(transformLoadData(changes, persistOptions.remote, false));
597
+ transformedChanges = transformLoadData$1(changes, persistOptions.remote, false);
447
598
  }
448
- if (transformedChanges.length > 0) {
449
- if (transformedChanges.some((change) => state.isPromise(change))) {
450
- transformedChanges = await Promise.all(transformedChanges);
599
+ if (localState.numSavesOutstanding > 0) {
600
+ if (transformedChanges) {
601
+ if (!localState.pendingSaveResults) {
602
+ localState.pendingSaveResults = [];
603
+ }
604
+ localState.pendingSaveResults.push(transformedChanges);
451
605
  }
452
- onChangeRemote(() => state.mergeIntoObservable(obs, ...transformedChanges));
453
606
  }
454
- if (shouldSaveMetadata && !state.isEmpty(metadata)) {
455
- updateMetadata(obs, localState, syncState, persistOptions, metadata);
607
+ else {
608
+ let allChanges = [...(localState.pendingSaveResults || []), transformedChanges];
609
+ if (allChanges.length > 0) {
610
+ if (allChanges.some((change) => state.isPromise(change))) {
611
+ allChanges = await Promise.all(allChanges);
612
+ }
613
+ onChangeRemote$1(() => state.mergeIntoObservable(obs, ...allChanges));
614
+ }
615
+ if (shouldSaveMetadata && !state.isEmpty(metadata)) {
616
+ updateMetadata$1(obs, localState, syncState, persistOptions, metadata);
617
+ }
618
+ localState.pendingSaveResults = [];
456
619
  }
457
620
  }
458
- (_d = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onSet) === null || _d === void 0 ? void 0 : _d.call(configRemote);
621
+ onAfterSet === null || onAfterSet === void 0 ? void 0 : onAfterSet();
459
622
  }
460
623
  }
461
624
  }
462
625
  }
463
- function onObsChange(obs, syncState, localState, persistOptions, { changes }) {
464
- if (!state.internal.globalState.isLoadingLocal) {
465
- const inRemoteChange = state.internal.globalState.isLoadingRemote$.peek();
626
+ function onObsChange$1(obs, syncState, localState, persistOptions, { changes, loading, remote }) {
627
+ if (!loading) {
628
+ const inRemoteChange = remote;
466
629
  const isApplyingPending = localState.isApplyingPending;
467
630
  // Queue changes in a microtask so that multiple changes within a frame get run together
468
- _queuedChanges.push({
631
+ _queuedChanges$1.push({
469
632
  obs: obs,
470
633
  syncState,
471
634
  localState,
@@ -474,17 +637,17 @@ function onObsChange(obs, syncState, localState, persistOptions, { changes }) {
474
637
  inRemoteChange,
475
638
  isApplyingPending: isApplyingPending,
476
639
  });
477
- if (_queuedChanges.length === 1) {
478
- queueMicrotask(processQueuedChanges);
640
+ if (_queuedChanges$1.length === 1) {
641
+ queueMicrotask(processQueuedChanges$1);
479
642
  }
480
643
  }
481
644
  }
482
- async function loadLocal(obs, persistOptions, syncState, localState) {
645
+ async function loadLocal$1(obs, persistOptions, syncState, localState) {
483
646
  var _a;
484
647
  const { local } = persistOptions;
485
648
  const localPersistence = persistOptions.pluginLocal || observablePersistConfiguration.pluginLocal;
486
649
  if (local) {
487
- const { table, config } = parseLocalConfig(local);
650
+ const { table, config } = parseLocalConfig$1(local);
488
651
  if (!localPersistence) {
489
652
  throw new Error('Local persistence is not configured');
490
653
  }
@@ -503,7 +666,7 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
503
666
  }
504
667
  const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
505
668
  localState.persistenceLocal = persistenceLocal;
506
- if (!initialized.get()) {
669
+ if (!initialized.peek()) {
507
670
  await state.when(initialized);
508
671
  }
509
672
  // If persistence has an asynchronous load, wait for it
@@ -517,28 +680,40 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
517
680
  let value = persistenceLocal.getTable(table, config);
518
681
  const metadata = persistenceLocal.getMetadata(table, config);
519
682
  if (metadata) {
520
- metadatas.set(obs, metadata);
683
+ // @ts-expect-error Migration from old version
684
+ if (!metadata.lastSync && metadata.modified) {
685
+ // @ts-expect-error Migration from old
686
+ metadata.lastSync = metadata.modified;
687
+ }
688
+ metadatas$1.set(obs, metadata);
521
689
  localState.pendingChanges = metadata.pending;
522
- syncState.dateModified.set(metadata.modified);
690
+ // TODOV3 Remove dateModified
691
+ syncState.assign({
692
+ dateModified: metadata.lastSync,
693
+ lastSync: metadata.lastSync,
694
+ });
523
695
  }
524
696
  // Merge the data from local persistence into the default state
525
- if (value !== null && value !== undefined) {
697
+ if (value !== undefined) {
526
698
  const { transform, fieldTransforms } = config;
527
- value = transformLoadData(value, { transform, fieldTransforms }, true);
699
+ value = transformLoadData$1(value, { transform, fieldTransforms }, true);
528
700
  if (state.isPromise(value)) {
529
701
  value = await value;
530
702
  }
531
- state.batch(() => {
532
- // isLoadingLocal prevents saving remotely when two different persistences
533
- // are set on the same observable
534
- state.internal.globalState.isLoadingLocal = true;
535
- // We want to merge the local data on top of any initial state the object is created with
703
+ // isLoadingLocal prevents saving remotely when two different persistences
704
+ // are set on the same observable
705
+ state.internal.globalState.isLoadingLocal = true;
706
+ // We want to merge the local data on top of any initial state the object is created with
707
+ const prevValue = obs.peek();
708
+ if (value === null && (!prevValue || prevValue[symbolLinked$3])) {
709
+ obs.set(value);
710
+ }
711
+ else {
536
712
  state.mergeIntoObservable(obs, value);
537
- }, () => {
538
- state.internal.globalState.isLoadingLocal = false;
539
- });
713
+ }
714
+ state.internal.globalState.isLoadingLocal = false;
540
715
  }
541
- const node = state.getNode(obs);
716
+ const node = getNode$1(obs);
542
717
  node.state.peek().clearLocal = () => Promise.all([
543
718
  persistenceLocal.deleteTable(table, config),
544
719
  persistenceLocal.deleteMetadata(table, config),
@@ -547,17 +722,13 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
547
722
  syncState.isLoadedLocal.set(true);
548
723
  }
549
724
  function persistObservable(initialOrObservable, persistOptions) {
550
- var _a;
551
- const obs = (state.isObservable(initialOrObservable)
725
+ const obs$ = (state.isObservable(initialOrObservable)
552
726
  ? initialOrObservable
553
727
  : state.observable(state.isFunction(initialOrObservable) ? initialOrObservable() : initialOrObservable));
554
- const node = state.getNode(obs);
555
- if (process.env.NODE_ENV === 'development' && ((_a = obs === null || obs === void 0 ? void 0 : obs.peek()) === null || _a === void 0 ? void 0 : _a._state)) {
556
- console.warn('[legend-state] WARNING: persistObservable creates a property named "_state" but your observable already has "state" in it');
557
- }
558
- // Merge remote persist options with clobal options
728
+ const node = getNode$1(obs$);
729
+ // Merge remote persist options with global options
559
730
  if (persistOptions.remote) {
560
- persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, persistOptions.remote);
731
+ persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, removeNullUndefined(persistOptions.remote));
561
732
  }
562
733
  let { remote } = persistOptions;
563
734
  const { local } = persistOptions;
@@ -569,12 +740,11 @@ function persistObservable(initialOrObservable, persistOptions) {
569
740
  isLoaded: false,
570
741
  isEnabledLocal: true,
571
742
  isEnabledRemote: true,
572
- refreshNum: 0,
573
743
  clearLocal: undefined,
574
744
  sync: () => Promise.resolve(),
575
745
  getPendingChanges: () => localState.pendingChanges,
576
746
  }));
577
- loadLocal(obs, persistOptions, syncState, localState);
747
+ loadLocal$1(obs$, persistOptions, syncState, localState);
578
748
  if (remote || remotePersistence) {
579
749
  if (!remotePersistence) {
580
750
  throw new Error('Remote persistence is not configured');
@@ -598,88 +768,92 @@ function persistObservable(initialOrObservable, persistOptions) {
598
768
  let isSynced = false;
599
769
  sync = async () => {
600
770
  var _a, _b;
601
- if (!isSynced) {
602
- isSynced = true;
603
- const dateModified = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.modified;
604
- const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
605
- if (get) {
606
- const runGet = () => {
607
- get({
608
- state: syncState,
609
- obs,
610
- options: persistOptions,
611
- dateModified,
612
- onError: (error) => {
613
- var _a;
614
- (_a = remote.onGetError) === null || _a === void 0 ? void 0 : _a.call(remote, error);
615
- },
616
- onGet: () => {
617
- node.state.assign({
618
- isLoaded: true,
619
- error: undefined,
620
- });
621
- },
622
- onChange: async ({ value, path = [], pathTypes = [], mode = 'set', dateModified }) => {
623
- // Note: value is the constructed value, path is used for setInObservableAtPath
624
- // to start the set into the observable from the path
625
- if (value !== undefined) {
626
- value = transformLoadData(value, remote, true);
627
- if (state.isPromise(value)) {
628
- value = await value;
629
- }
630
- const invertedMap = remote.fieldTransforms && invertFieldMap(remote.fieldTransforms);
631
- if (path.length && invertedMap) {
632
- path = transformPath(path, pathTypes, invertedMap);
633
- }
634
- if (mode === 'dateModified') {
635
- if (dateModified && !state.isEmpty(value)) {
636
- onChangeRemote(() => {
637
- state.setInObservableAtPath(obs, path, pathTypes, value, 'assign');
638
- });
639
- }
640
- }
641
- else {
642
- const pending = localState.pendingChanges;
643
- if (pending) {
644
- Object.keys(pending).forEach((key) => {
645
- const p = key.split('/').filter((p) => p !== '');
646
- const { v, t } = pending[key];
647
- if (value[p[0]] !== undefined) {
648
- value = state.setAtPath(value, p, t, v, obs.peek(), (path, value) => {
649
- delete pending[key];
650
- pending[path.join('/')] = {
651
- p: null,
652
- v: value,
653
- t: t.slice(0, path.length),
654
- };
655
- });
656
- }
657
- });
658
- }
659
- onChangeRemote(() => {
660
- state.setInObservableAtPath(obs, path, pathTypes, value, mode);
771
+ const lastSync = (_a = metadatas$1.get(obs$)) === null || _a === void 0 ? void 0 : _a.lastSync;
772
+ const pending = localState.pendingChanges;
773
+ const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
774
+ if (get) {
775
+ const runGet = () => {
776
+ get({
777
+ state: syncState,
778
+ obs: obs$,
779
+ options: persistOptions,
780
+ lastSync,
781
+ dateModified: lastSync,
782
+ onError: (error) => {
783
+ var _a;
784
+ (_a = remote.onGetError) === null || _a === void 0 ? void 0 : _a.call(remote, error);
785
+ },
786
+ onGet: () => {
787
+ node.state.assign({
788
+ isLoaded: true,
789
+ error: undefined,
790
+ });
791
+ },
792
+ onChange: async ({ value, path = [], pathTypes = [], mode = 'set', lastSync }) => {
793
+ // Note: value is the constructed value, path is used for setInObservableAtPath
794
+ // to start the set into the observable from the path
795
+ if (value !== undefined) {
796
+ value = transformLoadData$1(value, remote, true);
797
+ if (state.isPromise(value)) {
798
+ value = await value;
799
+ }
800
+ const invertedMap = remote.fieldTransforms && invertFieldMap(remote.fieldTransforms);
801
+ if (path.length && invertedMap) {
802
+ path = transformPath(path, pathTypes, invertedMap);
803
+ }
804
+ if (mode === 'lastSync' || mode === 'dateModified') {
805
+ if (lastSync && !state.isEmpty(value)) {
806
+ onChangeRemote$1(() => {
807
+ state.setInObservableAtPath(obs$, path, pathTypes, value, 'assign');
661
808
  });
662
809
  }
663
810
  }
664
- if (dateModified && local) {
665
- updateMetadata(obs, localState, syncState, persistOptions, {
666
- modified: dateModified,
811
+ else {
812
+ const pending = localState.pendingChanges;
813
+ if (pending) {
814
+ Object.keys(pending).forEach((key) => {
815
+ const p = key.split('/').filter((p) => p !== '');
816
+ const { v, t } = pending[key];
817
+ if (t.length === 0 || !value) {
818
+ value = v;
819
+ }
820
+ else if (value[p[0]] !== undefined) {
821
+ value = state.setAtPath(value, p, t, v, obs$.peek(), (path, value) => {
822
+ delete pending[key];
823
+ pending[path.join('/')] = {
824
+ p: null,
825
+ v: value,
826
+ t: t.slice(0, path.length),
827
+ };
828
+ });
829
+ }
830
+ });
831
+ }
832
+ onChangeRemote$1(() => {
833
+ state.setInObservableAtPath(obs$, path, pathTypes, value, mode);
667
834
  });
668
835
  }
669
- },
670
- });
671
- };
672
- runGet();
673
- }
674
- else {
675
- node.state.assign({
676
- isLoaded: true,
677
- error: undefined,
836
+ }
837
+ if (lastSync && local) {
838
+ updateMetadata$1(obs$, localState, syncState, persistOptions, {
839
+ lastSync,
840
+ });
841
+ }
842
+ },
678
843
  });
679
- }
844
+ };
845
+ runGet();
846
+ }
847
+ else {
848
+ node.state.assign({
849
+ isLoaded: true,
850
+ error: undefined,
851
+ });
852
+ }
853
+ if (!isSynced) {
854
+ isSynced = true;
680
855
  // Wait for remote to be ready before saving pending
681
856
  await state.when(() => syncState.isLoaded.get() || (remote.allowSetIfError && syncState.error.get()));
682
- const pending = localState.pendingChanges;
683
857
  if (pending && !state.isEmpty(pending)) {
684
858
  localState.isApplyingPending = true;
685
859
  const keys = Object.keys(pending);
@@ -692,8 +866,11 @@ function persistObservable(initialOrObservable, persistOptions) {
692
866
  changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
693
867
  }
694
868
  // Send the changes into onObsChange so that they get persisted remotely
695
- onObsChange(obs, syncState, localState, persistOptions, {
696
- value: obs.peek(),
869
+ // TODO: Not sure why this needs to as unknown as Observable
870
+ onObsChange$1(obs$, syncState, localState, persistOptions, {
871
+ value: obs$.peek(),
872
+ loading: false,
873
+ remote: false,
697
874
  // TODO getPrevious if any remote persistence layers need it
698
875
  getPrevious: () => undefined,
699
876
  changes,
@@ -702,17 +879,15 @@ function persistObservable(initialOrObservable, persistOptions) {
702
879
  }
703
880
  }
704
881
  };
705
- // If remote is manual, then sync() must be called manually
706
- if (remote.manual) {
707
- syncState.assign({ sync });
708
- }
882
+ syncState.assign({ sync });
709
883
  }
710
884
  // Wait for this node and all parent nodes up the hierarchy to be loaded
711
885
  const onAllLoadedLocal = () => {
712
886
  var _a, _b;
713
887
  let parentNode = node;
714
888
  while (parentNode) {
715
- if (((_b = (_a = parentNode.state) === null || _a === void 0 ? void 0 : _a.isLoadedLocal) === null || _b === void 0 ? void 0 : _b.get()) === false) {
889
+ if (((_b = (_a = parentNode.state) === null || _a === void 0 ? void 0 : _a.isLoadedLocal) === null || _b === void 0 ? void 0 : _b.get()) ===
890
+ false) {
716
891
  return false;
717
892
  }
718
893
  parentNode = parentNode.parent;
@@ -725,100 +900,925 @@ function persistObservable(initialOrObservable, persistOptions) {
725
900
  if (remote && !remote.manual) {
726
901
  sync();
727
902
  }
728
- obs.onChange(onObsChange.bind(this, obs, syncState, localState, persistOptions));
903
+ obs$.onChange(onObsChange$1.bind(this, obs$, syncState, localState, persistOptions));
729
904
  });
730
- return obs;
905
+ return obs$;
731
906
  }
732
907
 
733
- const { getProxy, globalState, setupRetry } = state.internal;
734
- function persistActivateNode() {
735
- globalState.activateNode = function activateNodePersist(node, refresh, wasPromise, newValue) {
736
- const { onSetFn, subscriber, lastSync, cacheOptions, retryOptions } = node.activationState;
737
- let onChange = undefined;
738
- const pluginRemote = {
739
- get: async (params) => {
740
- onChange = params.onChange;
741
- if (state.isPromise(newValue)) {
742
- try {
743
- newValue = await newValue;
744
- // eslint-disable-next-line no-empty
745
- }
746
- catch (_a) { }
747
- }
748
- if (lastSync.value) {
749
- params.dateModified = lastSync.value;
750
- }
751
- return newValue;
752
- },
753
- };
754
- if (onSetFn) {
755
- // TODO: Work out these types better
756
- pluginRemote.set = async (params) => {
757
- var _a;
758
- if ((_a = node.state) === null || _a === void 0 ? void 0 : _a.isLoaded.get()) {
759
- return new Promise((resolve) => {
760
- const attemptNum = { current: 0 };
761
- const run = async () => {
762
- let changes = {};
763
- let maxModified = 0;
764
- let didError = false;
765
- let onError;
766
- if (retryOptions) {
767
- onError = setupRetry(retryOptions, run, attemptNum).handleError;
768
- }
769
- await onSetFn(params, {
770
- update: (params) => {
771
- const { value, dateModified } = params;
772
- maxModified = Math.max(dateModified || 0, maxModified);
773
- changes = state.mergeIntoObservable(changes, value);
774
- },
775
- onError: () => {
776
- didError = true;
777
- onError === null || onError === void 0 ? void 0 : onError();
778
- },
779
- refresh,
780
- });
781
- if (!didError) {
782
- resolve({ changes, dateModified: maxModified || undefined });
783
- }
784
- };
785
- run();
786
- });
908
+ const observableSyncConfiguration = {};
909
+
910
+ function syncObservableAdapter({ get, set }) {
911
+ const ret = {};
912
+ if (get) {
913
+ ret.get = (async (params) => {
914
+ try {
915
+ let value = get(params);
916
+ if (state.isPromise(value)) {
917
+ value = await value;
787
918
  }
788
- };
919
+ params.onChange({
920
+ value,
921
+ lastSync: params.lastSync,
922
+ mode: params.mode,
923
+ });
924
+ params.onGet();
925
+ // eslint-disable-next-line no-empty
926
+ }
927
+ catch (_a) { }
928
+ });
929
+ }
930
+ if (set) {
931
+ ret.set = set;
932
+ }
933
+ return ret;
934
+ }
935
+
936
+ const { createPreviousHandler, clone, getValueAtPath, globalState: globalState$1, symbolLinked: symbolLinked$2, getNode, getNodeValue: getNodeValue$1 } = state.internal;
937
+ const mapSyncPlugins = new WeakMap();
938
+ const metadatas = new WeakMap();
939
+ const promisesLocalSaves = new Set();
940
+ function parseLocalConfig(config) {
941
+ return config
942
+ ? state.isString(config)
943
+ ? { table: config, config: { name: config } }
944
+ : { table: config.name, config }
945
+ : {};
946
+ }
947
+ function doInOrder(arg1, arg2) {
948
+ return state.isPromise(arg1) ? arg1.then(arg2) : arg2(arg1);
949
+ }
950
+ function onChangeRemote(cb) {
951
+ state.endBatch(true);
952
+ // Remote changes should only update local state
953
+ globalState$1.isLoadingRemote = true;
954
+ state.beginBatch();
955
+ cb();
956
+ // Reset isLoadingRemote before ending the batch so it doesn't
957
+ // apply to any side effects
958
+ globalState$1.isLoadingRemote = false;
959
+ state.endBatch(true);
960
+ }
961
+ function transformSaveData(value, path, pathTypes, { transform }) {
962
+ if (transform === null || transform === void 0 ? void 0 : transform.save) {
963
+ const constructed = state.constructObjectWithPath(path, pathTypes, value);
964
+ const saved = transform.save(constructed);
965
+ value = state.deconstructObjectWithPath(path, pathTypes, saved);
966
+ }
967
+ return value;
968
+ }
969
+ function transformLoadData(value, { transform }, doUserTransform, method) {
970
+ if (doUserTransform && (transform === null || transform === void 0 ? void 0 : transform.load)) {
971
+ value = transform.load(value, method);
972
+ }
973
+ return value;
974
+ }
975
+ async function updateMetadataImmediate(obs, localState, syncState, syncOptions, newMetadata) {
976
+ const saves = Array.from(promisesLocalSaves);
977
+ if (saves.length > 0) {
978
+ await Promise.all(saves);
979
+ }
980
+ const { pluginPersist } = localState;
981
+ const { table, config } = parseLocalConfig(syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.persist);
982
+ // Save metadata
983
+ const oldMetadata = metadatas.get(obs);
984
+ const { lastSync, pending } = newMetadata;
985
+ const needsUpdate = pending || (lastSync && (!oldMetadata || lastSync !== oldMetadata.lastSync));
986
+ if (needsUpdate) {
987
+ const metadata = Object.assign({}, oldMetadata, newMetadata);
988
+ metadatas.set(obs, metadata);
989
+ if (pluginPersist) {
990
+ await pluginPersist.setMetadata(table, metadata, config);
789
991
  }
790
- if (subscriber) {
791
- subscriber({
792
- update: (params) => {
793
- if (!onChange) {
794
- // TODO: Make this message better
795
- console.log('[legend-state] Cannot update immediately before the first return');
796
- }
797
- else {
798
- onChange(params);
799
- }
800
- },
801
- refresh,
992
+ if (lastSync) {
993
+ syncState.assign({
994
+ lastSync: lastSync,
802
995
  });
803
996
  }
804
- persistObservable(getProxy(node), {
805
- pluginRemote,
806
- ...(cacheOptions || {}),
807
- remote: {
808
- retry: retryOptions,
809
- },
810
- });
811
- return { update: onChange };
812
- };
997
+ }
998
+ }
999
+ function updateMetadata(obs, localState, syncState, syncOptions, newMetadata) {
1000
+ if (localState.timeoutSaveMetadata) {
1001
+ clearTimeout(localState.timeoutSaveMetadata);
1002
+ }
1003
+ localState.timeoutSaveMetadata = setTimeout(() => updateMetadataImmediate(obs, localState, syncState, syncOptions, newMetadata), 0);
1004
+ }
1005
+ let _queuedChanges = [];
1006
+ const _queuedRemoteChanges = new Map();
1007
+ const _queuedRemoteChangesTimeouts = new Map();
1008
+ function mergeChanges(changes) {
1009
+ const changesByPath = new Map();
1010
+ const changesOut = [];
1011
+ // TODO: This could be even more robust by going deeper into paths like the firebase plugin's _updatePendingSave
1012
+ for (let i = 0; i < changes.length; i++) {
1013
+ const change = changes[i];
1014
+ const pathStr = change.path.join('/');
1015
+ const existing = changesByPath.get(pathStr);
1016
+ if (existing) {
1017
+ // If setting a value back to what it was, no need to save it
1018
+ if (change.valueAtPath === existing.prevAtPath) {
1019
+ changesOut.splice(changesOut.indexOf(change), 1);
1020
+ }
1021
+ else {
1022
+ existing.valueAtPath = change.valueAtPath;
1023
+ }
1024
+ }
1025
+ else {
1026
+ changesByPath.set(pathStr, change);
1027
+ changesOut.push(change);
1028
+ }
1029
+ }
1030
+ return changesOut;
1031
+ }
1032
+ function mergeQueuedChanges(allChanges) {
1033
+ const changesByObsRemote = new Map();
1034
+ const changesByObsLocal = new Map();
1035
+ const previousByObs = new Map();
1036
+ const outRemote = new Map();
1037
+ const outLocal = new Map();
1038
+ for (let i = 0; i < allChanges.length; i++) {
1039
+ const value = allChanges[i];
1040
+ const { obs, changes, inRemoteChange, getPrevious } = value;
1041
+ const targetMap = inRemoteChange ? outRemote : outLocal;
1042
+ const changesMap = inRemoteChange ? changesByObsRemote : changesByObsLocal;
1043
+ const existing = changesMap.get(obs);
1044
+ const newChanges = existing ? [...existing, ...changes] : changes;
1045
+ const merged = mergeChanges(newChanges);
1046
+ changesMap.set(obs, merged);
1047
+ value.changes = merged;
1048
+ if (!previousByObs.has(obs)) {
1049
+ previousByObs.set(obs, getPrevious());
1050
+ }
1051
+ value.valuePrevious = previousByObs.get(obs);
1052
+ targetMap.set(obs, value);
1053
+ }
1054
+ return Array.from(outRemote.values()).concat(Array.from(outLocal.values()));
1055
+ }
1056
+ async function processQueuedChanges() {
1057
+ var _a;
1058
+ // Get a local copy of the queued changes and clear the global queue
1059
+ const queuedChanges = mergeQueuedChanges(_queuedChanges);
1060
+ _queuedChanges = [];
1061
+ const pendingSyncOptions = new Set();
1062
+ for (let i = 0; i < queuedChanges.length; i++) {
1063
+ const change = queuedChanges[i];
1064
+ if (!change.inRemoteChange) {
1065
+ if (!_queuedRemoteChanges.has(change.syncOptions)) {
1066
+ _queuedRemoteChanges.set(change.syncOptions, []);
1067
+ }
1068
+ pendingSyncOptions.add(change.syncOptions);
1069
+ _queuedRemoteChanges.get(change.syncOptions).push(change);
1070
+ }
1071
+ }
1072
+ // Note: Summary of the order of operations these functions:
1073
+ // 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
1074
+ // We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
1075
+ // since some may take longer to transformSaveData than others.
1076
+ // 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
1077
+ // the current value again on next load, which isn't too bad.
1078
+ // 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
1079
+ // and attempted again.
1080
+ // 4. Wait for remote load or error if allowed
1081
+ // 5. Save to remote
1082
+ // 6. On successful save, merge changes (if any) back into observable
1083
+ // 7. Lastly, update metadata to clear pending and update lastSync. Doing this earlier could potentially cause
1084
+ // sync inconsistences so it's very important that this is last.
1085
+ const preppedChangesLocal = await Promise.all(queuedChanges.map(prepChangeLocal));
1086
+ // 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?
1087
+ await Promise.all(queuedChanges.map(prepChangeRemote));
1088
+ await Promise.all(preppedChangesLocal.map(doChangeLocal));
1089
+ for (const options of pendingSyncOptions) {
1090
+ const timeout = (_a = options.debounceSet) !== null && _a !== void 0 ? _a : observableSyncConfiguration === null || observableSyncConfiguration === void 0 ? void 0 : observableSyncConfiguration.debounceSet;
1091
+ const timeoutSaveRemote = _queuedRemoteChangesTimeouts.get(options);
1092
+ const run = () => processQueuedRemoteChanges(options);
1093
+ if (timeout) {
1094
+ if (timeoutSaveRemote) {
1095
+ clearTimeout(timeoutSaveRemote);
1096
+ }
1097
+ _queuedRemoteChangesTimeouts.set(options, setTimeout(run, timeout));
1098
+ }
1099
+ else {
1100
+ run();
1101
+ }
1102
+ }
1103
+ }
1104
+ async function processQueuedRemoteChanges(syncOptions) {
1105
+ const arr = _queuedRemoteChanges.get(syncOptions);
1106
+ if (arr === null || arr === void 0 ? void 0 : arr.length) {
1107
+ const queuedRemoteChanges = mergeQueuedChanges(arr);
1108
+ _queuedRemoteChanges.set(syncOptions, []);
1109
+ const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote));
1110
+ preppedChangesRemote.forEach(doChangeRemote);
1111
+ }
1112
+ }
1113
+ async function prepChangeLocal(queuedChange) {
1114
+ const { syncState, changes, localState, syncOptions, inRemoteChange, isApplyingPending } = queuedChange;
1115
+ const persist = syncOptions.persist;
1116
+ const { pluginSync } = localState;
1117
+ const { config: configLocal } = parseLocalConfig(persist);
1118
+ const configRemote = syncOptions;
1119
+ const saveLocal = (persist === null || persist === void 0 ? void 0 : persist.name) && !configLocal.readonly && !isApplyingPending && syncState.isPersistEnabled.peek();
1120
+ const saveRemote = !!(!inRemoteChange &&
1121
+ (pluginSync === null || pluginSync === void 0 ? void 0 : pluginSync.set) &&
1122
+ (configRemote === null || configRemote === void 0 ? void 0 : configRemote.enableSync) !== false &&
1123
+ syncState.isSyncEnabled.peek());
1124
+ if (saveLocal || saveRemote) {
1125
+ if (saveLocal && !syncState.isPersistLoaded.peek()) {
1126
+ console.error('[legend-state] WARNING: An observable was changed before being loaded from persist', persist);
1127
+ return undefined;
1128
+ }
1129
+ const changesLocal = [];
1130
+ const changesPaths = new Set();
1131
+ let promisesTransform = [];
1132
+ // Reverse order
1133
+ for (let i = changes.length - 1; i >= 0; i--) {
1134
+ const { path } = changes[i];
1135
+ let found = false;
1136
+ // Optimization to only save the latest update at each path. We might have multiple changes at the same path
1137
+ // and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
1138
+ // already processed. If a later change modifies a parent of an earlier change (which happens on delete()
1139
+ // it should be ignored as it's superseded by the parent modification.
1140
+ if (changesPaths.size > 0) {
1141
+ for (let u = 0; u < path.length; u++) {
1142
+ if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
1143
+ found = true;
1144
+ break;
1145
+ }
1146
+ }
1147
+ }
1148
+ if (!found) {
1149
+ const pathStr = path.join('/');
1150
+ changesPaths.add(pathStr);
1151
+ const { prevAtPath, valueAtPath, pathTypes } = changes[i];
1152
+ if (saveLocal) {
1153
+ const promiseTransformLocal = transformSaveData(valueAtPath, path, pathTypes, configLocal);
1154
+ promisesTransform.push(doInOrder(promiseTransformLocal, (valueTransformed) => {
1155
+ // Prepare the local change with the transformed path/value
1156
+ changesLocal.push({
1157
+ path,
1158
+ pathTypes,
1159
+ prevAtPath,
1160
+ valueAtPath: valueTransformed,
1161
+ pathStr,
1162
+ });
1163
+ }));
1164
+ }
1165
+ }
1166
+ }
1167
+ // If there's any transform promises, wait for them before saving
1168
+ promisesTransform = promisesTransform.filter(Boolean);
1169
+ if (promisesTransform.length > 0) {
1170
+ await Promise.all(promisesTransform);
1171
+ }
1172
+ return { queuedChange, changesLocal, saveRemote };
1173
+ }
1174
+ }
1175
+ async function prepChangeRemote(queuedChange) {
1176
+ const { syncState, changes, localState, syncOptions: syncOptions, inRemoteChange, isApplyingPending, valuePrevious, } = queuedChange;
1177
+ const persist = syncOptions.persist;
1178
+ const { pluginSync } = localState;
1179
+ const { config: configLocal } = parseLocalConfig(persist);
1180
+ const configRemote = syncOptions;
1181
+ const saveLocal = persist && !configLocal.readonly && !isApplyingPending && syncState.isPersistEnabled.peek();
1182
+ const saveRemote = !inRemoteChange && (pluginSync === null || pluginSync === void 0 ? void 0 : pluginSync.set) && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.enableSync) !== false && syncState.isSyncEnabled.peek();
1183
+ if (saveLocal || saveRemote) {
1184
+ if (saveLocal && !syncState.isPersistLoaded.peek()) {
1185
+ console.error('[legend-state] WARNING: An observable was changed before being loaded from persist', persist);
1186
+ return undefined;
1187
+ }
1188
+ const changesRemote = [];
1189
+ const changesPaths = new Set();
1190
+ let promisesTransform = [];
1191
+ // Reverse order
1192
+ for (let i = changes.length - 1; i >= 0; i--) {
1193
+ const { path } = changes[i];
1194
+ let found = false;
1195
+ // Optimization to only save the latest update at each path. We might have multiple changes at the same path
1196
+ // and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
1197
+ // already processed. If a later change modifies a parent of an earlier change (which happens on delete()
1198
+ // it should be ignored as it's superseded by the parent modification.
1199
+ if (changesPaths.size > 0) {
1200
+ for (let u = 0; u < path.length; u++) {
1201
+ if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
1202
+ found = true;
1203
+ break;
1204
+ }
1205
+ }
1206
+ }
1207
+ if (!found) {
1208
+ const pathStr = path.join('/');
1209
+ changesPaths.add(pathStr);
1210
+ const { prevAtPath, valueAtPath, pathTypes } = changes[i];
1211
+ if (saveRemote) {
1212
+ const promiseTransformRemote = transformSaveData(valueAtPath, path, pathTypes, configRemote || {});
1213
+ promisesTransform.push(doInOrder(promiseTransformRemote, (valueTransformed) => {
1214
+ var _a;
1215
+ // Prepare pending changes
1216
+ if (!localState.pendingChanges) {
1217
+ localState.pendingChanges = {};
1218
+ }
1219
+ // First look for existing pending changes at a higher level than this change
1220
+ // If they exist then merge this change into it
1221
+ let found = false;
1222
+ for (let i = 0; !found && i < path.length - 1; i++) {
1223
+ const pathParent = path.slice(0, i + 1).join('/');
1224
+ if ((_a = localState.pendingChanges[pathParent]) === null || _a === void 0 ? void 0 : _a.v) {
1225
+ found = true;
1226
+ const pathChild = path.slice(i + 1);
1227
+ const pathTypesChild = pathTypes.slice(i + 1);
1228
+ state.setAtPath(localState.pendingChanges[pathParent].v, pathChild, pathTypesChild, valueAtPath);
1229
+ }
1230
+ }
1231
+ if (!found) {
1232
+ // If an existing pending change is deeper than this change, just delete it
1233
+ // in favor of this wider change
1234
+ for (const key in localState.pendingChanges) {
1235
+ if (key !== pathStr && key.startsWith(pathStr)) {
1236
+ delete localState.pendingChanges[key];
1237
+ }
1238
+ }
1239
+ // The "p" saved in pending should be the previous state before changes,
1240
+ // so don't overwrite it if it already exists
1241
+ if (!localState.pendingChanges[pathStr]) {
1242
+ localState.pendingChanges[pathStr] = { p: prevAtPath !== null && prevAtPath !== void 0 ? prevAtPath : null, t: pathTypes };
1243
+ }
1244
+ // Pending value is the untransformed value because it gets loaded without transformment
1245
+ // and forwarded through to onObsChange where it gets transformed before save
1246
+ localState.pendingChanges[pathStr].v = valueAtPath;
1247
+ }
1248
+ // Prepare the remote change with the transformed path/value
1249
+ changesRemote.push({
1250
+ path,
1251
+ pathTypes,
1252
+ prevAtPath,
1253
+ valueAtPath: valueTransformed,
1254
+ pathStr,
1255
+ valuePrevious,
1256
+ });
1257
+ }));
1258
+ }
1259
+ }
1260
+ }
1261
+ // If there's any transform promises, wait for them before saving
1262
+ promisesTransform = promisesTransform.filter(Boolean);
1263
+ if (promisesTransform.length > 0) {
1264
+ await Promise.all(promisesTransform);
1265
+ }
1266
+ return { queuedChange, changesRemote };
1267
+ }
1268
+ }
1269
+ async function doChangeLocal(changeInfo) {
1270
+ if (!changeInfo)
1271
+ return;
1272
+ const { queuedChange, changesLocal, saveRemote } = changeInfo;
1273
+ const { obs, syncState, localState, syncOptions: syncOptions } = queuedChange;
1274
+ const { pluginPersist } = localState;
1275
+ const persist = syncOptions.persist;
1276
+ const { table, config: configLocal } = parseLocalConfig(persist);
1277
+ const shouldSaveMetadata = persist === null || persist === void 0 ? void 0 : persist.retrySync;
1278
+ if (saveRemote && shouldSaveMetadata) {
1279
+ // First save pending changes before saving local or remote
1280
+ await updateMetadataImmediate(obs, localState, syncState, syncOptions, {
1281
+ pending: localState.pendingChanges,
1282
+ });
1283
+ }
1284
+ if (changesLocal.length > 0) {
1285
+ // Save the changes to local cache before saving to remote. They are already marked as pending so
1286
+ // if remote sync fails or the app is closed before remote sync, it will attempt to sync them on the next load.
1287
+ let promiseSet = pluginPersist.set(table, changesLocal, configLocal);
1288
+ if (promiseSet) {
1289
+ promiseSet = promiseSet.then(() => {
1290
+ promisesLocalSaves.delete(promiseSet);
1291
+ });
1292
+ // Keep track of local save promises so that updateMetadata runs only after everything is saved
1293
+ promisesLocalSaves.add(promiseSet);
1294
+ // await the local save before proceeding to save remotely
1295
+ await promiseSet;
1296
+ }
1297
+ }
1298
+ }
1299
+ async function doChangeRemote(changeInfo) {
1300
+ var _a, _b;
1301
+ if (!changeInfo)
1302
+ return;
1303
+ const { queuedChange, changesRemote } = changeInfo;
1304
+ const { obs, syncState, localState, syncOptions, valuePrevious: previous } = queuedChange;
1305
+ const { pluginPersist, pluginSync } = localState;
1306
+ const persist = syncOptions.persist;
1307
+ const { table, config: configLocal } = parseLocalConfig(persist);
1308
+ const { allowSetIfGetError, onBeforeSet, onSetError, waitForSet, onAfterSet } = syncOptions || {};
1309
+ const shouldSaveMetadata = persist === null || persist === void 0 ? void 0 : persist.retrySync;
1310
+ if (changesRemote.length > 0) {
1311
+ // Wait for remote to be ready before saving
1312
+ await state.when(() => syncState.isLoaded.get() || (allowSetIfGetError && syncState.error.get()));
1313
+ if (waitForSet) {
1314
+ const waitFor = state.isFunction(waitForSet)
1315
+ ? waitForSet({ changes: changesRemote, value: obs.peek() })
1316
+ : waitForSet;
1317
+ if (waitFor) {
1318
+ await state.when(waitFor);
1319
+ }
1320
+ }
1321
+ let value = obs.peek();
1322
+ const transformSave = (_a = syncOptions === null || syncOptions === void 0 ? void 0 : syncOptions.transform) === null || _a === void 0 ? void 0 : _a.save;
1323
+ if (transformSave) {
1324
+ value = transformSave(value);
1325
+ }
1326
+ onBeforeSet === null || onBeforeSet === void 0 ? void 0 : onBeforeSet();
1327
+ localState.numSavesOutstanding = (localState.numSavesOutstanding || 0) + 1;
1328
+ let savedPromise = pluginSync.set({
1329
+ obs,
1330
+ syncState: syncState,
1331
+ options: syncOptions,
1332
+ changes: changesRemote,
1333
+ value,
1334
+ valuePrevious: previous,
1335
+ });
1336
+ if (state.isPromise(savedPromise)) {
1337
+ savedPromise = savedPromise.catch((err) => onSetError === null || onSetError === void 0 ? void 0 : onSetError(err));
1338
+ }
1339
+ const saved = await savedPromise;
1340
+ localState.numSavesOutstanding--;
1341
+ // If this remote save changed anything then update cache and metadata
1342
+ // Because save happens after a timeout and they're batched together, some calls to save will
1343
+ // return saved data and others won't, so those can be ignored.
1344
+ if (saved) {
1345
+ const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
1346
+ const { changes, lastSync } = saved;
1347
+ if (pathStrs.length > 0) {
1348
+ let transformedChanges = undefined;
1349
+ const metadata = {};
1350
+ if (persist) {
1351
+ const pendingMetadata = (_b = pluginPersist.getMetadata(table, configLocal)) === null || _b === void 0 ? void 0 : _b.pending;
1352
+ const pending = localState.pendingChanges;
1353
+ for (let i = 0; i < pathStrs.length; i++) {
1354
+ const pathStr = pathStrs[i];
1355
+ // Clear pending for this path
1356
+ if (pendingMetadata === null || pendingMetadata === void 0 ? void 0 : pendingMetadata[pathStr]) {
1357
+ // Remove pending from persisted medata state
1358
+ delete pendingMetadata[pathStr];
1359
+ metadata.pending = pendingMetadata;
1360
+ }
1361
+ // Clear pending for this path if not already removed by above
1362
+ // pendingMetadata === pending sometimes
1363
+ if (pending === null || pending === void 0 ? void 0 : pending[pathStr]) {
1364
+ // Remove pending from local state
1365
+ delete pending[pathStr];
1366
+ }
1367
+ }
1368
+ if (lastSync) {
1369
+ metadata.lastSync = lastSync;
1370
+ }
1371
+ }
1372
+ // Remote can optionally have data that needs to be merged back into the observable,
1373
+ // for example Firebase may update dateModified with the server timestamp
1374
+ if (changes && !state.isEmpty(changes)) {
1375
+ transformedChanges = transformLoadData(changes, syncOptions, false, 'set');
1376
+ }
1377
+ if (localState.numSavesOutstanding > 0) {
1378
+ if (transformedChanges) {
1379
+ if (!localState.pendingSaveResults) {
1380
+ localState.pendingSaveResults = [];
1381
+ }
1382
+ localState.pendingSaveResults.push(transformedChanges);
1383
+ }
1384
+ }
1385
+ else {
1386
+ let allChanges = [...(localState.pendingSaveResults || []), transformedChanges].filter((v) => v !== undefined);
1387
+ if (allChanges.length > 0) {
1388
+ if (allChanges.some((change) => state.isPromise(change))) {
1389
+ allChanges = await Promise.all(allChanges);
1390
+ }
1391
+ onChangeRemote(() => state.mergeIntoObservable(obs, ...allChanges));
1392
+ }
1393
+ if (persist) {
1394
+ if (shouldSaveMetadata && !state.isEmpty(metadata)) {
1395
+ updateMetadata(obs, localState, syncState, syncOptions, metadata);
1396
+ }
1397
+ }
1398
+ localState.pendingSaveResults = [];
1399
+ }
1400
+ onAfterSet === null || onAfterSet === void 0 ? void 0 : onAfterSet();
1401
+ }
1402
+ }
1403
+ }
1404
+ }
1405
+ function onObsChange(obs, syncState, localState, syncOptions, { changes, loading, remote, getPrevious }) {
1406
+ if (!loading) {
1407
+ const inRemoteChange = remote;
1408
+ const isApplyingPending = localState.isApplyingPending;
1409
+ // Queue changes in a microtask so that multiple changes within a frame get run together
1410
+ _queuedChanges.push({
1411
+ obs: obs,
1412
+ syncState,
1413
+ localState,
1414
+ syncOptions,
1415
+ changes,
1416
+ inRemoteChange,
1417
+ isApplyingPending: isApplyingPending,
1418
+ getPrevious,
1419
+ });
1420
+ if (_queuedChanges.length === 1) {
1421
+ queueMicrotask(processQueuedChanges);
1422
+ }
1423
+ }
1424
+ }
1425
+ async function loadLocal(obs, syncOptions, syncState, localState) {
1426
+ var _a, _b;
1427
+ const { persist } = syncOptions;
1428
+ if (persist) {
1429
+ const PersistPlugin = persist.plugin || ((_a = observableSyncConfiguration.persist) === null || _a === void 0 ? void 0 : _a.plugin);
1430
+ const { table, config } = parseLocalConfig(persist);
1431
+ const node = getNode(obs);
1432
+ if (!PersistPlugin) {
1433
+ throw new Error('Local persist is not configured');
1434
+ }
1435
+ // Ensure there's only one instance of the cache plugin
1436
+ if (!mapSyncPlugins.has(PersistPlugin)) {
1437
+ const persistPlugin = new PersistPlugin();
1438
+ const mapValue = { plugin: persistPlugin, initialized: state.observable(false) };
1439
+ mapSyncPlugins.set(PersistPlugin, mapValue);
1440
+ if (persistPlugin.initialize) {
1441
+ const initializePromise = (_b = persistPlugin.initialize) === null || _b === void 0 ? void 0 : _b.call(persistPlugin, (observableSyncConfiguration === null || observableSyncConfiguration === void 0 ? void 0 : observableSyncConfiguration.persist) || {});
1442
+ if (state.isPromise(initializePromise)) {
1443
+ await initializePromise;
1444
+ }
1445
+ }
1446
+ mapValue.initialized.set(true);
1447
+ }
1448
+ const { plugin, initialized: initialized$ } = mapSyncPlugins.get(PersistPlugin);
1449
+ const persistPlugin = plugin;
1450
+ localState.pluginPersist = persistPlugin;
1451
+ if (!initialized$.peek()) {
1452
+ await state.when(initialized$);
1453
+ }
1454
+ // If cache has an asynchronous load, wait for it
1455
+ if (persistPlugin.loadTable) {
1456
+ const promise = persistPlugin.loadTable(table, config);
1457
+ if (promise) {
1458
+ await promise;
1459
+ }
1460
+ }
1461
+ // Get current value for init
1462
+ const prevValue = getNodeValue$1(node);
1463
+ // Get the value from state
1464
+ let value = persistPlugin.getTable(table, prevValue, config);
1465
+ const metadata = persistPlugin.getMetadata(table, config);
1466
+ if (metadata) {
1467
+ metadatas.set(obs, metadata);
1468
+ localState.pendingChanges = metadata.pending;
1469
+ syncState.assign({
1470
+ lastSync: metadata.lastSync,
1471
+ });
1472
+ }
1473
+ // Merge the data from local cache into the default state
1474
+ if (value !== undefined) {
1475
+ const { transform } = config;
1476
+ value = transformLoadData(value, { transform }, true, 'get');
1477
+ if (state.isPromise(value)) {
1478
+ value = await value;
1479
+ }
1480
+ // isLoadingLocal prevents saving remotely when two different caches
1481
+ // are set on the same observable
1482
+ state.internal.globalState.isLoadingLocal = true;
1483
+ // We want to merge the local data on top of any initial state the object is created with
1484
+ if (value === null && (!prevValue || prevValue[symbolLinked$2])) {
1485
+ obs.set(value);
1486
+ }
1487
+ else {
1488
+ state.mergeIntoObservable(obs, value);
1489
+ }
1490
+ state.internal.globalState.isLoadingLocal = false;
1491
+ }
1492
+ node.state.peek().clearPersist = () => Promise.all([
1493
+ persistPlugin.deleteTable(table, config),
1494
+ persistPlugin.deleteMetadata(table, config),
1495
+ ]);
1496
+ }
1497
+ syncState.isPersistLoaded.set(true);
1498
+ }
1499
+ function syncObservable(obs$, syncOptionsOrSynced) {
1500
+ let syncOptions = syncOptionsOrSynced;
1501
+ // If it's a synced then get the SyncOptions from it
1502
+ if (state.isFunction(syncOptions)) {
1503
+ syncOptions = syncOptions()[symbolLinked$2];
1504
+ }
1505
+ const node = getNode(obs$);
1506
+ // Merge remote sync options with global options
1507
+ syncOptions = state.mergeIntoObservable({
1508
+ syncMode: 'auto',
1509
+ }, clone(observableSyncConfiguration), removeNullUndefined(syncOptions || {}));
1510
+ const localState = {};
1511
+ let sync;
1512
+ const syncState = (node.state = state.observable({
1513
+ isPersistLoaded: false,
1514
+ isLoaded: !syncOptions.get,
1515
+ isPersistEnabled: true,
1516
+ isSyncEnabled: true,
1517
+ clearPersist: undefined,
1518
+ sync: () => Promise.resolve(),
1519
+ getPendingChanges: () => localState.pendingChanges,
1520
+ }));
1521
+ loadLocal(obs$, syncOptions, syncState, localState);
1522
+ localState.pluginSync = syncObservableAdapter(syncOptions);
1523
+ if (syncOptions.get) {
1524
+ let isSynced = false;
1525
+ sync = async () => {
1526
+ var _a, _b;
1527
+ const lastSync = (_a = metadatas.get(obs$)) === null || _a === void 0 ? void 0 : _a.lastSync;
1528
+ const pending = localState.pendingChanges;
1529
+ const get = (_b = localState.pluginSync.get) === null || _b === void 0 ? void 0 : _b.bind(localState.pluginSync);
1530
+ if (get) {
1531
+ const runGet = () => {
1532
+ const onChange = async ({ value, mode, lastSync }) => {
1533
+ mode = mode || syncOptions.mode || 'set';
1534
+ if (value !== undefined) {
1535
+ value = transformLoadData(value, syncOptions, true, 'get');
1536
+ if (state.isPromise(value)) {
1537
+ value = await value;
1538
+ }
1539
+ const pending = localState.pendingChanges;
1540
+ const currentValue = obs$.peek();
1541
+ if (pending) {
1542
+ let didChangeMetadata = false;
1543
+ Object.keys(pending).forEach((key) => {
1544
+ const p = key.split('/').filter((p) => p !== '');
1545
+ const { v, t } = pending[key];
1546
+ if (t.length === 0 || !value) {
1547
+ if (state.isObject(value) && state.isObject(v)) {
1548
+ Object.assign(value, v);
1549
+ }
1550
+ else {
1551
+ value = v;
1552
+ }
1553
+ }
1554
+ else if (value[p[0]] !== undefined) {
1555
+ const curValue = getValueAtPath(currentValue, p);
1556
+ const newValue = getValueAtPath(value, p);
1557
+ if (JSON.stringify(curValue) === JSON.stringify(newValue)) {
1558
+ delete pending[key];
1559
+ didChangeMetadata = true;
1560
+ }
1561
+ else {
1562
+ value = state.setAtPath(value, p, t, v, 'merge', obs$.peek(), (path, value) => {
1563
+ delete pending[key];
1564
+ pending[path.join('/')] = {
1565
+ p: null,
1566
+ v: value,
1567
+ t: t.slice(0, path.length),
1568
+ };
1569
+ });
1570
+ }
1571
+ }
1572
+ });
1573
+ if (didChangeMetadata) {
1574
+ updateMetadata(obs$, localState, syncState, syncOptions, {
1575
+ pending,
1576
+ });
1577
+ }
1578
+ }
1579
+ onChangeRemote(() => {
1580
+ if (mode === 'assign' && state.isObject(value)) {
1581
+ obs$.assign(value);
1582
+ }
1583
+ else if (mode === 'append' && state.isArray(value)) {
1584
+ obs$.push(...value);
1585
+ }
1586
+ else if (mode === 'prepend' && state.isArray(value)) {
1587
+ obs$.splice(0, 0, ...value);
1588
+ }
1589
+ else if (mode === 'merge') {
1590
+ state.mergeIntoObservable(obs$, value);
1591
+ }
1592
+ else {
1593
+ obs$.set(value);
1594
+ }
1595
+ });
1596
+ }
1597
+ if (lastSync && syncOptions.persist) {
1598
+ updateMetadata(obs$, localState, syncState, syncOptions, {
1599
+ lastSync,
1600
+ });
1601
+ }
1602
+ };
1603
+ get({
1604
+ state: syncState,
1605
+ obs: obs$,
1606
+ options: syncOptions,
1607
+ lastSync,
1608
+ dateModified: lastSync,
1609
+ onError: (error) => {
1610
+ var _a;
1611
+ (_a = syncOptions.onGetError) === null || _a === void 0 ? void 0 : _a.call(syncOptions, error);
1612
+ },
1613
+ onGet: () => {
1614
+ const isFirstLoad = !node.state.isLoaded.peek();
1615
+ node.state.assign({
1616
+ isLoaded: true,
1617
+ error: undefined,
1618
+ });
1619
+ if (isFirstLoad && syncOptions.subscribe) {
1620
+ syncOptions.subscribe({
1621
+ node,
1622
+ update: (params) => {
1623
+ params.mode || (params.mode = syncOptions.mode || 'merge');
1624
+ onChange(params);
1625
+ },
1626
+ refresh: sync,
1627
+ });
1628
+ }
1629
+ },
1630
+ onChange,
1631
+ });
1632
+ };
1633
+ runGet();
1634
+ }
1635
+ else {
1636
+ node.state.assign({
1637
+ isLoaded: true,
1638
+ error: undefined,
1639
+ });
1640
+ }
1641
+ if (!isSynced) {
1642
+ isSynced = true;
1643
+ // Wait for remote to be ready before saving pending
1644
+ await state.when(() => syncState.isLoaded.get() || (syncOptions.allowSetIfGetError && syncState.error.get()));
1645
+ if (pending && !state.isEmpty(pending)) {
1646
+ localState.isApplyingPending = true;
1647
+ const keys = Object.keys(pending);
1648
+ // Bundle up all the changes from pending
1649
+ const changes = [];
1650
+ for (let i = 0; i < keys.length; i++) {
1651
+ const key = keys[i];
1652
+ const path = key.split('/').filter((p) => p !== '');
1653
+ const { p, v, t } = pending[key];
1654
+ changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
1655
+ }
1656
+ // Send the changes into onObsChange so that they get synced remotely
1657
+ const value = getNodeValue$1(node);
1658
+ onObsChange(obs$, syncState, localState, syncOptions, {
1659
+ value,
1660
+ loading: false,
1661
+ remote: false,
1662
+ getPrevious: createPreviousHandler(value, changes),
1663
+ changes,
1664
+ });
1665
+ localState.isApplyingPending = false;
1666
+ }
1667
+ }
1668
+ };
1669
+ syncState.assign({ sync });
1670
+ }
1671
+ // Wait for this node and all parent nodes up the hierarchy to be loaded
1672
+ const onAllPersistLoaded = () => {
1673
+ var _a, _b;
1674
+ let parentNode = node;
1675
+ while (parentNode) {
1676
+ if (((_b = (_a = parentNode.state) === null || _a === void 0 ? void 0 : _a.isPersistLoaded) === null || _b === void 0 ? void 0 : _b.get()) === false) {
1677
+ return false;
1678
+ }
1679
+ parentNode = parentNode.parent;
1680
+ }
1681
+ return true;
1682
+ };
1683
+ // When all is loaded locally we can start syncing and listening for changes
1684
+ state.when(onAllPersistLoaded, function () {
1685
+ // If remote is not manual, then sync() is called automatically
1686
+ if (syncOptions.get && syncOptions.syncMode === 'auto') {
1687
+ sync();
1688
+ }
1689
+ obs$.onChange(onObsChange.bind(this, obs$, syncState, localState, syncOptions));
1690
+ });
1691
+ return syncState;
1692
+ }
1693
+
1694
+ const { getProxy, globalState, runWithRetry, symbolLinked: symbolLinked$1, setNodeValue, getNodeValue } = state.internal;
1695
+ function enableActivateSyncedNode() {
1696
+ globalState.activateSyncedNode = function activateSyncedNode(node, newValue) {
1697
+ const obs$ = getProxy(node);
1698
+ if (node.activationState) {
1699
+ // If it is a Synced
1700
+ const { get, initial, set, retry } = node.activationState;
1701
+ let onChange = undefined;
1702
+ const pluginRemote = {};
1703
+ let promiseReturn = undefined;
1704
+ // Not sure why this disable is needed, but it's needed to make the linter happy
1705
+ // eslint-disable-next-line prefer-const
1706
+ let syncState;
1707
+ const refresh = () => syncState === null || syncState === void 0 ? void 0 : syncState.sync();
1708
+ if (get) {
1709
+ pluginRemote.get = (params) => {
1710
+ var _a;
1711
+ onChange = params.onChange;
1712
+ const updateLastSync = (lastSync) => (params.lastSync = lastSync);
1713
+ const existingValue = getNodeValue(node);
1714
+ const value = runWithRetry(node, { attemptNum: 0, retry: retry || ((_a = params.options) === null || _a === void 0 ? void 0 : _a.retry) }, () => {
1715
+ const paramsToGet = {
1716
+ value: state.isFunction(existingValue) || (existingValue === null || existingValue === void 0 ? void 0 : existingValue[symbolLinked$1]) ? undefined : existingValue,
1717
+ lastSync: params.lastSync,
1718
+ updateLastSync,
1719
+ mode: params.mode,
1720
+ refresh,
1721
+ };
1722
+ const ret = get(paramsToGet);
1723
+ params.mode = paramsToGet.mode;
1724
+ return ret;
1725
+ });
1726
+ promiseReturn = value;
1727
+ return value;
1728
+ };
1729
+ }
1730
+ if (set) {
1731
+ // TODO: Work out these types better
1732
+ pluginRemote.set = async (params) => {
1733
+ var _a, _b;
1734
+ if ((_a = node.state) === null || _a === void 0 ? void 0 : _a.isLoaded.get()) {
1735
+ const retryAttempts = { attemptNum: 0, retry: retry || ((_b = params.options) === null || _b === void 0 ? void 0 : _b.retry) };
1736
+ return runWithRetry(node, retryAttempts, async (retryEvent) => {
1737
+ let changes = {};
1738
+ let maxModified = 0;
1739
+ if (!node.state.isLoaded.peek()) {
1740
+ await state.whenReady(node.state.isLoaded);
1741
+ }
1742
+ const cancelRetry = () => {
1743
+ retryEvent.cancel = true;
1744
+ };
1745
+ await set({
1746
+ ...params,
1747
+ node,
1748
+ update: (params) => {
1749
+ const { value, lastSync } = params;
1750
+ maxModified = Math.max(lastSync || 0, maxModified);
1751
+ changes = state.mergeIntoObservable(changes, value);
1752
+ },
1753
+ retryNum: retryAttempts.attemptNum,
1754
+ cancelRetry,
1755
+ refresh,
1756
+ fromSubscribe: false,
1757
+ });
1758
+ return { changes, lastSync: maxModified || undefined };
1759
+ });
1760
+ }
1761
+ };
1762
+ }
1763
+ const nodeVal = getNodeValue(node);
1764
+ if (promiseReturn !== undefined) {
1765
+ newValue = promiseReturn;
1766
+ }
1767
+ else if (nodeVal !== undefined && !state.isFunction(nodeVal)) {
1768
+ newValue = nodeVal;
1769
+ }
1770
+ else {
1771
+ newValue = initial;
1772
+ }
1773
+ setNodeValue(node, promiseReturn ? undefined : newValue);
1774
+ // @ts-expect-error TODO fix these types
1775
+ syncState = syncObservable(obs$, { ...node.activationState, ...pluginRemote });
1776
+ return { update: onChange, value: newValue };
1777
+ }
1778
+ else {
1779
+ // If it is not a Synced
1780
+ let update = undefined;
1781
+ const get = async (params) => {
1782
+ update = params.refresh;
1783
+ if (state.isPromise(newValue)) {
1784
+ try {
1785
+ newValue = await newValue;
1786
+ }
1787
+ catch (_a) {
1788
+ // TODO Once we have global retry settings this should retry
1789
+ }
1790
+ }
1791
+ return newValue;
1792
+ };
1793
+ syncObservable(obs$, {
1794
+ get,
1795
+ });
1796
+ return { update: update, value: newValue };
1797
+ }
1798
+ };
1799
+ }
1800
+
1801
+ const { symbolLinked } = state.internal;
1802
+ function synced(params) {
1803
+ installPersistActivateNode();
1804
+ return (() => ({
1805
+ [symbolLinked]: { ...params, synced: true },
1806
+ }));
1807
+ }
1808
+ let didInstall = false;
1809
+ function installPersistActivateNode() {
1810
+ if (!didInstall) {
1811
+ enableActivateSyncedNode();
1812
+ didInstall = true;
1813
+ }
813
1814
  }
814
1815
 
815
1816
  function isInRemoteChange() {
816
- return state.internal.globalState.isLoadingRemote$.get();
1817
+ return state.internal.globalState.isLoadingRemote;
817
1818
  }
818
1819
  const internal = {
819
1820
  observablePersistConfiguration,
820
1821
  };
821
- persistActivateNode();
822
1822
 
823
1823
  exports.configureObservablePersistence = configureObservablePersistence;
824
1824
  exports.internal = internal;
@@ -826,6 +1826,7 @@ exports.invertFieldMap = invertFieldMap;
826
1826
  exports.isInRemoteChange = isInRemoteChange;
827
1827
  exports.mapPersistences = mapPersistences;
828
1828
  exports.persistObservable = persistObservable;
1829
+ exports.synced = synced;
829
1830
  exports.transformObject = transformObject;
830
1831
  exports.transformPath = transformPath;
831
1832
  //# sourceMappingURL=persist.js.map