chore: remove unused resource pattern files (Phase 3)

removed ~27kb of completely unused code:
- search.resource.svelte.ts and test (custom query pattern)
- party.resource.svelte.ts (deprecated, unused)
- job.resource.svelte.ts (unused)
- CLEANUP_PLAN.md (outdated)

all functionality replaced by TanStack Query patterns in phases 1 & 2
This commit is contained in:
Justin Edmund 2025-11-29 03:39:50 -08:00
parent 149f30c538
commit 6ad3b65f7f
5 changed files with 0 additions and 1443 deletions

View file

@ -1,147 +0,0 @@
# Svelte-Main Branch Cleanup Plan
## Overview
This document outlines the remaining work needed to clean up the `svelte-main` branch and get the build green.
## Completed Fixes
### 1. Environment/Generated Module Issues
- Ran Paraglide codegen to generate translation files in `src/lib/paraglide/`
- Added type declarations for `$env/static/public` module in `src/lib/types/declarations.d.ts`
### 2. Broken Imports from Removed Legacy API Layer
- Updated `SearchSidebar.svelte` to use new adapter layer
- Updated `Party.svelte` to use new adapter layer
- Updated `teams/new/+page.svelte` to use new adapter layer
### 3. Type Shims for External Libraries
- Added comprehensive type declarations for `wx-svelte-grid` in `src/lib/types/declarations.d.ts`
### 4. RequestOptions Cache Type Incompatibility
- Fixed `RequestOptions` interface in `src/lib/api/adapters/types.ts` to exclude 'cache' from RequestInit extension
- Added both `cacheTime?: number` and `cache?: RequestCache` properties
- Updated `base.adapter.ts` to use `cacheTime` instead of `cache` for duration
### 5. Users Resource Module
- Updated `src/lib/api/resources/users.ts` to use `userAdapter` instead of removed `../core` module
- Changed function signature from `update(fetch, userId, params)` to `update(userId, params)`
### 6. UserSettingsModal.svelte Fixes
- Fixed Switch import path (case sensitivity: `switch.svelte` -> `Switch.svelte`)
- Fixed `users.update` call signature
- Removed invalid footer snippet definition
- Removed unused `Snippet` import
### 7. Character Type in Entity Adapter
- Added missing properties to Character type in `entity.adapter.ts`:
- `gender?: number`
- `proficiency?: number[]`
- `race?: number[]`
- `hp?: { minHp, maxHp, maxHpFlb }`
- `atk?: { minAtk, maxAtk, maxAtkFlb }`
- `uncap?: { flb, ulb, transcendence }`
### 8. Adapters Index File
- Created `src/lib/api/adapters/index.ts` to export all adapters and types
### 9. Character Type in Entities
- Added missing properties to Character type in `src/lib/types/api/entities.ts`:
- `gender`, `race`, `proficiency`, `hp`, `atk`
## Remaining Type Errors (~378 errors)
### High Priority (Most Impactful)
#### 1. 'firstItem' and 'item' Possibly Undefined (27 errors)
- **Location**: `src/routes/teams/new/+page.svelte`
- **Issue**: TypeScript strict null checks flagging array access without null guards
- **Fix**: Add null checks before accessing `items[0]` and in forEach loops
#### 2. PartyCtx Missing openPicker Property (8 errors)
- **Location**: Various components using party context
- **Issue**: `PartyCtx` type doesn't include `openPicker` method
- **Fix**: Update `PartyCtx` type definition to include `openPicker` method
#### 3. Missing Paraglide Translation Keys (18 errors)
- **Keys**: `context_view_details`, `context_replace`, `context_remove`
- **Location**: `src/lib/paraglide/messages`
- **Fix**: Add missing translation keys to `project.inlang/messages/en.json` and `ja.json`
#### 4. Summon/Weapon Missing hp/atk Properties (18 errors)
- **Location**: Entity adapter types
- **Issue**: Summon and Weapon types in `entity.adapter.ts` need hp/atk properties
- **Fix**: Update Summon type to include `hp` and `atk` nested objects
### Medium Priority
#### 5. exactOptionalPropertyTypes Violations (~15 errors)
- **Issue**: Props with `undefined` values being passed to components that don't accept undefined
- **Fix**: Update component Props interfaces to accept `undefined` for optional properties
#### 6. Select.svelte ItemIndicator Errors (4 errors)
- **Issue**: `Select.ItemIndicator` doesn't exist in bits-ui
- **Fix**: Check bits-ui documentation for correct component name or remove usage
#### 7. Button.svelte Icon Type Issues (2 errors)
- **Issue**: `icon` prop is `string | undefined` but Icon component expects `string`
- **Fix**: Add conditional rendering or default value for icon prop
#### 8. DropdownItem.svelte asChild Issue (2 errors)
- **Issue**: `asChild` prop doesn't exist on DropdownMenu.Item in bits-ui
- **Fix**: Use `child` snippet pattern instead of `asChild` prop
### Lower Priority
#### 9. maxLength vs maxlength (4 errors)
- **Issue**: HTML attribute should be lowercase `maxlength`
- **Fix**: Change `maxLength` to `maxlength` in input elements
#### 10. Button Variant "outlined" (3 errors)
- **Issue**: "outlined" is not a valid Button variant
- **Fix**: Use correct variant name (check Button component for valid variants)
#### 11. SearchResult Type Mismatch (5 errors)
- **Issue**: `SearchResult<any>[]` vs `SearchResult[]` type mismatch
- **Fix**: Update function signatures to use consistent SearchResult type
## Files Modified in This Session
1. `src/lib/api/adapters/types.ts` - RequestOptions cache fix
2. `src/lib/api/adapters/base.adapter.ts` - cacheTime usage
3. `src/lib/api/adapters/entity.adapter.ts` - Character type properties
4. `src/lib/api/adapters/index.ts` - New file for exports
5. `src/lib/api/resources/users.ts` - Updated to use userAdapter
6. `src/lib/types/declarations.d.ts` - wx-svelte-grid and $env type shims
7. `src/lib/types/api/entities.ts` - Character type properties
8. `src/lib/components/UserSettingsModal.svelte` - Multiple fixes
9. `src/lib/components/panels/SearchSidebar.svelte` - Accepted upstream version
10. `src/lib/components/party/Party.svelte` - granblueId fix
11. `src/routes/teams/new/+page.svelte` - Accepted upstream version
## Commands to Verify Progress
```bash
# Count remaining errors
pnpm check 2>&1 | grep -c "Error:"
# Analyze error patterns
pnpm check 2>&1 | grep "Error:" | sort | uniq -c | sort -rn | head -20
# Run lint
pnpm lint
# Run build
pnpm build
```
## Next Steps
1. Fix the 'firstItem'/'item' possibly undefined errors in teams/new/+page.svelte
2. Add missing Paraglide translation keys
3. Update PartyCtx type to include openPicker
4. Update Summon type in entity.adapter.ts to include hp/atk
5. Fix exactOptionalPropertyTypes violations
6. Fix bits-ui component usage (Select.ItemIndicator, DropdownItem asChild)
7. Run `pnpm check` to verify all errors are resolved
8. Run `pnpm lint` and `pnpm build`
9. Create PR with all fixes

