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

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 +365 -116
  46. package/persist.js.map +1 -1
  47. package/persist.mjs +366 -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,53 @@ 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 = [];
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
+ if (!existing) {
300
+ value.changes = merged;
301
+ out.push(value);
302
+ }
303
+ }
304
+ return out;
305
+ }
238
306
  async function processQueuedChanges() {
307
+ var _a;
239
308
  // Get a local copy of the queued changes and clear the global queue
240
- const queuedChanges = _queuedChanges;
309
+ const queuedChanges = mergeQueuedChanges(_queuedChanges);
241
310
  _queuedChanges = [];
311
+ _queuedRemoteChanges.push(...queuedChanges.filter((c) => !c.inRemoteChange));
242
312
  // Note: Summary of the order of operations these functions:
243
313
  // 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
244
314
  // We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
245
315
  // since some may take longer to transformSaveData than others.
246
- const changes = await Promise.all(queuedChanges.map(prepChange));
247
316
  // 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
248
317
  // the current value again on next load, which isn't too bad.
249
318
  // 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
@@ -251,25 +320,46 @@ async function processQueuedChanges() {
251
320
  // 4. Wait for remote load or error if allowed
252
321
  // 5. Save to remote
253
322
  // 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
323
+ // 7. Lastly, update metadata to clear pending and update lastSync. Doing this earlier could potentially cause
255
324
  // sync inconsistences so it's very important that this is last.
256
- changes.forEach(doChange);
325
+ const preppedChangesLocal = await Promise.all(queuedChanges.map(prepChangeLocal));
326
+ // 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?
327
+ await Promise.all(queuedChanges.map(prepChangeRemote));
328
+ await Promise.all(preppedChangesLocal.map(doChangeLocal));
329
+ const timeout = (_a = observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.remoteOptions) === null || _a === void 0 ? void 0 : _a.saveTimeout;
330
+ if (timeout) {
331
+ if (timeoutSaveRemote) {
332
+ clearTimeout(timeoutSaveRemote);
333
+ }
334
+ timeoutSaveRemote = setTimeout(processQueuedRemoteChanges, timeout);
335
+ }
336
+ else {
337
+ processQueuedRemoteChanges();
338
+ }
257
339
  }
258
- async function prepChange(queuedChange) {
340
+ async function processQueuedRemoteChanges() {
341
+ const queuedRemoteChanges = mergeQueuedChanges(_queuedRemoteChanges);
342
+ _queuedRemoteChanges = [];
343
+ const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote));
344
+ preppedChangesRemote.forEach(doChangeRemote);
345
+ }
346
+ async function prepChangeLocal(queuedChange) {
259
347
  const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
260
348
  const local = persistOptions.local;
261
349
  const { persistenceRemote } = localState;
262
350
  const { config: configLocal } = parseLocalConfig(local);
263
351
  const configRemote = persistOptions.remote;
264
352
  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();
353
+ const saveRemote = !!(!inRemoteChange &&
354
+ (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) &&
355
+ !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) &&
356
+ syncState.isEnabledRemote.peek());
266
357
  if (saveLocal || saveRemote) {
267
358
  if (saveLocal && !syncState.isLoadedLocal.peek()) {
268
359
  console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
269
- return;
360
+ return undefined;
270
361
  }
271
362
  const changesLocal = [];
272
- const changesRemote = [];
273
363
  const changesPaths = new Set();
274
364
  let promisesTransform = [];
275
365
  // Reverse order
@@ -308,6 +398,52 @@ async function prepChange(queuedChange) {
308
398
  }
309
399
  }));
310
400
  }
