@legendapp/state 2.2.0-next.3 → 2.2.0-next.31

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 (108) hide show
  1. package/babel.js.map +1 -1
  2. package/config/enableDirectAccess.d.ts +1 -1
  3. package/config/enableDirectPeek.d.ts +1 -1
  4. package/config/enableReactDirectRender.js.map +1 -1
  5. package/config/enableReactDirectRender.mjs.map +1 -1
  6. package/config/enableReactTracking.d.ts +4 -3
  7. package/config/enableReactTracking.js.map +1 -1
  8. package/config/enableReactTracking.mjs.map +1 -1
  9. package/config/enableReactUse.d.ts +1 -1
  10. package/helpers/fetch.d.ts +4 -3
  11. package/helpers/fetch.js.map +1 -1
  12. package/helpers/fetch.mjs.map +1 -1
  13. package/helpers/pageHash.js.map +1 -1
  14. package/helpers/pageHash.mjs.map +1 -1
  15. package/helpers/pageHashParams.js.map +1 -1
  16. package/helpers/pageHashParams.mjs.map +1 -1
  17. package/helpers/time.d.ts +2 -2
  18. package/helpers/time.js.map +1 -1
  19. package/helpers/time.mjs.map +1 -1
  20. package/history.js.map +1 -1
  21. package/history.mjs.map +1 -1
  22. package/index.d.ts +15 -4
  23. package/index.js +534 -242
  24. package/index.js.map +1 -1
  25. package/index.mjs +533 -243
  26. package/index.mjs.map +1 -1
  27. package/package.json +2 -10
  28. package/persist-plugins/async-storage.js.map +1 -1
  29. package/persist-plugins/async-storage.mjs.map +1 -1
  30. package/persist-plugins/fetch.js.map +1 -1
  31. package/persist-plugins/fetch.mjs.map +1 -1
  32. package/persist-plugins/firebase.js.map +1 -1
  33. package/persist-plugins/firebase.mjs.map +1 -1
  34. package/persist-plugins/indexeddb.js.map +1 -1
  35. package/persist-plugins/indexeddb.mjs.map +1 -1
  36. package/persist-plugins/local-storage.js +10 -2
  37. package/persist-plugins/local-storage.js.map +1 -1
  38. package/persist-plugins/local-storage.mjs +10 -2
  39. package/persist-plugins/local-storage.mjs.map +1 -1
  40. package/persist-plugins/mmkv.js.map +1 -1
  41. package/persist-plugins/mmkv.mjs.map +1 -1
  42. package/persist-plugins/query.js.map +1 -1
  43. package/persist-plugins/query.mjs.map +1 -1
  44. package/persist.d.ts +15 -1
  45. package/persist.js +363 -116
  46. package/persist.js.map +1 -1
  47. package/persist.mjs +364 -117
  48. package/persist.mjs.map +1 -1
  49. package/react-hooks/createObservableHook.js +1 -1
  50. package/react-hooks/createObservableHook.js.map +1 -1
  51. package/react-hooks/createObservableHook.mjs +1 -1
  52. package/react-hooks/createObservableHook.mjs.map +1 -1
  53. package/react-hooks/useFetch.d.ts +4 -3
  54. package/react-hooks/useFetch.js.map +1 -1
  55. package/react-hooks/useFetch.mjs.map +1 -1
  56. package/react-hooks/useHover.js.map +1 -1
  57. package/react-hooks/useHover.mjs.map +1 -1
  58. package/react-hooks/useMeasure.js.map +1 -1
  59. package/react-hooks/useMeasure.mjs.map +1 -1
  60. package/react-hooks/useObservableNextRouter.js.map +1 -1
  61. package/react-hooks/useObservableNextRouter.mjs.map +1 -1
  62. package/react-hooks/useObservableQuery.js.map +1 -1
  63. package/react-hooks/useObservableQuery.mjs.map +1 -1
  64. package/react-hooks/usePersistedObservable.d.ts +2 -2
  65. package/react-hooks/usePersistedObservable.js +3 -3
  66. package/react-hooks/usePersistedObservable.js.map +1 -1
  67. package/react-hooks/usePersistedObservable.mjs +3 -3
  68. package/react-hooks/usePersistedObservable.mjs.map +1 -1
  69. package/react.js +13 -8
  70. package/react.js.map +1 -1
  71. package/react.mjs +14 -9
  72. package/react.mjs.map +1 -1
  73. package/src/ObservableObject.d.ts +8 -4
  74. package/src/ObservablePrimitive.d.ts +2 -1
  75. package/src/activated.d.ts +3 -0
  76. package/src/batching.d.ts +3 -1
  77. package/src/computed.d.ts +1 -1
  78. package/src/config/enableDirectAccess.d.ts +1 -1
  79. package/src/config/enableDirectPeek.d.ts +1 -1
  80. package/src/config/enableReactTracking.d.ts +4 -3
  81. package/src/config/enableReactUse.d.ts +1 -1
  82. package/src/createObservable.d.ts +2 -2
  83. package/src/globals.d.ts +11 -4
  84. package/src/helpers/fetch.d.ts +4 -3
  85. package/src/helpers/time.d.ts +2 -2
  86. package/src/helpers.d.ts +3 -2
  87. package/src/history/trackHistory.d.ts +1 -1
  88. package/src/observable.d.ts +6 -15
  89. package/src/observableInterfaces.d.ts +60 -331
  90. package/src/observableTypes.d.ts +93 -0
  91. package/src/persist/observablePersistRemoteFunctionsAdapter.d.ts +1 -1
  92. package/src/persist/persistActivateNode.d.ts +1 -0
  93. package/src/persist/persistHelpers.d.ts +1 -1
  94. package/src/persist/persistObservable.d.ts +3 -12
  95. package/src/persistTypes.d.ts +229 -0
  96. package/src/proxy.d.ts +2 -1
  97. package/src/react/Computed.d.ts +1 -1
  98. package/src/react/Switch.d.ts +3 -3
  99. package/src/react/reactInterfaces.d.ts +2 -1
  100. package/src/react/usePauseProvider.d.ts +3 -3
  101. package/src/react/useWhen.d.ts +2 -2
  102. package/src/react-hooks/useFetch.d.ts +4 -3
  103. package/src/react-hooks/usePersistedObservable.d.ts +2 -2
  104. package/src/retry.d.ts +6 -0
  105. package/src/trackSelector.d.ts +3 -2
  106. package/src/when.d.ts +6 -2
  107. package/trace.js.map +1 -1
  108. package/trace.mjs.map +1 -1
