@legendapp/state 2.2.0-next.74 → 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 (60) hide show
  1. package/helpers/time.d.ts +2 -2
  2. package/index.d.ts +1 -1
  3. package/index.js +82 -31
  4. package/index.js.map +1 -1
  5. package/index.mjs +81 -32
  6. package/index.mjs.map +1 -1
  7. package/package.json +16 -1
  8. package/persist.js +122 -129
  9. package/persist.js.map +1 -1
  10. package/persist.mjs +122 -129
  11. package/persist.mjs.map +1 -1
  12. package/react.js +5 -5
  13. package/react.js.map +1 -1
  14. package/react.mjs +6 -6
  15. package/react.mjs.map +1 -1
  16. package/src/ObservableObject.ts +34 -15
  17. package/src/batching.ts +9 -3
  18. package/src/computed.ts +4 -2
  19. package/src/globals.ts +17 -7
  20. package/src/helpers.ts +3 -3
  21. package/src/history/undoRedo.ts +111 -0
  22. package/src/is.ts +7 -0
  23. package/src/observableInterfaces.ts +6 -5
  24. package/src/observableTypes.ts +5 -0
  25. package/src/observe.ts +1 -1
  26. package/src/react/For.tsx +6 -6
  27. package/src/sync/activateSyncedNode.ts +9 -25
  28. package/src/sync/syncHelpers.ts +53 -12
  29. package/src/sync/syncObservable.ts +117 -101
  30. package/src/sync-plugins/crud.ts +384 -0
  31. package/src/sync-plugins/fetch.ts +57 -27
  32. package/src/sync-plugins/keel.ts +447 -0
  33. package/src/sync-plugins/supabase.ts +225 -0
  34. package/src/syncTypes.ts +12 -6
  35. package/src/when.ts +6 -1
  36. package/sync-plugins/crud.d.ts +40 -0
  37. package/sync-plugins/crud.js +275 -0
  38. package/sync-plugins/crud.js.map +1 -0
  39. package/sync-plugins/crud.mjs +271 -0
  40. package/sync-plugins/crud.mjs.map +1 -0
  41. package/sync-plugins/fetch.d.ts +8 -7
  42. package/sync-plugins/fetch.js +34 -12
  43. package/sync-plugins/fetch.js.map +1 -1
  44. package/sync-plugins/fetch.mjs +35 -13
  45. package/sync-plugins/fetch.mjs.map +1 -1
  46. package/sync-plugins/keel.d.ts +91 -0
  47. package/sync-plugins/keel.js +278 -0
  48. package/sync-plugins/keel.js.map +1 -0
  49. package/sync-plugins/keel.mjs +274 -0
  50. package/sync-plugins/keel.mjs.map +1 -0
  51. package/sync-plugins/supabase.d.ts +32 -0
  52. package/sync-plugins/supabase.js +134 -0
  53. package/sync-plugins/supabase.js.map +1 -0
  54. package/sync-plugins/supabase.mjs +131 -0
  55. package/sync-plugins/supabase.mjs.map +1 -0
  56. package/sync.d.ts +1 -0
  57. package/sync.js +157 -127
  58. package/sync.js.map +1 -1
  59. package/sync.mjs +156 -129
  60. package/sync.mjs.map +1 -1
