@legendapp/state 2.2.0-next.3 → 2.2.0-next.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/babel.js.map +1 -1
- package/config/enableDirectAccess.d.ts +1 -1
- package/config/enableDirectPeek.d.ts +1 -1
- package/config/enableReactDirectRender.js.map +1 -1
- package/config/enableReactDirectRender.mjs.map +1 -1
- package/config/enableReactTracking.d.ts +4 -3
- package/config/enableReactTracking.js.map +1 -1
- package/config/enableReactTracking.mjs.map +1 -1
- package/config/enableReactUse.d.ts +1 -1
- package/helpers/fetch.d.ts +4 -3
- package/helpers/fetch.js.map +1 -1
- package/helpers/fetch.mjs.map +1 -1
- package/helpers/pageHash.js.map +1 -1
- package/helpers/pageHash.mjs.map +1 -1
- package/helpers/pageHashParams.js.map +1 -1
- package/helpers/pageHashParams.mjs.map +1 -1
- package/helpers/time.d.ts +2 -2
- package/helpers/time.js.map +1 -1
- package/helpers/time.mjs.map +1 -1
- package/history.js.map +1 -1
- package/history.mjs.map +1 -1
- package/index.d.ts +15 -4
- package/index.js +534 -242
- package/index.js.map +1 -1
- package/index.mjs +533 -243
- package/index.mjs.map +1 -1
- package/package.json +2 -10
- package/persist-plugins/async-storage.js.map +1 -1
- package/persist-plugins/async-storage.mjs.map +1 -1
- package/persist-plugins/fetch.js.map +1 -1
- package/persist-plugins/fetch.mjs.map +1 -1
- package/persist-plugins/firebase.js.map +1 -1
- package/persist-plugins/firebase.mjs.map +1 -1
- package/persist-plugins/indexeddb.js.map +1 -1
- package/persist-plugins/indexeddb.mjs.map +1 -1
- package/persist-plugins/local-storage.js +10 -2
- package/persist-plugins/local-storage.js.map +1 -1
- package/persist-plugins/local-storage.mjs +10 -2
- package/persist-plugins/local-storage.mjs.map +1 -1
- package/persist-plugins/mmkv.js.map +1 -1
- package/persist-plugins/mmkv.mjs.map +1 -1
- package/persist-plugins/query.js.map +1 -1
- package/persist-plugins/query.mjs.map +1 -1
- package/persist.d.ts +15 -1
- package/persist.js +363 -116
- package/persist.js.map +1 -1
- package/persist.mjs +364 -117
- package/persist.mjs.map +1 -1
- package/react-hooks/createObservableHook.js +1 -1
- package/react-hooks/createObservableHook.js.map +1 -1
- package/react-hooks/createObservableHook.mjs +1 -1
- package/react-hooks/createObservableHook.mjs.map +1 -1
- package/react-hooks/useFetch.d.ts +4 -3
- package/react-hooks/useFetch.js.map +1 -1
- package/react-hooks/useFetch.mjs.map +1 -1
- package/react-hooks/useHover.js.map +1 -1
- package/react-hooks/useHover.mjs.map +1 -1
- package/react-hooks/useMeasure.js.map +1 -1
- package/react-hooks/useMeasure.mjs.map +1 -1
- package/react-hooks/useObservableNextRouter.js.map +1 -1
- package/react-hooks/useObservableNextRouter.mjs.map +1 -1
- package/react-hooks/useObservableQuery.js.map +1 -1
- package/react-hooks/useObservableQuery.mjs.map +1 -1
- package/react-hooks/usePersistedObservable.d.ts +2 -2
- package/react-hooks/usePersistedObservable.js +3 -3
- package/react-hooks/usePersistedObservable.js.map +1 -1
- package/react-hooks/usePersistedObservable.mjs +3 -3
- package/react-hooks/usePersistedObservable.mjs.map +1 -1
- package/react.js +13 -8
- package/react.js.map +1 -1
- package/react.mjs +14 -9
- package/react.mjs.map +1 -1
- package/src/ObservableObject.d.ts +8 -4
- package/src/ObservablePrimitive.d.ts +2 -1
- package/src/activated.d.ts +3 -0
- package/src/batching.d.ts +3 -1
- package/src/computed.d.ts +1 -1
- package/src/config/enableDirectAccess.d.ts +1 -1
- package/src/config/enableDirectPeek.d.ts +1 -1
- package/src/config/enableReactTracking.d.ts +4 -3
- package/src/config/enableReactUse.d.ts +1 -1
- package/src/createObservable.d.ts +2 -2
- package/src/globals.d.ts +11 -4
- package/src/helpers/fetch.d.ts +4 -3
- package/src/helpers/time.d.ts +2 -2
- package/src/helpers.d.ts +3 -2
- package/src/history/trackHistory.d.ts +1 -1
- package/src/observable.d.ts +6 -15
- package/src/observableInterfaces.d.ts +60 -331
- package/src/observableTypes.d.ts +93 -0
- package/src/persist/observablePersistRemoteFunctionsAdapter.d.ts +1 -1
- package/src/persist/persistActivateNode.d.ts +1 -0
- package/src/persist/persistHelpers.d.ts +1 -1
- package/src/persist/persistObservable.d.ts +3 -12
- package/src/persistTypes.d.ts +229 -0
- package/src/proxy.d.ts +2 -1
- package/src/react/Computed.d.ts +1 -1
- package/src/react/Switch.d.ts +3 -3
- package/src/react/reactInterfaces.d.ts +2 -1
- package/src/react/usePauseProvider.d.ts +3 -3
- package/src/react/useWhen.d.ts +2 -2
- package/src/react-hooks/useFetch.d.ts +4 -3
- package/src/react-hooks/usePersistedObservable.d.ts +2 -2
- package/src/retry.d.ts +6 -0
- package/src/trackSelector.d.ts +3 -2
- package/src/when.d.ts +6 -2
- package/trace.js.map +1 -1
- 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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
165
|
+
function removeNullUndefined(val) {
|
|
166
|
+
if (val) {
|
|
167
|
+
Object.keys(val).forEach((key) => {
|
|
168
|
+
const v = val[key];
|
|
169
|
+
if (v === null || v === undefined) {
|
|
170
|
+
delete val[key];
|
|
171
|
+
}
|
|
172
|
+
else if (state.isObject(v)) {
|
|
173
|
+
removeNullUndefined(v);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return val;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { globalState: globalState$1 } = state.internal;
|
|
154
181
|
const mapPersistences = new WeakMap();
|
|
155
182
|
const metadatas = new WeakMap();
|
|
156
183
|
const promisesLocalSaves = new Set();
|
|
@@ -165,11 +192,12 @@ function doInOrder(arg1, arg2) {
|
|
|
165
192
|
return state.isPromise(arg1) ? arg1.then(arg2) : arg2(arg1);
|
|
166
193
|
}
|
|
167
194
|
function onChangeRemote(cb) {
|
|
168
|
-
state.when(() => !globalState.isLoadingRemote$.get(), () => {
|
|
195
|
+
state.when(() => !globalState$1.isLoadingRemote$.get(), () => {
|
|
196
|
+
state.endBatch(true);
|
|
169
197
|
// Remote changes should only update local state
|
|
170
|
-
globalState.isLoadingRemote$.set(true);
|
|
198
|
+
globalState$1.isLoadingRemote$.set(true);
|
|
171
199
|
state.batch(cb, () => {
|
|
172
|
-
globalState.isLoadingRemote$.set(false);
|
|
200
|
+
globalState$1.isLoadingRemote$.set(false);
|
|
173
201
|
});
|
|
174
202
|
});
|
|
175
203
|
}
|
|
@@ -216,16 +244,19 @@ async function updateMetadataImmediate(obs, localState, syncState, persistOption
|
|
|
216
244
|
const { table, config } = parseLocalConfig(local);
|
|
217
245
|
// Save metadata
|
|
218
246
|
const oldMetadata = metadatas.get(obs);
|
|
219
|
-
const {
|
|
220
|
-
const needsUpdate = pending || (
|
|
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 (
|
|
228
|
-
syncState.
|
|
255
|
+
if (lastSync) {
|
|
256
|
+
syncState.assign({
|
|
257
|
+
lastSync: lastSync,
|
|
258
|
+
dateModified: lastSync,
|
|
259
|
+
});
|
|
229
260
|
}
|
|
230
261
|
}
|
|
231
262
|
}
|
|
@@ -237,15 +268,51 @@ function updateMetadata(obs, localState, syncState, persistOptions, newMetadata)
|
|
|
237
268
|
localState.timeoutSaveMetadata = setTimeout(() => updateMetadataImmediate(obs, localState, syncState, persistOptions, newMetadata), ((_a = persistOptions === null || persistOptions === void 0 ? void 0 : persistOptions.remote) === null || _a === void 0 ? void 0 : _a.metadataTimeout) || 0);
|
|
238
269
|
}
|
|
239
270
|
let _queuedChanges = [];
|
|
271
|
+
let _queuedRemoteChanges = [];
|
|
272
|
+
let timeoutSaveRemote = undefined;
|
|
273
|
+
function mergeChanges(changes) {
|
|
274
|
+
const changesByPath = new Map();
|
|
275
|
+
const changesOut = [];
|
|
276
|
+
// TODO: This could be even more robust by going deeper into paths like the firebase plugin's _updatePendingSave
|
|
277
|
+
for (let i = 0; i < changes.length; i++) {
|
|
278
|
+
const change = changes[i];
|
|
279
|
+
const pathStr = change.path.join('/');
|
|
280
|
+
const existing = changesByPath.get(pathStr);
|
|
281
|
+
if (existing) {
|
|
282
|
+
existing.valueAtPath = change.valueAtPath;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
changesByPath.set(pathStr, change);
|
|
286
|
+
changesOut.push(change);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return changesOut;
|
|
290
|
+
}
|
|
291
|
+
function mergeQueuedChanges(allChanges) {
|
|
292
|
+
const changesByObs = new Map();
|
|
293
|
+
const out = new Map();
|
|
294
|
+
for (let i = 0; i < allChanges.length; i++) {
|
|
295
|
+
const value = allChanges[i];
|
|
296
|
+
const { obs, changes } = value;
|
|
297
|
+
const existing = changesByObs.get(obs);
|
|
298
|
+
const newChanges = existing ? [...existing, ...changes] : changes;
|
|
299
|
+
const merged = mergeChanges(newChanges);
|
|
300
|
+
changesByObs.set(obs, merged);
|
|
301
|
+
value.changes = merged;
|
|
302
|
+
out.set(obs, value);
|
|
303
|
+
}
|
|
304
|
+
return Array.from(out.values());
|
|
305
|
+
}
|
|
240
306
|
async function processQueuedChanges() {
|
|
307
|
+
var _a;
|
|
241
308
|
// Get a local copy of the queued changes and clear the global queue
|
|
242
|
-
const queuedChanges = _queuedChanges;
|
|
309
|
+
const queuedChanges = mergeQueuedChanges(_queuedChanges);
|
|
243
310
|
_queuedChanges = [];
|
|
311
|
+
_queuedRemoteChanges.push(...queuedChanges.filter((c) => !c.inRemoteChange));
|
|
244
312
|
// Note: Summary of the order of operations these functions:
|
|
245
313
|
// 1. Prepare all changes for saving. This may involve waiting for promises if the user has asynchronous transform.
|
|
246
314
|
// We need to prepare all of the changes in the queue before saving so that the saves happen in the correct order,
|
|
247
315
|
// since some may take longer to transformSaveData than others.
|
|
248
|
-
const changes = await Promise.all(queuedChanges.map(prepChange));
|
|
249
316
|
// 2. Save pending to the metadata table first. If this is the only operation that succeeds, it would try to save
|
|
250
317
|
// the current value again on next load, which isn't too bad.
|
|
251
318
|
// 3. Save local changes to storage. If they never make it to remote, then on the next load they will be pending
|
|
@@ -253,25 +320,46 @@ async function processQueuedChanges() {
|
|
|
253
320
|
// 4. Wait for remote load or error if allowed
|
|
254
321
|
// 5. Save to remote
|
|
255
322
|
// 6. On successful save, merge changes (if any) back into observable
|
|
256
|
-
// 7. Lastly, update metadata to clear pending and update
|
|
323
|
+
// 7. Lastly, update metadata to clear pending and update lastSync. Doing this earlier could potentially cause
|
|
257
324
|
// sync inconsistences so it's very important that this is last.
|
|
258
|
-
|
|
325
|
+
const preppedChangesLocal = await Promise.all(queuedChanges.map(prepChangeLocal));
|
|
326
|
+
// TODO Clean this up: We only need to prep this now in ordre to save pending changes, don't need any of the other stuff. Should split that up?
|
|
327
|
+
await Promise.all(queuedChanges.map(prepChangeRemote));
|
|
328
|
+
await Promise.all(preppedChangesLocal.map(doChangeLocal));
|
|
329
|
+
const timeout = (_a = observablePersistConfiguration === null || observablePersistConfiguration === void 0 ? void 0 : observablePersistConfiguration.remoteOptions) === null || _a === void 0 ? void 0 : _a.saveTimeout;
|
|
330
|
+
if (timeout) {
|
|
331
|
+
if (timeoutSaveRemote) {
|
|
332
|
+
clearTimeout(timeoutSaveRemote);
|
|
333
|
+
}
|
|
334
|
+
timeoutSaveRemote = setTimeout(processQueuedRemoteChanges, timeout);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
processQueuedRemoteChanges();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function processQueuedRemoteChanges() {
|
|
341
|
+
const queuedRemoteChanges = mergeQueuedChanges(_queuedRemoteChanges);
|
|
342
|
+
_queuedRemoteChanges = [];
|
|
343
|
+
const preppedChangesRemote = await Promise.all(queuedRemoteChanges.map(prepChangeRemote));
|
|
344
|
+
preppedChangesRemote.forEach(doChangeRemote);
|
|
259
345
|
}
|
|
260
|
-
async function
|
|
346
|
+
async function prepChangeLocal(queuedChange) {
|
|
261
347
|
const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
|
|
262
348
|
const local = persistOptions.local;
|
|
263
349
|
const { persistenceRemote } = localState;
|
|
264
350
|
const { config: configLocal } = parseLocalConfig(local);
|
|
265
351
|
const configRemote = persistOptions.remote;
|
|
266
352
|
const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
|
|
267
|
-
const saveRemote = !inRemoteChange &&
|
|
353
|
+
const saveRemote = !!(!inRemoteChange &&
|
|
354
|
+
(persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) &&
|
|
355
|
+
!(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) &&
|
|
356
|
+
syncState.isEnabledRemote.peek());
|
|
268
357
|
if (saveLocal || saveRemote) {
|
|
269
358
|
if (saveLocal && !syncState.isLoadedLocal.peek()) {
|
|
270
359
|
console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
|
|
271
|
-
return;
|
|
360
|
+
return undefined;
|
|
272
361
|
}
|
|
273
362
|
const changesLocal = [];
|
|
274
|
-
const changesRemote = [];
|
|
275
363
|
const changesPaths = new Set();
|
|
276
364
|
let promisesTransform = [];
|
|
277
365
|
// Reverse order
|
|
@@ -310,6 +398,52 @@ async function prepChange(queuedChange) {
|
|
|
310
398
|
}
|
|
311
399
|
}));
|
|
312
400
|
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// If there's any transform promises, wait for them before saving
|
|
404
|
+
promisesTransform = promisesTransform.filter(Boolean);
|
|
405
|
+
if (promisesTransform.length > 0) {
|
|
406
|
+
await Promise.all(promisesTransform);
|
|
407
|
+
}
|
|
408
|
+
return { queuedChange, changesLocal, saveRemote };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function prepChangeRemote(queuedChange) {
|
|
412
|
+
const { syncState, changes, localState, persistOptions, inRemoteChange, isApplyingPending } = queuedChange;
|
|
413
|
+
const local = persistOptions.local;
|
|
414
|
+
const { persistenceRemote } = localState;
|
|
415
|
+
const { config: configLocal } = parseLocalConfig(local);
|
|
416
|
+
const configRemote = persistOptions.remote;
|
|
417
|
+
const saveLocal = local && !configLocal.readonly && !isApplyingPending && syncState.isEnabledLocal.peek();
|
|
418
|
+
const saveRemote = !inRemoteChange && (persistenceRemote === null || persistenceRemote === void 0 ? void 0 : persistenceRemote.set) && !(configRemote === null || configRemote === void 0 ? void 0 : configRemote.readonly) && syncState.isEnabledRemote.peek();
|
|
419
|
+
if (saveLocal || saveRemote) {
|
|
420
|
+
if (saveLocal && !syncState.isLoadedLocal.peek()) {
|
|
421
|
+
console.error('[legend-state] WARNING: An observable was changed before being loaded from persistence', local);
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
const changesRemote = [];
|
|
425
|
+
const changesPaths = new Set();
|
|
426
|
+
let promisesTransform = [];
|
|
427
|
+
// Reverse order
|
|
428
|
+
for (let i = changes.length - 1; i >= 0; i--) {
|
|
429
|
+
const { path } = changes[i];
|
|
430
|
+
let found = false;
|
|
431
|
+
// Optimization to only save the latest update at each path. We might have multiple changes at the same path
|
|
432
|
+
// and we only need the latest value, so it starts from the end of the array, skipping any earlier changes
|
|
433
|
+
// already processed. If a later change modifies a parent of an earlier change (which happens on delete()
|
|
434
|
+
// it should be ignored as it's superseded by the parent modification.
|
|
435
|
+
if (changesPaths.size > 0) {
|
|
436
|
+
for (let u = 0; u < path.length; u++) {
|
|
437
|
+
if (changesPaths.has((u === path.length - 1 ? path : path.slice(0, u + 1)).join('/'))) {
|
|
438
|
+
found = true;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (!found) {
|
|
444
|
+
const pathStr = path.join('/');
|
|
445
|
+
changesPaths.add(pathStr);
|
|
446
|
+
const { prevAtPath, valueAtPath, pathTypes } = changes[i];
|
|
313
447
|
if (saveRemote) {
|
|
314
448
|
const promiseTransformRemote = transformOutData(valueAtPath, path, pathTypes, configRemote || {});
|
|
315
449
|
promisesTransform.push(doInOrder(promiseTransformRemote, ({ path: pathTransformed, value: valueTransformed }) => {
|
|
@@ -367,21 +501,20 @@ async function prepChange(queuedChange) {
|
|
|
367
501
|
if (promisesTransform.length > 0) {
|
|
368
502
|
await Promise.all(promisesTransform);
|
|
369
503
|
}
|
|
370
|
-
return { queuedChange,
|
|
504
|
+
return { queuedChange, changesRemote };
|
|
371
505
|
}
|
|
372
506
|
}
|
|
373
|
-
async function
|
|
374
|
-
var _a, _b, _c, _d;
|
|
507
|
+
async function doChangeLocal(changeInfo) {
|
|
375
508
|
if (!changeInfo)
|
|
376
509
|
return;
|
|
377
|
-
const { queuedChange, changesLocal,
|
|
510
|
+
const { queuedChange, changesLocal, saveRemote } = changeInfo;
|
|
378
511
|
const { obs, syncState, localState, persistOptions } = queuedChange;
|
|
379
|
-
const { persistenceLocal
|
|
512
|
+
const { persistenceLocal } = localState;
|
|
380
513
|
const local = persistOptions.local;
|
|
381
514
|
const { table, config: configLocal } = parseLocalConfig(local);
|
|
382
515
|
const configRemote = persistOptions.remote;
|
|
383
516
|
const shouldSaveMetadata = local && (configRemote === null || configRemote === void 0 ? void 0 : configRemote.offlineBehavior) === 'retry';
|
|
384
|
-
if (
|
|
517
|
+
if (saveRemote && shouldSaveMetadata) {
|
|
385
518
|
// First save pending changes before saving local or remote
|
|
386
519
|
await updateMetadataImmediate(obs, localState, syncState, persistOptions, {
|
|
387
520
|
pending: localState.pendingChanges,
|
|
@@ -401,29 +534,46 @@ async function doChange(changeInfo) {
|
|
|
401
534
|
await promiseSet;
|
|
402
535
|
}
|
|
403
536
|
}
|
|
537
|
+
}
|
|
538
|
+
async function doChangeRemote(changeInfo) {
|
|
539
|
+
var _a, _b;
|
|
540
|
+
if (!changeInfo)
|
|
541
|
+
return;
|
|
542
|
+
const { queuedChange, changesRemote } = changeInfo;
|
|
543
|
+
const { obs, syncState, localState, persistOptions } = queuedChange;
|
|
544
|
+
const { persistenceLocal, persistenceRemote } = localState;
|
|
545
|
+
const local = persistOptions.local;
|
|
546
|
+
const { table, config: configLocal } = parseLocalConfig(local);
|
|
547
|
+
const { offlineBehavior, allowSetIfError, onBeforeSet, onSetError, waitForSet, onSet } = persistOptions.remote || {};
|
|
548
|
+
const shouldSaveMetadata = local && offlineBehavior === 'retry';
|
|
404
549
|
if (changesRemote.length > 0) {
|
|
405
550
|
// Wait for remote to be ready before saving
|
|
406
|
-
await state.when(() => syncState.isLoaded.get() || (
|
|
551
|
+
await state.when(() => syncState.isLoaded.get() || (allowSetIfError && syncState.error.get()));
|
|
552
|
+
if (waitForSet) {
|
|
553
|
+
await state.when(state.isFunction(waitForSet) ? waitForSet(changesRemote) : waitForSet);
|
|
554
|
+
}
|
|
407
555
|
const value = obs.peek();
|
|
408
|
-
|
|
409
|
-
|
|
556
|
+
onBeforeSet === null || onBeforeSet === void 0 ? void 0 : onBeforeSet();
|
|
557
|
+
localState.numSavesOutstanding = (localState.numSavesOutstanding || 0) + 1;
|
|
558
|
+
const saved = await ((_a = persistenceRemote.set({
|
|
410
559
|
obs,
|
|
411
560
|
syncState: syncState,
|
|
412
561
|
options: persistOptions,
|
|
413
562
|
changes: changesRemote,
|
|
414
563
|
value,
|
|
415
|
-
})) === null ||
|
|
564
|
+
})) === null || _a === void 0 ? void 0 : _a.catch((err) => onSetError === null || onSetError === void 0 ? void 0 : onSetError(err)));
|
|
565
|
+
localState.numSavesOutstanding--;
|
|
416
566
|
// If this remote save changed anything then update persistence and metadata
|
|
417
567
|
// Because save happens after a timeout and they're batched together, some calls to save will
|
|
418
568
|
// return saved data and others won't, so those can be ignored.
|
|
419
569
|
if (saved) {
|
|
420
570
|
const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
|
|
421
|
-
const { changes,
|
|
571
|
+
const { changes, lastSync } = saved;
|
|
422
572
|
if (pathStrs.length > 0) {
|
|
423
573
|
if (local) {
|
|
424
574
|
const metadata = {};
|
|
425
|
-
const pending = (
|
|
426
|
-
let transformedChanges =
|
|
575
|
+
const pending = (_b = persistenceLocal.getMetadata(table, configLocal)) === null || _b === void 0 ? void 0 : _b.pending;
|
|
576
|
+
let transformedChanges = undefined;
|
|
427
577
|
for (let i = 0; i < pathStrs.length; i++) {
|
|
428
578
|
const pathStr = pathStrs[i];
|
|
429
579
|
// Clear pending for this path
|
|
@@ -433,25 +583,37 @@ async function doChange(changeInfo) {
|
|
|
433
583
|
metadata.pending = pending;
|
|
434
584
|
}
|
|
435
585
|
}
|
|
436
|
-
if (
|
|
437
|
-
metadata.
|
|
586
|
+
if (lastSync) {
|
|
587
|
+
metadata.lastSync = lastSync;
|
|
438
588
|
}
|
|
439
589
|
// Remote can optionally have data that needs to be merged back into the observable,
|
|
440
590
|
// for example Firebase may update dateModified with the server timestamp
|
|
441
591
|
if (changes && !state.isEmpty(changes)) {
|
|
442
|
-
transformedChanges
|
|
592
|
+
transformedChanges = transformLoadData(changes, persistOptions.remote, false);
|
|
443
593
|
}
|
|
444
|
-
if (
|
|
445
|
-
if (transformedChanges
|
|
446
|
-
|
|
594
|
+
if (localState.numSavesOutstanding > 0) {
|
|
595
|
+
if (transformedChanges) {
|
|
596
|
+
if (!localState.pendingSaveResults) {
|
|
597
|
+
localState.pendingSaveResults = [];
|
|
598
|
+
}
|
|
599
|
+
localState.pendingSaveResults.push(transformedChanges);
|
|
447
600
|
}
|
|
448
|
-
onChangeRemote(() => state.mergeIntoObservable(obs, ...transformedChanges));
|
|
449
601
|
}
|
|
450
|
-
|
|
451
|
-
|
|
602
|
+
else {
|
|
603
|
+
let allChanges = [...(localState.pendingSaveResults || []), transformedChanges];
|
|
604
|
+
if (allChanges.length > 0) {
|
|
605
|
+
if (allChanges.some((change) => state.isPromise(change))) {
|
|
606
|
+
allChanges = await Promise.all(allChanges);
|
|
607
|
+
}
|
|
608
|
+
onChangeRemote(() => state.mergeIntoObservable(obs, ...allChanges));
|
|
609
|
+
}
|
|
610
|
+
if (shouldSaveMetadata && !state.isEmpty(metadata)) {
|
|
611
|
+
updateMetadata(obs, localState, syncState, persistOptions, metadata);
|
|
612
|
+
}
|
|
613
|
+
localState.pendingSaveResults = [];
|
|
452
614
|
}
|
|
453
615
|
}
|
|
454
|
-
|
|
616
|
+
onSet === null || onSet === void 0 ? void 0 : onSet();
|
|
455
617
|
}
|
|
456
618
|
}
|
|
457
619
|
}
|
|
@@ -499,7 +661,7 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
|
|
|
499
661
|
}
|
|
500
662
|
const { persist: persistenceLocal, initialized } = mapPersistences.get(localPersistence);
|
|
501
663
|
localState.persistenceLocal = persistenceLocal;
|
|
502
|
-
if (!initialized.
|
|
664
|
+
if (!initialized.peek()) {
|
|
503
665
|
await state.when(initialized);
|
|
504
666
|
}
|
|
505
667
|
// If persistence has an asynchronous load, wait for it
|
|
@@ -513,12 +675,21 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
|
|
|
513
675
|
let value = persistenceLocal.getTable(table, config);
|
|
514
676
|
const metadata = persistenceLocal.getMetadata(table, config);
|
|
515
677
|
if (metadata) {
|
|
678
|
+
// @ts-expect-error Migration from old version
|
|
679
|
+
if (!metadata.lastSync && metadata.modified) {
|
|
680
|
+
// @ts-expect-error Migration from old
|
|
681
|
+
metadata.lastSync = metadata.modified;
|
|
682
|
+
}
|
|
516
683
|
metadatas.set(obs, metadata);
|
|
517
684
|
localState.pendingChanges = metadata.pending;
|
|
518
|
-
|
|
685
|
+
// TODOV3 Remove dateModified
|
|
686
|
+
syncState.assign({
|
|
687
|
+
dateModified: metadata.lastSync,
|
|
688
|
+
lastSync: metadata.lastSync,
|
|
689
|
+
});
|
|
519
690
|
}
|
|
520
691
|
// Merge the data from local persistence into the default state
|
|
521
|
-
if (value !==
|
|
692
|
+
if (value !== undefined) {
|
|
522
693
|
const { transform, fieldTransforms } = config;
|
|
523
694
|
value = transformLoadData(value, { transform, fieldTransforms }, true);
|
|
524
695
|
if (state.isPromise(value)) {
|
|
@@ -529,7 +700,12 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
|
|
|
529
700
|
// are set on the same observable
|
|
530
701
|
state.internal.globalState.isLoadingLocal = true;
|
|
531
702
|
// We want to merge the local data on top of any initial state the object is created with
|
|
532
|
-
|
|
703
|
+
if (value === null && !obs.peek()) {
|
|
704
|
+
obs.set(value);
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
state.mergeIntoObservable(obs, value);
|
|
708
|
+
}
|
|
533
709
|
}, () => {
|
|
534
710
|
state.internal.globalState.isLoadingLocal = false;
|
|
535
711
|
});
|
|
@@ -542,18 +718,11 @@ async function loadLocal(obs, persistOptions, syncState, localState) {
|
|
|
542
718
|
}
|
|
543
719
|
syncState.isLoadedLocal.set(true);
|
|
544
720
|
}
|
|
545
|
-
function persistObservable(
|
|
546
|
-
var _a;
|
|
547
|
-
const obs = (state.isObservable(initialOrObservable)
|
|
548
|
-
? initialOrObservable
|
|
549
|
-
: state.observable(state.isFunction(initialOrObservable) ? initialOrObservable() : initialOrObservable));
|
|
721
|
+
function persistObservable(obs, persistOptions) {
|
|
550
722
|
const node = state.getNode(obs);
|
|
551
|
-
if (process.env.NODE_ENV === 'development' && ((_a = obs === null || obs === void 0 ? void 0 : obs.peek()) === null || _a === void 0 ? void 0 : _a._state)) {
|
|
552
|
-
console.warn('[legend-state] WARNING: persistObservable creates a property named "_state" but your observable already has "state" in it');
|
|
553
|
-
}
|
|
554
723
|
// Merge remote persist options with clobal options
|
|
555
724
|
if (persistOptions.remote) {
|
|
556
|
-
persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, persistOptions.remote);
|
|
725
|
+
persistOptions.remote = Object.assign({}, observablePersistConfiguration.remoteOptions, removeNullUndefined(persistOptions.remote));
|
|
557
726
|
}
|
|
558
727
|
let { remote } = persistOptions;
|
|
559
728
|
const { local } = persistOptions;
|
|
@@ -596,33 +765,28 @@ function persistObservable(initialOrObservable, persistOptions) {
|
|
|
596
765
|
var _a, _b;
|
|
597
766
|
if (!isSynced) {
|
|
598
767
|
isSynced = true;
|
|
599
|
-
const
|
|
768
|
+
const lastSync = (_a = metadatas.get(obs)) === null || _a === void 0 ? void 0 : _a.lastSync;
|
|
769
|
+
const pending = localState.pendingChanges;
|
|
600
770
|
const get = (_b = localState.persistenceRemote.get) === null || _b === void 0 ? void 0 : _b.bind(localState.persistenceRemote);
|
|
601
771
|
if (get) {
|
|
602
|
-
let attemptNum = 0;
|
|
603
772
|
const runGet = () => {
|
|
604
773
|
get({
|
|
605
774
|
state: syncState,
|
|
606
775
|
obs,
|
|
607
776
|
options: persistOptions,
|
|
608
|
-
|
|
777
|
+
lastSync,
|
|
778
|
+
dateModified: lastSync,
|
|
609
779
|
onError: (error) => {
|
|
610
780
|
var _a;
|
|
611
|
-
if (remote.retry) {
|
|
612
|
-
const { backoff, delay = 1000, infinite, times = 3, maxDelay = 30000, } = remote.retry;
|
|
613
|
-
if (infinite || attemptNum++ < times) {
|
|
614
|
-
const delayTime = Math.min(delay * (backoff === 'constant' ? 1 : 2 ** attemptNum), maxDelay);
|
|
615
|
-
setTimeout(runGet, delayTime);
|
|
616
|
-
// Don't error when retrying
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
781
|
(_a = remote.onGetError) === null || _a === void 0 ? void 0 : _a.call(remote, error);
|
|
621
782
|
},
|
|
622
783
|
onGet: () => {
|
|
623
|
-
|
|
784
|
+
node.state.assign({
|
|
785
|
+
isLoaded: true,
|
|
786
|
+
error: undefined,
|
|
787
|
+
});
|
|
624
788
|
},
|
|
625
|
-
onChange: async ({ value, path = [], pathTypes = [], mode = 'set',
|
|
789
|
+
onChange: async ({ value, path = [], pathTypes = [], mode = 'set', lastSync }) => {
|
|
626
790
|
// Note: value is the constructed value, path is used for setInObservableAtPath
|
|
627
791
|
// to start the set into the observable from the path
|
|
628
792
|
if (value !== undefined) {
|
|
@@ -634,10 +798,12 @@ function persistObservable(initialOrObservable, persistOptions) {
|
|
|
634
798
|
if (path.length && invertedMap) {
|
|
635
799
|
path = transformPath(path, pathTypes, invertedMap);
|
|
636
800
|
}
|
|
637
|
-
if (mode === 'dateModified') {
|
|
638
|
-
if (
|
|
801
|
+
if (mode === 'lastSync' || mode === 'dateModified') {
|
|
802
|
+
if (lastSync && !state.isEmpty(value)) {
|
|
639
803
|
onChangeRemote(() => {
|
|
640
|
-
state.setInObservableAtPath(
|
|
804
|
+
state.setInObservableAtPath(
|
|
805
|
+
// @ts-expect-error Fix this type
|
|
806
|
+
obs, path, pathTypes, value, 'assign');
|
|
641
807
|
});
|
|
642
808
|
}
|
|
643
809
|
}
|
|
@@ -647,7 +813,10 @@ function persistObservable(initialOrObservable, persistOptions) {
|
|
|
647
813
|
Object.keys(pending).forEach((key) => {
|
|
648
814
|
const p = key.split('/').filter((p) => p !== '');
|
|
649
815
|
const { v, t } = pending[key];
|
|
650
|
-
if (
|
|
816
|
+
if (t.length === 0 || !value) {
|
|
817
|
+
value = v;
|
|
818
|
+
}
|
|
819
|
+
else if (value[p[0]] !== undefined) {
|
|
651
820
|
value = state.setAtPath(value, p, t, v, obs.peek(), (path, value) => {
|
|
652
821
|
delete pending[key];
|
|
653
822
|
pending[path.join('/')] = {
|
|
@@ -660,13 +829,14 @@ function persistObservable(initialOrObservable, persistOptions) {
|
|
|
660
829
|
});
|
|
661
830
|
}
|
|
662
831
|
onChangeRemote(() => {
|
|
832
|
+
// @ts-expect-error Fix this type
|
|
663
833
|
state.setInObservableAtPath(obs, path, pathTypes, value, mode);
|
|
664
834
|
});
|
|
665
835
|
}
|
|
666
836
|
}
|
|
667
|
-
if (
|
|
837
|
+
if (lastSync && local) {
|
|
668
838
|
updateMetadata(obs, localState, syncState, persistOptions, {
|
|
669
|
-
|
|
839
|
+
lastSync,
|
|
670
840
|
});
|
|
671
841
|
}
|
|
672
842
|
},
|
|
@@ -675,11 +845,13 @@ function persistObservable(initialOrObservable, persistOptions) {
|
|
|
675
845
|
runGet();
|
|
676
846
|
}
|
|
677
847
|
else {
|
|
678
|
-
|
|
848
|
+
node.state.assign({
|
|
849
|
+
isLoaded: true,
|
|
850
|
+
error: undefined,
|
|
851
|
+
});
|
|
679
852
|
}
|
|
680
853
|
// Wait for remote to be ready before saving pending
|
|
681
854
|
await state.when(() => syncState.isLoaded.get() || (remote.allowSetIfError && syncState.error.get()));
|
|
682
|
-
const pending = localState.pendingChanges;
|
|
683
855
|
if (pending && !state.isEmpty(pending)) {
|
|
684
856
|
localState.isApplyingPending = true;
|
|
685
857
|
const keys = Object.keys(pending);
|
|
@@ -692,6 +864,7 @@ function persistObservable(initialOrObservable, persistOptions) {
|
|
|
692
864
|
changes.push({ path, valueAtPath: v, prevAtPath: p, pathTypes: t });
|
|
693
865
|
}
|
|
694
866
|
// Send the changes into onObsChange so that they get persisted remotely
|
|
867
|
+
// @ts-expect-error Fix this type
|
|
695
868
|
onObsChange(obs, syncState, localState, persistOptions, {
|
|
696
869
|
value: obs.peek(),
|
|
697
870
|
// TODO getPrevious if any remote persistence layers need it
|
|
@@ -727,49 +900,122 @@ function persistObservable(initialOrObservable, persistOptions) {
|
|
|
727
900
|
}
|
|
728
901
|
obs.onChange(onObsChange.bind(this, obs, syncState, localState, persistOptions));
|
|
729
902
|
});
|
|
730
|
-
return
|
|
903
|
+
return syncState;
|
|
731
904
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
905
|
+
|
|
906
|
+
const { getProxy, globalState, runWithRetry, symbolActivated } = state.internal;
|
|
907
|
+
function persistActivateNode() {
|
|
908
|
+
globalState.activateNode = function activateNodePersist(node, refresh, wasPromise, newValue) {
|
|
909
|
+
if (node.activationState) {
|
|
910
|
+
const { get, initial, onSet, subscribe, cache, retry, offlineBehavior, waitForSet } = node.activationState;
|
|
911
|
+
let onChange = undefined;
|
|
912
|
+
const pluginRemote = {};
|
|
913
|
+
if (get) {
|
|
914
|
+
pluginRemote.get = (params) => {
|
|
915
|
+
onChange = params.onChange;
|
|
916
|
+
const updateLastSync = (lastSync) => (params.lastSync = lastSync);
|
|
917
|
+
const setMode = (mode) => (params.mode = mode);
|
|
918
|
+
const nodeValue = state.getNodeValue(node);
|
|
919
|
+
const value = runWithRetry(node, { attemptNum: 0 }, () => {
|
|
920
|
+
return get({
|
|
921
|
+
value: state.isFunction(nodeValue) || (nodeValue === null || nodeValue === void 0 ? void 0 : nodeValue[symbolActivated]) ? undefined : nodeValue,
|
|
922
|
+
lastSync: params.lastSync,
|
|
923
|
+
updateLastSync,
|
|
924
|
+
setMode,
|
|
925
|
+
refresh,
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
return value;
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
if (onSet) {
|
|
932
|
+
// TODO: Work out these types better
|
|
933
|
+
pluginRemote.set = async (params) => {
|
|
934
|
+
var _a;
|
|
935
|
+
if ((_a = node.state) === null || _a === void 0 ? void 0 : _a.isLoaded.get()) {
|
|
936
|
+
const retryAttempts = { attemptNum: 0 };
|
|
937
|
+
return runWithRetry(node, retryAttempts, async (retryEvent) => {
|
|
938
|
+
let changes = {};
|
|
939
|
+
let maxModified = 0;
|
|
940
|
+
if (!node.state.isLoaded.peek()) {
|
|
941
|
+
await state.whenReady(node.state.isLoaded);
|
|
942
|
+
}
|
|
943
|
+
const cancelRetry = () => {
|
|
944
|
+
retryEvent.cancel = true;
|
|
945
|
+
};
|
|
946
|
+
await onSet({
|
|
947
|
+
...params,
|
|
948
|
+
node,
|
|
949
|
+
update: (params) => {
|
|
950
|
+
const { value, lastSync } = params;
|
|
951
|
+
maxModified = Math.max(lastSync || 0, maxModified);
|
|
952
|
+
changes = state.mergeIntoObservable(changes, value);
|
|
953
|
+
},
|
|
954
|
+
retryNum: retryAttempts.attemptNum,
|
|
955
|
+
cancelRetry,
|
|
956
|
+
refresh,
|
|
957
|
+
fromSubscribe: false,
|
|
958
|
+
});
|
|
959
|
+
return { changes, lastSync: maxModified || undefined };
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (subscribe) {
|
|
965
|
+
subscribe({
|
|
966
|
+
node,
|
|
967
|
+
update: (params) => {
|
|
968
|
+
if (!onChange) {
|
|
969
|
+
// TODO: Make this message better
|
|
970
|
+
console.log('[legend-state] Cannot update immediately before the first return');
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
onChange(params);
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
refresh,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
persistObservable(getProxy(node), {
|
|
980
|
+
pluginRemote,
|
|
981
|
+
...(cache || {}),
|
|
982
|
+
remote: {
|
|
983
|
+
retry: retry,
|
|
984
|
+
offlineBehavior,
|
|
985
|
+
waitForSet,
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
const nodeVal = state.getNodeValue(node);
|
|
989
|
+
if (nodeVal !== undefined) {
|
|
990
|
+
newValue = nodeVal;
|
|
740
991
|
}
|
|
741
|
-
if (
|
|
742
|
-
|
|
992
|
+
else if (newValue === undefined) {
|
|
993
|
+
newValue = initial;
|
|
743
994
|
}
|
|
744
|
-
return newValue;
|
|
745
|
-
}
|
|
995
|
+
return { update: onChange, value: newValue };
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
let onChange = undefined;
|
|
999
|
+
const pluginRemote = {
|
|
1000
|
+
get: async (params) => {
|
|
1001
|
+
onChange = params.onChange;
|
|
1002
|
+
if (state.isPromise(newValue)) {
|
|
1003
|
+
try {
|
|
1004
|
+
newValue = await newValue;
|
|
1005
|
+
// eslint-disable-next-line no-empty
|
|
1006
|
+
}
|
|
1007
|
+
catch (_a) { }
|
|
1008
|
+
}
|
|
1009
|
+
return newValue;
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
persistObservable(getProxy(node), {
|
|
1013
|
+
pluginRemote,
|
|
1014
|
+
});
|
|
1015
|
+
return { update: onChange, value: newValue };
|
|
1016
|
+
}
|
|
746
1017
|
};
|
|
747
|
-
|
|
748
|
-
// TODO: Work out these types better
|
|
749
|
-
pluginRemote.set = onSetFn;
|
|
750
|
-
}
|
|
751
|
-
if (subscriber) {
|
|
752
|
-
subscriber({
|
|
753
|
-
update: (params) => {
|
|
754
|
-
if (!onChange) {
|
|
755
|
-
// TODO: Make this message better
|
|
756
|
-
console.log('[legend-state] Cannot update immediately before the first return');
|
|
757
|
-
}
|
|
758
|
-
else {
|
|
759
|
-
onChange(params);
|
|
760
|
-
}
|
|
761
|
-
},
|
|
762
|
-
refresh,
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
persistObservable(getProxy(node), {
|
|
766
|
-
pluginRemote,
|
|
767
|
-
...(cacheOptions || {}),
|
|
768
|
-
remote: {
|
|
769
|
-
retry: retryOptions,
|
|
770
|
-
},
|
|
771
|
-
});
|
|
772
|
-
};
|
|
1018
|
+
}
|
|
773
1019
|
|
|
774
1020
|
function isInRemoteChange() {
|
|
775
1021
|
return state.internal.globalState.isLoadingRemote$.get();
|
|
@@ -777,6 +1023,7 @@ function isInRemoteChange() {
|
|
|
777
1023
|
const internal = {
|
|
778
1024
|
observablePersistConfiguration,
|
|
779
1025
|
};
|
|
1026
|
+
persistActivateNode();
|
|
780
1027
|
|
|
781
1028
|
exports.configureObservablePersistence = configureObservablePersistence;
|
|
782
1029
|
exports.internal = internal;
|