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:
parent
149f30c538
commit
6ad3b65f7f
5 changed files with 0 additions and 1443 deletions
147
CLEANUP_PLAN.md
147
CLEANUP_PLAN.md
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in a new issue