@@ -0,0 +1,384 @@
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';
18
+
19
+ export type CrudResult<T> = T;
20
+
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';
25
+ }
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>;
30
+ }
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>;
41
+ fieldUpdatedAt?: string;
42
+ fieldCreatedAt?: string;
43
+ updatePartial?: boolean;
44
+ changesSince?: 'all' | 'last-sync';
45
+ }
46
+
47
+ type InitialValue<T, TAsOption extends CrudAsOption> = TAsOption extends 'Map'
48
+ ? Map<string, T>
49
+ : TAsOption extends 'object'
50
+ ? Record<string, T>
51
+ : TAsOption extends 'first'
52
+ ? T
53
+ : T[];
54
+
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[];
62
+
63
+ let _asOption: CrudAsOption;
64
+
65
+ function transformOut<T1, T2>(data: T1, transform: undefined | ((value: T1) => T2)) {
66
+ return transform ? transform(clone(data)) : data;
67
+ }
68
+
69
+ // TODO
70
+ export function createTransform<T extends Record<string, any>, T2 extends Record<string, any>>(
71
+ ...keys: (keyof T | { from: keyof T; to: keyof T2 })[]
72
+ ): SyncTransform<T2, T> {
73
+ return {
74
+ load: (value: T) => {
75
+ (keys as string[]).forEach((key) => {
76
+ const keyRemote = isObject(key) ? key.from : key;
77
+ const keyLocal = isObject(key) ? key.to : key;
78
+ const v = value[keyRemote];
79
+ if (!isNullOrUndefined(v)) {
80
+ value[keyLocal as keyof T] = isString(v) ? JSON.parse(v as string) : v;
81
+ }
82
+ if (keyLocal !== keyRemote) {
83
+ delete value[keyRemote];
84
+ }
85
+ });
86
+ return value as unknown as T2;
87
+ },
88
+ save: (value: T2) => {
89
+ (keys as string[]).forEach((key: string) => {
90
+ const keyRemote = isObject(key) ? key.from : key;
91
+ const keyLocal = isObject(key) ? key.to : key;
92
+ let v = (value as any)[keyLocal];
93
+ if (!isNullOrUndefined(v)) {
94
+ if (isArray(v)) {
95
+ v = v.filter((val) => !isNullOrUndefined(val));
96
+ }
97
+ value[keyRemote as keyof T2] =
98
+ isNumber(v) || isObject(v) || isArray(v) ? (JSON.stringify(v) as any) : v;
99
+ }
100
+ if (keyLocal !== keyRemote) {
101
+ delete value[keyLocal];
102
+ }
103
+ });
104
+ return value as unknown as T;
105
+ },
106
+ };
107
+ }
108
+
109
+ // TODO
110
+ export function combineTransforms<T, T2>(
111
+ transform1: SyncTransform<T2, T>,
112
+ ...transforms: Partial<SyncTransform<T2, T>>[]
113
+ ): SyncTransform<T2, T> {
114
+ return {
115
+ load: (value: T) => {
116
+ let inValue = transform1.load?.(value) as any;
117
+ transforms.forEach((transform) => {
118
+ if (transform.load) {
119
+ inValue = transform.load(inValue);
120
+ }
121
+ });
122
+ return inValue;
123
+ },
124
+ save: (value: T2) => {
125
+ let outValue = value as any;
126
+ transforms.forEach((transform) => {
127
+ if (transform.save) {
128
+ outValue = transform.save(outValue);
129
+ }
130
+ });
131
+ return transform1.save?.(outValue) ?? outValue;
132
+ },
133
+ };
134
+ }
135
+
136
+ export function syncedCrud<TRemote extends { id: string | number }, TLocal = TRemote>(
137
+ props: SyncedCrudPropsBase<TRemote, TLocal> & SyncedCrudPropsSingle<TRemote, TLocal>,
138
+ ): SyncedCrudReturnType<TLocal, 'first'>;
139
+ export function syncedCrud<
140
+ TRemote extends { id: string | number },
141
+ TLocal = TRemote,
142
+ TAsOption extends CrudAsOption = 'object',
143
+ >(
144
+ props: SyncedCrudPropsBase<TRemote, TLocal> & SyncedCrudPropsMany<TRemote, TLocal, TAsOption>,
145
+ ): SyncedCrudReturnType<TLocal, Exclude<TAsOption, 'first'>>;
146
+ export function syncedCrud<
147
+ TRemote extends { id: string | number },
148
+ TLocal = TRemote,
149
+ TAsOption extends CrudAsOption = 'object',
150
+ >(
151
+ props: SyncedCrudPropsBase<TRemote, TLocal> &
152
+ (SyncedCrudPropsSingle<TRemote, TLocal> & SyncedCrudPropsMany<TRemote, TLocal, TAsOption>),
153
+ ): SyncedCrudReturnType<TLocal, TAsOption> {
154
+ const {
155
+ get: getFn,
156
+ list: listFn,
157
+ create: createFn,
158
+ update: updateFn,
159
+ delete: deleteFn,
160
+ transform,
161
+ fieldCreatedAt,
162
+ fieldUpdatedAt,
163
+ updatePartial,
164
+ onSaved,
165
+ mode: modeParam,
166
+ changesSince,
167
+ ...rest
168
+ } = props;
169
+
170
+ let asType = props.as as TAsOption;
171
+
172
+ if (!asType) {
173
+ asType = (getFn ? 'first' : _asOption || 'object') as CrudAsOption as TAsOption;
174
+ }
175
+
176
+ const asMap = asType === 'Map';
177
+
178
+ const get: undefined | ((params: SyncedGetParams) => Promise<TLocal>) =
179
+ getFn || listFn
180
+ ? async (getParams: SyncedGetParams) => {
181
+ const { updateLastSync, lastSync } = getParams;
182
+ if (listFn) {
183
+ if (changesSince === 'last-sync' && lastSync) {
184
+ getParams.mode = modeParam || (asType === 'first' ? 'set' : 'assign');
185
+ }
186
+
187
+ const data = (await listFn(getParams)) || [];
188
+ let newLastSync = 0;
189
+ for (let i = 0; i < data.length; i++) {
190
+ const updated =
191
+ (data[i] as any)[fieldUpdatedAt as any] || (data[i] as any)[fieldCreatedAt as any];
192
+ if (updated) {
193
+ newLastSync = Math.max(newLastSync, +new Date(updated));
194
+ }
195
+ }
196
+ if (newLastSync && newLastSync !== lastSync) {
197
+ updateLastSync(newLastSync);
198
+ }
199
+ let transformed = data as unknown as TLocal[];
200
+ if (transform?.load) {
201
+ transformed = await Promise.all(data.map(transform.load));
202
+ }
203
+ if (asType === 'first') {
204
+ return transformed.length > 0 ? transformed[0] : null;
205
+ } else {
206
+ const out: Record<string, any> = asMap ? new Map() : {};
207
+ transformed.forEach((result: any) => {
208
+ const value = result.__deleted ? internal.symbolDelete : result;
209
+ asMap ? (out as Map<any, any>).set(result.id, value) : (out[result.id] = value);
210
+ });
211
+ return out;
212
+ }
213
+ } else if (getFn) {
214
+ const data = await getFn(getParams);
215
+
216
+ let transformed = data as unknown as TLocal;
217
+ if (data) {
218
+ const newLastSync =
219
+ (data as any)[fieldUpdatedAt as any] || (data as any)[fieldCreatedAt as any];
220
+ if (newLastSync && newLastSync !== lastSync) {
221
+ updateLastSync(newLastSync);
222
+ }
223
+ if (transform?.load) {
224
+ transformed = await transform.load(data);
225
+ }
226
+ }
227
+
228
+ return transformed 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, TLocal>();
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
+ 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] }));
258
+ }
259
+ } else {
260
+ console.error('[legend-state]: added item without an id');
261
+ }
262
+ } else if (path.length === 0) {
263
+ const id = prevAtPath?.id;
264
+ if (id) {
265
+ deletes.set(id, prevAtPath);
266
+ }
267
+ }
268
+ } else {
269
+ let itemsChanged: any[] | undefined = undefined;
270
+ let isCreateGuess: boolean;
271
+ if (path.length === 0) {
272
+ isCreateGuess =
273
+ !(fieldCreatedAt || fieldUpdatedAt) &&
274
+ !(
275
+ (asMap
276
+ ? Array.from((valueAtPath as Map<any, any>).values())
277
+ : isArray(valueAtPath)
278
+ ? valueAtPath
279
+ : Object.values(valueAtPath)
280
+ )?.length > 0
281
+ );
282
+ itemsChanged = asMap
283
+ ? Array.from((valueAtPath as Map<any, any>).values())
284
+ : isArray(valueAtPath)
285
+ ? valueAtPath
286
+ : Object.values(valueAtPath);
287
+ } else {
288
+ const itemKey = path[0];
289
+ const itemValue = asMap ? value.get(itemKey) : value[itemKey];
290
+ isCreateGuess = !(fieldCreatedAt || fieldUpdatedAt) && path.length === 1 && !prevAtPath;
291
+ if (!itemValue) {
292
+ if (path.length === 1 && prevAtPath) {
293
+ if (deleteFn) {
294
+ deletes.set(itemKey, prevAtPath);
295
+ } else {
296
+ console.log('[legend-state] missing delete function');
297
+ }
298
+ }
299
+ } else {
300
+ itemsChanged = [itemValue];
301
+ }
302
+ }
303
+ itemsChanged?.forEach((item) => {
304
+ const isCreate = fieldCreatedAt
305
+ ? !item[fieldCreatedAt!]
306
+ : fieldUpdatedAt
307
+ ? !item[fieldUpdatedAt]
308
+ : isCreateGuess;
309
+ if (isCreate) {
310
+ if (createFn) {
311
+ creates.set(item.id, item);
312
+ } else {
313
+ console.log('[legend-state] missing create function');
314
+ }
315
+ } else {
316
+ if (updateFn) {
317
+ updates.set(item.id, item);
318
+ } else {
319
+ console.log('[legend-state] missing update function');
320
+ }
321
+ }
322
+ });
323
+ }
324
+ });
325
+
326
+ const saveResult = async (
327
+ itemKey: string,
328
+ input: TRemote,
329
+ data: CrudResult<TRemote>,
330
+ isCreate: boolean,
331
+ ) => {
332
+ if (data && onSaved) {
333
+ const dataLoaded: TLocal = (transform?.load ? transform.load(data as any) : data) as any;
334
+
335
+ const savedOut = onSaved(dataLoaded, input, isCreate);
336
+
337
+ if (savedOut) {
338
+ const createdAt = fieldCreatedAt ? savedOut[fieldCreatedAt as keyof TLocal] : undefined;
339
+ const updatedAt = fieldUpdatedAt ? savedOut[fieldUpdatedAt as keyof TLocal] : undefined;
340
+
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
+ }
350
+ }
351
+ };
352
+
353
+ return Promise.all([
354
+ ...Array.from(creates).map(([itemKey, itemValue]) => {
355
+ const createObj = transformOut(itemValue, transform?.save) as TRemote;
356
+ return createFn!(createObj, params).then((result) =>
357
+ saveResult(itemKey, createObj, result as any, true),
358
+ );
359
+ }),
360
+ ...Array.from(updates).map(([itemKey, itemValue]) => {
361
+ const toSave = updatePartial
362
+ ? diffObjects(asType === 'first' ? valuePrevious : valuePrevious[itemKey], itemValue)
363
+ : itemValue;
364
+ const changed = transformOut(toSave as TLocal, transform?.save) as TRemote;
365
+
366
+ if (Object.keys(changed).length > 0) {
367
+ return updateFn!(changed, params).then((result) =>
368
+ saveResult(itemKey, changed, result as any, false),
369
+ );
370
+ }
371
+ }),
372
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
373
+ ...Array.from(deletes).map(([_, itemValue]) => deleteFn!(itemValue as TRemote, params)),
374
+ ]);
375
+ }
376
+ : undefined;
377
+
378
+ return synced<any>({
379
+ set,
380
+ get,
381
+ mode: modeParam,
382
+ ...rest,
383
+ });
384
+ }
@@ -1,42 +1,72 @@
1
- import { Synced, SyncedOptions, SyncedSetParams, isString } from '@legendapp/state';
2
- import { synced } from '@legendapp/state/persist';
1
+ import { Selector, SyncTransform, Synced, SyncedOptions, SyncedSetParams, computeSelector } from '@legendapp/state';
2
+ import { synced } from '@legendapp/state/sync';
3
3
 
