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