From c37c4f0101401893a91a8a671670687eea8361b4 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 3 Dec 2025 07:37:03 -0800 Subject: [PATCH] fix request cancellation issue in batch add weapons/summons The addWeapons/addSummons methods were using Promise.all with Array.fill() which created arrays where all elements referenced the same object. This caused the request deduplication logic in BaseAdapter to cancel previous requests since they all had the same body/requestId. Fix: - Use Array.from() with spread to create unique object instances - Execute requests sequentially to avoid deduplication conflicts - Improve error handling in AddToCollectionModal to filter CancelledErrors --- src/lib/api/adapters/collection.adapter.ts | 20 ++++-- .../collection/AddToCollectionModal.svelte | 71 +++++++++++-------- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/src/lib/api/adapters/collection.adapter.ts b/src/lib/api/adapters/collection.adapter.ts index 74efa643..510cf614 100644 --- a/src/lib/api/adapters/collection.adapter.ts +++ b/src/lib/api/adapters/collection.adapter.ts @@ -203,13 +203,19 @@ export class CollectionAdapter extends BaseAdapter { inputs: Array ): Promise { // Expand inputs based on quantity + // Note: We create individual objects to ensure unique request IDs for deduplication const expanded = inputs.flatMap((input) => { const count = input.quantity ?? 1 // eslint-disable-next-line @typescript-eslint/no-unused-vars const { quantity, ...rest } = input - return Array(count).fill(rest) as CollectionWeaponInput[] + return Array.from({ length: count }, () => ({ ...rest })) as CollectionWeaponInput[] }) - return Promise.all(expanded.map((input) => this.addWeapon(input))) + // Execute sequentially to avoid request deduplication issues + const results: CollectionWeapon[] = [] + for (const input of expanded) { + results.push(await this.addWeapon(input)) + } + return results } /** @@ -282,13 +288,19 @@ export class CollectionAdapter extends BaseAdapter { inputs: Array ): Promise { // Expand inputs based on quantity + // Note: We create individual objects to ensure unique request IDs for deduplication const expanded = inputs.flatMap((input) => { const count = input.quantity ?? 1 // eslint-disable-next-line @typescript-eslint/no-unused-vars const { quantity, ...rest } = input - return Array(count).fill(rest) as CollectionSummonInput[] + return Array.from({ length: count }, () => ({ ...rest })) as CollectionSummonInput[] }) - return Promise.all(expanded.map((input) => this.addSummon(input))) + // Execute sequentially to avoid request deduplication issues + const results: CollectionSummon[] = [] + for (const input of expanded) { + results.push(await this.addSummon(input)) + } + return results } /** diff --git a/src/lib/components/collection/AddToCollectionModal.svelte b/src/lib/components/collection/AddToCollectionModal.svelte index bdc3a463..8e791eb7 100644 --- a/src/lib/components/collection/AddToCollectionModal.svelte +++ b/src/lib/components/collection/AddToCollectionModal.svelte @@ -219,42 +219,55 @@ } async function handleAdd() { + // Capture selected data before any state changes + const currentEntityType = entityType + const characterInputs = + currentEntityType === 'character' + ? Array.from(selectedIds).map((characterId) => ({ + characterId, + uncapLevel: 4, + transcendenceStep: 0 + })) + : [] + const weaponInputs = + currentEntityType === 'weapon' + ? Array.from(selectedQuantities.entries()).map(([weaponId, quantity]) => ({ + weaponId, + quantity, + uncapLevel: 3, + transcendenceStep: 0 + })) + : [] + const summonInputs = + currentEntityType === 'summon' + ? Array.from(selectedQuantities.entries()).map(([summonId, quantity]) => ({ + summonId, + quantity, + uncapLevel: 3, + transcendenceStep: 0 + })) + : [] + try { - if (entityType === 'character') { - if (selectedIds.size === 0) return - - const inputs = Array.from(selectedIds).map((characterId) => ({ - characterId, - uncapLevel: 4, - transcendenceStep: 0 - })) - await addCharacterMutation.mutateAsync(inputs) - } else if (entityType === 'weapon') { - if (selectedQuantities.size === 0) return - - const inputs = Array.from(selectedQuantities.entries()).map(([weaponId, quantity]) => ({ - weaponId, - quantity, - uncapLevel: 3, - transcendenceStep: 0 - })) - await addWeaponMutation.mutateAsync(inputs) + if (currentEntityType === 'character') { + if (characterInputs.length === 0) return + await addCharacterMutation.mutateAsync(characterInputs) + } else if (currentEntityType === 'weapon') { + if (weaponInputs.length === 0) return + await addWeaponMutation.mutateAsync(weaponInputs) } else { - if (selectedQuantities.size === 0) return - - const inputs = Array.from(selectedQuantities.entries()).map(([summonId, quantity]) => ({ - summonId, - quantity, - uncapLevel: 3, - transcendenceStep: 0 - })) - await addSummonMutation.mutateAsync(inputs) + if (summonInputs.length === 0) return + await addSummonMutation.mutateAsync(summonInputs) } + // Close modal after successful mutation open = false onOpenChange?.(false) } catch (error) { - console.error(`Failed to add ${entityNames[entityType].plural}:`, error) + // Only log non-cancellation errors + if (error && typeof error === 'object' && 'name' in error && error.name !== 'CancelledError') { + console.error(`Failed to add ${entityNames[currentEntityType].plural}:`, error) + } } }