@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.js CHANGED
@@ -139,9 +139,21 @@ function observablePersistRemoteFunctionsAdapter({ get, set, }) {
139
139
  const ret = {};
140
140
  if (get) {
141
141
  ret.get = (async (params) => {
142
- const value = (await get(params));
143
- params.onChange({ value, dateModified: params.dateModified || Date.now() });
144
- params.onGet();
142
+ try {
143
+ let value = get(params);
144
+ if (state.isPromise(value)) {
145
+ value = await value;
146
+ }
147
+ params.onChange({
148
+ value,
149
+ dateModified: params.dateModified,
150
+ lastSync: params.lastSync,
151
+ mode: params.mode,
152
+ });
153
+ params.onGet();
154
+ // eslint-disable-next-line no-empty
155
+ }
156
+ catch (_a) { }
145
157
  });
146
158
  }
147
159
  if (set) {
@@ -150,7 +162,22 @@ function observablePersistRemoteFunctionsAdapter({ get, set, }) {
150
162
  return ret;
151
163
  }
152
164
 
153
- const { getProxy, globalState } = state.internal;
165
+ function removeNullUndefined(val) {
166
+ if (val) {
167
+ Object.keys(val).forEach((key) => {
168
+ const v = val[key];
169
+ if (v === null || v === undefined) {
170
+ delete val[key];
171
+ }
172
+ else if (state.isObject(v)) {
173
+ removeNullUndefined(v);
174
+ }
175
+ });
176
+ }
177
+ return val;
178
+ }
179
+
180
+ const { globalState: globalState$1 } = state.internal;
154
181
  const mapPersistences = new WeakMap();
155
182
  const metadatas = new WeakMap();
156
183
  const promisesLocalSaves = new Set();
@@ -165,11 +192,12 @@ function doInOrder(arg1, arg2) {
165
192
  return state.isPromise(arg1) ? arg1.then(arg2) : arg2(arg1);
166
193
  }
167
194
  function onChangeRemote(cb) {
168
- state.when(() => !globalState.isLoadingRemote$.get(), () => {
195
+ state.when(() => !globalState$1.isLoadingRemote$.get(), () => {
196
+ state.endBatch(true);
169
197
  // Remote changes should only update local state
170
- globalState.isLoadingRemote$.set(true);
198
+ globalState$1.isLoadingRemote$.set(true);
171
199
  state.batch(cb, () => {
172
- globalState.isLoadingRemote$.set(false);
200
+ globalState$1.isLoadingRemote$.set(false);
173
201
  });
174
202
  });
175
203
  }
@@ -216,16 +244,19 @@ async function updateMetadataImmediate(obs, localState, syncState, persistOption
216
244
  const { table, config } = parseLocalConfig(local);
217
245
  // Save metadata
218
246
  const oldMetadata = metadatas.get(obs);
219
- const { modified, pending } = newMetadata;
220
- const needsUpdate = pending || (modified && (!oldMetadata || modified !== oldMetadata.modified));
247
+ const { lastSync, pending } = newMetadata;
248
+ const needsUpdate = pending || (lastSync && (!oldMetadata || lastSync !== oldMetadata.lastSync));
221
249
  if (needsUpdate) {
222
250
  const metadata = Object.assign({}, oldMetadata, newMetadata);
223
251
  metadatas.set(obs, metadata);
224
252
  if (persistenceLocal) {
225
253
  await persistenceLocal.setMetadata(table, metadata, config);
226
254
  }
227
- if (modified) {
228
- syncState.dateModified.set(modified);
255
+ if (lastSync) {
256
+ syncState.assign({
257
+ lastSync: lastSync,
258
+ dateModified: lastSync,
259
+ });
229
260
  }
230
261
  }
231
262
  }
@@ -237,15 +268,51 @@ function updateMetadata(obs, localState, syncState, persistOptions, newMetadata)
237
268
  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);
238
269
  }
239
270
  let _queuedChanges = [];
271
+ let _queuedRemoteChanges = [];
272
+ let timeoutSaveRemote = undefined;
273
+ function mergeChanges(changes) {
274
+ const changesByPath = new Map();
275
+ const changesOut = [];
276
+ // TODO: This could be even more robust by going deeper into paths like the firebase plugin's _updatePendingSave
277
+ for (let i = 0; i < changes.length; i++) {
278
+ const change = changes[i];
279
+ const pathStr = change.path.join('/');
280
+ const existing = changesByPath.get(pathStr);
281
+ if (existing) {
282
+ existing.valueAtPath = change.valueAtPath;
283
+ }
284
+ else {
285
+ changesByPath.set(pathStr, change);
286
+ changesOut.push(change);
287
+ }
288
+ }
289
+ return changesOut;
290
+ }
291
+ function mergeQueuedChanges(allChanges) {
292
+ const changesByObs = new Map();
293
+ const out = new Map();
294
+ for (let i = 0; i < allChanges.length; i++) {
295
+ const value = allChanges[i];
296
+ const { obs, changes } = value;
297
+ const existing = changesByObs.get(obs);
298
+ const newChanges = existing ? [...existing, ...changes] : changes;
299
+ const merged = mergeChanges(newChanges);
300
+ changesByObs.set(obs, merged);
301
+ value.changes = merged;
302
+ out.set(obs, value);
303
+ }
304
+ return Array.from(out.values());
305
+ }
240
306
  async function processQueuedChanges() {
307
+ var _a;
241
308
  // Get a local copy of the queued changes and clear the global queue
242
- const queuedChanges = _queuedChanges;
309
+ const queuedChanges = mergeQueuedChanges(_queuedChanges);
243
310
  _queuedChanges = [];
311
+ _queuedRemoteChanges.push(...queuedChanges.filter((c) => !c.inRemoteChange));
244
312
  // Note: Summary of the order of operations these functions:
245
313
  // 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
246
314
  // We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
247
315
  // since some may take longer to transformSaveData than others.
248
- const changes = await Promise.all(queuedChanges.map(prepChange));
249
316
  // 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
250
317
  // the current value again on next load, which isn't too bad.
251
318
  // 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
@@ -253,25 +320,46 @@ async function processQueuedChanges() {
253
320
  // 4. Wait for remote load or error if allowed
254
321
  // 5. Save to remote
255
322
  // 6. On successful save, merge changes (if any) back into observable
256
- // 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
257
324
  // sync inconsistences so it's very important that this is last.
258
- 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
+ }
339
+ }
340
+ async function processQueuedRemoteChanges() {
341
+ const queuedRemoteChanges = mergeQueuedChanges(_queuedRemoteChanges);
342
+ _queuedRemoteChanges = [];
343
+ const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote));
344
+ preppedChangesRemote.forEach(doChangeRemote);
259
345
  }
260
- async function prepChange(queuedChange) {
346
+ async function prepChangeLocal(queuedChange) {
261
347
  const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
262
348
  const local = persistOptions.local;
263
349
  const { persistenceRemote } = localState;
264
350
  const { config: configLocal } = parseLocalConfig(local);
265
351
  const configRemote = persistOptions.remote;
266
352
  const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
267
- 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());
268
357
  if (saveLocal || saveRemote) {
269
358
  if (saveLocal && !syncState.isLoadedLocal.peek()) {
270
359
  console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
271
- return;
360
+ return undefined;
272
361
  }
273
362
  const changesLocal = [];
274
- const changesRemote = [];
275
363
  const changesPaths = new Set();
276
364
  let promisesTransform = [];
277
365
  // Reverse order
@@ -310,6 +398,52 @@ async function prepChange(queuedChange) {
310
398
  }
311
399
  }));
