@legendapp/state 2.2.0-next.75 → 2.2.0-next.76

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 (48) hide show
  1. package/helpers/time.d.ts +2 -2
  2. package/index.js +7 -5
  3. package/index.js.map +1 -1
  4. package/index.mjs +7 -5
  5. package/index.mjs.map +1 -1
  6. package/package.json +11 -1
  7. package/persist.js +82 -87
  8. package/persist.js.map +1 -1
  9. package/persist.mjs +82 -87
  10. package/persist.mjs.map +1 -1
  11. package/src/batching.ts +2 -0
  12. package/src/computed.ts +4 -2
  13. package/src/globals.ts +1 -1
  14. package/src/helpers.ts +1 -1
  15. package/src/history/undoRedo.ts +111 -0
  16. package/src/observableInterfaces.ts +6 -5
  17. package/src/observe.ts +1 -1
  18. package/src/sync/activateSyncedNode.ts +2 -20
  19. package/src/sync/syncObservable.ts +88 -73
  20. package/src/sync-plugins/crud.ts +109 -98
  21. package/src/sync-plugins/fetch.ts +56 -26
  22. package/src/sync-plugins/keel.ts +447 -0
  23. package/src/sync-plugins/supabase.ts +225 -0
  24. package/src/syncTypes.ts +10 -4
  25. package/sync-plugins/crud.d.ts +27 -26
  26. package/sync-plugins/crud.js +50 -42
  27. package/sync-plugins/crud.js.map +1 -1
  28. package/sync-plugins/crud.mjs +50 -42
  29. package/sync-plugins/crud.mjs.map +1 -1
  30. package/sync-plugins/fetch.d.ts +8 -7
  31. package/sync-plugins/fetch.js +33 -11
  32. package/sync-plugins/fetch.js.map +1 -1
  33. package/sync-plugins/fetch.mjs +34 -12
  34. package/sync-plugins/fetch.mjs.map +1 -1
  35. package/sync-plugins/keel.d.ts +91 -0
  36. package/sync-plugins/keel.js +278 -0
  37. package/sync-plugins/keel.js.map +1 -0
  38. package/sync-plugins/keel.mjs +274 -0
  39. package/sync-plugins/keel.mjs.map +1 -0
  40. package/sync-plugins/supabase.d.ts +32 -0
  41. package/sync-plugins/supabase.js +134 -0
  42. package/sync-plugins/supabase.js.map +1 -0
  43. package/sync-plugins/supabase.mjs +131 -0
  44. package/sync-plugins/supabase.mjs.map +1 -0
  45. package/sync.js +82 -87
  46. package/sync.js.map +1 -1
  47. package/sync.mjs +83 -88
  48. package/sync.mjs.map +1 -1
