feat: add TanStack Query v6 integration with SSR support (#441)
## Summary
This PR establishes the foundation for migrating from custom Svelte 5
resource classes to TanStack Query v6 for server state management. It
adds:
**Query Options Factories** (in `src/lib/api/queries/`):
- `party.queries.ts` - Party fetching with infinite scroll support
- `job.queries.ts` - Job and skill queries with pagination
- `user.queries.ts` - User profile, parties, and favorites queries
**Mutation Configurations** (in `src/lib/api/mutations/`):
- `party.mutations.ts` - Party CRUD with cache invalidation
- `grid.mutations.ts` - Weapon/character/summon mutations with
optimistic updates
- `job.mutations.ts` - Job and skill update mutations
**Deprecation Notices**:
- Added `@deprecated` JSDoc to `search.resource.svelte.ts` and
`party.resource.svelte.ts` with migration examples
**SSR Integration** (Phase 4):
- Created `+layout.ts` to initialize QueryClient for SSR support
- Updated `+layout.svelte` to receive QueryClient from load function
- Added SSR utilities in `src/lib/query/ssr.ts`:
- `withInitialData()` - for pages using +page.server.ts
- `prefetchQuery()` / `prefetchInfiniteQuery()` - for pages using
+page.ts
- `setQueryData()` - for direct cache population
- Added documentation in `src/lib/query/README.md`
**Component Wiring Examples** (Phase 5):
- `JobSelectionSidebar.svelte` - Migrated from `createJobResource()` to
`createQuery(() => jobQueries.list())`. Demonstrates client-side query
pattern with automatic loading/error states.
- `teams/[id]/+page.svelte` - Added `withInitialData()` pattern for SSR
integration. Server-fetched party data is used as initial cache value
with background refetching support.
**Migration Guide**:
- Added `src/lib/query/MIGRATION.md` with follow-up prompts for
remaining component migrations (JobSkillSelectionSidebar, search modal,
user profile, teams explore, Party mutations, resource class removal)
## Updates Since Last Revision
Fixed TypeScript type errors in the TanStack Query integration:
- `party.queries.ts`: Made `total` and `perPage` optional in
`PartyPageResult` interface to match adapter return type
- `ssr.ts`: Fixed `withInitialData` to properly handle null values using
`NonNullable<TData>` return type
- `job.mutations.ts`: Fixed slot indexing by casting through `unknown`
to `keyof typeof updatedSkills`
Type checks now pass for all files modified in this PR (16 remaining
errors are pre-existing project issues unrelated to this PR - paraglide
modules not generated, hooks.ts implicit anys).
## Review & Testing Checklist for Human
- [ ] **Verify app loads correctly**: The `+layout.ts` and
`+layout.svelte` changes are critical path - confirm the app still
renders
- [ ] **Test JobSelectionSidebar**: Open job selection sidebar and
verify jobs load correctly, search/filter works, and retry button works
on error
- [ ] **Test teams/[id] page**: Navigate to a party detail page and
verify it renders without loading flash (SSR data should be immediate)
- [ ] **Review type casts**: Check `job.mutations.ts:135` - the `as
unknown as keyof typeof` cast for slot indexing is a workaround for
jobSkills having string literal keys ('0', '1', '2', '3') while slot is
a number
- [ ] **Verify withInitialData behavior**: The `NonNullable<TData>`
return type change in `ssr.ts` should work correctly with `data.party`
which can be `Party | null`
**Recommended test plan**:
1. Run `pnpm install` to ensure dependencies are up to date
2. Start dev server and verify the app loads without errors
3. Navigate to a party detail page (`/teams/[shortcode]`) - should
render immediately without loading state
4. Open job selection sidebar (click job icon on a party you can edit) -
verify jobs load and filtering works
5. Test error handling by temporarily breaking network - verify retry
button appears
### Notes
- Pre-existing project issues remain (paraglide modules not generated,
hooks.ts implicit anys) - these are unrelated to this PR
- Local testing could not run due to missing node_modules (vite not
found) - project setup issue
- TanStack Query devtools installation was skipped due to Storybook
version conflicts
- The existing `search.queries.ts` file was used as the pattern
reference for new query factories
- SSR approach uses hybrid pattern: existing `+page.server.ts` files
work with `withInitialData()`, while new pages can use `prefetchQuery()`
in `+page.ts`
- Migration guide includes 6 follow-up prompts for completing the
remaining component migrations
**Link to Devin run**:
https://app.devin.ai/sessions/33e97a98ae3e415aa4dc35378cad3a2b
**Requested by**: Justin Edmund (justin@jedmund.com) / @jedmund
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Justin Edmund <justin@jedmund.com>
This commit is contained in:
parent
53405da7eb
commit
5764161803
15 changed files with 2294 additions and 40 deletions
|
|
@ -4,6 +4,27 @@
|
|||
* 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
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,23 @@
|
|||
* 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
|
||||
*/
|
||||
|
||||
|
|
@ -227,4 +244,4 @@ export class SearchResource {
|
|||
*/
|
||||
export function createSearchResource(options?: SearchResourceOptions): SearchResource {
|
||||
return new SearchResource(options)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
568
src/lib/api/mutations/grid.mutations.ts
Normal file
568
src/lib/api/mutations/grid.mutations.ts
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
/**
|
||||
* Grid Mutation Configurations
|
||||
*
|
||||
* Provides mutation configurations for grid item operations (weapons, characters, summons)
|
||||
* with cache invalidation and optimistic updates using TanStack Query v6.
|
||||
*
|
||||
* @module api/mutations/grid
|
||||
*/
|
||||
|
||||
import { useQueryClient, createMutation } from '@tanstack/svelte-query'
|
||||
import {
|
||||
gridAdapter,
|
||||
type CreateGridWeaponParams,
|
||||
type CreateGridCharacterParams,
|
||||
type CreateGridSummonParams,
|
||||
type UpdateUncapParams,
|
||||
type ResolveConflictParams
|
||||
} from '$lib/api/adapters/grid.adapter'
|
||||
import { partyKeys } from '$lib/api/queries/party.queries'
|
||||
import type { Party, GridWeapon, GridCharacter, GridSummon } from '$lib/types/api/party'
|
||||
|
||||
// ============================================================================
|
||||
// Weapon Mutations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create grid weapon mutation
|
||||
*
|
||||
* Adds a weapon to a party's grid.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useCreateGridWeapon } from '$lib/api/mutations/grid.mutations'
|
||||
*
|
||||
* const createWeapon = useCreateGridWeapon()
|
||||
*
|
||||
* function handleAddWeapon() {
|
||||
* createWeapon.mutate({
|
||||
* partyId: 'party-uuid',
|
||||
* weaponId: 'weapon-id',
|
||||
* position: 1
|
||||
* })
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useCreateGridWeapon() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: CreateGridWeaponParams) => gridAdapter.createWeapon(params),
|
||||
onSuccess: (_data, params) => {
|
||||
// Invalidate the party to refetch with new weapon
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(params.partyId) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update grid weapon mutation
|
||||
*
|
||||
* Updates a weapon in a party's grid with optimistic updates.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useUpdateGridWeapon } from '$lib/api/mutations/grid.mutations'
|
||||
*
|
||||
* const updateWeapon = useUpdateGridWeapon()
|
||||
*
|
||||
* function handleUpdateWeapon(id: string, partyShortcode: string) {
|
||||
* updateWeapon.mutate({
|
||||
* id,
|
||||
* partyShortcode,
|
||||
* updates: { element: 2 }
|
||||
* })
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useUpdateGridWeapon() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ id, updates }: { id: string; partyShortcode: string; updates: Partial<GridWeapon> }) =>
|
||||
gridAdapter.updateWeapon(id, updates),
|
||||
onMutate: async ({ id, partyShortcode, updates }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.weapons) {
|
||||
const updatedWeapons = previousParty.weapons.map((w) =>
|
||||
w.id === id ? { ...w, ...updates } : w
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
weapons: updatedWeapons
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete grid weapon mutation
|
||||
*
|
||||
* Removes a weapon from a party's grid.
|
||||
*/
|
||||
export function useDeleteGridWeapon() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: { id?: string; partyId: string; partyShortcode: string; position?: number }) =>
|
||||
gridAdapter.deleteWeapon({ id: params.id, partyId: params.partyId, position: params.position }),
|
||||
onMutate: async ({ partyShortcode, id, position }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.weapons) {
|
||||
const updatedWeapons = previousParty.weapons.filter((w) =>
|
||||
id ? w.id !== id : w.position !== position
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
weapons: updatedWeapons
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update weapon uncap mutation
|
||||
*
|
||||
* Updates a weapon's uncap level with optimistic updates.
|
||||
*/
|
||||
export function useUpdateWeaponUncap() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: UpdateUncapParams & { partyShortcode: string }) =>
|
||||
gridAdapter.updateWeaponUncap(params),
|
||||
onMutate: async ({ partyShortcode, id, uncapLevel, transcendenceStep }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.weapons) {
|
||||
const updatedWeapons = previousParty.weapons.map((w) =>
|
||||
w.id === id
|
||||
? {
|
||||
...w,
|
||||
uncapLevel,
|
||||
...(transcendenceStep !== undefined && { transcendenceStep })
|
||||
}
|
||||
: w
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
weapons: updatedWeapons
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve weapon conflict mutation
|
||||
*
|
||||
* Resolves conflicts when adding a weapon that conflicts with existing weapons.
|
||||
*/
|
||||
export function useResolveWeaponConflict() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: ResolveConflictParams & { partyShortcode: string }) =>
|
||||
gridAdapter.resolveWeaponConflict(params),
|
||||
onSuccess: (_data, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Character Mutations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create grid character mutation
|
||||
*
|
||||
* Adds a character to a party's grid.
|
||||
*/
|
||||
export function useCreateGridCharacter() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: CreateGridCharacterParams) => gridAdapter.createCharacter(params),
|
||||
onSuccess: (_data, params) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(params.partyId) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update grid character mutation
|
||||
*
|
||||
* Updates a character in a party's grid with optimistic updates.
|
||||
*/
|
||||
export function useUpdateGridCharacter() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ id, updates }: { id: string; partyShortcode: string; updates: Partial<GridCharacter> }) =>
|
||||
gridAdapter.updateCharacter(id, updates),
|
||||
onMutate: async ({ id, partyShortcode, updates }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.characters) {
|
||||
const updatedCharacters = previousParty.characters.map((c) =>
|
||||
c.id === id ? { ...c, ...updates } : c
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
characters: updatedCharacters
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete grid character mutation
|
||||
*
|
||||
* Removes a character from a party's grid.
|
||||
*/
|
||||
export function useDeleteGridCharacter() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: { id?: string; partyId: string; partyShortcode: string; position?: number }) =>
|
||||
gridAdapter.deleteCharacter({ id: params.id, partyId: params.partyId, position: params.position }),
|
||||
onMutate: async ({ partyShortcode, id, position }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.characters) {
|
||||
const updatedCharacters = previousParty.characters.filter((c) =>
|
||||
id ? c.id !== id : c.position !== position
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
characters: updatedCharacters
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update character uncap mutation
|
||||
*
|
||||
* Updates a character's uncap level with optimistic updates.
|
||||
*/
|
||||
export function useUpdateCharacterUncap() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: UpdateUncapParams & { partyShortcode: string }) =>
|
||||
gridAdapter.updateCharacterUncap(params),
|
||||
onMutate: async ({ partyShortcode, id, uncapLevel, transcendenceStep }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.characters) {
|
||||
const updatedCharacters = previousParty.characters.map((c) =>
|
||||
c.id === id
|
||||
? {
|
||||
...c,
|
||||
uncapLevel,
|
||||
...(transcendenceStep !== undefined && { transcendenceStep })
|
||||
}
|
||||
: c
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
characters: updatedCharacters
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve character conflict mutation
|
||||
*
|
||||
* Resolves conflicts when adding a character that conflicts with existing characters.
|
||||
*/
|
||||
export function useResolveCharacterConflict() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: ResolveConflictParams & { partyShortcode: string }) =>
|
||||
gridAdapter.resolveCharacterConflict(params),
|
||||
onSuccess: (_data, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Summon Mutations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create grid summon mutation
|
||||
*
|
||||
* Adds a summon to a party's grid.
|
||||
*/
|
||||
export function useCreateGridSummon() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: CreateGridSummonParams) => gridAdapter.createSummon(params),
|
||||
onSuccess: (_data, params) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(params.partyId) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update grid summon mutation
|
||||
*
|
||||
* Updates a summon in a party's grid with optimistic updates.
|
||||
*/
|
||||
export function useUpdateGridSummon() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ id, updates }: { id: string; partyShortcode: string; updates: Partial<GridSummon> }) =>
|
||||
gridAdapter.updateSummon(id, updates),
|
||||
onMutate: async ({ id, partyShortcode, updates }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.summons) {
|
||||
const updatedSummons = previousParty.summons.map((s) =>
|
||||
s.id === id ? { ...s, ...updates } : s
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
summons: updatedSummons
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete grid summon mutation
|
||||
*
|
||||
* Removes a summon from a party's grid.
|
||||
*/
|
||||
export function useDeleteGridSummon() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: { id?: string; partyId: string; partyShortcode: string; position?: number }) =>
|
||||
gridAdapter.deleteSummon({ id: params.id, partyId: params.partyId, position: params.position }),
|
||||
onMutate: async ({ partyShortcode, id, position }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.summons) {
|
||||
const updatedSummons = previousParty.summons.filter((s) =>
|
||||
id ? s.id !== id : s.position !== position
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
summons: updatedSummons
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update summon uncap mutation
|
||||
*
|
||||
* Updates a summon's uncap level with optimistic updates.
|
||||
*/
|
||||
export function useUpdateSummonUncap() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: UpdateUncapParams & { partyShortcode: string }) =>
|
||||
gridAdapter.updateSummonUncap(params),
|
||||
onMutate: async ({ partyShortcode, id, uncapLevel, transcendenceStep }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.summons) {
|
||||
const updatedSummons = previousParty.summons.map((s) =>
|
||||
s.id === id
|
||||
? {
|
||||
...s,
|
||||
uncapLevel,
|
||||
...(transcendenceStep !== undefined && { transcendenceStep })
|
||||
}
|
||||
: s
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
summons: updatedSummons
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update quick summon mutation
|
||||
*
|
||||
* Updates a summon's quick summon setting with optimistic updates.
|
||||
*/
|
||||
export function useUpdateQuickSummon() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: {
|
||||
id?: string
|
||||
partyId: string
|
||||
partyShortcode: string
|
||||
position?: number
|
||||
quickSummon: boolean
|
||||
}) =>
|
||||
gridAdapter.updateQuickSummon({
|
||||
id: params.id,
|
||||
partyId: params.partyId,
|
||||
position: params.position,
|
||||
quickSummon: params.quickSummon
|
||||
}),
|
||||
onMutate: async ({ partyShortcode, id, quickSummon }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(partyShortcode))
|
||||
|
||||
if (previousParty?.summons) {
|
||||
const updatedSummons = previousParty.summons.map((s) =>
|
||||
s.id === id ? { ...s, quickSummon } : s
|
||||
)
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), {
|
||||
...previousParty,
|
||||
summons: updatedSummons
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { partyShortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(partyShortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { partyShortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(partyShortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
184
src/lib/api/mutations/job.mutations.ts
Normal file
184
src/lib/api/mutations/job.mutations.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/**
|
||||
* Job Mutation Configurations
|
||||
*
|
||||
* Provides mutation configurations for job-related operations
|
||||
* with cache invalidation using TanStack Query v6.
|
||||
*
|
||||
* @module api/mutations/job
|
||||
*/
|
||||
|
||||
import { useQueryClient, createMutation } from '@tanstack/svelte-query'
|
||||
import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
||||
import { partyKeys } from '$lib/api/queries/party.queries'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
|
||||
/**
|
||||
* Update party job mutation
|
||||
*
|
||||
* Updates the job for a party with optimistic updates.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useUpdatePartyJob } from '$lib/api/mutations/job.mutations'
|
||||
*
|
||||
* const updateJob = useUpdatePartyJob()
|
||||
*
|
||||
* function handleJobSelect(jobId: string) {
|
||||
* updateJob.mutate({ shortcode: 'abc123', jobId })
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useUpdatePartyJob() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ shortcode, jobId }: { shortcode: string; jobId: string }) =>
|
||||
partyAdapter.updateJob(shortcode, jobId),
|
||||
onMutate: async ({ shortcode, jobId }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(shortcode))
|
||||
|
||||
// Optimistically update the job ID
|
||||
// Note: We don't have the full job object here, so we just update the ID
|
||||
// The full job will be fetched when the query is invalidated
|
||||
if (previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(shortcode), {
|
||||
...previousParty,
|
||||
job: previousParty.job ? { ...previousParty.job, id: jobId } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { shortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(shortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { shortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update party job skills mutation
|
||||
*
|
||||
* Updates the job skills for a party.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useUpdatePartyJobSkills } from '$lib/api/mutations/job.mutations'
|
||||
*
|
||||
* const updateSkills = useUpdatePartyJobSkills()
|
||||
*
|
||||
* function handleSkillsUpdate(skills: Array<{ id: string; slot: number }>) {
|
||||
* updateSkills.mutate({ shortcode: 'abc123', skills })
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useUpdatePartyJobSkills() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({
|
||||
shortcode,
|
||||
skills
|
||||
}: {
|
||||
shortcode: string
|
||||
skills: Array<{ id: string; slot: number }>
|
||||
}) => partyAdapter.updateJobSkills(shortcode, skills),
|
||||
onSuccess: (_data, { shortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove party job skill mutation
|
||||
*
|
||||
* Removes a job skill from a party.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useRemovePartyJobSkill } from '$lib/api/mutations/job.mutations'
|
||||
*
|
||||
* const removeSkill = useRemovePartyJobSkill()
|
||||
*
|
||||
* function handleRemoveSkill(slot: number) {
|
||||
* removeSkill.mutate({ shortcode: 'abc123', slot })
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useRemovePartyJobSkill() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ shortcode, slot }: { shortcode: string; slot: number }) =>
|
||||
partyAdapter.removeJobSkill(shortcode, slot),
|
||||
onMutate: async ({ shortcode, slot }) => {
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(shortcode))
|
||||
|
||||
// Optimistically remove the skill from the slot
|
||||
// Convert slot number to string key to match jobSkills type (0-3)
|
||||
if (previousParty?.jobSkills) {
|
||||
const updatedSkills = { ...previousParty.jobSkills }
|
||||
const key = String(slot) as unknown as keyof typeof updatedSkills
|
||||
delete updatedSkills[key]
|
||||
queryClient.setQueryData(partyKeys.detail(shortcode), {
|
||||
...previousParty,
|
||||
jobSkills: updatedSkills
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, { shortcode }, context) => {
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(shortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { shortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update party accessory mutation
|
||||
*
|
||||
* Updates the accessory for a party.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useUpdatePartyAccessory } from '$lib/api/mutations/job.mutations'
|
||||
*
|
||||
* const updateAccessory = useUpdatePartyAccessory()
|
||||
*
|
||||
* function handleAccessorySelect(accessoryId: string) {
|
||||
* updateAccessory.mutate({ shortcode: 'abc123', accessoryId })
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useUpdatePartyAccessory() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ shortcode, accessoryId }: { shortcode: string; accessoryId: string }) =>
|
||||
partyAdapter.updateAccessory(shortcode, accessoryId),
|
||||
onSuccess: (_data, { shortcode }) => {
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
313
src/lib/api/mutations/party.mutations.ts
Normal file
313
src/lib/api/mutations/party.mutations.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
/**
|
||||
* Party Mutation Configurations
|
||||
*
|
||||
* Provides mutation configurations for party CRUD operations
|
||||
* with cache invalidation and optimistic updates using TanStack Query v6.
|
||||
*
|
||||
* @module api/mutations/party
|
||||
*/
|
||||
|
||||
import { useQueryClient, createMutation } from '@tanstack/svelte-query'
|
||||
import {
|
||||
partyAdapter,
|
||||
type CreatePartyParams,
|
||||
type UpdatePartyParams
|
||||
} from '$lib/api/adapters/party.adapter'
|
||||
import { partyKeys } from '$lib/api/queries/party.queries'
|
||||
import { userKeys } from '$lib/api/queries/user.queries'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
|
||||
/**
|
||||
* Create party mutation
|
||||
*
|
||||
* Creates a new party and invalidates relevant caches.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useCreateParty } from '$lib/api/mutations/party.mutations'
|
||||
*
|
||||
* const createParty = useCreateParty()
|
||||
*
|
||||
* function handleCreate() {
|
||||
* createParty.mutate({ name: 'My Party', visibility: 'public' })
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useCreateParty() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: CreatePartyParams) => partyAdapter.create(params),
|
||||
onSuccess: (party) => {
|
||||
// Set the new party in cache
|
||||
queryClient.setQueryData(partyKeys.detail(party.shortcode), party)
|
||||
// Invalidate party lists to include the new party
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.lists() })
|
||||
// Invalidate user's party lists
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.all })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update party mutation
|
||||
*
|
||||
* Updates an existing party with optimistic updates.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useUpdateParty } from '$lib/api/mutations/party.mutations'
|
||||
*
|
||||
* const updateParty = useUpdateParty()
|
||||
*
|
||||
* function handleUpdate() {
|
||||
* updateParty.mutate({ shortcode: 'abc123', name: 'Updated Name' })
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useUpdateParty() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (params: UpdatePartyParams) => partyAdapter.update(params),
|
||||
onMutate: async (params) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(params.shortcode) })
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(params.shortcode))
|
||||
|
||||
// Optimistically update the cache
|
||||
if (previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(params.shortcode), {
|
||||
...previousParty,
|
||||
...params
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, params, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(params.shortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, params) => {
|
||||
// Always refetch after error or success
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(params.shortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete party mutation
|
||||
*
|
||||
* Deletes a party and removes it from all caches.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useDeleteParty } from '$lib/api/mutations/party.mutations'
|
||||
*
|
||||
* const deleteParty = useDeleteParty()
|
||||
*
|
||||
* function handleDelete(shortcode: string) {
|
||||
* deleteParty.mutate(shortcode)
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useDeleteParty() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (shortcode: string) => partyAdapter.delete(shortcode),
|
||||
onSuccess: (_data, shortcode) => {
|
||||
// Remove the party from cache
|
||||
queryClient.removeQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
// Invalidate party lists
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.lists() })
|
||||
// Invalidate user's party lists
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.all })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remix party mutation
|
||||
*
|
||||
* Creates a copy of an existing party.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useRemixParty } from '$lib/api/mutations/party.mutations'
|
||||
*
|
||||
* const remixParty = useRemixParty()
|
||||
*
|
||||
* function handleRemix(shortcode: string) {
|
||||
* remixParty.mutate(shortcode)
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useRemixParty() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (shortcode: string) => partyAdapter.remix(shortcode),
|
||||
onSuccess: (newParty) => {
|
||||
// Set the new party in cache
|
||||
queryClient.setQueryData(partyKeys.detail(newParty.shortcode), newParty)
|
||||
// Invalidate party lists to include the new party
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.lists() })
|
||||
// Invalidate user's party lists
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.all })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Favorite party mutation
|
||||
*
|
||||
* Adds a party to the user's favorites.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useFavoriteParty } from '$lib/api/mutations/party.mutations'
|
||||
*
|
||||
* const favoriteParty = useFavoriteParty()
|
||||
*
|
||||
* function handleFavorite(shortcode: string) {
|
||||
* favoriteParty.mutate(shortcode)
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useFavoriteParty() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (shortcode: string) => partyAdapter.favorite(shortcode),
|
||||
onMutate: async (shortcode) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(shortcode))
|
||||
|
||||
// Optimistically update the cache
|
||||
if (previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(shortcode), {
|
||||
...previousParty,
|
||||
favorited: true
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, shortcode, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(shortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, shortcode) => {
|
||||
// Invalidate favorites list
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.favorites() })
|
||||
// Refetch the party to get accurate state
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfavorite party mutation
|
||||
*
|
||||
* Removes a party from the user's favorites.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useUnfavoriteParty } from '$lib/api/mutations/party.mutations'
|
||||
*
|
||||
* const unfavoriteParty = useUnfavoriteParty()
|
||||
*
|
||||
* function handleUnfavorite(shortcode: string) {
|
||||
* unfavoriteParty.mutate(shortcode)
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useUnfavoriteParty() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (shortcode: string) => partyAdapter.unfavorite(shortcode),
|
||||
onMutate: async (shortcode) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousParty = queryClient.getQueryData<Party>(partyKeys.detail(shortcode))
|
||||
|
||||
// Optimistically update the cache
|
||||
if (previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(shortcode), {
|
||||
...previousParty,
|
||||
favorited: false
|
||||
})
|
||||
}
|
||||
|
||||
return { previousParty }
|
||||
},
|
||||
onError: (_err, shortcode, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousParty) {
|
||||
queryClient.setQueryData(partyKeys.detail(shortcode), context.previousParty)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, shortcode) => {
|
||||
// Invalidate favorites list
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.favorites() })
|
||||
// Refetch the party to get accurate state
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.detail(shortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate preview mutation
|
||||
*
|
||||
* Triggers regeneration of a party's preview image.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useRegeneratePreview } from '$lib/api/mutations/party.mutations'
|
||||
*
|
||||
* const regeneratePreview = useRegeneratePreview()
|
||||
*
|
||||
* function handleRegenerate(shortcode: string) {
|
||||
* regeneratePreview.mutate(shortcode)
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useRegeneratePreview() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (shortcode: string) => partyAdapter.regeneratePreview(shortcode),
|
||||
onSuccess: (_data, shortcode) => {
|
||||
// Invalidate preview status to trigger refetch
|
||||
queryClient.invalidateQueries({ queryKey: partyKeys.preview(shortcode) })
|
||||
}
|
||||
}))
|
||||
}
|
||||
182
src/lib/api/queries/job.queries.ts
Normal file
182
src/lib/api/queries/job.queries.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* Job Query Options Factory
|
||||
*
|
||||
* Provides type-safe, reusable query configurations for job operations
|
||||
* using TanStack Query v6 patterns.
|
||||
*
|
||||
* @module api/queries/job
|
||||
*/
|
||||
|
||||
import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query'
|
||||
import {
|
||||
jobAdapter,
|
||||
type SearchJobSkillsParams
|
||||
} from '$lib/api/adapters/job.adapter'
|
||||
import type { Job, JobSkill, JobAccessory } from '$lib/types/api/entities'
|
||||
|
||||
/**
|
||||
* Standard page result format for job skill infinite queries
|
||||
*/
|
||||
export interface JobSkillPageResult {
|
||||
results: JobSkill[]
|
||||
page: number
|
||||
totalPages: number
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Job query options factory
|
||||
*
|
||||
* Provides query configurations for all job-related operations.
|
||||
* These can be used with `createQuery`, `createInfiniteQuery`, or for prefetching.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
* import { jobQueries } from '$lib/api/queries/job.queries'
|
||||
*
|
||||
* // All jobs
|
||||
* const jobs = createQuery(() => jobQueries.list())
|
||||
*
|
||||
* // Skills for a specific job with infinite scroll
|
||||
* const skills = createInfiniteQuery(() => jobQueries.skills(jobId, { query: searchTerm }))
|
||||
* ```
|
||||
*/
|
||||
export const jobQueries = {
|
||||
/**
|
||||
* All jobs list query options
|
||||
*
|
||||
* @returns Query options for fetching all jobs
|
||||
*/
|
||||
list: () =>
|
||||
queryOptions({
|
||||
queryKey: ['jobs'] as const,
|
||||
queryFn: () => jobAdapter.getAll(),
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes - jobs rarely change
|
||||
gcTime: 1000 * 60 * 60 // 1 hour
|
||||
}),
|
||||
|
||||
/**
|
||||
* Single job query options
|
||||
*
|
||||
* @param id - Job ID
|
||||
* @returns Query options for fetching a single job
|
||||
*/
|
||||
byId: (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['jobs', id] as const,
|
||||
queryFn: () => jobAdapter.getById(id),
|
||||
enabled: !!id,
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
gcTime: 1000 * 60 * 60 // 1 hour
|
||||
}),
|
||||
|
||||
/**
|
||||
* Job skills query options (non-paginated)
|
||||
*
|
||||
* @param jobId - Job ID to fetch skills for
|
||||
* @returns Query options for fetching all skills for a job
|
||||
*/
|
||||
skillsByJob: (jobId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['jobs', jobId, 'skills'] as const,
|
||||
queryFn: () => jobAdapter.getSkills(jobId),
|
||||
enabled: !!jobId,
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
gcTime: 1000 * 60 * 60 // 1 hour
|
||||
}),
|
||||
|
||||
/**
|
||||
* Job skills search infinite query options
|
||||
*
|
||||
* @param jobId - Job ID to search skills for
|
||||
* @param params - Optional search parameters (query, filters, locale)
|
||||
* @returns Infinite query options for searching job skills
|
||||
*/
|
||||
skills: (
|
||||
jobId: string,
|
||||
params?: Omit<SearchJobSkillsParams, 'jobId' | 'page'>
|
||||
) =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: ['jobs', jobId, 'skills', 'search', params] as const,
|
||||
queryFn: async ({ pageParam }): Promise<JobSkillPageResult> => {
|
||||
const response = await jobAdapter.searchSkills({
|
||||
jobId,
|
||||
...params,
|
||||
page: pageParam
|
||||
})
|
||||
return {
|
||||
results: response.results,
|
||||
page: response.page,
|
||||
totalPages: response.totalPages,
|
||||
total: response.total
|
||||
}
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.page < lastPage.totalPages) {
|
||||
return lastPage.page + 1
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
enabled: !!jobId,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes - search results can change
|
||||
gcTime: 1000 * 60 * 15 // 15 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* Job accessories query options
|
||||
*
|
||||
* @param jobId - Job ID to fetch accessories for
|
||||
* @returns Query options for fetching job accessories
|
||||
*/
|
||||
accessories: (jobId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['jobs', jobId, 'accessories'] as const,
|
||||
queryFn: () => jobAdapter.getAccessories(jobId),
|
||||
enabled: !!jobId,
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
gcTime: 1000 * 60 * 60 // 1 hour
|
||||
}),
|
||||
|
||||
/**
|
||||
* All job skills query options (not filtered by job)
|
||||
*
|
||||
* @returns Query options for fetching all job skills
|
||||
*/
|
||||
allSkills: () =>
|
||||
queryOptions({
|
||||
queryKey: ['jobs', 'skills', 'all'] as const,
|
||||
queryFn: () => jobAdapter.getAllSkills(),
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
gcTime: 1000 * 60 * 60 // 1 hour
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query key helpers for cache invalidation
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { useQueryClient } from '@tanstack/svelte-query'
|
||||
* import { jobKeys } from '$lib/api/queries/job.queries'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* // Invalidate all jobs
|
||||
* queryClient.invalidateQueries({ queryKey: jobKeys.all })
|
||||
*
|
||||
* // Invalidate skills for a specific job
|
||||
* queryClient.invalidateQueries({ queryKey: jobKeys.skills('job-id') })
|
||||
* ```
|
||||
*/
|
||||
export const jobKeys = {
|
||||
all: ['jobs'] as const,
|
||||
lists: () => [...jobKeys.all] as const,
|
||||
detail: (id: string) => [...jobKeys.all, id] as const,
|
||||
skills: (jobId: string) => [...jobKeys.all, jobId, 'skills'] as const,
|
||||
skillsSearch: (jobId: string, params?: Omit<SearchJobSkillsParams, 'jobId' | 'page'>) =>
|
||||
[...jobKeys.skills(jobId), 'search', params] as const,
|
||||
accessories: (jobId: string) => [...jobKeys.all, jobId, 'accessories'] as const,
|
||||
allSkills: () => [...jobKeys.all, 'skills', 'all'] as const
|
||||
}
|
||||
185
src/lib/api/queries/party.queries.ts
Normal file
185
src/lib/api/queries/party.queries.ts
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
/**
|
||||
* Party Query Options Factory
|
||||
*
|
||||
* Provides type-safe, reusable query configurations for party operations
|
||||
* using TanStack Query v6 patterns.
|
||||
*
|
||||
* @module api/queries/party
|
||||
*/
|
||||
|
||||
import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query'
|
||||
import {
|
||||
partyAdapter,
|
||||
type ListUserPartiesParams
|
||||
} from '$lib/api/adapters/party.adapter'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
|
||||
/**
|
||||
* Standard page result format for infinite queries
|
||||
*/
|
||||
export interface PartyPageResult {
|
||||
results: Party[]
|
||||
page: number
|
||||
totalPages: number
|
||||
total?: number
|
||||
perPage?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for listing parties
|
||||
*/
|
||||
export interface ListPartiesParams {
|
||||
page?: number
|
||||
per?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Party query options factory
|
||||
*
|
||||
* Provides query configurations for all party-related operations.
|
||||
* These can be used with `createQuery`, `createInfiniteQuery`, or for prefetching.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
* import { partyQueries } from '$lib/api/queries/party.queries'
|
||||
*
|
||||
* // Single party by shortcode
|
||||
* const party = createQuery(() => partyQueries.byShortcode(shortcode))
|
||||
*
|
||||
* // Infinite list of parties
|
||||
* const parties = createInfiniteQuery(() => partyQueries.list())
|
||||
* ```
|
||||
*/
|
||||
export const partyQueries = {
|
||||
/**
|
||||
* Single party query options
|
||||
*
|
||||
* @param shortcode - Party shortcode identifier
|
||||
* @returns Query options for fetching a single party
|
||||
*/
|
||||
byShortcode: (shortcode: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['party', shortcode] as const,
|
||||
queryFn: () => partyAdapter.getByShortcode(shortcode),
|
||||
enabled: !!shortcode,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 30 // 30 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* Public parties list (explore page) infinite query options
|
||||
*
|
||||
* @param params - Optional pagination parameters
|
||||
* @returns Infinite query options for listing public parties
|
||||
*/
|
||||
list: (params?: Omit<ListPartiesParams, 'page'>) =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: ['parties', 'list', params] as const,
|
||||
queryFn: async ({ pageParam }): Promise<PartyPageResult> => {
|
||||
const response = await partyAdapter.list({
|
||||
...params,
|
||||
page: pageParam
|
||||
})
|
||||
return {
|
||||
results: response.results,
|
||||
page: response.page,
|
||||
totalPages: response.totalPages,
|
||||
total: response.total,
|
||||
perPage: response.perPage
|
||||
}
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.page < lastPage.totalPages) {
|
||||
return lastPage.page + 1
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes - parties change more frequently
|
||||
gcTime: 1000 * 60 * 15 // 15 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* User parties list infinite query options
|
||||
*
|
||||
* @param username - Username to fetch parties for
|
||||
* @param params - Optional filter parameters
|
||||
* @returns Infinite query options for listing user's parties
|
||||
*/
|
||||
userParties: (
|
||||
username: string,
|
||||
params?: Omit<ListUserPartiesParams, 'username' | 'page'>
|
||||
) =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: ['parties', 'user', username, params] as const,
|
||||
queryFn: async ({ pageParam }): Promise<PartyPageResult> => {
|
||||
const response = await partyAdapter.listUserParties({
|
||||
username,
|
||||
...params,
|
||||
page: pageParam
|
||||
})
|
||||
return {
|
||||
results: response.results,
|
||||
page: response.page,
|
||||
totalPages: response.totalPages,
|
||||
total: response.total,
|
||||
perPage: response.perPage
|
||||
}
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.page < lastPage.totalPages) {
|
||||
return lastPage.page + 1
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
enabled: !!username,
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
gcTime: 1000 * 60 * 15 // 15 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* Party preview status query options
|
||||
*
|
||||
* @param shortcode - Party shortcode identifier
|
||||
* @returns Query options for fetching party preview status
|
||||
*/
|
||||
previewStatus: (shortcode: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['party', shortcode, 'preview'] as const,
|
||||
queryFn: () => partyAdapter.getPreviewStatus(shortcode),
|
||||
enabled: !!shortcode,
|
||||
staleTime: 1000 * 30, // 30 seconds - preview status changes
|
||||
gcTime: 1000 * 60 * 5 // 5 minutes
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query key helpers for cache invalidation
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { useQueryClient } from '@tanstack/svelte-query'
|
||||
* import { partyKeys } from '$lib/api/queries/party.queries'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* // Invalidate a specific party
|
||||
* queryClient.invalidateQueries({ queryKey: partyKeys.detail('abc123') })
|
||||
*
|
||||
* // Invalidate all party lists
|
||||
* queryClient.invalidateQueries({ queryKey: partyKeys.lists() })
|
||||
* ```
|
||||
*/
|
||||
export const partyKeys = {
|
||||
all: ['parties'] as const,
|
||||
lists: () => [...partyKeys.all, 'list'] as const,
|
||||
list: (params?: ListPartiesParams) => [...partyKeys.lists(), params] as const,
|
||||
userLists: () => [...partyKeys.all, 'user'] as const,
|
||||
userList: (username: string, params?: Omit<ListUserPartiesParams, 'username'>) =>
|
||||
[...partyKeys.userLists(), username, params] as const,
|
||||
details: () => ['party'] as const,
|
||||
detail: (shortcode: string) => [...partyKeys.details(), shortcode] as const,
|
||||
preview: (shortcode: string) => [...partyKeys.detail(shortcode), 'preview'] as const
|
||||
}
|
||||
218
src/lib/api/queries/user.queries.ts
Normal file
218
src/lib/api/queries/user.queries.ts
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
/**
|
||||
* User Query Options Factory
|
||||
*
|
||||
* Provides type-safe, reusable query configurations for user operations
|
||||
* using TanStack Query v6 patterns.
|
||||
*
|
||||
* @module api/queries/user
|
||||
*/
|
||||
|
||||
import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query'
|
||||
import { userAdapter, type UserInfo, type UserProfile } from '$lib/api/adapters/user.adapter'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
|
||||
/**
|
||||
* Standard page result format for user parties infinite queries
|
||||
*/
|
||||
export interface UserPartiesPageResult {
|
||||
results: Party[]
|
||||
page: number
|
||||
totalPages: number
|
||||
total: number
|
||||
perPage: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard page result format for favorites infinite queries
|
||||
*/
|
||||
export interface FavoritesPageResult {
|
||||
items: Party[]
|
||||
page: number
|
||||
totalPages: number
|
||||
total: number
|
||||
perPage: number
|
||||
}
|
||||
|
||||
/**
|
||||
* User query options factory
|
||||
*
|
||||
* Provides query configurations for all user-related operations.
|
||||
* These can be used with `createQuery`, `createInfiniteQuery`, or for prefetching.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
* import { userQueries } from '$lib/api/queries/user.queries'
|
||||
*
|
||||
* // Current user
|
||||
* const currentUser = createQuery(() => userQueries.me())
|
||||
*
|
||||
* // User profile with parties
|
||||
* const profile = createQuery(() => userQueries.profile(username))
|
||||
*
|
||||
* // User's parties with infinite scroll
|
||||
* const parties = createInfiniteQuery(() => userQueries.parties(username))
|
||||
* ```
|
||||
*/
|
||||
export const userQueries = {
|
||||
/**
|
||||
* Current user query options
|
||||
*
|
||||
* @returns Query options for fetching the current authenticated user
|
||||
*/
|
||||
me: () =>
|
||||
queryOptions({
|
||||
queryKey: ['user', 'me'] as const,
|
||||
queryFn: () => userAdapter.getCurrentUser(),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 30 // 30 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* User info query options
|
||||
*
|
||||
* @param username - Username to fetch info for
|
||||
* @returns Query options for fetching user info
|
||||
*/
|
||||
info: (username: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['user', username, 'info'] as const,
|
||||
queryFn: () => userAdapter.getInfo(username),
|
||||
enabled: !!username,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 30 // 30 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* User profile query options (includes first page of parties)
|
||||
*
|
||||
* @param username - Username to fetch profile for
|
||||
* @returns Query options for fetching user profile
|
||||
*/
|
||||
profile: (username: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['user', username, 'profile'] as const,
|
||||
queryFn: () => userAdapter.getProfile(username),
|
||||
enabled: !!username,
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes - profile data changes
|
||||
gcTime: 1000 * 60 * 15 // 15 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* User parties infinite query options
|
||||
*
|
||||
* @param username - Username to fetch parties for
|
||||
* @returns Infinite query options for fetching user's parties
|
||||
*/
|
||||
parties: (username: string) =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: ['user', username, 'parties'] as const,
|
||||
queryFn: async ({ pageParam }): Promise<UserPartiesPageResult> => {
|
||||
const response = await userAdapter.getProfileParties(username, pageParam)
|
||||
return {
|
||||
results: response.results,
|
||||
page: response.page,
|
||||
totalPages: response.totalPages,
|
||||
total: response.total,
|
||||
perPage: response.perPage
|
||||
}
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.page < lastPage.totalPages) {
|
||||
return lastPage.page + 1
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
enabled: !!username,
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
gcTime: 1000 * 60 * 15 // 15 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* User favorites infinite query options
|
||||
*
|
||||
* @returns Infinite query options for fetching user's favorite parties
|
||||
*/
|
||||
favorites: () =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: ['user', 'favorites'] as const,
|
||||
queryFn: async ({ pageParam }): Promise<FavoritesPageResult> => {
|
||||
const response = await userAdapter.getFavorites({ page: pageParam })
|
||||
return {
|
||||
items: response.items,
|
||||
page: response.page,
|
||||
totalPages: response.totalPages,
|
||||
total: response.total,
|
||||
perPage: response.perPage
|
||||
}
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.page < lastPage.totalPages) {
|
||||
return lastPage.page + 1
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
staleTime: 1000 * 60 * 2, // 2 minutes
|
||||
gcTime: 1000 * 60 * 15 // 15 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* Username availability check query options
|
||||
*
|
||||
* @param username - Username to check availability for
|
||||
* @returns Query options for checking username availability
|
||||
*/
|
||||
checkUsername: (username: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['user', 'check', 'username', username] as const,
|
||||
queryFn: () => userAdapter.checkUsernameAvailability(username),
|
||||
enabled: !!username && username.length >= 3,
|
||||
staleTime: 1000 * 30, // 30 seconds - availability can change
|
||||
gcTime: 1000 * 60 * 5 // 5 minutes
|
||||
}),
|
||||
|
||||
/**
|
||||
* Email availability check query options
|
||||
*
|
||||
* @param email - Email to check availability for
|
||||
* @returns Query options for checking email availability
|
||||
*/
|
||||
checkEmail: (email: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['user', 'check', 'email', email] as const,
|
||||
queryFn: () => userAdapter.checkEmailAvailability(email),
|
||||
enabled: !!email && email.includes('@'),
|
||||
staleTime: 1000 * 30, // 30 seconds - availability can change
|
||||
gcTime: 1000 * 60 * 5 // 5 minutes
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query key helpers for cache invalidation
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { useQueryClient } from '@tanstack/svelte-query'
|
||||
* import { userKeys } from '$lib/api/queries/user.queries'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* // Invalidate current user
|
||||
* queryClient.invalidateQueries({ queryKey: userKeys.me() })
|
||||
*
|
||||
* // Invalidate a user's profile
|
||||
* queryClient.invalidateQueries({ queryKey: userKeys.profile('username') })
|
||||
* ```
|
||||
*/
|
||||
export const userKeys = {
|
||||
all: ['user'] as const,
|
||||
me: () => [...userKeys.all, 'me'] as const,
|
||||
info: (username: string) => [...userKeys.all, username, 'info'] as const,
|
||||
profile: (username: string) => [...userKeys.all, username, 'profile'] as const,
|
||||
parties: (username: string) => [...userKeys.all, username, 'parties'] as const,
|
||||
favorites: () => [...userKeys.all, 'favorites'] as const,
|
||||
checkUsername: (username: string) => [...userKeys.all, 'check', 'username', username] as const,
|
||||
checkEmail: (email: string) => [...userKeys.all, 'check', 'email', email] as const
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { createQuery } from '@tanstack/svelte-query'
|
||||
import type { Job } from '$lib/types/api/entities'
|
||||
import { createJobResource } from '$lib/api/adapters/resources/job.resource.svelte'
|
||||
import { jobQueries } from '$lib/api/queries/job.queries'
|
||||
import { getJobTierName, getJobTierOrder } from '$lib/utils/jobUtils'
|
||||
import JobItem from '../job/JobItem.svelte'
|
||||
import JobTierSelector from '../job/JobTierSelector.svelte'
|
||||
|
|
@ -17,14 +17,13 @@
|
|||
|
||||
let { currentJobId, onSelectJob }: Props = $props()
|
||||
|
||||
// Create job resource
|
||||
const jobResource = createJobResource()
|
||||
// TanStack Query v6: Use createQuery with thunk pattern for reactivity
|
||||
// Jobs are cached for 30 minutes and shared across all components
|
||||
const jobsQuery = createQuery(() => jobQueries.list())
|
||||
|
||||
// State
|
||||
// State for filtering (local UI state, not server state)
|
||||
let searchQuery = $state('')
|
||||
let selectedTiers = $state<Set<string>>(new Set(['4', '5', 'ex2', 'o1'])) // Default to IV, V, EXII, OI
|
||||
let loading = $state(false)
|
||||
let error = $state<string | undefined>()
|
||||
|
||||
// Available tiers with short labels for display
|
||||
const tiers = [
|
||||
|
|
@ -48,29 +47,11 @@
|
|||
selectedTiers = newSet
|
||||
}
|
||||
|
||||
// Fetch jobs on mount
|
||||
onMount(() => {
|
||||
loadJobs()
|
||||
})
|
||||
|
||||
async function loadJobs() {
|
||||
loading = true
|
||||
error = undefined
|
||||
|
||||
try {
|
||||
await jobResource.fetchJobs()
|
||||
} catch (e: any) {
|
||||
error = e.message || 'Failed to load jobs'
|
||||
console.error('Error loading jobs:', e)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter jobs based on search and filters
|
||||
// TanStack Query handles loading/error states, we just filter the data
|
||||
const filteredJobs = $derived(
|
||||
(() => {
|
||||
let jobs = jobResource.jobs.data || []
|
||||
let jobs = jobsQuery.data || []
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery) {
|
||||
|
|
@ -135,16 +116,16 @@
|
|||
</div>
|
||||
|
||||
<div class="jobs-container">
|
||||
{#if loading}
|
||||
{#if jobsQuery.isLoading}
|
||||
<div class="loading-state">
|
||||
<Icon name="loader-2" size={32} />
|
||||
<p>Loading jobs...</p>
|
||||
</div>
|
||||
{:else if error}
|
||||
{:else if jobsQuery.isError}
|
||||
<div class="error-state">
|
||||
<Icon name="alert-circle" size={32} />
|
||||
<p>{error}</p>
|
||||
<Button size="small" onclick={loadJobs}>Retry</Button>
|
||||
<p>{jobsQuery.error?.message || 'Failed to load jobs'}</p>
|
||||
<Button size="small" onclick={() => jobsQuery.refetch()}>Retry</Button>
|
||||
</div>
|
||||
{:else if Object.keys(filteredJobs).length === 0}
|
||||
<div class="empty-state">
|
||||
|
|
|
|||
217
src/lib/query/MIGRATION.md
Normal file
217
src/lib/query/MIGRATION.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# TanStack Query Migration Guide
|
||||
|
||||
This document contains follow-up prompts for migrating remaining components to TanStack Query v6.
|
||||
|
||||
## Migration Status
|
||||
|
||||
### Completed (PR #441)
|
||||
- Query options factories: `party.queries.ts`, `job.queries.ts`, `user.queries.ts`
|
||||
- Mutation configurations: `party.mutations.ts`, `grid.mutations.ts`, `job.mutations.ts`
|
||||
- SSR utilities: `withInitialData`, `prefetchQuery`, `prefetchInfiniteQuery`
|
||||
- Example components: `JobSelectionSidebar.svelte`, `teams/[id]/+page.svelte`
|
||||
|
||||
### Pending Migration
|
||||
|
||||
The following components still use direct adapter calls or resource classes and should be migrated in future PRs.
|
||||
|
||||
---
|
||||
|
||||
## Follow-Up Prompt 1: Job Skill Selection Sidebar
|
||||
|
||||
**Scope**: Migrate `JobSkillSelectionSidebar.svelte` to use TanStack Query
|
||||
|
||||
**Prompt**:
|
||||
```
|
||||
Migrate the JobSkillSelectionSidebar component to use TanStack Query v6.
|
||||
|
||||
The component currently uses InfiniteScrollResource for paginated skill loading.
|
||||
Replace it with createInfiniteQuery using jobQueries.skills().
|
||||
|
||||
Files to modify:
|
||||
- src/lib/components/sidebar/JobSkillSelectionSidebar.svelte
|
||||
|
||||
Reference implementation:
|
||||
- src/lib/components/sidebar/JobSelectionSidebar.svelte (already migrated)
|
||||
- src/lib/api/queries/job.queries.ts (jobQueries.skills for infinite query)
|
||||
|
||||
Key changes:
|
||||
1. Replace InfiniteScrollResource with createInfiniteQuery
|
||||
2. Use jobQueries.skills(jobId, { query: searchTerm }) for the query options
|
||||
3. Handle pagination with query.fetchNextPage() and query.hasNextPage
|
||||
4. Update loading/error states to use query.isLoading, query.isError
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Follow-Up Prompt 2: Search Modal Migration
|
||||
|
||||
**Scope**: Migrate search functionality to use TanStack Query
|
||||
|
||||
**Prompt**:
|
||||
```
|
||||
Migrate the search functionality to use TanStack Query v6.
|
||||
|
||||
The existing search.queries.ts has infinite query options for weapons, characters,
|
||||
summons, and job skills. Wire these up to the actual search components.
|
||||
|
||||
Files to modify:
|
||||
- src/lib/components/search/SearchPanel.svelte (or equivalent)
|
||||
- src/lib/features/search/openSearchSidebar.svelte.ts
|
||||
|
||||
Reference:
|
||||
- src/lib/api/queries/search.queries.ts (existing query options)
|
||||
- src/lib/components/InfiniteScrollQuery.svelte (existing TanStack Query component)
|
||||
|
||||
Key changes:
|
||||
1. Use createInfiniteQuery with searchQueries.weapons/characters/summons
|
||||
2. Implement debounced search input (debounce the value, not the query)
|
||||
3. Use InfiniteScrollQuery component for rendering results
|
||||
4. Remove dependency on SearchResource class
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Follow-Up Prompt 3: User Profile Page
|
||||
|
||||
**Scope**: Migrate user profile page to use TanStack Query with SSR
|
||||
|
||||
**Prompt**:
|
||||
```
|
||||
Migrate the [username]/+page.svelte to use TanStack Query v6 with SSR.
|
||||
|
||||
The page currently fetches user profile and parties in +page.server.ts.
|
||||
Add TanStack Query integration using the withInitialData pattern.
|
||||
|
||||
Files to modify:
|
||||
- src/routes/[username]/+page.svelte
|
||||
|
||||
Reference:
|
||||
- src/routes/teams/[id]/+page.svelte (already migrated with withInitialData)
|
||||
- src/lib/api/queries/user.queries.ts (userQueries.profile, userQueries.parties)
|
||||
- src/lib/query/ssr.ts (withInitialData helper)
|
||||
|
||||
Key changes:
|
||||
1. Add createQuery for user profile with withInitialData
|
||||
2. Add createInfiniteQuery for user parties with initialData from server
|
||||
3. Use $derived to prefer query data over server data
|
||||
4. Enable background refetching for fresh data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Follow-Up Prompt 4: Teams Explore Page
|
||||
|
||||
**Scope**: Migrate teams explore page to use TanStack Query
|
||||
|
||||
**Prompt**:
|
||||
```
|
||||
Migrate the teams/explore page to use TanStack Query v6 for party listing.
|
||||
|
||||
The page displays a paginated list of public parties with filtering.
|
||||
|
||||
Files to modify:
|
||||
- src/routes/teams/explore/+page.svelte
|
||||
- src/routes/teams/explore/+page.server.ts (if needed)
|
||||
|
||||
Reference:
|
||||
- src/lib/api/queries/party.queries.ts (partyQueries.list for infinite query)
|
||||
- src/lib/query/ssr.ts (prefetchInfiniteQuery for SSR)
|
||||
|
||||
Key changes:
|
||||
1. Use createInfiniteQuery with partyQueries.list(filters)
|
||||
2. Implement filter state that triggers query refetch
|
||||
3. Use infinite scroll or "Load More" button with query.fetchNextPage()
|
||||
4. Consider prefetching first page in +page.ts for faster initial load
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Follow-Up Prompt 5: Party Component Mutations
|
||||
|
||||
**Scope**: Wire up Party component to use TanStack Query mutations
|
||||
|
||||
**Prompt**:
|
||||
```
|
||||
Migrate the Party component to use TanStack Query mutations for CRUD operations.
|
||||
|
||||
The Party component currently uses PartyService and GridService directly.
|
||||
Replace these with TanStack Query mutations for automatic cache invalidation.
|
||||
|
||||
Files to modify:
|
||||
- src/lib/components/party/Party.svelte
|
||||
|
||||
Reference:
|
||||
- src/lib/api/mutations/party.mutations.ts (useUpdateParty, useDeleteParty, etc.)
|
||||
- src/lib/api/mutations/grid.mutations.ts (useUpdateGridWeapon, etc.)
|
||||
- src/lib/api/mutations/job.mutations.ts (useUpdateJob, useUpdateSkills)
|
||||
|
||||
Key changes:
|
||||
1. Import and use mutation hooks (useUpdateParty, useDeleteParty, etc.)
|
||||
2. Replace direct service calls with mutation.mutate()
|
||||
3. Leverage optimistic updates for immediate UI feedback
|
||||
4. Use mutation.isPending for loading states
|
||||
5. Use mutation.error for error handling
|
||||
|
||||
Note: This is a larger refactor. Consider breaking it into sub-tasks:
|
||||
- 5a: Party metadata mutations (name, description, visibility)
|
||||
- 5b: Grid weapon mutations
|
||||
- 5c: Grid character mutations
|
||||
- 5d: Grid summon mutations
|
||||
- 5e: Job and skill mutations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Follow-Up Prompt 6: Remove Deprecated Resource Classes
|
||||
|
||||
**Scope**: Remove deprecated resource classes after migration is complete
|
||||
|
||||
**Prompt**:
|
||||
```
|
||||
Remove the deprecated resource classes now that TanStack Query migration is complete.
|
||||
|
||||
Files to delete:
|
||||
- src/lib/api/adapters/resources/search.resource.svelte.ts
|
||||
- src/lib/api/adapters/resources/party.resource.svelte.ts
|
||||
- src/lib/api/adapters/resources/job.resource.svelte.ts
|
||||
- src/lib/api/adapters/resources/infiniteScroll.resource.svelte.ts
|
||||
|
||||
Pre-requisites:
|
||||
1. Verify no components import these files (use grep)
|
||||
2. Ensure all functionality has been migrated to TanStack Query
|
||||
3. Run build to confirm no import errors
|
||||
|
||||
Steps:
|
||||
1. Search for any remaining imports of resource classes
|
||||
2. Migrate any remaining usages to TanStack Query
|
||||
3. Delete the resource files
|
||||
4. Update any barrel exports (index.ts files)
|
||||
5. Run build and tests to verify
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
Use this checklist to track overall migration progress:
|
||||
|
||||
- [x] Create query options factories
|
||||
- [x] Create mutation configurations
|
||||
- [x] Add SSR utilities
|
||||
- [x] Migrate JobSelectionSidebar (example)
|
||||
- [x] Migrate teams/[id] page (SSR example)
|
||||
- [ ] Migrate JobSkillSelectionSidebar
|
||||
- [ ] Migrate search functionality
|
||||
- [ ] Migrate user profile page
|
||||
- [ ] Migrate teams explore page
|
||||
- [ ] Migrate Party component mutations
|
||||
- [ ] Remove deprecated resource classes
|
||||
- [ ] Add TanStack Query devtools (optional)
|
||||
|
||||
## Notes
|
||||
|
||||
- Always test locally before pushing changes
|
||||
- Run `npm run build` to verify TypeScript compilation
|
||||
- The existing adapters remain unchanged - TanStack Query wraps them
|
||||
- Cache invalidation is handled automatically by mutations
|
||||
- SSR uses hybrid approach: existing +page.server.ts + withInitialData pattern
|
||||
119
src/lib/query/README.md
Normal file
119
src/lib/query/README.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# TanStack Query SSR Integration
|
||||
|
||||
This directory contains utilities for integrating TanStack Query v6 with SvelteKit's server-side rendering.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The project uses a hybrid approach for SSR:
|
||||
|
||||
1. **QueryClient in Layout**: The `QueryClient` is created in `+layout.ts` and passed to `+layout.svelte` via the load function. This enables prefetching in child page load functions.
|
||||
|
||||
2. **Server Data with initialData**: Pages that use `+page.server.ts` can pass server-fetched data as `initialData` to TanStack Query using the `withInitialData()` helper.
|
||||
|
||||
3. **Prefetching in +page.ts**: Pages that use `+page.ts` (universal load functions) can use `prefetchQuery()` to populate the QueryClient cache before rendering.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Pattern 1: Using Server Data with initialData
|
||||
|
||||
For pages that already fetch data in `+page.server.ts`:
|
||||
|
||||
```svelte
|
||||
<!-- +page.svelte -->
|
||||
<script lang="ts">
|
||||
import { createQuery } from '@tanstack/svelte-query'
|
||||
import { partyQueries } from '$lib/api/queries/party.queries'
|
||||
import { withInitialData } from '$lib/query/ssr'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data } = $props<{ data: PageData }>()
|
||||
|
||||
// Use server-fetched party as initial data
|
||||
// The query won't refetch until the data becomes stale
|
||||
const party = createQuery(() => ({
|
||||
...partyQueries.byShortcode(data.party?.shortcode ?? ''),
|
||||
...withInitialData(data.party),
|
||||
enabled: !!data.party?.shortcode
|
||||
}))
|
||||
</script>
|
||||
|
||||
{#if $party.data}
|
||||
<h1>{$party.data.name}</h1>
|
||||
{/if}
|
||||
```
|
||||
|
||||
### Pattern 2: Prefetching in Universal Load Functions
|
||||
|
||||
For pages that can use `+page.ts` (not server-only):
|
||||
|
||||
```typescript
|
||||
// +page.ts
|
||||
import type { PageLoad } from './$types'
|
||||
import { prefetchQuery } from '$lib/query/ssr'
|
||||
import { partyQueries } from '$lib/api/queries/party.queries'
|
||||
|
||||
export const load: PageLoad = async ({ parent, params }) => {
|
||||
const { queryClient } = await parent()
|
||||
|
||||
// Prefetch party data into the cache
|
||||
await prefetchQuery(queryClient, partyQueries.byShortcode(params.id))
|
||||
|
||||
// No need to return data - it's already in the QueryClient cache
|
||||
return { shortcode: params.id }
|
||||
}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!-- +page.svelte -->
|
||||
<script lang="ts">
|
||||
import { createQuery } from '@tanstack/svelte-query'
|
||||
import { partyQueries } from '$lib/api/queries/party.queries'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
let { data } = $props<{ data: PageData }>()
|
||||
|
||||
// Data is already in cache from prefetch - no loading state on initial render
|
||||
const party = createQuery(() => partyQueries.byShortcode(data.shortcode))
|
||||
</script>
|
||||
|
||||
{#if $party.data}
|
||||
<h1>{$party.data.name}</h1>
|
||||
{/if}
|
||||
```
|
||||
|
||||
### Pattern 3: Infinite Queries with Prefetching
|
||||
|
||||
For paginated data:
|
||||
|
||||
```typescript
|
||||
// +page.ts
|
||||
import type { PageLoad } from './$types'
|
||||
import { prefetchInfiniteQuery } from '$lib/query/ssr'
|
||||
import { partyQueries } from '$lib/api/queries/party.queries'
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { queryClient } = await parent()
|
||||
|
||||
// Prefetch first page of parties
|
||||
await prefetchInfiniteQuery(queryClient, partyQueries.list())
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Server-Only to TanStack Query
|
||||
|
||||
1. **Keep existing +page.server.ts** - No changes needed to server load functions
|
||||
2. **Add TanStack Query to component** - Use `createQuery` with `withInitialData`
|
||||
3. **Benefit from caching** - Subsequent navigations use cached data
|
||||
|
||||
### From Custom Resources to TanStack Query
|
||||
|
||||
1. **Replace resource imports** with query/mutation imports
|
||||
2. **Use createQuery** instead of resource state
|
||||
3. **Use mutations** for CRUD operations with automatic cache invalidation
|
||||
|
||||
## Files
|
||||
|
||||
- `queryClient.ts` - QueryClient factory (legacy, kept for reference)
|
||||
- `ssr.ts` - SSR utilities (withInitialData, prefetchQuery, etc.)
|
||||
190
src/lib/query/ssr.ts
Normal file
190
src/lib/query/ssr.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/**
|
||||
* SSR Utilities for TanStack Query
|
||||
*
|
||||
* Provides utilities for integrating server-side data fetching with TanStack Query.
|
||||
* These utilities support the initialData pattern for pages that use +page.server.ts
|
||||
* load functions.
|
||||
*
|
||||
* @module query/ssr
|
||||
*/
|
||||
|
||||
import type { QueryClient } from '@tanstack/svelte-query'
|
||||
|
||||
/**
|
||||
* Options for creating a query with initial data from SSR
|
||||
*/
|
||||
export interface InitialDataOptions<TData> {
|
||||
/**
|
||||
* The data fetched on the server to use as initial data.
|
||||
* TanStack Query accepts TData | undefined but NOT null.
|
||||
*/
|
||||
initialData?: TData
|
||||
|
||||
/**
|
||||
* Optional timestamp when the data was fetched on the server.
|
||||
* If not provided, defaults to 0 (will be considered stale immediately).
|
||||
* Use Date.now() on the server to get accurate timestamps.
|
||||
*/
|
||||
initialDataUpdatedAt?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates query options with initial data from server-side rendering.
|
||||
*
|
||||
* Use this helper when you have data fetched in a +page.server.ts load function
|
||||
* and want to use it as initial data for a TanStack Query.
|
||||
*
|
||||
* Note: This helper strips `null` from the input since TanStack Query's
|
||||
* initialData only accepts `TData | undefined`, not `null`.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { createQuery } from '@tanstack/svelte-query'
|
||||
* import { partyQueries } from '$lib/api/queries/party.queries'
|
||||
* import { withInitialData } from '$lib/query/ssr'
|
||||
* import type { PageData } from './$types'
|
||||
*
|
||||
* let { data } = $props<{ data: PageData }>()
|
||||
*
|
||||
* // Use server-fetched party as initial data
|
||||
* const party = createQuery(() => ({
|
||||
* ...partyQueries.byShortcode(data.party?.shortcode ?? ''),
|
||||
* ...withInitialData(data.party)
|
||||
* }))
|
||||
* </script>
|
||||
* ```
|
||||
*
|
||||
* @param initialData - The data fetched on the server (null is converted to undefined)
|
||||
* @param updatedAt - Optional timestamp when data was fetched (defaults to 0)
|
||||
* @returns Query options object with initialData and initialDataUpdatedAt
|
||||
*/
|
||||
export function withInitialData<TData>(
|
||||
initialData: TData | undefined | null,
|
||||
updatedAt?: number
|
||||
): InitialDataOptions<NonNullable<TData>> {
|
||||
return {
|
||||
initialData: (initialData ?? undefined) as NonNullable<TData> | undefined,
|
||||
initialDataUpdatedAt: updatedAt ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetches a query on the server and returns the data.
|
||||
*
|
||||
* Use this in +page.ts load functions when you want to prefetch data
|
||||
* into the QueryClient cache. This is the recommended approach for
|
||||
* pages that don't use +page.server.ts.
|
||||
*
|
||||
* Note: This will NOT work with +page.server.ts load functions.
|
||||
* Use withInitialData() instead for server-only load functions.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // +page.ts
|
||||
* import type { PageLoad } from './$types'
|
||||
* import { prefetchQuery } from '$lib/query/ssr'
|
||||
* import { partyQueries } from '$lib/api/queries/party.queries'
|
||||
*
|
||||
* export const load: PageLoad = async ({ parent, params }) => {
|
||||
* const { queryClient } = await parent()
|
||||
*
|
||||
* await prefetchQuery(queryClient, partyQueries.byShortcode(params.id))
|
||||
*
|
||||
* // No need to return data - it's in the cache
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param queryClient - The QueryClient instance from parent layout
|
||||
* @param options - Query options from a query factory
|
||||
*/
|
||||
export async function prefetchQuery<TData>(
|
||||
queryClient: QueryClient,
|
||||
options: {
|
||||
queryKey: readonly unknown[]
|
||||
queryFn: () => Promise<TData>
|
||||
staleTime?: number
|
||||
gcTime?: number
|
||||
}
|
||||
): Promise<void> {
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: options.queryKey,
|
||||
queryFn: options.queryFn,
|
||||
staleTime: options.staleTime,
|
||||
gcTime: options.gcTime
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetches an infinite query on the server.
|
||||
*
|
||||
* Use this in +page.ts load functions when you want to prefetch
|
||||
* paginated data into the QueryClient cache.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // +page.ts
|
||||
* import type { PageLoad } from './$types'
|
||||
* import { prefetchInfiniteQuery } from '$lib/query/ssr'
|
||||
* import { partyQueries } from '$lib/api/queries/party.queries'
|
||||
*
|
||||
* export const load: PageLoad = async ({ parent }) => {
|
||||
* const { queryClient } = await parent()
|
||||
*
|
||||
* await prefetchInfiniteQuery(queryClient, partyQueries.list())
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param queryClient - The QueryClient instance from parent layout
|
||||
* @param options - Infinite query options from a query factory
|
||||
*/
|
||||
export async function prefetchInfiniteQuery<TData>(
|
||||
queryClient: QueryClient,
|
||||
options: {
|
||||
queryKey: readonly unknown[]
|
||||
queryFn: (context: { pageParam: number }) => Promise<TData>
|
||||
initialPageParam: number
|
||||
staleTime?: number
|
||||
gcTime?: number
|
||||
}
|
||||
): Promise<void> {
|
||||
await queryClient.prefetchInfiniteQuery({
|
||||
queryKey: options.queryKey,
|
||||
queryFn: options.queryFn,
|
||||
initialPageParam: options.initialPageParam,
|
||||
staleTime: options.staleTime,
|
||||
gcTime: options.gcTime
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets query data directly in the cache.
|
||||
*
|
||||
* Use this when you have data from a server load function and want
|
||||
* to populate the QueryClient cache directly. This is useful when
|
||||
* migrating from server-only load functions to TanStack Query.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In a component or effect
|
||||
* import { useQueryClient } from '@tanstack/svelte-query'
|
||||
* import { setQueryData } from '$lib/query/ssr'
|
||||
* import { partyKeys } from '$lib/api/queries/party.queries'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* // Populate cache with server data
|
||||
* setQueryData(queryClient, partyKeys.detail(shortcode), serverParty)
|
||||
* ```
|
||||
*
|
||||
* @param queryClient - The QueryClient instance
|
||||
* @param queryKey - The query key to set data for
|
||||
* @param data - The data to set in the cache
|
||||
*/
|
||||
export function setQueryData<TData>(
|
||||
queryClient: QueryClient,
|
||||
queryKey: readonly unknown[],
|
||||
data: TData
|
||||
): void {
|
||||
queryClient.setQueryData(queryKey, data)
|
||||
}
|
||||
|
|
@ -11,14 +11,12 @@
|
|||
import { authStore } from '$lib/stores/auth.store'
|
||||
import { browser } from '$app/environment'
|
||||
import { QueryClientProvider } from '@tanstack/svelte-query'
|
||||
import { createQueryClient } from '$lib/query/queryClient'
|
||||
|
||||
const queryClient = createQueryClient()
|
||||
import type { LayoutData } from './$types'
|
||||
|
||||
// Get `data` and `children` from the router via $props()
|
||||
// Use a more flexible type that allows additional properties from child pages
|
||||
// QueryClient is now created in +layout.ts for SSR support
|
||||
const { data, children } = $props<{
|
||||
data: any // Allow any data to pass through from child pages
|
||||
data: LayoutData & { [key: string]: any } // Allow any data to pass through from child pages
|
||||
children: () => any
|
||||
}>()
|
||||
|
||||
|
|
@ -110,7 +108,7 @@
|
|||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryClientProvider client={data.queryClient}>
|
||||
<Tooltip.Provider>
|
||||
<div class="app-container" class:sidebar-open={sidebar.isOpen}>
|
||||
<div class="main-pane">
|
||||
|
|
|
|||
34
src/routes/+layout.ts
Normal file
34
src/routes/+layout.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Root Layout Load Function
|
||||
*
|
||||
* Creates a QueryClient instance for SSR support with TanStack Query v6.
|
||||
* The QueryClient is created here so it can be used for prefetching in
|
||||
* child page load functions.
|
||||
*
|
||||
* @module routes/+layout
|
||||
*/
|
||||
|
||||
import type { LayoutLoad } from './$types'
|
||||
import { browser } from '$app/environment'
|
||||
import { QueryClient } from '@tanstack/svelte-query'
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// Disable queries on server - they will be prefetched explicitly
|
||||
enabled: browser,
|
||||
// Cache data for 5 minutes before considering it stale
|
||||
staleTime: 1000 * 60 * 5,
|
||||
// Keep unused data in cache for 30 minutes
|
||||
gcTime: 1000 * 60 * 30,
|
||||
// Retry failed requests twice
|
||||
retry: 2,
|
||||
// Don't refetch on window focus by default
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { queryClient }
|
||||
}
|
||||
|
|
@ -1,12 +1,39 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types'
|
||||
import Party from '$lib/components/party/Party.svelte'
|
||||
import { createQuery } from '@tanstack/svelte-query'
|
||||
import { partyQueries } from '$lib/api/queries/party.queries'
|
||||
import { withInitialData } from '$lib/query/ssr'
|
||||
|
||||
let { data }: { data: PageData } = $props()
|
||||
|
||||
/**
|
||||
* TanStack Query v6 SSR Integration Example
|
||||
*
|
||||
* This demonstrates the `withInitialData` pattern for pages using +page.server.ts.
|
||||
* The server-fetched party data is used as initial data for the query, which means:
|
||||
*
|
||||
* 1. No loading state on initial render (data is already available)
|
||||
* 2. The query can refetch in the background when data becomes stale
|
||||
* 3. The data is cached and shared across components using the same query key
|
||||
*
|
||||
* Note: The Party component currently manages its own state, so we pass the
|
||||
* server data directly. In a future refactor, the Party component could use
|
||||
* this query directly for automatic cache updates and background refetching.
|
||||
*/
|
||||
const partyQuery = createQuery(() => ({
|
||||
...partyQueries.byShortcode(data.party?.shortcode ?? ''),
|
||||
...withInitialData(data.party),
|
||||
enabled: !!data.party?.shortcode
|
||||
}))
|
||||
|
||||
// Use query data if available, fall back to server data
|
||||
// This allows the query to refetch and update the UI automatically
|
||||
const party = $derived(partyQuery.data ?? data.party)
|
||||
</script>
|
||||
|
||||
{#if data?.party}
|
||||
<Party party={data.party} canEdit={data.canEdit || false} authUserId={data.authUserId} />
|
||||
{#if party}
|
||||
<Party party={party} canEdit={data.canEdit || false} authUserId={data.authUserId} />
|
||||
{:else}
|
||||
<div>
|
||||
<h1>Party not found</h1>
|
||||
|
|
|
|||
Loading…
Reference in a new issue