@legendapp/state 2.2.0-next.2 → 2.2.0-next.21

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 +14 -9
  23. package/index.js +522 -226
  24. package/index.js.map +1 -1
  25. package/index.mjs +521 -227
  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 +3 -3
  33. package/persist-plugins/firebase.js.map +1 -1
  34. package/persist-plugins/firebase.mjs +3 -3
  35. package/persist-plugins/firebase.mjs.map +1 -1
  36. package/persist-plugins/indexeddb.js.map +1 -1
  37. package/persist-plugins/indexeddb.mjs.map +1 -1
  38. package/persist-plugins/local-storage.js +1 -1
  39. package/persist-plugins/local-storage.js.map +1 -1
  40. package/persist-plugins/local-storage.mjs +1 -1
  41. package/persist-plugins/local-storage.mjs.map +1 -1
  42. package/persist-plugins/mmkv.js.map +1 -1
  43. package/persist-plugins/mmkv.mjs.map +1 -1
  44. package/persist-plugins/query.js.map +1 -1
  45. package/persist-plugins/query.mjs.map +1 -1
  46. package/persist.d.ts +16 -1
  47. package/persist.js +406 -138
  48. package/persist.js.map +1 -1
  49. package/persist.mjs +407 -139
  50. package/persist.mjs.map +1 -1
  51. package/react-hooks/createObservableHook.js +1 -1
  52. package/react-hooks/createObservableHook.js.map +1 -1
  53. package/react-hooks/createObservableHook.mjs +1 -1
  54. package/react-hooks/createObservableHook.mjs.map +1 -1
  55. package/react-hooks/useFetch.d.ts +4 -3
  56. package/react-hooks/useFetch.js.map +1 -1
  57. package/react-hooks/useFetch.mjs.map +1 -1
  58. package/react-hooks/useHover.js.map +1 -1
  59. package/react-hooks/useHover.mjs.map +1 -1
  60. package/react-hooks/useMeasure.js.map +1 -1
  61. package/react-hooks/useMeasure.mjs.map +1 -1
  62. package/react-hooks/useObservableNextRouter.js.map +1 -1
  63. package/react-hooks/useObservableNextRouter.mjs.map +1 -1
  64. package/react-hooks/useObservableQuery.js.map +1 -1
  65. package/react-hooks/useObservableQuery.mjs.map +1 -1
  66. package/react-hooks/usePersistedObservable.d.ts +2 -2
  67. package/react-hooks/usePersistedObservable.js +3 -3
  68. package/react-hooks/usePersistedObservable.js.map +1 -1
  69. package/react-hooks/usePersistedObservable.mjs +3 -3
  70. package/react-hooks/usePersistedObservable.mjs.map +1 -1
  71. package/react.js +13 -8
  72. package/react.js.map +1 -1
  73. package/react.mjs +14 -9
  74. package/react.mjs.map +1 -1
  75. package/src/ObservableObject.d.ts +8 -4
  76. package/src/ObservablePrimitive.d.ts +2 -1
  77. package/src/activated.d.ts +3 -0
  78. package/src/batching.d.ts +3 -1
  79. package/src/computed.d.ts +1 -1
  80. package/src/config/enableDirectAccess.d.ts +1 -1
  81. package/src/config/enableDirectPeek.d.ts +1 -1
  82. package/src/config/enableReactTracking.d.ts +4 -3
  83. package/src/config/enableReactUse.d.ts +1 -1
  84. package/src/createObservable.d.ts +2 -2
  85. package/src/globals.d.ts +10 -9
  86. package/src/helpers/fetch.d.ts +4 -3
  87. package/src/helpers/time.d.ts +2 -2
  88. package/src/helpers.d.ts +3 -2
  89. package/src/history/trackHistory.d.ts +1 -1
  90. package/src/observable.d.ts +6 -15
  91. package/src/observableInterfaces.d.ts +68 -315
  92. package/src/observableTypes.d.ts +93 -0
  93. package/src/persist/observablePersistRemoteFunctionsAdapter.d.ts +1 -1
  94. package/src/persist/persistActivateNode.d.ts +1 -0
  95. package/src/persist/persistHelpers.d.ts +1 -1
  96. package/src/persist/persistObservable.d.ts +3 -3
  97. package/src/persistTypes.d.ts +229 -0
  98. package/src/proxy.d.ts +2 -1
  99. package/src/react/Computed.d.ts +1 -1
  100. package/src/react/Switch.d.ts +3 -3
  101. package/src/react/reactInterfaces.d.ts +2 -1
  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 +4 -0
  107. package/src/trackSelector.d.ts +3 -2
  108. package/src/when.d.ts +6 -2
  109. package/trace.js.map +1 -1
  110. package/trace.mjs.map +1 -1