View file

@ -1,250 +0,0 @@
/**
* Tests for SearchResource
*
* These tests verify the reactive search resource functionality
* including debouncing, state management, and cancellation.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { SearchResource } from '../search.resource.svelte'
import { SearchAdapter } from '../../search.adapter'
import type { SearchResponse } from '../../search.adapter'
describe('SearchResource', () => {
let resource: SearchResource
let mockAdapter: SearchAdapter
let originalFetch: typeof global.fetch
beforeEach(() => {
originalFetch = global.fetch
// Create mock adapter
mockAdapter = new SearchAdapter({
baseURL: 'https://api.example.com'
})
// Create resource with short debounce for testing
resource = new SearchResource({
adapter: mockAdapter,
debounceMs: 10,
initialParams: { locale: 'en' }
})
})
afterEach(() => {
global.fetch = originalFetch
resource.cancelAll()
vi.clearAllTimers()
})
describe('initialization', () => {
it('should initialize with empty state', () => {
expect(resource.weapons.loading).toBe(false)
expect(resource.weapons.data).toBeUndefined()
expect(resource.weapons.error).toBeUndefined()
expect(resource.characters.loading).toBe(false)
expect(resource.summons.loading).toBe(false)
expect(resource.all.loading).toBe(false)
})
it('should accept initial parameters', () => {
const customResource = new SearchResource({
initialParams: {
locale: 'ja',
page: 2
}
})
// We'll verify these are used in the search tests
expect(customResource).toBeDefined()
})
})
describe('search operations', () => {
it('should search weapons with debouncing', async () => {
const mockResponse: SearchResponse = {
results: [
{
id: '1',
granblueId: 'weapon_1',
name: { en: 'Test Weapon' },
element: 1,
rarity: 5,
searchableType: 'Weapon'
}
],
total: 1
}
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
results: [
{
id: '1',
granblue_id: 'weapon_1',
name: { en: 'Test Weapon' },
element: 1,
rarity: 5,
searchable_type: 'Weapon'
}
],
total: 1
})
})
// Start search
resource.searchWeapons({ query: 'test' })
// Should be loading immediately
expect(resource.weapons.loading).toBe(true)
// Wait for debounce + response
await new Promise(resolve => setTimeout(resolve, 50))
// Should have results
expect(resource.weapons.loading).toBe(false)
expect(resource.weapons.data).toEqual(mockResponse)
expect(resource.weapons.error).toBeUndefined()
// Verify API was called with merged params
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: JSON.stringify({
locale: 'en', // From initialParams
page: 1,
query: 'test'
})
})
)
})
it('should handle search errors', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
statusText: 'Server Error',
json: async () => ({ error: 'Internal error' })
})
resource.searchCharacters({ query: 'error' })
// Wait for debounce + response
await new Promise(resolve => setTimeout(resolve, 50))
expect(resource.characters.loading).toBe(false)
expect(resource.characters.data).toBeUndefined()
expect(resource.characters.error).toMatchObject({
code: 'SERVER_ERROR',
status: 500
})
})
it('should cancel previous search when new one starts', async () => {
let callCount = 0
global.fetch = vi.fn().mockImplementation(() => {
callCount++
return new Promise(resolve => {
setTimeout(() => {
resolve({
ok: true,
json: async () => ({ results: [] })
})
}, 100) // Slow response
})
})
// Start first search
resource.searchAll({ query: 'first' })
// Start second search before first completes
await new Promise(resolve => setTimeout(resolve, 20))
resource.searchAll({ query: 'second' })
// Wait for completion
await new Promise(resolve => setTimeout(resolve, 150))
// Should have made 2 calls but only the second one's result should be set
expect(callCount).toBe(2)
})
})
describe('state management', () => {
it('should clear specific search type', () => {
// Set some mock data
resource.weapons = {
loading: false,
data: { results: [] }
}
resource.characters = {
loading: false,
data: { results: [] }
}
// Clear weapons only
resource.clear('weapons')
expect(resource.weapons.data).toBeUndefined()
expect(resource.weapons.loading).toBe(false)
expect(resource.characters.data).toBeDefined() // Should remain
})
it('should clear all search results', () => {
// Set mock data for all types
resource.weapons = { loading: false, data: { results: [] } }
resource.characters = { loading: false, data: { results: [] } }
resource.summons = { loading: false, data: { results: [] } }
resource.all = { loading: false, data: { results: [] } }
// Clear all
resource.clearAll()
expect(resource.weapons.data).toBeUndefined()
expect(resource.characters.data).toBeUndefined()
expect(resource.summons.data).toBeUndefined()
expect(resource.all.data).toBeUndefined()
})
it('should update base parameters', () => {
resource.updateBaseParams({ locale: 'ja', per: 50 })
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ results: [] })
})
resource.searchWeapons({ query: 'test' })
// Wait for debounce
setTimeout(() => {
expect(global.fetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: expect.stringContaining('"locale":"ja"')
})
)
}, 50)
})
})
describe('cancellation', () => {
it('should cancel specific search type', () => {
const cancelSpy = vi.spyOn(resource, 'cancelSearch')
resource.clear('weapons')
expect(cancelSpy).toHaveBeenCalledWith('weapons')
})
it('should cancel all searches', () => {
const cancelAllSpy = vi.spyOn(resource, 'cancelAll')
resource.clearAll()
expect(cancelAllSpy).toHaveBeenCalled()
})
})
})

View file

@ -1,372 +0,0 @@
/**
* Reactive Job Resource using Svelte 5 Runes and Runed
*
* Provides reactive state management for job-related operations with
* automatic loading states, error handling, and caching.
*
* @module adapters/resources/job
*/
import { JobAdapter, jobAdapter } from '../job.adapter'
import type { Job, JobSkill, JobAccessory } from '$lib/types/api/entities'
import type { AdapterError, AdapterOptions } from '../types'
/**
* Job resource configuration options
*/
export interface JobResourceOptions {
/** Job adapter instance to use */
adapter?: JobAdapter
/** Cache duration in milliseconds */
cacheDuration?: number
}
/**
* State for job data
*/
interface JobState<T> {
data: T | undefined
loading: boolean
error: AdapterError | undefined
lastFetch: number | undefined
}
/**
* Creates a reactive job resource for job data management
* This is a Svelte 5 universal reactive state (works in both components and modules)
*
* @example
* ```svelte
* <script>
* import { createJobResource } from '$lib/api/adapters/resources/job.resource.svelte'
*
* const jobResource = createJobResource()
*
* // Fetch all jobs
* $effect(() => {
* jobResource.fetchJobs()
* })
* </script>
*
* {#if jobResource.jobs.loading}
* <p>Loading jobs...</p>
* {:else if jobResource.jobs.error}
* <p>Error: {jobResource.jobs.error.message}</p>
* {:else if jobResource.jobs.data}
* {#each jobResource.jobs.data as job}
* <div>{job.name.en}</div>
* {/each}
* {/if}
* ```
*/
export class JobResource {
// Private adapter instance
private adapter: JobAdapter
private cacheDuration: number
// Reactive state for job data
jobs = $state<JobState<Job[]>>({ loading: false, data: undefined, error: undefined, lastFetch: undefined })
currentJob = $state<JobState<Job>>({ loading: false, data: undefined, error: undefined, lastFetch: undefined })
jobSkills = $state<JobState<JobSkill[]>>({ loading: false, data: undefined, error: undefined, lastFetch: undefined })
jobAccessories = $state<JobState<JobAccessory[]>>({ loading: false, data: undefined, error: undefined, lastFetch: undefined })
allSkills = $state<JobState<JobSkill[]>>({ loading: false, data: undefined, error: undefined, lastFetch: undefined })
// Track active requests
private activeRequests = new Map<string, AbortController>()
constructor(options: JobResourceOptions = {}) {
this.adapter = options.adapter || jobAdapter
this.cacheDuration = options.cacheDuration || 5 * 60 * 1000 // 5 minutes default
}
/**
* Check if cached data is still valid
*/
private isCacheValid(state: JobState<any>): boolean {
if (!state.data || !state.lastFetch) return false
return Date.now() - state.lastFetch < this.cacheDuration
}
/**
* Fetch all jobs
*/
async fetchJobs(force = false): Promise<Job[]> {
// Return cached data if valid and not forced
if (!force && this.isCacheValid(this.jobs) && this.jobs.data) {
return this.jobs.data
}
// Cancel any existing request
this.cancelRequest('jobs')
// Create new abort controller
const controller = new AbortController()
this.activeRequests.set('jobs', controller)
// Update loading state
this.jobs = { ...this.jobs, loading: true, error: undefined }
try {
const data = await this.adapter.getAll()
this.jobs = { data, loading: false, lastFetch: Date.now(), error: undefined }
return data
} catch (error: any) {
if (error.code !== 'CANCELLED') {
this.jobs = {
...this.jobs,
loading: false,
error: error as AdapterError
}
}
throw error
} finally {
this.activeRequests.delete('jobs')
}
}
/**
* Fetch a single job by ID
*/
async fetchJob(id: string, force = false): Promise<Job> {
// Check if this job is already loaded
if (!force && this.currentJob.data?.id === id && this.isCacheValid(this.currentJob)) {
return this.currentJob.data
}
// Cancel any existing request
this.cancelRequest('currentJob')
// Create new abort controller
const controller = new AbortController()
this.activeRequests.set('currentJob', controller)
// Update loading state
this.currentJob = { ...this.currentJob, loading: true, error: undefined }
try {
const data = await this.adapter.getById(id)
this.currentJob = { data, loading: false, lastFetch: Date.now(), error: undefined }
return data
} catch (error: any) {
if (error.code !== 'CANCELLED') {
this.currentJob = {
...this.currentJob,
loading: false,
error: error as AdapterError
}
}
throw error
} finally {
this.activeRequests.delete('currentJob')
}
}
/**
* Fetch skills for a specific job
*/
async fetchJobSkills(jobId: string, force = false): Promise<JobSkill[]> {
// Cancel any existing request
this.cancelRequest('jobSkills')
// Create new abort controller
const controller = new AbortController()
this.activeRequests.set('jobSkills', controller)
// Update loading state
this.jobSkills = { ...this.jobSkills, loading: true, error: undefined }
try {
const data = await this.adapter.getSkills(jobId)
this.jobSkills = { data, loading: false, lastFetch: Date.now(), error: undefined }
return data
} catch (error: any) {
if (error.code !== 'CANCELLED') {
this.jobSkills = {
...this.jobSkills,
loading: false,
error: error as AdapterError
}
}
throw error
} finally {
this.activeRequests.delete('jobSkills')
}
}
/**
* Fetch accessories for a specific job
*/
async fetchJobAccessories(jobId: string, force = false): Promise<JobAccessory[]> {
// Cancel any existing request
this.cancelRequest('jobAccessories')
// Create new abort controller
const controller = new AbortController()
this.activeRequests.set('jobAccessories', controller)
// Update loading state
this.jobAccessories = { ...this.jobAccessories, loading: true, error: undefined }
try {
const data = await this.adapter.getAccessories(jobId)
this.jobAccessories = { data, loading: false, lastFetch: Date.now(), error: undefined }
return data
} catch (error: any) {
if (error.code !== 'CANCELLED') {
this.jobAccessories = {
...this.jobAccessories,
loading: false,
error: error as AdapterError
}
}
throw error
} finally {
this.activeRequests.delete('jobAccessories')
}
}
/**
* Fetch all available job skills
*/
async fetchAllSkills(force = false): Promise<JobSkill[]> {
// Return cached data if valid and not forced
if (!force && this.isCacheValid(this.allSkills) && this.allSkills.data) {
return this.allSkills.data
}
// Cancel any existing request
this.cancelRequest('allSkills')
// Create new abort controller
const controller = new AbortController()
this.activeRequests.set('allSkills', controller)
// Update loading state
this.allSkills = { ...this.allSkills, loading: true, error: undefined }
try {
const data = await this.adapter.getAllSkills()
this.allSkills = { data, loading: false, lastFetch: Date.now(), error: undefined }
return data
} catch (error: any) {
if (error.code !== 'CANCELLED') {
this.allSkills = {
...this.allSkills,
loading: false,
error: error as AdapterError
}
}
throw error
} finally {
this.activeRequests.delete('allSkills')
}
}
/**
* Update party job
*/
async updatePartyJob(partyId: string, jobId: string): Promise<void> {
await this.adapter.updatePartyJob(partyId, jobId)
}
/**
* Update party job skills
*/
async updatePartyJobSkills(
partyId: string,
skills: Array<{ id: string; slot: number }>
): Promise<void> {
await this.adapter.updatePartyJobSkills(partyId, skills)
}
/**
* Remove party job skill
*/
async removePartyJobSkill(partyId: string, slot: number): Promise<void> {
await this.adapter.removePartyJobSkill(partyId, slot)
}
/**
* Cancel an active request
*/
private cancelRequest(key: string) {
const controller = this.activeRequests.get(key)
if (controller) {
controller.abort()
this.activeRequests.delete(key)
}
}
/**
* Cancel all active requests
*/
cancelAll() {
this.activeRequests.forEach(controller => controller.abort())
this.activeRequests.clear()
}
/**
* Clear cached data
*/
clearCache() {
this.jobs = { loading: false, data: undefined, error: undefined, lastFetch: undefined }
this.currentJob = { loading: false, data: undefined, error: undefined, lastFetch: undefined }
this.jobSkills = { loading: false, data: undefined, error: undefined, lastFetch: undefined }
this.jobAccessories = { loading: false, data: undefined, error: undefined, lastFetch: undefined }
this.allSkills = { loading: false, data: undefined, error: undefined, lastFetch: undefined }
}
}
/**
* Create a new job resource instance
*/
export function createJobResource(options?: JobResourceOptions): JobResource {
return new JobResource(options)
}
/**
* Helper to group jobs by tier (row)
*/
export function groupJobsByTier(jobs: Job[]): Record<string, Job[]> {
const tiers: Record<string, Job[]> = {
'1': [],
'2': [],
'3': [],
'4': [],
'5': [],
'ex': [],
'ex2': []
}
for (const job of jobs) {
const tier = job.row.toString().toLowerCase()
if (tier in tiers && tiers[tier]) {
tiers[tier].push(job)
}
}
// Sort jobs within each tier by order
for (const tier in tiers) {
if (tiers[tier]) {
tiers[tier].sort((a, b) => a.order - b.order)
}
}
return tiers
}
/**
* Helper to get tier display name
*/
export function getTierDisplayName(tier: string): string {
const tierNames: Record<string, string> = {
'1': 'Class I',
'2': 'Class II',
'3': 'Class III',
'4': 'Class IV',
'5': 'Class V',
'ex': 'Extra',
'ex2': 'Extra II'
}
return tierNames[tier] || tier
}

