@legendapp/state 2.2.0-next.7 → 2.2.0-next.71

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