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