package/persist.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { symbolDelete, isString, isArray, isObject, constructObjectWithPath, deconstructObjectWithPath, internal as internal$1, isObservable, observable, isFunction, getNode, when, isPromise, batch, mergeIntoObservable, isEmpty, setAtPath, setInObservableAtPath } from '@legendapp/state';
1
+ import { symbolDelete, isString, isArray, isObject, constructObjectWithPath, deconstructObjectWithPath, isPromise, getNode, observable, when, internal as internal$1, batch, mergeIntoObservable, isEmpty, setAtPath, endBatch, setInObservableAtPath, getNodeValue, isFunction, whenReady } from '@legendapp/state';
2
2
 
3
3
  const observablePersistConfiguration = {};
4
4
  function configureObservablePersistence(options) {
@@ -137,9 +137,21 @@ function observablePersistRemoteFunctionsAdapter({ get, set, }) {
137
137
  const ret = {};
138
138
  if (get) {
139
139
  ret.get = (async (params) => {
140
- const value = (await get(params));
141
- params.onChange({ value, dateModified: params.dateModified || Date.now() });
142
- params.onGet();
140
+ try {
141
+ let value = get(params);
142
+ if (isPromise(value)) {
143
+ value = await value;
144
+ }
145
+ params.onChange({
146
+ value,
147
+ dateModified: params.dateModified,
148
+ lastSync: params.lastSync,
149
+ mode: params.mode,
150
+ });
151
+ params.onGet();
152
+ // eslint-disable-next-line no-empty
153
+ }
154
+ catch (_a) { }
143
155
  });
144
156
  }
145
157
  if (set) {
@@ -148,7 +160,22 @@ function observablePersistRemoteFunctionsAdapter({ get, set, }) {
148
160
  return ret;
149
161
  }
150
162
 
151
- const { getProxy, globalState } = internal$1;
163
+ function removeNullUndefined(val) {
164
+ if (val) {
165
+ Object.keys(val).forEach((key) => {
166
+ const v = val[key];
167
+ if (v === null || v === undefined) {
168
+ delete val[key];
169
+ }
170
+ else if (isObject(v)) {
171
+ removeNullUndefined(v);
172
+ }
173
+ });
174
+ }
175
+ return val;
176
+ }
177
+
178
+ const { globalState: globalState$1 } = internal$1;
152
179
  const mapPersistences = new WeakMap();
153
180
  const metadatas = new WeakMap();
154
181
  const promisesLocalSaves = new Set();
@@ -163,11 +190,12 @@ function doInOrder(arg1, arg2) {
163
190
  return isPromise(arg1) ? arg1.then(arg2) : arg2(arg1);
164
191
  }
165
192
  function onChangeRemote(cb) {
166
- when(() => !globalState.isLoadingRemote$.get(), () => {
193
+ when(() => !globalState$1.isLoadingRemote$.get(), () => {
194
+ endBatch(true);
167
195
  // Remote changes should only update local state
168
- globalState.isLoadingRemote$.set(true);
196
+ globalState$1.isLoadingRemote$.set(true);
169
197
  batch(cb, () => {
170
- globalState.isLoadingRemote$.set(false);
198
+ globalState$1.isLoadingRemote$.set(false);
171
199
  });
172
200
  });
173
201
  }
@@ -214,35 +242,78 @@ async function updateMetadataImmediate(obs, localState, syncState, persistOption
214
242
  const { table, config } = parseLocalConfig(local);
215
243
  // Save metadata
216
244
  const oldMetadata = metadatas.get(obs);
217
- const { modified, pending } = newMetadata;
218
- const needsUpdate = pending || (modified && (!oldMetadata || modified !== oldMetadata.modified));
245
+ const { lastSync, pending } = newMetadata;
246
+ const needsUpdate = pending || (lastSync && (!oldMetadata || lastSync !== oldMetadata.lastSync));
219
247
  if (needsUpdate) {
220
248
  const metadata = Object.assign({}, oldMetadata, newMetadata);
221
249
  metadatas.set(obs, metadata);
222
250
  if (persistenceLocal) {
223
251
  await persistenceLocal.setMetadata(table, metadata, config);
224
252
  }
225
- if (modified) {
226
- syncState.dateModified.set(modified);
253
+ if (lastSync) {
254
+ syncState.assign({
255
+ lastSync: lastSync,
256
+ dateModified: lastSync,
257
+ });
227
258
  }
228
259
  }
229
260
  }
230
261
  function updateMetadata(obs, localState, syncState, persistOptions, newMetadata) {
262
+ var _a;
231
263
  if (localState.timeoutSaveMetadata) {
232
264
  clearTimeout(localState.timeoutSaveMetadata);
233
265
  }
234
- localState.timeoutSaveMetadata = setTimeout(() => updateMetadataImmediate(obs, localState, syncState, persistOptions, newMetadata), 30);
266
+ localState.timeoutSaveMetadata = setTimeout(() => updateMetadataImmediate(obs, localState, syncState, persistOptions, newMetadata), ((_a = persistOptions === null || persistOptions === void 0 ? void 0 : persistOptions.remote) === null || _a === void 0 ? void 0 : _a.metadataTimeout) || 0);
235
267
  }
236
268
  let _queuedChanges = [];
269
+ let _queuedRemoteChanges = [];
270
+ let timeoutSaveRemote = undefined;
271
+ function mergeChanges(changes) {
272
+ const changesByPath = new Map();
273
+ const changesOut = [];
274
+ // TODO: This could be even more robust by going deeper into paths like the firebase plugin's _updatePendingSave
275
+ for (let i = 0; i < changes.length; i++) {
276
+ const change = changes[i];
277
+ const pathStr = change.path.join('/');
278
+ const existing = changesByPath.get(pathStr);
279
+ if (existing) {
280
+ existing.valueAtPath = change.valueAtPath;
281
+ }
282
+ else {
283
+ changesByPath.set(pathStr, change);
284
+ changesOut.push(change);
285
+ }
286
+ }
287
+ return changesOut;
288
+ }
289
+ function mergeQueuedChanges(allChanges) {
290
+ const changesByObs = new Map();
291
+ const out = [];
292
+ for (let i = 0; i < allChanges.length; i++) {
293
+ const value = allChanges[i];
294
+ const { obs, changes } = value;
295
+ const existing = changesByObs.get(obs);
296
+ const newChanges = existing ? [...existing, ...changes] : changes;
297
+ const merged = mergeChanges(newChanges);
298
+ changesByObs.set(obs, merged);
299
+ if (!existing) {
300
+ value.changes = merged;
301
+ out.push(value);
302
+ }
303
+ }
304
+ return out;
305
+ }
237
306
  async function processQueuedChanges() {
307
+ var _a;
238
308
  // Get a local copy of the queued changes and clear the global queue
239
- const queuedChanges = _queuedChanges;
309
+ const queuedChanges = mergeQueuedChanges(_queuedChanges);
240
310
  _queuedChanges = [];
311
+ _queuedRemoteChanges.push(...queuedChanges.filter((c) => !c.inRemoteChange));
241
312
  // Note: Summary of the order of operations these functions:
242
313
  // 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
243
314
  // We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
244
315
  // since some may take longer to transformSaveData than others.
245
- const changes = await Promise.all(queuedChanges.map(prepChange));
316
+ const preppedChangesLocal = await Promise.all(queuedChanges.map(prepChangeLocal));
246
317
  // 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
247
318
  // the current value again on next load, which isn't too bad.
248
319
  // 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
@@ -250,25 +321,43 @@ async function processQueuedChanges() {
250
321
  // 4. Wait for remote load or error if allowed
251
322
  // 5. Save to remote
252
323
  // 6. On successful save, merge changes (if any) back into observable
253
- // 7. Lastly, update metadata to clear pending and update dateModified. Doing this earlier could potentially cause
324
+ // 7. Lastly, update metadata to clear pending and update lastSync. Doing this earlier could potentially cause
254
325
  // sync inconsistences so it's very important that this is last.
255
- changes.forEach(doChange);
326
+ await Promise.all(preppedChangesLocal.map(doChangeLocal));
327
+ const timeout = (_a = observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.remoteOptions) === null || _a === void 0 ? void 0 : _a.saveTimeout;
328
+ if (timeout) {
329
+ if (timeoutSaveRemote) {
330
+ clearTimeout(timeoutSaveRemote);
331
+ }
332
+ timeoutSaveRemote = setTimeout(processQueuedRemoteChanges, timeout);
333
+ }
334
+ else {
335
+ processQueuedRemoteChanges();
336
+ }
256
337
  }
257
- async function prepChange(queuedChange) {
338
+ async function processQueuedRemoteChanges() {
339
+ const queuedRemoteChanges = mergeQueuedChanges(_queuedRemoteChanges);
340
+ _queuedRemoteChanges = [];
341
+ const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote));
342
+ preppedChangesRemote.forEach(doChangeRemote);
343
+ }
344
+ async function prepChangeLocal(queuedChange) {
258
345
  const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
259
346
  const local = persistOptions.local;
260
347
  const { persistenceRemote } = localState;
261
348
  const { config: configLocal } = parseLocalConfig(local);
262
349
  const configRemote = persistOptions.remote;
263
350
  const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
264
- const saveRemote = !inRemoteChange && (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
351
+ const saveRemote = !!(!inRemoteChange &&
352
+ (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) &&
353
+ !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) &&
354
+ syncState.isEnabledRemote.peek());
265
355
  if (saveLocal || saveRemote) {
266
356
  if (saveLocal && !syncState.isLoadedLocal.peek()) {
267
357
  console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
268
- return;
358
+ return undefined;
269
359
  }
270
360
  const changesLocal = [];
271
- const changesRemote = [];
272
361
  const changesPaths = new Set();
273
362
  let promisesTransform = [];
274
363
  // Reverse order
@@ -307,6 +396,52 @@ async function prepChange(queuedChange) {
307
396
  }
308
397
  }));
309
398
  }
399
+ }
400
+ }
401
+ // If there's any transform promises, wait for them before saving
402
+ promisesTransform = promisesTransform.filter(Boolean);
403
+ if (promisesTransform.length > 0) {
404
+ await Promise.all(promisesTransform);
405
+ }
406
+ return { queuedChange, changesLocal, saveRemote };
407
+ }
408
+ }
409
+ async function prepChangeRemote(queuedChange) {
410
+ const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
411
+ const local = persistOptions.local;
412
+ const { persistenceRemote } = localState;
413
+ const { config: configLocal } = parseLocalConfig(local);
414
+ const configRemote = persistOptions.remote;
415
+ const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
416
+ const saveRemote = !inRemoteChange && (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
417
+ if (saveLocal || saveRemote) {
418
+ if (saveLocal && !syncState.isLoadedLocal.peek()) {
419
+ console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
420
+ return undefined;
421
+ }
422
+ const changesRemote = [];
423
+ const changesPaths = new Set();
424
+ let promisesTransform = [];
425
+ // Reverse order
426
+ for (let i = changes.length - 1; i >= 0; i--) {
427
+ const { path } = changes[i];
428
+ let found = false;
429
+ // Optimization to only save the latest update at each path. We might have multiple changes at the same path
430
+ // and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
431
+ // already processed. If a later change modifies a parent of an earlier change (which happens on delete()
432
+ // it should be ignored as it's superseded by the parent modification.
433
+ if (changesPaths.size > 0) {
434
+ for (let u = 0; u < path.length; u++) {
435
+ if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
436
+ found = true;
437
+ break;
438
+ }
439
+ }
440
+ }
441
+ if (!found) {
442
+ const pathStr = path.join('/');
443
+ changesPaths.add(pathStr);
444
+ const { prevAtPath, valueAtPath, pathTypes } = changes[i];
310
445
  if (saveRemote) {
311
446
  const promiseTransformRemote = transformOutData(valueAtPath, path, pathTypes, configRemote || {});
312
447
  promisesTransform.push(doInOrder(promiseTransformRemote, ({ path: pathTransformed, value: valueTransformed }) => {
@@ -364,21 +499,20 @@ async function prepChange(queuedChange) {
364
499
  if (promisesTransform.length > 0) {
365
500
  await Promise.all(promisesTransform);
366
501
  }
367
- return { queuedChange, changesLocal, changesRemote };
502
+ return { queuedChange, changesRemote };
368
503
  }
369
504
  }
370
- async function doChange(changeInfo) {
371
- var _a, _b, _c, _d;
505
+ async function doChangeLocal(changeInfo) {
372
506
  if (!changeInfo)
373
507
  return;
374
- const { queuedChange, changesLocal, changesRemote } = changeInfo;
508
+ const { queuedChange, changesLocal, saveRemote } = changeInfo;
375
509
  const { obs, syncState, localState, persistOptions } = queuedChange;
376
- const { persistenceLocal, persistenceRemote } = localState;
510
+ const { persistenceLocal } = localState;
377
511
  const local = persistOptions.local;
378
512
  const { table, config: configLocal } = parseLocalConfig(local);
379
513
  const configRemote = persistOptions.remote;
380
514
  const shouldSaveMetadata = local && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.offlineBehavior) === 'retry';
381
- if (changesRemote.length > 0 && shouldSaveMetadata) {
515
+ if (saveRemote && shouldSaveMetadata) {
382
516
  // First save pending changes before saving local or remote
383
517
  await updateMetadataImmediate(obs, localState, syncState, persistOptions, {
384
518
  pending: localState.pendingChanges,
@@ -398,11 +532,24 @@ async function doChange(changeInfo) {
398
532
  await promiseSet;
399
533
  }
400
534
  }
535
+ }
536
+ async function doChangeRemote(changeInfo) {
537
+ var _a, _b, _c, _d;
538
+ if (!changeInfo)
539
+ return;
540
+ const { queuedChange, changesRemote } = changeInfo;
541
+ const { obs, syncState, localState, persistOptions } = queuedChange;
542
+ const { persistenceLocal, persistenceRemote } = localState;
543
+ const local = persistOptions.local;
544
+ const { table, config: configLocal } = parseLocalConfig(local);
545
+ const configRemote = persistOptions.remote;
546
+ const shouldSaveMetadata = local && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.offlineBehavior) === 'retry';
401
547
  if (changesRemote.length > 0) {
402
548
  // Wait for remote to be ready before saving
403
549
  await when(() => syncState.isLoaded.get() || ((configRemote === null || configRemote === void 0 ? void 0 : configRemote.allowSetIfError) && syncState.error.get()));
404
550
  const value = obs.peek();
405
551
  (_a = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onBeforeSet) === null || _a === void 0 ? void 0 : _a.call(configRemote);
552
+ localState.numSavesOutstanding = (localState.numSavesOutstanding || 0) + 1;
406
553
  const saved = await ((_b = persistenceRemote.set({
407
554
  obs,
408
555
  syncState: syncState,
@@ -410,17 +557,18 @@ async function doChange(changeInfo) {
410
557
  changes: changesRemote,
411
558
  value,
412
559
  })) === 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); }));
560
+ localState.numSavesOutstanding--;
413
561
  // If this remote save changed anything then update persistence and metadata
414
562
  // Because save happens after a timeout and they're batched together, some calls to save will
415
563
  // return saved data and others won't, so those can be ignored.
416
564
  if (saved) {
417
565
  const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
418
- const { changes, dateModified } = saved;
566
+ const { changes, lastSync } = saved;
419
567
  if (pathStrs.length > 0) {
420
568
  if (local) {
421
569
  const metadata = {};
422
570
  const pending = (_c = persistenceLocal.getMetadata(table, configLocal)) === null || _c === void 0 ? void 0 : _c.pending;
423
- let transformedChanges = [];
571
+ let transformedChanges = undefined;
424
572
  for (let i = 0; i < pathStrs.length; i++) {
425
573
  const pathStr = pathStrs[i];
426
574
  // Clear pending for this path
@@ -430,22 +578,34 @@ async function doChange(changeInfo) {
430
578
  metadata.pending = pending;
431
579
  }
432
580
  }
433
- if (dateModified) {
434
- metadata.modified = dateModified;
581
+ if (lastSync) {
582
+ metadata.lastSync = lastSync;
435
583
  }
436
584
  // Remote can optionally have data that needs to be merged back into the observable,
437
585
  // for example Firebase may update dateModified with the server timestamp
438
586
  if (changes && !isEmpty(changes)) {
439
- transformedChanges.push(transformLoadData(changes, persistOptions.remote, false));
587
+ transformedChanges = transformLoadData(changes, persistOptions.remote, false);
440
588
  }
441
- if (transformedChanges.length > 0) {
442
- if (transformedChanges.some((change) => isPromise(change))) {
443
- transformedChanges = await Promise.all(transformedChanges);
589
+ if (localState.numSavesOutstanding > 0) {
590
+ if (transformedChanges) {
591
+ if (!localState.pendingSaveResults) {
592
+ localState.pendingSaveResults = [];
593
+ }
594
+ localState.pendingSaveResults.push(transformedChanges);
444
595
  }
445
- onChangeRemote(() => mergeIntoObservable(obs, ...transformedChanges));
446
596
  }
447
- if (shouldSaveMetadata && !isEmpty(metadata)) {
448
- updateMetadata(obs, localState, syncState, persistOptions, metadata);
597
+ else {
598
+ let allChanges = [...(localState.pendingSaveResults || []), transformedChanges];
599
+ if (allChanges.length > 0) {
600
+ if (allChanges.some((change) => isPromise(change))) {
601
+ allChanges = await Promise.all(allChanges);
602
+ }
603
+ onChangeRemote(() => mergeIntoObservable(obs, ...allChanges));
604
+ }
605
+ if (shouldSaveMetadata && !isEmpty(metadata)) {
606
+ updateMetadata(obs, localState, syncState, persistOptions, metadata);
607
+ }
608
+ localState.pendingSaveResults = [];
449
609
  }
450
610
  }
451
611
  (_d = configRemote === null || configRemote === void 0 ? void 0 : configRemote.onSet) === null || _d === void 0 ? void 0 : _d.call(configRemote);
@@ -496,7 +656,7 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
496
656
  }
497
657
  const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
498
658
  localState.persistenceLocal = persistenceLocal;
499
- if (!initialized.get()) {
659
+ if (!initialized.peek()) {
500
660
  await when(initialized);
501
661
  }
502
662
  // If persistence has an asynchronous load, wait for it
@@ -510,12 +670,21 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
510
670
  let value = persistenceLocal.getTable(table, config);
511
671
  const metadata = persistenceLocal.getMetadata(table, config);
512
672
  if (metadata) {
673
+ // @ts-expect-error Migration from old version
674
+ if (!metadata.lastSync && metadata.modified) {
675
+ // @ts-expect-error Migration from old
676
+ metadata.lastSync = metadata.modified;
677
+ }
513
678
  metadatas.set(obs, metadata);
514
679
  localState.pendingChanges = metadata.pending;
515
- syncState.dateModified.set(metadata.modified);
680
+ // TODOV3 Remove dateModified
681
+ syncState.assign({
682
+ dateModified: metadata.lastSync,
683
+ lastSync: metadata.lastSync,
684
+ });
516
685
  }
517
686
  // Merge the data from local persistence into the default state
518
- if (value !== null && value !== undefined) {
687
+ if (value !== undefined) {
519
688
  const { transform, fieldTransforms } = config;
520
689
  value = transformLoadData(value, { transform, fieldTransforms }, true);
521
690
  if (isPromise(value)) {
@@ -526,7 +695,12 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
526
695
  // are set on the same observable
527
696
  internal$1.globalState.isLoadingLocal = true;
528
697
  // We want to merge the local data on top of any initial state the object is created with
529
- mergeIntoObservable(obs, value);
698
+ if (value === null && !obs.peek()) {
699
+ obs.set(value);
700
+ }
701
+ else {
702
+ mergeIntoObservable(obs, value);
703
+ }
530
704
  }, () => {
531
705
  internal$1.globalState.isLoadingLocal = false;
532
706
  });
@@ -539,18 +713,11 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
539
713
  }
540
714
  syncState.isLoadedLocal.set(true);
541
715
  }
542
- function persistObservable(initialOrObservable, persistOptions) {
543
- var _a;
544
- const obs = (isObservable(initialOrObservable)
545
- ? initialOrObservable
546
- : observable(isFunction(initialOrObservable) ? initialOrObservable() : initialOrObservable));
716
+ function persistObservable(obs, persistOptions) {
547
717
  const node = getNode(obs);
548
- if (process.env.NODE_ENV === 'development' && ((_a = obs === null || obs === void 0 ? void 0 : obs.peek()) === null || _a === void 0 ? void 0 : _a._state)) {
549
- console.warn('[legend-state] WARNING: persistObservable creates a property named "_state" but your observable already has "state" in it');
550
- }
551
718
  // Merge remote persist options with clobal options
552
719
  if (persistOptions.remote) {
553
- persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, persistOptions.remote);
720
+ persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, removeNullUndefined(persistOptions.remote));
554
721
  }
555
722
  let { remote } = persistOptions;
556
723
  const { local } = persistOptions;
@@ -562,6 +729,7 @@ function persistObservable(initialOrObservable, persistOptions) {
562
729
  isLoaded: false,
563
730
  isEnabledLocal: true,
564
731
  isEnabledRemote: true,
732
+ refreshNum: 0,
565
733
  clearLocal: undefined,
566
734
  sync: () => Promise.resolve(),
567
735
  getPendingChanges: () => localState.pendingChanges,
@@ -592,73 +760,93 @@ function persistObservable(initialOrObservable, persistOptions) {
592
760
  var _a, _b;
593
761
  if (!isSynced) {
594
762
  isSynced = true;
595
- const dateModified = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.modified;
763
+ const lastSync = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.lastSync;
764
+ const pending = localState.pendingChanges;
596
765
  const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
597
766
  if (get) {
598
- get({
599
- state: syncState,
600
- obs,
601
- options: persistOptions,
602
- dateModified,
603
- onGet: () => {
604
- syncState.isLoaded.set(true);
605
- },
606
- onChange: async ({ value, path = [], pathTypes = [], mode = 'set', dateModified }) => {
607
- // Note: value is the constructed value, path is used for setInObservableAtPath
608
- // to start the set into the observable from the path
609
- if (value !== undefined) {
610
- value = transformLoadData(value, remote, true);
611
- if (isPromise(value)) {
612
- value = await value;
613
- }
614
- const invertedMap = remote.fieldTransforms && invertFieldMap(remote.fieldTransforms);
615
- if (path.length && invertedMap) {
616
- path = transformPath(path, pathTypes, invertedMap);
617
- }
618
- if (mode === 'dateModified') {
619
- if (dateModified && !isEmpty(value)) {
767
+ const runGet = () => {
768
+ get({
769
+ state: syncState,
770
+ obs,
771
+ options: persistOptions,
772
+ lastSync,
773
+ dateModified: lastSync,
774
+ onError: (error) => {
775
+ var _a;
776
+ (_a = remote.onGetError) === null || _a === void 0 ? void 0 : _a.call(remote, error);
777
+ },
778
+ onGet: () => {
779
+ node.state.assign({
780
+ isLoaded: true,
781
+ error: undefined,
782
+ });
783
+ },
784
+ onChange: async ({ value, path = [], pathTypes = [], mode = 'set', lastSync }) => {
785
+ // Note: value is the constructed value, path is used for setInObservableAtPath
786
+ // to start the set into the observable from the path
787
+ if (value !== undefined) {
788
+ value = transformLoadData(value, remote, true);
789
+ if (isPromise(value)) {
790
+ value = await value;
791
+ }
792
+ const invertedMap = remote.fieldTransforms && invertFieldMap(remote.fieldTransforms);
793
+ if (path.length && invertedMap) {
794
+ path = transformPath(path, pathTypes, invertedMap);
795
+ }
796
+ if (mode === 'lastSync' || mode === 'dateModified') {
797
+ if (lastSync && !isEmpty(value)) {
798
+ onChangeRemote(() => {
799
+ setInObservableAtPath(
800
+ // @ts-expect-error Fix this type
801
+ obs, path, pathTypes, value, 'assign');
802
+ });
803
+ }
804
+ }
805
+ else {
806
+ const pending = localState.pendingChanges;
807
+ if (pending) {
808
+ Object.keys(pending).forEach((key) => {
809
+ const p = key.split('/').filter((p) => p !== '');
810
+ const { v, t } = pending[key];
811
+ if (t.length === 0) {
812
+ value = v;
813
+ }
814
+ else if (value[p[0]] !== undefined) {
815
+ value = setAtPath(value, p, t, v, obs.peek(), (path, value) => {
816
+ delete pending[key];
817
+ pending[path.join('/')] = {
818
+ p: null,
819
+ v: value,
820
+ t: t.slice(0, path.length),
821
+ };
822
+ });
823
+ }
824
+ });
825
+ }
620
826
  onChangeRemote(() => {
621
- setInObservableAtPath(obs, path, pathTypes, value, 'assign');
827
+ // @ts-expect-error Fix this type
828
+ setInObservableAtPath(obs, path, pathTypes, value, mode);
622
829
  });
623
830
  }
624
831
  }
625
- else {
626
- const pending = localState.pendingChanges;
627
- if (pending) {
628
- Object.keys(pending).forEach((key) => {
629
- const p = key.split('/').filter((p) => p !== '');
630
- const { v, t } = pending[key];
631
- if (value[p[0]] !== undefined) {
632
- value = setAtPath(value, p, t, v, obs.peek(), (path, value) => {
633
- delete pending[key];
634
- pending[path.join('/')] = {
635
- p: null,
636
- v: value,
637
- t: t.slice(0, path.length),
638
- };
639
- });
640
- }
641
- });
642
- }
643
- onChangeRemote(() => {
644
- setInObservableAtPath(obs, path, pathTypes, value, mode);
832
+ if (lastSync && local) {
833
+ updateMetadata(obs, localState, syncState, persistOptions, {
834
+ lastSync,
645
835
  });
646
836
  }
647
- }
648
- if (dateModified && local) {
649
- updateMetadata(obs, localState, syncState, persistOptions, {
650
- modified: dateModified,
651
- });
652
- }
653
- },
654
- });
837
+ },
838
+ });
839
+ };
840
+ runGet();
655
841
  }
656
842
  else {
657
- syncState.isLoaded.set(true);
843
+ node.state.assign({
844
+ isLoaded: true,
845
+ error: undefined,
846
+ });
658
847
  }
659
848
  // Wait for remote to be ready before saving pending
660
849
  await when(() => syncState.isLoaded.get() || (remote.allowSetIfError && syncState.error.get()));
661
- const pending = localState.pendingChanges;
662
850
  if (pending && !isEmpty(pending)) {
663
851
  localState.isApplyingPending = true;
664
852
  const keys = Object.keys(pending);
@@ -671,6 +859,7 @@ function persistObservable(initialOrObservable, persistOptions) {
671
859
  changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
672
860
  }
673
861
  // Send the changes into onObsChange so that they get persisted remotely
862
+ // @ts-expect-error Fix this type
674
863
  onObsChange(obs, syncState, localState, persistOptions, {
675
864
  value: obs.peek(),
676
865
  // TODO getPrevious if any remote persistence layers need it
@@ -706,44 +895,122 @@ function persistObservable(initialOrObservable, persistOptions) {
706
895
  }
707
896
  obs.onChange(onObsChange.bind(this, obs, syncState, localState, persistOptions));
708
897
  });
709
- return obs;
898
+ return syncState;
710
899
  }
711
- globalState.activateNode = function activateNodePersist(node, newValue, setter, subscriber, cacheOptions, lastSync) {
712
- let onChange = undefined;
713
- const pluginRemote = {
714
- get: async (params) => {
715
- onChange = params.onChange;
716
- if (isPromise(newValue)) {
717
- newValue = await newValue;
900
+
901
+ const { getProxy, globalState, runWithRetry, symbolActivated } = internal$1;
902
+ function persistActivateNode() {
903
+ globalState.activateNode = function activateNodePersist(node, refresh, wasPromise, newValue) {
904
+ if (node.activationState) {
905
+ const { get, initial, onSet, subscribe, cache, retry, offlineBehavior } = node.activationState;
906
+ let onChange = undefined;
907
+ const pluginRemote = {};
908
+ if (get) {
909
+ pluginRemote.get = (params) => {
910
+ onChange = params.onChange;
911
+ const updateLastSync = (lastSync) => (params.lastSync = lastSync);
912
+ const setMode = (mode) => (params.mode = mode);
913
+ const nodeValue = getNodeValue(node);
914
+ const value = runWithRetry(node, { attemptNum: 0 }, () => {
915
+ return get({
916
+ value: isFunction(nodeValue) || (nodeValue === null || nodeValue === void 0 ? void 0 : nodeValue[symbolActivated]) ? undefined : nodeValue,
917
+ lastSync: params.lastSync,
918
+ updateLastSync,
919
+ setMode,
920
+ });
921
+ });
922
+ return value;
923
+ };
924
+ }
925
+ if (onSet) {
926
+ // TODO: Work out these types better
927
+ pluginRemote.set = async (params) => {
928
+ var _a;
929
+ if ((_a = node.state) === null || _a === void 0 ? void 0 : _a.isLoaded.get()) {
930
+ return runWithRetry(node, { attemptNum: 0 }, async () => {
931
+ let changes = {};
932
+ let maxModified = 0;
933
+ let didError = false;
934
+ if (!node.state.isLoaded.peek()) {
935
+ await whenReady(node.state.isLoaded);
936
+ }
937
+ await onSet({
938
+ ...params,
939
+ node,
940
+ update: (params) => {
941
+ const { value, lastSync } = params;
942
+ maxModified = Math.max(lastSync || 0, maxModified);
943
+ changes = mergeIntoObservable(changes, value);
944
+ },
945
+ onError: () => {
946
+ // TODO Is this necessary?
947
+ didError = true;
948
+ throw new Error();
949
+ },
950
+ refresh,
951
+ fromSubscribe: false,
952
+ });
953
+ if (!didError) {
954
+ return { changes, lastSync: maxModified || undefined };
955
+ }
956
+ });
957
+ }
958
+ };
959
+ }
960
+ if (subscribe) {
961
+ subscribe({
962
+ node,
963
+ update: (params) => {
964
+ if (!onChange) {
965
+ // TODO: Make this message better
966
+ console.log('[legend-state] Cannot update immediately before the first return');
967
+ }
968
+ else {
969
+ onChange(params);
970
+ }
971
+ },
972
+ refresh,
973
+ });
974
+ }
975
+ persistObservable(getProxy(node), {
976
+ pluginRemote,
977
+ ...(cache || {}),
978
+ remote: {
979
+ retry: retry,
980
+ offlineBehavior,
981
+ },
982
+ });
983
+ const nodeVal = getNodeValue(node);
984
+ if (nodeVal !== undefined) {
985
+ newValue = nodeVal;
718
986
  }
719
- if (lastSync.value) {
720
- params.dateModified = lastSync.value;
987
+ else if (newValue === undefined) {
988
+ newValue = initial;
721
989
  }
722
- return newValue;
723
- },
990
+ return { update: onChange, value: newValue };
991
+ }
992
+ else {
993
+ let onChange = undefined;
994
+ const pluginRemote = {
995
+ get: async (params) => {
996
+ onChange = params.onChange;
997
+ if (isPromise(newValue)) {
998
+ try {
999
+ newValue = await newValue;
1000
+ // eslint-disable-next-line no-empty
1001
+ }
1002
+ catch (_a) { }
1003
+ }
1004
+ return newValue;
1005
+ },
1006
+ };
1007
+ persistObservable(getProxy(node), {
1008
+ pluginRemote,
1009
+ });
1010
+ return { update: onChange, value: newValue };
1011
+ }
724
1012
  };
725
- if (setter) {
726
- pluginRemote.set = setter;
727
- }
728
- if (subscriber) {
729
- subscriber({
730
- update: (params) => {
731
- if (!onChange) {
732
- // TODO: Make this message better
733
- console.log('[legend-state] Cannot update immediately before the first return');
734
- }
735
- else {
736
- onChange(params);
737
- }
738
- },
739
- });
740
- }
741
- persistObservable(getProxy(node), {
742
- pluginRemote,
743
- ...(cacheOptions || {}),
744
- });
745
- return { update: onChange };
746
- };
1013
+ }
747
1014
 
748
1015
  function isInRemoteChange() {
749
1016
  return internal$1.globalState.isLoadingRemote$.get();
@@ -751,6 +1018,7 @@ function isInRemoteChange() {
751
1018
  const internal = {
752
1019
  observablePersistConfiguration,
753
1020
  };
1021
+ persistActivateNode();
754
1022
 
755
1023
  export { configureObservablePersistence, internal, invertFieldMap, isInRemoteChange, mapPersistences, persistObservable, transformObject, transformPath };
756
1024
  //# sourceMappingURL=persist.mjs.map