Skip to content

Commit

Permalink
refactor: entry registry types
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Jan 10, 2024
1 parent 60c3aa2 commit c637a78
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 89 deletions.
67 changes: 9 additions & 58 deletions src/data-fetching-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,6 @@ import { EntryNodeKey, TreeMapNode } from './tree-map'

export type UseQueryStatus = 'pending' | 'error' | 'success'

export interface UseQueryStateEntry<TResult = unknown, TError = unknown> {
// TODO: is it worth to be a shallowRef?
data: Ref<TResult | undefined>

error: ShallowRef<TError | null>

/**
* Returns whether the request is still pending its first call. Alias for `status.value === 'pending'`
*/
isPending: ComputedRef<boolean>

/**
* Returns whether the request is currently fetching data.
*/
isFetching: ShallowRef<boolean>

/**
* The status of the request. `pending` indicates that no request has been made yet and there is no cached data to
* display (`data.value = undefined`). `error` indicates that the last request failed. `success` indicates that the
* last request succeeded.
*/
status: ShallowRef<UseQueryStatus>
}

// TODO: rename to avoid conflict
/**
* Raw data of a query entry. Can be serialized from the server and used to hydrate the store.
*/
Expand All @@ -65,31 +40,6 @@ export interface UseQueryStateEntryRaw<TResult = unknown, TError = unknown> {
when: number
}

export interface UseQueryPropertiesEntry<TResult = unknown, TError = unknown> {
/**
* Ensures the current data is fresh. If the data is stale, refetch, if not return as is.
* @returns a promise that resolves when the refresh is done
*/
refresh: () => Promise<TResult>

/**
* Ignores fresh data and triggers a new fetch
* @returns a promise that resolves when the refresh is done
*/
refetch: () => Promise<TResult>

pending: null | {
refreshCall: Promise<void>
when: number
}

previous: null | UseQueryStateEntryRaw<TResult, TError>
}

// export interface UseQueryEntry<TResult = unknown, TError = Error>
// extends UseQueryStateEntry<TResult, TError>,
// UseQueryPropertiesEntry<TResult, TError> {}

export class UseQueryEntry<TResult = unknown, TError = any> {
data: Ref<TResult | undefined>
error: ShallowRef<TError | null>
Expand Down Expand Up @@ -152,7 +102,7 @@ export class UseQueryEntry<TResult = unknown, TError = any> {
when: 0,
data: undefined as TResult | undefined,
error: null as TError | null,
} satisfies UseQueryPropertiesEntry<TResult, TError>['previous']
} satisfies UseQueryEntry<TResult, TError>['previous']

