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