312
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];
313
447
  if (saveRemote) {
314
448
  const promiseTransformRemote = transformOutData(valueAtPath, path, pathTypes, configRemote || {});
315
449
  promisesTransform.push(doInOrder(promiseTransformRemote, ({ path: pathTransformed, value: valueTransformed }) => {
@@ -367,21 +501,20 @@ async function prepChange(queuedChange) {
367
501
  if (promisesTransform.length > 0) {
368
502
  await Promise.all(promisesTransform);
369
503
  }
370
- return { queuedChange, changesLocal, changesRemote };
504
+ return { queuedChange, changesRemote };
371
505
  }
372
506
  }
373
- async function doChange(changeInfo) {
374
- var _a, _b, _c, _d;
507
+ async function doChangeLocal(changeInfo) {
375
508
  if (!changeInfo)
376
509
  return;
377
- const { queuedChange, changesLocal, changesRemote } = changeInfo;
510
+ const { queuedChange, changesLocal, saveRemote } = changeInfo;
378
511
  const { obs, syncState, localState, persistOptions } = queuedChange;
379
- const { persistenceLocal, persistenceRemote } = localState;
512
+ const { persistenceLocal } = localState;
380
513
  const local = persistOptions.local;
381
514
  const { table, config: configLocal } = parseLocalConfig(local);
382
515
  const configRemote = persistOptions.remote;
383
516
  const shouldSaveMetadata = local && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.offlineBehavior) === 'retry';
384
- if (changesRemote.length > 0 && shouldSaveMetadata) {
517
+ if (saveRemote && shouldSaveMetadata) {
385
518
  // First save pending changes before saving local or remote
386
519
  await updateMetadataImmediate(obs, localState, syncState, persistOptions, {
387
520
  pending: localState.pendingChanges,
@@ -401,29 +534,46 @@ async function doChange(changeInfo) {
401
534
  await promiseSet;
402
535
  }
403
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';
404
549
  if (changesRemote.length > 0) {
405
550
  // Wait for remote to be ready before saving
406
- await state.when(() => syncState.isLoaded.get() || ((configRemote === null || configRemote === void 0 ? void 0 : configRemote.allowSetIfError) && syncState.error.get()));
551
+ await state.when(() => syncState.isLoaded.get() || (allowSetIfError && syncState.error.get()));
552
+ if (waitForSet) {
553
+ await state.when(state.isFunction(waitForSet) ? waitForSet(changesRemote) : waitForSet);
554
+ }
407
555
  const value = obs.peek();
408
- (_a = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onBeforeSet) === null || _a === void 0 ? void 0 : _a.call(configRemote);
409
- 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({
410
559
  obs,
411
560
  syncState: syncState,
412
561
  options: persistOptions,
413
562
  changes: changesRemote,
414
563
  value,
415
- })) === 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--;
416
566
  // If this remote save changed anything then update persistence and metadata
417
567
  // Because save happens after a timeout and they're batched together, some calls to save will
418
568
  // return saved data and others won't, so those can be ignored.
419
569
  if (saved) {
420
570
  const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
421
- const { changes, dateModified } = saved;
571
+ const { changes, lastSync } = saved;
422
572
  if (pathStrs.length > 0) {
423
573
  if (local) {
424
574
  const metadata = {};
425
- const pending = (_c = persistenceLocal.getMetadata(table, configLocal)) === null || _c === void 0 ? void 0 : _c.pending;
426
- let transformedChanges = [];
575
+ const pending = (_b = persistenceLocal.getMetadata(table, configLocal)) === null || _b === void 0 ? void 0 : _b.pending;
576
+ let transformedChanges = undefined;
427
577
  for (let i = 0; i < pathStrs.length; i++) {
428
578
  const pathStr = pathStrs[i];
429
579
  // Clear pending for this path
@@ -433,25 +583,37 @@ async function doChange(changeInfo) {
433
583
  metadata.pending = pending;
434
584
  }
435
585
  }
436
- if (dateModified) {
437
- metadata.modified = dateModified;
586
+ if (lastSync) {
587
+ metadata.lastSync = lastSync;
438
588
  }
439
589
  // Remote can optionally have data that needs to be merged back into the observable,
440
590
  // for example Firebase may update dateModified with the server timestamp
441
591
  if (changes && !state.isEmpty(changes)) {
442
- transformedChanges.push(transformLoadData(changes, persistOptions.remote, false));
592
+ transformedChanges = transformLoadData(changes, persistOptions.remote, false);
443
593
  }
444
- if (transformedChanges.length > 0) {
445
- if (transformedChanges.some((change) => state.isPromise(change))) {
446
- 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);
447
600
  }
448
- onChangeRemote(() => state.mergeIntoObservable(obs, ...transformedChanges));
449
601
  }
450
- if (shouldSaveMetadata && !state.isEmpty(metadata)) {
451
- updateMetadata(obs, localState, syncState, persistOptions, metadata);
602
+ else {
603
+ let allChanges = [...(localState.pendingSaveResults || []), transformedChanges];
604
+ if (allChanges.length > 0) {
605
+ if (allChanges.some((change) => state.isPromise(change))) {
606
+ allChanges = await Promise.all(allChanges);
607
+ }
608
+ onChangeRemote(() => state.mergeIntoObservable(obs, ...allChanges));
609
+ }
610
+ if (shouldSaveMetadata && !state.isEmpty(metadata)) {
611
+ updateMetadata(obs, localState, syncState, persistOptions, metadata);
612
+ }
613
+ localState.pendingSaveResults = [];
452
614
  }
453
615
  }
454
- (_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();
455
617
  }
456
618
  }
457
619
  }
@@ -499,7 +661,7 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
499
661
  }
500
662
  const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
501
663
  localState.persistenceLocal = persistenceLocal;
502
- if (!initialized.get()) {
664
+ if (!initialized.peek()) {
503
665
  await state.when(initialized);
504
666
  }
505
667
  // If persistence has an asynchronous load, wait for it
@@ -513,12 +675,21 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
513
675
  let value = persistenceLocal.getTable(table, config);
514
676
  const metadata = persistenceLocal.getMetadata(table, config);
515
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
+ }
516
683
  metadatas.set(obs, metadata);
517
684
  localState.pendingChanges = metadata.pending;
518
- syncState.dateModified.set(metadata.modified);
685
+ // TODOV3 Remove dateModified
686
+ syncState.assign({
687
+ dateModified: metadata.lastSync,
688
+ lastSync: metadata.lastSync,
689
+ });
519
690
  }
520
691
  // Merge the data from local persistence into the default state
521
- if (value !== null && value !== undefined) {
692
+ if (value !== undefined) {
522
693
  const { transform, fieldTransforms } = config;
523
694
  value = transformLoadData(value, { transform, fieldTransforms }, true);
524
695
  if (state.isPromise(value)) {
@@ -529,7 +700,12 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
529
700
  // are set on the same observable
530
701
  state.internal.globalState.isLoadingLocal = true;
531
702
  // We want to merge the local data on top of any initial state the object is created with
532
- state.mergeIntoObservable(obs, value);
703
+ if (value === null && !obs.peek()) {
704
+ obs.set(value);
705
+ }
706
+ else {
707
+ state.mergeIntoObservable(obs, value);
708
+ }
533
709
  }, () => {
534
710
  state.internal.globalState.isLoadingLocal = false;
535
711
  });
@@ -542,18 +718,11 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
542
718
  }
543
719
  syncState.isLoadedLocal.set(true);
544
720
  }
545
- function persistObservable(initialOrObservable, persistOptions) {
546
- var _a;
547
- const obs = (state.isObservable(initialOrObservable)
548
- ? initialOrObservable
549
- : state.observable(state.isFunction(initialOrObservable) ? initialOrObservable() : initialOrObservable));
721
+ function persistObservable(obs, persistOptions) {
550
722
  const node = state.getNode(obs);
551
- if (process.env.NODE_ENV === 'development' && ((_a = obs === null || obs === void 0 ? void 0 : obs.peek()) === null || _a === void 0 ? void 0 : _a._state)) {
552
- console.warn('[legend-state] WARNING: persistObservable creates a property named "_state" but your observable already has "state" in it');
553
- }
554
723
  // Merge remote persist options with clobal options
555
724
  if (persistOptions.remote) {
556
- persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, persistOptions.remote);
725
+ persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, removeNullUndefined(persistOptions.remote));
557
726
  }
558
727
  let { remote } = persistOptions;
559
728
  const { local } = persistOptions;
@@ -596,33 +765,28 @@ function persistObservable(initialOrObservable, persistOptions) {
596
765
  var _a, _b;
597
766
  if (!isSynced) {
598
767
  isSynced = true;
599
- 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;
600
770
  const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
601
771
  if (get) {
602
- let attemptNum = 0;
603
772
  const runGet = () => {
604
773
  get({
605
774
  state: syncState,
606
775
  obs,
607
776
  options: persistOptions,
608
- dateModified,
777
+ lastSync,
778
+ dateModified: lastSync,
609
779
  onError: (error) => {
610
780
  var _a;
611
- if (remote.retry) {
612
- const { backoff, delay = 1000, infinite, times = 3, maxDelay = 30000, } = remote.retry;
613
- if (infinite || attemptNum++ < times) {
614
- const delayTime = Math.min(delay * (backoff === 'constant' ? 1 : 2 ** attemptNum), maxDelay);
615
- setTimeout(runGet, delayTime);
616
- // Don't error when retrying
617
- return;
618
- }
619
- }
620
781
  (_a = remote.onGetError) === null || _a === void 0 ? void 0 : _a.call(remote, error);
621
782
  },
622
783
  onGet: () => {
623
- syncState.isLoaded.set(true);
784
+ node.state.assign({
785
+ isLoaded: true,
786
+ error: undefined,
787
+ });
624
788
  },
625
- onChange: async ({ value, path = [], pathTypes = [], mode = 'set', dateModified }) => {
789
+ onChange: async ({ value, path = [], pathTypes = [], mode = 'set', lastSync }) => {
626
790
  // Note: value is the constructed value, path is used for setInObservableAtPath
627
791
  // to start the set into the observable from the path
628
792
  if (value !== undefined) {
@@ -634,10 +798,12 @@ function persistObservable(initialOrObservable, persistOptions) {
634
798
  if (path.length && invertedMap) {
635
799
  path = transformPath(path, pathTypes, invertedMap);
636
800
  }
637
- if (mode === 'dateModified') {
638
- if (dateModified && !state.isEmpty(value)) {
801
+ if (mode === 'lastSync' || mode === 'dateModified') {
802
+ if (lastSync && !state.isEmpty(value)) {
639
803
  onChangeRemote(() => {
640
- state.setInObservableAtPath(obs, path, pathTypes, value, 'assign');
804
+ state.setInObservableAtPath(
805
+ // @ts-expect-error Fix this type
806
+ obs, path, pathTypes, value, 'assign');
641
807
  });
642
808
  }
643
809
  }
@@ -647,7 +813,10 @@ function persistObservable(initialOrObservable, persistOptions) {
647
813
  Object.keys(pending).forEach((key) => {
648
814
  const p = key.split('/').filter((p) => p !== '');
649
815
  const { v, t } = pending[key];
650
- if (value[p[0]] !== undefined) {
816
+ if (t.length === 0 || !value) {
817
+ value = v;
818
+ }
819
+ else if (value[p[0]] !== undefined) {
651
820
  value = state.setAtPath(value, p, t, v, obs.peek(), (path, value) => {
652
821
  delete pending[key];
653
822
  pending[path.join('/')] = {
@@ -660,13 +829,14 @@ function persistObservable(initialOrObservable, persistOptions) {
660
829
  });
661
830
  }
662
831
  onChangeRemote(() => {
832
+ // @ts-expect-error Fix this type
663
833
  state.setInObservableAtPath(obs, path, pathTypes, value, mode);
664
834
  });
665
835
  }
666
836
  }
667
- if (dateModified && local) {
837
+ if (lastSync && local) {
668
838
  updateMetadata(obs, localState, syncState, persistOptions, {
669
- modified: dateModified,
839
+ lastSync,
670
840
  });
671
841
  }
672
842
  },
@@ -675,11 +845,13 @@ function persistObservable(initialOrObservable, persistOptions) {
675
845
  runGet();
676
846
  }
677
847
  else {
678
- syncState.isLoaded.set(true);
848
+ node.state.assign({
849
+ isLoaded: true,
850
+ error: undefined,
851
+ });
679
852
  }
680
853
  // Wait for remote to be ready before saving pending
681
854
  await state.when(() => syncState.isLoaded.get() || (remote.allowSetIfError && syncState.error.get()));
682
- const pending = localState.pendingChanges;
683
855
  if (pending && !state.isEmpty(pending)) {
684
856
  localState.isApplyingPending = true;
685
857
  const keys = Object.keys(pending);
@@ -692,6 +864,7 @@ function persistObservable(initialOrObservable, persistOptions) {
692
864
  changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
693
865
  }
694
866
  // Send the changes into onObsChange so that they get persisted remotely
867
+ // @ts-expect-error Fix this type
695
868
  onObsChange(obs, syncState, localState, persistOptions, {
696
869
  value: obs.peek(),
697
870
  // TODO getPrevious if any remote persistence layers need it
@@ -727,49 +900,122 @@ function persistObservable(initialOrObservable, persistOptions) {
727
900
  }
728
901
  obs.onChange(onObsChange.bind(this, obs, syncState, localState, persistOptions));
729
902
  });
730
- return obs;
903
+ return syncState;
731
904
  }
732
- globalState.activateNode = function activateNodePersist(node, refresh, newValue) {
733
- const { onSetFn, subscriber, lastSync, cacheOptions, retryOptions } = node.activationState;
734
- let onChange = undefined;
735
- const pluginRemote = {
736
- get: async (params) => {
737
- onChange = params.onChange;
738
- if (state.isPromise(newValue)) {
739
- newValue = await newValue;
905
+
906
+ const { getProxy, globalState, runWithRetry, symbolActivated } = state.internal;
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 = state.getNodeValue(node);
919
+ const value = runWithRetry(node, { attemptNum: 0 }, () => {
920
+ return get({
921
+ value: state.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 state.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 = state.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 = state.getNodeValue(node);
989
+ if (nodeVal !== undefined) {
990
+ newValue = nodeVal;
740
991
  }
741
- if (lastSync.value) {
742
- params.dateModified = lastSync.value;
992
+ else if (newValue === undefined) {
993
+ newValue = initial;
743
994
  }
744
- return newValue;
745
- },
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 (state.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
+ }
746
1017
  };
747
- if (onSetFn) {
748
- // TODO: Work out these types better
749
- pluginRemote.set = onSetFn;
750
- }
751
- if (subscriber) {
752
- subscriber({
753
- update: (params) => {
754
- if (!onChange) {
755
- // TODO: Make this message better
756
- console.log('[legend-state] Cannot update immediately before the first return');
757
- }
758
- else {
759
- onChange(params);
760
- }
761
- },
762
- refresh,
763
- });
764
- }
765
- persistObservable(getProxy(node), {
766
- pluginRemote,
767
- ...(cacheOptions || {}),
768
- remote: {
769
- retry: retryOptions,
770
- },
771
- });
772
- };
1018
+ }
773
1019
 
774
1020
  function isInRemoteChange() {
775
1021
  return state.internal.globalState.isLoadingRemote$.get();
@@ -777,6 +1023,7 @@ function isInRemoteChange() {
777
1023
  const internal = {
778
1024
  observablePersistConfiguration,
779
1025
  };
1026
+ persistActivateNode();
780
1027
 
781
1028
  exports.configureObservablePersistence = configureObservablePersistence;
782
1029
  exports.internal = internal;