View file

@ -1,427 +0,0 @@
/**
* Reactive Party Resource using Svelte 5 Runes
*
* Provides reactive state management for party operations with
* automatic loading states, error handling, and optimistic updates.
*
* @deprecated This resource class is deprecated in favor of TanStack Query.
* Use `partyQueries` from `$lib/api/queries/party.queries` and
* mutation hooks from `$lib/api/mutations/party.mutations` instead.
*
* Migration example:
* ```typescript
* // Before (PartyResource)
* const party = createPartyResource()
* party.load('ABC123')
* party.update({ shortcode: 'ABC123', name: 'New Name' })
*
* // After (TanStack Query)
* import { createQuery } from '@tanstack/svelte-query'
* import { partyQueries } from '$lib/api/queries/party.queries'
* import { useUpdateParty } from '$lib/api/mutations/party.mutations'
*
* const party = createQuery(() => partyQueries.byShortcode('ABC123'))
* const updateParty = useUpdateParty()
* updateParty.mutate({ shortcode: 'ABC123', name: 'New Name' })
* ```
*
* @module adapters/resources/party
*/
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'
/**
* Party resource configuration options
*/
export interface PartyResourceOptions {
/** Party adapter instance to use */
adapter?: PartyAdapter
/** Enable optimistic updates for mutations */
optimistic?: boolean
}
/**
* Resource state for a single party
*/
interface PartyState {
data?: Party
loading: boolean
error?: AdapterError
updating?: boolean
}
/**
* Resource state for party lists
*/
interface PartyListState {
parties: Party[]
total?: number
page?: number
totalPages?: number
loading: boolean
error?: AdapterError
}
/**
* Creates a reactive party resource for managing parties
*
* @example
* ```svelte
* <script>
* import { createPartyResource } from '$lib/api/adapters/resources/party.resource.svelte'
*
* const party = createPartyResource()
*
* // Load a party
* party.load('ABC123')
*
* // Update party details
* party.update({
* shortcode: 'ABC123',
* name: 'New Name'
* })
* </script>
*
* {#if party.current.loading}
* <p>Loading party...</p>
* {:else if party.current.error}
* <p>Error: {party.current.error.message}</p>
* {:else if party.current.data}
* <h1>{party.current.data.name}</h1>
* {/if}
* ```
*/
export class PartyResource {
private adapter: PartyAdapter
private optimistic: boolean
// Reactive state for current party
current = $state<PartyState>({ loading: false })
// Reactive state for user parties list
userParties = $state<PartyListState>({
parties: [],
loading: false
})
// Track active requests for cancellation
private activeRequests = new SvelteMap<string, AbortController>()
constructor(options: PartyResourceOptions = {}) {
this.adapter = options.adapter || partyAdapter
this.optimistic = options.optimistic ?? true
}
/**
* Loads a party by shortcode
*/
async load(shortcode: string): Promise<Party | undefined> {
// Cancel any existing load request
this.cancelRequest('load')
const controller = new AbortController()
this.activeRequests.set('load', controller)
// 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: unknown) {
if (error && typeof error === 'object' && 'code' in error && error.code !== 'CANCELLED') {
this.current = {
...this.current,
loading: false,
error: error as AdapterError
}
}
} finally {
this.activeRequests.delete('load')
}
}
/**
* Creates a new party
*/
async create(params: CreatePartyParams): Promise<Party | 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)
this.current = { data: party, loading: false, updating: false }
// Add to user parties if loaded
if (this.userParties.parties.length > 0) {
this.userParties.parties = [party, ...this.userParties.parties]
if (this.userParties.total !== undefined) {
this.userParties.total++
}
}
return party
} catch (error: unknown) {
this.current = {
...this.current,
updating: false,
error: error as AdapterError
}
}
}
/**
* Updates the current party
*/
async update(params: UpdatePartyParams): Promise<Party | undefined> {
// Optimistic update
if (this.optimistic && this.current.data) {
const optimisticData = {
...this.current.data,
...params,
updatedAt: new SvelteDate().toISOString()
}
this.current = {
...this.current,
data: optimisticData as Party,
updating: true
}
} else {
this.current = { ...this.current, updating: true }
}
try {
const party = await this.adapter.update(params)
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)
if (index !== -1) {
this.userParties.parties[index] = party
}
return party
} catch (error: unknown) {
// Revert optimistic update on error
if (this.optimistic) {
await this.load(params.shortcode)
}
this.current = {
...this.current,
updating: false,
error: error as AdapterError
}
}
}
/**
* Deletes the current party
*/
async delete(shortcode: string): Promise<boolean> {
// 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)
// Clear current party
this.current = { loading: false, updating: false }
// Remove from user parties list
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: unknown) {
this.current = {
...this.current,
updating: false,
error: error as AdapterError
}
return false
}
}
/**
* Creates a remix (copy) of a party
*/
async remix(shortcode: string): Promise<Party | 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)
this.current = { data: party, loading: false, updating: false }
// Add to user parties if it's the current user's remix
if (this.userParties.parties.length > 0) {
this.userParties.parties = [party, ...this.userParties.parties]
if (this.userParties.total !== undefined) {
this.userParties.total++
}
}
return party
} catch (error: unknown) {
this.current = {
...this.current,
updating: false,
error: error as AdapterError
}
}
}
/**
* Loads parties for a specific user
*/
async loadUserParties(
username: string,
params: Omit<Parameters<PartyAdapter['listUserParties']>[0], 'username'> = {}
): Promise<void> {
// Cancel any existing user parties request
this.cancelRequest('userParties')
const controller = new AbortController()
this.activeRequests.set('userParties', controller)
// 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({
username,
...params
})
this.userParties = {
parties: response.results,
total: response.total,
page: response.page,
totalPages: response.totalPages,
loading: false
}
} catch (error: unknown) {
if (error && typeof error === 'object' && 'code' in error && error.code !== 'CANCELLED') {
this.userParties = {
...this.userParties,
loading: false,
error: error as AdapterError
}
}
} finally {
this.activeRequests.delete('userParties')
}
}
/**
* Updates the job for the current party
*/
async updateJob(
partyId: string,
jobId: string,
skills?: Array<{ id: string; slot: number }>,
accessoryId?: string
): Promise<Party | 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
let party = await this.adapter.updateJob(partyId, jobId)
// Update skills if provided
if (skills) {
party = await this.adapter.updateJobSkills(partyId, skills)
}
// 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: unknown) {
this.current = {
...this.current,
updating: false,
error: error as AdapterError
}
}
}
/**
* Cancels an active request
*/
private cancelRequest(key: string) {
const controller = this.activeRequests.get(key)
if (controller) {
controller.abort()
this.activeRequests.delete(key)
}
}
/**
* Cancels all active requests
*/
cancelAll() {
this.activeRequests.forEach((controller) => controller.abort())
this.activeRequests.clear()
}
/**
* Clears the current party state
*/
clearCurrent() {
this.cancelRequest('load')
this.current = { loading: false }
}
/**
* Clears the user parties state
*/
clearUserParties() {
this.cancelRequest('userParties')
this.userParties = { parties: [], loading: false }
}
/**
* Clears all states
*/
clearAll() {
this.cancelAll()
this.current = { loading: false }
this.userParties = { parties: [], loading: false }
}
/**
* Clears the adapter's cache
*/
clearCache(shortcode?: string) {
this.adapter.clearPartyCache(shortcode)
}
}
/**
* Factory function for creating party resources
*/
export function createPartyResource(options?: PartyResourceOptions): PartyResource {
return new PartyResource(options)
}