// we create an object and verify we are the most recent pending request
// before doing anything
Expand Down Expand Up @@ -210,8 +160,6 @@ export const useDataFetchingStore = defineStore('PiniaColada', () => {
// free the memory
delete existingState.entriesRaw
const entryRegistry = shallowReactive(new TreeMapNode<UseQueryEntry>())
// these are not reactive as they are mostly functions and should not be serialized as part of the state
const entryPropertiesRegistry = new TreeMapNode<UseQueryPropertiesEntry>()

// FIXME: start from here: replace properties entry with a QueryEntry that is created when needed and contains all the needed part, included functions

Expand Down Expand Up @@ -245,7 +193,7 @@ export const useDataFetchingStore = defineStore('PiniaColada', () => {
* @param refetch - whether to force a refresh of the data
*/
function invalidateEntry(key: UseQueryKey[], refetch = false) {
const entry = entryPropertiesRegistry.get(key.map(stringifyFlatObject))
const entry = entryRegistry.get(key.map(stringifyFlatObject))

// nothing to invalidate
if (!entry) {
Expand All @@ -261,6 +209,7 @@ export const useDataFetchingStore = defineStore('PiniaColada', () => {
// reset any pending request
entry.pending = null
// force refresh
// @ts-expect-error: FIXME:
entry.refetch()
}
}
Expand All @@ -270,7 +219,7 @@ export const useDataFetchingStore = defineStore('PiniaColada', () => {
data: TResult | ((data: Ref<TResult | undefined>) => void)
) {
const entry = entryRegistry.get(key.map(stringifyFlatObject)) as
| UseQueryStateEntry<TResult>
| UseQueryEntry<TResult>
| undefined
if (!entry) {
return
Expand All @@ -288,19 +237,21 @@ export const useDataFetchingStore = defineStore('PiniaColada', () => {
}

function prefetch(key: UseQueryKey[]) {
const entry = entryPropertiesRegistry.get(key.map(stringifyFlatObject))
const entry = entryRegistry.get(key.map(stringifyFlatObject))
if (!entry) {
console.warn(
`⚠️ trying to prefetch "${String(key)}" but it's not in the registry`
)
return
}
entry.refetch()
// @ts-expect-error: FIXME:
entry.refetch(options)
}

return {
// TODO: remove
entriesRaw,
entryStateRegistry: entryRegistry,
entryRegistry,

ensureEntry,
invalidateEntry,
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export {
export {
useDataFetchingStore,
type UseQueryStatus,
serialize,
createTreeMap,
} from './data-fetching-store'

// TODO: idea of plugin that persists the cached values
64 changes: 41 additions & 23 deletions src/use-query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { GlobalMountOptions } from 'node_modules/@vue/test-utils/dist/types'
import { delay, isSpy, runTimers } from '../test/utils'
import {
UseQueryStateEntry,
UseQueryEntry,
UseQueryStatus,
useDataFetchingStore,
} from './data-fetching-store'
Expand Down Expand Up @@ -127,26 +127,46 @@ describe('useQuery', () => {
expect(wrapper.vm.error).toEqual(new Error('foo'))
})

it.todo('skips fetching if initial data is present in store', async () => {
it('uses initial data if present in store', async () => {
const pinia = createPinia()
const status = shallowRef<UseQueryStatus>('pending')

const stateEntry = {
data: ref(2),
error: shallowRef(null),
isPending: computed(() => status.value === 'pending'),
isFetching: shallowRef(false),
status,
} satisfies UseQueryStateEntry
const entryStateRegistry = shallowReactive(
new TreeMapNode<UseQueryStateEntry>(['key'], stateEntry)

const entryRegistry = shallowReactive(
new TreeMapNode<UseQueryEntry>(
['key'],
new UseQueryEntry(2, null, Date.now())
)
)
pinia.state.value.PiniaColada = { entryRegistry }
const { wrapper, fetcher } = mountSimple(
{ staleTime: 1000 },
{ plugins: [pinia] }
)
pinia.state.value.PiniaColada = { entryStateRegistry }
const { wrapper, fetcher } = mountSimple({}, { plugins: [pinia] })
await runTimers()

// without waiting for times the data is present
expect(wrapper.vm.data).toBe(2)
})

it.todo('avoids fetching if initial data is fresh', async () => {
const pinia = createPinia()

const entryRegistry = shallowReactive(
new TreeMapNode<UseQueryEntry>(
['key'],
// fresh data
new UseQueryEntry(2, null, Date.now())
)
)
pinia.state.value.PiniaColada = { entryRegistry }
const { wrapper, fetcher } = mountSimple(
// 1s stale time
{ staleTime: 1000 },
{ plugins: [pinia] }
)

await runTimers()
// it should not fetch and use the initial data
expect(fetcher).toHaveBeenCalledTimes(0)
expect(wrapper.vm.data).toBe(2)
})
})

Expand Down Expand Up @@ -326,7 +346,7 @@ describe('useQuery', () => {
async () => {
const pinia = createPinia()
pinia.state.value.PiniaColada = {
entryStateRegistry: shallowReactive(new TreeMapNode(['key'], 60)),
entryRegistry: shallowReactive(new TreeMapNode(['key'], 60)),
}
const { wrapper, fetcher } = mountSimple(
{
Expand Down Expand Up @@ -424,9 +444,7 @@ describe('useQuery', () => {
wrapper.vm.refresh()
expect(wrapper.vm.error).toEqual(new Error('fail'))
await runTimers()

expect(wrapper.vm.error).toEqual(null)
expect(wrapper.vm.data).toBe(42)
})
})

Expand All @@ -438,12 +456,12 @@ describe('useQuery', () => {
await runTimers()

const cacheClient = useDataFetchingStore()
expect(entryNodeSize(cacheClient.entryStateRegistry)).toBe(1)
expect(entryNodeSize(cacheClient.entryRegistry)).toBe(1)

mountSimple({ key: ['todos', 2] }, { plugins: [pinia] })
await runTimers()

expect(entryNodeSize(cacheClient.entryStateRegistry)).toBe(2)
expect(entryNodeSize(cacheClient.entryRegistry)).toBe(2)
})

it('populates the entry registry', async () => {
Expand All @@ -453,7 +471,7 @@ describe('useQuery', () => {
await runTimers()

const cacheClient = useDataFetchingStore()
expect(entryNodeSize(cacheClient.entryStateRegistry)).toBe(3)
expect(entryNodeSize(cacheClient.entryRegistry)).toBe(3)
})

it('order in object keys does not matter', async () => {
Expand All @@ -473,7 +491,7 @@ describe('useQuery', () => {
await runTimers()

const cacheClient = useDataFetchingStore()
expect(entryNodeSize(cacheClient.entryStateRegistry)).toBe(2)
expect(entryNodeSize(cacheClient.entryRegistry)).toBe(2)
})
})
})
45 changes: 37 additions & 8 deletions src/use-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,49 @@ import {
MaybeRefOrGetter,
watch,
getCurrentInstance,
ComputedRef,
} from 'vue'
import {
UseQueryPropertiesEntry,
UseQueryStateEntry,
useDataFetchingStore,
} from './data-fetching-store'
import { UseQueryStatus, useDataFetchingStore } from './data-fetching-store'
import { EntryNodeKey } from './tree-map'

/**
* Return type of `useQuery()`.
*/
export interface UseQueryReturn<TResult = unknown, TError = Error>
extends UseQueryStateEntry<TResult, TError>,
Pick<UseQueryPropertiesEntry<TResult, TError>, 'refresh' | 'refetch'> {}
export interface UseQueryReturn<TResult = unknown, TError = Error> {
// TODO: is it worth to be a shallowRef?
data: Ref<TResult | undefined>

error: ShallowRef<TError | null>

/**
* Returns whether the request is still pending its first call. Alias for `status.value === 'pending'`
*/
isPending: ComputedRef<boolean>

/**
* Returns whether the request is currently fetching data.
*/
isFetching: ShallowRef<boolean>

/**
* The status of the request. `pending` indicates that no request has been made yet and there is no cached data to
* display (`data.value = undefined`). `error` indicates that the last request failed. `success` indicates that the
* last request succeeded.
*/
status: ShallowRef<UseQueryStatus>

/**
* Ensures the current data is fresh. If the data is stale, refetch, if not return as is.
* @returns a promise that resolves when the refresh is done
*/
refresh: () => Promise<TResult>

/**
* Ignores fresh data and triggers a new fetch
* @returns a promise that resolves when the refresh is done
*/
refetch: () => Promise<TResult>
}

/**
* `true` refetch if data is stale, false never refetch, 'always' always refetch.
Expand Down

0 comments on commit c637a78

Please sign in to comment.