@@ -5,6 +5,7 @@ import type {
5
5
  NodeValue,
6
6
  Observable,
7
7
  ObservableObject,
8
+ ObservableOnChangeParams,
8
9
  ObservableParam,
9
10
  ObservablePersistPlugin,
10
11
  ObservableSyncClass,
@@ -15,6 +16,7 @@ import type {
15
16
  Synced,
16
17
  SyncedOptions,
17
18
  TypeAtPath,
19
+ UpdateFnParams,
18
20
  } from '@legendapp/state';
19
21
  import {
20
22
  beginBatch,
@@ -863,6 +865,82 @@ export function syncObservable<T>(
863
865
 
864
866
  if (get) {
865
867
  const runGet = () => {
868
+ const onChange = async ({ value, mode, lastSync }: UpdateFnParams) => {
869
+ mode = mode || syncOptions.mode || 'set';
870
+ if (value !== undefined) {
871
+ value = transformLoadData(value, syncOptions, true);
872
+ if (isPromise(value)) {
873
+ value = await (value as Promise<T>);
874
+ }
875
+
876
+ const pending = localState.pendingChanges;
877
+ const currentValue = obs$.peek();
878
+ if (pending) {
879
+ let didChangeMetadata = false;
880
+ Object.keys(pending).forEach((key) => {
881
+ const p = key.split('/').filter((p) => p !== '');
882
+ const { v, t } = pending[key];
883
+
884
+ if (t.length === 0 || !value) {
885
+ if (isObject(value) && isObject(v)) {
886
+ Object.assign(value, v);
887
+ } else {
888
+ value = v;
889
+ }
890
+ } else if ((value as any)[p[0]] !== undefined) {
891
+ const curValue = getValueAtPath(currentValue as object, p);
892
+ const newValue = getValueAtPath(value as object, p);
893
+ if (JSON.stringify(curValue) === JSON.stringify(newValue)) {
894
+ delete pending[key];
895
+ didChangeMetadata = true;
896
+ } else {
897
+ (value as any) = setAtPath(
898
+ value as any,
899
+ p,
900
+ t,
901
+ v,
902
+ 'merge',
903
+ obs$.peek(),
904
+ (path: string[], value: any) => {
905
+ delete pending[key];
906
+ pending[path.join('/')] = {
907
+ p: null,
908
+ v: value,
909
+ t: t.slice(0, path.length),
910
+ };
911
+ },
912
+ );
913
+ }
914
+ }
915
+ });
916
+
917
+ if (didChangeMetadata) {
918
+ updateMetadata(obs$, localState, syncState, syncOptions, {
919
+ pending,
920
+ });
921
+ }
922
+ }
923
+
924
+ onChangeRemote(() => {
925
+ if (mode === 'assign' && isObject(value)) {
926
+ (obs$ as unknown as Observable<object>).assign(value);
927
+ } else if (mode === 'append' && isArray(value)) {
928
+ (obs$ as unknown as Observable<any[]>).push(...value);
929
+ } else if (mode === 'prepend' && isArray(value)) {
930
+ (obs$ as unknown as Observable<any[]>).splice(0, 0, ...value);
931
+ } else if (mode === 'merge') {
932
+ mergeIntoObservable(obs$, value);
933
+ } else {
934
+ obs$.set(value);
935
+ }
936
+ });
937
+ }
938
+ if (lastSync && syncOptions.persist) {
939
+ updateMetadata(obs$, localState, syncState, syncOptions, {
940
+ lastSync,
941
+ });
942
+ }
943
+ };
866
944
  get({
867
945
  state: syncState,
868
946
  obs: obs$,
@@ -873,87 +951,24 @@ export function syncObservable<T>(
873
951
  syncOptions.onGetError?.(error);
874
952
  },
875
953
  onGet: () => {
954
+ const isFirstLoad = !node.state!.isLoaded.peek();
876
955
  node.state!.assign({
877
956
  isLoaded: true,
878
957
  error: undefined,
879
958
  });
880
- },
881
- onChange: async ({ value, mode, lastSync }) => {
882
- mode = mode || syncOptions.mode || 'set';
883
- if (value !== undefined) {
884
- value = transformLoadData(value, syncOptions, true);
885
- if (isPromise(value)) {
886
- value = await (value as Promise<T>);
887
- }
888
-
889
- const pending = localState.pendingChanges;
890
- const currentValue = obs$.peek();
891
- if (pending) {
892
- let didChangeMetadata = false;
893
- Object.keys(pending).forEach((key) => {
894
- const p = key.split('/').filter((p) => p !== '');
895
- const { v, t } = pending[key];
896
-
897
- if (t.length === 0 || !value) {
898
- if (isObject(value) && isObject(v)) {
899
- Object.assign(value, v);
900
- } else {
901
- value = v;
902
- }
903
- } else if ((value as any)[p[0]] !== undefined) {
904
- const curValue = getValueAtPath(currentValue as object, p);
905
- const newValue = getValueAtPath(value as object, p);
906
- if (JSON.stringify(curValue) === JSON.stringify(newValue)) {
907
- delete pending[key];
908
- didChangeMetadata = true;
909
- } else {
910
- (value as any) = setAtPath(
911
- value as any,
912
- p,
913
- t,
914
- v,
915
- 'merge',
916
- obs$.peek(),
917
- (path: string[], value: any) => {
918
- delete pending[key];
919
- pending[path.join('/')] = {
920
- p: null,
921
- v: value,
922
- t: t.slice(0, path.length),
923
- };
924
- },
925
- );
926
- }
927
- }
928
- });
929
959
 
930
- if (didChangeMetadata) {
931
- updateMetadata(obs$, localState, syncState, syncOptions, {
932
- pending,
933
- });
934
- }
935
- }
936
-
937
- onChangeRemote(() => {
938
- if (mode === 'assign' && isObject(value)) {
939
- (obs$ as unknown as Observable<object>).assign(value);
940
- } else if (mode === 'append' && isArray(value)) {
941
- (obs$ as unknown as Observable<any[]>).push(...value);
942
- } else if (mode === 'prepend' && isArray(value)) {
943
- (obs$ as unknown as Observable<any[]>).splice(0, 0, ...value);
944
- } else if (mode === 'merge') {
945
- mergeIntoObservable(obs$, value);
946
- } else {
947
- obs$.set(value);
948
- }
949
- });
950
- }
951
- if (lastSync && syncOptions.persist) {
952
- updateMetadata(obs$, localState, syncState, syncOptions, {
953
- lastSync,
960
+ if (isFirstLoad && syncOptions.subscribe) {
961
+ syncOptions.subscribe({
962
+ node,
963
+ update: (params: ObservableOnChangeParams) => {
964
+ params.mode ||= syncOptions.mode || 'merge';
965
+ onChange(params);
966
+ },
967
+ refresh: sync,
954
968
  });
955
969
  }
956
970
  },
971
+ onChange,
957
972
  });
958
973
  };
959
974
  runGet();
@@ -14,54 +14,55 @@ import { synced, diffObjects } from '@legendapp/state/sync';
14
14
 
15
15
  const { clone } = internal;
16
16
 
17
- export type CrudAsOption = 'Map' | 'object' | 'first' | 'array';
17
+ export type CrudAsOption = 'Map' | 'object' | 'first';
18
18
 
19
19
  export type CrudResult<T> = T;
20
20
 
21
- export interface SyncedCrudPropsSingle<TGet> {
22
- get?: (params: SyncedGetParams) => Promise<CrudResult<TGet | null>> | CrudResult<TGet | null>;
23
- initial?: TGet;
21
+ export interface SyncedCrudPropsSingle<TRemote, TLocal> {
22
+ get?: (params: SyncedGetParams) => Promise<CrudResult<TRemote | null>> | CrudResult<TRemote | null>;
23
+ initial?: InitialValue<TLocal, 'first'>;
24
+ as?: never | 'first';
24
25
  }
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>;
26
+ export interface SyncedCrudPropsMany<TRemote, TLocal, TAsOption extends CrudAsOption> {
27
+ list?: (params: SyncedGetParams) => Promise<CrudResult<TRemote[] | null>> | CrudResult<TRemote[] | null>;
28
+ as?: TAsOption;
29
+ initial?: InitialValue<TLocal, TAsOption>;
29
30
  }
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>;
31
+ export interface SyncedCrudPropsBase<TRemote extends { id: string | number }, TLocal = TRemote>
32
+ extends Omit<SyncedOptions<TLocal>, 'get' | 'set' | 'transform' | 'initial'> {
33
+ create?(input: TRemote, params: SyncedSetParams<TRemote>): Promise<CrudResult<TRemote> | null | undefined>;
34
+ update?(
35
+ input: Partial<TRemote>,
36
+ params: SyncedSetParams<TRemote>,
37
+ ): Promise<CrudResult<Partial<TRemote> | null | undefined>>;
38
+ delete?(input: TRemote, params: SyncedSetParams<TRemote>): Promise<CrudResult<any>>;
39
+ onSaved?(saved: TLocal, input: TRemote, isCreate: boolean): Partial<TLocal> | void;
40
+ transform?: SyncTransform<TLocal, TRemote>;
38
41
  fieldUpdatedAt?: string;
42
+ fieldCreatedAt?: string;
39
43
  updatePartial?: boolean;
44
+ changesSince?: 'all' | 'last-sync';
40
45
  }
41
46
 
42
- type OutputType<TGet, TSet> = [TSet] extends [unknown] ? TGet : Partial<TGet> & TSet;
43
-
44
- type InitialValue<T, TOption extends CrudAsOption> = TOption extends 'Map'
47
+ type InitialValue<T, TAsOption extends CrudAsOption> = TAsOption extends 'Map'
45
48
  ? Map<string, T>
46
- : TOption extends 'object'
49
+ : TAsOption extends 'object'
47
50
  ? Record<string, T>
48
- : TOption extends 'first'
51
+ : TAsOption extends 'first'
49
52
  ? T
50
53
  : T[];
51
54
 
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
- > & {};
55
+ export type SyncedCrudReturnType<TLocal, TAsOption extends CrudAsOption> = TAsOption extends 'Map'
56
+ ? Map<string, TLocal>
57
+ : TAsOption extends 'object'
58
+ ? Record<string, TLocal>
59
+ : TAsOption extends 'first'
60
+ ? TLocal
61
+ : TLocal[];
61
62
 
62
63
  let _asOption: CrudAsOption;
63
64
 
64
- function transformOut<T>(data: T, transform: undefined | ((value: T) => T)) {
65
+ function transformOut<T1, T2>(data: T1, transform: undefined | ((value: T1) => T2)) {
65
66
  return transform ? transform(clone(data)) : data;
66
67
  }
67
68
 
@@ -132,25 +133,24 @@ export function combineTransforms<T, T2>(
132
133
  };
133
134
  }
134
135
 
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'>;
136
+ export function syncedCrud<TRemote extends { id: string | number }, TLocal = TRemote>(
137
+ props: SyncedCrudPropsBase<TRemote, TLocal> & SyncedCrudPropsSingle<TRemote, TLocal>,
138
+ ): SyncedCrudReturnType<TLocal, 'first'>;
138
139
  export function syncedCrud<
139
- TGet extends { id: string },
140
- TSet = TGet,
141
- TOut = TGet,
142
- TOption extends CrudAsOption = 'object',
140
+ TRemote extends { id: string | number },
141
+ TLocal = TRemote,
142
+ TAsOption extends CrudAsOption = 'object',
143
143
  >(
144
- props: SyncedCrudPropsBase<TGet, TSet, TOut> & SyncedCrudPropsMany<TGet, TOption>,
145
- ): SyncedCrudReturnType<TOut, TSet, Exclude<TOption, 'first'>>;
144
+ props: SyncedCrudPropsBase<TRemote, TLocal> & SyncedCrudPropsMany<TRemote, TLocal, TAsOption>,
145
+ ): SyncedCrudReturnType<TLocal, Exclude<TAsOption, 'first'>>;
146
146
  export function syncedCrud<
147
- TGet extends { id: string },
148
- TSet = TGet,
149
- TOut = TGet,
150
- TOption extends CrudAsOption = 'object',
147
+ TRemote extends { id: string | number },
148
+ TLocal = TRemote,
149
+ TAsOption extends CrudAsOption = 'object',
151
150
  >(
152
- props: SyncedCrudPropsBase<TGet, TSet, TOut> & (SyncedCrudPropsSingle<TGet> & SyncedCrudPropsMany<TGet, TOption>),
153
- ): SyncedCrudReturnType<TOut, TSet, TOption> {
151
+ props: SyncedCrudPropsBase<TRemote, TLocal> &
152
+ (SyncedCrudPropsSingle<TRemote, TLocal> & SyncedCrudPropsMany<TRemote, TLocal, TAsOption>),
153
+ ): SyncedCrudReturnType<TLocal, TAsOption> {
154
154
  const {
155
155
  get: getFn,
156
156
  list: listFn,
@@ -158,38 +158,37 @@ export function syncedCrud<
158
158
  update: updateFn,
159
159
  delete: deleteFn,
160
160
  transform,
161
+ fieldCreatedAt,
161
162
  fieldUpdatedAt,
162
- generateId,
163
163
  updatePartial,
164
164
  onSaved,
165
165
  mode: modeParam,
166
+ changesSince,
166
167
  ...rest
167
168
  } = props;
168
169
 
169
- let asType = props.as;
170
+ let asType = props.as as TAsOption;
170
171
 
171
172
  if (!asType) {
172
- asType = (getFn ? 'first' : _asOption || 'array') as CrudAsOption as TOption;
173
+ asType = (getFn ? 'first' : _asOption || 'object') as CrudAsOption as TAsOption;
173
174
  }
174
175
 
175
176
  const asMap = asType === 'Map';
176
177
 
177
- const ensureId = (obj: { id: string }) => obj.id || (obj.id = generateId!());
178
-
179
- const get: undefined | ((params: SyncedGetParams) => Promise<TOut>) =
178
+ const get: undefined | ((params: SyncedGetParams) => Promise<TLocal>) =
180
179
  getFn || listFn
181
180
  ? async (getParams: SyncedGetParams) => {
182
181
  const { updateLastSync, lastSync } = getParams;
183
182
  if (listFn) {
184
- if (lastSync) {
185
- getParams.mode =
186
- modeParam || (asType === 'array' ? 'append' : asType === 'first' ? 'set' : 'assign');
183
+ if (changesSince === 'last-sync' && lastSync) {
184
+ getParams.mode = modeParam || (asType === 'first' ? 'set' : 'assign');
187
185
  }
188
186
 
189
- let data = await listFn(getParams);
187
+ const data = (await listFn(getParams)) || [];
190
188
  let newLastSync = 0;
191
189
  for (let i = 0; i < data.length; i++) {
192
- const updated = (data[i] as any)[fieldUpdatedAt as any];
190
+ const updated =
191
+ (data[i] as any)[fieldUpdatedAt as any] || (data[i] as any)[fieldCreatedAt as any];
193
192
  if (updated) {
194
193
  newLastSync = Math.max(newLastSync, +new Date(updated));
195
194
  }
@@ -197,35 +196,36 @@ export function syncedCrud<
197
196
  if (newLastSync && newLastSync !== lastSync) {
198
197
  updateLastSync(newLastSync);
199
198
  }
199
+ let transformed = data as unknown as TLocal[];
200
200
  if (transform?.load) {
201
- data = data.map(transform.load) as any;
201
+ transformed = await Promise.all(data.map(transform.load));
202
202
  }
203
203
  if (asType === 'first') {
204
- return data.length > 0 ? data[0] : lastSync ? {} : null;
205
- } else if (asType === 'array') {
206
- return data;
204
+ return transformed.length > 0 ? transformed[0] : null;
207
205
  } else {
208
206
  const out: Record<string, any> = asMap ? new Map() : {};
209
- data.forEach((result: any) => {
207
+ transformed.forEach((result: any) => {
210
208
  const value = result.__deleted ? internal.symbolDelete : result;
211
209
  asMap ? (out as Map<any, any>).set(result.id, value) : (out[result.id] = value);
212
210
  });
213
211
  return out;
214
212
  }
215
213
  } else if (getFn) {
216
- let data = await getFn(getParams);
214
+ const data = await getFn(getParams);
217
215
 
216
+ let transformed = data as unknown as TLocal;
218
217
  if (data) {
219
- const newLastSync = (data as any)[fieldUpdatedAt as any];
218
+ const newLastSync =
219
+ (data as any)[fieldUpdatedAt as any] || (data as any)[fieldCreatedAt as any];
220
220
  if (newLastSync && newLastSync !== lastSync) {
221
221
  updateLastSync(newLastSync);
222
222
  }
223
223
  if (transform?.load) {
224
- data = transform.load(data as any) as any;
224
+ transformed = await transform.load(data);
225
225
  }
226
226
  }
227
227
 
228
- return data as any;
228
+ return transformed as any;
229
229
  }
230
230
  }
231
231
  : undefined;
@@ -234,27 +234,30 @@ export function syncedCrud<
234
234
  createFn || updateFn || deleteFn
235
235
  ? async (params: SyncedSetParams<any> & { retryAsCreate?: boolean }) => {
236
236
  const { value, changes, update, retryAsCreate, valuePrevious } = params;
237
- const creates = new Map<string, TSet>();
237
+ const creates = new Map<string, TLocal>();
238
238
  const updates = new Map<string, object>();
239
239
  const deletes = new Map<string, object>();
240
240
 
241
241
  changes.forEach(({ path, prevAtPath, valueAtPath }) => {
242
242
  if (asType === 'first') {
243
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);
244
+ const id = value?.id;
245
+ if (id) {
246
+ const isCreate = fieldCreatedAt ? !value[fieldCreatedAt!] : !prevAtPath;
247
+ if (isCreate || retryAsCreate) {
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] }));
254
258
  }
255
259
  } else {
256
- const key = path[0];
257
- updates.set(id, Object.assign(updates.get(id) || { id }, { [key]: value[key] }));
260
+ console.error('[legend-state]: added item without an id');
258
261
  }
259
262
  } else if (path.length === 0) {
260
263
  const id = prevAtPath?.id;
@@ -267,7 +270,7 @@ export function syncedCrud<
267
270
  let isCreateGuess: boolean;
268
271
  if (path.length === 0) {
269
272
  isCreateGuess =
270
- !fieldUpdatedAt &&
273
+ !(fieldCreatedAt || fieldUpdatedAt) &&
271
274
  !(
272
275
  (asMap
273
276
  ? Array.from((valueAtPath as Map<any, any>).values())
@@ -284,7 +287,7 @@ export function syncedCrud<
284
287
  } else {
285
288
  const itemKey = path[0];
286
289
  const itemValue = asMap ? value.get(itemKey) : value[itemKey];
287
- isCreateGuess = !fieldUpdatedAt && path.length === 1 && !prevAtPath;
290
+ isCreateGuess = !(fieldCreatedAt || fieldUpdatedAt) && path.length === 1 && !prevAtPath;
288
291
  if (!itemValue) {
289
292
  if (path.length === 1 && prevAtPath) {
290
293
  if (deleteFn) {
@@ -298,8 +301,11 @@ export function syncedCrud<
298
301
  }
299
302
  }
300
303
  itemsChanged?.forEach((item) => {
301
- ensureId(item);
302
- const isCreate = fieldUpdatedAt ? !item[fieldUpdatedAt!] : isCreateGuess;
304
+ const isCreate = fieldCreatedAt
305
+ ? !item[fieldCreatedAt!]
306
+ : fieldUpdatedAt
307
+ ? !item[fieldUpdatedAt]
308
+ : isCreateGuess;
303
309
  if (isCreate) {
304
310
  if (createFn) {
305
311
  creates.set(item.id, item);
@@ -319,39 +325,43 @@ export function syncedCrud<
319
325
 
320
326
  const saveResult = async (
321
327
  itemKey: string,
322
- input: object,
323
- data: CrudResult<TSet>,
328
+ input: TRemote,
329
+ data: CrudResult<TRemote>,
324
330
  isCreate: boolean,
325
331
  ) => {
326
332
  if (data && onSaved) {
327
- const dataLoaded: TGet = (transform?.load ? transform.load(data as any) : data) as any;
333
+ const dataLoaded: TLocal = (transform?.load ? transform.load(data as any) : data) as any;
328
334
 
329
335
  const savedOut = onSaved(dataLoaded, input, isCreate);
330
336
 
331
- const updatedAt = fieldUpdatedAt ? savedOut[fieldUpdatedAt as keyof TGet] : undefined;
337
+ if (savedOut) {
338
+ const createdAt = fieldCreatedAt ? savedOut[fieldCreatedAt as keyof TLocal] : undefined;
339
+ const updatedAt = fieldUpdatedAt ? savedOut[fieldUpdatedAt as keyof TLocal] : undefined;
332
340
 
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
- });
341
+ const value =
342
+ itemKey !== 'undefined' && asType !== 'first' ? { [itemKey]: savedOut } : savedOut;
343
+ update({
344
+ value,
345
+ lastSync:
346
+ updatedAt || createdAt ? +new Date(updatedAt || (createdAt as any)) : undefined,
347
+ mode: 'merge',
348
+ });
349
+ }
340
350
  }
341
351
  };
342
352
 
343
353
  return Promise.all([
344
354
  ...Array.from(creates).map(([itemKey, itemValue]) => {
345
- const createObj = transformOut(itemValue, transform?.save as any);
355
+ const createObj = transformOut(itemValue, transform?.save) as TRemote;
346
356
  return createFn!(createObj, params).then((result) =>
347
- saveResult(itemKey, createObj as object, result as any, true),
357
+ saveResult(itemKey, createObj, result as any, true),
348
358
  );
349
359
  }),
350
360
  ...Array.from(updates).map(([itemKey, itemValue]) => {
351
361
  const toSave = updatePartial
352
362
  ? diffObjects(asType === 'first' ? valuePrevious : valuePrevious[itemKey], itemValue)
353
363
  : itemValue;
354
- const changed = transformOut(toSave as TGet, transform?.save as any);
364
+ const changed = transformOut(toSave as TLocal, transform?.save) as TRemote;
355
365
 
356
366
  if (Object.keys(changed).length > 0) {
357
367
  return updateFn!(changed, params).then((result) =>
@@ -360,7 +370,7 @@ export function syncedCrud<
360
370
  }
361
371
  }),
362
372
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
363
- ...Array.from(deletes).map(([_, itemValue]) => deleteFn!(itemValue as TGet, params)),
373
+ ...Array.from(deletes).map(([_, itemValue]) => deleteFn!(itemValue as TRemote, params)),
364
374
  ]);
365
375
  }
366
376
  : undefined;
@@ -368,6 +378,7 @@ export function syncedCrud<
368
378
  return synced<any>({
369
379
  set,
370
380
  get,
381
+ mode: modeParam,
371
382
  ...rest,
372
383
  });
373
384
  }