Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dedupe directive #1352

Merged
merged 15 commits into from
Sep 13, 2024
7 changes: 7 additions & 0 deletions .changeset/wild-pandas-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'houdini-svelte': patch
'houdini-react': patch
'houdini': patch
---

Add @dedupe directive
8 changes: 7 additions & 1 deletion e2e/_api/graphql.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export const typeDefs = /* GraphQL */ `
before: String
first: Int
last: Int
delay: Int
snapshot: String!
): UserConnection!
usersList(limit: Int = 4, offset: Int, snapshot: String!): [User!]!
Expand Down Expand Up @@ -450,7 +451,12 @@ export const resolvers = {
}
throw new GraphQLError('No authorization found', { code: 403 })
},
usersConnection(_, args) {
usersConnection: async (_, args) => {
// simulate network delay
if (args.delay) {
await sleep(args.delay)
}

return connectionFromArray(getUserSnapshot(args.snapshot), args)
},
user: async (_, args) => {
Expand Down
1 change: 1 addition & 0 deletions e2e/_api/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ type Query {
before: String
first: Int
last: Int
delay: Int
snapshot: String!
): UserConnection!
usersList(limit: Int = 4, offset: Int, snapshot: String!): [User!]!
Expand Down
15 changes: 15 additions & 0 deletions e2e/react/src/routes/pagination/query/dedupe/+page.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
query DedupePaginationFetch {
usersConnection(first: 2, delay: 1000, snapshot: "dedupe-pagination-fetch") @paginate {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
node {
name
}
}
}
}
21 changes: 21 additions & 0 deletions e2e/react/src/routes/pagination/query/dedupe/+page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PageProps } from './$types'

export default function ({ DedupePaginationFetch, DedupePaginationFetch$handle }: PageProps) {
return (
<div>
<div id="result">
{DedupePaginationFetch.usersConnection.edges
.map(({ node }) => node?.name)
.join(', ')}
</div>

<div id="pageInfo">
{JSON.stringify(DedupePaginationFetch.usersConnection.pageInfo)}
</div>

<button id="next" onClick={() => DedupePaginationFetch$handle.loadNext()}>
next
</button>
</div>
)
}
40 changes: 40 additions & 0 deletions e2e/react/src/routes/pagination/query/dedupe/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import test, { expect, type Response } from '@playwright/test'
import { routes } from '~/utils/routes'
import { expect_1_gql, expect_to_be, goto } from '~/utils/testsHelper'

test('pagination before previous request was finished', async ({ page }) => {
await goto(page, routes.pagination_dedupe)

await expect_to_be(page, 'Bruce Willis, Samuel Jackson')

// Adapted from `expect_n_gql` in lib/utils/testsHelper.ts
let nbResponses = 0
async function fnRes(response: Response) {
if (response.url().endsWith(routes.api)) {
nbResponses++
}
}

page.on('response', fnRes)

// Click the "next page" button twice
await page.click('button[id=next]')
await page.click('button[id=next]')

// Give the query some time to execute
await page.waitForTimeout(1000)

// Check that only one gql request happened.
expect(nbResponses).toBe(1)

page.removeListener('response', fnRes)

await expect_to_be(page, 'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks')

// Fetching the 3rd page still works ok.
await expect_1_gql(page, 'button[id=next]')
await expect_to_be(
page,
'Bruce Willis, Samuel Jackson, Morgan Freeman, Tom Hanks, Will Smith, Harrison Ford'
)
})
2 changes: 2 additions & 0 deletions e2e/react/src/utils/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const routes = {
api: '/_api',
hello: '/hello-world',
scalars: '/scalars',
componentFields_simple: '/component_fields/simple',
Expand All @@ -10,6 +11,7 @@ export const routes = {
pagination_query_forwards: '/pagination/query/connection-forwards',
pagination_query_bidirectional: '/pagination/query/connection-bidirectional',
pagination_query_offset: '/pagination/query/offset',
pagination_dedupe: '/pagination/query/dedupe',
pagination_query_offset_singlepage: '/pagination/query/offset-singlepage',
pagination_query_offset_variable: '/pagination/query/offset-variable/2',
optimistic_keys: '/optimistic-keys',
Expand Down
16 changes: 14 additions & 2 deletions packages/houdini-react/src/runtime/hooks/useDocumentHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,21 @@ export function useDocumentHandle<
) => {
return async (value: any) => {
setLoading(true)
const result = await fn(value)
let result: _Result | null = null
let err: Error | null = null
try {
result = await fn(value)
} catch (e) {
err = e as Error
}
setLoading(false)
return result
// ignore abort errors when loading pages
if (err && err.name !== 'AbortError') {
throw err
}

// we're done
return result || observer.state
}
}

Expand Down
22 changes: 20 additions & 2 deletions packages/houdini-svelte/src/runtime/stores/pagination/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,30 @@ export class QueryStoreCursor<
args?: Parameters<Required<CursorHandlers<_Data, _Input>>['loadPreviousPage']>[0]
) {
const handlers = await this.#handlers()
return await handlers.loadPreviousPage(args)
try {
return await handlers.loadPreviousPage(args)
} catch (e) {
const err = e as Error
// if the error is an abort error then we don't want to throw
if (err.name === 'AbortError') {
} else {
throw err
}
}
}

async loadNextPage(args?: Parameters<CursorHandlers<_Data, _Input>['loadNextPage']>[0]) {
const handlers = await this.#handlers()
return await handlers.loadNextPage(args)
try {
return await handlers.loadNextPage(args)
} catch (e) {
const err = e as Error
// if the error is an abort error then we don't want to throw
if (err.name === 'AbortError') {
} else {
throw err
}
}
}

subscribe(
Expand Down
26 changes: 26 additions & 0 deletions packages/houdini/src/codegen/generators/artifacts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ export default function artifactGenerator(stats: {
let selectionSet: graphql.SelectionSetNode
let originalSelectionSet: graphql.SelectionSetNode | null = null

// extract the deduplication behavior
let dedupe: QueryArtifact['dedupe']

const fragmentDefinitions = doc.document.definitions
.filter<graphql.FragmentDefinitionNode>(
(definition): definition is graphql.FragmentDefinitionNode =>
Expand Down Expand Up @@ -222,6 +225,22 @@ export default function artifactGenerator(stats: {
})
}

const dedupeDirective = operation.directives?.find(
(directive) => directive.name.value === config.dedupeDirective
)
if (dedupeDirective) {
const cancelFirstArg = dedupeDirective.arguments?.find(
(arg) => arg.name.value === 'cancelFirst'
)

dedupe =
cancelFirstArg &&
cancelFirstArg.value.kind === 'BooleanValue' &&
cancelFirstArg.value
? 'first'
: 'last'
}

// use this selection set
selectionSet = operation.selectionSet
if (originalParsed.definitions[0].kind === 'OperationDefinition') {
Expand Down Expand Up @@ -320,6 +339,13 @@ export default function artifactGenerator(stats: {
const hash_value = hashPluginBaseRaw({ config, document: { ...doc, artifact } })
artifact.hash = hash_value

if (
artifact.kind === 'HoudiniQuery' ||
(artifact.kind === 'HoudiniMutation' && dedupe)
) {
artifact.dedupe = dedupe
}

// apply the visibility mask to the artifact so that only
// fields in the direct selection are visible
applyMask(
Expand Down
Loading
Loading