View file

@ -1,247 +0,0 @@
/**
* Reactive Search Resource using Svelte 5 Runes and Runed
*
* Provides reactive state management for search operations with
* automatic loading states, error handling, and debouncing.
*
* @deprecated This resource class is deprecated in favor of TanStack Query.
* Use `searchQueries` from `$lib/api/queries/search.queries` with `createInfiniteQuery` instead.
*
* Migration example:
* ```typescript
* // Before (SearchResource)
* const search = createSearchResource({ debounceMs: 300 })
* search.searchWeapons({ query })
*
* // After (TanStack Query)
* import { createInfiniteQuery } from '@tanstack/svelte-query'
* import { searchQueries } from '$lib/api/queries/search.queries'
*
* let debouncedQuery = $state('')
* const weapons = createInfiniteQuery(() => searchQueries.weapons(debouncedQuery))
* ```
*
* @module adapters/resources/search
*/
import { useDebounce } from 'runed'
import { SearchAdapter, searchAdapter, type SearchParams, type SearchResponse } from '../search.adapter'
import type { AdapterError } from '../types'
/**
* Search resource configuration options
*/
export interface SearchResourceOptions {
/** Search adapter instance to use */
adapter?: SearchAdapter
/** Debounce delay in milliseconds for search queries */
debounceMs?: number
/** Initial search parameters */
initialParams?: SearchParams
}
/**
* Search result state for a specific entity type
*/
interface SearchState {
data?: SearchResponse
loading: boolean
error?: AdapterError
}
/**
* Creates a reactive search resource for entity searching
* This is a Svelte 5 universal reactive state (works in both components and modules)
*
* @example
* ```svelte
* <script>
* import { createSearchResource } from '$lib/api/adapters/resources/search.resource.svelte'
*
* const search = createSearchResource({
* debounceMs: 300,
* initialParams: {
* locale: 'en'
* }
* })
*
* let query = $state('')
*
* // Reactive search on query change
* $effect(() => {
* if (query) {
* search.searchWeapons({ query })
* }
* })
* </script>
*
* <input bind:value={query} placeholder="Search weapons..." />
*
* {#if search.weapons.loading}
* <p>Searching...</p>
* {:else if search.weapons.error}
* <p>Error: {search.weapons.error.message}</p>
* {:else if search.weapons.data}
* <ul>
* {#each search.weapons.data.results as result}
* <li>{result.name.en}</li>
* {/each}
* </ul>
* {/if}
* ```
*/
export class SearchResource {
// Private adapter instance
private adapter: SearchAdapter
// Base parameters for all searches
private baseParams: SearchParams
// Debounce delay
private debounceMs: number
// Reactive state for each search type
all = $state<SearchState>({ loading: false })
weapons = $state<SearchState>({ loading: false })
characters = $state<SearchState>({ loading: false })
summons = $state<SearchState>({ loading: false })
// Track active requests for cancellation
private activeRequests = new Map<string, AbortController>()
constructor(options: SearchResourceOptions = {}) {
this.adapter = options.adapter || searchAdapter
this.debounceMs = options.debounceMs || 300
this.baseParams = options.initialParams || {}
}
/**
* Creates a debounced search function for a specific entity type
*/
private createDebouncedSearch(
type: 'all' | 'weapons' | 'characters' | 'summons'
) {
const searchFn = async (params: SearchParams) => {
// Cancel any existing request for this type
this.cancelSearch(type)
// Create new abort controller
const controller = new AbortController()
this.activeRequests.set(type, controller)
// Update loading state
this[type] = { ...this[type], loading: true }
try {
// Merge base params with provided params
const mergedParams = { ...this.baseParams, ...params }
// Call appropriate adapter method
let response: SearchResponse
switch (type) {
case 'all':
response = await this.adapter.searchAll(mergedParams)
break
case 'weapons':
response = await this.adapter.searchWeapons(mergedParams)
break
case 'characters':
response = await this.adapter.searchCharacters(mergedParams)
break
case 'summons':
response = await this.adapter.searchSummons(mergedParams)
break
}
// Update state with results
this[type] = { data: response, loading: false }
} catch (error: any) {
// Don't update state if request was cancelled
if (error.code !== 'CANCELLED') {
this[type] = {
...this[type],
loading: false,
error: error as AdapterError
}
}
} finally {
this.activeRequests.delete(type)
}
}
// Create a debounced wrapper using useDebounce
const debouncedSearch = useDebounce(
(params: SearchParams) => searchFn(params),
() => this.debounceMs
)
// Return a function that calls the debounced search
return (params: SearchParams) => debouncedSearch(params)
}
// Create debounced search methods
searchAll = this.createDebouncedSearch('all')
searchWeapons = this.createDebouncedSearch('weapons')
searchCharacters = this.createDebouncedSearch('characters')
searchSummons = this.createDebouncedSearch('summons')
/**
* Cancels an active search request
*/
cancelSearch(type: 'all' | 'weapons' | 'characters' | 'summons') {
const controller = this.activeRequests.get(type)
if (controller) {
controller.abort()
this.activeRequests.delete(type)
}
}
/**
* Cancels all active search requests
*/
cancelAll() {
this.activeRequests.forEach(controller => controller.abort())
this.activeRequests.clear()
}
/**
* Clears results for a specific search type
*/
clear(type: 'all' | 'weapons' | 'characters' | 'summons') {
this.cancelSearch(type)
this[type] = { loading: false }
}
/**
* Clears all search results
*/
clearAll() {
this.cancelAll()
this.all = { loading: false }
this.weapons = { loading: false }
this.characters = { loading: false }
this.summons = { loading: false }
}
/**
* Clears the adapter's cache
*/
clearCache() {
this.adapter.clearSearchCache()
}
/**
* Updates base parameters for all searches
*/
updateBaseParams(params: SearchParams) {
this.baseParams = { ...this.baseParams, ...params }
}
}
/**
* Factory function for creating search resources
* Provides a more functional API if preferred over class instantiation
*/
export function createSearchResource(options?: SearchResourceOptions): SearchResource {
return new SearchResource(options)
}