@legendapp/state 3.0.0-beta.46 → 3.0.0-beta.48

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.
@@ -9,6 +9,11 @@ var { waitForSet, runWithRetry } = sync.internal;
9
9
  function transformOut(data, transform) {
10
10
  return transform ? transform(clone(data)) : data;
11
11
  }
12
+ function warnMissingId(value) {
13
+ if (process.env.NODE_ENV === "development") {
14
+ console.warn("[legend-state] syncedCrud received an item without an id", value);
15
+ }
16
+ }
12
17
  function ensureId(obj, fieldId, generateId, node) {
13
18
  if (!obj[fieldId]) {
14
19
  obj[fieldId] = generateId();
@@ -43,40 +48,89 @@ function arrayToRecord(arr, keyField) {
43
48
  for (let i = 0; i < arr.length; i++) {
44
49
  const v = arr[i];
45
50
  const key = v[keyField];
46
- record[key] = v;
51
+ if (state.isNullOrUndefined(key)) {
52
+ warnMissingId(v);
53
+ } else {
54
+ record[key] = v;
55
+ }
47
56
  }
48
57
  }
49
58
  return record;
50
59
  }
51
- function retrySet(params, retry, action, itemKey, itemValue, change, queuedRetries, itemValueFull, actionFn, saveResult) {
52
- if (action === "delete") {
53
- if (queuedRetries.create.has(itemKey)) {
54
- queuedRetries.create.delete(itemKey);
55
- }
56
- if (queuedRetries.update.has(itemKey)) {
57
- queuedRetries.update.delete(itemKey);
58
- }
59
- } else {
60
- if (queuedRetries.delete.has(itemKey)) {
61
- queuedRetries.delete.delete(itemKey);
62
- }
60
+ function mergeQueuedRetryValue(currentValue, nextValue) {
61
+ if (currentValue && nextValue && typeof currentValue === "object" && typeof nextValue === "object") {
62
+ Object.assign(currentValue, nextValue);
63
+ return currentValue;
64
+ }
65
+ return nextValue;
66
+ }
67
+ function queueRetryValue(queuedRetries, itemKey, itemValue, itemValueFull) {
68
+ const queuedRetry = queuedRetries.get(itemKey);
69
+ if (queuedRetry) {
70
+ queuedRetry.value = mergeQueuedRetryValue(queuedRetry.value, itemValue);
71
+ queuedRetry.fullValue = mergeQueuedRetryValue(queuedRetry.fullValue, itemValueFull);
72
+ return queuedRetry;
63
73
  }
64
- const queuedRetry = queuedRetries[action].get(itemKey);
74
+ const nextQueuedRetry = {
75
+ value: itemValue,
76
+ fullValue: itemValueFull
77
+ };
78
+ queuedRetries.set(itemKey, nextQueuedRetry);
79
+ return nextQueuedRetry;
80
+ }
81
+ function cancelQueuedRetry(queuedRetries, itemKey) {
82
+ const queuedRetry = queuedRetries.get(itemKey);
65
83
  if (queuedRetry) {
66
- itemValue = Object.assign(queuedRetry, itemValue);
84
+ queuedRetry.cancelled = true;
85
+ queuedRetries.delete(itemKey);
86
+ }
87
+ }
88
+ function retrySet(params, retry, action, itemKey, itemValue, change, queuedRetries, itemValueFull, actionFn, saveResult, options) {
89
+ if (action === "delete") {
90
+ cancelQueuedRetry(queuedRetries.create, itemKey);
91
+ cancelQueuedRetry(queuedRetries.update, itemKey);
92
+ } else {
93
+ cancelQueuedRetry(queuedRetries.delete, itemKey);
67
94
  }
68
- queuedRetries[action].set(itemKey, itemValue);
69
- const clonedValue = clone(itemValueFull);
95
+ const queuedRetry = queueRetryValue(queuedRetries[action], itemKey, itemValue, itemValueFull);
70
96
  const paramsWithChanges = { ...params, changes: [change] };
71
- return runWithRetry(
72
- paramsWithChanges,
73
- retry,
74
- "create_" + itemKey,
75
- () => actionFn(itemValue, paramsWithChanges).then((result) => {
76
- queuedRetries[action].delete(itemKey);
77
- return saveResult(itemKey, clonedValue, result, true, change);
78
- })
79
- );
97
+ const runAttempt = () => {
98
+ var _a;
99
+ if (queuedRetry.cancelled) {
100
+ return Promise.resolve(void 0);
101
+ }
102
+ const refreshed = (_a = options == null ? void 0 : options.refreshValue) == null ? void 0 : _a.call(options);
103
+ const runWithRefreshedValue = (retryValue) => {
104
+ if (retryValue === false || queuedRetry.cancelled) {
105
+ queuedRetry.cancelled = true;
106
+ queuedRetries[action].delete(itemKey);
107
+ return Promise.resolve(void 0);
108
+ }
109
+ if (retryValue) {
110
+ queuedRetry.value = retryValue.value;
111
+ queuedRetry.fullValue = retryValue.fullValue;
112
+ }
113
+ const attemptValue = queuedRetry.value;
114
+ const attemptFullValue = clone(queuedRetry.fullValue);
115
+ return actionFn(attemptValue, paramsWithChanges).catch((error) => {
116
+ var _a2;
117
+ (_a2 = options == null ? void 0 : options.onAttemptFailure) == null ? void 0 : _a2.call(options);
118
+ throw error;
119
+ }).then(async (result) => {
120
+ queuedRetries[action].delete(itemKey);
121
+ if (queuedRetry.cancelled) {
122
+ return result;
123
+ }
124
+ await saveResult(itemKey, attemptFullValue, result, true, change);
125
+ return result;
126
+ });
127
+ };
128
+ return state.isPromise(refreshed) ? refreshed.then(runWithRefreshedValue) : runWithRefreshedValue(refreshed);
129
+ };
130
+ return runWithRetry(paramsWithChanges, retry, action + "_" + itemKey, runAttempt).catch((error) => {
131
+ params.update({ value: {}, failedChanges: [change] });
132
+ throw error;
133
+ });
80
134
  }
81
135
  function syncedCrud(props) {
82
136
  const {
@@ -102,12 +156,53 @@ function syncedCrud(props) {
102
156
  ...rest
103
157
  } = props;
104
158
  const fieldId = fieldIdProp || "id";
105
- const pendingCreates = /* @__PURE__ */ new Set();
159
+ const pendingCreates = /* @__PURE__ */ new Map();
106
160
  const queuedRetries = {
107
161
  create: /* @__PURE__ */ new Map(),
108
162
  update: /* @__PURE__ */ new Map(),
109
163
  delete: /* @__PURE__ */ new Map()
110
164
  };
165
+ const beginPendingCreate = (id, change) => {
166
+ const pendingCreate = pendingCreates.get(id) || { hasFailed: false };
167
+ pendingCreate.hasFailed = false;
168
+ pendingCreate.change || (pendingCreate.change = change);
169
+ pendingCreates.set(id, pendingCreate);
170
+ return pendingCreate;
171
+ };
172
+ const clearPendingCreate = (id) => {
173
+ if (!state.isNullOrUndefined(id)) {
174
+ pendingCreates.delete(id);
175
+ cancelQueuedRetry(queuedRetries.create, id);
176
+ }
177
+ };
178
+ const cancelFailedPendingCreate = (id) => {
179
+ var _a;
180
+ if (!state.isNullOrUndefined(id) && ((_a = pendingCreates.get(id)) == null ? void 0 : _a.hasFailed)) {
181
+ clearPendingCreate(id);
182
+ return true;
183
+ }
184
+ return false;
185
+ };
186
+ const clearPendingCreateForValue = (value) => {
187
+ if (value && typeof value === "object") {
188
+ clearPendingCreate(value[fieldId]);
189
+ }
190
+ };
191
+ const clearPendingCreatesForValues = (values) => {
192
+ if (values == null ? void 0 : values.length) {
193
+ for (let i = 0; i < values.length; i++) {
194
+ clearPendingCreateForValue(values[i]);
195
+ }
196
+ }
197
+ };
198
+ const markPendingCreateFailed = (itemKey) => {
199
+ const pendingCreate = pendingCreates.get(itemKey);
200
+ if (pendingCreate && !pendingCreate.hasFailed) {
201
+ pendingCreate.hasFailed = true;
202
+ cancelQueuedRetry(queuedRetries.update, itemKey);
203
+ cancelQueuedRetry(queuedRetries.delete, itemKey);
204
+ }
205
+ };
111
206
  let asType = props.as;
112
207
  if (!asType) {
113
208
  asType = getFn ? "value" : "object";
@@ -127,9 +222,19 @@ function syncedCrud(props) {
127
222
  if (asArray) {
128
223
  out.push(result);
129
224
  } else if (asMap) {
130
- out.set(value[fieldId], result);
225
+ const key = value[fieldId];
226
+ if (state.isNullOrUndefined(key)) {
227
+ warnMissingId(value);
228
+ } else {
229
+ out.set(key, result);
230
+ }
131
231
  } else {
132
- out[value[fieldId]] = result;
232
+ const key = value[fieldId];
233
+ if (state.isNullOrUndefined(key)) {
234
+ warnMissingId(value);
235
+ } else {
236
+ out[key] = result;
237
+ }
133
238
  }
134
239
  }
135
240
  }
@@ -167,6 +272,7 @@ function syncedCrud(props) {
167
272
  };
168
273
  const processResults = (data) => {
169
274
  data || (data = []);
275
+ clearPendingCreatesForValues(data);
170
276
  if (fieldUpdatedAt) {
171
277
  const newLastSync = computeLastSync(data, fieldUpdatedAt, fieldCreatedAt);
172
278
  if (newLastSync && newLastSync !== lastSync) {
@@ -183,6 +289,7 @@ function syncedCrud(props) {
183
289
  } else if (getFn) {
184
290
  const dataPromise = getFn(getParams);
185
291
  const processData = (data) => {
292
+ clearPendingCreateForValue(data);
186
293
  let transformed = data;
187
294
  if (data) {
188
295
  const newLastSync = data[fieldUpdatedAt] || data[fieldCreatedAt];
@@ -217,6 +324,32 @@ function syncedCrud(props) {
217
324
  !state.isNullOrUndefined(itemValue[fieldId]) ? { [fieldId]: itemValue[fieldId] } : {}
218
325
  ) : itemValue;
219
326
  };
327
+ const getCurrentValueAtKey = (itemKey) => {
328
+ const currentPeeked = state.getNodeValue(node);
329
+ if (asType === "value") {
330
+ return currentPeeked;
331
+ }
332
+ if (asType === "array") {
333
+ return state.isArray(currentPeeked) ? currentPeeked.find((value2) => (value2 == null ? void 0 : value2[fieldId]) === itemKey) : void 0;
334
+ }
335
+ if (asType === "Map") {
336
+ return currentPeeked == null ? void 0 : currentPeeked.get(itemKey);
337
+ }
338
+ return currentPeeked == null ? void 0 : currentPeeked[itemKey];
339
+ };
340
+ const isFailedCreateReadyToRetry = (itemKey) => {
341
+ const pendingCreate = pendingCreates.get(itemKey);
342
+ return !!(pendingCreate == null ? void 0 : pendingCreate.hasFailed) && !pendingCreate.promise;
343
+ };
344
+ const addCreate = (id, itemValue, change) => {
345
+ const existingPendingCreate = pendingCreates.get(id);
346
+ if ((existingPendingCreate == null ? void 0 : existingPendingCreate.hasFailed) && !existingPendingCreate.promise && change.path.length === 0) {
347
+ existingPendingCreate.change = change;
348
+ }
349
+ const pendingCreate = beginPendingCreate(id, change);
350
+ changesById.set(id, pendingCreate.change);
351
+ creates.set(id, itemValue);
352
+ };
220
353
  changes.forEach((change) => {
221
354
  var _a, _b;
222
355
  const { path, prevAtPath, valueAtPath, pathTypes } = change;
@@ -229,12 +362,14 @@ function syncedCrud(props) {
229
362
  const id = value[fieldId];
230
363
  if (!state.isNullOrUndefined(id)) {
231
364
  changesById.set(id, change);
232
- if (pendingCreates.has(id)) {
365
+ if (isFailedCreateReadyToRetry(id)) {
366
+ isCreate = true;
367
+ } else if (pendingCreates.has(id)) {
233
368
  isCreate = false;
234
369
  }
235
370
  if (isCreate || retryAsCreate) {
236
371
  if (createFn) {
237
- creates.set(id, value);
372
+ addCreate(id, value, change);
238
373
  } else {
239
374
  console.warn("[legend-state] missing create function");
240
375
  }
@@ -259,8 +394,11 @@ function syncedCrud(props) {
259
394
  console.error("[legend-state]: added synced item without an id");
260
395
  }
261
396
  } else if (path.length === 0) {
262
- deletes.add(prevAtPath);
263
- changesById.set(prevAtPath[fieldId], change);
397
+ const id = prevAtPath == null ? void 0 : prevAtPath[fieldId];
398
+ if (!cancelFailedPendingCreate(id)) {
399
+ deletes.add(prevAtPath);
400
+ changesById.set(prevAtPath[fieldId], change);
401
+ }
264
402
  }
265
403
  } else {
266
404
  let itemsChanged = [];
@@ -283,7 +421,11 @@ function syncedCrud(props) {
283
421
  for (let i = 0; i < lengthPrev; i++) {
284
422
  const key = keysPrev[i];
285
423
  if (!keysSet.has(key)) {
286
- deletes.add(prevAsObject[key]);
424
+ const prevValue = prevAsObject[key];
425
+ const id = prevValue == null ? void 0 : prevValue[fieldId];
426
+ if (!cancelFailedPendingCreate(id)) {
427
+ deletes.add(prevValue);
428
+ }
287
429
  }
288
430
  }
289
431
  for (let i = 0; i < length; i++) {
@@ -295,7 +437,8 @@ function syncedCrud(props) {
295
437
  return false;
296
438
  } else {
297
439
  const isDiff = !prevAsObject || !sync.deepEqual(value2, prev);
298
- if (isDiff) {
440
+ const isFailedCreateRetry = isFailedCreateReadyToRetry(value2 == null ? void 0 : value2[fieldId]);
441
+ if (isDiff || isFailedCreateRetry) {
299
442
  itemsChanged.push([getUpdateValue(value2, prev), prev, value2]);
300
443
  }
301
444
  }
@@ -305,8 +448,11 @@ function syncedCrud(props) {
305
448
  const itemValue = asMap ? value.get(itemKey) : value[itemKey];
306
449
  if (!itemValue) {
307
450
  if (path.length === 1 && prevAtPath) {
308
- deletes.add(prevAtPath);
309
- changesById.set(prevAtPath[fieldId], change);
451
+ const id = prevAtPath[fieldId];
452
+ if (!cancelFailedPendingCreate(id)) {
453
+ deletes.add(prevAtPath);
454
+ changesById.set(id, change);
455
+ }
310
456
  }
311
457
  } else {
312
458
  if (generateId) {
@@ -322,27 +468,26 @@ function syncedCrud(props) {
322
468
  }
323
469
  }
324
470
  itemsChanged == null ? void 0 : itemsChanged.forEach(([item, prev, fullValue]) => {
325
- const isCreate = !pendingCreates.has(item[fieldId]) && (fieldCreatedAt ? !item[fieldCreatedAt] && !(prev == null ? void 0 : prev[fieldCreatedAt]) : fieldUpdatedAt ? !item[fieldUpdatedAt] && !(prev == null ? void 0 : prev[fieldCreatedAt]) : state.isNullOrUndefined(prev));
471
+ const id = item[fieldId];
472
+ const isFailedCreateRetry = isFailedCreateReadyToRetry(id);
473
+ const isCreate = isFailedCreateRetry || !pendingCreates.has(id) && (fieldCreatedAt ? !item[fieldCreatedAt] && !(prev == null ? void 0 : prev[fieldCreatedAt]) : fieldUpdatedAt ? !item[fieldUpdatedAt] && !(prev == null ? void 0 : prev[fieldCreatedAt]) : state.isNullOrUndefined(prev));
326
474
  if (isCreate) {
327
- if (!item[fieldId]) {
475
+ if (!id) {
328
476
  console.error("[legend-state]: added item without an id");
329
477
  }
330
478
  if (createFn) {
331
- const id = item[fieldId];
332
- changesById.set(id, change);
333
- pendingCreates.add(id);
334
- creates.set(id, item);
479
+ addCreate(id, isFailedCreateRetry ? fullValue : item, change);
335
480
  } else {
336
481
  console.warn("[legend-state] missing create function");
337
482
  }
338
483
  } else {
339
484
  if (updateFn) {
340
- const id = item[fieldId];
341
- changesById.set(id, change);
342
- updates.set(id, updates.has(id) ? Object.assign(updates.get(id), item) : item);
485
+ const id2 = item[fieldId];
486
+ changesById.set(id2, change);
487
+ updates.set(id2, updates.has(id2) ? Object.assign(updates.get(id2), item) : item);
343
488
  updateFullValues.set(
344
- id,
345
- updateFullValues.has(id) ? Object.assign(updateFullValues.get(id), fullValue) : fullValue
489
+ id2,
490
+ updateFullValues.has(id2) ? Object.assign(updateFullValues.get(id2), fullValue) : fullValue
346
491
  );
347
492
  } else {
348
493
  console.warn("[legend-state] missing update function");
@@ -413,7 +558,28 @@ function syncedCrud(props) {
413
558
  await waitForSet(waitForSetParam, changes, itemValue, { type: "create" });
414
559
  }
415
560
  const createObj = await transformOut(itemValue, transform == null ? void 0 : transform.save);
416
- return retrySet(
561
+ const pendingCreate = pendingCreates.get(itemKey) || { hasFailed: false };
562
+ pendingCreates.set(itemKey, pendingCreate);
563
+ const refreshCreateValue = async () => {
564
+ const currentPendingCreate = pendingCreates.get(itemKey);
565
+ if (!currentPendingCreate) {
566
+ return false;
567
+ }
568
+ if (!currentPendingCreate.hasFailed) {
569
+ return void 0;
570
+ }
571
+ const currentValue = getCurrentValueAtKey(itemKey);
572
+ if (state.isNullOrUndefined(currentValue)) {
573
+ clearPendingCreate(itemKey);
574
+ return false;
575
+ }
576
+ const nextCreateObj = await transformOut(
577
+ clone(currentValue),
578
+ transform == null ? void 0 : transform.save
579
+ );
580
+ return { value: nextCreateObj, fullValue: nextCreateObj };
581
+ };
582
+ const createPromise = retrySet(
417
583
  params,
418
584
  retry,
419
585
  "create",
@@ -423,13 +589,31 @@ function syncedCrud(props) {
423
589
  queuedRetries,
424
590
  createObj,
425
591
  createFn,
426
- saveResult
427
- ).then(() => {
428
- pendingCreates.delete(itemKey);
592
+ saveResult,
593
+ {
594
+ onAttemptFailure: () => markPendingCreateFailed(itemKey),
595
+ refreshValue: refreshCreateValue
596
+ }
597
+ );
598
+ pendingCreate.promise = createPromise;
599
+ return createPromise.then((result) => {
600
+ if (pendingCreates.get(itemKey) === pendingCreate) {
601
+ pendingCreates.delete(itemKey);
602
+ }
603
+ return result;
604
+ }).catch((error) => {
605
+ if (pendingCreates.get(itemKey) === pendingCreate) {
606
+ pendingCreate.promise = void 0;
607
+ }
608
+ throw error;
429
609
  });
430
610
  }),
431
611
  // Handle updates
432
612
  ...Array.from(updates).map(async ([itemKey, itemValue]) => {
613
+ const pendingCreate = pendingCreates.get(itemKey);
614
+ if ((pendingCreate == null ? void 0 : pendingCreate.hasFailed) && pendingCreate.promise) {
615
+ await pendingCreate.promise;
616
+ }
433
617
  const fullValue = updateFullValues.get(itemKey);
434
618
  if (waitForSetParam) {
435
619
  await waitForSet(waitForSetParam, changes, fullValue, { type: "update" });
@@ -512,6 +696,7 @@ function syncedCrud(props) {
512
696
  paramsForUpdate.lastSync = newLastSync;
513
697
  }
514
698
  const rowsTransformed = (transform == null ? void 0 : transform.load) ? await transformRows(rows) : rows;
699
+ clearPendingCreatesForValues(rows);
515
700
  paramsForUpdate.value = resultsToOutType(rowsTransformed);
516
701
  params.update(paramsForUpdate);
517
702
  }