Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/svelte-query/src/containers.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createSubscriber } from 'svelte/reactivity'
import { SvelteSet, createSubscriber } from 'svelte/reactivity'

type VoidFn = () => void
type Subscriber = (update: VoidFn) => void | VoidFn
Expand Down Expand Up @@ -30,7 +30,7 @@ export function createRawRef<T extends {} | Array<unknown>>(
init: T,
): [T, (newValue: T) => void] {
const refObj = (Array.isArray(init) ? [] : {}) as T
const hiddenKeys = new Set<PropertyKey>()
const hiddenKeys = new SvelteSet<PropertyKey>()
const out = new Proxy(refObj, {
set(target, prop, value, receiver) {
hiddenKeys.delete(prop)
Expand Down
56 changes: 40 additions & 16 deletions packages/svelte-query/src/createBaseQuery.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { untrack } from 'svelte'
import { useIsRestoring } from './useIsRestoring.js'
import { useQueryClient } from './useQueryClient.js'
import { createRawRef } from './containers.svelte.js'
import { watchChanges } from './utils.svelte.js'
import type { QueryClient, QueryKey, QueryObserver } from '@tanstack/query-core'
import type {
Accessor,
Expand Down Expand Up @@ -39,12 +39,26 @@ export function createBaseQuery<
})

/** Creates the observer */
const observer = $derived(
// svelte-ignore state_referenced_locally - intentional, initial value
let observer = $state(
new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
client,
untrack(() => resolvedOptions),
resolvedOptions,
),
)
watchChanges(
() => client,
'pre',
() => {
observer = new Observer<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>(client, resolvedOptions)
},
)

function createResult() {
const result = observer.getOptimisticResult(resolvedOptions)
Expand All @@ -65,19 +79,29 @@ export function createBaseQuery<
return unsubscribe
})

$effect.pre(() => {
observer.setOptions(resolvedOptions)
// The only reason this is necessary is because of `isRestoring`.
// Because we don't subscribe while restoring, the following can occur:
// - `isRestoring` is true
// - `isRestoring` becomes false
// - `observer.subscribe` and `observer.updateResult` is called in the above effect,
// but the subsequent `fetch` has already completed
// - `result` misses the intermediate restored-but-not-fetched state
//
// this could technically be its own effect but that doesn't seem necessary
update(createResult())
})
watchChanges(
() => resolvedOptions,
'pre',
() => {
observer.setOptions(resolvedOptions)
},
)
watchChanges(
() => [resolvedOptions, observer],
'pre',
() => {
// The only reason this is necessary is because of `isRestoring`.
// Because we don't subscribe while restoring, the following can occur:
// - `isRestoring` is true
// - `isRestoring` becomes false
// - `observer.subscribe` and `observer.updateResult` is called in the above effect,
// but the subsequent `fetch` has already completed
// - `result` misses the intermediate restored-but-not-fetched state
//
// this could technically be its own effect but that doesn't seem necessary
update(createResult())
},
)

return query
}
71 changes: 42 additions & 29 deletions packages/svelte-query/src/createMutation.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { onDestroy } from 'svelte'

import { MutationObserver, noop, notifyManager } from '@tanstack/query-core'
import { useQueryClient } from './useQueryClient.js'
import { watchChanges } from './utils.svelte.js'
import type {
Accessor,
CreateMutateFunction,
Expand All @@ -24,48 +23,62 @@ export function createMutation<
options: Accessor<CreateMutationOptions<TData, TError, TVariables, TContext>>,
queryClient?: Accessor<QueryClient>,
): CreateMutationResult<TData, TError, TVariables, TContext> {
const client = useQueryClient(queryClient?.())
const client = $derived(useQueryClient(queryClient?.()))

const observer = $derived(
// svelte-ignore state_referenced_locally - intentional, initial value
let observer = $state(
// svelte-ignore state_referenced_locally - intentional, initial value
new MutationObserver<TData, TError, TVariables, TContext>(
client,
options(),
),
)

const mutate = $state<
CreateMutateFunction<TData, TError, TVariables, TContext>
>((variables, mutateOptions) => {
observer.mutate(variables, mutateOptions).catch(noop)
})
watchChanges(
() => client,
'pre',
() => {
observer = new MutationObserver(client, options())
},
)

$effect.pre(() => {
observer.setOptions(options())
})

const result = $state(observer.getCurrentResult())

const unsubscribe = observer.subscribe((val) => {
notifyManager.batchCalls(() => {
Object.assign(result, val)
})()
const mutate = <CreateMutateFunction<TData, TError, TVariables, TContext>>((
variables,
mutateOptions,
) => {
observer.mutate(variables, mutateOptions).catch(noop)
})

onDestroy(() => {
unsubscribe()
let result = $derived(observer.getCurrentResult())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lachlancollins do you think we need to keep the Svelte peer dep pinned to as low as 5.7.0, or could we bump that to at least 5.25.0 so that assigning to $derived like this is supported? This was part of eslint --fix, but if we need to stay on the lower version, I'll have to revert this back to a manually managed $state.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to bump the peer dependency version!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hmnd I've updated this on the target branch.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this change(?) might have caused tests to start failing. It's difficult to know though, as the force pushes have erased the history.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, I found the diff in a branch I cloned earlier. Tests are all passing again, so I think I'll merge this! Thanks heaps for your time @hmnd !

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for getting this merged @lachlancollins! Sorry about the force pushes 😅


$effect.pre(() => {
const unsubscribe = observer.subscribe((val) => {
notifyManager.batchCalls(() => {
Object.assign(result, val)
})()
})
return unsubscribe
})

const resultProxy = $derived(
new Proxy(result, {
get: (_, prop) => {
const r = {
...result,
mutate,
mutateAsync: result.mutate,
}
if (prop == 'value') return r
// @ts-expect-error
return r[prop]
},
}),
)

// @ts-expect-error
return new Proxy(result, {
get: (_, prop) => {
const r = {
...result,
mutate,
mutateAsync: result.mutate,
}
if (prop == 'value') return r
// @ts-expect-error
return r[prop]
},
})
return resultProxy
}
6 changes: 3 additions & 3 deletions packages/svelte-query/src/createQueries.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { QueriesObserver } from '@tanstack/query-core'
import { untrack } from 'svelte'
import { useIsRestoring } from './useIsRestoring.js'
import { createRawRef } from './containers.svelte.js'
import { useQueryClient } from './useQueryClient.js'
Expand Down Expand Up @@ -216,11 +215,12 @@ export function createQueries<
}),
)

// can't do same as createMutation, as QueriesObserver has no `setOptions` method
const observer = $derived(
new QueriesObserver<TCombinedResult>(
client,
untrack(() => resolvedQueryOptions),
untrack(() => combine as QueriesObserverOptions<TCombinedResult>),
resolvedQueryOptions,
combine as QueriesObserverOptions<TCombinedResult>,
),
)

Expand Down
44 changes: 44 additions & 0 deletions packages/svelte-query/src/utils.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { untrack } from 'svelte'
// modified from the great https://github.com/svecosystem/runed
function runEffect(
flush: 'post' | 'pre',
effect: () => void | VoidFunction,
): void {
switch (flush) {
case 'post':
$effect(effect)
break
case 'pre':
$effect.pre(effect)
break
}
}
type Getter<T> = () => T
export const watchChanges = <T>(
sources: Getter<T> | Array<Getter<T>>,
flush: 'post' | 'pre',
effect: (
values: T | Array<T>,
previousValues: T | undefined | Array<T | undefined>,
) => void,
) => {
let active = false
let previousValues: T | undefined | Array<T | undefined> = Array.isArray(
sources,
)
? []
: undefined
runEffect(flush, () => {
const values = Array.isArray(sources)
? sources.map((source) => source())
: sources()
if (!active) {
active = true
previousValues = values
return
}
const cleanup = untrack(() => effect(values, previousValues))
previousValues = values
return cleanup
})
}
14 changes: 14 additions & 0 deletions packages/svelte-query/tests/ProviderWrapper.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import { QueryClient } from '@tanstack/query-core'
import { QueryClientProvider } from '../src/index.js'
import { Snippet } from 'svelte'

const {
queryClient = new QueryClient(),
children,
}: { queryClient?: QueryClient; children: Snippet } = $props()
</script>

<QueryClientProvider client={queryClient}>
{@render children()}
</QueryClientProvider>
41 changes: 33 additions & 8 deletions packages/svelte-query/tests/createQuery.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,12 @@ describe('createQuery', () => {
await withEffectRoot(async () => {
const { promise, resolve } = promiseWithResolvers<string>()

const query = $derived(
createQuery<string, Error>(
() => ({
queryKey: key,
queryFn: () => promise,
}),
() => queryClient,
),
const query = createQuery<string, Error>(
() => ({
queryKey: key,
queryFn: () => promise,
}),
() => queryClient,
)

expect(query).toEqual(
Expand Down Expand Up @@ -1892,4 +1890,31 @@ describe('createQuery', () => {
expect(query.error?.message).toBe('Local Error')
}),
)

it(
'should support changing provided query client',
withEffectRoot(async () => {
const queryClient1 = new QueryClient()
const queryClient2 = new QueryClient()

let queryClient = $state(queryClient1)

const key = ['test']

createQuery(
() => ({
queryKey: key,
queryFn: () => Promise.resolve('prefetched'),
}),
() => queryClient,
)

expect(queryClient1.getQueryCache().find({ queryKey: key })).toBeDefined()

queryClient = queryClient2
flushSync()

expect(queryClient2.getQueryCache().find({ queryKey: key })).toBeDefined()
}),
)
})
27 changes: 7 additions & 20 deletions packages/svelte-query/tests/useIsFetching/BaseExample.svelte
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
<script lang="ts">
import { QueryClient } from '@tanstack/query-core'
import { createQuery, useIsFetching } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'

const queryClient = new QueryClient()
let ready = $state(false)

const isFetching = useIsFetching(undefined, queryClient)

const query = createQuery(
() => ({
queryKey: ['test'],
queryFn: () => sleep(10).then(() => 'test'),
enabled: ready,
}),
() => queryClient,
)
import ProviderWrapper from '../ProviderWrapper.svelte'
import FetchStatus from './FetchStatus.svelte'
import Query from './Query.svelte'
</script>

<button onclick={() => (ready = true)}>setReady</button>
<ProviderWrapper>
<FetchStatus />

<div>isFetching: {isFetching.current}</div>
<div>Data: {query.data ?? 'undefined'}</div>
<Query />
</ProviderWrapper>
6 changes: 6 additions & 0 deletions packages/svelte-query/tests/useIsFetching/FetchStatus.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script lang="ts">
import { useIsFetching } from '../../src/index.js'
const isFetching = useIsFetching()
</script>

<div>isFetching: {isFetching.current}</div>
19 changes: 19 additions & 0 deletions packages/svelte-query/tests/useIsFetching/Query.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import { createQuery } from '../../src/index.js'
import { sleep } from '@tanstack/query-test-utils'

let ready = $state(false)

const query = createQuery(() => ({
queryKey: ['test'],
queryFn: async () => {
await sleep(10)
return 'test'
},
enabled: ready,
}))
</script>

<button onclick={() => (ready = true)}>setReady</button>

<div>Data: {query.data ?? 'undefined'}</div>
Loading
Loading