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

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