package/persist.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { symbolDelete, isString, isArray, isObject, constructObjectWithPath, deconstructObjectWithPath, internal as internal$1, isObservable, observable, isFunction, getNode, when, isPromise, batch, mergeIntoObservable, isEmpty, setAtPath, setInObservableAtPath } from '@legendapp/state';
1
+ import { symbolDelete, isString, isArray, isObject, constructObjectWithPath, deconstructObjectWithPath, isPromise, getNode, observable, when, internal as internal$1, batch, mergeIntoObservable, isEmpty, isFunction, setAtPath, endBatch, setInObservableAtPath, getNodeValue, whenReady } from '@legendapp/state';
2
2
 
3
3
  const observablePersistConfiguration = {};
4
4
  function configureObservablePersistence(options) {
@@ -137,9 +137,21 @@ function observablePersistRemoteFunctionsAdapter({ get, set, }) {
137
137
  const ret = {};
138
138
  if (get) {
139
139
  ret.get = (async (params) => {
140
- const value = (await get(params));
141
- params.onChange({ value, dateModified: params.dateModified || Date.now() });
142
- params.onGet();
140
+ try {
141
+ let value = get(params);
142
+ if (isPromise(value)) {
143
+ value = await value;
144
+ }
145
+ params.onChange({
146
+ value,
147
+ dateModified: params.dateModified,
148
+ lastSync: params.lastSync,
149
+ mode: params.mode,
150
+ });
151
+ params.onGet();
152
+ // eslint-disable-next-line no-empty
153
+ }
154
+ catch (_a) { }
143
155
  });
144
156
  }
145
157
  if (set) {
@@ -148,7 +160,22 @@ function observablePersistRemoteFunctionsAdapter({ get, set, }) {
148
160
  return ret;
149
161
  }
150
162
 
151
- const { getProxy, globalState } = internal$1;
163
+ function removeNullUndefined(val) {
164
+ if (val) {
165
+ Object.keys(val).forEach((key) => {
166
+ const v = val[key];
167
+ if (v === null || v === undefined) {
168
+ delete val[key];
169
+ }
170
+ else if (isObject(v)) {
171
+ removeNullUndefined(v);
172
+ }
173
+ });
174
+ }
175
+ return val;
176
+ }
177
+
178
+ const { globalState: globalState$1 } = internal$1;
152
179
  const mapPersistences = new WeakMap();
153
180
  const metadatas = new WeakMap();
154
181
  const promisesLocalSaves = new Set();
@@ -163,11 +190,12 @@ function doInOrder(arg1, arg2) {
163
190
  return isPromise(arg1) ? arg1.then(arg2) : arg2(arg1);
164
191
  }
165
192
  function onChangeRemote(cb) {
166
- when(() => !globalState.isLoadingRemote$.get(), () => {
193
+ when(() => !globalState$1.isLoadingRemote$.get(), () => {
194
+ endBatch(true);
167
195
  // Remote changes should only update local state
168
- globalState.isLoadingRemote$.set(true);
196
+ globalState$1.isLoadingRemote$.set(true);
169
197
  batch(cb, () => {
170
- globalState.isLoadingRemote$.set(false);
198
+ globalState$1.isLoadingRemote$.set(false);
171
199
  });
172
200
  });
173
201
  }
@@ -214,16 +242,19 @@ async function updateMetadataImmediate(obs, localState, syncState, persistOption
214
242
  const { table, config } = parseLocalConfig(local);
215
243
  // Save metadata
216
244
  const oldMetadata = metadatas.get(obs);
217
- const { modified, pending } = newMetadata;
218
- const needsUpdate = pending || (modified && (!oldMetadata || modified !== oldMetadata.modified));
245
+ const { lastSync, pending } = newMetadata;
246
+ const needsUpdate = pending || (lastSync && (!oldMetadata || lastSync !== oldMetadata.lastSync));
219
247
  if (needsUpdate) {
220
248
  const metadata = Object.assign({}, oldMetadata, newMetadata);
221
249
  metadatas.set(obs, metadata);
222
250
  if (persistenceLocal) {
223
251
  await persistenceLocal.setMetadata(table, metadata, config);
224
252
  }
225
- if (modified) {
226
- syncState.dateModified.set(modified);
253
+ if (lastSync) {
254
+ syncState.assign({
255
+ lastSync: lastSync,
256
+ dateModified: lastSync,
257
+ });
227
258
  }
228
259
  }
229
260
  }
@@ -235,15 +266,51 @@ function updateMetadata(obs, localState, syncState, persistOptions, newMetadata)
235
266
  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);
236
267
  }
237
268
  let _queuedChanges = [];
269
+ let _queuedRemoteChanges = [];
270
+ let timeoutSaveRemote = undefined;
271
+ function mergeChanges(changes) {
272
+ const changesByPath = new Map();
273
+ const changesOut = [];
274
+ // TODO: This could be even more robust by going deeper into paths like the firebase plugin's _updatePendingSave
275
+ for (let i = 0; i < changes.length; i++) {
276
+ const change = changes[i];
277
+ const pathStr = change.path.join('/');
278
+ const existing = changesByPath.get(pathStr);
279
+ if (existing) {
280
+ existing.valueAtPath = change.valueAtPath;
281
+ }
282
+ else {
283
+ changesByPath.set(pathStr, change);
284
+ changesOut.push(change);
285
+ }
286
+ }
287
+ return changesOut;
288
+ }
289
+ function mergeQueuedChanges(allChanges) {
290
+ const changesByObs = new Map();
291
+ const out = new Map();
292
+ for (let i = 0; i < allChanges.length; i++) {
293
+ const value = allChanges[i];
294
+ const { obs, changes } = value;
295
+ const existing = changesByObs.get(obs);
296
+ const newChanges = existing ? [...existing, ...changes] : changes;
297
+ const merged = mergeChanges(newChanges);
298
+ changesByObs.set(obs, merged);
299
+ value.changes = merged;
300
+ out.set(obs, value);
301
+ }
302
+ return Array.from(out.values());
303
+ }
238
304
  async function processQueuedChanges() {
305
+ var _a;
239
306
  // Get a local copy of the queued changes and clear the global queue
240
- const queuedChanges = _queuedChanges;
307
+ const queuedChanges = mergeQueuedChanges(_queuedChanges);
241
308
  _queuedChanges = [];
309
+ _queuedRemoteChanges.push(...queuedChanges.filter((c) => !c.inRemoteChange));
242
310
  // Note: Summary of the order of operations these functions:
243
311
  // 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
244
312
  // We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
245
313
  // since some may take longer to transformSaveData than others.
246
- const changes = await Promise.all(queuedChanges.map(prepChange));
247
314
  // 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
248
315
  // the current value again on next load, which isn't too bad.
249
316
  // 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
@@ -251,25 +318,46 @@ async function processQueuedChanges() {
251
318
  // 4. Wait for remote load or error if allowed
252
319
  // 5. Save to remote
253
320
  // 6. On successful save, merge changes (if any) back into observable
254
- // 7. Lastly, update metadata to clear pending and update dateModified. Doing this earlier could potentially cause
321
+ // 7. Lastly, update metadata to clear pending and update lastSync. Doing this earlier could potentially cause
255
322
  // sync inconsistences so it's very important that this is last.
256
- changes.forEach(doChange);
323
+ const preppedChangesLocal = await Promise.all(queuedChanges.map(prepChangeLocal));
324
+ // 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?
325
+ await Promise.all(queuedChanges.map(prepChangeRemote));
326
+ await Promise.all(preppedChangesLocal.map(doChangeLocal));
327
+ const timeout = (_a = observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.remoteOptions) === null || _a === void 0 ? void 0 : _a.saveTimeout;
328
+ if (timeout) {
329
+ if (timeoutSaveRemote) {
330
+ clearTimeout(timeoutSaveRemote);
331
+ }
332
+ timeoutSaveRemote = setTimeout(processQueuedRemoteChanges, timeout);
333
+ }
334
+ else {
335
+ processQueuedRemoteChanges();
336
+ }
337
+ }
338
+ async function processQueuedRemoteChanges() {
339
+ const queuedRemoteChanges = mergeQueuedChanges(_queuedRemoteChanges);
340
+ _queuedRemoteChanges = [];
341
+ const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote));
342
+ preppedChangesRemote.forEach(doChangeRemote);
257
343
  }
258
- async function prepChange(queuedChange) {
344
+ async function prepChangeLocal(queuedChange) {
259
345
  const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
260
346
  const local = persistOptions.local;
261
347
  const { persistenceRemote } = localState;
262
348
  const { config: configLocal } = parseLocalConfig(local);
263
349
  const configRemote = persistOptions.remote;
264
350
  const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
265
- const saveRemote = !inRemoteChange && (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
351
+ const saveRemote = !!(!inRemoteChange &&
352
+ (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) &&
353
+ !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) &&
354
+ syncState.isEnabledRemote.peek());
266
355
  if (saveLocal || saveRemote) {
267
356
  if (saveLocal && !syncState.isLoadedLocal.peek()) {
268
357
  console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
269
- return;
358
+ return undefined;
270
359
  }
271
360
  const changesLocal = [];
272
- const changesRemote = [];
273
361
  const changesPaths = new Set();
274
362
  let promisesTransform = [];
275
363
  // Reverse order
@@ -308,6 +396,52 @@ async function prepChange(queuedChange) {
308
396
  }
309
397
  }));
310
398
  }
399
+ }
400
+ }
401
+ // If there's any transform promises, wait for them before saving
402
+ promisesTransform = promisesTransform.filter(Boolean);
403
+ if (promisesTransform.length > 0) {
404
+ await Promise.all(promisesTransform);
405
+ }
406
+ return { queuedChange, changesLocal, saveRemote };
407
+ }
408
+ }
409
+ async function prepChangeRemote(queuedChange) {
410
+ const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
411
+ const local = persistOptions.local;
412
+ const { persistenceRemote } = localState;
413
+ const { config: configLocal } = parseLocalConfig(local);
414
+ const configRemote = persistOptions.remote;
415
+ const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
416
+ const saveRemote = !inRemoteChange && (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
417
+ if (saveLocal || saveRemote) {
418
+ if (saveLocal && !syncState.isLoadedLocal.peek()) {
419
+ console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
420
+ return undefined;
421
+ }
422
+ const changesRemote = [];
423
+ const changesPaths = new Set();
424
+ let promisesTransform = [];
425
+ // Reverse order
426
+ for (let i = changes.length - 1; i >= 0; i--) {
427
+ const { path } = changes[i];
428
+ let found = false;
429
+ // Optimization to only save the latest update at each path. We might have multiple changes at the same path
430
+ // and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
431
+ // already processed. If a later change modifies a parent of an earlier change (which happens on delete()
432
+ // it should be ignored as it's superseded by the parent modification.
433
+ if (changesPaths.size > 0) {
434
+ for (let u = 0; u < path.length; u++) {
435
+ if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
436
+ found = true;
437
+ break;
438
+ }
439
+ }
440
+ }
441
+ if (!found) {
442
+ const pathStr = path.join('/');
443
+ changesPaths.add(pathStr);
444
+ const { prevAtPath, valueAtPath, pathTypes } = changes[i];
311
445
  if (saveRemote) {
312
446
  const promiseTransformRemote = transformOutData(valueAtPath, path, pathTypes, configRemote || {});
313
447
  promisesTransform.push(doInOrder(promiseTransformRemote, ({ path: pathTransformed, value: valueTransformed }) => {
@@ -365,21 +499,20 @@ async function prepChange(queuedChange) {
365
499
  if (promisesTransform.length > 0) {
366
500
  await Promise.all(promisesTransform);
367
501
  }
368
- return { queuedChange, changesLocal, changesRemote };
502
+ return { queuedChange, changesRemote };
369
503
  }
370
504
  }
371
- async function doChange(changeInfo) {
372
- var _a, _b, _c, _d;
505
+ async function doChangeLocal(changeInfo) {
373
506
  if (!changeInfo)
374
507
  return;
375
- const { queuedChange, changesLocal, changesRemote } = changeInfo;
508
+ const { queuedChange, changesLocal, saveRemote } = changeInfo;
376
509
  const { obs, syncState, localState, persistOptions } = queuedChange;
377
- const { persistenceLocal, persistenceRemote } = localState;
510
+ const { persistenceLocal } = localState;
378
511
  const local = persistOptions.local;
379
512
  const { table, config: configLocal } = parseLocalConfig(local);
380
513
  const configRemote = persistOptions.remote;
381
514
  const shouldSaveMetadata = local && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.offlineBehavior) === 'retry';
382
- if (changesRemote.length > 0 && shouldSaveMetadata) {
515
+ if (saveRemote && shouldSaveMetadata) {
383
516
  // First save pending changes before saving local or remote
384
517
  await updateMetadataImmediate(obs, localState, syncState, persistOptions, {
385
518
  pending: localState.pendingChanges,
@@ -399,29 +532,46 @@ async function doChange(changeInfo) {
399
532
  await promiseSet;
400
533
  }
401
534
  }
535
+ }
536
+ async function doChangeRemote(changeInfo) {
537
+ var _a, _b;
538
+ if (!changeInfo)
539
+ return;
540
+ const { queuedChange, changesRemote } = changeInfo;
541
+ const { obs, syncState, localState, persistOptions } = queuedChange;
542
+ const { persistenceLocal, persistenceRemote } = localState;
543
+ const local = persistOptions.local;
544
+ const { table, config: configLocal } = parseLocalConfig(local);
545
+ const { offlineBehavior, allowSetIfError, onBeforeSet, onSetError, waitForSet, onSet } = persistOptions.remote || {};
546
+ const shouldSaveMetadata = local && offlineBehavior === 'retry';
402
547
  if (changesRemote.length > 0) {
403
548
  // Wait for remote to be ready before saving
404
- await when(() => syncState.isLoaded.get() || ((configRemote === null || configRemote === void 0 ? void 0 : configRemote.allowSetIfError) && syncState.error.get()));
549
+ await when(() => syncState.isLoaded.get() || (allowSetIfError && syncState.error.get()));
550
+ if (waitForSet) {
551
+ await when(isFunction(waitForSet) ? waitForSet(changesRemote) : waitForSet);
552
+ }
405
553
  const value = obs.peek();
406
- (_a = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onBeforeSet) === null || _a === void 0 ? void 0 : _a.call(configRemote);
407
- const saved = await ((_b = persistenceRemote.set({
554
+ onBeforeSet === null || onBeforeSet === void 0 ? void 0 : onBeforeSet();
555
+ localState.numSavesOutstanding = (localState.numSavesOutstanding || 0) + 1;
556
+ const saved = await ((_a = persistenceRemote.set({
408
557
  obs,
409
558
  syncState: syncState,
410
559
  options: persistOptions,
411
560
  changes: changesRemote,
412
561
  value,
413
- })) === 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); }));
562
+ })) === null || _a === void 0 ? void 0 : _a.catch((err) => onSetError === null || onSetError === void 0 ? void 0 : onSetError(err)));
563
+ localState.numSavesOutstanding--;
414
564
  // If this remote save changed anything then update persistence and metadata
415
565
  // Because save happens after a timeout and they're batched together, some calls to save will
416
566
  // return saved data and others won't, so those can be ignored.
417
567
  if (saved) {
418
568
  const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
419
- const { changes, dateModified } = saved;
569
+ const { changes, lastSync } = saved;
420
570
  if (pathStrs.length > 0) {
421
571
  if (local) {
422
572
  const metadata = {};
423
- const pending = (_c = persistenceLocal.getMetadata(table, configLocal)) === null || _c === void 0 ? void 0 : _c.pending;
424
- let transformedChanges = [];
573
+ const pending = (_b = persistenceLocal.getMetadata(table, configLocal)) === null || _b === void 0 ? void 0 : _b.pending;
574
+ let transformedChanges = undefined;
425
575
  for (let i = 0; i < pathStrs.length; i++) {
426
576
  const pathStr = pathStrs[i];
427
577
  // Clear pending for this path
@@ -431,25 +581,37 @@ async function doChange(changeInfo) {
431
581
  metadata.pending = pending;
432
582
  }
433
583
  }
434
- if (dateModified) {
435
- metadata.modified = dateModified;
584
+ if (lastSync) {
585
+ metadata.lastSync = lastSync;
436
586
  }
437
587
  // Remote can optionally have data that needs to be merged back into the observable,
438
588
  // for example Firebase may update dateModified with the server timestamp
439
589
  if (changes && !isEmpty(changes)) {
440
- transformedChanges.push(transformLoadData(changes, persistOptions.remote, false));
590
+ transformedChanges = transformLoadData(changes, persistOptions.remote, false);
441
591
  }
442
- if (transformedChanges.length > 0) {
443
- if (transformedChanges.some((change) => isPromise(change))) {
444
- transformedChanges = await Promise.all(transformedChanges);
592
+ if (localState.numSavesOutstanding > 0) {
593
+ if (transformedChanges) {
594
+ if (!localState.pendingSaveResults) {
595
+ localState.pendingSaveResults = [];
596
+ }
597
+ localState.pendingSaveResults.push(transformedChanges);
445
598
  }
446
- onChangeRemote(() => mergeIntoObservable(obs, ...transformedChanges));
447
599
  }
448
- if (shouldSaveMetadata && !isEmpty(metadata)) {
449
- updateMetadata(obs, localState, syncState, persistOptions, metadata);
600
+ else {
601
+ let allChanges = [...(localState.pendingSaveResults || []), transformedChanges];
602
+ if (allChanges.length > 0) {
603
+ if (allChanges.some((change) => isPromise(change))) {
604
+ allChanges = await Promise.all(allChanges);
605
+ }
606
+ onChangeRemote(() => mergeIntoObservable(obs, ...allChanges));
607
+ }
608
+ if (shouldSaveMetadata && !isEmpty(metadata)) {
609
+ updateMetadata(obs, localState, syncState, persistOptions, metadata);
610
+ }
611
+ localState.pendingSaveResults = [];
450
612
  }
451
613
  }
452
- (_d = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onSet) === null || _d === void 0 ? void 0 : _d.call(configRemote);
614
+ onSet === null || onSet === void 0 ? void 0 : onSet();
453
615
  }
454
616
  }
455
617
  }
@@ -497,7 +659,7 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
497
659
  }
498
660
  const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
499
661
  localState.persistenceLocal = persistenceLocal;
500
- if (!initialized.get()) {
662
+ if (!initialized.peek()) {
501
663
  await when(initialized);
502
664
  }
503
665
  // If persistence has an asynchronous load, wait for it
@@ -511,12 +673,21 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
511
673
  let value = persistenceLocal.getTable(table, config);
512
674
  const metadata = persistenceLocal.getMetadata(table, config);
513
675
  if (metadata) {
676
+ // @ts-expect-error Migration from old version
677
+ if (!metadata.lastSync && metadata.modified) {
678
+ // @ts-expect-error Migration from old
679
+ metadata.lastSync = metadata.modified;
680
+ }
514
681
  metadatas.set(obs, metadata);
515
682
  localState.pendingChanges = metadata.pending;
516
- syncState.dateModified.set(metadata.modified);
683
+ // TODOV3 Remove dateModified
684
+ syncState.assign({
685
+ dateModified: metadata.lastSync,
686
+ lastSync: metadata.lastSync,
687
+ });
517
688
  }
518
689
  // Merge the data from local persistence into the default state
519
- if (value !== null && value !== undefined) {
690
+ if (value !== undefined) {
520
691
  const { transform, fieldTransforms } = config;
521
692
  value = transformLoadData(value, { transform, fieldTransforms }, true);
522
693
  if (isPromise(value)) {
@@ -527,7 +698,12 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
527
698
  // are set on the same observable
528
699
  internal$1.globalState.isLoadingLocal = true;
529
700
  // We want to merge the local data on top of any initial state the object is created with
530
- mergeIntoObservable(obs, value);
701
+ if (value === null && !obs.peek()) {
702
+ obs.set(value);
703
+ }
704
+ else {
705
+ mergeIntoObservable(obs, value);
706
+ }
531
707
  }, () => {
532
708
  internal$1.globalState.isLoadingLocal = false;
533
709
  });
@@ -540,18 +716,11 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
540
716
  }
541
717
  syncState.isLoadedLocal.set(true);
542
718
  }
543
- function persistObservable(initialOrObservable, persistOptions) {
544
- var _a;
545
- const obs = (isObservable(initialOrObservable)
546
- ? initialOrObservable
547
- : observable(isFunction(initialOrObservable) ? initialOrObservable() : initialOrObservable));
719
+ function persistObservable(obs, persistOptions) {
548
720
  const node = getNode(obs);
549
- if (process.env.NODE_ENV === 'development' && ((_a = obs === null || obs === void 0 ? void 0 : obs.peek()) === null || _a === void 0 ? void 0 : _a._state)) {
550
- console.warn('[legend-state] WARNING: persistObservable creates a property named "_state" but your observable already has "state" in it');
551
- }
552
721
  // Merge remote persist options with clobal options
553
722
  if (persistOptions.remote) {
554
- persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, persistOptions.remote);
723
+ persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, removeNullUndefined(persistOptions.remote));
555
724
  }
556
725
  let { remote } = persistOptions;
557
726
  const { local } = persistOptions;
@@ -594,33 +763,28 @@ function persistObservable(initialOrObservable, persistOptions) {
594
763
  var _a, _b;
595
764
  if (!isSynced) {
596
765
  isSynced = true;
597
- const dateModified = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.modified;
766
+ const lastSync = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.lastSync;
767
+ const pending = localState.pendingChanges;
598
768
  const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
599
769
  if (get) {
600
- let attemptNum = 0;
601
770
  const runGet = () => {
602
771
  get({
603
772
  state: syncState,
604
773
  obs,
605
774
  options: persistOptions,
606
- dateModified,
775
+ lastSync,
776
+ dateModified: lastSync,
607
777
  onError: (error) => {
608
778
  var _a;
609
- if (remote.retry) {
610
- const { backoff, delay = 1000, infinite, times = 3, maxDelay = 30000, } = remote.retry;
611
- if (infinite || attemptNum++ < times) {
612
- const delayTime = Math.min(delay * (backoff === 'constant' ? 1 : 2 ** attemptNum), maxDelay);
613
- setTimeout(runGet, delayTime);
614
- // Don't error when retrying
615
- return;
616
- }
617
- }
618
779
  (_a = remote.onGetError) === null || _a === void 0 ? void 0 : _a.call(remote, error);
619
780
  },
620
781
  onGet: () => {
621
- syncState.isLoaded.set(true);
782
+ node.state.assign({
783
+ isLoaded: true,
784
+ error: undefined,
785
+ });
622
786
  },
623
- onChange: async ({ value, path = [], pathTypes = [], mode = 'set', dateModified }) => {
787
+ onChange: async ({ value, path = [], pathTypes = [], mode = 'set', lastSync }) => {
624
788
  // Note: value is the constructed value, path is used for setInObservableAtPath
625
789
  // to start the set into the observable from the path
626
790
  if (value !== undefined) {
@@ -632,10 +796,12 @@ function persistObservable(initialOrObservable, persistOptions) {
632
796
  if (path.length && invertedMap) {
633
797
  path = transformPath(path, pathTypes, invertedMap);
634
798
  }
635
- if (mode === 'dateModified') {
636
- if (dateModified && !isEmpty(value)) {
799
+ if (mode === 'lastSync' || mode === 'dateModified') {
800
+ if (lastSync && !isEmpty(value)) {
637
801
  onChangeRemote(() => {
638
- setInObservableAtPath(obs, path, pathTypes, value, 'assign');
802
+ setInObservableAtPath(
803
+ // @ts-expect-error Fix this type
804
+ obs, path, pathTypes, value, 'assign');
639
805
  });
640
806
  }
641
807
  }
@@ -645,7 +811,10 @@ function persistObservable(initialOrObservable, persistOptions) {
645
811
  Object.keys(pending).forEach((key) => {
646
812
  const p = key.split('/').filter((p) => p !== '');
647
813
  const { v, t } = pending[key];
648
- if (value[p[0]] !== undefined) {
814
+ if (t.length === 0 || !value) {
815
+ value = v;
816
+ }
817
+ else if (value[p[0]] !== undefined) {
649
818
  value = setAtPath(value, p, t, v, obs.peek(), (path, value) => {
650
819
  delete pending[key];
651
820
  pending[path.join('/')] = {
@@ -658,13 +827,14 @@ function persistObservable(initialOrObservable, persistOptions) {
658
827
  });
659
828
  }
660
829
  onChangeRemote(() => {
830
+ // @ts-expect-error Fix this type
661
831
  setInObservableAtPath(obs, path, pathTypes, value, mode);
662
832
  });
663
833
  }
664
834
  }
665
- if (dateModified && local) {
835
+ if (lastSync && local) {
666
836
  updateMetadata(obs, localState, syncState, persistOptions, {
667
- modified: dateModified,
837
+ lastSync,
668
838
  });
669
839
  }
670
840
  },
@@ -673,11 +843,13 @@ function persistObservable(initialOrObservable, persistOptions) {
673
843
  runGet();
674
844
  }
675
845
  else {
676
- syncState.isLoaded.set(true);
846
+ node.state.assign({
847
+ isLoaded: true,
848
+ error: undefined,
849
+ });
677
850
  }
678
851
  // Wait for remote to be ready before saving pending
679
852
  await when(() => syncState.isLoaded.get() || (remote.allowSetIfError && syncState.error.get()));
680
- const pending = localState.pendingChanges;
681
853
  if (pending && !isEmpty(pending)) {
682
854
  localState.isApplyingPending = true;
683
855
  const keys = Object.keys(pending);
@@ -690,6 +862,7 @@ function persistObservable(initialOrObservable, persistOptions) {
690
862
  changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
691
863
  }
692
864
  // Send the changes into onObsChange so that they get persisted remotely
865
+ // @ts-expect-error Fix this type
693
866
  onObsChange(obs, syncState, localState, persistOptions, {
694
867
  value: obs.peek(),
695
868
  // TODO getPrevious if any remote persistence layers need it
@@ -725,49 +898,122 @@ function persistObservable(initialOrObservable, persistOptions) {
725
898
  }
726
899
  obs.onChange(onObsChange.bind(this, obs, syncState, localState, persistOptions));
727
900
  });
728
- return obs;
901
+ return syncState;
729
902
  }
730
- globalState.activateNode = function activateNodePersist(node, refresh, newValue) {
731
- const { onSetFn, subscriber, lastSync, cacheOptions, retryOptions } = node.activationState;
732
- let onChange = undefined;
733
- const pluginRemote = {
734
- get: async (params) => {
735
- onChange = params.onChange;
736
- if (isPromise(newValue)) {
737
- newValue = await newValue;
903
+
904
+ const { getProxy, globalState, runWithRetry, symbolActivated } = internal$1;
905
+ function persistActivateNode() {
906
+ globalState.activateNode = function activateNodePersist(node, refresh, wasPromise, newValue) {
907
+ if (node.activationState) {
908
+ const { get, initial, onSet, subscribe, cache, retry, offlineBehavior, waitForSet } = node.activationState;
909
+ let onChange = undefined;
910
+ const pluginRemote = {};
911
+ if (get) {
912
+ pluginRemote.get = (params) => {
913
+ onChange = params.onChange;
914
+ const updateLastSync = (lastSync) => (params.lastSync = lastSync);
915
+ const setMode = (mode) => (params.mode = mode);
916
+ const nodeValue = getNodeValue(node);
917
+ const value = runWithRetry(node, { attemptNum: 0 }, () => {
918
+ return get({
919
+ value: isFunction(nodeValue) || (nodeValue === null || nodeValue === void 0 ? void 0 : nodeValue[symbolActivated]) ? undefined : nodeValue,
920
+ lastSync: params.lastSync,
921
+ updateLastSync,
922
+ setMode,
923
+ refresh,
924
+ });
925
+ });
926
+ return value;
927
+ };
928
+ }
929
+ if (onSet) {
930
+ // TODO: Work out these types better
931
+ pluginRemote.set = async (params) => {
932
+ var _a;
933
+ if ((_a = node.state) === null || _a === void 0 ? void 0 : _a.isLoaded.get()) {
934
+ const retryAttempts = { attemptNum: 0 };
935
+ return runWithRetry(node, retryAttempts, async (retryEvent) => {
936
+ let changes = {};
937
+ let maxModified = 0;
938
+ if (!node.state.isLoaded.peek()) {
939
+ await whenReady(node.state.isLoaded);
940
+ }
941
+ const cancelRetry = () => {
942
+ retryEvent.cancel = true;
943
+ };
944
+ await onSet({
945
+ ...params,
946
+ node,
947
+ update: (params) => {
948
+ const { value, lastSync } = params;
949
+ maxModified = Math.max(lastSync || 0, maxModified);
950
+ changes = mergeIntoObservable(changes, value);
951
+ },
952
+ retryNum: retryAttempts.attemptNum,
953
+ cancelRetry,
954
+ refresh,
955
+ fromSubscribe: false,
956
+ });
957
+ return { changes, lastSync: maxModified || undefined };
958
+ });
959
+ }
960
+ };
961
+ }
962
+ if (subscribe) {
963
+ subscribe({
964
+ node,
965
+ update: (params) => {
966
+ if (!onChange) {
967
+ // TODO: Make this message better
968
+ console.log('[legend-state] Cannot update immediately before the first return');
969
+ }
970
+ else {
971
+ onChange(params);
972
+ }
973
+ },
974
+ refresh,
975
+ });
976
+ }
977
+ persistObservable(getProxy(node), {
978
+ pluginRemote,
979
+ ...(cache || {}),
980
+ remote: {
981
+ retry: retry,
982
+ offlineBehavior,
983
+ waitForSet,
984
+ },
985
+ });
986
+ const nodeVal = getNodeValue(node);
987
+ if (nodeVal !== undefined) {
988
+ newValue = nodeVal;
738
989
  }
739
- if (lastSync.value) {
740
- params.dateModified = lastSync.value;
990
+ else if (newValue === undefined) {
991
+ newValue = initial;
741
992
  }
742
- return newValue;
743
- },
993
+ return { update: onChange, value: newValue };
994
+ }
995
+ else {
996
+ let onChange = undefined;
997
+ const pluginRemote = {
998
+ get: async (params) => {
999
+ onChange = params.onChange;
1000
+ if (isPromise(newValue)) {
1001
+ try {
1002
+ newValue = await newValue;
1003
+ // eslint-disable-next-line no-empty
1004
+ }
1005
+ catch (_a) { }
1006
+ }
1007
+ return newValue;
1008
+ },
1009
+ };
1010
+ persistObservable(getProxy(node), {
1011
+ pluginRemote,
1012
+ });
1013
+ return { update: onChange, value: newValue };
1014
+ }
744
1015
  };
745
- if (onSetFn) {
746
- // TODO: Work out these types better
747
- pluginRemote.set = onSetFn;
748
- }
749
- if (subscriber) {
750
- subscriber({
751
- update: (params) => {
752
- if (!onChange) {
753
- // TODO: Make this message better
754
- console.log('[legend-state] Cannot update immediately before the first return');
755
- }
756
- else {
757
- onChange(params);
758
- }
759
- },
760
- refresh,
761
- });
762
- }
763
- persistObservable(getProxy(node), {
764
- pluginRemote,
765
- ...(cacheOptions || {}),
766
- remote: {
767
- retry: retryOptions,
768
- },
769
- });
770
- };
1016
+ }
771
1017
 
772
1018
  function isInRemoteChange() {
773
1019
  return internal$1.globalState.isLoadingRemote$.get();
@@ -775,6 +1021,7 @@ function isInRemoteChange() {
775
1021
  const internal = {
776
1022
  observablePersistConfiguration,
777
1023
  };
1024
+ persistActivateNode();
778
1025
 
779
1026
  export { configureObservablePersistence, internal, invertFieldMap, isInRemoteChange, mapPersistences, persistObservable, transformObject, transformPath };
780
1027
  //# sourceMappingURL=persist.mjs.map