From 5dc0a75ccef66ded475b2f5a172d643c59973122 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Fri, 28 Nov 2025 11:04:36 -0800 Subject: [PATCH] misc fixes for grid service and resources - let backend compute default uncap levels - fix InfiniteScroll resource lifecycle (don't destroy on unmount) - improve party resource error handling with type narrowing - use SvelteMap/SvelteDate for reactivity --- .../resources/party.resource.svelte.ts | 69 ++++++++++++------- src/lib/components/InfiniteScroll.svelte | 8 +-- src/lib/services/grid.service.ts | 7 +- 3 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/lib/api/adapters/resources/party.resource.svelte.ts b/src/lib/api/adapters/resources/party.resource.svelte.ts index 4fa0c0e4..f1141f4e 100644 --- a/src/lib/api/adapters/resources/party.resource.svelte.ts +++ b/src/lib/api/adapters/resources/party.resource.svelte.ts @@ -7,7 +7,13 @@ * @module adapters/resources/party */ -import { PartyAdapter, partyAdapter, type CreatePartyParams, type UpdatePartyParams } from '../party.adapter' +import { SvelteDate, SvelteMap } from 'svelte/reactivity' +import { + PartyAdapter, + partyAdapter, + type CreatePartyParams, + type UpdatePartyParams +} from '../party.adapter' import type { Party } from '$lib/types/api/party' import type { AdapterError } from '../types' @@ -86,7 +92,7 @@ export class PartyResource { }) // Track active requests for cancellation - private activeRequests = new Map() + private activeRequests = new SvelteMap() constructor(options: PartyResourceOptions = {}) { this.adapter = options.adapter || partyAdapter @@ -103,14 +109,16 @@ export class PartyResource { const controller = new AbortController() this.activeRequests.set('load', controller) - this.current = { ...this.current, loading: true, error: undefined } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { error: _error, ...rest } = this.current + this.current = { ...rest, loading: true } try { const party = await this.adapter.getByShortcode(shortcode) this.current = { data: party, loading: false } return party - } catch (error: any) { - if (error.code !== 'CANCELLED') { + } catch (error: unknown) { + if (error && typeof error === 'object' && 'code' in error && error.code !== 'CANCELLED') { this.current = { ...this.current, loading: false, @@ -126,7 +134,9 @@ export class PartyResource { * Creates a new party */ async create(params: CreatePartyParams): Promise { - this.current = { ...this.current, updating: true, error: undefined } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { error: _error, ...rest } = this.current + this.current = { ...rest, updating: true } try { const party = await this.adapter.create(params) @@ -141,7 +151,7 @@ export class PartyResource { } return party - } catch (error: any) { + } catch (error: unknown) { this.current = { ...this.current, updating: false, @@ -159,7 +169,7 @@ export class PartyResource { const optimisticData = { ...this.current.data, ...params, - updatedAt: new Date().toISOString() + updatedAt: new SvelteDate().toISOString() } this.current = { ...this.current, @@ -175,15 +185,13 @@ export class PartyResource { this.current = { data: party, loading: false, updating: false } // Update in user parties list if present - const index = this.userParties.parties.findIndex( - p => p.shortcode === params.shortcode - ) + const index = this.userParties.parties.findIndex((p) => p.shortcode === params.shortcode) if (index !== -1) { this.userParties.parties[index] = party } return party - } catch (error: any) { + } catch (error: unknown) { // Revert optimistic update on error if (this.optimistic) { await this.load(params.shortcode) @@ -200,7 +208,9 @@ export class PartyResource { * Deletes the current party */ async delete(shortcode: string): Promise { - this.current = { ...this.current, updating: true, error: undefined } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { error: _error, ...rest } = this.current + this.current = { ...rest, updating: true } try { await this.adapter.delete(shortcode) @@ -209,15 +219,13 @@ export class PartyResource { this.current = { loading: false, updating: false } // Remove from user parties list - this.userParties.parties = this.userParties.parties.filter( - p => p.shortcode !== shortcode - ) + this.userParties.parties = this.userParties.parties.filter((p) => p.shortcode !== shortcode) if (this.userParties.total !== undefined && this.userParties.total > 0) { this.userParties.total-- } return true - } catch (error: any) { + } catch (error: unknown) { this.current = { ...this.current, updating: false, @@ -231,7 +239,9 @@ export class PartyResource { * Creates a remix (copy) of a party */ async remix(shortcode: string): Promise { - this.current = { ...this.current, updating: true, error: undefined } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { error: _error, ...rest } = this.current + this.current = { ...rest, updating: true } try { const party = await this.adapter.remix(shortcode) @@ -246,7 +256,7 @@ export class PartyResource { } return party - } catch (error: any) { + } catch (error: unknown) { this.current = { ...this.current, updating: false, @@ -268,7 +278,9 @@ export class PartyResource { const controller = new AbortController() this.activeRequests.set('userParties', controller) - this.userParties = { ...this.userParties, loading: true, error: undefined } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { error: _error, ...rest } = this.userParties + this.userParties = { ...rest, loading: true } try { const response = await this.adapter.listUserParties({ @@ -283,8 +295,8 @@ export class PartyResource { totalPages: response.totalPages, loading: false } - } catch (error: any) { - if (error.code !== 'CANCELLED') { + } catch (error: unknown) { + if (error && typeof error === 'object' && 'code' in error && error.code !== 'CANCELLED') { this.userParties = { ...this.userParties, loading: false, @@ -305,7 +317,9 @@ export class PartyResource { skills?: Array<{ id: string; slot: number }>, accessoryId?: string ): Promise { - this.current = { ...this.current, updating: true, error: undefined } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { error: _error, ...rest } = this.current + this.current = { ...rest, updating: true } try { // Update job first @@ -317,10 +331,13 @@ export class PartyResource { } // TODO: Handle accessory update when API supports it + if (accessoryId) { + party = await this.adapter.updateAccessory(partyId, accessoryId) + } this.current = { data: party, loading: false, updating: false } return party - } catch (error: any) { + } catch (error: unknown) { this.current = { ...this.current, updating: false, @@ -344,7 +361,7 @@ export class PartyResource { * Cancels all active requests */ cancelAll() { - this.activeRequests.forEach(controller => controller.abort()) + this.activeRequests.forEach((controller) => controller.abort()) this.activeRequests.clear() } @@ -386,4 +403,4 @@ export class PartyResource { */ export function createPartyResource(options?: PartyResourceOptions): PartyResource { return new PartyResource(options) -} \ No newline at end of file +} diff --git a/src/lib/components/InfiniteScroll.svelte b/src/lib/components/InfiniteScroll.svelte index d2d3c86f..7acf8d60 100644 --- a/src/lib/components/InfiniteScroll.svelte +++ b/src/lib/components/InfiniteScroll.svelte @@ -31,11 +31,9 @@ if (sentinel && resource) { resource.bindSentinel(sentinel) } - - // Cleanup on unmount - return () => { - resource?.destroy() - } + // Note: We intentionally don't destroy the resource here. + // The parent component owns the resource lifecycle and calls destroy() + // when appropriate (e.g., when filters change and a new resource is created). }) // Accessibility: Announce new content to screen readers diff --git a/src/lib/services/grid.service.ts b/src/lib/services/grid.service.ts index ba6ab333..fadb1c2a 100644 --- a/src/lib/services/grid.service.ts +++ b/src/lib/services/grid.service.ts @@ -1,7 +1,6 @@ import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party' import { gridAdapter } from '$lib/api/adapters/grid.adapter' import { partyAdapter } from '$lib/api/adapters/party.adapter' -import { getDefaultMaxUncapLevel } from '$lib/utils/uncap' export interface GridOperation { type: 'add' | 'replace' | 'remove' | 'move' | 'swap' @@ -38,12 +37,12 @@ export class GridService { options?: { mainhand?: boolean; shortcode?: string } ): Promise { try { + // Note: The backend computes the correct uncap level based on the weapon's FLB/ULB/transcendence flags const gridWeapon = await gridAdapter.createWeapon({ partyId, weaponId, position, mainhand: options?.mainhand, - uncapLevel: getDefaultMaxUncapLevel('weapon'), transcendenceStep: 0 }, this.buildHeaders(editKey)) @@ -207,13 +206,13 @@ export class GridService { editKey?: string, options?: { main?: boolean; friend?: boolean; shortcode?: string } ): Promise { + // Note: The backend computes the correct uncap level based on the summon's FLB/ULB/transcendence flags const gridSummon = await gridAdapter.createSummon({ partyId, summonId, position, main: options?.main, friend: options?.friend, - uncapLevel: getDefaultMaxUncapLevel('summon'), transcendenceStage: 0 }, this.buildHeaders(editKey)) @@ -356,11 +355,11 @@ export class GridService { options?: { shortcode?: string } ): Promise { try { + // Note: The backend computes the correct uncap level based on the character's special/FLB/ULB flags const gridCharacter = await gridAdapter.createCharacter({ partyId, characterId, position, - uncapLevel: getDefaultMaxUncapLevel('character'), transcendenceStep: 0 }, this.buildHeaders(editKey))