401
+ }
402
+ }
403
+ // If there's any transform promises, wait for them before saving
404
+ promisesTransform = promisesTransform.filter(Boolean);
405
+ if (promisesTransform.length > 0) {
406
+ await Promise.all(promisesTransform);
407
+ }
408
+ return { queuedChange, changesLocal, saveRemote };
409
+ }
410
+ }
411
+ async function prepChangeRemote(queuedChange) {
412
+ const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
413
+ const local = persistOptions.local;
414
+ const { persistenceRemote } = localState;
415
+ const { config: configLocal } = parseLocalConfig(local);
416
+ const configRemote = persistOptions.remote;
417
+ const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
418
+ const saveRemote = !inRemoteChange && (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
419
+ if (saveLocal || saveRemote) {
420
+ if (saveLocal && !syncState.isLoadedLocal.peek()) {
421
+ console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
422
+ return undefined;
423
+ }
424
+ const changesRemote = [];
425
+ const changesPaths = new Set();
426
+ let promisesTransform = [];
427
+ // Reverse order
428
+ for (let i = changes.length - 1; i >= 0; i--) {
429
+ const { path } = changes[i];
430
+ let found = false;
431
+ // Optimization to only save the latest update at each path. We might have multiple changes at the same path
432
+ // and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
433
+ // already processed. If a later change modifies a parent of an earlier change (which happens on delete()
434
+ // it should be ignored as it's superseded by the parent modification.
435
+ if (changesPaths.size > 0) {
436
+ for (let u = 0; u < path.length; u++) {
437
+ if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
438
+ found = true;
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ if (!found) {
444
+ const pathStr = path.join('/');
445
+ changesPaths.add(pathStr);
446
+ const { prevAtPath, valueAtPath, pathTypes } = changes[i];
311
447
  if (saveRemote) {
312
448
  const promiseTransformRemote = transformOutData(valueAtPath, path, pathTypes, configRemote || {});
313
449
  promisesTransform.push(doInOrder(promiseTransformRemote, ({ path: pathTransformed, value: valueTransformed }) => {
@@ -365,21 +501,20 @@ async function prepChange(queuedChange) {
365
501
  if (promisesTransform.length > 0) {
366
502
  await Promise.all(promisesTransform);
367
503
  }
368
- return { queuedChange, changesLocal, changesRemote };
504
+ return { queuedChange, changesRemote };
369
505
  }
370
506
  }
371
- async function doChange(changeInfo) {
372
- var _a, _b, _c, _d;
507
+ async function doChangeLocal(changeInfo) {
373
508
  if (!changeInfo)
374
509
  return;
375
- const { queuedChange, changesLocal, changesRemote } = changeInfo;
510
+ const { queuedChange, changesLocal, saveRemote } = changeInfo;
376
511
  const { obs, syncState, localState, persistOptions } = queuedChange;
377
- const { persistenceLocal, persistenceRemote } = localState;
512
+ const { persistenceLocal } = localState;
378
513
  const local = persistOptions.local;
379
514
  const { table, config: configLocal } = parseLocalConfig(local);
380
515
  const configRemote = persistOptions.remote;
381
516
  const shouldSaveMetadata = local && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.offlineBehavior) === 'retry';
382
- if (changesRemote.length > 0 && shouldSaveMetadata) {
517
+ if (saveRemote && shouldSaveMetadata) {
383
518
  // First save pending changes before saving local or remote
384
519
  await updateMetadataImmediate(obs, localState, syncState, persistOptions, {
385
520
  pending: localState.pendingChanges,
@@ -399,29 +534,46 @@ async function doChange(changeInfo) {
399
534
  await promiseSet;
400
535
  }
401
536
  }
537
+ }
538
+ async function doChangeRemote(changeInfo) {
539
+ var _a, _b;
540
+ if (!changeInfo)
541
+ return;
542
+ const { queuedChange, changesRemote } = changeInfo;
543
+ const { obs, syncState, localState, persistOptions } = queuedChange;
544
+ const { persistenceLocal, persistenceRemote } = localState;
545
+ const local = persistOptions.local;
546
+ const { table, config: configLocal } = parseLocalConfig(local);
547
+ const { offlineBehavior, allowSetIfError, onBeforeSet, onSetError, waitForSet, onSet } = persistOptions.remote || {};
548
+ const shouldSaveMetadata = local && offlineBehavior === 'retry';
402
549
  if (changesRemote.length > 0) {
403
550
  // 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()));
551
+ await when(() => syncState.isLoaded.get() || (allowSetIfError && syncState.error.get()));
552
+ if (waitForSet) {
553
+ await when(isFunction(waitForSet) ? waitForSet(changesRemote) : waitForSet);
554
+ }
405
555
  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({
556
+ onBeforeSet === null || onBeforeSet === void 0 ? void 0 : onBeforeSet();
557
+ localState.numSavesOutstanding = (localState.numSavesOutstanding || 0) + 1;
558
+ const saved = await ((_a = persistenceRemote.set({
408
559
  obs,
409
560
  syncState: syncState,
410
561
  options: persistOptions,
411
562
  changes: changesRemote,
412
563
  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); }));
564
+ })) === null || _a === void 0 ? void 0 : _a.catch((err) => onSetError === null || onSetError === void 0 ? void 0 : onSetError(err)));
565
+ localState.numSavesOutstanding--;
414
566
  // If this remote save changed anything then update persistence and metadata
415
567
  // Because save happens after a timeout and they're batched together, some calls to save will
416
568
  // return saved data and others won't, so those can be ignored.
417
569
  if (saved) {
418
570
  const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
419
- const { changes, dateModified } = saved;
571
+ const { changes, lastSync } = saved;
420
572
  if (pathStrs.length > 0) {
421
573
  if (local) {
422
574
  const metadata = {};
423
- const pending = (_c = persistenceLocal.getMetadata(table, configLocal)) === null || _c === void 0 ? void 0 : _c.pending;
424
- let transformedChanges = [];
575
+ const pending = (_b = persistenceLocal.getMetadata(table, configLocal)) === null || _b === void 0 ? void 0 : _b.pending;
576
+ let transformedChanges = undefined;
425
577
  for (let i = 0; i < pathStrs.length; i++) {
426
578
  const pathStr = pathStrs[i];
427
579
  // Clear pending for this path
@@ -431,25 +583,37 @@ async function doChange(changeInfo) {
431
583
  metadata.pending = pending;
432
584
  }
433
585
  }
434
- if (dateModified) {
435
- metadata.modified = dateModified;
586
+ if (lastSync) {
587
+ metadata.lastSync = lastSync;
436
588
  }
437
589
  // Remote can optionally have data that needs to be merged back into the observable,
438
590
  // for example Firebase may update dateModified with the server timestamp
439
591
  if (changes && !isEmpty(changes)) {
440
- transformedChanges.push(transformLoadData(changes, persistOptions.remote, false));
592
+ transformedChanges = transformLoadData(changes, persistOptions.remote, false);
441
593
  }
442
- if (transformedChanges.length > 0) {
443
- if (transformedChanges.some((change) => isPromise(change))) {
444
- transformedChanges = await Promise.all(transformedChanges);
594
+ if (localState.numSavesOutstanding > 0) {
595
+ if (transformedChanges) {
596
+ if (!localState.pendingSaveResults) {
597
+ localState.pendingSaveResults = [];
598
+ }
599
+ localState.pendingSaveResults.push(transformedChanges);
445
600
  }
446
- onChangeRemote(() => mergeIntoObservable(obs, ...transformedChanges));
447
601
  }
448
- if (shouldSaveMetadata && !isEmpty(metadata)) {
449
- updateMetadata(obs, localState, syncState, persistOptions, metadata);
602
+ else {
603
+ let allChanges = [...(localState.pendingSaveResults || []), transformedChanges];
604
+ if (allChanges.length > 0) {
605
+ if (allChanges.some((change) => isPromise(change))) {
606
+ allChanges = await Promise.all(allChanges);
607
+ }
608
+ onChangeRemote(() => mergeIntoObservable(obs, ...allChanges));
609
+ }
610
+ if (shouldSaveMetadata && !isEmpty(metadata)) {
611
+ updateMetadata(obs, localState, syncState, persistOptions, metadata);
612
+ }
613
+ localState.pendingSaveResults = [];
450
614
  }
451
615
  }
452
- (_d = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onSet) === null || _d === void 0 ? void 0 : _d.call(configRemote);
616
+ onSet === null || onSet === void 0 ? void 0 : onSet();
453
617
  }
454
618
  }
455
619
  }
@@ -497,7 +661,7 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
497
661
  }
498
662
  const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
499
663
  localState.persistenceLocal = persistenceLocal;
500
- if (!initialized.get()) {
664
+ if (!initialized.peek()) {
501
665
  await when(initialized);
502
666
  }
503
667
  // If persistence has an asynchronous load, wait for it
@@ -511,12 +675,21 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
511
675
  let value = persistenceLocal.getTable(table, config);
512
676
  const metadata = persistenceLocal.getMetadata(table, config);
513
677
  if (metadata) {
678
+ // @ts-expect-error Migration from old version
679
+ if (!metadata.lastSync && metadata.modified) {
680
+ // @ts-expect-error Migration from old
681
+ metadata.lastSync = metadata.modified;
682
+ }
514
683
  metadatas.set(obs, metadata);
515
684
  localState.pendingChanges = metadata.pending;
516
- syncState.dateModified.set(metadata.modified);
685
+ // TODOV3 Remove dateModified
686
+ syncState.assign({
687
+ dateModified: metadata.lastSync,
688
+ lastSync: metadata.lastSync,
689
+ });
517
690
  }
518
691
  // Merge the data from local persistence into the default state
519
- if (value !== null && value !== undefined) {
692
+ if (value !== undefined) {
520
693
  const { transform, fieldTransforms } = config;
521
694
  value = transformLoadData(value, { transform, fieldTransforms }, true);
522
695
  if (isPromise(value)) {
@@ -527,7 +700,12 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
527
700
  // are set on the same observable
528
701
  internal$1.globalState.isLoadingLocal = true;
529
702
  // We want to merge the local data on top of any initial state the object is created with
530
- mergeIntoObservable(obs, value);
703
+ if (value === null && !obs.peek()) {
704
+ obs.set(value);
705
+ }
706
+ else {
707
+ mergeIntoObservable(obs, value);
708
+ }
531
709
  }, () => {
532
710
  internal$1.globalState.isLoadingLocal = false;
533
711
  });
@@ -540,18 +718,11 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
540
718
  }
541
719
  syncState.isLoadedLocal.set(true);
542
720
  }
543
- function persistObservable(initialOrObservable, persistOptions) {
544
- var _a;
545
- const obs = (isObservable(initialOrObservable)
546
- ? initialOrObservable
547
- : observable(isFunction(initialOrObservable) ? initialOrObservable() : initialOrObservable));
721
+ function persistObservable(obs, persistOptions) {
548
722
  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
723
  // Merge remote persist options with clobal options
553
724
  if (persistOptions.remote) {
554
- persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, persistOptions.remote);
725
+ persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, removeNullUndefined(persistOptions.remote));
555
726
  }
556
727
  let { remote } = persistOptions;
557
728
  const { local } = persistOptions;
@@ -594,33 +765,28 @@ function persistObservable(initialOrObservable, persistOptions) {
594
765
  var _a, _b;
595
766
  if (!isSynced) {
596
767
  isSynced = true;
597
- const dateModified = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.modified;
768
+ const lastSync = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.lastSync;
769
+ const pending = localState.pendingChanges;
598
770
  const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
599
771
  if (get) {
600
- let attemptNum = 0;
601
772
  const runGet = () => {
602
773
  get({
603
774
  state: syncState,
604
775
  obs,
605
776
  options: persistOptions,
606
- dateModified,
777
+ lastSync,
778
+ dateModified: lastSync,
607
779
  onError: (error) => {
608
780
  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
781
  (_a = remote.onGetError) === null || _a === void 0 ? void 0 : _a.call(remote, error);
619
782
  },
620
783
  onGet: () => {
621
- syncState.isLoaded.set(true);
784
+ node.state.assign({
785
+ isLoaded: true,
786
+ error: undefined,
787
+ });
622
788
  },
623
- onChange: async ({ value, path = [], pathTypes = [], mode = 'set', dateModified }) => {
789
+ onChange: async ({ value, path = [], pathTypes = [], mode = 'set', lastSync }) => {
624
790
  // Note: value is the constructed value, path is used for setInObservableAtPath
625
791
  // to start the set into the observable from the path
626
792
  if (value !== undefined) {
@@ -632,10 +798,12 @@ function persistObservable(initialOrObservable, persistOptions) {
632
798
  if (path.length && invertedMap) {
633
799
  path = transformPath(path, pathTypes, invertedMap);
634
800
  }
635
- if (mode === 'dateModified') {
636
- if (dateModified && !isEmpty(value)) {
801
+ if (mode === 'lastSync' || mode === 'dateModified') {
802
+ if (lastSync && !isEmpty(value)) {
637
803
  onChangeRemote(() => {
638
- setInObservableAtPath(obs, path, pathTypes, value, 'assign');
804
+ setInObservableAtPath(
805
+ // @ts-expect-error Fix this type
806
+ obs, path, pathTypes, value, 'assign');
639
807
  });
640
808
  }
641
809
  }
@@ -645,7 +813,10 @@ function persistObservable(initialOrObservable, persistOptions) {
645
813
  Object.keys(pending).forEach((key) => {
646
814
  const p = key.split('/').filter((p) => p !== '');
647
815
  const { v, t } = pending[key];
648
- if (value[p[0]] !== undefined) {
816
+ if (t.length === 0 || !value) {
817
+ value = v;
818
+ }
819
+ else if (value[p[0]] !== undefined) {
649
820
  value = setAtPath(value, p, t, v, obs.peek(), (path, value) => {
650
821
  delete pending[key];
651
822
  pending[path.join('/')] = {
@@ -658,13 +829,14 @@ function persistObservable(initialOrObservable, persistOptions) {
658
829
  });
659
830
  }
660
831
  onChangeRemote(() => {
832
+ // @ts-expect-error Fix this type
661
833
  setInObservableAtPath(obs, path, pathTypes, value, mode);
662
834
  });
663
835
  }
664
836
  }
665
- if (dateModified && local) {
837
+ if (lastSync && local) {
666
838
  updateMetadata(obs, localState, syncState, persistOptions, {
667
- modified: dateModified,
839
+ lastSync,
668
840
  });
669
841
  }
670
842
  },
@@ -673,11 +845,13 @@ function persistObservable(initialOrObservable, persistOptions) {
673
845
  runGet();
674
846
  }
675
847
  else {
676
- syncState.isLoaded.set(true);
848
+ node.state.assign({
849
+ isLoaded: true,
850
+ error: undefined,
851
+ });
677
852
  }
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,6 +864,7 @@ 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
867
+ // @ts-expect-error Fix this type
693
868
  onObsChange(obs, syncState, localState, persistOptions, {
694
869
  value: obs.peek(),
695
870
  // TODO getPrevious if any remote persistence layers need it
@@ -725,49 +900,122 @@ function persistObservable(initialOrObservable, persistOptions) {
725
900
  }
726
901
  obs.onChange(onObsChange.bind(this, obs, syncState, localState, persistOptions));
727
902
  });
728
- return obs;
903
+ return syncState;
729
904
  }
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;
905
+
906
+ const { getProxy, globalState, runWithRetry, symbolActivated } = internal$1;
907
+ function persistActivateNode() {
908
+ globalState.activateNode = function activateNodePersist(node, refresh, wasPromise, newValue) {
909
+ if (node.activationState) {
910
+ const { get, initial, onSet, subscribe, cache, retry, offlineBehavior, waitForSet } = node.activationState;
911
+ let onChange = undefined;
912
+ const pluginRemote = {};
913
+ if (get) {
914
+ pluginRemote.get = (params) => {
915
+ onChange = params.onChange;
916
+ const updateLastSync = (lastSync) => (params.lastSync = lastSync);
917
+ const setMode = (mode) => (params.mode = mode);
918
+ const nodeValue = getNodeValue(node);
919
+ const value = runWithRetry(node, { attemptNum: 0 }, () => {
920
+ return get({
921
+ value: isFunction(nodeValue) || (nodeValue === null || nodeValue === void 0 ? void 0 : nodeValue[symbolActivated]) ? undefined : nodeValue,
922
+ lastSync: params.lastSync,
923
+ updateLastSync,
924
+ setMode,
925
+ refresh,
926
+ });
927
+ });
928
+ return value;
929
+ };
930
+ }
931
+ if (onSet) {
932
+ // TODO: Work out these types better
933
+ pluginRemote.set = async (params) => {
934
+ var _a;
935
+ if ((_a = node.state) === null || _a === void 0 ? void 0 : _a.isLoaded.get()) {
936
+ const retryAttempts = { attemptNum: 0 };
937
+ return runWithRetry(node, retryAttempts, async (retryEvent) => {
938
+ let changes = {};
939
+ let maxModified = 0;
940
+ if (!node.state.isLoaded.peek()) {
941
+ await whenReady(node.state.isLoaded);
942
+ }
943
+ const cancelRetry = () => {
944
+ retryEvent.cancel = true;
945
+ };
946
+ await onSet({
947
+ ...params,
948
+ node,
949
+ update: (params) => {
950
+ const { value, lastSync } = params;
951
+ maxModified = Math.max(lastSync || 0, maxModified);
952
+ changes = mergeIntoObservable(changes, value);
953
+ },
954
+ retryNum: retryAttempts.attemptNum,
955
+ cancelRetry,
956
+ refresh,
957
+ fromSubscribe: false,
958
+ });
959
+ return { changes, lastSync: maxModified || undefined };
960
+ });
961
+ }
962
+ };
963
+ }
964
+ if (subscribe) {
965
+ subscribe({
966
+ node,
967
+ update: (params) => {
968
+ if (!onChange) {
969
+ // TODO: Make this message better
970
+ console.log('[legend-state] Cannot update immediately before the first return');
971
+ }
972
+ else {
973
+ onChange(params);
974
+ }
975
+ },
976
+ refresh,
977
+ });
978
+ }
979
+ persistObservable(getProxy(node), {
980
+ pluginRemote,
981
+ ...(cache || {}),
982
+ remote: {
983
+ retry: retry,
984
+ offlineBehavior,
985
+ waitForSet,
986
+ },
987
+ });
988
+ const nodeVal = getNodeValue(node);
989
+ if (nodeVal !== undefined) {
990
+ newValue = nodeVal;
738
991
  }
739
- if (lastSync.value) {
740
- params.dateModified = lastSync.value;
992
+ else if (newValue === undefined) {
993
+ newValue = initial;
741
994
  }
742
- return newValue;
743
- },
995
+ return { update: onChange, value: newValue };
996
+ }
997
+ else {
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
+ // eslint-disable-next-line no-empty
1006
+ }
1007
+ catch (_a) { }
1008
+ }
1009
+ return newValue;
1010
+ },
1011
+ };
1012
+ persistObservable(getProxy(node), {
1013
+ pluginRemote,
1014
+ });
1015
+ return { update: onChange, value: newValue };
1016
+ }
744
1017
  };
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
- };
1018
+ }
771
1019
 
772
1020
  function isInRemoteChange() {
773
1021
  return internal$1.globalState.isLoadingRemote$.get();
@@ -775,6 +1023,7 @@ function isInRemoteChange() {
775
1023
  const internal = {
776
1024
  observablePersistConfiguration,
777
1025
  };
1026
+ persistActivateNode();
778
1027
 
779
1028
  export { configureObservablePersistence, internal, invertFieldMap, isInRemoteChange, mapPersistences, persistObservable, transformObject, transformPath };
780
1029
  //# sourceMappingURL=persist.mjs.map