@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/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
@@ -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 isMap = value instanceof Map;
96
- const keys = isMap ? Array.from(value.keys()) : Object.keys(value);
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(isMap ? value.get(A)! : value[A], isMap ? value.get(B)! : value[B], A, B));
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 (isMap ? value.get(key) : value[key]) {
103
- const item$ = isMap ? each!.get(key) : (each as ObservableObject<Record<string, any>>)[key];
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
- return get!({
40
+ const paramsToGet = {
43
41
  value:
44
42
  isFunction(existingValue) || existingValue?.[symbolLinked] ? undefined : existingValue,
45
43
  lastSync: params.lastSync!,
46
44
  updateLastSync,
47
- setMode,
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;
@@ -1,15 +1,56 @@
1
- import { isObject } from '@legendapp/state';
2
-
3
- export function removeNullUndefined<T extends Record<string, any>>(val: T) {
4
- if (val) {
5
- Object.keys(val).forEach((key) => {
6
- const v = val[key];
7
- if (v === null || v === undefined) {
8
- delete val[key];
9
- } else if (isObject(v)) {
10
- removeNullUndefined(v);
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
- return val;
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
- existing.valueAtPath = change.valueAtPath;
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
- (inRemoteChange ? outRemote : outLocal).set(obs, value);
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 configRemote = syncOptions;
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 { offlineBehavior, allowSetIfGetError, onBeforeSet, onSetError, waitForSet, onAfterSet } =
576
+ const { allowSetIfGetError, onBeforeSet, onSetError, waitForSet, onAfterSet } =
572
577
  syncOptions || ({} as SyncedOptions);
573
- const shouldSaveMetadata = persist && offlineBehavior === 'retry';
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
- // Remote can optionally have data that needs to be merged back into the observable,
648
- // for example Firebase may update dateModified with the server timestamp
649
- if (changes && !isEmpty(changes)) {
650
- transformedChanges = transformLoadData(changes, syncOptions, false);
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
- if (localState.numSavesOutstanding > 0) {
654
- if (transformedChanges) {
655
- if (!localState.pendingSaveResults) {
656
- localState.pendingSaveResults = [];
657
- }
658
- localState.pendingSaveResults.push(transformedChanges);
659
+ if (localState.numSavesOutstanding > 0) {
660
+ if (transformedChanges) {
661
+ if (!localState.pendingSaveResults) {
662
+ localState.pendingSaveResults = [];
659
663
  }
660
- } else {
661
- let allChanges = [...(localState.pendingSaveResults || []), transformedChanges].filter(
662
- (v) => v !== undefined,
663
- );
664
- if (allChanges.length > 0) {
665
- if (allChanges.some((change) => isPromise(change))) {
666
- allChanges = await Promise.all(allChanges);
667
- }
668
- onChangeRemote(() => mergeIntoObservable(obs, ...allChanges));
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/persist';
2
+ import { synced } from '@legendapp/state/sync';
3
3
 
4
4
  export interface SyncedFetchProps extends Omit<SyncedOptions, 'get' | 'set'> {
5
5
  get: string | RequestInfo;