@legendapp/state 2.2.0-next.4 → 2.2.0-next.41

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 (110) 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 +13 -4
  23. package/index.js +655 -448
  24. package/index.js.map +1 -1
  25. package/index.mjs +652 -447
  26. package/index.mjs.map +1 -1
  27. package/package.json +1 -12
  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 +412 -180
  46. package/persist.js.map +1 -1
  47. package/persist.mjs +413 -181
  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 +4 -3
  66. package/react-hooks/usePersistedObservable.js.map +1 -1
  67. package/react-hooks/usePersistedObservable.mjs +4 -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 +6 -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 +3 -3
  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 +10 -8
  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 -3
  87. package/src/history/trackHistory.d.ts +1 -1
  88. package/src/is.d.ts +2 -0
  89. package/src/observable.d.ts +7 -15
  90. package/src/observableInterfaces.d.ts +56 -348
  91. package/src/observableTypes.d.ts +85 -0
  92. package/src/persist/observablePersistRemoteFunctionsAdapter.d.ts +1 -1
  93. package/src/persist/persistActivateNode.d.ts +0 -17
  94. package/src/persist/persistHelpers.d.ts +1 -1
  95. package/src/persist/persistObservable.d.ts +2 -3
  96. package/src/persistTypes.d.ts +196 -0
  97. package/src/proxy.d.ts +5 -5
  98. package/src/react/Computed.d.ts +1 -1
  99. package/src/react/Switch.d.ts +3 -3
  100. package/src/react/reactInterfaces.d.ts +2 -1
  101. package/src/react/useComputed.d.ts +5 -5
  102. package/src/react/usePauseProvider.d.ts +3 -3
  103. package/src/react/useWhen.d.ts +2 -2
  104. package/src/react-hooks/useFetch.d.ts +4 -3
  105. package/src/react-hooks/usePersistedObservable.d.ts +2 -2
  106. package/src/retry.d.ts +6 -0
  107. package/src/trackSelector.d.ts +3 -2
  108. package/src/when.d.ts +6 -2
  109. package/trace.js.map +1 -1
  110. package/trace.mjs.map +1 -1
package/persist.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { symbolDelete, isString, isArray, isObject, constructObjectWithPath, deconstructObjectWithPath, isObservable, observable, isFunction, getNode, when, batch, internal as internal$1, isPromise, mergeIntoObservable, isEmpty, setInObservableAtPath, setAtPath } 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,6 +160,21 @@ function observablePersistRemoteFunctionsAdapter({ get, set, }) {
148
160
  return ret;
149
161
  }
150
162
 
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
+
151
178
  const { globalState: globalState$1 } = internal$1;
152
179
  const mapPersistences = new WeakMap();
153
180
  const metadatas = new WeakMap();
@@ -164,6 +191,7 @@ function doInOrder(arg1, arg2) {
164
191
  }