4
- export interface SyncedFetchProps extends Omit<SyncedOptions, 'get' | 'set'> {
5
- get: string | RequestInfo;
6
- set?: string | RequestInfo;
4
+ export interface SyncedFetchProps<TRemote, TLocal> extends Omit<SyncedOptions, 'get' | 'set' | 'transform'> {
5
+ get: Selector<string>;
6
+ set?: Selector<string>;
7
7
  getInit?: RequestInit;
8
8
  setInit?: RequestInit;
9
+ transform?: SyncTransform<TLocal, TRemote>;
9
10
  valueType?: 'arrayBuffer' | 'blob' | 'formData' | 'json' | 'text';
10
- onSetValueType?: 'arrayBuffer' | 'blob' | 'formData' | 'json' | 'text';
11
- onSet?: (params: SyncedSetParams<any>) => void;
11
+ onSavedValueType?: 'arrayBuffer' | 'blob' | 'formData' | 'json' | 'text';
12
+ onSaved?(saved: TLocal, input: TRemote): Partial<TLocal> | void;
12
13
  }
13
14
 
14
- export function syncedFetch<T>({
15
- get,
16
- set,
17
- getInit,
18
- setInit,
19
- valueType,
20
- onSet,
21
- onSetValueType,
22
- }: SyncedFetchProps): Synced<T> {
23
- const ret: SyncedOptions = {
24
- get: () => fetch(get, getInit).then((response) => response[valueType || 'json']()),
15
+ export function syncedFetch<TRemote, TLocal = TRemote>(props: SyncedFetchProps<TRemote, TLocal>): Synced<TLocal> {
16
+ const {
17
+ get: getParam,
18
+ set: setParam,
19
+ getInit,
20
+ setInit,
21
+ valueType,
22
+ onSaved,
23
+ onSavedValueType,
24
+ transform,
25
+ ...rest
26
+ } = props;
27
+ const get = async () => {
28
+ const url = computeSelector(getParam);
29
+ const response = await fetch(url, getInit);
30
+
31
+ if (!response.ok) {
32
+ throw new Error(response.statusText);
33
+ }
34
+
35
+ let value = await response[valueType || 'json']();
36
+
37
+ if (transform?.load) {
38
+ value = transform?.load(value);
39
+ }
40
+
41
+ return value;
25
42
  };
26
43
 
27
- if (set) {
28
- ret.set = async (params: SyncedSetParams<any>) => {
29
- const requestInfo = isString(set) ? ({ url: set } as RequestInfo) : set;
44
+ let set: ((params: SyncedSetParams<TRemote>) => void | Promise<any>) | undefined = undefined;
45
+ if (setParam) {
46
+ set = async ({ value, update }: SyncedSetParams<any>) => {
47
+ const url = computeSelector(setParam);
48
+
30
49
  const response = await fetch(
31
- Object.assign({ method: 'POST' }, requestInfo, { body: JSON.stringify(params.value) }),
32
- setInit,
50
+ url,
51
+ Object.assign({ method: 'POST' }, setInit, { body: JSON.stringify(value) }),
33
52
  );
34
- if (onSet) {
35
- params.value = response[onSetValueType || valueType || 'json']();
36
- onSet(params);
53
+ if (!response.ok) {
54
+ throw new Error(response.statusText);
55
+ }
56
+ if (onSaved) {
57
+ const responseValue = await response[onSavedValueType || valueType || 'json']();
58
+ const transformed = transform?.load ? await transform.load(responseValue) : responseValue;
59
+ const valueSave = onSaved(transformed, value);
60
+ update({
61
+ value: valueSave,
62
+ });
37
63
  }
38
64
  };
39
65
  }
40
66
 
41
- return synced(ret);
67
+ return synced({
68
+ ...rest,
69
+ get,
70
+ set,
71
+ });
42
72
  }