diff --git a/src/createQueryClient.spec-d.ts b/src/createQueryClient.spec-d.ts index 866e4c5..d7f9066 100644 --- a/src/createQueryClient.spec-d.ts +++ b/src/createQueryClient.spec-d.ts @@ -25,22 +25,26 @@ describe('query', () => { test('tags', async () => { const action = () => 'response' - const { query } = createQueryClient() - const numberTag = tag() + const { query, setQueryData } = createQueryClient() const stringTag = tag() const untypedTag = tag() - // @ts-expect-error - number tag not assignable to string action - query(action, [], { tags: [numberTag, stringTag] }) - - // @ts-expect-error - number tag not assignable to string action - query(action, [], { tags: () => [numberTag, stringTag] }) - query(action, [], { tags: [stringTag, untypedTag] }) query(action, [], { tags: () => [stringTag, untypedTag] }) query(action, [], { tags: [untypedTag] }) query(action, [], { tags: () => [untypedTag] }) + + setQueryData(stringTag, (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + 'bar' + }) + + // @ts-expect-error - sharedTag has data: never, can't return anything useful + setQueryData(untypedTag, (data) => { + expectTypeOf(data).toEqualTypeOf() + return 'could be corrupting' + }) }) }) }) @@ -99,59 +103,78 @@ describe('defineQuery', () => { }) describe('setQueryData', () => { - test('tags', async () => { + test('naked tag is not setQueryData-able (data: never)', () => { + const { setQueryData } = createQueryClient() + const myTag = tag() + + // @ts-expect-error - data: never, return must be never (effectively impossible) + setQueryData(myTag, () => 'anything') + }) + + test('typed tag passes data through with its declared type', () => { const { setQueryData } = createQueryClient() const numberTag = tag() - const stringTag = tag() - const untypedTag = tag() - setQueryData(untypedTag, (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' + setQueryData(numberTag, (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + 1 }) + // @ts-expect-error - returning wrong type setQueryData(numberTag, (data) => { expectTypeOf(data).toEqualTypeOf() - return 2 + return 'wrong type' }) + }) - setQueryData(stringTag, (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'new string' - }) + test('can type with single kind as well', () => { + const { setQueryData } = createQueryClient() + const usersTag = tag<{ user: { id: number } }>(['user']) - setQueryData([untypedTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' + setQueryData(usersTag.user, (data) => { + expectTypeOf(data).toEqualTypeOf<{ id: number }>() + return data }) + }) - setQueryData([numberTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 2 - }) + test('object handler on parent typed per kind', () => { + const { setQueryData } = createQueryClient() - // this is kinda interesting, no matter the data the return type is the union :thinking: - // so there's not really a type safe way to update multiple queries at once - setQueryData([numberTag, stringTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' - }) + type User = { id: string, name: string, email: string } + type Potato = { genus: string, species: string } + + const genericTag = tag<{ user: User, potato: Potato }>(['user', 'potato']) - setQueryData([untypedTag, stringTag, numberTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' + setQueryData(genericTag, { + user: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + }, + potato: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + }, }) - // @ts-expect-error - number tag not assignable to string action - setQueryData(numberTag, (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'string' + // function form on parent is locked when kinds are disjoint: + // setter takes UnionToIntersection = never + // @ts-expect-error - return type cannot satisfy never + setQueryData(genericTag, () => ({ id: '123', name: 'John', email: 'a@b' })) + + // function form on a specific kind works + setQueryData(genericTag.user, (data) => { + expectTypeOf(data).toEqualTypeOf() + return data }) + }) - // @ts-expect-error - number tag not assignable to string action - setQueryData([numberTag, stringTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return [] + test('object handler must cover every declared kind', () => { + const { setQueryData } = createQueryClient() + const genericTag = tag<{ a: number, b: string }>(['a', 'b']) + + // @ts-expect-error - missing 'b' kind in handler + setQueryData(genericTag, { + a: (data) => data, }) }) @@ -252,12 +275,12 @@ describe('refreshQueryData', () => { test('tags', () => { const { refreshQueryData } = createQueryClient() - const numberTag = tag() - const stringTag = tag() + const sharedTag = tag<{ count: number, name: string }>(['count', 'name']) const action = (param: number) => param - refreshQueryData(numberTag) - refreshQueryData([numberTag, stringTag]) + refreshQueryData(sharedTag) + refreshQueryData(sharedTag.count) + refreshQueryData(sharedTag.name) refreshQueryData(action) refreshQueryData(action, [2]) diff --git a/src/createQueryClient.spec.ts b/src/createQueryClient.spec.ts index 484ff8c..4c6e20e 100644 --- a/src/createQueryClient.spec.ts +++ b/src/createQueryClient.spec.ts @@ -562,41 +562,50 @@ describe('setQueryData', () => { await vi.runOnlyPendingTimersAsync() - setQueryData(stringTag, () => { - return 'bar' - }) - - setQueryData(numberTag, () => { - return 2 - }) + setQueryData(stringTag, () => 'bar') + setQueryData(numberTag, () => 2) expect(stringQuery.data).toBe('bar') expect(numberQuery.data).toBe(2) }) - test('tags', async () => { + test('kind tag setter only matches that kind\'s queries', async () => { const { setQueryData, query } = createQueryClient() - const stringTag = tag() - const numberTag = tag() + const sharedTag = tag<{ name: string, count: number }>(['name', 'count']) const stringAction = () => 'foo' const numberAction = () => 1 - const stringQuery = query(stringAction, [], { tags: [stringTag] }) - const numberQuery = query(numberAction, [], { tags: [numberTag] }) + const stringQuery = query(stringAction, [], { tags: [sharedTag.name] }) + const numberQuery = query(numberAction, [], { tags: [sharedTag.count] }) await vi.runOnlyPendingTimersAsync() - setQueryData([stringTag], () => { - return 'bar' - }) + setQueryData(sharedTag.name, (data) => data + '-bar') - setQueryData([numberTag], () => { - return 2 + expect(stringQuery.data).toBe('foo-bar') + expect(numberQuery.data).toBe(1) + }) + + test('object handler on parent dispatches to each kind', async () => { + const { setQueryData, query } = createQueryClient() + const sharedTag = tag<{ name: string, count: number }>(['name', 'count']) + + const stringAction = () => 'foo' + const numberAction = () => 1 + + const stringQuery = query(stringAction, [], { tags: [sharedTag.name] }) + const numberQuery = query(numberAction, [], { tags: [sharedTag.count] }) + + await vi.runOnlyPendingTimersAsync() + + setQueryData(sharedTag, { + name: (data) => data + '-bar', + count: (data) => data + 10, }) - expect(stringQuery.data).toBe('bar') - expect(numberQuery.data).toBe(2) + expect(stringQuery.data).toBe('foo-bar') + expect(numberQuery.data).toBe(11) }) test('action', async () => { @@ -656,8 +665,8 @@ describe('refreshQueryData', () => { const numberAction = vi.fn() const stringAction = vi.fn() - const numberTag = tag() - const stringTag = tag() + const numberTag = tag() + const stringTag = tag() query(numberAction, [], { tags: [numberTag] }) query(stringAction, [], { tags: [stringTag] }) @@ -795,7 +804,7 @@ describe('mutate', () => { [undefined], ])('refreshes tagged queries: %s', async (refreshQueryData) => { const { mutate, query } = createQueryClient() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn() @@ -817,7 +826,7 @@ describe('mutate', () => { test('does not refresh tagged queries if refreshQueryData is false', async () => { const { mutate, query } = createQueryClient() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn() @@ -839,7 +848,7 @@ describe('mutate', () => { test('does not refresh tagged queries if the action throws an error', async () => { const { mutate, query } = createQueryClient() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn(() => { throw new Error() @@ -1047,7 +1056,7 @@ describe('useMutation', () => { test('setQueryDataBefore and setQueryDataAfter are called when the mutation is executed', async () => { const { useMutation, query } = createQueryClient() const { promise, resolve } = Promise.withResolvers() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn(() => promise) const setQueryDataBefore = vi.fn() @@ -1306,8 +1315,8 @@ describe('defineMutation', () => { const { promise, resolve } = Promise.withResolvers() const mutationPayload = 1 const mutationAction = (value: number) => promise.then(() => value) - const tagA = tag() - const tagB = tag() + const tagA = tag() + const tagB = tag() const queryAResponse = 1 const queryBResponse = 1 const queryAAction = () => queryAResponse @@ -1627,8 +1636,8 @@ describe('defineMutation', () => { const { promise, resolve } = Promise.withResolvers() const mutationPayload = 1 const mutationAction = (value: number) => promise.then(() => value) - const tagA = tag() - const tagB = tag() + const tagA = tag() + const tagB = tag() const queryAResponse = 1 const queryBResponse = 1 const queryAAction = () => queryAResponse diff --git a/src/createQueryClient.ts b/src/createQueryClient.ts index 855a6f5..e7ddd8a 100644 --- a/src/createQueryClient.ts +++ b/src/createQueryClient.ts @@ -13,7 +13,7 @@ import { } from './types/client' import { createQueryGroups } from './createQueryGroups' import { createUseQuery } from './createUseQuery' -import { isQueryTag, isQueryTags, QueryTag } from './types/tags' +import { isQueryTag, QueryTag } from './types/tags' import { isArray } from './utilities/arrays' import { assertNever } from './utilities/assert' import { QueryGroup } from './createQueryGroup' @@ -59,8 +59,8 @@ export function createQueryClient(options?: ClientOptions): QueryClient { } const setQueryData: SetQueryData = ( - param1: QueryTag | QueryTag[] | QueryAction, - param2: Parameters | QueryDataSetter, + param1: QueryTag | QueryAction, + param2: Parameters | QueryDataSetter | Record, param3?: QueryDataSetter, ): void => { const setDataForGroups = (groups: QueryGroup[], setter: QueryDataSetter): void => { @@ -72,12 +72,25 @@ export function createQueryClient(options?: ClientOptions): QueryClient { }) } - if (isQueryTag(param1) || isQueryTags(param1)) { - const tags = param1 - const setter = param2 as QueryDataSetter - const groups = getQueryGroups(tags) + if (isQueryTag(param1)) { + const queryTag = param1 - setDataForGroups(groups, setter) + if (typeof param2 === 'function') { + const setter = param2 + const groups = getQueryGroups(queryTag) + + setDataForGroups(groups, setter) + } else { + const handler = param2 as unknown as Record + + for (const kind of queryTag.kinds) { + const childTag = (queryTag as unknown as Record)[kind] + const setter = handler[kind] + const groups = getQueryGroups(childTag) + + setDataForGroups(groups, setter) + } + } return } @@ -107,12 +120,12 @@ export function createQueryClient(options?: ClientOptions): QueryClient { } const refreshQueryData: RefreshQueryData = ( - param1: QueryTag | QueryTag[] | QueryAction, + param1: QueryTag | QueryAction, param2?: Parameters, ): void => { - if (isQueryTag(param1) || isQueryTags(param1)) { - const tags = param1 - const groups = getQueryGroups(tags) + if (isQueryTag(param1)) { + const queryTag = param1 + const groups = getQueryGroups(queryTag) groups.forEach((group) => { group.execute() @@ -133,7 +146,7 @@ export function createQueryClient(options?: ClientOptions): QueryClient { return } - assertNever(param1, 'Invalid arguments given to setQueryData') + assertNever(param1, 'Invalid arguments given to refreshQueryData') } const mutate: MutationFunction = (action, parameters, options) => { diff --git a/src/createQueryGroup.spec.ts b/src/createQueryGroup.spec.ts index 25144b0..915c296 100644 --- a/src/createQueryGroup.spec.ts +++ b/src/createQueryGroup.spec.ts @@ -244,31 +244,27 @@ describe('given group with tags', () => { test('can check if it has a tag', async () => { const group = createQueryGroup(vi.fn(), []) const tag1 = tag() - const tag2 = tag((value: string) => value) + const tag2 = tag() expect(group.hasTag(tag1)).toBe(false) const query1 = group.createQuery({ tags: [tag1] }) - const query2 = group.createQuery({ tags: [tag2('foo')] }) + const query2 = group.createQuery({ tags: [tag2] }) - // need executed to happen for tag factories await vi.advanceTimersByTimeAsync(0) expect(group.hasTag(tag1)).toBe(true) - expect(group.hasTag(tag2('foo'))).toBe(true) - expect(group.hasTag(tag2('bar'))).toBe(false) + expect(group.hasTag(tag2)).toBe(true) query1.dispose() expect(group.hasTag(tag1)).toBe(false) - expect(group.hasTag(tag2('foo'))).toBe(true) - expect(group.hasTag(tag2('bar'))).toBe(false) + expect(group.hasTag(tag2)).toBe(true) query2.dispose() expect(group.hasTag(tag1)).toBe(false) - expect(group.hasTag(tag2('foo'))).toBe(false) - expect(group.hasTag(tag2('bar'))).toBe(false) + expect(group.hasTag(tag2)).toBe(false) }) }) diff --git a/src/createQueryGroupTags.ts b/src/createQueryGroupTags.ts index 1875e2a..8ff791c 100644 --- a/src/createQueryGroupTags.ts +++ b/src/createQueryGroupTags.ts @@ -2,52 +2,55 @@ import { QueryTag } from './types/tags' import { TagKey } from './getTagKey' export function createQueryGroupTags() { - const tags = new Map>() - const queries = new Map>() + const tagKeyToQueryIds = new Map>() + const queryIdToTags = new Map>() function clear(): void { - tags.clear() - queries.clear() + tagKeyToQueryIds.clear() + queryIdToTags.clear() } function has(tag: QueryTag): boolean { - return tags.has(tag.key) + return tagKeyToQueryIds.has(tag.key) } - function getQueryIdsByTag(tag: QueryTag): Set { - if (!tags.has(tag.key)) { - tags.set(tag.key, new Set()) + function getQueryIdsForKey(key: TagKey): Set { + if (!tagKeyToQueryIds.has(key)) { + tagKeyToQueryIds.set(key, new Set()) } - return tags.get(tag.key)! + return tagKeyToQueryIds.get(key)! } function getTagsByQueryId(queryId: number): Set { - if (!queries.has(queryId)) { - queries.set(queryId, new Set()) + if (!queryIdToTags.has(queryId)) { + queryIdToTags.set(queryId, new Set()) } - return queries.get(queryId)! + return queryIdToTags.get(queryId)! } function addTag(tag: QueryTag, queryId: number): void { - getQueryIdsByTag(tag).add(queryId) + for (const key of tag.keys) { + getQueryIdsForKey(key).add(queryId) + } + getTagsByQueryId(queryId).add(tag) } function removeTag(tag: QueryTag, queryId: number): void { - const queryTags = getQueryIdsByTag(tag) - const tagQueries = getTagsByQueryId(queryId) - - queryTags.delete(queryId) - tagQueries.delete(tag) - - if (queryTags.size === 0) { - tags.delete(tag.key) + for (const key of tag.keys) { + const queryIds = getQueryIdsForKey(key) + queryIds.delete(queryId) + if (queryIds.size === 0) { + tagKeyToQueryIds.delete(key) + } } - if (tagQueries.size === 0) { - queries.delete(queryId) + const tagSet = getTagsByQueryId(queryId) + tagSet.delete(tag) + if (tagSet.size === 0) { + queryIdToTags.delete(queryId) } } diff --git a/src/createUseQuery.ts b/src/createUseQuery.ts index 17e9878..6cc39ba 100644 --- a/src/createUseQuery.ts +++ b/src/createUseQuery.ts @@ -1,7 +1,7 @@ import { CreateQuery } from './createQueryGroups' import { Query, QueryAction, QueryActionArgs } from './types/query' import { onScopeDispose, ref, toRef, toRefs, toValue, watch } from 'vue' -import equal from "fast-deep-equal" +import equal from 'fast-deep-equal' import { isDefined } from './utilities' import { UseQueryOptions } from './types/client' @@ -23,7 +23,7 @@ export function createUseQuery(createQuery: CreateQuery, action: QueryAction, pa watch(() => ({ enabled: enabled.value, parameters: toValue(parameters) }) as const, ({ enabled, parameters }, previous) => { const isSameParameters = previous && isDefined(previous.parameters) && equal(previous.parameters, parameters) - const isSameEnabled = previous && previous.enabled === enabled + const isSameEnabled = previous?.enabled === enabled if (isSameParameters && isSameEnabled) { return diff --git a/src/getAllTags.spec.ts b/src/getAllTags.spec.ts index b932cf4..76e14c4 100644 --- a/src/getAllTags.spec.ts +++ b/src/getAllTags.spec.ts @@ -4,18 +4,18 @@ import { tag } from './tag' const tagA = tag() const tagB = tag() -const tagC = tag((input: string) => input) +const tagC = tag() test('given tags returns all tags', () => { - const tags = getAllTags([tagA, tagB, tagC('foo')], undefined) + const tags = getAllTags([tagA, tagB, tagC], undefined) - expect(tags).toEqual([tagA, tagB, tagC('foo')]) + expect(tags).toEqual([tagA, tagB, tagC]) }) test('given a function returns all tags', () => { - const tags = getAllTags((input: string) => [tagA, tagB, tagC(input)], 'foo') + const tags = getAllTags(() => [tagA, tagB, tagC], 'foo') - expect(tags).toEqual([tagA, tagB, tagC('foo')]) + expect(tags).toEqual([tagA, tagB, tagC]) }) test('given no tags returns an empty array', () => { diff --git a/src/tag.spec-d.ts b/src/tag.spec-d.ts index 312f569..2473909 100644 --- a/src/tag.spec-d.ts +++ b/src/tag.spec-d.ts @@ -1,38 +1,38 @@ import { expectTypeOf, test, vi } from 'vitest' -import { QueryTag, QueryTagFactory, Unset } from '@/types/tags' +import { QueryTag } from '@/types/tags' import { createQueryClient } from './createQueryClient' import { tag } from './tag' -test('tag function returns a tag when no callback is provided', () => { +test('tag function returns a QueryTag', () => { const value = tag() expectTypeOf(value).toExtend() + expectTypeOf(value).toEqualTypeOf>() }) -test('tag function returns a tag factory when a callback is provided', () => { - const factory = tag((string: string) => string) +test('tag() returns a typed QueryTag', () => { + const value = tag() - expectTypeOf(factory).toExtend>() - - const value = factory('foo') - - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(value).toEqualTypeOf>() }) -test('tag function returns a typed tag when data generic is provided', () => { - const value = tag() +test('tag with kinds exposes each kind as a typed descendant tag', () => { + type User = { id: number } + type Potato = { genus: string } - expectTypeOf(value).toEqualTypeOf>() -}) + const usersTag = tag<{ user: User, potato: Potato }>(['user', 'potato']) -test('tag factory returns a typed tag when data generic is provided', () => { - const factory = tag((value: string) => value) + expectTypeOf(usersTag.user).toEqualTypeOf>() + expectTypeOf(usersTag.potato).toEqualTypeOf>() +}) - expectTypeOf(factory).toEqualTypeOf>() +test('parent of kinds has union data type', () => { + type User = { id: number } + type Potato = { genus: string } - const value = factory('foo') + const usersTag = tag<{ user: User, potato: Potato }>(['user', 'potato']) - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(usersTag.data).toEqualTypeOf() }) test('query from query function with tags callback is called with the query data', () => { diff --git a/src/tag.spec.ts b/src/tag.spec.ts index e8d9ac9..e8b16cf 100644 --- a/src/tag.spec.ts +++ b/src/tag.spec.ts @@ -8,19 +8,32 @@ test('tags are unique', () => { expect(tag1).not.toBe(tag2) }) -test('tag factories are unique', () => { - const factory1 = tag((string: string) => string) - const factory2 = tag((string: string) => string) - const value1 = factory1('foo') - const value2 = factory2('foo') +test('tag with kinds exposes each kind as a property tag', () => { + const usersTag = tag<{ user: { id: number }, potato: { genus: string } }>(['user', 'potato']) - expect(value1).not.toBe(value2) + expect(usersTag.user).toBeDefined() + expect(usersTag.potato).toBeDefined() + expect(usersTag.user.key).not.toBe(usersTag.potato.key) + expect(usersTag.user.key).not.toBe(usersTag.key) }) -test('tag factory returns the same key when given the same value', () => { - const factory = tag((string: string) => string) - const value1 = factory('foo') - const value2 = factory('foo') +test('kind tags carry the parent key as an ancestor', () => { + const usersTag = tag<{ user: { id: number } }>(['user']) - expect(value1.key).toBe(value2.key) + expect(usersTag.user.keys).toContain(usersTag.key) + expect(usersTag.user.keys).toContain(usersTag.user.key) +}) + +test('tag without kinds has empty kinds array', () => { + const baseTag = tag() + const numberTag = tag() + + expect(baseTag.kinds).toEqual([]) + expect(numberTag.kinds).toEqual([]) +}) + +test('tag with kinds reports its kind names', () => { + const usersTag = tag<{ user: { id: number }, potato: { genus: string } }>(['user', 'potato']) + + expect(usersTag.kinds).toEqual(['user', 'potato']) }) diff --git a/src/tag.ts b/src/tag.ts index 1acb2d9..2308c61 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -1,24 +1,31 @@ import { createSequence } from './createSequence' -import { getTagKey } from './getTagKey' -import { QueryTagFactory, QueryTagCallback, QueryTag, Unset, unset } from './types/tags' +import { getTagKey, TagKey } from './getTagKey' +import { QueryTag, QueryTagKinds, unset } from './types/tags' const createTagId = createSequence() -function createQueryTag(id: number, value: unknown): QueryTag { - return { +function createTag(parentKeys: readonly TagKey[], kindNames: readonly string[]): QueryTag { + const id = createTagId() + const ownKey = getTagKey(id, undefined) + const keys = Object.freeze([...parentKeys, ownKey]) + + const queryTag: Record = { data: unset, - key: getTagKey(id, value), + key: ownKey, + keys, + kinds: Object.freeze([...kindNames]), } -} -export function tag(): QueryTag -export function tag(callback: QueryTagCallback): QueryTagFactory -export function tag(callback?: QueryTagCallback): QueryTag | QueryTagFactory { - const id = createTagId() - - if (callback) { - return (value) => createQueryTag(id, callback(value)) + for (const kindName of kindNames) { + queryTag[kindName] = createTag(keys, []) } - return createQueryTag(id, undefined) + return queryTag as unknown as QueryTag +} + +export function tag(): QueryTag +export function tag(): QueryTag +export function tag(kinds: (keyof TKinds & string)[]): QueryTag +export function tag(kinds?: readonly string[]) { + return createTag([], kinds ?? []) } diff --git a/src/types/client.ts b/src/types/client.ts index 768e885..f6f8a05 100644 --- a/src/types/client.ts +++ b/src/types/client.ts @@ -1,7 +1,7 @@ import { MutationFunction, MutationComposition, DefineMutation } from './mutation' import { Query, QueryOptions, QueryAction, QueryActionArgs, QueryData } from './query' -import { QueryTag, QueryTagType } from './tags' -import { DefaultValue } from './utilities' +import { QueryTag, QueryTagKindsOf, QueryTagType } from './tags' +import { DefaultValue, UnionToIntersection } from './utilities' export type QueryClient = { query: QueryFunction, @@ -62,14 +62,21 @@ export type DefinedQuery< export type QueryDataSetter = (data: T) => T +type KindHandler = { [K in keyof TKinds]: QueryDataSetter } + +export type SetQueryDataValue = + keyof QueryTagKindsOf extends never + ? QueryDataSetter> + : QueryDataSetter>> | KindHandler> + export type SetQueryData = { - (tag: TQueryTag | TQueryTag[], setter: QueryDataSetter>): void, + (tag: TQueryTag, setter: SetQueryDataValue): void, (action: TAction, setter: QueryDataSetter>): void, (action: TAction, parameters: Parameters, setter: QueryDataSetter>): void, } export type RefreshQueryData = { - (tag: TQueryTag | TQueryTag[]): void, + (tag: QueryTag): void, (action: QueryAction): void, (action: TAction, parameters: Parameters): void, } diff --git a/src/types/query.ts b/src/types/query.ts index a3a5cb7..0368279 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -1,6 +1,6 @@ import { RetryOptions } from '@/utilities/retry' import { Getter } from './getters' -import { QueryTag, Unset } from '@/types/tags' +import { QueryTag } from '@/types/tags' import { DefaultValue } from './utilities' export type QueryAction = (...args: any[]) => any @@ -19,7 +19,7 @@ export type QueryActionArgs< export type QueryTags< TAction extends QueryAction = QueryAction -> = QueryTag | Unset>[] | ((value: QueryData) => QueryTag | Unset>[]) +> = QueryTag>[] | ((value: QueryData) => QueryTag>[]) export type QueryOptions< TAction extends QueryAction = QueryAction, diff --git a/src/types/tags.ts b/src/types/tags.ts index 892fd30..76aab5e 100644 --- a/src/types/tags.ts +++ b/src/types/tags.ts @@ -3,26 +3,49 @@ import { TagKey } from '@/getTagKey' export const unset = Symbol('unset') export type Unset = typeof unset +export type QueryTagKinds = Record + export type QueryTag< - TData = unknown + TData = unknown, + TKinds extends QueryTagKinds = Record > = { /** * @private * @internal - * This property is unused, but necessary to preserve the type for TData because unused generics are ignored by typescript. + * Phantom field used purely to preserve the TData generic in the type; + * the runtime value is always the `unset` symbol regardless of TData. */ data: TData, + /** + * The tag's own unique key. + */ key: TagKey, + /** + * Own key plus all ancestor keys, in root-to-leaf order. A query tagged + * with this tag is registered against every key in this list, so + * setQueryData / invalidateQueries on any ancestor matches the query. + */ + keys: readonly TagKey[], + /** + * Names of the descendant kinds declared on this tag, in declaration order. + * Empty for leaf tags. Used at runtime to dispatch object-form + * setQueryData handlers to the correct descendant tag. + */ + kinds: readonly string[], +} & { + [K in keyof TKinds]: QueryTag } export type QueryTagType = TQueryTag extends QueryTag - ? TData extends Unset - ? unknown - : TData + ? TData + : never + +export type QueryTagKindsOf = TQueryTag extends QueryTag + ? TKinds : never export function isQueryTag(tag: unknown): tag is QueryTag { - return typeof tag === 'object' && tag !== null && 'data' in tag && 'key' in tag + return typeof tag === 'object' && tag !== null && 'data' in tag && 'key' in tag && 'keys' in tag && 'kinds' in tag } export function isQueryTags(tags: unknown): tags is QueryTag[] { diff --git a/src/types/utilities.ts b/src/types/utilities.ts index 24e26ad..3d14cf5 100644 --- a/src/types/utilities.ts +++ b/src/types/utilities.ts @@ -1 +1,4 @@ export type DefaultValue = unknown extends TValue ? TDefault : TValue + +export type UnionToIntersection = + (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never diff --git a/src/utilities/createDefinedMutationOptions.ts b/src/utilities/createDefinedMutationOptions.ts index 047ecea..a435b70 100644 --- a/src/utilities/createDefinedMutationOptions.ts +++ b/src/utilities/createDefinedMutationOptions.ts @@ -47,14 +47,18 @@ export function createDefinedMutationOptions({ const tags = getAllTags(options?.tags, undefined) const setter = (data: QueryData) => setQueryDataBefore(data, context) - setQueryData(tags, setter) + for (const tag of tags) { + setQueryData(tag, setter) + } } if (definedSetQueryDataBefore) { const tags = getAllTags(definedOptions?.tags, undefined) const setter = (data: QueryData) => definedSetQueryDataBefore(data, context) - setQueryData(tags, setter) + for (const tag of tags) { + setQueryData(tag, setter) + } } onExecute?.(context) @@ -66,16 +70,26 @@ export function createDefinedMutationOptions({ const definedTags = getAllTags(definedOptions?.tags, context.data) if (shouldRefreshQueryData) { - refreshQueryData(tags) - refreshQueryData(definedTags) + for (const tag of tags) { + refreshQueryData(tag) + } + for (const tag of definedTags) { + refreshQueryData(tag) + } } if (setQueryDataAfter) { - setQueryData(tags, (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context)) + const setter = (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context) + for (const tag of tags) { + setQueryData(tag, setter) + } } if (definedSetQueryDataAfter) { - setQueryData(definedTags, (queryData: QueryData): QueryData => definedSetQueryDataAfter(queryData, context)) + const setter = (queryData: QueryData): QueryData => definedSetQueryDataAfter(queryData, context) + for (const tag of definedTags) { + setQueryData(tag, setter) + } } onSuccess?.(context) diff --git a/src/utilities/createMutationOptions.ts b/src/utilities/createMutationOptions.ts index e7e726b..27d7ab2 100644 --- a/src/utilities/createMutationOptions.ts +++ b/src/utilities/createMutationOptions.ts @@ -26,7 +26,10 @@ export function createMutationOptions({ options, setQueryData, refreshQueryData payload: context.payload, } satisfies MutationTagsBeforeContext) - setQueryData(tags, (queryData: QueryData): QueryData => setQueryDataBefore(queryData, context)) + const setter = (queryData: QueryData): QueryData => setQueryDataBefore(queryData, context) + for (const tag of tags) { + setQueryData(tag, setter) + } } onExecute?.(context) @@ -39,11 +42,16 @@ export function createMutationOptions({ options, setQueryData, refreshQueryData } satisfies MutationTagsAfterContext) if (options?.refreshQueryData ?? true) { - refreshQueryData(tags) + for (const tag of tags) { + refreshQueryData(tag) + } } if (setQueryDataAfter) { - setQueryData(tags, (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context)) + const setter = (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context) + for (const tag of tags) { + setQueryData(tag, setter) + } } onSuccess?.(context)