165
192
  function onChangeRemote(cb) {
166
193
  when(() => !globalState$1.isLoadingRemote$.get(), () => {
194
+ endBatch(true);
167
195
  // Remote changes should only update local state
168
196
  globalState$1.isLoadingRemote$.set(true);
169
197
  batch(cb, () => {
@@ -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,54 @@ 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 changesByObsRemote = new Map();
291
+ const changesByObsLocal = new Map();
292
+ const outRemote = new Map();
293
+ const outLocal = new Map();
294
+ for (let i = 0; i < allChanges.length; i++) {
295
+ const value = allChanges[i];
296
+ const { obs, changes, inRemoteChange } = value;
297
+ const changesMap = inRemoteChange ? changesByObsRemote : changesByObsLocal;
298
+ const existing = changesMap.get(obs);
299
+ const newChanges = existing ? [...existing, ...changes] : changes;
300
+ const merged = mergeChanges(newChanges);
301
+ changesMap.set(obs, merged);
302
+ value.changes = merged;
303
+ (inRemoteChange ? outRemote : outLocal).set(obs, value);
304
+ }
305
+ return Array.from(outRemote.values()).concat(Array.from(outLocal.values()));
306
+ }
238
307
  async function processQueuedChanges() {
308
+ var _a;
239
309
  // Get a local copy of the queued changes and clear the global queue
240
- const queuedChanges = _queuedChanges;
310
+ const queuedChanges = mergeQueuedChanges(_queuedChanges);
241
311
  _queuedChanges = [];
312
+ _queuedRemoteChanges.push(...queuedChanges.filter((c) => !c.inRemoteChange));
242
313
  // Note: Summary of the order of operations these functions:
243
314
  // 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
244
315
  // We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
245
316
  // since some may take longer to transformSaveData than others.
246
- const changes = await Promise.all(queuedChanges.map(prepChange));
247
317
  // 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
248
318
  // the current value again on next load, which isn't too bad.
249
319
  // 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
@@ -251,25 +321,46 @@ async function processQueuedChanges() {
251
321
  // 4. Wait for remote load or error if allowed
252
322
  // 5. Save to remote
253
323
  // 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
324
+ // 7. Lastly, update metadata to clear pending and update lastSync. Doing this earlier could potentially cause
255
325
  // sync inconsistences so it's very important that this is last.
256
- changes.forEach(doChange);
326
+ const preppedChangesLocal = await Promise.all(queuedChanges.map(prepChangeLocal));
327
+ // TODO Clean this up: We only need to prep this now in ordre to save pending changes, don't need any of the other stuff. Should split that up?
328
+ await Promise.all(queuedChanges.map(prepChangeRemote));
329
+ await Promise.all(preppedChangesLocal.map(doChangeLocal));
330
+ const timeout = (_a = observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.remoteOptions) === null || _a === void 0 ? void 0 : _a.saveTimeout;
331
+ if (timeout) {
332
+ if (timeoutSaveRemote) {
333
+ clearTimeout(timeoutSaveRemote);
334
+ }
335
+ timeoutSaveRemote = setTimeout(processQueuedRemoteChanges, timeout);
336
+ }
337
+ else {
338
+ processQueuedRemoteChanges();
339
+ }
257
340
  }
258
- async function prepChange(queuedChange) {
341
+ async function processQueuedRemoteChanges() {
342
+ const queuedRemoteChanges = mergeQueuedChanges(_queuedRemoteChanges);
343
+ _queuedRemoteChanges = [];
344
+ const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote));
345
+ preppedChangesRemote.forEach(doChangeRemote);
346
+ }
347
+ async function prepChangeLocal(queuedChange) {
259
348
  const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
260
349
  const local = persistOptions.local;
261
350
  const { persistenceRemote } = localState;
262
351
  const { config: configLocal } = parseLocalConfig(local);
263
352
  const configRemote = persistOptions.remote;
264
353
  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();
354
+ const saveRemote = !!(!inRemoteChange &&
355
+ (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) &&
356
+ !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) &&
357
+ syncState.isEnabledRemote.peek());
266
358
  if (saveLocal || saveRemote) {
267
359
  if (saveLocal && !syncState.isLoadedLocal.peek()) {
268
360
  console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
269
- return;
361
+ return undefined;
270
362
  }
271
363
  const changesLocal = [];
272
- const changesRemote = [];
273
364
  const changesPaths = new Set();
274
365
  let promisesTransform = [];
275
366
  // Reverse order
@@ -308,6 +399,52 @@ async function prepChange(queuedChange) {
308
399
  }
309
400
  }));
310
401
  }
402
+ }
403
+ }
404
+ // If there's any transform promises, wait for them before saving
405
+ promisesTransform = promisesTransform.filter(Boolean);
406
+ if (promisesTransform.length > 0) {
407
+ await Promise.all(promisesTransform);
408
+ }
409
+ return { queuedChange, changesLocal, saveRemote };
410
+ }
411
+ }
412
+ async function prepChangeRemote(queuedChange) {
413
+ const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
414
+ const local = persistOptions.local;
415
+ const { persistenceRemote } = localState;
416
+ const { config: configLocal } = parseLocalConfig(local);
417
+ const configRemote = persistOptions.remote;
418
+ const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
419
+ const saveRemote = !inRemoteChange && (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
420
+ if (saveLocal || saveRemote) {
421
+ if (saveLocal && !syncState.isLoadedLocal.peek()) {
422
+ console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
423
+ return undefined;
424
+ }
425
+ const changesRemote = [];
426
+ const changesPaths = new Set();
427
+ let promisesTransform = [];
428
+ // Reverse order
429
+ for (let i = changes.length - 1; i >= 0; i--) {
430
+ const { path } = changes[i];
431
+ let found = false;
432
+ // Optimization to only save the latest update at each path. We might have multiple changes at the same path
433
+ // and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
434
+ // already processed. If a later change modifies a parent of an earlier change (which happens on delete()
435
+ // it should be ignored as it's superseded by the parent modification.
436
+ if (changesPaths.size > 0) {
437
+ for (let u = 0; u < path.length; u++) {
438
+ if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
439
+ found = true;
440
+ break;
441
+ }
442
+ }
443
+ }
444
+ if (!found) {
445
+ const pathStr = path.join('/');
446
+ changesPaths.add(pathStr);
447
+ const { prevAtPath, valueAtPath, pathTypes } = changes[i];
311
448
  if (saveRemote) {
312
449
  const promiseTransformRemote = transformOutData(valueAtPath, path, pathTypes, configRemote || {});
313
450
  promisesTransform.push(doInOrder(promiseTransformRemote, ({ path: pathTransformed, value: valueTransformed }) => {
@@ -365,21 +502,20 @@ async function prepChange(queuedChange) {
365
502
  if (promisesTransform.length > 0) {
366
503
  await Promise.all(promisesTransform);
367
504
  }
368
- return { queuedChange, changesLocal, changesRemote };
505
+ return { queuedChange, changesRemote };
369
506
  }
370
507
  }
371
- async function doChange(changeInfo) {
372
- var _a, _b, _c, _d;
508
+ async function doChangeLocal(changeInfo) {
373
509
  if (!changeInfo)
374
510
  return;
375
- const { queuedChange, changesLocal, changesRemote } = changeInfo;
511
+ const { queuedChange, changesLocal, saveRemote } = changeInfo;
376
512
  const { obs, syncState, localState, persistOptions } = queuedChange;
377
- const { persistenceLocal, persistenceRemote } = localState;
513
+ const { persistenceLocal } = localState;
378
514
  const local = persistOptions.local;
379
515
  const { table, config: configLocal } = parseLocalConfig(local);
380
516
  const configRemote = persistOptions.remote;
381
517
  const shouldSaveMetadata = local && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.offlineBehavior) === 'retry';
382
- if (changesRemote.length > 0 && shouldSaveMetadata) {
518
+ if (saveRemote && shouldSaveMetadata) {
383
519
  // First save pending changes before saving local or remote
384
520
  await updateMetadataImmediate(obs, localState, syncState, persistOptions, {
385
521
  pending: localState.pendingChanges,
@@ -399,29 +535,46 @@ async function doChange(changeInfo) {
399
535
  await promiseSet;
400
536
  }
401
537
  }
538
+ }
539
+ async function doChangeRemote(changeInfo) {
540
+ var _a, _b;
541
+ if (!changeInfo)
542
+ return;
543
+ const { queuedChange, changesRemote } = changeInfo;
544
+ const { obs, syncState, localState, persistOptions } = queuedChange;
545
+ const { persistenceLocal, persistenceRemote } = localState;
546
+ const local = persistOptions.local;
547
+ const { table, config: configLocal } = parseLocalConfig(local);
548
+ const { offlineBehavior, allowSetIfError, onBeforeSet, onSetError, waitForSet, onSet } = persistOptions.remote || {};
549
+ const shouldSaveMetadata = local && offlineBehavior === 'retry';
402
550
  if (changesRemote.length > 0) {
403
551
  // 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()));
552
+ await when(() => syncState.isLoaded.get() || (allowSetIfError && syncState.error.get()));
553
+ if (waitForSet) {
554
+ await when(isFunction(waitForSet) ? waitForSet(changesRemote) : waitForSet);
555
+ }
405
556
  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({
557
+ onBeforeSet === null || onBeforeSet === void 0 ? void 0 : onBeforeSet();
558
+ localState.numSavesOutstanding = (localState.numSavesOutstanding || 0) + 1;
559
+ const saved = await ((_a = persistenceRemote.set({
408
560
  obs,
409
561
  syncState: syncState,
410
562
  options: persistOptions,
411
563
  changes: changesRemote,
412
564
  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); }));
565
+ })) === null || _a === void 0 ? void 0 : _a.catch((err) => onSetError === null || onSetError === void 0 ? void 0 : onSetError(err)));
566
+ localState.numSavesOutstanding--;
414
567
  // If this remote save changed anything then update persistence and metadata
415
568
  // Because save happens after a timeout and they're batched together, some calls to save will
416
569
  // return saved data and others won't, so those can be ignored.
417
570
  if (saved) {
418
571
  const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
419
- const { changes, dateModified } = saved;
572
+ const { changes, lastSync } = saved;
420
573
  if (pathStrs.length > 0) {
421
574
  if (local) {
422
575
  const metadata = {};
423
- const pending = (_c = persistenceLocal.getMetadata(table, configLocal)) === null || _c === void 0 ? void 0 : _c.pending;
424
- let transformedChanges = [];
576
+ const pending = (_b = persistenceLocal.getMetadata(table, configLocal)) === null || _b === void 0 ? void 0 : _b.pending;
577
+ let transformedChanges = undefined;
425
578
  for (let i = 0; i < pathStrs.length; i++) {
426
579
  const pathStr = pathStrs[i];
427
580
  // Clear pending for this path
@@ -431,25 +584,37 @@ async function doChange(changeInfo) {
431
584
  metadata.pending = pending;
432
585
  }
433
586
  }
434
- if (dateModified) {
435
- metadata.modified = dateModified;
587
+ if (lastSync) {
588
+ metadata.lastSync = lastSync;
436
589
  }
437
590
  // Remote can optionally have data that needs to be merged back into the observable,
438
591
  // for example Firebase may update dateModified with the server timestamp
439
592
  if (changes && !isEmpty(changes)) {
440
- transformedChanges.push(transformLoadData(changes, persistOptions.remote, false));
593
+ transformedChanges = transformLoadData(changes, persistOptions.remote, false);
441
594
  }
442
- if (transformedChanges.length > 0) {
443
- if (transformedChanges.some((change) => isPromise(change))) {
444
- transformedChanges = await Promise.all(transformedChanges);
595
+ if (localState.numSavesOutstanding > 0) {
596
+ if (transformedChanges) {
597
+ if (!localState.pendingSaveResults) {
598
+ localState.pendingSaveResults = [];
599
+ }
600
+ localState.pendingSaveResults.push(transformedChanges);
445
601
  }
446
- onChangeRemote(() => mergeIntoObservable(obs, ...transformedChanges));
447
602
  }
448
- if (shouldSaveMetadata && !isEmpty(metadata)) {
449
- updateMetadata(obs, localState, syncState, persistOptions, metadata);
603
+ else {
604
+ let allChanges = [...(localState.pendingSaveResults || []), transformedChanges];
605
+ if (allChanges.length > 0) {
606
+ if (allChanges.some((change) => isPromise(change))) {
607
+ allChanges = await Promise.all(allChanges);
608
+ }
609
+ onChangeRemote(() => mergeIntoObservable(obs, ...allChanges));
610
+ }
611
+ if (shouldSaveMetadata && !isEmpty(metadata)) {
612
+ updateMetadata(obs, localState, syncState, persistOptions, metadata);
613
+ }
614
+ localState.pendingSaveResults = [];
450
615
  }
451
616
  }
452
- (_d = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onSet) === null || _d === void 0 ? void 0 : _d.call(configRemote);
617
+ onSet === null || onSet === void 0 ? void 0 : onSet();
453
618
  }
454
619
  }
455
620
  }
@@ -497,7 +662,7 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
497
662
  }
498
663
  const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
499
664
  localState.persistenceLocal = persistenceLocal;
500
- if (!initialized.get()) {
665
+ if (!initialized.peek()) {
501
666
  await when(initialized);
502
667
  }
503
668
  // If persistence has an asynchronous load, wait for it
@@ -511,12 +676,21 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
511
676
  let value = persistenceLocal.getTable(table, config);
512
677
  const metadata = persistenceLocal.getMetadata(table, config);
513
678
  if (metadata) {
679
+ // @ts-expect-error Migration from old version
680
+ if (!metadata.lastSync && metadata.modified) {
681
+ // @ts-expect-error Migration from old
682
+ metadata.lastSync = metadata.modified;
683
+ }
514
684
  metadatas.set(obs, metadata);
515
685
  localState.pendingChanges = metadata.pending;
516
- syncState.dateModified.set(metadata.modified);
686
+ // TODOV3 Remove dateModified
687
+ syncState.assign({
688
+ dateModified: metadata.lastSync,
689
+ lastSync: metadata.lastSync,
690
+ });
517
691
  }
518
692
  // Merge the data from local persistence into the default state
519
- if (value !== null && value !== undefined) {
693
+ if (value !== undefined) {
520
694
  const { transform, fieldTransforms } = config;
521
695
  value = transformLoadData(value, { transform, fieldTransforms }, true);
522
696
  if (isPromise(value)) {
@@ -527,7 +701,12 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
527
701
  // are set on the same observable
528
702
  internal$1.globalState.isLoadingLocal = true;
529
703
  // We want to merge the local data on top of any initial state the object is created with
530
- mergeIntoObservable(obs, value);
704
+ if (value === null && !obs.peek()) {
705
+ obs.set(value);
706
+ }
707
+ else {
708
+ mergeIntoObservable(obs, value);
709
+ }
531
710
  }, () => {
532
711
  internal$1.globalState.isLoadingLocal = false;
533
712
  });
@@ -540,18 +719,11 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
540
719
  }
541
720
  syncState.isLoadedLocal.set(true);
542
721
  }
543
- function persistObservable(initialOrObservable, persistOptions) {
544
- var _a;
545
- const obs = (isObservable(initialOrObservable)
546
- ? initialOrObservable
547
- : observable(isFunction(initialOrObservable) ? initialOrObservable() : initialOrObservable));
722
+ function persistObservable(obs, persistOptions) {
548
723
  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
724
  // Merge remote persist options with clobal options
553
725
  if (persistOptions.remote) {
554
- persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, persistOptions.remote);
726
+ persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, removeNullUndefined(persistOptions.remote));
555
727
  }
556
728
  let { remote } = persistOptions;
557
729
  const { local } = persistOptions;
@@ -592,92 +764,95 @@ function persistObservable(initialOrObservable, persistOptions) {
592
764
  let isSynced = false;
593
765
  sync = async () => {
594
766
  var _a, _b;
595
- if (!isSynced) {
596
- isSynced = true;
597
- const dateModified = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.modified;
598
- const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
599
- if (get) {
600
- let attemptNum = 0;
601
- const runGet = () => {
602
- get({
603
- state: syncState,
604
- obs,
605
- options: persistOptions,
606
- dateModified,
607
- onError: (error) => {
608
- 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
- }
767
+ const lastSync = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.lastSync;
768
+ const pending = localState.pendingChanges;
769
+ const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
770
+ if (get) {
771
+ const runGet = () => {
772
+ get({
773
+ state: syncState,
774
+ obs,
775
+ options: persistOptions,
776
+ lastSync,
777
+ dateModified: lastSync,
778
+ onError: (error) => {
779
+ var _a;
780
+ (_a = remote.onGetError) === null || _a === void 0 ? void 0 : _a.call(remote, error);
781
+ },
782
+ onGet: () => {
783
+ node.state.assign({
784
+ isLoaded: true,
785
+ error: undefined,
786
+ });
787
+ },
788
+ onChange: async ({ value, path = [], pathTypes = [], mode = 'set', lastSync }) => {
789
+ // Note: value is the constructed value, path is used for setInObservableAtPath
790
+ // to start the set into the observable from the path
791
+ if (value !== undefined) {
792
+ value = transformLoadData(value, remote, true);
793
+ if (isPromise(value)) {
794
+ value = await value;
617
795
  }
618
- (_a = remote.onGetError) === null || _a === void 0 ? void 0 : _a.call(remote, error);
619
- },
620
- onGet: () => {
621
- syncState.isLoaded.set(true);
622
- },
623
- onChange: async ({ value, path = [], pathTypes = [], mode = 'set', dateModified }) => {
624
- // Note: value is the constructed value, path is used for setInObservableAtPath
625
- // to start the set into the observable from the path
626
- if (value !== undefined) {
627
- value = transformLoadData(value, remote, true);
628
- if (isPromise(value)) {
629
- value = await value;
630
- }
631
- const invertedMap = remote.fieldTransforms && invertFieldMap(remote.fieldTransforms);
632
- if (path.length && invertedMap) {
633
- path = transformPath(path, pathTypes, invertedMap);
634
- }
635
- if (mode === 'dateModified') {
636
- if (dateModified && !isEmpty(value)) {
637
- onChangeRemote(() => {
638
- setInObservableAtPath(obs, path, pathTypes, value, 'assign');
639
- });
640
- }
641
- }
642
- else {
643
- const pending = localState.pendingChanges;
644
- if (pending) {
645
- Object.keys(pending).forEach((key) => {
646
- const p = key.split('/').filter((p) => p !== '');
647
- const { v, t } = pending[key];
648
- if (value[p[0]] !== undefined) {
649
- value = setAtPath(value, p, t, v, obs.peek(), (path, value) => {
650
- delete pending[key];
651
- pending[path.join('/')] = {
652
- p: null,
653
- v: value,
654
- t: t.slice(0, path.length),
655
- };
656
- });
657
- }
658
- });
659
- }
796
+ const invertedMap = remote.fieldTransforms && invertFieldMap(remote.fieldTransforms);
797
+ if (path.length && invertedMap) {
798
+ path = transformPath(path, pathTypes, invertedMap);
799
+ }
800
+ if (mode === 'lastSync' || mode === 'dateModified') {
801
+ if (lastSync && !isEmpty(value)) {
660
802
  onChangeRemote(() => {
661
- setInObservableAtPath(obs, path, pathTypes, value, mode);
803
+ setInObservableAtPath(
804
+ // @ts-expect-error Fix this type
805
+ obs, path, pathTypes, value, 'assign');
662
806
  });
663
807
  }
664
808
  }
665
- if (dateModified && local) {
666
- updateMetadata(obs, localState, syncState, persistOptions, {
667
- 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(() => {
831
+ // @ts-expect-error Fix this type
832
+ setInObservableAtPath(obs, path, pathTypes, value, mode);
668
833
  });
669
834
  }
670
- },
671
- });
672
- };
673
- runGet();
674
- }
675
- else {
676
- syncState.isLoaded.set(true);
677
- }
835
+ }
836
+ if (lastSync && local) {
837
+ updateMetadata(obs, localState, syncState, persistOptions, {
838
+ lastSync,
839
+ });
840
+ }
841
+ },
842
+ });
843
+ };
844
+ runGet();
845
+ }
846
+ else {
847
+ node.state.assign({
848
+ isLoaded: true,
849
+ error: undefined,
850
+ });
851
+ }
852
+ if (!isSynced) {
853
+ isSynced = true;
678
854
  // Wait for remote to be ready before saving pending
679
855
  await when(() => syncState.isLoaded.get() || (remote.allowSetIfError && syncState.error.get()));
680
- const pending = localState.pendingChanges;
681
856
  if (pending && !isEmpty(pending)) {
682
857
  localState.isApplyingPending = true;
683
858
  const keys = Object.keys(pending);
@@ -690,6 +865,7 @@ function persistObservable(initialOrObservable, persistOptions) {
690
865
  changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
691
866
  }
692
867
  // Send the changes into onObsChange so that they get persisted remotely
868
+ // TODO: Not sure why this needs to as unknown as Observable
693
869
  onObsChange(obs, syncState, localState, persistOptions, {
694
870
  value: obs.peek(),
695
871
  // TODO getPrevious if any remote persistence layers need it
@@ -700,10 +876,7 @@ function persistObservable(initialOrObservable, persistOptions) {
700
876
  }
701
877
  }
702
878
  };
703
- // If remote is manual, then sync() must be called manually
704
- if (remote.manual) {
705
- syncState.assign({ sync });
706
- }
879
+ syncState.assign({ sync });
707
880
  }
708
881
  // Wait for this node and all parent nodes up the hierarchy to be loaded
709
882
  const onAllLoadedLocal = () => {
@@ -725,64 +898,123 @@ 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
903
 
731
- const { getProxy, globalState } = internal$1;
904
+ const { getProxy, globalState, runWithRetry, symbolActivated } = internal$1;
732
905
  function persistActivateNode() {
733
906
  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
- newValue = await newValue;
741
- }
742
- if (lastSync.value) {
743
- params.dateModified = lastSync.value;
744
- }
745
- return newValue;
746
- },
747
- };
748
- if (onSetFn) {
749
- // TODO: Work out these types better
750
- pluginRemote.set = (params) => {
751
- var _a;
752
- if ((_a = node.state) === null || _a === void 0 ? void 0 : _a.isLoaded.get()) {
753
- onSetFn(params, {
754
- update: onChange,
755
- updateLastSync: () => {
756
- console.log('TODO updateLastSync');
757
- },
758
- applyRemoteChange: onChangeRemote,
907
+ if (node.activationState) {
908
+ // If it is an Activated
909
+ const { get, initial, onSet, subscribe, cache, retry, offlineBehavior, waitForSet } = node.activationState;
910
+ let onChange = undefined;
911
+ const pluginRemote = {};
912
+ if (get) {
913
+ pluginRemote.get = (params) => {
914
+ onChange = params.onChange;
915
+ const updateLastSync = (lastSync) => (params.lastSync = lastSync);
916
+ const setMode = (mode) => (params.mode = mode);
917
+ const nodeValue = getNodeValue(node);
918
+ const value = runWithRetry(node, { attemptNum: 0 }, () => {
919
+ return get({
920
+ value: isFunction(nodeValue) || (nodeValue === null || nodeValue === void 0 ? void 0 : nodeValue[symbolActivated]) ? undefined : nodeValue,
921
+ lastSync: params.lastSync,
922
+ updateLastSync,
923
+ setMode,
924
+ refresh,
925
+ });
759
926
  });
760
- }
761
- };
762
- }
763
- if (subscriber) {
764
- subscriber({
765
- update: (params) => {
766
- if (!onChange) {
767
- // TODO: Make this message better
768
- console.log('[legend-state] Cannot update immediately before the first return');
927
+ return value;
928
+ };
929
+ }
930
+ if (onSet) {
931
+ // TODO: Work out these types better
932
+ pluginRemote.set = async (params) => {
933
+ var _a;
934
+ if ((_a = node.state) === null || _a === void 0 ? void 0 : _a.isLoaded.get()) {
935
+ const retryAttempts = { attemptNum: 0 };
936
+ return runWithRetry(node, retryAttempts, async (retryEvent) => {
937
+ let changes = {};
938
+ let maxModified = 0;
939
+ if (!node.state.isLoaded.peek()) {
940
+ await whenReady(node.state.isLoaded);
941
+ }
942
+ const cancelRetry = () => {
943
+ retryEvent.cancel = true;
944
+ };
945
+ await onSet({
946
+ ...params,
947
+ node,
948
+ update: (params) => {
949
+ const { value, lastSync } = params;
950
+ maxModified = Math.max(lastSync || 0, maxModified);
951
+ changes = mergeIntoObservable(changes, value);
952
+ },
953
+ retryNum: retryAttempts.attemptNum,
954
+ cancelRetry,
955
+ refresh,
956
+ fromSubscribe: false,
957
+ });
958
+ return { changes, lastSync: maxModified || undefined };
959
+ });
769
960
  }
770
- else {
771
- onChange(params);
961
+ };
962
+ }
963
+ if (subscribe) {
964
+ subscribe({
965
+ node,
966
+ update: (params) => {
967
+ if (!onChange) {
968
+ // TODO: Make this message better
969
+ console.log('[legend-state] Cannot update immediately before the first return');
970
+ }
971
+ else {
972
+ onChange(params);
973
+ }
974
+ },
975
+ refresh,
976
+ });
977
+ }
978
+ persistObservable(getProxy(node), {
979
+ pluginRemote,
980
+ ...(cache || {}),
981
+ remote: {
982
+ retry: retry,
983
+ offlineBehavior,
984
+ waitForSet,
985
+ },
986
+ });
987
+ const nodeVal = getNodeValue(node);
988
+ if (nodeVal !== undefined) {
989
+ newValue = nodeVal;
990
+ }
991
+ else if (newValue === undefined) {
992
+ newValue = initial;
993
+ }
994
+ return { update: onChange, value: newValue };
995
+ }
996
+ else {
997
+ // If it is not an Activated
998
+ let onChange = undefined;
999
+ const pluginRemote = {
1000
+ get: async (params) => {
1001
+ onChange = params.onChange;
1002
+ if (isPromise(newValue)) {
1003
+ try {
1004
+ newValue = await newValue;
1005
+ }
1006
+ catch (_a) {
1007
+ // TODO Once we have global retry settings this should retry
1008
+ }
772
1009
  }
1010
+ return newValue;
773
1011
  },
774
- refresh,
775
- applyRemoteChange: onChangeRemote,
1012
+ };
1013
+ persistObservable(getProxy(node), {
1014
+ pluginRemote,
776
1015
  });
1016
+ return { update: onChange, value: newValue };
777
1017
  }
778
- persistObservable(getProxy(node), {
779
- pluginRemote,
780
- ...(cacheOptions || {}),
781
- remote: {
782
- retry: retryOptions,
783
- },
784
- });
785
- return { update: onChange };
786
1018
  };
787
1019
  }
788
1020