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
This commit is contained in:
Justin Edmund 2025-11-28 11:04:36 -08:00
parent b1bfe82507
commit 5dc0a75cce
3 changed files with 49 additions and 35 deletions

View file

@ -7,7 +7,13 @@
* @module adapters/resources/party * @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 { Party } from '$lib/types/api/party'
import type { AdapterError } from '../types' import type { AdapterError } from '../types'
@ -86,7 +92,7 @@ export class PartyResource {
}) })
// Track active requests for cancellation // Track active requests for cancellation
private activeRequests = new Map<string, AbortController>() private activeRequests = new SvelteMap<string, AbortController>()
constructor(options: PartyResourceOptions = {}) { constructor(options: PartyResourceOptions = {}) {
this.adapter = options.adapter || partyAdapter this.adapter = options.adapter || partyAdapter
@ -103,14 +109,16 @@ export class PartyResource {
const controller = new AbortController() const controller = new AbortController()
this.activeRequests.set('load', controller) 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 { try {
const party = await this.adapter.getByShortcode(shortcode) const party = await this.adapter.getByShortcode(shortcode)
this.current = { data: party, loading: false } this.current = { data: party, loading: false }
return party return party
} catch (error: any) { } catch (error: unknown) {
if (error.code !== 'CANCELLED') { if (error && typeof error === 'object' && 'code' in error && error.code !== 'CANCELLED') {
this.current = { this.current = {
...this.current, ...this.current,
loading: false, loading: false,
@ -126,7 +134,9 @@ export class PartyResource {
* Creates a new party * Creates a new party
*/ */
async create(params: CreatePartyParams): Promise<Party | undefined> { async create(params: CreatePartyParams): Promise<Party | undefined> {
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 { try {
const party = await this.adapter.create(params) const party = await this.adapter.create(params)
@ -141,7 +151,7 @@ export class PartyResource {
} }
return party return party
} catch (error: any) { } catch (error: unknown) {
this.current = { this.current = {
...this.current, ...this.current,
updating: false, updating: false,
@ -159,7 +169,7 @@ export class PartyResource {
const optimisticData = { const optimisticData = {
...this.current.data, ...this.current.data,
...params, ...params,
updatedAt: new Date().toISOString() updatedAt: new SvelteDate().toISOString()
} }
this.current = { this.current = {
...this.current, ...this.current,
@ -175,15 +185,13 @@ export class PartyResource {
this.current = { data: party, loading: false, updating: false } this.current = { data: party, loading: false, updating: false }
// Update in user parties list if present // Update in user parties list if present
const index = this.userParties.parties.findIndex( const index = this.userParties.parties.findIndex((p) => p.shortcode === params.shortcode)
p => p.shortcode === params.shortcode
)
if (index !== -1) { if (index !== -1) {
this.userParties.parties[index] = party this.userParties.parties[index] = party
} }
return party return party
} catch (error: any) { } catch (error: unknown) {
// Revert optimistic update on error // Revert optimistic update on error
if (this.optimistic) { if (this.optimistic) {
await this.load(params.shortcode) await this.load(params.shortcode)
@ -200,7 +208,9 @@ export class PartyResource {
* Deletes the current party * Deletes the current party
*/ */
async delete(shortcode: string): Promise<boolean> { async delete(shortcode: string): Promise<boolean> {
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 { try {
await this.adapter.delete(shortcode) await this.adapter.delete(shortcode)
@ -209,15 +219,13 @@ export class PartyResource {
this.current = { loading: false, updating: false } this.current = { loading: false, updating: false }
// Remove from user parties list // Remove from user parties list
this.userParties.parties = this.userParties.parties.filter( this.userParties.parties = this.userParties.parties.filter((p) => p.shortcode !== shortcode)
p => p.shortcode !== shortcode
)
if (this.userParties.total !== undefined && this.userParties.total > 0) { if (this.userParties.total !== undefined && this.userParties.total > 0) {
this.userParties.total-- this.userParties.total--
} }
return true return true
} catch (error: any) { } catch (error: unknown) {
this.current = { this.current = {
...this.current, ...this.current,
updating: false, updating: false,
@ -231,7 +239,9 @@ export class PartyResource {
* Creates a remix (copy) of a party * Creates a remix (copy) of a party
*/ */
async remix(shortcode: string): Promise<Party | undefined> { async remix(shortcode: string): Promise<Party | undefined> {
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 { try {
const party = await this.adapter.remix(shortcode) const party = await this.adapter.remix(shortcode)
@ -246,7 +256,7 @@ export class PartyResource {
} }
return party return party
} catch (error: any) { } catch (error: unknown) {
this.current = { this.current = {
...this.current, ...this.current,
updating: false, updating: false,
@ -268,7 +278,9 @@ export class PartyResource {
const controller = new AbortController() const controller = new AbortController()
this.activeRequests.set('userParties', controller) 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 { try {
const response = await this.adapter.listUserParties({ const response = await this.adapter.listUserParties({
@ -283,8 +295,8 @@ export class PartyResource {
totalPages: response.totalPages, totalPages: response.totalPages,
loading: false loading: false
} }
} catch (error: any) { } catch (error: unknown) {
if (error.code !== 'CANCELLED') { if (error && typeof error === 'object' && 'code' in error && error.code !== 'CANCELLED') {
this.userParties = { this.userParties = {
...this.userParties, ...this.userParties,
loading: false, loading: false,
@ -305,7 +317,9 @@ export class PartyResource {
skills?: Array<{ id: string; slot: number }>, skills?: Array<{ id: string; slot: number }>,
accessoryId?: string accessoryId?: string
): Promise<Party | undefined> { ): Promise<Party | undefined> {
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 { try {
// Update job first // Update job first
@ -317,10 +331,13 @@ export class PartyResource {
} }
// TODO: Handle accessory update when API supports it // 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 } this.current = { data: party, loading: false, updating: false }
return party return party
} catch (error: any) { } catch (error: unknown) {
this.current = { this.current = {
...this.current, ...this.current,
updating: false, updating: false,
@ -344,7 +361,7 @@ export class PartyResource {
* Cancels all active requests * Cancels all active requests
*/ */
cancelAll() { cancelAll() {
this.activeRequests.forEach(controller => controller.abort()) this.activeRequests.forEach((controller) => controller.abort())
this.activeRequests.clear() this.activeRequests.clear()
} }
@ -386,4 +403,4 @@ export class PartyResource {
*/ */
export function createPartyResource(options?: PartyResourceOptions): PartyResource { export function createPartyResource(options?: PartyResourceOptions): PartyResource {
return new PartyResource(options) return new PartyResource(options)
} }

View file

@ -31,11 +31,9 @@
if (sentinel && resource) { if (sentinel && resource) {
resource.bindSentinel(sentinel) resource.bindSentinel(sentinel)
} }
// Note: We intentionally don't destroy the resource here.
// Cleanup on unmount // The parent component owns the resource lifecycle and calls destroy()
return () => { // when appropriate (e.g., when filters change and a new resource is created).
resource?.destroy()
}
}) })
// Accessibility: Announce new content to screen readers // Accessibility: Announce new content to screen readers

View file

@ -1,7 +1,6 @@
import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party' import type { Party, GridWeapon, GridSummon, GridCharacter } from '$lib/types/api/party'
import { gridAdapter } from '$lib/api/adapters/grid.adapter' import { gridAdapter } from '$lib/api/adapters/grid.adapter'
import { partyAdapter } from '$lib/api/adapters/party.adapter' import { partyAdapter } from '$lib/api/adapters/party.adapter'
import { getDefaultMaxUncapLevel } from '$lib/utils/uncap'
export interface GridOperation { export interface GridOperation {
type: 'add' | 'replace' | 'remove' | 'move' | 'swap' type: 'add' | 'replace' | 'remove' | 'move' | 'swap'
@ -38,12 +37,12 @@ export class GridService {
options?: { mainhand?: boolean; shortcode?: string } options?: { mainhand?: boolean; shortcode?: string }
): Promise<GridUpdateResult> { ): Promise<GridUpdateResult> {
try { try {
// Note: The backend computes the correct uncap level based on the weapon's FLB/ULB/transcendence flags
const gridWeapon = await gridAdapter.createWeapon({ const gridWeapon = await gridAdapter.createWeapon({
partyId, partyId,
weaponId, weaponId,
position, position,
mainhand: options?.mainhand, mainhand: options?.mainhand,
uncapLevel: getDefaultMaxUncapLevel('weapon'),
transcendenceStep: 0 transcendenceStep: 0
}, this.buildHeaders(editKey)) }, this.buildHeaders(editKey))
@ -207,13 +206,13 @@ export class GridService {
editKey?: string, editKey?: string,
options?: { main?: boolean; friend?: boolean; shortcode?: string } options?: { main?: boolean; friend?: boolean; shortcode?: string }
): Promise<Party> { ): Promise<Party> {
// Note: The backend computes the correct uncap level based on the summon's FLB/ULB/transcendence flags
const gridSummon = await gridAdapter.createSummon({ const gridSummon = await gridAdapter.createSummon({
partyId, partyId,
summonId, summonId,
position, position,
main: options?.main, main: options?.main,
friend: options?.friend, friend: options?.friend,
uncapLevel: getDefaultMaxUncapLevel('summon'),
transcendenceStage: 0 transcendenceStage: 0
}, this.buildHeaders(editKey)) }, this.buildHeaders(editKey))
@ -356,11 +355,11 @@ export class GridService {
options?: { shortcode?: string } options?: { shortcode?: string }
): Promise<GridUpdateResult> { ): Promise<GridUpdateResult> {
try { try {
// Note: The backend computes the correct uncap level based on the character's special/FLB/ULB flags
const gridCharacter = await gridAdapter.createCharacter({ const gridCharacter = await gridAdapter.createCharacter({
partyId, partyId,
characterId, characterId,
position, position,
uncapLevel: getDefaultMaxUncapLevel('character'),
transcendenceStep: 0 transcendenceStep: 0
}, this.buildHeaders(editKey)) }, this.buildHeaders(editKey))