@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.
- package/helpers/time.d.ts +2 -2
- package/index.d.ts +1 -1
- package/index.js +82 -31
- package/index.js.map +1 -1
- package/index.mjs +81 -32
- package/index.mjs.map +1 -1
- package/package.json +16 -1
- package/persist.js +122 -129
- package/persist.js.map +1 -1
- package/persist.mjs +122 -129
- 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 +9 -3
- package/src/computed.ts +4 -2
- package/src/globals.ts +17 -7
- package/src/helpers.ts +3 -3
- package/src/history/undoRedo.ts +111 -0
- package/src/is.ts +7 -0
- package/src/observableInterfaces.ts +6 -5
- package/src/observableTypes.ts +5 -0
- package/src/observe.ts +1 -1
- package/src/react/For.tsx +6 -6
- package/src/sync/activateSyncedNode.ts +9 -25
- package/src/sync/syncHelpers.ts +53 -12
- package/src/sync/syncObservable.ts +117 -101
- package/src/sync-plugins/crud.ts +384 -0
- package/src/sync-plugins/fetch.ts +57 -27
- package/src/sync-plugins/keel.ts +447 -0
- package/src/sync-plugins/supabase.ts +225 -0
- package/src/syncTypes.ts +12 -6
- package/src/when.ts +6 -1
- package/sync-plugins/crud.d.ts +40 -0
- package/sync-plugins/crud.js +275 -0
- package/sync-plugins/crud.js.map +1 -0
- package/sync-plugins/crud.mjs +271 -0
- package/sync-plugins/crud.mjs.map +1 -0
- package/sync-plugins/fetch.d.ts +8 -7
- package/sync-plugins/fetch.js +34 -12
- package/sync-plugins/fetch.js.map +1 -1
- package/sync-plugins/fetch.mjs +35 -13
- package/sync-plugins/fetch.mjs.map +1 -1
- package/sync-plugins/keel.d.ts +91 -0
- package/sync-plugins/keel.js +278 -0
- package/sync-plugins/keel.js.map +1 -0
- package/sync-plugins/keel.mjs +274 -0
- package/sync-plugins/keel.mjs.map +1 -0
- package/sync-plugins/supabase.d.ts +32 -0
- package/sync-plugins/supabase.js +134 -0
- package/sync-plugins/supabase.js.map +1 -0
- package/sync-plugins/supabase.mjs +131 -0
- package/sync-plugins/supabase.mjs.map +1 -0
- package/sync.d.ts +1 -0
- package/sync.js +157 -127
- package/sync.js.map +1 -1
- package/sync.mjs +156 -129
- 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,
|
|
2
|
-
import { synced } from '@legendapp/state/
|
|
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
|
|
6
|
-
set?: string
|
|
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
|
-
|
|
11
|
-
|
|
11
|
+
onSavedValueType?: 'arrayBuffer' | 'blob' | 'formData' | 'json' | 'text';
|
|
12
|
+
onSaved?(saved: TLocal, input: TRemote): Partial<TLocal> | void;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export function syncedFetch<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
setInit,
|
|
50
|
+
url,
|
|
51
|
+
Object.assign({ method: 'POST' }, setInit, { body: JSON.stringify(value) }),
|
|
33
52
|
);
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
67
|
+
return synced({
|
|
68
|
+
...rest,
|
|
69
|
+
get,
|
|
70
|
+
set,
|
|
71
|
+
});
|
|
42
72
|
}
|