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

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