Skip to content

Commit

Permalink
chore: cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Dec 21, 2023
1 parent d955754 commit e2a0a14
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 98 deletions.
6 changes: 0 additions & 6 deletions src/data-fetching-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,6 @@ export const useDataFetchingStore = defineStore('PiniaColada', () => {
// this allows use to attach reactive effects to the scope later on
const scope = getCurrentScope()!

// no reactive on this one as it's only used internally and is not needed for hydration
// const queryEntriesRegistry = new Map<
// UseQueryKey,
// UseDataFetchingQueryEntry<unknown, unknown>
// >()

function ensureEntry<TResult = unknown, TError = Error>(
key: UseQueryKey,
{
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export {
export {
USE_QUERY_DEFAULTS,
useQuery,
type UseDataFetchingQueryEntry,
type UseQueryKey,
type UseQueryOptions,
type UseQueryOptionsWithDefaults,
Expand Down
133 changes: 98 additions & 35 deletions src/use-query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,52 +53,115 @@ describe('useQuery', () => {
return { wrapper, fetcher }
}

it('fetches data and updates on mount', async () => {
const { wrapper } = mountSimple()
describe('initial fetch', () => {
it('fetches data and updates on mount', async () => {
const { wrapper } = mountSimple()

expect(wrapper.vm.data).toBeUndefined()
await runTimers()
expect(wrapper.vm.data).toBe(42)
})
expect(wrapper.vm.data).toBeUndefined()
await runTimers()
expect(wrapper.vm.data).toBe(42)
})

it('sets the fetching state', async () => {
const { wrapper } = mountSimple()
it('sets the fetching state', async () => {
const { wrapper } = mountSimple()

expect(wrapper.vm.isFetching).toBe(true)
await runTimers()
expect(wrapper.vm.isFetching).toBe(false)
})
expect(wrapper.vm.isFetching).toBe(true)
await runTimers()
expect(wrapper.vm.isFetching).toBe(false)
})

it('sets the pending state', async () => {
const { wrapper } = mountSimple()
it('sets the pending state', async () => {
const { wrapper } = mountSimple()

expect(wrapper.vm.isPending).toBe(true)
await runTimers()
expect(wrapper.vm.isPending).toBe(false)
})
expect(wrapper.vm.isPending).toBe(true)
await runTimers()
expect(wrapper.vm.isPending).toBe(false)
})

it('sets the error state', async () => {
const { wrapper } = mountSimple({
fetcher: async () => {
throw new Error('foo')
},
it('sets the error state', async () => {
const { wrapper } = mountSimple({
fetcher: async () => {
throw new Error('foo')
},
})

expect(wrapper.vm.error).toBeNull()
await runTimers()
expect(wrapper.vm.error).toEqual(new Error('foo'))
})

it('exposes a status state', async () => {
const { wrapper } = mountSimple()

expect(wrapper.vm.status).toBe('pending')
await runTimers()
expect(wrapper.vm.status).toBe('success')
})

expect(wrapper.vm.error).toBeNull()
await runTimers()
expect(wrapper.vm.error).toEqual(new Error('foo'))
// NOTE: is this worth adding?
it.skip('it works with a synchronously thrown Error', async () => {
const { wrapper } = mountSimple({
fetcher: () => {
throw new Error('foo')
},
})

expect(wrapper.vm.error).toBeNull()
await runTimers()
expect(wrapper.vm.error).toEqual(new Error('foo'))
})
})

// NOTE: is this worth adding?
it.skip('it works with a synchronously thrown Error', async () => {
const { wrapper } = mountSimple({
fetcher: () => {
throw new Error('foo')
},
describe('staleTime', () => {
it('does not fetch again if staleTime has not elapsed', async () => {
const { wrapper, fetcher } = mountSimple({ staleTime: 1000 })

await runTimers()
expect(wrapper.vm.data).toBe(42)
expect(fetcher).toHaveBeenCalledTimes(1)
// await wrapper.vm.fetch()

// should not trigger a new fetch because staleTime has not passed
mountSimple()
await runTimers()

expect(fetcher).toHaveBeenCalledTimes(1)
expect(wrapper.vm.data).toBe(42)
})

expect(wrapper.vm.error).toBeNull()
await runTimers()
expect(wrapper.vm.error).toEqual(new Error('foo'))
it.todo(
'refreshes the data after the staleTime if called again',
async () => {
const { wrapper, fetcher } = mountSimple({ staleTime: 1000 })

await runTimers()
expect(wrapper.vm.data).toBe(42)
expect(fetcher).toHaveBeenCalledTimes(1)
// await wrapper.vm.fetch()
vi.advanceTimersByTime(1000)
await runTimers()

mountSimple()

expect(fetcher).toHaveBeenCalledTimes(2)
expect(wrapper.vm.data).toBe(42)
}
)
})

describe.skip('refresh', () => {
it('refreshes the data', async () => {
const { wrapper, fetcher } = mountSimple()

await runTimers()
expect(wrapper.vm.data).toBe(42)
expect(fetcher).toHaveBeenCalledTimes(1)

mountSimple()

await runTimers()
expect(fetcher).toHaveBeenCalledTimes(2)
expect(wrapper.vm.data).toBe(42)
})
})
})
66 changes: 10 additions & 56 deletions src/use-query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useEventListener } from './utils'
import { IS_CLIENT, useEventListener } from './utils'
import {
type ComputedRef,
computed,
onMounted,
onServerPrefetch,
Expand All @@ -9,53 +8,15 @@ import {
ShallowRef,
Ref,
} from 'vue'
import { useDataFetchingStore } from './data-fetching-store'

export interface UseQueryReturn<TResult = unknown, TError = Error> {
data: Ref<TResult | undefined>
error: ShallowRef<TError | null>
isFetching: Ref<boolean>
isPending: Ref<boolean>
refresh: () => Promise<void>
}

export interface UseDataFetchingQueryEntry<TResult = unknown, TError = any> {
data: () => TResult | undefined
error: () => TError | null
/**
* Returns whether the request is still pending its first call
*/
isPending: () => boolean
/**
* Returns whether the request is currently fetching data
*/
isFetching: () => boolean

// TODO: should we just have refresh and allow a parameter to force a refresh? instead of having fetch and refresh
/**
* Refreshes the data ignoring any cache but still decouples the refreshes (only one refresh at a time)
* @returns a promise that resolves when the refresh is done
*/
refresh: () => Promise<void>
/**
* Fetches the data but only if it's not already fetching
* @returns a promise that resolves when the refresh is done
*/
fetch: () => Promise<TResult>
import {
UseQueryPropertiesEntry,
UseQueryStateEntry,
useDataFetchingStore,
} from './data-fetching-store'

pending: null | {
refreshCall: Promise<void>
when: number
}
previous: null | {
/**
* When was this data fetched the last time in ms
*/
when: number
data: TResult | undefined
error: TError | null
}
}
export interface UseQueryReturn<TResult = unknown, TError = Error>
extends UseQueryStateEntry<TResult, TError>,
Pick<UseQueryPropertiesEntry<TResult, TError>, 'refresh'> {}

export type UseQueryKey = string | symbol

Expand Down Expand Up @@ -150,18 +111,11 @@ export function useQuery<TResult, TError = Error>(
error: entry.value.error,
isFetching: entry.value.isFetching,
isPending: entry.value.isPending,
status: entry.value.status,

// TODO: do we need to force bound to the entry?
refresh: () => entry.value.refresh(),
} satisfies UseQueryReturn<TResult, TError>

return queryReturn
}

/**
* Notes for exercise:
* - Start only with the data, error, and isLoading, no cache, no refresh
* - Start without the options about refreshing, and mutations
*/

const IS_CLIENT = typeof window !== 'undefined'
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ export function useEventListener(
target.removeEventListener(event, listener)
})
}

export const IS_CLIENT = typeof window !== 'undefined'

0 comments on commit e2a0a14

Please sign in to comment.