Skip to content

Commit

Permalink
Add @Dedupe directive (#1352)
Browse files Browse the repository at this point in the history
Co-authored-by: Seppe Dekeyser <[email protected]>
  • Loading branch information
AlecAivazis and SeppahBaws authored Sep 13, 2024
1 parent 41307a2 commit 2cf22c7
Show file tree
Hide file tree
Showing 22 changed files with 443 additions and 32 deletions.
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

0 comments on commit 2cf22c7

Please sign in to comment.