diff --git a/.changeset/thin-worms-check.md b/.changeset/thin-worms-check.md new file mode 100644 index 000000000..b69f56901 --- /dev/null +++ b/.changeset/thin-worms-check.md @@ -0,0 +1,5 @@ +--- +'houdini': patch +--- + +Add match argument to dedupe directive diff --git a/packages/houdini/src/codegen/generators/artifacts/index.ts b/packages/houdini/src/codegen/generators/artifacts/index.ts index 17ee695a8..2abd512b0 100644 --- a/packages/houdini/src/codegen/generators/artifacts/index.ts +++ b/packages/houdini/src/codegen/generators/artifacts/index.ts @@ -4,6 +4,7 @@ import * as recast from 'recast' import type { CachePolicies, Config, + DedupeMatchModes, Document, DocumentArtifact, MutationArtifact, @@ -12,6 +13,7 @@ import type { } from '../../../lib' import { ArtifactKind, + DedupeMatchMode, HoudiniError, cleanupFiles, fs, @@ -233,12 +235,22 @@ export default function artifactGenerator(stats: { (arg) => arg.name.value === 'cancelFirst' ) - dedupe = - cancelFirstArg && - cancelFirstArg.value.kind === 'BooleanValue' && - cancelFirstArg.value - ? 'first' - : 'last' + const matchArg = dedupeDirective.arguments?.find( + (arg) => arg.name.value === 'match' + ) + + dedupe = { + cancel: + cancelFirstArg && + cancelFirstArg.value.kind === 'BooleanValue' && + cancelFirstArg.value + ? 'first' + : 'last', + match: + matchArg && matchArg.value.kind === 'EnumValue' + ? (matchArg.value.value as DedupeMatchModes) + : DedupeMatchMode.Operation, + } } // use this selection set diff --git a/packages/houdini/src/codegen/generators/artifacts/tests/artifacts.test.ts b/packages/houdini/src/codegen/generators/artifacts/tests/artifacts.test.ts index 66006179d..fbe8fdf57 100644 --- a/packages/houdini/src/codegen/generators/artifacts/tests/artifacts.test.ts +++ b/packages/houdini/src/codegen/generators/artifacts/tests/artifacts.test.ts @@ -846,7 +846,11 @@ test('paginate over unions', async function () { }, "pluginData": {}, - "dedupe": "last", + + "dedupe": { + "cancel": "last", + "match": "Variables" + }, "input": { "fields": { @@ -4932,7 +4936,11 @@ describe('mutation artifacts', function () { }, "pluginData": {}, - "dedupe": "last", + + "dedupe": { + "cancel": "last", + "match": "Variables" + }, "input": { "fields": { @@ -5185,7 +5193,11 @@ describe('mutation artifacts', function () { }, "pluginData": {}, - "dedupe": "last", + + "dedupe": { + "cancel": "last", + "match": "Variables" + }, "input": { "fields": { @@ -7774,7 +7786,12 @@ test('persists dedupe which', async function () { }, "pluginData": {}, - "dedupe": "last", + + "dedupe": { + "cancel": "last", + "match": "Operation" + }, + "policy": "CacheOrNetwork", "partial": false }; @@ -7846,7 +7863,12 @@ test('persists dedupe first', async function () { }, "pluginData": {}, - "dedupe": "first", + + "dedupe": { + "cancel": "first", + "match": "Operation" + }, + "policy": "CacheOrNetwork", "partial": false }; @@ -7854,3 +7876,159 @@ test('persists dedupe first', async function () { "HoudiniHash=3dfb64916aa4359cf85f08b3544bbc7382fd818935c5a0e92f324a2d2519c227"; `) }) + +describe('Parses the correct matching mode', function () { + test('match mode variables', async function () { + // the config to use in tests + const config = testConfig() + // the documents to test + const docs: Document[] = [ + mockCollectedDoc(` + query FindUser @dedupe(match: Variables) { + usersByOffset { + name + } + } + `), + ] + + // execute the generator + await runPipeline(config, docs) + + // load the contents of the file + expect(docs[0]).toMatchInlineSnapshot(` + export default { + "name": "FindUser", + "kind": "HoudiniQuery", + "hash": "63be02f78e12d6dd155da0aac94892e700a5be1eeb66dfc2305740ce2464dd3b", + + "raw": \`query FindUser { + usersByOffset { + name + id + } + } + \`, + + "rootType": "Query", + "stripVariables": [], + + "selection": { + "fields": { + "usersByOffset": { + "type": "User", + "keyRaw": "usersByOffset", + + "selection": { + "fields": { + "name": { + "type": "String", + "keyRaw": "name", + "visible": true + }, + + "id": { + "type": "ID", + "keyRaw": "id", + "visible": true + } + } + }, + + "visible": true + } + } + }, + + "pluginData": {}, + + "dedupe": { + "cancel": "last", + "match": "Variables" + }, + + "policy": "CacheOrNetwork", + "partial": false + }; + + "HoudiniHash=f3faa6e93bde578b11490f9a32518e410a47ec242b0ef94331fc4fb5b01ace20"; + `) + }) + + test('match mode operation', async function () { + // the config to use in tests + const config = testConfig() + // the documents to test + const docs: Document[] = [ + mockCollectedDoc(` + query FindUser @dedupe(match: Operation) { + usersByOffset { + name + } + } + `), + ] + + // execute the generator + await runPipeline(config, docs) + + // load the contents of the file + expect(docs[0]).toMatchInlineSnapshot(` + export default { + "name": "FindUser", + "kind": "HoudiniQuery", + "hash": "63be02f78e12d6dd155da0aac94892e700a5be1eeb66dfc2305740ce2464dd3b", + + "raw": \`query FindUser { + usersByOffset { + name + id + } + } + \`, + + "rootType": "Query", + "stripVariables": [], + + "selection": { + "fields": { + "usersByOffset": { + "type": "User", + "keyRaw": "usersByOffset", + + "selection": { + "fields": { + "name": { + "type": "String", + "keyRaw": "name", + "visible": true + }, + + "id": { + "type": "ID", + "keyRaw": "id", + "visible": true + } + } + }, + + "visible": true + } + } + }, + + "pluginData": {}, + + "dedupe": { + "cancel": "last", + "match": "Operation" + }, + + "policy": "CacheOrNetwork", + "partial": false + }; + + "HoudiniHash=1e1c9cd888a109d85a8bda7c3470aeb645b25678fa17916a3b016816b7a9d783"; + `) + }) +}) diff --git a/packages/houdini/src/codegen/generators/artifacts/tests/pagination.test.ts b/packages/houdini/src/codegen/generators/artifacts/tests/pagination.test.ts index 5533d2994..86ff7be5d 100644 --- a/packages/houdini/src/codegen/generators/artifacts/tests/pagination.test.ts +++ b/packages/houdini/src/codegen/generators/artifacts/tests/pagination.test.ts @@ -781,7 +781,11 @@ test('cursor as scalar gets the right pagination query argument types', async fu }, "pluginData": {}, - "dedupe": "last", + + "dedupe": { + "cancel": "last", + "match": "Variables" + }, "input": { "fields": { diff --git a/packages/houdini/src/codegen/generators/definitions/enums.test.ts b/packages/houdini/src/codegen/generators/definitions/enums.test.ts index 883d8b16a..424363c6e 100644 --- a/packages/houdini/src/codegen/generators/definitions/enums.test.ts +++ b/packages/houdini/src/codegen/generators/definitions/enums.test.ts @@ -24,6 +24,14 @@ test('generates runtime definitions for each enum', async function () { expect(parsedQuery).toMatchInlineSnapshot(` type ValuesOf = T[keyof T] + export declare const DedupeMatchMode: { + readonly Variables: "Variables"; + readonly Operation: "Operation"; + readonly None: "None"; + } + + export type DedupeMatchMode$options = ValuesOf + /** Documentation of testenum1 */ export declare const TestEnum1: { /** Documentation of Value1 */ @@ -72,5 +80,11 @@ test('generates runtime definitions for each enum', async function () { "Value3": "Value3", "Value2": "Value2" }; + + export const DedupeMatchMode = { + "Variables": "Variables", + "Operation": "Operation", + "None": "None" + }; `) }) diff --git a/packages/houdini/src/codegen/generators/definitions/schema.test.ts b/packages/houdini/src/codegen/generators/definitions/schema.test.ts index d2a2242ad..ee3a5c7ef 100644 --- a/packages/houdini/src/codegen/generators/definitions/schema.test.ts +++ b/packages/houdini/src/codegen/generators/definitions/schema.test.ts @@ -39,8 +39,11 @@ test('adds internal documents to schema', async function () { """ @dedupe is used to prevent an operation from running more than once at the same time. If the cancelFirst arg is set to true, the response already in flight will be canceled instead of the second one. + If match is set to Operation, then a request will be deduplicated any time there is a request with the same operation. + If it's set to Variables then the request will only be deduplicated if the variables match. If match is set to None, + then the request will never be deduplicated. """ - directive @dedupe(cancelFirst: Boolean) on QUERY | MUTATION + directive @dedupe(cancelFirst: Boolean, match: DedupeMatchMode) on QUERY | MUTATION """@optimisticKey is used to identify a field as an optimistic key""" directive @optimisticKey on FIELD @@ -101,6 +104,12 @@ test('adds internal documents to schema', async function () { SinglePage } + enum DedupeMatchMode { + Variables + Operation + None + } + scalar ViewerIDFromSession `) }) @@ -137,8 +146,11 @@ test('list operations are included', async function () { """ @dedupe is used to prevent an operation from running more than once at the same time. If the cancelFirst arg is set to true, the response already in flight will be canceled instead of the second one. + If match is set to Operation, then a request will be deduplicated any time there is a request with the same operation. + If it's set to Variables then the request will only be deduplicated if the variables match. If match is set to None, + then the request will never be deduplicated. """ - directive @dedupe(cancelFirst: Boolean) on QUERY | MUTATION + directive @dedupe(cancelFirst: Boolean, match: DedupeMatchMode) on QUERY | MUTATION """@optimisticKey is used to identify a field as an optimistic key""" directive @optimisticKey on FIELD @@ -199,6 +211,12 @@ test('list operations are included', async function () { SinglePage } + enum DedupeMatchMode { + Variables + Operation + None + } + scalar ViewerIDFromSession directive @User_delete repeatable on FIELD @@ -254,8 +272,11 @@ test('list operations are included but delete directive should not be in when we """ @dedupe is used to prevent an operation from running more than once at the same time. If the cancelFirst arg is set to true, the response already in flight will be canceled instead of the second one. + If match is set to Operation, then a request will be deduplicated any time there is a request with the same operation. + If it's set to Variables then the request will only be deduplicated if the variables match. If match is set to None, + then the request will never be deduplicated. """ - directive @dedupe(cancelFirst: Boolean) on QUERY | MUTATION + directive @dedupe(cancelFirst: Boolean, match: DedupeMatchMode) on QUERY | MUTATION """@optimisticKey is used to identify a field as an optimistic key""" directive @optimisticKey on FIELD @@ -316,6 +337,12 @@ test('list operations are included but delete directive should not be in when we SinglePage } + enum DedupeMatchMode { + Variables + Operation + None + } + scalar ViewerIDFromSession directive @User_delete repeatable on FIELD @@ -384,8 +411,11 @@ test("writing twice doesn't duplicate definitions", async function () { """ @dedupe is used to prevent an operation from running more than once at the same time. If the cancelFirst arg is set to true, the response already in flight will be canceled instead of the second one. + If match is set to Operation, then a request will be deduplicated any time there is a request with the same operation. + If it's set to Variables then the request will only be deduplicated if the variables match. If match is set to None, + then the request will never be deduplicated. """ - directive @dedupe(cancelFirst: Boolean) on QUERY | MUTATION + directive @dedupe(cancelFirst: Boolean, match: DedupeMatchMode) on QUERY | MUTATION """@optimisticKey is used to identify a field as an optimistic key""" directive @optimisticKey on FIELD @@ -446,6 +476,12 @@ test("writing twice doesn't duplicate definitions", async function () { SinglePage } + enum DedupeMatchMode { + Variables + Operation + None + } + scalar ViewerIDFromSession `) }) diff --git a/packages/houdini/src/codegen/transforms/paginate.test.ts b/packages/houdini/src/codegen/transforms/paginate.test.ts index 4095b16f8..e296578a8 100644 --- a/packages/houdini/src/codegen/transforms/paginate.test.ts +++ b/packages/houdini/src/codegen/transforms/paginate.test.ts @@ -1061,7 +1061,56 @@ test('query with forwards cursor paginate', async function () { // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users($first: Int = 10, $after: String) @dedupe { + query Users($first: Int = 10, $after: String) @dedupe(match: Variables) { + usersByForwardsCursor(first: $first, after: $after) @paginate { + edges { + node { + id + } + } + edges { + cursor + node { + __typename + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + `) +}) + +test('suppress pagination dedupe', async function () { + const docs = [ + mockCollectedDoc( + ` + query Users { + usersByForwardsCursor(first: 10) @paginate { + edges { + node { + id + } + } + } + } + ` + ), + ] + + // run the pipeline + const config = testConfig({ + supressPaginationDeduplication: true, + }) + await runPipeline(config, docs) + + // load the contents of the file + expect(docs[0]?.document).toMatchInlineSnapshot(` + query Users($first: Int = 10, $after: String) { usersByForwardsCursor(first: $first, after: $after) @paginate { edges { node { @@ -1108,7 +1157,7 @@ test('query with custom first args', async function () { // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users($limit: Int!, $after: String) @dedupe { + query Users($limit: Int!, $after: String) @dedupe(match: Variables) { usersByForwardsCursor(first: $limit, after: $after) @paginate { edges { node { @@ -1155,7 +1204,7 @@ test('query with backwards cursor paginate', async function () { // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users @dedupe { + query Users @dedupe(match: Variables) { usersByBackwardsCursor(last: 10) @paginate { edges { node { @@ -1198,7 +1247,7 @@ test('query with offset paginate', async function () { // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users($limit: Int = 10, $offset: Int) @dedupe { + query Users($limit: Int = 10, $offset: Int) @dedupe(match: Variables) { usersByOffset(limit: $limit, offset: $offset) @paginate { id } @@ -1229,7 +1278,7 @@ test('query with backwards cursor on full paginate', async function () { // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users($first: Int, $after: String, $last: Int = 10, $before: String) @dedupe { + query Users($first: Int, $after: String, $last: Int = 10, $before: String) @dedupe(match: Variables) { usersByCursor(last: $last, first: $first, after: $after, before: $before) @paginate { edges { node { @@ -1276,7 +1325,7 @@ test('query with forwards cursor on full paginate', async function () { // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users($first: Int = 10, $after: String, $last: Int, $before: String) @dedupe { + query Users($first: Int = 10, $after: String, $last: Int, $before: String) @dedupe(match: Variables) { usersByCursor(first: $first, after: $after, last: $last, before: $before) @paginate { edges { node { @@ -1323,7 +1372,7 @@ test("don't generate unsupported directions", async function () { // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users($first: Int = 10, $after: String) @dedupe { + query Users($first: Int = 10, $after: String) @dedupe(match: Variables) { usersByForwardsCursor(first: $first, after: $after) @paginate { edges { node { @@ -1370,7 +1419,7 @@ test("forwards cursor paginated query doesn't overlap variables", async function // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users($first: Int!, $after: String, $last: Int, $before: String) @dedupe { + query Users($first: Int!, $after: String, $last: Int, $before: String) @dedupe(match: Variables) { usersByCursor(first: $first, after: $after, last: $last, before: $before) @paginate { edges { node { @@ -1417,7 +1466,7 @@ test("backwards cursor paginated query doesn't overlap variables", async functio // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users($last: Int!, $first: Int, $after: String, $before: String) @dedupe { + query Users($last: Int!, $first: Int, $after: String, $before: String) @dedupe(match: Variables) { usersByCursor(last: $last, first: $first, after: $after, before: $before) @paginate { edges { node { @@ -1460,7 +1509,7 @@ test("offset paginated query doesn't overlap variables", async function () { // load the contents of the file expect(docs[0]?.document).toMatchInlineSnapshot(` - query Users($limit: Int! = 10, $offset: Int) @dedupe { + query Users($limit: Int! = 10, $offset: Int) @dedupe(match: Variables) { usersByOffset(limit: $limit, offset: $offset) @paginate { id } diff --git a/packages/houdini/src/codegen/transforms/paginate.ts b/packages/houdini/src/codegen/transforms/paginate.ts index c73f6018e..f4e4ba91f 100644 --- a/packages/houdini/src/codegen/transforms/paginate.ts +++ b/packages/houdini/src/codegen/transforms/paginate.ts @@ -2,7 +2,7 @@ import * as graphql from 'graphql' import type { Config, Document } from '../../lib' import { HoudiniError, parentTypeFromAncestors, unwrapType, wrapType } from '../../lib' -import { ArtifactKind, type PaginateModes } from '../../runtime/lib/types' +import { ArtifactKind, DedupeMatchMode, type PaginateModes } from '../../runtime/lib/types' import { fragmentArguments as collectFragmentArguments, type FragmentArgument, @@ -225,16 +225,31 @@ export default async function paginate(config: Config, documents: Document[]): P return { ...node, variableDefinitions: finalVariables, - directives: [ - ...(node.directives || []), - { - kind: graphql.Kind.DIRECTIVE, - name: { - kind: graphql.Kind.NAME, - value: config.dedupeDirective, - }, - }, - ], + directives: config.configFile.supressPaginationDeduplication + ? node.directives + : [ + ...(node.directives || []), + { + kind: graphql.Kind.DIRECTIVE, + name: { + kind: graphql.Kind.NAME, + value: config.dedupeDirective, + }, + arguments: [ + { + kind: 'Argument', + name: { + kind: 'Name', + value: 'match', + }, + value: { + kind: 'EnumValue', + value: DedupeMatchMode.Variables, + }, + }, + ], + }, + ], } as graphql.OperationDefinitionNode }, // if we are dealing with a fragment definition we'll need to add the arguments directive if it doesn't exist diff --git a/packages/houdini/src/codegen/transforms/schema.ts b/packages/houdini/src/codegen/transforms/schema.ts index 6ee2b32f3..9e235099c 100644 --- a/packages/houdini/src/codegen/transforms/schema.ts +++ b/packages/houdini/src/codegen/transforms/schema.ts @@ -3,7 +3,7 @@ import * as graphql from 'graphql' import type { Config, Document } from '../../lib' import { siteURL } from '../../lib' -import { CachePolicy, PaginateMode } from '../../runtime/lib/types' +import { CachePolicy, DedupeMatchMode, PaginateMode } from '../../runtime/lib/types' import { fragmentArguments } from './fragmentVariables' // graphqlExtensions adds a few different things to the graphql schema @@ -52,13 +52,24 @@ directive @${config.paginateDirective}(${config.listOrPaginateNameArg}: String, """ directive @${config.listPrependDirective} on FRAGMENT_SPREAD +enum DedupeMatchMode { + ${DedupeMatchMode.Variables} + ${DedupeMatchMode.Operation} + ${DedupeMatchMode.None} +} + """ @${ config.dedupeDirective } is used to prevent an operation from running more than once at the same time. If the cancelFirst arg is set to true, the response already in flight will be canceled instead of the second one. + If match is set to Operation, then a request will be deduplicated any time there is a request with the same operation. + If it's set to Variables then the request will only be deduplicated if the variables match. If match is set to None, + then the request will never be deduplicated. """ -directive @${config.dedupeDirective}(cancelFirst: Boolean) on QUERY | MUTATION +directive @${ + config.dedupeDirective + }(cancelFirst: Boolean, match: DedupeMatchMode) on QUERY | MUTATION """ @${config.optimisticKeyDirective} is used to identify a field as an optimistic key diff --git a/packages/houdini/src/runtime/client/documentStore.ts b/packages/houdini/src/runtime/client/documentStore.ts index 4414ec9de..ad8745dd7 100644 --- a/packages/houdini/src/runtime/client/documentStore.ts +++ b/packages/houdini/src/runtime/client/documentStore.ts @@ -15,7 +15,7 @@ import type { CachePolicies, GraphQLVariables, } from '../lib/types' -import { ArtifactKind } from '../lib/types' +import { ArtifactKind, DedupeMatchMode } from '../lib/types' import { cachePolicy } from './plugins' // the list of states to step in what direction @@ -24,7 +24,10 @@ const steps = { backwards: ['end', 'afterNetwork'], } as const -let inflightRequests: Record = {} +let inflightRequests: Record< + string, + { variables: Record | null | undefined; controller: AbortController } +> = {} export class DocumentStore< _Data extends GraphQLObject, @@ -50,7 +53,11 @@ export class DocumentStore< serverSideFallback?: boolean controllerKey(variables: any) { - return this.artifact.name + const usedVariables = + 'dedupe' in this.artifact && this.artifact.dedupe?.match !== DedupeMatchMode.Variables + ? {} + : variables + return `${this.artifact.name}@${stableStringify(usedVariables)}` } constructor({ @@ -141,15 +148,22 @@ export class DocumentStore< }: SendParams = {}) { // if the document we are sending is meant to be deduped, then we need to look for an existing // controller for the document - if ('dedupe' in this.artifact) { + if ( + 'dedupe' in this.artifact && + this.artifact.dedupe && + this.artifact.dedupe.match !== 'None' + ) { + // if we are matching on variables then we should use that for the controller key, otherwise + // just use an empty object + const dedupeKey = this.controllerKey(variables) + // if there is already a pending request - if (inflightRequests[this.controllerKey(variables)]) { - // we have to abort _something_ - if (this.artifact.dedupe === 'first') { + if (inflightRequests[dedupeKey]) { + if (this.artifact.dedupe.cancel === 'first') { // cancel the existing one - inflightRequests[this.controllerKey(variables)].abort() + inflightRequests[dedupeKey].controller.abort() // and register the new one - inflightRequests[this.controllerKey(variables)] = abortController + inflightRequests[dedupeKey].controller = abortController } // otherwise we have to abort this one else { @@ -158,7 +172,10 @@ export class DocumentStore< } // register this abort controller as being in flight else { - inflightRequests[this.controllerKey(variables)] = abortController + inflightRequests[dedupeKey] = { + variables, + controller: abortController, + } } } @@ -744,3 +761,34 @@ export type SendParams = { silenceEcho?: boolean abortController?: AbortController } + +// stableStringify is a deterministic version of JSON.stringify +type JsonValue = string | number | boolean | null | JsonArray | JsonObject +type JsonArray = JsonValue[] +type JsonObject = { [key: string]: JsonValue } + +// stable stringify is a deterministic version of JSON.stringify that sorts object keys +function stableStringify(obj: JsonValue): string { + return JSON.stringify(sortObject(obj)) +} + +// sortObject sorts the keys of an object recursively +function sortObject(obj: JsonValue): JsonValue { + // Handle non-object cases + if (obj === null || typeof obj !== 'object') { + return obj + } + + // Handle arrays + if (Array.isArray(obj)) { + return obj.map(sortObject) + } + + // Handle objects + return Object.keys(obj) + .sort() + .reduce((result, key) => { + result[key] = sortObject(obj[key]) + return result + }, {}) +} diff --git a/packages/houdini/src/runtime/lib/config.ts b/packages/houdini/src/runtime/lib/config.ts index ac153cec5..122c1fbaf 100644 --- a/packages/houdini/src/runtime/lib/config.ts +++ b/packages/houdini/src/runtime/lib/config.ts @@ -162,6 +162,11 @@ export type ConfigFile = { */ defaultPaginateMode?: PaginateModes + /** + * Prevents the runtime from deduplicating pagination requests + */ + supressPaginationDeduplication?: boolean + /** * A list of fields to use when computing a record’s id. The default value is ['id']. For more information: https://www.houdinigraphql.com/guides/caching-data#custom-ids */ diff --git a/packages/houdini/src/runtime/lib/types.ts b/packages/houdini/src/runtime/lib/types.ts index d4ced0ccf..abdde2f6e 100644 --- a/packages/houdini/src/runtime/lib/types.ts +++ b/packages/houdini/src/runtime/lib/types.ts @@ -10,6 +10,14 @@ export const CachePolicy = { export type CachePolicies = ValuesOf +export const DedupeMatchMode = { + Variables: 'Variables', + Operation: 'Operation', + None: 'None', +} as const + +export type DedupeMatchModes = ValuesOf + export const PaginateMode = { Infinite: 'Infinite', SinglePage: 'SinglePage', @@ -77,12 +85,18 @@ export type QueryArtifact = BaseCompiledDocument<'HoudiniQuery'> & { policy?: CachePolicies partial?: boolean enableLoadingState?: 'global' | 'local' - dedupe?: 'first' | 'last' + dedupe?: { + cancel: 'first' | 'last' + match: DedupeMatchModes + } } export type MutationArtifact = BaseCompiledDocument<'HoudiniMutation'> & { optimisticKeys?: boolean - dedupe?: 'first' | 'last' + dedupe?: { + cancel: 'first' | 'last' + match: DedupeMatchModes + } } export type FragmentArtifact = BaseCompiledDocument<'HoudiniFragment'> & { diff --git a/packages/plugin-svelte-global-stores/package.json b/packages/plugin-svelte-global-stores/package.json index a38d5e3f4..229b322f0 100644 --- a/packages/plugin-svelte-global-stores/package.json +++ b/packages/plugin-svelte-global-stores/package.json @@ -58,4 +58,4 @@ }, "main": "./build/plugin-cjs/index.js", "types": "./build/plugin/index.d.ts" -} \ No newline at end of file +} diff --git a/site/src/routes/api/config/+page.svx b/site/src/routes/api/config/+page.svx index 4231fd675..8c4899c17 100644 --- a/site/src/routes/api/config/+page.svx +++ b/site/src/routes/api/config/+page.svx @@ -51,6 +51,7 @@ By default, your config file can contain the following values: - `defaultPaginateMode` (optional, default: `"Infinite"`): The default mode for pagination. One of `"Infinite"` or `"SinglePage"`. - `defaultListPosition` (optional, default: "first"): One of `"first"` or `"last"` to indicate the default location for list operations. - `plugins` (optional): An object containing the set of plugins you want to add to your houdini application. The keys are plugin names, the values are plugin-specific configuration. The actual plugin API is undocumented and considered unstable while we try out various things internally. For an overview of your framework plugin's specific configuration, see below. +- `supressPaginationDeduplication` (optional, default `false): Prevents the runtime from deduplicating pagination requests ## Svelte Plugin diff --git a/site/src/routes/api/graphql-magic/+page.svx b/site/src/routes/api/graphql-magic/+page.svx index c1a827fb1..7666ff99b 100644 --- a/site/src/routes/api/graphql-magic/+page.svx +++ b/site/src/routes/api/graphql-magic/+page.svx @@ -60,11 +60,14 @@ mutation UncompleteItem($id: ID!) { } ``` -### `@dedupe(cancelFirst: Boolean)` + +### `@dedupe(cancelFirst: Boolean, match: DedupeMatch)` `@dedupe` lets you control wether or not multiple copies of the same operation (query or mutation) are allowed to run at the same time. If you pass `true` for the `cancelFirst` argument then the first copy of the operation will be canceled (including any in-flight requests). -If you pass `false` (or pass nothing at all) then the second request won't trigger if there is already one pending. +If you pass `false` (or pass nothing at all) then the second request won't trigger if there is already one pending. `match` can take 3 different values. +`Operation` will dedupe the request if there are any pending executions of the same operation, `Variables` will only dedupe the request if the variables match, and +`None` will never dedupe. ### `@when_not`