@legendapp/state 2.2.0-next.73 → 2.2.0-next.75
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/index.d.ts +1 -1
- package/index.js +79 -26
- package/index.js.map +1 -1
- package/index.mjs +78 -27
- package/index.mjs.map +1 -1
- package/package.json +6 -1
- package/persist.js +49 -44
- package/persist.js.map +1 -1
- package/persist.mjs +49 -44
- package/persist.mjs.map +1 -1
- package/react.js +5 -5
- package/react.js.map +1 -1
- package/react.mjs +6 -6
- package/react.mjs.map +1 -1
- package/src/ObservableObject.ts +34 -15
- package/src/batching.ts +7 -3
- package/src/globals.ts +16 -6
- package/src/helpers.ts +5 -2
- package/src/is.ts +7 -0
- package/src/observableTypes.ts +5 -0
- package/src/react/For.tsx +6 -6
- package/src/sync/activateSyncedNode.ts +7 -5
- package/src/sync/syncHelpers.ts +53 -12
- package/src/sync/syncObservable.ts +37 -30
- package/src/sync-plugins/crud.ts +373 -0
- package/src/sync-plugins/fetch.ts +1 -1
- package/src/syncTypes.ts +2 -2
- package/src/when.ts +6 -1
- package/sync-plugins/crud.d.ts +39 -0
- package/sync-plugins/crud.js +267 -0
- package/sync-plugins/crud.js.map +1 -0
- package/sync-plugins/crud.mjs +263 -0
- package/sync-plugins/crud.mjs.map +1 -0
- package/sync-plugins/fetch.js +2 -2
- package/sync-plugins/fetch.js.map +1 -1
- package/sync-plugins/fetch.mjs +1 -1
- package/sync.d.ts +1 -0
- package/sync.js +84 -42
- package/sync.js.map +1 -1
- package/sync.mjs +83 -44
- package/sync.mjs.map +1 -1
package/src/is.ts
CHANGED
|
@@ -30,6 +30,13 @@ export function isBoolean(obj: unknown): obj is boolean {
|
|
|
30
30
|
export function isPromise<T>(obj: unknown): obj is Promise<T> {
|
|
31
31
|
return obj instanceof Promise;
|
|
32
32
|
}
|
|
33
|
+
export function isMap(obj: unknown): obj is Map<any, any> {
|
|
34
|
+
return obj instanceof Map;
|
|
35
|
+
}
|
|
36
|
+
export function isNumber(obj: unknown): obj is number {
|
|
37
|
+
const n = obj as number;
|
|
38
|
+
return n - n < 1;
|
|
39
|
+
}
|
|
33
40
|
export function isEmpty(obj: object): boolean {
|
|
34
41
|
// Looping and returning false on the first property is faster than Object.keys(obj).length === 0
|
|
35
42
|
// https://jsbench.me/qfkqv692c8
|
package/src/observableTypes.ts
CHANGED
|
@@ -41,11 +41,16 @@ interface ObservableObjectFns<T> {
|
|
|
41
41
|
|
|
42
42
|
interface ObservableObjectFunctions<T = Record<string, any>> extends ObservablePrimitive<T>, ObservableObjectFns<T> {}
|
|
43
43
|
|
|
44
|
+
type MapKey<T extends Map<any, any> | WeakMap<any, any>> = Parameters<T['has']>[0];
|
|
45
|
+
type MapValue<T extends Map<any, any> | WeakMap<any, any>> = Parameters<T['get']>[0];
|
|
44
46
|
type ObservableMap<T extends Map<any, any> | WeakMap<any, any>> = Omit<T, 'get' | 'size'> &
|
|
45
47
|
Omit<ObservablePrimitive<T>, 'get' | 'size'> & {
|
|
46
48
|
get(key: Parameters<T['get']>[0]): Observable<Parameters<T['set']>[1]>;
|
|
47
49
|
get(): T;
|
|
48
50
|
size: ImmutableObservableBase<number>;
|
|
51
|
+
assign(
|
|
52
|
+
value: Record<MapKey<T>, MapValue<T>> | Map<MapKey<T>, MapValue<T>> | WeakMap<MapKey<T>, MapValue<T>>,
|
|
53
|
+
): Observable<T>;
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
type ObservableSet<T extends Set<any> | WeakSet<any>> = Omit<T, 'size'> &
|
package/src/react/For.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Observable, ObservableObject, ObservableParam } from '@legendapp/state';
|
|
2
|
-
import { internal, isArray, isFunction } from '@legendapp/state';
|
|
2
|
+
import { internal, isArray, isFunction, isMap } from '@legendapp/state';
|
|
3
3
|
import { FC, ReactElement, createElement, memo, useMemo, useRef } from 'react';
|
|
4
4
|
import { observer } from './reactive-observer';
|
|
5
5
|
import { useSelector } from './useSelector';
|
|
@@ -92,15 +92,15 @@ export function For<T, TProps>({
|
|
|
92
92
|
}
|
|
93
93
|
} else {
|
|
94
94
|
// Render the values of the object / Map
|
|
95
|
-
const
|
|
96
|
-
const keys =
|
|
95
|
+
const asMap = isMap(value);
|
|
96
|
+
const keys = asMap ? Array.from(value.keys()) : Object.keys(value);
|
|
97
97
|
if (sortValues) {
|
|
98
|
-
keys.sort((A, B) => sortValues(
|
|
98
|
+
keys.sort((A, B) => sortValues(asMap ? value.get(A)! : value[A], asMap ? value.get(B)! : value[B], A, B));
|
|
99
99
|
}
|
|
100
100
|
for (let i = 0; i < keys.length; i++) {
|
|
101
101
|
const key = keys[i];
|
|
102
|
-
if (
|
|
103
|
-
const item$ =
|
|
102
|
+
if (asMap ? value.get(key) : value[key]) {
|
|
103
|
+
const item$ = asMap ? each!.get(key) : (each as ObservableObject<Record<string, any>>)[key];
|
|
104
104
|
const props: ForItemProps<any> & { key: string; item: Observable<any> } = {
|
|
105
105
|
key,
|
|
106
106
|
id: key,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
GetMode,
|
|
3
2
|
NodeValue,
|
|
4
3
|
Observable,
|
|
5
4
|
ObservableOnChangeParams,
|
|
@@ -35,18 +34,21 @@ export function enableActivateSyncedNode() {
|
|
|
35
34
|
pluginRemote.get = (params: ObservableSyncGetParams<any>) => {
|
|
36
35
|
onChange = params.onChange;
|
|
37
36
|
const updateLastSync = (lastSync: number) => (params.lastSync = lastSync);
|
|
38
|
-
const setMode = (mode: GetMode) => (params.mode = mode);
|
|
39
37
|
|
|
40
38
|
const existingValue = getNodeValue(node);
|
|
41
39
|
const value = runWithRetry(node, { attemptNum: 0 }, () => {
|
|
42
|
-
|
|
40
|
+
const paramsToGet = {
|
|
43
41
|
value:
|
|
44
42
|
isFunction(existingValue) || existingValue?.[symbolLinked] ? undefined : existingValue,
|
|
45
43
|
lastSync: params.lastSync!,
|
|
46
44
|
updateLastSync,
|
|
47
|
-
|
|
45
|
+
mode: params.mode!,
|
|
48
46
|
refresh,
|
|
49
|
-
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ret = get!(paramsToGet);
|
|
50
|
+
params.mode = paramsToGet.mode;
|
|
51
|
+
return ret;
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
promiseReturn = value;
|
package/src/sync/syncHelpers.ts
CHANGED
|
@@ -1,15 +1,56 @@
|
|
|
1
|
-
import { isObject } from '@legendapp/state';
|
|
2
|
-
|
|
3
|
-
export function removeNullUndefined<T extends Record<string, any>>(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { isDate, isNullOrUndefined, isObject } from '@legendapp/state';
|
|
2
|
+
|
|
3
|
+
export function removeNullUndefined<T extends Record<string, any>>(a: T, recursive?: boolean): T {
|
|
4
|
+
const out: T = {} as T;
|
|
5
|
+
Object.keys(a).forEach((key: keyof T) => {
|
|
6
|
+
if (a[key] !== null && a[key] !== undefined) {
|
|
7
|
+
out[key] = recursive && isObject(a[key]) ? removeNullUndefined(a[key]) : a[key];
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function diffObjects<T extends Record<string, any>>(obj1: T, obj2: T, deep: boolean = false): Partial<T> {
|
|
15
|
+
const diff: Partial<T> = {};
|
|
16
|
+
if (!obj1) return obj2 || diff;
|
|
17
|
+
if (!obj2) return obj1 || diff;
|
|
18
|
+
|
|
19
|
+
const keys = new Set<keyof T>([...Object.keys(obj1), ...Object.keys(obj2)] as (keyof T)[]);
|
|
20
|
+
|
|
21
|
+
keys.forEach((key) => {
|
|
22
|
+
const o1 = obj1[key];
|
|
23
|
+
const o2 = obj2[key];
|
|
24
|
+
if (deep ? !deepEqual(o1, o2) : o1 !== o2) {
|
|
25
|
+
if (!isDate(o1) || !isDate(o2) || o1.getTime() !== o2.getTime()) {
|
|
26
|
+
diff[key] = o2;
|
|
11
27
|
}
|
|
12
|
-
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return diff;
|
|
32
|
+
}
|
|
33
|
+
export function deepEqual<T extends Record<string, any> = any>(
|
|
34
|
+
a: T,
|
|
35
|
+
b: T,
|
|
36
|
+
ignoreFields?: string[],
|
|
37
|
+
nullVsUndefined?: boolean,
|
|
38
|
+
): boolean {
|
|
39
|
+
if (a === b) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (isNullOrUndefined(a) !== isNullOrUndefined(b)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (nullVsUndefined) {
|
|
47
|
+
a = removeNullUndefined(a, /*recursive*/ true);
|
|
48
|
+
b = removeNullUndefined(b, /*recursive*/ true);
|
|
13
49
|
}
|
|
14
|
-
|
|
50
|
+
|
|
51
|
+
const replacer = ignoreFields
|
|
52
|
+
? (key: string, value: any) => (ignoreFields.includes(key) ? undefined : value)
|
|
53
|
+
: undefined;
|
|
54
|
+
|
|
55
|
+
return JSON.stringify(a, replacer) === JSON.stringify(b, replacer);
|
|
15
56
|
}
|
|
@@ -207,7 +207,12 @@ function mergeChanges(changes: Change[]) {
|
|
|
207
207
|
const pathStr = change.path.join('/');
|
|
208
208
|
const existing = changesByPath.get(pathStr);
|
|
209
209
|
if (existing) {
|
|
210
|
-
|
|
210
|
+
// If setting a value back to what it was, no need to save it
|
|
211
|
+
if (change.valueAtPath === existing.prevAtPath) {
|
|
212
|
+
changesOut.splice(changesOut.indexOf(change), 1);
|
|
213
|
+
} else {
|
|
214
|
+
existing.valueAtPath = change.valueAtPath;
|
|
215
|
+
}
|
|
211
216
|
} else {
|
|
212
217
|
changesByPath.set(pathStr, change);
|
|
213
218
|
changesOut.push(change);
|
|
@@ -226,6 +231,7 @@ function mergeQueuedChanges(allChanges: QueuedChange[]) {
|
|
|
226
231
|
for (let i = 0; i < allChanges.length; i++) {
|
|
227
232
|
const value = allChanges[i];
|
|
228
233
|
const { obs, changes, inRemoteChange, getPrevious } = value;
|
|
234
|
+
const targetMap = inRemoteChange ? outRemote : outLocal;
|
|
229
235
|
const changesMap = inRemoteChange ? changesByObsRemote : changesByObsLocal;
|
|
230
236
|
const existing = changesMap.get(obs);
|
|
231
237
|
const newChanges = existing ? [...existing, ...changes] : changes;
|
|
@@ -236,7 +242,7 @@ function mergeQueuedChanges(allChanges: QueuedChange[]) {
|
|
|
236
242
|
previousByObs.set(obs, getPrevious());
|
|
237
243
|
}
|
|
238
244
|
value.valuePrevious = previousByObs.get(obs);
|
|
239
|
-
|
|
245
|
+
targetMap.set(obs, value);
|
|
240
246
|
}
|
|
241
247
|
return Array.from(outRemote.values()).concat(Array.from(outLocal.values()));
|
|
242
248
|
}
|
|
@@ -532,8 +538,7 @@ async function doChangeLocal(changeInfo: PreppedChangeLocal | undefined) {
|
|
|
532
538
|
|
|
533
539
|
const persist = syncOptions.persist;
|
|
534
540
|
const { table, config: configLocal } = parseLocalConfig(persist!);
|
|
535
|
-
const
|
|
536
|
-
const shouldSaveMetadata = persist && configRemote?.offlineBehavior === 'retry';
|
|
541
|
+
const shouldSaveMetadata = persist?.retrySync;
|
|
537
542
|
|
|
538
543
|
if (saveRemote && shouldSaveMetadata) {
|
|
539
544
|
// First save pending changes before saving local or remote
|
|
@@ -568,9 +573,9 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) {
|
|
|
568
573
|
|
|
569
574
|
const persist = syncOptions.persist;
|
|
570
575
|
const { table, config: configLocal } = parseLocalConfig(persist!);
|
|
571
|
-
const {
|
|
576
|
+
const { allowSetIfGetError, onBeforeSet, onSetError, waitForSet, onAfterSet } =
|
|
572
577
|
syncOptions || ({} as SyncedOptions);
|
|
573
|
-
const shouldSaveMetadata = persist
|
|
578
|
+
const shouldSaveMetadata = persist?.retrySync;
|
|
574
579
|
|
|
575
580
|
if (changesRemote.length > 0) {
|
|
576
581
|
// Wait for remote to be ready before saving
|
|
@@ -618,11 +623,11 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) {
|
|
|
618
623
|
const pathStrs = Array.from(new Set(changesRemote.map((change) => change.pathStr)));
|
|
619
624
|
const { changes, lastSync } = saved;
|
|
620
625
|
if (pathStrs.length > 0) {
|
|
626
|
+
let transformedChanges: object | undefined = undefined;
|
|
627
|
+
const metadata: PersistMetadata = {};
|
|
621
628
|
if (persist) {
|
|
622
|
-
const metadata: PersistMetadata = {};
|
|
623
629
|
const pendingMetadata = pluginPersist!.getMetadata(table, configLocal)?.pending;
|
|
624
630
|
const pending = localState.pendingChanges;
|
|
625
|
-
let transformedChanges: object | undefined = undefined;
|
|
626
631
|
|
|
627
632
|
for (let i = 0; i < pathStrs.length; i++) {
|
|
628
633
|
const pathStr = pathStrs[i];
|
|
@@ -643,37 +648,39 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) {
|
|
|
643
648
|
if (lastSync) {
|
|
644
649
|
metadata.lastSync = lastSync;
|
|
645
650
|
}
|
|
651
|
+
}
|
|
646
652
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
653
|
+
// Remote can optionally have data that needs to be merged back into the observable,
|
|
654
|
+
// for example Firebase may update dateModified with the server timestamp
|
|
655
|
+
if (changes && !isEmpty(changes)) {
|
|
656
|
+
transformedChanges = transformLoadData(changes, syncOptions, false);
|
|
657
|
+
}
|
|
652
658
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
localState.pendingSaveResults.push(transformedChanges);
|
|
659
|
+
if (localState.numSavesOutstanding > 0) {
|
|
660
|
+
if (transformedChanges) {
|
|
661
|
+
if (!localState.pendingSaveResults) {
|
|
662
|
+
localState.pendingSaveResults = [];
|
|
659
663
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
664
|
+
localState.pendingSaveResults.push(transformedChanges);
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
667
|
+
let allChanges = [...(localState.pendingSaveResults || []), transformedChanges].filter(
|
|
668
|
+
(v) => v !== undefined,
|
|
669
|
+
);
|
|
670
|
+
if (allChanges.length > 0) {
|
|
671
|
+
if (allChanges.some((change) => isPromise(change))) {
|
|
672
|
+
allChanges = await Promise.all(allChanges);
|
|
669
673
|
}
|
|
674
|
+
onChangeRemote(() => mergeIntoObservable(obs, ...allChanges));
|
|
675
|
+
}
|
|
670
676
|
|
|
677
|
+
if (persist) {
|
|
671
678
|
if (shouldSaveMetadata && !isEmpty(metadata)) {
|
|
672
679
|
updateMetadata(obs, localState, syncState, syncOptions, metadata);
|
|
673
680
|
}
|
|
674
|
-
|
|
675
|
-
localState.pendingSaveResults = [];
|
|
676
681
|
}
|
|
682
|
+
|
|
683
|
+
localState.pendingSaveResults = [];
|
|
677
684
|
}
|
|
678
685
|
onAfterSet?.();
|
|
679
686
|
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SyncTransform,
|
|
3
|
+
SyncedGetParams,
|
|
4
|
+
SyncedOptions,
|
|
5
|
+
SyncedSetParams,
|
|
6
|
+
internal,
|
|
7
|
+
isArray,
|
|
8
|
+
isNullOrUndefined,
|
|
9
|
+
isNumber,
|
|
10
|
+
isObject,
|
|
11
|
+
isString,
|
|
12
|
+
} from '@legendapp/state';
|
|
13
|
+
import { synced, diffObjects } from '@legendapp/state/sync';
|
|
14
|
+
|
|
15
|
+
const { clone } = internal;
|
|
16
|
+
|
|
17
|
+
export type CrudAsOption = 'Map' | 'object' | 'first' | 'array';
|
|
18
|
+
|
|
19
|
+
export type CrudResult<T> = T;
|
|
20
|
+
|
|
21
|
+
export interface SyncedCrudPropsSingle<TGet> {
|
|
22
|
+
get?: (params: SyncedGetParams) => Promise<CrudResult<TGet | null>> | CrudResult<TGet | null>;
|
|
23
|
+
initial?: TGet;
|
|
24
|
+
}
|
|
25
|
+
export interface SyncedCrudPropsMany<TGet, TOption extends CrudAsOption> {
|
|
26
|
+
list?: (params: SyncedGetParams) => Promise<CrudResult<TGet[]>> | CrudResult<TGet[]>;
|
|
27
|
+
as?: TOption;
|
|
28
|
+
initial?: InitialValue<TGet, TOption>;
|
|
29
|
+
}
|
|
30
|
+
export interface SyncedCrudPropsBase<TGet extends { id: string }, TSet = TGet, TOut = TGet>
|
|
31
|
+
extends Omit<SyncedOptions<TGet>, 'get' | 'set' | 'subscribe' | 'transform' | 'initial'> {
|
|
32
|
+
generateId?: () => string;
|
|
33
|
+
create?: (input: TSet, params: SyncedSetParams<TSet>) => Promise<CrudResult<TGet>>;
|
|
34
|
+
update?: (input: Partial<TGet>, params: SyncedSetParams<TSet>) => Promise<CrudResult<Partial<TGet>>>;
|
|
35
|
+
delete?: (input: TGet, params: SyncedSetParams<TSet>) => Promise<CrudResult<any>>;
|
|
36
|
+
onSaved?: (saved: Partial<TGet>, input: Partial<TGet>, isCreate: boolean) => Partial<TGet>;
|
|
37
|
+
transform?: SyncTransform<TOut, TGet>;
|
|
38
|
+
fieldUpdatedAt?: string;
|
|
39
|
+
updatePartial?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type OutputType<TGet, TSet> = [TSet] extends [unknown] ? TGet : Partial<TGet> & TSet;
|
|
43
|
+
|
|
44
|
+
type InitialValue<T, TOption extends CrudAsOption> = TOption extends 'Map'
|
|
45
|
+
? Map<string, T>
|
|
46
|
+
: TOption extends 'object'
|
|
47
|
+
? Record<string, T>
|
|
48
|
+
: TOption extends 'first'
|
|
49
|
+
? T
|
|
50
|
+
: T[];
|
|
51
|
+
|
|
52
|
+
export type SyncedCrudReturnType<TGet, TSet, TOption extends CrudAsOption> = Promise<
|
|
53
|
+
TOption extends 'Map'
|
|
54
|
+
? Map<string, OutputType<TGet, TSet>>
|
|
55
|
+
: TOption extends 'object'
|
|
56
|
+
? Record<string, OutputType<TGet, TSet>>
|
|
57
|
+
: TOption extends 'first'
|
|
58
|
+
? OutputType<TGet, TSet>
|
|
59
|
+
: TGet[]
|
|
60
|
+
> & {};
|
|
61
|
+
|
|
62
|
+
let _asOption: CrudAsOption;
|
|
63
|
+
|
|
64
|
+
function transformOut<T>(data: T, transform: undefined | ((value: T) => T)) {
|
|
65
|
+
return transform ? transform(clone(data)) : data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// TODO
|
|
69
|
+
export function createTransform<T extends Record<string, any>, T2 extends Record<string, any>>(
|
|
70
|
+
...keys: (keyof T | { from: keyof T; to: keyof T2 })[]
|
|
71
|
+
): SyncTransform<T2, T> {
|
|
72
|
+
return {
|
|
73
|
+
load: (value: T) => {
|
|
74
|
+
(keys as string[]).forEach((key) => {
|
|
75
|
+
const keyRemote = isObject(key) ? key.from : key;
|
|
76
|
+
const keyLocal = isObject(key) ? key.to : key;
|
|
77
|
+
const v = value[keyRemote];
|
|
78
|
+
if (!isNullOrUndefined(v)) {
|
|
79
|
+
value[keyLocal as keyof T] = isString(v) ? JSON.parse(v as string) : v;
|
|
80
|
+
}
|
|
81
|
+
if (keyLocal !== keyRemote) {
|
|
82
|
+
delete value[keyRemote];
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return value as unknown as T2;
|
|
86
|
+
},
|
|
87
|
+
save: (value: T2) => {
|
|
88
|
+
(keys as string[]).forEach((key: string) => {
|
|
89
|
+
const keyRemote = isObject(key) ? key.from : key;
|
|
90
|
+
const keyLocal = isObject(key) ? key.to : key;
|
|
91
|
+
let v = (value as any)[keyLocal];
|
|
92
|
+
if (!isNullOrUndefined(v)) {
|
|
93
|
+
if (isArray(v)) {
|
|
94
|
+
v = v.filter((val) => !isNullOrUndefined(val));
|
|
95
|
+
}
|
|
96
|
+
value[keyRemote as keyof T2] =
|
|
97
|
+
isNumber(v) || isObject(v) || isArray(v) ? (JSON.stringify(v) as any) : v;
|
|
98
|
+
}
|
|
99
|
+
if (keyLocal !== keyRemote) {
|
|
100
|
+
delete value[keyLocal];
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return value as unknown as T;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// TODO
|
|
109
|
+
export function combineTransforms<T, T2>(
|
|
110
|
+
transform1: SyncTransform<T2, T>,
|
|
111
|
+
...transforms: Partial<SyncTransform<T2, T>>[]
|
|
112
|
+
): SyncTransform<T2, T> {
|
|
113
|
+
return {
|
|
114
|
+
load: (value: T) => {
|
|
115
|
+
let inValue = transform1.load?.(value) as any;
|
|
116
|
+
transforms.forEach((transform) => {
|
|
117
|
+
if (transform.load) {
|
|
118
|
+
inValue = transform.load(inValue);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return inValue;
|
|
122
|
+
},
|
|
123
|
+
save: (value: T2) => {
|
|
124
|
+
let outValue = value as any;
|
|
125
|
+
transforms.forEach((transform) => {
|
|
126
|
+
if (transform.save) {
|
|
127
|
+
outValue = transform.save(outValue);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return transform1.save?.(outValue) ?? outValue;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function syncedCrud<TGet extends { id: string }, TSet = TGet, TOut = TGet>(
|
|
136
|
+
props: SyncedCrudPropsBase<TGet, TSet, TOut> & SyncedCrudPropsSingle<TGet>,
|
|
137
|
+
): SyncedCrudReturnType<TOut, TSet, 'first'>;
|
|
138
|
+
export function syncedCrud<
|
|
139
|
+
TGet extends { id: string },
|
|
140
|
+
TSet = TGet,
|
|
141
|
+
TOut = TGet,
|
|
142
|
+
TOption extends CrudAsOption = 'object',
|
|
143
|
+
>(
|
|
144
|
+
props: SyncedCrudPropsBase<TGet, TSet, TOut> & SyncedCrudPropsMany<TGet, TOption>,
|
|
145
|
+
): SyncedCrudReturnType<TOut, TSet, Exclude<TOption, 'first'>>;
|
|
146
|
+
export function syncedCrud<
|
|
147
|
+
TGet extends { id: string },
|
|
148
|
+
TSet = TGet,
|
|
149
|
+
TOut = TGet,
|
|
150
|
+
TOption extends CrudAsOption = 'object',
|
|
151
|
+
>(
|
|
152
|
+
props: SyncedCrudPropsBase<TGet, TSet, TOut> & (SyncedCrudPropsSingle<TGet> & SyncedCrudPropsMany<TGet, TOption>),
|
|
153
|
+
): SyncedCrudReturnType<TOut, TSet, TOption> {
|
|
154
|
+
const {
|
|
155
|
+
get: getFn,
|
|
156
|
+
list: listFn,
|
|
157
|
+
create: createFn,
|
|
158
|
+
update: updateFn,
|
|
159
|
+
delete: deleteFn,
|
|
160
|
+
transform,
|
|
161
|
+
fieldUpdatedAt,
|
|
162
|
+
generateId,
|
|
163
|
+
updatePartial,
|
|
164
|
+
onSaved,
|
|
165
|
+
mode: modeParam,
|
|
166
|
+
...rest
|
|
167
|
+
} = props;
|
|
168
|
+
|
|
169
|
+
let asType = props.as;
|
|
170
|
+
|
|
171
|
+
if (!asType) {
|
|
172
|
+
asType = (getFn ? 'first' : _asOption || 'array') as CrudAsOption as TOption;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const asMap = asType === 'Map';
|
|
176
|
+
|
|
177
|
+
const ensureId = (obj: { id: string }) => obj.id || (obj.id = generateId!());
|
|
178
|
+
|
|
179
|
+
const get: undefined | ((params: SyncedGetParams) => Promise<TOut>) =
|
|
180
|
+
getFn || listFn
|
|
181
|
+
? async (getParams: SyncedGetParams) => {
|
|
182
|
+
const { updateLastSync, lastSync } = getParams;
|
|
183
|
+
if (listFn) {
|
|
184
|
+
if (lastSync) {
|
|
185
|
+
getParams.mode =
|
|
186
|
+
modeParam || (asType === 'array' ? 'append' : asType === 'first' ? 'set' : 'assign');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let data = await listFn(getParams);
|
|
190
|
+
let newLastSync = 0;
|
|
191
|
+
for (let i = 0; i < data.length; i++) {
|
|
192
|
+
const updated = (data[i] as any)[fieldUpdatedAt as any];
|
|
193
|
+
if (updated) {
|
|
194
|
+
newLastSync = Math.max(newLastSync, +new Date(updated));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (newLastSync && newLastSync !== lastSync) {
|
|
198
|
+
updateLastSync(newLastSync);
|
|
199
|
+
}
|
|
200
|
+
if (transform?.load) {
|
|
201
|
+
data = data.map(transform.load) as any;
|
|
202
|
+
}
|
|
203
|
+
if (asType === 'first') {
|
|
204
|
+
return data.length > 0 ? data[0] : lastSync ? {} : null;
|
|
205
|
+
} else if (asType === 'array') {
|
|
206
|
+
return data;
|
|
207
|
+
} else {
|
|
208
|
+
const out: Record<string, any> = asMap ? new Map() : {};
|
|
209
|
+
data.forEach((result: any) => {
|
|
210
|
+
const value = result.__deleted ? internal.symbolDelete : result;
|
|
211
|
+
asMap ? (out as Map<any, any>).set(result.id, value) : (out[result.id] = value);
|
|
212
|
+
});
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
} else if (getFn) {
|
|
216
|
+
let data = await getFn(getParams);
|
|
217
|
+
|
|
218
|
+
if (data) {
|
|
219
|
+
const newLastSync = (data as any)[fieldUpdatedAt as any];
|
|
220
|
+
if (newLastSync && newLastSync !== lastSync) {
|
|
221
|
+
updateLastSync(newLastSync);
|
|
222
|
+
}
|
|
223
|
+
if (transform?.load) {
|
|
224
|
+
data = transform.load(data as any) as any;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return data as any;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
: undefined;
|
|
232
|
+
|
|
233
|
+
const set =
|
|
234
|
+
createFn || updateFn || deleteFn
|
|
235
|
+
? async (params: SyncedSetParams<any> & { retryAsCreate?: boolean }) => {
|
|
236
|
+
const { value, changes, update, retryAsCreate, valuePrevious } = params;
|
|
237
|
+
const creates = new Map<string, TSet>();
|
|
238
|
+
const updates = new Map<string, object>();
|
|
239
|
+
const deletes = new Map<string, object>();
|
|
240
|
+
|
|
241
|
+
changes.forEach(({ path, prevAtPath, valueAtPath }) => {
|
|
242
|
+
if (asType === 'first') {
|
|
243
|
+
if (value) {
|
|
244
|
+
let id = value?.id;
|
|
245
|
+
const isCreate = fieldUpdatedAt ? !value[fieldUpdatedAt!] : !prevAtPath;
|
|
246
|
+
if (isCreate || retryAsCreate) {
|
|
247
|
+
id = ensureId(value);
|
|
248
|
+
creates.set(id, value);
|
|
249
|
+
} else if (path.length === 0) {
|
|
250
|
+
if (valueAtPath) {
|
|
251
|
+
updates.set(id, valueAtPath);
|
|
252
|
+
} else if (prevAtPath) {
|
|
253
|
+
deletes.set(prevAtPath?.id, prevAtPath);
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
const key = path[0];
|
|
257
|
+
updates.set(id, Object.assign(updates.get(id) || { id }, { [key]: value[key] }));
|
|
258
|
+
}
|
|
259
|
+
} else if (path.length === 0) {
|
|
260
|
+
const id = prevAtPath?.id;
|
|
261
|
+
if (id) {
|
|
262
|
+
deletes.set(id, prevAtPath);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
let itemsChanged: any[] | undefined = undefined;
|
|
267
|
+
let isCreateGuess: boolean;
|
|
268
|
+
if (path.length === 0) {
|
|
269
|
+
isCreateGuess =
|
|
270
|
+
!fieldUpdatedAt &&
|
|
271
|
+
!(
|
|
272
|
+
(asMap
|
|
273
|
+
? Array.from((valueAtPath as Map<any, any>).values())
|
|
274
|
+
: isArray(valueAtPath)
|
|
275
|
+
? valueAtPath
|
|
276
|
+
: Object.values(valueAtPath)
|
|
277
|
+
)?.length > 0
|
|
278
|
+
);
|
|
279
|
+
itemsChanged = asMap
|
|
280
|
+
? Array.from((valueAtPath as Map<any, any>).values())
|
|
281
|
+
: isArray(valueAtPath)
|
|
282
|
+
? valueAtPath
|
|
283
|
+
: Object.values(valueAtPath);
|
|
284
|
+
} else {
|
|
285
|
+
const itemKey = path[0];
|
|
286
|
+
const itemValue = asMap ? value.get(itemKey) : value[itemKey];
|
|
287
|
+
isCreateGuess = !fieldUpdatedAt && path.length === 1 && !prevAtPath;
|
|
288
|
+
if (!itemValue) {
|
|
289
|
+
if (path.length === 1 && prevAtPath) {
|
|
290
|
+
if (deleteFn) {
|
|
291
|
+
deletes.set(itemKey, prevAtPath);
|
|
292
|
+
} else {
|
|
293
|
+
console.log('[legend-state] missing delete function');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
itemsChanged = [itemValue];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
itemsChanged?.forEach((item) => {
|
|
301
|
+
ensureId(item);
|
|
302
|
+
const isCreate = fieldUpdatedAt ? !item[fieldUpdatedAt!] : isCreateGuess;
|
|
303
|
+
if (isCreate) {
|
|
304
|
+
if (createFn) {
|
|
305
|
+
creates.set(item.id, item);
|
|
306
|
+
} else {
|
|
307
|
+
console.log('[legend-state] missing create function');
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
if (updateFn) {
|
|
311
|
+
updates.set(item.id, item);
|
|
312
|
+
} else {
|
|
313
|
+
console.log('[legend-state] missing update function');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const saveResult = async (
|
|
321
|
+
itemKey: string,
|
|
322
|
+
input: object,
|
|
323
|
+
data: CrudResult<TSet>,
|
|
324
|
+
isCreate: boolean,
|
|
325
|
+
) => {
|
|
326
|
+
if (data && onSaved) {
|
|
327
|
+
const dataLoaded: TGet = (transform?.load ? transform.load(data as any) : data) as any;
|
|
328
|
+
|
|
329
|
+
const savedOut = onSaved(dataLoaded, input, isCreate);
|
|
330
|
+
|
|
331
|
+
const updatedAt = fieldUpdatedAt ? savedOut[fieldUpdatedAt as keyof TGet] : undefined;
|
|
332
|
+
|
|
333
|
+
const value =
|
|
334
|
+
itemKey !== 'undefined' && asType !== 'first' ? { [itemKey]: savedOut } : savedOut;
|
|
335
|
+
update({
|
|
336
|
+
value,
|
|
337
|
+
lastSync: updatedAt ? +new Date(updatedAt as any) : undefined,
|
|
338
|
+
mode: 'merge',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return Promise.all([
|
|
344
|
+
...Array.from(creates).map(([itemKey, itemValue]) => {
|
|
345
|
+
const createObj = transformOut(itemValue, transform?.save as any);
|
|
346
|
+
return createFn!(createObj, params).then((result) =>
|
|
347
|
+
saveResult(itemKey, createObj as object, result as any, true),
|
|
348
|
+
);
|
|
349
|
+
}),
|
|
350
|
+
...Array.from(updates).map(([itemKey, itemValue]) => {
|
|
351
|
+
const toSave = updatePartial
|
|
352
|
+
? diffObjects(asType === 'first' ? valuePrevious : valuePrevious[itemKey], itemValue)
|
|
353
|
+
: itemValue;
|
|
354
|
+
const changed = transformOut(toSave as TGet, transform?.save as any);
|
|
355
|
+
|
|
356
|
+
if (Object.keys(changed).length > 0) {
|
|
357
|
+
return updateFn!(changed, params).then((result) =>
|
|
358
|
+
saveResult(itemKey, changed, result as any, false),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}),
|
|
362
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
363
|
+
...Array.from(deletes).map(([_, itemValue]) => deleteFn!(itemValue as TGet, params)),
|
|
364
|
+
]);
|
|
365
|
+
}
|
|
366
|
+
: undefined;
|
|
367
|
+
|
|
368
|
+
return synced<any>({
|
|
369
|
+
set,
|
|
370
|
+
get,
|
|
371
|
+
...rest,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Synced, SyncedOptions, SyncedSetParams, isString } from '@legendapp/state';
|
|
2
|
-
import { synced } from '@legendapp/state/
|
|
2
|
+
import { synced } from '@legendapp/state/sync';
|
|
3
3
|
|
|
4
4
|
export interface SyncedFetchProps extends Omit<SyncedOptions, 'get' | 'set'> {
|
|
5
5
|
get: string | RequestInfo;
|