Repository: urql-graphql/urql Branch: main Commit: 71f049c6abbb Files: 671 Total size: 2.5 MB Directory structure: gitextract_87_w7_87/ ├── .changeset/ │ ├── README.md │ ├── config.json │ ├── late-boats-listen.md │ └── shiny-pets-give.md ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── RFC.md │ │ ├── bug_report.yaml │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── actions/ │ │ ├── discord-message/ │ │ │ ├── action.mjs │ │ │ └── action.yml │ │ └── pnpm-run/ │ │ ├── action.mjs │ │ └── action.yml │ └── workflows/ │ ├── ci.yml │ ├── mirror.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs/ │ ├── README.md │ ├── advanced/ │ │ ├── README.md │ │ ├── authentication.md │ │ ├── authoring-exchanges.md │ │ ├── auto-populate-mutations.md │ │ ├── debugging.md │ │ ├── persistence-and-uploads.md │ │ ├── retry-operations.md │ │ ├── server-side-rendering.md │ │ ├── subscriptions.md │ │ └── testing.md │ ├── api/ │ │ ├── README.md │ │ ├── auth-exchange.md │ │ ├── core.md │ │ ├── execute-exchange.md │ │ ├── graphcache.md │ │ ├── preact.md │ │ ├── refocus-exchange.md │ │ ├── request-policy-exchange.md │ │ ├── retry-exchange.md │ │ ├── svelte.md │ │ ├── urql.md │ │ └── vue.md │ ├── architecture.md │ ├── basics/ │ │ ├── README.md │ │ ├── core.md │ │ ├── document-caching.md │ │ ├── errors.md │ │ ├── react-preact.md │ │ ├── solid-start.md │ │ ├── solid.md │ │ ├── svelte.md │ │ ├── typescript-integration.md │ │ ├── ui-patterns.md │ │ └── vue.md │ ├── comparison.md │ ├── graphcache/ │ │ ├── README.md │ │ ├── cache-updates.md │ │ ├── errors.md │ │ ├── local-directives.md │ │ ├── local-resolvers.md │ │ ├── normalized-caching.md │ │ ├── offline.md │ │ └── schema-awareness.md │ └── showcase.md ├── examples/ │ ├── README.md │ ├── pnpm-workspace.yaml │ ├── with-apq/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── LocationsList.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-defer-stream-directives/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── server/ │ │ │ ├── apollo-server.js │ │ │ ├── graphql-yoga.js │ │ │ └── schema.js │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── Songs.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-graphcache-pagination/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── PaginatedNpmSearch.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-graphcache-updates/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── client.js │ │ │ ├── index.jsx │ │ │ └── pages/ │ │ │ ├── Links.jsx │ │ │ └── LoginForm.jsx │ │ └── vite.config.js │ ├── with-infinite-pagination/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── SearchResults.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-multipart/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── FileUpload.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-next/ │ │ ├── README.md │ │ ├── app/ │ │ │ ├── layout.tsx │ │ │ ├── non-rsc/ │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── next-env.d.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── with-pagination/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── PaginatedNpmSearch.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-react/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── PokemonList.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-react-native/ │ │ ├── App.js │ │ ├── README.md │ │ ├── app.json │ │ ├── index.js │ │ ├── package.json │ │ └── src/ │ │ └── screens/ │ │ └── PokemonList.js │ ├── with-refresh-auth/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── authStore.js │ │ │ ├── client.js │ │ │ ├── index.jsx │ │ │ └── pages/ │ │ │ ├── LoginForm.jsx │ │ │ └── Profile.jsx │ │ └── vite.config.js │ ├── with-retry/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── Color.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-solid/ │ │ ├── .eslintrc.js │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── PokemonList.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-solid-start/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app.config.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app.tsx │ │ │ ├── entry-client.tsx │ │ │ ├── entry-server.tsx │ │ │ └── routes/ │ │ │ └── index.tsx │ │ └── tsconfig.json │ ├── with-subscriptions-via-fetch/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── server/ │ │ │ ├── graphql-yoga.js │ │ │ └── schema.js │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── Songs.jsx │ │ │ └── index.jsx │ │ └── vite.config.js │ ├── with-svelte/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.svelte │ │ │ ├── PokemonList.svelte │ │ │ └── main.js │ │ └── vite.config.mjs │ └── with-vue3/ │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── PokemonList.vue │ │ └── main.js │ └── vite.config.js ├── exchanges/ │ ├── auth/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── authExchange.test.ts │ │ │ ├── authExchange.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── context/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── context.test.ts │ │ │ ├── context.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── execute/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── execute.test.ts │ │ │ ├── execute.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── graphcache/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── benchmarks/ │ │ │ ├── 10000Reads.html │ │ │ ├── 10000ReadsComplex.html │ │ │ ├── 10000Writes.html │ │ │ ├── 10000WritesComplex.html │ │ │ ├── 1000Reads.html │ │ │ ├── 1000ReadsComplex.html │ │ │ ├── 1000Writes.html │ │ │ ├── 1000WritesComplex.html │ │ │ ├── 100Reads.html │ │ │ ├── 100ReadsComplex.html │ │ │ ├── 100Writes.html │ │ │ ├── 100WritesComplex.html │ │ │ ├── 50000Reads.html │ │ │ ├── 50000Writes.html │ │ │ ├── 5000Reads.html │ │ │ ├── 5000Writes.html │ │ │ ├── 500Reads.html │ │ │ ├── 500Writes.html │ │ │ ├── addTodo.html │ │ │ ├── benchmarks.js │ │ │ ├── entities.js │ │ │ ├── makeEntries.js │ │ │ ├── operations.js │ │ │ ├── package.json │ │ │ ├── readMe.md │ │ │ ├── updateTodo.html │ │ │ └── urqlClient.js │ │ ├── cypress/ │ │ │ ├── fixtures/ │ │ │ │ └── example.json │ │ │ ├── plugins/ │ │ │ │ └── index.js │ │ │ └── support/ │ │ │ ├── component-index.html │ │ │ └── component.js │ │ ├── cypress.config.js │ │ ├── e2e-tests/ │ │ │ ├── query.spec.tsx │ │ │ └── updates.spec.tsx │ │ ├── help.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── ast/ │ │ │ │ ├── graphql.ts │ │ │ │ ├── index.ts │ │ │ │ ├── node.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── schemaPredicates.test.ts │ │ │ │ ├── schemaPredicates.ts │ │ │ │ ├── traversal.test.ts │ │ │ │ ├── traversal.ts │ │ │ │ ├── variables.test.ts │ │ │ │ └── variables.ts │ │ │ ├── cacheExchange-types.test.ts │ │ │ ├── cacheExchange.test.ts │ │ │ ├── cacheExchange.ts │ │ │ ├── default-storage/ │ │ │ │ └── index.ts │ │ │ ├── extras/ │ │ │ │ ├── index.ts │ │ │ │ ├── relayPagination.test.ts │ │ │ │ ├── relayPagination.ts │ │ │ │ ├── simplePagination.test.ts │ │ │ │ └── simplePagination.ts │ │ │ ├── helpers/ │ │ │ │ ├── help.ts │ │ │ │ └── operation.ts │ │ │ ├── index.ts │ │ │ ├── offlineExchange.test.ts │ │ │ ├── offlineExchange.ts │ │ │ ├── operations/ │ │ │ │ ├── invalidate.ts │ │ │ │ ├── query.test.ts │ │ │ │ ├── query.ts │ │ │ │ ├── shared.test.ts │ │ │ │ ├── shared.ts │ │ │ │ ├── write.test.ts │ │ │ │ └── write.ts │ │ │ ├── store/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── store.test.ts.snap │ │ │ │ ├── data.test.ts │ │ │ │ ├── data.ts │ │ │ │ ├── keys.ts │ │ │ │ ├── store.test.ts │ │ │ │ └── store.ts │ │ │ ├── test-utils/ │ │ │ │ ├── altered_root_schema.json │ │ │ │ ├── examples-1.test.ts │ │ │ │ ├── examples-2.test.ts │ │ │ │ ├── examples-3.test.ts │ │ │ │ ├── relayPagination_schema.json │ │ │ │ ├── simple_schema.json │ │ │ │ ├── suite.test.ts │ │ │ │ └── utils.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── persisted/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── persistedExchange.test.ts │ │ │ ├── persistedExchange.ts │ │ │ ├── sha256.ts │ │ │ └── test-utils.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── populate/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── helpers/ │ │ │ │ ├── help.ts │ │ │ │ ├── node.ts │ │ │ │ └── traverse.ts │ │ │ ├── index.ts │ │ │ ├── populateExchange.test.ts │ │ │ └── populateExchange.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── refocus/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── refocusExchange.test.ts │ │ │ └── refocusExchange.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── request-policy/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── requestPolicyExchange.test.ts │ │ │ └── requestPolicyExchange.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── retry/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── retryExchange.test.ts │ │ │ └── retryExchange.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ └── throw-on-error/ │ ├── CHANGELOG.md │ ├── README.md │ ├── jsr.json │ ├── package.json │ ├── src/ │ │ ├── index.ts │ │ ├── throwOnErrorExchange.test.ts │ │ └── throwOnErrorExchange.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── package.json ├── packages/ │ ├── core/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __snapshots__/ │ │ │ │ └── client.test.ts.snap │ │ │ ├── client.test.ts │ │ │ ├── client.ts │ │ │ ├── exchanges/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ ├── fetch.test.ts.snap │ │ │ │ │ └── subscription.test.ts.snap │ │ │ │ ├── cache.test.ts │ │ │ │ ├── cache.ts │ │ │ │ ├── compose.test.ts │ │ │ │ ├── compose.ts │ │ │ │ ├── debug.test.ts │ │ │ │ ├── debug.ts │ │ │ │ ├── fallback.test.ts │ │ │ │ ├── fallback.ts │ │ │ │ ├── fetch.test.ts │ │ │ │ ├── fetch.ts │ │ │ │ ├── index.ts │ │ │ │ ├── map.test.ts │ │ │ │ ├── map.ts │ │ │ │ ├── ssr.test.ts │ │ │ │ ├── ssr.ts │ │ │ │ ├── subscription.test.ts │ │ │ │ └── subscription.ts │ │ │ ├── gql.test.ts │ │ │ ├── gql.ts │ │ │ ├── index.ts │ │ │ ├── internal/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ └── fetchSource.test.ts.snap │ │ │ │ ├── fetchOptions.test.ts │ │ │ │ ├── fetchOptions.ts │ │ │ │ ├── fetchSource.test.ts │ │ │ │ ├── fetchSource.ts │ │ │ │ └── index.ts │ │ │ ├── test-utils/ │ │ │ │ ├── index.ts │ │ │ │ └── samples.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── __snapshots__/ │ │ │ │ └── error.test.ts.snap │ │ │ ├── collectTypenames.test.ts │ │ │ ├── collectTypenames.ts │ │ │ ├── error.test.ts │ │ │ ├── error.ts │ │ │ ├── formatDocument.test.ts │ │ │ ├── formatDocument.ts │ │ │ ├── graphql.ts │ │ │ ├── hash.test.ts │ │ │ ├── hash.ts │ │ │ ├── index.ts │ │ │ ├── operation.ts │ │ │ ├── request.test.ts │ │ │ ├── request.ts │ │ │ ├── result.test.ts │ │ │ ├── result.ts │ │ │ ├── streamUtils.ts │ │ │ ├── variables.test.ts │ │ │ └── variables.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── introspection/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── getIntrospectedSchema.ts │ │ │ ├── index.ts │ │ │ └── minifyIntrospectionQuery.ts │ │ └── tsconfig.json │ ├── next-urql/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── DataHydrationContext.ts │ │ │ ├── Provider.ts │ │ │ ├── htmlescape.ts │ │ │ ├── index.ts │ │ │ ├── rsc.ts │ │ │ ├── useQuery.ts │ │ │ └── useUrqlValue.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── preact-urql/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── Mutation.test.tsx │ │ │ │ ├── Mutation.ts │ │ │ │ ├── Query.test.tsx │ │ │ │ ├── Query.ts │ │ │ │ ├── Subscription.test.tsx │ │ │ │ ├── Subscription.ts │ │ │ │ └── index.ts │ │ │ ├── context.ts │ │ │ ├── hooks/ │ │ │ │ ├── constants.ts │ │ │ │ ├── index.ts │ │ │ │ ├── useMutation.test.tsx │ │ │ │ ├── useMutation.ts │ │ │ │ ├── useQuery.test.tsx │ │ │ │ ├── useQuery.ts │ │ │ │ ├── useRequest.ts │ │ │ │ ├── useSource.ts │ │ │ │ ├── useSubscription.test.tsx │ │ │ │ └── useSubscription.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── react-urql/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── core/ │ │ │ ├── index.d.ts │ │ │ ├── index.esm.js │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── cypress/ │ │ │ ├── fixtures/ │ │ │ │ └── example.json │ │ │ └── support/ │ │ │ ├── component-index.html │ │ │ └── component.js │ │ ├── cypress.config.js │ │ ├── e2e-tests/ │ │ │ └── useQuery.spec.tsx │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── Mutation.test.tsx │ │ │ │ ├── Mutation.ts │ │ │ │ ├── Query.test.tsx │ │ │ │ ├── Query.ts │ │ │ │ ├── Subscription.ts │ │ │ │ └── index.ts │ │ │ ├── context.ts │ │ │ ├── hooks/ │ │ │ │ ├── __snapshots__/ │ │ │ │ │ ├── useMutation.test.tsx.snap │ │ │ │ │ ├── useQuery.test.tsx.snap │ │ │ │ │ └── useSubscription.test.tsx.snap │ │ │ │ ├── cache.ts │ │ │ │ ├── index.ts │ │ │ │ ├── state.ts │ │ │ │ ├── useMutation.test.tsx │ │ │ │ ├── useMutation.ts │ │ │ │ ├── useQuery.spec.ts │ │ │ │ ├── useQuery.test.tsx │ │ │ │ ├── useQuery.ts │ │ │ │ ├── useRequest.test.ts │ │ │ │ ├── useRequest.ts │ │ │ │ ├── useSubscription.test.tsx │ │ │ │ └── useSubscription.ts │ │ │ ├── index.ts │ │ │ └── test-utils/ │ │ │ └── ssr.test.tsx │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── site/ │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── plugins/ │ │ │ ├── assets-fix/ │ │ │ │ └── node.api.js │ │ │ ├── monorepo-fix/ │ │ │ │ └── node.api.js │ │ │ ├── preact/ │ │ │ │ └── node.api.js │ │ │ └── react-router/ │ │ │ └── browser.api.js │ │ ├── public/ │ │ │ ├── browserconfig.xml │ │ │ └── site.webmanifest │ │ ├── src/ │ │ │ ├── analytics.js │ │ │ ├── app.js │ │ │ ├── assets/ │ │ │ │ ├── anchor.js │ │ │ │ └── chevron.js │ │ │ ├── components/ │ │ │ │ ├── body-copy.js │ │ │ │ ├── button.js │ │ │ │ ├── footer.js │ │ │ │ ├── header.js │ │ │ │ ├── link.js │ │ │ │ ├── loading.js │ │ │ │ ├── markdown.js │ │ │ │ ├── mdx.js │ │ │ │ ├── navigation.js │ │ │ │ ├── panel.js │ │ │ │ ├── scroll-to-top.js │ │ │ │ ├── secondary-title.js │ │ │ │ ├── section-title.js │ │ │ │ ├── sidebar-search-input.js │ │ │ │ ├── sidebar.js │ │ │ │ └── wrapper.js │ │ │ ├── constants.js │ │ │ ├── google-analytics.js │ │ │ ├── google-tag-manager.js │ │ │ ├── html.js │ │ │ ├── index.js │ │ │ ├── screens/ │ │ │ │ ├── 404/ │ │ │ │ │ ├── 404.js │ │ │ │ │ └── index.js │ │ │ │ ├── docs/ │ │ │ │ │ ├── article.js │ │ │ │ │ ├── header.js │ │ │ │ │ └── index.js │ │ │ │ └── home/ │ │ │ │ ├── _content.js │ │ │ │ ├── features.js │ │ │ │ ├── get-started.js │ │ │ │ ├── hero.js │ │ │ │ ├── index.js │ │ │ │ └── more-oss.js │ │ │ └── styles/ │ │ │ ├── global.js │ │ │ └── theme.js │ │ ├── static.config.js │ │ └── vercel.json │ ├── solid-start-urql/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── context.test.tsx │ │ │ ├── context.ts │ │ │ ├── createMutation.test.ts │ │ │ ├── createMutation.ts │ │ │ ├── createQuery.test.tsx │ │ │ ├── createQuery.ts │ │ │ ├── createSubscription.test.ts │ │ │ ├── createSubscription.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── solid-urql/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── context.ts │ │ │ ├── createMutation.test.ts │ │ │ ├── createMutation.ts │ │ │ ├── createQuery.test.tsx │ │ │ ├── createQuery.ts │ │ │ ├── createSubscription.test.ts │ │ │ ├── createSubscription.ts │ │ │ ├── index.ts │ │ │ ├── suspense.test.tsx │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── storage-rn/ │ │ ├── CHANGELOG.md │ │ ├── LICENCE │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── makeAsyncStorage.test.ts │ │ │ └── makeAsyncStorage.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ ├── svelte-urql/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── jsr.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── common.ts │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── mutationStore.test.ts │ │ │ ├── mutationStore.ts │ │ │ ├── queryStore.test.ts │ │ │ ├── queryStore.ts │ │ │ ├── subscriptionStore.test.ts │ │ │ └── subscriptionStore.ts │ │ ├── tsconfig.json │ │ └── vitest.config.ts │ └── vue-urql/ │ ├── CHANGELOG.md │ ├── README.md │ ├── jsr.json │ ├── package.json │ ├── src/ │ │ ├── index.ts │ │ ├── useClient.test.ts │ │ ├── useClient.ts │ │ ├── useClientHandle.ts │ │ ├── useMutation.test.ts │ │ ├── useMutation.ts │ │ ├── useQuery.test.ts │ │ ├── useQuery.ts │ │ ├── useSubscription.test.ts │ │ ├── useSubscription.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── pnpm-workspace.yaml ├── scripts/ │ ├── actions/ │ │ ├── build-all.mjs │ │ ├── lib/ │ │ │ ├── commands.mjs │ │ │ ├── constants.mjs │ │ │ ├── github.mjs │ │ │ └── packages.mjs │ │ └── pack-all.mjs │ ├── babel/ │ │ ├── transform-debug-target.mjs │ │ ├── transform-invariant-warning.mjs │ │ └── transform-pipe.mjs │ ├── changesets/ │ │ ├── changelog.js │ │ ├── jsr.mjs │ │ └── version.mjs │ ├── eslint/ │ │ └── preset.js │ ├── prepare/ │ │ ├── index.js │ │ └── postinstall.js │ ├── rollup/ │ │ ├── cleanup-plugin.mjs │ │ ├── config.mjs │ │ ├── plugins.mjs │ │ └── settings.mjs │ └── vitest/ │ └── setup.js ├── tsconfig.json ├── vercel.json └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/README.md ================================================ # Changesets Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@0.3.0/schema.json", "changelog": "../scripts/changesets/changelog.js", "commit": false, "access": "public", "baseBranch": "main", "updateInternalDependencies": "minor", "snapshot": { "prereleaseTemplate": "{tag}-{commit}", "useCalculatedVersion": true }, "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true, "updateInternalDependents": "out-of-range" } } ================================================ FILE: .changeset/late-boats-listen.md ================================================ --- '@urql/solid-start': minor --- Fix SSR runtime failures caused by importing SolidStart's `action` API at module load time by reading `action` from `Provider` context instead. ================================================ FILE: .changeset/shiny-pets-give.md ================================================ --- '@urql/solid-start': patch --- Fix `createSubscription` to use `@urql/solid-start` context instead of re-exporting the Solid-only implementation from `@urql/solid`. ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] charset = utf-8 indent_size = 2 end_of_line = lf indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.md] max_line_length = 100 trim_trailing_whitespace = false [COMMIT_EDITMSG] max_line_length = 0 ================================================ FILE: .gitattributes ================================================ * text=auto ================================================ FILE: .github/CODEOWNERS ================================================ /.github/ @urql-graphql/core /.changeset/config.json @urql-graphql/core /scripts/actions/* @urql-graphql/core /scripts/prepare/* @urql-graphql/core /scripts/rollup/* @urql-graphql/core /scripts/changesets/* @urql-graphql/core ================================================ FILE: .github/ISSUE_TEMPLATE/RFC.md ================================================ --- name: 'RFC' about: Propose an enhancement / feature and start a discussion title: 'RFC: Your Proposal' labels: "future \U0001F52E" --- ## Summary ## Proposed Solution ## Requirements ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: "\U0001F41E Bug report" description: Report an issue with urql labels: [] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: textarea id: bug-description attributes: label: Describe the bug description: Please describe your bug clearly and concisely. placeholder: Bug description validations: required: true - type: input id: reproduction attributes: label: Reproduction description: Please provide a link to a reproduction. Templates can be found in the [examples folder](https://github.com/urql-graphql/urql/tree/main/examples). A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required. If a report is vague (e.g. just a generic error message) and if no reproduction is provided the issue will be auto-closed. placeholder: Reproduction validations: required: true - type: textarea id: urql-version attributes: label: Urql version description: The versions of the relevant urql packages you are using placeholder: urql v2.0.0 validations: required: true - type: checkboxes id: checkboxes attributes: label: Validations description: Before submitting the issue, please make sure you do the following options: - label: I can confirm that this is a bug report, and not a feature request, RFC, question, or discussion, for which [GitHub Discussions](https://github.com/urql-graphql/urql/discussions) should be used required: true - label: Read the [docs](https://formidable.com/open-source/urql/docs/). required: true - label: Follow our [Code of Conduct](https://github.com/urql-graphql/urql/blob/main/CODE_OF_CONDUCT.md) required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Ask a question url: https://github.com/urql-graphql/urql/discussions about: Ask questions and discuss with other community members - name: Join the Discord url: https://urql.dev/discord about: Chat with maintainers and other community members ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Summary ## Set of changes ================================================ FILE: .github/actions/discord-message/action.mjs ================================================ import * as core from '@actions/core'; import * as github from '@actions/github'; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL; const octokit = github.getOctokit(GITHUB_TOKEN); const formatBody = input => { const titleRe = /(?:^|\n)#+[^\n]+/g; const updatedDepsRe = /\n-\s*Updated dependencies[\s\S]+\n(\n\s+-[\s\S]+)*/gi; const markdownLinkRe = /\[([^\]]+)\]\(([^\)]+)\)/g; const creditRe = new RegExp( `Submitted by (?:undefined|${markdownLinkRe.source})`, 'ig' ); const repeatedNewlineRe = /(?:\n[ ]*)*(\n[ ]*)/g; return input .replace(titleRe, '') .replace(updatedDepsRe, '') .replace(creditRe, (_match, text, url) => { if (!text || /@kitten|@JoviDeCroock/i.test(text)) return ''; return `Submitted by [${text}](${url})`; }) .replace(markdownLinkRe, (_match, text, url) => `[${text}](<${url}>)`) .replace(repeatedNewlineRe, (_match, text) => (text ? ` ${text}` : '\n')) .trim(); }; async function getReleaseBody(name, version) { const tag = `${name}@${version}`; const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); const result = await octokit.rest.repos.getReleaseByTag({ owner, repo, tag }); const release = result.status === 200 ? result.data : undefined; if (!release || !release.body) return; const title = `:package: [${tag}](<${release.html_url}>)`; const body = formatBody(release.body); if (!body) return; return `${title}\n${body}`; } async function main() { const inputPackages = core.getInput('publishedPackages'); let packages; try { packages = JSON.parse(inputPackages); } catch (e) { console.error('invalid JSON in publishedPackages input.'); return; } // Get releases const releasePromises = packages.map(entry => { return getReleaseBody(entry.name, entry.version); }); const content = (await Promise.allSettled(releasePromises)) .map(x => x.status === 'fulfilled' && x.value) .filter(Boolean) .join('\n\n'); // Send message through a discord webhook or bot const response = await fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content }), }); if (!response.ok) { console.error( 'Something went wrong while sending the discord webhook.', response.status ); console.error(await response.text()); } } main().then().catch(console.error); ================================================ FILE: .github/actions/discord-message/action.yml ================================================ name: 'Send a discord message' description: 'Send a discord message as a result of an urql publish.' inputs: publishedPackages: description: > A JSON array to present the published packages. The format is `[{"name": "@xx/xx", "version": "1.2.0"}, {"name": "@xx/xy", "version": "0.8.9"}]` runs: using: 'node20' main: 'action.mjs' ================================================ FILE: .github/actions/pnpm-run/action.mjs ================================================ import { execa } from 'execa'; const run = execa('pnpm', ['run', process.env.INPUT_COMMAND], { cwd: process.cwd(), }); run.stdout.pipe(process.stdout); run.stderr.pipe(process.stderr); run .then(result => process.exit(result.exitCode)) .catch(error => process.exit(error.exitCode || -1)); ================================================ FILE: .github/actions/pnpm-run/action.yml ================================================ name: 'Run a pnpm command' description: 'Locally run a forked pnpm command as an action.' inputs: command: description: 'Command' default: 'help' runs: using: 'node20' main: 'action.mjs' ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: pull_request_review: types: [submitted, edited] branches: changeset-release/main jobs: check: name: Checks runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout Repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 18 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: 9 run_install: false - name: Get pnpm store directory id: pnpm-store run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT - name: Use pnpm store uses: actions/cache@v4 id: pnpm-cache with: path: | ~/.cache/Cypress ${{ steps.pnpm-store.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm- - name: Install Dependencies run: pnpm install --frozen-lockfile --prefer-offline - name: TypeScript run: pnpm run check - name: Linting run: pnpm run lint - name: Unit Tests run: pnpm run test - name: Check for slow types run: pnpm jsr:dryrun react-e2e: name: React E2E runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout Repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 18 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: 9 run_install: false - name: Get pnpm store directory id: pnpm-store run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT - name: Use pnpm store uses: actions/cache@v4 id: pnpm-cache with: path: | ~/.cache/Cypress ${{ steps.pnpm-store.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm- - name: Install Dependencies run: pnpm install --frozen-lockfile --prefer-offline - name: Build run: pnpm -F @urql/core build && pnpm -F urql build - name: e2e tests 🧪 uses: cypress-io/github-action@v6 with: install: false command: pnpm cypress run --component working-directory: packages/react-urql graphcache-e2e: name: Graphcache E2E runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout Repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 18 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: 9 run_install: false - name: Get pnpm store directory id: pnpm-store run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT - name: Use pnpm store uses: actions/cache@v4 id: pnpm-cache with: path: | ~/.cache/Cypress ${{ steps.pnpm-store.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm- - name: Install Dependencies run: pnpm install --frozen-lockfile --prefer-offline - name: Build run: pnpm -F "@urql/core" -F urql -F "@urql/exchange-execute" build - name: e2e tests 🧪 uses: cypress-io/github-action@v6 with: install: false command: pnpm cypress run --component working-directory: exchanges/graphcache build: name: Build runs-on: ubuntu-latest timeout-minutes: 10 strategy: matrix: node: [0, 1, 2] env: NODE_TOTAL: 3 NODE_INDEX: ${{matrix.node}} steps: - name: Checkout Repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 18 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: 9 run_install: false - name: Get pnpm store directory id: pnpm-store run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT - name: Use pnpm store uses: actions/cache@v4 id: pnpm-cache with: path: | ~/.cache/Cypress ${{ steps.pnpm-store.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm- - name: Install Dependencies run: pnpm install --frozen-lockfile --prefer-offline - name: Build run: pnpm build - name: Pack uses: ./.github/actions/pnpm-run with: command: pack ================================================ FILE: .github/workflows/mirror.yml ================================================ # Mirrors to https://tangled.sh/@kitten.sh (knot.kitten.sh) name: Mirror (Git Backup) on: push: branches: - main jobs: mirror: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Mirror env: MIRROR_SSH_KEY: ${{ secrets.MIRROR_SSH_KEY }} GIT_SSH_COMMAND: 'ssh -o StrictHostKeyChecking=yes' run: | mkdir -p ~/.ssh echo "$MIRROR_SSH_KEY" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H knot.kitten.sh >> ~/.ssh/known_hosts git remote add mirror "git@knot.kitten.sh:kitten.sh/${GITHUB_REPOSITORY#*/}" git push --mirror mirror ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - main jobs: release: name: Release runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: contents: write id-token: write issues: write repository-projects: write deployments: write packages: write pull-requests: write steps: - name: Checkout Repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 20 registry-url: "https://registry.npmjs.org" - name: Update npm run: npm install -g npm@latest - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: 9 run_install: false - name: Get pnpm store directory id: pnpm-store run: echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT - name: Use pnpm store uses: actions/cache@v4 id: pnpm-cache with: path: | ~/.cache/Cypress ${{ steps.pnpm-store.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm- - name: Install Dependencies run: pnpm install - name: PR or Publish id: changesets uses: changesets/action@v1.5.3 with: version: pnpm changeset:version publish: pnpm changeset:publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Notify discord id: discord-msg if: steps.changesets.outputs.published == 'true' uses: ./.github/actions/discord-message with: publishedPackages: ${{ steps.changesets.outputs.publishedPackages }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - name: Publish Prerelease if: steps.changesets.outputs.published != 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git reset --hard origin/main pnpm changeset version --no-git-tag --snapshot canary pnpm changeset publish --no-git-tag --snapshot canary --tag canary ================================================ FILE: .gitignore ================================================ /.idea /.vscode **/node_modules *.log .rts2_cache* .husky dist/ build/ coverage/ package-lock.json .DS_Store .next packages/*/LICENSE exchanges/*/LICENSE # TODO: Figure out how to remove these: tmp/ dist/ examples/yarn.lock examples/pnpm-lock.yaml examples/package-lock.json examples/*/public examples/*/yarn.lock examples/*/pnpm-lock.yaml examples/*/package-lock.json examples/*/ios/ examples/*/android/ examples/*/.watchmanconfig examples/*/metro.config.js examples/*/babel.config.js ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team: - phil@0no.co - grant.sander@formidable.com - jovi@preact.dev All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Development Thanks for contributing! We want to ensure that `urql` evolves and fulfills its idea of extensibility and flexibility by seeing continuous improvements and enhancements, no matter how small or big they might be. If you're about to add a new exchange, please consider publishing it as a separate package. ## How to contribute? We follow fairly standard but lenient rules around pull requests and issues. Please pick a title that describes your change briefly, optionally in the imperative mood if possible. If you have an idea for a feature or want to fix a bug, consider opening an issue first. We're also happy to discuss and help you open a PR and get your changes in! - If you have a question, try [creating a GitHub Discussions thread.](https://github.com/urql-graphql/urql/discussions/new) - If you think you've found a bug, [open a new issue.](https://github.com/urql-graphql/urql/issues/new/choose) - or, if you found a bug you'd like to fix, [open a PR.](https://github.com/urql-graphql/urql/compare) - If you'd like to propose a change [open an RFC issue.](https://github.com/urql-graphql/urql/issues/new?labels=future+%F0%9F%94%AE&template=RFC.md&title=RFC%3A+Your+Proposal) You can read more about the RFC process [below](#how-do-i-propose-changes). ### What are the issue conventions? There are **no strict conventions**, but we do have two templates in place that will fit most issues, since questions and other discussion start on GitHub Discussions. The bug template is fairly standard and the rule of thumb is to try to explain **what you expected** and **what you got instead.** Following this makes it very clear whether it's a known behavior, an unexpected issue, or an undocumented quirk. We do ask that issues _aren’t_ created for questions, or where a bug is likely to be either caused by misusage or misconfiguration. In short, if you can’t provide a reproduction of the issue, then it may be the case that you’ve got a question instead. If you need a template for creating a reproduction, all of our examples can be opened in isolated sandboxes or modified as you see fit: https://github.com/urql-graphql/urql/tree/main/examples ### How do I propose changes? We follow an **RFC proposal process**. This allows anyone to propose a new feature or a change, and allows us to communicate our current planned features or changes, so any technical discussion, progress, or upcoming changes are always **documented transparently.** You can [find the RFC template](https://github.com/urql-graphql/urql/issues/new/choose) in our issue creator. All RFCs are added to the [RFC Lifecycle board.](https://github.com/urql-graphql/urql/projects/3) This board tracks where an RFC stands and who's working on it until it's completed. Bugs and PRs may end up on there too if no corresponding RFC exists or was necessary. RFCs are typically first added to "In Discussion" until we believe they're ready to be worked on. This step may either be short, skipped, or rather long, if no plan is in place for a change yet. So if you see a way to help, please leave some suggestions. ### What are the PR conventions? This also comes with **no strict conventions**. We only ask you to follow the PR template we have in place more strictly here than the templates for issues, since it asks you to list a summary (maybe even with a short explanation) and a list of technical changes. If you're **resolving** an issue please don't forget to add `Resolve #123` to the description so that it's automatically linked, so that there's no ambiguity and which issue is being addressed (if any) You'll find that a comment by the "Changeset" bot may pop up. If you don't know what a **changeset** is and why it's asking you to document your changes, read on at ["How do I document a change for the changelog"](#how-do-i-document-a-change-for-the-changelog) We also typically **name** our PRs with a slightly descriptive title, e.g. `(shortcode) - Title`, where shortcode is either the name of a package, e.g. `(core)` and the title is an imperative mood description, e.g. "Update X" or "Refactor Y." ## How do I set up the project? Luckily it's not hard to get started. You can install dependencies [using `pnpm`](https://pnpm.io/installation#using-corepack). Please don't use `npm` or `yarn` to respect the lockfile. ```sh pnpm install ``` There are multiple commands you can run in the root folder to test your changes: ```sh # TypeScript checks: pnpm run check # Linting (prettier & eslint): pnpm run lint # Unit Tests (for all packages): pnpm run test # Builds (for all packages): pnpm run build ``` You can find the main packages in `packages/*` and the addon exchanges in `exchanges/*`. Each package also has its own scripts that are common and shared between all packages. ```sh # Unit Tests for the current package: pnpm run test # Linting (prettier & eslint): pnpm run lint # Build the current package: pnpm run build # TypeScript checks for the current package: pnpm run check ``` While you can run `build` globally in the interest of time it's advisable to only run it on the packages you're working on. Note that TypeScript checks don't require any packages to be built. ## How do I test my changes? It's always good practice to run the tests when making changes. If you're unsure which packages may be affected by your new tests or changes you may run `pnpm test` in the root of the repository. If your editor is not set up with type checks you may also want to run `pnpm run check` on your changes. Additionally you can head to any example in the `examples/` folder and run them. There you'll also need to install their dependencies as they're isolated projects, without a lockfile and without linking to packages in the monorepos. All examples are started using the `package.json`'s `start` script. ## How do I lint my code? We ensure consistency in `urql`'s codebase using `eslint` and `prettier`. They are run on a `precommit` hook, so if something's off they'll try to automatically fix up your code, or display an error. If you have them set up in your editor, even better! ## How do I document a change for the changelog? This project uses [changesets](https://github.com/atlassian/changesets). This means that for every PR there must be documentation for what has been changed and which package is affected. You can document a change by running `changeset`, which will ask you which packages have changed and whether the change is major/minor/patch. It will then ask you to write a change entry as markdown. ```sh # In the root of the urql repository call: pnpm changeset ``` This will create a new "changeset file" in the `.changeset` folder, which you should commit and push, so that it's added to your PR. This will eventually end up in the package's `CHANGELOG.md` file when we do a release. You won't need to add a changeset if you're simply making "non-visible" changes to the docs or other files that aren't published to the npm registry. [Read more about adding a `changeset` here.](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md#i-am-in-a-multi-package-repository-a-mono-repo) ## How do I release new versions of our packages? Hold up, that's **automated**! Since we use `changeset` to document our changes, which determines what goes into the changelog and what kind of version bump a change should make, you can also use the tool to check what's currently posed to change after a release batch using: `pnpm changeset status`. We have a [GitHub Actions workflow](./.github/workflow/release.yml) which is triggered whenever new changes are merged. It will always open a **"Version Packages" PR** which is kept up-to-date. This PR documents all changes that are made and will show in its description what all new changelogs are going to contain for their new entries. Once a "Version Packages" PR is approved by a contributor and merged, the action will automatically take care of creating the release, publishing all updated packages to the npm registry, and creating appropriate tags on GitHub too. This process is automated, but the changelog should be checked for errors. As to **when** to merge the automated PR and publish? Maybe not after every change. Typically there are two release batches: hotfixes and release batches. We expect that a hotfix for a single package should go out as quickly as possible if it negatively affects users. For **release batches** however, it's common to assume that if one change is made to a package that more will follow in the same week. So waiting for **a day or two** when other changes are expected will make sense to keep the fatigue as low as possible for downstream maintainers. ## How do I upgrade all dependencies? It may be a good idea to keep all dependencies on the `urql` repository **up-to-date** every now and then. Typically we do this by running `pnpm update --interactive --latest` and checking one-by-one which dependencies will need to be bumped. In case of any security issues it may make sense to just run `pnpm update [package]`. While this is rare with `pnpm`, upgrading some transitive dependencies may accidentally duplicate them if two packages depend on different compatible version ranges. This can be fixed by running: ```sh npx pnpm-deduplicate pnpm install ``` It's common to then **create a PR** (with a changeset documenting the packages that need to reflect new changes if any `dependencies` have changed) with the name of "(chore) - Upgrade direct and transitive dependencies" or something similar. ## How do I add a new package? First of all we need to know **where** to put the package. - Exchanges should be added to `exchanges/` and the folder should be the plain name of the exchange. Since the `package.json:name` is following the convention of `@urql/exchange-*` the folder should just be without this conventional prefix. - All other packages should be added to `packages/`. Typically all packages should be named `@urql/*` and their folders should be named exactly this without the prefix or `*-urql`. Optionally if the package will be named `*-urql` then the folder can take on the same name. When adding a new package, start by **copying** a `package.json` file from another project. You may want to alter the following fields first: - `name` - `version` (either start at `0.1.0` or `1.0.0`) - `description` - `repository.directory` - `keywords` Make sure to also alter the `devDependencies`, `peerDependencies`, and `dependencies` to match the new package's needs. **The `main` and `module` fields follow a convention:** All output bundles will always be output in the `./dist` folder by `rollup`, which is set up in the `build` script. Their filenames are a "kebab case" (dash-cased) version of the `name` field with an appropriate extension (`.esm.js` for `module` and `.cjs.js` for `main`). If your entrypoint won't be at `src/index.ts` you may alter it. But the `types` field has to match the same file relative to the `dist/types` folder, where `rollup` will output the TypeScript declaration files. When setting up your package make sure to create a `src/index.ts` file (or any other file which you've pointed `package.json:source` to). Also don't forget to copy over the `tsconfig.json` from another package (You won't need to change it). The `scripts.prepare` task is set up to check your new `package.json` file for correctness. So in case you get anything wrong, you'll get a short error when running `pnpm` after setting your new project up. Just in case! 😄 Afterwards you can check whether everything is working correctly by running: ```sh pnpm install pnpm run check ``` At this point, **don't publish** the package or a prerelease yourself if you can avoid it. If you can't or have already, we'll need to get the **rights** fixed by adding the package to the `@urql` scope. Typically what we do is: ```sh npm access grant read-write urql:developers [package] ``` ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018–2020 Formidable, Copyright (c) urql GraphQL Team and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
urql

A highly customisable and versatile GraphQL client

CI Status Weekly downloads Discord

## ✨ Features - 📦 **One package** to get a working GraphQL client in React, Preact, Vue, Solid and Svelte - ⚙️ Fully **customisable** behaviour [via "exchanges"](https://formidable.com/open-source/urql/docs/advanced/authoring-exchanges/) - 🗂 Logical but simple default behaviour and document caching - 🌱 Normalized caching via [`@urql/exchange-graphcache`](https://formidable.com/open-source/urql/docs/graphcache) - 🔬 Easy debugging with the [`urql` devtools browser extensions](https://formidable.com/open-source/urql/docs/advanced/debugging/) `urql` is a GraphQL client that exposes a set of helpers for several frameworks. It's built to be highly customisable and versatile so you can take it from getting started with your first GraphQL project all the way to building complex apps and experimenting with GraphQL clients. **📃 For more information, [check out the docs](https://formidable.com/open-source/urql/docs/).** ## 💙 [Sponsors](https://github.com/sponsors/urql-graphql)
BigCommerce
BigCommerce
WunderGraph
WunderGraph
The Guild
The Guild
BeatGig
BeatGig
## 🙌 Contributing **The urql project was founded by [Formidable](https://formidable.com/) and is actively developed by the urql GraphQL team.** If you'd like to get involved, [check out our Contributor's guide.](https://github.com/urql-graphql/urql/blob/main/CONTRIBUTING.md) ## 📦 [Releases](https://github.com/urql-graphql/urql/releases) All new releases and updates are listed on GitHub with full changelogs. Each package in this repository further contains an independent `CHANGELOG.md` file with the historical changelog, for instance, [here’s `@urql/core`’s changelog](https://github.com/urql-graphql/urql/blob/main/packages/core/CHANGELOG.md). If you’re upgrading to v4, [check out our migration guide, posted as an issue.](https://github.com/urql-graphql/urql/issues/3114) New releases are prepared using [changesets](https://github.com/urql-graphql/urql/blob/main/CONTRIBUTING.md#how-do-i-document-a-change-for-the-changelog), which are changelog entries added to each PR, and we have “Version Packages” PRs that once merged will release new versions of `urql` packages. You can use `@canary` releases from `npm` if you’d like to get a preview of the merged changes. ## 📃 [Documentation](https://urql.dev/goto/docs) The documentation contains everything you need to know about `urql`, and contains several sections in order of importance when you first get started: - **[Basics](https://formidable.com/open-source/urql/docs/basics/)** — contains the ["Getting Started" guide](https://formidable.com/open-source/urql/docs/#where-to-start) and all you need to know when first using `urql`. - **[Architecture](https://formidable.com/open-source/urql/docs/architecture/)** — explains how `urql` functions and is built. - **[Advanced](https://formidable.com/open-source/urql/docs/advanced/)** — covers more uncommon use-cases and things you don't immediately need when getting started. - **[Graphcache](https://formidable.com/open-source/urql/docs/graphcache/)** — documents ["Normalized Caching" support](https://formidable.com/open-source/urql/docs/graphcache/normalized-caching/) which enables more complex apps and use-cases. - **[API](https://formidable.com/open-source/urql/docs/api/)** — the API documentation for each individual package. Furthermore, all APIs and packages are self-documented using TSDocs. If you’re using a language server for TypeScript, the documentation for each API should pop up in your editor when hovering `urql`’s code and APIs. _You can find the raw markdown files inside this repository's `docs` folder._ ================================================ FILE: docs/README.md ================================================ --- title: Overview order: 1 --- # Overview `urql` is a highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow. It's built to be both easy to use for newcomers to GraphQL, and extensible, to grow to support dynamic single-app applications and highly customized GraphQL infrastructure. In short, `urql` prioritizes usability and adaptability. As you're adopting GraphQL, `urql` becomes your primary data layer and can handle content-heavy pages through ["Document Caching"](./basics/document-caching.md) as well as dynamic and data-heavy apps through ["Normalized Caching"](./graphcache/normalized-caching.md). `urql` can be understood as a collection of connected parts and packages. When we only need to install a single package for our framework of choice. We're then able to declaratively send GraphQL requests to our API. All framework packages — like `urql` (for React), `@urql/preact`, `@urql/svelte`, `@urql/solid`/`@urql/solid-start` and `@urql/vue` — wrap the [core package, `@urql/core`](./basics/core.md), which we can imagine as the brain of `urql` with most of its logic. As we progress with implementing `urql` into our application, we're later able to extend it by adding ["addon packages", which we call _Exchanges_](./advanced/authoring-exchanges.md) If at this point you're still unsure of whether to use `urql`, [have a look at the **Comparison** page](./comparison.md) and check whether `urql` supports all features you're looking for. ## Where to start We have **Getting Started** guides for: - [**React/Preact**](./basics/react-preact.md) covers how to work with the bindings for React/Preact. - [**Vue**](./basics/vue.md) covers how to work with the bindings for Vue 3. - [**Svelte**](./basics/svelte.md) covers how to work with the bindings for Svelte. - [**Solid**](./basics/solid.md) covers how to work with the bindings for Solid. - [**SolidStart**](./basics/solid-start.md) covers how to work with the bindings for SolidStart. - [**Core Package**](./basics/core.md) covers the shared "core APIs" and how we can use them directly in Node.js or imperatively. Each of these sections will walk you through the specific instructions for the framework bindings, including how to install and set them up, how to write queries, and how to send mutations. ## Following the Documentation This documentation is split into groups or sections that cover different levels of usage or areas of interest. - **Basics** is the section where we'll want to start learning about `urql` as it contains "Getting Started" guides for our framework of choice. - **Architecture** then explains more about how `urql` functions, what it's made up of, and covers the main aspects of the `Client` and exchanges. - **Advanced** covers all more uncommon use-cases and contains guides that we won't need immediately when we get started with `urql`. - **Graphcache** documents one of the most important addons to `urql`, which adds ["Normalized Caching" support](./graphcache/normalized-caching.md) to the `Client` and enables more complex use-cases, smarter caching, and more dynamic apps to function. - **Showcase** aims to list users of `urql`, third-party packages, and other helpful resources, like tutorials and guides. - **API** contains a detailed documentation on each package's APIs. The documentation links to each of these as appropriate, but if we're unsure of how to use a utility or package, we can go here directly to look up how to use a specific API. We hope you grow to love `urql`! ================================================ FILE: docs/advanced/README.md ================================================ --- title: Advanced order: 4 --- # Advanced In this chapter we'll dive into various topics of "advanced" `urql` usage. This is admittedly a catch-all chapter of various use-cases that can only be covered after [the "Architecture" chapter.](../architecture.md) - [**Subscriptions**](./subscriptions.md) covers how to use `useSubscription` and how to set up GraphQL subscriptions with `urql`. - [**Persistence & Uploads**](./persistence-and-uploads.md) teaches us how to set up Automatic Persisted Queries and File Uploads using the two respective packages. - [**Server-side Rendering**](./server-side-rendering.md) guides us through how to set up server-side rendering and rehydration. - [**Debugging**](./debugging.md) shows us the [`urql` devtools](https://github.com/urql-graphql/urql-devtools/) and how to add our own debug events for its event view. - [**Retrying operations**](./retry-operations.md) shows the `retryExchange` which allows you to retry operations when they've failed. - [**Authentication**](./authentication.md) describes how to implement authentication using the `authExchange` - [**Testing**](./testing.md) covers how to test components that use `urql` particularly in React. - [**Authoring Exchanges**](./authoring-exchanges.md) describes how to implement exchanges from scratch and how they work internally. This is a good basis to understanding how some features in this section function. - [**Auto-populate Mutations**](./auto-populate-mutations.md) presents the `populateExchange` addon, which can make it easier to update normalized data after mutations. ================================================ FILE: docs/advanced/authentication.md ================================================ --- title: Authentication order: 6 --- # Authentication Most APIs include some type of authentication, usually in the form of an auth token that is sent with each request header. The purpose of the [`authExchange`](../api/auth-exchange.md) is to provide a flexible API that facilitates the typical JWT-based authentication flow. > **Note:** [You can find a code example for `@urql/exchange-auth` in an example in the `urql` repository.](https://github.com/urql-graphql/urql/tree/main/examples/with-refresh-auth) ## Typical Authentication Flow **Initial login** — the user opens the application and authenticates for the first time. They enter their credentials and receive an auth token. The token is saved to storage that is persisted though sessions, e.g. `localStorage` on the web or `AsyncStorage` in React Native. The token is added to each subsequent request in an auth header. **Resume** — the user opens the application after having authenticated in the past. In this case, we should already have the token in persisted storage. We fetch the token from storage and add to each request, usually as an auth header. **Forced log out due to invalid token** — the user's session could become invalid for a variety reasons: their token expired, they requested to be signed out of all devices, or their session was invalidated remotely. In this case, we would want to also log them out in the application, so they could have the opportunity to log in again. To do this, we want to clear any persisted storage, and redirect them to the application home or login page. **User initiated log out** — when the user chooses to log out of the application, we usually send a logout request to the API, then clear any tokens from persisted storage, and redirect them to the application home or login page. **Refresh (optional)** — this is not always implemented; if your API supports it, the user will receive both an auth token, and a refresh token. The auth token is usually valid for a shorter duration of time (e.g. 1 week) than the refresh token (e.g. 6 months), and the latter can be used to request a new auth token if the auth token has expired. The refresh logic is triggered either when the JWT is known to be invalid (e.g. by decoding it and inspecting the expiry date), or when an API request returns with an unauthorized response. For graphQL APIs, it is usually an error code, instead of a 401 HTTP response, but both can be supported. When the token has been successfully refreshed (this can be done as a mutation to the graphQL API or a request to a different API endpoint, depending on implementation), we will save the new token in persisted storage, and retry the failed request with the new auth header. The user should be logged out and persisted storage cleared if the refresh fails or if the re-executing the query with the new token fails with an auth error for the second time. ## Installation & Setup First, install the `@urql/exchange-auth` alongside `urql`: ```sh yarn add @urql/exchange-auth # or npm install --save @urql/exchange-auth ``` You'll then need to add the `authExchange`, that this package exposes to your `Client`. The `authExchange` is an asynchronous exchange, so it must be placed in front of all `fetchExchange`s but after all other synchronous exchanges, like the `cacheExchange`. ```js import { Client, cacheExchange, fetchExchange } from 'urql'; import { authExchange } from '@urql/exchange-auth'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [ cacheExchange, authExchange(async utils => { return { /* config... */ }; }), fetchExchange, ], }); ``` You pass an initialization function to the `authExchange`. This function is called by the exchange when it first initializes. It'll let you receive an object of utilities and you must return a (promisified) object of configuration options. Let's discuss each of the [configuration options](../api/auth-exchange.md#options) and how to use them in turn. ### Configuring the initializer function (initial load) The initializer function must return a promise of a configuration object and hence also gives you an opportunity to fetch your authentication state from storage. ```js async function initializeAuthState() { const token = localStorage.getItem('token'); const refreshToken = localStorage.getItem('refreshToken'); return { token, refreshToken }; } authExchange(async utils => { let { token, refreshToken } = initializeAuthState(); return { /* config... */ }; }); ``` The first step here is to retrieve our tokens from a kind of storage, which may be asynchronous as well, as illustrated by `initializeAuthState`. In React Native, this is very similar, but because persisted storage in React Native is always asynchronous and promisified, we would await our tokens. This works because the function that `authExchange` is async, i.e. must return a `Promise`. ```js async function initializeAuthState() { const token = await AsyncStorage.getItem(TOKEN_KEY); const refreshToken = await AsyncStorage.getItem(REFRESH_KEY); return { token, refreshToken }; } authExchange(async utils => { let { token, refreshToken } = initializeAuthState(); return { /* config... */ }; }); ``` ### Configuring `addAuthToOperation` The purpose of `addAuthToOperation` is to apply an auth state to each request. Here, we'll use the tokens we retrieved from storage and add them to our operations. In this example, we're using a utility we're passed, `appendHeaders`. This utility is a simply shortcut to quickly add HTTP headers via `fetchOptions` to an `Operation`, however, we may as well be editing the `Operation` context here using `makeOperation`. ```js authExchange(async utils => { let token = await AsyncStorage.getItem(TOKEN_KEY); let refreshToken = await AsyncStorage.getItem(REFRESH_KEY); return { addAuthToOperation(operation) { if (!token) return operation; return utils.appendHeaders(operation, { Authorization: `Bearer ${token}`, }); }, // ... }; }); ``` First, we check that we have a non-null `token`. Then we apply it to the request using the `appendHeaders` utility as an `Authorization` header. We could also be using `makeOperation` here to update the context in any other way, such as: ```js import { makeOperation } from '@urql/core'; makeOperation(operation.kind, operation, { ...operation.context, someAuthThing: token, }); ``` ### Configuring `didAuthError` This function lets the `authExchange` know what is defined to be an API error for your API. `didAuthError` is called by `authExchange` when it receives an `error` on an `OperationResult`, which is of type [`CombinedError`](../api/core.md#combinederror). We can for example check the error's `graphQLErrors` array in `CombinedError` to determine if an auth error has occurred. While your API may implement this differently, an authentication error on an execution result may look a little like this if your API uses `extensions.code` on errors: ```js { data: null, errors: [ { message: 'Unauthorized: Token has expired', extensions: { code: 'FORBIDDEN' }, } ] } ``` If you're building a new API, using `extensions` on errors is the recommended approach to add metadata to your errors. We'll be able to determine whether any of the GraphQL errors were due to an unauthorized error code, which would indicate an auth failure: ```js authExchange(async utils => { // ... return { // ... didAuthError(error, _operation) { return error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN'); }, }; }); ``` For some GraphQL APIs, the authentication error is only communicated via a 401 HTTP status as is common in RESTful APIs, which is suboptimal, but which we can still write a check for. ```js authExchange(async utils => { // ... return { // ... didAuthError(error, _operation) { return error.response?.status === 401; }, }; }); ``` If `didAuthError` returns `true`, it will trigger the `authExchange` to trigger the logic for asking for re-authentication via `refreshAuth`. ### Configuring `refreshAuth` (triggered after an auth error has occurred) If the API doesn't support any sort of token refresh, this is where we could simply log the user out. ```js authExchange(async utils => { // ... return { // ... async refreshAuth() { logout(); }, }; }); ``` Here, `logout()` is a placeholder that is called when we got an error, so that we can redirect to a login page again and clear our tokens from local storage or otherwise. If we had a way to refresh our token using a refresh token, we can attempt to get a new token for the user first: ```js authExchange(async utils => { let token = localStorage.getItem('token'); let refreshToken = localStorage.getItem('refreshToken'); return { // ... async refreshAuth() { const result = await utils.mutate(REFRESH, { refreshToken }); if (result.data?.refreshLogin) { // Update our local variables and write to our storage token = result.data.refreshLogin.token; refreshToken = result.data.refreshLogin.refreshToken; localStorage.setItem('token', token); localStorage.setItem('refreshToken', refreshToken); } else { // This is where auth has gone wrong and we need to clean up and redirect to a login page localStorage.clear(); logout(); } }, }; }); ``` Here we use the special `mutate` utility method provided by the `authExchange` to do the token refresh. This is a useful method to use if your GraphQL API expects you to make a GraphQL mutation to update your authentication state. It will send the mutation and bypass all authentication and prior exchanges. If your authentication is not handled via GraphQL but a REST endpoint, you can use the `fetch` API here however instead of a mutation. All other requests will be paused while `refreshAuth` runs, so we won't have to deal with multiple authentication errors or refreshes at once. ### Configuring `willAuthError` `willAuthError` is an optional parameter and is run _before_ a request is made. We can use it to trigger an authentication error and let the `authExchange` run our `refreshAuth` function without the need to first let a request fail with an authentication error. For example, we can use this to predict an authentication error, for instance, because of expired JWT tokens. ```js authExchange(async utils => { // ... return { // ... willAuthError(_operation) { // Check whether `token` JWT is expired return false; }, }; }); ``` This can be really useful when we know when our authentication state is invalid and want to prevent even sending any operation that we know will fail with an authentication error. However, we have to be careful on how we define this function, if some queries or login mutations are sent to our API without being logged in. In these cases, it's better to either detect the mutations we'd like to allow or return `false` when a token isn't set in storage yet. If we'd like to detect a mutation that will never fail with an authentication error, we could for instance write the following logic: ```js authExchange(async utils => { // ... return { // ... willAuthError(operation) { if ( operation.kind === 'mutation' && // Here we find any mutation definition with the "login" field operation.query.definitions.some(definition => { return ( definition.kind === 'OperationDefinition' && definition.selectionSet.selections.some(node => { // The field name is just an example, since signup may also be an exception return node.kind === 'Field' && node.name.value === 'login'; }) ); }) ) { return false; } else if (false /* is JWT expired? */) { return true; } else { return false; } }, }; }); ``` Alternatively, you may decide to let all operations through if your token isn't set in storage, i.e. if you have no prior authentication state. ## Handling Logout by reacting to Errors We can also handle authentication errors in a `mapExchange` instead of the `authExchange`. To do this, we'll need to add the `mapExchange` to the exchanges array, _before_ the `authExchange`. The order is very important here: ```js import { createClient, cacheExchange, fetchExchange, mapExchange } from 'urql'; import { authExchange } from '@urql/exchange-auth'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [ cacheExchange, mapExchange({ onError(error, _operation) { const isAuthError = error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN'); if (isAuthError) { logout(); } }, }), authExchange(async utils => { return { /* config */ }; }), fetchExchange, ], }); ``` The `mapExchange` will only receive an auth error when the auth exchange has already tried and failed to handle it. This means we have either failed to refresh the token, or there is no token refresh functionality. If we receive an auth error in the `mapExchange`'s `onError` function (as defined in the `didAuthError` configuration section above), then we can be confident that it is an authentication error that the `authExchange` isn't able to recover from, and the user should be logged out. ## Cache Invalidation on Logout If we're dealing with multiple authentication states at the same time, e.g. logouts, we need to ensure that the `Client` is reinitialized whenever the authentication state changes. Here's an example of how we may do this in React if necessary: ```jsx import { createClient, Provider } from 'urql'; const App = ({ isLoggedIn }: { isLoggedIn: boolean | null }) => { const client = useMemo(() => { if (isLoggedIn === null) { return null; } return createClient({ /* config */ }); }, [isLoggedIn]); if (!client) { return null; } return { {/* app content */} } } ``` When the application launches, the first thing we do is check whether the user has any authentication tokens in persisted storage. This will tell us whether to show the user the logged in or logged out view. The `isLoggedIn` prop should always be updated based on authentication state change. For instance, we may set it to `true` after the user has authenticated and their tokens have been added to storage, and set it to `false` once the user has been logged out and their tokens have been cleared. It's important to clear or add tokens to a storage _before_ updating the prop in order for the auth exchange to work correctly. This pattern of creating a new `Client` when changing authentication states is especially useful since it will also recreate our client-side cache and invalidate all cached data. ================================================ FILE: docs/advanced/authoring-exchanges.md ================================================ --- title: Authoring Exchanges order: 8 --- # Exchange Author Guide As we've learned [on the "Architecture" page](../architecture.md) page, `urql`'s `Client` structures its data as an event hub. We have an input stream of operations, which are instructions for the `Client` to provide a result. These results then come from an output stream of operation results. _Exchanges_ are responsible for performing the important transform from the operations (input) stream to the results stream. Exchanges are handler functions that deal with these input and output streams. They're one of `urql`'s key components, and are needed to implement vital pieces of logic such as caching, fetching, deduplicating requests, and more. In other words, Exchanges are handlers that fulfill our GraphQL requests and can change the stream of operations or results. In this guide we'll learn more about how exchanges work and how we can write our own exchanges. ## An Exchange Signature Exchanges are akin to [middleware in Redux](https://redux.js.org/advanced/middleware) due to the way that they apply transforms. ```ts import { Client, Operation, OperationResult } from '@urql/core'; type ExchangeInput = { forward: ExchangeIO; client: Client }; type Exchange = (input: ExchangeInput) => ExchangeIO; type ExchangeIO = (ops$: Source) => Source; ``` The first parameter to an exchange is a `forward` function that refers to the next Exchange in the chain. The second second parameter is the `Client` being used. Exchanges always return an `ExchangeIO` function (this applies to the `forward` function as well), which accepts the source of [_Operations_](../api/core.md#operation) and returns a source of [_Operation Results_](../api/core.md#operationresult). - [Read more about streams on the "Architecture" page.](../architecture.md#stream-patterns-in-urql) - [Read more about the _Exchange_ type signature on the API docs.](../api/core.md#exchange) ## Using Exchanges The `Client` accepts an `exchanges` option that. Initially, we may choose to just set this to two very standard exchanges — `cacheExchange` and `fetchExchange`. In essence these exchanges build a pipeline that runs in the order they're passed; _Operations_ flow in from the start to the end, and _Results_ are returned through the chain in reverse. Suppose we pass the `cacheExchange` and then the `fetchExchange` to the `exchanges`. **First,** operations are checked against the cache. Depending on the `requestPolicy`, cached results can be resolved from here instead, which would mean that the cache sends back the result, and the operation doesn't travel any further in the chain. **Second,** operations are sent to the API, and the result is turned into an `OperationResult`. **Lastly,** operation results then travel through the exchanges in _reverse order_, which is because exchanges are a pipeline where all operations travel forward deeper into the exchange chain, and then backwards. When these results pass through the cache then the `cacheExchange` stores the result. ```js import { Client, fetchExchange, cacheExchange } from 'urql'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` We can add more exchanges to this chain, for instance, we can add the `mapExchange`, which can call a callback whenever it sees [a `CombinedError`](../basics/errors.md) occur on a result. ```js import { Client, fetchExchange, cacheExchange, mapExchange } from 'urql'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [ cacheExchange, mapExchange({ onError(error) { console.error(error); }, }), fetchExchange, ], }); ``` This is an example for adding a synchronous exchange to the chain that only reacts to results. It doesn't add any special behavior for operations travelling through it. An example for an asynchronous exchange that looks at both operations and results [we may look at the `retryExchange` which retries failed operations.](../advanced/retry-operations.md) ## The Rules of Exchanges Before we can start writing some exchanges, there are a couple of consistent patterns and limitations that must be adhered to when writing an exchange. We call these the "rules of Exchanges", which also come in useful when trying to learn what Exchanges actually are. For reference, this is a basic template for an exchange: ```js const noopExchange = ({ client, forward }) => { return operations$ => { // <-- The ExchangeIO function const operationResult$ = forward(operations$); return operationResult$; }; }; ``` This exchange does nothing else than forward all operations and return all results. Hence, it's called a `noopExchange` — an exchange that doesn't do anything. ### Forward and Return Composition When you create a `Client` and pass it an array of exchanges, `urql` composes them left-to-right. If we look at our previous `noopExchange` example in context, we can track what it does if it is located between the `cacheExchange` and the `fetchExchange`. ```js import { Client, cacheExchange, fetchExchange } from 'urql'; const noopExchange = ({ client, forward }) => { return operations$ => { // <-- The ExchangeIO function // We receive a stream of Operations from `cacheExchange` which // we can modify before... const forwardOperations$ = operations$; // ...calling `forward` with the modified stream. The `forward` // function is the next exchange's `ExchangeIO` function, in this // case `fetchExchange`. const operationResult$ = forward(operations$); // We get back `fetchExchange`'s stream of results, which we can // also change before returning, which is what `cacheExchange` // will receive when calling `forward`. return operationResult$; }; }; const client = new Client({ exchanges: [cacheExchange, noopExchange, fetchExchange], }); ``` ### How to Avoid Accidentally Dropping Operations Typically the `operations$` stream will send you `query`, `mutation`, `subscription`, and `teardown`. There is no constraint for new operations to be added later on or a custom exchange adding new operations altogether. This means that you have to take "unknown" operations into account and not `filter` operations too aggressively. ```js import { pipe, filter, merge } from 'wonka'; // DON'T: drop unknown operations ({ forward }) => operations$ => { // This doesn't handle operations that aren't queries const queries = pipe( operations$, filter(op => op.kind === 'query') ); return forward(queries); }; // DO: forward operations that you don't handle ({ forward }) => operations$ => { const queries = pipe( operations$, filter(op => op.kind === 'query') ); const rest = pipe( operations$, filter(op => op.kind !== 'query') ); return forward(merge([queries, rest])); }; ``` If operations are grouped and/or filtered by what the exchange is handling, then it's also important to make that any streams of operations not handled by the exchange should also be forwarded. ### Synchronous first, Asynchronous last By default exchanges and Wonka streams are as predictable as possible. Every operator in Wonka runs synchronously until asynchronicity is introduced. This may happen when using a timing utility from Wonka, like [`delay`](https://wonka.kitten.sh/api/operators#delay) or [`throttle`](https://wonka.kitten.sh/api/operators#throttle) This can also happen because the exchange inherently does something asynchronous, like fetching some data or using a promise. When writing exchanges, some will inevitably be asynchronous. For example if they're fetching results, performing authentication, or other tasks that you have to wait for. This can cause problems, because the behavior in `urql` is built to be _synchronous_ first. This is very helpful for suspense mode and allowing components receive cached data on their initial mount without rerendering. This why **all exchanges should be ordered synchronous first and asynchronous last**. What we for instance repeat as the default setup in our docs is this: ```js import { Client, cacheExchange, fetchExchange } from 'urql'; new Client({ // ... exchanges: [cacheExchange, fetchExchange]; }); ``` The `cacheExchange` is completely synchronous. The `fetchExchange` is asynchronous since it makes a `fetch` request and waits for a server response. If we put an asynchronous exchange in front of the `cacheExchange`, that would be unexpected, and since all results would then be delayed, nothing would ever be "cached" and instead always take some amount of time to be returned. When you're adding more exchanges, it's often crucial to put them in a specific order. For instance, an authentication exchange will need to go before the `fetchExchange`, a secondary cache will probably have to go in front of the default cache exchange. To ensure the correct behavior of suspense mode and the initialization of our hooks, it's vital to order exchanges so that synchronous ones come before asynchronous ones. ================================================ FILE: docs/advanced/auto-populate-mutations.md ================================================ --- title: Auto-populate Mutations order: 9 --- # Automatically populating Mutations The `populateExchange` allows you to auto-populate selection sets in your mutations using the `@populate` directive. In combination with [Graphcache](../graphcache/README.md) this is a useful tool to update the data in your application automatically following a mutation, when your app grows, and it becomes harder to track all fields that have been queried before. > **NOTE:** The `populateExchange` is _experimental_! Certain patterns and usage paths > like GraphQL field arguments aren't covered yet, and the exchange hasn't been extensively used > yet. ## Installation and Setup The `populateExchange` can be installed via the `@urql/exchange-populate` package. ```sh yarn add @urql/exchange-populate # or npm install --save @urql/exchange-populate ``` Afterwards we can set the `populateExchange` up by adding it to our list of `exchanges` in the client options. ```ts import { Client, fetchExchange } from '@urql/core'; import { populateExchange } from '@urql/exchange-populate'; const client = new Client({ // ... exchanges: [populateExchange({ schema }), cacheExchange, fetchExchange], }); ``` The `populateExchange` should be placed in front of the `cacheExchange`, especially if you're using [Graphcache](../graphcache/README.md), since it won't understand the `@populate` directive on its own. It should also be placed in front the `cacheExchange` to avoid unnecessary work. Adding the `populateExchange` now enables us to use the `@populate` directive in our mutations. The `schema` option is the introspection result for your backend graphql schema, more information about how to get your schema can be found [in the "Schema Awareness" Page of the Graphcache documentation.](../graphcache/schema-awareness.md#getting-your-schema). ## Example usage Consider the following queries, which have been requested in other parts of your application: ```graphql # Query 1 { todos { id name } } # Query 2 { todos { id createdAt } } ``` Without the `populateExchange` you may write a mutation like the following which returns a newly created todo item: ```graphql # Without populate mutation addTodo(id: ID!) { addTodo(id: $id) { id # To update Query 1 & 2 name # To update Query 1 createdAt # To update Query 2 } } ``` By using `populateExchange`, you no longer need to manually specify the selection set required to update your other queries. Instead you can just add the `@populate` directive. ```graphql # With populate mutation addTodo(id: ID!) { addTodo(id: $id) @populate } ``` ### Choosing when to populate You may not want to populate your whole mutation response. To reduce your payload, pass populate lower in your query. ```graphql mutation addTodo(id: ID!) { addTodo(id: $id) { id user @populate } } ``` ### Using aliases If you find yourself using multiple queries with variables, it may be necessary to [use aliases](https://graphql.org/learn/queries/#aliases) to allow merging of queries. > **Note:** This caveat may change in the future or this restriction may be lifted. **Invalid usage** ```graphql # Query 1 { todos(first: 10) { id name } } # Query 2 { todos(last: 20) { id createdAt } } ``` **Usage with aliases** ```graphql # Query 1 { firstTodos: todos(first: 10) { id name } } # Query 2 { lastTodos: todos(last: 20) { id createdAt } } ``` ================================================ FILE: docs/advanced/debugging.md ================================================ --- title: Debugging order: 4 --- # Debugging We've tried to make debugging in `urql` as seamless as possible by creating tools for users of `urql` and those creating their own exchanges. ## Devtools It's easiest to debug `urql` with the [`urql` devtools.](https://github.com/urql-graphql/urql-devtools/) It offers tools to inspect internal ["Debug Events"](#debug-events) as they happen, to explore data as your app is seeing it, and to quickly trigger GraphQL queries. [For instructions on how to set up the devtools, check out `@urql/devtools`'s readme in its repository.](https://github.com/urql-graphql/urql-devtools) ![Urql Devtools Timeline](../assets/devtools-timeline.png) ## Debug events The "Debug Events" are internally what displays more information to the user on the devtools' "Events" tab than just [Operations](../api/core.md#operation) and [Operation Results](../api/core.md#operationresult). Events may be fired inside exchanges to add additional development logging to an exchange. The `fetchExchange` for instance will fire a `fetchRequest` event when a request is initiated and either a `fetchError` or `fetchSuccess` event when a result comes back from the GraphQL API. The [Devtools](#browser-devtools) aren't the only way to observe these internal events. Anyone can start listening to these events for debugging events by calling the [`Client`'s](../api/core.md#client) `client.subscribeToDebugTarget()` method. Unlike `Operation`s these events are fire-and-forget events that are only used for debugging. Hence, they shouldn't be used for anything but logging and not for messaging. **Debug events are also entirely disabled in production.** ### Subscribing to Debug Events Internally the `devtoolsExchange` calls the `client.subscribeToDebugTarget`, but if we're looking to build custom debugging tools, it's also possible to call this function directly and to replace the `devtoolsExchange`. ``` const { unsubscribe } = client.subscribeToDebugTarget(event => { if (event.source === 'cacheExchange') return; console.log(event); // { type, message, operation, data, source, timestamp } }); ``` As demonstrated above, the `client.subscribeToDebugTarget` accepts a callback function and returns a subscription with an `unsubscribe` method. We've seen this pattern in the prior ["Stream Patterns" section on the "Architecture" page.](../architecture.md) ## Adding your own Debug Events Debug events are a means of sharing implementation details to consumers of an exchange. If you're creating an exchange and want to share relevant information with the `devtools`, then you may want to start adding your own events. #### Dispatching an event [On the "Authoring Exchanges" page](./authoring-exchanges.md) we've learned about the [`ExchangeInput` object](../api/core.md#exchangeinput), which comes with a `client` and a `forward` property. It also contains a `dispatchDebug` property. It is called with an object containing the following properties: | Prop | Type | Description | | ----------- | ----------- | ------------------------------------------------------------------------------------- | | `type` | `string` | A unique type identifier for the Debug Event. | | `message` | `string` | A human readable description of the event. | | `operation` | `Operation` | The [`Operation`](../api/core.md#operation) that the event targets. | | `data` | `?object` | This is an optional payload to include any data that may become useful for debugging. | For instance, we may call `dispatchDebug` with our `fetchRequest` event. This is the event that the `fetchExchange` uses to notify us that a request has commenced: ```ts export const fetchExchange: Exchange = ({ forward, dispatchDebug }) => { // ... return ops$ => { return pipe( ops$, // ... mergeMap(operation => { dispatchDebug({ type: 'fetchRequest', message: 'A network request has been triggered', operation, data: { /* ... */ }, }); // ... }) ); }; }; ``` If we're adding new events that aren't included in the main `urql` repository and are using TypeScript, we may also declare a fixed type for the `data` property, so we can guarantee a consistent payload for our Debug Events. This also prevents accidental conflicts. ```ts // urql.d.ts import '@urql/core'; declare module '@urql/core' { interface DebugEventTypes { customEventType: { somePayload: string }; } } ``` Read more about extending types, like `urql`'s `DebugEventTypes` on the [TypeScript docs on declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html). ### Tips Lastly, in summary, here are a few tips, that are important when we're adding new Debug Events to custom exchanges: - ✅ **Share internal details**: Frequent debug messages on key events inside your exchange are very useful when later inspecting them, e.g. in the `devtools`. - ✅ **Create unique event types** : Key events should be easily identifiable and have a unique names. - ❌ **Don't listen to debug events inside your exchange**: While it's possible to call `client.subscribeToDebugTarget` in an exchange it's only valuable when creating a debugging exchange, like the `devtoolsExchange`. - ❌ **Don't send warnings in debug events**: Informing your user about warnings isn't effective when the event isn't seen. You should still rely on `console.warn` so all users see your important warnings. ================================================ FILE: docs/advanced/persistence-and-uploads.md ================================================ --- title: Persistence & Uploads order: 1 --- # Persisted Queries and Uploads `urql` supports (Automatic) Persisted Queries, and File Uploads via GraphQL Multipart requests. For persisted queries to work, some setup work is needed, while File Upload support is built into `@urql/core@4`. ## Automatic Persisted Queries Persisted Queries allow us to send requests to the GraphQL API that can easily be cached on the fly, both by the GraphQL API itself and potential CDN caching layers. This is based on the unofficial [GraphQL Persisted Queries Spec](https://github.com/apollographql/apollo-link-persisted-queries#apollo-engine). With Automatic Persisted Queries the client hashes the GraphQL query and turns it into an SHA256 hash and sends this hash instead of the full query. If the server has seen this GraphQL query before it will recognise it by its hash and process the GraphQL API request as usual, otherwise it may respond using a `PersistedQueryNotFound` error. In that case the client is supposed to instead send the full GraphQL query, and the hash together, which will cause the query to be "registered" with the server. Additionally, we could also decide to send these hashed queries as GET requests instead of POST requests. If we only send the persisted queries with hashes as GET requests then they become a lot easier for a CDN to cache, as by default most caches would not cache POST requests automatically. In `urql`, we may use the `@urql/exchange-persisted` package's `persistedExchange` to enable support for Automatic Persisted Queries. This exchange works alongside other fetch or subscription exchanges by adding metadata for persisted queries to each GraphQL request by modifying the `extensions` object of operations. > **Note:** [You can find a code example for `@urql/exchange-persisted` in an example in the `urql` repository.](https://github.com/urql-graphql/urql/tree/main/examples/with-apq) ### Installation & Setup First install `@urql/exchange-persisted` alongside `urql`: ```sh yarn add @urql/exchange-persisted # or npm install --save @urql/exchange-persisted ``` You'll then need to add the `persistedExchange` function, that this package exposes, to your `exchanges`, in front of exchanges that communicate with the API: ```js import { Client, fetchExchange, cacheExchange } from 'urql'; import { persistedExchange } from '@urql/exchange-persisted'; const client = new Client({ url: 'http://localhost:1234/graphql', exchanges: [ cacheExchange, persistedExchange({ preferGetForPersistedQueries: true, }), fetchExchange, ], }); ``` As we can see, typically it's recommended to set `preferGetForPersistedQueries` to `true` to encourage persisted queries to use GET requests instead of POST so that CDNs can do their job. When set to `true` or `'within-url-limit'`, persisted queries will use GET requests if the resulting URL doesn't exceed the 2048 character limit. The `fetchExchange` can see the modifications that the `persistedExchange` is making to operations, and understands to leave out the `query` from any request as needed. The same should be happening to the `subscriptionExchange`, if you're using it for queries. ### Customizing Hashing The `persistedExchange` also accepts a `generateHash` option. This may be used to swap out the exchange's default method of generating SHA256 hashes. By default, the exchange will use the built-in [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) when it's available, and in Node.js it'll use the [Node Crypto Module](https://nodejs.org/api/crypto.html) instead. If you're using [the `graphql-persisted-document-loader` for Webpack](https://github.com/leoasis/graphql-persisted-document-loader), for instance, then you will already have a loader generating SHA256 hashes for you at compile time. In that case we could swap out the `generateHash` function with a much simpler one that uses the `generateHash` function's second argument, a GraphQL `DocumentNode` object. ```js persistedExchange({ generateHash: (_, document) => document.documentId, }); ``` If you're using **React Native** then you may not have access to the Web Crypto API, which means that you have to provide your own SHA256 function to the `persistedExchange`. Luckily, we can do so easily by using the first argument `generateHash` receives, a GraphQL query as a string. ```js import sha256 from 'hash.js/lib/hash/sha/256'; persistedExchange({ async generateHash(query) { return sha256().update(query).digest('hex'); }, }); ``` Additionally, if the API only expects persisted queries and not arbitrary ones and all queries are pre-registered against the API then the `persistedExchange` may be put into a **non-automatic** persisted queries mode by giving it the `enforcePersistedQueries: true` option. This disables any retry logic and assumes that persisted queries will be handled like regular GraphQL requests. ## File Uploads GraphQL server APIs commonly support the [GraphQL Multipart Request spec](https://github.com/jaydenseric/graphql-multipart-request-spec) to allow for File Uploads directly with a GraphQL API. If a GraphQL API supports this, we can pass a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) or a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) directly into our variables and define the corresponding scalar for our variable, which is often called `File` or `Upload`. In a browser, the `File` object may often be retrieved via a [file input](https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications), for example. > **Note:** If you are using your own version of `File` and `Blob` ensure you are properly extending the > so it can be properly identified as a file. The `@urql/core@4` package supports File Uploads natively, so we won't have to do any installation or setup work. When `urql` sees a `File` or a `Blob` anywhere in your `variables`, it switches to a `multipart/form-data` request, converts the request to a `FormData` object, according to the GraphQL Multipart Request specification, and sends it off to the API. > **Note:** Previously, this worked by installing the `@urql/multipart-fetch-exchange` package. > however, this package has been deprecated and file uploads are now built into `@urql/core@4`. [You can find a code example for file uploads in an example in the `urql` repository.](https://github.com/urql-graphql/urql/tree/main/examples/with-multipart) ================================================ FILE: docs/advanced/retry-operations.md ================================================ --- title: Retrying Operations order: 5 --- # Retrying Operations The `retryExchange` lets us retry specific operation, by default it will retry only network errors, but we can specify additional options to add functionality. > **Note:** [You can find a code example for `@urql/exchange-retry` in an example in the `urql` repository.](https://github.com/urql-graphql/urql/tree/main/examples/with-retry) ## Installation and Setup First install `@urql/exchange-retry` alongside `urql`: ```sh yarn add @urql/exchange-retry # or npm install --save @urql/exchange-retry ``` You'll then need to add the `retryExchange`, exposed by this package, to your `urql` Client: ```js import { Client, cacheExchange, fetchExchange } from 'urql'; import { retryExchange } from '@urql/exchange-retry'; // None of these options have to be added, these are the default values. const options = { initialDelayMs: 1000, maxDelayMs: 15000, randomDelay: true, maxNumberAttempts: 2, retryIf: err => err && err.networkError, }; // Note the position of the retryExchange - it should be placed prior to the // fetchExchange and after the cacheExchange for it to function correctly const client = new Client({ url: 'http://localhost:1234/graphql', exchanges: [ cacheExchange, retryExchange(options), // Use the retryExchange factory to add a new exchange fetchExchange, ], }); ``` We want to place the `retryExchange` before the `fetchExchange` so that retries are only performed _after_ the operation has passed through the cache and has attempted to fetch. ## The Options There are a set of optional options that allow for fine-grained control over the `retry` mechanism. We have the `initialDelayMs` to specify at what interval the `retrying` should start, this means that if we specify `1000` that when our `operation` fails we'll wait 1 second and then retry it. Next up is the `maxDelayMs`, our `retryExchange` will keep increasing the time between retries, so we don't spam our server with requests it can't complete, this option ensures we don't exceed a certain threshold. This time between requests will increase with a random `back-off` factor multiplied by the `initialDelayMs`, read more about the [thundering herd problem](https://en.wikipedia.org/wiki/Thundering_herd_problem). Talking about increasing the `delay` randomly, `randomDelay` allows us to disable this. When this option is set to `false` we'll only increase the time between attempts with the `initialDelayMs`. This means if we fail the first time we'll have 1 second wait, next fail we'll have 2 seconds and so on. We can declare the maximum number of attempts (including the initial request) with `maxNumberAttempts`, otherwise, it defaults to 2 (which means one retry). If you want it to retry indefinitely, you can simply pass in `Number.POSITIVE_INFINITY`. [For more information on the available options check out the API Docs.](../api/retry-exchange.md) ## Reacting to Different Errors We can introduce specific triggers for the `retryExchange` to start retrying operations, let's look at an example: ```js import { Client, cacheExchange, fetchExchange } from 'urql'; import { retryExchange } from '@urql/exchange-retry'; const client = new Client({ url: 'http://localhost:1234/graphql', exchanges: [ cacheExchange, retryExchange({ retryIf: error => { return !!(error.graphQLErrors.length > 0 || error.networkError); }, }), fetchExchange, ], }); ``` In the above example we'll retry when we have `graphQLErrors` or a `networkError`, we can go more granular and check for certain errors in `graphQLErrors`. ## Failover / Fallback In case of a network error, e.g., when part the infrastructure is down, but a fallback GraphQL endpoint is available, e.g., from a different provider on a different domain, the `retryWith` option allows for client-side failover. This could also be used in case of a `graphQLError`, for example, when APIs are deployed via a windowing strategy, i.e., a newer version at URL X, while an older one remains at Y. Note that finer granularity depending on custom requirements may be applicable, and that this does not allow for balancing load. ```js const fallbackUrl = 'http://localhost:1337/anotherGraphql'; const options = { initialDelayMs: 1000, maxDelayMs: 15000, randomDelay: true, maxNumberAttempts: 2, retryWith: (error, operation) => { if (error.networkError) { const context = { ...operation.context, url: fallbackUrl }; return { ...operation, context }; } return null; }, }; ``` ================================================ FILE: docs/advanced/server-side-rendering.md ================================================ --- title: Server-side Rendering order: 3 --- # Server-side Rendering In server-side rendered applications we often need to set our application up so that data will be fetched on the server-side and later sent down to the client for hydration. `urql` supports this through the `ssrExchange.` ## The SSR Exchange The `ssrExchange` has two functions. On the server-side it's able to gather all results as they're being fetched, which can then be serialized and sent to the client. On the client-side it's able to use these serialized results to rehydrate and render the application without refetching this data. To start out with the `ssrExchange` we have to add the exchange to our `Client`: ```js import { Client, cacheExchange, fetchExchange, ssrExchange } from '@urql/core'; const isServerSide = typeof window === 'undefined'; // The `ssrExchange` must be initialized with `isClient` and `initialState` const ssr = ssrExchange({ isClient: !isServerSide, initialState: !isServerSide ? window.__URQL_DATA__ : undefined, }); const client = new Client({ exchanges: [ cacheExchange, ssr, // Add `ssr` in front of the `fetchExchange` fetchExchange, ], }); ``` The `ssrExchange` must be initialized with the `isClient` and `initialState` options. The `isClient` option tells the exchange whether it's on the server- or client-side. In our example we use `typeof window` to determine this, but in Webpack environments you may also be able to use `process.browser`. Optionally, we may also choose to enable `staleWhileRevalidate`. When enabled this flag will ensure that although a result may have been rehydrated from our SSR result, another refetch `network-only` operation will be issued, to update stale data. This is useful for statically generated sites (SSG) that may ship stale data to our application initially. The `initialState` option should be set to the serialized data you retrieve on your server-side. This data may be retrieved using methods on `ssrExchange()`. You can retrieve the serialized data after server-side rendering using `ssr.extractData()`: ```js // Extract and serialise the data like so from the `ssr` instance // we've previously created by calling `ssrExchange()` const data = JSON.stringify(ssr.extractData()); const markup = ''; // The render code for our framework goes here const html = `
${markup}
`; ``` This will provide `__URQL_DATA__` globally, which we've used in our first example to inject data into the `ssrExchange` on the client-side. Alternatively you can also call `restoreData` as long as this call happens synchronously before the `client` starts receiving queries. ```js const isServerSide = typeof window === 'undefined'; const ssr = ssrExchange({ isClient: !isServerSide }); if (!isServerSide) { ssr.restoreData(window.__URQL_DATA__); } ``` ## Using `react-ssr-prepass` In the previous examples we've set up the `ssrExchange`, however with React this still requires us to manually execute our queries before rendering a server-side React app [using `renderToString` or `renderToNodeStream`](https://reactjs.org/docs/react-dom-server.html#rendertostring). For React, `urql` has a "Suspense mode" that [allows data fetching to interrupt rendering](https://reactjs.org/docs/concurrent-mode-suspense.html). However, Suspense is not supported by React during server-side rendering. Using [the `react-ssr-prepass` package](https://github.com/FormidableLabs/react-ssr-prepass) however, we can implement a prerendering step before we let React server-side render, which allows us to automatically fetch all data that the app requires with Suspense. This technique is commonly referred to as a "two-pass approach", since our React element is traversed twice. To set this up, first we'll install `react-ssr-prepass`. It has a peer dependency on `react-is` and `react`. ```sh yarn add react-ssr-prepass react-is react-dom # or npm install --save react-ssr-prepass react-is react-dom ``` Next, we'll modify our server-side code and add `react-ssr-prepass` in front of `renderToString`. ```jsx import { renderToString } from 'react-dom/server'; import prepass from 'react-ssr-prepass'; import { Client, cacheExchange, fetchExchange, ssrExchange, Provider, } from 'urql'; const handleRequest = async (req, res) => { // ... const ssr = ssrExchange({ isClient: false }); const client = new Client({ url: 'https://??', suspense: true, // This activates urql's Suspense mode on the server-side exchanges: [cacheExchange, ssr, fetchExchange] }); const element = ( ); // Using `react-ssr-prepass` this prefetches all data await prepass(element); // This is the usual React SSR rendering code const markup = renderToString(element); // Extract the data after prepass and rendering const data = JSON.stringify(ssr.extractData()); res.status(200).send(`
${markup}
`); }; ``` It's important to set enable the `suspense` option on the `Client`, which switches it to support React suspense. ### With Preact If you're using Preact instead of React, there's a drop-in replacement package for `react-ssr-prepass`, which is called `preact-ssr-prepass`. It only has a peer dependency on Preact, and we can install it like so: ```sh yarn add preact-ssr-prepass preact # or npm install --save preact-ssr-prepass preact ``` All above examples for `react-ssr-prepass` will still be the same, except that instead of using the `urql` package we'll have to import from `@urql/preact`, and instead of `react-ssr-prepass` we'll have to import from. `preact-ssr-prepass`. ## Next.js If you're using [Next.js](https://nextjs.org/) you can save yourself a lot of work by using `@urql/next`. The `@urql/next` package is set to work with Next 13. To set up `@urql/next`, first we'll install `@urql/next` and `urql` as peer dependencies: ```sh yarn add @urql/next urql graphql # or npm install --save @urql/next urql graphql ``` We now have two ways to leverage `@urql/next`, one being part of a Server component or being part of the general `app/` folder. In a server component we will import from `@urql/next/rsc` ```ts // app/page.tsx import React from 'react'; import { cacheExchange, createClient, fetchExchange, gql } from '@urql/core'; import { registerUrql } from '@urql/next/rsc'; const makeClient = () => { return createClient({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', exchanges: [cacheExchange, fetchExchange], }); }; const { getClient } = registerUrql(makeClient); export default async function Home() { const result = await getClient().query(PokemonsQuery, {}); return (

This is rendered as part of an RSC

    {result.data.pokemons.map((x: any) => (
  • {x.name}
  • ))}
); } ``` When we aren't leveraging server components we will import the things we will need to do a bit more setup, we go to the `client` component's layout file and structure it as the following. ```tsx // app/client/layout.tsx 'use client'; import { useMemo } from 'react'; import { UrqlProvider, ssrExchange, cacheExchange, fetchExchange, createClient } from '@urql/next'; export default function Layout({ children }: React.PropsWithChildren) { const [client, ssr] = useMemo(() => { const ssr = ssrExchange({ isClient: typeof window !== 'undefined', }); const client = createClient({ url: 'https://trygql.formidable.dev/graphql/web-collections', exchanges: [cacheExchange, ssr, fetchExchange], suspense: true, }); return [client, ssr]; }, []); return ( {children} ); } ``` It is important that we pass both a client as well as the `ssrExchange` to the `Provider` this way we will be able to restore the data that Next streams to the client later on when we are hydrating. The next step is to query data in your client components by means of the `useQuery` method defined in `@urql/next`. ```tsx // app/client/page.tsx 'use client'; import Link from 'next/link'; import { Suspense } from 'react'; import { useQuery, gql } from '@urql/next'; export default function Page() { return ( ); } const PokemonsQuery = gql` query { pokemons(limit: 10) { id name } } `; function Pokemons() { const [result] = useQuery({ query: PokemonsQuery }); return (

This is rendered as part of SSR

    {result.data.pokemons.map((x: any) => (
  • {x.name}
  • ))}
); } ``` The data queried in the above component will be rendered on the server and re-hydrated back on the client. When using multiple Suspense boundaries these will also get flushed as they complete and re-hydrated. > When data is used throughout the application we advise against > rendering this as part of a server-component so you can benefit > from the client-side cache. ### Invalidating data from a server-component When data is rendered by a server component but you dispatch a mutation from a client component the server won't automatically know that the server-component on the client needs refreshing. You can forcefully tell the server to do so by using the Next router and calling `.refresh()`. ```tsx import { useRouter } from 'next/navigation'; const Todo = () => { const router = useRouter(); const executeMutation = async () => { await updateTodo(); router.refresh(); }; }; ``` ### Disabling RSC fetch caching You can pass `fetchOptions: { cache: "no-store" }` to the `createClient` constructor to avoid running into cached fetches with server-components. ## Legacy Next.js (pages) If you're using [Next.js](https://nextjs.org/) with the classic `pages` you can instead use `next-urql`. To set up `next-urql`, first we'll install `next-urql` with `react-is` and `urql` as peer dependencies: ```sh yarn add next-urql react-is urql graphql # or npm install --save next-urql react-is urql graphql ``` The peer dependency on `react-is` is inherited from `react-ssr-prepass` requiring it. Note that if you are using Next before v9.4 you'll need to polyfill fetch, this can be done through [`isomorphic-unfetch`](https://www.npmjs.com/package/isomorphic-unfetch). We're now able to wrap any page or `_app.js` using the `withUrqlClient` higher-order component. If we wrap `_app.js` we won't have to wrap any individual page. ```js // pages/index.js import React from 'react'; import { useQuery } from 'urql'; import { withUrqlClient } from 'next-urql'; const Index = () => { const [result] = useQuery({ query: '{ test }', }); // ... }; export default withUrqlClient((_ssrExchange, ctx) => ({ // ...add your Client options here url: 'http://localhost:3000/graphql', }))(Index); ``` The `withUrqlClient` higher-order component function accepts the usual `Client` options as an argument. This may either just be an object, or a function that receives the Next.js' `getInitialProps` context. One added caveat is that these options may not include the `exchanges` option because `next-urql` injects the `ssrExchange` automatically at the right location. If you're setting up custom exchanges you'll need to instead provide them in the `exchanges` property of the returned client object. ```js import { cacheExchange, fetchExchange } from '@urql/core'; import { withUrqlClient } from 'next-urql'; export default withUrqlClient(ssrExchange => ({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, ssrExchange, fetchExchange], }))(Index); ``` Unless the component that is being wrapped already has a `getInitialProps` method, `next-urql` won't add its own SSR logic, which automatically fetches queries during server-side rendering. This can be explicitly enabled by passing the `{ ssr: true }` option as a second argument to `withUrqlClient`. When you are using `getStaticProps`, `getServerSideProps`, or `getStaticPaths`, you should opt-out of `Suspense` by setting the `neverSuspend` option to `true` in your `withUrqlClient` configuration. During the prepass of your component tree `next-urql` can't know how these functions will alter the props passed to your page component. This injection could change the `variables` used in your `useQuery`. This will lead to error being thrown during the subsequent `toString` pass, which isn't supported in React 16. ### SSR with { ssr: true } The `withUrqlClient` only wraps our component tree with the context provider by default. To enable SSR, the easiest way is specifying the `{ ssr: true }` option as a second argument to `withUrqlClient`: ```js import { cacheExchange, fetchExchange } from '@urql/core'; import { withUrqlClient } from 'next-urql'; export default withUrqlClient( ssrExchange => ({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, ssrExchange, fetchExchange], }), { ssr: true } // Enables server-side rendering using `getInitialProps` )(Index); ``` Be aware that wrapping the `_app` component using `withUrqlClient` with the `{ ssr: true }` option disables Next's ["Automatic Static Optimization"](https://nextjs.org/docs/advanced-features/automatic-static-optimization) for **all our pages**. It is thus preferred to enable server-side rendering on a per-page basis. ### SSR with getStaticProps or getServerSideProps Enabling server-side rendering using `getStaticProps` and `getServerSideProps` is a little more involved, but has two major benefits: 1. allows **direct schema execution** for performance optimisation 2. allows performing extra operations in those functions To make the functions work with the `withUrqlClient` wrapper, return the `urqlState` prop with the extracted data from the `ssrExchange`: ```js import { withUrqlClient, initUrqlClient } from 'next-urql'; import { ssrExchange, cacheExchange, fetchExchange, useQuery } from 'urql'; const TODOS_QUERY = ` query { todos { id text } } `; function Todos() { const [res] = useQuery({ query: TODOS_QUERY }); return (
{res.data.todos.map(todo => (
{todo.id} - {todo.text}
))}
); } export async function getStaticProps(ctx) { const ssrCache = ssrExchange({ isClient: false }); const client = initUrqlClient( { url: 'your-url', exchanges: [cacheExchange, ssrCache, fetchExchange], }, false ); // This query is used to populate the cache for the query // used on this page. await client.query(TODOS_QUERY).toPromise(); return { props: { // urqlState is a keyword here so withUrqlClient can pick it up. urqlState: ssrCache.extractData(), }, revalidate: 600, }; } export default withUrqlClient( ssr => ({ url: 'your-url', }) // Cannot specify { ssr: true } here so we don't wrap our component in getInitialProps )(Todos); ``` The above example will make sure the page is rendered as a static-page, It's important that you fully pre-populate your cache so in our case we were only interested in getting our todos, if there are child components relying on data you'll have to make sure these are fetched as well. The `getServerSideProps` and `getStaticProps` functions only run on the **server-side** — any code used in them is automatically stripped away from the client-side bundle using the [next-code-elimination tool](https://next-code-elimination.vercel.app/). This allows **executing our schema directly** using `@urql/exchange-execute` if we have access to our GraphQL server: ```js import { withUrqlClient, initUrqlClient } from 'next-urql'; import { ssrExchange, cacheExchange, fetchExchange, useQuery } from 'urql'; import { executeExchange } from '@urql/exchange-execute'; import { schema } from '@/server/graphql'; // our GraphQL server's executable schema const TODOS_QUERY = ` query { todos { id text } } `; function Todos() { const [res] = useQuery({ query: TODOS_QUERY }); return (
{res.data.todos.map(todo => (
{todo.id} - {todo.text}
))}
); } export async function getServerSideProps(ctx) { const ssrCache = ssrExchange({ isClient: false }); const client = initUrqlClient( { url: '', // not needed without `fetchExchange` exchanges: [ cacheExchange, ssrCache, executeExchange({ schema }), // replaces `fetchExchange` ], }, false ); await client.query(TODOS_QUERY).toPromise(); return { props: { urqlState: ssrCache.extractData(), }, }; } export default withUrqlClient(ssr => ({ url: 'your-url', }))(Todos); ``` Direct schema execution skips one network round trip by accessing your resolvers directly instead of performing a `fetch` API call. ### Stale While Revalidate If we choose to use Next's static site generation (SSG or ISG) we may be embedding data in our initial payload that's stale on the client. In this case, we may want to update this data immediately after rehydration. We can pass `staleWhileRevalidate: true` to `withUrqlClient`'s second option argument to Switch it to a mode where it'll refresh its rehydrated data immediately by issuing another network request. ```js export default withUrqlClient( ssr => ({ url: 'your-url', }), { staleWhileRevalidate: true } )(...); ``` Now, although on rehydration we'll receive the stale data from our `ssrExchange` first, it'll also immediately issue another `network-only` operation to update the data. During this revalidation our stale results will be marked using `result.stale`. While this is similar to what we see with `cache-and-network` without server-side rendering, it isn't quite the same. Changing the request policy wouldn't actually refetch our data on rehydration as the `ssrExchange` is simply a replacement of a full network request. Hence, this flag allows us to treat this case separately. ### Resetting the client instance In rare scenario's you possibly will have to reset the client instance (reset all cache, ...), this is an uncommon scenario, and we consider it "unsafe" so evaluate this carefully for yourself. When this does seem like the appropriate solution any component wrapped with `withUrqlClient` will receive the `resetUrqlClient` property, when invoked this will create a new top-level client and reset all prior operations. ## Vue Suspense In Vue 3 a [new feature was introduced](https://vuedose.tips/go-async-in-vue-3-with-suspense/) that natively allows components to suspend while data is loading, which works universally on the server and on the client, where a replacement loading template is rendered on a parent while data is loading. We've previously seen how we can change our usage of `useQuery`'s `PromiseLike` result to [make use of Vue Suspense on the "Queries" page.](../basics/vue.md#vue-suspense) Any component's `setup()` function can be updated to instead be an `async setup()` function, in other words, to return a `Promise` instead of directly returning its data. This means that we can update any `setup()` function to make use of Suspense. On the server-side we can then use `@vue/server-renderer`'s `renderToString`, which will return a `Promise` that resolves when all suspense-related loading is completed. ```jsx import { createSSRApp } = from 'vue' import { renderToString } from '@vue/server-renderer'; import urql, { createClient, cacheExchange, fetchExchange, ssrExchange } from '@urql/vue'; const handleRequest = async (req, res) => { // This is where we'll put our root component const app = createSSRApp(Root) // NOTE: All we care about here is that the SSR Exchange is included const ssr = ssrExchange({ isClient: false }); app.use(urql, { exchanges: [cacheExchange, ssr, fetchExchange] }); const markup = await renderToString(app); const data = JSON.stringify(ssr.extractData()); res.status(200).send(`
${markup}
`); }; ``` This effectively renders our Vue app on the server-side and provides the client-side data for rehydration that we've set up in the above [SSR Exchange section](#the-ssr-exchange) to use. ================================================ FILE: docs/advanced/subscriptions.md ================================================ --- title: Subscriptions order: 0 --- # Subscriptions One feature of `urql` that was not mentioned in the ["Basics" sections](../basics/README.md) is `urql`'s APIs and ability to handle GraphQL subscriptions. ## The Subscription Exchange To add support for subscriptions we need to add the `subscriptionExchange` to our `Client`. ```js import { Client, cacheExchange, fetchExchange, subscriptionExchange } from 'urql'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [ cacheExchange, fetchExchange, subscriptionExchange({ forwardSubscription, }), ], }); ``` Read more about Exchanges and how they work [on the "Authoring Exchanges" page.](./authoring-exchanges.md) or what they are [on the "Architecture" page.](../architecture.md) In the above example, we add the `subscriptionExchange` to the `Client` with the default exchanges added before it. The `subscriptionExchange` is a factory that accepts additional options and returns the actual `Exchange` function. It does not make any assumption over the transport protocol and scheme that is used. Instead, we need to pass a `forwardSubscription` function. The `forwardSubscription` is called when the `subscriptionExchange` receives an `Operation`, so typically, when you’re executing a GraphQL subscription. This will call the `forwardSubscription` function with a GraphQL request body, in the same shape that a GraphQL HTTP API may receive it as JSON input. If you’re using TypeScript, you may notice that the input that `forwardSubscription` receives has an optional `query` property. This is because of persisted query support. For some transports, the `query` property may have to be defaulted to an empty string, which matches the GraphQL over HTTP specification more closely. When we define this function it must return an "Observable-like" object, which needs to follow the [Observable spec](https://github.com/tc39/proposal-observable), which comes down to having an object with a `.subscribe()` method accepting an observer. ### Setting up `graphql-ws` For backends supporting `graphql-ws`, we recommend using the [graphql-ws](https://github.com/enisdenjo/graphql-ws) client. ```js import { Client, cacheExchange, fetchExchange, subscriptionExchange } from 'urql'; import { createClient as createWSClient } from 'graphql-ws'; const wsClient = createWSClient({ url: 'ws://localhost/graphql', }); const client = new Client({ url: '/graphql', exchanges: [ cacheExchange, fetchExchange, subscriptionExchange({ forwardSubscription(request) { const input = { ...request, query: request.query || '' }; return { subscribe(sink) { const unsubscribe = wsClient.subscribe(input, sink); return { unsubscribe }; }, }; }, }), ], }); ``` In this example, we're creating a `SubscriptionClient`, are passing in a URL and some parameters, and are using the `SubscriptionClient`'s `request` method to create a Subscription Observable, which we return to the `subscriptionExchange` inside `forwardSubscription`. [Read more on the `graphql-ws` README.](https://github.com/enisdenjo/graphql-ws/blob/master/README.md) ### Setting up `subscriptions-transport-ws` For backends supporting `subscriptions-transport-ws`, [Apollo's `subscriptions-transport-ws` package](https://github.com/apollographql/subscriptions-transport-ws) can be used. > The `subscriptions-transport-ws` package isn't actively maintained. If your API supports the new protocol or you can swap the package out, consider using [`graphql-ws`](#setting-up-graphql-ws) instead. ```js import { Client, cacheExchange, fetchExchange, subscriptionExchange } from 'urql'; import { SubscriptionClient } from 'subscriptions-transport-ws'; const subscriptionClient = new SubscriptionClient('ws://localhost/graphql', { reconnect: true }); const client = new Client({ url: '/graphql', exchanges: [ cacheExchange, fetchExchange, subscriptionExchange({ forwardSubscription: request => subscriptionClient.request(request), }), ], }); ``` In this example, we're creating a `SubscriptionClient`, are passing in a URL and some parameters, and are using the `SubscriptionClient`'s `request` method to create a Subscription Observable, which we return to the `subscriptionExchange` inside `forwardSubscription`. [Read more about `subscription-transport-ws` on its README.](https://github.com/apollographql/subscriptions-transport-ws/blob/master/README.md) ### Using `fetch` for subscriptions Some GraphQL backends (for example GraphQL Yoga) support built-in transport protocols that can execute subscriptions via a simple HTTP fetch call. In fact, this is how `@defer` and `@stream` directives are supported. These transports can also be used for subscriptions. ```js import { Client, cacheExchange, fetchExchange, subscriptionExchange } from 'urql'; const client = new Client({ url: '/graphql', fetchSubscriptions: true, exchanges: [cacheExchange, fetchExchange], }); ``` In this example, we only need to enable `fetchSubscriptions: true` on the `Client`, and the `fetchExchange` will be used to send subscriptions to the API. If your API supports this transport, it will stream results back to the `fetchExchange`. [You can find a code example of subscriptions via `fetch` in an example in the `urql` repository.](https://github.com/urql-graphql/urql/tree/main/examples/with-subscriptions-via-fetch) ## React & Preact The `useSubscription` hooks comes with a similar API to `useQuery`, which [we've learned about in the "Queries" page in the "Basics" section.](../basics/react-preact.md#queries) Its usage is extremely similar in that it accepts options, which may contain `query` and `variables`. However, it also accepts a second argument, which is a reducer function, similar to what you would pass to `Array.prototype.reduce`. It receives the previous set of data that this function has returned or `undefined`. As the second argument, it receives the event that has come in from the subscription. You can use this to accumulate the data over time, which is useful for a list for example. In the following example, we create a subscription that informs us of new messages. We will concatenate the incoming messages so that we can display all messages that have come in over the subscription across events. ```js import React from 'react'; import { useSubscription } from 'urql'; const newMessages = ` subscription MessageSub { newMessages { id from text } } `; const handleSubscription = (messages = [], response) => { return [response.newMessages, ...messages]; }; const Messages = () => { const [res] = useSubscription({ query: newMessages }, handleSubscription); if (!res.data) { return

No new messages

; } return (
    {res.data.map(message => (

    {message.from}: "{message.text}"

    ))}
); }; ``` As we can see, the `res.data` is being updated and transformed by the `handleSubscription` function. This works over time, so as new messages come in, we will append them to the list of previous messages. [Read more about the `useSubscription` API in the API docs for it.](../api/urql.md#usesubscription) ## Svelte The `subscriptionStore` function in `@urql/svelte` comes with a similar API to `query`, which [we've learned about in the "Queries" page in the "Basics" section.](../basics/svelte.md#queries) Its usage is extremely similar in that it accepts an `operationStore`, which will typically contain our GraphQL subscription query. In the following example, we create a subscription that informs us of new messages. ```js {#if !$messages.data}

No new messages

{:else}
    {#each $messages.data.newMessages as message}
  • {message.from}: "{message.text}"
  • {/each}
{/if} ``` As we can see, `$messages.data` is being updated and transformed by the `$messages` subscriptionStore. This works over time, so as new messages come in, we will append them to the list of previous messages. `subscriptionStore` optionally accepts a second argument, a handler function, allowing custom update behavior from the subscription. [Read more about the `subscription` API in the API docs for it.](../api/svelte.md#subscriptionstore) ## Vue The `useSubscription` API is very similar to `useQuery`, which [we've learned about in the "Queries" page in the "Basics" section.](../basics/vue.md#queries) Its usage is extremely similar in that it accepts options, which may contain `query` and `variables`. However, it also accepts a second argument, which is a reducer function, similar to what you would pass to `Array.prototype.reduce`. It receives the previous set of data that this function has returned or `undefined`. As the second argument, it receives the event that has come in from the subscription. You can use this to accumulate the data over time, which is useful for a list for example. In the following example, we create a subscription that informs us of new messages. We will concatenate the incoming messages so that we can display all messages that have come in over the subscription across events. ```jsx ``` As we can see, the `result.data` is being updated and transformed by the `handleSubscription` function. This works over time, so as new messages come in, we will append them to the list of previous messages. [Read more about the `useSubscription` API in the API docs for it.](../api/vue.md#usesubscription) ## One-off Subscriptions When you're using subscriptions directly without `urql`'s framework bindings, you can use the `Client`'s `subscription` method for one-off subscriptions. This method is similar to the ones for mutations and subscriptions [that we've seen before on the "Core Package" page.](../basics/core.md) This method will always [returns a Wonka stream](../architecture.md#the-wonka-library) and doesn't have a `.toPromise()` shortcut method, since promises won't return the multiple values that a subscription may deliver. Let's convert the above example to one without framework code, as we may use subscriptions in a Node.js environment. ```js import { gql } from '@urql/core'; const MessageSub = gql` subscription MessageSub { newMessages { id from text } } `; const { unsubscribe } = client.subscription(MessageSub).subscribe(result => { console.log(result); // { data: ... } }); ``` ================================================ FILE: docs/advanced/testing.md ================================================ --- title: Testing order: 7 --- # Testing Testing with `urql` can be done in a multitude of ways. The most effective and straightforward method is to mock the `Client` to force your components into a fixed state during testing. The following examples demonstrate this method of testing for React and the `urql` package only, however the pattern itself can be adapted for any framework-bindings of `urql`. ## Mocking the client For the most part, urql's hooks are just adapters for talking to the urql client. The way in which they do this is by making calls to the client via context. - `useQuery` calls `executeQuery` - `useMutation` calls `executeMutation` - `useSubscription` calls `executeSubscription` In the section ["Stream Patterns" on the "Architecture" page](../architecture.md) we've seen, that all methods on the client operate with and return streams. These streams are created using [the Wonka library](../architecture.md#the-wonka-library), and we're able to create streams ourselves to mock the different states of our operations, e.g. fetching, errors, or success with data. You'll probably use one of these utility functions to create streams: - `never`: This stream doesn’t emit any values and never completes, which puts our `urql` code in a permanent `fetching: true` state. - `fromValue`: This utility function accepts a value and emits it immediately, which we can use to mock a result from the server. - `makeSubject`: Allows us to create a source and imperatively push responses, which is useful to test subscription and simulate changes, i.e. multiple states. Creating a mock `Client` is pretty quick as we'll create an object that contains the `Client`'s methods that the React `urql` hooks use. We'll mock the appropriate `execute` functions that we need to mock a set of hooks. After we've created the mock `Client` we can wrap components with the `Provider` from `urql` and pass it. Here's an example client mock being used while testing a component. ```tsx import { mount } from 'enzyme'; import { Provider } from 'urql'; import { never } from 'wonka'; import { MyComponent } from './MyComponent'; it('renders', () => { const mockClient = { executeQuery: jest.fn(() => never), executeMutation: jest.fn(() => never), executeSubscription: jest.fn(() => never), }; const wrapper = mount( ); }); ``` ## Testing calls to the client Once you have your mock setup, calls to the client can be tested. ```tsx import { mount } from 'enzyme'; import { Provider } from 'urql'; import { MyComponent } from './MyComponent'; it('skips the query', () => { mount( ); expect(mockClient.executeQuery).toBeCalledTimes(0); }); ``` Testing mutations and subscriptions also work in a similar fashion. ```tsx import { mount } from 'enzyme'; import { Provider } from 'urql'; import { MyComponent } from './MyComponent'; it('triggers a mutation', () => { const wrapper = mount( ); const variables = { name: 'Carla' }; wrapper.find('input').simulate('change', { currentTarget: { value: variables.name } }); wrapper.find('button').simulate('click'); expect(mockClient.executeMutation).toBeCalledTimes(1); expect(mockClient.executeMutation).toBeCalledWith(expect.objectContaining({ variables }), {}); }); ``` ## Forcing states For testing render output, or creating fixtures, you may want to force the state of your components. ### Fetching Fetching states can be simulated by returning a stream, which never returns. Wonka provides a utility for this, aptly called `never`. Here's a fixture, which stays in the _fetching_ state. ```tsx import { Provider } from 'urql'; import { never } from 'wonka'; import { MyComponent } from './MyComponent'; const fetchingState = { executeQuery: () => never, }; export default ( ); ``` ### Response (success) Response states are simulated by providing a stream, which contains a network response. For single responses, Wonka's `fromValue` function can do this for us. **Example snapshot test of response state** ```tsx import { mount } from 'enzyme'; import { Provider } from 'urql'; import { fromValue } from 'wonka'; import { MyComponent } from './MyComponent'; it('matches snapshot', () => { const responseState = { executeQuery: () => fromValue({ data: { posts: [ { id: 1, title: 'Post title', content: 'This is a post' }, { id: 3, title: 'Final post', content: 'Final post here' }, ], }, }), }; const wrapper = mount( ); expect(wrapper).toMatchSnapshot(); }); ``` ### Response (error) Error responses are similar to success responses, only the value in the stream is changed. ```tsx import { Provider, CombinedError } from 'urql'; import { fromValue } from 'wonka'; const errorState = { executeQuery: () => fromValue({ error: new CombinedError({ networkError: Error('something went wrong!'), }), }), }; ``` ### Handling multiple hooks Returning different values for many `useQuery` calls can be done by introducing conditionals into the mocked client functions. ```tsx import { fromValue } from 'wonka'; let mockClient; beforeEach(() => { mockClient = () => { executeQuery: ({ query }) => { if (query === GET_USERS) { return fromValue(usersResponse); } if (query === GET_POSTS) { return fromValue(postsResponse); } }; }; }); ``` The above client we've created mocks all three operations — queries, mutations and subscriptions — to always remain in the `fetching: true` state. Generally when we're _hoisting_ our mocked client and reuse it across multiple tests we have to be mindful not to instantiate the mocks outside of Jest's lifecycle functions (like `it`, `beforeEach`, `beforeAll` and such) as it may otherwise reset our mocked functions' return values or implementation. ## Subscriptions Testing subscriptions can be done by simulating the arrival of new data over time. To do this we may use the `interval` utility from Wonka, which emits values on a timer, and for each value we can map over the response that we'd like to mock. If you prefer to have more control on when the new data is arriving you can use the `makeSubject` utility from Wonka. You can see more details in the next section. Here's an example of testing a list component, which uses a subscription. ```tsx import { OperationContext, makeOperation } from '@urql/core'; import { mount } from 'enzyme'; import { Provider } from 'urql'; import { MyComponent } from './MyComponent'; it('should update the list', done => { const mockClient = { executeSubscription: jest.fn(query => pipe( interval(200), map((i: number) => ({ // To mock a full result, we need to pass a mock operation back as well operation: makeOperation('subscription', query, {} as OperationContext), data: { posts: { id: i, title: 'Post title', content: 'This is a post' } }, })) ) ), }; let index = 0; const wrapper = mount( ); setTimeout(() => { expect(wrapper.find('.list').children()).toHaveLength(index + 1); // See how many items are in the list index++; if (index === 2) done(); }, 200); }); ``` ## Simulating changes Simulating multiple responses can be useful, particularly testing `useEffect` calls dependent on changing query responses. For this, a _subject_ is the way to go. In short, it's a stream that you can push responses to. The `makeSubject` function from Wonka is what you'll want to use for this purpose. Below is an example of simulating subsequent responses (such as a cache update/refetch) in a test. ```tsx import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { Provider } from 'urql'; import { makeSubject } from 'wonka'; import { MyComponent } from './MyComponent'; const { source: stream, next: pushResponse } = makeSubject(); it('shows notification on updated data', () => { const mockedClient = { executeQuery: jest.fn(() => stream), }; const wrapper = mount( ); // First response act(() => { pushResponse({ data: { posts: [{ id: 1, title: 'Post title', content: 'This is a post' }], }, }); }); expect(wrapper.find('dialog').exists()).toBe(false); // Second response act(() => { pushResponse({ data: { posts: [ { id: 1, title: 'Post title', content: 'This is a post' }, { id: 1, title: 'Post title', content: 'This is a post' }, ], }, }); }); expect(wrapper.find('dialog').exists()).toBe(true); }); ``` ================================================ FILE: docs/api/README.md ================================================ --- title: API order: 9 --- # API `urql` is a collection of multiple packages. You'll likely be using one of the framework bindings package or exchange packages, which are all listed in this section. Most of these packages will refer to or use utilities and types from the `@urql/core` package. [Read more about the core package on the "Core" page.](../basics/core.md) > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. - [`@urql/core` API docs](./core.md) - [`urql` React API docs](./urql.md) - [`@urql/preact` Preact API docs](./preact.md) - [`@urql/svelte` Svelte API docs](./svelte.md) - [`@urql/exchange-graphcache` API docs](./graphcache.md) - [`@urql/exchange-retry` API docs](./retry-exchange.md) - [`@urql/exchange-execute` API docs](./execute-exchange.md) - [`@urql/exchange-request-policy` API docs](./request-policy-exchange.md) - [`@urql/exchange-auth` API docs](./auth-exchange.md) - [`@urql/exchange-refocus` API docs](./refocus-exchange.md) ================================================ FILE: docs/api/auth-exchange.md ================================================ --- title: '@urql/exchange-auth' order: 10 --- # Authentication Exchange > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. The `@urql/exchange-auth` package contains an addon `authExchange` for `urql` that aims to make it easy to implement complex authentication and reauthentication flows as are typically found with JWT token based API authentication. ## Installation and Setup First install `@urql/exchange-auth` alongside `urql`: ```sh yarn add @urql/exchange-auth # or npm install --save @urql/exchange-auth ``` You'll then need to add the `authExchange`, that this package exposes to your `Client`. The `authExchange` is an asynchronous exchange, so it must be placed in front of all `fetchExchange`s but after all other synchronous exchanges, like the `cacheExchange`. ```js import { createClient, cacheExchange, fetchExchange } from 'urql'; import { authExchange } from '@urql/exchange-auth'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [ cacheExchange, authExchange(async utils => { return { /* config... */ }; }), fetchExchange, ], }); ``` The `authExchange` accepts an initialization function. This function is called when your exchange and `Client` first start up, and must return an object of options wrapped in a `Promise`, which is used to configure how your authentication method works. You can use this function to first retrieve your authentication state from a kind of local storage, or to call your API to validate your authentication state first. The relevant configuration options, returned to the `authExchange`, then determine how the `authExchange` behaves: - `addAuthToOperation` must be provided to tell `authExchange` how to add authentication information to an operation, e.g. how to add the authentication state to an operation's fetch headers. - `willAuthError` may be provided to detect expired tokens or tell whether an operation will likely fail due to an authentication error. - `didAuthError` may be provided to let the `authExchange` detect authentication errors from the API on results. - `refreshAuth` is called when an authentication error occurs and gives you an opportunity to update your authentication state. Afterwards, the `authExchange` will retry your operation. [Read more examples in the documentation given here.](../advanced/authentication.md) ================================================ FILE: docs/api/core.md ================================================ --- title: '@urql/core' order: 0 --- # @urql/core > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. The `@urql/core` package is the basis of all framework bindings. Each bindings-package, like [`urql` for React](./urql.md) or [`@urql/preact`](./preact.md), will reuse the core logic and reexport all exports from `@urql/core`. Therefore if you're not accessing utilities directly, aren't in a Node.js environment, and are using framework bindings, you'll likely want to import from your framework bindings package directly. [Read more about `urql`'s core on the "Core Package" page.](../basics/core.md) ## Client The `Client` manages all operations and ongoing requests to the exchange pipeline. It accepts several options on creation. `@urql/core` also exposes `createClient()` that is just a convenient alternative to calling `new Client()`. | Input | Type | Description | | ----------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `exchanges` | `Exchange[]` | An array of `Exchange`s that the client should use | | `url` | `string` | The GraphQL API URL as used by `fetchExchange` | | `fetchOptions` | `RequestInit \| () => RequestInit` | Additional `fetchOptions` that `fetch` in `fetchExchange` should use to make a request | | `fetch` | `typeof fetch` | An alternative implementation of `fetch` that will be used by the `fetchExchange` instead of `window.fetch` | | `suspense` | `?boolean` | Activates the experimental React suspense mode, which can be used during server-side rendering to prefetch data | | `requestPolicy` | `?RequestPolicy` | Changes the default request policy that will be used. By default, this will be `cache-first`. | | `preferGetMethod` | `?boolean \| 'force' \| 'within-url-limit'` | This is picked up by the `fetchExchange` and will force all queries (not mutations) to be sent using the HTTP GET method instead of POST if the length of the resulting URL doesn't exceed 2048 characters. When `'force'` is passed a GET request is always sent regardless of how long the resulting URL is. | ### client.executeQuery Accepts a [`GraphQLRequest`](#graphqlrequest) and optionally `Partial`, and returns a [`Source`](#operationresult) — a stream of query results that can be subscribed to. Internally, subscribing to the returned source will create an [`Operation`](#operation), with `kind` set to `'query'`, and dispatch it on the exchanges pipeline. If no subscribers are listening to this operation anymore and unsubscribe from the query sources, the `Client` will dispatch a "teardown" operation. - [Instead of using this method directly, you may want to use the `client.query` shortcut instead.](#clientquery) - [See `createRequest` for a utility that creates `GraphQLRequest` objects.](#createrequest) ### client.executeSubscription This is functionally the same as `client.executeQuery`, but creates operations for subscriptions instead, with `kind` set to `'subscription'`. ### client.executeMutation This is functionally the same as `client.executeQuery`, but creates operations for mutations instead, with `kind` set to `'mutation'`. A mutation source is always guaranteed to only respond with a single [`OperationResult`](#operationresult) and then complete. ### client.query This is a shorthand method for [`client.executeQuery`](#clientexecutequery), which accepts a query (`DocumentNode | string`) and variables separately and creates a [`GraphQLRequest`](#graphqlrequest) [`createRequest`](#createrequest) automatically. The returned `Source` will also have an added `toPromise` method, so the stream can be conveniently converted to a promise. ```js import { pipe, subscribe } from 'wonka'; const { unsubscribe } = pipe( client.query('{ test }', { /* vars */ }), subscribe(result => { console.log(result); // OperationResult }) ); // or with toPromise, which also limits this to one result client .query('{ test }', { /* vars */ }) .toPromise() .then(result => { console.log(result); // OperationResult }); ``` [Read more about how to use this API on the "Core Package" page.](../basics/core.md#one-off-queries-and-mutations) ### client.mutation This is similar to [`client.query`](#clientquery), but dispatches mutations instead. [Read more about how to use this API on the "Core Package" page.](../basics/core.md#one-off-queries-and-mutations) ### client.subscription This is similar to [`client.query`](#clientquery), but does not provide a `toPromise()` helper method on the streams it returns. [Read more about how to use this API on the "Subscriptions" page.](../advanced/subscriptions.md) ### client.reexecuteOperation This method is commonly used in _Exchanges_ to reexecute an [`Operation`](#operation) on the `Client`. It will only reexecute when there are still subscribers for the given [`Operation`](#operation). For an example, this method is used by the `cacheExchange` when an [`OperationResult`](#operationresult) is invalidated in the cache and needs to be refetched. ### client.readQuery This method is typically used to read data synchronously from a cache. It returns an [`OperationResult`](#operationresult) if a value is returned immediately or `null` if no value is returned while cancelling all side effects. ## CombinedError The `CombinedError` is used in `urql` to normalize network errors and `GraphQLError`s if anything goes wrong during a GraphQL request. | Input | Type | Description | | --------------- | -------------------------------- | ---------------------------------------------------------------------------------- | | `networkError` | `?Error` | An unexpected error that might've occurred when trying to send the GraphQL request | | `graphQLErrors` | `?Array` | GraphQL Errors (if any) that were returned by the GraphQL API | | `response` | `?any` | The raw response object (if any) from the `fetch` call | [Read more about errors in `urql` on the "Error" page.](../basics/errors.md) ## Types ### GraphQLRequest This often comes up as the **input** for every GraphQL request. It consists of `query` and optionally `variables`. | Prop | Type | Description | | ----------- | -------------- | --------------------------------------------------------------------------------------------------------------------- | | `key` | `number` | A unique key that identifies this exact combination of `query` and `variables`, which is derived using a stable hash. | | `query` | `DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. | | `variables` | `?object` | The variables to be used with the GraphQL request. | The `key` property is a hash of both the `query` and the `variables`, to uniquely identify the request. When `variables` are passed it is ensured that they're stably stringified so that the same variables in a different order will result in the same `key`, since variables are order-independent in GraphQL. [A `GraphQLRequest` may be manually created using the `createRequest` helper.](#createrequest) ### OperationType This determines what _kind of operation_ the exchanges need to perform. This is one of: - `'subscription'` - `'query'` - `'mutation'` - `'teardown'` The `'teardown'` operation is special in that it instructs exchanges to cancel any ongoing operations with the same key as the `'teardown'` operation that is received. ### Operation The input for every exchange that informs GraphQL requests. It extends the [`GraphQLRequest` type](#graphqlrequest) and contains these additional properties: | Prop | Type | Description | | --------- | ------------------ | --------------------------------------------- | | `kind` | `OperationType` | The type of GraphQL operation being executed. | | `context` | `OperationContext` | Additional metadata passed to exchange. | An `Operation` also contains the `operationName` property, which is a deprecated alias of the `kind` property and outputs a deprecation warning if it's used. ### RequestPolicy This determines the strategy that a cache exchange should use to fulfill an operation. When you implement a custom cache exchange it's recommended that these policies are handled. - `'cache-first'` (default) - `'cache-only'` - `'network-only'` - `'cache-and-network'` [Read more about request policies on the "Document Caching" page.](../basics/document-caching.md#request-policies) ### OperationContext The context often carries options or metadata for individual exchanges, but may also contain custom data that can be passed from almost all API methods in `urql` that deal with [`Operation`s](#operation). Some of these options are set when the `Client` is initialised, so in the following list of properties you'll likely see some options that exist on the `Client` as well. | Prop | Type | Description | | --------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `fetchOptions` | `?RequestInit \| (() => RequestInit)` | Additional `fetchOptions` that `fetch` in `fetchExchange` should use to make a request. | | `fetch` | `typeof fetch` | An alternative implementation of `fetch` that will be used by the `fetchExchange` instead of `window.fetch` | | `requestPolicy` | `RequestPolicy` | An optional [request policy](../basics/document-caching.md#request-policies) that should be used specifying the cache strategy. | | `url` | `string` | The GraphQL endpoint, when using GET you should use absolute url's | | `meta` | `?OperationDebugMeta` | Metadata that is only available in development for devtools. | | `suspense` | `?boolean` | Whether suspense is enabled. | | `preferGetMethod` | `?boolean \| 'force' \| 'within-url-limit'` | Instructs the `fetchExchange` to use HTTP GET for queries. | | `additionalTypenames` | `?string[]` | Allows you to tell the operation that it depends on certain typenames (used in document-cache.) | It also accepts additional, untyped parameters that can be used to send more information to custom exchanges. ### OperationResult The result of every GraphQL request, i.e. an `Operation`. It's very similar to what comes back from a typical GraphQL API, but slightly enriched and normalized. | Prop | Type | Description | | ------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `operation` | `Operation` | The operation that this is a result for | | `data` | `?any` | Data returned by the specified query | | `error` | `?CombinedError` | A [`CombinedError`](#combinederror) instances that wraps network or `GraphQLError`s (if any) | | `extensions` | `?Record` | Extensions that the GraphQL server may have returned. | | `stale` | `?boolean` | A flag that may be set to `true` by exchanges to indicate that the `data` is incomplete or out-of-date, and that the result will be updated soon. | ### ExchangeInput This is the input that an [`Exchange`](#exchange) receives when it's initialized by the [`Client`](#client) | Input | Type | Description | | --------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | | `forward` | `ExchangeIO` | The unction responsible for receiving an observable operation and returning a result | | `client` | `Client` | The urql application-wide client library. Each execute method starts a GraphQL request and returns a stream of results. | ### Exchange An exchange represents abstractions of small chunks of logic in `urql`. They're small building blocks and similar to "middleware". [Read more about _Exchanges_ on the "Authoring Exchanges" page.](../advanced/authoring-exchanges.md) An exchange is defined to be a function that receives [`ExchangeInput`](#exchangeinput) and returns an `ExchangeIO` function. The `ExchangeIO` function in turn will receive a stream of operations, and must return a stream of results. If the exchange is purely transforming data, like the `mapExchange` for instance, it'll call `forward`, which is the next Exchange's `ExchangeIO` function to get a stream of results. ```js type ExchangeIO = (Source) => Source; type Exchange = ExchangeInput => ExchangeIO; ``` [If you haven't yet seen streams you can read more about "Stream Patterns" on the "Architecture" page.](../architecture.md) ## Exchanges ### cacheExchange The `cacheExchange` as [described on the "Document Caching" page.](../basics/document-caching.md). It's of type `Exchange`. ### subscriptionExchange The `subscriptionExchange` as [described on the "Subscriptions" page.](../advanced/subscriptions.md). It's of type `Options => Exchange`. It accepts a single input: `{ forwardSubscription }`. This is a function that receives an enriched operation and must return an Observable-like object that streams `GraphQLResult`s with `data` and `errors`. The `forwardSubscription` function is commonly connected to the [`subscriptions-transport-ws` package](https://github.com/apollographql/subscriptions-transport-ws). ### ssrExchange The `ssrExchange` as [described on the "Server-side Rendering" page.](../advanced/server-side-rendering.md). It's of type `Options => Exchange`. It accepts three inputs, `initialState` which is completely optional and populates the server-side rendered data with a rehydrated cache, `isClient` which can be set to `true` or `false` to tell the `ssrExchange` whether to write to (server-side) or read from (client-side) the cache, and `staleWhileRevalidate` which will treat rehydrated data as stale and refetch up-to-date data by reexecuring the operation using a `network-only` requests policy. By default, `isClient` defaults to `true` when the `Client.suspense` mode is disabled and to `false` when the `Client.suspense` mode is enabled. This can be used to extract data that has been queried on the server-side, which is also described in the Basics section, and is also used on the client-side to restore server-side rendered data. When called, this function creates an `Exchange`, which also has two methods on it: - `.restoreData(data)` which can be used to inject data, typically on the client-side. - `.extractData()` which is typically used on the server-side to extract the server-side rendered data. Basically, the `ssrExchange` is a small cache that collects data during the server-side rendering pass, and allows you to populate the cache on the client-side with the same data. During React rehydration this cache will be emptied, and it will become inactive and won't change the results of queries after rehydration. It needs to be used _after_ other caching Exchanges like the `cacheExchange`, but before any _asynchronous_ Exchange like the `fetchExchange`. ### debugExchange An exchange that writes incoming `Operation`s to `console.log` and writes completed `OperationResult`s to `console.log`. This exchange is disabled in production and is based on the `mapExchange`. If you'd like to customise it, you can replace it with a custom `mapExchange`. ### fetchExchange The `fetchExchange` of type `Exchange` is responsible for sending operations of type `'query'` and `'mutation'` to a GraphQL API using `fetch`. ### mapExchange The `mapExchange` allows you to: - react to or replace operations with `onOperation`, - react to or replace results with `onResult`, - and; react to errors in results with `onError`. It can therefore be used to quickly react to the core events in the `Client` without writing a custom exchange, effectively allowing you to ship your own `debugExchange`. ```ts mapExchange({ onOperation(operation) { console.log('operation', operation); }, onResult(result) { console.log('result', result); }, }); ``` It can also be used to react only to errors, which is the same as checking for `result.error`: ```ts mapExchange({ onError(error, operation) { console.log(`The operation ${operation.key} has errored with:`, error); }, }); ``` Lastly, it can be used to map operations and results, which may be useful to update the `OperationContext` or perform other standard tasks that require you to wait for a result: ```ts import { mapExchange, makeOperation } from '@urql/core'; mapExchange({ async onOperation(operation) { // NOTE: This is only for illustration purposes return makeOperation(operation.kind, operation, { ...operation.context, test: true, }); }, async onResult(result) { // NOTE: This is only for illustration purposes if (result.data === undefined) result.data = null; return result; }, }); ``` ### errorExchange (deprecated) An exchange that lets you inspect errors. This can be useful for logging, or reacting to different types of errors (e.g. logging the user out in case of a permission error). In newer versions of `@urql/core`, it's identical to the `mapExchange` and its export has been replaced as the `mapExchange` also allows you to pass an `onError` function. ## Utilities ### gql This is a `gql` tagged template literal function, similar to the one that's also commonly known from `graphql-tag`. It can be used to write GraphQL documents in a tagged template literal and returns a parsed `DocumentNode` that's primed against the `createRequest`'s cache for `key`s. ```js import { gql } from '@urql/core'; const SharedFragment = gql` fragment UserFrag on User { id name } `; gql` query { user ...UserFrag } ${SharedFragment} `; ``` Unlike `graphql-tag`, this function outputs a warning in development when names of fragments in the document are duplicated. It does not output warnings when fragment names were duplicated globally however. ### stringifyVariables This function is a variation of `JSON.stringify` that sorts any object's keys that is being stringified to ensure that two objects with a different order of keys will be stably stringified to the same string. ```js stringifyVariables({ a: 1, b: 2 }); // {"a":1,"b":2} stringifyVariables({ b: 2, a: 1 }); // {"a":1,"b":2} ``` ### createRequest This utility accepts a GraphQL query of type `string | DocumentNode` and optionally an object of variables, and returns a [`GraphQLRequest` object](#graphqlrequest). Since the [`client.executeQuery`](#clientexecutequery) and other execute methods only accept [`GraphQLRequest`s](#graphqlrequest), this helper is commonly used to create that request first. The [`client.query`](#clientquery) and [`client.mutation`](#clientmutation) methods use this helper as well to create requests. The helper takes care of creating a unique `key` for the `GraphQLRequest`. This is a hash of the `query` and `variables` if they're passed. The `variables` will be stringified using [`stringifyVariables`](#stringifyvariables), which outputs a stable JSON string. Additionally, this utility will ensure that the `query` reference will remain stable. This means that if the same `query` will be passed in as a string or as a fresh `DocumentNode`, then the output will always have the same `DocumentNode` reference. ### makeOperation This utility is used to either turn a [`GraphQLRequest` object](#graphqlrequest) into a new [`Operation` object](#operation) or to copy an `Operation`. It adds the `kind` property, and the `operationName` alias that outputs a deprecation warning. It accepts three arguments: - An `Operation`'s `kind` (See [`OperationType`](#operationtype) - A [`GraphQLRequest` object](#graphqlrequest) or another [`Operation`](#operation) that should be copied. - and; optionally a [partial `OperationContext` object.](#operationcontext). This argument may be left out if the context is to be copied from the operation that may be passed as a second argument. Hence some valid uses of the utility are: ```js // Create a new operation from scratch makeOperation('query', createRequest(query, variables), client.createOperationContext(opts)); // Turn an operation into a 'teardown' operation makeOperation('teardown', operation); // Copy an existing operation while modifying its context makeOperation(operation.kind, operation, { ...operation.context, preferGetMethod: true, }); ``` ### makeResult This is a helper function that converts a GraphQL API result to an [`OperationResult`](#operationresult). It accepts an [`Operation`](#operation), the API result, and optionally the original `FetchResponse` for debugging as arguments, in that order. ### makeErrorResult This is a helper function that creates an [`OperationResult`](#operationresult) for GraphQL API requests that failed with a generic or network error. It accepts an [`Operation`](#operation), the error, and optionally the original `FetchResponse` for debugging as arguments, in that order. ### formatDocument This utility is used by the [`cacheExchange`](#cacheexchange) and by [Graphcache](../graphcache/README.md) to add `__typename` fields to GraphQL `DocumentNode`s. ### composeExchanges This utility accepts an array of `Exchange`s and composes them into a single one. It chains them in the order that they're given, left to right. ```js function composeExchanges(Exchange[]): Exchange; ``` This can be used to combine some exchanges and is also used by [`Client`](#client) to handle the `exchanges` input. ================================================ FILE: docs/api/execute-exchange.md ================================================ --- title: '@urql/exchange-execute' order: 6 --- # Execute Exchange > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. The `@urql/exchange-execute` package contains an addon `executeExchange` for `urql` that may be used to execute queries against a local schema. It is therefore a drop-in replacement for the default _fetchExchange_ and useful for the server-side, debugging, or testing. ## Installation and Setup First install `@urql/exchange-execute` alongside `urql`: ```sh yarn add @urql/exchange-execute # or npm install --save @urql/exchange-execute ``` You'll then need to add the `executeExchange`, exposed by this package, to your `Client`. It'll typically replace the `fetchExchange` or similar exchanges and must be used last if possible, since it'll handle operations and return results. ```js import { createClient, cacheExchange } from 'urql'; import { executeExchange } from '@urql/exchange-execute'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [ cacheExchange, executeExchange({ /* config */ }), ], }); ``` The `executeExchange` accepts an object of options, which are all similar to the arguments that `graphql/execution/execute` accepts. Typically you'd pass it the `schema` option, some resolvers if your schema isn't already executable as `fieldResolver` / `typeResolver` / `rootValue`, and a `context` value or function. ## Options | Option | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `schema` | This is of type `GraphQLSchema` and accepts either a schema that is or isn't executable. This field is _required_ while all other fields are _optional_. | | `rootValue` | The root value that `graphql`'s `execute` will use when starting to execute the schema. | | `fieldResolver` | A given field resolver function. Creating an executable schema may be easier than providing this, but this resolver will be passed on to `execute` as expected. | | `typeResolver` | A given type resolver function. Creating an executable schema may be easier than providing this, but this resolver will be passed on to `execute` as expected. | | `context` | This may either be a function that receives an [`Operation`](./core.md#operation) and returns the context value, or just a plain context value. Similarly to a GraphQL server this is useful as all resolvers will have access to your `context` | ================================================ FILE: docs/api/graphcache.md ================================================ --- title: '@urql/exchange-graphcache' order: 4 --- # @urql/exchange-graphcache > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. The `@urql/exchange-graphcache` package contains an addon `cacheExchange` for `urql` that may be used to replace the default [`cacheExchange`](./core.md#cacheexchange), which switches `urql` from using ["Document Caching"](../basics/document-caching.md) to ["Normalized Caching"](../graphcache/normalized-caching.md). [Read more about how to use and configure _Graphcache_ in the "Graphcache" section](../graphcache/README.md) ## cacheExchange The `cacheExchange` function, as exported by `@urql/exchange-graphcache`, accepts a single object of options and returns an [`Exchange`](./core.md#exchange). | Input | Description | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `keys` | A mapping of key generator functions for types that are used to override the default key generation that _Graphcache_ uses to normalize data for given types. | | `resolvers` | A nested mapping of resolvers, which are used to override the record or entity that _Graphcache_ resolves for a given field for a type. | | `directives` | A mapping of directives, which are functions accepting directive arguments and returning a resolver, which can be referenced by `@localDirective` or `@_localDirective` in queries. | | `updates` | A nested mapping of updater functions for mutation and subscription fields, which may be used to add side-effects that update other parts of the cache when the given subscription or mutation field is written to the cache. | | `optimistic` | A mapping of mutation fields to resolvers that may be used to provide _Graphcache_ with an optimistic result for a given mutation field that should be applied to the cached data temporarily. | | `schema` | A serialized GraphQL schema that is used by _Graphcache_ to resolve partial data, interfaces, and enums. The schema also used to provide helpful warnings for [schema awareness](../graphcache/schema-awareness.md). | | `storage` | A persisted storage interface that may be provided to preserve cache data for [offline support](../graphcache/offline.md). | | `globalIDs` | A boolean or list of typenames that have globally unique ids, this changes how graphcache internally keys the entities. This can be useful for complex interface relationships. | | `logger` | A function that will be invoked for warning/debug/... logs | The `@urql/exchange-graphcache` package also exports the `offlineExchange`; which is identical to the `cacheExchange` but activates [offline support](../graphcache/offline.md) when the `storage` option is passed. ### `keys` option This is a mapping of typenames to `KeyGenerator` functions. ```ts interface KeyingConfig { [typename: string]: (data: Data) => null | string; } ``` It may be used to alter how _Graphcache_ generates the key it uses for normalization for individual types. The key generator function may also always return `null` when a type should always be embedded. [Read more about how to set up `keys` in the "Key Generation" section of the "Normalized Caching" page.](../graphcache/normalized-caching.md#key-generation) ### `resolvers` option This configuration is a mapping of typenames to field names to `Resolver` functions. A resolver may be defined to override the entity or record that a given field on a type should resolve on the cache. ```ts interface ResolverConfig { [typeName: string]: { [fieldName: string]: Resolver; }; } ``` A `Resolver` receives four arguments when it's called: `parent`, `args`, `cache`, and `info`. | Argument | Type | Description | | -------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `parent` | `Data` | The parent entity that the given field is on. | | `args` | `object` | The arguments for the given field the updater is executed on. | | `cache` | `Cache` | The cache using which data can be read or written. [See `Cache`.](#cache) | | `info` | `Info` | Additional metadata and information about the current operation and the current field. [See `Info`.](#info) | We can use the arguments it receives to either return new data based on just the arguments and other cache information, but we may also read information about the parent and return new data for the current field. ```js { Todo: { createdAt(parent, args, cache) { // Read `createdAt` on the parent but return a Date instance const date = cache.resolve(parent, 'createdAt'); return new Date(date); } } } ``` [Read more about how to set up `resolvers` on the "Computed Queries" page.](../graphcache/local-resolvers.md) ### `updates` option The `updates` configuration is a mapping of `'Mutation' | 'Subscription'` to field names to `UpdateResolver` functions. An update resolver may be defined to add side-effects that run when a given mutation field or subscription field is written to the cache. These side-effects are helpful to update data in the cache that is implicitly changed on the GraphQL API, that _Graphcache_ can't know about automatically. For mutation fields that don't have an updater, Graphcache has a fallback: if a returned entity isn't currently found in the cache, it assumes a create-mutation and invalidates cached entities of that type. This behavior was introduced in Graphcache v7 and is skipped once an updater for the mutation field is added. ```ts interface UpdatesConfig { Mutation: { [fieldName: string]: UpdateResolver; }; Subscription: { [fieldName: string]: UpdateResolver; }; } ``` An `UpdateResolver` receives four arguments when it's called: `result`, `args`, `cache`, and `info`. | Argument | Type | Description | | -------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `result` | `any` | Always the entire `data` object from the mutation or subscription. | | `args` | `object` | The arguments for the given field the updater is executed on. | | `cache` | `Cache` | The cache using which data can be read or written. [See `Cache`.](#cache) | | `info` | `Info` | Additional metadata and information about the current operation and the current field. [See `Info`.](#info) | It's possible to derive more information about the current update using the `info` argument. For instance this metadata contains the current `fieldName` of the updater which may be used to make an updater function more reusable, along with `parentKey` and other key fields. It also contains `variables` and `fragments` which remain the same for the entire write operation, and additionally it may have the `error` field set to describe whether the current field is `null` because the API encountered a `GraphQLError`. [Read more about how to set up `updates` on the "Custom Updates" page.](../graphcache/cache-updates.md) ### `optimistic` option The `optimistic` configuration is a mapping of Mutation field names to `OptimisticMutationResolver` functions, which return optimistic mutation results for given fields. These results are used by _Graphcache_ to optimistically update the cache data, which provides an immediate and temporary change to its data before a mutation completes. ```ts interface OptimisticMutationConfig { [mutationFieldName: string]: OptimisticMutationResolver; } ``` A `OptimisticMutationResolver` receives three arguments when it's called: `variables`, `cache`, and `info`. | Argument | Type | Description | | -------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `args` | `object` | The arguments that the given mutation field received. | | `cache` | `Cache` | The cache using which data can be read or written. [See `Cache`.](#cache) | | `info` | `Info` | Additional metadata and information about the current operation and the current field. [See `Info`.](#info) | [Read more about how to set up `optimistic` on the "Custom Updates" page.](../graphcache/cache-updates.md) ### `schema` option The `schema` option may be used to pass a `IntrospectionQuery` data to _Graphcache_, in other words it's used to provide schema information to it. This schema is then used to resolve and return partial results when querying, which are results that the cache can partially resolve as long as no required fields are missing. [Read more about how to use the `schema` option on the "Schema Awareness" page.](../graphcache/schema-awareness.md) ### `storage` option The `storage` option is an interface of methods that are used by the `offlineExchange` to persist the cache's data to persisted storage on the user's device. it > **NOTE:** Offline Support is currently experimental! It hasn't been extensively tested yet and > may not always behave as expected. Please try it out with caution! | Method | Type | Description | | ----------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `writeData` | `(delta: SerializedEntries) => Promise` | This provided method must be able to accept an object of key-value entries that will be persisted to the storage. This method is called as a batch of updated entries becomes ready. | | `readData` | `() => Promise` | This provided method must be able to return a single combined object of previous key-value entries that have been previously preserved using `writeData`. It's only called on startup. | | `writeMetadata` | `(json: SerializedRequest[]) => void` | This provided method must be able to persist metadata for the cache. For backwards compatibility it should be able to accept any JSON data. | | `readMetadata` | `() => Promise` | This provided method must be able to read the persisted metadata that has previously been written using `writeMetadata`. It's only called on startup. | | `onOnline` | `(cb: () => void) => void` | This method must be able to accept a callback that is called when the user's device comes back online. | | `onCacheHydrated` | `() => void` | This method will be called when the `cacheExchange` has finished hydrating the data coming from storage. | These options are split into three parts: - The `writeMetadata` and `readMetadata` methods are used to persist in-progress optimistic mutations to a storage so that they may be retried if the app has been closed while some optimistic mutations were still in progress. - The `writeData` and `readData` methods are used to persist any cache data. This is the normalized data that _Graphcache_ usually keeps in memory. The `cacheExchange` will frequently call `writeData` with a partial object of its cache data, which `readData` must then be able to return in a single combined object on startup. We call the partial objects that `writeData` is called with "deltas". - The `onOnline` method is only used to receive a trigger that determines whether the user's device has come back online, which is used to retry optimistic mutations that have previously failed due to being offline. The `storage` option may also be used with the `cacheExchange` instead of the `offlineExchange`, but will then only use `readData` and `writeData` to persist its cache data. This is not full offline support, but will rather be "persistence support". [Read more about how to use the `storage` option on the "Offline Support" page.](../graphcache/offline.md) ## Cache An instance of the `Cache` interface is passed to every resolvers and updater function. It may be used to read cached data or write cached data, which may be used in combination with the [`cacheExchange` configuration](#cacheexchange) to alter the default behaviour of _Graphcache_. ### keyOfEntity The `cache.keyOfEntity` method may be called with a partial `Data` object and will return the key for that object, or `null` if it's not keyable. An object may not be keyable if it's missing the `__typename` or `id` (which falls back to `_id`) fields. This method does take the [`keys` configuration](#keys-option) into account. ```js cache.keyOfEntity({ __typename: 'Todo', id: 1 }); // 'Todo:1' cache.keyOfEntity({ __typename: 'Query' }); // 'Query' cache.keyOfEntity({ __typename: 'Unknown' }); // null ``` There's an alternative method, `cache.keyOfField` which generates a key for a given field. This is only rarely needed but similar to `cache.keyOfEntity`. This method accepts a field name and optionally a field's arguments. ```js cache.keyOfField('todo'); // 'todo' cache.keyOfField('todo', { id: 1 }); // 'todo({"id":1})' ``` Internally, these are the keys that records and links are stored on per entity. ### resolve This method retrieves a value or link for a given field, given a partially keyable `Data` object or entity, a field name, and optionally the field's arguments. Internally this method accesses the cache by using `cache.keyOfEntity` and `cache.keyOfField`. ```js // This may resolve a link: cache.resolve({ __typename: 'Query' }, 'todo', { id: 1 }); // 'Todo:1' // This may also resolve records / scalar values: cache.resolve({ __typename: 'Todo', id: 1 }, 'id'); // 1 // You can also chain multiple calls to `cache.resolve`! cache.resolve(cache.resolve({ __typename: 'Query' }, 'todo', { id: 1 }), 'id'); // 1 ``` As you can see in the last example of this code snippet, the `Data` object can also be replaced by an entity key, which makes it possible to pass a key from `cache.keyOfEntity` or another call to `cache.resolve` instead of the partial entity. > **Note:** Because `cache.resolve` may return either a scalar value or another entity key, it may > be dangerous to use in some cases. It's a good idea to make sure first whether the field you're > reading will be a key or a value. The `cache.resolve` method may also be called with a field key as generated by `cache.keyOfField`. ```js cache.resolve({ __typename: 'Query' }, cache.keyOfField('todo', { id: 1 })); // 'Todo:1' ``` This specialized case is likely only going to be useful in combination with [`cache.inspectFields`](#inspectfields). ### inspectFields The `cache.inspectFields` method may be used to interrogate the cache about all available fields on a specific entity. It accepts a partial entity or an entity key, like [`cache.resolve`](#resolve)'s first argument. When calling the method this returns an array of `FieldInfo` objects, one per field (including differing arguments) that is known to the cache. The `FieldInfo` interface has three properties: `fieldKey`, `fieldName`, and `arguments`: | Argument | Type | Description | | ----------- | ---------------- | ------------------------------------------------------------------------------- | | `fieldName` | `string` | The field's name (without any arguments, just the name) | | `arguments` | `object \| null` | The field's arguments, or `null` if the field doesn't have any arguments | | `fieldKey` | `string` | The field's cache key, which is similar to what `cache.keyOfField` would return | This works on any given entity. When calling this method the cache works in reverse on its data structure, by parsing the entity's individual field keys. p ```js cache.inspectFields({ __typename: 'Query' }); /* [ { fieldName: 'todo', arguments: { id: 1 }, fieldKey: 'id({"id":1})' }, { fieldName: 'todo', arguments: { id: 2 }, fieldKey: 'id({"id":2})' }, ... ] */ ``` ### readFragment `cache.readFragment` accepts a GraphQL `DocumentNode` as the first argument and a partial entity or an entity key as the second, like [`cache.resolve`](#resolve)'s first argument. The method will then attempt to read the entity according to the fragment entirely from the cached data. If any data is uncached and missing it'll return `null`. ```js import { gql } from '@urql/core'; cache.readFragment( gql` fragment _ on Todo { id text } `, { id: 1 } ); // Data or null ``` Note that the `__typename` may be left out on the partial entity if the fragment isn't on an interface or union type, since in that case the `__typename` is already present on the fragment itself. If any fields on the fragment require variables, you can pass them as the third argument like so: ```js import { gql } from '@urql/core'; cache.readFragment( gql` fragment _ on User { id permissions(byGroupId: $groupId) } `, { id: 1 }, // this identifies the fragment (User) entity { groupId: 5 } // any additional field variables ); ``` If you need a specific fragment in a document containing multiple you can leverage the fourth argument like this: ```js import { gql } from '@urql/core'; cache.readFragment( gql` fragment todoFields on Todo { id } fragment userFields on User { id } `, { id: 1 }, // this identifies the fragment (User) entity undefined, 'userFields' // if not passed we take the first fragment, in this case todoFields ); ``` [Read more about using `readFragment` on the ["Local Resolvers" page.](../graphcache/local-resolvers.md#reading-a-fragment) ### readQuery The `cache.readQuery` method is similar to `cache.readFragment`, but instead of reading a fragment from cache, it reads an entire query. The only difference between how these two methods are used is `cache.readQuery`'s input, which is an object instead of two arguments. The method accepts a `{ query, variables }` object as the first argument, where `query` may either be a `DocumentNode` or a `string` and variables may optionally be an object. ```js cache.readQuery({ query: ` query ($id: ID!) { todo(id: $id) { id, text } } `, variables: { id: 1 } ); // Data or null ``` [Read more about using `readQuery` on the ["Local Resolvers" page.](../graphcache/local-resolvers.md#reading-a-query) ### link Corresponding to [`cache.resolve`](#resolve), the `cache.link` method allows links in the cache to be updated. While the `cache.resolve` method reads both records and links from the cache, the `cache.link` method will only ever write links as fragments (See [`cache.writeFragment`](#writefragment) below) are more suitable for updating scalar data in the cache. The arguments for `cache.link` are identical to [`cache.resolve`](#resolve) and the field's arguments are optional. However, the last argument must always be a link, meaning `null`, an entity key, a keyable entity, or a list of these. In other words, `cache.link` accepts an entity to write to as its first argument, with the same arguments as `cache.keyOfEntity`. It then accepts one or two arguments that are passed to `cache.keyOfField` to get the targeted field key. And lastly, you may pass a list or a single entity (or an entity key). ```js // Link Query.todo field to a todo item cache.link({ __typename: 'Query' }, 'todo', { __typename: 'Todo', id: 1 }); // You may also pass arguments instead: cache.link({ __typename: 'Query' }, 'todo', { id: 1 }, { __typename: 'Todo', id: 1 }); // Or use entity keys instead of the entities themselves: cache.link('Query', 'todo', cache.keyOfEntity({ __typename: 'Todo', id: 1 })); ``` The method may [output a warning](../graphcache/errors.md#12-cant-generate-a-key-for-writefragment-or-link) when any of the entities were passed as objects but aren't keyable, which is useful when a scalar or a non-keyable object have been passed to `cache.link` accidentally. ### writeFragment Corresponding to [`cache.readFragment`](#readfragments), the `cache.writeFragment` method allows data in the cache to be updated. The arguments for `cache.writeFragment` are identical to [`cache.readFragment`](#readfragment), however the second argument, `data`, should not only contain properties that are necessary to derive an entity key from the given data, but also the fields that will be written: ```js import { gql } from '@urql/core'; cache.writeFragment( gql` fragment _ on Todo { text } `, { id: 1, text: 'New Todo Text' } ); ``` In the example we can see that the `writeFragment` method returns `undefined`. Furthermore we pass `id` in our `data` object so that an entity key can be written, but the fragment itself doesn't have to include these fields. If you need a specific fragment in a document containing multiple you can leverage the fourth argument like this: ```js import { gql } from '@urql/core'; cache.writeFragment( gql` fragment todoFields on Todo { id text } fragment userFields on User { id name } `, { id: 1, name: 'New Name' } undefined, 'userFields' // if not passed we take the first fragment, in this case todoFields ); ``` [Read more about using `writeFragment` on the ["Custom Updates" page.](../graphcache/cache-updates.md#cachewritefragment) ### updateQuery Similarly to [`cache.writeFragment`](#writefragment), there's an analogous method for [`cache.readQuery`](#readquery) that may be used to update query data. The `cache.updateQuery` method accepts the same `{ query, variables }` object input as its first argument, which is the query we'd like to write to the cache. As a second argument the method accepts an updater function. This function will be called with the query data that is already in the cache (which may be `null` if the data is uncached) and must return the new data that should be written to the cache. ```js const TodoQuery = ` query ($id: ID!) { todo(id: $id) { id, text } } `; cache.updateQuery({ query: TodoQuery, variables: { id: 1 } }, data => { if (!data) return null; data.todo.text = 'New Todo Text'; return data; }); ``` As we can see, our updater may return `null` to cancel updating any data, which we do in case the query data is uncached. We can also see that data can simply be mutated and doesn't have to be altered immutably. This is because all data from the cache is already a deep copy and hence we can do to it whatever we want. [Read more about using `updateQuery` on the "Custom Updates" page.](../graphcache/cache-updates.md#cacheupdatequery) ### invalidate The `cache.invalidate` method can be used to delete (i.e. "evict") an entity from the cache entirely. This will cause it to disappear from all queries in _Graphcache_. Its arguments are identical to [`cache.resolve`](#resolve). Since deleting an entity will lead to some queries containing missing and uncached data, calling `invalidate` may lead to additional GraphQL requests being sent, unless you're using [_Graphcache_'s "Schema Awareness" feature](../graphcache/schema-awareness.md), which takes optional fields into account. This method accepts a partial entity or an entity key as its first argument, similar to [`cache.resolve`](#resolve)'s first argument. ```js cache.invalidate({ __typename: 'Todo', id: 1 }); // Invalidates Todo:1 ``` Additionally `cache.invalidate` may be used to delete specific fields only, which can be useful when for instance a list is supposed to be evicted from cache, where a full invalidation may be impossible. This is often the case when a field on the root `Query` needs to be deleted. This method therefore accepts two additional arguments, similar to [`cache.resolve`](#resolve). ```js // Invalidates `Query.todos` with the `first: 10` argument: cache.invalidate('Query', 'todos', { first: 10 }); ``` ## Info This is a metadata object that is passed to every resolver and updater function. It contains basic information about the current GraphQL document and query, and also some information on the current field that a given resolver or updater is called on. | Argument | Type | Description | | ---------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `parent` | `Data` | The field's parent entity's data, as it was written or read up until now, which means it may be incomplete. [Use `cache.resolve`](#resolve) to read from it. | | `parentTypeName` | `string` | The field's parent entity's typename | | `parentKey` | `string` | The field's parent entity's cache key (if any) | | `parentFieldKey` | `string` | The current key's cache key, which is the parent entity's key combined with the current field's key (This is mostly obsolete) | | `fieldName` | `string` | The current field's name | | `fragments` | `{ [name: string]: FragmentDefinitionNode }` | A dictionary of fragments from the current GraphQL document | | `variables` | `object` | The current GraphQL operation's variables (may be an empty object) | | `error` | `GraphQLError \| undefined` | The current GraphQLError for a given field. This will always be `undefined` for resolvers and optimistic updaters, but may be present for updaters when the API has returned an error for a given field. | | `partial` | `?boolean` | This may be set to `true` at any point in time (by your custom resolver or by _Graphcache_) to indicate that some data is uncached and missing | | `optimistic` | `?boolean` | This is only `true` when an optimistic mutation update is running | > **Note:** Using `info` is regarded as a last resort. Please only use information from it if > there's no other solution to get to the metadata you need. We don't regard the `Info` API as > stable and may change it with a simple minor version bump. ## The `/extras` import The `extras` subpackage is published with _Graphcache_ and contains helpers and utilities that don't have to be included in every app or aren't needed by all users of _Graphcache_. All utilities from extras may be imported from `@urql/exchange-graphcache/extras`. Currently the `extras` subpackage only contains the [pagination resolvers that have been mentioned on the "Computed Queries" page.](../graphcache/local-resolvers.md#pagination) ### simplePagination Accepts a single object of optional options and returns a resolver that can be inserted into the [`cacheExchange`'s](#cacheexchange) [`resolvers` configuration.](#resolvers-option) | Argument | Type | Description | | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `offsetArgument` | `?string` | The field arguments' property, as passed to the resolver, that contains the current offset, i.e. the number of items to be skipped. Defaults to `'skip'`. | | `limitArgument` | `?string` | The field arguments' property, as passed to the resolver, that contains the current page size limit, i.e. the number of items on each page. Defaults to `'limit'`. | | `mergeMode` | `'after' \| 'before'` | This option defines whether pages are merged before or after preceding ones when paginating. Defaults to `'after'`. | Once set up, the resulting resolver is able to automatically concatenate all pages of a given field automatically. Queries to this resolvers will from then on only return the infinite, combined list of all pages. [Read more about `simplePagination` on the "Computed Queries" page.](../graphcache/local-resolvers.md#simple-pagination) ### relayPagination Accepts a single object of optional options and returns a resolver that can be inserted into the [`cacheExchange`'s](#cacheexchange) [`resolvers` configuration.](#resolvers-option) | Argument | Type | Description | | ----------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mergeMode` | `'outwards' \| 'inwards'` | With Relay pagination, pages can be queried forwards and backwards using `after` and `before` cursors. This option defines whether pages that have been queried backwards should be concatenated before (outwards) or after (inwards) all pages that have been queried forwards. | Once set up, the resulting resolver is able to automatically concatenate all pages of a given field automatically. Queries to this resolvers will from then on only return the infinite, combined list of all pages. [Read more about `relayPagnation` on the "Computed Queries" page.](../graphcache/local-resolvers.md#relay-pagination) ## The `/default-storage` import The `default-storage` subpackage is published with _Graphcache_ and contains a default storage interface that may be used with the [`storage` option.](#storage-option) It contains the `makeDefaultStorage` export which is a factory function that accepts a few options and returns a full [storage interface](#storage-option). This storage by default persists to [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). | Argument | Type | Description | | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `idbName` | `string` | The name of the IndexedDB database that is used and created if needed. By default this is set to `"graphcache-v3"` | | `maxAge` | `number` | The maximum age of entries that the storage should use in whole days. By default the storage will discard entries that are older than seven days. | ================================================ FILE: docs/api/preact.md ================================================ --- title: '@urql/preact' order: 2 --- # @urql/preact > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. The `@urql/preact` API is the same as the React `urql` API. Please refer to [the "urql" API docs](./urql.md) for details on the Preact API. ================================================ FILE: docs/api/refocus-exchange.md ================================================ --- title: '@urql/exchange-refocus' order: 11 --- # Refocus Exchange > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. `@urql/exchange-refocus` is an exchange for the `urql` that tracks currently active operations and redispatches them when the window regains focus ## Quick Start Guide First install `@urql/exchange-refocus` alongside `urql`: ```sh yarn add @urql/exchange-refocus # or npm install --save @urql/exchange-refocus ``` Then add it to your `Client`, preferably in front of your `cacheExchange` ```js import { createClient, cacheExchange, fetchExchange } from 'urql'; import { refocusExchange } from '@urql/exchange-refocus'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [refocusExchange(), cacheExchange, fetchExchange], }); ``` ================================================ FILE: docs/api/request-policy-exchange.md ================================================ --- title: '@urql/exchange-request-policy' order: 9 --- # Request Policy Exchange > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. The `@urql/exchange-request-policy` package contains an addon `requestPolicyExchange` for `urql` that may be used to upgrade [Operations' Request Policies](./core.md#requestpolicy) on a time-to-live basis. [Read more about request policies on the "Document Caching" page.](../basics/document-caching.md#request-policies) This exchange will conditionally upgrade `cache-first` and `cache-only` operations to use `cache-and-network`, so that the client gets an opportunity to update its cached data, when the operation hasn't been seen within the given `ttl` time. This is often preferable to setting the default policy to `cache-and-network` to avoid an unnecessarily high amount of requests to be sent to the API when switching pages. ## Installation and Setup First install `@urql/exchange-request-policy` alongside `urql`: ```sh yarn add @urql/exchange-request-policy # or npm install --save @urql/exchange-request-policy ``` Then add it to your `Client`, preferably in front of the `cacheExchange` and in front of any asynchronous exchanges, like the `fetchExchange`: ```js import { createClient, cacheExchange, fetchExchange } from 'urql'; import { requestPolicyExchange } from '@urql/exchange-request-policy'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [ requestPolicyExchange({ /* config */ }), cacheExchange, fetchExchange, ], }); ``` ## Options | Option | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ttl` | The "time-to-live" until an `Operation` will be upgraded to the `cache-and-network` policy in milliseconds. By default 5 minutes is set. | | `shouldUpgrade` | An optional function that receives an `Operation` as the only argument and may return `true` or `false` depending on whether an operation should be upgraded. This can be used to filter out operations that should never be upgraded to `cache-and-network`. | ================================================ FILE: docs/api/retry-exchange.md ================================================ --- title: '@urql/exchange-retry' order: 5 --- # Retry Exchange > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. The `@urql/exchange-retry` package contains an addon `retryExchange` for `urql` that may be used to let failed operations be retried, typically when a previous operation has failed with a network error. [Read more about how to use and configure the `retryExchange` on the "Retry Operations" page.](../advanced/retry-operations.md) ## Options | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `initialDelayMs` | Specify at what interval the `retrying` should start, this means that if we specify `1000` that when our `operation` fails we'll wait 1 second and then retry it. | | `maxDelayMs` | The maximum delay between retries. The `retryExchange` will keep increasing the time between retries so that the server doesn't receive simultaneous requests it can't complete. This time between requests will increase with a random `back-off` factor applied to the `initialDelayMs`, read more about the [thundering herd problem](https://en.wikipedia.org/wiki/Thundering_herd_problem). | | `randomDelay` | Allows the randomized delay described above to be disabled. When this option is set to `false` there will be exactly a `initialDelayMs` wait between each retry. | | `maxNumberAttempts` | Defines the maximum number of attempts (including the initial request). For example, `2` means one retry after the initial attempt. | | `retryIf` | Apply a custom test to the returned error to determine whether it should be retried. | | `retryWith` | Apply a transform function allowing you to selectively replace a retried `Operation` or return a nullish value. This will act like `retryIf` where a truthy value retries (`retryIf` takes precedence and overrides this function.) | ================================================ FILE: docs/api/svelte.md ================================================ --- title: '@urql/svelte' order: 3 --- # Svelte API > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. ## queryStore The `queryStore` factory accepts properties as inputs and returns a Svelte pausable, readable store of results, with type `OperationResultStore & Pausable`. | Argument | Type | Description | | --------------- | -------------------------- | -------------------------------------------------------------------------------------------------------- | | `client` | `Client` | The [`Client`](./core.md#Client) to use for the operation. | | `query` | `string \| DocumentNode \` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. | | `variables` | `?object` | The variables to be used with the GraphQL request. | | `requestPolicy` | `?RequestPolicy` | An optional [request policy](./core.md#requestpolicy) that should be used specifying the cache strategy. | | `pause` | `?boolean` | A boolean flag instructing [execution to be paused](../basics/vue.md#pausing-usequery). | | `context` | `?object` | Holds the contextual information for the query. | This store is pausable, which means that the result has methods on it to `pause()` or `resume()` the subscription of the operation. [Read more about how to use the `queryStore` API on the "Queries" page.](../basics/svelte.md#queries) ## mutationStore The `mutationStore` factory accepts properties as inputs and returns a Svelte readable store of a result. | Argument | Type | Description | | ----------- | -------------------------- | ---------------------------------------------------------------------------------- | | `client` | `Client` | The [`Client`](./core.md#Client) to use for the operation. | | `query` | `string \| DocumentNode \` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. | | `variables` | `?object` | The variables to be used with the GraphQL request. | | `context` | `?object` | Holds the contextual information for the query. | [Read more about how to use the `mutation` API on the "Mutations" page.](../basics/svelte.md#mutations) ## subscriptionStore The `subscriptionStore` utility function accepts the same inputs as `queryStore` does as its first argument, [see above](#querystore). The function also optionally accepts a second argument, a `handler` function. This function has the following type signature: ```js type SubscriptionHandler = (previousData: R | undefined, data: T) => R; ``` This function will be called with the previous data (or `undefined`) and the new data that's incoming from a subscription event, and may be used to "reduce" the data over time, altering the value of `result.data`. [Read more about how to use the `subscription` API on the "Subscriptions" page.](../advanced/subscriptions.md#svelte) ## OperationResultStore A Svelte Readble store of an [`OperationResult`](./core.md#operationresult). This store will be updated as the incoming data changes. | Prop | Type | Description | | ------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `data` | `?any` | Data returned by the specified query | | `error` | `?CombinedError` | A [`CombinedError`](./core.md#combinederror) instances that wraps network or `GraphQLError`s (if any) | | `extensions` | `?Record` | Extensions that the GraphQL server may have returned. | | `stale` | `boolean` | A flag that may be set to `true` by exchanges to indicate that the `data` is incomplete or out-of-date, and that the result will be updated soon. | | `fetching` | `boolean` | A flag that indicates whether the operation is currently in progress, which means that the `data` and `error` is out-of-date for the given inputs. | ## Pausable The `queryStore` and `subscriptionStore`'s stores are pausable. This means they inherit the following properties from the `Pausable` store. | Prop | Type | Description | | ----------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `isPaused$` | `Readable` | A Svelte readable store indicating whether the operation is currently paused. Essentially, this is equivalent to `!fetching` | | `pause()` | `pause(): void` | This method pauses the ongoing operation. | | `resume()` | `resume(): void` | This method resumes the previously paused operation. | ## Context API In `urql`'s Svelte bindings, the [`Client`](./core.md#client) is passed into the factories for stores above manually. This is to cater to greater flexibility. However, for convenience's sake, instead of keeping a `Client` singleton, we may also use [Svelte's Context API](https://svelte.dev/tutorial/context-api). `@urql/svelte` provides wrapper functions around Svelte's [`setContext`](https://svelte.dev/docs#run-time-svelte-setcontext) and [`getContext`](https://svelte.dev/docs#run-time-svelte-getcontext) functions: - `setContextClient` - `getContextClient` - `initContextClient` (a shortcut for `createClient` + `setContextClient`) ================================================ FILE: docs/api/urql.md ================================================ --- title: urql (React) order: 1 --- # React API > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. ## useQuery Accepts a single required options object as an input with the following properties: | Prop | Type | Description | | --------------- | ------------------------ | -------------------------------------------------------------------------------------------------------- | | `query` | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. | | `variables` | `?object` | The variables to be used with the GraphQL request. | | `requestPolicy` | `?RequestPolicy` | An optional [request policy](./core.md#requestpolicy) that should be used specifying the cache strategy. | | `pause` | `?boolean` | A boolean flag instructing [execution to be paused](../basics/react-preact.md#pausing-usequery). | | `context` | `?object` | Holds the contextual information for the query. | This hook returns a tuple of the shape `[result, executeQuery]`. - The `result` is an object with the shape of an [`OperationResult`](./core.md#operationresult) with an added `fetching: boolean` property, indicating whether the query is being fetched. - The `executeQuery` function optionally accepts [`Partial`](./core.md#operationcontext) and reexecutes the current query when it's called. When `pause` is set to `true` this executes the query, overriding the otherwise paused hook. [Read more about how to use the `useQuery` API on the "Queries" page.](../basics/react-preact.md#queries) ## useMutation Accepts a single `query` argument of type `string | DocumentNode` and returns a tuple of the shape `[result, executeMutation]`. - The `result` is an object with the shape of an [`OperationResult`](./core.md#operationresult) with an added `fetching: boolean` property, indicating whether the mutation is being executed. - The `executeMutation` function accepts variables and optionally [`Partial`](./core.md#operationcontext) and may be used to start executing a mutation. It returns a `Promise` resolving to an [`OperationResult`](./core.md#operationresult). [Read more about how to use the `useMutation` API on the "Mutations" page.](../basics/react-preact.md#mutations) ## useSubscription Accepts a single required options object as an input with the following properties: | Prop | Type | Description | | ----------- | ------------------------ | ------------------------------------------------------------------------------------------------ | | `query` | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. | | `variables` | `?object` | The variables to be used with the GraphQL request. | | `pause` | `?boolean` | A boolean flag instructing [execution to be paused](../basics/react-preact.md#pausing-usequery). | | `context` | `?object` | Holds the contextual information for the query. | The hook optionally accepts a second argument, which may be a handler function with a type signature of: ```js type SubscriptionHandler = (previousData: R | undefined, data: T) => R; ``` This function will be called with the previous data (or `undefined`) and the new data that's incoming from a subscription event, and may be used to "reduce" the data over time, altering the value of `result.data`. This hook returns a tuple of the shape `[result, executeSubscription]`. - The `result` is an object with the shape of an [`OperationResult`](./core.md#operationresult). - The `executeSubscription` function optionally accepts [`Partial`](./core.md#operationcontext) and restarts the current subscription when it's called. When `pause` is set to `true` this starts the subscription, overriding the otherwise paused hook. The `fetching: boolean` property on the `result` may change to `false` when the server proactively ends the subscription. By default, `urql` is unable able to start subscriptions, since this requires some additional setup. [Read more about how to use the `useSubscription` API on the "Subscriptions" page.](../advanced/subscriptions.md) ## Query Component This component is a wrapper around [`useQuery`](#usequery), exposing a [render prop API](https://reactjs.org/docs/render-props.html) for cases where hooks aren't desirable. The API of the `Query` component mirrors the API of [`useQuery`](#usequery). The props that `` accepts are the same as `useQuery`'s options object. A function callback must be passed to `children` that receives the query result and must return a React element. The second argument of the hook's tuple, `executeQuery` is passed as an added property on the query result. ## Mutation Component This component is a wrapper around [`useMutation`](#usemutation), exposing a [render prop API](https://reactjs.org/docs/render-props.html) for cases where hooks aren't desirable. The `Mutation` component accepts a `query` prop, and a function callback must be passed to `children` that receives the mutation result and must return a React element. The second argument of `useMutation`'s returned tuple, `executeMutation` is passed as an added property on the mutation result object. ## Subscription Component This component is a wrapper around [`useSubscription`](#usesubscription), exposing a [render prop API](https://reactjs.org/docs/render-props.html) for cases where hooks aren't desirable. The API of the `Subscription` component mirrors the API of [`useSubscription`](#usesubscription). The props that `` accepts are the same as `useSubscription`'s options object, with an added, optional `handler` prop that may be passed, which for the `useSubscription` hook is instead the second argument. A function callback must be passed to `children` that receives the subscription result and must return a React element. The second argument of the hook's tuple, `executeSubscription` is passed as an added property on the subscription result. ## Context `urql` is used in React by adding a provider around where the [`Client`](./core.md#client) is supposed to be used. Internally this means that `urql` creates a [React Context](https://reactjs.org/docs/context.html). All created parts of this context are exported by `urql`, namely: - `Context` - `Provider` - `Consumer` To keep examples brief, `urql` creates a default client with the `url` set to `'/graphql'`. This client will be used when no `Provider` wraps any of `urql`'s hooks. However, to prevent this default client from being used accidentally, a warning is output in the console for the default client. ### useClient `urql` also exports a `useClient` hook, which is a convenience wrapper like the following: ```js import React from 'react'; import { Context } from 'urql'; const useClient = () => React.useContext(Context); ``` However, this hook is also responsible for outputting the default client warning that's mentioned above, and should thus be preferred over manually using `useContext` with `urql`'s `Context`. ================================================ FILE: docs/api/vue.md ================================================ --- title: '@urql/vue' order: 3 --- # Vue API > **Note:** These API docs are deprecated as we now keep TSDocs in all published packages. > You can view TSDocs while using these packages in your editor, as long as it supports the > TypeScript Language Server. > We're planning to replace these API docs with a separate web app soon. ## useQuery Accepts a single required options object as an input with the following properties: | Prop | Type | Description | | --------------- | ------------------------ | -------------------------------------------------------------------------------------------------------- | | `query` | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. | | `variables` | `?object` | The variables to be used with the GraphQL request. | | `requestPolicy` | `?RequestPolicy` | An optional [request policy](./core.md#requestpolicy) that should be used specifying the cache strategy. | | `pause` | `?boolean` | A boolean flag instructing [execution to be paused](../basics/vue.md#pausing-usequery). | | `context` | `?object` | Holds the contextual information for the query. | Each of these inputs may also be [reactive](https://v3.vuejs.org/api/refs-api.html) (e.g. a `ref`) and are allowed to change over time which will issue a new query. This function returns an object with the shape of an [`OperationResult`](./core.md#operationresult) with an added `fetching` property, indicating whether the query is currently being fetched and an `isPaused` property which will indicate whether `useQuery` is currently paused and won't automatically start querying. All of the properties on this result object are also marked as [reactive](https://v3.vuejs.org/api/refs-api.html) using `ref` and will update accordingly as the query is executed. The result furthermore carries several utility methods: | Method | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pause()` | This will pause automatic querying, which is equivalent to setting `pause.value = true` | | `resume()` | This will resume a paused automatic querying, which is equivalent to setting `pause.value = false` | | `executeQuery(opts)` | This will execute a new query with the given partial [`Partial`](./core.md#operationcontext) regardless of whether the query is currently paused or not. This also returns the result object again for chaining. | Furthermore the returned result object of `useQuery` is also a `PromiseLike`, which allows you to take advantage of [Vue 3's experimental Suspense feature.](https://vuedose.tips/go-async-in-vue-3-with-suspense/) When the promise is used, e.g. you `await useQuery(...)` then the `PromiseLike` will only resolve once a result from the API is available. [Read more about how to use the `useQuery` API on the "Queries" page.](../basics/vue.md#queries) ## useMutation Accepts a single `query` argument of type `string | DocumentNode` and returns a result object with the shape of an [`OperationResult`](./core.md#operationresult) with an added `fetching` property. All of the properties on this result object are also marked as [reactive](https://v3.vuejs.org/api/refs-api.html) using `ref` and will update accordingly as the mutation is executed. The object also carries a special `executeMutation` method, which accepts variables and optionally a [`Partial`](./core.md#operationcontext) and may be used to start executing a mutation. It returns a `Promise` resolving to an [`OperationResult`](./core.md#operationresult) [Read more about how to use the `useMutation` API on the "Mutations" page.](../basics/vue.md#mutations) ## useSubscription Accepts a single required options object as an input with the following properties: | Prop | Type | Description | | ----------- | ------------------------ | --------------------------------------------------------------------------------------- | | `query` | `string \| DocumentNode` | The query to be executed. Accepts as a plain string query or GraphQL DocumentNode. | | `variables` | `?object` | The variables to be used with the GraphQL request. | | `pause` | `?boolean` | A boolean flag instructing [execution to be paused](../basics/vue.md#pausing-usequery). | | `context` | `?object` | Holds the contextual information for the subscription. | Each of these inputs may also be [reactive](https://v3.vuejs.org/api/refs-api.html) (e.g. a `ref`) and are allowed to change over time which will issue a new query. `useSubscription` also optionally accepts a second argument, which may be a handler function with a type signature of: ```js type SubscriptionHandler = (previousData: R | undefined, data: T) => R; ``` This function will be called with the previous data (or `undefined`) and the new data that's incoming from a subscription event, and may be used to "reduce" the data over time, altering the value of `result.data`. This function returns an object with the shape of an [`OperationResult`](./core.md#operationresult) with an added `fetching` property, indicating whether the subscription is currently running and an `isPaused` property which will indicate whether `useSubscription` is currently paused. All of the properties on this result object are also marked as [reactive](https://v3.vuejs.org/api/refs-api.html) using `ref` and will update accordingly as the query is executed. The result furthermore carries several utility methods: | Method | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pause()` | This will pause the subscription, which is equivalent to setting `pause.value = true` | | `resume()` | This will resume the subscription, which is equivalent to setting `pause.value = false` | | `executeSubscription(opts)` | This will start a new subscription with the given partial [`Partial`](./core.md#operationcontext) regardless of whether the subscription is currently paused or not. This also returns the result object again for chaining. | [Read more about how to use the `useSubscription` API on the "Subscriptions" page.](../advanced/subscriptions.md#vue) ## useClientHandle The `useClientHandle()` function may, like the other `use*` functions, be called either in `setup()` or another lifecycle hook, and returns a so called "client handle". Using this `handle` we can access the [`Client`](./core.md#client) directly via the `client` property or call the other `use*` functions as methods, which will be directly bound to this `client`. This may be useful when chaining these methods inside an `async setup()` lifecycle function. | Method | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `client` | Contains the raw [`Client`](./core.md#client) reference, which allows the `Client` to be used directly. | | `useQuery(...)` | Accepts the same arguments as the `useQuery` function, but will always use the `Client` from the handle's context. | | `useMutation(...)` | Accepts the same arguments as the `useMutation` function, but will always use the `Client` from the handle's context. | | `useSubscription(...)` | Accepts the same arguments as the `useSubscription` function, but will always use the `Client` from the handle's context. | ## Context API In Vue the [`Client`](./core.md#client) is provided either to your app or to a parent component of a given subtree and is then subsequently injected whenever one of the above composition functions is used. You can provide the `Client` from any of your components using the `provideClient` function. Alternatively, `@urql/vue` also has a default export of a [Vue Plugin function](https://v3.vuejs.org/guide/plugins.html#using-a-plugin). Both `provideClient` and the plugin function either accept an [instance of `Client`](./core.md#client) or the same options that `createClient` accepts as inputs. ================================================ FILE: docs/architecture.md ================================================ --- title: Architecture order: 3 --- # Architecture `urql` is a highly customizable and flexible GraphQL client. As you use it in your app, it's split into three parts: - Bindings — such as for React, Preact, Vue, or Svelte — which interact with `@urql/core`'s `Client`. - The Client — as created [with the core `@urql/core` package](./basics/core.md), which interacts with "exchanges" to execute GraphQL operations, and which you can also use directly. - Exchanges, which provide functionality like fetching or caching to the `Client`. By default, `urql` aims to provide the minimal amount of features that allow us to build an app quickly. However, `urql` has also been designed to be a GraphQL Client that grows with our usage and demands. As we go from building our smallest or first GraphQL apps to utilising its full functionality, we have tools at our disposal to extend and customize `urql` to our liking. ## Using GraphQL Clients You may have worked with a GraphQL API previously and noticed that using GraphQL in your app can be as straightforward as sending a plain HTTP request with your query to fetch some data. GraphQL also provides an opportunity to abstract away a lot of the manual work that goes with sending these queries and managing the data. Ultimately, this lets you focus on building your app without having to handle the technical details of state management in detail. Specifically, `urql` simplifies three common aspects of using GraphQL: - Sending queries and mutations and receiving results _declaratively_ - Abstracting _caching_ and state management internally - Providing a central point of _extensibility_ and integration with your API In the following sections we'll talk about the way that `urql` solves these three problems and how the logic is abstracted away internally. ## Requests and Operations on the Client If `urql` was a train it would take several stops to arrive at its terminus, our API. It starts with us defining queries or mutations by writing in GraphQL's query language. Any GraphQL request can be abstracted into its query documents and its variables. ```js import { gql } from '@urql/core'; const query = gql` query ($name: String!) { helloWorld(name: $name) } `; const request = createRequest(query, { name: 'Urkel', }); ``` In `urql`, these GraphQL requests are treated as unique objects and each GraphQL request will have a `key` generated for them. This `key` is a hash of the query document and the variables you provide and are set on the `key` property of a [`GraphQLRequest`](./api/core.md#graphqlrequest). Whenever we decide to send our GraphQL requests to a GraphQL API we start by using `urql`'s [`Client`](./api/core.md#client). The `Client` accepts several options to configure its behaviour and the behaviour of exchanges, like the `fetchExchange`. For instance, we can pass it a `url` which the `fetchExchange` will use to make a `fetch` call to our GraphQL API. ```js import { Client, cacheExchange, fetchExchange } from '@urql/core'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` Above, we're defining a `Client` that is ready to accept our requests. It will apply basic document caching and will send uncached requests to the `url` we pass it. The bindings that we've seen in [the "Basics" section](./basics/README.md), like `useQuery` for React for example, interact with [the `Client`](./api/core.md#client) directly and are a thin abstraction. Some methods can be called on it directly however, as seen [on the "Core Usage" page](./basics/core.md#one-off-queries-and-mutations). ```js // Given our request and client defined above, we can call const subscription = client.executeQuery(request).subscribe(result => { console.log(result.data); }); ``` As we've seen, `urql` defines our query documents and variables as [`GraphQLRequest`s](./api/core.md#graphqlrequest). However, since we have more metadata that is needed, like our `url` option on the `Client`, `urql` internally creates [`Operation`s](./api/core.md#operation) each time a request is executed. The operations are then forwarded to the exchanges, like the `cacheExchange` and `fetchExchange`. An "Operation" is an extension of `GraphQLRequest`s. Not only do they carry the `query`, `variables`, and a `key` property, they will also identify the `kind` of operation that is executed, like `"query"` or `"mutation"`, and they contain the `Client`'s options on `operation.context`. ![Operations and Results](./assets/urql-event-hub.png) This means, once we hand over a GraphQL request to the `Client`, it will create an `Operation`, and then hand it over to the exchanges until a result comes back. As shown in the diagram, each operation is like an event or signal for a GraphQL request to start, and the exchanges will eventually send back a corresponding result. However, because the cache can send updates to us whenever it detects a change, or you could cancel a GraphQL request before it finishes, a special "teardown" `Operation` also exists, which cancels ongoing requests. ## The Client and Exchanges To reiterate, when we use `urql`'s bindings for our framework of choice, methods are called on the `Client`, but we never see the operations that are created in the background from our bindings. We call a method like `client.executeQuery` (or it's called for us in the bindings), an operation is issued internally when we subscribe with a callback, and later, we're given results. ![Operations stream and results stream](./assets/urql-client-architecture.png) While we know that, for us, we're only interested in a single [`Operation`](./api/core.md#operation) and its [`OperationResult`s](./api/core.md#operationresult) at a time, the `Client` treats these as one big stream. The `Client` sees an incoming flow of all of our operations. As we've learned before, each operation carries a `key` and each result we receive carries the original `operation`. Because an `OperationResult` also carries an `operation` property the `Client` will always know which results correspond to an individual operation. However, internally, all of our operations are processed at the same time concurrently. However, from our perspective: - We subscribe to a "stream" and expect to get results on a callback - The `Client` issues the operation, and we'll receive some results back eventually as either the cache responds (synchronously), or the request gets sent to our API. - We eventually unsubscribe, and the `Client` issues a "teardown" operation with the same `key` as the original operation, which concludes our flow. The `Client` itself doesn't actually know what to do with operations. Instead, it sends them through "exchanges". Exchanges are akin to [middleware in Redux](https://redux.js.org/advanced/middleware) and have access to all operations and all results. Multiple exchanges are chained to process our operations and to execute logic on them, one of them being the `fetchExchange`, which as the name implies sends our requests to our API. ### How operations get to exchanges We now know how we get to operations and to the `Client`: - Any bindings or calls to the `Client` create an **operation** - This operation identifies itself as either a `"query"`, `"mutation"` or `"subscription"` and has a unique `key`. - This operation is sent into the **exchanges** and eventually ends up at the `fetchExchange` (or a similar exchange) - The operation is sent to the API and a **result** comes back, which is wrapped in an `OperationResult` - The `Client` filters the `OperationResult` by the `operation.key` and — via a callback — gives us a **stream of results**. To come back to our train analogy from earlier, an operation, like a train, travels from one end of the track to the terminus — our API. The results then come back on the same path as they're just travelling the same line in reverse. ### The Exchanges By default, the `Client` doesn't do anything with GraphQL requests. It contains only the logic to manage and differentiate between active and inactive requests and converts them to operations. To actually do something with our GraphQL requests, it needs _exchanges_, which are like plugins that you can pass to create a pipeline of how GraphQL operations are executed. By default, you may want to add the `cacheExchange` and the `fetchExchange` from `@urql/core`: - `cacheExchange`: Caches GraphQL results with ["Document Caching"](./basics/document-caching.md) - `fetchExchange`: Executes GraphQL requests with a `fetch` HTTP call ```js import { Client, cacheExchange, fetchExchange } from '@urql/core'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` As we can tell, exchanges define not only how GraphQL requests are executed and handled, but also get control over caching. Exchanges can be used to change almost any behaviour in the `Client`, although internally they only handle incoming & outgoing requests and incoming & outgoing results. Some more exchanges that we can use with our `Client` are: - [`mapExchange`](./api/core.md#mapexchange): Allows changing and reacting to operations, results, and errors - [`ssrExchange`](./advanced/server-side-rendering.md): Allows for a server-side renderer to collect results for client-side rehydration. - [`retryExchange`](./advanced/retry-operations.md): Allows operations to be retried on errors - [`persistedExchange`](./advanced/persistence-and-uploads.md#automatic-persisted-queries): Provides support for Automatic Persisted Queries - [`authExchange`](./advanced/authentication.md): Allows refresh authentication to be implemented easily. - [`requestPolicyExchange`](./api/request-policy-exchange.md): Automatically refreshes results given a TTL. - `devtoolsExchange`: Provides the ability to use the [urql-devtools](https://github.com/urql-graphql/urql-devtools) We can even swap out our [document cache](./basics/document-caching.md), which is implemented by `@urql/core`'s `cacheExchange`, with `urql`'s [normalized cache, Graphcache](./graphcache/README.md). [Read more about exchanges and how to write them from scratch on the "Authoring Exchanges" page.](./advanced/authoring-exchanges.md) ## Stream Patterns in `urql` In the previous sections we've learned a lot about how the `Client` works, but we've always learned it in vague terms — for instance, we've learned that we get a "stream of results" or `urql` sees all operations as "one stream of operations" that it sends to the exchanges. But, **what are streams?** Generally we refer to _streams_ as abstractions that allow us to program with asynchronous events over time. Within the context of JavaScript we're specifically thinking in terms of [Observables](https://github.com/tc39/proposal-observable) and [Reactive Programming with Observables.](http://reactivex.io/documentation/observable.html) These concepts may sound intimidating, but from a high-level view what we're talking about can be thought of as a combination of promises and iterables (e.g. arrays). We're dealing with multiple events, but our callback is called over time. It's like calling `forEach` on an array but expecting the results to come in asynchronously. As a user, if we're using the one framework bindings that we've seen in [the "Basics" section](./basics/README.md), we may never see these streams in action or may never use them even, since the bindings internally use them for us. But if we [use the `Client` directly](./basics/core.md#one-off-queries-and-mutations) or write exchanges then we'll see streams and will have to deal with their API. ### Stream patterns with the client When we call methods on the `Client` like [`client.executeQuery`](./api/core.md#clientexecutequery) or [`client.query`](./api/core.md#clientquery) then these will return a "stream" of results. It's normal for GraphQL subscriptions to deliver multiple results, however, even GraphQL queries can give you multiple results in `urql`. This is because operations influence one another. When a cache invalidates a query, this query may refetch, and a new result is delivered to your application. Multiple results mean that once you subscribe to a GraphQL query via the `Client`, you may receive new results in the future. ```js import { gql } from '@urql/core'; const QUERY = gql` query Test($id: ID!) { getUser(id: $id) { id name } } `; client.query(QUERY, { id: 'test' }).subscribe(result => { console.log(result); // { data: ... } }); ``` Read more about the available APIs on the `Client` in the [Core API docs](./api/core.md). Internally, these streams and all exchanges are written using a library called [`wonka`](https://wonka.kitten.sh/basics/background), which is a tiny Observable-like library. It is used to write exchanges and when we interact with the `Client` it is used internally as well. ================================================ FILE: docs/basics/README.md ================================================ --- title: Basics order: 2 --- # Basics In this chapter we'll explain the basics of `urql` and how to get started with using it without any prior knowledge. - [**React/Preact**](./react-preact.md) covers how to work with the bindings for React/Preact. - [**Vue**](./vue.md) covers how to work with the bindings for Vue 3. - [**Svelte**](./svelte.md) covers how to work with the bindings for Svelte. - [**Core Package**](./core.md) defines why a shared package exists that contains the main logic of `urql`, and how we can use it directly in Node.js. After reading the page for your bindings and the "Core" page you may want to the next two pages in this section of the documentation: - [**Document Caching**](./document-caching.md) explains the default cache mechanism of `urql`, as opposed to the opt-in [Normalized Cache](../graphcache/normalized-caching.md). - [**Errors**](../basics/errors.md) contains information on error handling in `urql`. - [**UI-Patterns**](../basics/ui-patterns.md) presents some common UI-patterns with `urql`. ================================================ FILE: docs/basics/core.md ================================================ --- title: Core / Node.js order: 3 --- # Core and Node.js Usage The `@urql/core` package contains `urql`'s `Client`, some common utilities, and some default _Exchanges_. These are the shared, default parts of `urql` that we will be using no matter which framework we're interacting with. All framework bindings — meaning `urql`, `@urql/preact`, `@urql/svelte`, and `@urql/vue` — reexport all exports of our `@urql/core` core library. This means that if we want to use `urql`'s `Client` imperatively or with Node.js we'd use `@urql/core`'s utilities or the `Client` directly. In other words, if we're using framework bindings then writing `import { Client } from "@urql/vue"` for instance is the same as `import { Client } from "@urql/core"`. This means that we can use the core utilities and exports that are shared between all bindings directly or install `@urql/core` separately. We can even use `@urql/core` directly without any framework bindings. ## Installation As we said above, if we are using bindings then those will already have installed `@urql/core` as they depend on it. They also all re-export all exports from `@urql/core`, so we can use those regardless of which bindings we've installed. However, it's also possible to explicitly install `@urql/core` or use it standalone, e.g. in a Node.js environment. ```sh yarn add @urql/core # or npm install --save @urql/core ``` Since all bindings and all exchanges depend on `@urql/core`, we may sometimes run into problems where the package manager installs _two versions_ of `@urql/core`, which is a duplication problem. This can cause type errors in TypeScript or cause some parts of our application to bundle two different versions of the package or use slightly different utilities. We can fix this by deduplicating our dependencies. ```sh # npm npm dedupe # pnpm pnpm dedupe # yarn npx yarn-deduplicate && yarn ``` ## GraphQL Tags A notable utility function is the `gql` tagged template literal function, which is a drop-in replacement for `graphql-tag`, if you're coming from other GraphQL clients. Wherever `urql` accepts a query document, we can either pass a string or a `DocumentNode`. `gql` is a utility that allows a `DocumentNode` to be created directly, and others to be interpolated into it, which is useful for fragments for instance. This function will often also mark GraphQL documents for syntax highlighting in most code editors. In most examples we may have passed a string to define a query document, like so: ```js const TodosQuery = ` query { todos { id title } } `; ``` We may also use the `gql` tag function to create a `DocumentNode` directly: ```js import { gql } from '@urql/core'; const TodosQuery = gql` query { todos { id title } } `; ``` Since all framework bindings also re-export `@urql/core`, we may also import `gql` from `'urql'`, `'@urql/svelte'` and other bindings directly. We can also start interpolating other documents into the tag function. This is useful to compose fragment documents into a larger query, since it's common to define fragments across components of an app to spread out data dependencies. If we accidentally use a duplicate fragment name in a document, `gql` will log a warning, since GraphQL APIs won't accept duplicate names. ```js import { gql } from '@urql/core'; const TodoFragment = gql` fragment SmallTodo on Todo { id title } `; const TodosQuery = gql` query { todos { ...TodoFragment } } ${TodoFragment} `; ``` This usage will look familiar when coming from the `graphql-tag` package. The `gql` API is identical, and its output is approximately the same. The two packages are also intercompatible. However, one small change in `@urql/core`'s implementation is that your fragment names don't have to be globally unique, since it's possible to create some one-off fragments occasionally, especially for `@urql/exchange-graphcache`'s configuration. It also pre-generates a "hash key" for the `DocumentNode` which is what `urql` does anyway, thus avoiding some extra work compared to when the `graphql-tag` package is used with `urql`. ## Using the `urql` Client The `Client` is the main "hub" and store for everything that `urql` does. It is used by all framework bindings and from the other pages in the "Basics" section we can see that creating a `Client` comes up across all bindings and use-cases for `urql`. [Read more about the `Client` and `urql`'s architecture on the "Architecture" page.](../architecture.md) ### Setting up the `Client` The `@urql/core` package exports a `Client` class, which we can use to create the GraphQL client. This central `Client` manages all of our GraphQL requests and results. ```js import { Client, cacheExchange, fetchExchange } from '@urql/core'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` At the bare minimum we'll need to pass an API's `url`, and the `fetchExchange`, when we create a `Client` to get started. Another common option is `fetchOptions`. This option allows us to customize the options that will be passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object, or a function returning an options object. In the following example we'll add a token to each `fetch` request that our `Client` sends to our GraphQL API. ```js const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], fetchOptions: () => { const token = getToken(); return { headers: { authorization: token ? `Bearer ${token}` : '' }, }; }, }); ``` ### The `Client`s options As we've seen above, the most important options for the `Client` are `url` and `exchanges`. The `url` option is used by the `fetchExchange` to send GraphQL requests to an API. The `exchanges` option is of particular importance however because it tells the `Client` what to do with our GraphQL requests: ```js import { Client, cacheExchange, fetchExchange } from '@urql/core'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` For instance, here, the `Client`'s caching and fetching features are only available because we're passing it exchanges. In the above example, the `Client` will try to first read a GraphQL request from a local cache, and if this request isn't cached it'll make an HTTP request. The caching in `urql` is also implemented as an exchange, so for instance, the behavior described on the ["Document Caching" page](./document-caching.md) is all contained within the `cacheExchange` above. Later, [in the "Advanced" section](../advanced/README.md) we'll see many more features that `urql` supports by adding new exchanges to this list. On [the "Architecture" page](../architecture.md) we'll also learn more about what exchanges are and why they exist. ### One-off Queries and Mutations When you're using `urql` to send one-off queries or mutations — rather than in full framework code, where updates are important — it's common to convert the streams that we get to promises. The `client.query` and `client.mutation` methods have a shortcut to do just that. ```js const QUERY = ` query Test($id: ID!) { getUser(id: $id) { id name } } `; client .query(QUERY, { id: 'test' }) .toPromise() .then(result => { console.log(result); // { data: ... } }); ``` In the above example we're executing a query on the client, are passing some variables and are calling the `toPromise()` method on the return value to execute the request immediately and get the result as a promise. This may be useful when we don't plan on cancelling queries, or we don't care about future updates to this data and are just looking to query a result once. This can also be written using async/await by simply awaiting the return value of `client.query`: ```js const QUERY = ` query Test($id: ID!) { getUser(id: $id) { id name } } `; async function query() { const result = await client.query(QUERY, { id: 'test' }); console.log(result); // { data: ... } } ``` The same can be done for mutations by calling the `client.mutation` method instead of the `client.query` method. It's worth noting that promisifying a query result will always only give us _one_ result, because we're not calling `subscribe`. This means that we'll never see cache updates when we're asking for a single result like we do above. #### Reading only cache data Similarly there's a way to read data from the cache synchronously, provided that the cache has received a result for a given query before. The `Client` has a `readQuery` method, which is a shortcut for just that. ```js const QUERY = ` query Test($id: ID!) { getUser(id: $id) { id name } } `; const result = client.readQuery(QUERY, { id: 'test' }); result; // null or { data: ... } ``` In the above example we call `readQuery` and receive a result immediately. This result will be `null` if the `cacheExchange` doesn't have any results cached for the given query. ### Subscribing to Results GraphQL Clients are by their nature "reactive", meaning that when we execute a query, we expect to get future results for this query. [On the "Document Caching" page](./document-caching.md) we'll learn how mutations can invalidate results in the cache. This process (and others just like it) can cause our query to be refetched. In essence, if we're subscribing to results rather than using a promise, like we've seen above, then we're able to see future changes for our query's results. If a mutation causes a query to be refetched from our API in the background then we'll see a new result. If we execute a query somewhere else then we'll get notified of the new API result as well, as long as we're subscribed. ```js const QUERY = ` query Test($id: ID!) { getUser(id: $id) { id name } } `; const { unsubscribe } = client.query(QUERY, { id: 'test' }).subscribe(result => { console.log(result); // { data: ... } }); ``` This code example is similar to the one before. However, instead of sending a one-off query, we're subscribing to the query. Internally, this causes the `Client` to do the same, but the subscription means that our callback may be called repeatedly. We may get future results as well as the first one. This also works synchronously. As we've seen before `client.readQuery` can give us a result immediately if our cache already has a result for the given query. The same principle applies here! Our callback will be called synchronously if the cache already has a result. Once we're not interested in any results anymore, we need to clean up after ourselves by calling `unsubscribe`. This stops the subscription and makes sure that the `Client` doesn't actively update the query anymore or refetches it. We can think of this pattern as being very similar to events or event hubs. We're using [the Wonka library for our streams](https://wonka.kitten.sh/basics/background), which we'll learn more about [on the "Architecture" page](../architecture.md). But we can think of this as React's effects being called over time, or as `window.addEventListener`. ## Common Utilities in Core The `@urql/core` package contains other utilities that are shared between multiple addon packages. This is a short but non-exhaustive list. It contains, - [`CombinedError`](../api/core.md#combinederror) - our abstraction to combine one or more `GraphQLError`(s) and a `NetworkError` - `makeResult` and `makeErrorResult` - utilities to create _Operation Results_ - [`createRequest`](../api/core.md#createrequest) - a utility function to create a request from a query, and some variables (which generate a stable _Operation Key_) There are other utilities not mentioned here. Read more about the `@urql/core` API in the [API docs](../api/core.md). ## Reading on This concludes the introduction for using `@urql/core` without any framework bindings. This showed just a couple of ways to use `gql` or the `Client`, however you may also want to learn more about [how to use `urql`'s streams](../architecture.md#stream-patterns-in-urql). Furthermore, apart from the framework binding introductions, there are some other pages that provide more information on how to get fully set up with `urql`: - [How does the default "document cache" work?](./document-caching.md) - [How are errors handled and represented?](./errors.md) - [A quick overview of `urql`'s architecture and structure.](../architecture.md) - [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md) ================================================ FILE: docs/basics/document-caching.md ================================================ --- title: Document Caching order: 4 --- # Document Caching By default, `urql` uses a concept called _Document Caching_. It will avoid sending the same requests to a GraphQL API repeatedly by caching the result of each query. This works like the cache in a browser. `urql` creates a key for each request that is sent based on a query and its variables. The default _document caching_ logic is implemented in the default `cacheExchange`. We'll learn more about ["Exchanges" on the "Architecture" page.](../architecture.md) ## Operation Keys ![Keys for GraphQL Requests](../assets/urql-operation-keys.png) Once a result comes in it's cached indefinitely by its key. This means that each unique request can have exactly one cached result. However, we also need to invalidate the cached results so that requests are sent again and updated, when we know that some results are out-of-date. With document caching we assume that a result may be invalidated by a mutation that executes on data that has been queried previously. In GraphQL the client can request additional type information by adding the `__typename` field to a query's _selection set_. This field returns the name of the type for an object in the results, and we use it to detect commonalities and data dependencies between queries and mutations. ![Document Caching](../assets/urql-document-caching.png) In short, when we send a mutation that contains types that another query's results contains as well, that query's result is removed from the cache. This is an aggressive form of cache invalidation. However, it works well for content-driven sites, while it doesn't deal with normalized data or IDs. ## Request Policies The _request policy_ that is defined will alter what the default document cache does. By default, the cache will prefer cached results and will otherwise send a request, which is called `cache-first`. In total there are four different policies that we can use: - `cache-first` (the default) prefers cached results and falls back to sending an API request when no prior result is cached. - `cache-and-network` returns cached results but also always sends an API request, which is perfect for displaying data quickly while keeping it up-to-date. - `network-only` will always send an API request and will ignore cached results. - `cache-only` will always return cached results or `null`. The `cache-and-network` policy is particularly useful, since it allows us to display data instantly if it has been cached, but also refreshes data in our cache in the background. This means though that `fetching` will be `false` for cached results although an API request may still be ongoing in the background. For this reason there's another field on results, `result.stale`, which indicates that the cached result is either outdated or that another request is being sent in the background. [Read more about which request policies are available in the API docs.](../api/core.md#requestpolicy-type) ## Document Cache Gotchas This cache has a small trade-off! If we request a list of data, and the API returns an empty list, then the cache won't be able to see the `__typename` of said list and invalidate it. There are two ways to fix this issue, supplying `additionalTypenames` to the context of your query or [switch to "Normalized Caching" instead](../graphcache/normalized-caching.md). ### Adding typenames This will elaborate about the first fix for empty lists, the `additionalTypenames`. Example where this would occur: ```js const query = `query { todos { id name } }`; const result = { todos: [] }; ``` At this point we don't know what types are possible for this query, so a best practice when using the default cache is to add `additionalTypenames` for this query. ```js // Keep the reference stable. const context = useMemo(() => ({ additionalTypenames: ['Todo'] }), []); const [result] = useQuery({ query, context }); ``` Now the cache will know when to invalidate this query even when the list is empty. We may also use this feature for mutations, since occasionally mutations must invalidate data that isn't directly connected to a mutation by a `__typename`. ```js const [result, execute] = useMutation(`mutation($name: String!) { createUser(name: $name) }`); const onClick = () => { execute({ name: 'newName' }, { additionalTypenames: ['Wallet'] }); }; ``` Now our `mutation` knows that when it completes it has an additional type to invalidate. ================================================ FILE: docs/basics/errors.md ================================================ --- title: Errors order: 5 --- # Error handling When we use a GraphQL API there are two kinds of errors we may encounter: Network Errors and GraphQL Errors from the API. Since it's common to encounter either of them, there's a [`CombinedError`](../api/core.md#combinederror) class that can hold and abstract either. We may encounter a `CombinedError` when using `urql` wherever an `error` may be returned, typically in results from the API. The `CombinedError` can have one of two properties that describe what went wrong. - The `networkError` property will contain any error that stopped `urql` from making a network request. - The `graphQLErrors` property may be an array that contains [normalized `GraphQLError`s as they were received in the `errors` array from a GraphQL API.](https://graphql.org/graphql-js/error/) Additionally, the `message` of the error will be generated and combined from the errors for debugging purposes. ![Combined errors](../assets/urql-combined-error.png) It's worth noting that an `error` can coexist and be returned in a successful request alongside `data`. This is because in GraphQL a query can have partially failed but still contain some data. In that case `CombinedError` will be passed to us with `graphQLErrors`, while `data` may still be set. ================================================ FILE: docs/basics/react-preact.md ================================================ --- title: React/Preact Bindings order: 0 --- # React/Preact This guide covers how to install and setup `urql` and the `Client`, as well as query and mutate data, with React and Preact. Since the `urql` and `@urql/preact` packages share most of their API and are used in the same way, when reading the documentation on React, all examples are essentially the same, except that we'd want to use the `@urql/preact` package instead of the `urql` package. ## Getting started ### Installation Installing `urql` is as quick as you'd expect, and you won't need any other packages to get started with at first. We'll install the package with our package manager of choice. ```sh yarn add urql # or npm install --save urql ``` To use `urql` with Preact, we have to install `@urql/preact` instead of `urql` and import from that package instead. Otherwise all examples for Preact will be the same. Most libraries related to GraphQL also need the `graphql` package to be installed as a peer dependency, so that they can adapt to your specific versioning requirements. That's why we'll need to install `graphql` alongside `urql`. Both the `urql` and `graphql` packages follow [semantic versioning](https://semver.org) and all `urql` packages will define a range of compatible versions of `graphql`. Watch out for breaking changes in the future however, in which case your package manager may warn you about `graphql` being out of the defined peer dependency range. ### Setting up the `Client` The `urql` and `@urql/preact` packages export a `Client` class, which we can use to create the GraphQL client. This central `Client` manages all of our GraphQL requests and results. ```js import { Client, cacheExchange, fetchExchange } from 'urql'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` At the bare minimum we'll need to pass an API's `url` and `exchanges` when we create a `Client` to get started. Another common option is `fetchOptions`. This option allows us to customize the options that will be passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object, or a function returning an options object. In the following example we'll add a token to each `fetch` request that our `Client` sends to our GraphQL API. ```js const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], fetchOptions: () => { const token = getToken(); return { headers: { authorization: token ? `Bearer ${token}` : '' }, }; }, }); ``` ### Providing the `Client` To make use of the `Client` in React & Preact we will have to provide it via the [Context API](https://reactjs.org/docs/context.html). This may be done with the help of the `Provider` export. ```jsx import { Client, Provider, cacheExchange, fetchExchange } from 'urql'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); const App = () => ( ); ``` Now every component and element inside and under the `Provider` can use GraphQL queries that will be sent to our API. ## Queries Both libraries offer a `useQuery` hook and a `Query` component. The latter accepts the same parameters, but we won't cover it in this guide. [Look it up in the API docs if you prefer render-props components.](../api/urql.md#query-component) ### Run a first query For the following examples, we'll imagine that we're querying data from a GraphQL API that contains todo items. Let's dive right into it! ```jsx import { gql, useQuery } from 'urql'; const TodosQuery = gql` query { todos { id title } } `; const Todos = () => { const [result, reexecuteQuery] = useQuery({ query: TodosQuery, }); const { data, fetching, error } = result; if (fetching) return

Loading...

; if (error) return

Oh no... {error.message}

; return (
    {data.todos.map(todo => (
  • {todo.title}
  • ))}
); }; ``` Here we have implemented our first GraphQL query to fetch todos. We see that `useQuery` accepts options and returns a tuple. In this case we've set the `query` option to our GraphQL query. The tuple we then get in return is an array that contains a result object, and a re-execute function. The result object contains several properties. The `fetching` field indicates whether the hook is loading data, `data` contains the actual `data` from the API's result, and `error` is set when either the request to the API has failed or when our API result contained some `GraphQLError`s, which we'll get into later on the ["Errors" page](./errors.md). ### Variables Typically we'll also need to pass variables to our queries, for instance, if we are dealing with pagination. For this purpose the `useQuery` hook also accepts a `variables` option, which we can use to supply variables to our query. ```jsx const TodosListQuery = gql` query ($from: Int!, $limit: Int!) { todos(from: $from, limit: $limit) { id title } } `; const Todos = ({ from, limit }) => { const [result, reexecuteQuery] = useQuery({ query: TodosListQuery, variables: { from, limit }, }); // ... }; ``` As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the `POST` request body that is sent to our GraphQL API. Whenever the `variables` (or the `query`) option on the `useQuery` hook changes `fetching` will switch to `true`, and a new request will be sent to our API, unless a result has already been cached previously. ### Pausing `useQuery` In some cases we may want `useQuery` to execute a query when a pre-condition has been met, and not execute the query otherwise. For instance, we may be building a form and want a validation query to only take place when a field has been filled out. Since hooks in React can't just be commented out, the `useQuery` hook accepts a `pause` option that temporarily _freezes_ all changes and stops requests. In the previous example we've defined a query with mandatory arguments. The `$from` and `$limit` variables have been defined to be non-nullable `Int!` values. Let's pause the query we've just written to not execute when these variables are empty, to prevent `null` variables from being executed. We can do this by setting the `pause` option to `true`: ```jsx const Todos = ({ from, limit }) => { const shouldPause = from === undefined || from === null || limit === undefined || limit === null; const [result, reexecuteQuery] = useQuery({ query: TodosListQuery, variables: { from, limit }, pause: shouldPause, }); // ... }; ``` Now whenever the mandatory `$from` or `$limit` variables aren't supplied the query won't be executed. This also means that `result.data` won't change, which means we'll still have access to our old data even though the variables may have changed. ### Request Policies As has become clear in the previous sections of this page, the `useQuery` hook accepts more options than just `query` and `variables`. Another option we should touch on is `requestPolicy`. The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By default, this is set to `cache-first`, which means that we prefer to get results from our cache, but are falling back to sending an API request. Request policies aren't specific to `urql`'s React API, but are a common feature in its core. [You can learn more about how the cache behaves given the four different policies on the "Document Caching" page.](../basics/document-caching.md) ```jsx const [result, reexecuteQuery] = useQuery({ query: TodosListQuery, variables: { from, limit }, requestPolicy: 'cache-and-network', }); ``` Specifically, a new request policy may be passed directly to the `useQuery` hook as an option. This policy is then used for this specific query. In this case, `cache-and-network` is used and the query will be refreshed from our API even after our cache has given us a cached result. Internally, the `requestPolicy` is just one of several "**context** options". The `context` provides metadata apart from the usual `query` and `variables` we may pass. This means that we may also change the `Client`'s default `requestPolicy` by passing it there. ```js import { Client, cacheExchange, fetchExchange } from 'urql'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], // every operation will by default use cache-and-network rather // than cache-first now: requestPolicy: 'cache-and-network', }); ``` ### Context Options As mentioned, the `requestPolicy` option on `useQuery` is a part of `urql`'s context options. In fact, there are several more built-in context options, and the `requestPolicy` option is one of them. Another option we've already seen is the `url` option, which determines our API's URL. These options aren't limited to the `Client` and may also be passed per query. ```jsx import { useMemo } from 'react'; import { useQuery } from 'urql'; const Todos = ({ from, limit }) => { const [result, reexecuteQuery] = useQuery({ query: TodosListQuery, variables: { from, limit }, context: useMemo( () => ({ requestPolicy: 'cache-and-network', url: 'http://localhost:3000/graphql?debug=true', }), [] ), }); // ... }; ``` As we can see, the `context` property for `useQuery` accepts any known `context` option and can be used to alter them per query rather than globally. The `Client` accepts a subset of `context` options, while the `useQuery` option does the same for a single query. [You can find a list of all `Context` options in the API docs.](../api/core.md#operationcontext) ### Reexecuting Queries The `useQuery` hook updates and executes queries whenever its inputs, like the `query` or `variables` change, but in some cases we may find that we need to programmatically trigger a new query. This is the purpose of the `reexecuteQuery` function, which is the second item in the tuple that `useQuery` returns. Triggering a query programmatically may be useful in a couple of cases. It can for instance be used to refresh the hook's data. In these cases we may also override the `requestPolicy` of our query just once and set it to `network-only` to skip the cache. ```jsx const Todos = ({ from, limit }) => { const [result, reexecuteQuery] = useQuery({ query: TodosListQuery, variables: { from, limit }, }); const refresh = () => { // Refetch the query and skip the cache reexecuteQuery({ requestPolicy: 'network-only' }); }; }; ``` Calling `refresh` in the above example will execute the query again forcefully, and will skip the cache, since we're passing `requestPolicy: 'network-only'`. Furthermore the `reexecuteQuery` function can also be used to programmatically start a query even when `pause` is set to `true`, which would usually stop all automatic queries. This can be used to perform one-off actions, or to set up polling. ```jsx import { useEffect } from 'react'; import { useQuery } from 'urql'; const Todos = ({ from, limit }) => { const [result, reexecuteQuery] = useQuery({ query: TodosListQuery, variables: { from, limit }, pause: true, }); useEffect(() => { if (result.fetching) return; // Set up to refetch in one second, if the query is idle const timerId = setTimeout(() => { reexecuteQuery({ requestPolicy: 'network-only' }); }, 1000); return () => clearTimeout(timerId); }, [result.fetching, reexecuteQuery]); // ... }; ``` There are some more tricks we can use with `useQuery`. [Read more about its API in the API docs for it.](../api/urql.md#usequery) ## Mutations Both libraries offer a `useMutation` hook and a `Mutation` component. The latter accepts the same parameters, but we won't cover it in this guide. [Look it up in the API docs if you prefer render-props components.](../api/urql.md#mutation-component) ### Sending a mutation Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an example! We'll set up a mutation that _updates_ a todo item's title. ```jsx const UpdateTodo = ` mutation ($id: ID!, $title: String!) { updateTodo (id: $id, title: $title) { id title } } `; const Todo = ({ id, title }) => { const [updateTodoResult, updateTodo] = useMutation(UpdateTodo); }; ``` Similar to the `useQuery` output, `useMutation` returns a tuple. The first item in the tuple again contains `fetching`, `error`, and `data` — it's identical since this is a common pattern of how `urql` presents [operation results](../api/core.md#operationresult). Unlike the `useQuery` hook, the `useMutation` hook doesn't execute automatically. At this point in our example, no mutation will be performed. To execute our mutation we instead have to call the execute function — `updateTodo` in our example — which is the second item in the tuple. ### Using the mutation result When calling our `updateTodo` function we have two ways of getting to the result as it comes back from our API. We can either use the first value of the returned tuple, our `updateTodoResult`, or we can use the promise that `updateTodo` returns. ```jsx const Todo = ({ id, title }) => { const [updateTodoResult, updateTodo] = useMutation(UpdateTodo); const submit = newTitle => { const variables = { id, title: newTitle || '' }; updateTodo(variables).then(result => { // The result is almost identical to `updateTodoResult` with the exception // of `result.fetching` not being set. // It is an OperationResult. }); }; }; ``` The result is useful when your UI has to display progress on the mutation, and the returned promise is particularly useful when you're adding side effects that run after the mutation has completed. ### Handling mutation errors It's worth noting that the promise we receive when calling the execute function will never reject. Instead it will always return a promise that resolves to a result. If you're checking for errors, you should use `result.error` instead, which will be set to a `CombinedError` when any kind of errors occurred while executing your mutation. [Read more about errors on our "Errors" page.](./errors.md) ```jsx const Todo = ({ id, title }) => { const [updateTodoResult, updateTodo] = useMutation(UpdateTodo); const submit = newTitle => { const variables = { id, title: newTitle || '' }; updateTodo(variables).then(result => { if (result.error) { console.error('Oh no!', result.error); } }); }; }; ``` There are some more tricks we can use with `useMutation`.
[Read more about its API in the API docs for it.](../api/urql.md#usemutation) ## Reading on This concludes the introduction for using `urql` with React or Preact. The rest of the documentation is mostly framework-agnostic and will apply to either `urql` in general, or the `@urql/core` package, which is the same between all framework bindings. Hence, next we may want to learn more about one of the following to learn more about the internals: - [How does the default "document cache" work?](./document-caching.md) - [How are errors handled and represented?](./errors.md) - [A quick overview of `urql`'s architecture and structure.](../architecture.md) - [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md) ================================================ FILE: docs/basics/solid-start.md ================================================ --- title: SolidStart Bindings order: 3 --- # SolidStart This guide covers how to use `@urql/solid-start` with SolidStart applications. The `@urql/solid-start` package integrates urql with SolidStart's native data fetching primitives like `query()`, `action()`, `createAsync()`, and `useAction()`. > **Note:** This guide is for SolidStart applications with SSR. If you're building a client-side only SolidJS app, see the [Solid guide](./solid.md) instead. See the [comparison section](#solidjs-vs-solidstart) below for key differences between the packages. ## Getting started ### Installation Installing `@urql/solid-start` requires both the package and its peer dependencies: ```sh yarn add @urql/solid-start @urql/solid @urql/core graphql # or npm install --save @urql/solid-start @urql/solid @urql/core graphql # or pnpm add @urql/solid-start @urql/solid @urql/core graphql ``` The `@urql/solid-start` package depends on `@urql/solid` for shared utilities and re-exports some primitives that work identically on both client and server. ### Setting up the `Client` The `@urql/solid-start` package exports a `Client` class from `@urql/core`. This central `Client` manages all of our GraphQL requests and results. ```js import { createClient, cacheExchange, fetchExchange } from '@urql/solid-start'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` At the bare minimum we'll need to pass an API's `url` and `exchanges` when we create a `Client`. For server-side requests, you'll often want to customize `fetchOptions` to include headers like cookies or authorization tokens: ```js import { getRequestEvent } from 'solid-js/web'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], fetchOptions: () => { const event = getRequestEvent(); return { headers: { cookie: event?.request.headers.get('cookie') || '', }, }; }, }); ``` ### Providing the `Client` To make use of the `Client` in SolidStart we will provide it via Solid's Context API using the `Provider` export. The Provider also needs the `query` and `action` functions from `@solidjs/router`: ```jsx // src/root.tsx or src/app.tsx import { Router, action, query } from '@solidjs/router'; import { FileRoutes } from '@solidjs/start/router'; import { Suspense } from 'solid-js'; import { createClient, Provider, cacheExchange, fetchExchange } from '@urql/solid-start'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); export default function App() { return ( ( {props.children} )} > ); } ``` Now every route and component inside the `Provider` can use GraphQL queries and mutations that will be sent to our API. The `query` and `action` functions are provided in context so that `createQuery` and `createMutation` can access them automatically. ## Queries The `@urql/solid-start` package offers a `createQuery` primitive that integrates with SolidStart's `query()` and `createAsync()` primitives for optimal server-side rendering and streaming. ### Run a first query For the following examples, we'll imagine that we're querying data from a GraphQL API that contains todo items. ```jsx // src/routes/todos.tsx import { Suspense, For, Show } from 'solid-js'; import { createAsync } from '@solidjs/router'; import { gql } from '@urql/core'; import { createQuery } from '@urql/solid-start'; const TodosQuery = gql` query { todos { id title } } `; export default function Todos() { const queryTodos = createQuery(TodosQuery, 'todos-list'); const result = createAsync(() => queryTodos()); return ( Loading...

}>
    {(todo) =>
  • {todo.title}
  • }
); } ``` The `createQuery` primitive integrates with SolidStart's data fetching system: 1. It wraps SolidStart's `query()` function to execute URQL queries with proper router context 2. The `query` function is automatically retrieved from the URQL context (no manual injection needed) 3. The second parameter is a cache key (string) for SolidStart's router 4. The returned function is wrapped with `createAsync()` to get the reactive result 5. `createQuery` must be called inside a component where it has access to the context The query automatically executes on both the server (during SSR) and the client, with SolidStart handling serialization and hydration. ### Variables Typically we'll also need to pass variables to our queries. Pass variables as an option in the fourth parameter: ```jsx // src/routes/todos/[page].tsx import { Suspense, For, Show } from 'solid-js'; import { useParams, createAsync } from '@solidjs/router'; import { gql } from '@urql/core'; import { createQuery } from '@urql/solid-start'; const TodosListQuery = gql` query ($from: Int!, $limit: Int!) { todos(from: $from, limit: $limit) { id title } } `; export default function TodosPage() { const params = useParams(); const queryTodos = createQuery(TodosListQuery, 'todos-paginated', { variables: { from: parseInt(params.page) * 10, limit: 10, }, }); const result = createAsync(() => queryTodos()); return ( Loading...

}>
    {(todo) =>
  • {todo.title}
  • }
); } ``` For dynamic variables that change based on reactive values, you'll need to recreate the query function when dependencies change. ### Request Policies The `requestPolicy` option determines how results are retrieved from the cache: ```jsx const queryTodos = createQuery(TodosQuery, 'todos-list', { requestPolicy: 'cache-and-network', }); const result = createAsync(() => queryTodos()); ``` Available policies: - `cache-first` (default): Prefer cached results, fall back to network - `cache-only`: Only use cached results, never send network requests - `network-only`: Always send a network request, ignore cache - `cache-and-network`: Return cached results immediately, then fetch from network [Learn more about request policies on the "Document Caching" page.](./document-caching.md) ### Revalidation There are two approaches to revalidating data in SolidStart with urql: 1. **urql's cache invalidation** - Invalidates specific queries or entities in urql's cache, causing automatic refetches 2. **SolidStart's revalidation** - Uses SolidStart's router revalidation to reload route data Both approaches work well, and you can choose based on your needs. urql's invalidation is more granular and works at the query level, while SolidStart's revalidation works at the route level. #### Manual Revalidation with urql You can manually revalidate queries using urql's cache invalidation with the `keyFor` helper. This invalidates specific queries in urql's cache and triggers automatic refetches: ```jsx // src/routes/todos.tsx import { Suspense, For, Show } from 'solid-js'; import { createAsync } from '@solidjs/router'; import { gql, keyFor } from '@urql/core'; import { createQuery, useClient } from '@urql/solid-start'; const TodosQuery = gql` query { todos { id title } } `; export default function Todos() { const client = useClient(); const queryTodos = createQuery(TodosQuery, 'todos-list'); const result = createAsync(() => queryTodos()); const handleRefresh = () => { // Invalidate the todos query using keyFor const key = keyFor(TodosQuery); client.reexecuteOperation(client.createRequestOperation('query', { key, query: TodosQuery })); }; return (
Loading...

}>
    {(todo) =>
  • {todo.title}
  • }
); } ``` #### Manual Revalidation with SolidStart Alternatively, you can use SolidStart's built-in `revalidate` function to reload route data. This is useful when you want to refresh all queries on a specific route: ```jsx // src/routes/todos.tsx import { Suspense, For, Show } from 'solid-js'; import { createAsync, revalidate } from '@solidjs/router'; import { gql } from '@urql/core'; import { createQuery } from '@urql/solid-start'; const TodosQuery = gql` query { todos { id title } } `; export default function Todos() { const queryTodos = createQuery(TodosQuery, 'todos-list'); const result = createAsync(() => queryTodos()); const handleRefresh = async () => { // Revalidate the current route - refetches all queries on this page await revalidate(); }; return (
Loading...

}>
    {(todo) =>
  • {todo.title}
  • }
); } ``` #### Revalidation After Mutations A common pattern is to revalidate after a mutation succeeds. You can choose either approach: **Using urql's cache invalidation:** ```jsx // src/routes/todos/new.tsx import { useNavigate } from '@solidjs/router'; import { gql, keyFor } from '@urql/core'; import { createMutation, useClient } from '@urql/solid-start'; const TodosQuery = gql` query { todos { id title } } `; const CreateTodo = gql` mutation ($title: String!) { createTodo(title: $title) { id title } } `; export default function NewTodo() { const navigate = useNavigate(); const client = useClient(); const [state, createTodo] = createMutation(CreateTodo); const handleSubmit = async (e: Event) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const title = formData.get('title') as string; const result = await createTodo({ title }); if (!result.error) { // Invalidate todos query using keyFor const key = keyFor(TodosQuery); client.reexecuteOperation(client.createRequestOperation('query', { key, query: TodosQuery })); navigate('/todos'); } }; return (
); } ``` **Using SolidStart's revalidation:** ```jsx // src/routes/todos/new.tsx import { useNavigate } from '@solidjs/router'; import { gql } from '@urql/core'; import { createMutation } from '@urql/solid-start'; import { revalidate } from '@solidjs/router'; const CreateTodo = gql` mutation ($title: String!) { createTodo(title: $title) { id title } } `; export default function NewTodo() { const navigate = useNavigate(); const [state, createTodo] = createMutation(CreateTodo); const handleSubmit = async (e: Event) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const title = formData.get('title') as string; const result = await createTodo({ title }); if (!result.error) { // Revalidate the /todos route to refetch all its queries await revalidate('/todos'); navigate('/todos'); } }; return (
); } ``` #### Automatic Revalidation with Actions When using SolidStart actions, you can configure automatic revalidation by returning the appropriate response: ```jsx import { action, revalidate } from '@solidjs/router'; import { gql } from '@urql/core'; const createTodoAction = action(async (formData: FormData) => { const title = formData.get('title') as string; // Perform mutation const result = await client.mutation(CreateTodo, { title }).toPromise(); if (!result.error) { // Revalidate multiple routes if needed await revalidate(['/todos', '/']); } return result; }); ``` #### Choosing Between Approaches **Use urql's `keyFor` and `reexecuteOperation` when:** - You need to refetch a specific query after a mutation - You want fine-grained control over which queries to refresh - You're working with multiple queries on the same route and only want to refetch one **Use SolidStart's `revalidate` when:** - You want to refresh all data on a route - You're navigating to a different route and want to ensure fresh data - You want to leverage SolidStart's routing system for cache management Both approaches are valid and can even be used together depending on your application's needs. ### Context Options Context options can be passed to customize the query behavior: ```jsx const queryTodos = createQuery(TodosQuery, 'todos-list', { context: { requestPolicy: 'cache-and-network', fetchOptions: { headers: { 'X-Custom-Header': 'value', }, }, }, }); const result = createAsync(() => queryTodos()); ``` [You can find a list of all `Context` options in the API docs.](../api/core.md#operationcontext) ## Mutations The `@urql/solid-start` package offers a `createMutation` primitive that integrates with SolidStart's `action()` and `useAction()` primitives. ### Sending a mutation Mutations in SolidStart are executed using actions. Here's an example of updating a todo item: ```jsx // src/routes/todos/[id]/edit.tsx import { gql } from '@urql/core'; import { createMutation } from '@urql/solid-start'; import { useParams, useNavigate } from '@solidjs/router'; import { Show } from 'solid-js'; const UpdateTodo = gql` mutation ($id: ID!, $title: String!) { updateTodo(id: $id, title: $title) { id title } } `; export default function EditTodo() { const params = useParams(); const navigate = useNavigate(); const [state, updateTodo] = createMutation(UpdateTodo); const handleSubmit = async (e: Event) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); const title = formData.get('title') as string; const result = await updateTodo({ id: params.id, title, }); if (!result.error) { navigate(`/todos/${params.id}`); } }; return (

Error: {state.error.message}

); } ``` The `createMutation` primitive returns a tuple: 1. A reactive state object containing `fetching`, `error`, and `data` 2. An execute function that triggers the mutation You can optionally provide a custom `key` parameter to control how mutations are cached by SolidStart's router: ```jsx const [state, updateTodo] = createMutation(UpdateTodo, 'update-todo-mutation'); ``` ### Progressive enhancement with actions SolidStart actions work with and without JavaScript enabled. Here's how to set up a mutation that works progressively: ```jsx import { action, redirect } from '@solidjs/router'; import { gql } from '@urql/core'; import { createMutation } from '@urql/solid-start'; const CreateTodo = gql` mutation ($title: String!) { createTodo(title: $title) { id title } } `; export default function NewTodo() { const [state, createTodo] = createMutation(CreateTodo); const handleSubmit = async (formData: FormData) => { const title = formData.get('title') as string; const result = await createTodo({ title }); if (!result.error) { return redirect('/todos'); } }; return (

Error: {state.error.message}

); } ``` ### Using mutation results The mutation state is reactive and updates automatically as the mutation progresses: ```jsx const [state, updateTodo] = createMutation(UpdateTodo); createEffect(() => { if (state.data) { console.log('Mutation succeeded:', state.data); } if (state.error) { console.error('Mutation failed:', state.error); } if (state.fetching) { console.log('Mutation in progress...'); } }); ``` The execute function also returns a promise that resolves to the result: ```jsx const [state, updateTodo] = createMutation(UpdateTodo); const handleUpdate = async () => { const result = await updateTodo({ id: '1', title: 'Updated' }); if (result.error) { console.error('Oh no!', result.error); } else { console.log('Success!', result.data); } }; ``` ### Handling mutation errors Mutation promises never reject. Instead, check the `error` field on the result: ```jsx const [state, updateTodo] = createMutation(UpdateTodo); const handleUpdate = async () => { const result = await updateTodo({ id: '1', title: 'Updated' }); if (result.error) { // CombinedError with network or GraphQL errors console.error('Mutation failed:', result.error); // Check for specific error types if (result.error.networkError) { console.error('Network error:', result.error.networkError); } if (result.error.graphQLErrors.length > 0) { console.error('GraphQL errors:', result.error.graphQLErrors); } } }; ``` [Read more about error handling on the "Errors" page.](./errors.md) ## Subscriptions For GraphQL subscriptions, `@urql/solid-start` provides a `createSubscription` primitive that uses the same SolidStart `Provider` context as `createQuery` and `createMutation`: ```jsx import { gql } from '@urql/core'; import { createSubscription } from '@urql/solid-start'; import { createSignal, For } from 'solid-js'; const NewTodos = gql` subscription { newTodos { id title } } `; export default function TodoSubscription() { const [todos, setTodos] = createSignal([]); const handleSubscription = (previousData, newData) => { setTodos(current => [...current, newData.newTodos]); return newData; }; const [result] = createSubscription( { query: NewTodos, }, handleSubscription ); return (

Live Updates

    {todo =>
  • {todo.title}
  • }
); } ``` Note that GraphQL subscriptions typically require WebSocket support. You'll need to configure your client with a subscription exchange like `subscriptionExchange` from `@urql/core`. ## Server-Side Rendering SolidStart automatically handles server-side rendering and hydration. The `createQuery` primitive works seamlessly on both server and client: 1. On the server, queries execute during SSR and their results are serialized 2. On the client, SolidStart hydrates the data without refetching 3. Subsequent navigations use the standard cache policies ### SSR Considerations When using `createQuery` in SolidStart: - Queries execute on the server during initial page load - Results are automatically streamed to the client - The client hydrates with the server data - No manual script injection or data serialization needed - SolidStart handles all the complexity automatically ### Handling cookies and authentication For authenticated requests, forward cookies and headers from the server request: ```jsx import { getRequestEvent } from 'solid-js/web'; import { createClient, cacheExchange, fetchExchange } from '@urql/solid-start'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], fetchOptions: () => { const event = getRequestEvent(); const headers: Record = {}; // Forward cookies for authenticated requests if (event) { const cookie = event.request.headers.get('cookie'); if (cookie) { headers.cookie = cookie; } } return { headers }; }, }); ``` ## SolidJS vs SolidStart ### When to Use Each Package | Use Case | Package | Why | | ------------------ | ------------------- | ---------------------------------------------------------------- | | Client-side SPA | `@urql/solid` | Optimized for client-only apps, uses SolidJS reactivity patterns | | SolidStart SSR App | `@urql/solid-start` | Integrates with SolidStart's routing, SSR, and action system | ### Key Differences #### Queries **@urql/solid** (Client-side): ```tsx import { createQuery } from '@urql/solid'; const [result] = createQuery({ query: TodosQuery }); // Returns: [Accessor, Accessor] ``` **@urql/solid-start** (SSR): ```tsx import { createQuery } from '@urql/solid-start'; import { createAsync } from '@solidjs/router'; const queryTodos = createQuery(TodosQuery, 'todos'); const todos = createAsync(() => queryTodos()); // Returns: Accessor // Works with SSR and SolidStart's caching ``` #### Mutations **@urql/solid** (Client-side): ```tsx import { createMutation } from '@urql/solid'; const [result, executeMutation] = createMutation(AddTodoMutation); await executeMutation({ title: 'New Todo' }); // Returns: [Accessor, ExecuteMutation] ``` **@urql/solid-start** (SSR with Actions): ```tsx import { createMutation } from '@urql/solid-start'; import { useAction, useSubmission } from '@solidjs/router'; const addTodoAction = createMutation(AddTodoMutation, 'add-todo'); const addTodo = useAction(addTodoAction); const submission = useSubmission(addTodoAction); await addTodo({ title: 'New Todo' }); // Integrates with SolidStart's action system for progressive enhancement ``` ### Why Different APIs? - **SSR Support**: SolidStart queries run on the server and stream to the client - **Router Integration**: Automatic caching and invalidation with SolidStart's router - **Progressive Enhancement**: Actions work without JavaScript enabled - **Suspense**: Native support for SolidJS Suspense boundaries ### Migration If you're moving from a SolidJS SPA to SolidStart: 1. Change imports from `@urql/solid` to `@urql/solid-start` 2. Wrap queries with `createAsync()` 3. Update mutations to use the action pattern with `useAction()` and `useSubmission()` For more details, see the [Solid bindings documentation](./solid.md). ## Reading on This concludes the introduction for using `@urql/solid-start` with SolidStart. For more information: - [Solid bindings documentation](./solid.md) - for client-only features - [How does the default "document cache" work?](./document-caching.md) - [How are errors handled and represented?](./errors.md) - [A quick overview of `urql`'s architecture and structure.](../architecture.md) - [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md) ================================================ FILE: docs/basics/solid.md ================================================ --- title: Solid Bindings order: 3 --- # Solid This guide covers how to install and setup `@urql/solid` and the `Client`, as well as query and mutate data with Solid. The `@urql/solid` package provides reactive primitives that integrate seamlessly with Solid's fine-grained reactivity system. > **Note:** This guide is for client-side SolidJS applications. If you're building a SolidStart application with SSR, see the [SolidStart guide](./solid-start.md) instead. The packages use different APIs optimized for their respective use cases. ## Getting started ### Installation Installing `@urql/solid` is quick and you won't need any other packages to get started with at first. We'll install the package with our package manager of choice. ```sh yarn add @urql/solid graphql # or npm install --save @urql/solid graphql # or pnpm add @urql/solid graphql ``` Most libraries related to GraphQL also need the `graphql` package to be installed as a peer dependency, so that they can adapt to your specific versioning requirements. That's why we'll need to install `graphql` alongside `@urql/solid`. Both the `@urql/solid` and `graphql` packages follow [semantic versioning](https://semver.org) and all `@urql/solid` packages will define a range of compatible versions of `graphql`. Watch out for breaking changes in the future however, in which case your package manager may warn you about `graphql` being out of the defined peer dependency range. ### Setting up the `Client` The `@urql/solid` package exports a `Client` class from `@urql/core`, which we can use to create the GraphQL client. This central `Client` manages all of our GraphQL requests and results. ```js import { createClient, cacheExchange, fetchExchange } from '@urql/solid'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` At the bare minimum we'll need to pass an API's `url` and `exchanges` when we create a `Client` to get started. Another common option is `fetchOptions`. This option allows us to customize the options that will be passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object, or a function returning an options object. In the following example we'll add a token to each `fetch` request that our `Client` sends to our GraphQL API. ```js const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], fetchOptions: () => { const token = getToken(); return { headers: { authorization: token ? `Bearer ${token}` : '' }, }; }, }); ``` ### Providing the `Client` To make use of the `Client` in Solid we will have to provide it via Solid's Context API. This may be done with the help of the `Provider` export. ```jsx import { render } from 'solid-js/web'; import { createClient, Provider, cacheExchange, fetchExchange } from '@urql/solid'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); const App = () => ( ); render(() => , document.getElementById('root')); ``` Now every component inside and under the `Provider` can use GraphQL queries that will be sent to our API. ## Queries The `@urql/solid` package offers a `createQuery` primitive that integrates with Solid's fine-grained reactivity system. ### Run a first query For the following examples, we'll imagine that we're querying data from a GraphQL API that contains todo items. Let's dive right into it! ```jsx import { Suspense, For } from 'solid-js'; import { gql } from '@urql/core'; import { createQuery } from '@urql/solid'; const TodosQuery = gql` query { todos { id title } } `; const Todos = () => { const [result] = createQuery({ query: TodosQuery, }); return ( Loading...

}>
    {(todo) =>
  • {todo.title}
  • }
); }; ``` Here we have implemented our first GraphQL query to fetch todos. We see that `createQuery` accepts options and returns a tuple. In this case we've set the `query` option to our GraphQL query. The tuple we then get in return is an array where the first item is an accessor function that returns the result object. The result object contains several properties. The `fetching` field indicates whether the query is loading data, `data` contains the actual `data` from the API's result, and `error` is set when either the request to the API has failed or when our API result contained some `GraphQLError`s, which we'll get into later on the ["Errors" page](./errors.md). ### Variables Typically we'll also need to pass variables to our queries, for instance, if we are dealing with pagination. For this purpose `createQuery` also accepts a `variables` option, which can be reactive. ```jsx const TodosListQuery = gql` query ($from: Int!, $limit: Int!) { todos(from: $from, limit: $limit) { id title } } `; const Todos = (props) => { const [result] = createQuery({ query: TodosListQuery, variables: () => ({ from: props.from, limit: props.limit }), }); // ... }; ``` The `variables` option can be passed as a static object or as an accessor function that returns the variables. When using an accessor, the query will automatically re-execute when the variables change. ```jsx import { Suspense, For, createSignal } from 'solid-js'; import { gql } from '@urql/core'; import { createQuery } from '@urql/solid'; const TodosListQuery = gql` query ($from: Int!, $limit: Int!) { todos(from: $from, limit: $limit) { id title } } `; const Todos = () => { const [from, setFrom] = createSignal(0); const limit = 10; const [result] = createQuery({ query: TodosListQuery, variables: () => ({ from: from(), limit }), }); return (
Loading...

}>
    {(todo) =>
  • {todo.title}
  • }
); }; ``` Whenever the variables change, `fetching` will switch to `true`, and a new request will be sent to our API, unless a result has already been cached previously. ### Pausing `createQuery` In some cases we may want `createQuery` to execute a query when a pre-condition has been met, and not execute the query otherwise. For instance, we may be building a form and want a validation query to only take place when a field has been filled out. The `createQuery` primitive accepts a `pause` option that temporarily stops the query from executing. ```jsx const Todos = (props) => { const shouldPause = () => props.from == null || props.limit == null; const [result] = createQuery({ query: TodosListQuery, variables: () => ({ from: props.from, limit: props.limit }), pause: shouldPause, }); // ... }; ``` Now whenever the mandatory variables aren't supplied the query won't be executed. This also means that `result().data` won't change, which means we'll still have access to our old data even though the variables may have changed. ### Request Policies The `createQuery` primitive accepts a `requestPolicy` option that determines how results are retrieved from our `Client`'s cache. By default, this is set to `cache-first`, which means that we prefer to get results from our cache, but are falling back to sending an API request. Request policies aren't specific to `@urql/solid`, but are a common feature in urql's core. [You can learn more about how the cache behaves given the four different policies on the "Document Caching" page.](./document-caching.md) ```jsx const [result] = createQuery({ query: TodosListQuery, variables: () => ({ from: props.from, limit: props.limit }), requestPolicy: 'cache-and-network', }); ``` The `requestPolicy` can be passed as a static string or as an accessor function. When using `cache-and-network`, the query will be refreshed from our API even after our cache has given us a cached result. ### Context Options The `requestPolicy` option is part of urql's context options. In fact, there are several more built-in context options. These options can be passed via the `context` parameter. ```jsx const [result] = createQuery({ query: TodosListQuery, variables: () => ({ from: props.from, limit: props.limit }), context: () => ({ requestPolicy: 'cache-and-network', url: 'http://localhost:3000/graphql?debug=true', }), }); ``` [You can find a list of all `Context` options in the API docs.](../api/core.md#operationcontext) ### Reexecuting Queries The `createQuery` primitive updates and executes queries automatically when reactive inputs change, but in some cases we may need to programmatically trigger a new query. This is the purpose of the second item in the tuple that `createQuery` returns. ```jsx const Todos = () => { const [result, reexecuteQuery] = createQuery({ query: TodosListQuery, variables: { from: 0, limit: 10 }, }); const refresh = () => { // Refetch the query and skip the cache reexecuteQuery({ requestPolicy: 'network-only' }); }; return (
Loading...

}>
    {(todo) =>
  • {todo.title}
  • }
); }; ``` Calling `refresh` in the above example will execute the query again forcefully, and will skip the cache, since we're passing `requestPolicy: 'network-only'`. ## Mutations The `@urql/solid` package offers a `createMutation` primitive for executing GraphQL mutations. ### Sending a mutation Let's again pick up an example with an imaginary GraphQL API for todo items. We'll set up a mutation that updates a todo item's title. ```jsx import { gql } from '@urql/core'; import { createMutation } from '@urql/solid'; const UpdateTodo = gql` mutation ($id: ID!, $title: String!) { updateTodo (id: $id, title: $title) { id title } } `; const Todo = (props) => { const [result, updateTodo] = createMutation(UpdateTodo); const handleSubmit = (newTitle) => { updateTodo({ id: props.id, title: newTitle }); }; return (

Updating...

Error: {result().error.message}

{/* Your form UI here */}
); }; ``` Similar to `createQuery`, `createMutation` returns a tuple. The first item is an accessor that returns the result object containing `fetching`, `error`, and `data` — identical to query results. The second item is the execute function that triggers the mutation. Unlike `createQuery`, `createMutation` doesn't execute automatically. We must call the execute function with the mutation variables. ### Using the mutation result The mutation result is available both through the reactive accessor and through the promise returned by the execute function. ```jsx const Todo = (props) => { const [result, updateTodo] = createMutation(UpdateTodo); const handleSubmit = (newTitle) => { const variables = { id: props.id, title: newTitle }; updateTodo(variables).then((result) => { // The result is almost identical to result() from the accessor // It is an OperationResult. if (!result.error) { console.log('Todo updated!', result.data); } }); }; return (

Updating...

{/* Your form UI here */}
); }; ``` The reactive accessor is useful when your UI needs to display progress on the mutation, and the returned promise is particularly useful for side effects that run after the mutation completes. ### Handling mutation errors The promise returned by the execute function will never reject. Instead it will always return a promise that resolves to a result. If you're checking for errors, you should use `result.error`, which will be set to a `CombinedError` when any kind of errors occurred while executing your mutation. [Read more about errors on our "Errors" page.](./errors.md) ```jsx const Todo = (props) => { const [result, updateTodo] = createMutation(UpdateTodo); const handleSubmit = (newTitle) => { const variables = { id: props.id, title: newTitle }; updateTodo(variables).then((result) => { if (result.error) { console.error('Oh no!', result.error); } }); }; // ... }; ``` ## Subscriptions The `@urql/solid` package offers a `createSubscription` primitive for handling GraphQL subscriptions with Solid's reactive system. ### Setting up a subscription GraphQL subscriptions allow you to receive real-time updates from your GraphQL API. Here's an example of how to set up a subscription: ```jsx import { gql } from '@urql/core'; import { createSubscription } from '@urql/solid'; const NewTodos = gql` subscription { newTodos { id title } } `; const TodoSubscription = () => { const [result] = createSubscription({ query: NewTodos, }); return (

Waiting for updates...

Error: {result().error.message}

New todo: {result().data.newTodos.title}

); }; ``` ### Handling subscription data Unlike queries and mutations, subscriptions can emit multiple results over time. You can use a `handler` function to accumulate or process subscription events: ```jsx import { createSignal } from 'solid-js'; const TodoSubscription = () => { const [todos, setTodos] = createSignal([]); const handleSubscription = (previousData, newData) => { setTodos(current => [...current, newData.newTodos]); return newData; }; const [result] = createSubscription( { query: NewTodos, }, handleSubscription ); return (
    {(todo) =>
  • {todo.title}
  • }
); }; ``` The handler function receives the previous data and the new data from the subscription, allowing you to accumulate results or transform them as needed. ## Reading on This concludes the introduction for using `@urql/solid` with Solid. The rest of the documentation is mostly framework-agnostic and will apply to either `urql` in general, or the `@urql/core` package, which is the same between all framework bindings. Hence, next we may want to learn more about one of the following: - [How does the default "document cache" work?](./document-caching.md) - [How are errors handled and represented?](./errors.md) - [A quick overview of `urql`'s architecture and structure.](../architecture.md) - [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md) ================================================ FILE: docs/basics/svelte.md ================================================ --- title: Svelte Bindings order: 2 --- # Svelte ## Getting started This "Getting Started" guide covers how to install and set up `urql` and provide a `Client` for Svelte. The `@urql/svelte` package, which provides bindings for Svelte, doesn't fundamentally function differently from `@urql/preact` or `urql` and uses the same [Core Package and `Client`](./core.md). ### Installation Installing `@urql/svelte` is quick and no other packages are immediately necessary. ```sh yarn add @urql/svelte # or npm install --save @urql/svelte ``` Most libraries related to GraphQL also need the `graphql` package to be installed as a peer dependency, so that they can adapt to your specific versioning requirements. That's why we'll need to install `graphql` alongside `@urql/svelte`. Both the `@urql/svelte` and `graphql` packages follow [semantic versioning](https://semver.org) and all `@urql/svelte` packages will define a range of compatible versions of `graphql`. Watch out for breaking changes in the future however, in which case your package manager may warn you about `graphql` being out of the defined peer dependency range. Note: if using Vite as your bundler, you might stumble upon the error `Function called outside component initialization`, which will prevent the page from loading. To fix it, you must add `@urql/svelte` to Vite's configuration property [`optimizeDeps.exclude`](https://vitejs.dev/config/#dep-optimization-options): ```js { optimizeDeps: { exclude: ['@urql/svelte'], } // other properties } ``` ### Setting up the `Client` The `@urql/svelte` package exports a `Client` class, which we can use to create the GraphQL client. This central `Client` manages all of our GraphQL requests and results. ```js import { Client, cacheExchange, fetchExchange } from '@urql/svelte'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` At the bare minimum we'll need to pass an API's `url` and `exchanges` when we create a `Client` to get started. Another common option is `fetchOptions`. This option allows us to customize the options that will be passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or a function returning an options object. In the following example we'll add a token to each `fetch` request that our `Client` sends to our GraphQL API. ```js const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], fetchOptions: () => { const token = getToken(); return { headers: { authorization: token ? `Bearer ${token}` : '' }, }; }, }); ``` ### Providing the `Client` To make use of the `Client` in Svelte we will have to provide it via the [Context API](https://svelte.dev/tutorial/context-api). From a parent component to its child components. This will share one `Client` with the rest of our app, if we for instance provide the `Client` ```html ``` The `setContextClient` method internally calls [Svelte's `setContext` function](https://svelte.dev/docs#run-time-svelte-setcontext). The `@urql/svelte` package also exposes a `getContextClient` function that uses [`getContext`](https://svelte.dev/docs#run-time-svelte-getcontext) to retrieve the `Client` in child components. This is used to input the client into `@urql/svelte`'s API. ## Queries We'll implement queries using the `queryStore` function from `@urql/svelte`. The `queryStore` function creates a [Svelte Writable store](https://svelte.dev/docs#writable). You can use it to initialise a data container in `urql`. This store holds on to our query inputs, like the GraphQL query and variables, which we can change to launch new queries. It also exposes the query's eventual result, which we can then observe. ### Run a first query For the following examples, we'll imagine that we're querying data from a GraphQL API that contains todo items. Let's dive right into it! ```js {#if $todos.fetching}

Loading...

{:else if $todos.error}

Oh no... {$todos.error.message}

{:else}
    {#each $todos.data.todos as todo}
  • {todo.title}
  • {/each}
{/if} ``` Here we have implemented our first GraphQL query to fetch todos. We're first creating a `queryStore` which will start our GraphQL query. The `todos` store can now be used like any other Svelte store using a [reactive auto-subscription](https://svelte.dev/tutorial/auto-subscriptions) in Svelte. This means that we prefix `$todos` with a dollar symbol, which automatically subscribes us to its changes. ### Variables Typically we'll also need to pass variables to our queries, for instance, if we are dealing with pagination. For this purpose the `queryStore` also accepts a `variables` argument, which we can use to supply variables to our query. ```js ... ``` > Note that we prefix the variable with `$` so Svelte knows that this store is reactive As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the `POST` request body that is sent to our GraphQL API. The `queryStore` also supports being actively changed. This will hook into Svelte's reactivity model as well and cause the `query` utility to start a new operation. ```js ``` ### Pausing Queries In some cases we may want our queries to not execute until a pre-condition has been met. Since the `query` operation exists for the entire component lifecycle however, it can't just be stopped and started at will. Instead, the `queryStore` accepts a key named `pause` that will tell the store that is starts out as paused. For instance, we may start out with a paused store and then unpause it once a callback is invoked: ```html ``` ### Request Policies The `queryStore` also accepts another key apart from `query` and `variables`. Optionally you may pass a `requestPolicy`. The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By default, this is set to `cache-first`, which means that we prefer to get results from our cache, but are falling back to sending an API request. Request policies aren't specific to `urql`'s Svelte bindings, but are a common feature in its core. [You can learn more about how the cache behaves given the four different policies on the "Document Caching" page.](../basics/document-caching.md) ```js ... ``` As we can see, the `requestPolicy` is easily changed by passing it directly as a "context option" when creating a `queryStore`. Internally, the `requestPolicy` is just one of several "**context** options". The `context` provides metadata apart from the usual `query` and `variables` we may pass. This means that we may also change the `Client`'s default `requestPolicy` by passing it there. ```js import { Client, cacheExchange, fetchExchange } from '@urql/svelte'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], // every operation will by default use cache-and-network rather // than cache-first now: requestPolicy: 'cache-and-network', }); ``` ### Context Options As mentioned, the `requestPolicy` option that we're passing to the `queryStore` is a part of `urql`'s context options. In fact, there are several more built-in context options, and the `requestPolicy` option is one of them. Another option we've already seen is the `url` option, which determines our API's URL. ```js ... ``` As we can see, the `context` argument for `queryStore` accepts any known `context` option and can be used to alter them per query rather than globally. The `Client` accepts a subset of `context` options, while the `queryStore` argument does the same for a single query. They're then merged for your operation and form a full `Context` object for each operation, which means that any given query is able to override them as needed. [You can find a list of all `Context` options in the API docs.](../api/core.md#operationcontext) ### Reexecuting queries Sometimes we'll need to arbitrarly reexecute a query to check for new data on the server, this can be done through: ```jsx ``` We use the `requestPolicy` with value `network-only` so we don't hit our cache and dispatch a refresh, if it updates the data the `todos` will be updated due to our cache updating. ### Reading on There are some more tricks we can use with `queryStore`. [Read more about its API in the API docs for it.](../api/svelte.md#queryStore) ## Mutations The `mutationStore` function is similar to the `queryStore` function but is triggered manually and can accept a [`GraphQLRequest` object](../api/core.md#graphqlrequest). ### Sending a mutation Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an example! We'll set up a mutation that _updates_ a todo item's title. ```html ``` This small call to `mutationStore` accepts a `query` property (besides the `variables` property) and returns the `OperationResult` as a store. Unlike the `query` function, we don't want the mutation to start automatically hence we enclose it in a function. The `result` will be updated with the `fetching`, `data`, ... as a normal query would which you can in-turn use in your UI. ### Handling mutation errors It's worth noting that the promise we receive when calling the execute function will never reject. Instead it will always return a promise that resolves to an `mutationStore`, even if the mutation has failed. If you're checking for errors, you should use `mutationStore.error` instead, which will be set to a `CombinedError` when any kind of errors occurred while executing your mutation. [Read more about errors on our "Errors" page.](./errors.md) ```jsx mutateTodo({ id, title: newTitle }).then(result => { if (result.error) { console.error('Oh no!', result.error); } }); ``` ## Reading on This concludes the introduction for using `urql` with Svelte. The rest of the documentation is mostly framework-agnostic and will apply to either `urql` in general, or the `@urql/core` package, which is the same between all framework bindings. Hence, next we may want to learn more about one of the following to learn more about the internals: - [How does the default "document cache" work?](./document-caching.md) - [How are errors handled and represented?](./errors.md) - [A quick overview of `urql`'s architecture and structure.](../architecture.md) - [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md) ================================================ FILE: docs/basics/typescript-integration.md ================================================ --- title: TypeScript integration order: 7 --- # URQL and TypeScript URQL, with the help of [GraphQL Code Generator](https://www.the-guild.dev/graphql/codegen), can leverage the typed-design of GraphQL Schemas to generate TypeScript types on the flight. ## Getting started ### Installation To get and running, install the following packages: ```sh yarn add -D graphql typescript @graphql-codegen/cli @graphql-codegen/client-preset # or npm install -D graphql typescript @graphql-codegen/cli @graphql-codegen/client-preset ``` Then, add the following script to your `package.json`: ```json { "scripts": { "codegen": "graphql-codegen" } } ``` Now, let's create a configuration file for our current framework setup: ### Configuration #### React project configuration Create the following `codegen.ts` configuration file: ```ts import { CodegenConfig } from '@graphql-codegen/cli'; const config: CodegenConfig = { schema: '', documents: ['src/**/*.tsx'], ignoreNoDocuments: true, // for better experience with the watcher generates: { './src/gql/': { preset: 'client', plugins: [], }, }, }; export default config; ``` #### Vue project configuration Create the following `codegen.ts` configuration file: ```ts import type { CodegenConfig } from '@graphql-codegen/cli'; const config: CodegenConfig = { schema: '', documents: ['src/**/*.vue'], ignoreNoDocuments: true, // for better experience with the watcher generates: { './src/gql/': { preset: 'client', config: { useTypeImports: true, }, plugins: [], }, }, }; export default config; ``` ## Typing queries, mutations and subscriptions Now that your project is properly configured, let's start codegen in watch mode: ```sh yarn codegen # or npm run codegen ``` This will generate a `./src/gql` folder that exposes a `graphql()` function. Let's use this `graphql()` function to write our GraphQL Queries, Mutations and Subscriptions. Here, an example with the React bindings, however, the usage remains the same for Vue and Svelte bindings: ```tsx import React from 'react'; import { useQuery } from 'urql'; import './App.css'; import Film from './Film'; import { graphql } from '../src/gql'; const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ ` query allFilmsWithVariablesQuery($first: Int!) { allFilms(first: $first) { edges { node { ...FilmItem } } } } `); function App() { // `data` is typed! const [{ data }] = useQuery({ query: allFilmsWithVariablesQueryDocument, variables: { first: 10 }, }); return (
{data && (
    {data.allFilms?.edges?.map( (e, i) => e?.node && )}
)}
); } export default App; ``` _Examples with Vue are available [in the GraphQL Code Generator repository](https://github.com/dotansimha/graphql-code-generator/tree/master/examples/vue/urql)_. Using the generated `graphql()` function to write your GraphQL document results in instantly typed result and variables for queries, mutations and subscriptions! Let's now see how to go further with GraphQL fragments. ## Getting further with Fragments > Using GraphQL Fragments helps to explicitly declaring the data dependencies of your UI component and safely accessing only the data it needs. Our `` component relies on the `FilmItem` definition, passed through the `film` props: ```tsx // ... import Film from './Film'; import { graphql } from '../src/gql'; const allFilmsWithVariablesQueryDocument = graphql(/* GraphQL */ ` query allFilmsWithVariablesQuery($first: Int!) { allFilms(first: $first) { edges { node { ...FilmItem } } } } `); function App() { // ... return (
{data && (
    {data.allFilms?.edges?.map( (e, i) => e?.node && )}
)}
); } // ... ``` GraphQL Code Generator generates type helpers to type your component props based on Fragments (for example, the `film=` prop) and retrieve your fragment's data (see example below). Again, here is an example with the React bindings: ```tsx import { FragmentType, useFragment } from './gql/fragment-masking'; import { graphql } from '../src/gql'; // again, we use the generated `graphql()` function to write GraphQL documents 👀 export const FilmFragment = graphql(/* GraphQL */ ` fragment FilmItem on Film { id title releaseDate producers } `); const Film = (props: { // `film` property has the correct type 🎉 film: FragmentType; }) => { // `film` is of type `FilmFragment`, with no extraneous properties ⚡️ const film = useFragment(FilmFragment, props.film); return (

{film.title}

{film.releaseDate}

); }; export default Film; ``` _Examples with Vue are available [in the GraphQL Code Generator repository](https://github.com/dotansimha/graphql-code-generator/tree/master/examples/vue/urql)_. You will notice that our `` component leverages 2 imports from our generated code (from `../src/gql`): the `FragmentType` type helper and the `useFragment()` function. - we use `FragmentType` to get the corresponding Fragment TypeScript type - later on, we use `useFragment()` to retrieve the properly film property ================================================ FILE: docs/basics/ui-patterns.md ================================================ --- title: UI-Patterns order: 6 --- # UI Patterns > This page is incomplete. You can help us expanding it by suggesting more patterns or asking us about common problems you're facing on [GitHub Discussions](https://github.com/urql-graphql/urql/discussions). Generally, `urql`'s API surface is small and compact. Some common problems that we're facing when building apps may look like they're not a built-in feature, however, there are several patterns that even a lean UI can support. This page is a collection of common UI patterns and problems we may face with GraphQL and how we can tackle them in `urql`. These examples will be written in React but apply to any other framework. ## Infinite scrolling "Infinite Scrolling" is the approach of loading more data into a page's list without splitting that list up across multiple pages. There are a few ways of going about this. In our [normalized caching chapter on the topic](../graphcache/local-resolvers.md#pagination) we see an approach with `urql`'s normalized cache, which is suitable to get started quickly. However, this approach also requires some UI code as well to keep track of pages. Let's have a look at how we can create a UI implementation that makes use of this normalized caching feature. ```js import React from 'react'; import { useQuery, gql } from 'urql'; const PageQuery = gql` query Page($first: Int!, $after: String) { todos(first: $first, after: $after) { nodes { id name } pageInfo { hasNextPage endCursor } } } `; const SearchResultPage = ({ variables, isLastPage, onLoadMore }) => { const [{ data, fetching, error }] = useQuery({ query: PageQuery, variables }); const todos = data?.todos; return (
{error &&

Oh no... {error.message}

} {fetching &&

Loading...

} {todos && ( <> {todos.nodes.map(todo => (
{todo.id}: {todo.name}
))} {isLastPage && todos.pageInfo.hasNextPage && ( )} )}
); }; const Search = () => { const [pageVariables, setPageVariables] = useState([ { first: 10, after: '', }, ]); return (
{pageVariables.map((variables, i) => ( setPageVariables([...pageVariables, { after, first: 10 }])} /> ))}
); }; ``` Here we keep an array of all `variables` we've encountered and use them to render their respective `result` page. This only rerenders the additional page rather than having a long list that constantly changes. [You can find a full code example of this pattern in our example folder on the topic of pagination.](https://github.com/urql-graphql/urql/tree/main/examples/with-pagination) This code doesn't take changing variables into account, which will affect the cursors. For an example that takes full infinite scrolling into account, [you can find a full code example of an extended pattern in our example folder on the topic of infinite pagination.](https://github.com/urql-graphql/urql/tree/main/examples/with-infinite-pagination) ## Prefetching data We sometimes find it necessary to load data for a new page before that page is opened, for instance while a JS bundle is still loading. We may do this with help of the `Client`, by calling methods without using the React bindings directly. ```js import React from 'react'; import { useClient, gql } from 'urql'; const TodoQuery = gql` query Todo($id: ID!) { todo(id: $id) { id name } } `; const Component = () => { const client = useClient(); const router = useRouter(); const transitionPage = React.useCallback(async id => { const loadJSBundle = import('./page.js'); const loadData = client.query(TodoQuery, { id }).toPromise(); await Promise.all([loadJSBundle, loadData]); router.push(`/todo/${id}`); }, []); return ; }; ``` Here we're calling `client.query` to prepare a query when the transition begins. We then call `toPromise()` on this query which activates it. Our `Client` and its cache share results, which means that we've already kicked off or even completed the query before we're on the new page. ## Lazy query It's often required to "lazily" start a query, either at a later point or imperatively. This means that we don't start a query when a new component is mounted immediately. Parts of `urql` that automatically start, like the `useQuery` hook, have a concept of a [`pause` option.](./react-preact.md#pausing-usequery) This option is used to prevent the hook from automatically starting a new query. ```js import React from 'react'; import { useQuery, gql } from 'urql'; const TodoQuery = gql` query Todos { todos { id name } } `; const Component = () => { const [result, fetch] = useQuery({ query: TodoQuery, pause: true }); const router = useRouter(); return ; }; ``` We can unpause the hook to start fetching, or, like in this example, call its returned function to manually kick off the query. ## Reacting to focus and stale time In urql we leverage our extensibility pattern named "Exchanges" to manipulate the way data comes in and goes out of our client. - [Stale time](https://github.com/urql-graphql/urql/tree/main/exchanges/request-policy) - [Focus](https://github.com/urql-graphql/urql/tree/main/exchanges/refocus) When we want to introduce one of these patterns we add the package and add it to the `exchanges` property of our `Client`. In the case of these two we'll have to add it before the cache else our requests will never get upgraded. ```js import { Client, cacheExchange, fetchExchange } from 'urql'; import { refocusExchange } from '@urql/exchange-refocus'; const client = new Client({ url: 'some-url', exchanges: [refocusExchange(), cacheExchange, fetchExchange], }); ``` That's all we need to do to react to these patterns. ================================================ FILE: docs/basics/vue.md ================================================ --- title: Vue Bindings order: 1 --- # Vue ## Getting started The `@urql/vue` bindings have been written with [Vue 3](https://github.com/vuejs/vue-next/releases/tag/v3.0.0) in mind and use Vue's newer [Composition API](https://v3.vuejs.org/guide/composition-api-introduction.html). This gives the `@urql/vue` bindings capabilities to be more easily integrated into your existing `setup()` functions. ### Installation Installing `@urql/vue` is quick and no other packages are immediately necessary. ```sh yarn add @urql/vue graphql # or npm install --save @urql/vue graphql ``` Most libraries related to GraphQL also need the `graphql` package to be installed as a peer dependency, so that they can adapt to your specific versioning requirements. That's why we'll need to install `graphql` alongside `@urql/vue`. Both the `@urql/vue` and `graphql` packages follow [semantic versioning](https://semver.org) and all `@urql/vue` packages will define a range of compatible versions of `graphql`. Watch out for breaking changes in the future however, in which case your package manager may warn you about `graphql` being out of the defined peer dependency range. ### Setting up the `Client` The `@urql/vue` package exports a `Client` class, which we can use to create the GraphQL client. This central `Client` manages all of our GraphQL requests and results. ```js import { Client, cacheExchange, fetchExchange } from '@urql/vue'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); ``` At the bare minimum we'll need to pass an API's `url` and `exchanges` when we create a `Client` to get started. Another common option is `fetchOptions`. This option allows us to customize the options that will be passed to `fetch` when a request is sent to the given API `url`. We may pass in an options object or a function returning an options object. In the following example we'll add a token to each `fetch` request that our `Client` sends to our GraphQL API. ```js const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], fetchOptions: () => { const token = getToken(); return { headers: { authorization: token ? `Bearer ${token}` : '' }, }; }, }); ``` ### Providing the `Client` To make use of the `Client` in Vue we will have to provide from a parent component to its child components. This will share one `Client` with the rest of our app. In `@urql/vue` there are two different ways to achieve this. The first method is to use `@urql/vue`'s `provideClient` function. This must be called in any of your parent components and accepts either a `Client` directly or just the options that you'd pass to `Client`. ```html ``` Alternatively we may use the exported `install` function and treat `@urql/vue` as a plugin by importing its default export and using it [as a plugin](https://v3.vuejs.org/guide/plugins.html#using-a-plugin). ```js import { createApp } from 'vue'; import Root from './App.vue'; import urql, { cacheExchange, fetchExchange } from '@urql/vue'; const app = createApp(Root); app.use(urql, { url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], }); app.mount('#app'); ``` The plugin also accepts `Client`'s options or a `Client` as its inputs. ## Queries We'll implement queries using the `useQuery` function from `@urql/vue`. ### Run a first query For the following examples, we'll imagine that we're querying data from a GraphQL API that contains todo items. Let's dive right into it! ```jsx ``` Here we have implemented our first GraphQL query to fetch todos. We see that `useQuery` accepts options and returns a result object. In this case we've set the `query` option to our GraphQL query. The result object contains several properties. The `fetching` field indicates whether we're currently loading data, `data` contains the actual `data` from the API's result, and `error` is set when either the request to the API has failed or when our API result contained some `GraphQLError`s, which we'll get into later on the ["Errors" page](./errors.md). All of these properties on the result are derived from the [shape of `OperationResult`](../api/core.md#operationresult) and are marked as [reactive ](https://v3.vuejs.org/guide/reactivity-fundamentals.html), which means they may update while the query is running, which will automatically update your UI. ### Variables Typically we'll also need to pass variables to our queries, for instance, if we are dealing with pagination. For this purpose `useQuery` also accepts a `variables` input, which we can use to supply variables to our query. ```jsx ``` As when we're sending GraphQL queries manually using `fetch`, the variables will be attached to the `POST` request body that is sent to our GraphQL API. All inputs that are passed to `useQuery` may also be [reactive state](https://v3.vuejs.org/guide/reactivity-fundamentals.html). This means that both the inputs and outputs of `useQuery` are reactive and may change over time. ```jsx ``` ### Pausing `useQuery` In some cases we may want `useQuery` to execute a query when a pre-condition has been met, and not execute the query otherwise. For instance, we may be building a form and want a validation query to only take place when a field has been filled out. Since with Vue 3's Composition API we won't just conditionally call `useQuery` we can instead pass a reactive `pause` input to `useQuery`. In the previous example we've defined a query with mandatory arguments. The `$from` and `$limit` variables have been defined to be non-nullable `Int!` values. Let's pause the query we've just written to not execute when these variables are empty, to prevent `null` variables from being executed. We can do this by computing `pause` to become `true` whenever these variables are falsy: ```js import { reactive } from 'vue' import { gql, useQuery } from '@urql/vue'; export default { props: ['from', 'limit'], setup({ from, limit }) { const shouldPause = computed(() => from == null || limit == null); return useQuery({ query: gql` query ($from: Int!, $limit: Int!) { todos(from: $from, limit: $limit) { id title } } `, variables: { from, limit }, pause: shouldPause }); } }; ``` Now whenever the mandatory `$from` or `$limit` variables aren't supplied the query won't be executed. This also means that `result.data` won't change, which means we'll still have access to our old data even though the variables may have changed. It's worth noting that depending on whether `from` and `limit` are reactive or not you may have to change how `pause` is computed. But there's also an imperative alternative to this API. Not only does the result you get back from `useQuery` have an `isPaused` ref, it also has `pause()` and `resume()` methods. ```jsx ``` This means that no matter whether you're in or outside of `setup()` or rather supplying the inputs to `useQuery` or using the outputs, you'll have access to ways to pause or unpause the query. ### Request Policies As has become clear in the previous sections of this page, the `useQuery` hook accepts more options than just `query` and `variables`. Another option we should touch on is `requestPolicy`. The `requestPolicy` option determines how results are retrieved from our `Client`'s cache. By default this is set to `cache-first`, which means that we prefer to get results from our cache, but are falling back to sending an API request. Request policies aren't specific to `urql`'s Vue bindings, but are a common feature in its core. [You can learn more about how the cache behaves given the four different policies on the "Document Caching" page.](../basics/document-caching.md) ```js import { useQuery } from '@urql/vue'; export default { setup() { return useQuery({ query: TodosQuery, requestPolicy: 'cache-and-network', }); }, }; ``` Specifically, a new request policy may be passed directly to `useQuery` as an option. This policy is then used for this specific query. In this case, `cache-and-network` is used and the query will be refreshed from our API even after our cache has given us a cached result. Internally, the `requestPolicy` is just one of several "**context** options". The `context` provides metadata apart from the usual `query` and `variables` we may pass. This means that we may also change the `Client`'s default `requestPolicy` by passing it there. ```js import { Client, cacheExchange, fetchExchange } from '@urql/vue'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange, fetchExchange], // every operation will by default use cache-and-network rather // than cache-first now: requestPolicy: 'cache-and-network', }); ``` ### Context Options As mentioned, the `requestPolicy` option on `useQuery` is a part of `urql`'s context options. In fact, there are several more built-in context options, and the `requestPolicy` option is one of them. Another option we've already seen is the `url` option, which determines our API's URL. These options aren't limited to the `Client` and may also be passed per query. ```jsx import { useQuery } from '@urql/vue'; export default { setup() { return useQuery({ query: TodosQuery, context: { requestPolicy: 'cache-and-network', url: 'http://localhost:3000/graphql?debug=true', }, }); }, }; ``` As we can see, the `context` property for `useQuery` accepts any known `context` option and can be used to alter them per query rather than globally. The `Client` accepts a subset of `context` options, while the `useQuery` option does the same for a single query. [You can find a list of all `Context` options in the API docs.](../api/core.md#operationcontext) ### Reexecuting Queries The `useQuery` hook updates and executes queries whenever its inputs, like the `query` or `variables` change, but in some cases we may find that we need to programmatically trigger a new query. This is the purpose of the `executeQuery` method which is a method on the result object that `useQuery` returns. Triggering a query programmatically may be useful in a couple of cases. It can for instance be used to refresh data that is currently being displayed. In these cases we may also override the `requestPolicy` of our query just once and set it to `network-only` to skip the cache. ```js import { gql, useQuery } from '@urql/vue'; export default { setup() { const result = useQuery({ query: gql` { todos { id title } } `, }); return { data: result.data, fetching: result.fetching, error: result.error, refresh() { result.executeQuery({ requestPolicy: 'network-only', }); }, }; }, }; ``` Calling `refresh` in the above example will execute the query again forcefully, and will skip the cache, since we're passing `requestPolicy: 'network-only'`. Furthermore the `executeQuery` function can also be used to programmatically start a query even when `pause` is set to `true`, which would usually stop all automatic queries. This can be used to perform one-off actions, or to set up polling. ### Vue Suspense In Vue 3 a [new feature was introduced](https://vuedose.tips/go-async-in-vue-3-with-suspense/) that natively allows components to suspend while data is loading, which works universally on the server and on the client, where a replacement loading template is rendered on a parent while data is loading. Any component's `setup()` function can be updated to instead be an `async setup()` function, in other words, to return a `Promise` instead of directly returning its data. This means that we can update any `setup()` function to make use of Suspense. The `useQuery`'s returned result supports this, since it is a `PromiseLike`. We can update one of our examples to have a suspending component by changing our usage of `useQuery`: ```jsx ``` As we can see, `await useQuery(...)` here suspends the component and what we render will not have to handle the loading states of `useQuery` at all. Instead in Vue Suspense we'll have to wrap a parent component in a "Suspense boundary." This boundary is what switches a parent to a loading state while parts of its children are fetching data. The suspense promise is in essence "bubbling up" until it finds a "Suspense boundary". ``` ``` As long as any parent component is wrapping our component which uses `async setup()` in this boundary, we'll get Vue Suspense to work correctly and trigger this loading state. When a child suspends this component will switch to using its `#fallback` template rather than its `#default` template. ### Chaining calls in Vue Suspense As shown [above](#vue-suspense), in Vue Suspense the `async setup()` lifecycle function can be used to set up queries in advance, wait for them to have fetched some data, and then let the component render as usual. However, because the `async setup()` function can be used with `await`-ed promise calls, we may run into situations where we're trying to call functions like `useQuery()` after we've already awaited another promise and will be outside of the synchronous scope of the `setup()` lifecycle. This means that the `useQuery` (and `useSubscription` & `useMutation`) functions won't have access to the `Client` anymore that we'd have set up using `provideClient`. To prevent this, we can create something called a "client handle" using the `useClientHandle` function. ```js import { gql, useClientHandle } from '@urql/vue'; export default { async setup() { const handle = useClientHandle(); await Promise.resolve(); // NOTE: This could be any await call const result = await handle.useQuery({ query: gql` { todos { id title } } `, }); return { data: result.data }; }, }; ``` As we can see, when we use `handle.useQuery()` we're able to still create query results although we've interrupted the synchronous `setup()` lifecycle with a `Promise.resolve()` delay. This would also allow us to create chained queries by using [`computed`](https://v3.vuejs.org/guide/reactivity-computed-watchers.html#computed-values) to use an output from a preceding result in a next `handle.useQuery()` call. ### Reading on There are some more tricks we can use with `useQuery`. [Read more about its API in the API docs for it.](../api/vue.md#usequery) ## Mutations The `useMutation` function is similar to `useQuery` but is triggered manually and accepts only a `DocumentNode` or `string` as an input. ### Sending a mutation Let's again pick up an example with an imaginary GraphQL API for todo items, and dive into an example! We'll set up a mutation that _updates_ a todo item's title. ```js import { gql, useMutation } from '@urql/vue'; export default { setup() { const { executeMutation: updateTodo } = useMutation(gql` mutation ($id: ID!, $title: String!) { updateTodo(id: $id, title: $title) { id title } } `); return { updateTodo }; }, }; ``` Similar to the `useQuery` output, `useMutation` returns a result object, which reflects the data of an executed mutation. That means it'll contain the familiar `fetching`, `error`, and `data` properties — it's identical since this is a common pattern of how `urql` presents [operation results](../api/core.md#operationresult). Unlike the `useQuery` hook, the `useMutation` hook doesn't execute automatically. At this point in our example, no mutation will be performed. To execute our mutation we instead have to call the `executeMutation` method on the result with some variables. ### Using the mutation result When calling our `updateTodo` function we have two ways of getting to the result as it comes back from our API. We can either use the result itself, since all properties related to the last [operation result](../api/core.md#operationresult) are marked as [reactive ](https://v3.vuejs.org/guide/reactivity-fundamentals.html) — or we can use the promise that the `executeMutation` method returns when it's called: ```js import { gql, useMutation } from '@urql/vue'; export default { setup() { const updateTodoResult = useMutation(gql` mutation ($id: ID!, $title: String!) { updateTodo(id: $id, title: $title) { id title } } `); return { updateTodo(id, title) { const variables = { id, title: title || '' }; updateTodoResult.executeMutation(variables).then(result => { // The result is almost identical to `updateTodoResult` with the exception // of `result.fetching` not being set and its properties not being reactive. // It is an OperationResult. }); }, }; }, }; ``` The reactive result that `useMutation` returns is useful when your UI has to display progress or results on the mutation, and the returned promise is particularly useful when you're adding side-effects that run after the mutation has completed. ### Handling mutation errors It's worth noting that the promise we receive when calling the execute function will never reject. Instead it will always return a promise that resolves to a result. If you're checking for errors, you should use `result.error` instead, which will be set to a `CombinedError` when any kind of errors occurred while executing your mutation. [Read more about errors on our "Errors" page.](./errors.md) ```js import { gql, useMutation } from '@urql/vue'; export default { setup() { const updateTodoResult = useMutation(gql` mutation ($id: ID!, $title: String!) { updateTodo(id: $id, title: $title) { id title } } `); return { updateTodo(id, title) { const variables = { id, title: title || '' }; updateTodoResult.executeMutation(variables).then(result => { if (result.error) { console.error('Oh no!', result.error); } }); }, }; }, }; ``` There are some more tricks we can use with `useMutation`.
[Read more about its API in the API docs for it.](../api/vue.md#usemutation) ## Reading on This concludes the introduction for using `urql` with Vue. The rest of the documentation is mostly framework-agnostic and will apply to either `urql` in general or the `@urql/core` package, which is the same between all framework bindings. Hence, next we may want to learn more about one of the following to learn more about the internals: - [How does the default "document cache" work?](./document-caching.md) - [How are errors handled and represented?](./errors.md) - [A quick overview of `urql`'s architecture and structure.](../architecture.md) - [Setting up other features, like authentication, uploads, or persisted queries.](../advanced/README.md) ================================================ FILE: docs/comparison.md ================================================ --- title: Comparison order: 8 --- # Comparison > This comparison page aims to be detailed, unbiased, and up-to-date. If you see any information that > may be inaccurate or could be improved otherwise, please feel free to suggest changes. The most common question that you may encounter with GraphQL is what client to choose when you are getting started. We aim to provide an unbiased and detailed comparison of several options on this page, so that you can make an **informed decision**. All options come with several drawbacks and advantages, and all of these clients have been around for a while now. A little known fact is that `urql` in its current form and architecture has already existed since February 2019, and its normalized cache has been around since September 2019. Overall, we would recommend to make your decision based on whether your required features are supported, which patterns you'll use (or restrictions thereof), and you may want to look into whether all the parts and features you're interested in are well maintained. ## Comparison by Features This section is a list of commonly used features of a GraphQL client and how it's either supported or not by our listed alternatives. We're using Relay and Apollo to compare against as the other most common choices of GraphQL clients. All features are marked to indicate the following: - ✅ Supported 1st-class and documented. - 🔶 Supported and documented, but requires custom user-code to implement. - 🟡 Supported, but as an unofficial 3rd-party library. (Provided it's commonly used) - 🛑 Not officially supported or documented. ### Core Features | | urql | Apollo | Relay | | ------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | Extensible on a network level | ✅ Exchanges | ✅ Links | ✅ Network Layers | | Extensible on a cache / control flow level | ✅ Exchanges | 🛑 | 🛑 | | Base Bundle Size | **10kB** (11kB with bindings) | ~50kB (55kB with React hooks) | 45kB (66kB with bindings) | | Devtools | ✅ | ✅ | ✅ | | Subscriptions | 🔶 [Docs](./advanced/subscriptions.md) | 🔶 [Docs](https://www.apollographql.com/docs/react/data/subscriptions/#setting-up-the-transport) | 🔶 [Docs](https://relay.dev/docs/guided-tour/updating-data/graphql-subscriptions/#configuring-the-network-layer) | | Client-side Rehydration | ✅ [Docs](./advanced/server-side-rendering.md) | ✅ [Docs](https://www.apollographql.com/docs/react/performance/server-side-rendering) | 🛑 | | Polled Queries | 🔶 | ✅ | ✅ | | Lazy Queries | ✅ | ✅ | ✅ | | Stale while Revalidate / Cache and Network | ✅ | ✅ | ✅ | | Focus Refetching | ✅ `@urql/exchange-refocus` | 🛑 | 🛑 | | Stale Time Configuration | ✅ `@urql/exchange-request-policy` | ✅ | 🛑 | | Persisted Queries | ✅ `@urql/exchange-persisted` | ✅ `apollo-link-persisted-queries` | 🔶 | | Batched Queries | 🛑 | ✅ `apollo-link-batch-http` | 🟡 `react-relay-network-layer` | | Live Queries | ✅ (via Incremental Delivery) | 🛑 | ✅ | | Defer & Stream Directives | ✅ | ✅ / 🛑 (`@defer` is supported in >=3.7.0, `@stream` is not yet supported) | 🟡 (unreleased) | | Switching to `GET` method | ✅ | ✅ | 🟡 `react-relay-network-layer` | | File Uploads | ✅ | 🟡 `apollo-upload-client` | 🛑 | | Retrying Failed Queries | ✅ `@urql/exchange-retry` | ✅ `apollo-link-retry` | ✅ `DefaultNetworkLayer` | | Easy Authentication Flows | ✅ `@urql/exchange-auth` | 🛑 (no docs for refresh-based authentication) | 🟡 `react-relay-network-layer` | | Automatic Refetch after Mutation | ✅ (with document cache) | 🛑 | ✅ | Typically these are all additional addon features that you may expect from a GraphQL client, no matter which framework you use it with. It's worth mentioning that all three clients support some kind of extensibility API, which allows you to change when and how queries are sent to an API. These are easy to use primitives particularly in Apollo, with links, and in `urql` with exchanges. The major difference in `urql` is that all caching logic is abstracted in exchanges too, which makes it easy to swap the caching logic or other behavior out (and hence makes `urql` slightly more customizable.) A lot of the added exchanges for persisted queries, file uploads, retrying, and other features are implemented by the urql-team, while there are some cases where first-party support isn't provided in Relay or Apollo. This doesn't mean that these features can't be used with these clients, but that you'd have to lean on community libraries or maintaining/implementing them yourself. One thing of note is our lack of support for batched queries in `urql`. We explicitly decided not to support this in our [first-party packages](https://github.com/urql-graphql/urql/issues/800#issuecomment-626342821) as the benefits are not present anymore in most cases with HTTP/2 and established patterns by Relay that recommend hoisting all necessary data requirements to a page-wide query. ### Framework Bindings | | urql | Apollo | Relay | | ------------------------------ | -------------- | ------------------- | ------------------ | | React Bindings | ✅ | ✅ | ✅ | | React Concurrent Hooks Support | ✅ | ✅ | ✅ | | React Suspense | ✅ | 🛑 | ✅ | | Next.js Integration | ✅ `next-urql` | 🟡 | 🔶 | | Preact Support | ✅ | 🔶 | 🔶 | | Svelte Bindings | ✅ | 🟡 `svelte-apollo` | 🟡 `svelte-relay` | | Vue Bindings | ✅ | 🟡 `vue-apollo` | 🟡 `vue-relay` | | Angular Bindings | 🛑 | 🟡 `apollo-angular` | 🟡 `relay-angular` | | Initial Data on mount | ✅ | ✅ | ✅ | ### Caching and State | | urql | Apollo | Relay | | ------------------------------------------------------- | --------------------------------------------------------------------- | ----------------------------------- | ---------------------------------------------- | | Caching Strategy | Document Caching, Normalized Caching with `@urql/exchange-graphcache` | Normalized Caching | Normalized Caching (schema restrictions apply) | | Added Bundle Size | +8kB (with Graphcache) | +0 (default) | +0 (default) | | Automatic Garbage Collection | ✅ | 🔶 | ✅ | | Local State Management | 🛑 | ✅ | ✅ | | Pagination Support | 🔶 | 🔶 | ✅ | | Optimistic Updates | ✅ | ✅ | ✅ | | Local Updates | ✅ | ✅ | ✅ | | Out-of-band Cache Updates | 🛑 (stays true to server data) | ✅ | ✅ | | Local Resolvers and Redirects | ✅ | ✅ | 🛑 | | Complex Resolvers (nested non-normalized return values) | ✅ | 🛑 | 🛑 | | Commutativity Guarantees | ✅ | 🛑 | ✅ | | Partial Results | ✅ | ✅ | 🛑 | | Safe Partial Results (schema-based) | ✅ | 🔶 (experimental via `useFragment`) | 🛑 | | Persistence Support | ✅ | ✅ `apollo-cache-persist` | 🟡 `@wora/relay-store` | | Offline Support | ✅ | 🛑 | 🟡 `@wora/relay-offline` | `urql` is the only of the three clients that doesn't pick [normalized caching](./graphcache/normalized-caching.md) as its default caching strategy. Typically this is seen by users as easier and quicker to get started with. All entries in this table for `urql` typically refer to the optional `@urql/exchange-graphcache` package. Once you need the same features that you'll find in Relay and Apollo, it's possible to migrate to Graphcache. Graphcache is also slightly different from Apollo's cache and more opinionated as it doesn't allow arbitrary cache updates to be made. Local state management is not provided by choice, but could be implemented as an exchange. For more details, [see discussion here](https://github.com/urql-graphql/urql/issues/323#issuecomment-756226783). `urql` is the only library that provides [Offline Support](./graphcache/offline.md) out of the box as part of Graphcache's feature set. There are a number of options for Apollo and Relay including writing your own logic for offline caching, which can be particularly successful in Relay, but for `@urql/exchange-graphcache` we chose to include it as a feature since it also strengthened other guarantees that the cache makes. Relay does in fact have similar guarantees as [`urql`'s Commutativity Guarantees](./graphcache/normalized-caching/#deterministic-cache-updates), which are more evident when applying list updates out of order under more complex network conditions. ## About Bundle Size `urql` is known and often cited as a "lightweight GraphQL client," which is one of its advantages but not its main goal. It manages to be this small by careful size management, just like other libraries like Preact. You may find that adding features like `@urql/exchange-persisted-fetch` and `@urql/exchange-graphcache` only slightly increases your bundle size as we're aiming to reduce bloat, but often this comparison is hard to make. When you start comparing bundle sizes of these three GraphQL clients you should keep in mind that: - Some dependencies may be external and the above sizes listed are total minified+gzipped sizes - `@urql/core` imports from `wonka` for stream utilities and `@0no-co/graphql.web` for GraphQL query language utilities - Other GraphQL clients may import other exernal dependencies. - All `urql` packages reuse parts of `@urql/core` and `wonka`, which means adding all their total sizes up doesn't give you a correct result of their total expected bundle size. ================================================ FILE: docs/graphcache/README.md ================================================ --- title: Graphcache order: 5 --- # Graphcache In `urql`, caching is fully configurable via [exchanges](../architecture.md), and the default `cacheExchange` in `urql` offers a ["Document Cache"](../basics/document-caching.md), which is usually enough for sites that heavily rely on static content. However as an app grows more complex it's likely that the data and state that `urql` manages, will also grow more complex and introduce interdependencies between data. To solve this problem most GraphQL clients resort to caching data in a normalized format, similar to how [data is often structured in Redux.](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape/) In `urql`, normalized caching is an opt-in feature, which is provided by the `@urql/exchange-graphcache` package, _Graphcache_ for short. ## Features The following pages introduce different features in _Graphcache_, which together make it a compelling alternative to the standard [document cache](../basics/document-caching.md) that `urql` uses by default. - 🔁 [**Fully reactive, normalized caching.**](./normalized-caching.md) _Graphcache_ stores data in a normalized data structure. Query, mutation and subscription results may update one another if they share data, and the app will rerender or refetch data accordingly. This often allows your app to make fewer API requests, since data may already be in the cache. - 💾 [**Custom cache resolvers**](./local-resolvers.md) Since all queries are fully resolved in the cache before and after they're sent, you can add custom resolvers that enable you to format data, implement pagination, or implement cache redirects. - 💭 [**Subscription and Mutation updates**](./cache-updates.md) You can implement update functions that tell _Graphcache_ how to update its data after a mutation has been executed, or whenever a subscription sends a new event. This allows the cache to reactively update itself without queries having to perform a refetch. - 🏃 [**Optimistic mutation updates**](./cache-updates.md) When implemented, optimistic updates can provide the data that the GraphQL API is expected to send back before the request succeeds, which allows the app to instantly render an update while the GraphQL mutation is executed in the background. - 🧠 [**Opt-in schema awareness**](./schema-awareness.md) _Graphcache_ also optionally accepts your entire schema, which allows it to resolve _partial data_ before making a request to the GraphQL API, allowing an app to render everything that's cached before receiving all missing data. It also allows _Graphcache_ to output more helpful warnings and to handle interfaces and enums correctly without heuristics. - 📡 [**Offline support**](./offline.md) _Graphcache_ can persist and rehydrate its entire state, allowing an offline application to be built that is able to execute queries against the cache although the device is offline. - 🐛 [**Errors and warnings**](./errors.md). All potential errors are documented with information on how you may be able to fix them. ## Installation and Setup We can add _Graphcache_ by installing the `@urql/exchange-graphcache` package. Using the package won't increase your bundle size by as much as platforms like [Bundlephobia](https://bundlephobia.com/result?p=@urql/exchange-graphcache) may suggest, since it shares the dependency on `wonka` and `@urql/core` with the framework bindings package, e.g. `urql` or `@urql/preact`, that you're already using. ```sh yarn add @urql/exchange-graphcache # or npm install --save @urql/exchange-graphcache ``` The package exports the `cacheExchange` which replaces the default `cacheExchange` in `@urql/core`. This new `cacheExchange` must be instantiated using some options, which are used to customise _Graphcache_ as introduced in the ["Features" section above.](#features) However, you can get started without passing any options. ```js import { Client, fetchExchange } from 'urql'; import { cacheExchange } from '@urql/exchange-graphcache'; const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cacheExchange({}), fetchExchange], }); ``` This will automatically enable normalized caching, and you may find that in a lot of cases, _Graphcache_ already does what you'd expect it to do without any additional configuration. We'll explore how to customize and set up different parts of _Graphcache_ on the following pages. [Read more about "Normalized Caching" on the next page.](./normalized-caching.md) ================================================ FILE: docs/graphcache/cache-updates.md ================================================ --- title: Cache Updates order: 4 --- # Cache Updates As we've learned [on the page on "Normalized Caching"](./normalized-caching.md#normalizing-relational-data), when Graphcache receives an API result it will traverse and store all its data to its cache in a normalized structure. Each entity that is found in a result will be stored under the entity's key. A query's result is represented as a graph, which can also be understood as a tree structure, starting from the root `Query` entity, which then connects to other entities via links, which are relations stored as keys, where each entity has records that store scalar values, which are the tree's leafs. On the previous page, on ["Local Resolvers"](./local-resolvers.md), we've seen how resolvers can be attached to fields to manually resolve other entities (or transform record fields). Local Resolvers passively _compute_ results and change how Graphcache traverses and sees its locally cached data, however, for **mutations** and **subscriptions** we cannot passively compute data. When Graphcache receives a mutation or subscription result it still traverses it using the query document as we've learned when reading about how Graphcache stores normalized data, [quote](./normalized-caching.md/#storing-normalized-data): > Any mutation or subscription can also be written to this data structure. Once Graphcache finds a > keyable entity in their results it's written to its relational table, which may update other > queries in our application. This means that mutations and subscriptions still write and update entities in the cache. These updates are then reflected on all active queries that our app uses. However, there are limitations to this. While resolvers can be used to passively change data for queries, for mutations and subscriptions we sometimes have to write **updaters** to update links and relations. This is often necessary when a given mutation or subscription deliver a result that is more granular than the cache needs to update all affected entities. Previously, we've learned about cache updates [on the "Normalized Caching" page](./normalized-caching.md#manual-cache-updates). The `updates` option on `cacheExchange` accepts a map for `Mutation` or `Subscription` keys on which we can add "updater functions" to react to mutation or subscription results. These `updates` functions look similar to ["Local Resolvers"](./local-resolvers.md) that we've seen in the last section and similar to [GraphQL.js' resolvers on the server-side](https://www.graphql-tools.com/docs/resolvers/). ```js cacheExchange({ updates: { Mutation: { mutationField: (result, args, cache, info) => { // ... }, }, Subscription: { subscriptionField: (result, args, cache, info) => { // ... }, }, }, }); ``` ## Default mutation invalidation Starting in [Graphcache v7](https://github.com/urql-graphql/urql/blob/main/exchanges/graphcache/CHANGELOG.md#700), mutations without a configured `updates.Mutation.` updater have a fallback behavior: - If the mutation returns an entity that can't be found in the cache yet, Graphcache treats this as a "create" mutation. - Graphcache then invalidates cached entities of the same `__typename` as the one returned from the mutation, which can trigger related queries to refetch. As soon as you define an updater for that mutation field, this fallback behavior no longer runs and your updater fully controls what happens after the mutation write. An "updater" may be attached to a `Mutation` or `Subscription` field and accepts four positional arguments, which are the same as [the resolvers' arguments](./local-resolvers.md): - `result`: The full API result that's being written to the cache. Typically we'd want to avoid coupling by only looking at the current field that the updater is attached to, but it's worth noting that we can access any part of the result. - `args`: The arguments that the field has been called with, which will be replaced with an empty object if the field hasn't been called with any arguments. - `cache`: The `cache` instance, which gives us access to methods allowing us to interact with the local cache. Its full API can be found [in the API docs](../api/graphcache.md#cache). On this page we use it frequently to read from and write to the cache. - `info`: This argument shouldn't be used frequently, but it contains running information about the traversal of the query document. It allows us to make resolvers reusable or to retrieve information about the entire query. Its full API can be found [in the API docs](../api/graphcache.md#info). The cache updaters return value is disregarded (and typed as `void` in TypeScript), which makes any method that they call on the `cache` instance a side effect, which may trigger additional cache changes and updates all affected queries as we modify them. ## Why do we need cache updates? When we’re designing a GraphQL schema well, we won’t need to write many cache updaters for Graphcache. For example, we may have a mutation to update a username on a `User`, which can trivially update the cache without us writing an updater because it resolves the `User`. ```graphql query User($id: ID!) { user(id: $id) { __typename # "User" id username } } mutation UpdateUsername($id: ID!, $username: String!) { updateUser(id: $id, username: $username) { __typename # "User" id username } } ``` In the above example, `Query.user` returns a `User`, which is then updated by a mutation on `Mutation.updateUser`. Since the mutation also queries the `User`, the updated username will automatically be applied by Graphcache. If the mutation field didn’t return a `User`, then this wouldn’t be possible, and while we can write an updater in Graphcache for it, we should consider this poor schema design. An updater instead becomes absolutely necessary when a mutation can’t reasonably return what has changed or when we can’t manually define a selection set that’d be even able to select all fields that may update. Some examples may include: - `Mutation.deleteUser`, since we’ll need to invalidate an entity - `Mutation.createUser`, since a list may now have to include a new entity - `Mutation.createBook`, since a given entity, e.g. `User` may have a field `User.books` that now needs to be updated. In short, we may need to write a cache updater for any **relation** (i.e. link) that we can’t query via our GraphQL mutation directly, since there’ll be changes to our data that Graphcache won’t be able to see and store. In a later section on this page, [we’ll learn about the `cache.link` method.](#writing-links-individually) This method is used to update a field to point at a different entity. In other words, `cache.link` is used to update a relation from one entity field to one or more other child entities. This is the most common update we’ll need and it’s preferable to always try to use `cache.link`, unless we need to update a scalar. ## Manually updating entities If a mutation field's result isn't returning the full entity it updates then it becomes impossible for Graphcache to update said entity automatically. For instance, we may have a mutation like the following: ```graphql mutation UpdateTodo($todoId: ID!, $date: String!) { updateTodoDate(id: $todoId, date: $date) } ``` In this hypothetical case instead of `Mutation.updateDate` resolving to the full `Todo` object type it instead results in a scalar. This could be fixed by changing the `Mutation` in our API's schema to instead return the full `Todo` entity, which would allow us to run the mutation as such, which updates the `Todo` in our cache automatically: ```graphql mutation UpdateTodo($todoId: ID!, $date: String!) { updateTodoDate(id: $todoId, date: $date) { ...Todo_date } } fragment Todo_date on Todo { id updatedAt } ``` However, if this isn't possible we can instead write an updater that updates our `Todo` entity manually by using the `cache.writeFragment` method: ```js import { gql } from '@urql/core'; cacheExchange({ updates: { Mutation: { updateTodoDate(_result, args, cache, _info) { const fragment = gql` fragment _ on Todo { id updatedAt } `; cache.writeFragment(fragment, { id: args.id, updatedAt: args.date }); }, }, }, }); ``` The `cache.writeFragment` method is similar to the `cache.readFragment` method that we've seen [on the "Local Resolvers" page before](./local-resolvers.md#reading-a-fragment). Instead of reading data for a given fragment it instead writes data to the cache. > **Note:** In the above example, we've used > [the `gql` tag function](../api/core.md#gql) because `writeFragment` only accepts > GraphQL `DocumentNode`s as inputs, and not strings. ### Cache Updates outside updaters Cache updates are **not** possible outside `updates`'s functions. If we attempt to store the `cache` in a variable and call its methods outside any `updates` functions (or functions, like `resolvers`) then Graphcache will throw an error. Methods like these cannot be called outside the `cacheExchange`'s `updates` functions, because all updates are isolated to be _reactive_ to mutations and subscription events. In Graphcache, out-of-band updates aren't permitted because the cache attempts to only represent the server's state. This limitation keeps the data of the cache true to the server data we receive from API results and makes its behaviour much more predictable. If we still manage to call any of the cache's methods outside its callbacks in its configuration, we will receive [a "(2) Invalid Cache Call" error](./errors.md#2-invalid-cache-call). ### Updaters on arbitrary types Cache updates **may** be configured for arbitrary types and not just for `Mutation` or `Subscription` fields. However, this can potentially be **dangerous** and is an easy trap to fall into. It is allowed though because it allows for some nice tricks and workarounds. Given an updater on an arbitrary type, e.g. `Todo.author`, we can chain updates onto this field whenever it’s written. The updater can then be triggerd by Graphcache during _any_ operation; mutations, queries, and subscriptions. When this update is triggered, it allows us to add more arbitrary updates onto this field. > **Note:** If you’re looking to use this because you’re nesting mutations onto other object types, > e.g. `Mutation.author.updateName`, please consider changing your schema first before using this. > Namespacing mutations is not recommended and changes the execution order to be concurrent rather > than sequential when you use multiple nested mutation fields. ## Updating lists or links Mutations that create new entities are pretty common, and it's not uncommon to attempt to update the cache when a mutation result for these "creation" mutations come back, since this avoids an additional roundtrip to our APIs. While it's possible for these mutations to return any affected entities that carry the lists as well, often these lists live on fields on or below the `Query` root type, which means that we'd be sending a rather large API result. For large amounts of pages this is especially infeasible. Instead, most schemas opt to instead just return the entity that's just been created: ```graphql mutation NewTodo($text: String!) { createTodo(id: $todoId, text: $text) { id text } } ``` If we have a corresponding field on `Query.todos` that contains all of our `Todo` entities then this means that we'll need to create an updater that automatically adds the `Todo` to our list: ```js cacheExchange({ updates: { Mutation: { createTodo(result, _args, cache, _info) { const TodoList = gql` { todos { id } } `; cache.updateQuery({ query: TodoList }, data => { return { ...data, todos: [...data.todos, result.createTodo], }; }); }, }, }, }); ``` Here we use the `cache.updateQuery` method, which is similar to the [`cache.readQuery` method](./local-resolvers.md#reading-a-query) that we've seen on the "Local Resolvers" page before. This method accepts a callback, which will give us the `data` of the query, as read from the locally cached data, and we may return an updated version of this data. While we may want to instinctively opt for immutably copying and modifying this data, we're actually allowed to mutate it directly, since it's just a copy of the data that's been read by the cache. This `data` may also be `null` if the cache doesn't actually have enough locally cached information to fulfil the query. This is important because resolvers aren't actually applied to cache methods in updaters. All resolvers are ignored, so it becomes impossible to accidentally commit transformed data to our cache. We could safely add a resolver for `Todo.createdAt` and wouldn't have to worry about an updater accidentally writing it to the cache's internal data structure. ### Writing links individually As long as we're only updating links (as in 'relations') then we may also use the [`cache.link` method](../api/graphcache.md#link). This method is the "write equivalent" of [the `cache.resolve` method, as seen on the "Local Resolvers" page before.](./local-resolvers.md#resolving-other-fields) We can use this method to update any relation in our cache, so the example above could also be rewritten to use `cache.link` and `cache.resolve` rather than `cache.updateQuery`. ```js cacheExchange({ updates: { Mutation: { createTodo(result, _args, cache, _info) { const todos = cache.resolve('Query', 'todos'); if (Array.isArray(todos)) { cache.link('Query', 'todos', [...todos, result.createTodo]); } }, }, }, }); ``` This method can be combined with more than just `cache.resolve`, for instance, it's a good fit with `cache.inspectFields`. However, when you're writing records (as in 'scalar' values) `cache.writeFragment` and `cache.updateQuery` are still the only methods that you can use. But since this kind of data is often written automatically by the normalized cache, often updating a link is the only modification we may want to make. ## Updating many unknown links In the previous section we've seen how to update data, like a list, when a mutation result enters the cache. However, we've used a rather simple example when we've looked at a single list on a known field. In many schemas pagination is quite common, and when we for instance delete a todo then knowing the lists to update becomes unknowable. We cannot know ahead of time how many pages (and its variables) we've already accessed. This knowledge in fact _shouldn't_ be available to Graphcache. Querying the `Client` is an entirely separate concern that's often colocated with some part of our UI code. ```graphql mutation RemoveTodo($id: ID!) { removeTodo(id: $id) } ``` Suppose we have the above mutation, which deletes a `Todo` entity by its ID. Our app may query a list of these items over many pages with separate queries being sent to our API, which makes it hard to know the fields that should be checked: ```graphql query PaginatedTodos($skip: Int) { todos(skip: $skip) { id text } } ``` Instead, we can **introspect an entity's fields** to find the fields we may want to update dynamically. This is possible thanks to [the `cache.inspectFields` method](../api/graphcache.md#inspectfields). This method accepts a key, or a keyable entity like the `cache.keyOfEntity` method that [we've seen on the "Local Resolvers" page](./local-resolvers.md#resolving-by-keys) or the `cache.resolve` method's first argument. ```js cacheExchange({ updates: { Mutation: { removeTodo(_result, args, cache, _info) { const TodoList = gql` query (skip: $skip) { todos(skip: $skip) { id } } `; const fields = cache .inspectFields('Query') .filter(field => field.fieldName === 'todos') .forEach(field => { cache.updateQuery( { query: TodoList, variables: { skip: field.arguments.skip }, }, data => { data.todos = data.todos.filter(todo => todo.id !== args.id); return data; } ); }); }, }, }, }); ``` To implement an updater for our example's `removeTodo` mutation field we may use the `cache.inspectFields('Query')` method to retrieve a list of all fields on the `Query` root entity. This list will contain all known fields on the `"Query"` entity. Each field is described as an object with three properties: - `fieldName`: The field's name; in this case we're filtering for all `todos` listing fields. - `arguments`: The arguments for the given field, since each field that accepts arguments can be accessed multiple times with different arguments. In this example we're looking at `arguments.skip` to find all unique pages. - `fieldKey`: This is the field's key, which can come in useful to retrieve a field using `cache.resolve(entityKey, fieldKey)` to prevent the arguments from having to be stringified repeatedly. To summarise, we filter the list of fields in our example down to only the `todos` fields and iterate over each of our `arguments` for the `todos` field to filter all lists to remove the `Todo` from them. ### Inspecting arbitrary entities We're not required to only inspecting fields on the `Query` root entity. Instead, we can inspect fields on any entity by passing a different partial, keyable entity or key to `cache.inspectFields`. For instance, if we had a `Todo` entity and wanted to get all of its known fields then we could pass in a partial `Todo` entity just as well: ```js cache.inspectFields({ __typename: 'Todo', id: args.id, }); ``` ## Invalidating Entities Admittedly, it's sometimes almost impossible to write updaters for all mutations. It's often even hard to predict what our APIs may do when they receive a mutation. An update of an entity may change the sorting of a list, or remove an item from a list in a way we can't predict, since we don't have access to a full database to run the API locally. In cases like these it may be advisable to trigger a refetch instead and let the cache update itself by sending queries that have invalidated data associated to them to our API again. This process is called **invalidation** since it removes data from Graphcache's locally cached data. We may use the cache's [`cache.invalidate` method](../api/graphcache.md#invalidate) to either invalidate entire entities or individual fields. It has the same signature as [the `cache.resolve` method](../api/graphcache.md#resolve), which we've already seen [on the "Local Resolvers" page as well](./local-resolvers.md#resolving-other-fields). We can simplify the previous update we've written with a call to `cache.invalidate`: ```js cacheExchange({ updates: { Mutation: { removeTodo(_result, args, cache, _info) { cache.invalidate({ __typename: 'Todo', id: args.id, }); }, }, }, }); ``` Like any other cache update, this will cause all queries that use this `Todo` entity to be updated against the cache. Since we've invalidated the `Todo` item they're using these queries will be refetched and sent to our API. If we're using ["Schema Awareness"](./schema-awareness.md) then these queries' results may actually be temporarily updated with a partial result, but in general we should observe that queries with data that has been invalidated will be refetched as some of their data isn't cached anymore. ### Invalidating individual fields We may also want to only invalidate individual fields, since maybe not all queries have to be immediately updated. We can pass a field (and optional arguments) to the `cache.invalidate` method as well to only invalidate a single field. For instance, we can use this to invalidate our lists instead of invalidating the entity itself. This can be useful if we know that modifying an entity will cause our list to be sorted differently, for instance. ```js cacheExchange({ updates: { Mutation: { updateTodo(_result, args, cache, _info) { const key = 'Query'; const fields = cache .inspectFields(key) .filter(field => field.fieldName === 'todos') .forEach(field => { cache.invalidate(key, field.fieldKey); // or alternatively: cache.invalidate(key, field.fieldName, field.arguments); }); }, }, }, }); ``` In this example we've attached an updater to a `Mutation.updateTodo` field. We react to this mutation by enumerating all `todos` listing fields using `cache.inspectFields` and targetedly invalidate only these fields, which causes all queries using these listing fields to be refetched. ### Invalidating a type We can also invalidate all the entities of a given type, this could be handy in the case of a list update or when you aren't sure what entity is affected. This can be done by only passing the relevant `__typename` to the `invalidate` function. ```js cacheExchange({ updates: { Mutation: { deleteTodo(_result, args, cache, _info) { cache.invalidate('Todo'); }, }, }, }); ``` ## Optimistic updates If we know what result a mutation may return, why wait for the GraphQL API to fulfill our mutations? In addition to the `updates` configuration, we can also pass an `optimistic` option to the `cacheExchange`. This option is a factory function that allows us to create a "virtual" result for a mutation. This temporary result can be applied immediately to the cache to give our users the illusion that mutations were executed immediately, which is a great method to reduce waiting time and to make our apps feel snappier. This technique is often used with one-off mutations that are assumed to succeed, like starring a repository, or liking a tweet. In such cases it's often desirable to make the interaction feel as instant as possible. The `optimistic` configuration is similar to our `resolvers` or `updates` configuration, except that it only receives a single map for mutation fields. We can attach optimistic functions to any mutation field to make it generate an optimistic that is applied to the cache while the `Client` waits for a response from our API. An "optimistic" function accepts three positional arguments, which are the same as the resolvers' or updaters' arguments, except for the first one: The `optimistic` functions receive the same arguments as `updates` functions, except for `parent`, since we don't have any server data to work with: - `args`: The arguments that the field has been called with, which will be replaced with an empty object if the field hasn't been called with any arguments. - `cache`: The `cache` instance, which gives us access to methods allowing us to interact with the local cache. Its full API can be found [in the API docs](../api/graphcache.md#cache). On this page we use it frequently to read from and write to the cache. - `info`: This argument shouldn't be used frequently, but it contains running information about the traversal of the query document. It allows us to make resolvers reusable or to retrieve information about the entire query. Its full API can be found [in the API docs](../api/graphcache.md#info). The usual `parent` argument isn't present since optimistic functions don't have any server data to handle or deal with and instead create this data. When a mutation is run that contains one or more optimistic mutation fields, Graphcache picks these up and generates immediate changes, which it applies to the cache. The `resolvers` functions also trigger as if the results were real server results. This modification is temporary. Once a result from the API comes back it's reverted, which leaves us in a state where the cache can apply the "real" result to the cache. > Note: While optimistic mutations are waiting for results from the API all queries that may alter > our optimistic data are paused (or rather queued up) and all optimistic mutations will be reverted > at the same time. This means that optimistic results can stack but will never accidentally be > confused with "real" data in your configuration. In the following example we assume that we'd like to implement an optimistic result for a `favoriteTodo` mutation, like such: ```graphql mutation FavoriteTodo(id: $id) { favoriteTodo(id: $id) { id favorite updatedAt } } ``` The mutation is rather simple and all we have to do is create a function that imitates the result that the API is assumed to send back: ```js const cache = cacheExchange({ optimistic: { favoriteTodo(args, cache, info) { return { __typename: 'Todo', id: args.id, favorite: true, }; }, }, }); ``` This optimistic mutation will be applied to the cache. If any `updates` configuration exists for `Mutation.favoriteTodo` then it will be executed using the optimistic result. Once the mutation result comes back from our API this temporary change will be rolled back and discarded. In the above example optimistic mutation function we also see that `updatedAt` is not present in our optimistic return value. That’s because we don’t always have to (or can) match our mutations’ selection sets perfectly. Instead, Graphcache will skip over fields and use cached fields for any we leave out. This can even work on nested entities and fields. However, leaving out fields can sometimes cause the optimistic update to not apply when we accidentally cause any query that needs to update accordingly to only be partially cached. In other words, if our optimistic updates cause a cache miss, we won’t see them being applied. Sometimes we may need to apply optimistic updates to fields that accept arguments. For instance, our `favorite` field may have a date cut-off: ```graphql mutation FavoriteTodo(id: $id) { favoriteTodo(id: $id) { id favorite(since: ONE_MONTH_AGO) updatedAt } } ``` To solve this, we can return a method on the optimistic result our `optimistic` update function returns: ```js const cache = cacheExchange({ optimistic: { favoriteTodo(args, cache, info) { return { __typename: 'Todo', id: args.id, favorite(_args, cache, info) { return true; }, }, }, }, }); ``` The function signature and arguments it receives is identical to the toplevel optimistic function you define, and is basically like a nested optimistic function. ### Variables for Optimistic Updates Sometimes it's not possible for us to retrieve all data that an optimistic update requires to create a "fake result" from the cache or from all existing variables. This is why Graphcache allows for a small escape hatch for these scenarios, which allows us to access additional variables, which we may want to pass from our UI code to the mutation. For instance, given a mutation like the following we may add more variables than the mutation specifies: ```graphql mutation UpdateTodo($id: ID!, $text: ID!) { updateTodo(id: $id, text: $text) { id text } } ``` In the above mutation we've only defined an `$id` and `$text` variable. Graphcache typically filters variables using our query document definitions, which means that our API will never receive any variables other than the ones we've defined. However, we're able to pass additional variables to our mutation, e.g. `{ extra }`, and since `$extra` isn't defined it will be filtered once the mutation is sent to the API. An optimistic mutation however will still be able to access this variable, like so: ```js cacheExchange({ updates: { Mutation: { updateTodo(_result, _args, _cache, info) { const extraVariable = info.variables.extra; }, }, }, }); ``` ### Reading on [On the next page we'll learn about "Schema Awareness".](./schema-awareness.md) ================================================ FILE: docs/graphcache/errors.md ================================================ --- title: Errors order: 8 --- # Help! **This document lists out all errors and warnings in `@urql/exchange-graphcache`.** Any unexpected behaviour or condition will be marked by an error or warning in development. This will output as a helpful little message. Sometimes, however, this message may not actually tell you about everything that's going on. This is a supporting document that explains every error and attempts to give more information on how you may be able to fix some issues or avoid these errors/warnings. ## (1) Invalid GraphQL document > Invalid GraphQL document: All GraphQL documents must contain an OperationDefinition > node for a query, subscription or mutation. There are multiple places where you're passing in GraphQL documents, either through methods on `Cache` (e.g. `cache.updateQuery`) or via `urql` using the `Client` or hooks like `useQuery`. Your queries must always contain a main operation, one of: query, mutation, or subscription. This error occurs when this is missing, because the `DocumentNode` is maybe empty or only contains fragments. ## (2) Invalid Cache call > Invalid Cache call: The cache may only be accessed or mutated during > operations like write or query, or as part of its resolvers, updaters, > or optimistic configs. If you're somehow accessing the `Cache` (an instance of `Store`) outside any of the usual operations then this error will be thrown. Please make sure that you're only calling methods on the `cache` as part of configs that you pass to your `cacheExchange`. Outside these functions the cache must not be changed. However when you're not using the `cacheExchange` and are trying to use the `Store` on its own, then you may run into issues where its global state wasn't initialised correctly. This is a safe-guard to prevent any asynchronous work to take place, or to avoid mutating the cache outside any normal operation. ## (3) Invalid Object type > Invalid Object type: The type `???` is not an object in the defined schema, > but the GraphQL document is traversing it. When you're passing an introspected schema to the cache exchange, it is able to check whether all your queries are valid. This error occurs when an unknown type is found as part of a query or fragment. Check whether your schema is up-to-date or whether you're using an invalid typename somewhere, maybe due to a typo. ## (4) Invalid field > Invalid field: The field `???` does not exist on `???`, > but the GraphQL document expects it to exist.
> Traversal will continue, however this may lead to undefined behavior! Similarly to the previous warning, when you're passing an introspected schema to the cache exchange, it is able to check whether all your queries are valid. This warning occurs when an unknown field is found on a selection set as part of a query or fragment. Check whether your schema is up-to-date or whether you're using an invalid field somewhere, maybe due to a typo. As the warning states, this won't lead any operation to abort, or an error to be thrown! ## (5) Invalid Abstract type > Invalid Abstract type: The type `???` is not an Interface or Union type > in the defined schema, but a fragment in the GraphQL document is using it > as a type condition. When you're passing an introspected schema to the cache exchange, it becomes able to deterministically check whether an entity in the cache matches a fragment's type condition. This applies to full fragments (`fragment _ on Interface`) or inline fragments (`... on Interface`), that apply to interfaces instead of to a concrete object typename. Check whether your schema is up-to-date or whether you're using an invalid field somewhere, maybe due to a typo. ## (6) readFragment(...) was called with an empty fragment > readFragment(...) was called with an empty fragment. > You have to call it with at least one fragment in your GraphQL document. You probably have called `cache.readFragment` with a GraphQL document that doesn't contain a main fragment. This error occurs when no main fragment can be found, because the `DocumentNode` is maybe empty or does not contain fragments. When you're calling a fragment method, please ensure that you're only passing fragments in your GraphQL document. The first fragment will be used to start writing data. This also occurs when you pass in a `fragmentName` but a fragment with the given name can't be found in the `DocumentNode`. ## (7) Can't generate a key for readFragment(...) > Can't generate a key for readFragment(...). > You have to pass an `id` or `_id` field or create a custom `keys` config for `???`. You probably have called `cache.readFragment` with data that the cache can't generate a key for. This may either happen because you're missing the `id` or `_id` field or some other fields for your custom `keys` config. Please make sure that you include enough properties on your data so that `readFragment` can generate a key. ## (8) Invalid resolver data > Invalid resolver value: The resolver at `???` returned an invalid typename that > could not be reconciled with the cache. This error may occur when you provide a cache resolver for a field using `resolvers` config. The value that you returns needs to contain a `__typename` field and this field must match the `__typename` field that exists in the cache, if any. This is because it's not possible to return a different type for a single field. Please check your schema for the type that your resolver has to return, then add a `__typename` field to your returned resolver value that matches this type. ## (9) Invalid resolver value > Invalid resolver value: The field at `???` is a scalar (number, boolean, etc), > but the GraphQL query expects a selection set for this field. The GraphQL query that has been walked contains a selection set at the place where your resolver is located. This means that a full entity object needs to be returned, but instead the cache received a number, boolean, or another scalar from your resolver. Please check that your resolvers return scalars where there's no selection set, and entities where there is one. ## (10) writeOptimistic(...) was called with an operation that isn't a mutation > writeOptimistic(...) was called with an operation that is not a mutation. > This case is unsupported and should never occur. This should never happen, please open an issue if it does. This occurs when `writeOptimistic` attempts to write an optimistic result for a query or subscription, instead of a mutation. ## (11) writeFragment(...) was called with an empty fragment > writeFragment(...) was called with an empty fragment. > You have to call it with at least one fragment in your GraphQL document. You probably have called `cache.writeFragment` with a GraphQL document that doesn't contain a main fragment. This error occurs when no main fragment can be found, because the `DocumentNode` is maybe empty or does not contain fragments. When you're calling a fragment method, please ensure that you're only passing fragments in your GraphQL document. The first fragment will be used to start writing data. This also occurs when you pass in a `fragmentName` but a fragment with the given name can't be found in the `DocumentNode`. ## (12) Can't generate a key for writeFragment(...) or link(...) > Can't generate a key for writeFragment(...) [or link(...) data. > You have to pass an `id` or `_id` field or create a custom `keys` config for `???`. You probably have called `cache.writeFragment` or `cache.link` with data that the cache can't generate a key for. This may either happen because you're missing the `id` or `_id` field or some other fields for your custom `keys` config. Please make sure that you include enough properties on your data so that `writeFragment` or `cache.link` can generate a key. On `cache.link` the entities must either be an existing entity key, or a keyable entity. ## (13) Invalid undefined > Invalid undefined: The field at `???` is `undefined`, but the GraphQL query expects a > scalar (number, boolean, etc) / selection set for this field. As data is written to the cache, this warning is issued when `undefined` is encountered. GraphQL results should never contain an `undefined` value, so this warning will let you know the part of your result that did contain `undefined`. ## (14) Couldn't find \_\_typename when writing. > Couldn't find `__typename` when writing. > If you're writing to the cache manually have to pass a `__typename` property on each entity in your data. You probably have called `cache.writeFragment` or `cache.updateQuery` with data that is missing a `__typename` field for an entity where your document contains a selection set. The cache won't be able to generate a key for entities that are missing the `__typename` field. Please make sure that you include enough properties on your data so that `write` can generate a key. ## (15) Invalid key > Invalid key: The GraphQL query at the field at `???` has a selection set, > but no key could be generated for the data at this field. > You have to request `id` or `_id` fields for all selection sets or create a > custom `keys` config for `???`. > Entities without keys will be embedded directly on the parent entity. > If this is intentional, create a `keys` config for `???` that always returns null. This error occurs when the cache can't generate a key for an entity. The key would then effectively be `null`, and the entity won't be cached by a key. Conceptually this means that an entity won't be normalized but will indeed be cached by the parent's key and field, which is displayed in the first part of the warning. This may mean that you forgot to include an `id` or `_id` field. But if your entity at that place doesn't have any `id` fields, then you may have to create a custom `keys` config. This `keys` function either needs to return a unique ID for your entity, or it needs to explicitly return `null` to silence this warning. ## (16) Heuristic Fragment Matching > Heuristic Fragment Matching: A fragment is trying to match against the `???` type, > but the type condition is `???`. Since GraphQL allows for interfaces `???` may be > an interface. > A schema needs to be defined for this match to be deterministic, otherwise > the fragment will be matched heuristically! This warning is issued on fragment matching. Fragment matching is the process of matching a fragment against a piece of data in the cache and that data's `__typename` field. When the `__typename` field doesn't match the fragment's type, then we may be dealing with an interface and/or enum. In such a case the fragment may _still match_ if it's referring to an interface (`... on Interface`). Graphcache is supposed to be usable without much config, so what it does in this case is apply a heuristic match. In a heuristic fragment match we check whether all fields on the fragment are present in the cache, which is then treated as a fragment match. When you pass an introspected schema to the cache, this warning will never be displayed as the cache can then do deterministic fragment matching using schema information. ## (17) Invalid type > Invalid type: The type `???` is used with @populate but does not exist. When you're using the populate exchange with an introspected schema and add the `@populate` directive to fields it first checks whether the type is valid and exists on the schema. If the field does not have enough type information because it doesn't exist on the schema or does not match expectations then this warning is logged. Check whether your schema is up-to-date or whether you're using an invalid field somewhere, maybe due to a typo. ## (18) Invalid TypeInfo state > Invalid TypeInfo state: Found no flat schema type when one was expected. When you're using the populate exchange with an introspected schema, it will start collecting used fragments and selection sets on all of your queries. This error may occur if it hits unexpected types or inexistent types when doing so. Check whether your schema is up-to-date or whether you're using an invalid field somewhere, maybe due to a typo. Please open an issue if it happens on a query that you expect to be supported by the `populateExchange`. ## (19) Can't generate a key for invalidate(...) > Can't generate a key for invalidate(...). > You need to pass in a valid key (**typename:id) or an object with the "**typename" property and an "id" or "\_id" property. You probably have called `cache.invalidate` with data that the cache can't generate a key for. This may either happen because you're missing the `__typename` and `id` or `_id` field or if the last two aren't applicable to this entity a custom `keys` entry. ## (20) Invalid Object type > Invalid Object type: The type `???` is not an object in the defined schema, > but the `keys` option is referencing it. When you're passing an introspected schema to the cache exchange, it is able to check whether your `opts.keys` is valid. This error occurs when an unknown type is found in `opts.keys`. Check whether your schema is up-to-date, or whether you're using an invalid typename in `opts.keys`, maybe due to a typo. ## (21) Invalid updates type > Invalid updates field: The type `???` is not an object in the defined schema, > but the `updates` config is referencing it. When you're passing an introspected schema to the cache exchange, it is able to check whether your `opts.updates` config is valid. This error occurs when an unknown type is found in the `opts.updates` config. Check whether your schema is up-to-date, or whether you've got a typo in `opts.updates`. ## (22) Invalid updates field > Invalid updates field: `???` on `???` is not in the defined schema, > but the `updates` config is referencing it. When you're passing an introspected schema to the cache exchange, it is able to check whether your `opts.updates` config is valid. This error occurs when an unknown field is found in `opts.updates[typename]`. Check whether your schema is up-to-date, or whether you're using an invalid field name in `opts.updates`, maybe due to a typo. ## (23) Invalid resolver > Invalid resolver: `???` is not in the defined schema, but the `resolvers` > option is referencing it. When you're passing an introspected schema to the cache exchange, it is able to check whether your `opts.resolvers` is valid. This error occurs when an unknown query, type or field is found in `opts.resolvers`. Check whether your schema is up-to-date, or whether you've got a typo in `opts.resolvers`. ## (24) Invalid optimistic mutation > Invalid optimistic mutation field: `???` is not a mutation field in the defined schema, > but the `optimistic` option is referencing it. When you're passing an introspected schema to the cache exchange, it is able to check whether your `opts.optimistic` is valid. This error occurs when a field in `opts.optimistic` is not in the schema's `Mutation` fields. Check whether your schema is up-to-date, or whether you've got a typo in `Mutation` or `opts.optimistic`. ## (25) Invalid root traversal > Invalid root traversal: A selection was being read on `???` which is an uncached root type. > The `Mutation` and `Subscription` types are special Operation Root Types and cannot be read back > from the cache. In GraphQL every schema has three [Operation Root Types](https://spec.graphql.org/June2018/#sec-Root-Operation-Types). The `Query` type is the only one that is cached in Graphcache's normalized cache, since it's the root of all normalized cache data, i.e. all data is linked and connects back to the `Query` type. The `Subscription` and `Mutation` types are special and uncached; they may link to entities that will be updated in the normalized cache data, but are themselves not cached, since they're never directly queried. When your schema treats `Mutation` or `Subscription` like regular entity types you may get this warning. This may happen because you've used the default reserved names `Mutation` or `Subscription` for entities rather than as special Operation Root Types, and haven't specified this in the schema. Hence this issue can often be fixed by either enabling [Schema Awareness](./schema-awareness.md) or by adding a `schema` definition to your GraphQL Schema like so: ```graphql schema { query: Query mutation: YourMutation subscription: YourSubscription } ``` Where `YourMutation` and `YourSubscription` are your custom Operation Root Types, instead of relying on the default names `"Mutation"` and `"Subscription"`. ## (26) Invalid abstract resolver > Invalid resolver: `???` does not map to a concrete type in the schema, > but the resolvers option is referencing it. Implement the resolver for the types that `??` instead. When you're passing an introspected schema to the cache exchange, it is able to check whether your `opts.resolvers` is valid. This error occurs when you are using an `interface` or `union` rather than an implemented type for these. Check the type mentioned and change it to one of the specific types. ## (27) Invalid Cache write > Invalid Cache write: You may not write to the cache during cache reads. > Accesses to `cache.writeFragment`, `cache.updateQuery`, and `cache.link` may > not be made inside `resolvers` for instance. If you're using the `Cache` inside your `cacheExchange` config you receive it either inside callbacks that are called when the cache is queried (e.g. `resolvers`) or when data is written to the cache (e.g. `updates`). You may not write to the cache when it's being queried. Please make sure that you're not calling `cache.updateQuery`, `cache.writeFragment`, or `cache.link` inside `resolvers`. ## (28) Resolver and directive match the same field When you have a resolver defined on a field you shouln't be combining it with a directive as the directive will apply and the resolver will be void. ================================================ FILE: docs/graphcache/local-directives.md ================================================ --- title: Local Directives order: 3 --- # Local Directives Previously, we've learned about local resolvers [on the "Normalized Caching" page](./normalized-caching.md#manually-resolving-entities) and [the "Local Resolvers" page](./local-resolvers.md). Resolvers allow us to change the data that Graphcache resolvers for a given field on a given type. This, in turn, allows us to change which links and data are returned in a query’s result, which otherwise may not be cached or be returned in a different shape. Resolvers are useful to globally change how a field behaves, for instance, to tell Graphcache that a `Query.item(id: $id)` field returns an item of type `Item` with the `id` field, or to transform a value before it’s used in the UI. However, resolvers are limited to changing the behaviour globally, not to change a field’s behaviour per query. This is why **local directives** exist. ## Adding client-only directives Any directive in our GraphQL documents that’s prefixed with an underscore character (`_`) will be filtered by `@urql/core`. This means that our GraphQL API never sees it and it becomes a “client-only directive”. No matter whether we prefix a directive or not however, we can define local resolvers for directives in Graphcache’s configuration and make conditional local resolvers. ```js cacheExchange({ directives: { pagination(directiveArgs) { // This resolver is called for @_pagination directives return (parent, args, cache, info) => { return null; }; }, }, }); ``` Once we define a directive on the `directives` configuration object, we can reference it in our GraphQL queries. As per the above example, if we now reference `@_pagination` in a query, the resolver that’s returned in the configuration will be applied to the field, just like a local resolver. We can also reference the directive using `@pagination`, however, this will mean that it’s also sent to the API, so this usually isn’t what we want. ## Client-controlled Nullability Graphcache comes with two directives built-in by default. The `optional` and `required` directives. These directives can be used as an alternative to [the Schema Awareness feature’s](./schema-awareness.md) ability to generate partial results. If we were to write a query that contains `@_optional` on a field, then the field is always allowed to be nullable. In case it’s not cached, Graphcache will be able to replace it with a `null` value. Similarly, if we annotate a field with `@_required`, the value is not optional and, even if the cache knows the value is set to `null`, it will become required and Graphcache will either cascade to the next higher parent field annotated with `@_optional`, or will mark a query as a cache-miss. ## Pagination Previously, in [the “Local Resolvers” page’s Pagination section](./local-resolvers.md#pagination) we defined a local resolver to add infinite pagination to a given type’s field. If we add the `simplePagination` or `relayPagination` helpers as directives instead, we can still use our schema’s pagination field as normal, and instead, only use infinite pagination as required. ```js import { simplePagination } from '@urql/exchange-graphcache/extras'; import { relayPagination } from '@urql/exchange-graphcache/extras'; cacheExchange({ directives: { simplePagination: options => simplePagination({ ...options }), relayPagination: options => relayPagination({ ...options }), }, }); ``` Defining directives for our resolver factory functions means that we can now use them selectively. ```graphql { todos(first: 10) @_relayPagination(mergeMode: "outwards") { id text } } ``` ### Reading on [On the next page we'll learn about "Cache Updates".](./cache-updates.md) ================================================ FILE: docs/graphcache/local-resolvers.md ================================================ --- title: Local Resolvers order: 2 --- # Local Resolvers Previously, we've learned about local resolvers [on the "Normalized Caching" page](./normalized-caching.md#manually-resolving-entities). They allow us to change the data that Graphcache reads as it queries against its local cache, return links that would otherwise not be cached, or even transform scalar records on the fly. The `resolvers` option on `cacheExchange` accepts a map of types with a nested map of fields, which means that we can add local resolvers to any field of any type. For example: ```js cacheExchange({ resolvers: { Todo: { updatedAt: parent => new Date(parent.updatedAt), }, }, }); ``` In the above example, what Graphcache does when it encounters the `updatedAt` field on `Todo` types. Similarly to how Graphcache knows [how to generate keys](./normalized-caching.md#custom-keys-and-non-keyable-entities) and looks up our custom `keys` configuration functions per `__typename`, it also uses our `resolvers` configuration on each field it queries from its locally cached data. A local resolver function in Graphcache has a similar signature to [GraphQL.js' resolvers on the server-side](https://www.graphql-tools.com/docs/resolvers/), so their shape should look familiar to us. ```js { TypeName: { fieldName: (parent, args, cache, info) => { return null; // new value }, }, } ``` A resolver may be attached to any type's field and accepts four positional arguments: - `parent`: The object on which the field will be added to, which contains the data as it's being queried. It will contain the current field's raw value if it's a scalar, which allows us to manipulate scalar values, like `parent.updatedAt` in the previous example. - `args`: The arguments that the field is being called with, which will be replaced with an empty object if the field hasn't been called with any arguments. For example, if the field is queried as `name(capitalize: true)` then `args` would be `{ capitalize: true }`. - `cache`: Unlike in GraphQL.js this will not be the context, but a `cache` instance, which gives us access to methods allowing us to interact with the local cache. Its full API can be found [in the API docs](../api/graphcache.md#cache). - `info`: This argument shouldn't be used frequently, but it contains running information about the traversal of the query document. It allows us to make resolvers reusable or to retrieve information about the entire query. Its full API can be found [in the API docs](../api/graphcache.md#info). The local resolvers may return any value that fits the query document's shape, however we must ensure that what we return matches the types of our schema. It, for instance, isn't possible to turn a record field into a link, i.e. replace a scalar with an entity. Instead, local resolvers are useful to transform records, like dates in our previous example, or to imitate server-side logic to allow Graphcache to retrieve more data from its cache without sending a query to our API. Furthermore, while we see on this page that we get access to methods like `cache.resolve` and other methods to read from our cache, only ["Cache Updates"](./cache-updates.md) get to write and change the cache. If you call `cache.updateQuery`, `cache.writeFragment`, or `cache.link` in resolvers, you‘ll get an error, since it‘s not possible to update the cache while reading from it. When writing a resolver you’ll mostly use `cache.resolve`, which can be chained, to read field values from the cache. When a field points to another entity we may get a key, but resolvers are allowed to return keys or partial entities containing keys. > **Note:** This essentially means that resolvers can return either scalar values for fields without > selection sets, and either partial entities or keys for fields with selection sets, i.e. > links / relations. When we return `null`, this will be interpreted a the literal GraphQL Null scalar, > while returning `undefined` will cause a cache miss. ## Transforming Records As we've explored in the ["Normalized Caching" page's section on records](./normalized-caching.md#storing-normalized-data), "records" are scalars and any fields in your query without selection sets. This could be a field with a string value, number, or any other field that resolves to a [scalar type](https://graphql.org/learn/schema/#scalar-types) rather than another entity i.e. object type. At the beginning of this page we've already seen an example of a local resolver that we've attached to a record field where we've added a resolver to a `Todo.updatedAt` field: ```js cacheExchange({ resolvers: { Todo: { updatedAt: parent => new Date(parent.updatedAt), }, }, }); ``` A query that contains this field may look like `{ todo { updatedAt } }`, which clearly shows us that this field is a scalar since it doesn't have any selection set on the `updatedAt` field. In our example, we access this field's value and parse it as a `new Date()`. This shows us that it doesn't matter for scalar fields what kind of value we return. We may parse strings into more granular JS-native objects or replace values entirely. We may also run into situations where we'd like to generalise the resolver and not make it dependent on the exact field it's being attached to. In these cases, the [`info` object](../api/graphcache.md#info) can be very helpful as it provides us information about the current query traversal, and the part of the query document the cache is processing. The `info.fieldName` property is one of these properties and lets us know the field that the resolver is operating on. Hence, we can create a reusable resolver like so: ```js const transformToDate = (parent, _args, _cache, info) => new Date(parent[info.fieldName]); cacheExchange({ resolvers: { Todo: { updatedAt: transformToDate }, }, }); ``` The resolver is now much more reusable, which is particularly handy if we're creating resolvers that we'd like to apply to multiple fields. The [`info` object has several more fields](../api/graphcache.md#info) that are all similarly useful to abstract our resolvers. We also haven't seen yet how to handle a field's arguments. If we have a field that accepts arguments we can use those as well as they're passed to us with the second argument of a resolver: ```js cacheExchange({ resolvers: { Todo: { text: (parent, args) => { return args.capitalize && parent.text ? parent.text.toUpperCase() : parent.text; }, }, }, }); ``` This is actually unlikely to be of use with records and scalar values as our API will have to be able to use these arguments just as well. In other words, while you may be able to pass any arguments to a field in your query, your GraphQL API's schema must accept these arguments in the first place. However, this is still useful if we're trying to imitate what the API is doing, which will become more relevant in the following examples and sections. ## Resolving Entities We've already briefly seen that resolvers can be used to replace a link in Graphcache's local data on the ["Normalized Caching" page](./normalized-caching.md#manually-resolving-entities). Given that Graphcache [stores entities in a normalized data structure](./normalized-caching.md#storing-normalized-data) there may be multiple fields on a given schema that can be used to get to the same entity. For instance, the schema may allow for the same entity to be looked up by an ID while this entity may also appear somewhere else in a list or on an entirely different field. When links (or relations) like these are cached by Graphcache it is able to look up the entities automatically, e.g. if we've sent a `{ todo(id: 1) { id } }` query to our API once then Graphcache will have seen that this field leads to the entity it returns and can query it automatically from its cache. However, if we have a list like `{ todos { id } }` we may have seen and cached a specific entity, but as we browse the app and query for `{ todo(id: 1) { id } }`, Graphcache isn't able to automatically find this entity even if it has cached it already and will send a request to our API. In many cases we can create a local resolvers to instead tell the cache where to look for a specific entity by returning partial information for it. Any resolver on a relational field, meaning any field that links to an object type (or a list of object types) in the schema, may return a partial entity that tells the cache how to resolve it. Hence, we're able to implement a resolver for the previously shown `todo(id: $id)` field as such: ```js cacheExchange({ resolvers: { Query: { todo: (_, args) => ({ __typename: 'Todo', id: args.id }), }, }, }); ``` The `__typename` field is required. Graphcache will [use its keying logic](./normalized-caching.md#custom-keys-and-non-keyable-entities), and your custom `keys` configuration to generate a key for this entity and will then be able to look this entity up in its local cache. As with regular queries, the resolver is known to return a link since the `todo(id: $id) { id }` will be used with a selection set, querying fields on the entity. ### Resolving by keys Resolvers can also directly return keys. We've previously learned [on the "Normalized Caching" page](./normalized-caching.md#custom-keys-and-non-keyable-entities) that the key for our example above would look something like `"Todo:1"` for `todo(id: 1)`. While it isn't advisable to create keys manually in your resolvers, if you returned a key directly this would still work. Essentially, returning `{ __typename, id }` may sometimes be the same as returning the key manually. The `cache` that we receive as an argument on resolvers has a method for this logic, [the `cache.keyOfEntity` method](../api/graphcache.md#keyofentity). While it doesn't make much sense in this case, our example can be rewritten as: ```js cacheExchange({ resolvers: { Query: { todo: (_, args, cache) => cache.keyOfEntity({ __typename: 'Todo', id: args.id }), }, }, }); ``` And while it's not advisable to create keys ourselves, the resolvers' `cache` and `info` arguments give us ample opportunities to use and pass around keys. One example is the `info.parentKey` property. This property [on the `info` object](../api/graphcache.md#info) will always be set to the key of the entity that the resolver is currently run on. For instance, for the above resolver it may be `"Query"`, for for a resolver on `Todo.updatedAt` it may be `"Todo:1"`. ## Resolving other fields In the above two examples we've seen how a resolver can replace Graphcache's logic, which usually reads links and records only from its locally cached data. We've seen how a field on a record can use `parent[fieldName]` to access its cached record value and transform it and how a resolver for a link can return a partial entity [or a key](#resolving-by-keys). However sometimes we'll need to resolve data from other fields in our resolvers. > **Note:** For records, if the other field is on the same `parent` entity, it may seem logical to access it on > `parent[otherFieldName]` as well, however the `parent` object will only be sparsely populated with > fields that the cache has already queried prior to reaching the resolver. > In the previous example, where we've created a resolver for `Todo.updatedAt` and accessed > `parent.updatedAt` to transform its value the `parent.updatedAt` field is essentially a shortcut > that allows us to get to the record quickly. Instead we can use [the `cache.resolve` method](../api/graphcache.md#resolve). This method allows us to access Graphcache's cached data directly. It is used to resolve records or links on any given entity and accepts three arguments: - `entity`: This is the entity on which we'd like to access a field. We may either pass a keyable, partial entity, e.g. `{ __typename: 'Todo', id: 1 }` or a key. It takes the same inputs as [the `cache.keyOfEntity` method](../api/graphcache.md#keyofentity), which we've seen earlier in the ["Resolving by keys" section](#resolving-by-keys). It also accepts `null` which causes it to return `null`, which is useful for chaining multiple `resolve` calls for deeply accessing a field. - `fieldName`: This is the field's name we'd like to access. If we're looking for the record on `Todo.updatedAt` we would pass `"updatedAt"` and would receive the record value for this field. If we pass a field that is a _link_ to another entity then we'd pass that field's name (e.g. `"author"` for `Todo.author`) and `cache.resolve` will return a key instead of a record value. - `fieldArgs`: Optionally, as the third argument we may pass the field's arguments, e.g. `{ id: 1 }` if we're trying to access `todo(id: 1)` for instance. This means that we can rewrite our original `Todo.updatedAt` example as follows, if we'd like to avoid using the `parent[fieldName]` shortcut: ```js cacheExchange({ resolvers: { Todo: { updatedAt: (parent, _args, cache) => new Date(cache.resolve(parent, 'updatedAt')), }, }, }); ``` When we call `cache.resolve(parent, "updatedAt")`, the cache will look up the `"updatedAt"` field on the `parent` entity, i.e. on the current `Todo` entity. > **Note:** We've also previously learned that `parent` may not contain all fields that the entity may have and > may hence be missing its keyable fields, like `id`, so why does this then work? > It works because `cache.resolve(parent)` is a shortcut for `cache.resolve(info.parentKey)`. Like the `info.fieldName` property `info.parentKey` gives us information about the current state of Graphcache's query operation. In this case, `info.parentKey` tells us what the parent's key is. However, since `cache.resolve(parent)` is much more intuitive we can write that instead since this is a supported shortcut. From this follows that we may also use `cache.resolve` to access other fields. Let's suppose we'd want `updatedAt` to default to the entity's `createdAt` field when it's actually `null`. In such a case we could write a resolver like so: ```js cacheExchange({ resolvers: { Todo: { updatedAt: (parent, _args, cache) => parent.updatedAt || cache.resolve(parent, 'createdAt'), }, }, }); ``` As we can see, we're effortlessly able to access other records from the cache, provided these fields are actually cached. If they aren't `cache.resolve` will return `null` instead. Beyond records, we're also able to resolve links and hence jump to records from another entity. Let's suppose we have an `author { id, createdAt }` field on the `Todo` and would like `Todo.createdAt` to simply copy the author's `createdAt` field. We can chain `cache.resolve` calls to get to this value: ```js cacheExchange({ resolvers: { Todo: { createdAt: (parent, _args, cache) => cache.resolve(cache.resolve(parent, 'author') /* "Author:1" */, 'createdAt'), }, }, }); ``` The return value of `cache.resolve` changes depending on what data the cache has stored. While it may return records for fields without selection sets, in other cases it may give you the key of other entities ("links") instead. It can even give you arrays of keys or records when the field's value contains a list. When a value is not present in the cache, `cache.resolve` will instead return `undefined` to signal that a value is uncached. Similarly, a resolver may return `undefined` to tell Graphcache that the field isn’t cached and that a call to the API is necessary. `cache.resolve` is a pretty flexible method that allows us to access arbitrary values from our cache, however, we have to be careful about what value will be resolved by it, since the cache can't know itself what type of value it may return. The last trick this method allows you to apply is to access arbitrary fields on the root `Query` type. If we call `cache.resolve("Query", ...)` then we're also able to access arbitrary fields starting from the root `Query` of the cached data. (If you're using [Schema Awareness](./schema-awareness.md) the name `"Query"` may vary for you depending on your schema.) We're not constrained to accessing fields on the `parent` of a resolver but can also attempt to break out and access fields on any other entity we know of. ## Resolving Partial Data Local resolvers also allow for more advanced use-cases when it comes to links and object types. Previously we've seen how a resolver is able to link up a given field to an entity, which causes this field to resolve an entity directly instead of it being checked against any cached links: ```js cacheExchange({ resolvers: { Query: { todo: (_, args) => ({ __typename: 'Todo', id: args.id }), }, }, }); ``` In this example, while `__typename` and `id` are required to make this entity keyable, we're also able to add on more fields to this object to override values later on in our selection. For instance, we can write a resolver that links `Query.todo` directly to our `Todo` entity but also only updates the `createdAt` field directly in the same resolver, if it is indeed accessed via the `Query.todo` field: ```js cacheExchange({ resolvers: { Query: { todo: (_, args) => ({ __typename: 'Todo', id: args.id, createdAt: new Date().toString(), }), }, }, }); ``` Here we've replaced the `createdAt` value of the `Todo` when it's accessed via this manual resolver. If it was accessed someplace else, for instance via a `Query.todos` listing field, this override wouldn't apply. We can even apply overrides to nested fields, which helps us to create complex resolvers for other use cases like pagination. [Read more on the topic of "Pagination" in the section below.](#pagination) ## Computed Queries We've now seen how the `cache` has several powerful methods, like [the `cache.resolve` method](../api/graphcache.md#resolve), which allow us to access any data in the cache while writing resolvers for individual fields. Additionally the cache has more methods that allow us to access more data at a time, like `cache.readQuery` and `cache.readFragment`. ### Reading a query At any point, the `cache` allows us to read entirely separate queries in our resolvers, which starts a separate virtual operation in our resolvers. When we call `cache.readQuery` with a query and variables we can execute an entirely new GraphQL query against our cached data: ```js import { gql } from '@urql/core'; import { cacheExchange } from '@urql/exchange-graphcache'; const cache = cacheExchange({ updates: { Mutation: { addTodo: (result, args, cache) => { const data = cache.readQuery({ query: Todos, variables: { from: 0, limit: 10 } }); }, }, }, }); ``` This way we'll get the stored data for the `TodosQuery` for the given `variables`. [Read more about `cache.readQuery` in the Graphcache API docs.](../api/graphcache.md#readquery) ### Reading a fragment The store also allows us to read a fragment for any given entity. The `cache.readFragment` method accepts a `fragment` and an `id`. This looks like the following. ```js import { gql } from '@urql/core'; import { cacheExchange } from '@urql/exchange-graphcache'; const cache = cacheExchange({ resolvers: { Query: { Todo: (parent, args, cache) => { return cache.readFragment( gql` fragment _ on Todo { id text } `, { id: 1 } ); }, }, }, }); ``` > **Note:** In the above example, we've used > [the `gql` tag function](../api/core.md#gql) because `readFragment` only accepts > GraphQL `DocumentNode`s as inputs, and not strings. This way we'll read the entire fragment that we've passed for the `Todo` for the given key, in this case `{ id: 1 }`. [Read more about `cache.readFragment` in the Graphcache API docs.](../api/graphcache.md#readfragment) ### Cache methods outside of `resolvers` The cache read methods are not possible outside of GraphQL operations. This means these methods will be limited to the different `Graphcache` configuration methods. ## Living with limitations of Local Resolvers Local Resolvers are powerful tools using which we can tell Graphcache what to do with a certain field beyond using results it’s seen on prior API results. However, it’s limitations come from this very intention they were made for. Resolvers are meant to augment Graphcache and teach it what to do with some fields. Sometimes this is trivial and simple (like most examples on this page), but other times, fields are incredibly complex to reproduce and hence resolvers become more complex. This section is not exhaustive, but documents some of the more commonly asked for features of resolvers. However, beyond the cases listed below, resolvers are limited and: - can't manipulate or see other fields on the current entity, or fields above it. - can't update the cache (they're only “computations” but don't change the cache) - can't change the query document that's sent to the API ### Writing reusable resolvers As we've seen before in the ["Transforming Records" section above](#transforming-records), we can write generic resolvers by using the fourth argument that resolvers receive, the `ResolveInfo` object. This `info` object gives our resolvers some context on where they’re being executed and gives it information about the current field and its surroundings. For instance, while Graphcache has a convenience helper to access a current record on the parent object for scalar values, it doesn't for links. Hence, if we're trying to read relationships we have to use `cache.resolve`. ```js cacheExchange({ resolvers: { Todo: { // This works: updatedAt: parent => parent.updatedAt, // This won't work: author: parent => parent.author, }, }, }); ``` The `info` object actually gives us two ways of accessing the original field's value: ```js const resolver = (parent, args, cache, info) => { // This is the full version const original = cache.resolve(info.parentKey, info.fieldName, args); // But we can replace `info.parentKey` with `parent` as a shortcut const original = cache.resolve(parent, info.fieldName, args); // And we can also avoid re-using arguments by using `fieldKey` const original = cache.resolve(parent, info.fieldKey); }; ``` Apart from telling us how to access the originally cached field value, we can also get more information from `info` about our field. For instance, we can: - Read the current field's name using `info.fieldName` - Read the current field's key using `info.parentFieldKey` - Read the current parent entity's key using `info.parentKey` - Read the current parent entity's typename using `info.parentTypename` - Access the current operation's raw variables using `info.variables` - Access the current operation's raw fragments using `info.fragments` ### Causing cache misses and partial misses When we write resolvers we provide Graphcache with a value for the current field, or rather with "behavior", that it will execute no matter whether this field is also cached or not. This means that, unless our resolver returns `undefined`, if the query doesn't have any other cache misses, Graphcache will consider the field a cache hit and will, unless other cache misses occur, not make a network request. > **Note:** An exception for this is [Schema Awareness](./schema-awareness.md), which can > automatically cause partial cache misses. However, sometimes we may want a resolver to return a result, while still sending a GraphQL API request in the background to update our resolver’s values. To achieve this we can update the `info.partial` field. ```js cacheExchange({ resolvers: { Todo: { author(parent, args, cache, info) { const author = cache.resolve(parent, info.fieldKey); if (author === null) { info.partial = true; } return author; }, }, }, }); ``` Suppose we have a field that our GraphQL schema _sometimes_ returns a `null` value for, but that may be upated with a value in the future. In the above example, we wrote a resolver that sets `info.partial = true` if a field’s value is `null`. This causes Graphcache to consider the result “partial and stale” and will cause it to make a background request to the API, while still delivering the outdated result. ### Conditionally applying resolvers We may not always want a resolver to be used. While sometimes this can be dangerous (if your resolver affects the shape and types of your fields), in other cases this is necessary. For instance, if your resolver handles infinite-scroll pagination, like the examples [in the next section](#pagination), then you may not always want to apply this resolver. For this reason, Graphcache also supports [“local directives”, which are introduced on the next docs page.](./local-directives.md) ## Pagination `Graphcache` offers some preset `resolvers` to help us out with endless scrolling pagination, also known as "infinite pagination". It comes with two more advanced but generalised resolvers that can be applied to two specific pagination use-cases. They're not meant to implement infinite pagination for _any app_, instead they're useful when we'd like to add infinite pagination to an app quickly to try it out or if we're unable to replace it with separate components per page in environments like React Native, where a `FlatList` would require a flat, infinite list of items. > **Note:** If you don't need a flat array of results, you can also achieve infinite pagination > with only UI code. [You can find a code example of UI infinite pagination in our example folder.](https://github.com/urql-graphql/urql/tree/main/examples/with-pagination) [You can find a code example of infinite pagination with Graphcahce in our example folder.](https://github.com/urql-graphql/urql/tree/main/examples/with-graphcache-pagination). Please keep in mind that this patterns has some limitations when you're handling cache updates. Deleting old pages from the cache selectively may be difficult, so the UI pattern in the above note is preferred. ### Simple Pagination Given we have a schema that uses some form of `offset` and `limit` based pagination, we can use the `simplePagination` exported from `@urql/exchange-graphcache/extras` to achieve an endless scroller. This helper will concatenate all queries performed to one long data structure. ```js import { cacheExchange } from '@urql/exchange-graphcache'; import { simplePagination } from '@urql/exchange-graphcache/extras'; const cache = cacheExchange({ resolvers: { Query: { todos: simplePagination(), }, }, }); ``` This form of pagination accepts an object as an argument, we can specify two options in here `limitArgument` and `offsetArgument` these will default to `limit` and `skip` respectively. This way we can use the keywords that are in our queries. We may also add the `mergeMode` option, which defaults to `'after'` and can otherwise be set to `'before'`. This will handle in which order pages are merged when paginating. The default `after` mode assumes that pages that come in last should be merged _after_ the first pages. The `'before'` mode assumes that pages that come in last should be merged _before_ the first pages, which can be helpful in a reverse endless scroller (E.g. Chat App). Example series of requests: ``` // An example where mergeMode: after works better skip: 0, limit: 3 => 1, 2, 3 skip: 3, limit: 3 => 4, 5, 6 mergeMode: after => 1, 2, 3, 4, 5, 6 ✔️ mergeMode: before => 4, 5, 6, 1, 2, 3 // An example where mergeMode: before works better skip: 0, limit: 3 => 4, 5, 6 skip: 3, limit: 3 => 1, 2, 3 mergeMode: after => 4, 5, 6, 1, 2, 3 mergeMode: before => 1, 2, 3, 4, 5, 6 ✔️ ``` ### Relay Pagination Given we have a [relay-compatible schema](https://facebook.github.io/relay/graphql/connections.htm) on our backend, we can offer the possibility of endless data resolving. This means that when we fetch the next page in our data received in `useQuery` we'll see the previous pages as well. This is useful for endless scrolling. We can achieve this by importing `relayPagination` from `@urql/exchange-graphcache/extras`. ```js import { cacheExchange } from '@urql/exchange-graphcache'; import { relayPagination } from '@urql/exchange-graphcache/extras'; const cache = cacheExchange({ resolvers: { Query: { todos: relayPagination(), }, // Or if the pagination happens in a nested field: User: { todos: relayPagination(), }, }, }); ``` `relayPagination` accepts an object of options, for now we are offering one option and that is the `mergeMode`. This defaults to `inwards` and can otherwise be set to `outwards`. This will handle how pages are merged when we paginate forwards and backwards at the same time. outwards pagination assumes that pages that come in last should be merged before the first pages, so that the list grows outwards in both directions. The default inwards pagination assumes that pagination last pages is part of the same list and come after first pages. Hence it merges pages so that they converge in the middle. Example series of requests: ``` first: 1 => node 1, endCursor: a first: 1, after: a => node 2, endCursor: b ... last: 1 => node 99, startCursor: c last: 1, before: c => node 89, startCursor: d ``` With inwards merging the nodes will be in this order: `[1, 2, ..., 89, 99]` And with outwards merging: `[..., 89, 99, 1, 2, ...]` The helper happily supports schema that return nodes rather than individually-cursored edges. For each paginated type, we must either always request nodes, or always request edges -- otherwise the lists cannot be stiched together. ### Reading on [On the next page we'll learn about "Cache Directives".](./local-directives.md) ================================================ FILE: docs/graphcache/normalized-caching.md ================================================ --- title: Normalized Caching order: 1 --- # Normalized Caching In GraphQL, like its name suggests, we create schemas that express the relational nature of our data. When we create and query against a `Query` type we walk a graph that starts at the root `Query` type and walks through relational types. Rather than querying for normalized data, in GraphQL our queries request a specific shape of denormalized data, a view into our relational data that can be re-normalized automatically. As the GraphQL API walks our query documents it may read from a relational database and _entities_ and scalar values are copied into a JSON document that matches our query document. The type information of our entities isn't lost however. A query document may still ask the GraphQL API about what entity it's dealing with using the `__typename` field, which dynamically introspects an entity's type. This means that GraphQL clients can automatically re-normalize data as results come back from the API by using the `__typename` field and keyable fields like an `id` or `_id` field, which are already common conventions in GraphQL schemas. In other words, normalized caches can build up a relational database of tables in-memory for our application. For our apps normalized caches can enable more sophisticated use-cases, where different API requests update data in other parts of the app and automatically update data in our cache as we query our GraphQL API. Normalized caches can essentially keep the UI of our applications up-to-date when relational data is detected across multiple queries, mutations, or subscriptions. ## Normalizing Relational Data As previously mentioned, a GraphQL schema creates a tree of types where our application's data always starts from the `Query` root type and is modified by other data that's incoming from either a selection on `Mutation` or `Subscription`. All data that we query from the `Query` type will contain relations between "entities", JSON objects that are hierarchical. A normalized cache seeks to turn this denormalized JSON blob back into a relational data structure, which stores all entities by a key that can be looked up directly. Since GraphQL documents give the API a strict specification on how it traverses a schema, the JSON data that the cache receives from the API will always match the GraphQL query document that has been used to query this data. A common misconception is that normalized caches in GraphQL store data by the query document somehow, however, the only thing a normalized cache cares about is that it can use our GraphQL query documents to walk the structure of the JSON data it received from the API. ```graphql { __typename todo(id: 1) { __typename id title author { __typename id name } } } ``` ```json { "__typename": "Query", "todo": { "__typename": "Todo", "id": 1, "title": "implement graphcache", "author": { "__typename": "Author", "id": 1, "name": "urql-team" } } } ``` Above, we see an example of a GraphQL query document and a corresponding JSON result from a GraphQL API. In GraphQL, we never lose access to the underlying types of the data. Normalized caches can ask for the `__typename` field in selection sets automatically and will find out which type a JSON object corresponds to. Generally, a normalized cache must do one of two things with a query document like the above: - It must be able to walk the query document and JSON data of the result and cache the data, normalizing it in the process and storing it in relational tables. - It must later be able to walk the query document and recreate this JSON data just by reading data from its cache, by reading entries from its in-memory relational tables. While the normalized cache can't know the exact type of each field, thanks to the GraphQL query language it can make a couple of assumptions. The normalized cache can walk the query document. Each field that has no selection set (like `title` in the above example) must be a "record", a field that may only be set to a scalar. Each field that does have a selection set must be another "entity" or a list of "entities". The latter fields with selection sets are our relations between entities, like a foreign key in relational databases. Furthermore, the normalized cache can then read the `__typename` field on related entities. This is called _Type Name Introspection_ and is how it finds out about the types of each entity. From the above document we can assume the following relations: - `Query.todo(id: 1)` → `Todo` - `Todo.author` → `Author` However, this isn't quite enough yet to store the relations from GraphQL results. The normalized cache must also generate primary keys for each entity so that it can store them in table-like data structures. This is for instance why [Relay enforces](https://relay.dev/docs/guides/graphql-server-specification/#object-identification) that each entity must have an `id` field. This allows it to assume that there's an obvious primary key for each entity it may query. Instead, `urql`'s Graphcache and Apollo assume that there _may_ be an `id` or `_id` field in a given selection set. If Graphcache can't find these two fields it'll issue a warning, however a custom `keys` configuration may be used to generate custom keys for a given type. With this logic the normalized cache will actually create the following "links" between its relational data: - `"Query"`, `.todo(id: 1)` → `"Todo:1"` - `"Todo:1"`, `.author` → `"Author:1"` As we can see, the `Query` root type itself has a constant key of `"Query"`. All relational data originates here, since the GraphQL schema is a graph and, like a tree, all selections on a GraphQL query document originate from it. Internally, the normalized cache now stores field values on entities by their primary keys. The above can also be said or written as: - The `Query` entity's `todo` field with `{"id": 1}` arguments points to the `Todo:1` entity. - The `Todo:1` entity's `author` field points to the `Author:1` entity. In Graphcache, these "links" are stored in a nested structure per-entity. "Records" are kept separate from this relational data. ![Normalization is based on types, keys, and relations. This information can all be inferred from the query document.](../assets/query-document-info.png) ## Storing Normalized Data At its core, normalizing data means that we take individual fields and store them in a table. In our case we store all values of fields in a dictionary of their primary key, generated from an ID or other key and type name, and the field’s name and arguments, if it has any. | Primary Key | Field | Value | | ---------------------- | ----------------------------------------------- | ------------------------ | | Type name and ID (Key) | Field name (not alias) and optionally arguments | Scalar value or relation | To reiterate we have three pieces of information that are stored in tables: - The entity's key can be derived from its type name via the `__typename` field and a keyable field. By default _Graphcache_ will check the `id` and `_id` fields, however this is configurable. - The field's name (like `todo`) and optional arguments. If the field has any arguments then we can normalize it by JSON stringifying the arguments, making sure that the JSON key is stable by sorting its keys. - Lastly, we may store relations as either `null`, a primary key that refers to another entity, or a list of such. For storing "records" we can store the scalars in a separate table. In _Graphcache_ the data structure for these tables looks a little like the following, where each entity has a record from fields to other entity keys: ```js { links: Map { 'Query': Record { 'todo({"id":1})': 'Todo:1' }, 'Todo:1': Record { 'author': 'Author:1' }, 'Author:1': Record { }, } } ``` We can see how the normalized cache is now able to traverse a GraphQL query by starting on the `Query` entity and retrieve relations for other fields. To retrieve "records" which are all fields with scalar values and no selection sets, _Graphcache_ keeps a second table around with an identical structure. This table only contains scalar values, which keeps our non-relational data away from our "links": ```js { records: Map { 'Query': Record { '__typename': 'Query' }, 'Todo:1': Record { '__typename': 'Todo', 'id': 1, 'title': 'implement graphcache' }, 'Author:1': Record { '__typename': 'Author', 'id': 1, 'name': 'urql-team' }, } } ``` This is very similar to how we'd go about creating a state management store manually, except that _Graphcache_ can use the GraphQL document to perform this normalization automatically. What we gain from this normalization is that we have a data structure that we can both read from and write to, to reproduce the API results for GraphQL query documents. Any mutation or subscription can also be written to this data structure. Once _Graphcache_ finds a keyable entity in their results it's written to its relational table which may update other queries in our application. Similarly queries may share data between one another which means that they effectively share entities using this approach and can update one another. In other words, once we have a primary key like `"Todo:1"` we may find this primary key again in other entities in other GraphQL results. ## Custom Keys and Non-Keyable Entities In the above introduction we've learned that while _Graphcache_ doesn't enforce `id` fields on each entity, it checks for the `id` and `_id` fields by default. There are many situations in which entities may either not have a key field or have different keys. As _Graphcache_ traverses JSON data and a GraphQL query document to write data to the cache you may see a warning from it along the lines of ["Invalid key: [...] No key could be generated for the data at this field."](./errors.md/#15-invalid-key) _Graphcache_ has many warnings like these that attempt to detect undesirable behaviour and helps us to update our configuration or queries accordingly. In the simplest cases, we may simply have forgotten to add the `id` field to the selection set of our GraphQL query document. However, what if the field is instead called `uuid` and our query looks accordingly different? ```graphql { item { uuid } } ``` In the above selection set we have an `item` field that has a `uuid` field rather than an `id` field. This means that _Graphcache_ won't automatically be able to generate a primary key for this entity. Instead, we have to help it generate a key by passing it a custom `keys` config: ```js cacheExchange({ keys: { Item: data => data.uuid, }, }); ``` We may add a function as an entry to the `keys` configuration. The property here, `"Item"` must be the typename of the entity for which we're generating a key. The function may return an arbitarily generated key. So for our `item` field, which in our example schema gives us an `Item` entity, we can create a `keys` configuration entry that creates a key from the `uuid` field rather than the `id` field. This also raises a question, **what does _Graphcache_ do with unkeyable data by default? And, what if my data has no key?**
This special case is what we call "embedded data". Not all types in a GraphQL schema will have keyable fields and some types may just abstract data without themselves being relational. They may be "edges", entities that have a field pointing to other entities that simply connect two entities, or data types like a `GeoJson` or `Image` type. In these cases, where the normalized cache encounters unkeyable types, it will create an embedded key by using the parent's primary key and combining it with the field key. This means that "embedded entities" are only reachable from a specific field on their parent entities. They're globally unique and aren't strictly speaking relational data. ```graphql { __typename todo(id: 1) { id image { url width height } } } ``` In the above example we're querying an `Image` type on a `Todo`. This imaginary `Image` type has no key because the image is embedded data and will only ever be associated to this `Todo`. In other words, the API's schema doesn't consider it necessary to have a primary key field for this type. Maybe it doesn't even have an ID in our backend's database. We _could_ assign this type an imaginary key (maybe based on the `url`) but in fact if it's not shared data it wouldn't make much sense to do so. When _Graphcache_ attempts to store this entity it will issue the previously mentioned warning. Internally, it'll then generate an embedded key for this entity based on the parent entity. If the parent entity's key is `Todo:1` then the embedded key for our `Image` will become `Todo:1.image`. This is also how this entity will be stored internally by _Graphcache_: ```js { records: Map { 'Todo:1.image': Record { '__typename': 'Image', 'url': '...', 'width': 1024, 'height': 768 }, } } ``` This doesn't however mute the warning that _Graphcache_ outputs, since it believes we may have made a mistake. The warning itself gives us advice on how to mute it: > If this is intentional, create a keys config for `Image` that always returns null. Meaning, that we can add an entry to our `keys` config for our non-keyable type that explicitly returns `null`, which tells _Graphcache_ that the entity has no key: ```js cacheExchange({ keys: { Image: () => null, }, }); ``` ### Flexible Key Generation In some cases, you may want to create a pattern for your key generation. For instance, you may want to say "create a special key for every type ending in `'Node'`. In such a case we recommend creating a small JS `Proxy` to take care of key generation for you and making the keys functional. ```js cacheExchange({ keys: new Proxy( { Image: () => null, }, { get(target, prop, receiver) { if (prop.endsWith('Node')) { return data => data.uid; } const fallback = data => data.uuid; return target[prop] || fallback; }, } ), }); ``` In the above example, we dynamically change the key generator depending on the typename. When a typename ends in `'Node'`, we return a key generator that uses the `uid` field. We still fall back to an object of manual key generation functions however. Lastly though, when a type doesn't have a predefined key generator, we change the default behavior from using `id` and `_id` fields to using `uuid` fields. ## Non-Automatic Relations and Updates While _Graphcache_ is able to store and update our entities in an in-memory relational data structure, which keeps the same entities in singular unique locations, a GraphQL API may make a lot of implicit changes to the relations of data as it runs or have trivial relations that our cache doesn't need to see to resolve. Like with the `keys` config, we have two more configuration options to combat this: `resolvers` and `updates`. ### Manually resolving entities Some fields in our configuration can be resolved without checking the GraphQL API for relations. The `resolvers` config allows us to create a list of client-side resolvers where we can read from the cache directly as _Graphcache_ creates a local GraphQL result from its cached data. ```graphql { todo(id: 1) { id } } ``` Previously we've looked at the above query to illustrate how data from a GraphQL API may be written to _Graphcache_'s relational data structure to store the links and entities in a result against this GraphQL query document. However, it may be possible for another query to have already written this `Todo` entity to the cache. So, **how do we resolve a relation manually?** In such a case, _Graphcache_ may have seen and stored the `Todo` entity but isn't aware of the relation between `Query.todo({"id":1})` and the `Todo:1` entity. However, we can tell _Graphcache_ which entity it should look for when it accesses the `Query.todo` field by creating a resolver for it: ```js cacheExchange({ resolvers: { Query: { todo(parent, args, cache, info) { return { __typename: 'Todo', id: args.id }; }, }, }, }); ``` A resolver is a function that's similar to [GraphQL.js' resolvers on the server-side](https://www.graphql-tools.com/docs/resolvers/). They receive the parent data, the field's arguments, access to _Graphcache_'s cached data, and an `info` object. [The entire function signature and more explanations can be found in the API docs.](../api/graphcache.md#resolvers-option) Since it can access the field's arguments from the GraphQL query document, we can return a partial `Todo` entity. As long as this object is keyable, it will tell _Graphcache_ what the key of the returned entity is. In other words, we've told it how to get to a `Todo` from the `Query.todo` field. This mechanism is immensely more powerful than this example. We have other use-cases that resolvers may be used for: - Resolvers can be applied to fields with records, which means that it can be used to change or transform scalar values. For instance, we can update a string or parse a `Date` right inside a resolver. - Resolvers can return deeply nested results, which will be layered on top of the in-memory relational cached data of _Graphcache_, which means that it can emulate infinite pagination and other complex behaviour. - Resolvers can change when a cache miss or hit occurs. Returning `null` means that a field’s value is literally `null`, which will not cause a cache miss, while returning `undefined` will mean a field’s value is uncached. - Resolvers can return either partial entities or keys, so we can chain `cache.resolve` calls to read fields from the cache, even when a field is pointing at another entity, since we can return keys to the other entity directly. [Read more about resolvers on the following page about "Local Resolvers".](./local-resolvers.md) ### Manual cache updates While `resolvers`, as shown above, operate while _Graphcache_ is reading from its in-memory cache, `updates` are a configuration option that operate while _Graphcache_ is writing to its cached data. Specifically, these functions can be used to add more updates onto what a `Mutation` or `Subscription` may automatically update. As stated before, a GraphQL schema's data may undergo a lot of implicit changes when we send it a `Mutation` or `Subscription`. A new item that we create may for instance manipulate a completely different item or even a list. Often mutations and subscriptions alter relations that their selection sets wouldn't necessarily see. Since mutations and subscriptions operate on a different root type, rather than the `Query` root type, we often need to update links in the rest of our data when a mutation is executed. ```graphql query TodosList { todos { id title } } mutation AddTodo($title: String!) { addTodo(title: $title) { id title } } ``` In a simple example, like the one above, we have a list of todos in a query and create a new todo using the `Mutation.addTodo` mutation field. When the mutation is executed and we get the result back, _Graphcache_ already writes the `Todo` item to its normalized cache. However, we also want to add the new `Todo` item to the list on `Query.todos`: ```js import { gql } from '@urql/core'; cacheExchange({ updates: { Mutation: { addTodo(result, args, cache, info) { const query = gql` { todos { id } } `; cache.updateQuery({ query }, data => { data.todos.push(result.addTodo); return data; }); }, }, }, }); ``` In this code example we can first see that the signature of the `updates` entry is very similar to the one of `resolvers`. However, we're seeing the `cache` in use for the first time. The `cache` object (as [documented in the API docs](../api/graphcache.md#cache)) gives us access to _Graphcache_'s mechanisms directly. Not only can we resolve data using it, we can directly start sub-queries or sub-writes manually. These are full normalized cache runs inside other runs. In this case we're calling `cache.updateQuery` on a list of `Todo` items while the `Mutation` that added the `Todo` is already being written to the cache. As we can see, we may perform manual changes inside of `updates` functions, which can be used to affect other parts of the cache (like `Query.todos` here) beyond the automatic updates that a normalized cache is expected to perform. We get methods like `cache.updateQuery`, `cache.writeFragment`, and `cache.link` in our updater functions, which aren't available to us in local resolvers, and can only be used in these `updates` entries to change the data that the cache holds. [Read more about writing cache updates on the "Cache Updates" page.](./cache-updates.md) ## Deterministic Cache Updates Above, in [the "Storing Normalized Data" section](#storing-normalized-data), we've talked about how Graphcache is able to store normalized data. However, apart from storing this data there are a couple of caveats that many applications simply ignore, skip, or simplify when they implement a store to cache their data in. Amongst features like [Optimistic Updates](./cache-updates.md#optimistic-updates) and [Offline Support](./offline.md), Graphcache supports several features that allow our API results to be more unreliable. Essentially we don't expect API results to always come back in order or on time. However, we expect Graphcache to prevent us from making "indeterministic cache updates", meaning that we expect it to handle API results that come back in a random order and delayed gracefully. In terms of the ["Manual Cache Updates"](#manual-cache-updates) that we've talked about above and [Optimistic Updates](./cache-updates.md#optimistic-updates) the limitations are pretty simple at first and if we use Graphcache as usual we may not even notice them: - When we make an _optimistic_ change, we define what a mutation's result may look like once the API responds in the future and apply this temporary result immediately. We store this temporary data in a separate "layer". Once the real result comes back this layer can be deleted and the real API result can be applied as usual. - When multiple _optimistic updates_ are made at the same time, we never allow these layers to be deleted separately. Instead Graphcache waits for all mutations to complete before deleting the optimistic layers and applying the real API result. This means that a mutation update cannot accidentally commit optimistic data to the cache permanently. - While an _optimistic update_ has been applied, Graphcache stops refetching any queries that contain this optimistic data so that it doesn't "flip back" to its non-optimistic state without the optimistic update being applied. Otherwise we'd see a "flicker" in the UI. These three principles are the basic mechanisms we can expect from Graphcache. The summary is: **Graphcache groups optimistic mutations and pauses queries so that optimistic updates look as expected,** which is an implementation detail we can mostly ignore when using it. However, one implementation detail we cannot ignore is the last mechanism in Graphcache which is called **"Commutativity"**. As we can tell, "optimistic updates" need to store their normalized results on a separate layer. This means that the previous data structure we've seen in Graphcache is actually more like a list, with many tables of links and entities. Each layer may contain optimistic results and have an order of preference. However, this order also applies to queries. Since queries are run in one order but their API results can come back to us in a very different order, if we access enough pages in a random order things can sometimes look rather weird. We may see that in an application on a slow network connection the results may vary depending on when their results came back. ![Commutativity means that we store data in separate layers.](../assets/commutative-layers.png) Instead, Graphcache actually uses layers for any API result it receives. In case, an API result arrives out-of-order, it sorts them by precedence — or rather by when they've been requested. Overall, we don't have to worry about this, but Graphcache has mechanisms that keep our updates safe. ## Reading on This concludes the introduction to Graphcache with a short overview of how it works, what it supports, and some hidden mechanisms and internals. Next we may want to learn more about how to use it and more of its features: - [How do we write "Local Resolvers"?](./local-resolvers.md) - [How to set up "Cache Updates" and "Optimistic Updates"?](./cache-updates.md) - [What is Graphcache's "Schema Awareness" feature for?](./schema-awareness.md) - [How do I enable "Offline Support"?](./offline.md) ================================================ FILE: docs/graphcache/offline.md ================================================ --- title: Offline Support order: 7 --- # Offline Support _Graphcache_ allows you to build an offline-first app with built-in offline and persistence support, by adding a `storage` interface. In combination with its [Schema Awareness](./schema-awareness.md) support and [Optimistic Updates](./cache-updates.md#optimistic-updates) this can be used to build an application that serves cached data entirely from memory when a user's device is offline and still display optimistically executed mutations. ## Setup Everything that's needed to set up offline-support is already packaged in the `@urql/exchange-graphcache` package. We initially recommend setting up the [Schema Awareness](./schema-awareness.md). This adds our server-side schema information to the cache, which allows it to make decisions on what partial data complies with the schema. This is useful since the offline cache may often be lacking some data but may then be used to display the partial data we do have, as long as missing data is actually marked as optional in the schema. Furthermore, if we have any mutations that the user doesn't interact with after triggering them (for instance, "liking a post"), we can set up [Optimistic Updates](./cache-updates.md#optimistic-updates) for these mutations, which allows them to be reflected in our UI before sending a request to the API. To actually now set up offline support, we'll swap out the `cacheExchange` with the `offlineExchange` that's also exported by `@urql/exchange-graphcache`. ```js import { Client, fetchExchange } from 'urql'; import { offlineExchange } from '@urql/exchange-graphcache'; const cache = offlineExchange({ schema, updates: { /* ... */ }, optimistic: { /* ... */ }, }); const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cache, fetchExchange], }); ``` This activates offline support, however we'll also need to provide the `storage` option to the `offlineExchange`. The `storage` is an adapter that contains methods for storing cache data in a persisted storage interface on the user's device. By default, we can use the default storage option that `@urql/exchange-graphcache` comes with. This default storage uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) to persist the cache's data. We can use this default storage by importing the `makeDefaultStorage` function from `@urql/exchange-graphcache/default-storage`. ```js import { Client, fetchExchange } from 'urql'; import { offlineExchange } from '@urql/exchange-graphcache'; import { makeDefaultStorage } from '@urql/exchange-graphcache/default-storage'; const storage = makeDefaultStorage({ idbName: 'graphcache-v3', // The name of the IndexedDB database maxAge: 7, // The maximum age of the persisted data in days }); const cache = offlineExchange({ schema, storage, updates: { /* ... */ }, optimistic: { /* ... */ }, }); const client = new Client({ url: 'http://localhost:3000/graphql', exchanges: [cache, fetchExchange], }); ``` ## React Native For React Native, we can use the async storage package `@urql/storage-rn`. Before installing the [library](https://github.com/urql-graphql/urql/tree/main/packages/storage-rn), ensure you have installed the necessary peer dependencies: - NetInfo ([RN](https://github.com/react-native-netinfo/react-native-netinfo) | [Expo](https://docs.expo.dev/versions/latest/sdk/netinfo/)) and - AsyncStorage ([RN](https://react-native-async-storage.github.io/async-storage/docs/install) | [Expo](https://docs.expo.dev/versions/v42.0.0/sdk/async-storage/)). ```sh yarn add @urql/storage-rn # or npm install --save @urql/storage-rn ``` You can then create the custom storage and use it in the offline exchange: ```js import { makeAsyncStorage } from '@urql/storage-rn'; const storage = makeAsyncStorage({ dataKey: 'graphcache-data', // The AsyncStorage key used for the data (defaults to graphcache-data) metadataKey: 'graphcache-metadata', // The AsyncStorage key used for the metadata (defaults to graphcache-metadata) maxAge: 7, // How long to persist the data in storage (defaults to 7 days) }); ``` ## Offline Behavior _Graphcache_ applies several mechanisms that improve the consistency of the cache and how it behaves when it's used in highly cached-dependent scenarios, including when it's used with its offline support. We've previously read about some of these guarantees on the ["Normalized Caching" page.](./normalized-caching.md) While the client is offline, _Graphcache_ will also apply some opinionated mechanisms to queries and mutations. When a query fails with a Network Error, which indicates that the client is offline the `offlineExchange` won't deliver the error for this query to avoid it from being surfaced to the user. This works particularly well in combination with ["Schema Awareness"](./schema-awareness.md) which will deliver as much of a partial query result as possible. In combination with the [`cache-and-network` request policy](../basics/document-caching.md#request-policies) we can now ensure that we display as much data as possible when the user is offline while still keeping the cache up-to-date when the user is online. A similar mechanism is applied to optimistic mutations when the user is offline. Normal non-optimistic mutations are executed as usual and may fail with a network error. Optimistic mutations however will be queued up and may be retried when the app is restarted or when the user comes back online. If we wish to customize when an operation result from the API is deemed an operation that has failed because the device is offline, we can pass a custom `isOfflineError` function to the `offlineExchange`, like so: ```js const cache = offlineExchange({ isOfflineError(error, _result) { return !!error.networkError; }, // ... }); ``` However, this is optional, and the default function checks for common offline error messages and checks `navigator.onLine` for you. ## Custom Storages In the [Setup section](#setup) we've learned how to use the default storage engine to store persisted cache data in IndexedDB. You can also write custom storage engines, if the default one doesn't align with your expectations or requirements. One limitation of our default storage engine is for instance that data is stored time limited with a maximum age, which prevents the database from becoming too full, but a custom storage engine may have different strategies for dealing with this. [The API docs list the entire interface for the `storage` option.](../api/graphcache.md#storage-option) There we can see the methods we need to implement to implement a custom storage engine. Following is an example of the simplest possible storage engine, which uses the browser's [Local Storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Initially we'll implement the basic persistence methods, `readData` and `writeData`. ```js const makeLocalStorage = () => { const cache = {}; return { writeData(delta) { return Promise.resolve().then(() => { Object.assign(cache, delta); localStorage.setItem('data', JSON.stringify(cache)); }); }, readData() { return Promise.resolve().then(() => { const local = localStorage.getItem('data') || null; Object.assign(cache, JSON.parse(local)); return cache; }); }, }; }; ``` As we can see, the `writeData` method only sends us "deltas", partial objects that only describe updated cache data rather than all cache data. The implementation of `writeMetadata` and `readMetadata` will however be even simpler, since it always sends us complete data. ```js const makeLocalStorage = () => { return { /* ... */ writeMetadata(data) { localStorage.setItem('metadata', JSON.stringify(data)); }, readMetadata() { return Promise.resolve().then(() => { const metadataJson = localStorage.getItem('metadata') || null; return JSON.parse(metadataJson); }); }, }; }; ``` Lastly, the `onOnline` method will likely always look the same, as long as your `storage` is intended to work for browsers only: ```js const makeLocalStorage = () => { return { /* ... */ onOnline(cb: () => void) { window.addEventListener('online', () => { cb(); }); }, }; }; ``` ================================================ FILE: docs/graphcache/schema-awareness.md ================================================ --- title: Schema Awareness order: 5 --- # Schema Awareness Previously, [on the "Normalized Caching" page](./normalized-caching.md) we've seen how Graphcache stores normalized data in its store and how it traverses GraphQL documents to do so. What we've seen is that just using the GraphQL document for traversal, and the `__typename` introspection field Graphcache is able to build a normalized caching structure that keeps our application up-to-date across API results, allows it to store data by entities and keys, and provides us configuration options to write [manual cache updates](./cache-updates.md) and [local resolvers](./local-resolvers.md). While this is all possible without any information about a GraphQL API's schema, the `schema` option on `cacheExchange` allows us to pass an introspected schema to Graphcache: ```js const introspectedSchema = { __schema: { queryType: { name: 'Query' }, mutationType: { name: 'Mutation' }, subscriptionType: { name: 'Subscription' }, }, }; cacheExchange({ schema: introspectedSchema }); ``` In GraphQL, [APIs allow for the entire schema to be "introspected"](https://graphql.org/learn/introspection/), which are special GraphQL queries that give us information on what the API supports. This information can either be retrieved from a GraphQL API directly or from the GraphQL.js Schema and contains a list of all types, the types' fields, scalars, and other information. In Graphcache we can pass this schema information to enable several features that aren't enabled if we don't pass any information to this option: - Fragments will be matched deterministically: A fragment can be written to be on an interface type or multiple fragments can be spread for separate union'ed types in a selection set. In many cases, if Graphcache doesn't have any schema information then it won't know what possible types a field can return and may sometimes make a guess and [issue a warning](./errors.md#16-heuristic-fragment-matching). If we pass Graphcache a `schema` then it'll be able to match fragments deterministically. - A schema may have non-default names for its root types; `Query`, `Mutation`, and `Subscription`. The names can be changed by passing `schema` information to `cacheExchange` which is important if the root type appears elsewhere in the schema, e.g. if the `Query` can be accessed on a `Mutation` field's result. - We may write a lot of configuration for our `cacheExchange` but if we pass a `schema` then it'll start checking whether any of the configuration options actually don't exist, maybe because we've typo'd them. This is a small detail but can make a large difference in a longer configuration. - Lastly; a schema contains information on **which fields are optional or required**. When Graphcache has a schema it knows optional fields that may be left out, and it'll be able to generate "partial results". ### Partial Results As we navigate an app that uses Graphcache we may be in states where some of our data is already cached while some aren't. Graphcache normalizes data and stores it in tables for links and records for each entity, which means that sometimes it can maybe even execute a query against its cache that it hasn't sent to the API before. [On the "Local Resolvers" page](./local-resolvers.md#resolving-entities) we've seen how to write resolvers that resolve entities without having to have seen a link from an API result before. If Graphcache uses these resolvers and previously cached data we often run into situations where a "partial result" could already be generated, which is what Graphcache does when it has `schema` information. ![A "partial result" is an incomplete result of information that Graphcache already had cached before it sent an API result.](../assets/partial-results.png) Without a `schema` and information on which fields are optional, Graphcache will consider a "partial result" as a cache miss. If we don't have all the information for a query then we can't execute it against the locally cached data after all. However, an API's schema contains information on which fields are required and optional, and if our apps are typed with this schema and TypeScript, can't we then use and handle these partial results before a request is sent to the API? This is the idea behind "Schema Awareness" and "Partial Results". When Graphcache has `schema` information it may give us partial results [with the `stale` flag set](../api/core.md#operationresult) while it fetches the full result from the API in the background. This allows our apps to show some information while more is loading. ## Getting your schema But how do you get an introspected `schema`? The process of introspecting a schema is running an introspection query on the GraphQL API, which will give us our `IntrospectionQuery` result. So an introspection is just another query we can run against our GraphQL APIs or schemas. As long as `introspection` is turned on and permitted, we can download an introspection schema by running a normal GraphQL query against the API and save the result in a JSON file. ```js import { getIntrospectionQuery } from 'graphql'; import fetch from 'node-fetch'; // or your preferred request in Node.js import * as fs from 'fs'; fetch('http://localhost:3000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ variables: {}, query: getIntrospectionQuery({ descriptions: false }), }), }) .then(result => result.json()) .then(({ data }) => { fs.writeFile('./schema.json', JSON.stringify(data), err => { if (err) { console.error('Writing failed:', err); return; } console.log('Schema written!'); }); }); ``` Alternatively, if you're already using [GraphQL Code Generator](https://graphql-code-generator.com/) you can use [their `@graphql-codegen/introspection` plugin](https://graphql-code-generator.com/docs/plugins/introspection) to do the same automatically against a local schema. Furthermore it's also possible to [`execute`](https://graphql.org/graphql-js/execution/#execute) the introspection query directly against your `GraphQLSchema`. ## Optimizing a schema An `IntrospectionQuery` JSON blob from a GraphQL API can without modification become quite large. The shape of this data is `{ "__schema": ... }` and this _schema_ data will contain information on all directives, types, input objects, scalars, deprecation, enums, and more. This can quickly add up and one of the largest schemas, the GitHub GraphQL API's schema, has an introspection size of about 1.1MB, or about 50KB gzipped. However, we can use the `@urql/introspection` package's `minifyIntrospectionQuery` helper to reduce the size of this introspection data. This helper strips out information on directives, scalars, input types, deprecation, enums, and redundant fields to only leave information that _Graphcache_ actually requires. In the example of the GitHub GraphQL API this reduces the introspected data to around 20kB gzipped, which is much more acceptable. ### Installation & Setup First, install the `@urql/introspection` package: ```sh yarn add @urql/introspection # or npm install --save @urql/introspection ``` You'll then need to integrate it into your introspection script or in another place where it can optimise the introspection data. For this example, we'll just add it to the fetching script from [above](#getting-your-schema). ```js import { getIntrospectionQuery } from 'graphql'; import fetch from 'node-fetch'; // or your preferred request in Node.js import * as fs from 'fs'; import { getIntrospectedSchema, minifyIntrospectionQuery } from '@urql/introspection'; fetch('http://localhost:3000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ variables: {}, query: getIntrospectionQuery({ descriptions: false }), }), }) .then(result => result.json()) .then(({ data }) => { const minified = minifyIntrospectionQuery(getIntrospectedSchema(data)); fs.writeFileSync('./schema.json', JSON.stringify(minified)); }); ``` The `getIntrospectionSchema ` doesn't only accept `IntrospectionQuery` JSON data as inputs, but also allows you to pass a JSON string, `GraphQLSchema`, or GraphQL Schema SDL strings. It's a convenience helper and not needed in the above example. ## Integrating a schema Once we have a schema that's already saved to a JSON file, we can load it and pass it to the `cacheExchange`'s `schema` option: ```js import schema from './schema.json'; const cache = cacheExchange({ schema }); ``` It may be worth checking what your bundler or framework does when you import a JSON file. Typically you can reduce the parsing time by making sure it's turned into a string and parsed using `JSON.parse` ================================================ FILE: docs/showcase.md ================================================ --- title: Showcase order: 7 --- # Showcase `urql` wouldn't be the same without our growing and loving community of users, maintainers and supporters. This page is specifically dedicated to all of you! ## Used by folks at TripAdvisor GitHub Egghead Gatsby The Atlantic loveholidays Swan Open Social Sturdy ## Articles & Tutorials - [Egghead Course](https://egghead.io/lessons/graphql-set-up-an-urql-graphql-provider-in-react?pl=introduction-to-urql-a-react-graphql-client-faaa2bf5) by [Ian Jones](https://twitter.com/_jonesian). - [How To GraphQL: React + urql](https://www.howtographql.com/react-urql/0-introduction/) ## Community packages - [`reason-urql`](https://github.com/FormidableLabs/reason-urql): The official Reason bindings for `urql`. - [`urql-persisted-queries`](https://github.com/Daniel15/urql-persisted-queries): Support for Apollo-style persisted queries. - [`urql-computed-queries`](https://github.com/Drawbotics/urql-computed-exchange): An exchange to compute fields on-the-fly using the `@computed` directive. - [`graphql-code-generator`](https://graphql-code-generator.com/docs/plugins/typescript-urql): A plugin that helps you make typesafe hooks/components with urql. - [`urql-custom-scalars-exchange`](https://github.com/clentfort/urql-custom-scalars-exchange): An exchange to automatically convert scalars. - [`@grafbase/urql-exchange`](https://github.com/grafbase/playground/tree/main/packages/grafbase-urql-exchange): URQL-exchange for handling Server Sent Events (SSE) with Grafbase GraphQL Live Queries. to automatically convert scalars. - [`urql-rest-exchange`](https://github.com/iamsavani/urql-rest-exchange): A custom exchange for URQL that supports GraphQL queries/mutations via REST endpoints - [`urql-exhaustive-additional-typenames-exchange`](https://github.com/route06/urql-exhaustive-additional-typenames-exchange): URQL-exchange that add all list fields of the operation to additionalTypenames to help document caching ================================================ FILE: examples/README.md ================================================
Example
with-react
Shows a basic query in urql with React. Open in StackBlitz Open in CodeSandbox
with-react-native
Shows a basic query in urql with React Native. Open in StackBlitz Open in CodeSandbox
with-svelte
Shows a basic query in @urql/svelte with Svelte. Open in StackBlitz Open in CodeSandbox
with-vue3
Shows a basic query in @urql/vue with Vue 3. Open in StackBlitz Open in CodeSandbox
with-next
Shows a basic query in next-urql with Next.js. Open in StackBlitz Open in CodeSandbox
with-pagination
Shows how to generically set up pagination with urql in UI code. Open in StackBlitz Open in CodeSandbox
with-infinite-pagination
Shows how to generically set up infinite scrolling pagination with urql in UI code. Open in StackBlitz Open in CodeSandbox
with-apq
Shows Automatic Persisted Queries with @urql/exchange-persisted. Open in StackBlitz Open in CodeSandbox
with-graphcache-updates
Shows manual cache updates with @urql/exchange-graphcache. Open in StackBlitz Open in CodeSandbox
with-graphcache-pagination
Shows the automatic infinite pagination helpers from @urql/exchange-graphcache. Open in StackBlitz Open in CodeSandbox
with-multipart
Shows file upload support integrated in @urql/core. Open in StackBlitz Open in CodeSandbox
with-refresh-auth
Shows authentication with refresh tokens using @urql/exchange-auth. Open in StackBlitz Open in CodeSandbox
with-retry
Shows retrying of failed operations with @urql/exchange-retry. Open in StackBlitz Open in CodeSandbox
with-defer-stream-directives
Demonstrates urql and @urql/exchange-graphcache with built-in support for @defer and @stream. Open in StackBlitz Open in CodeSandbox
with-subscriptions-via-fetch
Demonstrates @urql/core's built-in support for executing subscriptions with a GraphQL Yoga API via the fetchExchange. Open in StackBlitz Open in CodeSandbox
================================================ FILE: examples/pnpm-workspace.yaml ================================================ packages: - '*' ================================================ FILE: examples/with-apq/README.md ================================================ # With Automatic Persisted Queries

Open in StackBlitz Open in CodeSandbox

This example shows `urql` in use with `@urql/exchange-persisted-fetch`'s `persistedFetchExchange` to support [Automatic Persisted Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/). This largely follows the ["Persisted Queries" docs page](https://formidable.com/open-source/urql/docs/advanced/persistence-and-uploads/#automatic-persisted-queries) and uses the [`trygql.formidable.dev/graphql/apq-weather` schema](https://github.com/FormidableLabs/trygql). To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/App.jsx`](src/App.jsx) - The `persistedFetchExchange` from `@urql/exchange-persisted-fetch` in [`src/App.jsx`](src/App.jsx) - A query for locations in [`src/LocationsList.jsx`](src/pages/LocationsList.jsx) ================================================ FILE: examples/with-apq/index.html ================================================ with-apq
================================================ FILE: examples/with-apq/package.json ================================================ { "name": "with-apq", "version": "0.0.0", "private": true, "scripts": { "start": "vite" }, "dependencies": { "@urql/core": "^6.0.1", "@urql/exchange-persisted": "^5.0.1", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-apq/src/App.jsx ================================================ import React from 'react'; import { Client, Provider, fetchExchange } from 'urql'; import { persistedExchange } from '@urql/exchange-persisted'; import LocationsList from './LocationsList'; const client = new Client({ url: 'https://trygql.formidable.dev/graphql/apq-weather', exchanges: [ persistedExchange({ preferGetForPersistedQueries: true, }), fetchExchange, ], }); function App() { return ( ); } export default App; ================================================ FILE: examples/with-apq/src/LocationsList.jsx ================================================ import React from 'react'; import { gql, useQuery } from 'urql'; const LOCATIONS_QUERY = gql` query Locations($query: String!) { locations(query: $query) { id name } } `; const LocationsList = () => { const [result] = useQuery({ query: LOCATIONS_QUERY, variables: { query: 'LON' }, }); const { data, fetching, error } = result; return (
{fetching &&

Loading...

} {error &&

Oh no... {error.message}

} {data && (
    {data.locations.map(location => (
  • {location.name}
  • ))}
)}
); }; export default LocationsList; ================================================ FILE: examples/with-apq/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-apq/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-defer-stream-directives/README.md ================================================ # With `@defer` / `@stream` Directives

Open in StackBlitz Open in CodeSandbox

This example shows `urql` in use [with `@defer` and `@stream` directives](https://graphql.org/blog/2020-12-08-improving-latency-with-defer-and-stream-directives) in GraphQL. To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/App.jsx`](src/App.jsx) - A local `graphql-yoga` server set up to test deferred and streamed results in [`server/`](server/). ================================================ FILE: examples/with-defer-stream-directives/index.html ================================================ with-defer-stream-directives
================================================ FILE: examples/with-defer-stream-directives/package.json ================================================ { "name": "with-defer-stream-directives", "version": "0.0.0", "private": true, "scripts": { "server:apollo": "node server/apollo-server.js", "server:yoga": "node server/graphql-yoga.js", "client": "vite", "start": "run-p client server:yoga" }, "pnpm": { "peerDependencyRules": { "allowedVersions": { "graphql": "17" } } }, "dependencies": { "@graphql-yoga/plugin-defer-stream": "^1.7.1", "@urql/core": "^6.0.1", "@urql/exchange-graphcache": "^9.0.0", "graphql": "17.0.0-alpha.2", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@apollo/server": "^4.4.1", "@vitejs/plugin-react": "^3.1.0", "graphql-yoga": "^3.7.1", "npm-run-all": "^4.1.5", "vite": "^4.2.0" } } ================================================ FILE: examples/with-defer-stream-directives/server/apollo-server.js ================================================ // NOTE: This currently fails because responses for @defer/@stream are not sent // as multipart responses, but the request fails silently with an empty JSON response payload const { ApolloServer } = require('@apollo/server'); const { startStandaloneServer } = require('@apollo/server/standalone'); const { schema } = require('./schema'); const server = new ApolloServer({ schema }); startStandaloneServer(server, { listen: { port: 3004, }, }); ================================================ FILE: examples/with-defer-stream-directives/server/graphql-yoga.js ================================================ const { createYoga } = require('graphql-yoga'); const { useDeferStream } = require('@graphql-yoga/plugin-defer-stream'); const { createServer } = require('http'); const { schema } = require('./schema'); const yoga = createYoga({ schema, plugins: [useDeferStream()], }); const server = createServer(yoga); server.listen(3004); ================================================ FILE: examples/with-defer-stream-directives/server/schema.js ================================================ const { GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString, } = require('graphql'); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: () => ({ alphabet: { type: new GraphQLList( new GraphQLObjectType({ name: 'Alphabet', fields: { char: { type: GraphQLString, }, }, }) ), resolve: async function* () { for (let letter = 65; letter <= 90; letter++) { await new Promise(resolve => setTimeout(resolve, 500)); yield { char: String.fromCharCode(letter) }; } }, }, song: { type: new GraphQLObjectType({ name: 'Song', fields: () => ({ firstVerse: { type: GraphQLString, resolve: () => "Now I know my ABC's.", }, secondVerse: { type: GraphQLString, resolve: () => new Promise(resolve => setTimeout( () => resolve("Next time won't you sing with me?"), 5000 ) ), }, }), }), resolve: () => new Promise(resolve => setTimeout(() => resolve('goodbye'), 1000)), }, }), }), }); module.exports = { schema }; ================================================ FILE: examples/with-defer-stream-directives/src/App.jsx ================================================ import React from 'react'; import { Client, Provider, fetchExchange } from 'urql'; import { cacheExchange } from '@urql/exchange-graphcache'; import Songs from './Songs'; const cache = cacheExchange({ keys: { Alphabet: data => data.char, Song: () => null, }, }); const client = new Client({ url: 'http://localhost:3004/graphql', exchanges: [cache, fetchExchange], }); function App() { return ( ); } export default App; ================================================ FILE: examples/with-defer-stream-directives/src/Songs.jsx ================================================ import React from 'react'; import { gql, useQuery } from 'urql'; const SecondVerseFragment = gql` fragment secondVerseFields on Song { secondVerse } `; const SONGS_QUERY = gql` query App_Query { song { firstVerse ...secondVerseFields @defer } alphabet @stream(initialCount: 3) { char } } ${SecondVerseFragment} `; const Song = React.memo(function Song({ song }) { return (

{song.firstVerse}

{song.secondVerse}

); }); const LocationsList = () => { const [result] = useQuery({ query: SONGS_QUERY, }); const { data } = result; return (
{data && ( <> {data.alphabet.map(i => (
{i.char}
))} )}
); }; export default LocationsList; ================================================ FILE: examples/with-defer-stream-directives/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-defer-stream-directives/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-graphcache-pagination/README.md ================================================ # With Graphcache's Pagination

Open in StackBlitz Open in CodeSandbox

This example shows `urql` in use with `@urql/exchange-graphcache`'s infinite pagination helpers to merge several pages of a Relay-compliant schema into an infinite list. This largely follows the ["Pagination" section on the "Local Resolvers" docs page](https://formidable.com/open-source/urql/docs/graphcache/local-resolvers/#pagination) and uses the [`trygql.formidable.dev/graphql/relay-npm` schema](https://github.com/FormidableLabs/trygql). To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/App.js`](src/App.js) - The `cacheExchange` from `@urql/exchange-graphcache` in [`src/App.js`](src/App.js) - A paginated query for packages in [`src/pages/PaginatedNpmSearch.js`](src/pages/PaginatedNpmSearch.js) ================================================ FILE: examples/with-graphcache-pagination/index.html ================================================ with-graphcache-pagination
================================================ FILE: examples/with-graphcache-pagination/package.json ================================================ { "name": "with-graphcache-pagination", "version": "0.0.0", "private": true, "scripts": { "start": "vite" }, "dependencies": { "@urql/core": "^6.0.1", "@urql/exchange-graphcache": "^9.0.0", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-graphcache-pagination/src/App.jsx ================================================ import React from 'react'; import { Client, Provider, fetchExchange } from 'urql'; import { cacheExchange } from '@urql/exchange-graphcache'; import { relayPagination } from '@urql/exchange-graphcache/extras'; import PaginatedNpmSearch from './PaginatedNpmSearch'; const client = new Client({ url: 'https://trygql.formidable.dev/graphql/relay-npm', exchanges: [ cacheExchange({ resolvers: { Query: { search: relayPagination(), }, }, }), fetchExchange, ], }); function App() { return ( ); } export default App; ================================================ FILE: examples/with-graphcache-pagination/src/PaginatedNpmSearch.jsx ================================================ import React, { useState } from 'react'; import { gql, useQuery } from 'urql'; const limit = 5; const query = 'graphql'; const NPM_SEARCH = gql` query Search($query: String!, $first: Int!, $after: String) { search(query: $query, first: $first, after: $after) { edges { node { id name } } pageInfo { hasNextPage endCursor } } } `; const PaginatedNpmSearch = () => { const [after, setAfter] = useState(''); const [result] = useQuery({ query: NPM_SEARCH, variables: { query, first: limit, after }, }); const { data, fetching, error } = result; const searchResults = data?.search; return (
{error &&

Oh no... {error.message}

} {fetching &&

Loading...

} {searchResults && ( <> {searchResults.edges.map(({ node }) => (
{node.id}: {node.name}
))} {searchResults.pageInfo.hasNextPage && ( )} )}
); }; export default PaginatedNpmSearch; ================================================ FILE: examples/with-graphcache-pagination/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-graphcache-pagination/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-graphcache-updates/README.md ================================================ # With Graphcache's Pagination

Open in StackBlitz Open in CodeSandbox

This example shows `urql` in use with `@urql/exchange-graphcache` and demonstrates a manual cache update, as explained in [the "Cache Updates" docs page](https://formidable.com/open-source/urql/docs/graphcache/cache-updates/). This example uses the [`trygql.formidable.dev/graphql/web-collections` schema](https://github.com/FormidableLabs/trygql) and builds on top of the [`with-refresh-auth` example](../with-refresh-auth) so that we can authenticate with the schema before creating links on it. To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/client.js`](src/client.js) - The `cacheExchange` from `@urql/exchange-graphcache` in [`src/client.js`](src/client.js) - A links list and link creation in [`src/pages/Links.jsx`](src/pages/Links.jsx) ================================================ FILE: examples/with-graphcache-updates/index.html ================================================ with-graphcache-updates
================================================ FILE: examples/with-graphcache-updates/package.json ================================================ { "name": "with-graphcache-updates", "version": "0.0.0", "private": true, "scripts": { "start": "vite" }, "dependencies": { "@urql/core": "^6.0.1", "@urql/exchange-auth": "^3.0.0", "@urql/exchange-graphcache": "^9.0.0", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-graphcache-updates/src/App.jsx ================================================ import React, { useState, useEffect } from 'react'; import { Provider } from 'urql'; import client from './client'; import Links from './pages/Links'; import LoginForm from './pages/LoginForm'; const Home = () => { const [isLoggedIn, setIsLoggedIn] = useState(false); const onLoginSuccess = auth => { localStorage.setItem('authToken', auth.token); setIsLoggedIn(true); }; useEffect(() => { if (localStorage.getItem('authToken')) { setIsLoggedIn(true); } }, []); return isLoggedIn ? : ; }; function App() { return ( ); } export default App; ================================================ FILE: examples/with-graphcache-updates/src/client.js ================================================ import { Client, fetchExchange, gql } from 'urql'; import { authExchange } from '@urql/exchange-auth'; import { cacheExchange } from '@urql/exchange-graphcache'; const cache = cacheExchange({ updates: { Mutation: { createLink(result, _args, cache, _info) { const LinksList = gql` query Links($first: Int!) { links(first: $first) { nodes { id } } } `; const linksPages = cache .inspectFields('Query') .filter(field => field.fieldName === 'links'); if (linksPages.length > 0) { const lastField = linksPages[linksPages.length - 1]; cache.updateQuery( { query: LinksList, variables: { first: lastField.arguments.first }, }, data => { return { ...data, links: { ...data.links, nodes: [...data.links.nodes, result.createLink.node], }, }; } ); } }, }, }, }); const auth = authExchange(async utilities => { let token = localStorage.getItem('authToken'); return { addAuthToOperation(operation) { if (!token) return operation; return token ? utilities.appendHeaders(operation, { Authorization: `Bearer ${token}`, }) : operation; }, didAuthError(error) { return error.graphQLErrors.some( e => e.extensions?.code === 'UNAUTHORIZED' ); }, willAuthError(operation) { if (!token) { // Detect our login mutation and let this operation through: return ( operation.kind !== 'mutation' || // Here we find any mutation definition with the "signin" field !operation.query.definitions.some(definition => { return ( definition.kind === 'OperationDefinition' && definition.selectionSet.selections.some(node => { // The field name is just an example, since register may also be an exception return node.kind === 'Field' && node.name.value === 'signin'; }) ); }) ); } return false; }, async refreshAuth() { token = localStorage.getItem('authToken'); if (!token) { // This is where auth has gone wrong and we need to clean up and redirect to a login page localStorage.clear(); window.location.reload(); } }, }; }); const client = new Client({ url: 'https://trygql.formidable.dev/graphql/web-collections', exchanges: [cache, auth, fetchExchange], }); export default client; ================================================ FILE: examples/with-graphcache-updates/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-graphcache-updates/src/pages/Links.jsx ================================================ import React from 'react'; import { gql, useQuery, useMutation } from 'urql'; const LINKS_QUERY = gql` query Links($first: Int!) { links(first: $first) { nodes { id title canonicalUrl } } } `; const CREATE_LINK_MUTATION = gql` mutation CreateLink($url: URL!) { createLink(url: $url) { node { id title canonicalUrl } } } `; const Links = () => { const [linksResult] = useQuery({ query: LINKS_QUERY, variables: { first: 10 }, }); const [createResult, createLink] = useMutation(CREATE_LINK_MUTATION); const onSubmitLink = event => { event.preventDefault(); const { target } = event; createLink({ url: new FormData(target).get('link') }).then(() => target.reset() ); }; return (
{linksResult.error &&

Oh no... {linksResult.error.message}

} {linksResult.data && ( )}
{createResult.fetching ?

Submitting...

: null} {createResult.error ? (

Oh no... {createResult.error.message}

) : null}
); }; export default Links; ================================================ FILE: examples/with-graphcache-updates/src/pages/LoginForm.jsx ================================================ import React from 'react'; import { gql, useMutation } from 'urql'; const LOGIN_MUTATION = gql` mutation Login($input: LoginInput!) { signin(input: $input) { refreshToken token } } `; const REGISTER_MUTATION = gql` mutation Register($input: LoginInput!) { register(input: $input) { refreshToken token } } `; const LoginForm = ({ onLoginSuccess }) => { const [loginResult, login] = useMutation(LOGIN_MUTATION); const [registerResult, register] = useMutation(REGISTER_MUTATION); const onSubmitLogin = event => { event.preventDefault(); const data = new FormData(event.target); const username = data.get('username'); const password = data.get('password'); login({ input: { username, password } }).then(result => { if (!result.error && result.data && result.data.signin) { onLoginSuccess(result.data.signin); } }); }; const onSubmitRegister = event => { event.preventDefault(); const data = new FormData(event.target); const username = data.get('username'); const password = data.get('password'); register({ input: { username, password } }).then(result => { if (!result.error && result.data && result.data.register) { onLoginSuccess(result.data.register); } }); }; const disabled = loginResult.fetching || registerResult.fetching; return ( <>
{loginResult.fetching ?

Logging in...

: null} {loginResult.error ?

Oh no... {loginResult.error.message}

: null}

Login

{registerResult.fetching ?

Signing up...

: null} {registerResult.error ? (

Oh no... {registerResult.error.message}

) : null}

Register

); }; export default LoginForm; ================================================ FILE: examples/with-graphcache-updates/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-infinite-pagination/README.md ================================================ # With Infinite Pagination (in React)

Open in StackBlitz Open in CodeSandbox

This example shows how to implement **infinite scroll** pagination with `urql` in your React UI code. It's slightly different than the [`with-pagination`](../with-pagination) example and shows how to implement a full infinitely scrolling list with only your UI code, while fulfilling the following requirements: - Unlike with [`with-graphcache-pagination`](../with-graphcache-pagination), the `urql` cache doesn't have to know about your infinite list, and this works with any cache, even the document cache - Unlike with [`with-pagination`](../with-pagination), your list can use cursors, and each page can update, while keeping the variables for the next page dynamic. - It uses no added state, no extra processing of lists, and you need no effects. In other words, unless you need a flat array of items (e.g. unless you’re using React Native’s `FlatList`), this is the simplest way to implement an infinitely scrolling, paginated list. This example is also reapplicable to other libraries, like Svelte or Vue. To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/App.js`](src/App.jsx) - This also contains a search input which is used as input for the GraphQL queries - All pagination components are in [`src/SearchResults.jsx`](src/SearchResults.jsx) - The `SearchRoot` component loads the first page of results and renders `SearchPage` - The `SearchPage` displays cached results, and otherwise only starts a network request on a button press - The `Package` component is used for each result item ================================================ FILE: examples/with-infinite-pagination/index.html ================================================ with-pagination
================================================ FILE: examples/with-infinite-pagination/package.json ================================================ { "name": "with-pagination", "version": "0.0.0", "private": true, "scripts": { "start": "vite" }, "dependencies": { "@urql/core": "^6.0.1", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-infinite-pagination/src/App.jsx ================================================ import React, { useState } from 'react'; import { Client, Provider, cacheExchange, fetchExchange } from 'urql'; import SearchRoot from './SearchResults'; const client = new Client({ // The GraphQL API we use here uses the NPM registry // We'll use it to display search results for packages url: 'https://trygql.formidable.dev/graphql/relay-npm', exchanges: [cacheExchange, fetchExchange], }); // We will be able to enter a search term, and this term // will render search results function PaginatedNpmSearch() { const [search, setSearch] = useState('urql'); const setSearchValue = event => { event.preventDefault(); setSearch(event.currentTarget.value); }; return ( <>

Type to search for npm packages

{/* Try changing the search input, then changing it back... */} {/* If you do this, all cached pages will display immediately! */}
{/* The component contains all querying logic */}
); } function App() { return ( ); } export default App; ================================================ FILE: examples/with-infinite-pagination/src/SearchResults.jsx ================================================ import React, { useCallback } from 'react'; import { gql, useQuery } from 'urql'; // We define a fragment, just to define the data // that our item component will use in the results list const packageFragment = gql` fragment SearchPackage on Package { id name latest: version(selector: "latest") { version } } `; // The main query fetches the first page of results and gets our `PageInfo` // This tells us whether more pages are present which we can query. const rootQuery = gql` query SearchRoot($searchTerm: String!, $resultsPerPage: Int!) { search(query: $searchTerm, first: $resultsPerPage) { edges { cursor node { ...SearchPackage } } pageInfo { hasNextPage endCursor } } } ${packageFragment} `; // We split the next pages we load into a separate query. In this example code, // both queries could be the same, but we keep them separate for educational // purposes. // In a real app, your "root query" would often fetch more data than the search page query. const pageQuery = gql` query SearchPage( $searchTerm: String! $resultsPerPage: Int! $afterCursor: String! ) { search(query: $searchTerm, first: $resultsPerPage, after: $afterCursor) { edges { cursor node { ...SearchPackage } } pageInfo { hasNextPage endCursor } } } ${packageFragment} `; // This is the component that we render in `./App.jsx`. // It accepts our variables as props. const SearchRoot = ({ searchTerm = 'urql', resultsPerPage = 10 }) => { const [rootResult] = useQuery({ query: rootQuery, variables: { searchTerm, resultsPerPage, }, }); if (rootResult.fetching) { return Loading...; } // Here, we render the results as a list into a fragment, and if `hasNextPage` // is truthy, we immediately render for the next page. const connection = rootResult.data?.search; return ( <> {connection?.edges?.length === 0 ? No Results : null} {connection?.edges.map(edge => ( ))} {/* The component receives the same props, plus the `afterCursor` for its variables */} {connection?.pageInfo.hasNextPage ? ( ) : rootResult.fetching ? ( Loading... ) : null} ); }; // The is rendered for each page of results, except for the root query. // It renders *itself* recursively, for the next page of results. const SearchPage = ({ searchTerm, resultsPerPage, afterCursor }) => { // Each fetches its own page results! const [pageResult, executeQuery] = useQuery({ query: pageQuery, // Initially, we *only* want to display results if, they're cached requestPolicy: 'cache-only', // We don't want to run the query if we don't have a cursor (in this example, this will never happen) pause: !afterCursor, variables: { searchTerm, resultsPerPage, afterCursor, }, }); // We only load more results, by allowing the query to make a network request, if // a button has pressed. // In your app, you may want to do this automatically if the user can see the end of // your list, e.g. via an IntersectionObserver. const onLoadMore = useCallback(() => { // This tells the query above to execute and instead of `cache-only`, which forbids // network requests, we now allow them. executeQuery({ requestPolicy: 'cache-first' }); }, [executeQuery]); if (pageResult.fetching) { return Loading...; } const connection = pageResult.data?.search; return ( <> {/* If our query has nodes, we render them here. The page renders its own results */} {connection?.edges.map(edge => ( ))} {/* If we have a next page, we now render it recursively! */} {/* As before, the next will not fetch immediately, but only query from cache */} {connection?.pageInfo.hasNextPage ? ( ) : pageResult.fetching ? ( Loading... ) : null} {!connection && !pageResult.fetching ? ( ) : null} ); }; // This is the component that then renders each result item const Package = ({ node }) => (
{node.name} @{node.latest.version}
); export default SearchRoot; ================================================ FILE: examples/with-infinite-pagination/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-infinite-pagination/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-multipart/README.md ================================================ # With Multipart File Upload

Open in StackBlitz Open in CodeSandbox

This example shows `urql` in use with `@urql/exchange-multipart-fetch`'s `multipartFetchExchange` to support file uploads in GraphQL. This largely follows the ["File Uploads" docs page](https://formidable.com/open-source/urql/docs/advanced/persistence-and-uploads/) and uses the [`trygql.formidable.dev/graphql/uploads-mock` schema](https://github.com/FormidableLabs/trygql). To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/App.jsx`](src/App.jsx) - A basic file upload form in [`src/FileUpload.jsx`](src/FileUpload.jsx) ================================================ FILE: examples/with-multipart/index.html ================================================ with-multipart
================================================ FILE: examples/with-multipart/package.json ================================================ { "name": "with-multipart", "version": "0.0.0", "private": true, "scripts": { "start": "vite" }, "dependencies": { "@urql/core": "^6.0.1", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-multipart/src/App.jsx ================================================ import React from 'react'; import { Client, Provider, fetchExchange } from 'urql'; import FileUpload from './FileUpload'; const client = new Client({ url: 'https://trygql.formidable.dev/graphql/uploads-mock', exchanges: [fetchExchange], }); function App() { return ( ); } export default App; ================================================ FILE: examples/with-multipart/src/FileUpload.jsx ================================================ import React, { useState } from 'react'; import { gql, useMutation } from 'urql'; const UPLOAD_FILE = gql` mutation UploadFile($file: Upload!) { uploadFile(file: $file) { filename } } `; const FileUpload = () => { const [selectedFile, setSelectedFile] = useState(); const [result, uploadFile] = useMutation(UPLOAD_FILE); const { data, fetching, error } = result; const handleFileUpload = () => { uploadFile({ file: selectedFile }); }; const handleFileChange = event => { setSelectedFile(event.target.files[0]); }; return (
{fetching &&

Loading...

} {error &&

Oh no... {error.message}

} {data && data.uploadFile ? (

File uploaded to {data.uploadFile.filename}

) : (
)}
); }; export default FileUpload; ================================================ FILE: examples/with-multipart/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-multipart/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-next/README.md ================================================ # With Next.js

Open in StackBlitz Open in CodeSandbox

This example shows `next-urql` and `urql` in use with Next.js as explained [in the "Next.js" section on the "Server-side Rendering" docs page](https://formidable.com/open-source/urql/docs/advanced/server-side-rendering/#nextjs). To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` ================================================ FILE: examples/with-next/app/layout.tsx ================================================ export const metadata = { title: 'Create Next App', description: 'Generated by create next app', }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ================================================ FILE: examples/with-next/app/non-rsc/layout.tsx ================================================ 'use client'; import { useMemo } from 'react'; import { UrqlProvider, ssrExchange, cacheExchange, fetchExchange, createClient, } from '@urql/next'; export default function Layout({ children }: React.PropsWithChildren) { const [client, ssr] = useMemo(() => { const ssr = ssrExchange({ isClient: typeof window !== 'undefined', }); const client = createClient({ url: 'https://graphql-pokeapi.graphcdn.app/', exchanges: [cacheExchange, ssr, fetchExchange], suspense: true, }); return [client, ssr]; }, []); return ( {children} ); } ================================================ FILE: examples/with-next/app/non-rsc/page.tsx ================================================ 'use client'; import Link from 'next/link'; import { Suspense } from 'react'; import { useQuery, gql } from '@urql/next'; export default function Page() { return ( ); } const PokemonsQuery = gql` query { pokemons(limit: 10) { results { id name } } } `; function Pokemons() { const [result] = useQuery({ query: PokemonsQuery }); return (

This is rendered as part of SSR

    {result.data ? result.data.pokemons.results.map((x: any) => (
  • {x.name}
  • )) : JSON.stringify(result.error)}
RSC
); } const PokemonQuery = gql` query ($name: String!) { pokemon(name: $name) { id name } } `; function Pokemon(props: any) { const [result] = useQuery({ query: PokemonQuery, variables: { name: props.name }, }); return (

{result.data && result.data.pokemon.name}

); } ================================================ FILE: examples/with-next/app/page.tsx ================================================ import Link from 'next/link'; import { cacheExchange, createClient, fetchExchange, gql } from '@urql/core'; import { registerUrql } from '@urql/next/rsc'; const makeClient = () => { return createClient({ url: 'https://graphql-pokeapi.graphcdn.app/', exchanges: [cacheExchange, fetchExchange], }); }; const { getClient } = registerUrql(makeClient); const PokemonsQuery = gql` query { pokemons(limit: 10) { results { id name } } } `; export default async function Home() { const result = await getClient().query(PokemonsQuery, {}); return (

This is rendered as part of an RSC

    {result.data ? result.data.pokemons.results.map((x: any) => (
  • {x.name}
  • )) : JSON.stringify(result.error)}
Non RSC
); } ================================================ FILE: examples/with-next/next-env.d.ts ================================================ /// /// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. ================================================ FILE: examples/with-next/package.json ================================================ { "name": "with-next", "version": "0.0.0", "private": true, "dependencies": { "@urql/core": "^6.0.1", "@urql/next": "^2.0.0", "graphql": "^16.6.0", "next": "13.4.2", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "scripts": { "dev": "next dev", "start": "next", "build": "next build" }, "devDependencies": { "@types/react": "18.2.6" } } ================================================ FILE: examples/with-next/tsconfig.json ================================================ { "compilerOptions": { "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": false, "forceConsistentCasingInFileNames": true, "noEmit": true, "incremental": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "plugins": [ { "name": "next" } ] }, "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] } ================================================ FILE: examples/with-pagination/README.md ================================================ # With Pagination (in React)

Open in StackBlitz Open in CodeSandbox

This example shows how to implement pagination with `urql` in your React UI code. It renders several pages as fragments with one component managing the variables for the page queries. This example is also reapplicable to other libraries, like Svelte or Vue. To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/App.js`](src/App.jsx) - A managing component called `PaginatedNpmSearch` set up to render all pages in [`src/PaginatedNpmSearch.jss`](src/PaginatedNpmSearch.jsx) - A page component called `SearchResultPage` running page queries in [`src/PaginatedNpmSearch.jsx`](src/PaginatedNpmSearch.jsx) ================================================ FILE: examples/with-pagination/index.html ================================================ with-pagination
================================================ FILE: examples/with-pagination/package.json ================================================ { "name": "with-pagination", "version": "0.0.0", "private": true, "scripts": { "start": "vite" }, "dependencies": { "@urql/core": "^6.0.1", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-pagination/src/App.jsx ================================================ import React from 'react'; import { Client, Provider, cacheExchange, fetchExchange } from 'urql'; import PaginatedNpmSearch from './PaginatedNpmSearch'; const client = new Client({ url: 'https://trygql.formidable.dev/graphql/relay-npm', exchanges: [cacheExchange, fetchExchange], }); function App() { return ( ); } export default App; ================================================ FILE: examples/with-pagination/src/PaginatedNpmSearch.jsx ================================================ import React, { useState } from 'react'; import { gql, useQuery } from 'urql'; const limit = 5; const query = 'graphql'; const NPM_SEARCH = gql` query Search($query: String!, $first: Int!, $after: String) { search(query: $query, first: $first, after: $after) { nodes { id name } pageInfo { hasNextPage endCursor } } } `; const SearchResultPage = ({ variables, onLoadMore, isLastPage }) => { const [result] = useQuery({ query: NPM_SEARCH, variables }); const { data, fetching, error } = result; const searchResults = data?.search; return (
{error &&

Oh no... {error.message}

} {fetching &&

Loading...

} {searchResults && ( <> {searchResults.nodes.map(packageInfo => (
{packageInfo.id}: {packageInfo.name}
))} {isLastPage && searchResults.pageInfo.hasNextPage && ( )} )}
); }; const PaginatedNpmSearch = () => { const [pageVariables, setPageVariables] = useState([ { query, first: limit, after: '', }, ]); return (
{pageVariables.map((variables, i) => ( setPageVariables([...pageVariables, { after, first: limit, query }]) } /> ))}
); }; export default PaginatedNpmSearch; ================================================ FILE: examples/with-pagination/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-pagination/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-react/README.md ================================================ # With React

Open in StackBlitz Open in CodeSandbox

This example shows `urql` in use with React, as explained on the ["React/Preact" page of the "Basics" documentation.](https://formidable.com/open-source/urql/docs/basics/react-preact/) To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/App.jsx`](src/App.jsx) - A query for pokémon in [`src/PokemonList.jsx`](src/PokemonList.jsx) ================================================ FILE: examples/with-react/index.html ================================================ with-react
================================================ FILE: examples/with-react/package.json ================================================ { "name": "with-react", "version": "0.0.0", "private": true, "scripts": { "start": "vite" }, "dependencies": { "@urql/core": "^6.0.1", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-react/src/App.jsx ================================================ import React from 'react'; import { Client, Provider, cacheExchange, fetchExchange } from 'urql'; import PokemonList from './PokemonList'; const client = new Client({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', exchanges: [cacheExchange, fetchExchange], }); function App() { return ( ); } export default App; ================================================ FILE: examples/with-react/src/PokemonList.jsx ================================================ import React from 'react'; import { gql, useQuery } from 'urql'; const POKEMONS_QUERY = gql` query Pokemons { pokemons(limit: 10) { id name } } `; const PokemonList = () => { const [result] = useQuery({ query: POKEMONS_QUERY }); const { data, fetching, error } = result; return (
{fetching &&

Loading...

} {error &&

Oh no... {error.message}

} {data && (
    {data.pokemons.map(pokemon => (
  • {pokemon.name}
  • ))}
)}
); }; export default PokemonList; ================================================ FILE: examples/with-react/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-react/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-react-native/App.js ================================================ import React from 'react'; import { Client, Provider, cacheExchange, fetchExchange } from 'urql'; import PokemonList from './src/screens/PokemonList'; const client = new Client({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', exchanges: [cacheExchange, fetchExchange], }); const App = () => { return ( ); }; export default App; ================================================ FILE: examples/with-react-native/README.md ================================================ # With React Native

Open in StackBlitz Open in CodeSandbox

This example shows `urql` in use with React Native. To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React Native app with a client set up in [`App.js`](./App.js) - A query for pokémon in [`src/screens/PokemonList.js`](src/screens/PokemonList.js) ================================================ FILE: examples/with-react-native/app.json ================================================ { "name": "withReactNative", "displayName": "withReactNative" } ================================================ FILE: examples/with-react-native/index.js ================================================ /** * @format */ import { AppRegistry } from 'react-native'; import App from './App'; import { name as appName } from './app.json'; AppRegistry.registerComponent(appName, () => App); ================================================ FILE: examples/with-react-native/package.json ================================================ { "name": "with-react-native", "version": "0.0.0", "private": true, "scripts": { "android": "react-native run-android", "ios": "react-native run-ios", "start": "react-native start" }, "dependencies": { "@urql/core": "^6.0.1", "graphql": "^16.6.0", "react": "18.2.0", "react-native": "0.71.4", "urql": "^5.0.1" }, "devDependencies": { "@babel/core": "^7.12.9", "@babel/preset-env": "^7.20.2", "@babel/runtime": "^7.12.5", "metro-react-native-babel-preset": "^0.76.0" } } ================================================ FILE: examples/with-react-native/src/screens/PokemonList.js ================================================ import React from 'react'; import { SafeAreaView, StyleSheet, Text, FlatList, View } from 'react-native'; import { gql, useQuery } from 'urql'; const POKEMONS_QUERY = gql` query Pokemons { pokemons(limit: 10) { id name } } `; const Item = ({ name }) => ( {name} ); const PokemonList = () => { const [result] = useQuery({ query: POKEMONS_QUERY }); const { data, fetching, error } = result; const renderItem = ({ item }) => ; return ( {fetching && Loading...} {error && Oh no... {error.message}} item.id} /> ); }; const styles = StyleSheet.create({ container: { flex: 1, }, item: { backgroundColor: '#dadada', padding: 20, marginVertical: 8, marginHorizontal: 16, }, title: { fontSize: 20, }, }); export default PokemonList; ================================================ FILE: examples/with-refresh-auth/README.md ================================================ # With Refresh Authentication

Open in StackBlitz Open in CodeSandbox

This example shows `urql` in use with `@urql/exchange-auth`'s `authExchange` to support authentication token and refresh token logic. This largely follows the ["Authentication" docs page](https://formidable.com/open-source/urql/docs/advanced/authentication/) and uses the [`trygql.formidable.dev/graphql/web-collections` schema](https://github.com/FormidableLabs/trygql). To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app set up in [`src/App.jsx`](src/App.jsx) - Some authentication glue code to store the tokens in [`src/authStore.js`](src/authStore.js) - The `Client` and the `authExchange` from `@urql/exchange-auth` set up in [`src/client.js`](src/client.js) - A basic login form in [`src/pages/LoginForm.jsx`](src/pages/LoginForm.jsx) - And a basic login guard on [`src/App.jsx`](src/App.jsx) (Note: This isn't using a query in this particular component, since this is just an example) ================================================ FILE: examples/with-refresh-auth/index.html ================================================ with-refresh-auth
================================================ FILE: examples/with-refresh-auth/package.json ================================================ { "name": "with-refresh-auth", "version": "0.0.0", "private": true, "scripts": { "start": "vite" }, "dependencies": { "@urql/core": "^6.0.1", "@urql/exchange-auth": "^3.0.0", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-refresh-auth/src/App.jsx ================================================ import React, { useState, useEffect } from 'react'; import { Provider } from 'urql'; import client from './client'; import { getToken, saveAuthData } from './authStore'; import Profile from './pages/Profile'; import LoginForm from './pages/LoginForm'; const Home = () => { const [isLoggedIn, setIsLoggedIn] = useState(false); const onLoginSuccess = auth => { saveAuthData(auth); setIsLoggedIn(true); }; useEffect(() => { if (getToken()) { setIsLoggedIn(true); } }, []); return isLoggedIn ? ( ) : ( ); }; function App() { return ( ); } export default App; ================================================ FILE: examples/with-refresh-auth/src/authStore.js ================================================ const TOKEN_KEY = 'token'; const REFRESH_TOKEN_KEY = 'refresh_token'; export const saveAuthData = ({ token, refreshToken }) => { localStorage.setItem(TOKEN_KEY, token); localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); }; export const getToken = () => { return localStorage.getItem(TOKEN_KEY); }; export const getRefreshToken = () => { return localStorage.getItem(REFRESH_TOKEN_KEY); }; export const clearStorage = () => { localStorage.clear(); }; ================================================ FILE: examples/with-refresh-auth/src/client.js ================================================ import { Client, fetchExchange, cacheExchange, gql } from 'urql'; import { authExchange } from '@urql/exchange-auth'; import { getRefreshToken, getToken, saveAuthData, clearStorage, } from './authStore'; const REFRESH_TOKEN_MUTATION = gql` mutation RefreshCredentials($refreshToken: String!) { refreshCredentials(refreshToken: $refreshToken) { refreshToken token } } `; const auth = authExchange(async utilities => { let token = getToken(); let refreshToken = getRefreshToken(); return { addAuthToOperation(operation) { return token ? utilities.appendHeaders(operation, { Authorization: `Bearer ${token}`, }) : operation; }, didAuthError(error) { return error.graphQLErrors.some( e => e.extensions?.code === 'UNAUTHORIZED' ); }, willAuthError(operation) { // Sync tokens on every operation token = getToken(); refreshToken = getRefreshToken(); if (!token) { // Detect our login mutation and let this operation through: return ( operation.kind !== 'mutation' || // Here we find any mutation definition with the "signin" field !operation.query.definitions.some(definition => { return ( definition.kind === 'OperationDefinition' && definition.selectionSet.selections.some(node => { // The field name is just an example, since register may also be an exception return node.kind === 'Field' && node.name.value === 'signin'; }) ); }) ); } return false; }, async refreshAuth() { if (refreshToken) { const result = await utilities.mutate(REFRESH_TOKEN_MUTATION, { refreshToken, }); if (result.data?.refreshCredentials) { token = result.data.refreshCredentials.token; refreshToken = result.data.refreshCredentials.refreshToken; saveAuthData({ token, refreshToken }); return; } } // This is where auth has gone wrong and we need to clean up and redirect to a login page clearStorage(); window.location.reload(); }, }; }); const client = new Client({ url: 'https://trygql.formidable.dev/graphql/web-collections', exchanges: [cacheExchange, auth, fetchExchange], }); export default client; ================================================ FILE: examples/with-refresh-auth/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-refresh-auth/src/pages/LoginForm.jsx ================================================ import React from 'react'; import { gql, useMutation } from 'urql'; const LOGIN_MUTATION = gql` mutation Login($input: LoginInput!) { signin(input: $input) { refreshToken token } } `; const REGISTER_MUTATION = gql` mutation Register($input: LoginInput!) { register(input: $input) { refreshToken token } } `; const LoginForm = ({ onLoginSuccess }) => { const [loginResult, login] = useMutation(LOGIN_MUTATION); const [registerResult, register] = useMutation(REGISTER_MUTATION); const onSubmitLogin = event => { event.preventDefault(); const data = new FormData(event.target); const username = data.get('username'); const password = data.get('password'); login({ input: { username, password } }).then(result => { if (!result.error && result.data && result.data.signin) { onLoginSuccess(result.data.signin); } }); }; const onSubmitRegister = event => { event.preventDefault(); const data = new FormData(event.target); const username = data.get('username'); const password = data.get('password'); register({ input: { username, password } }).then(result => { if (!result.error && result.data && result.data.register) { onLoginSuccess(result.data.register); } }); }; const disabled = loginResult.fetching || registerResult.fetching; return ( <>
{loginResult.fetching ?

Logging in...

: null} {loginResult.error ?

Oh no... {loginResult.error.message}

: null}

Login

{registerResult.fetching ?

Signing up...

: null} {registerResult.error ? (

Oh no... {registerResult.error.message}

) : null}

Register

); }; export default LoginForm; ================================================ FILE: examples/with-refresh-auth/src/pages/Profile.jsx ================================================ import React from 'react'; import { gql, useQuery } from 'urql'; const PROFILE_QUERY = gql` query Profile { me { id username createdAt } } `; const Profile = () => { const [result] = useQuery({ query: PROFILE_QUERY }); const { data, fetching, error } = result; return (
{fetching &&

Loading...

} {error &&

Oh no... {error.message}

} {data?.me && ( <>

profile data

id: {data.me.id}

username: {data.me.username}

)}
); }; export default Profile; ================================================ FILE: examples/with-refresh-auth/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-retry/README.md ================================================ # Integrating `@urql/exchange-retry`’s retryExchange

Open in StackBlitz Open in CodeSandbox

Integrating urql is as simple as: 1. Install packages ```sh yarn add urql graphql # or npm install --save urql graphql ``` 2. Add [retry exchange](https://formidable.com/open-source/urql/docs/advanced/retry-operations/) ```sh yarn add @urql/exchange-retry # or npm install --save @urql/exchange-retry ``` 3. Setting up the Client and adding the `retryExchange` [here](src/App.js) 4. Execute the Query [here](src/pages/Color.js) # With Retry This example shows `urql` in use with `@urql/exchange-retry`'s `retryExchange` to implement retrying failed operations. This largely follows the ["Retrying Operations" docs page](https://formidable.com/open-source/urql/docs/advanced/retry-operations/) and uses the [`trygql.formidable.dev/graphql/intermittent-colors` schema](https://github.com/FormidableLabs/trygql), which emits a special `NO_SOUP` error randomly. To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/App.jsx`](src/App.jsx) - The `retryExchange` from `@urql/exchange-retry` in [`src/App.jsx`](src/App.jsx) - A random colour query in [`src/Color.jsx`](src/pages/Color.jsx) ================================================ FILE: examples/with-retry/index.html ================================================ with-retry
================================================ FILE: examples/with-retry/package.json ================================================ { "name": "with-retry", "version": "0.0.0", "private": true, "scripts": { "start": "vite" }, "dependencies": { "@urql/core": "^6.0.1", "@urql/exchange-retry": "^2.0.0", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-retry/src/App.jsx ================================================ import React from 'react'; import { Client, fetchExchange, Provider } from 'urql'; import { retryExchange } from '@urql/exchange-retry'; import Color from './Color'; const client = new Client({ url: 'https://trygql.formidable.dev/graphql/intermittent-colors', exchanges: [ retryExchange({ maxNumberAttempts: 10, maxDelayMs: 500, retryIf: error => { // NOTE: With this deemo schema we have a specific random error to look out for: return ( error.graphQLErrors.some(x => x.extensions?.code === 'NO_SOUP') || !!error.networkError ); }, }), fetchExchange, ], }); function App() { return ( ); } export default App; ================================================ FILE: examples/with-retry/src/Color.jsx ================================================ import React from 'react'; import { gql, useQuery } from 'urql'; const RANDOM_COLOR_QUERY = gql` query RandomColor { randomColor { name hex } } `; const RandomColorDisplay = () => { const [result] = useQuery({ query: RANDOM_COLOR_QUERY }); const { data, fetching, error } = result; return (
{fetching &&

Loading...

} {error &&

Oh no... {error.message}

} {data && (
{data.randomColor.name}
)} {result.operation && (

To get a result, the retry exchange retried:{' '} {result.operation.context.retryCount || 0} times.

)}
); }; export default RandomColorDisplay; ================================================ FILE: examples/with-retry/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-retry/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-solid/.eslintrc.js ================================================ module.exports = { rules: { 'react/react-in-jsx-scope': 'off', }, }; ================================================ FILE: examples/with-solid/README.md ================================================ # URQL with Solid This example demonstrates how to use URQL with Solid.js. ## Features - Basic query with `createQuery` - Client setup with `Provider` - Suspense integration ## Getting Started ```bash pnpm install pnpm start ``` Then open [http://localhost:5173](http://localhost:5173) in your browser. ## What's Inside - `src/App.jsx` - Sets up the URQL client and provider - `src/PokemonList.jsx` - Demonstrates `createQuery` with Suspense ## Learn More - [Solid Documentation](https://www.solidjs.com/) - [URQL Solid Documentation](https://formidable.com/open-source/urql/docs/basics/solid/) ================================================ FILE: examples/with-solid/index.html ================================================ URQL with Solid
================================================ FILE: examples/with-solid/package.json ================================================ { "name": "with-solid", "version": "0.0.0", "private": true, "scripts": { "start": "vite", "build": "vite build" }, "dependencies": { "@urql/core": "^6.0.1", "@urql/solid": "^1.0.1", "graphql": "^16.9.0", "solid-js": "^1.9.10" }, "devDependencies": { "vite": "^7.3.1", "vite-plugin-solid": "^2.11.10" } } ================================================ FILE: examples/with-solid/src/App.jsx ================================================ /** @jsxImportSource solid-js */ import { createClient, Provider, cacheExchange, fetchExchange, } from '@urql/solid'; import PokemonList from './PokemonList'; const client = createClient({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', exchanges: [cacheExchange, fetchExchange], }); function App() { return ( ); } export default App; ================================================ FILE: examples/with-solid/src/PokemonList.jsx ================================================ /** @jsxImportSource solid-js */ import { Suspense, For } from 'solid-js'; import { gql } from '@urql/core'; import { createQuery } from '@urql/solid'; const POKEMONS_QUERY = gql` query Pokemons { pokemons(limit: 10) { id name } } `; const PokemonList = () => { const [result] = createQuery({ query: POKEMONS_QUERY }); return (

Pokemon List

Loading...

}>
    {pokemon =>
  • {pokemon.name}
  • }
); }; export default PokemonList; ================================================ FILE: examples/with-solid/src/index.jsx ================================================ /** @jsxImportSource solid-js */ import { render } from 'solid-js/web'; import App from './App'; render(() => , document.getElementById('root')); ================================================ FILE: examples/with-solid/vite.config.js ================================================ import { defineConfig } from 'vite'; import solid from 'vite-plugin-solid'; export default defineConfig({ plugins: [solid()], }); ================================================ FILE: examples/with-solid-start/.gitignore ================================================ /.output /.vinxi .DS_Store ================================================ FILE: examples/with-solid-start/README.md ================================================ # URQL with SolidStart This example demonstrates how to use URQL with SolidStart. ## Features - Basic query with `createQuery` - Client setup with `Provider` - SSR with automatic hydration - Route-level preloading - Suspense integration ## Getting Started ```bash pnpm install pnpm start ``` Then open [http://localhost:3000](http://localhost:3000) in your browser. ## What's Inside - `src/app.tsx` - Sets up the URQL client and router - `src/routes/index.tsx` - Demonstrates `createQuery` with preloading and Suspense ## Key Features Demonstrated ### Route Preloading The example uses SolidStart's `preload` function to start fetching data before the route component renders: ```tsx export const route = { preload: () => { const pokemons = createQuery({ query: POKEMONS_QUERY }); return pokemons(); // Start fetching }, } satisfies RouteDefinition; ``` ### Server-Side Rendering Queries automatically execute on the server during SSR and hydrate on the client without refetching. ## Learn More - [SolidStart Documentation](https://start.solidjs.com/) - [URQL SolidStart Documentation](https://formidable.com/open-source/urql/docs/basics/solid-start/) ================================================ FILE: examples/with-solid-start/app.config.ts ================================================ import { defineConfig } from '@solidjs/start/config'; export default defineConfig({}); ================================================ FILE: examples/with-solid-start/package.json ================================================ { "name": "with-solid-start", "version": "0.0.0", "private": true, "type": "module", "scripts": { "start": "vinxi dev", "build": "vinxi build" }, "dependencies": { "@solidjs/router": "^0.15.4", "@solidjs/start": "^1.2.1", "@urql/core": "^6.0.1", "@urql/solid-start": "^0.1.0", "graphql": "^16.9.0", "solid-js": "^1.9.10", "vinxi": "^0.5.0" }, "devDependencies": { "vite": "^7.3.1", "vite-plugin-solid": "^2.11.10" } } ================================================ FILE: examples/with-solid-start/src/app.tsx ================================================ import { Router, action, query } from '@solidjs/router'; import { FileRoutes } from '@solidjs/start/router'; import { Suspense } from 'solid-js'; import { createClient, Provider, cacheExchange, fetchExchange, } from '@urql/solid-start'; const client = createClient({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', exchanges: [cacheExchange, fetchExchange], }); export default function App() { return ( ( {props.children} )} > ); } ================================================ FILE: examples/with-solid-start/src/entry-client.tsx ================================================ // @refresh reload import { mount, StartClient } from '@solidjs/start/client'; mount(() => , document.getElementById('app')!); ================================================ FILE: examples/with-solid-start/src/entry-server.tsx ================================================ // @refresh reload import { createHandler, StartServer } from '@solidjs/start/server'; export default createHandler(() => ( ( URQL with SolidStart {assets}
{children}
{scripts} )} /> )); ================================================ FILE: examples/with-solid-start/src/routes/index.tsx ================================================ import { Suspense, For, Show, createSignal } from 'solid-js'; import { createAsync, useAction, useSubmission } from '@solidjs/router'; import { gql } from '@urql/core'; import { createQuery, createMutation } from '@urql/solid-start'; const POKEMONS_QUERY = gql` query Pokemons { pokemons(limit: 10) { id name } } `; const ADD_POKEMON_MUTATION = gql` mutation AddPokemon($name: String!) { addPokemon(name: $name) { id name } } `; export default function Home() { const queryPokemons = createQuery(POKEMONS_QUERY, 'list-pokemons'); const result = createAsync(() => queryPokemons()); // Create the mutation action inside the component where it has access to context const addPokemonAction = createMutation(ADD_POKEMON_MUTATION, 'add-pokemon'); const addPokemon = useAction(addPokemonAction); const submission = useSubmission(addPokemonAction); const [pokemonName, setPokemonName] = createSignal(''); const handleSubmit = async (e: Event) => { e.preventDefault(); const name = pokemonName(); if (!name) return; const result = await addPokemon({ name }); if (result.data) { setPokemonName(''); // Note: In a real app, you'd want to refetch or update the cache } }; return (

Pokemon List (SolidStart + URQL)

Add Pokemon

setPokemonName(e.currentTarget.value)} placeholder="Pokemon name" style={{ padding: '8px', 'font-size': '14px' }} />

Error: {submission.result.error.message}

Added: {submission.result.data.addPokemon.name}

Pokemon List

Loading...

}>
    {pokemon =>
  • {pokemon.name}
  • }
); } ================================================ FILE: examples/with-solid-start/tsconfig.json ================================================ { "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js", "module": "ESNext", "moduleResolution": "bundler", "target": "ESNext", "types": ["vite/client"], "isolatedModules": true, "resolveJsonModule": true, "esModuleInterop": true, "skipLibCheck": true, "strict": true, "noEmit": true } } ================================================ FILE: examples/with-subscriptions-via-fetch/README.md ================================================ # With Subscriptions via Fetch

Open in StackBlitz Open in CodeSandbox

This example shows `urql` in use with subscriptions running via a plain `fetch` HTTP request to GraphQL Yoga. This uses the [GraphQL Server-Sent Events](https://the-guild.dev/blog/graphql-over-sse) protocol, which means that the request streams in more results via a single HTTP response. This example also includes Graphcache ["Cache Updates"](https://formidable.com/open-source/urql/docs/graphcache/cache-updates/) to update a list with incoming items from the subscriptions. To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `urql` bindings and a React app with a client set up in [`src/App.jsx`](src/App.jsx) - A local `graphql-yoga` server set up to test subscriptions in [`server/`](server/). ================================================ FILE: examples/with-subscriptions-via-fetch/index.html ================================================ with-defer-stream-directives
================================================ FILE: examples/with-subscriptions-via-fetch/package.json ================================================ { "name": "with-subscriptions-via-fetch", "version": "0.0.0", "private": true, "scripts": { "server": "node server/graphql-yoga.js", "client": "vite", "start": "run-p client server" }, "dependencies": { "@urql/core": "^6.0.1", "@urql/exchange-graphcache": "^9.0.0", "graphql": "^16.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", "urql": "^5.0.1" }, "devDependencies": { "@vitejs/plugin-react": "^3.1.0", "graphql-yoga": "^3.7.1", "npm-run-all": "^4.1.5", "vite": "^4.2.0" } } ================================================ FILE: examples/with-subscriptions-via-fetch/server/graphql-yoga.js ================================================ const { createYoga } = require('graphql-yoga'); const { createServer } = require('http'); const { schema } = require('./schema'); const yoga = createYoga({ schema }); const server = createServer(yoga); server.listen(3004); ================================================ FILE: examples/with-subscriptions-via-fetch/server/schema.js ================================================ const { GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString, } = require('graphql'); const Alphabet = new GraphQLObjectType({ name: 'Alphabet', fields: { char: { type: GraphQLString, }, }, }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: () => ({ list: { type: new GraphQLList(Alphabet), resolve() { return [{ char: 'Where are my letters?' }]; }, }, }), }), subscription: new GraphQLObjectType({ name: 'Subscription', fields: () => ({ alphabet: { type: Alphabet, resolve(root) { return root; }, subscribe: async function* () { for (let letter = 65; letter <= 90; letter++) { await new Promise(resolve => setTimeout(resolve, 500)); yield { char: String.fromCharCode(letter) }; } }, }, }), }), }); module.exports = { schema }; ================================================ FILE: examples/with-subscriptions-via-fetch/src/App.jsx ================================================ import React from 'react'; import { Client, Provider, fetchExchange } from 'urql'; import { cacheExchange } from '@urql/exchange-graphcache'; import Songs from './Songs'; const cache = cacheExchange({ keys: { Alphabet: data => data.char, }, updates: { Subscription: { alphabet(parent, _args, cache) { const list = cache.resolve('Query', 'list') || []; list.push(parent.alphabet); cache.link('Query', 'list', list); }, }, }, }); const client = new Client({ url: 'http://localhost:3004/graphql', fetchSubscriptions: true, exchanges: [cache, fetchExchange], }); function App() { return ( ); } export default App; ================================================ FILE: examples/with-subscriptions-via-fetch/src/Songs.jsx ================================================ import React from 'react'; import { gql, useQuery, useSubscription } from 'urql'; const LIST_QUERY = gql` query List_Query { list { char } } `; const SONG_SUBSCRIPTION = gql` subscription App_Subscription { alphabet { char } } `; const ListQuery = () => { const [listResult] = useQuery({ query: LIST_QUERY, }); return (

List

{listResult?.data?.list.map(i => (
{i.char}
))}
); }; const SongSubscription = () => { const [songsResult] = useSubscription( { query: SONG_SUBSCRIPTION }, (prev = [], data) => [...prev, data.alphabet] ); return (

Song

{songsResult?.data?.map(i => (
{i.char}
))}
); }; const LocationsList = () => { return ( <> ); }; export default LocationsList; ================================================ FILE: examples/with-subscriptions-via-fetch/src/index.jsx ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; createRoot(document.getElementById('root')).render(); ================================================ FILE: examples/with-subscriptions-via-fetch/vite.config.js ================================================ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ================================================ FILE: examples/with-svelte/.gitignore ================================================ /node_modules/ /public/build/ .DS_Store ================================================ FILE: examples/with-svelte/README.md ================================================ # With Svelte

Open in StackBlitz Open in CodeSandbox

This example shows `@urql/svelte` in use with Svelte, as explained on the ["Svelte" page of the "Basics" documentation.](https://formidable.com/open-source/urql/docs/basics/svelte/) To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `@urql/svelte` bindings with a client set up in [`src/App.svelte`](src/App.svelte) - A query for pokémon in [`src/PokemonList.svelte`](src/pages/PokemonList.svelte) ================================================ FILE: examples/with-svelte/index.html ================================================ with-svelte
================================================ FILE: examples/with-svelte/package.json ================================================ { "name": "with-svelte", "private": true, "version": "0.0.0", "scripts": { "start": "vite", "build": "vite build", "serve": "vite preview" }, "dependencies": { "@urql/core": "^6.0.1", "@urql/svelte": "^5.0.0", "graphql": "^16.6.0", "svelte": "^4.0.5" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^2.4.2", "vite": "^4.2.0" } } ================================================ FILE: examples/with-svelte/src/App.svelte ================================================ ================================================ FILE: examples/with-svelte/src/PokemonList.svelte ================================================
{#if $pokemons.fetching} Loading... {:else if $pokemons.error} Oh no... {$pokemons.error.message} {:else}
    {#each $pokemons.data.pokemons as pokemon}
  • {pokemon.name}
  • {/each}
{/if}
================================================ FILE: examples/with-svelte/src/main.js ================================================ import App from './App.svelte'; var app = new App({ target: document.body, }); export default app; ================================================ FILE: examples/with-svelte/vite.config.mjs ================================================ import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [svelte()], }); ================================================ FILE: examples/with-vue3/.gitignore ================================================ node_modules .DS_Store dist *.local ================================================ FILE: examples/with-vue3/README.md ================================================ # With Vue 3

Open in StackBlitz Open in CodeSandbox

This example shows `@urql/vue` in use with Vue 3, as explained on the ["Vue" page of the "Basics" documentation.](https://formidable.com/open-source/urql/docs/basics/vue/) To run this example install dependencies and run the `start` script: ```sh yarn install yarn run start # or npm install npm run start ``` This example contains: - The `@urql/vue` bindings with a client set up in [`src/App.vue`](src/App.vue) - A suspense loading boundary in the `App` component in [`src/App.vue`](src/App.vue) - A query for pokémon in [`src/PokemonList.vue`](src/pages/PokemonList.vue) ================================================ FILE: examples/with-vue3/index.html ================================================ Vite App
================================================ FILE: examples/with-vue3/package.json ================================================ { "name": "with-vue3", "version": "0.0.0", "private": true, "scripts": { "start": "vite", "build": "vite build", "serve": "vite preview" }, "dependencies": { "@urql/core": "^6.0.1", "@urql/vue": "^2.0.0", "graphql": "^16.6.0", "vue": "^3.2.47" }, "devDependencies": { "@vitejs/plugin-vue": "^4.1.0", "vite": "^4.2.0" } } ================================================ FILE: examples/with-vue3/src/App.vue ================================================ ================================================ FILE: examples/with-vue3/src/PokemonList.vue ================================================ ================================================ FILE: examples/with-vue3/src/main.js ================================================ import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#app'); ================================================ FILE: examples/with-vue3/vite.config.js ================================================ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }); ================================================ FILE: exchanges/auth/CHANGELOG.md ================================================ # Changelog ## 3.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 2.2.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 2.2.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 2.1.6 ### Patch Changes - `authExchange()` will now block and pass on errors if the initialization function passed to it fails, and will retry indefinitely. It’ll also output a warning for these cases, as the initialization function (i.e. `authExchange(async (utils) => { /*...*/ })`) is not expected to reject/throw Submitted by [@kitten](https://github.com/kitten) (See [#3343](https://github.com/urql-graphql/urql/pull/3343)) ## 2.1.5 ### Patch Changes - Handle `refreshAuth` rejections and pass the resulting error on to `OperationResult`s on the authentication queue Submitted by [@kitten](https://github.com/kitten) (See [#3307](https://github.com/urql-graphql/urql/pull/3307)) ## 2.1.4 ### Patch Changes - ⚠️ Fix regression that caused teardowns to be ignored by an `authExchange`’s retry queue Submitted by [@kitten](https://github.com/kitten) (See [#3235](https://github.com/urql-graphql/urql/pull/3235)) ## 2.1.3 ### Patch Changes - Update build process to generate correct source maps Submitted by [@kitten](https://github.com/kitten) (See [#3201](https://github.com/urql-graphql/urql/pull/3201)) ## 2.1.2 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 2.1.1 ### Patch Changes - ⚠️ Fix operations created by `utilities.mutate()` erroneously being retried and sent again like a regular operation Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3164](https://github.com/urql-graphql/urql/pull/3164)) ## 2.1.0 ### Minor Changes - Update exchanges to drop redundant `share` calls, since `@urql/core`’s `composeExchanges` utility now automatically does so for us Submitted by [@kitten](https://github.com/kitten) (See [#3082](https://github.com/urql-graphql/urql/pull/3082)) ### Patch Changes - ⚠️ Fix source maps included with recently published packages, which lost their `sourcesContent`, including additional source files, and had incorrect paths in some of them Submitted by [@kitten](https://github.com/kitten) (See [#3053](https://github.com/urql-graphql/urql/pull/3053)) - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Avoid infinite loop when `didAuthError` keeps returning true Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3112](https://github.com/urql-graphql/urql/pull/3112)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 2.0.0 ### Major Changes - Implement new `authExchange` API, which removes the need for an `authState` (i.e. an internal authentication state) and removes `getAuth`, replacing it with a separate `refreshAuth` flow. The new API requires you to now pass an initializer function. This function receives a `utils` object with `utils.mutate` and `utils.appendHeaders` utility methods. It must return the configuration object, wrapped in a promise, and this configuration is similar to what we had before, if you're migrating to this. Its `refreshAuth` method is now only called after authentication errors occur and not on initialization. Instead, it's now recommended that you write your initialization logic in-line. ```js authExchange(async utils => { let token = localStorage.getItem('token'); let refreshToken = localStorage.getItem('refreshToken'); return { addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: `Bearer ${token}`, }); }, didAuthError(error) { return error.graphQLErrors.some( e => e.extensions?.code === 'FORBIDDEN' ); }, async refreshAuth() { const result = await utils.mutate(REFRESH, { token }); if (result.data?.refreshLogin) { token = result.data.refreshLogin.token; refreshToken = result.data.refreshLogin.refreshToken; localStorage.setItem('token', token); localStorage.setItem('refreshToken', refreshToken); } }, }; }); ``` Submitted by [@kitten](https://github.com/kitten) (See [#3012](https://github.com/urql-graphql/urql/pull/3012)) ### Patch Changes - ⚠️ Fix `willAuthError` not being called for operations that are waiting on the authentication state to update. This can actually lead to a common issue where operations that came in during the authentication initialization (on startup) will never have `willAuthError` called on them. This can cause an easy mistake where the initial authentication state is never checked to be valid Submitted by [@kitten](https://github.com/kitten) (See [#3017](https://github.com/urql-graphql/urql/pull/3017)) - Updated dependencies (See [#3007](https://github.com/urql-graphql/urql/pull/3007), [#2962](https://github.com/urql-graphql/urql/pull/2962), [#3007](https://github.com/urql-graphql/urql/pull/3007), [#3015](https://github.com/urql-graphql/urql/pull/3015), and [#3022](https://github.com/urql-graphql/urql/pull/3022)) - @urql/core@3.2.0 ## 1.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Minor Changes - Remove the `babel-plugin-modular-graphql` helper, this because the graphql package hasn't converted to ESM yet which gives issues in node environments, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2551](https://github.com/FormidableLabs/urql/pull/2551)) ### Patch Changes - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 0.1.7 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 0.1.6 ### Patch Changes - ⚠️ Fix willAuthError causing duplicate operations, by [@yankovalera](https://github.com/yankovalera) (See [#1849](https://github.com/FormidableLabs/urql/pull/1849)) - Updated dependencies (See [#1851](https://github.com/FormidableLabs/urql/pull/1851), [#1850](https://github.com/FormidableLabs/urql/pull/1850), and [#1852](https://github.com/FormidableLabs/urql/pull/1852)) - @urql/core@2.2.0 ## 0.1.5 ### Patch Changes - Expose `AuthContext` type, by [@arempe93](https://github.com/arempe93) (See [#1828](https://github.com/FormidableLabs/urql/pull/1828)) - Updated dependencies (See [#1829](https://github.com/FormidableLabs/urql/pull/1829)) - @urql/core@2.1.6 ## 0.1.4 ### Patch Changes - Allow `mutate` to infer the result's type when a `TypedDocumentNode` is passed via the usual generics, like `client.mutation` for instance, by [@younesmln](https://github.com/younesmln) (See [#1796](https://github.com/FormidableLabs/urql/pull/1796)) ## 0.1.3 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 0.1.2 ### Patch Changes - Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was previously confusing as `operationName` was effectively referring to two different things. You can safely upgrade to this new version, however to mute all deprecation warnings you will have to **upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please use [the new `makeOperation` helper function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead, by [@bkonkle](https://github.com/bkonkle) (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - Updated dependencies (See [#1094](https://github.com/FormidableLabs/urql/pull/1094) and [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - @urql/core@1.14.0 ## 0.1.1 ### Patch Changes - ⚠️ Fix an operation that triggers `willAuthError` with a truthy return value being sent off twice, by [@kitten](https://github.com/kitten) (See [#1075](https://github.com/FormidableLabs/urql/pull/1075)) ## v0.1.0 **Initial Release** ================================================ FILE: exchanges/auth/README.md ================================================

@urql/exchange-auth

An exchange for managing authentication in urql

`@urql/exchange-auth` is an exchange for the [`urql`](https://github.com/urql-graphql/urql) GraphQL client which helps handle auth headers and token refresh ## Quick Start Guide First install `@urql/exchange-auth` alongside `urql`: ```sh yarn add @urql/exchange-auth # or npm install --save @urql/exchange-auth ``` You'll then need to add the `authExchange`, that this package exposes to your `urql` Client ```js import { createClient, cacheExchange, fetchExchange } from 'urql'; import { makeOperation } from '@urql/core'; import { authExchange } from '@urql/exchange-auth'; const client = createClient({ url: 'http://localhost:1234/graphql', exchanges: [ cacheExchange, authExchange(async utils => { // called on initial launch, // fetch the auth state from storage (local storage, async storage etc) let token = localStorage.getItem('token'); let refreshToken = localStorage.getItem('refreshToken'); return { addAuthToOperation(operation) { if (token) { return utils.appendHeaders(operation, { Authorization: `Bearer ${token}`, }); } return operation; }, willAuthError(_operation) { // e.g. check for expiration, existence of auth etc return !token; }, didAuthError(error, _operation) { // check if the error was an auth error // this can be implemented in various ways, e.g. 401 or a special error code return error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN'); }, async refreshAuth() { // called when auth error has occurred // we should refresh the token with a GraphQL mutation or a fetch call, // depending on what the API supports const result = await mutate(refreshMutation, { token: authState?.refreshToken, }); if (result.data?.refreshLogin) { // save the new tokens in storage for next restart token = result.data.refreshLogin.token; refreshToken = result.data.refreshLogin.refreshToken; localStorage.setItem('token', token); localStorage.setItem('refreshToken', refreshToken); } else { // otherwise, if refresh fails, log clear storage and log out localStorage.clear(); logout(); } }, }; }), fetchExchange, ], }); ``` ## Handling Errors via the errorExchange Handling the logout logic in `refreshAuth` is the easiest way to get started, but it means the errors will always get swallowed by the `authExchange`. If you want to handle errors globally, this can be done using the `mapExchange`: ```js import { mapExchange } from 'urql'; // this needs to be placed ABOVE the authExchange in the exchanges array, otherwise the auth error // will show up hear before the auth exchange has had the chance to handle it mapExchange({ onError(error) { // we only get an auth error here when the auth exchange had attempted to refresh auth and // getting an auth error again for the second time const isAuthError = error.graphQLErrors.some( e => e.extensions?.code === 'FORBIDDEN', ); if (isAuthError) { // clear storage, log the user out etc } } }), ``` ================================================ FILE: exchanges/auth/jsr.json ================================================ { "name": "@urql/exchange-auth", "version": "3.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/auth/package.json ================================================ { "name": "@urql/exchange-auth", "version": "3.0.0", "description": "An exchange for managing authentication and token refresh in urql", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/auth" }, "keywords": [ "urql", "exchange", "auth", "authentication", "graphql", "exchanges" ], "main": "dist/urql-exchange-auth", "module": "dist/urql-exchange-auth.mjs", "types": "dist/urql-exchange-auth.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-auth.d.ts", "import": "./dist/urql-exchange-auth.mjs", "require": "./dist/urql-exchange-auth.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist extras", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "peerDependencies": { "@urql/core": "^6.0.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "devDependencies": { "@urql/core": "workspace:*", "graphql": "^16.0.0" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/auth/src/authExchange.test.ts ================================================ import { Source, pipe, fromValue, toPromise, take, makeSubject, share, publish, scan, tap, map, } from 'wonka'; import { makeOperation, CombinedError, Client, Operation, OperationResult, } from '@urql/core'; import { vi, expect, it } from 'vitest'; import { print } from 'graphql'; import { queryResponse, queryOperation, } from '../../../packages/core/src/test-utils'; import { authExchange } from './authExchange'; const makeExchangeArgs = () => { const operations: Operation[] = []; const result = vi.fn( (operation: Operation): OperationResult => ({ ...queryResponse, operation }) ); return { operations, result, exchangeArgs: { forward: (op$: Source) => pipe( op$, tap(op => operations.push(op)), map(result), share ), client: new Client({ url: '/api', exchanges: [], }), } as any, }; }; it('adds the auth header correctly', async () => { const { exchangeArgs } = makeExchangeArgs(); const res = await pipe( fromValue(queryOperation), authExchange(async utils => { const token = 'my-token'; return { addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: token, }); }, didAuthError: () => false, async refreshAuth() { /*noop*/ }, }; })(exchangeArgs), take(1), toPromise ); expect(res.operation.context.authAttempt).toBe(false); expect(res.operation.context.fetchOptions).toEqual({ ...(queryOperation.context.fetchOptions || {}), headers: { Authorization: 'my-token', }, }); }); it('adds the auth header correctly when intialized asynchronously', async () => { const { exchangeArgs } = makeExchangeArgs(); const res = await pipe( fromValue(queryOperation), authExchange(async utils => { // delayed initial auth await Promise.resolve(); const token = 'async-token'; return { addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: token, }); }, didAuthError: () => false, async refreshAuth() { /*noop*/ }, }; })(exchangeArgs), take(1), toPromise ); expect(res.operation.context.authAttempt).toBe(false); expect(res.operation.context.fetchOptions).toEqual({ ...(queryOperation.context.fetchOptions || {}), headers: { Authorization: 'async-token', }, }); }); it('supports calls to the mutate() method in refreshAuth()', async () => { const { exchangeArgs } = makeExchangeArgs(); const willAuthError = vi .fn() .mockReturnValueOnce(true) .mockReturnValue(false); const [mutateRes, res] = await pipe( fromValue(queryOperation), authExchange(async utils => { const token = 'async-token'; return { addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: token, }); }, willAuthError, didAuthError: () => false, async refreshAuth() { const result = await utils.mutate('mutation { auth }', undefined); expect(print(result.operation.query)).toBe('mutation {\n auth\n}'); }, }; })(exchangeArgs), take(2), scan((acc, res) => [...acc, res], [] as OperationResult[]), toPromise ); expect(mutateRes.operation.context.fetchOptions).toEqual({ headers: { Authorization: 'async-token', }, }); expect(res.operation.context.authAttempt).toBe(true); expect(res.operation.context.fetchOptions).toEqual({ method: 'POST', headers: { Authorization: 'async-token', }, }); }); it('adds the same token to subsequent operations', async () => { const { exchangeArgs } = makeExchangeArgs(); const { source, next } = makeSubject(); const result = vi.fn(); const auth$ = pipe( source, authExchange(async utils => { const token = 'my-token'; return { addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: token, }); }, didAuthError: () => false, async refreshAuth() { /*noop*/ }, }; })(exchangeArgs), tap(result), take(2), toPromise ); await new Promise(resolve => setTimeout(resolve)); next(queryOperation); next( makeOperation('query', queryOperation, { ...queryOperation.context, foo: 'bar', }) ); await auth$; expect(result).toHaveBeenCalledTimes(2); expect(result.mock.calls[0][0].operation.context.authAttempt).toBe(false); expect(result.mock.calls[0][0].operation.context.fetchOptions).toEqual({ ...(queryOperation.context.fetchOptions || {}), headers: { Authorization: 'my-token', }, }); expect(result.mock.calls[1][0].operation.context.authAttempt).toBe(false); expect(result.mock.calls[1][0].operation.context.fetchOptions).toEqual({ ...(queryOperation.context.fetchOptions || {}), headers: { Authorization: 'my-token', }, }); }); it('triggers authentication when an operation did error', async () => { const { exchangeArgs, result, operations } = makeExchangeArgs(); const { source, next } = makeSubject(); const didAuthError = vi.fn().mockReturnValueOnce(true); pipe( source, authExchange(async utils => { let token = 'initial-token'; return { addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: token, }); }, didAuthError, async refreshAuth() { token = 'final-token'; }, }; })(exchangeArgs), publish ); await new Promise(resolve => setTimeout(resolve)); result.mockReturnValueOnce({ ...queryResponse, operation: queryOperation, data: undefined, error: new CombinedError({ graphQLErrors: [{ message: 'Oops' }], }), }); next(queryOperation); expect(result).toHaveBeenCalledTimes(1); expect(didAuthError).toHaveBeenCalledTimes(1); await new Promise(resolve => setTimeout(resolve)); expect(result).toHaveBeenCalledTimes(2); expect(operations.length).toBe(2); expect(operations[0]).toHaveProperty( 'context.fetchOptions.headers.Authorization', 'initial-token' ); expect(operations[1]).toHaveProperty( 'context.fetchOptions.headers.Authorization', 'final-token' ); }); it('triggers authentication when an operation will error', async () => { const { exchangeArgs, result, operations } = makeExchangeArgs(); const { source, next } = makeSubject(); const willAuthError = vi .fn() .mockReturnValueOnce(true) .mockReturnValue(false); pipe( source, authExchange(async utils => { let token = 'initial-token'; return { addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: token, }); }, willAuthError, didAuthError: () => false, async refreshAuth() { token = 'final-token'; }, }; })(exchangeArgs), publish ); await new Promise(resolve => setTimeout(resolve)); next(queryOperation); expect(result).toHaveBeenCalledTimes(0); expect(willAuthError).toHaveBeenCalledTimes(1); await new Promise(resolve => setTimeout(resolve)); expect(result).toHaveBeenCalledTimes(1); expect(operations.length).toBe(1); expect(operations[0]).toHaveProperty( 'context.fetchOptions.headers.Authorization', 'final-token' ); }); it('calls willAuthError on queued operations', async () => { const { exchangeArgs, result, operations } = makeExchangeArgs(); const { source, next } = makeSubject(); let initialAuthResolve: ((_?: any) => void) | undefined; const willAuthError = vi .fn() .mockReturnValueOnce(true) .mockReturnValue(false); pipe( source, authExchange(async utils => { await new Promise(resolve => { initialAuthResolve = resolve; }); let token = 'token'; return { willAuthError, didAuthError: () => false, addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: token, }); }, async refreshAuth() { token = 'final-token'; }, }; })(exchangeArgs), publish ); await Promise.resolve(); next({ ...queryOperation, key: 1 }); next({ ...queryOperation, key: 2 }); expect(result).toHaveBeenCalledTimes(0); expect(willAuthError).toHaveBeenCalledTimes(0); expect(initialAuthResolve).toBeDefined(); initialAuthResolve!(); await new Promise(resolve => setTimeout(resolve)); expect(willAuthError).toHaveBeenCalledTimes(2); expect(result).toHaveBeenCalledTimes(2); expect(operations.length).toBe(2); expect(operations[0]).toHaveProperty( 'context.fetchOptions.headers.Authorization', 'final-token' ); expect(operations[1]).toHaveProperty( 'context.fetchOptions.headers.Authorization', 'final-token' ); }); it('does not infinitely retry authentication when an operation did error', async () => { const { exchangeArgs, result, operations } = makeExchangeArgs(); const { source, next } = makeSubject(); const didAuthError = vi.fn().mockReturnValue(true); pipe( source, authExchange(async utils => { let token = 'initial-token'; return { addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: token, }); }, didAuthError, async refreshAuth() { token = 'final-token'; }, }; })(exchangeArgs), publish ); await new Promise(resolve => setTimeout(resolve)); result.mockImplementation(x => ({ ...queryResponse, operation: { ...queryResponse.operation, ...x, }, data: undefined, error: new CombinedError({ graphQLErrors: [{ message: 'Oops' }], }), })); next(queryOperation); expect(result).toHaveBeenCalledTimes(1); expect(didAuthError).toHaveBeenCalledTimes(1); await new Promise(resolve => setTimeout(resolve)); expect(result).toHaveBeenCalledTimes(2); expect(operations.length).toBe(2); expect(operations[0]).toHaveProperty( 'context.fetchOptions.headers.Authorization', 'initial-token' ); expect(operations[1]).toHaveProperty( 'context.fetchOptions.headers.Authorization', 'final-token' ); }); it('passes on failing refreshAuth() errors to results', async () => { const { exchangeArgs, result } = makeExchangeArgs(); const didAuthError = vi.fn().mockReturnValue(true); const willAuthError = vi.fn().mockReturnValue(true); const res = await pipe( fromValue(queryOperation), authExchange(async utils => { const token = 'initial-token'; return { addAuthToOperation(operation) { return utils.appendHeaders(operation, { Authorization: token, }); }, didAuthError, willAuthError, async refreshAuth() { throw new Error('test'); }, }; })(exchangeArgs), take(1), toPromise ); expect(result).toHaveBeenCalledTimes(0); expect(didAuthError).toHaveBeenCalledTimes(0); expect(willAuthError).toHaveBeenCalledTimes(1); expect(res.error).toMatchInlineSnapshot('[CombinedError: [Network] test]'); }); it('passes on errors during initialization', async () => { const { source, next } = makeSubject(); const { exchangeArgs, result } = makeExchangeArgs(); const init = vi.fn().mockRejectedValue(new Error('oops!')); const output = vi.fn(); pipe(source, authExchange(init)(exchangeArgs), tap(output), publish); expect(result).toHaveBeenCalledTimes(0); expect(output).toHaveBeenCalledTimes(0); next(queryOperation); await new Promise(resolve => setTimeout(resolve)); expect(result).toHaveBeenCalledTimes(0); expect(output).toHaveBeenCalledTimes(1); expect(init).toHaveBeenCalledTimes(1); expect(output.mock.calls[0][0].error).toMatchInlineSnapshot( '[CombinedError: [Network] oops!]' ); next(queryOperation); await new Promise(resolve => setTimeout(resolve)); expect(result).toHaveBeenCalledTimes(0); expect(output).toHaveBeenCalledTimes(2); expect(init).toHaveBeenCalledTimes(2); expect(output.mock.calls[1][0].error).toMatchInlineSnapshot( '[CombinedError: [Network] oops!]' ); }); ================================================ FILE: exchanges/auth/src/authExchange.ts ================================================ import type { Source } from 'wonka'; import { pipe, map, filter, onStart, take, makeSubject, toPromise, merge, } from 'wonka'; import type { Operation, OperationContext, OperationResult, CombinedError, Exchange, DocumentInput, AnyVariables, OperationInstance, } from '@urql/core'; import { createRequest, makeOperation, makeErrorResult } from '@urql/core'; /** Utilities to use while refreshing authentication tokens. */ export interface AuthUtilities { /** Sends a mutation to your GraphQL API, bypassing earlier exchanges and authentication. * * @param query - a GraphQL document containing the mutation operation that will be executed. * @param variables - the variables used to execute the operation. * @param context - {@link OperationContext} options that'll be used in future exchanges. * @returns A `Promise` of an {@link OperationResult} for the GraphQL mutation. * * @remarks * The `mutation()` utility method is useful when your authentication requires you to make a GraphQL mutation * request to update your authentication tokens. In these cases, you likely wish to bypass prior exchanges and * the authentication in the `authExchange` itself. * * This method bypasses the usual mutation flow of the `Client` and instead issues the mutation as directly * as possible. This also means that it doesn’t carry your `Client`'s default {@link OperationContext} * options, so you may have to pass them again, if needed. */ mutate( query: DocumentInput, variables: Variables, context?: Partial ): Promise>; /** Adds additional HTTP headers to an `Operation`. * * @param operation - An {@link Operation} to add headers to. * @param headers - The HTTP headers to add to the `Operation`. * @returns The passed {@link Operation} with the headers added to it. * * @remarks * The `appendHeaders()` utility method is useful to add additional HTTP headers * to an {@link Operation}. It’s a simple convenience function that takes * `operation.context.fetchOptions` into account, since adding headers for * authentication is common. */ appendHeaders( operation: Operation, headers: Record ): Operation; } /** Configuration for the `authExchange` returned by the initializer function you write. */ export interface AuthConfig { /** Called for every operation to add authentication data to your operation. * * @param operation - An {@link Operation} that needs authentication tokens added. * @returns a new {@link Operation} with added authentication tokens. * * @remarks * The {@link authExchange} will call this function you provide and expects that you * add your authentication tokens to your operation here, on the {@link Operation} * that is returned. * * Hint: You likely want to modify your `fetchOptions.headers` here, for instance to * add an `Authorization` header. */ addAuthToOperation(operation: Operation): Operation; /** Called before an operation is forwaded onwards to make a request. * * @param operation - An {@link Operation} that needs authentication tokens added. * @returns a boolean, if true, authentication must be refreshed. * * @remarks * The {@link authExchange} will call this function before an {@link Operation} is * forwarded onwards to your following exchanges. * * When this function returns `true`, the `authExchange` will call * {@link AuthConfig.refreshAuth} before forwarding more operations * to prompt you to update your authentication tokens. * * Hint: If you define this function, you can use it to check whether your authentication * tokens have expired. */ willAuthError?(operation: Operation): boolean; /** Called after receiving an operation result to check whether it has failed with an authentication error. * * @param error - A {@link CombinedError} that a result has come back with. * @param operation - The {@link Operation} of that has failed. * @returns a boolean, if true, authentication must be refreshed. * * @remarks * The {@link authExchange} will call this function if it sees an {@link OperationResult} * with a {@link CombinedError} on it, implying that it may have failed due to an authentication * error. * * When this function returns `true`, the `authExchange` will call * {@link AuthConfig.refreshAuth} before forwarding more operations * to prompt you to update your authentication tokens. * Afterwards, this operation will be retried once. * * Hint: You should define a function that detects your API’s authentication * errors, e.g. using `result.extensions`. */ didAuthError(error: CombinedError, operation: Operation): boolean; /** Called to refresh the authentication state. * * @remarks * The {@link authExchange} will call this function if either {@link AuthConfig.willAuthError} * or {@link AuthConfig.didAuthError} have returned `true` prior, which indicates that the * authentication state you hold has expired or is out-of-date. * * When this function is called, you should refresh your authentication state. * For instance, if you have a refresh token and an access token, you should rotate * these tokens with your API by sending the refresh token. * * Hint: You can use the {@link fetch} API here, or use {@link AuthUtilities.mutate} * if your API requires a GraphQL mutation to refresh your authentication state. */ refreshAuth(): Promise; } const addAuthAttemptToOperation = ( operation: Operation, authAttempt: boolean ) => makeOperation(operation.kind, operation, { ...operation.context, authAttempt, }); /** Creates an `Exchange` handling control flow for authentication. * * @param init - An initializer function that returns an {@link AuthConfig} wrapped in a `Promise`. * @returns the created authentication {@link Exchange}. * * @remarks * The `authExchange` is used to create an exchange handling authentication and * the control flow of refresh authentication. * * You must pass an initializer function, which receives {@link AuthUtilities} and * must return an {@link AuthConfig} wrapped in a `Promise`. * When this exchange is used in your `Client`, it will first call your initializer * function, which gives you an opportunity to get your authentication state, e.g. * from local storage. * * You may then choose to validate this authentication state and update it, and must * then return an {@link AuthConfig}. * * This configuration defines how you add authentication state to {@link Operation | Operations}, * when your authentication state expires, when an {@link OperationResult} has errored * with an authentication error, and how to refresh your authentication state. * * @example * ```ts * authExchange(async (utils) => { * let token = localStorage.getItem('token'); * let refreshToken = localStorage.getItem('refreshToken'); * return { * addAuthToOperation(operation) { * return utils.appendHeaders(operation, { * Authorization: `Bearer ${token}`, * }); * }, * didAuthError(error) { * return error.graphQLErrors.some(e => e.extensions?.code === 'FORBIDDEN'); * }, * async refreshAuth() { * const result = await utils.mutate(REFRESH, { token }); * if (result.data?.refreshLogin) { * token = result.data.refreshLogin.token; * refreshToken = result.data.refreshLogin.refreshToken; * localStorage.setItem('token', token); * localStorage.setItem('refreshToken', refreshToken); * } * }, * }; * }); * ``` */ export function authExchange( init: (utilities: AuthUtilities) => Promise ): Exchange { return ({ client, forward }) => { const bypassQueue = new Set(); const retries = makeSubject(); const errors = makeSubject(); let retryQueue = new Map(); function flushQueue() { authPromise = undefined; const queue = retryQueue; retryQueue = new Map(); queue.forEach(retries.next); } function errorQueue(error: Error) { authPromise = undefined; const queue = retryQueue; retryQueue = new Map(); queue.forEach(operation => { errors.next(makeErrorResult(operation, error)); }); } let authPromise: Promise | void; let config: AuthConfig | null = null; return operations$ => { function initAuth() { authPromise = Promise.resolve() .then(() => init({ mutate( query: DocumentInput, variables: Variables, context?: Partial ): Promise> { const baseOperation = client.createRequestOperation( 'mutation', createRequest(query, variables), context ); return pipe( result$, onStart(() => { const operation = addAuthToOperation(baseOperation); bypassQueue.add( operation.context._instance as OperationInstance ); retries.next(operation); }), filter( result => result.operation.key === baseOperation.key && baseOperation.context._instance === result.operation.context._instance ), take(1), toPromise ); }, appendHeaders( operation: Operation, headers: Record ) { const fetchOptions = typeof operation.context.fetchOptions === 'function' ? operation.context.fetchOptions() : operation.context.fetchOptions || {}; return makeOperation(operation.kind, operation, { ...operation.context, fetchOptions: { ...fetchOptions, headers: { ...fetchOptions.headers, ...headers, }, }, }); }, }) ) .then((_config: AuthConfig) => { if (_config) config = _config; flushQueue(); }) .catch((error: Error) => { if (process.env.NODE_ENV !== 'production') { console.warn( 'authExchange()’s initialization function has failed, which is unexpected.\n' + 'If your initialization function is expected to throw/reject, catch this error and handle it explicitly.\n' + 'Unless this error is handled it’ll be passed onto any `OperationResult` instantly and authExchange() will block further operations and retry.', error ); } errorQueue(error); }); } initAuth(); function refreshAuth(operation: Operation) { // add to retry queue to try again later retryQueue.set( operation.key, addAuthAttemptToOperation(operation, true) ); // check that another operation isn't already doing refresh if (config && !authPromise) { authPromise = config.refreshAuth().then(flushQueue).catch(errorQueue); } } function willAuthError(operation: Operation) { return ( !operation.context.authAttempt && config && config.willAuthError && config.willAuthError(operation) ); } function didAuthError(result: OperationResult) { return ( config && config.didAuthError && config.didAuthError(result.error!, result.operation) ); } function addAuthToOperation(operation: Operation) { return config ? config.addAuthToOperation(operation) : operation; } const opsWithAuth$ = pipe( merge([retries.source, operations$]), map(operation => { if (operation.kind === 'teardown') { retryQueue.delete(operation.key); return operation; } else if ( operation.context._instance && bypassQueue.has(operation.context._instance) ) { return operation; } else if (operation.context.authAttempt) { return addAuthToOperation(operation); } else if (authPromise || !config) { if (!authPromise) initAuth(); if (!retryQueue.has(operation.key)) retryQueue.set( operation.key, addAuthAttemptToOperation(operation, false) ); return null; } else if (willAuthError(operation)) { refreshAuth(operation); return null; } return addAuthToOperation( addAuthAttemptToOperation(operation, false) ); }), filter(Boolean) ) as Source; const result$ = pipe(opsWithAuth$, forward); return merge([ errors.source, pipe( result$, filter(result => { if ( !bypassQueue.has(result.operation.context._instance) && result.error && didAuthError(result) && !result.operation.context.authAttempt ) { refreshAuth(result.operation); return false; } if (bypassQueue.has(result.operation.context._instance)) { bypassQueue.delete(result.operation.context._instance); } return true; }) ), ]); }; }; } ================================================ FILE: exchanges/auth/src/index.ts ================================================ export { authExchange } from './authExchange'; export type { AuthUtilities, AuthConfig } from './authExchange'; ================================================ FILE: exchanges/auth/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/auth/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: exchanges/context/CHANGELOG.md ================================================ # Changelog ## 1.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 0.3.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 0.3.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 0.2.1 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 0.2.0 ### Minor Changes - Update exchanges to drop redundant `share` calls, since `@urql/core`’s `composeExchanges` utility now automatically does so for us Submitted by [@kitten](https://github.com/kitten) (See [#3082](https://github.com/urql-graphql/urql/pull/3082)) ### Patch Changes - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs for all exchanges, documenting API internals Submitted by [@kitten](https://github.com/kitten) (See [#3072](https://github.com/urql-graphql/urql/pull/3072)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## v0.1.0 **Initial Release** ================================================ FILE: exchanges/context/README.md ================================================

@urql/exchange-context

An exchange for setting operation context in urql

`@urql/exchange-context` is an exchange for the [`urql`](https://github.com/urql-graphql/urql) GraphQL client which can set the operation context both synchronously as well as asynchronously ## Quick Start Guide First install `@urql/exchange-context` alongside `urql`: ```sh yarn add @urql/exchange-context # or npm install --save @urql/exchange-context ``` You'll then need to add the `contextExchange`, that this package exposes, to your `urql` Client, the positioning of this exchange depends on whether you set an async setter or not. If you set an async context-setter it's best placed after all the synchronous exchanges (in front of the fetchExchange). ```js import { createClient, cacheExchange, fetchExchange } from 'urql'; import { contextExchange } from '@urql/exchange-context'; const client = createClient({ url: 'http://localhost:1234/graphql', exchanges: [ cacheExchange, contextExchange({ getContext: async operation => { const token = await getToken(); return { ...operation.context, headers: { authorization: token } }; }, }), fetchExchange, ], }); ``` ================================================ FILE: exchanges/context/jsr.json ================================================ { "name": "@urql/exchange-context", "version": "1.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/context/package.json ================================================ { "name": "@urql/exchange-context", "version": "1.0.0", "description": "An exchange for setting (a)synchronous operation-context in urql", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/context" }, "keywords": [ "urql", "exchange", "context", "graphql", "exchanges" ], "main": "dist/urql-exchange-context", "module": "dist/urql-exchange-context.mjs", "types": "dist/urql-exchange-context.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-context.d.ts", "import": "./dist/urql-exchange-context.mjs", "require": "./dist/urql-exchange-context.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist extras", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "peerDependencies": { "@urql/core": "^6.0.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "devDependencies": { "@urql/core": "workspace:*", "graphql": "^16.0.0" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/context/src/context.test.ts ================================================ import { pipe, map, makeSubject, publish, tap } from 'wonka'; import { vi, expect, it, beforeEach } from 'vitest'; import { gql, createClient, Operation, OperationResult, ExchangeIO, } from '@urql/core'; import { queryResponse } from '../../../packages/core/src/test-utils'; import { contextExchange } from './context'; const queryOne = gql` { author { id name } } `; const queryOneData = { __typename: 'Query', author: { __typename: 'Author', id: '123', name: 'Author', }, }; const dispatchDebug = vi.fn(); let client, op, ops$, next; beforeEach(() => { client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); op = client.createRequestOperation('query', { key: 1, query: queryOne, }); ({ source: ops$, next } = makeSubject()); }); it(`calls getContext`, () => { const response = vi.fn((forwardOp: Operation): OperationResult => { return { ...queryResponse, operation: forwardOp, data: queryOneData, }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; const headers = { hello: 'world' }; pipe( contextExchange({ getContext: op => ({ ...op.context, headers }), })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); expect(response).toHaveBeenCalledTimes(1); expect(response.mock.calls[0][0].context.headers).toEqual(headers); expect(result).toHaveBeenCalledTimes(1); }); it(`calls getContext async`, async () => { const response = vi.fn((forwardOp: Operation): OperationResult => { return { ...queryResponse, operation: forwardOp, data: queryOneData, }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; const headers = { hello: 'world' }; pipe( contextExchange({ getContext: async op => { await Promise.resolve(); return { ...op.context, headers }; }, })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); await new Promise(res => { setTimeout(() => { expect(response).toHaveBeenCalledTimes(1); expect(response.mock.calls[0][0].context.headers).toEqual(headers); expect(result).toHaveBeenCalledTimes(1); res(null); }, 10); }); }); ================================================ FILE: exchanges/context/src/context.ts ================================================ import type { Exchange, Operation, OperationContext } from '@urql/core'; import { makeOperation } from '@urql/core'; import { fromPromise, fromValue, mergeMap, pipe } from 'wonka'; /** Input parameters for the {@link contextExchange}. */ export interface ContextExchangeArgs { /** Returns a new {@link OperationContext}, optionally wrapped in a `Promise`. * * @remarks * `getContext` is called for every {@link Operation} the `contextExchange` * receives and must return a new {@link OperationContext} or a `Promise` * of it. * * The new `OperationContext` will be used to update the `Operation`'s * context before it's forwarded to the next exchange. */ getContext( operation: Operation ): OperationContext | Promise; } /** Exchange factory modifying the {@link OperationContext} per incoming `Operation`. * * @param options - A {@link ContextExchangeArgs} configuration object. * @returns the created context {@link Exchange}. * * @remarks * The `contextExchange` allows the {@link OperationContext` to be easily * modified per `Operation`. This may be useful to dynamically change the * `Operation`’s parameters, even when we need to do so asynchronously. * * You must define a {@link ContextExchangeArgs.getContext} function, * which may return a `Promise` or `OperationContext`. * * Hint: If the `getContext` function passed to this exchange returns a * `Promise` it must be placed _after_ all synchronous exchanges, such as * a `cacheExchange`. * * @example * ```ts * import { Client, cacheExchange, fetchExchange } from '@urql/core'; * import { contextExchange } from '@urql/exchange-context'; * * const client = new Client({ * url: '', * exchanges: [ * cacheExchange, * contextExchange({ * async getContext(operation) { * const url = await loadDynamicUrl(); * return { * ...operation.context, * url, * }; * }, * }), * fetchExchange, * ], * }); * ``` */ export const contextExchange = ({ getContext }: ContextExchangeArgs): Exchange => ({ forward }) => { return ops$ => { return pipe( ops$, mergeMap(operation => { const result = getContext(operation); const isPromise = 'then' in result; if (isPromise) { return fromPromise( result.then((ctx: OperationContext) => makeOperation(operation.kind, operation, ctx) ) ); } else { return fromValue( makeOperation( operation.kind, operation, result as OperationContext ) ); } }), forward ); }; }; ================================================ FILE: exchanges/context/src/index.ts ================================================ export { contextExchange } from './context'; export type { ContextExchangeArgs } from './context'; ================================================ FILE: exchanges/context/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/context/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: exchanges/execute/CHANGELOG.md ================================================ # Changelog ## 3.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 2.3.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 2.3.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 2.2.2 ### Patch Changes - Update build process to generate correct source maps Submitted by [@kitten](https://github.com/kitten) (See [#3201](https://github.com/urql-graphql/urql/pull/3201)) ## 2.2.1 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 2.2.0 ### Minor Changes - Update exchanges to drop redundant `share` calls, since `@urql/core`’s `composeExchanges` utility now automatically does so for us Submitted by [@kitten](https://github.com/kitten) (See [#3082](https://github.com/urql-graphql/urql/pull/3082)) - Remove `getOperationName` export from `@urql/core` Submitted by [@kitten](https://github.com/kitten) (See [#3062](https://github.com/urql-graphql/urql/pull/3062)) ### Patch Changes - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs for all exchanges, documenting API internals Submitted by [@kitten](https://github.com/kitten) (See [#3072](https://github.com/urql-graphql/urql/pull/3072)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 2.1.1 ### Patch Changes - ⚠️ Fix type-generation, with a change in TS/Rollup the type generation took the paths as src and resolved them into the types dir, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2870](https://github.com/urql-graphql/urql/pull/2870)) - Updated dependencies (See [#2872](https://github.com/urql-graphql/urql/pull/2872), [#2870](https://github.com/urql-graphql/urql/pull/2870), and [#2871](https://github.com/urql-graphql/urql/pull/2871)) - @urql/core@3.1.1 ## 2.1.0 ### Minor Changes - The `context` option, which may be set to a context value or a function returning a context, can now return a `Promise` and will be correctly resolved and awaited, by [@YutaUra](https://github.com/YutaUra) (See [#2806](https://github.com/urql-graphql/urql/pull/2806)) ### Patch Changes - End iterator when teardown functions runs, previously it waited for one extra call to next, then ended the iterator, by [@danielkaxis](https://github.com/danielkaxis) (See [#2803](https://github.com/urql-graphql/urql/pull/2803)) - Updated dependencies (See [#2843](https://github.com/urql-graphql/urql/pull/2843), [#2847](https://github.com/urql-graphql/urql/pull/2847), [#2850](https://github.com/urql-graphql/urql/pull/2850), and [#2846](https://github.com/urql-graphql/urql/pull/2846)) - @urql/core@3.1.0 ## 2.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Minor Changes - Remove the `babel-plugin-modular-graphql` helper, this because the graphql package hasn't converted to ESM yet which gives issues in node environments, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2551](https://github.com/FormidableLabs/urql/pull/2551)) ### Patch Changes - ⚠️ fix return for context function argument, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2583](https://github.com/FormidableLabs/urql/pull/2583)) - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 1.2.3 ### Patch Changes - Support using default values with directives. Previously, using a variables with a default value within a directive would fail the validation if it is empty, by [@fathyb](https://github.com/fathyb) (See [#2435](https://github.com/FormidableLabs/urql/pull/2435)) ## 1.2.2 ### Patch Changes - Upgrade modular imports for graphql package, which fixes an issue in `@urql/exchange-execute`, where `graphql@16` files wouldn't resolve the old `subscribe` import from the correct file, by [@kitten](https://github.com/kitten) (See [#2149](https://github.com/FormidableLabs/urql/pull/2149)) ## 1.2.1 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 1.2.0 ### Minor Changes - Add subscription support, by [@Tigge](https://github.com/Tigge) (See [#2061](https://github.com/FormidableLabs/urql/pull/2061)) ### Patch Changes - Updated dependencies (See [#2074](https://github.com/FormidableLabs/urql/pull/2074)) - @urql/core@2.3.5 ## 1.1.0 ### Minor Changes - Support async iterated results, including subscriptions via `AsyncIterator` support and `@defer` / `@stream` if the appropriate version of `graphql` is used, e.g. `15.4.0-experimental-stream-defer.1`, by [@kitten](https://github.com/kitten) (See [#1854](https://github.com/FormidableLabs/urql/pull/1854)) ### Patch Changes - Updated dependencies (See [#1854](https://github.com/FormidableLabs/urql/pull/1854)) - @urql/core@2.3.0 ## 1.0.5 ### Patch Changes - Expose `ExecuteExchangeArgs` interface, by [@taneba](https://github.com/taneba) (See [#1837](https://github.com/FormidableLabs/urql/pull/1837)) - Updated dependencies (See [#1829](https://github.com/FormidableLabs/urql/pull/1829)) - @urql/core@2.1.6 ## 1.0.4 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 1.0.3 ### Patch Changes - Export `getOperationName` from `@urql/core` and use it in `@urql/exchange-execute`, fixing several imports, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1135](https://github.com/FormidableLabs/urql/pull/1135)) - Updated dependencies (See [#1135](https://github.com/FormidableLabs/urql/pull/1135)) - @urql/core@1.15.1 ## 1.0.2 ### Patch Changes - Add missing `.mjs` extension to all imports from `graphql` to fix Webpack 5 builds, which require extension-specific import paths for ESM bundles and packages. **This change allows you to safely upgrade to Webpack 5.**, by [@kitten](https://github.com/kitten) (See [#1094](https://github.com/FormidableLabs/urql/pull/1094)) - Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was previously confusing as `operationName` was effectively referring to two different things. You can safely upgrade to this new version, however to mute all deprecation warnings you will have to **upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please use [the new `makeOperation` helper function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead, by [@bkonkle](https://github.com/bkonkle) (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - Updated dependencies (See [#1094](https://github.com/FormidableLabs/urql/pull/1094) and [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - @urql/core@1.14.0 ## 1.0.1 ### Patch Changes - Upgrade to a minimum version of wonka@^4.0.14 to work around issues with React Native's minification builds, which use uglify-es and could lead to broken bundles, by [@kitten](https://github.com/kitten) (See [#842](https://github.com/FormidableLabs/urql/pull/842)) - Updated dependencies (See [#838](https://github.com/FormidableLabs/urql/pull/838) and [#842](https://github.com/FormidableLabs/urql/pull/842)) - @urql/core@1.12.0 ## v1.0.0 **Initial Release** ================================================ FILE: exchanges/execute/README.md ================================================

@urql/exchange-execute

An exchange for executing queries against a local schema in urql

`@urql/exchange-execute` is an exchange for the [`urql`](https://github.com/urql-graphql/urql) GraphQL client which executes queries against a local schema. This is a replacement for the default _fetchExchange_ which sends queries over HTTP/S to be executed remotely. ## Quick Start Guide First install `@urql/exchange-execute` alongside `urql`: ```sh yarn add @urql/exchange-execute # or npm install --save @urql/exchange-execute ``` You'll then need to add the `executeExchange`, that this package exposes, to your `urql` Client, by replacing the default fetch exchange with it: ```js import { createClient, cacheExchange } from 'urql'; import { executeExchange } from '@urql/exchange-execute'; const client = createClient({ url: 'http://localhost:1234/graphql', exchanges: [ cacheExchange, // Replace the default fetchExchange with the new one. executeExchange({ /* config */ }), ], }); ``` ## Usage The exchange takes the same arguments as the [_execute_ function](https://graphql.org/graphql-js/execution/#execute) provided by graphql-js. Here's a brief example of how it might be used: ```js import { buildSchema } from 'graphql'; // Create local schema const schema = buildSchema(` type Todo { id: ID! text: String! } type Query { todos: [Todo]! } type Mutation { addTodo(text: String!): Todo! } `); // Create local state let todos = []; // Create root value with resolvers const rootValue = { todos: () => todos, addTodo: (_, args) => { const todo = { id: todos.length.toString(), ...args }; todos = [...todos, todo]; return todo; } } // ... // Pass schema and root value to executeExchange executeExchange({ schema, rootValue, }), // ... ``` ================================================ FILE: exchanges/execute/jsr.json ================================================ { "name": "@urql/exchange-execute", "version": "3.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/execute/package.json ================================================ { "name": "@urql/exchange-execute", "version": "3.0.0", "description": "An exchange for executing queries against a local schema in urql", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/execute" }, "keywords": [ "urql", "exchange", "execute", "executable schema", "graphql", "exchanges" ], "main": "dist/urql-exchange-execute", "module": "dist/urql-exchange-execute.mjs", "types": "dist/urql-exchange-execute.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-execute.d.ts", "import": "./dist/urql-exchange-execute.mjs", "require": "./dist/urql-exchange-execute.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist extras", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "peerDependencies": { "@urql/core": "^6.0.0", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" }, "devDependencies": { "@urql/core": "workspace:*", "graphql": "^16.0.0" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/execute/src/execute.test.ts ================================================ import { vi, expect, it, beforeEach, afterEach, describe, Mock } from 'vitest'; vi.mock('graphql', async () => { const graphql = await vi.importActual('graphql'); return { __esModule: true, ...(graphql as object), print: vi.fn(() => '{ placeholder }'), execute: vi.fn(() => ({ key: 'value' })), subscribe: vi.fn(), }; }); import { fetchExchange } from '@urql/core'; import { executeExchange } from './execute'; import { execute, print, subscribe } from 'graphql'; import { pipe, fromValue, toPromise, take, makeSubject, empty, Source, } from 'wonka'; import { context, queryOperation, subscriptionOperation, } from '../../../packages/core/src/test-utils'; import { makeErrorResult, makeOperation, Client, OperationResult, } from '@urql/core'; const mocked = (x: any): any => x; const schema = 'STUB_SCHEMA' as any; const exchangeArgs = { forward: a => a, client: {}, } as any; const expectedQueryOperationName = 'getUser'; const expectedSubscribeOperationName = 'subscribeToUser'; const fetchMock = (globalThis as any).fetch as Mock; const mockHttpResponseData = { key: 'value' }; beforeEach(() => { vi.clearAllMocks(); mocked(print).mockImplementation(a => a as any); mocked(execute).mockResolvedValue({ data: mockHttpResponseData }); mocked(subscribe).mockImplementation(async function* x(this: any) { yield { data: { key: 'value1' } }; yield { data: { key: 'value2' } }; yield { data: { key: 'value3' } }; }); }); afterEach(() => { fetchMock.mockClear(); }); describe('on operation', () => { it('calls execute with args', async () => { const context = 'USER_ID=123'; await pipe( fromValue(queryOperation), executeExchange({ schema, context })(exchangeArgs), take(1), toPromise ); expect(mocked(execute)).toBeCalledTimes(1); expect(mocked(execute)).toBeCalledWith({ schema, document: queryOperation.query, rootValue: undefined, contextValue: context, variableValues: queryOperation.variables, operationName: expectedQueryOperationName, fieldResolver: undefined, typeResolver: undefined, subscribeFieldResolver: undefined, }); }); it('calls subscribe with args', async () => { const context = 'USER_ID=123'; await pipe( fromValue(subscriptionOperation), executeExchange({ schema, context })(exchangeArgs), take(3), toPromise ); expect(mocked(subscribe)).toBeCalledTimes(1); expect(mocked(subscribe)).toBeCalledWith({ schema, document: subscriptionOperation.query, rootValue: undefined, contextValue: context, variableValues: subscriptionOperation.variables, operationName: expectedSubscribeOperationName, fieldResolver: undefined, typeResolver: undefined, subscribeFieldResolver: undefined, }); }); it('calls execute after executing context as a function', async () => { const context = operation => { expect(operation).toBe(queryOperation); return 'CALCULATED_USER_ID=' + 8 * 10; }; await pipe( fromValue(queryOperation), executeExchange({ schema, context })(exchangeArgs), take(1), toPromise ); expect(mocked(execute)).toBeCalledTimes(1); expect(mocked(execute)).toBeCalledWith({ schema, document: queryOperation.query, rootValue: undefined, contextValue: 'CALCULATED_USER_ID=80', variableValues: queryOperation.variables, operationName: expectedQueryOperationName, fieldResolver: undefined, typeResolver: undefined, subscribeFieldResolver: undefined, }); }); it('calls execute after executing context as a function returning a Promise', async () => { const context = async operation => { expect(operation).toBe(queryOperation); return 'CALCULATED_USER_ID=' + 8 * 10; }; await pipe( fromValue(queryOperation), executeExchange({ schema, context })(exchangeArgs), take(1), toPromise ); expect(mocked(execute)).toBeCalledTimes(1); expect(mocked(execute)).toBeCalledWith({ schema, document: queryOperation.query, rootValue: undefined, contextValue: 'CALCULATED_USER_ID=80', variableValues: queryOperation.variables, operationName: expectedQueryOperationName, fieldResolver: undefined, typeResolver: undefined, subscribeFieldResolver: undefined, }); }); it('should return data from subscribe', async () => { const context = 'USER_ID=123'; const responseFromExecuteExchange = await pipe( fromValue(subscriptionOperation), executeExchange({ schema, context })(exchangeArgs), take(3), toPromise ); expect(responseFromExecuteExchange.data).toEqual({ key: 'value3' }); }); it('should return the same data as the fetch exchange', async () => { const context = 'USER_ID=123'; const responseFromExecuteExchange = await pipe( fromValue(queryOperation), executeExchange({ schema, context })(exchangeArgs), take(1), toPromise ); fetchMock.mockResolvedValue({ status: 200, headers: { get: () => 'application/json' }, text: vi .fn() .mockResolvedValue(JSON.stringify({ data: mockHttpResponseData })), }); const responseFromFetchExchange = await pipe( fromValue(queryOperation), fetchExchange({ dispatchDebug: vi.fn(), forward: () => empty as Source, client: {} as Client, }), toPromise ); expect(responseFromExecuteExchange.data).toEqual( responseFromFetchExchange.data ); expect(mocked(execute)).toBeCalledTimes(1); expect(fetchMock).toBeCalledTimes(1); }); it('should trim undefined values before calling execute()', async () => { const contextValue = 'USER_ID=123'; const operation = makeOperation( 'query', { ...queryOperation, variables: { ...queryOperation.variables, withLastName: undefined }, }, context ); await pipe( fromValue(operation), executeExchange({ schema, context: contextValue })(exchangeArgs), take(1), toPromise ); expect(mocked(execute)).toBeCalledTimes(1); expect(mocked(execute)).toBeCalledWith({ schema, document: queryOperation.query, rootValue: undefined, contextValue: contextValue, variableValues: queryOperation.variables, operationName: expectedQueryOperationName, fieldResolver: undefined, typeResolver: undefined, subscribeFieldResolver: undefined, }); const variables = mocked(execute).mock.calls[0][0].variableValues; for (const key in variables) { expect(variables[key]).not.toBeUndefined(); } }); }); describe('on success response', () => { it('returns operation result', async () => { const response = await pipe( fromValue(queryOperation), executeExchange({ schema })(exchangeArgs), take(1), toPromise ); expect(response).toEqual({ operation: queryOperation, data: mockHttpResponseData, hasNext: false, stale: false, }); }); }); describe('on error response', () => { const errors = ['error'] as any; beforeEach(() => { mocked(execute).mockResolvedValue({ errors }); }); it('returns operation result', async () => { const response = await pipe( fromValue(queryOperation), executeExchange({ schema })(exchangeArgs), take(1), toPromise ); expect(response).toHaveProperty('operation', queryOperation); expect(response).toHaveProperty('error'); }); }); describe('on thrown error', () => { const errors = ['error'] as any; beforeEach(() => { mocked(execute).mockRejectedValue({ errors }); }); it('returns operation result', async () => { const response = await pipe( fromValue(queryOperation), executeExchange({ schema })(exchangeArgs), take(1), toPromise ); const expected = makeErrorResult(queryOperation, errors); expect(response.operation).toBe(expected.operation); expect(response.data).toEqual(expected.data); expect(response.error).toEqual(expected.error); }); }); describe('on unsupported operation', () => { const operation = makeOperation( 'teardown', queryOperation, queryOperation.context ); it('returns operation result', async () => { const { source, next } = makeSubject(); const response = pipe( source, executeExchange({ schema })(exchangeArgs), take(1), toPromise ); next(operation); expect(await response).toEqual(operation); }); }); ================================================ FILE: exchanges/execute/src/execute.ts ================================================ import type { Source } from 'wonka'; import { pipe, filter, takeUntil, mergeMap, merge, make } from 'wonka'; import type { GraphQLSchema, GraphQLFieldResolver, GraphQLTypeResolver, ExecutionArgs, SubscriptionArgs, } from 'graphql'; import { execute, subscribe, Kind } from 'graphql'; import type { Exchange, ExecutionResult, Operation, OperationResult, } from '@urql/core'; import { makeResult, makeErrorResult, mergeResultPatch } from '@urql/core'; /** Input parameters for the {@link executeExchange}. * @see {@link ExecutionArgs} which this interface mirrors. */ export interface ExecuteExchangeArgs { /** GraphQL Schema definition that `Operation`s are execute against. */ schema: GraphQLSchema; /** Context object or a factory function creating a `context` object. * * @remarks * The `context` that is passed to the `schema` may either be passed * or created from an incoming `Operation`, which also allows it to * be recreated per `Operation`. */ context?: ((operation: Operation) => any) | any; rootValue?: any; fieldResolver?: GraphQLFieldResolver; typeResolver?: GraphQLTypeResolver; subscribeFieldResolver?: GraphQLFieldResolver; } type ExecuteParams = ExecutionArgs | SubscriptionArgs; const asyncIterator = typeof Symbol !== 'undefined' ? Symbol.asyncIterator : null; const makeExecuteSource = ( operation: Operation, _args: ExecuteParams ): Source => { return make(observer => { let iterator: AsyncIterator; let ended = false; Promise.resolve() .then(async () => ({ ..._args, contextValue: await _args.contextValue })) .then(args => { if (ended) return; if (operation.kind === 'subscription') { return subscribe(args) as any; } return execute(args) as any; }) .then((result: ExecutionResult | AsyncIterable) => { if (ended || !result) { return; } else if (!asyncIterator || !result[asyncIterator]) { observer.next(makeResult(operation, result as ExecutionResult)); return; } iterator = result[asyncIterator!](); let prevResult: OperationResult | null = null; function next({ done, value, }: { done?: boolean; value: ExecutionResult; }) { if (value) { observer.next( (prevResult = prevResult ? mergeResultPatch(prevResult, value) : makeResult(operation, value)) ); } if (!done && !ended) { return iterator.next().then(next); } } return iterator.next().then(next); }) .then(() => { observer.complete(); }) .catch(error => { observer.next(makeErrorResult(operation, error)); observer.complete(); }); return () => { if (iterator && iterator.return) iterator.return(); ended = true; }; }); }; /** Exchange factory that executes operations against a GraphQL schema. * * @param options - A {@link ExecuteExchangeArgs} configuration object. * @returns the created execute {@link Exchange}. * * @remarks * The `executeExchange` executes GraphQL operations against the `schema` * that it’s passed. As such, its options mirror the options that GraphQL.js’ * {@link execute} function accepts. */ export const executeExchange = (options: ExecuteExchangeArgs): Exchange => ({ forward }) => { return operations$ => { const executedOps$ = pipe( operations$, filter((operation: Operation) => { return ( operation.kind === 'query' || operation.kind === 'mutation' || operation.kind === 'subscription' ); }), mergeMap((operation: Operation) => { const { key } = operation; const teardown$ = pipe( operations$, filter(op => op.kind === 'teardown' && op.key === key) ); const contextValue = typeof options.context === 'function' ? options.context(operation) : options.context; // Filter undefined values from variables before calling execute() // to support default values within directives. const variableValues = Object.create(null); if (operation.variables) { for (const key in operation.variables) { if (operation.variables[key] !== undefined) { variableValues[key] = operation.variables[key]; } } } let operationName: string | undefined; for (const node of operation.query.definitions) { if (node.kind === Kind.OPERATION_DEFINITION) { operationName = node.name ? node.name.value : undefined; break; } } return pipe( makeExecuteSource(operation, { schema: options.schema, document: operation.query, rootValue: options.rootValue, contextValue, variableValues, operationName, fieldResolver: options.fieldResolver, typeResolver: options.typeResolver, subscribeFieldResolver: options.subscribeFieldResolver, }), takeUntil(teardown$) ); }) ); const forwardedOps$ = pipe( operations$, filter(operation => operation.kind === 'teardown'), forward ); return merge([executedOps$, forwardedOps$]); }; }; ================================================ FILE: exchanges/execute/src/index.ts ================================================ export { executeExchange } from './execute'; export type { ExecuteExchangeArgs } from './execute'; ================================================ FILE: exchanges/execute/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/execute/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: exchanges/graphcache/.gitignore ================================================ /extras /default-storage ================================================ FILE: exchanges/graphcache/CHANGELOG.md ================================================ # @urql/exchange-graphcache ## 9.0.0 ### Major Changes - Don't serialize data to IDB. This invalidates all existing data, but greatly improves performance of read/write operations Submitted by [@ThaUnknown](https://github.com/ThaUnknown) (See [#3824](https://github.com/urql-graphql/urql/pull/3824)) ## 8.1.0 ### Minor Changes - Add possibleTypes config for deterministic fragment matching Submitted by [@xuanduc987](https://github.com/xuanduc987) (See [#3805](https://github.com/urql-graphql/urql/pull/3805)) ## 8.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 7.2.4 ### Patch Changes - ⚠️ Fix compatibility with Typescript >5.5 (See: https://github.com/0no-co/graphql.web/pull/49) Submitted by [@andreisergiu98](https://github.com/andreisergiu98) (See [#3730](https://github.com/urql-graphql/urql/pull/3730)) - Updated dependencies (See [#3773](https://github.com/urql-graphql/urql/pull/3773), [#3767](https://github.com/urql-graphql/urql/pull/3767), [#3730](https://github.com/urql-graphql/urql/pull/3730), and [#3770](https://github.com/urql-graphql/urql/pull/3770)) - @urql/core@5.1.2 ## 7.2.3 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 7.2.2 ### Patch Changes - Remove addMetadata transform where we'd strip out metadata for production environments, this particularly affects OperationResult.context.metadata.cacheOutcome Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3744](https://github.com/urql-graphql/urql/pull/3744)) ## 7.2.1 ### Patch Changes - Update selection iterator implementation for JSC memory reduction Submitted by [@kitten](https://github.com/kitten) (See [#3693](https://github.com/urql-graphql/urql/pull/3693)) ## 7.2.0 ### Minor Changes - Allow @\_required directive to be used in combination with configured schemas Submitted by [@AndrewIngram](https://github.com/AndrewIngram) (See [#3685](https://github.com/urql-graphql/urql/pull/3685)) ## 7.1.3 ### Patch Changes - ⚠️ fix bug that mutation would cause dependent operations and reexecuting operations to become the same set Submitted by [@xuanduc987](https://github.com/xuanduc987) (See [#3665](https://github.com/urql-graphql/urql/pull/3665)) ## 7.1.2 ### Patch Changes - Disregard write-only operation when fragment-matching with schema awareness Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3621](https://github.com/urql-graphql/urql/pull/3621)) ## 7.1.1 ### Patch Changes - ⚠️ Fix where we would incorrectly match all fragment concrete types because they belong to the abstract type Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3603](https://github.com/urql-graphql/urql/pull/3603)) ## 7.1.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 7.0.2 ### Patch Changes - Only record dependencies that are changing data, this will reduce the amount of operations we re-invoke due to network-only/cache-and-network queries and mutations Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3564](https://github.com/urql-graphql/urql/pull/3564)) ## 7.0.1 ### Patch Changes - When invoking the automatic creation updater ignore the entity we are currently on in the mutation Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3560](https://github.com/urql-graphql/urql/pull/3560)) ## 7.0.0 ### Major Changes - Add a default updater for mutation fields who are lacking an updater and where the returned entity is not present in the cache Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3518](https://github.com/urql-graphql/urql/pull/3518)) - Remove deprecated `resolveFieldByKey`, use `cache.resolve` instead Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3520](https://github.com/urql-graphql/urql/pull/3520)) ### Minor Changes - Track abstract types being written so that we have a more reliable way of matching abstract fragments Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3548](https://github.com/urql-graphql/urql/pull/3548)) ### Patch Changes - ⚠️ Fix `invalidate` not applying when using a string to invalidate an entity Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3545](https://github.com/urql-graphql/urql/pull/3545)) - Upgrade `@0no-co/graphql.web` to `1.0.5` Submitted by [@kitten](https://github.com/kitten) (See [#3553](https://github.com/urql-graphql/urql/pull/3553)) - Updated dependencies (See [#3520](https://github.com/urql-graphql/urql/pull/3520), [#3553](https://github.com/urql-graphql/urql/pull/3553), and [#3520](https://github.com/urql-graphql/urql/pull/3520)) - @urql/core@5.0.0 ## 6.5.0 ### Minor Changes - Allow `@_optional` and `@_required` to be placed on fragment definitions and inline fragments Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3502](https://github.com/urql-graphql/urql/pull/3502)) - Track list of entity keys for a given type name. This enables enumerating and invalidating all entities of a given type within the normalized cache Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3501](https://github.com/urql-graphql/urql/pull/3501)) ### Patch Changes - Prevent `@defer` from being applied in child field selections. Previously, a child field (i.e. a nested field) under a `@defer`-ed fragment would also become optional, which was based on a prior version of the DeferStream spec which didn't require deferred fields to be delivered as a group Submitted by [@kitten](https://github.com/kitten) (See [#3517](https://github.com/urql-graphql/urql/pull/3517)) - ⚠️ Fix `store.resolve()` returning the exact link array that’s used by the cache. This can lead to subtle bugs when a user mutates the result returned by `cache.resolve()`, since this directly mutates what’s in the cache at that layer Submitted by [@kitten](https://github.com/kitten) (See [#3516](https://github.com/urql-graphql/urql/pull/3516)) - Updated dependencies (See [#3514](https://github.com/urql-graphql/urql/pull/3514), [#3505](https://github.com/urql-graphql/urql/pull/3505), [#3499](https://github.com/urql-graphql/urql/pull/3499), and [#3515](https://github.com/urql-graphql/urql/pull/3515)) - @urql/core@4.3.0 ## 6.4.1 ### Patch Changes - Set `stale: true` on cache results, even if a reexecution has been blocked by the loop protection, if the operation is already pending and in-flight Submitted by [@kitten](https://github.com/kitten) (See [#3493](https://github.com/urql-graphql/urql/pull/3493)) - ⚠️ Fix `@defer` state leaking into following operations Submitted by [@kitten](https://github.com/kitten) (See [#3497](https://github.com/urql-graphql/urql/pull/3497)) ## 6.4.0 ### Minor Changes - Allow the user to debug cache-misses by means of the new `logger` interface on the `cacheExchange`. A field miss will dispatch a `debug` log when it's not marked with `@_optional` or when it's non-nullable in the `schema` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3446](https://github.com/urql-graphql/urql/pull/3446)) - Add `onCacheHydrated` as an option for the `StorageAdapter` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3428](https://github.com/urql-graphql/urql/pull/3428)) - Add optional `logger` to the options, this allows you to filter out warnings or disable them all together Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3444](https://github.com/urql-graphql/urql/pull/3444)) ## 6.3.3 ### Patch Changes - ⚠️ Fix a typo that caused an inverted condition, for checking owned data, to cause incorrect results when handling `null` values and encountering them first Submitted by [@kitten](https://github.com/kitten) (See [#3371](https://github.com/urql-graphql/urql/pull/3371)) ## 6.3.2 ### Patch Changes - ⚠️ Fix extra variables in mutation results regressing by a change made in [#3317](https://github.com/urql-graphql/urql/pull/3317). The original operation wasn't being preserved anymore Submitted by [@kitten](https://github.com/kitten) (See [#3356](https://github.com/urql-graphql/urql/pull/3356)) ## 6.3.1 ### Patch Changes - Reset `partial` result marker when reading from selections when a child value sees a cache miss. This only affects resolvers on child values enabling `info.partial` while a parent may abort early instead Submitted by [@kitten](https://github.com/kitten) (See [#3340](https://github.com/urql-graphql/urql/pull/3340)) - ⚠️ Fix `@_optional` directive not setting `info.partial = true` on cache miss and fix usage of `info.parentKey` and `info.parentFieldKey` usage in default directives Submitted by [@kitten](https://github.com/kitten) (See [#3338](https://github.com/urql-graphql/urql/pull/3338)) - Replace implementation for `@_optional` and `@_required` with built-in handling inside cache reads to allow `@_optional` to work for nested selection sets Submitted by [@kitten](https://github.com/kitten) (See [#3341](https://github.com/urql-graphql/urql/pull/3341)) ## 6.3.0 ### Minor Changes - Allow scalar values on the parent to be accessed from `parent[info.fieldName]` consistently. Prior to this change `parent[fieldAlias]` would get populated, which wouldn’t always result in a field that’s consistently accessible Submitted by [@kitten](https://github.com/kitten) (See [#3336](https://github.com/urql-graphql/urql/pull/3336)) - Allow `cache.resolve` to return `undefined` when a value is not cached to make it easier to cause a cache miss in resolvers. **Reminder:** Returning `undefined` from a resolver means a field is uncached, while returning `null` means that a field’s value is `null` without causing a cache miss Submitted by [@kitten](https://github.com/kitten) (See [#3333](https://github.com/urql-graphql/urql/pull/3333)) ### Patch Changes - Record a dependency when `__typename` field is read. This removes a prior, outdated exception to avoid confusion when using `cache.resolve(entity, '__typename')` which doesn't cause the cache to record a dependency Submitted by [@kitten](https://github.com/kitten) (See [#3335](https://github.com/urql-graphql/urql/pull/3335)) - ⚠️ Fix cases where `ResolveInfo`’s `parentFieldKey` was incorrectly populated with a key that isn’t a field key (allowing for `cache.resolve(info.parentKey, info.parentFieldKey)` to be possible) but was instead set to `info.parentKey` combined with the field key Submitted by [@kitten](https://github.com/kitten) (See [#3336](https://github.com/urql-graphql/urql/pull/3336)) ## 6.2.0 ### Minor Changes - Implement **local directives**. It’s now possible to add client-only directives to queries by adding them to the `cacheExchange`’s new `directives` option. Directives accept an object of their arguments and return a resolver. When a field is annotated with a resolver, e.g. `@_optional` or `@_required`, their resolvers from the `directives` config are executed. This means it’s now possible to use `@_relayPagination` for example, by passing adding the `relayPagination` helper to the config. Due to the change in [#3317](https://github.com/urql-graphql/urql/pull/3317), any directive in queries that’s prefixed with an underscore (`_`) is only visible to Graphcache and not the API. Submitted by undefined (See https://github.com/urql-graphql/urql/pull/3306) ### Patch Changes - Use new `FormattedNode` / `formatDocument` functionality added to `@urql/core` to slightly speed up directive processing by using the client-side `_directives` dictionary that `formatDocument` adds Submitted by [@kitten](https://github.com/kitten) (See [#3317](https://github.com/urql-graphql/urql/pull/3317)) - Allow `offlineExchange` to once again issue all request policies, instead of mapping them to `cache-first`. When replaying operations after rehydrating it will now prioritise network policies, and before rehydrating receiving a network result will prevent a network request from being issued again Submitted by [@kitten](https://github.com/kitten) (See [#3308](https://github.com/urql-graphql/urql/pull/3308)) - Add `OperationContext.optimistic` flag as an internal indication on whether a mutation triggered an optimistic update in `@urql/exchange-graphcache`'s `cacheExchange` Submitted by [@kitten](https://github.com/kitten) (See [#3308](https://github.com/urql-graphql/urql/pull/3308)) - Updated dependencies (See [#3317](https://github.com/urql-graphql/urql/pull/3317) and [#3308](https://github.com/urql-graphql/urql/pull/3308)) - @urql/core@4.1.0 ## 6.1.4 ### Patch Changes - ⚠️ Fix untranspiled class property initializer syntax being leftover in build output. (Regression in #3053) Submitted by [@kitten](https://github.com/kitten) (See [#3275](https://github.com/urql-graphql/urql/pull/3275)) ## 6.1.3 ### Patch Changes - ⚠️ Fix `info.parentKey` not being correctly set for updaters or optimistic updaters Submitted by [@kitten](https://github.com/kitten) (See [#3267](https://github.com/urql-graphql/urql/pull/3267)) ## 6.1.2 ### Patch Changes - Make "Invalid undefined" warning heuristic smarter and allow for partial optimistic results. Previously, when a partial optimistic result would be passed, a warning would be issued, and in production, fields would be deleted from the cache. Instead, we now only issue a warning if these fields aren't cached already Submitted by [@kitten](https://github.com/kitten) (See [#3264](https://github.com/urql-graphql/urql/pull/3264)) - Optimistic mutation results should never result in dependent operations being blocked Submitted by [@kitten](https://github.com/kitten) (See [#3265](https://github.com/urql-graphql/urql/pull/3265)) ## 6.1.1 ### Patch Changes - ⚠️ Fix torn down queries not being removed from `offlineExchange`’s failed queue on rehydration Submitted by [@kitten](https://github.com/kitten) (See [#3236](https://github.com/urql-graphql/urql/pull/3236)) ## 6.1.0 ### Minor Changes - Add `globalIDs` configuration option to omit typenames in cache keys Submitted by [@kitten](https://github.com/kitten) (See [#3224](https://github.com/urql-graphql/urql/pull/3224)) ### Patch Changes - Update build process to generate correct source maps Submitted by [@kitten](https://github.com/kitten) (See [#3201](https://github.com/urql-graphql/urql/pull/3201)) - Prevent `offlineExchange` from issuing duplicate operations Submitted by [@kitten](https://github.com/kitten) (See [#3200](https://github.com/urql-graphql/urql/pull/3200)) - ⚠️ Fix reference equality not being preserved. This is a fix on top of [#3165](https://github.com/urql-graphql/urql/pull/3165), and was previously not addressed to avoid having to test for corner cases that are hard to cover. If you experience issues with this fix, please let us know Submitted by [@kitten](https://github.com/kitten) (See [#3228](https://github.com/urql-graphql/urql/pull/3228)) - Retry operations against offline cache and stabilize timing of flushing failed operations queue after rehydrating the storage data Submitted by [@kitten](https://github.com/kitten) (See [#3196](https://github.com/urql-graphql/urql/pull/3196)) ## 6.0.4 ### Patch Changes - ⚠️ Fix missing cache updates, when a query that was previously torn down restarts and retrieves results from the cache. In this case a regression caused cache updates to not be correctly applied to the queried results, since the operation wouldn’t be recognised properly Submitted by [@kitten](https://github.com/kitten) (See [#3193](https://github.com/urql-graphql/urql/pull/3193)) ## 6.0.3 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 6.0.2 ### Patch Changes - Prevent reusal of incoming API data in Graphcache’s produced (“owned”) data. This prevents us from copying the `__typename` and other superfluous fields Submitted by [@kitten](https://github.com/kitten) (See [#3165](https://github.com/urql-graphql/urql/pull/3165)) - ⚠️ Fix regression which caused `@defer` directives from becoming “sticky” and causing every subsequent cache read to be treated as if the field was deferred Submitted by [@kitten](https://github.com/kitten) (See [#3167](https://github.com/urql-graphql/urql/pull/3167)) - Apply `hasNext: true` and fallthrough logic to cached queries that contain deferred, uncached fields. Deferred query results will now be fetched against the API correctly, even if prior requests have been incomplete Submitted by [@kitten](https://github.com/kitten) (See [#3163](https://github.com/urql-graphql/urql/pull/3163)) - ⚠️ Fix `offlineExchange` duplicating offline mutations in failed queue Submitted by [@kitten](https://github.com/kitten) (See [#3158](https://github.com/urql-graphql/urql/pull/3158)) ## 6.0.1 ### Patch Changes - Remove inclusion and usage of optional chaining operator Submitted by [@kitten](https://github.com/kitten) (See [#3116](https://github.com/urql-graphql/urql/pull/3116)) ## 6.0.0 ### Major Changes - Remove dependence on `graphql` package and replace it with `@0no-co/graphql.web`, which reduces the default bundlesize impact of `urql` packages to a minimum. All types should remain compatible, even if you use `graphql` elsewhere in your app, and if other dependencies are using `graphql` you may alias it to `graphql-web-lite` Submitted by [@kitten](https://github.com/kitten) (See [#3097](https://github.com/urql-graphql/urql/pull/3097)) - Update `OperationResult.hasNext` and `OperationResult.stale` to be required fields. If you have a custom exchange creating results, you'll have to add these fields or use the `makeResult`, `mergeResultPatch`, or `makeErrorResult` helpers Submitted by [@kitten](https://github.com/kitten) (See [#3061](https://github.com/urql-graphql/urql/pull/3061)) ### Minor Changes - Update exchanges to drop redundant `share` calls, since `@urql/core`’s `composeExchanges` utility now automatically does so for us Submitted by [@kitten](https://github.com/kitten) (See [#3082](https://github.com/urql-graphql/urql/pull/3082)) ### Patch Changes - ⚠️ Fix source maps included with recently published packages, which lost their `sourcesContent`, including additional source files, and had incorrect paths in some of them Submitted by [@kitten](https://github.com/kitten) (See [#3053](https://github.com/urql-graphql/urql/pull/3053)) - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Restore variables correctly on mutations Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3046](https://github.com/urql-graphql/urql/pull/3046)) - Use `stringifyDocument` in `offlineExchange` rather than `print` and serialize `operation.extensions` as needed Submitted by [@kitten](https://github.com/kitten) (See [#3094](https://github.com/urql-graphql/urql/pull/3094)) - Add missing `hasNext` and `stale` passthroughs on caching exchanges Submitted by [@kitten](https://github.com/kitten) (See [#3059](https://github.com/urql-graphql/urql/pull/3059)) - Add TSDocs for all exchanges, documenting API internals Submitted by [@kitten](https://github.com/kitten) (See [#3072](https://github.com/urql-graphql/urql/pull/3072)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 5.2.0 ### Minor Changes - Add `isOfflineError` option to the `offlineExchange` to allow it to be customized to different conditions to determine whether an operation has failed because of a network error Submitted by [@robertherber](https://github.com/robertherber) (See [#3020](https://github.com/urql-graphql/urql/pull/3020)) - Allow `updates` config to react to arbitrary type updates other than just `Mutation` and `Subscription` fields. You’ll now be able to write updaters that react to any entity field being written to the cache, which allows for more granular invalidations. **Note:** If you’ve previously used `updates.Mutation` and `updated.Subscription` with a custom schema with custom root names, you‘ll get a warning since you’ll have to update your `updates` config to reflect this. This was a prior implementation mistake! Submitted by [@kitten](https://github.com/kitten) (See [#2979](https://github.com/urql-graphql/urql/pull/2979)) ### Patch Changes - ⚠️ Fix regression which caused partial results, whose refetches were blocked by the looping protection, to not have a `stale: true` flag added to them. This is a regression from https://github.com/urql-graphql/urql/pull/2831 and only applies to `cacheExchange`s that had the `schema` option set Submitted by [@kitten](https://github.com/kitten) (See [#2999](https://github.com/urql-graphql/urql/pull/2999)) - Add `invariant` to data layer that prevents cache writes during cache query operations. This prevents `cache.writeFragment`, `cache.updateQuery`, and `cache.link` from being called in `resolvers` for instance Submitted by [@kitten](https://github.com/kitten) (See [#2978](https://github.com/urql-graphql/urql/pull/2978)) - Updated dependencies (See [#3007](https://github.com/urql-graphql/urql/pull/3007), [#2962](https://github.com/urql-graphql/urql/pull/2962), [#3007](https://github.com/urql-graphql/urql/pull/3007), [#3015](https://github.com/urql-graphql/urql/pull/3015), and [#3022](https://github.com/urql-graphql/urql/pull/3022)) - @urql/core@3.2.0 ## 5.0.9 ### Patch Changes - ⚠️ Fix potential data loss in `offlineExchange` that's caused when `onOnline` triggers and flushes mutation queue before the mutation queue is used, by [@trcoffman](https://github.com/trcoffman) (See [#2945](https://github.com/urql-graphql/urql/pull/2945)) - Patch message for `(16) Heuristic Fragment Matching`, by [@inokawa](https://github.com/inokawa) (See [#2923](https://github.com/urql-graphql/urql/pull/2923)) - Patch message for (19) Can't generate a key for invalidate(...) error, by [@inokawa](https://github.com/inokawa) (See [#2918](https://github.com/urql-graphql/urql/pull/2918)) ## 5.0.8 ### Patch Changes - ⚠️ Fix operation being blocked for looping due to it not cancelling the looping protection when a `teardown` is received. This bug could be triggered when a shared query operation triggers again and causes a cache miss (e.g. due to an error). The re-execution of the operation would then be blocked as Graphcache considered it a "reexecution loop" rather than a legitimate execution triggered by the UI. (See https://github.com/urql-graphql/urql/pull/2737 for more information), by [@kitten](https://github.com/kitten) (See [#2876](https://github.com/urql-graphql/urql/pull/2876)) ## 5.0.7 ### Patch Changes - ⚠️ Fix type-generation, with a change in TS/Rollup the type generation took the paths as src and resolved them into the types dir, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2870](https://github.com/urql-graphql/urql/pull/2870)) - Updated dependencies (See [#2872](https://github.com/urql-graphql/urql/pull/2872), [#2870](https://github.com/urql-graphql/urql/pull/2870), and [#2871](https://github.com/urql-graphql/urql/pull/2871)) - @urql/core@3.1.1 ## 5.0.6 ### Patch Changes - Solve issue where partial data could cause loops between related queries, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2831](https://github.com/urql-graphql/urql/pull/2831)) - Add skipping of garbage collection runs when the cache is waiting for optimistic, deferred or other results in layers. This means that we only take an opportunity to run garbage collection after results have settled and are hence decreasing the chance of hogging the event loop when a run isn't needed, by [@kitten](https://github.com/kitten) (See [#2862](https://github.com/urql-graphql/urql/pull/2862)) - ⚠️ Fix a deadlock condition in Graphcache's layers, which is caused by subscriptions (or other deferred layers) starting before one-off mutation layers. This causes the mutation to not be completed, which keeps its data preferred above the deferred layer. That in turn means that layers stop squashing, which causes new results to be missing indefinitely, when they overlap, by [@kitten](https://github.com/kitten) (See [#2861](https://github.com/urql-graphql/urql/pull/2861)) - Updated dependencies (See [#2843](https://github.com/urql-graphql/urql/pull/2843), [#2847](https://github.com/urql-graphql/urql/pull/2847), [#2850](https://github.com/urql-graphql/urql/pull/2850), and [#2846](https://github.com/urql-graphql/urql/pull/2846)) - @urql/core@3.1.0 ## 5.0.5 ### Patch Changes - Set operations when updating the cache with a result, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2782](https://github.com/FormidableLabs/urql/pull/2782)) ## 5.0.4 ### Patch Changes - Ensure we aren't eagerly removing layers that are caused by subscriptions, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2771](https://github.com/FormidableLabs/urql/pull/2771)) ## 5.0.3 ### Patch Changes - ⚠️ Fix case where a mutation would also be counted in the loop-protection, this prevented partial queries from initiating refetches, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2761](https://github.com/FormidableLabs/urql/pull/2761)) - Updated dependencies (See [#2758](https://github.com/FormidableLabs/urql/pull/2758) and [#2762](https://github.com/FormidableLabs/urql/pull/2762)) - @urql/core@3.0.5 ## 5.0.2 ### Patch Changes - Preserve the original `DocumentNode` AST when updating the cache, to prevent results after a network request from differing and breaking referential equality due to added `__typename` fields, by [@kitten](https://github.com/kitten) (See [#2736](https://github.com/FormidableLabs/urql/pull/2736)) - ⚠️ Fix optimistic mutations containing partial results (`undefined` fields), which previously actually caused a hidden cache miss, which may then affect a subsequent non-optimistic mutation result, by [@kitten](https://github.com/kitten) (See [#2740](https://github.com/FormidableLabs/urql/pull/2740)) - Prevent cache misses from causing infinite network requests from being issued, when two operations manipulate each other while experiencing cache misses or are partially uncacheable, by [@kitten](https://github.com/kitten) (See [#2737](https://github.com/FormidableLabs/urql/pull/2737)) - ⚠️ Fix operation identities preventing users from deeply cloning operation contexts. Instead, we now use a client-wide counter (rolling over as needed). While this changes an internal data structure in `@urql/core` only, this change also affects the `offlineExchange` in `@urql/exchange-graphcache` due to it relying on the identity being previously an object rather than an integer, by [@kitten](https://github.com/kitten) (See [#2732](https://github.com/FormidableLabs/urql/pull/2732)) - ⚠️ Fix referential equality preservation in Graphcache failing after API results, due to a typo writing the API result rather than the updated cache result, by [@kitten](https://github.com/kitten) (See [#2741](https://github.com/FormidableLabs/urql/pull/2741)) - Updated dependencies (See [#2691](https://github.com/FormidableLabs/urql/pull/2691), [#2692](https://github.com/FormidableLabs/urql/pull/2692), and [#2732](https://github.com/FormidableLabs/urql/pull/2732)) - @urql/core@3.0.4 ## 5.0.1 ### Patch Changes - Adjust timing of when an introspected schema will be processed into field maps, interface maps, and union type maps. By making this lazy we can avoid excessive work when these maps aren't actually ever used, by [@kitten](https://github.com/kitten) (See [#2640](https://github.com/FormidableLabs/urql/pull/2640)) ## 5.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Prevent cache-hydration from buffering operations, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2612](https://github.com/FormidableLabs/urql/pull/2612)) - Implement stricter variables types, which require variables to always be passed and match TypeScript types when the generic is set or inferred. This is a breaking change for TypeScript users potentially, unless all types are adhered to, by [@kitten](https://github.com/kitten) (See [#2607](https://github.com/FormidableLabs/urql/pull/2607)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Minor Changes - Remove the `babel-plugin-modular-graphql` helper, this because the graphql package hasn't converted to ESM yet which gives issues in node environments, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2551](https://github.com/FormidableLabs/urql/pull/2551)) - Allow passing in `fragmentName` for `write` and `read` operations, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2609](https://github.com/FormidableLabs/urql/pull/2609)) ### Patch Changes - Graphcache's `optimistic` option now accepts optimistic mutation resolvers that return fields by name rather than alias. Previously, depending on which mutation was run, the optimistic resolvers would read your optimistic data by field alias (i.e. "alias" for `alias: id` rather than "id"). Instead, optimistic updates now correctly use field names and allow you to also pass resolvers as values on your optimistic config, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2616](https://github.com/FormidableLabs/urql/pull/2616)) - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 4.4.3 ### Patch Changes - Correctly reorder optimistic layers when we see repeated keys coming in, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2489](https://github.com/FormidableLabs/urql/pull/2489)) ## 4.4.2 ### Patch Changes - Keep track of mutations in the offline exchange so we can accurately recreate the original variables, there could be more variables for use in updater functions which we strip away in graphCache before sending to the API, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2472](https://github.com/FormidableLabs/urql/pull/2472)) ## 4.4.1 ### Patch Changes - Switch `isFragmentHeuristicallyMatching()` to always return `true` for writes, so that we give every fragment a chance to be applied and to write to the cache, by [@kitten](https://github.com/kitten) (See [#2455](https://github.com/FormidableLabs/urql/pull/2455)) - ⚠️ Fix default storage persisting data after `clear()` was called on it, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2458](https://github.com/FormidableLabs/urql/pull/2458)) - Updated dependencies (See [#2446](https://github.com/FormidableLabs/urql/pull/2446), [#2456](https://github.com/FormidableLabs/urql/pull/2456), and [#2457](https://github.com/FormidableLabs/urql/pull/2457)) - @urql/core@2.5.0 ## 4.4.0 ### Minor Changes - Fix issues with continuously updating operations (i.e. subscriptions and `hasNext: true` queries) by shifting their layers in Graphcache behind those that are still awaiting a result. This causes continuous updates to not overwrite one-off query results while still keeping continuously updating operations at the highest possible layer, by [@kitten](https://github.com/kitten) (See [#2419](https://github.com/FormidableLabs/urql/pull/2419)) ### Patch Changes - ⚠️ Fix ignore empty relay edges when using the `relayPagination` resolver, by [@tgriesser](https://github.com/tgriesser) (See [#2431](https://github.com/FormidableLabs/urql/pull/2431)) - Prevent creating unnecessary layers, which should improve performance slightly, by [@kitten](https://github.com/kitten) (See [#2419](https://github.com/FormidableLabs/urql/pull/2419)) ## 4.3.6 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 4.3.5 ### Patch Changes - ⚠️ Fix regression from [#1869](https://github.com/FormidableLabs/urql/pull/1869) that caused nullable lists to always cause a cache miss, if schema awareness is enabled, by [@kitten](https://github.com/kitten) (See [#1983](https://github.com/FormidableLabs/urql/pull/1983)) - Updated dependencies (See [#1985](https://github.com/FormidableLabs/urql/pull/1985)) - @urql/core@2.3.3 ## 4.3.4 ### Patch Changes - Improve perf by using String.indexOf in getField, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1957](https://github.com/FormidableLabs/urql/pull/1957)) - Updated dependencies (See [#1944](https://github.com/FormidableLabs/urql/pull/1944)) - @urql/core@2.3.2 ## 4.3.3 ### Patch Changes - Remove `hasNext: true` flag from stale responses. This was erroneously added in debugging, but leads to stale responses being marked with `hasNext`, which means the `dedupExchange` will keep waiting for further network responses, by [@kitten](https://github.com/kitten) (See [#1911](https://github.com/FormidableLabs/urql/pull/1911)) ## 4.3.2 ### Patch Changes - Cleanup the previous `onOnline` event-listener when called again, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1896](https://github.com/FormidableLabs/urql/pull/1896)) ## 4.3.1 ### Patch Changes - ⚠️ Fix previous results' `null` values spilling into the next result that Graphcache issues, which may prevent updates from being issued until the query is reexecuted. This was affecting any `null` links on data, and any queries that were issued before non-optimistic mutations, by [@kitten](https://github.com/kitten) (See [#1885](https://github.com/FormidableLabs/urql/pull/1885)) - Updated dependencies (See [#1870](https://github.com/FormidableLabs/urql/pull/1870) and [#1880](https://github.com/FormidableLabs/urql/pull/1880)) - @urql/core@2.3.1 ## 4.3.0 ### Minor Changes - Improve referential equality of deeply queried objects from the normalised cache for queries. Each query operation will now reuse the last known result and only incrementally change references as necessary, scanning over the previous result to identify whether anything has changed. This should help improve the performance of processing updates in UI frameworks (e.g. in React with `useMemo` or `React.memo`). (See [#1859](https://github.com/FormidableLabs/urql/pull/1859)) - Add **experimental** support for `@defer` and `@stream` responses for GraphQL. This implements the ["GraphQL Defer and Stream Directives"](https://github.com/graphql/graphql-spec/blob/4fd39e0/rfcs/DeferStream.md) and ["Incremental Delivery over HTTP"](https://github.com/graphql/graphql-over-http/blob/290b0e2/rfcs/IncrementalDelivery.md) specifications. If a GraphQL API supports `multipart/mixed` responses for deferred and streamed delivery of GraphQL results, `@urql/core` (and all its derived fetch implementations) will attempt to stream results. This is _only supported_ on browsers [supporting streamed fetch responses](https://developer.mozilla.org/en-US/docs/Web/API/Response/body), which excludes IE11. The implementation of streamed multipart responses is derived from [`meros` by `@maraisr`](https://github.com/maraisr/meros), and is subject to change if the RFCs end up changing, by [@kitten](https://github.com/kitten) (See [#1854](https://github.com/FormidableLabs/urql/pull/1854)) ### Patch Changes - ⚠️ Fix missing values cascading into lists causing a `null` item without the query being marked as stale and fetched from the API. This would happen in schema awareness when a required field, which isn't cached, cascades into a nullable list, by [@kitten](https://github.com/kitten) (See [#1869](https://github.com/FormidableLabs/urql/pull/1869)) - Updated dependencies (See [#1854](https://github.com/FormidableLabs/urql/pull/1854)) - @urql/core@2.3.0 ## 4.2.1 ### Patch Changes - ⚠️ Fix issue where operations that get dispatched synchronously after the cache restoration completes get forgotten, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1789](https://github.com/FormidableLabs/urql/pull/1789)) ## 4.2.0 ### Minor Changes - Fixed typing of OptimisticMutationResolver, by [@taneba](https://github.com/taneba) (See [#1765](https://github.com/FormidableLabs/urql/pull/1765)) ### Patch Changes - Type the `relayPagination` and `simplePagination` helpers return value as `Resolver` as there's no way to match them consistently to either generated or non-generated resolver types anymore, by [@kitten](https://github.com/kitten) (See [#1778](https://github.com/FormidableLabs/urql/pull/1778)) - Updated dependencies (See [#1776](https://github.com/FormidableLabs/urql/pull/1776) and [#1755](https://github.com/FormidableLabs/urql/pull/1755)) - @urql/core@2.1.5 ## 4.1.4 ### Patch Changes - Apply [`bivarianceHack`](https://stackoverflow.com/questions/52667959/what-is-the-purpose-of-bivariancehack-in-typescript-types) in the `graphcache` types to better support code-generated configs, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1687](https://github.com/FormidableLabs/urql/pull/1687)) - Updated dependencies (See [#1709](https://github.com/FormidableLabs/urql/pull/1709)) - @urql/core@2.1.4 ## 4.1.3 ### Patch Changes - ⚠️ Fix: add the `ENTRIES_STORE_NAME` to the clear transaction, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1685](https://github.com/FormidableLabs/urql/pull/1685)) - Updated dependencies (See [#1695](https://github.com/FormidableLabs/urql/pull/1695)) - @urql/core@2.1.3 ## 4.1.2 ### Patch Changes - Loosen type constraint on `ScalarObject` to account for custom scalar deserialization like `Date` for `DateTime`s, by [@kitten](https://github.com/kitten) (See [#1648](https://github.com/FormidableLabs/urql/pull/1648)) - Loosen the typing constraint on the cacheExchange generic, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1675](https://github.com/FormidableLabs/urql/pull/1675)) ## 4.1.1 ### Patch Changes - ⚠️ Fix an edge-case for which an introspection query during runtime could fail when schema-awareness was enabled in Graphcache, since built-in types weren't recognised as existent, by [@kitten](https://github.com/kitten) (See [#1631](https://github.com/FormidableLabs/urql/pull/1631)) ## 4.1.0 ### Minor Changes - Add `cache.link(...)` method to Graphcache. This method may be used in updaters to update links in the cache. It is hence the writing-equivalent of `cache.resolve()`, which previously didn't have any equivalent as such, which meant that only `cache.updateQuery` or `cache.writeFragment` could be used, even to update simple relations, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1551](https://github.com/FormidableLabs/urql/pull/1551)) - Add on a generic to `cacheExchange` and `offlineExchange` for future, experimental type-generation support, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1562](https://github.com/FormidableLabs/urql/pull/1562)) ### Patch Changes - ⚠️ Fix up internal types in Graphcache to improve their accuracy for catching more edge cases in its implementation. This only affects you if you previously imported any type related to `ScalarObject` from Graphcache which now is a more opaque type. We've also adjusted the `NullArray` types to be potentially nested, since lists in GraphQL can be nested arbitarily, which we were covering but didn't reflect in our types, by [@kitten](https://github.com/kitten) (See [#1591](https://github.com/FormidableLabs/urql/pull/1591)) - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - ⚠️ Fix list items being returned as `null` even for non-nullable lists, when the entities are missing in the cache. This could happen when a resolver was added returning entities or their keys. This behaviour is now (correctly) only applied to partial results with schema awareness, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1566](https://github.com/FormidableLabs/urql/pull/1566)) - Allow for the schema subscription and mutationType to be null, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1530](https://github.com/FormidableLabs/urql/pull/1530)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 4.0.0 ### Major Changes - Add improved error awareness to Graphcache. When Graphcache now receives a `GraphQLError` (via a `CombinedError`) it checks whether the `GraphQLError`'s `path` matches up with `null` values in the `data`. Any `null` values that the write operation now sees in the data will be replaced with a "cache miss" value (i.e. `undefined`) when it has an associated error. This means that errored fields from your GraphQL API will be marked as uncached and won't be cached. Instead the client will now attempt a refetch of the data so that errors aren't preventing future refetches or with schema awareness it will attempt a refetch automatically. Additionally, the `updates` functions will now be able to check whether the current field has any errors associated with it with `info.error`, by [@kitten](https://github.com/kitten) (See [#1356](https://github.com/FormidableLabs/urql/pull/1356)) ### Minor Changes - Allow `schema` option to be passed with a partial introspection result that only contains `queryType`, `mutationType`, and `subscriptionType` with their respective names. This allows you to pass `{ __schema: { queryType: { name: 'Query' } } }` and the likes to Graphcache's `cacheExchange` to alter the default root names without enabling full schema awareness, by [@kitten](https://github.com/kitten) (See [#1379](https://github.com/FormidableLabs/urql/pull/1379)) ### Patch Changes - Updated dependencies (See [#1374](https://github.com/FormidableLabs/urql/pull/1374), [#1357](https://github.com/FormidableLabs/urql/pull/1357), and [#1375](https://github.com/FormidableLabs/urql/pull/1375)) - @urql/core@2.0.0 ## 3.4.0 ### Minor Changes - Warn when using an interface or union field in the graphCache resolvers config, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1304](https://github.com/FormidableLabs/urql/pull/1304)) ### Patch Changes - ⚠️ Fix edge-case where query results would pick up invalidated fields from mutation results as they're written to the cache. This would cause invalid cache misses although the result was expected to just be passed through from the API result, by [@kitten](https://github.com/kitten) (See [#1300](https://github.com/FormidableLabs/urql/pull/1300)) - ⚠️ Fix a Relay Pagination edge case where overlapping ends of pages queried using the `last` argument would be in reverse order, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1311](https://github.com/FormidableLabs/urql/pull/1311)) ## 3.3.4 ### Patch Changes - ⚠️ Fix, add null as a possible type for the variables argument in `cache.invalidate`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1269](https://github.com/FormidableLabs/urql/pull/1269)) ## 3.3.3 ### Patch Changes - Update `cache.resolve(parent, ...)` case to enable _even more_ cases, for instance where `parent.__typename` isn't set yet. This was intended to be enabled in the previous patch but has been forgotten, by [@kitten](https://github.com/kitten) (See [#1219](https://github.com/FormidableLabs/urql/pull/1219)) - Deprecate `cache.resolveFieldByKey` in favour of `cache.resolve`, which functionally was already able to do the same, by [@kitten](https://github.com/kitten) (See [#1219](https://github.com/FormidableLabs/urql/pull/1219)) - Updated dependencies (See [#1225](https://github.com/FormidableLabs/urql/pull/1225)) - @urql/core@1.16.1 ## 3.3.2 ### Patch Changes - Update `cache` methods, for instance `cache.resolve`, to consistently accept the `parent` argument from `resolvers` and `updates` and alias it to the parent's key (which is usually found on `info.parentKey`). This usage of `cache.resolve(parent, ...)` was intuitive and is now supported as expected, by [@kitten](https://github.com/kitten) (See [#1208](https://github.com/FormidableLabs/urql/pull/1208)) ## 3.3.1 ### Patch Changes - ⚠️ Fix reusing original query data from APIs accidentally, which can lead to subtle mismatches in results when the API's incoming `query` results are being updated by the `cacheExchange`, to apply resolvers. Specifically this may lead to relations from being set back to `null` when the resolver returns a different list of links than the result, since some `null` relations may unintentionally exist but aren't related. If you're using `relayPagination` then this fix is critical, by [@kitten](https://github.com/kitten) (See [#1196](https://github.com/FormidableLabs/urql/pull/1196)) ## 3.3.0 ### Minor Changes - Increase the consistency of when and how the `__typename` field is added to results. Instead of adding it by default and automatically first, the `__typename` field will now be added along with the usual selection set. The `write` operation now automatically issues a warning if `__typename` isn't present where it's expected more often, which helps in debugging. Also the `__typename` field may now not proactively be added to root results, e.g. `"Query"`, by [@kitten](https://github.com/kitten) (See [#1185](https://github.com/FormidableLabs/urql/pull/1185)) ### Patch Changes - Replace `graphql/utilities/buildClientSchema.mjs` with a custom-tailored, lighter implementation built into `@urql/exchange-graphcache`. This will appear to increase its size by about `0.2kB gzip` but will actually save around `8.5kB gzip` to `9.4kB gzip` in any production bundle by using less of `graphql`'s code, by [@kitten](https://github.com/kitten) (See [#1189](https://github.com/FormidableLabs/urql/pull/1189)) - Updated dependencies (See [#1187](https://github.com/FormidableLabs/urql/pull/1187), [#1186](https://github.com/FormidableLabs/urql/pull/1186), and [#1186](https://github.com/FormidableLabs/urql/pull/1186)) - @urql/core@1.16.0 ## 3.2.0 ### Minor Changes - Add a `mergeMode: 'before' | 'after'` option to the `simplePagination` helper to define whether pages are merged before or after preceding ones when pagination, similar to `relayPagination`'s option, by [@hoangvvo](https://github.com/hoangvvo) (See [#1174](https://github.com/FormidableLabs/urql/pull/1174)) ### Patch Changes - Updated dependencies (See [#1168](https://github.com/FormidableLabs/urql/pull/1168)) - @urql/core@1.15.2 ## 3.1.11 ### Patch Changes - Add support for `TypedDocumentNode` to infer the type of the `OperationResult` and `Operation` for all methods, functions, and hooks that either directly or indirectly accept a `DocumentNode`. See [`graphql-typed-document-node` and the corresponding blog post for more information.](https://github.com/dotansimha/graphql-typed-document-node), by [@kitten](https://github.com/kitten) (See [#1113](https://github.com/FormidableLabs/urql/pull/1113)) - Updated dependencies (See [#1119](https://github.com/FormidableLabs/urql/pull/1119), [#1113](https://github.com/FormidableLabs/urql/pull/1113), [#1104](https://github.com/FormidableLabs/urql/pull/1104), and [#1123](https://github.com/FormidableLabs/urql/pull/1123)) - @urql/core@1.15.0 ## 3.1.10 ### Patch Changes - ⚠️ Fix a stray `operationName` deprecation warning in `@urql/exchange-graphcache`'s exchange logic, which adds the `meta.cacheOutcome` field to the operation's context, by [@kitten](https://github.com/kitten) (See [#1103](https://github.com/FormidableLabs/urql/pull/1103)) ## 3.1.9 ### Patch Changes - ⚠️ Fix the production build overwriting the development build. Specifically in the previous release we mistakenly replaced all development bundles with production bundles. This doesn't have any direct influence on how these packages work, but prevented development warnings from being logged or full errors from being thrown, by [@kitten](https://github.com/kitten) (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - Updated dependencies (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - @urql/core@1.14.1 ## 3.1.8 ### Patch Changes - Add missing `.mjs` extension to all imports from `graphql` to fix Webpack 5 builds, which require extension-specific import paths for ESM bundles and packages. **This change allows you to safely upgrade to Webpack 5.**, by [@kitten](https://github.com/kitten) (See [#1094](https://github.com/FormidableLabs/urql/pull/1094)) - Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was previously confusing as `operationName` was effectively referring to two different things. You can safely upgrade to this new version, however to mute all deprecation warnings you will have to **upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please use [the new `makeOperation` helper function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead, by [@bkonkle](https://github.com/bkonkle) (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - Updated dependencies (See [#1094](https://github.com/FormidableLabs/urql/pull/1094) and [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - @urql/core@1.14.0 ## 3.1.7 ### Patch Changes - Enforce atomic optimistic updates so that optimistic layers are cleared before they're reapplied. This is important for instance when an optimistic update is performed while offline and then reapplied while online, which would previously repeat the optimistic update on top of its past data changes, by [@kitten](https://github.com/kitten) (See [#1080](https://github.com/FormidableLabs/urql/pull/1080)) ## 3.1.6 ### Patch Changes - ⚠️ Fix optimistic updates not being allowed to be cumulative and apply on top of each other. Previously in [#866](https://github.com/FormidableLabs/urql/pull/866) we explicitly deemed this as unsafe which isn't correct anymore given that concrete, non-optimistic updates are now never applied on top of optimistic layers, by [@kitten](https://github.com/kitten) (See [#1074](https://github.com/FormidableLabs/urql/pull/1074)) ## 3.1.5 ### Patch Changes - Changes some internals of how selections are iterated over and remove some private exports. This will have no effect or fixes on how Graphcache functions, but may improve some minor performance characteristics of large queries, by [@kitten](https://github.com/kitten) (See [#1060](https://github.com/FormidableLabs/urql/pull/1060)) ## 3.1.4 ### Patch Changes - ⚠️ Fix inline fragments being skipped when they were missing a full type condition as per the GraphQL spec (e.g `{ ... { field } }`), by [@kitten](https://github.com/kitten) (See [#1040](https://github.com/FormidableLabs/urql/pull/1040)) ## 3.1.3 ### Patch Changes - ⚠️ Fix a case where the `offlineExchange` would not start processing operations after hydrating persisted data when no operations arrived in time by the time the persisted data was restored. This would be more evident in Preact and Svelte due to their internal short timings, by [@kitten](https://github.com/kitten) (See [#1019](https://github.com/FormidableLabs/urql/pull/1019)) ## 3.1.2 ### Patch Changes - ⚠️ Fix small pieces of code where polyfill-less ES5 usage was compromised. This was unlikely to have affected anyone in production as `Array.prototype.find` (the only usage of an ES6 method) is commonly used and polyfilled, by [@kitten](https://github.com/kitten) (See [#991](https://github.com/FormidableLabs/urql/pull/991)) - ⚠️ Fix queries that have erroed with a `NetworkError` (`isOfflineError`) not flowing back completely through the `cacheExchange`. These queries should also now be reexecuted when the client comes back online, by [@kitten](https://github.com/kitten) (See [#1011](https://github.com/FormidableLabs/urql/pull/1011)) - Updated dependencies (See [#1011](https://github.com/FormidableLabs/urql/pull/1011)) - @urql/core@1.13.1 ## 3.1.1 ### Patch Changes - ⚠️ Fix updaters config not working when Mutation/Subscription root names were altered. For instance, a Mutation named `mutation_root` could cause `store.updates` to be misread and cause a runtime error, by [@kitten](https://github.com/kitten) (See [#984](https://github.com/FormidableLabs/urql/pull/984)) - ⚠️ Fix operation results being obstructed by the `offlineExchange` when the network request has failed due to being offline and no cache result has been issued. Instead the `offlineExchange` will now retry with `cache-only` policy, by [@kitten](https://github.com/kitten) (See [#985](https://github.com/FormidableLabs/urql/pull/985)) ## 3.1.0 ### Minor Changes - Add support for `nodes` fields to the `relayPagination` helper, instead of only supporting the standard `edges`. (See [#897](https://github.com/FormidableLabs/urql/pull/897)) ### Patch Changes - Updated dependencies (See [#947](https://github.com/FormidableLabs/urql/pull/947), [#962](https://github.com/FormidableLabs/urql/pull/962), and [#957](https://github.com/FormidableLabs/urql/pull/957)) - @urql/core@1.13.0 ## 3.0.2 ### Patch Changes - Add special-case for fetching an introspection result in our schema-checking, this avoids an error when urql-devtools fetches the backend graphql schema, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#893](https://github.com/FormidableLabs/urql/pull/893)) - Mute warning when using built-in GraphQL fields, like `__type`, by [@kitten](https://github.com/kitten) (See [#919](https://github.com/FormidableLabs/urql/pull/919)) - ⚠️ Fix return type for resolvers to allow data objects to be returned with `__typename` as expected, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#927](https://github.com/FormidableLabs/urql/pull/927)) - Updated dependencies (See [#911](https://github.com/FormidableLabs/urql/pull/911) and [#908](https://github.com/FormidableLabs/urql/pull/908)) - @urql/core@1.12.3 ## 3.0.1 ### Patch Changes - Add warning for queries that traverse an Operation Root Type (Mutation / Subscription types occuring in a query result), by [@kitten](https://github.com/kitten) (See [#859](https://github.com/FormidableLabs/urql/pull/859)) - ⚠️ Fix storage implementation not preserving deleted values correctly or erroneously checking optimistically written entries for changes. This is fixed by adding a new default serializer to the `@urql/exchange-graphcache/default-storage` implementation, which will be incompatible with the old one, by [@kitten](https://github.com/kitten) (See [#866](https://github.com/FormidableLabs/urql/pull/866)) - Replace unnecessary `scheduleTask` polyfill with inline `Promise.resolve().then(fn)` calls, by [@kitten](https://github.com/kitten) (See [#861](https://github.com/FormidableLabs/urql/pull/861)) - Updated dependencies (See [#860](https://github.com/FormidableLabs/urql/pull/860) and [#861](https://github.com/FormidableLabs/urql/pull/861)) - @urql/core@1.12.1 ## 3.0.0 This major release comes with a couple of fixes and new **experimental offline support**, which we're very excited for! Please give it a try if your application is targeting Offline First! To migrate to this new major version, check the major breaking changes below. Mainly you will have to watch out for `cache.invalidateQuery` which has been removed. Instead you should now invalidate individual entities and fields using `cache.invalidate`. [Learn more about this method on our docs.](https://formidable.com/open-source/urql/docs/graphcache/custom-updates/#cacheinvalidate) ### Major Changes - Remove the deprecated `populateExchange` export from `@urql/exchange-graphcache`. If you're using the `populateExchange`, please install the separate `@urql/exchange-populate` package and import it from there, by [@kitten](https://github.com/kitten) (See [#840](https://github.com/FormidableLabs/urql/pull/840)) - The deprecated `cache.invalidateQuery()` method has been removed. Please migrate over to `cache.invalidate()` instead, which operates on individual fields instead of queries, by [@kitten](https://github.com/kitten) (See [#840](https://github.com/FormidableLabs/urql/pull/840)) ### Minor Changes - Implement experimental Offline Support in Graphcache. [Read more about how to use the Offline Support in our docs.](https://formidable.com/open-source/urql/docs/graphcache/offline/), by [@kitten](https://github.com/kitten) (See [#793](https://github.com/FormidableLabs/urql/pull/793)) - Issue warnings when an unknown type or field has been included in Graphcache's `opts` configuration to help spot typos. Checks `opts.keys`, `opts.updates`, `opts.resolvers` and `opts.optimistic`. (See [#820](https://github.com/FormidableLabs/urql/pull/820) and [#826](https://github.com/FormidableLabs/urql/pull/826)) ### Patch Changes - ⚠️ Fix resolvers being executed for data even when data is currently written. This behaviour could lead to interference with custom updaters that update fragments or queries, e.g. an updater that was receiving paginated data due to a pagination resolver. We've determined that generally it is undesirable to have any resolvers run during the cache update (writing) process, since it may lead to resolver data being accidentally written to the cache or for resolvers to interfere with custom user updates, by [@olistic](https://github.com/olistic) (See [#812](https://github.com/FormidableLabs/urql/pull/812)) - Upgrade to a minimum version of wonka@^4.0.14 to work around issues with React Native's minification builds, which use uglify-es and could lead to broken bundles, by [@kitten](https://github.com/kitten) (See [#842](https://github.com/FormidableLabs/urql/pull/842)) - Updated dependencies (See [#838](https://github.com/FormidableLabs/urql/pull/838) and [#842](https://github.com/FormidableLabs/urql/pull/842)) - @urql/core@1.12.0 ## 2.4.2 ### Patch Changes - Add `source` debug name to all `dispatchDebug` calls during build time to identify events by which exchange dispatched them, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#780](https://github.com/FormidableLabs/urql/pull/780)) - ⚠️ Fix Introspection Queries (or internal types in general) triggering lots of warnings for unkeyed entities, by [@kitten](https://github.com/kitten) (See [#779](https://github.com/FormidableLabs/urql/pull/779)) - Updated dependencies (See [#780](https://github.com/FormidableLabs/urql/pull/780)) - @urql/core@1.11.7 ## 2.4.1 ### Patch Changes - Add a `"./package.json"` entry to the `package.json`'s `"exports"` field for Node 14. This seems to be required by packages like `rollup-plugin-svelte` to function properly, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - ⚠️ Fix traversal issue, where when a prior selection set has set a nested result field to `null`, a subsequent traversal of this field attempts to access `prevData` on `null`, by [@kitten](https://github.com/kitten) (See [#772](https://github.com/FormidableLabs/urql/pull/772)) - Updated dependencies (See [#771](https://github.com/FormidableLabs/urql/pull/771) and [#771](https://github.com/FormidableLabs/urql/pull/771)) - @urql/exchange-populate@0.1.7 - @urql/core@1.11.6 ## 2.4.0 This release heavily improves on the intuitiveness of how Optimistic Updates work. It ensures that optimistic updates aren't accidentally discarded, by temporarily blocking some refetches when necessary. It also prevents optimistic mutation updates from becoming permanent, which could previously happen if an updater read optimistic data and rewrote it again. This isn't possible anymore as mutation results are applied as a batch. ### Minor Changes - Implement refetch blocking for queries that are affected by optimistic update. When a query would normally be refetched, either because it was partial or a cache-and-network operation, we now wait if it touched optimistic data for that optimistic mutation to complete. This prevents optimistic update data from unexpectedly disappearing, by [@kitten](https://github.com/kitten) (See [#750](https://github.com/FormidableLabs/urql/pull/750)) - Implement optimistic mutation result flushing. Mutation results for mutation that have had optimistic updates will now wait for all optimistic mutations to complete at the same time before being applied to the cache. This sometimes does delay cache updates to until after multiple mutations have completed, but it does prevent optimistic data from being accidentally committed permanently, which is more intuitive, by [@kitten](https://github.com/kitten) (See [#750](https://github.com/FormidableLabs/urql/pull/750)) ### Patch Changes - Adjust mutation results priority to always override query results as they arrive, similarly to subscriptions. This will prevent race conditions when mutations are slow to execute at the cost of some consistency, by [@kitten](https://github.com/kitten) (See [#745](https://github.com/FormidableLabs/urql/pull/745)) - Improve warning and error console output in development by cleaning up the GraphQL trace stack, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#751](https://github.com/FormidableLabs/urql/pull/751)) ## 2.3.8 ### Patch Changes Sorry for the many updates; Please only upgrade to `>=2.3.8` and don't use the deprecated `2.3.7` and `2.3.6` release. - ⚠️ Fix nested package path for @urql/core/internal and @urql/exchange-graphcache/extras, by [@kitten](https://github.com/kitten) (See [#734](https://github.com/FormidableLabs/urql/pull/734)) - Updated dependencies (See [#734](https://github.com/FormidableLabs/urql/pull/734)) - @urql/core@1.11.4 ## 2.3.7 ### Patch Changes - Make the extension of the main export unknown, which fixes a Webpack issue where the resolver won't pick `module` fields in `package.json` files once it's importing from another `.mjs` file, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#733](https://github.com/FormidableLabs/urql/pull/733)) - Updated dependencies (See [#733](https://github.com/FormidableLabs/urql/pull/733)) - @urql/core@1.11.2 ## 2.3.5 ### Patch Changes - ⚠️ Fix data persistence for embedded fields, by [@kitten](https://github.com/kitten) (See [#727](https://github.com/FormidableLabs/urql/pull/727)) ## 2.3.4 ### Patch Changes - Add debugging events to exchanges that add more detailed information on what is happening internally, which will be displayed by devtools like the urql [Chrome / Firefox extension](https://github.com/FormidableLabs/urql-devtools), by [@andyrichardson](https://github.com/andyrichardson) (See [#608](https://github.com/FormidableLabs/urql/pull/608)) - ⚠️ Fix persistence using special tab character in serialized keys and add sanitization to persistence key serializer, by [@kitten](https://github.com/kitten) (See [#715](https://github.com/FormidableLabs/urql/pull/715)) - Updated dependencies (See [#608](https://github.com/FormidableLabs/urql/pull/608), [#718](https://github.com/FormidableLabs/urql/pull/718), and [#722](https://github.com/FormidableLabs/urql/pull/722)) - @urql/core@1.11.0 ## 2.3.3 ### Patch Changes - ⚠️ Fix @urql/exchange-populate visitWithTypeInfo import by bumping babel-plugin-modular-graphql, by [@kitten](https://github.com/kitten) (See [#709](https://github.com/FormidableLabs/urql/pull/709)) - Updated dependencies (See [#709](https://github.com/FormidableLabs/urql/pull/709)) - @urql/exchange-populate@0.1.6 ## 2.3.2 ### Patch Changes - Pick modules from graphql package, instead of importing from graphql/index.mjs, by [@kitten](https://github.com/kitten) (See [#700](https://github.com/FormidableLabs/urql/pull/700)) - Change invalidation to check for undefined links since null is a valid value in graphql, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#694](https://github.com/FormidableLabs/urql/pull/694)) - Updated dependencies (See [#700](https://github.com/FormidableLabs/urql/pull/700)) - @urql/exchange-populate@0.1.5 - @urql/core@1.10.9 ## 2.3.1 ### Patch Changes - Add graphql@^15.0.0 to peer dependency range, by [@kitten](https://github.com/kitten) (See [#688](https://github.com/FormidableLabs/urql/pull/688)) - Forcefully bump @urql/core package in all bindings and in @urql/exchange-graphcache. We're aware that in some cases users may not have upgraded to @urql/core, even though that's within the typical patch range. Since the latest @urql/core version contains a patch that is required for `cache-and-network` to work, we're pushing another patch that now forcefully bumps everyone to the new version that includes this fix, by [@kitten](https://github.com/kitten) (See [#684](https://github.com/FormidableLabs/urql/pull/684)) - Reimplement persistence support to take commutative layers into account, by [@kitten](https://github.com/kitten) (See [#674](https://github.com/FormidableLabs/urql/pull/674)) - Updated dependencies (See [#688](https://github.com/FormidableLabs/urql/pull/688) and [#678](https://github.com/FormidableLabs/urql/pull/678)) - @urql/exchange-populate@0.1.4 - @urql/core@1.10.8 ## 2.3.0 ### Minor Changes - Support optimistic values for mutations without a selectionset, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#657](https://github.com/FormidableLabs/urql/pull/657)) ### Patch Changes - Refactor to replace dictionary-based (`Object.create(null)`) results with regular objects, by [@kitten](https://github.com/kitten) (See [#651](https://github.com/FormidableLabs/urql/pull/651)) - ⚠️ Fix case where a mutation-rootfield would cause an empty call to the cache.updates[mutationRootField], by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#654](https://github.com/FormidableLabs/urql/pull/654)) - Updated dependencies (See [#658](https://github.com/FormidableLabs/urql/pull/658) and [#650](https://github.com/FormidableLabs/urql/pull/650)) - @urql/core@1.10.5 ## 2.2.8 ### Patch Changes - ⚠️ Fix node resolution when using Webpack, which experiences a bug where it only resolves `package.json:main` instead of `module` when an `.mjs` file imports a package, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - Updated dependencies (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - @urql/exchange-populate@0.1.3 - @urql/core@1.10.4 ## 2.2.7 ### Patch Changes - ⚠️ Fix critical ordering bug in commutative queries and mutations. Subscriptions and queries would ad-hoc be receiving an empty optimistic layer accidentally. This leads to subscription results potentially being cleared, queries from being erased on a second write, and layers from sticking around on every second write or indefinitely. This affects versions `> 2.2.2` so please upgrade!, by [@kitten](https://github.com/kitten) (See [#638](https://github.com/FormidableLabs/urql/pull/638)) - ⚠️ Fix multipart conversion, in the `extract-files` dependency (used by multipart-fetch) there is an explicit check for the constructor property of an object. This made the files unretrievable, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#639](https://github.com/FormidableLabs/urql/pull/639)) - ⚠️ Fix Node.js Module support for v13 (experimental-modules) and v14. If your bundler doesn't support `.mjs` files and fails to resolve the new version, please double check your configuration for Webpack, or similar tools, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - Updated dependencies (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - @urql/exchange-populate@0.1.2 - @urql/core@1.10.3 ## 2.2.6 ### Patch Changes - ⚠️ Fix cache.inspectFields causing an undefined error for uninitialised or cleared commutative layers, by [@kitten](https://github.com/kitten) (See [#626](https://github.com/FormidableLabs/urql/pull/626)) - Improve Store constructor to accept an options object instead of separate arguments, identical to the cacheExchange options. (This is a patch, not a minor, since we consider Store part of the private API), by [@kitten](https://github.com/kitten) (See [#622](https://github.com/FormidableLabs/urql/pull/622)) - Allow a single field to be invalidated using cache.invalidate using two additional arguments, similar to store.resolve; This is a very small addition, so it's marked as a patch, by [@kitten](https://github.com/kitten) (See [#627](https://github.com/FormidableLabs/urql/pull/627)) - Prevent variables from being filtered and queries from being altered before they're forwarded, which prevented additional untyped variables from being used inside updater functions, by [@kitten](https://github.com/kitten) (See [#629](https://github.com/FormidableLabs/urql/pull/629)) - Expose generated result data on writeOptimistic and passthrough data on write operations, by [@kitten](https://github.com/kitten) (See [#613](https://github.com/FormidableLabs/urql/pull/613)) - Updated dependencies (See [#621](https://github.com/FormidableLabs/urql/pull/621)) - @urql/core@1.10.2 ## 2.2.5 ### Patch Changes - Refactor parts of Graphcache for a minor performance boost and bundlesize reductions, by [@kitten](https://github.com/kitten) (See [#611](https://github.com/FormidableLabs/urql/pull/611)) ## 2.2.4 ### Patch Changes - ⚠️ Fix Rollup bundle output being written to .es.js instead of .esm.js, by [@kitten](https://github.com/kitten) (See [#609](https://github.com/FormidableLabs/urql/pull/609)) - Updated dependencies (See [#609](https://github.com/FormidableLabs/urql/pull/609)) - @urql/core@1.10.1 ## 2.2.3 ### Patch Changes - Apply commutative layers to all operations, so now including mutations and subscriptions, to ensure that unordered data is written in the correct order, by [@kitten](https://github.com/kitten) (See [#593](https://github.com/FormidableLabs/urql/pull/593)) - Updated dependencies (See [#607](https://github.com/FormidableLabs/urql/pull/607) and [#601](https://github.com/FormidableLabs/urql/pull/601)) - @urql/core@1.10.0 ## 2.2.2 ### Patch Changes - ⚠️ Fix commutative layer edge case when lowest-priority layer comes back earlier than others, by [@kitten](https://github.com/kitten) (See [#587](https://github.com/FormidableLabs/urql/pull/587)) - Externalise @urql/exchange-populate from bundle, by [@kitten](https://github.com/kitten) (See [#590](https://github.com/FormidableLabs/urql/pull/590)) - ⚠️ Fix teardown events leading to broken commutativity, by [@kitten](https://github.com/kitten) (See [#588](https://github.com/FormidableLabs/urql/pull/588)) ## 2.2.1 ### Patch Changes - Remove the shared package, this will fix the types file generation for graphcache, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#579](https://github.com/FormidableLabs/urql/pull/579)) - Updated dependencies (See [#577](https://github.com/FormidableLabs/urql/pull/577)) - @urql/core@1.9.2 ## 2.2.0 ### Minor Changes - Add `cache.invalidate` to invalidate an entity directly to remove it from the cache and all subsequent cache results, e.g. `cache.invalidate({ __typename: 'Todo', id: 1 })`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#566](https://github.com/FormidableLabs/urql/pull/566)) ### Patch Changes - ⚠️ Fix `cache-only` operations being forwarded and triggering fetch requests, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#551](https://github.com/FormidableLabs/urql/pull/551)) - Apply Query results in-order and commutatively even when results arrive out-of-order, by [@kitten](https://github.com/kitten) (See [#565](https://github.com/FormidableLabs/urql/pull/565)) - Updated dependencies (See [#551](https://github.com/FormidableLabs/urql/pull/551), [#542](https://github.com/FormidableLabs/urql/pull/542), and [#544](https://github.com/FormidableLabs/urql/pull/544)) - @urql/core@1.9.1 ## 2.1.1 ### Patch Changes - Update the `updater` function type of `cache.updateQuery` to have a return type of `DataFields` so that `__typename` does not need to be defined, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#538](https://github.com/FormidableLabs/urql/pull/538)) - ⚠️ Fix updates not being triggered when optimistic updates diverge from the actual result. (See [#160](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/160)) - Refactor away SchemaPredicates helper to reduce bundlesize. (See [#161](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/161)) - Ensure that pagination helpers don't confuse pages that have less params with a query that has more params. (See [#156](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/156)) - Updated dependencies (See [#533](https://github.com/FormidableLabs/urql/pull/533), [#519](https://github.com/FormidableLabs/urql/pull/519), [#515](https://github.com/FormidableLabs/urql/pull/515), [#512](https://github.com/FormidableLabs/urql/pull/512), and [#518](https://github.com/FormidableLabs/urql/pull/518)) - @urql/core@1.9.0 ## 2.1.0 This release adds support for cache persistence which is bringing us one step closer to full offline-support, which we hope to bring you soon. It also allows `wonka@^4.0.0` as a dependency to be compatible with [`urql@1.8.0`](https://github.com/FormidableLabs/urql/blob/master/CHANGELOG.md#v180). It also fixes a couple of issues in our new `populateExchange`. - Refactor internal store code and simplify `Store` (see [#134](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/134)) - ✨ Implement store persistence support (see [#137](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/137)) - ✨ Apply GC to store persistence (see [#138](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/138)) - Remove unused case where scalars are written from an API when links are expected (see [#142](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/142)) - ⚠️ Add support for resolvers causing cache misses (see [#143](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/143)) - ⚠️ Fix nested types (e.g. `[Item!]!`) in `populateExchange` (see [#150](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/150)) - Fix duplicate fragments in `populateExchange` output (see [#151](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/151)) - Allow `wonka@^3.2.1||^4.0.0` to be used (see [#153](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/153)) ## 2.0.0 > **Note:** The minimum required version of `urql` for this release is now `1.7.0`! **Christmas came early!** This version improves performance again by about 25% over `1.2.2`. It also now ships with two new features: The `populateExchange` and automatic garbage collection. Including the `populateExchange` is optional. It records all fragments in any active queries, and populates mutation selection sets when the `@populate` directive is used based on typenames. If your schema includes `viewer` fields on mutations, which resolve back to your `Query` type, you can use this to automatically update your app's data when a mutation is made. _(More documentation on this is coming soon!)_ The garbage collection works by utilising an automatic reference counting algorithm rather than a mark & sweep algorithm. We feel this is the best tradeoff to maintain good performance during runtime while minimising the data that is unnecessarily retained in-memory. You don't have to do _anything_! Graphcache will do its newly added magic in the background. There are some breaking changes, if you're using `cache.resolveConnections` or `resolveValueOrLink` then you now need to use `inspectFields` and `resolveFieldByKey` instead. You may also now make use of `cache.keyOfField`. (More info on [#128](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/128)) - ✨ Implement `populateExchange` (see [#120](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/120)) - Improve type safety of `invariant` and `warning` (see [#121](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/121)) - Reduce size of `populateExchange` (see [#122](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/122)) - Move more code to KVMap (see [#125](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/125)) - Move deletion to setting `undefined` instead (see [#126](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/126)) - Fix multiple edge cases in the `relayPagination` helper, by [@rafeca](https://github.com/rafeca) (see [#127](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/127)) - ✨⚠️ Reimplement data structure and add garbage collection (see [#128](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/128)) - Use Closure Compiler (see [#131](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/131)) - Switch to using `urql/core` on `1.7.0` (see [#132](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/132)) ## 1.2.2 This patch replaces `pessimism` (our former underlying data structure) with a smaller implementation that just uses `Map`s, since we weren't relying on any immutability internally. This cuts down on bundlesize and massively on GC-pressure, which provides a large speedup on low-end devices. - Replace Pessimism with mutable store to prevent excessive GC work (see [#117](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/117)) ## 1.2.1 - Fix viewer fields (which return `Query` types) not being written or read correctly (see [#116](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/116)) ## 1.2.0 - ⚠️ Fix unions not being checked supported by schema predicates, by [@StevenLangbroek](https://github.com/StevenLangbroek) (see [#113](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/113)) - ✨ Add `simplePagination` helper for resolving simple, paginated lists (see [#115](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/115)) ## 1.1.2 - Fix `relayPagination` helper causing cache-misses for empty lists, by [@rafeca](https://github.com/rafeca) (see [#111](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/111)) ## 1.1.1 This is a minor release since it increases the peer dependency of `urql` to `>= 1.6.0`, due to the addition of the `stale` flag to partial responses and `cache-and-network` responses. This flag is useful to check whether more requests are being made in the background by `@urql/exchange-graphcache`. Additionally, this release adds a small stack to every error and warning that indicates where an error has occured. It lists out the query and all subsequent fragments it has been traversing so that errors and warnings can be traced more easily. - Add a query/fragment stack to all errors and warnings (see [#107](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/107)) - Add `stale: true` to all `cache-and-network` and partial responses (see [#108](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/108)) ## 1.0.3 - Fix `relayPagination` helper merging pages with different field arguments (see [#104](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/104)) ## 1.0.2 - Deduplicate connections in `Store.writeConnection` when possible (see [#103](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/103)) - Fix early bail-out in `relayPagination` helper (see [#103](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/103)) ## 1.0.1 - Trims down the size by 100 bytes (see [#96](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/96)) - Include the `/extras` build in the published version (see [#97](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/97)) - Invariant and warnings will now have an error code associated with a more elaborate explanation (see [#99](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/99)) - Invariant errors will now be included in your production bundle (see [#100](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/100)) - Fixes the relayPagination helper to correctly return partial results (see [#101](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/101)) - Add special case to relayPagination for first and last during inwards merge (see [#102](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/102)) ## 1.0.0 > **Note:** The minimum required version of `urql` for this release is now `1.5.1`! **Hooray it's `v1.0.0` time!** This doesn't mean that we won't be changing little things anymore, but we're so far happy with our API and trust Graphcache to work correctly. We will further iterate on this version with some **planned features**, like "fragment invalidation", garbage collection, and more. This version refactors the **cache resolvers** and adds some new special powers to them! You can now return almost anything from cache resolvers and trust that it'll do the right thing: - You can return entity keys, which will resolve the cached entities - You can return keyable entities, which will also be resolved from cache - You may also return unkeyable entities, which will be partially resolved from cache, with your resolved values taking precedence This can also be nested, so that unkeyable entities can eventually lead back to normal, cached entities! This has enabled us to expose the `relayPagination()` helper! This is a resolver that you can just drop into the `cacheExchange`'s `resolvers` config. It automatically does Relay-style pagination, which is now possible due to our more powerful resolvers! You can import it from `@urql/exchange-graphcache/extras`. - ✨ Add full cache resolver traversal (see [#91](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/91)) - ✨ Add a new `relayPagination` helper (see [#91](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/91)) - Add a `Cache` interface with all methods (that are safe for userland) having documentation (see [#91](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/91)) - ⚠ Fix non-default root keys (that aren't just `Query`) not being respected (see [#87](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/87)) ## 1.0.0-rc.11 - Fix `updates` not being called for `optimistic` results (see [#83](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/83)) - Add optional `variables` argument to `readFragment` and `writeFragment` (see [#84](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/84)) - ⚠ Fix field arguments not normalising optional `null` values (see [#85](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/85)) ## 1.0.0-rc.10 - ⚠ Fix removing cache entries by upgrading to Pessimism `1.1.4` (see [ae72d3](https://github.com/FormidableLabs/urql-exchange-graphcache/commit/ae72d3b1c8b3e5965e122d5509eb561f68579474)) ## 1.0.0-rc.9 - ⚠ Fix optimistic updates by upgrading to Pessimism `1.1.3` (see [#81](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/81)) ## 1.0.0-rc.8 - Fix warnings being shown for Relay `Connection` and `Edge` embedded types (see [#79](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/79)) - Implement `readFragment` method on `Store` (see [#73](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/73)) - Implement `readQuery` method on `Store` (see [#73](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/73)) - Improve `writeFragment` method on `Store` (see [#73](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/73)) ## 1.0.0-rc.7 - ⚠ Fix reexecuted operations due to dependencies not using `cache-first` (see [0bd58f6](https://github.com/FormidableLabs/urql-exchange-graphcache/commit/0bd58f6)) ## 1.0.0-rc.6 - ⚠ Fix concurrency issue where a single operation is reexecuted multiple times (see [#70](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/70)) - Skip writing `undefined` to the cache and log a warning in development (see [#71](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/71)) - Allow `query` to be passed as a string to `store.updateQuery` (see [#72](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/72)) ## 1.0.0-rc.5 - ⚠ Fix user-provided `keys` config not being able to return `null` (see [#68](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/68)) ## 1.0.0-rc.4 - ⚠ Fix development warnings throwing an error for root fields (see [#65](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/65)) ## 1.0.0-rc.3 _Note: This is release contains a bug that `v1.0.0-rc.4` fixes_ - Fix warning condition for missing entity keys (see [98287ae](https://github.com/FormidableLabs/urql-exchange-graphcache/commit/98287ae)) ## 1.0.0-rc.2 _Note: This is release contains a bug that `v1.0.0-rc.3` fixes_ - Add warnings for unknown fields based on the schema and deduplicate warnings (see [#63](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/63)) ## 1.0.0-rc.1 This is the first release that adds _schema awareness_. Passing a schema to Graphcache allows it to make deterministic assumptions about the cached results it generates from its data. It can deterministically match fragments to interfaces, instead of resorting to a heuristic, and it can provide _partial results_ for queries. With a `schema` passed to Graphcache, as long as only nullable fields are uncached and missing, it will still provide an initial cached result. - ✨ Add schema awareness using the `schema` option (see [#58](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/58)) - ✨ Allow for partial results to cascade missing values upwards (see [#59](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/59)) - Fix `store.keyOfEntity` not using root names from the schema (see [#62](https://github.com/FormidableLabs/urql-exchange-graphcache/pull/62)) ## 1.0.0-rc.0 This is where this CHANGELOG starts. For a log on what happened in `beta` and `alpha` releases, please read the commit history. ================================================ FILE: exchanges/graphcache/README.md ================================================

@urql/exchange-graphcache

An exchange for normalized caching support in urql

`@urql/exchange-graphcache` is a normalized cache exchange for the [`urql`](https://github.com/urql-graphql/urql) GraphQL client. This is a drop-in replacement for the default `cacheExchange` that, instead of document caching, caches normalized data by keys and connections between data. You can also pass your introspected GraphQL schema to the `cacheExchange`, which enables it to deliver partial results and match fragments deterministically! `urql` is already quite a comprehensive GraphQL client. However in several cases it may be desirable to have data update across the entirety of an app when a response updates some known pieces of data. [Learn more about Graphcache and normalized caching on our docs!](https://formidable.com/open-source/urql/docs/graphcache/) ## Quick Start Guide First install `@urql/exchange-graphcache` alongside `urql`: ```sh yarn add @urql/exchange-graphcache # or npm install --save @urql/exchange-graphcache ``` You'll then need to add the `cacheExchange`, that this package exposes, to your `urql` Client, by replacing the default cache exchange with it: ```js import { createClient, fetchExchange } from 'urql'; import { cacheExchange } from '@urql/exchange-graphcache'; const client = createClient({ url: 'http://localhost:1234/graphql', exchanges: [ // Replace the default cacheExchange with the new one cacheExchange({ /* optional config */ }), fetchExchange, ], }); ``` ================================================ FILE: exchanges/graphcache/benchmarks/10000Reads.html ================================================ 10000 Reads
================================================ FILE: exchanges/graphcache/benchmarks/10000ReadsComplex.html ================================================ 10000 Reads Complex
================================================ FILE: exchanges/graphcache/benchmarks/10000Writes.html ================================================ 10000 Writes
================================================ FILE: exchanges/graphcache/benchmarks/10000WritesComplex.html ================================================ 10000 Writes Complex
================================================ FILE: exchanges/graphcache/benchmarks/1000Reads.html ================================================ 1000 Reads
================================================ FILE: exchanges/graphcache/benchmarks/1000ReadsComplex.html ================================================ 1000 Reads Complex
================================================ FILE: exchanges/graphcache/benchmarks/1000Writes.html ================================================ 1000 Writes
================================================ FILE: exchanges/graphcache/benchmarks/1000WritesComplex.html ================================================ 1000 Writes Complex
================================================ FILE: exchanges/graphcache/benchmarks/100Reads.html ================================================ 100 Reads
================================================ FILE: exchanges/graphcache/benchmarks/100ReadsComplex.html ================================================ 100 Reads Complex
================================================ FILE: exchanges/graphcache/benchmarks/100Writes.html ================================================ 100 Writes
================================================ FILE: exchanges/graphcache/benchmarks/100WritesComplex.html ================================================ 100 Writes Complex
================================================ FILE: exchanges/graphcache/benchmarks/50000Reads.html ================================================ 50000 Reads
================================================ FILE: exchanges/graphcache/benchmarks/50000Writes.html ================================================ 50000 Writes
================================================ FILE: exchanges/graphcache/benchmarks/5000Reads.html ================================================ 5000 Writes
================================================ FILE: exchanges/graphcache/benchmarks/5000Writes.html ================================================ 5000 Writes
================================================ FILE: exchanges/graphcache/benchmarks/500Reads.html ================================================ 500 Writes
================================================ FILE: exchanges/graphcache/benchmarks/500Writes.html ================================================ 500 Writes
================================================ FILE: exchanges/graphcache/benchmarks/addTodo.html ================================================ Add Todo
================================================ FILE: exchanges/graphcache/benchmarks/benchmarks.js ================================================ import urqlClient from './urqlClient.js'; import { ALL_TODOS_QUERY, ALL_WRITERS_QUERY, ALL_BOOKS_QUERY, ALL_STORES_QUERY, ALL_EMPLOYEES_QUERY, ALL_AUTHORS_QUERY, ADD_TODO_MUTATION, UPDATE_TODO_MUTATION, ADD_TODOS_MUTATION, ADD_WRITERS_MUTATION, ADD_BOOKS_MUTATION, ADD_STORES_MUTATION, ADD_EMPLOYEES_MUTATION, ADD_AUTHORS_MUTATION, } from './operations.js'; // create functions that execute operations/queries/mutaitons to be benchmarked export const getAllTodos = async () => { const queryResult = await urqlClient.query(ALL_TODOS_QUERY).toPromise(); return queryResult.data.todos; }; export const getAllWriters = async () => { const queryResult = await urqlClient.query(ALL_WRITERS_QUERY).toPromise(); return queryResult.data.writers; }; export const getAllBooks = async () => { const queryResult = await urqlClient.query(ALL_BOOKS_QUERY).toPromise(); return queryResult.data.books; }; export const getAllStores = async () => { const queryResult = await urqlClient.query(ALL_STORES_QUERY).toPromise(); return queryResult.data.stores; }; export const getAllEmployees = async () => { const queryResult = await urqlClient.query(ALL_EMPLOYEES_QUERY).toPromise(); return queryResult.data.employees; }; export const getAllAuthors = async () => { const queryResult = await urqlClient.query(ALL_AUTHORS_QUERY).toPromise(); return queryResult.data.authors; }; export const addTodo = async () => { const newTodo = { text: 'New todo', complete: true }; const mutationResult = await urqlClient .mutation(ADD_TODO_MUTATION, newTodo) .toPromise(); return mutationResult.data.addTodo; }; export const updateTodo = async ({ id, complete }) => { const updatedTodo = { id, complete }; const mutationResult = await urqlClient .mutation(UPDATE_TODO_MUTATION, updatedTodo) .toPromise(); return mutationResult.data.updateTodo; }; export const addTodos = async todosToBeAdded => { const newTodos = { newTodos: { todos: todosToBeAdded } }; const mutationResult = await urqlClient .mutation(ADD_TODOS_MUTATION, newTodos) .toPromise(); return mutationResult.data.addTodos; }; export const addWriters = async writersToBeAdded => { const newWriters = { newWriters: { writers: writersToBeAdded } }; const mutationResult = await urqlClient .mutation(ADD_WRITERS_MUTATION, newWriters) .toPromise(); return mutationResult.data.addWriters; }; export const addBooks = async booksToBeAdded => { const newBooks = { newBooks: { books: booksToBeAdded } }; const mutationResult = await urqlClient .mutation(ADD_BOOKS_MUTATION, newBooks) .toPromise(); return mutationResult.data.addBooks; }; export const addStores = async storesToBeAdded => { const newStores = { newStores: { stores: storesToBeAdded } }; const mutationResult = await urqlClient .mutation(ADD_STORES_MUTATION, newStores) .toPromise(); return mutationResult.data.addStores; }; export const addEmployees = async employeesToBeAdded => { const newEmployees = { newEmployees: { employees: employeesToBeAdded } }; const mutationResult = await urqlClient .mutation(ADD_EMPLOYEES_MUTATION, newEmployees) .toPromise(); return mutationResult.data.addEmployees; }; export const addAuthors = async authorsToBeAdded => { const newAuthors = { newAuthors: { authors: authorsToBeAdded } }; const mutationResult = await urqlClient .mutation(ADD_AUTHORS_MUTATION, newAuthors) .toPromise(); return mutationResult.data.addAuthors; }; ================================================ FILE: exchanges/graphcache/benchmarks/entities.js ================================================ // functions to produce objects representing entities => todos, writers, books, stores, employees export const makeTodo = i => ({ id: `${i}`, text: `Todo ${i}`, complete: false, }); export const makeWriter = i => ({ id: `${i}`, name: `Writer ${i}`, amountOfBooks: Math.random() * 100, recognized: Boolean(i % 2), number: i, interests: 'Dragonball-Z', }); export const makeBook = i => ({ id: `${i}`, title: `Book ${i}`, published: Boolean(i % 2), genre: 'Fantasy', rating: (i / Math.random()) * 100, }); export const makeStore = i => ({ id: `${i}`, name: `Store ${i}`, country: 'USA', }); export const makeEmployee = i => ({ id: `${i}`, name: `Employee ${i}`, origin: 'USA', }); export const makeAuthor = i => ({ id: `${i}`, name: `Author ${i}`, recognized: Boolean(i % 2), book: { id: `${i}`, title: `Book ${i}`, published: Boolean(i % 2), genre: `Non-Fiction`, rating: (i / Math.random()) * 100, review: { id: `${i}`, score: i, name: `Review ${i}`, reviewer: { id: `${i}`, name: `Person ${i}`, verified: Boolean(i % 2), }, }, }, }); ================================================ FILE: exchanges/graphcache/benchmarks/makeEntries.js ================================================ // create a function that will take in a number of times to be run and a function that will produce an entry/entity export const makeEntries = (amount, makeEntry) => { // create array of entries to be outputted const entries = []; // iterate from 0 up to the amount inputted for (let i = 0; i < amount; i += 1) { // each iteration, create an entry and pass it the current index & push it into output array const entry = makeEntry(i); entries.push(entry); } // return array of entries return entries; }; ================================================ FILE: exchanges/graphcache/benchmarks/operations.js ================================================ // create operations, i.e., queries & mutations, to be performed export const ALL_TODOS_QUERY = ` query ALL_TODOS_QUERY { todos { id, text, complete } } `; export const ALL_WRITERS_QUERY = ` query ALL_WRITERS_QUERY { writers { id, name, amountOfBooks, recognized, number, interests } } `; export const ALL_BOOKS_QUERY = ` query ALL_BOOKS_QUERY { books { id, title, published, genre, rating } } `; export const ALL_STORES_QUERY = ` query ALL_STORES_QUERY { stores { id, name, country } } `; export const ALL_EMPLOYEES_QUERY = ` query ALL_EMPLOYEES_QUERY { employees { id, name, origin } } `; export const ALL_AUTHORS_QUERY = ` query ALL_AUTHORS_QUERY { authors { id, name, recognized, book } } `; export const ADD_TODO_MUTATION = ` mutation ADD_TODO_MUTATION($text: String!, $complete: Boolean!){ addTodo(text: $text, complete: $complete){ id, text, complete } } `; export const UPDATE_TODO_MUTATION = ` mutation UPDATE_TODO_MUTATION($id: ID!, $complete: Boolean!){ updateTodo(id: $id, complete: $complete){ id, text, complete } } `; export const ADD_TODOS_MUTATION = ` mutation ADD_TODOS_MUTATION($newTodos: NewTodosInput!){ addTodos(newTodos: $newTodos){ id, text, complete } } `; export const ADD_WRITERS_MUTATION = ` mutation ADD_WRITERS_MUTATION($newWriters: NewWritersInput!){ addWriters(newWriters: $newWriters){ id, name, amountOfBooks, recognized, number, interests } } `; export const ADD_BOOKS_MUTATION = ` mutation ADD_BOOKS_MUTATION($newBooks: NewBooksInput!){ addBooks(newBooks: $newBooks){ id, title, published, genre, rating } } `; export const ADD_STORES_MUTATION = ` mutation ADD_STORES_MUTATION($newStores: NewStoresInput!){ addStores(newStores: $newStores){ id, name, country } } `; export const ADD_EMPLOYEES_MUTATION = ` mutation ADD_EMPLOYEES_MUTATION($newEmployees: NewEmployeesInput!){ addEmployees(newEmployees: $newEmployees){ id, name, origin } } `; export const ADD_AUTHORS_MUTATION = ` mutation ADD_AUTHORS_MUTATION($newAuthors: NewAuthorsInput!){ addAuthors(newAuthors: $newAuthors){ id name recognized book } } `; ================================================ FILE: exchanges/graphcache/benchmarks/package.json ================================================ { "name": "@urql/exchange-graphcache-tachometer-benchmark", "version": "1.0.0", "description": "Comprehensive Tachometer benchmarks for urql/graphcache", "main": "index.js", "scripts": { "bench": "npm-run-all bench:*", "bench:addTodo": "tachometer addTodo.html", "bench:update": "tachometer updateTodo.html", "bench:write100": "tachometer 100Writes.html", "bench:write100c": "tachometer 100WritesComplex.html", "bench:write500": "tachometer 500Writes.html", "bench:write1000": "tachometer 1000Writes.html", "bench:write1000c": "tachometer 1000WritesComplex.html", "bench:write5000": "tachometer 5000Writes.html", "bench:write10000": "tachometer 10000Writes.html", "bench:write10000c": "tachometer 10000WritesComplex.html", "bench:write50000": "tachometer 50000Writes.html", "bench:read100": "tachometer 100Reads.html", "bench:read100c": "tachometer 100ReadsComplex.html", "bench:read500": "tachometer 500Reads.html", "bench:read1000": "tachometer 1000Reads.html", "bench:read1000c": "tachometer 1000ReadsComplex.html", "bench:read5000": "tachometer 5000Reads.html", "bench:read10000": "tachometer 10000Reads.html", "bench:read10000c": "tachometer 10000ReadsComplex.html", "bench:read50000": "tachometer 50000Reads.html" }, "license": "MIT", "dependencies": { "@urql/exchange-execute": "file:../../execute", "@urql/exchange-graphcache": "file:../", "graphql": "^16.9.0", "npm-run-all": "^4.1.5", "tachometer": "^0.7.1", "urql": "file:../../../packages/react-urql" } } ================================================ FILE: exchanges/graphcache/benchmarks/readMe.md ================================================ ## About This is a set of benchmarks assessing the performance of various graphCache operations. The operations are of varying sizes, complexitites, and are accomplished via a singular `urql` client instance. Client has a stubbed out GQL API (fetchExchange) to perform GQL operations against. ## Usage #### 1. Install dependencies in repo root. To get started, make sure to install necessary dependencies in the root directory of your clone. ```bash # In root directory yarn or npm i ``` #### 2. Run benchmark(s). The commands to run benchmarks follows a certain syntax: npm run `ActionQuantityComplexity` => i.e., npm run read500c read === Action 5000 === Quantity c === Complex Action & Quantity are required, but c is optional, as not all operations involve a more complex data structure. There are two exceptions that don't follow the beformentioned conventions for the commands to run benchmarks. They are `addTodo` & `updateTodo`. They are simply run as follows: ``` npm run addTodo npm run updateTodo ``` #### 3. Benchmark Expections Upon executing a command, `Tachometer` will automatically execute the benchmarks via your default browser. Done 50 times prior to returning benchmark result in the console where the command was launched. ================================================ FILE: exchanges/graphcache/benchmarks/updateTodo.html ================================================ Update Todo
================================================ FILE: exchanges/graphcache/benchmarks/urqlClient.js ================================================ import { createClient } from '@urql/core'; import { cacheExchange } from '@urql/exchange-graphcache'; import { executeExchange } from '@urql/exchange-execute'; import { buildSchema } from 'graphql'; import { ALL_TODOS_QUERY } from './operations'; export const cache = cacheExchange({ updates: { Mutation: { addTodo: (result, args, cache) => { cache.updateQuery({ query: ALL_TODOS_QUERY }, data => { data.todos.push(result.addTodo); return data; }); return result; }, }, }, }); // local schema to be used with Execute Exchange const schema = buildSchema(` type Todo { id: ID! text: String! complete: Boolean! } type Writer { id: ID! name: String amountOfBooks: Float! recognized: Boolean! number: Int! interests: String! } type Book { id: ID! title: String! published: Boolean! genre: String! rating: Float! review: Review } type Store { id: ID! name: String! country: String! } type Employee { id: ID! name: String! origin: String! } type Author { id: ID! name: String! recognized: Boolean! book: Book! } type Review { id: ID! score: Int! name: String! reviewer: Person! } type Person { id: ID! name: String! verfied: Boolean! } input NewTodo { id: ID! text: String! complete: Boolean! } input NewTodosInput { todos: [NewTodo]! } input NewWriter { id: ID! name: String amountOfBooks: Float! recognized: Boolean! number: Int! interests: String! } input NewWritersInput { writers: [NewWriter]! } input NewBook { id: ID! title: String! published: Boolean! genre: String! rating: Float! review: NewReview } input NewBooksInput { books: [NewBook]! } input NewStore { id: ID! name: String! country: String! } input NewStoresInput { stores: [NewStore]! } input NewEmployee { id: ID! name: String! origin: String! } input NewEmployeesInput { employees: [NewEmployee]! } input NewAuthor { id: ID! name: String! recognized: Boolean! book: NewBook! } input NewAuthorsInput { authors: [NewAuthor]! } input NewReview { id: ID! score: Int! name: String! reviewer: NewPerson! } input NewPerson { id: ID! name: String! verified: Boolean! } type Query { todos: [Todo]! writers: [Writer]! books: [Book]! stores: [Store]! employees: [Employee]! authors: [Author]! } type Mutation { addTodo( text: String!, complete: Boolean! ): Todo! updateTodo( id: ID!, complete: Boolean! ): Todo! addTodos( newTodos: NewTodosInput! ): [Todo]! addWriters( newWriters: NewWritersInput! ): [Writer]! addBooks( newBooks: NewBooksInput! ): [Book]! addStores( newStores: NewStoresInput! ): [Store]! addEmployees( newEmployees: NewEmployeesInput! ): [Employee]! addAuthors( newAuthors: NewAuthorsInput! ): [Author]! } `); // local state to be used with Execute Exchange const todos = []; const writers = []; const books = []; const stores = []; const employees = []; const authors = []; // root value with resolvers to be used with Execute Exchange const rootValue = { todos: () => { return todos; }, writers: () => { return writers; }, books: () => { return books; }, stores: () => { return stores; }, employees: () => { return employees; }, authors: () => { return authors; }, addTodo: args => { const todo = { id: todos.length.toString(), ...args }; todos.push(todo); return todo; }, updateTodo: ({ id, complete }) => { const [todoToBeUpdated] = todos.filter(todo => todo.id === id); todoToBeUpdated.complete = complete; return todoToBeUpdated; }, addTodos: ({ newTodos }) => { const todosToBeAdded = newTodos.todos; todos.push(...todosToBeAdded); return todos; }, addWriters: ({ newWriters }) => { const writersToBeAdded = newWriters.writers; writers.push(...writersToBeAdded); return writers; }, addBooks: ({ newBooks }) => { const booksToBeAdded = newBooks.books; books.push(...booksToBeAdded); return books; }, addStores: ({ newStores }) => { const storesToBeAdded = newStores.stores; stores.push(...storesToBeAdded); return stores; }, addEmployees: ({ newEmployees }) => { const employeesToBeAdded = newEmployees.employees; employees.push(...employeesToBeAdded); return employees; }, addAuthors: ({ newAuthors }) => { const authorsToBeAdded = newAuthors.authors; authors.push(...authorsToBeAdded); return authors; }, }; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [ cache, // cacheExchange({}), executeExchange({ schema, rootValue }), ], }); export default client; ================================================ FILE: exchanges/graphcache/cypress/fixtures/example.json ================================================ { "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" } ================================================ FILE: exchanges/graphcache/cypress/plugins/index.js ================================================ // eslint-disable-next-line const { startDevServer } = require('@cypress/vite-dev-server'); // eslint-disable-next-line const path = require('path'); /** * @type {Cypress.PluginConfig} */ module.exports = (on, _config) => { on('dev-server:start', options => startDevServer({ options, }) ); }; ================================================ FILE: exchanges/graphcache/cypress/support/component-index.html ================================================ Components App
================================================ FILE: exchanges/graphcache/cypress/support/component.js ================================================ // *********************************************************** // This example support/component.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** import { mount } from 'cypress/react'; Cypress.Commands.add('mount', mount); // Example use: // cy.mount() ================================================ FILE: exchanges/graphcache/cypress.config.js ================================================ // eslint-disable-next-line const { defineConfig } = require('cypress'); // eslint-disable-next-line const tsconfigPaths = require('vite-tsconfig-paths').default; module.exports = defineConfig({ video: false, e2e: { setupNodeEvents(_on, _config) { /*noop*/ }, supportFile: false, }, component: { specPattern: './**/e2e-tests/*spec.tsx', devServer: { framework: 'react', bundler: 'vite', viteConfig: { plugins: [tsconfigPaths()], server: { fs: { allow: ['../..'], }, }, }, }, }, }); ================================================ FILE: exchanges/graphcache/e2e-tests/query.spec.tsx ================================================ /// import * as React from 'react'; import { mount } from '@cypress/react'; import { Provider, createClient, useQuery, debugExchange } from 'urql'; import { executeExchange } from '@urql/exchange-execute'; import { buildSchema, introspectionFromSchema } from 'graphql'; import { cacheExchange } from '../src'; const schema = buildSchema(` type Query { movie: Movie } type Movie { id: String title: String metadata: Metadata } type Metadata { uri: String } `); const rootValue = { movie: async () => { await new Promise(resolve => setTimeout(resolve, 50)); return { id: 'foo', title: 'title', metadata: () => { throw new Error('Test'); }, }; }, }; describe('Graphcache Queries', () => { it('should not loop with no schema present', () => { const client = createClient({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', exchanges: [ cacheExchange({}), debugExchange, executeExchange({ schema, rootValue }), ], }); const FirstComponent = () => { const [{ fetching, error }] = useQuery({ query: `{ movie { id title metadata { uri } } }`, }); return (
{fetching === true ? ( 'loading' ) : (
First Component
{`Error: ${error?.message}`}
)}
); }; const SecondComponent = () => { const [{ error, fetching }] = useQuery({ query: `{ movie { id metadata { uri } } }`, }); if (fetching) { return
Loading...
; } return (
Second Component
{`Error: ${error?.message}`}
); }; mount( ); cy.get('#first-error').should('have.text', 'Error: [GraphQL] Test'); cy.get('#second-error').should('have.text', 'Error: [GraphQL] Test'); }); it('should not loop with schema present', () => { const client = createClient({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', exchanges: [ cacheExchange({ schema: introspectionFromSchema(schema) }), debugExchange, executeExchange({ schema, rootValue }), ], }); const FirstComponent = () => { const [{ fetching, data, error, stale }] = useQuery({ query: `{ movie { id title metadata { uri } } }`, }); return (
{fetching === true ? ( 'loading' ) : (
First Component
{`Data: ${data.movie?.title}`}
{`Error: ${error?.message}`}
{`Stale: ${!!stale}`}
)}
); }; const SecondComponent = () => { const [{ error, data, fetching, stale }] = useQuery({ query: `{ movie { id metadata { uri } } }`, }); if (fetching) { return
Loading...
; } return (
Second Component
{`Data: ${data.movie.id}`}
{`Error: ${error?.message}`}
{`Stale: ${!!stale}`}
); }; mount( ); cy.get('#first-data').should('have.text', 'Data: title'); cy.get('#second-data').should('have.text', 'Data: foo'); cy.get('#second-stale').should('have.text', 'Stale: false'); // TODO: ideally we would be able to keep the error here but... // cy.get('#first-error').should('have.text', 'Error: [GraphQL] Test'); // cy.get('#second-error').should('have.text', 'Error: [GraphQL] Test'); }); }); ================================================ FILE: exchanges/graphcache/e2e-tests/updates.spec.tsx ================================================ import * as React from 'react'; import { executeExchange } from '@urql/exchange-execute'; import { buildSchema } from 'graphql'; import { mount } from '@cypress/react'; import { Provider, createClient, gql, useQuery, useMutation, debugExchange, } from 'urql'; import { cacheExchange } from '../src'; const schema = buildSchema(` type Todo { id: ID! text: String! } type Query { todos: [Todo]! } type Mutation { updateTodo(id: ID! text: String!): Todo! } `); const todos: Array<{ id: string; text: string }> = [ { id: '1', text: 'testing urql' }, ]; const rootValue = { todos: () => { return todos; }, updateTodo: args => { const todo = todos.find(x => x.id === args.id); if (!todo) throw new Error("Can't find todo!"); todo.text = args.text; return todo; }, }; describe('Graphcache updates', () => { let client; beforeEach(() => { client = createClient({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', exchanges: [ cacheExchange({}), debugExchange, executeExchange({ schema, rootValue }), ], }); }); const TodosQuery = gql` query { todos { id text } } `; const UpdateMutation = gql` mutation ($id: ID!, $text: String!) { updateTodo(id: $id, text: $text) { id text } } `; it('Can automatically update entities who have been queried', () => { const Todos = () => { const [result] = useQuery({ query: TodosQuery }); const [, update] = useMutation(UpdateMutation); if (result.fetching) return

Loading...

; return (
    {result.data.todos.map(todo => (
  • {todo.text}
  • ))}
); }; mount( ); const target = { ...todos[0] }; cy.get('#todos-list > li').then(items => { expect(items.length).to.equal(todos.length); }); cy.get('#update-' + target.id).click(); cy.wait(500); cy.get('#todos-list > li').then(items => { expect(items.length).to.equal(todos.length); expect(items[0].innerText).to.contain(target.text + '_foo'); }); }); }); ================================================ FILE: exchanges/graphcache/help.md ================================================ This file (`exchanges/graphcache/help.md`) has been moved to `docs/graphcache/errors.md` If you are looking at this in a browser - ...and your URL looks like this: `github.com/urql-graphql/urql/blob/main/exchanges/graphcache/help.md#15` - ...in the URL, replace `exchanges/graphcache/help.md` with `docs/graphcache/errors.md` - ...and keep the `#15` - ...and then you will get help with your error! ================================================ FILE: exchanges/graphcache/jsr.json ================================================ { "name": "@urql/exchange-graphcache", "version": "9.0.0", "exports": { ".": "./src/index.ts", "./extras": "./src/extras/index.ts", "./default-storage": "./src/default-storage/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/graphcache/package.json ================================================ { "name": "@urql/exchange-graphcache", "version": "9.0.0", "description": "A normalized and configurable cache exchange for urql", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/graphcache", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/graphcache" }, "keywords": [ "urql", "state management", "normalized cache", "cache", "formidablelabs", "exchanges" ], "main": "dist/urql-exchange-graphcache", "module": "dist/urql-exchange-graphcache.mjs", "types": "dist/urql-exchange-graphcache.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-graphcache.d.ts", "import": "./dist/urql-exchange-graphcache.mjs", "require": "./dist/urql-exchange-graphcache.js", "source": "./src/index.ts" }, "./package.json": "./package.json", "./extras": { "types": "./dist/urql-exchange-graphcache-extras.d.ts", "import": "./dist/urql-exchange-graphcache-extras.mjs", "require": "./dist/urql-exchange-graphcache-extras.js", "source": "./src/extras/index.ts" }, "./default-storage": { "types": "./dist/urql-exchange-graphcache-default-storage.d.ts", "import": "./dist/urql-exchange-graphcache-default-storage.mjs", "require": "./dist/urql-exchange-graphcache-default-storage.js", "source": "./src/default-storage/index.ts" } }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/", "extras/", "default-storage/" ], "scripts": { "test": "vitest", "clean": "rimraf dist extras", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "peerDependencies": { "@urql/core": "^6.0.0" }, "dependencies": { "@0no-co/graphql.web": "^1.0.13", "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "devDependencies": { "@cypress/react": "^8.0.2", "@urql/core": "workspace:*", "@urql/exchange-execute": "workspace:*", "@urql/introspection": "workspace:*", "cypress": "^13.14.0", "graphql": "^16.6.0", "react": "^17.0.1", "react-dom": "^17.0.1", "urql": "workspace:*" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/graphcache/src/ast/graphql.ts ================================================ import type * as GraphQL from 'graphql'; type OrNever = void extends T ? never : T; export type IntrospectionQuery = | { readonly __schema: { queryType: { name: string; kind?: any }; mutationType?: { name: string; kind?: any } | null; subscriptionType?: { name: string; kind?: any } | null; types?: readonly IntrospectionType[]; }; } | OrNever; export type IntrospectionTypeRef = | { readonly kind: | 'SCALAR' | 'OBJECT' | 'INTERFACE' | 'ENUM' | 'UNION' | 'INPUT_OBJECT'; readonly name?: string; readonly ofType?: IntrospectionTypeRef; } | OrNever; export type IntrospectionInputTypeRef = | { readonly kind: 'SCALAR' | 'ENUM' | 'INPUT_OBJECT'; readonly name?: string; readonly ofType?: IntrospectionInputTypeRef; } | OrNever; export type IntrospectionInputValue = | { readonly name: string; readonly description?: string | null; readonly defaultValue?: string | null; readonly type: IntrospectionInputTypeRef; } | OrNever; export type IntrospectionType = | { readonly kind: string; readonly name: string; readonly fields?: readonly any[]; readonly interfaces?: readonly any[]; readonly possibleTypes?: readonly any[]; } | OrNever; ================================================ FILE: exchanges/graphcache/src/ast/index.ts ================================================ export * from './variables'; export * from './traversal'; export * from './schema'; export * from './schemaPredicates'; export * from './node'; ================================================ FILE: exchanges/graphcache/src/ast/node.ts ================================================ import type { NamedTypeNode, NameNode, DirectiveNode, SelectionNode, SelectionSetNode, FieldNode, FragmentDefinitionNode, } from '@0no-co/graphql.web'; import type { FormattedNode } from '@urql/core'; export type SelectionSet = readonly FormattedNode[]; const EMPTY_DIRECTIVES: Record = {}; /** Returns the directives dictionary of a given node */ export const getDirectives = (node: { _directives?: Record; }) => node._directives || EMPTY_DIRECTIVES; /** Returns the name of a given node */ export const getName = (node: { name: NameNode }): string => node.name.value; export const getFragmentTypeName = (node: FragmentDefinitionNode): string => node.typeCondition.name.value; /** Returns either the field's name or the field's alias */ export const getFieldAlias = (node: FieldNode): string => node.alias ? node.alias.value : node.name.value; const emptySelectionSet: SelectionSet = []; /** Returns the SelectionSet for a given inline or defined fragment node */ export const getSelectionSet = (node: { selectionSet?: FormattedNode; }): FormattedNode => (node.selectionSet ? node.selectionSet.selections : emptySelectionSet) as FormattedNode; export const getTypeCondition = (node: { typeCondition?: NamedTypeNode; }): string | null => node.typeCondition ? node.typeCondition.name.value : null; ================================================ FILE: exchanges/graphcache/src/ast/schema.ts ================================================ import type { IntrospectionQuery, IntrospectionTypeRef, IntrospectionInputValue, IntrospectionType, } from './graphql'; export interface SchemaField { name: string; type: IntrospectionTypeRef; args(): Record; } export interface SchemaObject { name: string; kind: 'INTERFACE' | 'OBJECT'; interfaces(): Record; fields(): Record; } export interface SchemaUnion { name: string; kind: 'UNION'; types(): Record; } export interface SchemaIntrospector { query: string | null; mutation: string | null; subscription: string | null; types?: Map; isSubType(abstract: string, possible: string): boolean; } export interface PartialIntrospectionSchema { queryType: { name: string; kind?: any }; mutationType?: { name: string; kind?: any } | null; subscriptionType?: { name: string; kind?: any } | null; types?: readonly any[]; } export type IntrospectionData = | IntrospectionQuery | { __schema: PartialIntrospectionSchema }; export const buildClientSchema = ({ __schema, }: IntrospectionData): SchemaIntrospector => { const typemap: Map = new Map(); const buildNameMap = ( arr: ReadonlyArray ): (() => { [name: string]: T }) => { let map: Record | void; return () => { if (!map) { map = {}; for (let i = 0; i < arr.length; i++) map[arr[i].name] = arr[i]; } return map; }; }; const buildType = ( type: IntrospectionType ): SchemaObject | SchemaUnion | void => { switch (type.kind) { case 'OBJECT': case 'INTERFACE': return { name: type.name, kind: type.kind as 'OBJECT' | 'INTERFACE', interfaces: buildNameMap(type.interfaces || []), fields: buildNameMap( type.fields!.map((field: any) => ({ name: field.name, type: field.type, args: buildNameMap(field.args), })) ), } as SchemaObject; case 'UNION': return { name: type.name, kind: type.kind as 'UNION', types: buildNameMap(type.possibleTypes || []), } as SchemaUnion; } }; const schema: SchemaIntrospector = { query: __schema.queryType ? __schema.queryType.name : null, mutation: __schema.mutationType ? __schema.mutationType.name : null, subscription: __schema.subscriptionType ? __schema.subscriptionType.name : null, types: undefined, isSubType(abstract: string, possible: string) { const abstractType = typemap.get(abstract); const possibleType = typemap.get(possible); if (!abstractType || !possibleType) { return false; } else if (abstractType.kind === 'UNION') { return !!abstractType.types()[possible]; } else if ( abstractType.kind !== 'OBJECT' && possibleType.kind === 'OBJECT' ) { return !!possibleType.interfaces()[abstract]; } else { return abstract === possible; } }, }; if (__schema.types) { schema.types = typemap; for (let i = 0; i < __schema.types.length; i++) { const type = __schema.types[i]; if (type && type.name) { const out = buildType(type); if (out) typemap.set(type.name, out); } } } return schema; }; ================================================ FILE: exchanges/graphcache/src/ast/schemaPredicates.test.ts ================================================ import { Kind, InlineFragmentNode } from 'graphql'; import { describe, it, expect } from 'vitest'; import { buildClientSchema } from './schema'; import * as SchemaPredicates from './schemaPredicates'; import { minifyIntrospectionQuery } from '@urql/introspection'; const mocked = (x: any): any => x; describe('SchemaPredicates', () => { const schema = buildClientSchema( // eslint-disable-next-line minifyIntrospectionQuery(require('../test-utils/simple_schema.json')) ); const frag = (value: string): InlineFragmentNode => ({ kind: Kind.INLINE_FRAGMENT, typeCondition: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value, }, }, selectionSet: { kind: Kind.SELECTION_SET, selections: [], }, }); it('should match fragments by interface/union', () => { expect( SchemaPredicates.isInterfaceOfType(schema, frag('ITodo'), 'BigTodo') ).toBeTruthy(); expect( SchemaPredicates.isInterfaceOfType(schema, frag('ITodo'), 'SmallTodo') ).toBeTruthy(); expect( SchemaPredicates.isInterfaceOfType(schema, frag('Search'), 'BigTodo') ).toBeTruthy(); expect( SchemaPredicates.isInterfaceOfType(schema, frag('Search'), 'SmallTodo') ).toBeTruthy(); expect( SchemaPredicates.isInterfaceOfType(schema, frag('ITodo'), 'Todo') ).toBeFalsy(); expect( SchemaPredicates.isInterfaceOfType(schema, frag('Search'), 'Todo') ).toBeFalsy(); const typeConditionLess = frag('Type'); (typeConditionLess as any).typeCondition = undefined; expect( SchemaPredicates.isInterfaceOfType(schema, typeConditionLess, 'Todo') ).toBeTruthy(); }); it('should indicate nullability', () => { expect( SchemaPredicates.isFieldNullable(schema, 'Todo', 'text', undefined) ).toBeFalsy(); expect( SchemaPredicates.isFieldNullable(schema, 'Todo', 'complete', undefined) ).toBeTruthy(); expect( SchemaPredicates.isFieldNullable(schema, 'Todo', 'author', undefined) ).toBeTruthy(); }); it('should handle unions of objects', () => { expect( SchemaPredicates.isInterfaceOfType( schema, frag('LatestTodoResult'), 'Todo' ) ).toBeTruthy(); expect( SchemaPredicates.isInterfaceOfType( schema, frag('LatestTodoResult'), 'NoTodosError' ) ).toBeTruthy(); expect( SchemaPredicates.isInterfaceOfType(schema, frag('Todo'), 'NoTodosError') ).toBeFalsy(); }); it('should throw if a requested type does not exist', () => { expect(() => SchemaPredicates.isFieldNullable( schema, 'SomeInvalidType', 'complete', undefined ) ).toThrow( 'The type `SomeInvalidType` is not an object in the defined schema, but the GraphQL document is traversing it.\nhttps://bit.ly/2XbVrpR#3' ); }); it('should warn in console if a requested field does not exist', () => { expect( SchemaPredicates.isFieldNullable(schema, 'Todo', 'goof', undefined) ).toBeFalsy(); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain('The field `goof` does not exist on `Todo`'); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#4'); }); }); ================================================ FILE: exchanges/graphcache/src/ast/schemaPredicates.ts ================================================ import type { InlineFragmentNode, FragmentDefinitionNode, } from '@0no-co/graphql.web'; import { warn, invariant } from '../helpers/help'; import { getTypeCondition } from './node'; import type { SchemaIntrospector, SchemaObject } from './schema'; import type { KeyingConfig, UpdatesConfig, ResolverConfig, OptimisticMutationConfig, Logger, } from '../types'; const BUILTIN_NAME = '__'; export const isFieldNullable = ( schema: SchemaIntrospector, typename: string, fieldName: string, logger: Logger | undefined ): boolean => { const field = getField(schema, typename, fieldName, logger); return !!field && field.type.kind !== 'NON_NULL'; }; export const isListNullable = ( schema: SchemaIntrospector, typename: string, fieldName: string, logger: Logger | undefined ): boolean => { const field = getField(schema, typename, fieldName, logger); if (!field) return false; const ofType = field.type.kind === 'NON_NULL' ? field.type.ofType : field.type; return ofType.kind === 'LIST' && ofType.ofType.kind !== 'NON_NULL'; }; export const isFieldAvailableOnType = ( schema: SchemaIntrospector, typename: string, fieldName: string, logger: Logger | undefined ): boolean => fieldName.indexOf(BUILTIN_NAME) === 0 || typename.indexOf(BUILTIN_NAME) === 0 || !!getField(schema, typename, fieldName, logger); export const isInterfaceOfType = ( schema: SchemaIntrospector, node: InlineFragmentNode | FragmentDefinitionNode, typename: string | void ): boolean => { if (!typename) return false; const typeCondition = getTypeCondition(node); if (!typeCondition || typename === typeCondition) { return true; } else if ( schema.types!.has(typeCondition) && schema.types!.get(typeCondition)!.kind === 'OBJECT' ) { return typeCondition === typename; } expectAbstractType(schema, typeCondition!); expectObjectType(schema, typename!); return schema.isSubType(typeCondition, typename); }; const getField = ( schema: SchemaIntrospector, typename: string, fieldName: string, logger: Logger | undefined ) => { if ( fieldName.indexOf(BUILTIN_NAME) === 0 || typename.indexOf(BUILTIN_NAME) === 0 ) return; expectObjectType(schema, typename); const object = schema.types!.get(typename) as SchemaObject; const field = object.fields()[fieldName]; if (!field) { warn( 'Invalid field: The field `' + fieldName + '` does not exist on `' + typename + '`, ' + 'but the GraphQL document expects it to exist.\n' + 'Traversal will continue, however this may lead to undefined behavior!', 4, logger ); } return field; }; function expectObjectType(schema: SchemaIntrospector, typename: string) { invariant( schema.types!.has(typename) && schema.types!.get(typename)!.kind === 'OBJECT', 'Invalid Object type: The type `' + typename + '` is not an object in the defined schema, ' + 'but the GraphQL document is traversing it.', 3 ); } function expectAbstractType(schema: SchemaIntrospector, typename: string) { invariant( schema.types!.has(typename) && (schema.types!.get(typename)!.kind === 'INTERFACE' || schema.types!.get(typename)!.kind === 'UNION'), 'Invalid Abstract type: The type `' + typename + '` is not an Interface or Union type in the defined schema, ' + 'but a fragment in the GraphQL document is using it as a type condition.', 5 ); } export function expectValidKeyingConfig( schema: SchemaIntrospector, keys: KeyingConfig, logger: Logger | undefined ): void { if (process.env.NODE_ENV !== 'production') { for (const key in keys) { if (!schema.types!.has(key)) { warn( 'Invalid Object type: The type `' + key + '` is not an object in the defined schema, but the `keys` option is referencing it.', 20, logger ); } } } } export function expectValidUpdatesConfig( schema: SchemaIntrospector, updates: UpdatesConfig, logger: Logger | undefined ): void { if (process.env.NODE_ENV === 'production') { return; } for (const typename in updates) { if (!updates[typename]) { continue; } else if (!schema.types!.has(typename)) { let addition = ''; if ( typename === 'Mutation' && schema.mutation && schema.mutation !== 'Mutation' ) { addition += '\nMaybe your config should reference `' + schema.mutation + '`?'; } else if ( typename === 'Subscription' && schema.subscription && schema.subscription !== 'Subscription' ) { addition += '\nMaybe your config should reference `' + schema.subscription + '`?'; } return warn( 'Invalid updates type: The type `' + typename + '` is not an object in the defined schema, but the `updates` config is referencing it.' + addition, 21, logger ); } const fields = (schema.types!.get(typename)! as SchemaObject).fields(); for (const fieldName in updates[typename]!) { if (!fields[fieldName]) { warn( 'Invalid updates field: `' + fieldName + '` on `' + typename + '` is not in the defined schema, but the `updates` config is referencing it.', 22, logger ); } } } } function warnAboutResolver(name: string, logger: Logger | undefined): void { warn( `Invalid resolver: \`${name}\` is not in the defined schema, but the \`resolvers\` option is referencing it.`, 23, logger ); } function warnAboutAbstractResolver( name: string, kind: 'UNION' | 'INTERFACE', logger: Logger | undefined ): void { warn( `Invalid resolver: \`${name}\` does not match to a concrete type in the schema, but the \`resolvers\` option is referencing it. Implement the resolver for the types that ${ kind === 'UNION' ? 'make up the union' : 'implement the interface' } instead.`, 26, logger ); } export function expectValidResolversConfig( schema: SchemaIntrospector, resolvers: ResolverConfig, logger: Logger | undefined ): void { if (process.env.NODE_ENV === 'production') { return; } for (const key in resolvers) { if (key === 'Query') { if (schema.query) { const validQueries = ( schema.types!.get(schema.query) as SchemaObject ).fields(); for (const resolverQuery in resolvers.Query || {}) { if (!validQueries[resolverQuery]) { warnAboutResolver('Query.' + resolverQuery, logger); } } } else { warnAboutResolver('Query', logger); } } else { if (!schema.types!.has(key)) { warnAboutResolver(key, logger); } else if ( schema.types!.get(key)!.kind === 'INTERFACE' || schema.types!.get(key)!.kind === 'UNION' ) { warnAboutAbstractResolver( key, schema.types!.get(key)!.kind as 'INTERFACE' | 'UNION', logger ); } else { const validTypeProperties = ( schema.types!.get(key) as SchemaObject ).fields(); for (const resolverProperty in resolvers[key] || {}) { if (!validTypeProperties[resolverProperty]) { warnAboutResolver(key + '.' + resolverProperty, logger); } } } } } } export function expectValidOptimisticMutationsConfig( schema: SchemaIntrospector, optimisticMutations: OptimisticMutationConfig, logger: Logger | undefined ): void { if (process.env.NODE_ENV === 'production') { return; } if (schema.mutation) { const validMutations = ( schema.types!.get(schema.mutation) as SchemaObject ).fields(); for (const mutation in optimisticMutations) { if (!validMutations[mutation]) { warn( `Invalid optimistic mutation field: \`${mutation}\` is not a mutation field in the defined schema, but the \`optimistic\` option is referencing it.`, 24, logger ); } } } } ================================================ FILE: exchanges/graphcache/src/ast/traversal.test.ts ================================================ import { formatDocument, gql } from '@urql/core'; import { describe, it, expect } from 'vitest'; import { getSelectionSet } from './node'; import { getMainOperation, shouldInclude } from './traversal'; describe('getMainOperation', () => { it('retrieves the first operation', () => { const doc = formatDocument(gql` query Query { field } `); const operation = getMainOperation(doc); expect(operation).toBe(doc.definitions[0]); }); it('throws when no operation is found', () => { const doc = formatDocument(gql` fragment _ on Query { field } `); expect(() => getMainOperation(doc)).toThrow(); }); }); describe('shouldInclude', () => { it('should include fields with truthy @include or falsy @skip directives', () => { const doc = formatDocument(gql` { fieldA @include(if: true) fieldB @skip(if: false) } `); const fieldA = getSelectionSet(getMainOperation(doc))[0]; const fieldB = getSelectionSet(getMainOperation(doc))[1]; expect(shouldInclude(fieldA, {})).toBe(true); expect(shouldInclude(fieldB, {})).toBe(true); }); it('should exclude fields with falsy @include or truthy @skip directives', () => { const doc = formatDocument(gql` { fieldA @include(if: false) fieldB @skip(if: true) } `); const fieldA = getSelectionSet(getMainOperation(doc))[0]; const fieldB = getSelectionSet(getMainOperation(doc))[1]; expect(shouldInclude(fieldA, {})).toBe(false); expect(shouldInclude(fieldB, {})).toBe(false); }); it('ignore other directives', () => { const doc = formatDocument(gql` { field @test(if: false) } `); const field = getSelectionSet(getMainOperation(doc))[0]; expect(shouldInclude(field, {})).toBe(true); }); it('ignore unknown arguments on directives', () => { const doc = formatDocument(gql` { field @skip(if: true, other: false) } `); const field = getSelectionSet(getMainOperation(doc))[0]; expect(shouldInclude(field, {})).toBe(false); }); it('ignore directives with invalid first arguments', () => { const doc = formatDocument(gql` { field @skip(other: true) } `); const field = getSelectionSet(getMainOperation(doc))[0]; expect(shouldInclude(field, {})).toBe(true); }); }); ================================================ FILE: exchanges/graphcache/src/ast/traversal.ts ================================================ import type { SelectionNode, DocumentNode, OperationDefinitionNode, FragmentSpreadNode, InlineFragmentNode, } from '@0no-co/graphql.web'; import { valueFromASTUntyped, Kind } from '@0no-co/graphql.web'; import type { FormattedNode } from '@urql/core'; import { getName, getDirectives } from './node'; import { invariant } from '../helpers/help'; import type { Fragments, Variables } from '../types'; function getMainOperation( doc: FormattedNode ): FormattedNode; function getMainOperation(doc: DocumentNode): OperationDefinitionNode; /** Returns the main operation's definition */ function getMainOperation(doc: DocumentNode): OperationDefinitionNode { for (let i = 0; i < doc.definitions.length; i++) { if (doc.definitions[i].kind === Kind.OPERATION_DEFINITION) { return doc.definitions[i] as FormattedNode; } } invariant( false, 'Invalid GraphQL document: All GraphQL documents must contain an OperationDefinition' + 'node for a query, subscription, or mutation.', 1 ); } export { getMainOperation }; /** Returns a mapping from fragment names to their selections */ export const getFragments = (doc: FormattedNode): Fragments => { const fragments: Fragments = {}; for (let i = 0; i < doc.definitions.length; i++) { const node = doc.definitions[i]; if (node.kind === Kind.FRAGMENT_DEFINITION) { fragments[getName(node)] = node; } } return fragments; }; /** Resolves @include and @skip directives to determine whether field is included. */ export const shouldInclude = ( node: FormattedNode, vars: Variables ): boolean => { const directives = getDirectives(node); if (directives.include || directives.skip) { // Finds any @include or @skip directive that forces the node to be skipped for (const name in directives) { const directive = directives[name]; if ( directive && (name === 'include' || name === 'skip') && directive.arguments && directive.arguments[0] && getName(directive.arguments[0]) === 'if' ) { // Return whether this directive forces us to skip // `@include(if: false)` or `@skip(if: true)` const value = valueFromASTUntyped(directive.arguments[0].value, vars); return name === 'include' ? !!value : !value; } } } return true; }; /** Resolves @defer directive to determine whether a fragment is potentially skipped. */ export const isDeferred = ( node: FormattedNode, vars: Variables ): boolean => { const { defer } = getDirectives(node); if (defer) { for (const argument of defer.arguments || []) { if (getName(argument) === 'if') { // Return whether `@defer(if: )` is enabled return !!valueFromASTUntyped(argument.value, vars); } } return true; } return false; }; /** Resolves @_optional and @_required directive to determine whether the fields in a fragment are conaidered optional. */ export const isOptional = ( node: FormattedNode ): boolean | undefined => { const { optional, required } = getDirectives(node); if (required) { return false; } if (optional) { return true; } return undefined; }; ================================================ FILE: exchanges/graphcache/src/ast/variables.test.ts ================================================ import { formatDocument, gql } from '@urql/core'; import { describe, it, expect } from 'vitest'; import { getMainOperation } from './traversal'; import { normalizeVariables, filterVariables } from './variables'; describe('normalizeVariables', () => { it('normalizes variables', () => { const input = { x: 42 }; const operation = getMainOperation( formatDocument(gql` query ($x: Int!) { field } `) ); const normalized = normalizeVariables(operation, input); expect(normalized).toEqual({ x: 42 }); }); it('normalizes variables with defaults', () => { const input = { x: undefined }; const operation = getMainOperation( formatDocument(gql` query ($x: Int! = 42) { field } `) ); const normalized = normalizeVariables(operation, input); expect(normalized).toEqual({ x: 42 }); }); it('normalizes variables even with missing fields', () => { const input = { x: undefined }; const operation = getMainOperation( formatDocument(gql` query ($x: Int!) { field } `) ); const normalized = normalizeVariables(operation, input); expect(normalized).toEqual({}); }); it('skips normalizing for queries without variables', () => { const operation = getMainOperation( formatDocument(gql` query { field } `) ); (operation as any).variableDefinitions = undefined; const normalized = normalizeVariables(operation, {}); expect(normalized).toEqual({}); }); it('preserves missing variables', () => { const operation = getMainOperation( formatDocument(gql` query { field } `) ); (operation as any).variableDefinitions = undefined; const normalized = normalizeVariables(operation, { test: true }); expect(normalized).toEqual({ test: true }); }); }); describe('filterVariables', () => { it('returns undefined when no variables are defined', () => { const operation = getMainOperation( formatDocument(gql` query { field } `) ); const vars = filterVariables(operation, { test: true }); expect(vars).toBe(undefined); }); it('filters out missing vars', () => { const input = { x: true, y: false }; const operation = getMainOperation( formatDocument(gql` query ($x: Int!) { field } `) ); const vars = filterVariables(operation, input); expect(vars).toEqual({ x: true }); }); it('ignores defaults', () => { const input = { x: undefined }; const operation = getMainOperation( formatDocument(gql` query ($x: Int! = 42) { field } `) ); const vars = filterVariables(operation, input); expect(vars).toEqual({ x: undefined }); }); }); ================================================ FILE: exchanges/graphcache/src/ast/variables.ts ================================================ import type { FieldNode, DirectiveNode, OperationDefinitionNode, } from '@0no-co/graphql.web'; import { valueFromASTUntyped } from '@0no-co/graphql.web'; import { getName } from './node'; import type { Variables } from '../types'; /** Evaluates a fields arguments taking vars into account */ export const getFieldArguments = ( node: FieldNode | DirectiveNode, vars: Variables ): null | Variables => { let args: null | Variables = null; if (node.arguments) { for (let i = 0, l = node.arguments.length; i < l; i++) { const arg = node.arguments[i]; const value = valueFromASTUntyped(arg.value, vars); if (value !== undefined && value !== null) { if (!args) args = {}; args[getName(arg)] = value as any; } } } return args; }; /** Returns a filtered form of variables with values missing that the query doesn't require */ export const filterVariables = ( node: OperationDefinitionNode, input: void | object ) => { if (!input || !node.variableDefinitions) { return undefined; } const vars = {}; for (let i = 0, l = node.variableDefinitions.length; i < l; i++) { const name = getName(node.variableDefinitions[i].variable); vars[name] = input[name]; } return vars; }; /** Returns a normalized form of variables with defaulted values */ export const normalizeVariables = ( node: OperationDefinitionNode, input: void | Record ): Variables => { const vars = {}; if (!input) return vars; if (node.variableDefinitions) { for (let i = 0, l = node.variableDefinitions.length; i < l; i++) { const def = node.variableDefinitions[i]; const name = getName(def.variable); vars[name] = input[name] === undefined && def.defaultValue ? valueFromASTUntyped(def.defaultValue, input) : input[name]; } } for (const key in input) { if (!(key in vars)) vars[key] = input[key]; } return vars; }; ================================================ FILE: exchanges/graphcache/src/cacheExchange-types.test.ts ================================================ import { describe, it } from 'vitest'; import { cacheExchange, Resolver as GraphCacheResolver, UpdateResolver as GraphCacheUpdateResolver, OptimisticMutationResolver as GraphCacheOptimisticMutationResolver, } from './index'; type Maybe = T | null; type Scalars = { ID: string; String: string; Boolean: boolean; Int: number; Float: number; }; type Author = { __typename?: 'Author'; id?: Maybe; name?: Maybe; friends?: Maybe>>; friendsPaginated?: Maybe>>; }; type MutationToggleTodoArgs = { id: Scalars['ID']; }; type Query = { __typename?: 'Query'; todos?: Maybe>>; }; type Todo = { __typename?: 'Todo'; id?: Maybe; text?: Maybe; complete?: Maybe; author?: Maybe; }; type WithTypename = { [K in Exclude]?: T[K]; } & { __typename: NonNullable }; type GraphCacheKeysConfig = { Todo?: (data: WithTypename) => null | string; }; type GraphCacheResolvers = { Query?: { todos?: GraphCacheResolver< WithTypename, Record, Array | string> >; }; Todo?: { id?: GraphCacheResolver< WithTypename, Record, Scalars['ID'] | string >; text?: GraphCacheResolver< WithTypename, Record, Scalars['String'] | string >; complete?: GraphCacheResolver< WithTypename, Record, Scalars['Boolean'] | string >; author?: GraphCacheResolver< WithTypename, Record, WithTypename | string >; }; }; type GraphCacheOptimisticUpdaters = { toggleTodo?: GraphCacheOptimisticMutationResolver< MutationToggleTodoArgs, WithTypename >; }; type GraphCacheUpdaters = { Mutation?: { toggleTodo?: GraphCacheUpdateResolver< { toggleTodo: WithTypename }, MutationToggleTodoArgs >; }; Subscription?: {}; }; type GraphCacheConfig = { updates?: GraphCacheUpdaters; keys?: GraphCacheKeysConfig; optimistic?: GraphCacheOptimisticUpdaters; resolvers?: GraphCacheResolvers; }; describe('typings', function () { it('should work with a generic', function () { cacheExchange({ keys: { Todo: data => data.id || null, }, updates: { Mutation: { toggleTodo: result => { result.toggleTodo.author?.name; }, }, }, resolvers: { Todo: { id: parent => parent.id + '_' + parent.complete, }, }, optimistic: { toggleTodo: (args, cache) => { return { __typename: 'Todo', complete: !cache.resolve( { __typename: 'Todo', id: args.id }, 'complete' ), id: args.id, }; }, }, }); }); }); ================================================ FILE: exchanges/graphcache/src/cacheExchange.test.ts ================================================ import { gql, createClient, ExchangeIO, Operation, OperationResult, CombinedError, } from '@urql/core'; import { print, stripIgnoredCharacters } from 'graphql'; import { vi, expect, it, describe } from 'vitest'; import { Source, pipe, share, map, merge, mergeMap, filter, fromValue, makeSubject, tap, publish, delay, } from 'wonka'; import { minifyIntrospectionQuery } from '@urql/introspection'; import { queryResponse } from '../../../packages/core/src/test-utils'; import { cacheExchange } from './cacheExchange'; const queryOne = gql` { author { id name } unrelated { id } } `; const queryOneData = { __typename: 'Query', author: { __typename: 'Author', id: '123', name: 'Author', }, unrelated: { __typename: 'Unrelated', id: 'unrelated', }, }; const dispatchDebug = vi.fn(); describe('data dependencies', () => { it('writes queries to the cache', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const op = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const expected = { __typename: 'Query', author: { id: '123', name: 'Author', __typename: 'Author', }, unrelated: { id: 'unrelated', __typename: 'Unrelated', }, }; const response = vi.fn((forwardOp: Operation): OperationResult => { expect(forwardOp.key).toBe(op.key); return { ...queryResponse, operation: forwardOp, data: expected }; }); const { source: ops$, next } = makeSubject(); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(op); next(op); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(2); expect(expected).toMatchObject(result.mock.calls[0][0].data); expect(result.mock.calls[1][0]).toHaveProperty( 'operation.context.meta.cacheOutcome', 'hit' ); expect(expected).toMatchObject(result.mock.calls[1][0].data); expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data); }); it('logs cache misses', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const op = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const expected = { __typename: 'Query', author: { id: '123', name: 'Author', __typename: 'Author', }, unrelated: { id: 'unrelated', __typename: 'Unrelated', }, }; const response = vi.fn((forwardOp: Operation): OperationResult => { expect(forwardOp.key).toBe(op.key); return { ...queryResponse, operation: forwardOp, data: expected }; }); const { source: ops$, next } = makeSubject(); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); const messages: string[] = []; pipe( cacheExchange({ logger(severity, message) { if (severity === 'debug') { messages.push(message); } }, })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(op); next(op); next({ ...op, query: gql` query ($id: ID!) { author(id: $id) { id name } } `, variables: { id: '123' }, }); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(2); expect(expected).toMatchObject(result.mock.calls[0][0].data); expect(result.mock.calls[1][0]).toHaveProperty( 'operation.context.meta.cacheOutcome', 'hit' ); expect(expected).toMatchObject(result.mock.calls[1][0].data); expect(result.mock.calls[1][0].data).toBe(result.mock.calls[0][0].data); expect(messages).toEqual([ 'No value for field "author" on entity "Query"', 'No value for field "author" with args {"id":"123"} on entity "Query"', ]); }); it('respects cache-only operations', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const op = client.createRequestOperation( 'query', { key: 1, query: queryOne, variables: undefined, }, { requestPolicy: 'cache-only', } ); const response = vi.fn((forwardOp: Operation): OperationResult => { expect(forwardOp.key).toBe(op.key); return { ...queryResponse, operation: forwardOp, data: queryOneData }; }); const { source: ops$, next } = makeSubject(); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(op); expect(response).toHaveBeenCalledTimes(0); expect(result).toHaveBeenCalledTimes(1); expect(result.mock.calls[0][0]).toHaveProperty( 'operation.context.meta.cacheOutcome', 'miss' ); expect(result.mock.calls[0][0].data).toBe(null); }); it('updates related queries when their data changes', () => { const queryMultiple = gql` { authors { id name } } `; const queryMultipleData = { __typename: 'Query', authors: [ { __typename: 'Author', id: '123', name: 'New Author Name', }, ], }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const opMultiple = client.createRequestOperation('query', { key: 2, query: queryMultiple, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: queryOneData }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opMultiple, data: queryMultipleData, }; } return undefined as any; }); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); const result = vi.fn(); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(opOne); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); next(opMultiple); expect(response).toHaveBeenCalledTimes(2); expect(reexec.mock.calls[0][0]).toHaveProperty('key', opOne.key); expect(result).toHaveBeenCalledTimes(3); // test for reference reuse const firstDataOne = result.mock.calls[0][0].data; const firstDataTwo = result.mock.calls[1][0].data; expect(firstDataOne).not.toBe(firstDataTwo); expect(firstDataOne.author).not.toBe(firstDataTwo.author); expect(firstDataOne.unrelated).toBe(firstDataTwo.unrelated); }); it('updates related queries when a mutation update touches query data', () => { vi.useFakeTimers(); const balanceFragment = gql` fragment BalanceFragment on Author { id balance { amount } } `; const queryById = gql` query ($id: ID!) { author(id: $id) { id name ...BalanceFragment } } ${balanceFragment} `; const queryByIdDataA = { __typename: 'Query', author: { __typename: 'Author', id: '1', name: 'Author 1', balance: { __typename: 'Balance', amount: 100, }, }, }; const queryByIdDataB = { __typename: 'Query', author: { __typename: 'Author', id: '2', name: 'Author 2', balance: { __typename: 'Balance', amount: 200, }, }, }; const mutation = gql` mutation ($userId: ID!, $amount: Int!) { updateBalance(userId: $userId, amount: $amount) { userId balance { amount } } } `; const mutationData = { __typename: 'Mutation', updateBalance: { __typename: 'UpdateBalanceResult', userId: '1', balance: { __typename: 'Balance', amount: 1000, }, }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation('query', { key: 1, query: queryById, variables: { id: 1 }, }); const opTwo = client.createRequestOperation('query', { key: 2, query: queryById, variables: { id: 2 }, }); const opMutation = client.createRequestOperation('mutation', { key: 3, query: mutation, variables: { userId: '1', amount: 1000 }, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: queryByIdDataA }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opTwo, data: queryByIdDataB }; } else if (forwardOp.key === 3) { return { ...queryResponse, operation: opMutation, data: mutationData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); const updates = { Mutation: { updateBalance: vi.fn((result, _args, cache) => { const { updateBalance: { userId, balance }, } = result; cache.writeFragment(balanceFragment, { id: userId, balance }); }), }, }; const keys = { Balance: () => null, }; pipe( cacheExchange({ updates, keys })({ forward, client, dispatchDebug })( ops$ ), tap(result), publish ); next(opTwo); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(2); next(opMutation); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(3); expect(updates.Mutation.updateBalance).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(1); expect(reexec.mock.calls[0][0].key).toBe(1); expect(result.mock.calls[2][0]).toHaveProperty( 'data.author.balance.amount', 1000 ); }); it('does not notify related queries when a mutation update does not change the data', () => { vi.useFakeTimers(); const balanceFragment = gql` fragment BalanceFragment on Author { id balance { amount } } `; const queryById = gql` query ($id: ID!) { author(id: $id) { id name ...BalanceFragment } } ${balanceFragment} `; const queryByIdDataA = { __typename: 'Query', author: { __typename: 'Author', id: '1', name: 'Author 1', balance: { __typename: 'Balance', amount: 100, }, }, }; const queryByIdDataB = { __typename: 'Query', author: { __typename: 'Author', id: '2', name: 'Author 2', balance: { __typename: 'Balance', amount: 200, }, }, }; const mutation = gql` mutation ($userId: ID!, $amount: Int!) { updateBalance(userId: $userId, amount: $amount) { userId balance { amount } } } `; const mutationData = { __typename: 'Mutation', updateBalance: { __typename: 'UpdateBalanceResult', userId: '1', balance: { __typename: 'Balance', amount: 100, }, }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation('query', { key: 1, query: queryById, variables: { id: 1 }, }); const opTwo = client.createRequestOperation('query', { key: 2, query: queryById, variables: { id: 2 }, }); const opMutation = client.createRequestOperation('mutation', { key: 3, query: mutation, variables: { userId: '1', amount: 1000 }, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: queryByIdDataA }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opTwo, data: queryByIdDataB }; } else if (forwardOp.key === 3) { return { ...queryResponse, operation: opMutation, data: mutationData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); const updates = { Mutation: { updateBalance: vi.fn((result, _args, cache) => { const { updateBalance: { userId, balance }, } = result; cache.writeFragment(balanceFragment, { id: userId, balance }); }), }, }; const keys = { Balance: () => null, }; pipe( cacheExchange({ updates, keys })({ forward, client, dispatchDebug })( ops$ ), tap(result), publish ); next(opTwo); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(2); next(opMutation); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(3); expect(updates.Mutation.updateBalance).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(0); }); it('does nothing when no related queries have changed', () => { const queryUnrelated = gql` { user { id name } } `; const queryUnrelatedData = { __typename: 'Query', user: { __typename: 'User', id: 'me', name: 'Me', }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const opUnrelated = client.createRequestOperation('query', { key: 2, query: queryUnrelated, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: queryOneData }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opUnrelated, data: queryUnrelatedData, }; } return undefined as any; }); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); const result = vi.fn(); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(opOne); expect(response).toHaveBeenCalledTimes(1); next(opUnrelated); expect(response).toHaveBeenCalledTimes(2); expect(reexec).not.toHaveBeenCalled(); expect(result).toHaveBeenCalledTimes(2); }); it('does not reach updater when mutation has no selectionset in optimistic phase', () => { vi.useFakeTimers(); const mutation = gql` mutation { concealAuthor } `; const mutationData = { __typename: 'Mutation', concealAuthor: true, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); vi.spyOn(client, 'reexecuteOperation').mockImplementation(next); const opMutation = client.createRequestOperation('mutation', { key: 1, query: mutation, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opMutation, data: mutationData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); const updates = { Mutation: { concealAuthor: vi.fn(), }, }; pipe( cacheExchange({ updates })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(opMutation); expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(0); vi.runAllTimers(); expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1); }); it('does reach updater when mutation has no selectionset in optimistic phase with optimistic update', () => { vi.useFakeTimers(); const mutation = gql` mutation { concealAuthor } `; const mutationData = { __typename: 'Mutation', concealAuthor: true, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); vi.spyOn(client, 'reexecuteOperation').mockImplementation(next); const opMutation = client.createRequestOperation('mutation', { key: 1, query: mutation, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opMutation, data: mutationData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); const updates = { Mutation: { concealAuthor: vi.fn(), }, }; const optimistic = { concealAuthor: vi.fn(() => true) as any, }; pipe( cacheExchange({ updates, optimistic })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(opMutation); expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1); expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(1); vi.runAllTimers(); expect(updates.Mutation.concealAuthor).toHaveBeenCalledTimes(2); }); it('marks errored null fields as uncached but delivers them as expected', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const query = gql` { field author { id } } `; const operation = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const queryResult: OperationResult = { ...queryResponse, operation, data: { __typename: 'Query', field: 'test', author: null, }, error: new CombinedError({ graphQLErrors: [ { message: 'Test', path: ['author'], }, ], }), }; const reexecuteOperation = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) return queryResult; return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(operation); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect(reexecuteOperation).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0]).toHaveProperty('data.author', null); }); it('mutation does not change number of reexecute request after a query', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next: nextOp } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(nextOp); const mutation = gql` mutation { updateNode { __typename id } } `; const normalQuery = gql` { __typename item { __typename id } } `; const extendedQuery = gql` { __typename item { __typename extended: id extra @_optional } } `; const mutationOp = client.createRequestOperation('mutation', { key: 0, query: mutation, variables: undefined, }); const normalOp = client.createRequestOperation( 'query', { key: 1, query: normalQuery, variables: undefined, }, { requestPolicy: 'cache-and-network', } ); const extendedOp = client.createRequestOperation( 'query', { key: 2, query: extendedQuery, variables: undefined, }, { requestPolicy: 'cache-only', } ); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 0) { return { operation: mutationOp, data: { __typename: 'Mutation', updateNode: { __typename: 'Node', id: 'id', }, }, stale: false, hasNext: false, }; } else if (forwardOp.key === 1) { return { operation: normalOp, data: { __typename: 'Query', item: { __typename: 'Node', id: 'id', }, }, stale: false, hasNext: false, }; } else if (forwardOp.key === 2) { return { operation: extendedOp, data: { __typename: 'Query', item: { __typename: 'Node', extended: 'id', extra: 'extra', }, }, stale: false, hasNext: false, }; } return undefined as any; }); const forward = (ops$: Source): Source => pipe(ops$, map(response), share); pipe(cacheExchange()({ forward, client, dispatchDebug })(ops$), publish); nextOp(normalOp); expect(reexec).toHaveBeenCalledTimes(0); nextOp(extendedOp); expect(reexec).toHaveBeenCalledTimes(0); // re-execute first operation reexec.mockClear(); nextOp(normalOp); expect(reexec).toHaveBeenCalledTimes(4); nextOp(mutationOp); // re-execute first operation after mutation reexec.mockClear(); nextOp(normalOp); expect(reexec).toHaveBeenCalledTimes(4); }); }); describe('directives', () => { it('returns optional fields as partial', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const query = gql` { todos { id text completed @_optional } } `; const operation = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const queryResult: OperationResult = { ...queryResponse, operation, data: { __typename: 'Query', todos: [ { id: '1', text: 'learn urql', __typename: 'Todo', }, ], }, }; const reexecuteOperation = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) return queryResult; return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(operation); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect(reexecuteOperation).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0].data).toEqual({ todos: [ { completed: null, id: '1', text: 'learn urql', }, ], }); }); it('Does not return partial data for nested selections', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const query = gql` { todo { ... on Todo @_optional { id text author { id name } } } } `; const operation = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const queryResult: OperationResult = { ...queryResponse, operation, data: { __typename: 'Query', todo: { id: '1', text: 'learn urql', __typename: 'Todo', author: { __typename: 'Author', }, }, }, }; const reexecuteOperation = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) return queryResult; return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(operation); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect(reexecuteOperation).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0].data).toEqual(null); }); it('returns partial results when an inline-fragment is marked as optional', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const query = gql` { todos { id text ... @_optional { ... on Todo { completed } } } } `; const operation = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const queryResult: OperationResult = { ...queryResponse, operation, data: { __typename: 'Query', todos: [ { id: '1', text: 'learn urql', __typename: 'Todo', }, ], }, }; const reexecuteOperation = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) return queryResult; return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(operation); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect(reexecuteOperation).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0].data).toEqual({ todos: [ { completed: null, id: '1', text: 'learn urql', }, ], }); }); it('does not return partial results when an inline-fragment is marked as optional with a required child fragment', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const query = gql` { todos { id ... on Todo @_optional { text ... on Todo @_required { completed } } } } `; const operation = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const queryResult: OperationResult = { ...queryResponse, operation, data: { __typename: 'Query', todos: [ { id: '1', text: 'learn urql', __typename: 'Todo', }, ], }, }; const reexecuteOperation = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) return queryResult; return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(operation); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect(reexecuteOperation).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0].data).toEqual(null); }); it('does not return partial results when an inline-fragment is marked as optional with a required field', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const query = gql` { todos { id ... on Todo @_optional { text completed @_required } } } `; const operation = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const queryResult: OperationResult = { ...queryResponse, operation, data: { __typename: 'Query', todos: [ { id: '1', text: 'learn urql', __typename: 'Todo', }, ], }, }; const reexecuteOperation = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) return queryResult; return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(operation); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect(reexecuteOperation).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0].data).toEqual(null); }); it('returns partial results when a fragment-definition is marked as optional', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const query = gql` { todos { id text ...Fields } } fragment Fields on Todo @_optional { completed } `; const operation = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const queryResult: OperationResult = { ...queryResponse, operation, data: { __typename: 'Query', todos: [ { id: '1', text: 'learn urql', __typename: 'Todo', }, ], }, }; const reexecuteOperation = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) return queryResult; return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(operation); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect(reexecuteOperation).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0].data).toEqual(null); }); it('does not return missing required fields', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const query = gql` { todos { id text completed @_required } } `; const operation = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const queryResult: OperationResult = { ...queryResponse, operation, data: { __typename: 'Query', todos: [ { id: '1', text: 'learn urql', __typename: 'Todo', }, ], }, }; const reexecuteOperation = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) return queryResult; return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(operation); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect( stripIgnoredCharacters(print(response.mock.calls[0][0].query)) ).toEqual('{todos{id text completed __typename}}'); expect(reexecuteOperation).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0].data).toEqual(null); }); it('does not return missing fields when nullable fields from a defined schema are marked as required in the query', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const initialQuery = gql` query { latestTodo { id } } `; const query = gql` { latestTodo { id author @_required { id name } } } `; const initialQueryOperation = client.createRequestOperation('query', { key: 1, query: initialQuery, variables: undefined, }); const queryOperation = client.createRequestOperation('query', { key: 2, query, variables: undefined, }); const initialQueryResult: OperationResult = { ...queryResponse, operation: initialQueryOperation, data: { __typename: 'Query', latestTodo: { __typename: 'Todo', id: '1', }, }, }; const queryResult: OperationResult = { ...queryResponse, operation: queryOperation, data: { __typename: 'Query', latestTodo: { __typename: 'Todo', id: '1', author: null, }, }, }; const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return initialQueryResult; } else if (forwardOp.key === 2) { return queryResult; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({ schema: minifyIntrospectionQuery( // eslint-disable-next-line require('./test-utils/simple_schema.json') ), })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(initialQueryOperation); vi.runAllTimers(); next(queryOperation); vi.runAllTimers(); expect(result.mock.calls[0][0].data).toEqual({ latestTodo: { id: '1', }, }); expect(result.mock.calls[1][0].data).toEqual(null); }); }); describe('optimistic updates', () => { it('writes optimistic mutations to the cache', () => { vi.useFakeTimers(); const mutation = gql` mutation { concealAuthor { id name } } `; const optimisticMutationData = { __typename: 'Mutation', concealAuthor: { __typename: 'Author', id: '123', name() { return '[REDACTED OFFLINE]'; }, }, }; const mutationData = { __typename: 'Mutation', concealAuthor: { __typename: 'Author', id: '123', name: '[REDACTED ONLINE]', }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const opMutation = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: queryOneData }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opMutation, data: mutationData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); const optimistic = { concealAuthor: vi.fn(() => optimisticMutationData.concealAuthor) as any, }; pipe( cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); next(opMutation); expect(response).toHaveBeenCalledTimes(1); expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(1); expect(result.mock.calls[1][0]?.data).toMatchObject({ author: { name: '[REDACTED OFFLINE]' }, }); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(2); expect(result).toHaveBeenCalledTimes(4); }); it('batches optimistic mutation result application', () => { vi.useFakeTimers(); const mutation = gql` mutation { concealAuthor { id name } } `; const optimisticMutationData = { __typename: 'Mutation', concealAuthor: { __typename: 'Author', id: '123', name: '[REDACTED OFFLINE]', }, }; const mutationData = { __typename: 'Mutation', concealAuthor: { __typename: 'Author', id: '123', name: '[REDACTED ONLINE]', }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const opMutationOne = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); const opMutationTwo = client.createRequestOperation('mutation', { key: 3, query: mutation, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: queryOneData }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opMutationOne, data: mutationData, }; } else if (forwardOp.key === 3) { return { ...queryResponse, operation: opMutationTwo, data: mutationData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(3), map(response), share); const optimistic = { concealAuthor: vi.fn(() => optimisticMutationData.concealAuthor) as any, }; pipe( cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), filter(x => x.operation.kind === 'mutation'), tap(result), publish ); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(0); next(opMutationOne); vi.advanceTimersByTime(1); next(opMutationTwo); expect(response).toHaveBeenCalledTimes(1); expect(optimistic.concealAuthor).toHaveBeenCalledTimes(2); expect(reexec).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(0); vi.advanceTimersByTime(2); expect(response).toHaveBeenCalledTimes(2); expect(reexec).toHaveBeenCalledTimes(2); expect(result).toHaveBeenCalledTimes(1); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(3); expect(reexec).toHaveBeenCalledTimes(2); expect(result).toHaveBeenCalledTimes(2); }); it('blocks refetches of overlapping queries', () => { vi.useFakeTimers(); const mutation = gql` mutation { concealAuthor { id name } } `; const optimisticMutationData = { __typename: 'Mutation', concealAuthor: { __typename: 'Author', id: '123', name: '[REDACTED OFFLINE]', }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation( 'query', { key: 1, query: queryOne, variables: undefined, }, { requestPolicy: 'cache-and-network', } ); const opMutation = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: queryOneData }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe( ops$, delay(1), filter(x => x.kind !== 'mutation'), map(response), share ); const optimistic = { concealAuthor: vi.fn(() => optimisticMutationData.concealAuthor) as any, }; pipe( cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); next(opMutation); expect(response).toHaveBeenCalledTimes(1); expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(1); expect(reexec.mock.calls[0][0]).toHaveProperty( 'context.requestPolicy', 'cache-first' ); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); next(opOne); expect(response).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(1); }); it('correctly clears on error', () => { vi.useFakeTimers(); const authorsQuery = gql` query { authors { id name } } `; const authorsQueryData = { __typename: 'Query', authors: [ { __typename: 'Author', id: '1', name: 'Author', }, ], }; const mutation = gql` mutation { addAuthor { id name } } `; const optimisticMutationData = { __typename: 'Mutation', addAuthor: { __typename: 'Author', id: '123', name: '[REDACTED OFFLINE]', }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation('query', { key: 1, query: authorsQuery, variables: undefined, }); const opMutation = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: authorsQueryData }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opMutation, error: 'error' as any, data: { __typename: 'Mutation', addAuthor: null }, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); const optimistic = { addAuthor: vi.fn(() => optimisticMutationData.addAuthor) as any, }; const updates = { Mutation: { addAuthor: vi.fn((data, _, cache) => { cache.updateQuery({ query: authorsQuery }, (prevData: any) => ({ ...prevData, authors: [...prevData.authors, data.addAuthor], })); }), }, }; pipe( cacheExchange({ optimistic, updates })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); next(opMutation); expect(response).toHaveBeenCalledTimes(1); expect(optimistic.addAuthor).toHaveBeenCalledTimes(1); expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(1); vi.runAllTimers(); expect(updates.Mutation.addAuthor).toHaveBeenCalledTimes(2); expect(response).toHaveBeenCalledTimes(2); expect(result).toHaveBeenCalledTimes(4); expect(reexec).toHaveBeenCalledTimes(2); next(opOne); vi.runAllTimers(); expect(result).toHaveBeenCalledTimes(5); }); it('does not block subsequent query operations', () => { vi.useFakeTimers(); const authorsQuery = gql` query { authors { id name } } `; const authorsQueryData = { __typename: 'Query', authors: [ { __typename: 'Author', id: '123', name: 'Author', }, ], }; const mutation = gql` mutation { deleteAuthor { id name } } `; const optimisticMutationData = { __typename: 'Mutation', deleteAuthor: { __typename: 'Author', id: '123', name: '[REDACTED OFFLINE]', }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation('query', { key: 1, query: authorsQuery, variables: undefined, }); const opMutation = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: authorsQueryData }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opMutation, data: { __typename: 'Mutation', deleteAuthor: optimisticMutationData.deleteAuthor, }, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); const optimistic = { deleteAuthor: vi.fn(() => optimisticMutationData.deleteAuthor) as any, }; const updates = { Mutation: { deleteAuthor: vi.fn((_data, _, cache) => { cache.invalidate({ __typename: 'Author', id: optimisticMutationData.deleteAuthor.id, }); }), }, }; pipe( cacheExchange({ optimistic, updates })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); next(opMutation); expect(response).toHaveBeenCalledTimes(1); expect(optimistic.deleteAuthor).toHaveBeenCalledTimes(1); expect(updates.Mutation.deleteAuthor).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); vi.runAllTimers(); expect(updates.Mutation.deleteAuthor).toHaveBeenCalledTimes(2); expect(response).toHaveBeenCalledTimes(2); expect(result).toHaveBeenCalledTimes(2); expect(reexec).toHaveBeenCalledTimes(2); expect(reexec.mock.calls[1][0]).toMatchObject(opOne); next(opOne); vi.runAllTimers(); expect(result).toHaveBeenCalledTimes(3); }); }); describe('mutation updates', () => { it('invalidates the type when the entity is not present in the cache', () => { vi.useFakeTimers(); const authorsQuery = gql` query { authors { id name } } `; const authorsQueryData = { __typename: 'Query', authors: [ { __typename: 'Author', id: '1', name: 'Author', }, ], }; const mutation = gql` mutation { addAuthor { id name } } `; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(next); const opOne = client.createRequestOperation('query', { key: 1, query: authorsQuery, variables: undefined, }); const opMutation = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: authorsQueryData }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opMutation, data: { __typename: 'Mutation', addAuthor: { id: '2', name: 'Author 2', __typename: 'Author' }, }, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); pipe( cacheExchange()({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); next(opMutation); expect(response).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(0); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(2); expect(result).toHaveBeenCalledTimes(2); expect(reexec).toHaveBeenCalledTimes(1); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(3); expect(result).toHaveBeenCalledTimes(3); expect(result.mock.calls[1][0].data).toEqual({ addAuthor: { id: '2', name: 'Author 2', }, }); }); }); describe('extra variables', () => { it('allows extra variables to be applied to updates', () => { vi.useFakeTimers(); const mutation = gql` mutation TestMutation($test: Boolean) { test(test: $test) { id } } `; const mutationData = { __typename: 'Mutation', test: { __typename: 'Author', id: '123', }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const opQuery = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const opMutation = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: { test: true, extra: 'extra', }, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: forwardOp, data: queryOneData }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: forwardOp, data: mutationData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(3), map(response), share); const optimistic = { test: vi.fn() as any, }; const updates = { Mutation: { test: vi.fn() as any, }, }; pipe( cacheExchange({ optimistic, updates })({ forward, client, dispatchDebug, })(ops$), filter(x => x.operation.kind === 'mutation'), tap(result), publish ); next(opQuery); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(0); next(opMutation); vi.advanceTimersByTime(1); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(0); expect(optimistic.test).toHaveBeenCalledTimes(1); expect(optimistic.test.mock.calls[0][2].variables).toEqual({ test: true, extra: 'extra', }); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(2); expect(result).toHaveBeenCalledTimes(1); expect(updates.Mutation.test).toHaveBeenCalledTimes(2); expect(updates.Mutation.test.mock.calls[1][3].variables).toEqual({ test: true, extra: 'extra', }); }); }); describe('custom resolvers', () => { it('follows resolvers on initial write', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const opOne = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: queryOneData }; } return undefined as any; }); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); const result = vi.fn(); const fakeResolver = vi.fn(); pipe( cacheExchange({ resolvers: { Author: { name: () => { fakeResolver(); return 'newName'; }, }, }, })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(opOne); expect(response).toHaveBeenCalledTimes(1); expect(fakeResolver).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect(result.mock.calls[0][0].data).toMatchObject({ author: { id: '123', name: 'newName', }, }); }); it('follows resolvers for mutations', () => { vi.useFakeTimers(); const mutation = gql` mutation { concealAuthor { id name __typename } } `; const mutationData = { __typename: 'Mutation', concealAuthor: { __typename: 'Author', id: '123', name: '[REDACTED ONLINE]', }, }; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const opOne = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const opMutation = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: opOne, data: queryOneData }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: opMutation, data: mutationData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); const fakeResolver = vi.fn(); pipe( cacheExchange({ resolvers: { Author: { name: () => { fakeResolver(); return 'newName'; }, }, }, })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(opOne); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); next(opMutation); expect(response).toHaveBeenCalledTimes(1); expect(fakeResolver).toHaveBeenCalledTimes(1); vi.runAllTimers(); expect(result.mock.calls[1][0].data).toEqual({ concealAuthor: { __typename: 'Author', id: '123', name: 'newName', }, }); }); it('follows nested resolvers for mutations', () => { vi.useFakeTimers(); const mutation = gql` mutation { concealAuthors { id name book { id title __typename } __typename } } `; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const query = gql` query { authors { id name book { id title __typename } __typename } } `; const queryOperation = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const mutationOperation = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); const mutationData = { __typename: 'Mutation', concealAuthors: [ { __typename: 'Author', id: '123', book: null, name: '[REDACTED ONLINE]', }, { __typename: 'Author', id: '456', name: 'Formidable', book: { id: '1', title: 'AwesomeGQL', __typename: 'Book', }, }, ], }; const queryData = { __typename: 'Query', authors: [ { __typename: 'Author', id: '123', name: '[REDACTED ONLINE]', book: null, }, { __typename: 'Author', id: '456', name: 'Formidable', book: { id: '1', title: 'AwesomeGQL', __typename: 'Book', }, }, ], }; const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: queryOperation, data: queryData, }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: mutationOperation, data: mutationData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); const fakeResolver = vi.fn(); const called: any[] = []; pipe( cacheExchange({ resolvers: { Query: { // TS-check author: (_parent, args) => ({ __typename: 'Author', id: args.id }), }, Author: { name: parent => { called.push(parent.name); fakeResolver(); return 'Secret Author'; }, }, Book: { title: parent => { called.push(parent.title); fakeResolver(); return 'Secret Book'; }, }, }, })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(queryOperation); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); expect(fakeResolver).toHaveBeenCalledTimes(3); next(mutationOperation); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(2); expect(fakeResolver).toHaveBeenCalledTimes(6); expect(result.mock.calls[1][0].data).toEqual({ concealAuthors: [ { __typename: 'Author', id: '123', book: null, name: 'Secret Author', }, { __typename: 'Author', id: '456', name: 'Secret Author', book: { id: '1', title: 'Secret Book', __typename: 'Book', }, }, ], }); expect(called).toEqual([ // Query '[REDACTED ONLINE]', 'Formidable', 'AwesomeGQL', // Mutation '[REDACTED ONLINE]', 'Formidable', 'AwesomeGQL', ]); }); }); describe('schema awareness', () => { it('reexecutes query and returns data on partial result', () => { vi.useFakeTimers(); const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') // Empty mock to avoid going in an endless loop, since we would again return // partial data. .mockImplementation(() => undefined); const initialQuery = gql` query { todos { id text __typename } } `; const query = gql` query { todos { id text complete author { id name __typename } __typename } } `; const initialQueryOperation = client.createRequestOperation('query', { key: 1, query: initialQuery, variables: undefined, }); const queryOperation = client.createRequestOperation('query', { key: 2, query, variables: undefined, }); const queryData = { __typename: 'Query', todos: [ { __typename: 'Todo', id: '123', text: 'Learn', }, { __typename: 'Todo', id: '456', text: 'Teach', }, ], }; const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: initialQueryOperation, data: queryData, }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: queryOperation, data: queryData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); pipe( cacheExchange({ schema: minifyIntrospectionQuery( // eslint-disable-next-line require('./test-utils/simple_schema.json') ), })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(initialQueryOperation); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0].data).toMatchObject({ todos: [ { __typename: 'Todo', id: '123', text: 'Learn', }, { __typename: 'Todo', id: '456', text: 'Teach', }, ], }); next(queryOperation); vi.runAllTimers(); expect(result).toHaveBeenCalledTimes(2); expect(reexec).toHaveBeenCalledTimes(1); expect(result.mock.calls[1][0].stale).toBe(true); expect(result.mock.calls[1][0].data).toEqual({ todos: [ { __typename: 'Todo', author: null, complete: null, id: '123', text: 'Learn', }, { __typename: 'Todo', author: null, complete: null, id: '456', text: 'Teach', }, ], }); expect(result.mock.calls[1][0]).toHaveProperty( 'operation.context.meta.cacheOutcome', 'partial' ); }); it('reexecutes query and returns data on partial results for nullable lists', () => { vi.useFakeTimers(); const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') // Empty mock to avoid going in an endless loop, since we would again return // partial data. .mockImplementation(() => undefined); const initialQuery = gql` query { todos { id __typename } } `; const query = gql` query { todos { id text __typename } } `; const initialQueryOperation = client.createRequestOperation('query', { key: 1, query: initialQuery, variables: undefined, }); const queryOperation = client.createRequestOperation('query', { key: 2, query, variables: undefined, }); const queryData = { __typename: 'Query', todos: [ { __typename: 'Todo', id: '123', }, { __typename: 'Todo', id: '456', }, ], }; const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) { return { ...queryResponse, operation: initialQueryOperation, data: queryData, }; } else if (forwardOp.key === 2) { return { ...queryResponse, operation: queryOperation, data: queryData, }; } return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, delay(1), map(response), share); pipe( cacheExchange({ schema: minifyIntrospectionQuery( // eslint-disable-next-line require('./test-utils/simple_schema.json') ), })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(initialQueryOperation); vi.runAllTimers(); expect(response).toHaveBeenCalledTimes(1); expect(reexec).toHaveBeenCalledTimes(0); expect(result.mock.calls[0][0].data).toMatchObject({ todos: [ { __typename: 'Todo', id: '123', }, { __typename: 'Todo', id: '456', }, ], }); next(queryOperation); vi.runAllTimers(); expect(result).toHaveBeenCalledTimes(2); expect(reexec).toHaveBeenCalledTimes(1); expect(result.mock.calls[1][0].stale).toBe(true); expect(result.mock.calls[1][0].data).toEqual({ todos: [null, null], }); expect(result.mock.calls[1][0]).toHaveProperty( 'operation.context.meta.cacheOutcome', 'partial' ); }); }); describe('looping protection', () => { it('applies stale to blocked looping queries', () => { let normalData: OperationResult | undefined; let extendedData: OperationResult | undefined; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next: nextOp } = makeSubject(); const { source: res$, next: nextRes } = makeSubject(); vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); const normalQuery = gql` { __typename item { __typename id } } `; const extendedQuery = gql` { __typename item { __typename extended: id extra @_optional } } `; const forward = (ops$: Source): Source => share( merge([ pipe( ops$, filter(() => false) ) as any, res$, ]) ); pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(result => { if (result.operation.kind === 'query') { if (result.operation.key === 1) { normalData = result; } else if (result.operation.key === 2) { extendedData = result; } } }), publish ); const normalOp = client.createRequestOperation( 'query', { key: 1, query: normalQuery, variables: undefined, }, { requestPolicy: 'cache-first', } ); const extendedOp = client.createRequestOperation( 'query', { key: 2, query: extendedQuery, variables: undefined, }, { requestPolicy: 'cache-first', } ); nextOp(normalOp); nextRes({ operation: normalOp, data: { __typename: 'Query', item: { __typename: 'Node', id: 'id', }, }, stale: false, hasNext: false, }); expect(normalData).toMatchObject({ stale: false }); expect(client.reexecuteOperation).toHaveBeenCalledTimes(0); nextOp(extendedOp); expect(extendedData).toMatchObject({ stale: true }); expect(client.reexecuteOperation).toHaveBeenCalledTimes(1); // Out of band re-execute first operation nextOp(normalOp); nextRes({ ...queryResponse, operation: normalOp, data: { __typename: 'Query', item: { __typename: 'Node', id: 'id', }, }, }); expect(normalData).toMatchObject({ stale: false }); expect(extendedData).toMatchObject({ stale: true }); expect(client.reexecuteOperation).toHaveBeenCalledTimes(3); nextOp(extendedOp); expect(normalData).toMatchObject({ stale: false }); expect(extendedData).toMatchObject({ stale: true }); expect(client.reexecuteOperation).toHaveBeenCalledTimes(3); nextRes({ ...queryResponse, operation: extendedOp, data: { __typename: 'Query', item: { __typename: 'Node', extended: 'id', extra: 'extra', }, }, }); expect(extendedData).toMatchObject({ stale: false }); expect(client.reexecuteOperation).toHaveBeenCalledTimes(4); }); }); describe('commutativity', () => { it('applies results that come in out-of-order commutatively and consistently', () => { vi.useFakeTimers(); let data: any; const client = createClient({ url: 'http://0.0.0.0', requestPolicy: 'cache-and-network', exchanges: [], }); const { source: ops$, next: next } = makeSubject(); const query = gql` { index } `; const result = (operation: Operation): Source => pipe( fromValue({ ...queryResponse, operation, data: { __typename: 'Query', index: operation.key, }, }), delay(operation.key === 2 ? 5 : operation.key * 10) ); const output = vi.fn(result => { data = result.data; }); const forward = (ops$: Source): Source => pipe( ops$, filter(op => op.kind !== 'teardown'), mergeMap(result) ); pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(output), publish ); next( client.createRequestOperation('query', { key: 1, query, variables: undefined, }) ); next( client.createRequestOperation('query', { key: 2, query, variables: undefined, }) ); // This shouldn't have any effect: next( client.createRequestOperation('teardown', { key: 2, query, variables: undefined, }) ); next( client.createRequestOperation('query', { key: 3, query, variables: undefined, }) ); vi.advanceTimersByTime(5); expect(output).toHaveBeenCalledTimes(1); expect(data.index).toBe(2); vi.advanceTimersByTime(10); expect(output).toHaveBeenCalledTimes(2); expect(data.index).toBe(2); vi.advanceTimersByTime(30); expect(output).toHaveBeenCalledTimes(3); expect(data.index).toBe(3); }); it('applies optimistic updates on top of commutative queries as query result comes in', () => { let data: any; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next: nextOp } = makeSubject(); const { source: res$, next: nextRes } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(nextOp); const query = gql` { node { id name } } `; const mutation = gql` mutation { node { id name } } `; const forward = (ops$: Source): Source => share( merge([ pipe( ops$, filter(() => false) ) as any, res$, ]) ); const optimistic = { node: () => ({ __typename: 'Node', id: 'node', name: 'optimistic', }), }; pipe( cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), tap(result => { if (result.operation.kind === 'query') { data = result.data; } }), publish ); const queryOpA = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const mutationOp = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); expect(data).toBe(undefined); nextOp(queryOpA); nextRes({ ...queryResponse, operation: queryOpA, data: { __typename: 'Query', node: { __typename: 'Node', id: 'node', name: 'query a', }, }, }); expect(data).toHaveProperty('node.name', 'query a'); nextOp(mutationOp); expect(reexec).toHaveBeenCalledTimes(1); expect(data).toHaveProperty('node.name', 'optimistic'); }); it('applies mutation results on top of commutative queries', () => { let data: any; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next: nextOp } = makeSubject(); const { source: res$, next: nextRes } = makeSubject(); const reexec = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(nextOp); const query = gql` { node { id name } } `; const mutation = gql` mutation { node { id name } } `; const forward = (ops$: Source): Source => share( merge([ pipe( ops$, filter(() => false) ) as any, res$, ]) ); pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(result => { if (result.operation.kind === 'query') { data = result.data; } }), publish ); const queryOpA = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const mutationOp = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); const queryOpB = client.createRequestOperation('query', { key: 3, query, variables: undefined, }); expect(data).toBe(undefined); nextOp(queryOpA); nextOp(mutationOp); nextOp(queryOpB); nextRes({ ...queryResponse, operation: queryOpA, data: { __typename: 'Query', node: { __typename: 'Node', id: 'node', name: 'query a', }, }, }); expect(data).toHaveProperty('node.name', 'query a'); nextRes({ ...queryResponse, operation: mutationOp, data: { __typename: 'Mutation', node: { __typename: 'Node', id: 'node', name: 'mutation', }, }, }); expect(reexec).toHaveBeenCalledTimes(3); expect(data).toHaveProperty('node.name', 'mutation'); nextRes({ ...queryResponse, operation: queryOpB, data: { __typename: 'Query', node: { __typename: 'Node', id: 'node', name: 'query b', }, }, }); expect(reexec).toHaveBeenCalledTimes(4); expect(data).toHaveProperty('node.name', 'mutation'); }); it('applies optimistic updates on top of commutative queries until mutation resolves', () => { let data: any; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next: nextOp } = makeSubject(); const { source: res$, next: nextRes } = makeSubject(); vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); const query = gql` { node { id name } } `; const mutation = gql` mutation { node { id name optimistic } } `; const forward = (ops$: Source): Source => share( merge([ pipe( ops$, filter(() => false) ) as any, res$, ]) ); const optimistic = { node: () => ({ __typename: 'Node', id: 'node', name: 'optimistic', }), }; pipe( cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$), tap(result => { if (result.operation.kind === 'query') { data = result.data; } }), publish ); const queryOp = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const mutationOp = client.createRequestOperation('mutation', { key: 2, query: mutation, variables: undefined, }); expect(data).toBe(undefined); nextOp(queryOp); nextOp(mutationOp); nextRes({ ...queryResponse, operation: queryOp, data: { __typename: 'Query', node: { __typename: 'Node', id: 'node', name: 'query a', }, }, }); expect(data).toHaveProperty('node.name', 'optimistic'); nextRes({ ...queryResponse, operation: mutationOp, data: { __typename: 'Query', node: { __typename: 'Node', id: 'node', name: 'mutation', }, }, }); expect(data).toHaveProperty('node.name', 'mutation'); }); it('allows subscription results to be commutative when necessary', () => { let data: any; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next: nextOp } = makeSubject(); const { source: res$, next: nextRes } = makeSubject(); vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); const query = gql` { node { id name } } `; const subscription = gql` subscription { node { id name } } `; const forward = (ops$: Source): Source => share( merge([ pipe( ops$, filter(() => false) ) as any, res$, ]) ); pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(result => { if (result.operation.kind === 'query') { data = result.data; } }), publish ); const queryOpA = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const subscriptionOp = client.createRequestOperation('subscription', { key: 3, query: subscription, variables: undefined, }); nextOp(queryOpA); // Force commutative layers to be created: nextOp( client.createRequestOperation('query', { key: 2, query, variables: undefined, }) ); nextOp(subscriptionOp); nextRes({ ...queryResponse, operation: queryOpA, data: { __typename: 'Query', node: { __typename: 'Node', id: 'node', name: 'query a', }, }, }); nextRes({ ...queryResponse, operation: subscriptionOp, data: { node: { __typename: 'Node', id: 'node', name: 'subscription', }, }, }); expect(data).toHaveProperty('node.name', 'subscription'); }); it('allows subscription results to be commutative above mutations', () => { let data: any; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next: nextOp } = makeSubject(); const { source: res$, next: nextRes } = makeSubject(); vi.spyOn(client, 'reexecuteOperation').mockImplementation(nextOp); const query = gql` { node { id name } } `; const subscription = gql` subscription { node { id name } } `; const mutation = gql` mutation { node { id name } } `; const forward = (ops$: Source): Source => share( merge([ pipe( ops$, filter(() => false) ) as any, res$, ]) ); pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(result => { if (result.operation.kind === 'query') { data = result.data; } }), publish ); const queryOpA = client.createRequestOperation('query', { key: 1, query, variables: undefined, }); const subscriptionOp = client.createRequestOperation('subscription', { key: 2, query: subscription, variables: undefined, }); const mutationOp = client.createRequestOperation('mutation', { key: 3, query: mutation, variables: undefined, }); nextOp(queryOpA); // Force commutative layers to be created: nextOp( client.createRequestOperation('query', { key: 2, query, variables: undefined, }) ); nextOp(subscriptionOp); nextRes({ ...queryResponse, operation: queryOpA, data: { __typename: 'Query', node: { __typename: 'Node', id: 'node', name: 'query a', }, }, }); nextOp(mutationOp); nextRes({ ...queryResponse, operation: mutationOp, data: { node: { __typename: 'Node', id: 'node', name: 'mutation', }, }, }); nextRes({ ...queryResponse, operation: subscriptionOp, data: { node: { __typename: 'Node', id: 'node', name: 'subscription a', }, }, }); nextRes({ ...queryResponse, operation: subscriptionOp, data: { node: { __typename: 'Node', id: 'node', name: 'subscription b', }, }, }); expect(data).toHaveProperty('node.name', 'subscription b'); }); it('applies deferred results to previous layers', () => { let normalData: OperationResult | undefined; let deferredData: OperationResult | undefined; let combinedData: OperationResult | undefined; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next: nextOp } = makeSubject(); const { source: res$, next: nextRes } = makeSubject(); client.reexecuteOperation = nextOp; const normalQuery = gql` { node { id name } } `; const deferredQuery = gql` { ... @defer { deferred { id name } } } `; const combinedQuery = gql` { node { id name } ... @defer { deferred { id name } } } `; const forward = (operations$: Source): Source => share( merge([ pipe( operations$, filter(() => false) ) as any, res$, ]) ); pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(result => { if (result.operation.kind === 'query') { if (result.operation.key === 1) { deferredData = result; } else if (result.operation.key === 42) { combinedData = result; } else { normalData = result; } } }), publish ); const combinedOp = client.createRequestOperation('query', { key: 42, query: combinedQuery, variables: undefined, }); const deferredOp = client.createRequestOperation('query', { key: 1, query: deferredQuery, variables: undefined, }); const normalOp = client.createRequestOperation('query', { key: 2, query: normalQuery, variables: undefined, }); nextOp(combinedOp); nextOp(deferredOp); nextOp(normalOp); nextRes({ ...queryResponse, operation: deferredOp, data: { __typename: 'Query', }, hasNext: true, }); expect(deferredData).not.toHaveProperty('deferred'); nextRes({ ...queryResponse, operation: normalOp, data: { __typename: 'Query', node: { __typename: 'Node', id: 2, name: 'normal', }, }, }); expect(normalData).toHaveProperty('data.node.id', 2); expect(combinedData).not.toHaveProperty('data.deferred'); expect(combinedData).toHaveProperty('data.node.id', 2); nextRes({ ...queryResponse, operation: deferredOp, data: { __typename: 'Query', deferred: { __typename: 'Node', id: 1, name: 'deferred', }, }, hasNext: true, }); expect(deferredData).toHaveProperty('hasNext', true); expect(deferredData).toHaveProperty('data.deferred.id', 1); expect(combinedData).toHaveProperty('hasNext', false); expect(combinedData).toHaveProperty('data.deferred.id', 1); expect(combinedData).toHaveProperty('data.node.id', 2); }); it('applies deferred logic only to deferred operations', () => { let failingData: OperationResult | undefined; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next: nextOp } = makeSubject(); const { source: res$ } = makeSubject(); const deferredQuery = gql` { ... @defer { deferred { id name } } } `; const failingQuery = gql` { deferred { id name } } `; const forward = (ops$: Source): Source => share( merge([ pipe( ops$, filter(() => false) ) as any, res$, ]) ); pipe( cacheExchange()({ forward, client, dispatchDebug })(ops$), tap(result => { if (result.operation.kind === 'query') { if (result.operation.key === 1) { failingData = result; } } }), publish ); const failingOp = client.createRequestOperation('query', { key: 1, query: failingQuery, variables: undefined, }); const deferredOp = client.createRequestOperation('query', { key: 2, query: deferredQuery, variables: undefined, }); nextOp(deferredOp); nextOp(failingOp); expect(failingData).not.toMatchObject({ hasNext: true }); }); }); describe('abstract types', () => { it('works with two responses giving different concrete types for a union', () => { const query = gql` query ($id: ID!) { field(id: $id) { id union { ... on Type1 { id name __typename } ... on Type2 { id title __typename } } __typename } } `; const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const { source: ops$, next } = makeSubject(); const operation1 = client.createRequestOperation('query', { key: 1, query, variables: { id: '1' }, }); const operation2 = client.createRequestOperation('query', { key: 2, query, variables: { id: '2' }, }); const queryResult1: OperationResult = { ...queryResponse, operation: operation1, data: { __typename: 'Query', field: { id: '1', __typename: 'Todo', union: { id: '1', name: 'test', __typename: 'Type1', }, }, }, }; const queryResult2: OperationResult = { ...queryResponse, operation: operation2, data: { __typename: 'Query', field: { id: '2', __typename: 'Todo', union: { id: '2', title: 'test', __typename: 'Type2', }, }, }, }; vi.spyOn(client, 'reexecuteOperation').mockImplementation(next); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === 1) return queryResult1; if (forwardOp.key === 2) return queryResult2; return undefined as any; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( cacheExchange({})({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(operation1); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); expect(result.mock.calls[0][0].data).toEqual({ field: { __typename: 'Todo', id: '1', union: { __typename: 'Type1', id: '1', name: 'test', }, }, }); next(operation2); expect(response).toHaveBeenCalledTimes(2); expect(result).toHaveBeenCalledTimes(2); expect(result.mock.calls[1][0].data).toEqual({ field: { __typename: 'Todo', id: '2', union: { __typename: 'Type2', id: '2', title: 'test', }, }, }); }); }); ================================================ FILE: exchanges/graphcache/src/cacheExchange.ts ================================================ import type { Exchange, Operation, OperationResult, RequestPolicy, CacheOutcome, } from '@urql/core'; import { formatDocument, makeOperation } from '@urql/core'; import type { Source } from 'wonka'; import { filter, map, merge, pipe, share, fromArray, mergeMap, empty, } from 'wonka'; import { _query } from './operations/query'; import { _write } from './operations/write'; import { addMetadata, toRequestPolicy } from './helpers/operation'; import { filterVariables, getMainOperation } from './ast'; import { Store } from './store/store'; import type { Data, Dependencies, CacheExchangeOpts } from './types'; import { initDataState, clearDataState, noopDataState, hydrateData, reserveLayer, hasLayer, } from './store/data'; interface OperationResultWithMeta extends Partial { operation: Operation; outcome: CacheOutcome; dependencies: Dependencies; hasNext: boolean; } type Operations = Set; type OperationMap = Map; type ResultMap = Map; type OptimisticDependencies = Map; type DependentOperations = Map; /** Exchange factory that creates a normalized cache exchange. * * @param opts - A {@link CacheExchangeOpts} configuration object. * @returns the created normalized cache {@link Exchange}. * * @remarks * Graphcache is a normalized cache, enabled by using the `cacheExchange` * in place of `@urql/core`’s. A normalized GraphQL cache uses typenames * and key fields in the result to share a single copy for each unique * entity across all queries. * * The `cacheExchange` may be passed a {@link CacheExchangeOpts} object * to define custom resolvers, custom updates for mutations, * optimistic updates, or to add custom key fields per type. * * @see {@link https://urql.dev/goto/docs/graphcache} for the full Graphcache docs. */ export const cacheExchange = >(opts?: C): Exchange => ({ forward, client, dispatchDebug }) => { const store = new Store(opts); if (opts && opts.storage) { store.data.hydrating = true; opts.storage.readData().then(entries => { hydrateData(store.data, opts!.storage!, entries); if (opts.storage!.onCacheHydrated) opts.storage!.onCacheHydrated(); }); } const optimisticKeysToDependencies: OptimisticDependencies = new Map(); const mutationResultBuffer: OperationResult[] = []; const operations: OperationMap = new Map(); const results: ResultMap = new Map(); const blockedDependencies: Dependencies = new Set(); const requestedRefetch: Operations = new Set(); const deps: DependentOperations = new Map(); let reexecutingOperations: Operations = new Set(); let dependentOperations: Operations = new Set(); const isBlockedByOptimisticUpdate = ( dependencies: Dependencies ): boolean => { for (const dep of dependencies.values()) if (blockedDependencies.has(dep)) return true; return false; }; const collectPendingOperations = ( pendingOperations: Operations, dependencies: undefined | Dependencies ) => { if (dependencies) { // Collect operations that will be updated due to cache changes for (const dep of dependencies.values()) { const keys = deps.get(dep); if (keys) for (const key of keys.values()) pendingOperations.add(key); } } }; const executePendingOperations = ( operation: Operation, pendingOperations: Operations, isOptimistic: boolean ) => { // Reexecute collected operations and delete them from the mapping for (const key of pendingOperations.values()) { if (key !== operation.key) { const op = operations.get(key); if (op) { // Collect all dependent operations if the reexecuting operation is a query if (operation.kind === 'query') dependentOperations.add(key); let policy: RequestPolicy = 'cache-first'; if (requestedRefetch.has(key)) { requestedRefetch.delete(key); policy = 'cache-and-network'; } client.reexecuteOperation(toRequestPolicy(op, policy)); } } } if (!isOptimistic) { // Upon completion, all dependent operations become reexecuting operations, preventing // them from reexecuting prior operations again, causing infinite loops const _reexecutingOperations = reexecutingOperations; reexecutingOperations = dependentOperations; if (operation.kind === 'query') { reexecutingOperations.add(operation.key); } (dependentOperations = _reexecutingOperations).clear(); } }; // This registers queries with the data layer to ensure commutativity const prepareForwardedOperation = (operation: Operation) => { let optimistic = false; if (operation.kind === 'query') { // Pre-reserve the position of the result layer reserveLayer(store.data, operation.key); operations.set(operation.key, operation); } else if (operation.kind === 'teardown') { // Delete reference to operation if any exists to release it operations.delete(operation.key); results.delete(operation.key); reexecutingOperations.delete(operation.key); // Mark operation layer as done noopDataState(store.data, operation.key); return operation; } else if ( operation.kind === 'mutation' && operation.context.requestPolicy !== 'network-only' ) { operations.set(operation.key, operation); // This executes an optimistic update for mutations and registers it if necessary initDataState('write', store.data, operation.key, true, false); const { dependencies } = _write( store, operation as any, undefined, undefined ); clearDataState(); if (dependencies.size) { // Update blocked optimistic dependencies for (const dep of dependencies.values()) blockedDependencies.add(dep); // Store optimistic dependencies for update optimisticKeysToDependencies.set(operation.key, dependencies); // Update related queries const pendingOperations: Operations = new Set(); collectPendingOperations(pendingOperations, dependencies); executePendingOperations(operation, pendingOperations, true); // Mark operation as optimistic optimistic = true; } } return makeOperation( operation.kind, { key: operation.key, query: formatDocument(operation.query), variables: operation.variables ? filterVariables( getMainOperation(operation.query), operation.variables ) : operation.variables, }, { ...operation.context, optimistic } ); }; // This updates the known dependencies for the passed operation const updateDependencies = (op: Operation, dependencies: Dependencies) => { for (const dep of dependencies.values()) { let depOps = deps.get(dep); if (!depOps) deps.set(dep, (depOps = new Set())); depOps.add(op.key); } }; // Retrieves a query result from cache and adds an `isComplete` hint // This hint indicates whether the result is "complete" or not const operationResultFromCache = ( operation: Operation ): OperationResultWithMeta => { initDataState('read', store.data, undefined, false, false); const result = _query( store, operation, results.get(operation.key), undefined ); clearDataState(); const cacheOutcome: CacheOutcome = result.data ? !result.partial && !result.hasNext ? 'hit' : 'partial' : 'miss'; results.set(operation.key, result.data); operations.set(operation.key, operation); updateDependencies(operation, result.dependencies); return { outcome: cacheOutcome, operation, data: result.data, dependencies: result.dependencies, hasNext: result.hasNext, }; }; // Take any OperationResult and update the cache with it const updateCacheWithResult = ( result: OperationResult, pendingOperations: Operations ): OperationResult => { // Retrieve the original operation to get unfiltered variables const operation = operations.get(result.operation.key) || result.operation; if (operation.kind === 'mutation') { // Collect previous dependencies that have been written for optimistic updates const dependencies = optimisticKeysToDependencies.get(operation.key); collectPendingOperations(pendingOperations, dependencies); optimisticKeysToDependencies.delete(operation.key); } if (operation.kind === 'subscription' || result.hasNext) reserveLayer(store.data, operation.key, true); let queryDependencies: undefined | Dependencies; let data: Data | null = result.data; if (data) { // Write the result to cache and collect all dependencies that need to be // updated initDataState('write', store.data, operation.key, false, false); const writeDependencies = _write( store, operation, data, result.error ).dependencies; clearDataState(); collectPendingOperations(pendingOperations, writeDependencies); const prevData = operation.kind === 'query' ? results.get(operation.key) : null; initDataState( 'read', store.data, operation.key, false, prevData !== data ); const queryResult = _query( store, operation, prevData || data, result.error ); clearDataState(); data = queryResult.data; if (operation.kind === 'query') { // Collect the query's dependencies for future pending operation updates queryDependencies = queryResult.dependencies; collectPendingOperations(pendingOperations, queryDependencies); results.set(operation.key, data); } } else { noopDataState(store.data, operation.key); } // Update this operation's dependencies if it's a query if (queryDependencies) { updateDependencies(result.operation, queryDependencies); } return { operation, data, error: result.error, extensions: result.extensions, hasNext: result.hasNext, stale: result.stale, }; }; return operations$ => { // Filter by operations that are cacheable and attempt to query them from the cache const cacheOps$ = pipe( operations$, filter( op => op.kind === 'query' && op.context.requestPolicy !== 'network-only' ), map(operationResultFromCache), share ); const nonCacheOps$ = pipe( operations$, filter( op => op.kind !== 'query' || op.context.requestPolicy === 'network-only' ) ); // Rebound operations that are incomplete, i.e. couldn't be queried just from the cache const cacheMissOps$ = pipe( cacheOps$, filter( res => res.outcome === 'miss' && res.operation.context.requestPolicy !== 'cache-only' && !isBlockedByOptimisticUpdate(res.dependencies) && !reexecutingOperations.has(res.operation.key) ), map(res => { dispatchDebug({ type: 'cacheMiss', message: 'The result could not be retrieved from the cache', operation: res.operation, }); return addMetadata(res.operation, { cacheOutcome: 'miss' }); }) ); // Resolve OperationResults that the cache was able to assemble completely and trigger // a network request if the current operation's policy is cache-and-network const cacheResult$ = pipe( cacheOps$, filter( res => res.outcome !== 'miss' || res.operation.context.requestPolicy === 'cache-only' ), map((res: OperationResultWithMeta): OperationResult => { const { requestPolicy } = res.operation.context; // We reexecute requests marked as `cache-and-network`, and partial responses, // if we wouldn't cause a request loop const shouldReexecute = requestPolicy !== 'cache-only' && (res.hasNext || requestPolicy === 'cache-and-network' || (requestPolicy === 'cache-first' && res.outcome === 'partial' && !reexecutingOperations.has(res.operation.key))); // Set stale to true anyway, even if the reexecute will be blocked, if the operation // is in progress. We can be reasonably sure of that if a layer has been reserved for it. const stale = requestPolicy !== 'cache-only' && (shouldReexecute || (res.outcome === 'partial' && reexecutingOperations.has(res.operation.key) && hasLayer(store.data, res.operation.key))); const result: OperationResult = { operation: addMetadata(res.operation, { cacheOutcome: res.outcome, }), data: res.data, error: res.error, extensions: res.extensions, stale: stale && !res.hasNext, hasNext: shouldReexecute && res.hasNext, }; if (!shouldReexecute) { /*noop*/ } else if (!isBlockedByOptimisticUpdate(res.dependencies)) { client.reexecuteOperation( toRequestPolicy( operations.get(res.operation.key) || res.operation, 'network-only' ) ); } else if (requestPolicy === 'cache-and-network') { requestedRefetch.add(res.operation.key); } dispatchDebug({ type: 'cacheHit', message: `A requested operation was found and returned from the cache.`, operation: res.operation, data: { value: result, }, }); return result; }) ); // Forward operations that aren't cacheable and rebound operations // Also update the cache with any network results const result$ = pipe( merge([nonCacheOps$, cacheMissOps$]), map(prepareForwardedOperation), forward ); // Results that can immediately be resolved const nonOptimisticResults$ = pipe( result$, filter( result => !optimisticKeysToDependencies.has(result.operation.key) ), map(result => { const pendingOperations: Operations = new Set(); // Update the cache with the incoming API result const cacheResult = updateCacheWithResult(result, pendingOperations); // Execute all dependent queries executePendingOperations(result.operation, pendingOperations, false); return cacheResult; }) ); // Prevent mutations that were previously optimistic from being flushed // immediately and instead clear them out slowly const optimisticMutationCompletion$ = pipe( result$, filter(result => optimisticKeysToDependencies.has(result.operation.key) ), mergeMap((result: OperationResult): Source => { const length = mutationResultBuffer.push(result); if (length < optimisticKeysToDependencies.size) { return empty; } for (let i = 0; i < mutationResultBuffer.length; i++) { reserveLayer(store.data, mutationResultBuffer[i].operation.key); } blockedDependencies.clear(); const results: OperationResult[] = []; const pendingOperations: Operations = new Set(); let bufferedResult: OperationResult | void; while ((bufferedResult = mutationResultBuffer.shift())) results.push( updateCacheWithResult(bufferedResult, pendingOperations) ); // Execute all dependent queries as a single batch executePendingOperations(result.operation, pendingOperations, false); return fromArray(results); }) ); return merge([ nonOptimisticResults$, optimisticMutationCompletion$, cacheResult$, ]); }; }; ================================================ FILE: exchanges/graphcache/src/default-storage/index.ts ================================================ import type { SerializedEntries, SerializedRequest, StorageAdapter, } from '../types'; const getRequestPromise = (request: IDBRequest): Promise => { return new Promise((resolve, reject) => { request.onerror = () => { reject(request.error); }; request.onsuccess = () => { resolve(request.result); }; }); }; const getTransactionPromise = (transaction: IDBTransaction): Promise => { return new Promise((resolve, reject) => { transaction.onerror = () => { reject(transaction.error); }; transaction.oncomplete = resolve; }); }; export interface StorageOptions { /** Name of the IndexedDB database that will be used. * @defaultValue `'graphcache-v4'` */ idbName?: string; /** Maximum age of cache entries (in days) after which data is discarded. * @defaultValue `7` days */ maxAge?: number; /** Gets Called when the exchange has hydrated the data from storage. */ onCacheHydrated?: () => void; } /** Sample storage adapter persisting to IndexedDB. */ export interface DefaultStorage extends StorageAdapter { /** Clears the entire IndexedDB storage. */ clear(): Promise; } /** Creates a default {@link StorageAdapter} which uses IndexedDB for storage. * * @param opts - A {@link StorageOptions} configuration object. * @returns the created {@link StorageAdapter}. * * @remarks * The default storage uses IndexedDB to persist the normalized cache for * offline use. It demonstrates that the cache can be chunked by timestamps. * * Note: We have no data on stability of this storage and our Offline Support * for large APIs or longterm use. Proceed with caution. */ export const makeDefaultStorage = (opts?: StorageOptions): DefaultStorage => { if (!opts) opts = {}; let callback: (() => void) | undefined; const DB_NAME = opts.idbName || 'graphcache-v4'; const ENTRIES_STORE_NAME = 'entries'; const METADATA_STORE_NAME = 'metadata'; let batch: Record = Object.create(null); const timestamp = Math.floor(new Date().valueOf() / (1000 * 60 * 60 * 24)); const maxAge = timestamp - (opts.maxAge || 7); const req = indexedDB.open(DB_NAME, 1); const database$ = getRequestPromise(req); req.onupgradeneeded = () => { req.result.createObjectStore(ENTRIES_STORE_NAME); req.result.createObjectStore(METADATA_STORE_NAME); }; return { clear() { return database$.then(database => { const transaction = database.transaction( [METADATA_STORE_NAME, ENTRIES_STORE_NAME], 'readwrite' ); transaction.objectStore(METADATA_STORE_NAME).clear(); transaction.objectStore(ENTRIES_STORE_NAME).clear(); batch = Object.create(null); return getTransactionPromise(transaction); }); }, readMetadata(): Promise { return database$.then( database => { return getRequestPromise( database .transaction(METADATA_STORE_NAME, 'readonly') .objectStore(METADATA_STORE_NAME) .get(METADATA_STORE_NAME) ); }, () => null ); }, writeMetadata(metadata: SerializedRequest[]) { database$.then( database => { return getRequestPromise( database .transaction(METADATA_STORE_NAME, 'readwrite') .objectStore(METADATA_STORE_NAME) .put(metadata, METADATA_STORE_NAME) ); }, () => { /* noop */ } ); }, writeData(entries: SerializedEntries): Promise { Object.assign(batch, entries); const toUndefined = () => undefined; return database$ .then(database => { return getRequestPromise( database .transaction(ENTRIES_STORE_NAME, 'readwrite') .objectStore(ENTRIES_STORE_NAME) .put(batch, timestamp) ); }) .then(toUndefined, toUndefined); }, readData(): Promise { const data: SerializedEntries = {}; return database$ .then(database => { const transaction = database.transaction( ENTRIES_STORE_NAME, 'readwrite' ); const store = transaction.objectStore(ENTRIES_STORE_NAME); const request = (store.openKeyCursor || store.openCursor).call(store); request.onsuccess = function () { if (this.result) { const { key } = this.result; if (typeof key !== 'number' || key < maxAge) { store.delete(key); } else { const request = store.get(key); request.onsuccess = () => { const result = request.result; if (key === timestamp) Object.assign(batch, result); Object.assign(data, result); }; } this.result.continue(); } }; return getTransactionPromise(transaction); }) .then( () => data, () => batch ); }, onCacheHydrated: opts.onCacheHydrated, onOnline(cb: () => void) { if (callback) { window.removeEventListener('online', callback); callback = undefined; } window.addEventListener( 'online', (callback = () => { cb(); }) ); }, }; }; ================================================ FILE: exchanges/graphcache/src/extras/index.ts ================================================ export { relayPagination } from './relayPagination'; export { simplePagination } from './simplePagination'; ================================================ FILE: exchanges/graphcache/src/extras/relayPagination.test.ts ================================================ import { gql } from '@urql/core'; import { it, expect, describe } from 'vitest'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store/store'; import { relayPagination } from './relayPagination'; function itemNode(numItem: number) { return { __typename: 'Item', id: numItem + '', }; } function itemEdge(numItem: number) { return { __typename: 'ItemEdge', node: itemNode(numItem), }; } describe('as resolver', () => { it('works with forward pagination', () => { const Pagination = gql` query ($cursor: String) { __typename items(first: 1, after: $cursor) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasNextPage endCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); const pageOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1)], nodes: [itemNode(1)], pageInfo: { __typename: 'PageInfo', hasNextPage: true, endCursor: '1', }, }, }; const pageTwo = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(2)], nodes: [itemNode(2)], pageInfo: { __typename: 'PageInfo', hasNextPage: false, endCursor: null, }, }, }; write(store, { query: Pagination, variables: { cursor: null } }, pageOne); write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo); const res = query(store, { query: Pagination }); expect(res.partial).toBe(false); expect(res.data).toEqual({ ...pageTwo, items: { ...pageTwo.items, edges: [pageOne.items.edges[0], pageTwo.items.edges[0]], nodes: [pageOne.items.nodes[0], pageTwo.items.nodes[0]], }, }); }); it('works with backwards pagination', () => { const Pagination = gql` query ($cursor: String) { __typename items(last: 1, before: $cursor) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasPreviousPage startCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); const pageOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(2)], nodes: [itemNode(2)], pageInfo: { __typename: 'PageInfo', hasPreviousPage: true, startCursor: '2', }, }, }; const pageTwo = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1)], nodes: [itemNode(1)], pageInfo: { __typename: 'PageInfo', hasPreviousPage: false, startCursor: null, }, }, }; write(store, { query: Pagination, variables: { cursor: null } }, pageOne); write(store, { query: Pagination, variables: { cursor: '2' } }, pageTwo); const res = query(store, { query: Pagination }); expect(res.partial).toBe(false); expect(res.data).toEqual({ ...pageTwo, items: { ...pageTwo.items, edges: [pageTwo.items.edges[0], pageOne.items.edges[0]], nodes: [pageTwo.items.nodes[0], pageOne.items.nodes[0]], }, }); }); it('handles duplicate edges', () => { const Pagination = gql` query ($cursor: String) { __typename items(first: 2, after: $cursor) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasNextPage endCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); const pageOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1), itemEdge(2)], nodes: [itemNode(1), itemNode(2)], pageInfo: { __typename: 'PageInfo', hasNextPage: true, endCursor: '2', }, }, }; const pageTwo = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(2), itemEdge(3)], nodes: [itemNode(2), itemNode(3)], pageInfo: { __typename: 'PageInfo', hasNextPage: false, endCursor: null, }, }, }; write(store, { query: Pagination, variables: { cursor: null } }, pageOne); write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo); const res = query(store, { query: Pagination }); expect(res.partial).toBe(false); expect(res.data).toEqual({ ...pageTwo, items: { ...pageTwo.items, edges: [ pageOne.items.edges[0], pageTwo.items.edges[0], pageTwo.items.edges[1], ], nodes: [ pageOne.items.nodes[0], pageTwo.items.nodes[0], pageTwo.items.nodes[1], ], }, }); }); it('works with simultaneous forward and backward pagination (outwards merging)', () => { const Pagination = gql` query ($first: Int, $last: Int, $before: String, $after: String) { __typename items(first: $first, last: $last, before: $before, after: $after) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasPreviousPage hasNextPage startCursor endCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination({ mergeMode: 'outwards' }), }, }, }); const pageOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1)], nodes: [itemNode(1)], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: false, startCursor: null, endCursor: '1', }, }, }; const pageTwo = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(2)], nodes: [itemNode(2)], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: true, startCursor: '2', endCursor: '2', }, }, }; const pageThree = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(-1)], nodes: [itemNode(-1)], pageInfo: { __typename: 'PageInfo', hasNextPage: false, hasPreviousPage: true, startCursor: '-1', endCursor: null, }, }, }; write( store, { query: Pagination, variables: { after: '1', first: 1 } }, pageOne ); write( store, { query: Pagination, variables: { after: '2', first: 1 } }, pageTwo ); write( store, { query: Pagination, variables: { before: '1', last: 1 } }, pageThree ); const res = query(store, { query: Pagination, variables: { before: '1', last: 1 }, }); expect(res.partial).toBe(false); expect(res.data).toEqual({ ...pageThree, items: { ...pageThree.items, edges: [ pageThree.items.edges[0], pageOne.items.edges[0], pageTwo.items.edges[0], ], nodes: [ pageThree.items.nodes[0], pageOne.items.nodes[0], pageTwo.items.nodes[0], ], pageInfo: { ...pageThree.items.pageInfo, hasPreviousPage: true, hasNextPage: true, startCursor: '-1', endCursor: '2', }, }, }); }); it('works with simultaneous forward and backward pagination (inwards merging)', () => { const Pagination = gql` query ($first: Int, $last: Int, $before: String, $after: String) { __typename items(first: $first, last: $last, before: $before, after: $after) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasPreviousPage hasNextPage startCursor endCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination({ mergeMode: 'inwards' }), }, }, }); const pageOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1)], nodes: [itemNode(1)], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: false, startCursor: null, endCursor: '1', }, }, }; const pageTwo = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(2)], nodes: [itemNode(2)], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: true, startCursor: '2', endCursor: '2', }, }, }; const pageThree = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(-1)], nodes: [itemNode(-1)], pageInfo: { __typename: 'PageInfo', hasNextPage: false, hasPreviousPage: true, startCursor: '-1', endCursor: null, }, }, }; write( store, { query: Pagination, variables: { after: '1', first: 1 } }, pageOne ); write( store, { query: Pagination, variables: { after: '2', first: 1 } }, pageTwo ); write( store, { query: Pagination, variables: { before: '1', last: 1 } }, pageThree ); const res = query(store, { query: Pagination, variables: { before: '1', last: 1 }, }); expect(res.partial).toBe(false); expect(res.data).toEqual({ ...pageThree, items: { ...pageThree.items, edges: [ pageOne.items.edges[0], pageTwo.items.edges[0], pageThree.items.edges[0], ], nodes: [ pageOne.items.nodes[0], pageTwo.items.nodes[0], pageThree.items.nodes[0], ], pageInfo: { ...pageThree.items.pageInfo, hasPreviousPage: true, hasNextPage: true, startCursor: '-1', endCursor: '2', }, }, }); }); it('prevents overlapping of pagination on different arguments', () => { const Pagination = gql` query ($filter: String) { items(first: 1, filter: $filter) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasNextPage endCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); const page = withId => ({ __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(withId)], nodes: [itemNode(withId)], pageInfo: { __typename: 'PageInfo', hasNextPage: false, endCursor: null, }, }, }); write( store, { query: Pagination, variables: { filter: 'one' } }, page('one') ); write( store, { query: Pagination, variables: { filter: 'two' } }, page('two') ); const resOne = query(store, { query: Pagination, variables: { filter: 'one' }, }); const resTwo = query(store, { query: Pagination, variables: { filter: 'two' }, }); const resThree = query(store, { query: Pagination, variables: { filter: 'three' }, }); expect(resOne.data).toHaveProperty('items.edges[0].node.id', 'one'); expect(resOne.data).toHaveProperty('items.edges.length', 1); expect(resTwo.data).toHaveProperty('items.edges[0].node.id', 'two'); expect(resTwo.data).toHaveProperty('items.edges.length', 1); expect(resThree.data).toEqual(null); }); it('returns an empty array of edges when the cache has zero edges stored', () => { const Pagination = gql` query { items(first: 1) { __typename edges { __typename } nodes { __typename } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); write( store, { query: Pagination }, { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [], nodes: [], }, } ); const res = query(store, { query: Pagination, }); expect(res.data).toHaveProperty('items', { __typename: 'ItemsConnection', edges: [], nodes: [], }); }); it('returns other fields on the same level as the edges', () => { const Pagination = gql` query { __typename items(first: 1) { __typename totalCount } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); write( store, { query: Pagination }, { __typename: 'Query', items: { __typename: 'ItemsConnection', totalCount: 2, }, } ); const resOne = query(store, { query: Pagination, }); expect(resOne.data).toHaveProperty('items', { __typename: 'ItemsConnection', totalCount: 2, }); }); it('returns a subset of the cached items if the query requests less items than the cached ones', () => { const Pagination = gql` query ($first: Int, $last: Int, $before: String, $after: String) { __typename items(first: $first, last: $last, before: $before, after: $after) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasPreviousPage hasNextPage startCursor endCursor } } } `; const store = new Store({ schema: require('../test-utils/relayPagination_schema.json'), resolvers: { Query: { items: relayPagination({ mergeMode: 'outwards' }), }, }, }); const results = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [ itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5), ], nodes: [ itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5), ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: false, startCursor: '1', endCursor: '5', }, }, }; write(store, { query: Pagination, variables: { first: 2 } }, results); const res = query(store, { query: Pagination, variables: { first: 2 }, }); expect(res.partial).toBe(false); expect(res.data).toEqual(results); }); it("returns the cached items even if they don't fullfil the query", () => { const Pagination = gql` query ($first: Int, $last: Int, $before: String, $after: String) { __typename items(first: $first, last: $last, before: $before, after: $after) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasPreviousPage hasNextPage startCursor endCursor } } } `; const store = new Store({ schema: require('../test-utils/relayPagination_schema.json'), resolvers: { Query: { items: relayPagination(), }, }, }); const results = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [ itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5), ], nodes: [ itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5), ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: false, startCursor: '1', endCursor: '5', }, }, }; write( store, { query: Pagination, variables: { after: '3', first: 3, last: 3 } }, results ); const res = query(store, { query: Pagination, variables: { after: '3', first: 3, last: 3 }, }); expect(res.partial).toBe(false); expect(res.data).toEqual(results); }); it('returns the cached items even when they come from a different query', () => { const Pagination = gql` query ($first: Int, $last: Int, $before: String, $after: String) { __typename items(first: $first, last: $last, before: $before, after: $after) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasPreviousPage hasNextPage startCursor endCursor } } } `; const store = new Store({ schema: require('../test-utils/relayPagination_schema.json'), resolvers: { Query: { items: relayPagination(), }, }, }); const results = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [ itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5), ], nodes: [ itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5), ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: false, startCursor: '1', endCursor: '5', }, }, }; write(store, { query: Pagination, variables: { first: 5 } }, results); const res = query(store, { query: Pagination, variables: { after: '3', first: 2, last: 2 }, }); expect(res.partial).toBe(true); expect(res.data).toEqual(results); }); it('caches and retrieves correctly queries with inwards pagination', () => { const Pagination = gql` query ($first: Int, $last: Int, $before: String, $after: String) { __typename items(first: $first, last: $last, before: $before, after: $after) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasPreviousPage hasNextPage startCursor endCursor } } } `; const store = new Store({ schema: require('../test-utils/relayPagination_schema.json'), resolvers: { Query: { items: relayPagination(), }, }, }); const results = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [ itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5), ], nodes: [ itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5), ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: false, startCursor: '1', endCursor: '5', }, }, }; write( store, { query: Pagination, variables: { after: '2', first: 2, last: 2 } }, results ); const res = query(store, { query: Pagination, variables: { after: '2', first: 2, last: 2 }, }); expect(res.partial).toBe(false); expect(res.data).toEqual(results); }); it('does not include a previous result when adding parameters', () => { const Pagination = gql` query ($first: Int, $filter: String) { __typename items(first: $first, filter: $filter) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasPreviousPage hasNextPage startCursor endCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); const results = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1), itemEdge(2)], nodes: [itemNode(1), itemNode(2)], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: false, startCursor: '1', endCursor: '2', }, }, }; const results2 = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [], nodes: [], pageInfo: { __typename: 'PageInfo', hasNextPage: false, hasPreviousPage: false, startCursor: '1', endCursor: '2', }, }, }; write(store, { query: Pagination, variables: { first: 2 } }, results); write( store, { query: Pagination, variables: { first: 2, filter: 'b' } }, results2 ); const res = query(store, { query: Pagination, variables: { first: 2, filter: 'b' }, }); expect(res.data).toEqual(results2); }); it('Works with edges absent from query', () => { const Pagination = gql` query ($first: Int, $last: Int, $before: String, $after: String) { __typename items(first: $first, last: $last, before: $before, after: $after) { __typename nodes { __typename id } pageInfo { __typename hasPreviousPage hasNextPage startCursor endCursor } } } `; const store = new Store({ schema: require('../test-utils/relayPagination_schema.json'), resolvers: { Query: { items: relayPagination({ mergeMode: 'outwards' }), }, }, }); const results = { __typename: 'Query', items: { __typename: 'ItemsConnection', nodes: [ itemNode(1), itemNode(2), itemNode(3), itemNode(4), itemNode(5), ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: false, startCursor: '1', endCursor: '5', }, }, }; write(store, { query: Pagination, variables: { first: 2 } }, results); const res = query(store, { query: Pagination, variables: { first: 2 }, }); expect(res.partial).toBe(false); expect(res.data).toEqual(results); }); it('Works with nodes absent from query', () => { const Pagination = gql` query ($first: Int, $last: Int, $before: String, $after: String) { __typename items(first: $first, last: $last, before: $before, after: $after) { __typename edges { __typename node { __typename id } } pageInfo { __typename hasPreviousPage hasNextPage startCursor endCursor } } } `; const store = new Store({ schema: require('../test-utils/relayPagination_schema.json'), resolvers: { Query: { items: relayPagination({ mergeMode: 'outwards' }), }, }, }); const results = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [ itemEdge(1), itemEdge(2), itemEdge(3), itemEdge(4), itemEdge(5), ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, hasPreviousPage: false, startCursor: '1', endCursor: '5', }, }, }; write(store, { query: Pagination, variables: { first: 2 } }, results); const res = query(store, { query: Pagination, variables: { first: 2 }, }); expect(res.partial).toBe(false); expect(res.data).toEqual(results); }); it('handles subsequent queries with larger last values', () => { const Pagination = gql` query ($last: Int!) { __typename items(last: $last) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasPreviousPage startCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); const pageOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(2)], nodes: [itemNode(2)], pageInfo: { __typename: 'PageInfo', hasPreviousPage: true, startCursor: '2', }, }, }; const pageTwo = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1), itemEdge(2)], nodes: [itemNode(1), itemNode(2)], pageInfo: { __typename: 'PageInfo', hasPreviousPage: false, startCursor: '1', }, }, }; const pageThree = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(0), itemEdge(1), itemEdge(2)], nodes: [itemNode(0), itemNode(1), itemNode(2)], pageInfo: { __typename: 'PageInfo', hasPreviousPage: false, startCursor: '0', }, }, }; write(store, { query: Pagination, variables: { last: 1 } }, pageOne); write(store, { query: Pagination, variables: { last: 2 } }, pageTwo); let res = query(store, { query: Pagination, variables: { last: 2 } }); expect(res.partial).toBe(false); expect(res.data).toEqual(pageTwo); write(store, { query: Pagination, variables: { last: 3 } }, pageThree); res = query(store, { query: Pagination, variables: { last: 3 } }); expect(res.partial).toBe(false); expect(res.data).toEqual(pageThree); }); it('handles subsequent queries with larger first values', () => { const Pagination = gql` query ($first: Int!) { __typename items(first: $first) { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasNextPage endCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); const pageOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1)], nodes: [itemNode(1)], pageInfo: { __typename: 'PageInfo', hasNextPage: true, endCursor: '1', }, }, }; const pageTwo = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1), itemEdge(2)], nodes: [itemNode(1), itemNode(2)], pageInfo: { __typename: 'PageInfo', hasNextPage: false, endCursor: '2', }, }, }; write(store, { query: Pagination, variables: { first: 1 } }, pageOne); write(store, { query: Pagination, variables: { first: 2 } }, pageTwo); const res = query(store, { query: Pagination, variables: { first: 2 } }); expect(res.partial).toBe(false); expect(res.data).toEqual(pageTwo); }); it('ignores empty pages when paginating', () => { const PaginationForward = gql` query ($first: Int!, $after: String) { __typename items(first: $first, after: $after) { __typename nodes { __typename id } pageInfo { __typename startCursor endCursor } } } `; const PaginationBackward = gql` query ($last: Int!, $before: String) { __typename items(last: $last, before: $before) { __typename nodes { __typename id } pageInfo { __typename startCursor endCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); const forwardOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', nodes: [itemNode(1), itemNode(2)], pageInfo: { __typename: 'PageInfo', startCursor: '1', endCursor: '2', }, }, }; const forwardAfter = { __typename: 'Query', items: { __typename: 'ItemsConnection', nodes: [], pageInfo: { __typename: 'PageInfo', startCursor: null, endCursor: null, }, }, }; const backwardBefore = { __typename: 'Query', items: { __typename: 'ItemsConnection', nodes: [], pageInfo: { __typename: 'PageInfo', startCursor: null, endCursor: null, }, }, }; write( store, { query: PaginationForward, variables: { first: 2 } }, forwardOne ); write( store, { query: PaginationBackward, variables: { last: 1, before: '1' } }, backwardBefore ); const res = query(store, { query: PaginationForward, variables: { first: 2 }, }); expect(res.partial).toBe(false); expect(res.data).toEqual(forwardOne); write( store, { query: PaginationForward, variables: { first: 1, after: '2' } }, forwardAfter ); expect(res.partial).toBe(false); expect(res.data).toEqual(forwardOne); }); it('allows for an empty page when this is the only result', () => { const Pagination = gql` query ($first: Int!, $after: String) { __typename items(first: $first, after: $after) { __typename nodes { __typename id } pageInfo { __typename startCursor endCursor } } } `; const store = new Store({ resolvers: { Query: { items: relayPagination(), }, }, }); const pageOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', nodes: [], pageInfo: { __typename: 'PageInfo', startCursor: null, endCursor: null, }, }, }; write(store, { query: Pagination, variables: { first: 2 } }, pageOne); const res = query(store, { query: Pagination, variables: { first: 2 }, }); expect(res.partial).toBe(false); expect(res.data).toEqual(pageOne); }); }); describe('as directive', () => { it('works with forward pagination', () => { const Pagination = gql` query ($cursor: String) { __typename items(first: 1, after: $cursor) @_relayPagination { __typename edges { __typename node { __typename id } } nodes { __typename id } pageInfo { __typename hasNextPage endCursor } } } `; const store = new Store({ directives: { relayPagination: () => relayPagination(), }, }); const pageOne = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(1)], nodes: [itemNode(1)], pageInfo: { __typename: 'PageInfo', hasNextPage: true, endCursor: '1', }, }, }; const pageTwo = { __typename: 'Query', items: { __typename: 'ItemsConnection', edges: [itemEdge(2)], nodes: [itemNode(2)], pageInfo: { __typename: 'PageInfo', hasNextPage: false, endCursor: null, }, }, }; write(store, { query: Pagination, variables: { cursor: null } }, pageOne); write(store, { query: Pagination, variables: { cursor: '1' } }, pageTwo); const res = query(store, { query: Pagination }); expect(res.partial).toBe(false); expect(res.data).toEqual({ ...pageTwo, items: { ...pageTwo.items, edges: [pageOne.items.edges[0], pageTwo.items.edges[0]], nodes: [pageOne.items.nodes[0], pageTwo.items.nodes[0]], }, }); }); }); ================================================ FILE: exchanges/graphcache/src/extras/relayPagination.ts ================================================ import { stringifyVariables } from '@urql/core'; import type { Cache, Resolver, Variables, NullArray } from '../types'; export type MergeMode = 'outwards' | 'inwards'; /** Input parameters for the {@link relayPagination} factory. */ export interface PaginationParams { /** Flip between inwards and outwards pagination. * * @remarks * This is only relevant if you’re querying pages using forwards and * backwards pagination at the same time. * When set to `'inwards'`, its default, pages that have been queried * forward are placed in front of all pages that were queried backwards. * When set to `'outwards'`, the two sets are merged in reverse. */ mergeMode?: MergeMode; } interface PageInfo { __typename: string; endCursor: null | string; startCursor: null | string; hasNextPage: boolean; hasPreviousPage: boolean; } interface Page { __typename: string; edges: NullArray; nodes: NullArray; pageInfo: PageInfo; } const defaultPageInfo: PageInfo = { __typename: 'PageInfo', endCursor: null, startCursor: null, hasNextPage: false, hasPreviousPage: false, }; const ensureKey = (x: any): string | null => (typeof x === 'string' ? x : null); const concatEdges = ( cache: Cache, leftEdges: NullArray, rightEdges: NullArray ) => { const ids = new Set(); for (let i = 0, l = leftEdges.length; i < l; i++) { const edge = leftEdges[i] as string | null; const node = cache.resolve(edge, 'node'); if (typeof node === 'string') ids.add(node); } const newEdges = leftEdges.slice(); for (let i = 0, l = rightEdges.length; i < l; i++) { const edge = rightEdges[i] as string | null; const node = cache.resolve(edge, 'node'); if (typeof node === 'string' && !ids.has(node)) { ids.add(node); newEdges.push(edge); } } return newEdges; }; const concatNodes = ( leftNodes: NullArray, rightNodes: NullArray ) => { const ids = new Set(); for (let i = 0, l = leftNodes.length; i < l; i++) { const node = leftNodes[i]; if (typeof node === 'string') ids.add(node); } const newNodes = leftNodes.slice(); for (let i = 0, l = rightNodes.length; i < l; i++) { const node = rightNodes[i]; if (typeof node === 'string' && !ids.has(node)) { ids.add(node); newNodes.push(node); } } return newNodes; }; const compareArgs = ( fieldArgs: Variables, connectionArgs: Variables ): boolean => { for (const key in connectionArgs) { if ( key === 'first' || key === 'last' || key === 'after' || key === 'before' ) { continue; } else if (!(key in fieldArgs)) { return false; } const argA = fieldArgs[key]; const argB = connectionArgs[key]; if ( typeof argA !== typeof argB || typeof argA !== 'object' ? argA !== argB : stringifyVariables(argA) !== stringifyVariables(argB) ) { return false; } } for (const key in fieldArgs) { if ( key === 'first' || key === 'last' || key === 'after' || key === 'before' ) { continue; } if (!(key in connectionArgs)) return false; } return true; }; const getPage = ( cache: Cache, entityKey: string, fieldKey: string ): Page | null => { const link = ensureKey(cache.resolve(entityKey, fieldKey)); if (!link) return null; const typename = cache.resolve(link, '__typename') as string; const edges = (cache.resolve(link, 'edges') || []) as NullArray; const nodes = (cache.resolve(link, 'nodes') || []) as NullArray; if (typeof typename !== 'string') { return null; } const page: Page = { __typename: typename, edges, nodes, pageInfo: defaultPageInfo, }; const pageInfoKey = cache.resolve(link, 'pageInfo'); if (typeof pageInfoKey === 'string') { const pageInfoType = ensureKey(cache.resolve(pageInfoKey, '__typename')); const endCursor = ensureKey(cache.resolve(pageInfoKey, 'endCursor')); const startCursor = ensureKey(cache.resolve(pageInfoKey, 'startCursor')); const hasNextPage = cache.resolve(pageInfoKey, 'hasNextPage'); const hasPreviousPage = cache.resolve(pageInfoKey, 'hasPreviousPage'); const pageInfo: PageInfo = (page.pageInfo = { __typename: typeof pageInfoType === 'string' ? pageInfoType : 'PageInfo', hasNextPage: typeof hasNextPage === 'boolean' ? hasNextPage : !!endCursor, hasPreviousPage: typeof hasPreviousPage === 'boolean' ? hasPreviousPage : !!startCursor, endCursor, startCursor, }); if (pageInfo.endCursor === null) { const edge = edges[edges.length - 1] as string | null; if (edge) { const endCursor = cache.resolve(edge, 'cursor'); pageInfo.endCursor = ensureKey(endCursor); } } if (pageInfo.startCursor === null) { const edge = edges[0] as string | null; if (edge) { const startCursor = cache.resolve(edge, 'cursor'); pageInfo.startCursor = ensureKey(startCursor); } } } return page; }; /** Creates a {@link Resolver} that combines pages that comply to the Relay pagination spec. * * @param params - A {@link PaginationParams} configuration object. * @returns the created Relay pagination {@link Resolver}. * * @remarks * `relayPagination` is a factory that creates a {@link Resolver} that can combine * multiple pages on a field that complies to the Relay pagination spec into a single, * combined list for infinite scrolling. * * This resolver will only work on fields that return a `Connection` GraphQL object * type, according to the Relay pagination spec. * * Hint: It's not recommended to use this when you can handle infinite scrolling * in your UI code instead. * * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers#relay-pagination} for more information. * @see {@link https://urql.dev/goto/docs/basics/ui-patterns/#infinite-scrolling} for an alternate approach. */ export const relayPagination = ( params: PaginationParams = {} ): Resolver => { const mergeMode = params.mergeMode || 'inwards'; return (_parent, fieldArgs, cache, info) => { const { parentKey: entityKey, fieldName } = info; const allFields = cache.inspectFields(entityKey); const fieldInfos = allFields.filter(info => info.fieldName === fieldName); const size = fieldInfos.length; if (size === 0) { return undefined; } let typename: string | null = null; let startEdges: NullArray = []; let endEdges: NullArray = []; let startNodes: NullArray = []; let endNodes: NullArray = []; let pageInfo: PageInfo = { ...defaultPageInfo }; for (let i = 0; i < size; i++) { const { fieldKey, arguments: args } = fieldInfos[i]; if (args === null || !compareArgs(fieldArgs, args)) { continue; } const page = getPage(cache, entityKey, fieldKey); if (page === null) { continue; } if (page.nodes.length === 0 && page.edges.length === 0 && typename) { continue; } if ( mergeMode === 'inwards' && typeof args.last === 'number' && typeof args.first === 'number' ) { const firstEdges = page.edges.slice(0, args.first + 1); const lastEdges = page.edges.slice(-args.last); const firstNodes = page.nodes.slice(0, args.first + 1); const lastNodes = page.nodes.slice(-args.last); startEdges = concatEdges(cache, startEdges, firstEdges); endEdges = concatEdges(cache, lastEdges, endEdges); startNodes = concatNodes(startNodes, firstNodes); endNodes = concatNodes(lastNodes, endNodes); pageInfo = page.pageInfo; } else if (args.after) { startEdges = concatEdges(cache, startEdges, page.edges); startNodes = concatNodes(startNodes, page.nodes); pageInfo.endCursor = page.pageInfo.endCursor; pageInfo.hasNextPage = page.pageInfo.hasNextPage; } else if (args.before) { endEdges = concatEdges(cache, page.edges, endEdges); endNodes = concatNodes(page.nodes, endNodes); pageInfo.startCursor = page.pageInfo.startCursor; pageInfo.hasPreviousPage = page.pageInfo.hasPreviousPage; } else if (typeof args.last === 'number') { endEdges = concatEdges(cache, page.edges, endEdges); endNodes = concatNodes(page.nodes, endNodes); pageInfo = page.pageInfo; } else { startEdges = concatEdges(cache, startEdges, page.edges); startNodes = concatNodes(startNodes, page.nodes); pageInfo = page.pageInfo; } if (page.pageInfo.__typename !== pageInfo.__typename) pageInfo.__typename = page.pageInfo.__typename; if (typename !== page.__typename) typename = page.__typename; } if (typeof typename !== 'string') { return undefined; } const hasCurrentPage = !!ensureKey( cache.resolve(entityKey, fieldName, fieldArgs) ); if (!hasCurrentPage) { if (!(info as any).store.schema) { return undefined; } else { info.partial = true; } } return { __typename: typename, edges: mergeMode === 'inwards' ? concatEdges(cache, startEdges, endEdges) : concatEdges(cache, endEdges, startEdges), nodes: mergeMode === 'inwards' ? concatNodes(startNodes, endNodes) : concatNodes(endNodes, startNodes), pageInfo: { __typename: pageInfo.__typename, endCursor: pageInfo.endCursor, startCursor: pageInfo.startCursor, hasNextPage: pageInfo.hasNextPage, hasPreviousPage: pageInfo.hasPreviousPage, }, }; }; }; ================================================ FILE: exchanges/graphcache/src/extras/simplePagination.test.ts ================================================ import { gql } from '@urql/core'; import { it, expect, describe } from 'vitest'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store/store'; import { MergeMode, simplePagination } from './simplePagination'; describe('as resolver', () => { it('works with forward pagination', () => { const Pagination = gql` query ($skip: Number, $limit: Number) { __typename persons(skip: $skip, limit: $limit) { __typename id name } } `; const store = new Store({ resolvers: { Query: { persons: simplePagination(), }, }, }); const pageOne = { __typename: 'Query', persons: [ { id: 1, name: 'Jovi', __typename: 'Person' }, { id: 2, name: 'Phil', __typename: 'Person' }, { id: 3, name: 'Andy', __typename: 'Person' }, ], }; const pageTwo = { __typename: 'Query', persons: [ { id: 4, name: 'Kadi', __typename: 'Person' }, { id: 5, name: 'Dom', __typename: 'Person' }, { id: 6, name: 'Sofia', __typename: 'Person' }, ], }; write( store, { query: Pagination, variables: { skip: 0, limit: 3 } }, pageOne ); const pageOneResult = query(store, { query: Pagination, variables: { skip: 0, limit: 3 }, }); expect(pageOneResult.data).toEqual(pageOne); write( store, { query: Pagination, variables: { skip: 3, limit: 3 } }, pageTwo ); const pageTwoResult = query(store, { query: Pagination, variables: { skip: 3, limit: 3 }, }); expect((pageTwoResult.data as any).persons).toEqual([ ...pageOne.persons, ...pageTwo.persons, ]); const pageThreeResult = query(store, { query: Pagination, variables: { skip: 6, limit: 3 }, }); expect(pageThreeResult.data).toEqual(null); }); it('works with backwards pagination', () => { const Pagination = gql` query ($skip: Number, $limit: Number) { __typename persons(skip: $skip, limit: $limit) { __typename id name } } `; const store = new Store({ resolvers: { Query: { persons: simplePagination({ mergeMode: 'before' }), }, }, }); const pageOne = { __typename: 'Query', persons: [ { id: 7, name: 'Jovi', __typename: 'Person' }, { id: 8, name: 'Phil', __typename: 'Person' }, { id: 9, name: 'Andy', __typename: 'Person' }, ], }; const pageTwo = { __typename: 'Query', persons: [ { id: 4, name: 'Kadi', __typename: 'Person' }, { id: 5, name: 'Dom', __typename: 'Person' }, { id: 6, name: 'Sofia', __typename: 'Person' }, ], }; write( store, { query: Pagination, variables: { skip: 0, limit: 3 } }, pageOne ); const pageOneResult = query(store, { query: Pagination, variables: { skip: 0, limit: 3 }, }); expect(pageOneResult.data).toEqual(pageOne); write( store, { query: Pagination, variables: { skip: 3, limit: 3 } }, pageTwo ); const pageTwoResult = query(store, { query: Pagination, variables: { skip: 3, limit: 3 }, }); expect((pageTwoResult.data as any).persons).toEqual([ ...pageTwo.persons, ...pageOne.persons, ]); const pageThreeResult = query(store, { query: Pagination, variables: { skip: 6, limit: 3 }, }); expect(pageThreeResult.data).toEqual(null); }); it('handles duplicates', () => { const Pagination = gql` query ($skip: Number, $limit: Number) { __typename persons(skip: $skip, limit: $limit) { __typename id name } } `; const store = new Store({ resolvers: { Query: { persons: simplePagination(), }, }, }); const pageOne = { __typename: 'Query', persons: [ { id: 1, name: 'Jovi', __typename: 'Person' }, { id: 2, name: 'Phil', __typename: 'Person' }, { id: 3, name: 'Andy', __typename: 'Person' }, ], }; const pageTwo = { __typename: 'Query', persons: [ { id: 3, name: 'Andy', __typename: 'Person' }, { id: 4, name: 'Kadi', __typename: 'Person' }, { id: 5, name: 'Dom', __typename: 'Person' }, ], }; write( store, { query: Pagination, variables: { skip: 0, limit: 3 } }, pageOne ); write( store, { query: Pagination, variables: { skip: 2, limit: 3 } }, pageTwo ); const result = query(store, { query: Pagination, variables: { skip: 2, limit: 3 }, }); expect(result.data).toEqual({ __typename: 'Query', persons: [...pageOne.persons, pageTwo.persons[1], pageTwo.persons[2]], }); }); it('should not return previous result when adding a parameter', () => { const Pagination = gql` query ($skip: Number, $limit: Number, $filter: String) { __typename persons(skip: $skip, limit: $limit, filter: $filter) { __typename id name } } `; const store = new Store({ resolvers: { Query: { persons: simplePagination(), }, }, }); const pageOne = { __typename: 'Query', persons: [ { id: 1, name: 'Jovi', __typename: 'Person' }, { id: 2, name: 'Phil', __typename: 'Person' }, { id: 3, name: 'Andy', __typename: 'Person' }, ], }; const emptyPage = { __typename: 'Query', persons: [], }; write( store, { query: Pagination, variables: { skip: 0, limit: 3 } }, pageOne ); write( store, { query: Pagination, variables: { skip: 0, limit: 3, filter: 'b' } }, emptyPage ); const res = query(store, { query: Pagination, variables: { skip: 0, limit: 3, filter: 'b' }, }); expect(res.data).toEqual({ __typename: 'Query', persons: [] }); }); it('should preserve the correct order in forward pagination', () => { const Pagination = gql` query ($skip: Number, $limit: Number) { __typename persons(skip: $skip, limit: $limit) { __typename id name } } `; const store = new Store({ resolvers: { Query: { persons: simplePagination({ mergeMode: 'after' }), }, }, }); const pageOne = { __typename: 'Query', persons: [ { id: 1, name: 'Jovi', __typename: 'Person' }, { id: 2, name: 'Phil', __typename: 'Person' }, { id: 3, name: 'Andy', __typename: 'Person' }, ], }; const pageTwo = { __typename: 'Query', persons: [ { id: 4, name: 'Kadi', __typename: 'Person' }, { id: 5, name: 'Dom', __typename: 'Person' }, { id: 6, name: 'Sofia', __typename: 'Person' }, ], }; write( store, { query: Pagination, variables: { skip: 3, limit: 3 } }, pageTwo ); write( store, { query: Pagination, variables: { skip: 0, limit: 3 } }, pageOne ); const result = query(store, { query: Pagination, variables: { skip: 3, limit: 3 }, }); expect(result.data).toEqual({ __typename: 'Query', persons: [...pageOne.persons, ...pageTwo.persons], }); }); it('should preserve the correct order in backward pagination', () => { const Pagination = gql` query ($skip: Number, $limit: Number) { __typename persons(skip: $skip, limit: $limit) { __typename id name } } `; const store = new Store({ resolvers: { Query: { persons: simplePagination({ mergeMode: 'before' }), }, }, }); const pageOne = { __typename: 'Query', persons: [ { id: 7, name: 'Jovi', __typename: 'Person' }, { id: 8, name: 'Phil', __typename: 'Person' }, { id: 9, name: 'Andy', __typename: 'Person' }, ], }; const pageTwo = { __typename: 'Query', persons: [ { id: 4, name: 'Kadi', __typename: 'Person' }, { id: 5, name: 'Dom', __typename: 'Person' }, { id: 6, name: 'Sofia', __typename: 'Person' }, ], }; write( store, { query: Pagination, variables: { skip: 3, limit: 3 } }, pageTwo ); write( store, { query: Pagination, variables: { skip: 0, limit: 3 } }, pageOne ); const result = query(store, { query: Pagination, variables: { skip: 3, limit: 3 }, }); expect(result.data).toEqual({ __typename: 'Query', persons: [...pageTwo.persons, ...pageOne.persons], }); }); it('prevents overlapping of pagination on different arguments', () => { const Pagination = gql` query ($skip: Number, $limit: Number, $filter: string) { __typename persons(skip: $skip, limit: $limit, filter: $filter) { __typename id name } } `; const store = new Store({ resolvers: { Query: { persons: simplePagination(), }, }, }); const page = withId => ({ __typename: 'Query', persons: [{ id: withId, name: withId, __typename: 'Person' }], }); write( store, { query: Pagination, variables: { filter: 'one', skip: 0, limit: 1 } }, page('one') ); write( store, { query: Pagination, variables: { filter: 'two', skip: 1, limit: 1 } }, page('two') ); const resOne = query(store, { query: Pagination, variables: { filter: 'one', skip: 0, limit: 1 }, }); const resTwo = query(store, { query: Pagination, variables: { filter: 'two', skip: 1, limit: 1 }, }); const resThree = query(store, { query: Pagination, variables: { filter: 'three', skip: 2, limit: 1 }, }); expect(resOne.data).toHaveProperty('persons[0].id', 'one'); expect(resOne.data).toHaveProperty('persons.length', 1); expect(resTwo.data).toHaveProperty('persons[0].id', 'two'); expect(resTwo.data).toHaveProperty('persons.length', 1); expect(resThree.data).toEqual(null); }); }); describe('as directive', () => { it('works with forward pagination', () => { const Pagination = gql` query ($skip: Number, $limit: Number) { __typename persons(skip: $skip, limit: $limit) @_simplePagination { __typename id name } } `; const store = new Store({ directives: { simplePagination: () => simplePagination(), }, }); const pageOne = { __typename: 'Query', persons: [ { id: 1, name: 'Jovi', __typename: 'Person' }, { id: 2, name: 'Phil', __typename: 'Person' }, { id: 3, name: 'Andy', __typename: 'Person' }, ], }; const pageTwo = { __typename: 'Query', persons: [ { id: 4, name: 'Kadi', __typename: 'Person' }, { id: 5, name: 'Dom', __typename: 'Person' }, { id: 6, name: 'Sofia', __typename: 'Person' }, ], }; write( store, { query: Pagination, variables: { skip: 0, limit: 3 } }, pageOne ); const pageOneResult = query(store, { query: Pagination, variables: { skip: 0, limit: 3 }, }); expect(pageOneResult.data).toEqual(pageOne); write( store, { query: Pagination, variables: { skip: 3, limit: 3 } }, pageTwo ); const pageTwoResult = query(store, { query: Pagination, variables: { skip: 3, limit: 3 }, }); expect((pageTwoResult.data as any).persons).toEqual([ ...pageOne.persons, ...pageTwo.persons, ]); const pageThreeResult = query(store, { query: Pagination, variables: { skip: 6, limit: 3 }, }); expect(pageThreeResult.data).toEqual(null); }); it('works with backwards pagination', () => { const Pagination = gql` query ($skip: Number, $limit: Number) { __typename persons(skip: $skip, limit: $limit) @_simplePagination(mergeMode: "before") { __typename id name } } `; const store = new Store({ directives: { simplePagination: directiveArguments => simplePagination({ mergeMode: directiveArguments!.mergeMode as MergeMode, }), }, }); const pageOne = { __typename: 'Query', persons: [ { id: 7, name: 'Jovi', __typename: 'Person' }, { id: 8, name: 'Phil', __typename: 'Person' }, { id: 9, name: 'Andy', __typename: 'Person' }, ], }; const pageTwo = { __typename: 'Query', persons: [ { id: 4, name: 'Kadi', __typename: 'Person' }, { id: 5, name: 'Dom', __typename: 'Person' }, { id: 6, name: 'Sofia', __typename: 'Person' }, ], }; write( store, { query: Pagination, variables: { skip: 0, limit: 3 } }, pageOne ); const pageOneResult = query(store, { query: Pagination, variables: { skip: 0, limit: 3 }, }); expect(pageOneResult.data).toEqual(pageOne); write( store, { query: Pagination, variables: { skip: 3, limit: 3 } }, pageTwo ); const pageTwoResult = query(store, { query: Pagination, variables: { skip: 3, limit: 3 }, }); expect((pageTwoResult.data as any).persons).toEqual([ ...pageTwo.persons, ...pageOne.persons, ]); const pageThreeResult = query(store, { query: Pagination, variables: { skip: 6, limit: 3 }, }); expect(pageThreeResult.data).toEqual(null); }); }); ================================================ FILE: exchanges/graphcache/src/extras/simplePagination.ts ================================================ import { stringifyVariables } from '@urql/core'; import type { Resolver, Variables, NullArray } from '../types'; export type MergeMode = 'before' | 'after'; /** Input parameters for the {@link simplePagination} factory. */ export interface PaginationParams { /** The name of the field argument used to define the page’s offset. */ offsetArgument?: string; /** The name of the field argument used to define the page’s length. */ limitArgument?: string; /** Flip between forward and backwards pagination. * * @remarks * When set to `'after'`, its default, pages are merged forwards and in order. * When set to `'before'`, pages are merged in reverse, putting later pages * in front of earlier ones. */ mergeMode?: MergeMode; } /** Creates a {@link Resolver} that combines pages of a primitive pagination field. * * @param options - A {@link PaginationParams} configuration object. * @returns the created pagination {@link Resolver}. * * @remarks * `simplePagination` is a factory that creates a {@link Resolver} that can combine * multiple lists on a paginated field into a single, combined list for infinite * scrolling. * * Hint: It's not recommended to use this when you can handle infinite scrolling * in your UI code instead. * * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers#simple-pagination} for more information. * @see {@link https://urql.dev/goto/docs/basics/ui-patterns/#infinite-scrolling} for an alternate approach. */ export const simplePagination = ({ offsetArgument = 'skip', limitArgument = 'limit', mergeMode = 'after', }: PaginationParams = {}): Resolver => { const compareArgs = ( fieldArgs: Variables, connectionArgs: Variables ): boolean => { for (const key in connectionArgs) { if (key === offsetArgument || key === limitArgument) { continue; } else if (!(key in fieldArgs)) { return false; } const argA = fieldArgs[key]; const argB = connectionArgs[key]; if ( typeof argA !== typeof argB || typeof argA !== 'object' ? argA !== argB : stringifyVariables(argA) !== stringifyVariables(argB) ) { return false; } } for (const key in fieldArgs) { if (key === offsetArgument || key === limitArgument) { continue; } if (!(key in connectionArgs)) return false; } return true; }; return (_parent, fieldArgs, cache, info) => { const { parentKey: entityKey, fieldName } = info; const allFields = cache.inspectFields(entityKey); const fieldInfos = allFields.filter(info => info.fieldName === fieldName); const size = fieldInfos.length; if (size === 0) { return undefined; } const visited = new Set(); let result: NullArray = []; let prevOffset: number | null = null; for (let i = 0; i < size; i++) { const { fieldKey, arguments: args } = fieldInfos[i]; if (args === null || !compareArgs(fieldArgs, args)) { continue; } const links = cache.resolve(entityKey, fieldKey) as string[]; const currentOffset = args[offsetArgument]; if ( links === null || links.length === 0 || typeof currentOffset !== 'number' ) { continue; } const tempResult: NullArray = []; for (let j = 0; j < links.length; j++) { const link = links[j]; if (visited.has(link)) continue; tempResult.push(link); visited.add(link); } if ( (!prevOffset || currentOffset > prevOffset) === (mergeMode === 'after') ) { result = [...result, ...tempResult]; } else { result = [...tempResult, ...result]; } prevOffset = currentOffset; } const hasCurrentPage = cache.resolve(entityKey, fieldName, fieldArgs); if (hasCurrentPage) { return result; } else if (!(info as any).store.schema) { return undefined; } else { info.partial = true; return result; } }; }; ================================================ FILE: exchanges/graphcache/src/helpers/help.ts ================================================ // These are guards that are used throughout the codebase to warn or error on // unexpected behaviour or conditions. // Every warning and error comes with a number that uniquely identifies them. // You can read more about the messages themselves in `docs/graphcache/errors.md` import type { ExecutableDefinitionNode, InlineFragmentNode, } from '@0no-co/graphql.web'; import type { Logger } from '../types'; import { Kind } from '@0no-co/graphql.web'; export type ErrorCode = | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28; type DebugNode = ExecutableDefinitionNode | InlineFragmentNode; // URL unfurls to https://formidable.com/open-source/urql/docs/graphcache/errors/ const helpUrl = '\nhttps://bit.ly/2XbVrpR#'; const cache = new Set(); export const currentDebugStack: string[] = []; export const popDebugNode = () => currentDebugStack.pop(); export const pushDebugNode = (typename: void | string, node: DebugNode) => { let identifier = ''; if (node.kind === Kind.INLINE_FRAGMENT) { identifier = typename ? `Inline Fragment on "${typename}"` : 'Inline Fragment'; } else if (node.kind === Kind.OPERATION_DEFINITION) { const name = node.name ? `"${node.name.value}"` : 'Unnamed'; identifier = `${name} ${node.operation}`; } else if (node.kind === Kind.FRAGMENT_DEFINITION) { identifier = `"${node.name.value}" Fragment`; } if (identifier) { currentDebugStack.push(identifier); } }; const getDebugOutput = (): string => currentDebugStack.length ? '\n(Caused At: ' + currentDebugStack.join(', ') + ')' : ''; export function invariant( condition: any, message: string, code: ErrorCode ): asserts condition { if (!condition) { let errorMessage = message || 'Minfied Error #' + code + '\n'; if (process.env.NODE_ENV !== 'production') { errorMessage += getDebugOutput(); } const error = new Error(errorMessage + helpUrl + code); error.name = 'Graphcache Error'; throw error; } } export function warn( message: string, code: ErrorCode, logger: Logger | undefined ) { if (!cache.has(message)) { if (logger) { logger('warn', message + getDebugOutput() + helpUrl + code); } else { console.warn(message + getDebugOutput() + helpUrl + code); } cache.add(message); } } ================================================ FILE: exchanges/graphcache/src/helpers/operation.ts ================================================ import type { Operation, RequestPolicy, OperationDebugMeta } from '@urql/core'; import { makeOperation } from '@urql/core'; // Returns the given operation result with added cacheOutcome meta field export const addMetadata = ( operation: Operation, meta: OperationDebugMeta ): Operation => makeOperation(operation.kind, operation, { ...operation.context, meta: { ...operation.context.meta, ...meta, }, }); // Copy an operation and change the requestPolicy to skip the cache export const toRequestPolicy = ( operation: Operation, requestPolicy: RequestPolicy ): Operation => { return makeOperation(operation.kind, operation, { ...operation.context, requestPolicy, }); }; ================================================ FILE: exchanges/graphcache/src/index.ts ================================================ export * from './types'; export { Store } from './store/store'; export { cacheExchange } from './cacheExchange'; export { offlineExchange } from './offlineExchange'; ================================================ FILE: exchanges/graphcache/src/offlineExchange.test.ts ================================================ import { gql, createClient, ExchangeIO, Operation, OperationResult, } from '@urql/core'; import { vi, expect, it, describe, beforeAll, afterAll } from 'vitest'; import { pipe, share, map, makeSubject, tap, publish } from 'wonka'; import { queryResponse } from '../../../packages/core/src/test-utils'; import { offlineExchange } from './offlineExchange'; const mutationOne = gql` mutation { updateAuthor { id name } } `; const mutationOneData = { __typename: 'Mutation', updateAuthor: { __typename: 'Author', id: '123', name: 'Author', }, }; const queryOne = gql` query { authors { id name __typename } } `; const queryOneData = { __typename: 'Query', authors: [ { id: '123', name: 'Me', __typename: 'Author', }, ], }; const dispatchDebug = vi.fn(); const storage = { onOnline: vi.fn(), writeData: vi.fn(() => Promise.resolve(undefined)), writeMetadata: vi.fn(() => Promise.resolve(undefined)), readData: vi.fn(() => Promise.resolve({})), readMetadata: vi.fn(() => Promise.resolve([])), }; describe('storage', () => { it('should read the metadata and dispatch operations on initialization', () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const reexecuteOperation = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(() => undefined); const op = client.createRequestOperation('mutation', { key: 1, query: mutationOne, variables: {}, }); const response = vi.fn((forwardOp: Operation): OperationResult => { expect(forwardOp.key).toBe(op.key); return { ...queryResponse, operation: forwardOp, data: mutationOneData, }; }); const { source: ops$ } = makeSubject(); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); vi.useFakeTimers(); pipe( offlineExchange({ storage })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); vi.runAllTimers(); expect(storage.readMetadata).toBeCalledTimes(1); expect(reexecuteOperation).toBeCalledTimes(0); }); }); describe('offline', () => { beforeAll(() => { vi.resetAllMocks(); vi.stubGlobal('navigator', { onLine: true }); }); afterAll(() => { vi.unstubAllGlobals(); }); it('should intercept errored mutations', () => { const onlineSpy = vi.spyOn(globalThis.navigator, 'onLine', 'get'); const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const queryOp = client.createRequestOperation('query', { key: 1, query: queryOne, variables: {}, }); const mutationOp = client.createRequestOperation('mutation', { key: 2, query: mutationOne, variables: {}, }); const response = vi.fn((forwardOp: Operation): OperationResult => { if (forwardOp.key === queryOp.key) { onlineSpy.mockReturnValueOnce(true); return { ...queryResponse, operation: forwardOp, data: queryOneData }; } else { onlineSpy.mockReturnValueOnce(false); return { ...queryResponse, operation: forwardOp, // @ts-ignore error: { networkError: new Error('failed to fetch') }, }; } }); const { source: ops$, next } = makeSubject(); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( offlineExchange({ storage, optimistic: { updateAuthor: () => ({ id: '123', name: 'URQL', __typename: 'Author', }), }, })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(queryOp); expect(result).toBeCalledTimes(1); expect(queryOneData).toMatchObject(result.mock.calls[0][0].data); next(mutationOp); expect(result).toBeCalledTimes(2); next(queryOp); expect(result).toBeCalledTimes(3); }); it('should intercept errored queries', async () => { const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const onlineSpy = vi .spyOn(navigator, 'onLine', 'get') .mockReturnValueOnce(false); const queryOp = client.createRequestOperation('query', { key: 1, query: queryOne, variables: undefined, }); const response = vi.fn((forwardOp: Operation): OperationResult => { onlineSpy.mockReturnValueOnce(false); return { operation: forwardOp, // @ts-ignore error: { networkError: new Error('failed to fetch') }, }; }); const { source: ops$, next } = makeSubject(); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response)); pipe( offlineExchange({ storage })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(queryOp); expect(result).toBeCalledTimes(1); expect(response).toBeCalledTimes(1); expect(result.mock.calls[0][0]).toEqual({ data: null, error: undefined, extensions: undefined, operation: expect.any(Object), hasNext: false, stale: false, }); expect(result.mock.calls[0][0]).toHaveProperty( 'operation.context.meta.cacheOutcome', 'miss' ); }); it('should flush the queue when we become online', async () => { let resolveOnOnlineCalled: () => void; const onOnlineCalled = new Promise( resolve => (resolveOnOnlineCalled = resolve) ); let flush: () => {}; storage.onOnline.mockImplementation(cb => { flush = cb; resolveOnOnlineCalled!(); }); const onlineSpy = vi.spyOn(navigator, 'onLine', 'get'); const client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); const mutationOp = client.createRequestOperation('mutation', { key: 1, query: mutationOne, variables: {}, }); const response = vi.fn((forwardOp: Operation): OperationResult => { onlineSpy.mockReturnValueOnce(false); return { operation: forwardOp, // @ts-ignore error: { networkError: new Error('failed to fetch') }, }; }); const { source: ops$, next } = makeSubject(); const result = vi.fn(); const forward: ExchangeIO = ops$ => pipe(ops$, map(response), share); pipe( offlineExchange({ storage, optimistic: { updateAuthor: () => ({ id: '123', name: 'URQL', __typename: 'Author', }), }, })({ forward, client, dispatchDebug })(ops$), tap(result), publish ); next(mutationOp); await onOnlineCalled; flush!(); }); }); ================================================ FILE: exchanges/graphcache/src/offlineExchange.ts ================================================ import { pipe, share, merge, makeSubject, filter, onPush } from 'wonka'; import type { Operation, OperationResult, Exchange, ExchangeIO, CombinedError, RequestPolicy, } from '@urql/core'; import { stringifyDocument, createRequest, makeOperation } from '@urql/core'; import type { SerializedRequest, CacheExchangeOpts, StorageAdapter, } from './types'; import { cacheExchange } from './cacheExchange'; import { toRequestPolicy } from './helpers/operation'; const policyLevel = { 'cache-only': 0, 'cache-first': 1, 'network-only': 2, 'cache-and-network': 3, } as const; /** Input parameters for the {@link offlineExchange}. * @remarks * This configuration object extends the {@link CacheExchangeOpts} * as the `offlineExchange` extends the regular {@link cacheExchange}. */ export interface OfflineExchangeOpts extends CacheExchangeOpts { /** Configures an offline storage adapter for Graphcache. * * @remarks * A {@link StorageAdapter} allows Graphcache to write data to an external, * asynchronous storage, and hydrate data from it when it first loads. * This allows you to preserve normalized data between restarts/reloads. * * @see {@link https://urql.dev/goto/docs/graphcache/offline} for the full Offline Support docs. */ storage: StorageAdapter; /** Predicate function to determine whether a {@link CombinedError} hints at a network error. * * @remarks * Not ever {@link CombinedError} means that the device is offline and by default * the `offlineExchange` will check for common network error messages and check * `navigator.onLine`. However, when `isOfflineError` is passed it can replace * the default offline detection. */ isOfflineError?( error: undefined | CombinedError, result: OperationResult ): boolean; } /** Exchange factory that creates a normalized cache exchange in Offline Support mode. * * @param opts - A {@link OfflineExchangeOpts} configuration object. * @returns the created normalized, offline cache {@link Exchange}. * * @remarks * The `offlineExchange` is a wrapper around the regular {@link cacheExchange} * which adds logic via the {@link OfflineExchangeOpts.storage} adapter to * recognize when it’s offline, when to retry failed mutations, and how * to handle longer periods of being offline. * * @see {@link https://urql.dev/goto/docs/graphcache/offline} for the full Offline Support docs. */ export const offlineExchange = (opts: C): Exchange => input => { const { storage } = opts; const isOfflineError = opts.isOfflineError || ((error: undefined | CombinedError) => error && error.networkError && !error.response && ((typeof navigator !== 'undefined' && navigator.onLine === false) || /request failed|failed to fetch|network\s?error/i.test( error.networkError.message ))); if ( storage && storage.onOnline && storage.readMetadata && storage.writeMetadata ) { const { forward: outerForward, client, dispatchDebug } = input; const { source: reboundOps$, next } = makeSubject(); const failedQueue: Operation[] = []; let hasRehydrated = false; let isFlushingQueue = false; const updateMetadata = () => { if (hasRehydrated) { const requests: SerializedRequest[] = []; for (let i = 0; i < failedQueue.length; i++) { const operation = failedQueue[i]; if (operation.kind === 'mutation') { requests.push({ query: stringifyDocument(operation.query), variables: operation.variables, extensions: operation.extensions, }); } } storage.writeMetadata!(requests); } }; const filterQueue = (key: number) => { for (let i = failedQueue.length - 1; i >= 0; i--) if (failedQueue[i].key === key) failedQueue.splice(i, 1); }; const flushQueue = () => { if (!isFlushingQueue) { const sent = new Set(); isFlushingQueue = true; for (let i = 0; i < failedQueue.length; i++) { const operation = failedQueue[i]; if (operation.kind === 'mutation' || !sent.has(operation.key)) { sent.add(operation.key); if (operation.kind !== 'subscription') { next(makeOperation('teardown', operation)); let overridePolicy: RequestPolicy = 'cache-first'; for (let i = 0; i < failedQueue.length; i++) { const { requestPolicy } = failedQueue[i].context; if (policyLevel[requestPolicy] > policyLevel[overridePolicy]) overridePolicy = requestPolicy; } next(toRequestPolicy(operation, overridePolicy)); } else { next(toRequestPolicy(operation, 'cache-first')); } } } isFlushingQueue = false; failedQueue.length = 0; updateMetadata(); } }; const forward: ExchangeIO = ops$ => { return pipe( outerForward(ops$), filter(res => { if ( hasRehydrated && res.operation.kind === 'mutation' && res.operation.context.optimistic && isOfflineError(res.error, res) ) { failedQueue.push(res.operation); updateMetadata(); return false; } return true; }), share ); }; const cacheResults$ = cacheExchange({ ...opts, storage: { ...storage, readData() { const hydrate = storage.readData(); return { async then(onEntries) { const mutations = await storage.readMetadata!(); for (let i = 0; mutations && i < mutations.length; i++) { failedQueue.push( client.createRequestOperation( 'mutation', createRequest(mutations[i].query, mutations[i].variables), mutations[i].extensions ) ); } onEntries!(await hydrate); storage.onOnline!(flushQueue); hasRehydrated = true; flushQueue(); }, }; }, }, })({ client, dispatchDebug, forward, }); return operations$ => { const opsAndRebound$ = merge([ reboundOps$, pipe( operations$, onPush(operation => { if (operation.kind === 'query' && !hasRehydrated) { failedQueue.push(operation); } else if (operation.kind === 'teardown') { filterQueue(operation.key); } }) ), ]); return pipe( cacheResults$(opsAndRebound$), filter(res => { if (res.operation.kind === 'query') { if (isOfflineError(res.error, res)) { next(toRequestPolicy(res.operation, 'cache-only')); failedQueue.push(res.operation); return false; } else if (!hasRehydrated) { filterQueue(res.operation.key); } } return true; }) ); }; } return cacheExchange(opts)(input); }; ================================================ FILE: exchanges/graphcache/src/operations/invalidate.ts ================================================ import * as InMemoryData from '../store/data'; import { keyOfField } from '../store/keys'; import type { FieldArgs } from '../types'; interface PartialFieldInfo { fieldKey: string; } export const invalidateEntity = ( entityKey: string, field?: string, args?: FieldArgs ) => { const fields: PartialFieldInfo[] = field ? [{ fieldKey: keyOfField(field, args) }] : InMemoryData.inspectFields(entityKey); for (let i = 0, l = fields.length; i < l; i++) { const { fieldKey } = fields[i]; if (InMemoryData.readLink(entityKey, fieldKey) !== undefined) { InMemoryData.writeLink(entityKey, fieldKey, undefined); } else { InMemoryData.writeRecord(entityKey, fieldKey, undefined); } } }; export const invalidateType = ( typename: string, excludedEntities: string[] ) => { const types = InMemoryData.getEntitiesForType(typename); for (const entity of types) { if (excludedEntities.includes(entity)) continue; invalidateEntity(entity); } }; ================================================ FILE: exchanges/graphcache/src/operations/query.test.ts ================================================ /* eslint-disable @typescript-eslint/no-var-requires */ import { gql } from '@urql/core'; import { minifyIntrospectionQuery } from '@urql/introspection'; import { describe, it, beforeEach, beforeAll, expect } from 'vitest'; import { Store } from '../store/store'; import { __initAnd_write as write } from './write'; import { __initAnd_query as query } from './query'; const TODO_QUERY = gql` query Todos { todos { id text complete author { id name known __typename } __typename } } `; describe('Query', () => { let schema, store, alteredRoot; beforeAll(() => { schema = minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ); alteredRoot = minifyIntrospectionQuery( require('../test-utils/altered_root_schema.json') ); }); beforeEach(() => { store = new Store({ schema }); write( store, { query: TODO_QUERY }, { __typename: 'Query', todos: [ { id: '0', text: 'Teach', __typename: 'Todo' }, { id: '1', text: 'Learn', __typename: 'Todo' }, ], } ); }); it('test partial results', () => { const result = query(store, { query: TODO_QUERY }); expect(result.partial).toBe(true); expect(result.data).toEqual({ todos: [ { id: '0', text: 'Teach', __typename: 'Todo', author: null, complete: null, }, { id: '1', text: 'Learn', __typename: 'Todo', author: null, complete: null, }, ], }); }); it('should warn once for invalid fields on an entity', () => { const INVALID_TODO_QUERY = gql` query InvalidTodo { todos { id text incomplete } } `; query(store, { query: INVALID_TODO_QUERY }); expect(console.warn).toHaveBeenCalledTimes(1); expect((console.warn as any).mock.calls[0][0]).toMatch( /Caused At: "InvalidTodo" query/ ); query(store, { query: INVALID_TODO_QUERY }); expect(console.warn).toHaveBeenCalledTimes(1); expect((console.warn as any).mock.calls[0][0]).toMatch(/incomplete/); }); it('should warn once for invalid sub-entities on an entity at the right stack', () => { const INVALID_TODO_QUERY = gql` query InvalidTodo { todos { ...ValidTodo ...InvalidFields } } fragment ValidTodo on Todo { id text } fragment InvalidFields on Todo { id writer { id } } `; query(store, { query: INVALID_TODO_QUERY }); expect(console.warn).toHaveBeenCalledTimes(1); expect((console.warn as any).mock.calls[0][0]).toMatch( /Caused At: "InvalidTodo" query, "InvalidFields" Fragment/ ); query(store, { query: INVALID_TODO_QUERY }); expect(console.warn).toHaveBeenCalledTimes(1); expect((console.warn as any).mock.calls[0][0]).toMatch(/writer/); }); // Issue#64 it('should not crash for valid queries', () => { const VALID_QUERY = gql` query getTodos { __typename todos { __typename id text } } `; // Use new store to ensure bug reproduction const store = new Store({ schema }); let { data } = query(store, { query: VALID_QUERY }); expect(data).toEqual(null); write( store, { query: VALID_QUERY }, // @ts-ignore { // Removing typename here would formerly crash this. todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }], } ); ({ data } = query(store, { query: VALID_QUERY })); expect(data).toEqual({ __typename: 'Query', todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }], }); expect(console.error).not.toHaveBeenCalled(); }); it('should respect altered root types', () => { const QUERY = gql` query getTodos { __typename todos { __typename id text } } `; const store = new Store({ schema: alteredRoot }); let { data } = query(store, { query: QUERY }); expect(data).toEqual(null); write( store, { query: QUERY }, { todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }], __typename: 'query_root', } ); ({ data } = query(store, { query: QUERY })); expect(data).toEqual({ __typename: 'query_root', todos: [{ __typename: 'Todo', id: '0', text: 'Solve bug' }], }); expect(console.warn).not.toHaveBeenCalled(); expect(console.error).not.toHaveBeenCalled(); }); it('should not allow subsequent reads when first result was null', () => { const QUERY_WRITE = gql` query writeTodos { __typename todos { __typename ...ValidRead } } fragment ValidRead on Todo { id } `; const QUERY_READ = gql` query getTodos { __typename todos { __typename ...MissingRead } todos { __typename id } } fragment MissingRead on Todo { id text } `; const store = new Store({ schema: alteredRoot, }); let { data } = query(store, { query: QUERY_READ }); expect(data).toEqual(null); write( store, { query: QUERY_WRITE }, { todos: [ { __typename: 'Todo', id: '0', }, ], __typename: 'Query', } ); ({ data } = query(store, { query: QUERY_READ })); expect(data).toEqual({ __typename: 'query_root', todos: [null], }); expect(console.warn).not.toHaveBeenCalled(); expect(console.error).not.toHaveBeenCalled(); }); it('should not allow subsequent reads when first result was null (with resolvers)', () => { const QUERY_WRITE = gql` query writeTodos { __typename todos { __typename ...ValidRead } } fragment ValidRead on Todo { id } `; const QUERY_READ = gql` query getTodos { __typename todos { __typename ...MissingRead } todos { __typename id } } fragment MissingRead on Todo { id text } `; const store = new Store({ schema, resolvers: { Query: { todos: (_parent, _args, cache) => cache.resolve('Query', 'todos'), }, }, }); let { data } = query(store, { query: QUERY_READ }); expect(data).toEqual(null); write( store, { query: QUERY_WRITE }, { todos: [ { __typename: 'Todo', id: '0', }, ], __typename: 'Query', } ); ({ data } = query(store, { query: QUERY_READ })); expect(data).toEqual({ __typename: 'Query', todos: [null], }); expect(console.warn).not.toHaveBeenCalled(); expect(console.error).not.toHaveBeenCalled(); }); it('should not mix references', () => { const QUERY_WRITE = gql` query writeTodos { __typename todos { __typename id textA textB } } `; const QUERY_READ = gql` query getTodos { __typename todos { __typename id textA } todos { __typename textB } } `; const store = new Store({ schema: alteredRoot, }); write( store, { query: QUERY_WRITE }, { todos: [ { __typename: 'Todo', id: '0', textA: 'a', textB: 'b', }, ], __typename: 'Query', } ); let data: any; data = query(store, { query: QUERY_READ }).data; expect(data).toEqual({ __typename: 'query_root', todos: [ { __typename: 'Todo', id: '0', textA: 'a', textB: 'b', }, ], }); const previousData = { __typename: 'query_root', todos: [ { __typename: 'Todo', id: '0', textA: 'a', textB: 'old', }, ], }; data = query(store, { query: QUERY_READ }, previousData).data; expect(data).toEqual({ __typename: 'query_root', todos: [ { __typename: 'Todo', id: '0', textA: 'a', textB: 'b', }, ], }); expect(previousData).toHaveProperty('todos.0.textA', 'a'); expect(previousData).toHaveProperty('todos.0.textB', 'old'); }); it('should keep references stable (1)', () => { const QUERY = gql` query todos { __typename todos { __typename test } todos { __typename id } } `; const store = new Store({ schema: alteredRoot, }); const expected = { todos: [ { __typename: 'Todo', id: '0', test: '0', }, { __typename: 'Todo', id: '1', test: '1', }, { __typename: 'Todo', id: '2', test: '2', }, ], __typename: 'query_root', }; write(store, { query: QUERY }, expected); const prevData = query( store, { query: QUERY }, { todos: [ { __typename: 'Todo', id: '0', test: 'prev-0', }, { __typename: 'Todo', id: '1', test: '1', }, { __typename: 'Todo', id: '2', test: '2', }, ], __typename: 'query_root', } ).data as any; const data = query(store, { query: QUERY }, prevData).data as any; expect(data).toEqual(expected); expect(prevData.todos[0]).toBe(data.todos[0]); expect(prevData.todos[1]).toBe(data.todos[1]); expect(prevData.todos[2]).toBe(data.todos[2]); expect(prevData.todos).toBe(data.todos); expect(prevData).toBe(data); }); it('should keep references stable (negative test)', () => { const QUERY = gql` query todos { __typename todos { __typename id } todos { __typename test } } `; const store = new Store({ schema: alteredRoot, }); const expected = { todos: [ { __typename: 'Todo', id: '0', test: '0', }, { __typename: 'Todo', id: '1', test: '1', }, { __typename: 'Todo', id: '2', test: '2', }, ], __typename: 'query_root', }; write(store, { query: QUERY }, expected); const prevData = query( store, { query: QUERY }, { todos: [ { __typename: 'Todo', id: '0', test: 'prev-0', }, { __typename: 'Todo', id: '1', test: '1', }, { __typename: 'Todo', id: '2', test: '2', }, ], __typename: 'query_root', } ).data as any; expected.todos[0].test = 'x'; write(store, { query: QUERY }, expected); const data = query(store, { query: QUERY }, prevData).data as any; expect(data).toEqual(expected); expect(prevData.todos[1]).toBe(data.todos[1]); expect(prevData.todos[2]).toBe(data.todos[2]); expect(prevData.todos[0]).not.toBe(data.todos[0]); expect(prevData.todos).not.toBe(data.todos); expect(prevData).not.toBe(data); }); }); ================================================ FILE: exchanges/graphcache/src/operations/query.ts ================================================ import type { FormattedNode, CombinedError } from '@urql/core'; import { formatDocument } from '@urql/core'; import type { FieldNode, DocumentNode, FragmentDefinitionNode, } from '@0no-co/graphql.web'; import type { SelectionSet } from '../ast'; import { getSelectionSet, getName, getFragmentTypeName, getFieldAlias, getFragments, getMainOperation, normalizeVariables, getFieldArguments, getDirectives, } from '../ast'; import type { Variables, Data, DataField, Link, OperationRequest, Dependencies, Resolver, } from '../types'; import { joinKeys, keyOfField } from '../store/keys'; import type { Store } from '../store/store'; import * as InMemoryData from '../store/data'; import { warn, pushDebugNode, popDebugNode } from '../helpers/help'; import type { Context } from './shared'; import { SelectionIterator, ensureData, makeContext, updateContext, getFieldError, deferRef, optionalRef, } from './shared'; import { isFieldAvailableOnType, isFieldNullable, isListNullable, } from '../ast'; export interface QueryResult { dependencies: Dependencies; partial: boolean; hasNext: boolean; data: null | Data; } /** Reads a GraphQL query from the cache. * @internal */ export const __initAnd_query = ( store: Store, request: OperationRequest, data?: Data | null | undefined, error?: CombinedError | undefined, key?: number ): QueryResult => { InMemoryData.initDataState('read', store.data, key); const result = _query(store, request, data, error); InMemoryData.clearDataState(); return result; }; /** Reads a GraphQL query from the cache. * @internal */ export const _query = ( store: Store, request: OperationRequest, input?: Data | null | undefined, error?: CombinedError | undefined ): QueryResult => { const query = formatDocument(request.query); const operation = getMainOperation(query); const rootKey = store.rootFields[operation.operation]; const rootSelect = getSelectionSet(operation); const ctx = makeContext( store, normalizeVariables(operation, request.variables), getFragments(query), rootKey, rootKey, error ); if (process.env.NODE_ENV !== 'production') { pushDebugNode(rootKey, operation); } // NOTE: This may reuse "previous result data" as indicated by the // `originalData` argument in readRoot(). This behaviour isn't used // for readSelection() however, which always produces results from // scratch const data = rootKey !== ctx.store.rootFields['query'] ? readRoot(ctx, rootKey, rootSelect, input || InMemoryData.makeData()) : readSelection( ctx, rootKey, rootSelect, input || InMemoryData.makeData() ); if (process.env.NODE_ENV !== 'production') { popDebugNode(); InMemoryData.getCurrentDependencies(); } return { dependencies: InMemoryData.currentDependencies!, partial: ctx.partial || !data, hasNext: ctx.hasNext, data: data || null, }; }; const readRoot = ( ctx: Context, entityKey: string, select: FormattedNode, input: Data ): Data => { const typename = ctx.store.rootNames[entityKey] ? entityKey : input.__typename; if (typeof typename !== 'string') { return input; } const selection = new SelectionIterator( entityKey, entityKey, false, undefined, select, ctx ); let node: FormattedNode | void; let hasChanged = InMemoryData.currentForeignData; const output = InMemoryData.makeData(input); while ((node = selection.next())) { const fieldAlias = getFieldAlias(node); const fieldValue = input[fieldAlias]; // Add the current alias to the walked path before processing the field's value ctx.__internal.path.push(fieldAlias); // We temporarily store the data field in here, but undefined // means that the value is missing from the cache let dataFieldValue: void | DataField; if (node.selectionSet && fieldValue !== null) { dataFieldValue = readRootField( ctx, getSelectionSet(node), ensureData(fieldValue) ); } else { dataFieldValue = fieldValue; } // Check for any referential changes in the field's value hasChanged = hasChanged || dataFieldValue !== fieldValue; if (dataFieldValue !== undefined) output[fieldAlias] = dataFieldValue!; // After processing the field, remove the current alias from the path again ctx.__internal.path.pop(); } return hasChanged ? output : input; }; const readRootField = ( ctx: Context, select: FormattedNode, originalData: Link ): Link => { if (Array.isArray(originalData)) { const newData = new Array(originalData.length); let hasChanged = InMemoryData.currentForeignData; for (let i = 0, l = originalData.length; i < l; i++) { // Add the current index to the walked path before reading the field's value ctx.__internal.path.push(i); // Recursively read the root field's value newData[i] = readRootField(ctx, select, originalData[i]); hasChanged = hasChanged || newData[i] !== originalData[i]; // After processing the field, remove the current index from the path ctx.__internal.path.pop(); } return hasChanged ? newData : originalData; } else if (originalData === null) { return null; } // Write entity to key that falls back to the given parentFieldKey const entityKey = ctx.store.keyOfEntity(originalData); if (entityKey !== null) { // We assume that since this is used for result data this can never be undefined, // since the result data has already been written to the cache return readSelection(ctx, entityKey, select, originalData) || null; } else { return readRoot(ctx, originalData.__typename, select, originalData); } }; export const _queryFragment = ( store: Store, query: FormattedNode, entity: Partial | string, variables?: Variables, fragmentName?: string ): Data | null => { const fragments = getFragments(query); let fragment: FormattedNode; if (fragmentName) { fragment = fragments[fragmentName]!; if (!fragment) { warn( 'readFragment(...) was called with a fragment name that does not exist.\n' + 'You provided ' + fragmentName + ' but could only find ' + Object.keys(fragments).join(', ') + '.', 6, store.logger ); return null; } } else { const names = Object.keys(fragments); fragment = fragments[names[0]]!; if (!fragment) { warn( 'readFragment(...) was called with an empty fragment.\n' + 'You have to call it with at least one fragment in your GraphQL document.', 6, store.logger ); return null; } } const typename = getFragmentTypeName(fragment); if (typeof entity !== 'string' && !entity.__typename) entity.__typename = typename; const entityKey = store.keyOfEntity(entity as Data); if (!entityKey) { warn( "Can't generate a key for readFragment(...).\n" + 'You have to pass an `id` or `_id` field or create a custom `keys` config for `' + typename + '`.', 7, store.logger ); return null; } if (process.env.NODE_ENV !== 'production') { pushDebugNode(typename, fragment); } const ctx = makeContext( store, variables || {}, fragments, typename, entityKey, undefined ); const result = readSelection( ctx, entityKey, getSelectionSet(fragment), InMemoryData.makeData() ) || null; if (process.env.NODE_ENV !== 'production') { popDebugNode(); } return result; }; function getFieldResolver( directives: ReturnType, typename: string, fieldName: string, ctx: Context ): Resolver | void { const resolvers = ctx.store.resolvers[typename]; const fieldResolver = resolvers && resolvers[fieldName]; let directiveResolver: Resolver | undefined; for (const name in directives) { const directiveNode = directives[name]; if ( directiveNode && name !== 'include' && name !== 'skip' && ctx.store.directives[name] ) { directiveResolver = ctx.store.directives[name]( getFieldArguments(directiveNode, ctx.variables) ); if (process.env.NODE_ENV === 'production') return directiveResolver; break; } } if (fieldResolver && directiveResolver) { warn( `A resolver and directive is being used at "${typename}.${fieldName}" simultaneously. Only the directive will apply.`, 28, ctx.store.logger ); } return directiveResolver || fieldResolver; } const readSelection = ( ctx: Context, key: string, select: FormattedNode, input: Data, result?: Data ): Data | undefined => { const { store } = ctx; const isQuery = key === store.rootFields.query; const entityKey = (result && store.keyOfEntity(result)) || key; if (!isQuery && !!ctx.store.rootNames[entityKey]) { warn( 'Invalid root traversal: A selection was being read on `' + entityKey + '` which is an uncached root type.\n' + 'The `' + ctx.store.rootFields.mutation + '` and `' + ctx.store.rootFields.subscription + '` types are special ' + 'Operation Root Types and cannot be read back from the cache.', 25, store.logger ); } const typename = !isQuery ? InMemoryData.readRecord(entityKey, '__typename') || (result && result.__typename) : key; if (typeof typename !== 'string') { return; } else if (result && typename !== result.__typename) { warn( 'Invalid resolver data: The resolver at `' + entityKey + '` returned an ' + 'invalid typename that could not be reconciled with the cache.', 8, store.logger ); return; } const selection = new SelectionIterator( typename, entityKey, false, undefined, select, ctx ); let hasFields = false; let hasNext = false; let hasChanged = InMemoryData.currentForeignData; let node: FormattedNode | void; const hasPartials = ctx.partial; const output = InMemoryData.makeData(input); while ((node = selection.next()) !== undefined) { // Derive the needed data from our node. const fieldName = getName(node); const fieldArgs = getFieldArguments(node, ctx.variables); const fieldAlias = getFieldAlias(node); const directives = getDirectives(node); const resolver = getFieldResolver(directives, typename, fieldName, ctx); const fieldKey = keyOfField(fieldName, fieldArgs); const key = joinKeys(entityKey, fieldKey); const fieldValue = InMemoryData.readRecord(entityKey, fieldKey); const resultValue = result ? result[fieldName] : undefined; if (process.env.NODE_ENV !== 'production' && store.schema && typename) { isFieldAvailableOnType( store.schema, typename, fieldName, ctx.store.logger ); } // Add the current alias to the walked path before processing the field's value ctx.__internal.path.push(fieldAlias); // We temporarily store the data field in here, but undefined // means that the value is missing from the cache let dataFieldValue: void | DataField = undefined; if (fieldName === '__typename') { // We directly assign the typename as it's already available dataFieldValue = typename; } else if (resultValue !== undefined && node.selectionSet === undefined) { // The field is a scalar and can be retrieved directly from the result dataFieldValue = resultValue; } else if (InMemoryData.currentOperation === 'read' && resolver) { // We have a resolver for this field. // Prepare the actual fieldValue, so that the resolver can use it, // as to avoid the user having to do `cache.resolve(parent, info.fieldKey)` // only to get a scalar value. let parent = output; if (node.selectionSet === undefined && fieldValue !== undefined) { parent = { ...output, [fieldAlias]: fieldValue, [fieldName]: fieldValue, }; } // We have to update the information in context to reflect the info // that the resolver will receive updateContext(ctx, parent, typename, entityKey, fieldKey, fieldName); dataFieldValue = resolver( parent, fieldArgs || ({} as Variables), store, ctx ); if (node.selectionSet) { // When it has a selection set we are resolving an entity with a // subselection. This can either be a list or an object. dataFieldValue = resolveResolverResult( ctx, typename, fieldName, key, getSelectionSet(node), (output[fieldAlias] !== undefined ? output[fieldAlias] : input[fieldAlias]) as Data, dataFieldValue, InMemoryData.ownsData(input) ); } if ( store.schema && dataFieldValue === null && !isFieldNullable(store.schema, typename, fieldName, ctx.store.logger) ) { // Special case for when null is not a valid value for the // current field return undefined; } } else if (!node.selectionSet) { // The field is a scalar but isn't on the result, so it's retrieved from the cache dataFieldValue = fieldValue; } else if (resultValue !== undefined) { // We start walking the nested resolver result here dataFieldValue = resolveResolverResult( ctx, typename, fieldName, key, getSelectionSet(node), (output[fieldAlias] !== undefined ? output[fieldAlias] : input[fieldAlias]) as Data, resultValue, InMemoryData.ownsData(input) ); } else { // Otherwise we attempt to get the missing field from the cache const link = InMemoryData.readLink(entityKey, fieldKey); if (link !== undefined) { dataFieldValue = resolveLink( ctx, link, typename, fieldName, getSelectionSet(node), (output[fieldAlias] !== undefined ? output[fieldAlias] : input[fieldAlias]) as Data, InMemoryData.ownsData(input) ); } else if (typeof fieldValue === 'object' && fieldValue !== null) { // The entity on the field was invalid but can still be recovered dataFieldValue = fieldValue; } } // Now that dataFieldValue has been retrieved it'll be set on data // If it's uncached (undefined) but nullable we can continue assembling // a partial query result if ( !deferRef && dataFieldValue === undefined && (directives.optional || (optionalRef && !directives.required) || !!getFieldError(ctx) || (!directives.required && store.schema && isFieldNullable(store.schema, typename, fieldName, ctx.store.logger))) ) { // The field is uncached or has errored, so it'll be set to null and skipped ctx.partial = true; dataFieldValue = null; } else if ( dataFieldValue === null && (directives.required || optionalRef === false) ) { if ( ctx.store.logger && process.env.NODE_ENV !== 'production' && InMemoryData.currentOperation === 'read' ) { ctx.store.logger( 'debug', `Got value "null" for required field "${fieldName}"${ fieldArgs ? ` with args ${JSON.stringify(fieldArgs)}` : '' } on entity "${entityKey}"` ); } dataFieldValue = undefined; } else { hasFields = hasFields || fieldName !== '__typename'; } // After processing the field, remove the current alias from the path again ctx.__internal.path.pop(); // Check for any referential changes in the field's value hasChanged = hasChanged || dataFieldValue !== input[fieldAlias]; if (dataFieldValue !== undefined) { output[fieldAlias] = dataFieldValue; } else if (deferRef) { hasNext = true; } else { if ( ctx.store.logger && process.env.NODE_ENV !== 'production' && InMemoryData.currentOperation === 'read' ) { ctx.store.logger( 'debug', `No value for field "${fieldName}"${ fieldArgs ? ` with args ${JSON.stringify(fieldArgs)}` : '' } on entity "${entityKey}"` ); } // If the field isn't deferred or partial then we have to abort and also reset // the partial field ctx.partial = hasPartials; return undefined; } } ctx.partial = ctx.partial || hasPartials; ctx.hasNext = ctx.hasNext || hasNext; return isQuery && ctx.partial && !hasFields ? undefined : hasChanged ? output : input; }; const resolveResolverResult = ( ctx: Context, typename: string, fieldName: string, key: string, select: FormattedNode, prevData: void | null | Data | Data[], result: void | DataField, isOwnedData: boolean ): DataField | void => { if (Array.isArray(result)) { const { store } = ctx; // Check whether values of the list may be null; for resolvers we assume // that they can be, since it's user-provided data const _isListNullable = store.schema ? isListNullable(store.schema, typename, fieldName, ctx.store.logger) : false; const hasPartials = ctx.partial; const data = InMemoryData.makeData(prevData, true); let hasChanged = InMemoryData.currentForeignData || !Array.isArray(prevData) || result.length !== prevData.length; for (let i = 0, l = result.length; i < l; i++) { // Add the current index to the walked path before reading the field's value ctx.__internal.path.push(i); // Recursively read resolver result const childResult = resolveResolverResult( ctx, typename, fieldName, joinKeys(key, `${i}`), select, prevData != null ? prevData[i] : undefined, result[i], isOwnedData ); // After processing the field, remove the current index from the path ctx.__internal.path.pop(); // Check the result for cache-missed values if (childResult === undefined && !_isListNullable) { ctx.partial = hasPartials; return undefined; } else { ctx.partial = ctx.partial || (childResult === undefined && _isListNullable); data[i] = childResult != null ? childResult : null; hasChanged = hasChanged || data[i] !== prevData![i]; } } return hasChanged ? data : prevData; } else if (result === null || result === undefined) { return result; } else if (isOwnedData && prevData === null) { return null; } else if (isDataOrKey(result)) { const data = (prevData || InMemoryData.makeData(prevData)) as Data; return typeof result === 'string' ? readSelection(ctx, result, select, data) : readSelection(ctx, key, select, data, result); } else { warn( 'Invalid resolver value: The field at `' + key + '` is a scalar (number, boolean, etc)' + ', but the GraphQL query expects a selection set for this field.', 9, ctx.store.logger ); return undefined; } }; const resolveLink = ( ctx: Context, link: Link | Link[], typename: string, fieldName: string, select: FormattedNode, prevData: void | null | Data | Data[], isOwnedData: boolean ): DataField | undefined => { if (Array.isArray(link)) { const { store } = ctx; const _isListNullable = store.schema ? isListNullable(store.schema, typename, fieldName, ctx.store.logger) : false; const newLink = InMemoryData.makeData(prevData, true); const hasPartials = ctx.partial; let hasChanged = InMemoryData.currentForeignData || !Array.isArray(prevData) || link.length !== prevData.length; for (let i = 0, l = link.length; i < l; i++) { // Add the current index to the walked path before reading the field's value ctx.__internal.path.push(i); // Recursively read the link const childLink = resolveLink( ctx, link[i], typename, fieldName, select, prevData != null ? prevData[i] : undefined, isOwnedData ); // After processing the field, remove the current index from the path ctx.__internal.path.pop(); // Check the result for cache-missed values if (childLink === undefined && !_isListNullable) { ctx.partial = hasPartials; return undefined; } else { ctx.partial = ctx.partial || (childLink === undefined && _isListNullable); newLink[i] = childLink || null; hasChanged = hasChanged || newLink[i] !== prevData![i]; } } return hasChanged ? newLink : (prevData as Data[]); } else if (link === null || (prevData === null && isOwnedData)) { return null; } return readSelection( ctx, link, select, (prevData || InMemoryData.makeData(prevData)) as Data ); }; const isDataOrKey = (x: any): x is string | Data => typeof x === 'string' || (typeof x === 'object' && typeof (x as any).__typename === 'string'); ================================================ FILE: exchanges/graphcache/src/operations/shared.test.ts ================================================ import { describe, it, expect } from 'vitest'; import { TypedDocumentNode, FormattedNode, formatDocument, gql, } from '@urql/core'; import { FieldNode } from '@0no-co/graphql.web'; import { SelectionIterator, deferRef } from './shared'; import { SelectionSet } from '../ast'; const selectionOfDocument = ( doc: TypedDocumentNode ): FormattedNode => { for (const definition of formatDocument(doc).definitions) if (definition.kind === 'OperationDefinition') return definition.selectionSet.selections as FormattedNode; return []; }; const ctx = {} as any; describe('SelectionIterator', () => { it('emits all fields', () => { const selection = selectionOfDocument(gql` { a b c } `); const iterate = new SelectionIterator( 'Query', 'Query', false, undefined, selection, ctx ); const result: FieldNode[] = []; let node: FieldNode | void; while ((node = iterate.next())) result.push(node); expect(result).toMatchInlineSnapshot(` [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "a", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "b", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "c", }, "selectionSet": undefined, }, ] `); }); it('skips fields that are skipped or not included', () => { const selection = selectionOfDocument(gql` { a @skip(if: true) b @include(if: false) } `); const iterate = new SelectionIterator( 'Query', 'Query', false, undefined, selection, ctx ); const result: FieldNode[] = []; let node: FieldNode | void; while ((node = iterate.next())) result.push(node); expect(result).toMatchInlineSnapshot('[]'); }); it('processes fragments', () => { const selection = selectionOfDocument(gql` { a ... { b } ... { ... { c } } } `); const iterate = new SelectionIterator( 'Query', 'Query', false, undefined, selection, ctx ); const result: FieldNode[] = []; let node: FieldNode | void; while ((node = iterate.next())) result.push(node); expect(result).toMatchInlineSnapshot(` [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "a", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "b", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "c", }, "selectionSet": undefined, }, ] `); }); it('updates deferred state as needed', () => { const selection = selectionOfDocument(gql` { a ... @defer { b } ... { ... @defer { c } } ... { ... { d } } ... @defer { ... { e } } ... { ... { f } } ... { g } h } `); const iterate = new SelectionIterator( 'Query', 'Query', false, undefined, selection, ctx ); const deferred: boolean[] = []; while (iterate.next()) deferred.push(deferRef); expect(deferred).toEqual([ false, // a true, // b true, // c false, // d true, // e false, // f false, // g false, // h ]); }); it('applies the parent’s defer state if needed', () => { const selection = selectionOfDocument(gql` { a ... @defer { b } ... { c } } `); const iterate = new SelectionIterator( 'Query', 'Query', true, undefined, selection, ctx ); const deferred: boolean[] = []; while (iterate.next()) deferred.push(deferRef); expect(deferred).toEqual([true, true, true]); }); }); ================================================ FILE: exchanges/graphcache/src/operations/shared.ts ================================================ import type { CombinedError, ErrorLike, FormattedNode } from '@urql/core'; import type { InlineFragmentNode, FragmentDefinitionNode, FieldNode, } from '@0no-co/graphql.web'; import { Kind } from '@0no-co/graphql.web'; import type { SelectionSet } from '../ast'; import { isDeferred, getTypeCondition, getSelectionSet, getName, isOptional, } from '../ast'; import { warn, pushDebugNode, popDebugNode } from '../helpers/help'; import { hasField, currentOperation, currentOptimistic, writeConcreteType, getConcreteTypes, isSeenConcreteType, } from '../store/data'; import { keyOfField } from '../store/keys'; import type { Store } from '../store/store'; import { getFieldArguments, shouldInclude, isInterfaceOfType } from '../ast'; import type { Fragments, Variables, DataField, NullArray, Link, Entity, Data, Logger, } from '../types'; export interface Context { store: Store; variables: Variables; fragments: Fragments; parentTypeName: string; parentKey: string; parentFieldKey: string; parent: Data; fieldName: string; error: ErrorLike | undefined; partial: boolean; hasNext: boolean; optimistic: boolean; __internal: { path: Array; errorMap: { [path: string]: ErrorLike } | undefined; }; } export let contextRef: Context | null = null; export let deferRef = false; export let optionalRef: boolean | undefined = undefined; // Checks whether the current data field is a cache miss because of a GraphQLError export const getFieldError = (ctx: Context): ErrorLike | undefined => ctx.__internal.path.length > 0 && ctx.__internal.errorMap ? ctx.__internal.errorMap[ctx.__internal.path.join('.')] : undefined; export const makeContext = ( store: Store, variables: Variables, fragments: Fragments, typename: string, entityKey: string, error: CombinedError | undefined ): Context => { const ctx: Context = { store, variables, fragments, parent: { __typename: typename }, parentTypeName: typename, parentKey: entityKey, parentFieldKey: '', fieldName: '', error: undefined, partial: false, hasNext: false, optimistic: currentOptimistic, __internal: { path: [], errorMap: undefined, }, }; if (error && error.graphQLErrors) { for (let i = 0; i < error.graphQLErrors.length; i++) { const graphQLError = error.graphQLErrors[i]; if (graphQLError.path && graphQLError.path.length) { if (!ctx.__internal.errorMap) ctx.__internal.errorMap = Object.create(null); ctx.__internal.errorMap![graphQLError.path.join('.')] = graphQLError; } } } return ctx; }; export const updateContext = ( ctx: Context, data: Data, typename: string, entityKey: string, fieldKey: string, fieldName: string ) => { contextRef = ctx; ctx.parent = data; ctx.parentTypeName = typename; ctx.parentKey = entityKey; ctx.parentFieldKey = fieldKey; ctx.fieldName = fieldName; ctx.error = getFieldError(ctx); }; const isFragmentHeuristicallyMatching = ( node: FormattedNode, typename: void | string, entityKey: string, vars: Variables, logger?: Logger ) => { if (!typename) return false; const typeCondition = getTypeCondition(node); if (!typeCondition || typename === typeCondition) return true; warn( 'Heuristic Fragment Matching: A fragment is trying to match against the `' + typename + '` type, ' + 'but the type condition is `' + typeCondition + '`. Since GraphQL allows for interfaces `' + typeCondition + '` may be an ' + 'interface.\nA schema needs to be defined for this match to be deterministic, ' + 'otherwise the fragment will be matched heuristically!', 16, logger ); return !getSelectionSet(node).some(node => { if (node.kind !== Kind.FIELD) return false; const fieldKey = keyOfField(getName(node), getFieldArguments(node, vars)); return !hasField(entityKey, fieldKey); }); }; export class SelectionIterator { typename: undefined | string; entityKey: string; ctx: Context; stack: { selectionSet: FormattedNode; index: number; defer: boolean; optional: boolean | undefined; }[]; // NOTE: Outside of this file, we expect `_defer` to always be reset to `false` constructor( typename: undefined | string, entityKey: string, _defer: false, _optional: undefined, selectionSet: FormattedNode, ctx: Context ); // NOTE: Inside this file we expect the state to be recursively passed on constructor( typename: undefined | string, entityKey: string, _defer: boolean, _optional: undefined | boolean, selectionSet: FormattedNode, ctx: Context ); constructor( typename: undefined | string, entityKey: string, _defer: boolean, _optional: boolean | undefined, selectionSet: FormattedNode, ctx: Context ) { this.typename = typename; this.entityKey = entityKey; this.ctx = ctx; this.stack = [ { selectionSet, index: 0, defer: _defer, optional: _optional, }, ]; } next(): FormattedNode | undefined { while (this.stack.length > 0) { let state = this.stack[this.stack.length - 1]; while (state.index < state.selectionSet.length) { const select = state.selectionSet[state.index++]; if (!shouldInclude(select, this.ctx.variables)) { /*noop*/ } else if (select.kind !== Kind.FIELD) { // A fragment is either referred to by FragmentSpread or inline const fragment = select.kind !== Kind.INLINE_FRAGMENT ? this.ctx.fragments[getName(select)] : select; if (fragment) { const isMatching = !fragment.typeCondition || (this.ctx.store.schema ? isInterfaceOfType( this.ctx.store.schema, fragment, this.typename ) : this.ctx.store.possibleTypeMap ? isSuperType( this.ctx.store.possibleTypeMap, fragment.typeCondition.name.value, this.typename ) : (currentOperation === 'read' && isFragmentMatching( fragment.typeCondition.name.value, this.typename )) || isFragmentHeuristicallyMatching( fragment, this.typename, this.entityKey, this.ctx.variables, this.ctx.store.logger )); if ( isMatching || (currentOperation === 'write' && !this.ctx.store.schema) ) { if (process.env.NODE_ENV !== 'production') pushDebugNode(this.typename, fragment); const isFragmentOptional = isOptional(select); if ( isMatching && fragment.typeCondition && this.typename !== fragment.typeCondition.name.value ) { writeConcreteType( fragment.typeCondition.name.value, this.typename! ); } this.stack.push( (state = { selectionSet: getSelectionSet(fragment), index: 0, defer: state.defer || isDeferred(select, this.ctx.variables), optional: isFragmentOptional !== undefined ? isFragmentOptional : state.optional, }) ); } } } else if (currentOperation === 'write' || !select._generated) { deferRef = state.defer; optionalRef = state.optional; return select; } } this.stack.pop(); if (process.env.NODE_ENV !== 'production') popDebugNode(); } return undefined; } } const isSuperType = ( possibleTypeMap: Map>, typeCondition: string, typename: string | void ) => { if (!typename) return false; if (typeCondition === typename) return true; const concreteTypes = possibleTypeMap.get(typeCondition); return concreteTypes && concreteTypes.has(typename); }; const isFragmentMatching = (typeCondition: string, typename: string | void) => { if (!typename) return false; if (typeCondition === typename) return true; const isProbableAbstractType = !isSeenConcreteType(typeCondition); if (!isProbableAbstractType) return false; const types = getConcreteTypes(typeCondition); return types.size && types.has(typename); }; export const ensureData = (x: DataField): Data | NullArray | null => x == null ? null : (x as Data | NullArray); export const ensureLink = (store: Store, ref: Link): Link => { if (!ref) { return ref || null; } else if (Array.isArray(ref)) { const link = new Array(ref.length); for (let i = 0, l = link.length; i < l; i++) link[i] = ensureLink(store, ref[i]); return link; } const link = store.keyOfEntity(ref); if (!link && ref && typeof ref === 'object') { warn( "Can't generate a key for link(...) item." + '\nYou have to pass an `id` or `_id` field or create a custom `keys` config for `' + ref.__typename + '`.', 12, store.logger ); } return link; }; ================================================ FILE: exchanges/graphcache/src/operations/write.test.ts ================================================ /* eslint-disable @typescript-eslint/no-var-requires */ import { gql, CombinedError } from '@urql/core'; import { minifyIntrospectionQuery } from '@urql/introspection'; import { vi, expect, it, beforeEach, describe, beforeAll } from 'vitest'; import { __initAnd_write as write } from './write'; import * as InMemoryData from '../store/data'; import { Store } from '../store/store'; const TODO_QUERY = gql` query todos { todos { id text complete author { id name known __typename } __typename } } `; describe('Query', () => { let schema, store; beforeAll(() => { schema = minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ); }); beforeEach(() => { store = new Store({ schema }); write( store, { query: TODO_QUERY }, { __typename: 'Query', todos: [ { id: '0', text: 'Teach', __typename: 'Todo' }, { id: '1', text: 'Learn', __typename: 'Todo' }, ], } ); vi.clearAllMocks(); }); it('should not crash for valid writes', async () => { const VALID_TODO_QUERY = gql` mutation { toggleTodo { id text complete } } `; write( store, { query: VALID_TODO_QUERY }, { __typename: 'Mutation', toggleTodo: { __typename: 'Todo', id: '0', text: 'Teach', complete: true, }, } ); expect(console.warn).not.toHaveBeenCalled(); expect(console.error).not.toHaveBeenCalled(); }); it('should warn once for invalid fields on an entity', () => { const INVALID_TODO_QUERY = gql` mutation { toggleTodo { id text incomplete } } `; write( store, { query: INVALID_TODO_QUERY }, { __typename: 'Mutation', toggleTodo: { __typename: 'Todo', id: '0', text: 'Teach', incomplete: false, }, } ); expect(console.warn).toHaveBeenCalledTimes(1); write( store, { query: INVALID_TODO_QUERY }, { __typename: 'Mutation', toggleTodo: { __typename: 'Todo', id: '0', text: 'Teach', incomplete: false, }, } ); expect(console.warn).toHaveBeenCalledTimes(1); expect((console.warn as any).mock.calls[0][0]).toMatch( /The field `incomplete` does not exist on `Todo`/ ); }); it('should warn once for invalid link fields on an entity', () => { const INVALID_TODO_QUERY = gql` mutation { toggleTodo { id text writer { id } } } `; write( store, { query: INVALID_TODO_QUERY }, { __typename: 'Mutation', toggleTodo: { __typename: 'Todo', id: '0', text: 'Teach', writer: { id: '0', }, }, } ); // Because of us indicating Todo:Writer as a scalar expect(console.warn).toHaveBeenCalledTimes(2); expect((console.warn as any).mock.calls[0][0]).toMatch( /The field `writer` does not exist on `Todo`/ ); }); it('should skip undefined values that are expected', () => { const query = gql` { field } `; // This should not overwrite the field write(store, { query }, { field: undefined } as any); // Because of us writing an undefined field expect(console.warn).toHaveBeenCalledTimes(2); expect((console.warn as any).mock.calls[1][0]).toMatch( /Invalid undefined: The field at `field`/ ); write(store, { query }, { field: 'test' } as any); write(store, { query }, { field: undefined } as any); InMemoryData.initDataState('read', store.data, null); // The field must still be `'test'` expect(InMemoryData.readRecord('Query', 'field')).toBe('test'); }); it('should write errored records as undefined rather than null', () => { const query = gql` { missingField setField } `; write( store, { query }, { missingField: null, setField: 'test' } as any, new CombinedError({ graphQLErrors: [ { message: 'Test', path: ['missingField'], }, ], }) ); InMemoryData.initDataState('read', store.data, null); // The setField must still be `'test'` expect(InMemoryData.readRecord('Query', 'setField')).toBe('test'); // The missingField must still be `undefined` expect(InMemoryData.readRecord('Query', 'missingField')).toBe(undefined); }); it('should write errored links as undefined rather than null', () => { const query = gql` { missingTodoItem: todos { id text } missingTodo: todo { id text } } `; write( store, { query }, { missingTodoItem: [null, { __typename: 'Todo', id: 1, text: 'Learn' }], missingTodo: null, } as any, new CombinedError({ graphQLErrors: [ { message: 'Test', path: ['missingTodoItem', 0], }, { message: 'Test', path: ['missingTodo'], }, ], }) ); InMemoryData.initDataState('read', store.data, null); expect(InMemoryData.readLink('Query', 'todos')).toEqual([ undefined, 'Todo:1', ]); expect(InMemoryData.readLink('Query', 'todo')).toEqual(undefined); }); }); ================================================ FILE: exchanges/graphcache/src/operations/write.ts ================================================ import type { FormattedNode, CombinedError } from '@urql/core'; import { formatDocument } from '@urql/core'; import type { FieldNode, DocumentNode, FragmentDefinitionNode, } from '@0no-co/graphql.web'; import type { SelectionSet } from '../ast'; import { getFragments, getMainOperation, normalizeVariables, getFieldArguments, isFieldAvailableOnType, getSelectionSet, getName, getFragmentTypeName, getFieldAlias, } from '../ast'; import { invariant, warn, pushDebugNode, popDebugNode } from '../helpers/help'; import type { NullArray, Variables, Data, Link, OperationRequest, Dependencies, EntityField, OptimisticMutationResolver, } from '../types'; import { joinKeys, keyOfField } from '../store/keys'; import type { Store } from '../store/store'; import * as InMemoryData from '../store/data'; import type { Context } from './shared'; import { SelectionIterator, ensureData, makeContext, updateContext, getFieldError, deferRef, } from './shared'; import { invalidateType } from './invalidate'; export interface WriteResult { data: null | Data; dependencies: Dependencies; } /** Writes a GraphQL response to the cache. * @internal */ export const __initAnd_write = ( store: Store, request: OperationRequest, data: Data, error?: CombinedError | undefined, key?: number ): WriteResult => { InMemoryData.initDataState('write', store.data, key || null); const result = _write(store, request, data, error); InMemoryData.clearDataState(); return result; }; export const __initAnd_writeOptimistic = ( store: Store, request: OperationRequest, key: number ): WriteResult => { if (process.env.NODE_ENV !== 'production') { invariant( getMainOperation(request.query).operation === 'mutation', 'writeOptimistic(...) was called with an operation that is not a mutation.\n' + 'This case is unsupported and should never occur.', 10 ); } InMemoryData.initDataState('write', store.data, key, true); const result = _write(store, request, {} as Data, undefined); InMemoryData.clearDataState(); return result; }; export const _write = ( store: Store, request: OperationRequest, data?: Data, error?: CombinedError | undefined ) => { if (process.env.NODE_ENV !== 'production') { InMemoryData.getCurrentDependencies(); } const query = formatDocument(request.query); const operation = getMainOperation(query); const result: WriteResult = { data: data || InMemoryData.makeData(), dependencies: InMemoryData.currentDependencies!, }; const kind = store.rootFields[operation.operation]; const ctx = makeContext( store, normalizeVariables(operation, request.variables), getFragments(query), kind, kind, error ); if (process.env.NODE_ENV !== 'production') { pushDebugNode(kind, operation); } writeSelection(ctx, kind, getSelectionSet(operation), result.data!); if (process.env.NODE_ENV !== 'production') { popDebugNode(); } return result; }; export const _writeFragment = ( store: Store, query: FormattedNode, data: Partial, variables?: Variables, fragmentName?: string ) => { const fragments = getFragments(query); let fragment: FormattedNode; if (fragmentName) { fragment = fragments[fragmentName]!; if (!fragment) { warn( 'writeFragment(...) was called with a fragment name that does not exist.\n' + 'You provided ' + fragmentName + ' but could only find ' + Object.keys(fragments).join(', ') + '.', 11, store.logger ); return null; } } else { const names = Object.keys(fragments); fragment = fragments[names[0]]!; if (!fragment) { warn( 'writeFragment(...) was called with an empty fragment.\n' + 'You have to call it with at least one fragment in your GraphQL document.', 11, store.logger ); return null; } } const typename = getFragmentTypeName(fragment); const dataToWrite = { __typename: typename, ...data } as Data; const entityKey = store.keyOfEntity(dataToWrite); if (!entityKey) { return warn( "Can't generate a key for writeFragment(...) data.\n" + 'You have to pass an `id` or `_id` field or create a custom `keys` config for `' + typename + '`.', 12, store.logger ); } if (process.env.NODE_ENV !== 'production') { pushDebugNode(typename, fragment); } const ctx = makeContext( store, variables || {}, fragments, typename, entityKey, undefined ); writeSelection(ctx, entityKey, getSelectionSet(fragment), dataToWrite); if (process.env.NODE_ENV !== 'production') { popDebugNode(); } }; const writeSelection = ( ctx: Context, entityKey: undefined | string, select: FormattedNode, data: Data ) => { // These fields determine how we write. The `Query` root type is written // like a normal entity, hence, we use `rootField` with a default to determine // this. All other root names (Subscription & Mutation) are in a different // write mode const rootField = ctx.store.rootNames[entityKey!] || 'query'; const isRoot = !!ctx.store.rootNames[entityKey!]; let typename = isRoot ? entityKey : data.__typename; if (!typename && entityKey && ctx.optimistic) { typename = InMemoryData.readRecord(entityKey, '__typename') as | string | undefined; } if (!typename) { warn( "Couldn't find __typename when writing.\n" + "If you're writing to the cache manually have to pass a `__typename` property on each entity in your data.", 14, ctx.store.logger ); return; } else if (!isRoot && entityKey) { InMemoryData.writeRecord(entityKey, '__typename', typename); InMemoryData.writeType(typename, entityKey); } const updates = ctx.store.updates[typename]; const selection = new SelectionIterator( typename, entityKey || typename, false, undefined, select, ctx ); let node: FormattedNode | void; while ((node = selection.next())) { const fieldName = getName(node); const fieldArgs = getFieldArguments(node, ctx.variables); const fieldKey = keyOfField(fieldName, fieldArgs); const fieldAlias = getFieldAlias(node); let fieldValue = data[ctx.optimistic ? fieldName : fieldAlias]; if ( // Skip typename fields and assume they've already been written above fieldName === '__typename' || // Fields marked as deferred that aren't defined must be skipped // Otherwise, we also ignore undefined values in optimistic updaters (fieldValue === undefined && (deferRef || (ctx.optimistic && rootField === 'query'))) ) { continue; } if (process.env.NODE_ENV !== 'production') { if (ctx.store.schema && typename && fieldName !== '__typename') { isFieldAvailableOnType( ctx.store.schema, typename, fieldName, ctx.store.logger ); } } // Add the current alias to the walked path before processing the field's value ctx.__internal.path.push(fieldAlias); // Execute optimistic mutation functions on root fields, or execute recursive functions // that have been returned on optimistic objects let resolver: OptimisticMutationResolver | undefined; if (ctx.optimistic && rootField === 'mutation') { resolver = ctx.store.optimisticMutations[fieldName]; if (!resolver) continue; } else if (ctx.optimistic && typeof fieldValue === 'function') { resolver = fieldValue as any; } // Execute the field-level resolver to retrieve its data if (resolver) { // We have to update the context to reflect up-to-date ResolveInfo updateContext( ctx, data, typename, entityKey || typename, fieldKey, fieldName ); fieldValue = ensureData(resolver(fieldArgs || {}, ctx.store, ctx)); } if (fieldValue === undefined) { if (process.env.NODE_ENV !== 'production') { if ( !entityKey || !InMemoryData.hasField(entityKey, fieldKey) || (ctx.optimistic && !InMemoryData.readRecord(entityKey, '__typename')) ) { const expected = node.selectionSet === undefined ? 'scalar (number, boolean, etc)' : 'selection set'; warn( 'Invalid undefined: The field at `' + fieldKey + '` is `undefined`, but the GraphQL query expects a ' + expected + ' for this field.', 13, ctx.store.logger ); } } continue; // Skip this field } if (node.selectionSet) { // Process the field and write links for the child entities that have been written if (entityKey && rootField === 'query') { const key = joinKeys(entityKey, fieldKey); const link = writeField( ctx, getSelectionSet(node), ensureData(fieldValue), key, ctx.optimistic ? InMemoryData.readLink(entityKey || typename, fieldKey) : undefined ); InMemoryData.writeLink(entityKey || typename, fieldKey, link); } else { writeField(ctx, getSelectionSet(node), ensureData(fieldValue)); } } else if (entityKey && rootField === 'query') { // This is a leaf node, so we're setting the field's value directly InMemoryData.writeRecord( entityKey || typename, fieldKey, (fieldValue !== null || !getFieldError(ctx) ? fieldValue : undefined) as EntityField ); } // We run side-effect updates after the default, normalized updates // so that the data is already available in-store if necessary const updater = updates && updates[fieldName]; if (updater) { // We have to update the context to reflect up-to-date ResolveInfo updateContext( ctx, data, typename, entityKey || typename, fieldKey, fieldName ); data[fieldName] = fieldValue; updater(data, fieldArgs || {}, ctx.store, ctx); } else if ( typename === ctx.store.rootFields['mutation'] && !ctx.optimistic ) { // If we're on a mutation that doesn't have an updater, we'll see // whether we can find the entity returned by the mutation in the cache. // if we don't we'll assume this is a create mutation and invalidate // the found __typename. if (fieldValue && Array.isArray(fieldValue)) { const excludedEntities: string[] = fieldValue.map( entity => ctx.store.keyOfEntity(entity) || '' ); for (let i = 0, l = fieldValue.length; i < l; i++) { const key = excludedEntities[i]; if (key && fieldValue[i].__typename) { const resolved = InMemoryData.readRecord(key, '__typename'); const count = InMemoryData!.getRefCount(key); if (resolved && !count) { invalidateType(fieldValue[i].__typename, excludedEntities); } } } } else if (fieldValue && typeof fieldValue === 'object') { const key = ctx.store.keyOfEntity(fieldValue as any); if (key) { const resolved = InMemoryData.readRecord(key, '__typename'); const count = InMemoryData.getRefCount(key); if ((!resolved || !count) && fieldValue.__typename) { invalidateType(fieldValue.__typename, [key]); } } } } // After processing the field, remove the current alias from the path again ctx.__internal.path.pop(); } }; // A pattern to match typenames of types that are likely never keyable const KEYLESS_TYPE_RE = /^__|PageInfo|(Connection|Edge)$/; const writeField = ( ctx: Context, select: FormattedNode, data: null | Data | NullArray, parentFieldKey?: string, prevLink?: Link ): Link | undefined => { if (Array.isArray(data)) { const newData = new Array(data.length); for (let i = 0, l = data.length; i < l; i++) { // Add the current index to the walked path before processing the link ctx.__internal.path.push(i); // Append the current index to the parentFieldKey fallback const indexKey = parentFieldKey ? joinKeys(parentFieldKey, `${i}`) : undefined; // Recursively write array data const prevIndex = prevLink != null ? prevLink[i] : undefined; const links = writeField(ctx, select, data[i], indexKey, prevIndex); // Link cannot be expressed as a recursive type newData[i] = links as string | null; // After processing the field, remove the current index from the path ctx.__internal.path.pop(); } return newData; } else if (data === null) { return getFieldError(ctx) ? undefined : null; } const entityKey = ctx.store.keyOfEntity(data) || (typeof prevLink === 'string' ? prevLink : null); const typename = data.__typename; if ( parentFieldKey && !ctx.store.keys[data.__typename] && entityKey === null && typeof typename === 'string' && !KEYLESS_TYPE_RE.test(typename) ) { warn( 'Invalid key: The GraphQL query at the field at `' + parentFieldKey + '` has a selection set, ' + 'but no key could be generated for the data at this field.\n' + 'You have to request `id` or `_id` fields for all selection sets or create ' + 'a custom `keys` config for `' + typename + '`.\n' + 'Entities without keys will be embedded directly on the parent entity. ' + 'If this is intentional, create a `keys` config for `' + typename + '` that always returns null.', 15, ctx.store.logger ); } const childKey = entityKey || parentFieldKey; writeSelection(ctx, childKey, select, data); return childKey || null; }; ================================================ FILE: exchanges/graphcache/src/store/__snapshots__/store.test.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`Store with storage > should be able to persist embedded data 1`] = ` { "Query%2eappointment({"id":"1"}).__typename": ""Appointment"", "Query%2eappointment({"id":"1"}).info": ""urql meeting"", "Query.appointment({"id":"1"})": ":"Query.appointment({\\"id\\":\\"1\\"})"", } `; exports[`Store with storage > should be able to store and rehydrate data 1`] = ` { "Appointment:1.__typename": ""Appointment"", "Appointment:1.id": ""1"", "Appointment:1.info": ""urql meeting"", "Query.appointment({"id":"1"})": ":"Appointment:1"", } `; ================================================ FILE: exchanges/graphcache/src/store/data.test.ts ================================================ import { describe, it, beforeEach, expect } from 'vitest'; import * as InMemoryData from './data'; import { keyOfField } from './keys'; let data: InMemoryData.InMemoryData; beforeEach(() => { data = InMemoryData.make('Query'); InMemoryData.initDataState('write', data, null); }); describe('garbage collection', () => { it('erases orphaned entities', () => { InMemoryData.writeRecord('Todo:1', '__typename', 'Todo'); InMemoryData.writeRecord('Todo:1', 'id', '1'); InMemoryData.writeRecord('Todo:2', '__typename', 'Todo'); InMemoryData.writeRecord('Query', '__typename', 'Query'); InMemoryData.writeLink('Query', 'todo', 'Todo:1'); InMemoryData.writeType('Todo', 'Todo:1'); InMemoryData.gc(); expect(InMemoryData.readLink('Query', 'todo')).toBe('Todo:1'); expect(InMemoryData.getEntitiesForType('Todo')).toEqual( new Set(['Todo:1']) ); InMemoryData.writeLink('Query', 'todo', undefined); InMemoryData.gc(); expect(InMemoryData.readLink('Query', 'todo')).toBe(undefined); expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); expect(InMemoryData.getEntitiesForType('Todo')).toEqual(new Set()); expect(InMemoryData.getCurrentDependencies()).toEqual( new Set(['Todo:1', 'Todo:2', 'Query.todo']) ); }); it('keeps readopted entities', () => { InMemoryData.writeRecord('Todo:1', '__typename', 'Todo'); InMemoryData.writeRecord('Todo:1', 'id', '1'); InMemoryData.writeRecord('Query', '__typename', 'Query'); InMemoryData.writeLink('Query', 'todo', 'Todo:1'); InMemoryData.writeLink('Query', 'todo', undefined); InMemoryData.writeLink('Query', 'newTodo', 'Todo:1'); InMemoryData.writeType('Todo', 'Todo:1'); InMemoryData.gc(); expect(InMemoryData.readLink('Query', 'newTodo')).toBe('Todo:1'); expect(InMemoryData.readLink('Query', 'todo')).toBe(undefined); expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); expect(InMemoryData.getEntitiesForType('Todo')).toEqual( new Set(['Todo:1']) ); expect(InMemoryData.getCurrentDependencies()).toEqual( new Set(['Todo:1', 'Query.todo', 'Query.newTodo']) ); }); it('keeps entities with multiple owners', () => { InMemoryData.writeRecord('Todo:1', '__typename', 'Todo'); InMemoryData.writeRecord('Todo:1', 'id', '1'); InMemoryData.writeRecord('Query', '__typename', 'Query'); InMemoryData.writeLink('Query', 'todoA', 'Todo:1'); InMemoryData.writeLink('Query', 'todoB', 'Todo:1'); InMemoryData.writeLink('Query', 'todoA', undefined); InMemoryData.gc(); expect(InMemoryData.readLink('Query', 'todoA')).toBe(undefined); expect(InMemoryData.readLink('Query', 'todoB')).toBe('Todo:1'); expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); expect(InMemoryData.getCurrentDependencies()).toEqual( new Set(['Todo:1', 'Query.todoA', 'Query.todoB']) ); }); it('skips entities with optimistic updates', () => { InMemoryData.writeRecord('Todo:1', '__typename', 'Todo'); InMemoryData.writeRecord('Todo:1', 'id', '1'); InMemoryData.writeLink('Query', 'todo', 'Todo:1'); InMemoryData.initDataState('write', data, 1, true); InMemoryData.writeLink('Query', 'temp', 'Todo:1'); InMemoryData.initDataState('write', data, 0, true); InMemoryData.writeLink('Query', 'todo', undefined); InMemoryData.gc(); expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); InMemoryData.reserveLayer(data, 1); InMemoryData.gc(); expect(InMemoryData.readRecord('Todo:1', 'id')).toBe('1'); // TODO: is it a problem that this fails, we are reading from Todo // but we are not updating anything expect(InMemoryData.getCurrentDependencies()).toEqual( new Set(['Query.todo']) ); }); it('erases child entities that are orphaned', () => { InMemoryData.writeRecord('Author:1', '__typename', 'Author'); InMemoryData.writeRecord('Author:1', 'id', '1'); InMemoryData.writeLink('Todo:1', 'author', 'Author:1'); InMemoryData.writeRecord('Todo:1', '__typename', 'Todo'); InMemoryData.writeRecord('Todo:1', 'id', '1'); InMemoryData.writeLink('Query', 'todo', 'Todo:1'); InMemoryData.writeType('Todo', 'Todo:1'); InMemoryData.writeType('Author', 'Author:1'); InMemoryData.writeLink('Query', 'todo', undefined); expect(InMemoryData.getEntitiesForType('Todo')).toEqual( new Set(['Todo:1']) ); expect(InMemoryData.getEntitiesForType('Author')).toEqual( new Set(['Author:1']) ); InMemoryData.gc(); expect(InMemoryData.readRecord('Todo:1', 'id')).toBe(undefined); expect(InMemoryData.readRecord('Author:1', 'id')).toBe(undefined); expect(InMemoryData.getEntitiesForType('Todo')).toEqual(new Set()); expect(InMemoryData.getEntitiesForType('Author')).toEqual(new Set()); expect(InMemoryData.getCurrentDependencies()).toEqual( new Set(['Author:1', 'Todo:1', 'Query.todo']) ); }); }); describe('inspectFields', () => { it('returns field infos for all links and records', () => { InMemoryData.writeRecord('Query', '__typename', 'Query'); InMemoryData.writeLink('Query', keyOfField('todo', { id: '1' }), 'Todo:1'); InMemoryData.writeRecord('Query', keyOfField('hasTodo', { id: '1' }), true); InMemoryData.writeLink('Query', 'randomTodo', 'Todo:1'); expect(InMemoryData.inspectFields('Query')).toMatchInlineSnapshot(` [ { "arguments": { "id": "1", }, "fieldKey": "todo({"id":"1"})", "fieldName": "todo", }, { "arguments": null, "fieldKey": "randomTodo", "fieldName": "randomTodo", }, { "arguments": null, "fieldKey": "__typename", "fieldName": "__typename", }, { "arguments": { "id": "1", }, "fieldKey": "hasTodo({"id":"1"})", "fieldName": "hasTodo", }, ] `); expect(InMemoryData.getCurrentDependencies()).toEqual( new Set([ 'Query.todo({"id":"1"})', 'Query.hasTodo({"id":"1"})', 'Query.randomTodo', ]) ); }); it('returns an empty array when an entity is unknown', () => { expect(InMemoryData.inspectFields('Random')).toEqual([]); expect(InMemoryData.getCurrentDependencies()).toEqual(new Set(['Random'])); }); it('returns field infos for all optimistic updates', () => { InMemoryData.initDataState('write', data, 1, true); InMemoryData.writeLink('Query', 'todo', 'Todo:1'); expect(InMemoryData.inspectFields('Random')).toMatchInlineSnapshot('[]'); }); it('avoids duplicate field infos', () => { InMemoryData.writeLink('Query', 'todo', 'Todo:1'); InMemoryData.initDataState('write', data, 1, true); InMemoryData.writeLink('Query', 'todo', 'Todo:2'); expect(InMemoryData.inspectFields('Query')).toMatchInlineSnapshot(` [ { "arguments": null, "fieldKey": "todo", "fieldName": "todo", }, ] `); }); }); describe('commutative changes', () => { it('always applies out-of-order updates in-order', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.reserveLayer(data, 2); InMemoryData.initDataState('write', data, 2); InMemoryData.writeRecord('Query', 'index', 2); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); InMemoryData.initDataState('write', data, 1); InMemoryData.writeRecord('Query', 'index', 1); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); expect(data.optimisticOrder).toEqual([]); }); it('creates optimistic layers that may be removed later', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.initDataState('write', data, 2, true); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); // Actively clearing out layer 2 InMemoryData.noopDataState(data, 2); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(undefined); InMemoryData.initDataState('write', data, 1); InMemoryData.writeRecord('Query', 'index', 1); InMemoryData.clearDataState(); InMemoryData.initDataState('write', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(1); InMemoryData.clearDataState(); expect(data.optimisticOrder).toEqual([]); }); it('discards optimistic order when concrete data is written', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.reserveLayer(data, 2); InMemoryData.reserveLayer(data, 3); InMemoryData.initDataState('write', data, 2, true); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.writeRecord('Query', 'optimistic', true); InMemoryData.clearDataState(); InMemoryData.initDataState('write', data, 3); InMemoryData.writeRecord('Query', 'index', 3); InMemoryData.clearDataState(); // Expect Layer 3 expect(data.optimisticOrder).toEqual([3, 2, 1]); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(3); expect(InMemoryData.readRecord('Query', 'optimistic')).toBe(true); // Write 2 again InMemoryData.initDataState('write', data, 2); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); // 2 has moved in front of 3 expect(data.optimisticOrder).toEqual([2, 3, 1]); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); expect(InMemoryData.readRecord('Query', 'optimistic')).toBe(undefined); }); it('overrides data using optimistic layers', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.reserveLayer(data, 2); InMemoryData.reserveLayer(data, 3); InMemoryData.initDataState('write', data, 2); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); InMemoryData.initDataState('write', data, 3); InMemoryData.writeRecord('Query', 'index', 3); InMemoryData.clearDataState(); // Regular write that isn't optimistic InMemoryData.initDataState('write', data, null); InMemoryData.writeRecord('Query', 'index', 1); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(3); expect(data.optimisticOrder).toEqual([3, 2, 1]); }); it('avoids optimistic layers when only one layer is pending', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.initDataState('write', data, 1); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); // This will be applied and visible since the above write isn't optimistic InMemoryData.initDataState('write', data, null); InMemoryData.writeRecord('Query', 'index', 1); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(1); expect(data.optimisticOrder).toEqual([]); }); it('continues applying optimistic layers even if the first one completes', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.reserveLayer(data, 2); InMemoryData.reserveLayer(data, 3); InMemoryData.reserveLayer(data, 4); InMemoryData.initDataState('write', data, 1); InMemoryData.writeRecord('Query', 'index', 1); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(1); InMemoryData.initDataState('write', data, 3); InMemoryData.writeRecord('Query', 'index', 3); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(3); InMemoryData.initDataState('write', data, 4); InMemoryData.writeRecord('Query', 'index', 4); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(4); InMemoryData.initDataState('write', data, 2); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(4); expect(data.optimisticOrder).toEqual([]); }); it('allows noopDataState to clear layers only if necessary', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.reserveLayer(data, 2); InMemoryData.noopDataState(data, 2); expect(data.optimisticOrder).toEqual([2, 1]); InMemoryData.noopDataState(data, 1); expect(data.optimisticOrder).toEqual([]); }); it('respects non-reserved optimistic layers', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.initDataState('write', data, 2, true); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); InMemoryData.reserveLayer(data, 3); expect(data.optimisticOrder).toEqual([3, 2, 1]); expect([...data.commutativeKeys]).toEqual([1, 3]); InMemoryData.initDataState('write', data, 1); InMemoryData.writeRecord('Query', 'index', 1); InMemoryData.clearDataState(); expect(data.optimisticOrder).toEqual([3, 2]); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); InMemoryData.initDataState('write', data, 3); InMemoryData.writeRecord('Query', 'index', 3); InMemoryData.clearDataState(); expect(data.optimisticOrder).toEqual([3, 2]); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(3); }); it('squashes when optimistic layers are completed', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.initDataState('write', data, 2, true); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); expect(data.optimisticOrder).toEqual([2, 1]); InMemoryData.initDataState('write', data, 1); InMemoryData.writeRecord('Query', 'index', 1); InMemoryData.clearDataState(); expect(data.optimisticOrder).toEqual([2]); // Delete optimistic layer InMemoryData.noopDataState(data, 2); expect(data.optimisticOrder).toEqual([]); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(1); }); it('squashes when optimistic layers are replaced with actual data', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.initDataState('write', data, 2, true); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); expect(data.optimisticOrder).toEqual([2, 1]); InMemoryData.initDataState('write', data, 1); InMemoryData.writeRecord('Query', 'index', 1); InMemoryData.clearDataState(); expect(data.optimisticOrder).toEqual([2]); // Convert optimistic layer to commutative layer InMemoryData.initDataState('write', data, 2); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); expect(data.optimisticOrder).toEqual([]); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); }); it('prevents inspectFields from failing for uninitialised layers', () => { InMemoryData.initDataState('write', data, null); InMemoryData.writeRecord('Query', 'test', true); InMemoryData.clearDataState(); InMemoryData.reserveLayer(data, 1); InMemoryData.initDataState('read', data, null); expect(InMemoryData.inspectFields('Query')).toEqual([ { arguments: null, fieldKey: 'test', fieldName: 'test', }, ]); }); it('allows reserveLayer to be called repeatedly', () => { InMemoryData.reserveLayer(data, 1); InMemoryData.reserveLayer(data, 1); expect(data.optimisticOrder).toEqual([1]); expect([...data.commutativeKeys]).toEqual([1]); }); it('allows reserveLayer to be called after registering an optimistc layer', () => { InMemoryData.noopDataState(data, 1, true); expect(data.optimisticOrder).toEqual([1]); expect(data.commutativeKeys.size).toBe(0); InMemoryData.reserveLayer(data, 1); expect(data.optimisticOrder).toEqual([1]); expect([...data.commutativeKeys]).toEqual([1]); }); }); describe('deferred changes', () => { it('keeps a deferred layer around until completion', () => { // initially it's unknown whether a layer is deferred InMemoryData.reserveLayer(data, 1, true); InMemoryData.reserveLayer(data, 2); InMemoryData.reserveLayer(data, 2); InMemoryData.initDataState('write', data, 2); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); // The layers must not be squashed expect(data.optimisticOrder).toEqual([2, 1]); // A future response may then clear the layer InMemoryData.reserveLayer(data, 1, false); InMemoryData.initDataState('write', data, 1); InMemoryData.writeRecord('Query', 'index', 1); InMemoryData.clearDataState(); // The layers must then be squashed expect(data.optimisticOrder).toEqual([]); }); it('does not erase data from a prior deferred layer when updating it', () => { // initially it's unknown whether a layer is deferred InMemoryData.reserveLayer(data, 1, true); InMemoryData.reserveLayer(data, 2, true); InMemoryData.initDataState('write', data, 2); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); // A subsequent reserve layer call should not erase the layer InMemoryData.reserveLayer(data, 2, true); InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); // The layers must not be squashed expect(data.optimisticOrder).toEqual([2, 1]); }); it('keeps a deferred layer around even if it is the lowest', () => { // initially it's unknown whether a layer is deferred InMemoryData.reserveLayer(data, 1); InMemoryData.reserveLayer(data, 2); InMemoryData.reserveLayer(data, 3); InMemoryData.initDataState('write', data, 2); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); // Mark layer 3 as deferred InMemoryData.reserveLayer(data, 3, true); // The value is unchanged InMemoryData.initDataState('read', data, null); expect(InMemoryData.readRecord('Query', 'index')).toBe(2); // The layers must not be squashed expect(data.optimisticOrder).toEqual([3, 2, 1]); // A future response may not clear the layer InMemoryData.initDataState('write', data, 1); InMemoryData.writeRecord('Query', 'index', 1); InMemoryData.clearDataState(); expect(data.optimisticOrder).toEqual([3]); // The layers must then be squashed InMemoryData.noopDataState(data, 3, false); expect(data.optimisticOrder).toEqual([]); }); it('unmarks deferred layers when they receive a noop write', () => { // initially it's unknown whether a layer is deferred InMemoryData.reserveLayer(data, 1); InMemoryData.reserveLayer(data, 2); InMemoryData.reserveLayer(data, 2); InMemoryData.initDataState('write', data, 2); InMemoryData.writeRecord('Query', 'index', 2); InMemoryData.clearDataState(); // The layer is marked as deferred via re-reserving it InMemoryData.reserveLayer(data, 1, true); InMemoryData.initDataState('write', data, 1); InMemoryData.clearDataState(); // The layer is then receiving a noop write InMemoryData.noopDataState(data, 1, false); expect(data.optimisticOrder).toEqual([]); }); }); ================================================ FILE: exchanges/graphcache/src/store/data.ts ================================================ import { stringifyVariables } from '@urql/core'; import type { Link, EntityField, FieldInfo, StorageAdapter, SerializedEntries, Dependencies, OperationType, DataField, Data, } from '../types'; import { serializeKeys, deserializeKeyInfo, fieldInfoOfKey, joinKeys, } from './keys'; import { invariant, currentDebugStack } from '../helpers/help'; type Dict = Record; type KeyMap = Map; type OperationMap = Map; interface NodeMap { optimistic: OperationMap>>; base: KeyMap>; } export interface InMemoryData { /** Flag for whether the data is waiting for hydration */ hydrating: boolean; /** Flag for whether deferred tasks have been scheduled yet */ defer: boolean; /** A list of entities that have been flagged for gargabe collection since no references to them are left */ gc: Set; /** A list of entity+field keys that will be persisted */ persist: Set; /** The API's "Query" typename which is needed to filter dependencies */ queryRootKey: string; /** Number of references to each entity (except "Query") */ refCount: KeyMap; /** A map of entity fields (key-value entries per entity) */ records: NodeMap; /** A map of entity links which are connections from one entity to another (key-value entries per entity) */ links: NodeMap; /** A map of typename to a list of entity-keys belonging to said type */ types: Map>; /** A set of Query operation keys that are in-flight and deferred/streamed */ deferredKeys: Set; /** A set of Query operation keys that are in-flight and awaiting a result */ commutativeKeys: Set; /** A set of Query operation keys that have been written to */ dirtyKeys: Set; /** The order of optimistic layers */ optimisticOrder: number[]; /** This may be a persistence adapter that will receive changes in a batch */ storage: StorageAdapter | null; /** A map of all the types we have encountered that did not map directly to a concrete type */ abstractToConcreteMap: Map>; } let currentOwnership: null | WeakSet = null; let currentDataMapping: null | WeakMap = null; let currentData: null | InMemoryData = null; let currentOptimisticKey: null | number = null; export let currentOperation: null | OperationType = null; export let currentDependencies: null | Dependencies = null; export let currentForeignData = false; export let currentOptimistic = false; export function makeData(data: DataField | void, isArray?: false): Data; export function makeData(data: DataField | void, isArray: true): DataField[]; /** Creates a new data object unless it's been created in this data run */ export function makeData(data?: DataField | void, isArray?: boolean) { let newData: Data | Data[] | undefined; if (data) { if (currentOwnership!.has(data)) return data; newData = currentDataMapping!.get(data) as any; } if (newData == null) { newData = (isArray ? [] : {}) as any; } if (data) { currentDataMapping!.set(data, newData); } currentOwnership!.add(newData); return newData; } export const ownsData = (data?: Data): boolean => !!data && currentOwnership!.has(data); /** Before reading or writing the global state needs to be initialised */ export const initDataState = ( operationType: OperationType, data: InMemoryData, layerKey?: number | null, isOptimistic?: boolean, isForeignData?: boolean ) => { currentOwnership = new WeakSet(); currentDataMapping = new WeakMap(); currentOperation = operationType; currentData = data; currentDependencies = new Set(); currentOptimistic = !!isOptimistic; currentForeignData = !!isForeignData; if (process.env.NODE_ENV !== 'production') { currentDebugStack.length = 0; } if (!layerKey) { currentOptimisticKey = null; } else if (currentOperation === 'read') { // We don't create new layers for read operations and instead simply // apply the currently available layer, if any currentOptimisticKey = layerKey; } else if ( isOptimistic || data.hydrating || data.optimisticOrder.length > 1 ) { // If this operation isn't optimistic and we see it for the first time, // then it must've been optimistic in the past, so we can proactively // clear the optimistic data before writing if (!isOptimistic && !data.commutativeKeys.has(layerKey)) { reserveLayer(data, layerKey); } else if (isOptimistic) { if ( data.optimisticOrder.indexOf(layerKey) !== -1 && !data.commutativeKeys.has(layerKey) ) { data.optimisticOrder.splice(data.optimisticOrder.indexOf(layerKey), 1); } // NOTE: This optimally shouldn't happen as it implies that an optimistic // write is being performed after a concrete write. data.commutativeKeys.delete(layerKey); } // An optimistic update of a mutation may force an optimistic layer, // or this Query update may be applied optimistically since it's part // of a commutative chain currentOptimisticKey = layerKey; createLayer(data, layerKey); } else { // Otherwise we don't create an optimistic layer and clear the // operation's one if it already exists // We also do this when only one layer exists to avoid having to squash // any layers at the end of writing this layer currentOptimisticKey = null; deleteLayer(data, layerKey); } }; /** Reset the data state after read/write is complete */ export const clearDataState = () => { // NOTE: This is only called to check for the invariant to pass if (process.env.NODE_ENV !== 'production') { getCurrentDependencies(); } const data = currentData!; const layerKey = currentOptimisticKey; currentOptimistic = false; currentOptimisticKey = null; // Determine whether the current operation has been a commutative layer if ( !data.hydrating && layerKey && data.optimisticOrder.indexOf(layerKey) > -1 ) { // Squash all layers in reverse order (low priority upwards) that have // been written already let i = data.optimisticOrder.length; while ( --i >= 0 && data.dirtyKeys.has(data.optimisticOrder[i]) && data.commutativeKeys.has(data.optimisticOrder[i]) ) squashLayer(data.optimisticOrder[i]); } currentOwnership = null; currentDataMapping = null; currentOperation = null; currentData = null; currentDependencies = null; if (process.env.NODE_ENV !== 'production') { currentDebugStack.length = 0; } if (process.env.NODE_ENV !== 'test') { // Schedule deferred tasks if we haven't already, and if either a persist or GC run // are likely to be needed if (!data.defer && (data.storage || !data.optimisticOrder.length)) { data.defer = true; setTimeout(() => { initDataState('read', data, null); gc(); persistData(); clearDataState(); data.defer = false; }); } } }; /** Initialises then resets the data state, which may squash this layer if necessary */ export const noopDataState = ( data: InMemoryData, layerKey: number | null, isOptimistic?: boolean ) => { if (layerKey && !isOptimistic) data.deferredKeys.delete(layerKey); initDataState('write', data, layerKey, isOptimistic); clearDataState(); }; /** As we're writing, we keep around all the records and links we've read or have written to */ export const getCurrentDependencies = (): Dependencies => { invariant( currentDependencies !== null, 'Invalid Cache call: The cache may only be accessed or mutated during' + 'operations like write or query, or as part of its resolvers, updaters, ' + 'or optimistic configs.', 2 ); return currentDependencies; }; const DEFAULT_EMPTY_SET = new Set(); export const make = (queryRootKey: string): InMemoryData => ({ hydrating: false, defer: false, gc: new Set(), types: new Map(), persist: new Set(), queryRootKey, refCount: new Map(), links: { optimistic: new Map(), base: new Map(), }, abstractToConcreteMap: new Map(), records: { optimistic: new Map(), base: new Map(), }, deferredKeys: new Set(), commutativeKeys: new Set(), dirtyKeys: new Set(), optimisticOrder: [], storage: null, }); /** Adds a node value to a NodeMap (taking optimistic values into account */ const setNode = ( map: NodeMap, entityKey: string, fieldKey: string, value: T ) => { if (process.env.NODE_ENV !== 'production') { invariant( currentOperation !== 'read', 'Invalid Cache write: You may not write to the cache during cache reads. ' + ' Accesses to `cache.writeFragment`, `cache.updateQuery`, and `cache.link` may ' + ' not be made inside `resolvers` for instance.', 27 ); } // Optimistic values are written to a map in the optimistic dict // All other values are written to the base map const keymap: KeyMap> = currentOptimisticKey ? map.optimistic.get(currentOptimisticKey)! : map.base; // On the map itself we get or create the entity as a dict let entity = keymap.get(entityKey) as Dict; if (entity === undefined) { keymap.set(entityKey, (entity = Object.create(null))); } // If we're setting undefined we delete the node's entry // On optimistic layers we actually set undefined so it can // override the base value if (value === undefined && !currentOptimisticKey) { delete entity[fieldKey]; } else { entity[fieldKey] = value; } }; /** Gets a node value from a NodeMap (taking optimistic values into account */ const getNode = ( map: NodeMap, entityKey: string, fieldKey: string ): T | undefined => { let node: Dict | undefined; // A read may be initialised to skip layers until its own, which is useful for // reading back written data. It won't skip over optimistic layers however let skip = !currentOptimistic && currentOperation === 'read' && currentOptimisticKey && currentData!.commutativeKeys.has(currentOptimisticKey); // This first iterates over optimistic layers (in order) for (let i = 0, l = currentData!.optimisticOrder.length; i < l; i++) { const layerKey = currentData!.optimisticOrder[i]; const optimistic = map.optimistic.get(layerKey); // If we're reading starting from a specific layer, we skip until a match skip = skip && layerKey !== currentOptimisticKey; // If the node and node value exists it is returned, including undefined if ( optimistic && (!skip || !currentData!.commutativeKeys.has(layerKey)) && (!currentOptimistic || currentOperation === 'write' || currentData!.commutativeKeys.has(layerKey)) && (node = optimistic.get(entityKey)) !== undefined && fieldKey in node ) { return node[fieldKey]; } } // Otherwise we read the non-optimistic base value node = map.base.get(entityKey); return node !== undefined ? node[fieldKey] : undefined; }; export function getRefCount(entityKey: string): number { return currentData!.refCount.get(entityKey) || 0; } /** Adjusts the reference count of an entity on a refCount dict by "by" and updates the gc */ const updateRCForEntity = (entityKey: string, by: number): void => { // Retrieve the reference count and adjust it by "by" const count = getRefCount(entityKey); const newCount = count + by > 0 ? count + by : 0; currentData!.refCount.set(entityKey, newCount); // Add it to the garbage collection batch if it needs to be deleted or remove it // from the batch if it needs to be kept if (!newCount) currentData!.gc.add(entityKey); else if (!count && newCount) currentData!.gc.delete(entityKey); }; /** Adjusts the reference counts of all entities of a link on a refCount dict by "by" and updates the gc */ const updateRCForLink = (link: Link | undefined, by: number): void => { if (Array.isArray(link)) { for (let i = 0, l = link.length; i < l; i++) updateRCForLink(link[i], by); } else if (typeof link === 'string') { updateRCForEntity(link, by); } }; /** Writes all parsed FieldInfo objects of a given node dict to a given array if it hasn't been seen */ const extractNodeFields = ( fieldInfos: FieldInfo[], seenFieldKeys: Set, node: Dict | undefined ): void => { if (node !== undefined) { for (const fieldKey in node) { if (!seenFieldKeys.has(fieldKey)) { // If the node hasn't been seen the serialized fieldKey is turnt back into // a rich FieldInfo object that also contains the field's name and arguments fieldInfos.push(fieldInfoOfKey(fieldKey)); seenFieldKeys.add(fieldKey); } } } }; /** Writes all parsed FieldInfo objects of all nodes in a NodeMap to a given array */ const extractNodeMapFields = ( fieldInfos: FieldInfo[], seenFieldKeys: Set, entityKey: string, map: NodeMap ) => { // Extracts FieldInfo for the entity in the base map extractNodeFields(fieldInfos, seenFieldKeys, map.base.get(entityKey)); // Then extracts FieldInfo for the entity from the optimistic maps for (let i = 0, l = currentData!.optimisticOrder.length; i < l; i++) { const optimistic = map.optimistic.get(currentData!.optimisticOrder[i]); if (optimistic !== undefined) { extractNodeFields(fieldInfos, seenFieldKeys, optimistic.get(entityKey)); } } }; /** Garbage collects all entities that have been marked as having no references */ export const gc = () => { // If we're currently awaiting deferred results, abort GC run if (currentData!.optimisticOrder.length) return; // Iterate over all entities that have been marked for deletion // Entities have been marked for deletion in `updateRCForEntity` if // their reference count dropped to 0 for (const entityKey of currentData!.gc.keys()) { // Remove the current key from the GC batch currentData!.gc.delete(entityKey); // Check first whether the entity has any references, // if so, we skip it from the GC run const rc = getRefCount(entityKey); if (rc > 0) continue; const record = currentData!.records.base.get(entityKey); // Delete the reference count, and delete the entity from the GC batch currentData!.refCount.delete(entityKey); currentData!.records.base.delete(entityKey); const typename = (record && record.__typename) as string | undefined; if (typename) { const type = currentData!.types.get(typename); if (type) type.delete(entityKey); } const linkNode = currentData!.links.base.get(entityKey); if (linkNode) { currentData!.links.base.delete(entityKey); for (const fieldKey in linkNode) updateRCForLink(linkNode[fieldKey], -1); } } }; const updateDependencies = (entityKey: string, fieldKey?: string) => { if (entityKey !== currentData!.queryRootKey) { currentDependencies!.add(entityKey); } else if (fieldKey !== undefined && fieldKey !== '__typename') { currentDependencies!.add(joinKeys(entityKey, fieldKey)); } }; const updatePersist = (entityKey: string, fieldKey: string) => { if (!currentOptimistic && currentData!.storage) { currentData!.persist.add(serializeKeys(entityKey, fieldKey)); } }; /** Reads an entity's field (a "record") from data */ export const readRecord = ( entityKey: string, fieldKey: string ): EntityField => { if (currentOperation === 'read') { updateDependencies(entityKey, fieldKey); } return getNode(currentData!.records, entityKey, fieldKey); }; /** Reads an entity's link from data */ export const readLink = ( entityKey: string, fieldKey: string ): Link | undefined => { if (currentOperation === 'read') { updateDependencies(entityKey, fieldKey); } return getNode(currentData!.links, entityKey, fieldKey); }; export const getEntitiesForType = (typename: string): Set => currentData!.types.get(typename) || DEFAULT_EMPTY_SET; export const writeType = (typename: string, entityKey: string) => { const existingTypes = currentData!.types.get(typename); if (!existingTypes) { const typeSet = new Set(); typeSet.add(entityKey); currentData!.types.set(typename, typeSet); } else { existingTypes.add(entityKey); } }; export const getConcreteTypes = (typename: string): Set => currentData!.abstractToConcreteMap.get(typename) || DEFAULT_EMPTY_SET; export const isSeenConcreteType = (typename: string): boolean => currentData!.types.has(typename); export const writeConcreteType = ( abstractType: string, concreteType: string ) => { const existingTypes = currentData!.abstractToConcreteMap.get(abstractType); if (!existingTypes) { const typeSet = new Set(); typeSet.add(concreteType); currentData!.abstractToConcreteMap.set(abstractType, typeSet); } else { existingTypes.add(concreteType); } }; /** Writes an entity's field (a "record") to data */ export const writeRecord = ( entityKey: string, fieldKey: string, value?: EntityField ) => { const existing = getNode(currentData!.records, entityKey, fieldKey); if (!isEqualLinkOrScalar(existing, value)) { updateDependencies(entityKey, fieldKey); updatePersist(entityKey, fieldKey); } setNode(currentData!.records, entityKey, fieldKey, value); }; export const hasField = (entityKey: string, fieldKey: string): boolean => readRecord(entityKey, fieldKey) !== undefined || readLink(entityKey, fieldKey) !== undefined; /** Writes an entity's link to data */ export const writeLink = ( entityKey: string, fieldKey: string, link?: Link | undefined ) => { // Retrieve the link NodeMap from either an optimistic or the base layer const links = currentOptimisticKey ? currentData!.links.optimistic.get(currentOptimisticKey) : currentData!.links.base; // Update the reference count for the link if (!currentOptimisticKey) { const entityLinks = links && links.get(entityKey); updateRCForLink(entityLinks && entityLinks[fieldKey], -1); updateRCForLink(link, 1); } const existing = getNode(currentData!.links, entityKey, fieldKey); if (!isEqualLinkOrScalar(existing, link)) { updateDependencies(entityKey, fieldKey); updatePersist(entityKey, fieldKey); } // Update the link setNode(currentData!.links, entityKey, fieldKey, link); }; /** Reserves an optimistic layer and preorders it */ export const reserveLayer = ( data: InMemoryData, layerKey: number, hasNext?: boolean ) => { // Find the current index for the layer, and remove it from // the order if it exists already let index = data.optimisticOrder.indexOf(layerKey); if (index > -1) data.optimisticOrder.splice(index, 1); if (hasNext) { data.deferredKeys.add(layerKey); // If the layer has future results then we'll move it past any layer that's // still empty, so currently pending operations will take precedence over it for ( index = index > -1 ? index : 0; index < data.optimisticOrder.length && !data.deferredKeys.has(data.optimisticOrder[index]) && (!data.dirtyKeys.has(data.optimisticOrder[index]) || !data.commutativeKeys.has(data.optimisticOrder[index])); index++ ); } else { data.deferredKeys.delete(layerKey); // Protect optimistic layers from being turned into non-optimistic layers // while preserving optimistic data if (index > -1 && !data.commutativeKeys.has(layerKey)) clearLayer(data, layerKey); index = 0; } // Register the layer with the deferred or "top" index and // mark it as commutative data.optimisticOrder.splice(index, 0, layerKey); data.commutativeKeys.add(layerKey); }; /** Checks whether a given layer exists */ export const hasLayer = (data: InMemoryData, layerKey: number) => data.commutativeKeys.has(layerKey) || data.optimisticOrder.indexOf(layerKey) > -1; /** Creates an optimistic layer of links and records */ const createLayer = (data: InMemoryData, layerKey: number) => { if (data.optimisticOrder.indexOf(layerKey) === -1) { data.optimisticOrder.unshift(layerKey); } if (!data.dirtyKeys.has(layerKey)) { data.dirtyKeys.add(layerKey); data.links.optimistic.set(layerKey, new Map()); data.records.optimistic.set(layerKey, new Map()); } }; /** Clears all links and records of an optimistic layer */ const clearLayer = (data: InMemoryData, layerKey: number) => { if (data.dirtyKeys.has(layerKey)) { data.dirtyKeys.delete(layerKey); data.records.optimistic.delete(layerKey); data.links.optimistic.delete(layerKey); data.deferredKeys.delete(layerKey); } }; /** Deletes links and records of an optimistic layer, and the layer itself */ const deleteLayer = (data: InMemoryData, layerKey: number) => { const index = data.optimisticOrder.indexOf(layerKey); if (index > -1) { data.optimisticOrder.splice(index, 1); data.commutativeKeys.delete(layerKey); } clearLayer(data, layerKey); }; /** Merges an optimistic layer of links and records into the base data */ const squashLayer = (layerKey: number) => { // Hide current dependencies from squashing operations const previousDependencies = currentDependencies; currentDependencies = new Set(); currentOperation = 'write'; const links = currentData!.links.optimistic.get(layerKey); if (links) { for (const entry of links.entries()) { const entityKey = entry[0]; const keyMap = entry[1]; for (const fieldKey in keyMap) { writeLink(entityKey, fieldKey, keyMap[fieldKey]); } } } const records = currentData!.records.optimistic.get(layerKey); if (records) { for (const entry of records.entries()) { const entityKey = entry[0]; const keyMap = entry[1]; for (const fieldKey in keyMap) { writeRecord(entityKey, fieldKey, keyMap[fieldKey]); } } } currentDependencies = previousDependencies; deleteLayer(currentData!, layerKey); }; /** Return an array of FieldInfo (info on all the fields and their arguments) for a given entity */ export const inspectFields = (entityKey: string): FieldInfo[] => { const { links, records } = currentData!; const fieldInfos: FieldInfo[] = []; const seenFieldKeys: Set = new Set(); // Update dependencies updateDependencies(entityKey); // Extract FieldInfos to the fieldInfos array for links and records // This also deduplicates by keeping track of fieldKeys in the seenFieldKeys Set extractNodeMapFields(fieldInfos, seenFieldKeys, entityKey, links); extractNodeMapFields(fieldInfos, seenFieldKeys, entityKey, records); return fieldInfos; }; export const persistData = () => { if (currentData!.storage) { currentOptimistic = true; currentOperation = 'read'; const entries: SerializedEntries = {}; for (const key of currentData!.persist.keys()) { const { entityKey, fieldKey } = deserializeKeyInfo(key); let x: void | Link | EntityField; if ((x = readLink(entityKey, fieldKey)) !== undefined) { entries[key] = `:${stringifyVariables(x)}`; } else if ((x = readRecord(entityKey, fieldKey)) !== undefined) { entries[key] = stringifyVariables(x); } else { entries[key] = undefined; } } currentOptimistic = false; currentData!.storage.writeData(entries); currentData!.persist.clear(); } }; export const hydrateData = ( data: InMemoryData, storage: StorageAdapter, entries: SerializedEntries ) => { initDataState('write', data, null); for (const key in entries) { const value = entries[key]; if (value !== undefined) { const { entityKey, fieldKey } = deserializeKeyInfo(key); if (value[0] === ':') { if (readLink(entityKey, fieldKey) === undefined) writeLink(entityKey, fieldKey, JSON.parse(value.slice(1))); } else { if (readRecord(entityKey, fieldKey) === undefined) writeRecord(entityKey, fieldKey, JSON.parse(value)); } } } data.storage = storage; data.hydrating = false; clearDataState(); }; function isEqualLinkOrScalar( a: Link | EntityField | undefined, b: Link | EntityField | undefined ) { if (typeof a !== typeof b) return false; if (a !== b) return false; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; return !a.some((el, index) => el !== b[index]); } return true; } ================================================ FILE: exchanges/graphcache/src/store/keys.ts ================================================ import { stringifyVariables } from '@urql/core'; import type { FieldArgs, FieldInfo, KeyInfo } from '../types'; export const keyOfField = (fieldName: string, args?: FieldArgs) => args ? `${fieldName}(${stringifyVariables(args)})` : fieldName; export const joinKeys = (parentKey: string, key: string) => `${parentKey}.${key}`; export const fieldInfoOfKey = (fieldKey: string): FieldInfo => { const parenIndex = fieldKey.indexOf('('); if (parenIndex > -1) { return { fieldKey, fieldName: fieldKey.slice(0, parenIndex), arguments: JSON.parse(fieldKey.slice(parenIndex + 1, -1)), }; } else { return { fieldKey, fieldName: fieldKey, arguments: null, }; } }; export const serializeKeys = (entityKey: string, fieldKey: string) => `${entityKey.replace(/\./g, '%2e')}.${fieldKey}`; export const deserializeKeyInfo = (key: string): KeyInfo => { const dotIndex = key.indexOf('.'); const entityKey = key.slice(0, dotIndex).replace(/%2e/g, '.'); const fieldKey = key.slice(dotIndex + 1); return { entityKey, fieldKey }; }; ================================================ FILE: exchanges/graphcache/src/store/store.test.ts ================================================ /* eslint-disable @typescript-eslint/no-var-requires */ import { minifyIntrospectionQuery } from '@urql/introspection'; import { formatDocument, gql } from '@urql/core'; import { vi, expect, it, beforeEach, describe } from 'vitest'; import { executeSync, getIntrospectionQuery, buildClientSchema, parse, } from 'graphql'; import { Data, StorageAdapter } from '../types'; import { makeContext, updateContext } from '../operations/shared'; import * as InMemoryData from './data'; import { Store } from './store'; import { noop } from '../test-utils/utils'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write, __initAnd_writeOptimistic as writeOptimistic, } from '../operations/write'; const mocked = (x: any): any => x; const Appointment = gql` query appointment($id: String) { __typename appointment(id: $id) { __typename id info } } `; const Todos = gql` query { __typename todos { __typename id text complete author { __typename id name } } } `; const TodosWithoutTypename = gql` query { __typename todos { id text complete author { id name } } } `; const todosData = { __typename: 'Query', todos: [ { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo', author: { id: '0', name: 'Jovi', __typename: 'Author' }, }, { id: '1', text: 'Pick up the kids', complete: true, __typename: 'Todo', author: { id: '1', name: 'Phil', __typename: 'Author' }, }, { id: '2', text: 'Install urql', complete: false, __typename: 'Todo', author: { id: '0', name: 'Jovi', __typename: 'Author' }, }, ], } as any; describe('Store', () => { it('supports unformatted query documents', () => { const store = new Store(); // NOTE: This is the query without __typename annotations write(store, { query: TodosWithoutTypename }, todosData); const result = query(store, { query: TodosWithoutTypename }); expect(result.data).toEqual({ todos: [ { id: '0', text: 'Go to the shops', complete: false, author: { id: '0', name: 'Jovi' }, }, { id: '1', text: 'Pick up the kids', complete: true, author: { id: '1', name: 'Phil' }, }, { id: '2', text: 'Install urql', complete: false, author: { id: '0', name: 'Jovi' }, }, ], __typename: 'Query', }); }); }); describe('Store with UpdatesConfig', () => { it("sets the store's updates field to the given argument", () => { const updatesOption = { Mutation: { toggleTodo: noop, }, Subscription: { newTodo: noop, }, }; const store = new Store({ updates: updatesOption, }); expect(store.updates.Mutation).toBe(updatesOption.Mutation); expect(store.updates.Subscription).toBe(updatesOption.Subscription); }); it('should not warn if Mutation/Subscription operations do exist in the schema', function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), updates: { Mutation: { toggleTodo: noop, }, Subscription: { newTodo: noop, }, }, }); expect(console.warn).not.toBeCalled(); }); it("should warn if Mutation operations don't exist in the schema", function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), updates: { Mutation: { doTheChaChaSlide: noop, }, }, }); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain( 'Invalid updates field: `doTheChaChaSlide` on `Mutation` is not in the defined schema' ); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#22'); }); it("should warn if Subscription operations don't exist in the schema", function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), updates: { Subscription: { someoneDidTheChaChaSlide: noop, }, }, }); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain( 'Invalid updates field: `someoneDidTheChaChaSlide` on `Subscription` is not in the defined schema' ); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#22'); }); }); describe('Store with KeyingConfig', () => { it('generates keys from custom keying function', () => { const store = new Store({ keys: { User: () => 'me', None: () => null, }, }); expect(store.keyOfEntity({ __typename: 'Any', id: '123' })).toBe('Any:123'); expect(store.keyOfEntity({ __typename: 'Any', _id: '123' })).toBe( 'Any:123' ); expect(store.keyOfEntity({ __typename: 'Any' })).toBe(null); expect(store.keyOfEntity({ __typename: 'User' })).toBe('User:me'); expect(store.keyOfEntity({ __typename: 'None' })).toBe(null); }); it('should not warn if keys do exist in the schema', function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), keys: { Todo: () => 'Todo', }, }); expect(console.warn).not.toBeCalled(); }); it("should warn if a key doesn't exist in the schema", function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), keys: { Todo: () => 'todo', NotInSchema: () => 'foo', }, }); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain( 'The type `NotInSchema` is not an object in the defined schema, but the `keys` option is referencing it' ); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#20'); }); }); describe('Store with Global IDs', () => { it('generates keys without typenames when set to true', () => { const store = new Store({ globalIDs: true }); expect(store.keyOfEntity({ __typename: 'Any', id: '123' })).toBe('123'); expect(store.keyOfEntity({ __typename: 'None', id: '123' })).toBe('123'); }); it('generates keys without typenames when matching an input set', () => { const store = new Store({ globalIDs: ['User'] }); expect(store.keyOfEntity({ __typename: 'Any', id: '123' })).toBe('Any:123'); expect(store.keyOfEntity({ __typename: 'User', id: '123' })).toBe('123'); }); }); describe('Store with ResolverConfig', () => { it("sets the store's resolvers field to the given argument", () => { const resolversOption = { Query: { latestTodo: () => 'todo', }, }; const store = new Store({ resolvers: resolversOption, }); expect(store.resolvers).toBe(resolversOption); }); it("sets the store's resolvers field to an empty default if not provided", () => { const store = new Store({}); expect(store.resolvers).toEqual({}); }); it('should not warn if resolvers do exist in the schema', function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), resolvers: { Query: { latestTodo: () => 'todo', todos: () => ['todo 1', 'todo 2'], }, Todo: { text: todo => (todo.text as string).toUpperCase(), author: todo => (todo.author as string).toUpperCase(), }, }, }); expect(console.warn).not.toBeCalled(); }); it("should warn if a Query doesn't exist in the schema", function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), resolvers: { Query: { todos: () => ['todo 1', 'todo 2'], // This query should be warned about. findDeletedTodos: () => ['todo 1', 'todo 2'], }, }, }); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain( 'Invalid resolver: `Query.findDeletedTodos` is not in the defined schema, but the `resolvers` option is referencing it' ); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23'); }); it("should warn if a type doesn't exist in the schema", function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), resolvers: { Todo: { complete: () => true, }, // This type should be warned about. Dinosaur: { isExtinct: () => true, }, }, }); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain( 'Invalid resolver: `Dinosaur` is not in the defined schema, but the `resolvers` option is referencing it' ); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23'); }); it('should warn when we use an interface type', function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), resolvers: { ITodo: { complete: () => true, }, }, }); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain( 'Invalid resolver: `ITodo` does not match to a concrete type in the schema, but the `resolvers` option is referencing it. Implement the resolver for the types that implement the interface instead.' ); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#26'); }); it("should warn if a type's property doesn't exist in the schema", function () { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), resolvers: { Todo: { complete: () => true, // This property should be warned about. isAboutDinosaurs: () => true, }, }, }); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain( 'Invalid resolver: `Todo.isAboutDinosaurs` is not in the defined schema, but the `resolvers` option is referencing it' ); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#23'); }); }); describe('Store with OptimisticMutationConfig', () => { let store; let context; beforeEach(() => { store = new Store({ optimistic: { addTodo: variables => { return { ...variables, } as Data; }, }, }); context = makeContext(store, {}, {}, 'Query', 'Query', undefined); write(store, { query: Todos }, todosData); InMemoryData.initDataState('read', store.data, null); }); it('should resolve a property', () => { const todoResult = store.resolve({ __typename: 'Todo', id: '0' }, 'text'); expect(todoResult).toEqual('Go to the shops'); const authorResult = store.resolve( { __typename: 'Author', id: '0' }, 'name' ); expect(authorResult).toBe('Jovi'); const result = store.resolve({ id: 0, __typename: 'Todo' }, 'text'); expect(result).toEqual('Go to the shops'); // TODO: we have no way of asserting this to really be the case. const deps = InMemoryData.getCurrentDependencies(); expect(deps).toEqual(new Set(['Todo:0', 'Author:0'])); InMemoryData.clearDataState(); }); it('should resolve current parent argument fields', () => { const randomData = { __typename: 'Todo', id: 1, createdAt: '2020-12-09' }; updateContext( context, randomData, 'Todo', 'Todo:1', 'createdAt', 'createdAt' ); expect(store.keyOfEntity(randomData)).toBe(context.parentKey); expect(store.keyOfEntity({})).not.toBe(context.parentKey); // Should work without a __typename field delete (randomData as any).__typename; expect(store.keyOfEntity(randomData)).toBe(context.parentKey); }); it('should resolve with a key as first argument', () => { const authorResult = store.resolve('Author:0', 'name'); expect(authorResult).toBe('Jovi'); const deps = InMemoryData.getCurrentDependencies(); expect(deps).toEqual(new Set(['Author:0'])); InMemoryData.clearDataState(); }); it('should resolve a link property', () => { const parent = { id: '0', text: 'test', author: undefined, __typename: 'Todo', }; const result = store.resolve(parent, 'author'); expect(result).toEqual('Author:0'); const deps = InMemoryData.getCurrentDependencies(); expect(deps).toEqual(new Set(['Todo:0'])); InMemoryData.clearDataState(); }); it('should invalidate null keys correctly', () => { const connection = gql` query test { exercisesConnection(page: { after: null, first: 10 }) { id } } `; write( store, { query: connection, }, { exercisesConnection: null, } as any ); let { data } = query(store, { query: connection }); InMemoryData.initDataState('write', store.data, null); expect((data as any).exercisesConnection).toEqual(null); const fields = store.inspectFields({ __typename: 'Query' }); fields.forEach(({ fieldName, arguments: args }) => { if (fieldName === 'exercisesConnection') { store.invalidate('Query', fieldName, args); } }); InMemoryData.clearDataState(); ({ data } = query(store, { query: connection })); expect(data).toBe(null); }); it('should be able to write a fragment', () => { InMemoryData.initDataState('write', store.data, null); store.writeFragment( gql` fragment _ on Todo { id text complete } `, { id: '0', text: 'update', complete: true, } ); const deps = InMemoryData.getCurrentDependencies(); expect(deps).toEqual(new Set(['Todo:0'])); const { data } = query(store, { query: Todos }); expect(data).toEqual({ __typename: 'Query', todos: [ { ...todosData.todos[0], text: 'update', complete: true, }, todosData.todos[1], todosData.todos[2], ], }); }); it('should be able to write a fragment by name', () => { InMemoryData.initDataState('write', store.data, null); store.writeFragment( gql` fragment authorFields on Author { id } fragment todoFields on Todo { id text complete } `, { id: '0', text: 'update', complete: true, }, undefined, 'todoFields' ); const deps = InMemoryData.getCurrentDependencies(); expect(deps).toEqual(new Set(['Todo:0'])); const { data } = query(store, { query: Todos }); expect(data).toEqual({ __typename: 'Query', todos: [ { ...todosData.todos[0], text: 'update', complete: true, }, todosData.todos[1], todosData.todos[2], ], }); }); it('should be able to read a fragment', () => { InMemoryData.initDataState('read', store.data, null); const result = store.readFragment( gql` fragment _ on Todo { id text complete __typename } `, { id: '0' } ); const deps = InMemoryData.getCurrentDependencies(); expect(deps).toEqual(new Set(['Todo:0'])); expect(result).toEqual({ id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo', }); InMemoryData.clearDataState(); }); it('should be able to read a fragment by name', () => { InMemoryData.initDataState('read', store.data, null); const result = store.readFragment( gql` fragment authorFields on Author { id text complete __typename } fragment todoFields on Todo { id text complete __typename } `, { id: '0' }, undefined, 'todoFields' ); const deps = InMemoryData.getCurrentDependencies(); expect(deps).toEqual(new Set(['Todo:0'])); expect(result).toEqual({ id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo', }); InMemoryData.clearDataState(); }); it('should be able to update a query', () => { InMemoryData.initDataState('write', store.data, null); store.updateQuery({ query: Todos }, data => ({ ...data, todos: [ ...data.todos, { __typename: 'Todo', id: '4', text: 'Test updateQuery', complete: false, author: { __typename: 'Author', id: '3', name: 'Andy', }, }, ], })); InMemoryData.clearDataState(); const { data: result } = query(store, { query: Todos, }); expect(result).toEqual({ __typename: 'Query', todos: [ ...todosData.todos, { __typename: 'Todo', id: '4', text: 'Test updateQuery', complete: false, author: { __typename: 'Author', id: '3', name: 'Andy', }, }, ], }); }); it('should be able to update a query with variables', () => { write( store, { query: Appointment, variables: { id: '1' }, }, { __typename: 'Query', appointment: { __typename: 'Appointment', id: '1', info: 'urql meeting', }, } ); InMemoryData.initDataState('write', store.data, null); store.updateQuery({ query: Appointment, variables: { id: '1' } }, data => ({ ...data, appointment: { ...data.appointment, info: 'urql meeting revisited', }, })); InMemoryData.clearDataState(); const { data: result } = query(store, { query: Appointment, variables: { id: '1' }, }); expect(result).toEqual({ __typename: 'Query', appointment: { id: '1', info: 'urql meeting revisited', __typename: 'Appointment', }, }); }); it('should be able to read a query', () => { InMemoryData.initDataState('read', store.data, null); const result = store.readQuery({ query: Todos }); const deps = InMemoryData.getCurrentDependencies(); expect(deps).toEqual( new Set([ 'Query.todos', 'Todo:0', 'Todo:1', 'Todo:2', 'Author:0', 'Author:1', ]) ); expect(result).toEqual({ __typename: 'Query', todos: todosData.todos, }); InMemoryData.clearDataState(); }); it('should be able to optimistically mutate', () => { const { dependencies } = writeOptimistic( store, { query: gql` mutation { addTodo( id: "1" text: "I'm optimistic about this feature" complete: true __typename: "Todo" ) { id text complete __typename } } `, }, 1 ); expect(dependencies).toEqual(new Set(['Todo:1'])); let { data } = query(store, { query: Todos }); expect(data).toEqual({ __typename: 'Query', todos: [ todosData.todos[0], { id: '1', text: "I'm optimistic about this feature", complete: true, __typename: 'Todo', author: { __typename: 'Author', id: '1', name: 'Phil', }, }, todosData.todos[2], ], }); InMemoryData.noopDataState(store.data, 1); ({ data } = query(store, { query: Todos })); expect(data).toEqual({ __typename: 'Query', todos: todosData.todos, }); }); it('should be able to optimistically mutate with partial data', () => { const { dependencies } = writeOptimistic( store, { query: gql` mutation { addTodo(id: "0", complete: true, __typename: "Todo") { id text complete __typename } } `, }, 1 ); expect(dependencies).toEqual(new Set(['Todo:0'])); let { data } = query(store, { query: Todos }); expect(data).toEqual({ __typename: 'Query', todos: [ { ...todosData.todos[0], complete: true, }, todosData.todos[1], todosData.todos[2], ], }); InMemoryData.noopDataState(store.data, 1); ({ data } = query(store, { query: Todos })); expect(data).toEqual({ __typename: 'Query', todos: todosData.todos, }); }); describe('Invalidating an entity', () => { it('removes an entity from a list by object-key.', () => { InMemoryData.initDataState('write', store.data, null); store.invalidate(todosData.todos[1]); const { data } = query(store, { query: Todos }); expect(data).toBe(null); }); it('removes an entity from a list by string-key.', () => { InMemoryData.initDataState('write', store.data, null); store.invalidate(store.keyOfEntity(todosData.todos[1])); const { data } = query(store, { query: Todos }); expect(data).toBe(null); }); }); describe('Invalidating a type', () => { it('removes an entity from a list.', () => { InMemoryData.initDataState('write', store.data, null); store.invalidate('Todo'); const { data } = query(store, { query: Todos }); expect(data).toBe(null); }); }); }); describe('Store with storage', () => { let store: Store; const expectedData = { __typename: 'Query', appointment: { __typename: 'Appointment', id: '1', info: 'urql meeting', }, }; beforeEach(() => { store = new Store(); }); it('should be able to store and rehydrate data', () => { const storage: StorageAdapter = { readData: vi.fn(), writeData: vi.fn(), }; store.data.storage = storage; write( store, { query: Appointment, variables: { id: '1' }, }, expectedData ); InMemoryData.initDataState('write', store.data, null); InMemoryData.persistData(); InMemoryData.clearDataState(); expect(storage.writeData).toHaveBeenCalled(); const serialisedStore = (storage.writeData as any).mock.calls[0][0]; expect(serialisedStore).toMatchSnapshot(); store = new Store(); InMemoryData.hydrateData(store.data, storage, serialisedStore); const { data } = query(store, { query: Appointment, variables: { id: '1' }, }); expect(data).toEqual(expectedData); }); it('should be able to persist embedded data', () => { const EmbeddedAppointment = gql` query appointment($id: String) { __typename appointment(id: $id) { __typename info } } `; const embeddedData = { ...expectedData, appointment: { ...expectedData.appointment, id: undefined, }, } as any; const storage: StorageAdapter = { readData: vi.fn(), writeData: vi.fn(), }; store.data.storage = storage; write( store, { query: EmbeddedAppointment, variables: { id: '1' }, }, embeddedData ); InMemoryData.initDataState('write', store.data, null); InMemoryData.persistData(); InMemoryData.clearDataState(); expect(storage.writeData).toHaveBeenCalled(); const serialisedStore = (storage.writeData as any).mock.calls[0][0]; expect(serialisedStore).toMatchSnapshot(); store = new Store(); InMemoryData.hydrateData(store.data, storage, serialisedStore); const { data } = query(store, { query: EmbeddedAppointment, variables: { id: '1' }, }); expect(data).toEqual(embeddedData); }); it('persists commutative layers and ignores optimistic layers', () => { const storage: StorageAdapter = { readData: vi.fn(), writeData: vi.fn(), }; store.data.storage = storage; InMemoryData.reserveLayer(store.data, 1); InMemoryData.initDataState('write', store.data, 1); InMemoryData.writeRecord('Query', 'base', true); InMemoryData.clearDataState(); InMemoryData.initDataState('write', store.data, 2, true); InMemoryData.writeRecord('Query', 'base', false); InMemoryData.clearDataState(); InMemoryData.initDataState('read', store.data, null); expect(InMemoryData.readRecord('Query', 'base')).toBe(false); InMemoryData.persistData(); InMemoryData.clearDataState(); expect(storage.writeData).toHaveBeenCalled(); const serialisedStore = (storage.writeData as any).mock.calls[0][0]; expect(serialisedStore).toEqual({ 'Query.base': 'true', }); store = new Store(); InMemoryData.hydrateData(store.data, storage, serialisedStore); InMemoryData.initDataState('write', store.data, null); expect(InMemoryData.readRecord('Query', 'base')).toBe(true); InMemoryData.clearDataState(); }); it("should warn if an optimistic field doesn't exist in the schema's mutations", () => { new Store({ schema: minifyIntrospectionQuery( require('../test-utils/simple_schema.json') ), updates: { Mutation: { toggleTodo: noop, }, }, optimistic: { toggleTodo: () => null, // This field should be warned about. deleteTodo: () => null, }, }); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain( 'Invalid optimistic mutation field: `deleteTodo` is not a mutation field in the defined schema, but the `optimistic` option is referencing it.' ); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#24'); }); it('should use different rootConfigs', () => { const fakeUpdater = vi.fn(); const store = new Store({ schema: { __schema: { queryType: { name: 'query_root', }, mutationType: { name: 'mutation_root', }, subscriptionType: { name: 'subscription_root', }, }, }, updates: { mutation_root: { toggleTodo: fakeUpdater, }, }, }); const mutationData = { toggleTodo: { __typename: 'Todo', id: 1, }, }; write(store, { query: Todos }, todosData); write( store, { query: gql` mutation { toggleTodo(id: 1) { id } } `, }, mutationData as any ); expect(fakeUpdater).toBeCalledTimes(1); }); it('should warn when __typename is missing when store.writeFragment is called', () => { InMemoryData.initDataState('write', store.data, null); store.writeFragment( parse(` fragment _ on Test { __typename id sub { id } } `), { id: 'test', sub: { id: 'test', }, } ); InMemoryData.clearDataState(); expect(console.warn).toBeCalledTimes(1); const warnMessage = mocked(console.warn).mock.calls[0][0]; expect(warnMessage).toContain( "Couldn't find __typename when writing.\nIf you're writing to the cache manually have to pass a `__typename` property on each entity in your data." ); expect(warnMessage).toContain('https://bit.ly/2XbVrpR#14'); }); }); describe('Store introspection', () => { it('should not warn for an introspection result root (of an unminified schema)', function () { // NOTE: Do not wrap this require in `minifyIntrospectionQuery`! // eslint-disable-next-line const schema = require('../test-utils/simple_schema.json'); const store = new Store({ schema }); const introspectionQuery = formatDocument(parse(getIntrospectionQuery())); query(store, { query: introspectionQuery }, schema); expect(console.warn).toBeCalledTimes(0); }); it('should not warn for an introspection result root (of a minified schema)', function () { // NOTE: Do not wrap this require in `minifyIntrospectionQuery`! // eslint-disable-next-line const schema = require('../test-utils/simple_schema.json'); const store = new Store({ schema: minifyIntrospectionQuery(schema) }); const introspectionQuery = formatDocument(parse(getIntrospectionQuery())); query(store, { query: introspectionQuery }, schema); expect(console.warn).toBeCalledTimes(0); }); it('should not warn for an introspection result with typenames', function () { const schema = buildClientSchema( require('../test-utils/simple_schema.json') ); const introspectionQuery = formatDocument(parse(getIntrospectionQuery())); const introspectionResult = executeSync({ document: introspectionQuery, schema, }).data as any; const store = new Store({ schema: minifyIntrospectionQuery(introspectionResult), }); write(store, { query: introspectionQuery }, introspectionResult); query(store, { query: introspectionQuery }); expect(console.warn).toBeCalledTimes(0); }); }); it('should link up entities', () => { const store = new Store(); const todo = gql` query test { todo(id: "1") { id title __typename } } `; const author = gql` query testAuthor { author(id: "1") { id name __typename } } `; write( store, { query: todo, }, { todo: { id: '1', title: 'learn urql', __typename: 'Todo', }, __typename: 'Query', } as any ); let { data } = query(store, { query: todo }); expect((data as any).todo).toEqual({ id: '1', title: 'learn urql', __typename: 'Todo', }); write( store, { query: author, }, { author: { __typename: 'Author', id: '1', name: 'Formidable' }, __typename: 'Query', } as any ); InMemoryData.initDataState('write', store.data, null); store.link((data as any).todo, 'author', { __typename: 'Author', id: '1', name: 'Formidable', }); InMemoryData.clearDataState(); const todoWithAuthor = gql` query test { todo(id: "1") { id title __typename author { id name __typename } } } `; ({ data } = query(store, { query: todoWithAuthor })); expect((data as any).todo).toEqual({ id: '1', title: 'learn urql', __typename: 'Todo', author: { __typename: 'Author', id: '1', name: 'Formidable', }, }); }); ================================================ FILE: exchanges/graphcache/src/store/store.ts ================================================ import type { TypedDocumentNode } from '@urql/core'; import { formatDocument, createRequest } from '@urql/core'; import type { Cache, FieldInfo, ResolverConfig, DataField, Variables, FieldArgs, Link, Data, QueryInput, UpdatesConfig, OptimisticMutationConfig, KeyingConfig, Entity, CacheExchangeOpts, DirectivesConfig, Logger, } from '../types'; import { invariant } from '../helpers/help'; import { contextRef, ensureLink } from '../operations/shared'; import { _query, _queryFragment } from '../operations/query'; import { _write, _writeFragment } from '../operations/write'; import { invalidateEntity, invalidateType } from '../operations/invalidate'; import { keyOfField } from './keys'; import * as InMemoryData from './data'; import type { SchemaIntrospector } from '../ast'; import { buildClientSchema, expectValidKeyingConfig, expectValidUpdatesConfig, expectValidResolversConfig, expectValidOptimisticMutationsConfig, } from '../ast'; type DocumentNode = TypedDocumentNode; type RootField = 'query' | 'mutation' | 'subscription'; /** Implementation of the {@link Cache} interface as created internally by the {@link cacheExchange}. * @internal */ export class Store< C extends Partial = Partial, > implements Cache { data: InMemoryData.InMemoryData; logger?: Logger; directives: DirectivesConfig; resolvers: ResolverConfig; updates: UpdatesConfig; optimisticMutations: OptimisticMutationConfig; keys: KeyingConfig; globalIDs: Set | boolean; schema?: SchemaIntrospector; possibleTypeMap?: Map>; rootFields: { query: string; mutation: string; subscription: string }; rootNames: { [name: string]: RootField | void }; constructor(opts?: C) { if (!opts) opts = {} as C; this.logger = opts.logger; this.resolvers = opts.resolvers || {}; this.directives = opts.directives || {}; this.optimisticMutations = opts.optimistic || {}; this.keys = opts.keys || {}; this.globalIDs = Array.isArray(opts.globalIDs) ? new Set(opts.globalIDs) : !!opts.globalIDs; let queryName = 'Query'; let mutationName = 'Mutation'; let subscriptionName = 'Subscription'; if (opts.schema) { const schema = buildClientSchema(opts.schema); queryName = schema.query || queryName; mutationName = schema.mutation || mutationName; subscriptionName = schema.subscription || subscriptionName; // Only add schema introspector if it has types info if (schema.types) this.schema = schema; } if (!this.schema && opts.possibleTypes) { this.possibleTypeMap = new Map(); for (const entry of Object.entries(opts.possibleTypes)) { const [abstractType, concreteTypes] = entry; this.possibleTypeMap.set(abstractType, new Set(concreteTypes)); } } this.updates = opts.updates || {}; this.rootFields = { query: queryName, mutation: mutationName, subscription: subscriptionName, }; this.rootNames = { [queryName]: 'query', [mutationName]: 'mutation', [subscriptionName]: 'subscription', }; this.data = InMemoryData.make(queryName); if (this.schema && process.env.NODE_ENV !== 'production') { expectValidKeyingConfig(this.schema, this.keys, this.logger); expectValidUpdatesConfig(this.schema, this.updates, this.logger); expectValidResolversConfig(this.schema, this.resolvers, this.logger); expectValidOptimisticMutationsConfig( this.schema, this.optimisticMutations, this.logger ); } } keyOfField(fieldName: string, fieldArgs?: FieldArgs) { return keyOfField(fieldName, fieldArgs); } keyOfEntity(data: Entity) { // In resolvers and updaters we may have a specific parent // object available that can be used to skip to a specific parent // key directly without looking at its incomplete properties if (contextRef && data === contextRef.parent) { return contextRef.parentKey; } else if (data == null || typeof data === 'string') { return data || null; } else if (!data.__typename) { return null; } else if (this.rootNames[data.__typename]) { return data.__typename; } let key: string | null = null; if (this.keys[data.__typename]) { key = this.keys[data.__typename](data) || null; } else if (data.id != null) { key = `${data.id}`; } else if (data._id != null) { key = `${data._id}`; } const typename = data.__typename; const globalID = this.globalIDs === true || (this.globalIDs && this.globalIDs.has(typename)); return globalID || !key ? key : `${typename}:${key}`; } resolve( entity: Entity, field: string, args?: FieldArgs ): DataField | undefined { const entityKey = this.keyOfEntity(entity); if (entityKey) { const fieldKey = keyOfField(field, args); const fieldValue = InMemoryData.readRecord(entityKey, fieldKey); if (fieldValue !== undefined) return fieldValue; let fieldLink = InMemoryData.readLink(entityKey, fieldKey); if (fieldLink !== undefined) fieldLink = ensureLink(this, fieldLink); return fieldLink; } } invalidate(entity: Entity, field?: string, args?: FieldArgs) { const entityKey = this.keyOfEntity(entity); const shouldInvalidateType = entity && typeof entity === 'string' && !field && !args && !this.resolve(entity, '__typename'); if (shouldInvalidateType) { invalidateType(entity, []); } else { invariant( entityKey, "Can't generate a key for invalidate(...).\n" + 'You have to pass an id or _id field or create a custom `keys` field for `' + (typeof entity === 'object' ? (entity as Data).__typename : entity + '`.'), 19 ); invalidateEntity(entityKey, field, args); } } inspectFields(entity: Entity): FieldInfo[] { const entityKey = this.keyOfEntity(entity); return entityKey ? InMemoryData.inspectFields(entityKey) : []; } updateQuery( input: QueryInput, updater: (data: T | null) => T | null ): void { const request = createRequest(input.query, input.variables!); const output = updater(this.readQuery(request)); if (output !== null) { _write(this, request, output as any, undefined); } } readQuery(input: QueryInput): T | null { const request = createRequest(input.query, input.variables!); return _query(this, request, undefined, undefined).data as T | null; } readFragment( fragment: DocumentNode | TypedDocumentNode, entity: string | Data | T, variables?: V, fragmentName?: string ): T | null { return _queryFragment( this, formatDocument(fragment), entity as Data, variables as any, fragmentName ) as T | null; } writeFragment( fragment: DocumentNode | TypedDocumentNode, data: T, variables?: V, fragmentName?: string ): void { _writeFragment( this, formatDocument(fragment), data as Data, variables as any, fragmentName ); } link( entity: Entity, field: string, args: FieldArgs, link: Link ): void; link(entity: Entity, field: string, link: Link): void; link( entity: Entity, field: string, ...rest: [FieldArgs, Link] | [Link] ): void { const args = rest.length === 2 ? rest[0] : null; const link = rest.length === 2 ? rest[1] : rest[0]; const entityKey = this.keyOfEntity(entity); if (entityKey) { InMemoryData.writeLink( entityKey, keyOfField(field, args), ensureLink(this, link) ); } } } ================================================ FILE: exchanges/graphcache/src/test-utils/altered_root_schema.json ================================================ { "__schema": { "queryType": { "name": "query_root", "__typename": "__Type" }, "mutationType": { "name": "mutation_root", "__typename": "__Type" }, "subscriptionType": null, "types": [ { "kind": "OBJECT", "name": "query_root", "fields": [ { "name": "todos", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", "name": "Todo", "ofType": null } } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "Todo", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "text", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "complete", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, { "name": "author", "args": [], "type": { "kind": "OBJECT", "name": "Author", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "ID", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "String", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "Boolean", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "Author", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "known", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "mutation_root", "fields": [ { "name": "toggleTodo", "args": [ { "name": "id", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } } ], "type": { "kind": "OBJECT", "name": "Todo", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__Schema", "fields": [ { "name": "types", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } } } }, { "name": "queryType", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } }, { "name": "mutationType", "args": [], "type": { "kind": "OBJECT", "name": "__Type", "ofType": null } }, { "name": "subscriptionType", "args": [], "type": { "kind": "OBJECT", "name": "__Type", "ofType": null } }, { "name": "directives", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Directive", "ofType": null } } } } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__Type", "fields": [ { "name": "kind", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "ENUM", "name": "__TypeKind", "ofType": null } } }, { "name": "name", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "fields", "args": [ { "name": "includeDeprecated", "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } ], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Field", "ofType": null } } } }, { "name": "interfaces", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } } }, { "name": "possibleTypes", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } } }, { "name": "enumValues", "args": [ { "name": "includeDeprecated", "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } ], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__EnumValue", "ofType": null } } } }, { "name": "inputFields", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } } } }, { "name": "ofType", "args": [], "type": { "kind": "OBJECT", "name": "__Type", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "ENUM", "name": "__TypeKind", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "SCALAR" }, { "name": "OBJECT" }, { "name": "INTERFACE" }, { "name": "UNION" }, { "name": "ENUM" }, { "name": "INPUT_OBJECT" }, { "name": "LIST" }, { "name": "NON_NULL" } ], "possibleTypes": null }, { "kind": "OBJECT", "name": "__Field", "fields": [ { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "args", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } } } } }, { "name": "type", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } }, { "name": "isDeprecated", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } }, { "name": "deprecationReason", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__InputValue", "fields": [ { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "type", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } }, { "name": "defaultValue", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__EnumValue", "fields": [ { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "isDeprecated", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } }, { "name": "deprecationReason", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__Directive", "fields": [ { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "locations", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "ENUM", "name": "__DirectiveLocation", "ofType": null } } } } }, { "name": "args", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } } } } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "ENUM", "name": "__DirectiveLocation", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "QUERY" }, { "name": "MUTATION" }, { "name": "SUBSCRIPTION" }, { "name": "FIELD" }, { "name": "FRAGMENT_DEFINITION" }, { "name": "FRAGMENT_SPREAD" }, { "name": "INLINE_FRAGMENT" }, { "name": "VARIABLE_DEFINITION" }, { "name": "SCHEMA" }, { "name": "SCALAR" }, { "name": "OBJECT" }, { "name": "FIELD_DEFINITION" }, { "name": "ARGUMENT_DEFINITION" }, { "name": "INTERFACE" }, { "name": "UNION" }, { "name": "ENUM" }, { "name": "ENUM_VALUE" }, { "name": "INPUT_OBJECT" }, { "name": "INPUT_FIELD_DEFINITION" } ], "possibleTypes": null }, { "kind": "INTERFACE", "name": "ITodo", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "text", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "complete", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, { "name": "author", "args": [], "type": { "kind": "OBJECT", "name": "Author", "ofType": null } } ], "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": [ { "kind": "OBJECT", "name": "BigTodo", "ofType": null }, { "kind": "OBJECT", "name": "SmallTodo", "ofType": null } ] }, { "kind": "OBJECT", "name": "BigTodo", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "text", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "complete", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, { "name": "author", "args": [], "type": { "kind": "OBJECT", "name": "Author", "ofType": null } }, { "name": "wallOfText", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } } ], "inputFields": null, "interfaces": [ { "kind": "INTERFACE", "name": "ITodo", "ofType": null } ], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "SmallTodo", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "text", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "complete", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, { "name": "author", "args": [], "type": { "kind": "OBJECT", "name": "Author", "ofType": null } }, { "name": "maxLength", "args": [], "type": { "kind": "SCALAR", "name": "Int", "ofType": null } } ], "inputFields": null, "interfaces": [ { "kind": "INTERFACE", "name": "ITodo", "ofType": null } ], "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "Int", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "ENUM", "name": "Todos", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "SmallTodo" }, { "name": "BigTodo" } ], "possibleTypes": null }, { "kind": "UNION", "name": "Search", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": [ { "kind": "OBJECT", "name": "SmallTodo", "ofType": null }, { "kind": "OBJECT", "name": "BigTodo", "ofType": null } ] }, { "kind": "ENUM", "name": "CacheControlScope", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "PUBLIC" }, { "name": "PRIVATE" } ], "possibleTypes": null }, { "kind": "SCALAR", "name": "Upload", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null } ] } } ================================================ FILE: exchanges/graphcache/src/test-utils/examples-1.test.ts ================================================ import { gql } from '@urql/core'; import { it, expect, afterEach } from 'vitest'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write, __initAnd_writeOptimistic as writeOptimistic, } from '../operations/write'; import * as InMemoryData from '../store/data'; import { Store } from '../store/store'; import { Data } from '../types'; const Todos = gql` query { __typename todos { __typename id complete text } } `; const TodoFragment = gql` fragment _ on Todo { __typename id text complete } `; const Todo = gql` query ($id: ID!) { __typename todo(id: $id) { id text complete } } `; const ToggleTodo = gql` mutation ($id: ID!) { __typename toggleTodo(id: $id) { __typename id text complete } } `; const NestedClearNameTodo = gql` mutation ($id: ID!) { __typename clearName(id: $id) { __typename todo { __typename id text complete } } } `; afterEach(() => { expect(console.warn).not.toHaveBeenCalled(); }); it('passes the "getting-started" example', () => { const store = new Store(); const todosData = { __typename: 'Query', todos: [ { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, { id: '1', text: 'Pick up the kids', complete: true, __typename: 'Todo' }, { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, ], }; const writeRes = write(store, { query: Todos }, todosData); expect(writeRes.dependencies).toEqual( new Set(['Query.todos', 'Todo:0', 'Todo:1', 'Todo:2']) ); let queryRes = query(store, { query: Todos }); expect(queryRes.data).toEqual(todosData); expect(queryRes.dependencies).toEqual(writeRes.dependencies); expect(queryRes.partial).toBe(false); const mutatedTodo = { ...todosData.todos[2], complete: true, }; const mutationRes = write( store, { query: ToggleTodo, variables: { id: '2' } }, { __typename: 'Mutation', toggleTodo: mutatedTodo, } ); expect(mutationRes.dependencies).toEqual(new Set(['Todo:2'])); queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data).toEqual({ ...todosData, todos: [...todosData.todos.slice(0, 2), mutatedTodo], }); const newMutatedTodo = { ...mutatedTodo, text: '', }; const newMutationRes = write( store, { query: NestedClearNameTodo, variables: { id: '2' } }, { __typename: 'Mutation', clearName: { __typename: 'ClearName', todo: newMutatedTodo, }, } ); expect(newMutationRes.dependencies).toEqual(new Set(['Todo:2'])); queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data).toEqual({ ...todosData, todos: [...todosData.todos.slice(0, 2), newMutatedTodo], }); }); it('resolves missing, nullable arguments on fields', () => { const store = new Store(); const GetWithVariables = gql` query { __typename todo(first: null) { __typename id } } `; const GetWithoutVariables = gql` query { __typename todo { __typename id } } `; const dataToWrite = { __typename: 'Query', todo: { __typename: 'Todo', id: '123', }, }; write(store, { query: GetWithVariables }, dataToWrite); const { data } = query(store, { query: GetWithoutVariables }); expect(data).toEqual(dataToWrite); }); it('should link entities', () => { const store = new Store({ resolvers: { Query: { todo: (_parent, args) => { return { __typename: 'Todo', ...args }; }, }, }, }); const todosData = { __typename: 'Query', todos: [ { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, { id: '1', text: 'Pick up the kids', complete: true, __typename: 'Todo' }, { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, ], }; write(store, { query: Todos }, todosData); const res = query(store, { query: Todo, variables: { id: '0' } }); expect(res.data).toEqual({ __typename: 'Query', todo: { id: '0', text: 'Go to the shops', complete: false, }, }); }); it('should not link entities when writing', () => { const store = new Store({ resolvers: { Todo: { text: () => '[redacted]', }, }, }); const todosData = { __typename: 'Query', todos: [ { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, { id: '1', text: 'Pick up the kids', complete: true, __typename: 'Todo' }, { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, ], }; write(store, { query: Todos }, todosData); InMemoryData.initDataState('write', store.data, null); let data = store.readFragment(TodoFragment, { __typename: 'Todo', id: '0' }); expect(data).toEqual({ id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo', }); InMemoryData.initDataState('read', store.data, null); data = store.readFragment(TodoFragment, { __typename: 'Todo', id: '0' }); expect(data).toEqual({ id: '0', text: '[redacted]', complete: false, __typename: 'Todo', }); }); it('respects property-level resolvers when given', () => { const store = new Store({ resolvers: { Todo: { text: () => 'hi' }, }, }); const todosData = { __typename: 'Query', todos: [ { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, { id: '1', text: 'Pick up the kids', complete: true, __typename: 'Todo' }, { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, ], }; const writeRes = write(store, { query: Todos }, todosData); expect(writeRes.dependencies).toEqual( new Set(['Query.todos', 'Todo:0', 'Todo:1', 'Todo:2']) ); let queryRes = query(store, { query: Todos }); expect(queryRes.data).toEqual({ __typename: 'Query', todos: [ { id: '0', text: 'hi', complete: false, __typename: 'Todo' }, { id: '1', text: 'hi', complete: true, __typename: 'Todo' }, { id: '2', text: 'hi', complete: false, __typename: 'Todo' }, ], }); expect(queryRes.dependencies).toEqual(writeRes.dependencies); expect(queryRes.partial).toBe(false); const mutatedTodo = { ...todosData.todos[2], complete: true, }; const mutationRes = write( store, { query: ToggleTodo, variables: { id: '2' } }, { __typename: 'Mutation', toggleTodo: mutatedTodo, } ); expect(mutationRes.dependencies).toEqual(new Set(['Todo:2'])); queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data).toEqual({ ...todosData, todos: [ { id: '0', text: 'hi', complete: false, __typename: 'Todo' }, { id: '1', text: 'hi', complete: true, __typename: 'Todo' }, { id: '2', text: 'hi', complete: true, __typename: 'Todo' }, ], }); }); it('respects Mutation update functions', () => { const store = new Store({ updates: { Mutation: { toggleTodo: function toggleTodo(result, _, cache) { cache.updateQuery({ query: Todos }, data => { if ( data && data.todos && result && result.toggleTodo && (result.toggleTodo as any).id === '1' ) { data.todos[1] = { id: '1', text: `${data.todos[1].text} (Updated)`, complete: (result.toggleTodo as any).complete, __typename: 'Todo', }; } else if (data && data.todos) { data.todos[Number((result.toggleTodo as any).id)] = { ...data.todos[Number((result.toggleTodo as any).id)], complete: (result.toggleTodo as any).complete, }; } return data as Data; }); }, }, }, }); const todosData = { __typename: 'Query', todos: [ { id: '0', text: 'Go to the shops', complete: false, __typename: 'Todo' }, { id: '1', text: 'Pick up the kids', complete: false, __typename: 'Todo', }, { id: '2', text: 'Install urql', complete: false, __typename: 'Todo' }, ], }; write(store, { query: Todos }, todosData); write( store, { query: ToggleTodo, variables: { id: '1' } }, { __typename: 'Mutation', toggleTodo: { ...todosData.todos[1], complete: true, }, } ); write( store, { query: ToggleTodo, variables: { id: '2' } }, { __typename: 'Mutation', toggleTodo: { ...todosData.todos[2], complete: true, }, } ); const queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data).toEqual({ ...todosData, todos: [ todosData.todos[0], { id: '1', text: 'Pick up the kids (Updated)', complete: true, __typename: 'Todo', }, { id: '2', text: 'Install urql', complete: true, __typename: 'Todo' }, ], }); }); it('respects arbitrary type update functions', () => { const store = new Store({ updates: { Todo: { text(result, _, cache) { const fragment = gql` fragment _ on Todo { id complete } `; cache.writeFragment(fragment, { id: result.id, complete: true, }); }, }, }, }); const todosData = { __typename: 'Query', todos: [ { id: '1', text: 'First', complete: false, __typename: 'Todo' }, { id: '2', text: 'Second', complete: false, __typename: 'Todo' }, ], }; write(store, { query: Todos }, todosData); const queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data).toEqual({ ...todosData, todos: [ { ...todosData.todos[0], complete: true, }, { ...todosData.todos[1], complete: true, }, ], }); }); it('correctly resolves optimistic updates on Relay schemas', () => { const store = new Store({ optimistic: { updateItem: variables => ({ __typename: 'UpdateItemPayload', item: { __typename: 'Item', id: variables.id as string, name: 'Offline', }, }), }, }); const queryData = { __typename: 'Query', root: { __typename: 'Root', id: 'root', items: { __typename: 'ItemConnection', edges: [ { __typename: 'ItemEdge', node: { __typename: 'Item', id: '1', name: 'Number One', }, }, { __typename: 'ItemEdge', node: { __typename: 'Item', id: '2', name: 'Number Two', }, }, ], }, }, }; const getRoot = gql` query GetRoot { root { __typename id items { __typename edges { __typename node { __typename id name } } } } } `; const updateItem = gql` mutation UpdateItem($id: ID!) { updateItem(id: $id) { __typename item { __typename id name } } } `; write(store, { query: getRoot }, queryData); const { dependencies } = writeOptimistic( store, { query: updateItem, variables: { id: '2' } }, 1 ); expect(dependencies.size).not.toBe(0); InMemoryData.noopDataState(store.data, 1); const queryRes = query(store, { query: getRoot }); expect(queryRes.partial).toBe(false); expect(queryRes.data).not.toBe(null); }); it('skips non-optimistic mutation fields on writes', () => { const store = new Store(); const updateItem = gql` mutation UpdateItem($id: ID!) { updateItem(id: $id) { __typename item { __typename id name } } } `; const { dependencies } = writeOptimistic( store, { query: updateItem, variables: { id: '2' } }, 1 ); expect(dependencies.size).toBe(0); }); it('allows cumulative optimistic updates', () => { let counter = 1; const store = new Store({ updates: { Mutation: { addTodo: (result, _, cache) => { cache.updateQuery({ query: Todos }, data => { (data as any).todos.push(result.addTodo); return data as Data; }); }, }, }, optimistic: { addTodo: () => ({ __typename: 'Todo', id: 'optimistic_' + ++counter, text: '', complete: false, }), }, }); const todosData = { __typename: 'Query', todos: [ { id: '0', complete: true, text: '0', __typename: 'Todo' }, { id: '1', complete: true, text: '1', __typename: 'Todo' }, ], }; write(store, { query: Todos }, todosData); const AddTodo = gql` mutation { __typename addTodo { __typename complete text id } } `; writeOptimistic(store, { query: AddTodo }, 1); writeOptimistic(store, { query: AddTodo }, 2); const queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data).toEqual({ ...todosData, todos: [ todosData.todos[0], todosData.todos[1], { __typename: 'Todo', text: '', complete: false, id: 'optimistic_2' }, { __typename: 'Todo', text: '', complete: false, id: 'optimistic_3' }, ], }); }); it('supports clearing a layer then reapplying optimistic updates', () => { let counter = 1; const store = new Store({ updates: { Mutation: { addTodo: (result, _, cache) => { cache.updateQuery({ query: Todos }, data => { (data as any).todos.push(result.addTodo); return data as Data; }); }, }, }, optimistic: { addTodo: () => ({ __typename: 'Todo', id: 'optimistic_' + ++counter, text: '', complete: false, }), }, }); const todosData = { __typename: 'Query', todos: [ { id: '0', complete: true, text: '0', __typename: 'Todo' }, { id: '1', complete: true, text: '1', __typename: 'Todo' }, ], }; write(store, { query: Todos }, todosData); const AddTodo = gql` mutation { __typename addTodo { __typename complete text id } } `; writeOptimistic(store, { query: AddTodo }, 1); writeOptimistic(store, { query: AddTodo }, 1); InMemoryData.noopDataState(store.data, 1); writeOptimistic(store, { query: AddTodo }, 1); writeOptimistic(store, { query: AddTodo }, 1); const queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data).toEqual({ ...todosData, todos: [ todosData.todos[0], todosData.todos[1], { __typename: 'Todo', text: '', complete: false, id: 'optimistic_4' }, { __typename: 'Todo', text: '', complete: false, id: 'optimistic_5' }, ], }); }); it('supports seeing the same optimistic key multiple times (correctly reorders)', () => { const store = new Store({ optimistic: { updateTodo: (args: any) => ({ __typename: 'Todo', id: args.id, complete: args.completed, }), }, }); const todosData = { __typename: 'Query', todos: [ { id: '0', complete: false, text: '0', __typename: 'Todo' }, { id: '1', complete: false, text: '1', __typename: 'Todo' }, ], }; write(store, { query: Todos }, todosData); const updateTodo = gql` mutation ($id: ID!, $completed: Boolean!) { __typename updateTodo(id: $id, completed: $completed) { __typename complete id } } `; writeOptimistic( store, { query: updateTodo, variables: { id: '0', completed: true } }, 1 ); let queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data?.todos?.[0]?.complete).toEqual(true); writeOptimistic( store, { query: updateTodo, variables: { id: '0', completed: false } }, 2 ); queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data?.todos?.[0]?.complete).toEqual(false); writeOptimistic( store, { query: updateTodo, variables: { id: '0', completed: true } }, 1 ); queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data?.todos?.[0]?.complete).toEqual(true); writeOptimistic( store, { query: updateTodo, variables: { id: '0', completed: false } }, 2 ); queryRes = query(store, { query: Todos }); expect(queryRes.partial).toBe(false); expect(queryRes.data?.todos?.[0]?.complete).toEqual(false); }); ================================================ FILE: exchanges/graphcache/src/test-utils/examples-2.test.ts ================================================ import { gql } from '@urql/core'; import { it, afterEach, expect } from 'vitest'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store/store'; const Item = gql` { todo { __typename id complete text } } `; const ItemDetailed = gql` { todo { __typename id details { __typename authors { __typename id } } } } `; const Pagination = gql` query { todos { __typename edges { __typename node { __typename id complete text } } pageInfo { __typename hasNextPage endCursor } } } `; afterEach(() => { expect(console.warn).not.toHaveBeenCalled(); }); it('allows custom resolvers to resolve nested, unkeyed data', () => { const store = new Store({ resolvers: { Query: { todos: () => ({ __typename: 'TodosConnection', edges: [ { __typename: 'TodoEdge', node: { __typename: 'Todo', id: '1', // The `complete` field will be overwritten here, but we're // leaving out the `text` field complete: true, }, }, ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, endCursor: '1', }, }), }, }, }); write( store, { query: Item }, { __typename: 'Query', todo: { __typename: 'Todo', id: '1', complete: false, text: 'Example', }, } ); const res = query(store, { query: Pagination }); expect(res.partial).toBe(false); expect(res.data).toEqual({ todos: { __typename: 'TodosConnection', edges: [ { __typename: 'TodoEdge', node: { __typename: 'Todo', id: '1', complete: true, // This is now true and not false! text: 'Example', // This is still present }, }, ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, endCursor: '1', }, }, }); }); it('allows custom resolvers to resolve nested, unkeyed data with embedded links', () => { const store = new Store({ resolvers: { Query: { todos: (_, __, cache) => ({ __typename: 'TodosConnection', edges: [ { __typename: 'TodoEdge', // This is a key instead of the full entity: node: cache.keyOfEntity({ __typename: 'Todo', id: '1' }), }, ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, endCursor: '1', }, }), }, }, }); write( store, { query: Item }, { __typename: 'Query', todo: { __typename: 'Todo', id: '1', complete: false, text: 'Example', }, } ); const res = query(store, { query: Pagination }); expect(res.partial).toBe(false); expect(res.data).toEqual({ todos: { __typename: 'TodosConnection', edges: [ { __typename: 'TodoEdge', node: { __typename: 'Todo', id: '1', complete: false, text: 'Example', }, }, ], pageInfo: { __typename: 'PageInfo', hasNextPage: true, endCursor: '1', }, }, }); }); it('allows custom resolvers to resolve mixed data (keyable and unkeyable)', () => { const store = new Store({ keys: { TodoDetails: () => null, }, resolvers: { Query: { todo: () => ({ __typename: 'Todo', id: '1', details: { __typename: 'TodoDetails', }, }), }, }, }); write( store, { query: ItemDetailed }, { __typename: 'Query', todo: { __typename: 'Todo', id: '1', details: { __typename: 'TodoDetails', authors: [ { __typename: 'Author', id: 'x', }, ], }, }, } ); const res = query(store, { query: ItemDetailed }); expect(res.partial).toBe(false); expect(res.dependencies.has('Author:x')).toBeTruthy(); expect(res.data).toEqual({ todo: { __typename: 'Todo', id: '1', details: { __typename: 'TodoDetails', authors: [ { __typename: 'Author', id: 'x', }, ], }, }, }); }); ================================================ FILE: exchanges/graphcache/src/test-utils/examples-3.test.ts ================================================ import { gql } from '@urql/core'; import { it, afterEach, expect } from 'vitest'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store/store'; afterEach(() => { expect(console.warn).not.toHaveBeenCalled(); }); it('allows viewer fields to overwrite the root Query data', () => { const store = new Store(); const get = gql` { int } `; const set = gql` mutation { mutate { viewer { int } } } `; write( store, { query: get }, { __typename: 'Query', int: 42, } ); write( store, { query: set }, { __typename: 'Mutation', mutate: { __typename: 'MutateResult', viewer: { __typename: 'Query', int: 43, }, }, } ); const res = query(store, { query: get }); expect(res.partial).toBe(false); expect(res.data).toEqual({ int: 43, }); }); ================================================ FILE: exchanges/graphcache/src/test-utils/relayPagination_schema.json ================================================ { "__schema": { "queryType": { "name": "Query" }, "mutationType": null, "subscriptionType": null, "types": [ { "kind": "OBJECT", "name": "Query", "description": null, "fields": [ { "name": "items", "description": null, "args": [ { "name": "after", "description": null, "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, { "name": "before", "description": null, "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, { "name": "first", "description": null, "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, "defaultValue": null }, { "name": "last", "description": null, "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, "defaultValue": null } ], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "ItemsConnection", "ofType": null } }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "String", "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "Int", "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "ItemsConnection", "description": null, "fields": [ { "name": "edges", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "ItemEdge", "ofType": null } } } }, "isDeprecated": false, "deprecationReason": null }, { "name": "totalCount", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Int", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "pageInfo", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "PageInfo", "ofType": null } }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "ItemEdge", "description": null, "fields": [ { "name": "cursor", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "node", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "Item", "ofType": null } }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "Item", "description": null, "fields": [ { "name": "id", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "name", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [ { "kind": "INTERFACE", "name": "Node", "ofType": null } ], "enumValues": null, "possibleTypes": null }, { "kind": "INTERFACE", "name": "Node", "description": null, "fields": [ { "name": "id", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": [ { "kind": "OBJECT", "name": "Item", "ofType": null } ] }, { "kind": "SCALAR", "name": "ID", "description": "The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"4\"`) or integer (such as `4`) input value will be accepted as an ID.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "PageInfo", "description": null, "fields": [ { "name": "endCursor", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { "name": "hasNextPage", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "hasPreviousPage", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "startCursor", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "Boolean", "description": "The `Boolean` scalar type represents `true` or `false`.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__Schema", "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", "fields": [ { "name": "types", "description": "A list of all types supported by this server.", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } } }, "isDeprecated": false, "deprecationReason": null }, { "name": "queryType", "description": "The type that query operations will be rooted at.", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "mutationType", "description": "If this server supports mutation, the type that mutation operations will be rooted at.", "args": [], "type": { "kind": "OBJECT", "name": "__Type", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { "name": "subscriptionType", "description": "If this server support subscription, the type that subscription operations will be rooted at.", "args": [], "type": { "kind": "OBJECT", "name": "__Type", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { "name": "directives", "description": "A list of all directives supported by this server.", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Directive", "ofType": null } } } }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__Type", "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", "fields": [ { "name": "kind", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "ENUM", "name": "__TypeKind", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "name", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { "name": "description", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { "name": "fields", "description": null, "args": [ { "name": "includeDeprecated", "description": null, "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, "defaultValue": "false" } ], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Field", "ofType": null } } }, "isDeprecated": false, "deprecationReason": null }, { "name": "interfaces", "description": null, "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } }, "isDeprecated": false, "deprecationReason": null }, { "name": "possibleTypes", "description": null, "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } }, "isDeprecated": false, "deprecationReason": null }, { "name": "enumValues", "description": null, "args": [ { "name": "includeDeprecated", "description": null, "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null }, "defaultValue": "false" } ], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__EnumValue", "ofType": null } } }, "isDeprecated": false, "deprecationReason": null }, { "name": "inputFields", "description": null, "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } } }, "isDeprecated": false, "deprecationReason": null }, { "name": "ofType", "description": null, "args": [], "type": { "kind": "OBJECT", "name": "__Type", "ofType": null }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "ENUM", "name": "__TypeKind", "description": "An enum describing what kind of type a given `__Type` is.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "SCALAR", "description": "Indicates this type is a scalar.", "isDeprecated": false, "deprecationReason": null }, { "name": "OBJECT", "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", "isDeprecated": false, "deprecationReason": null }, { "name": "INTERFACE", "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", "isDeprecated": false, "deprecationReason": null }, { "name": "UNION", "description": "Indicates this type is a union. `possibleTypes` is a valid field.", "isDeprecated": false, "deprecationReason": null }, { "name": "ENUM", "description": "Indicates this type is an enum. `enumValues` is a valid field.", "isDeprecated": false, "deprecationReason": null }, { "name": "INPUT_OBJECT", "description": "Indicates this type is an input object. `inputFields` is a valid field.", "isDeprecated": false, "deprecationReason": null }, { "name": "LIST", "description": "Indicates this type is a list. `ofType` is a valid field.", "isDeprecated": false, "deprecationReason": null }, { "name": "NON_NULL", "description": "Indicates this type is a non-null. `ofType` is a valid field.", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null }, { "kind": "OBJECT", "name": "__Field", "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", "fields": [ { "name": "name", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "description", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { "name": "args", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } } } }, "isDeprecated": false, "deprecationReason": null }, { "name": "type", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "isDeprecated", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "deprecationReason", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__InputValue", "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", "fields": [ { "name": "name", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "description", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { "name": "type", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "defaultValue", "description": "A GraphQL-formatted string representing the default value for this input value.", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__EnumValue", "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", "fields": [ { "name": "name", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "description", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { "name": "isDeprecated", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "deprecationReason", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__Directive", "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", "fields": [ { "name": "name", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "isDeprecated": false, "deprecationReason": null }, { "name": "description", "description": null, "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { "name": "locations", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "ENUM", "name": "__DirectiveLocation", "ofType": null } } } }, "isDeprecated": false, "deprecationReason": null }, { "name": "args", "description": null, "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } } } }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "ENUM", "name": "__DirectiveLocation", "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "QUERY", "description": "Location adjacent to a query operation.", "isDeprecated": false, "deprecationReason": null }, { "name": "MUTATION", "description": "Location adjacent to a mutation operation.", "isDeprecated": false, "deprecationReason": null }, { "name": "SUBSCRIPTION", "description": "Location adjacent to a subscription operation.", "isDeprecated": false, "deprecationReason": null }, { "name": "FIELD", "description": "Location adjacent to a field.", "isDeprecated": false, "deprecationReason": null }, { "name": "FRAGMENT_DEFINITION", "description": "Location adjacent to a fragment definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "FRAGMENT_SPREAD", "description": "Location adjacent to a fragment spread.", "isDeprecated": false, "deprecationReason": null }, { "name": "INLINE_FRAGMENT", "description": "Location adjacent to an inline fragment.", "isDeprecated": false, "deprecationReason": null }, { "name": "VARIABLE_DEFINITION", "description": "Location adjacent to a variable definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "SCHEMA", "description": "Location adjacent to a schema definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "SCALAR", "description": "Location adjacent to a scalar definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "OBJECT", "description": "Location adjacent to an object type definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "FIELD_DEFINITION", "description": "Location adjacent to a field definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "ARGUMENT_DEFINITION", "description": "Location adjacent to an argument definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "INTERFACE", "description": "Location adjacent to an interface definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "UNION", "description": "Location adjacent to a union definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "ENUM", "description": "Location adjacent to an enum definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "ENUM_VALUE", "description": "Location adjacent to an enum value definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "INPUT_OBJECT", "description": "Location adjacent to an input object type definition.", "isDeprecated": false, "deprecationReason": null }, { "name": "INPUT_FIELD_DEFINITION", "description": "Location adjacent to an input object field definition.", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null } ], "directives": [ { "name": "skip", "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [ { "name": "if", "description": "Skipped when true.", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, "defaultValue": null } ] }, { "name": "include", "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [ { "name": "if", "description": "Included when true.", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, "defaultValue": null } ] }, { "name": "deprecated", "description": "Marks an element of a GraphQL schema as no longer supported.", "locations": ["FIELD_DEFINITION", "ENUM_VALUE"], "args": [ { "name": "reason", "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax (as specified by [CommonMark](https://commonmark.org/).", "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": "\"No longer supported\"" } ] } ] } } ================================================ FILE: exchanges/graphcache/src/test-utils/simple_schema.json ================================================ { "__schema": { "queryType": { "name": "Query" }, "mutationType": { "name": "Mutation" }, "subscriptionType": { "name": "Subscription" }, "types": [ { "kind": "OBJECT", "name": "Query", "fields": [ { "name": "todos", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", "name": "Todo", "ofType": null } } }, { "name": "latestTodo", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "UNION", "name": "LatestTodoResult", "ofType": null } } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "name": "LatestTodoResult", "kind": "UNION", "args": [], "possibleTypes": [ { "kind": "OBJECT", "name": "Todo", "ofType": null }, { "kind": "OBJECT", "name": "NoTodosError", "ofType": null } ] }, { "kind": "OBJECT", "name": "NoTodosError", "interfaces": [], "fields": [ { "name": "message", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } }, "isDeprecated": false, "deprecationReason": null } ] }, { "kind": "OBJECT", "name": "Todo", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "text", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "complete", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, { "name": "author", "args": [], "type": { "kind": "OBJECT", "name": "Author", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "ID", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "String", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "Boolean", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "Author", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "known", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "Mutation", "fields": [ { "name": "toggleTodo", "args": [ { "name": "id", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } } ], "type": { "kind": "OBJECT", "name": "Todo", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "Subscription", "fields": [ { "name": "newTodo", "args": [], "type": { "kind": "OBJECT", "name": "Todo", "ofType": null }, "isDeprecated": false, "deprecationReason": null } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__Schema", "fields": [ { "name": "types", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } } } }, { "name": "queryType", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } }, { "name": "mutationType", "args": [], "type": { "kind": "OBJECT", "name": "__Type", "ofType": null } }, { "name": "subscriptionType", "args": [], "type": { "kind": "OBJECT", "name": "__Type", "ofType": null } }, { "name": "directives", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Directive", "ofType": null } } } } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__Type", "fields": [ { "name": "kind", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "ENUM", "name": "__TypeKind", "ofType": null } } }, { "name": "name", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "fields", "args": [ { "name": "includeDeprecated", "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } ], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Field", "ofType": null } } } }, { "name": "interfaces", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } } }, { "name": "possibleTypes", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } } }, { "name": "enumValues", "args": [ { "name": "includeDeprecated", "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } ], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__EnumValue", "ofType": null } } } }, { "name": "inputFields", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } } } }, { "name": "ofType", "args": [], "type": { "kind": "OBJECT", "name": "__Type", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "ENUM", "name": "__TypeKind", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "SCALAR" }, { "name": "OBJECT" }, { "name": "INTERFACE" }, { "name": "UNION" }, { "name": "ENUM" }, { "name": "INPUT_OBJECT" }, { "name": "LIST" }, { "name": "NON_NULL" } ], "possibleTypes": null }, { "kind": "OBJECT", "name": "__Field", "fields": [ { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "args", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } } } } }, { "name": "type", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } }, { "name": "isDeprecated", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } }, { "name": "deprecationReason", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__InputValue", "fields": [ { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "type", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__Type", "ofType": null } } }, { "name": "defaultValue", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__EnumValue", "fields": [ { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "isDeprecated", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } } }, { "name": "deprecationReason", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "__Directive", "fields": [ { "name": "name", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "description", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } }, { "name": "locations", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "ENUM", "name": "__DirectiveLocation", "ofType": null } } } } }, { "name": "args", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "__InputValue", "ofType": null } } } } } ], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null }, { "kind": "ENUM", "name": "__DirectiveLocation", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "QUERY" }, { "name": "MUTATION" }, { "name": "SUBSCRIPTION" }, { "name": "FIELD" }, { "name": "FRAGMENT_DEFINITION" }, { "name": "FRAGMENT_SPREAD" }, { "name": "INLINE_FRAGMENT" }, { "name": "VARIABLE_DEFINITION" }, { "name": "SCHEMA" }, { "name": "SCALAR" }, { "name": "OBJECT" }, { "name": "FIELD_DEFINITION" }, { "name": "ARGUMENT_DEFINITION" }, { "name": "INTERFACE" }, { "name": "UNION" }, { "name": "ENUM" }, { "name": "ENUM_VALUE" }, { "name": "INPUT_OBJECT" }, { "name": "INPUT_FIELD_DEFINITION" } ], "possibleTypes": null }, { "kind": "INTERFACE", "name": "ITodo", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "text", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "complete", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, { "name": "author", "args": [], "type": { "kind": "OBJECT", "name": "Author", "ofType": null } } ], "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": [ { "kind": "OBJECT", "name": "BigTodo", "ofType": null }, { "kind": "OBJECT", "name": "SmallTodo", "ofType": null } ] }, { "kind": "OBJECT", "name": "BigTodo", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "text", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "complete", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, { "name": "author", "args": [], "type": { "kind": "OBJECT", "name": "Author", "ofType": null } }, { "name": "wallOfText", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null } } ], "inputFields": null, "interfaces": [ { "kind": "INTERFACE", "name": "ITodo", "ofType": null } ], "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", "name": "SmallTodo", "fields": [ { "name": "id", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "ID", "ofType": null } } }, { "name": "text", "args": [], "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } }, { "name": "complete", "args": [], "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null } }, { "name": "author", "args": [], "type": { "kind": "OBJECT", "name": "Author", "ofType": null } }, { "name": "maxLength", "args": [], "type": { "kind": "SCALAR", "name": "Int", "ofType": null } } ], "inputFields": null, "interfaces": [ { "kind": "INTERFACE", "name": "ITodo", "ofType": null } ], "enumValues": null, "possibleTypes": null }, { "kind": "SCALAR", "name": "Int", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "ENUM", "name": "Todos", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "SmallTodo" }, { "name": "BigTodo" } ], "possibleTypes": null }, { "kind": "UNION", "name": "Search", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": [ { "kind": "OBJECT", "name": "SmallTodo", "ofType": null }, { "kind": "OBJECT", "name": "BigTodo", "ofType": null } ] }, { "kind": "ENUM", "name": "CacheControlScope", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ { "name": "PUBLIC" }, { "name": "PRIVATE" } ], "possibleTypes": null }, { "kind": "SCALAR", "name": "Upload", "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null } ] } } ================================================ FILE: exchanges/graphcache/src/test-utils/suite.test.ts ================================================ import { DocumentNode } from 'graphql'; import { gql } from '@urql/core'; import { it, expect } from 'vitest'; import { __initAnd_query as query } from '../operations/query'; import { __initAnd_write as write } from '../operations/write'; import { Store } from '../store/store'; interface TestCase { query: DocumentNode; variables?: any; data: any; } const expectCacheIntegrity = (testcase: TestCase) => { const store = new Store(); const request = { query: testcase.query, variables: testcase.variables }; const writeRes = write(store, request, testcase.data); const queryRes = query(store, request); expect(queryRes.data).not.toBe(null); expect(queryRes.data).toEqual(testcase.data); expect(queryRes.partial).toBe(false); expect(queryRes.dependencies).toEqual(writeRes.dependencies); }; it('int on query', () => { expectCacheIntegrity({ query: gql` { __typename int } `, data: { __typename: 'Query', int: 42 }, }); }); it('aliased field on query', () => { expectCacheIntegrity({ query: gql` { __typename anotherName: int } `, data: { __typename: 'Query', anotherName: 42 }, }); }); it('@skip directive on field on query', () => { expectCacheIntegrity({ query: gql` { __typename intA @skip(if: true) intB @skip(if: false) } `, data: { __typename: 'Query', intB: 2 }, }); }); it('@include directive on field on query', () => { expectCacheIntegrity({ query: gql` { __typename intA @include(if: true) intB @include(if: false) } `, data: { __typename: 'Query', intA: 2 }, }); }); it('random directive on field on query', () => { expectCacheIntegrity({ query: gql` { __typename int @shouldntMatter } `, data: { __typename: 'Query', int: 1 }, }); }); it('json on query', () => { expectCacheIntegrity({ query: gql` { __typename json } `, // The `__typename` field should not mislead the cache data: { __typename: 'Query', json: { __typename: 'Misleading', test: true }, }, }); }); it('nullable field on query', () => { expectCacheIntegrity({ query: gql` { __typename missing } `, data: { __typename: 'Query', missing: null }, }); }); it('int field with arguments on query', () => { expectCacheIntegrity({ query: gql` { __typename int(test: true) } `, data: { __typename: 'Query', int: 42 }, }); }); it('non-keyable entity on query', () => { expectCacheIntegrity({ query: gql` { __typename item { __typename name } } `, // This entity has no `id` or `_id` field data: { __typename: 'Query', item: { __typename: 'Item', name: 'Test' } }, }); }); it('non-IDable entity on query', () => { expectCacheIntegrity({ query: gql` { __typename item { __typename name } } `, // This entity has a `__typename` but no ID fields data: { __typename: 'Query', item: { __typename: 'Item', name: 'Test' } }, }); }); it('entity on query', () => { expectCacheIntegrity({ query: gql` { __typename item { __typename id name } } `, data: { __typename: 'Query', item: { __typename: 'Item', id: '1', name: 'Test' }, }, }); }); it('entity on aliased field on query', () => { expectCacheIntegrity({ query: gql` { __typename anotherName: item { __typename id name } } `, data: { __typename: 'Query', anotherName: { __typename: 'Item', id: '1', name: 'Test' }, }, }); }); it('entity with arguments on query', () => { expectCacheIntegrity({ query: gql` { __typename item(test: true) { __typename id name } } `, data: { __typename: 'Query', item: { __typename: 'Item', id: '1', name: 'Test' }, }, }); }); it('entity with Int-like ID on query', () => { expectCacheIntegrity({ query: gql` { __typename item { __typename id name } } `, // This is the same as above, but with a number on `id` data: { __typename: 'Query', item: { __typename: 'Item', id: 1, name: 'Test' }, }, }); }); it('entity list on query', () => { expectCacheIntegrity({ query: gql` { __typename items { __typename id } } `, data: { __typename: 'Query', items: [ { __typename: 'Item', id: 1 }, { __typename: 'Item', id: 2 }, ], }, }); }); it('nested entity list on query', () => { expectCacheIntegrity({ query: gql` { __typename items { __typename id } } `, data: { // This is the same as above, but with a nested array and added null values __typename: 'Query', items: [ { __typename: 'Item', id: 1 }, [{ __typename: 'Item', id: 2 }, null], null, ], }, }); }); it('entity list on query and inline fragment', () => { expectCacheIntegrity({ query: gql` { __typename items { __typename id } ... on Query { items { test } } } `, data: { __typename: 'Query', items: [{ __typename: 'Item', id: 1, test: true }, null], }, }); }); it('conditionless inline fragment', () => { expectCacheIntegrity({ query: gql` { __typename ... { test } } `, data: { __typename: 'Query', test: true, }, }); }); it('skipped conditionless inline fragment', () => { expectCacheIntegrity({ query: gql` { __typename ... @skip(if: true) { test } } `, data: { __typename: 'Query', }, }); }); it('entity list on query and spread fragment', () => { expectCacheIntegrity({ query: gql` query Test { __typename items { __typename id } ...TestFragment } fragment TestFragment on Query { items { test } } `, data: { __typename: 'Query', items: [{ __typename: 'Item', id: 1, test: true }, null], }, }); }); it('skipped spread fragment', () => { expectCacheIntegrity({ query: gql` query Test { __typename ...TestFragment @skip(if: true) } fragment TestFragment on Query { test } `, data: { __typename: 'Query', }, }); }); it('embedded objects on entities', () => { expectCacheIntegrity({ query: gql` { __typename user { __typename id posts { __typename edges { __typename node { __typename id } } } } } `, data: { __typename: 'Query', user: { __typename: 'User', id: 1, posts: { __typename: 'PostsConnection', edges: [ { __typename: 'PostsEdge', node: { __typename: 'Post', id: 1, }, }, ], }, }, }, }); }); it('nested viewer selections', () => { expectCacheIntegrity({ query: gql` { __typename int viewer { __typename int } } `, data: { __typename: 'Query', int: 42, viewer: { __typename: 'Query', int: 42, }, }, }); }); ================================================ FILE: exchanges/graphcache/src/test-utils/utils.ts ================================================ // eslint-disable-next-line export const noop = () => {}; ================================================ FILE: exchanges/graphcache/src/types.ts ================================================ import type { AnyVariables, DocumentInput, RequestExtensions, TypedDocumentNode, FormattedNode, ErrorLike, } from '@urql/core'; import type { DocumentNode, FragmentDefinitionNode } from '@0no-co/graphql.web'; import type { IntrospectionData } from './ast'; /** Nullable GraphQL list types of `T`. * * @remarks * Any GraphQL list of a given type `T` that is nullable is * expected to contain nullable values. Nested lists are * also taken into account in Graphcache. */ export type NullArray = Array>; /** Dictionary of GraphQL Fragment definitions by their names. * * @remarks * A map of {@link FragmentDefinitionNode | FragmentDefinitionNodes} by their * fragment names from the original GraphQL document that Graphcache is * executing. */ export interface Fragments { [fragmentName: string]: void | FormattedNode; } /** Non-object JSON values as serialized by a GraphQL API * @see {@link https://spec.graphql.org/October2021/#sel-DAPJDHAAEJHAKmzP} for the * GraphQL spec’s serialization format. */ export type Primitive = null | number | boolean | string; /** Any GraphQL scalar object * * @remarks * A GraphQL schema may define custom scalars that are resolved * and serialized as objects. These objects could also be turned * on the client-side into a non-JSON object, e.g. a `Date`. * * @see {@link https://spec.graphql.org/October2021/#sec-Scalars} for the * GraphQL spec’s information on custom scalars. */ export interface ScalarObject { constructor?: Function; [key: string]: any; } /** GraphQL scalar value * @see {@link https://spec.graphql.org/October2021/#sec-Scalars} for the GraphQL * spec’s definition of scalars */ export type Scalar = Primitive | ScalarObject; /** Fields that Graphcache expects on GraphQL object (“entity”) results. * * @remarks * Any object that comes back from a GraphQL API will have * a `__typename` field from GraphQL Object types. * * The `__typename` field must be present as Graphcache updates * GraphQL queries with type name introspection. * Furthermore, Graphcache always checks for its default key * fields, `id` and `_id` to be present. */ export interface SystemFields { /** GraphQL Object type name as returned by Type Name Introspection. * @see {@link https://spec.graphql.org/October2021/#sec-Type-Name-Introspection} for * more information on GraphQL’s Type Name introspection. */ __typename: string; _id?: string | number | null; id?: string | number | null; } /** Scalar values are stored separately from relations between entities. * @internal */ export type EntityField = undefined | Scalar | NullArray; /** Values on GraphQL object (“entity”) results. * * @remarks * Any field that comes back from a GraphQL API will have * values that are scalars, other objects, or arrays * of scalars or objects. */ export type DataField = Scalar | Data | NullArray | NullArray; /** Definition of GraphQL object (“entity”) fields. * * @remarks * Any object that comes back from a GraphQL API will have * values that are scalars, other objects, or arrays * of scalars or objects, i.e. the {@link DataField} type. */ export interface DataFields { [fieldName: string]: DataField; } /** Definition of GraphQL variables objects. * @remarks * Variables, as passed to GraphQL queries, can only contain scalar values. * * @see {@link https://spec.graphql.org/October2021/#sec-Coercing-Variable-Values} for the * GraphQL spec’s coercion of GraphQL variables. */ export interface Variables { [name: string]: Scalar | Scalar[] | Variables | NullArray; } /** Definition of GraphQL objects (“entities”). * * @remarks * An entity is expected to consist of a `__typename` * fields, optionally the default `id` or `_id` key * fields, and scalar values or other entities * otherwise. */ export type Data = SystemFields & DataFields; /** An entity, a key of an entity, or `null` * * @remarks * When Graphcache accepts a reference to an entity, you may pass it a key of an entity, * as retrieved for instance by {@link Cache.keyOfEntity} or a partial GraphQL object * (i.e. an object with a `__typename` and key field). */ export type Entity = undefined | null | Data | string; /** A key of an entity, or `null`; or a list of keys. * * @remarks * When Graphcache accepts a reference to one or more entities, you may pass it a * key, an entity, or a list of entities or keys. This is often passed to {@link Cache.link} * to update a field pointing to other GraphQL objects. */ export type Link = null | Key | NullArray; /** Arguments passed to a Graphcache field resolver. * * @remarks * Arguments a field receives are similar to variables and can * only contain scalars or other arguments objects. This * is equivalent to the {@link Variables} type. * * @see {@link https://spec.graphql.org/October2021/#sec-Coercing-Field-Arguments} for the * GraphQL spec’s coercion of field arguments. */ export type FieldArgs = Variables | null | undefined; /** Metadata about an entity’s cached field. * * @remarks * As returned by {@link Cache.inspectFields}, `FieldInfo` specifies an entity’s cached field, * split into the field’s key itself and the field’s original name and arguments. */ export interface FieldInfo { /** The field’s key which combines `fieldName` and `arguments`. */ fieldKey: string; /** The field’s name, as defined on a GraphQL Object type. */ fieldName: string; /** The arguments passed to the field as found on the cache. */ arguments: Variables | null; } /** A key to an entity field split back into the entity’s key and the field’s key part. * @internal */ export interface KeyInfo { entityKey: string; fieldKey: string; } /** Abstract type for GraphQL requests. * * @remarks * Similarly to `@urql/core`’s `GraphQLRequest` type, `OperationRequest` * requires the minimum fields that Grapcache requires to execute a * GraphQL operation: its query document and variables. */ export interface OperationRequest { query: FormattedNode | DocumentNode; variables?: any; } /** Metadata object passed to all resolver functions. * * @remarks * `ResolveInfo`, similar to GraphQL.js’ `GraphQLResolveInfo` object, * gives your resolvers a global state of the current GraphQL * document traversal. * * `parent`, `parenTypeName`, `parentKey`, and `parentFieldKey` * are particularly useful to make reusable resolver functions that * must know on which field and type they’re being called on. */ export interface ResolveInfo { /** The parent GraphQL object. * * @remarks * The GraphQL object that the resolver has been called on. Because this is * a reference to raw GraphQL data, this may be incomplete or contain * aliased fields! */ parent: Data; /** The parent object’s typename that the resolver has been called on. */ parentTypeName: string; /** The parent object’s entity key that the resolver has been called on. */ parentKey: string; /** Current field’s key that the resolver has been called on. */ parentFieldKey: string; /** Current field that the resolver has been called on. */ fieldName: string; /** Map of fragment definitions from the query document. */ fragments: Fragments; /** Full original {@link Variables} object on the {@link OperationRequest}. */ variables: Variables; /** Error that occurred for the current field, if any. * * @remarks * If a {@link GraphQLError.path} points at the current field, the error * will be set and provided here. This can be useful to recover from an * error on a specific field. */ error: ErrorLike | undefined; /** Flag used to indicate whether the current GraphQL query is only partially cached. * * @remarks * When Graphcache has {@link CacheExchangeOpts.schema} introspection information, * it can automatically generate partial results and trigger a full API request * in the background. * Hence, this field indicates whether any data so far has only been partially * resolved from the cache, and is only in use on {@link Resolver | Resolvers}. * * However, you can also flip this flag to `true` manually to indicate to * the {@link cacheExchange} that it should still make a network request. */ partial?: boolean; /** Flag used to indicate whether the current GraphQL mutation is optimistically executed. * * @remarks * An {@link UpdateResolver} is called for both API mutation responses and * optimistic mutation reuslts, as generated by {@link OptimisticMutationResolver}. * * Since an update sometimes needs to perform different actions if it’s run * optimistically, this flag is set to `true` during optimisti cupdates. */ optimistic?: boolean; /** Internal state used by Graphcache. * @internal */ __internal?: unknown; } /** GraphQL document and variables that should be queried against the cache. * * @remarks * `QueryInput` is a generic GraphQL request that should be executed against * cached data, as accepted by {@link cache.readQuery}. */ export interface QueryInput { query: DocumentInput; variables?: V; } /** Interface to interact with cached data, which resolvers receive. */ export interface Cache { /** Returns the cache key for a given entity or `null` if it’s unkeyable. * * @param entity - the {@link Entity} to generate a key for. * @returns the entity’s key or `null`. * * @remarks * `cache.keyOfEntity` may be called with a partial GraphQL object (“entity”) * and generates a key for it. It uses your {@link KeyingConfig} and otherwise * defaults to `id` and `_id` fields. * * If it’s passed a `string` or `null`, it will simply return what it’s been passed. * Objects that lack a `__typename` field will return `null`. */ keyOfEntity(entity: Entity | undefined): string | null; /** Returns the cache key for a field. * * @param fieldName - the field’s name. * @param args - the field’s arguments, if any. * @returns the field key * * @remarks * `cache.keyOfField` is used to create a field’s cache key from a given * field name and its arguments. This is used internally by {@link cache.resolve} * to combine an entity key and a field key into a path that normalized data is * accessed on in Graphcache’s internal data structure. */ keyOfField(fieldName: string, args?: FieldArgs): string | null; /** Returns a cached value on a given entity’s field. * * @param entity - a GraphQL object (“entity”) or an entity key. * @param fieldName - the field’s name. * @param args - the field’s arguments, if any. * @returns the field’s value or the entity key(s) this field is pointing at. * * @remarks * `cache.resolve` is used to retrieve either the cached value of a field, or * to get the relation of the field (“link”). When a cached field points at * another normalized entity, this method will return the related entity key * (or a list, if it’s pointing at a list of entities). * * As such, if you’re accessing a nested field, you may have to call * `cache.resolve` again and chain its calls. * * Hint: If you have a field key from {@link FieldInfo} or {@link cache.keyOfField}, * you may pass it as a second argument. * * @example * ```ts * const authorName = cache.resolve( * cache.resolve({ __typename: 'Book', id }, 'author'), * 'name' * ); * ``` */ resolve( entity: Entity | undefined, fieldName: string, args?: FieldArgs ): DataField | undefined; /** Returns a list of cached fields for a given GraphQL object (“entity”). * * @param entity - a GraphQL object (“entity”) or an entity key. * @returns a list of {@link FieldInfo} objects. * * @remarks * `cache.inspectFields` can be used to list out all known fields * of a given entity. This can be useful in an {@link UpdateResolver} * if you have a `Query` field that accepts many different arguments, * for instance a paginated field. * * The returned list of fields are all fields that the cache knows about, * and you may have to filter them by name or arguments to find only which * ones you need. * * Hint: This method is theoretically a slower operation than simple * cache lookups, as it has to decode field keys. It’s only recommended * to be used in updaters. */ inspectFields(entity: Entity): FieldInfo[]; /** Deletes a cached entity or an entity’s field. * * @param entity - a GraphQL object (“entity”) or an entity key. * @param fieldName - optionally, a field name. * @param args - optionally, the field’s arguments, if any. * * @remarks * `cache.invalidate` can be used in updaters to delete data from * the cache. This will cause the {@link cacheExchange} to reexecute * queries that contain the deleted data. * * If you only pass its first argument, the entire entity is deleted. * However, if a field name (and optionally, its arguments) are passed, * only a single field is erased. */ invalidate( entity: Entity | undefined, fieldName?: string, args?: FieldArgs ): void; /** Updates a GraphQL query‘s cached data. * * @param input - a {@link QueryInput}, which is a GraphQL query request. * @param updater - a function called with the query’s result or `null` in case of a cache miss, which * may return updated data, which is written to the cache using the query. * * @remarks * `cache.updateQuery` can be used to update data for an entire GraphQL query document. * When it's passed a GraphQL query request, it calls the passed `updater` function * with the cached result for this query. You may then modify and update the data and * return it, after which it’s written back to the cache. * * Hint: While this allows for large updates at once, {@link cache.link}, * {@link cache.resolve}, and {@link cache.writeFragment} are often better * choices for more granular and compact updater code. * * @example * ```ts * cache.updateQuery({ query: TodoList }, data => { * data.todos.push(newTodo); * return data; * }); * ``` */ updateQuery( input: QueryInput, updater: (data: T | null) => T | null ): void; /** Returns a GraphQL query‘s cached result. * * @param input - a {@link QueryInput}, which is a GraphQL query request. * @returns the cached data result of the query or `null`, in case of a cache miss. * * @remarks * `cache.readQuery` can be used to read an entire query’s data all at once * from the cache. * * This can be useful when typing out many {@link cache.resolve} * calls is too tedious. * * @example * ```ts * const data = cache.readQuery({ * query: TodosQuery, * variables: { from: 0, limit: 10 } * }); * ``` */ readQuery(input: QueryInput): T | null; /** Returns a GraphQL fragment‘s cached result. * * @param fragment - a {@link DocumentNode} containing a fragment definition. * @param entity - a GraphQL object (“entity”) or an entity key to read the fragment on. * @param variables - optionally, GraphQL variables, if the fragments use any. * @returns the cached data result of the fragment or `null`, in case of a cache miss. * * @remarks * `cache.readFragment` can be used to read an entire query’s data all at once * from the cache. * * It attempts to read the fragment starting from the `entity` that’s passed to it. * If the entity can’t be resolved or has mismatching types, `null` is returned. * * This can be useful when typing out many {@link cache.resolve} * calls is too tedious. * * @example * ```ts * const data = cache.readFragment( * gql`fragment _ on Todo { id, text }`, * { id: '123' } * ); * ``` */ readFragment( fragment: TypedDocumentNode | TypedDocumentNode, entity: string | Data | T, variables?: V ): T | null; /** Writes a GraphQL fragment to the cache. * * @param fragment - a {@link DocumentNode} containing a fragment definition. * @param data - a GraphQL object to be written with the given fragment. * @param variables - optionally, GraphQL variables, if the fragments use any. * * @remarks * `cache.writeFragment` can be used to write an entity to the cache. * The method will generate a key for the `data` it’s passed, and start writing * it using the fragment. * * This method is used when writing scalar values to the cache. * Since it's rare for an updater to write values to the cache, {@link cache.link} * only allows relations (“links”) to be updated, and `cache.writeFragment` is * instead used when writing multiple scalars. * * @example * ```ts * const data = cache.writeFragment( * gql`fragment _ on Todo { id, text }`, * { id: '123', text: 'New Text' } * ); * ``` */ writeFragment( fragment: TypedDocumentNode | TypedDocumentNode, data: T, variables?: V ): void; /** Updates the relation (“link”) from an entity’s field to another entity. * * @param entity - a GraphQL object (“entity”) or an entity key. * @param fieldName - the field’s name. * @param args - optionally, the field’s arguments, if any. * @param link - the GraphQL object(s) that should be set on this field. * * @remarks * The normalized cache stores relations between GraphQL objects separately. * As such, a field can be updated using `cache.link` to point to a new entity, * or a list of entities. * * In other words, `cache.link` is used to set a field to point to another * entity or a list of entities. * * @example * ```ts * const todos = cache.resolve('Query', 'todos'); * cache.link('Query', 'todos', [...todos, newTodo]); * ``` */ link( entity: Entity, field: string, args: FieldArgs, link: Link ): void; link(entity: Entity, field: string, value: Link): void; } /** Values a {@link Resolver} may return. * * @remarks * A resolver may return any value that a GraphQL object may contain. * * Additionally however, a resolver may return `undefined` to indicate that data * isn’t available from the cache, i.e. to trigger a cache miss. */ export type ResolverResult = | DataField | (DataFields & { __typename?: string }) | null | undefined; export type Logger = ( severity: 'debug' | 'error' | 'warn', message: string ) => void; /** Input parameters for the {@link cacheExchange}. */ export type CacheExchangeOpts = { /** Configure a custom-logger for graphcache, this function wll be called with a severity and a message. * * @remarks * By default we will invoke `console.warn` for warnings during development, however you might want to opt * out of this because you are re-using urql for a different library. This setting allows you to stub the logger * function or filter to only logs you want. */ logger?: Logger; /** Configures update functions which are called when the mapped fields are written to the cache. * * @remarks * `updates` are commonly used to define additional changes to the cache for * mutation or subscription fields. It may commonly be used to invalidate * cached data or to modify lists after mutations. * This is a map of types to fields to {@link UpdateResolver} functions. * * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates} for the full updates docs. */ updates?: UpdatesConfig; /** Configures resolvers which replace cached reuslts with custom values. * * @remarks * `resolvers` is a map of types to fields to {@link Resolver} functions. * These functions allow us to replace cached field values with a custom * result, either to replace values on GraphQL results, or to resolve * entities from the cache for queries that haven't been sent to the API * yet. * * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs. */ resolvers?: ResolverConfig; /** Configures directives which can perform custom logic on fields. * * @remarks * A {@link DirectivesConfig} may be passed to allow local directives to be used. For example, when `@_custom` is placed on a field and the configuration contains `custom` then this directive is executed by Graphcache. * * @see {@link https://urql.dev/goto/docs/graphcache/local-directives} for the full directives docs. */ directives?: DirectivesConfig; /** Configures optimistic updates to react to mutations instantly before an API response. * * @remarks * `optimistic` is a map of mutation fields to {@link OptimisticMutationResolver} functions. * These functions allow us to return result data for mutations to optimistically apply them. * Optimistic updates are temporary updates to the cache’s data which allow an app to * instantly reflect changes that a mutation will make. * * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates/#optimistic-updates} for the * full optimistic updates docs. */ optimistic?: OptimisticMutationConfig; /** Configures keying functions for GraphQL types. * * @remarks * `keys` is a map of GraphQL object type names to {@link KeyGenerator} functions. * If a type in your API has no key field or a key field that isn't the default * `id` or `_id` fields, you may define a custom key generator for the type. * * Hint: Graphcache will log warnings when it finds objects that have no keyable * fields, which will remind you to define these functions gradually for every * type that needs them. * * @see {@link https://urql.dev/goto/docs/graphcache/normalized-caching/#custom-keys-and-non-keyable-entities} for * the full keys docs. */ keys?: KeyingConfig; /** Enables global IDs for keying GraphQL types. * * @remarks * When `globalIDs` are enabled, GraphQL object type names will not contribute * to the keys of entities and instead only their ID fields (or `keys` return * values will be used. * * This is useful to overlap types of differing typenames. While this isn’t recommended * it can be necessary to represent more complex interface relationships. * * If this should only be applied to a limited set of type names, a list of * type names may be passed instead. */ globalIDs?: string[] | boolean; /** Configures abstract to concrete types mapping for GraphQL types. * * @remarks * This will disable heuristic fragment matching, allowing Graphcache to match * fragment deterministically. * * When both `possibleTypes` and `schema` is set, `possibleTypes` value will be * ignored. */ possibleTypes?: PossibleTypesConfig; /** Configures Graphcache with Schema Introspection data. * * @remarks * Passing a `schema` to Graphcache enables it to do non-heuristic fragment * matching, and be certain when a fragment matches against a union or interface * on your schema. * * It also enables a mode called “Schema Awareness”, which allows Graphcache to * return partial GraphQL results, `null`-ing out fields that aren’t in the cache * that are nullable on your schema, while requesting the full API response in * the background. * * @see {@link https://urql.dev/goto/urql/docs/graphcache/schema-awareness} for * the full keys docs on Schema Awareness. */ schema?: IntrospectionData; /** Configures an offline storage adapter for Graphcache. * * @remarks * A {@link StorageAdapter} allows Graphcache to write data to an external, * asynchronous storage, and hydrate data from it when it first loads. * This allows you to preserve normalized data between restarts/reloads. * * Hint: If you’re trying to use Graphcache’s Offline Support, you may * want to swap out the `cacheExchange` with the {@link offlineExchange}. * * @see {@link https://urql.dev/goto/docs/graphcache/offline} for the full Offline Support docs. */ storage?: StorageAdapter; }; /** Cache Resolver, which may resolve or replace data during cache reads. * * @param parent - The GraphQL object that is currently being constructed from cache data. * @param args - This field’s arguments. * @param cache - {@link Cache} interface. * @param info - {@link ResolveInfo} interface. * @returns a {@link ResolverResult}, which is an updated value, partial entity, or entity key * * @remarks * A `Resolver`, as defined on the {@link ResolverConfig}, is called for * a field’s type during cache reads, and can be used to deserialize or replace * scalar values, or to resolve an entity from cached data, even if the * current field hasn’t been cached from an API response yet. * * For instance, if you have a `Query.picture(id: ID!)` field, you may define * a resolver that returns `{ __typename: 'Picture', id: args.id }`, since you * know the key fields of the GraphQL object. * * @example * ```ts * cacheExchange({ * resolvers: { * Query: { * // resolvers can be used to resolve cached entities without API requests * todo: (_parent, args) => ({ __typename: 'Todo', id: args.id }), * }, * Todo: { * // resolvers can also be used to replace/deserialize scalars * updatedAt: parent => new Date(parent.updatedAt), * }, * }, * }); * ``` * * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs. */ export type Resolver< ParentData = DataFields, Args = Variables, Result = ResolverResult, > = { bivarianceHack( parent: ParentData, args: Args, cache: Cache, info: ResolveInfo ): Result; }['bivarianceHack']; /** Configures resolvers which replace cached reuslts with custom values. * * @remarks * A `ResolverConfig` is a map of types to fields to {@link Resolver} functions. * These functions allow us to replace cached field values with a custom * result, either to replace values on GraphQL results, or to resolve * entities from the cache for queries that haven't been sent to the API * yet. * * @see {@link https://urql.dev/goto/docs/graphcache/local-resolvers} for the full resolvers docs. */ export type ResolverConfig = { [typeName: string]: { [fieldName: string]: Resolver | void; } | void; }; export type Directive = ( directiveArguments: Record | null ) => Resolver; export type DirectivesConfig = { [directiveName: string]: Directive; }; /** Cache Updater, which defines additional cache updates after cache writes. * * @param parent - The GraphQL object that is currently being written to the cache. * @param args - This field’s arguments. * @param cache - {@link Cache} interface. * @param info - {@link ResolveInfo} interface. * * @remarks * An `UpdateResolver` (“updater”), as defined on the {@link UpdatesConfig}, is * called for a field’s type during cache writes, and can be used to instruct * the {@link Cache} to perform other cache updates at the same time. * * This is often used, for instance, to update lists or invalidate entities * after a mutation response has come back from the API. * * @example * ```ts * cacheExchange({ * updates: { * Mutation: { * // updaters can invalidate data from the cache * deleteAuthor: (_parent, args, cache) => { * cache.invalidate({ __typename: 'Author', id: args.id }); * }, * }, * }, * }); * ``` * * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates} for the * full cache updates docs. */ export type UpdateResolver = { bivarianceHack( parent: ParentData, args: Args, cache: Cache, info: ResolveInfo ): void; }['bivarianceHack']; /** A key functon, which is called to create a cache key for a GraphQL object (“entity”). * * @param data - The GraphQL object that a key is generated for. * @returns a key `string` or `null` or unkeyable objects. * * @remarks * By default, Graphcache will use an object’s `__typename`, and `id` or `_id` fields * to generate a key for an object. However, not all GraphQL objects will have a unique * field, and some objects don’t have a key at all. * * When one of your GraphQL object types has a different key field, you may define a * function on the {@link KeyingConfig} to return its key field. * You may also have objects that don’t have keys, like “Edge” objects, or scalar-like * objects. For these, you can define a function that returns `null`, which tells * Graphcache that it’s an embedded object, which only occurs on its parent and is * globally unique. * * @see {@link https://urql.dev/goto/docs/graphcache/normalized-caching/#custom-keys-and-non-keyable-entities} for * the full keys docs. * * @example * ```ts * cacheExchange({ * keys: { * Image: data => data.url, * LatLng: () => null, * }, * }); * ``` */ export type KeyGenerator = { bivarianceHack(data: Data): string | null; }['bivarianceHack']; /** Configures update functions which are called when the mapped fields are written to the cache. * * @remarks * `UpdatesConfig` is a map of types to fields to {@link UpdateResolver} functions. * These update functions are defined to instruct the cache to make additional changes * when a field is written to the cache. * * As changes are often made after a mutation or subscription, the `typeName` is * often set to `'Mutation'` or `'Subscription'`. * * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates} for the full updates docs. * * @example * ```ts * const updates = { * Mutation: { * deleteAuthor(_parent, args, cache) { * // Delete the Author from the cache when Mutation.deleteAuthor is sent * cache.invalidate({ __typename: 'Author', id: args.id }); * }, * }, * }; */ export type UpdatesConfig = { [typeName: string | 'Query' | 'Mutation' | 'Subscription']: { [fieldName: string]: UpdateResolver | void; } | void; }; /** Remaps result type to allow for nested optimistic mutation resolvers. * * @remarks * An {@link OptimisticMutationResolver} can not only return partial, nested * mutation result data, but may also contain more optimistic mutation resolvers * for nested fields, which allows fields with arguments to optimistically be * resolved to dynamic values. * * @see {@link OptimisticMutationConfig} for more information. */ export type MakeFunctional = T extends { __typename: string } ? WithTypename<{ [P in keyof T]?: MakeFunctional; }> : OptimisticMutationResolver | T; /** Optimistic mutation resolver, which may return data that a mutation response will return. * * @param args - This field’s arguments. * @param cache - {@link Cache} interface. * @param info - {@link ResolveInfo} interface. * @returns the field’s optimistic data * * @remarks * Graphcache can update its cache optimistically via the {@link OptimisticMutationConfig}. * An `OptimisticMutationResolver` should return partial data that a mutation will return * once it completes and we receive its result. * * For instance, it could return the data that a deletion mutation may return * optimistically, which might allow an updater to run early and your UI to update * instantly. * * The result that this function returns may miss some fields that your mutation may return, * especially if it contains GraphQL object that are already cached. It may also contain * other, nested resolvers, which allows you to handle fields that accept arguments. */ export type OptimisticMutationResolver< Args = Variables, Result = Link | Scalar, > = { bivarianceHack( args: Args, cache: Cache, info: ResolveInfo ): MakeFunctional; }['bivarianceHack']; /** Configures optimistic result functions which are called to get a mutation’s optimistic result. * * @remarks * `OptimisticMutationConfig` is a map of mutation fields to {@link OptimisticMutationResolver} * functions, which return result data for mutations to optimistically apply them. * Optimistic updates are temporary updates to the cache’s data which allow an app to * instantly reflect changes that a mutation will make. * * Hint: Results returned from optimistic functions may be partial, and may contain functions. * If the returned optimistic object contains functions on fields, these are executed as nested * optimistic resolver functions. * * @see {@link https://urql.dev/goto/docs/graphcache/cache-updates/#optimistic-updates} for the * full optimistic updates docs. * * @example * ```ts * const optimistic = { * updateProfile: (args) => ({ * __typename: 'UserProfile', * id: args.id, * name: args.newName, * }), * }; */ export type OptimisticMutationConfig = { [mutationFieldName: string]: OptimisticMutationResolver; }; /** Configures keying functions for GraphQL types. * * @remarks * `KeyingConfig` is a map of GraphQL object type names to {@link KeyGenerator} functions. * If a type in your API has no key field or a key field that isn't the default * `id` or `_id` fields, you may define a custom key generator for the type. * * Keys are important to a normalized cache, because they’re the identity of the object * that is shared across the cache, and helps the cache recognize shared/normalized data. * * Hint: Graphcache will log warnings when it finds objects that have no keyable * fields, which will remind you to define these functions gradually for every * type that needs them. * * @see {@link https://urql.dev/goto/docs/graphcache/normalized-caching/#custom-keys-and-non-keyable-entities} for * the full keys docs. * * @example * ```ts * const keys = { * Image: data => data.url, * LatLng: () => null, * }; * ``` */ export type KeyingConfig = { [typename: string]: KeyGenerator; }; export type PossibleTypesConfig = { [abstractType: string]: string[]; }; /** Serialized normalized caching data. */ export interface SerializedEntries { [key: string]: string | undefined; } /** A serialized GraphQL request for offline storage. */ export interface SerializedRequest { query: string; variables: AnyVariables | undefined; extensions?: RequestExtensions | undefined; } /** Interface for a storage adapter, used by the {@link offlineExchange} for Offline Support. * @see {@link https://urql.dev/goto/docs/graphcache/offline} for the full Offline Support docs. * @see `@urql/exchange-graphcache/default-storage` for an example implementation using IndexedDB. */ export interface StorageAdapter { /** Called to rehydrate data when the {@link cacheExchange} first loads. * @remarks * `readData` is called when Graphcache first starts up, and loads cache entries * using which it'll repopulate its normalized cache data. */ readData(): Promise; /** Called by the {@link cacheExchange} to write new data to the offline storage. * @remarks * `writeData` is called when Graphcache updated its cached data and wishes to * persist this data to the offline storage. The data is a partial object and * Graphcache does not write all its data at once. */ writeData(delta: SerializedEntries): Promise; /** Called to rehydrate metadata when the {@link offlineExchange} first loads. * @remarks * `readMetadata` is called when Graphcache first starts up, and loads * metadata informing it of pending mutations that failed while the device * was offline. */ readMetadata?(): Promise; /** Called by the {@link offlineExchange} to persist failed mutations. * @remarks * `writeMetadata` is called when a mutation failed to persist a queue * of failed mutations to the offline storage that must be retried when * the application is reloaded. */ writeMetadata?(json: SerializedRequest[]): void; /** Called to register a callback called when the device is back online. * @remarks * `onOnline` is called by the {@link offlineExchange} with a callback. * This callback must be called when the device comes back online and * will cause all failed mutations in the queue to be retried. */ onOnline?(cb: () => void): any; /** Called when the cache has been hydrated with the data from `readData` */ onCacheHydrated?(): any; } /** Set of keys that have been modified or accessed. * @internal */ export type Dependencies = Set; /** The type of cache operation being executed. * @internal */ export type OperationType = 'read' | 'write'; /** Casts a given object type to have a required typename field. * @internal */ export type WithTypename = T & { __typename: NonNullable; }; ================================================ FILE: exchanges/graphcache/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/graphcache/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: exchanges/persisted/CHANGELOG.md ================================================ # @urql/exchange-persisted-fetch ## 5.0.1 ### Patch Changes - Use nullish coalescing for `preferGetMethod` and `preferGetForPersistedQueries` so that `false` is kept if set Submitted by [@dargmuesli](https://github.com/dargmuesli) (See [#3812](https://github.com/urql-graphql/urql/pull/3812)) - Updated dependencies (See [#3812](https://github.com/urql-graphql/urql/pull/3812)) - @urql/core@6.0.1 ## 5.0.0 ### Major Changes - By default leverage GET for queries where the query-string + variables comes down to less than 2048 characters. When upgrading it's important to see whether your server supports `GET`, if it doesn't ideally adding support for it or alternatively setting `preferGetMethod` in the `createClient` method as well as `preferGetForPersistedQueries` for the persisted exchange to `false` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3789](https://github.com/urql-graphql/urql/pull/3789)) ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 4.3.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 4.3.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 4.2.0 ### Minor Changes - Add option to enable persisted-operations for subscriptions Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3549](https://github.com/urql-graphql/urql/pull/3549)) ## 4.1.1 ### Patch Changes - Warn about cached persisted-miss results in development, when a `persistedExchange()` sees a persisted-miss error for a result that's already seen a persisted-miss error (i.e. two misses). This shouldn't happen unless something is caching persisted errors and we should warn about this appropriately Submitted by [@kitten](https://github.com/kitten) (See [#3442](https://github.com/urql-graphql/urql/pull/3442)) ## 4.1.0 ### Minor Changes - Allow persisted query logic to be skipped by the `persistedExchange` if the passed `generateHash` function resolves to a nullish value. This allows (A)PQ to be selectively disabled for individual operations Submitted by [@kitten](https://github.com/kitten) (See [#3324](https://github.com/urql-graphql/urql/pull/3324)) ### Patch Changes - Updated dependencies (See [#3317](https://github.com/urql-graphql/urql/pull/3317) and [#3308](https://github.com/urql-graphql/urql/pull/3308)) - @urql/core@4.1.0 ## 4.0.1 ### Patch Changes - ⚠️ Fix `persistedExchange` ignoring teardowns in its initial operation mapping. Since the hash function is promisified, which defers any persisted operation, it needs to respect teardowns Submitted by [@kitten](https://github.com/kitten) (See [#3312](https://github.com/urql-graphql/urql/pull/3312)) ## 4.0.0 ### Major Changes - Update the `preferGetForPersistedQueries` option to include the `'force'` and `'within-url-limit'` values from the Client's `preferGetMethod` option. The default value of `true` no longer sets `OperationContext`'s `preferGetMethod` setting to `'force'`. Instead, the value of `preferGetForPersistedQueries` carries through to the `OperationContext`'s `preferGetMethod` setting for persisted queries Submitted by [@NWRichmond](https://github.com/NWRichmond) (See [#3192](https://github.com/urql-graphql/urql/pull/3192)) ## 3.0.1 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 3.0.0 ### Major Changes - Remove `persistedFetchExchange` and instead implement `persistedExchange`. This exchange must be placed in front of a terminating exchange (such as the default `fetchExchange` or a `subscriptionExchange` that supports persisted queries), and only modifies incoming operations to contain `extensions.persistedQuery`, which is sent on via the API. If the API expects Automatic Persisted Queries, requests are retried by this exchange internally Submitted by [@kitten](https://github.com/kitten) (See [#3057](https://github.com/urql-graphql/urql/pull/3057)) - Rename `@urql/exchange-persisted-fetch` to `@urql/exchange-persisted` Submitted by [@kitten](https://github.com/kitten) (See [#3057](https://github.com/urql-graphql/urql/pull/3057)) ### Minor Changes - Update exchanges to drop redundant `share` calls, since `@urql/core`’s `composeExchanges` utility now automatically does so for us Submitted by [@kitten](https://github.com/kitten) (See [#3082](https://github.com/urql-graphql/urql/pull/3082)) ### Patch Changes - Refactor SHA256 logic to save on bundlesize Submitted by [@kitten](https://github.com/kitten) (See [#3052](https://github.com/urql-graphql/urql/pull/3052)) - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs for all exchanges, documenting API internals Submitted by [@kitten](https://github.com/kitten) (See [#3072](https://github.com/urql-graphql/urql/pull/3072)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 2.1.0 ### Minor Changes - Adds enableForMutation option for exchange-persisted-fetch to enable persisted operations for mutations, by [@geekuillaume](https://github.com/geekuillaume) (See [#2951](https://github.com/urql-graphql/urql/pull/2951)) ## 2.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Minor Changes - Remove the `babel-plugin-modular-graphql` helper, this because the graphql package hasn't converted to ESM yet which gives issues in node environments, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2551](https://github.com/FormidableLabs/urql/pull/2551)) ### Patch Changes - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 1.3.4 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 1.3.3 ### Patch Changes - ⚠️ Fix Crypto API support for Web Workers and Node Crypto in ESM mode. Previously, when Node Crypto was required in Node ESM mode it would result in an error instead, since we didn't try a dynamic import fallback, by [@kitten](https://github.com/kitten) (See [#2123](https://github.com/FormidableLabs/urql/pull/2123)) ## 1.3.2 ### Patch Changes - Optimize for minification by avoiding direct eval call, by [@nderscore](https://github.com/nderscore) (See [#1744](https://github.com/FormidableLabs/urql/pull/1744)) - Updated dependencies (See [#1776](https://github.com/FormidableLabs/urql/pull/1776) and [#1755](https://github.com/FormidableLabs/urql/pull/1755)) - @urql/core@2.1.5 ## 1.3.1 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 1.3.0 ### Minor Changes - Add `enforcePersistedQueries` option to `persistedFetchExchange`, which disables automatic persisted queries and retry logic, and instead assumes that persisted queries will be handled like normal GraphQL requests, by [@kitten](https://github.com/kitten) (See [#1358](https://github.com/FormidableLabs/urql/pull/1358)) ### Patch Changes - Updated dependencies (See [#1374](https://github.com/FormidableLabs/urql/pull/1374), [#1357](https://github.com/FormidableLabs/urql/pull/1357), and [#1375](https://github.com/FormidableLabs/urql/pull/1375)) - @urql/core@2.0.0 ## 1.2.3 ### Patch Changes - ⚠️ Fix the production build overwriting the development build. Specifically in the previous release we mistakenly replaced all development bundles with production bundles. This doesn't have any direct influence on how these packages work, but prevented development warnings from being logged or full errors from being thrown, by [@kitten](https://github.com/kitten) (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - Updated dependencies (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - @urql/core@1.14.1 ## 1.2.2 ### Patch Changes - Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was previously confusing as `operationName` was effectively referring to two different things. You can safely upgrade to this new version, however to mute all deprecation warnings you will have to **upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please use [the new `makeOperation` helper function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead, by [@bkonkle](https://github.com/bkonkle) (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - Updated dependencies (See [#1094](https://github.com/FormidableLabs/urql/pull/1094) and [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - @urql/core@1.14.0 ## 1.2.1 ### Patch Changes - Omit the `Content-Type: application/json` HTTP header when using GET in the `fetchExchange`, `persistedFetchExchange`, or `multipartFetchExchange`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#957](https://github.com/FormidableLabs/urql/pull/957)) - Stops sending a persisted query if the hashing function fails, by [@lorenries](https://github.com/lorenries) (See [#934](https://github.com/FormidableLabs/urql/pull/934)) - Updated dependencies (See [#947](https://github.com/FormidableLabs/urql/pull/947), [#962](https://github.com/FormidableLabs/urql/pull/962), and [#957](https://github.com/FormidableLabs/urql/pull/957)) - @urql/core@1.13.0 ## 1.2.0 ### Minor Changes - Pass the parsed GraphQL-document as a second argument to the `generateHash` option, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#887](https://github.com/FormidableLabs/urql/pull/887)) ### Patch Changes - Updated dependencies (See [#911](https://github.com/FormidableLabs/urql/pull/911) and [#908](https://github.com/FormidableLabs/urql/pull/908)) - @urql/core@1.12.3 ## 1.1.0 ### Minor Changes - Adds support for custom hash functions by adding a `generateHash` option to the exchange, by [@lorenries](https://github.com/lorenries) (See [#870](https://github.com/FormidableLabs/urql/pull/870)) ### Patch Changes - Updated dependencies (See [#880](https://github.com/FormidableLabs/urql/pull/880) and [#885](https://github.com/FormidableLabs/urql/pull/885)) - @urql/core@1.12.2 ## 1.0.1 ### Patch Changes - Upgrade to a minimum version of wonka@^4.0.14 to work around issues with React Native's minification builds, which use uglify-es and could lead to broken bundles, by [@kitten](https://github.com/kitten) (See [#842](https://github.com/FormidableLabs/urql/pull/842)) - Updated dependencies (See [#838](https://github.com/FormidableLabs/urql/pull/838) and [#842](https://github.com/FormidableLabs/urql/pull/842)) - @urql/core@1.12.0 ## 1.0.0 ### Major Changes - Change the `persistedFetchExchange` to be an exchange factory. The `persistedFetchExchange` now expects to be called with options. An optional option, `preferGetForPersistedQueries`, may now be passed to send persisted queries as a GET request, even when the `Client`'s `preferGetMethod` option is `false`. To migrate you will have to update your usage of `persistedFetchExchange` from ```js import { persistedFetchExchange } from '@urql/exchange-persisted-fetch'; createClient({ exchanges: [persistedFetchExchange], }); ``` to the following: ```js import { persistedFetchExchange } from '@urql/exchange-persisted-fetch'; createClient({ exchanges: [ // Call the exchange and pass optional options: persistedFetchExchange(), ], }); ``` ### Patch Changes - Replace `js-sha256` polyfill for Node.js support with Node's Native Crypto API, by [@kitten](https://github.com/kitten) (See [#807](https://github.com/FormidableLabs/urql/pull/807)) - Updated dependencies (See [#798](https://github.com/FormidableLabs/urql/pull/798)) - @urql/core@1.11.8 ## 0.1.3 ### Patch Changes - Add `source` debug name to all `dispatchDebug` calls during build time to identify events by which exchange dispatched them, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#780](https://github.com/FormidableLabs/urql/pull/780)) - Updated dependencies (See [#780](https://github.com/FormidableLabs/urql/pull/780)) - @urql/core@1.11.7 ## 0.1.2 ### Patch Changes - Add a `"./package.json"` entry to the `package.json`'s `"exports"` field for Node 14. This seems to be required by packages like `rollup-plugin-svelte` to function properly, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - Updated dependencies (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - @urql/core@1.11.6 ## 0.1.1 ### Patch Changes - ⚠️ Fix `persistedFetchExchange` not sending the SHA256 hash extension after a cache miss (`PersistedQueryNotFound` error), by [@kitten](https://github.com/kitten) (See [#766](https://github.com/FormidableLabs/urql/pull/766)) ## 0.1.0 This is the initial release of `@urql/exchange-persisted-fetch` which adds Persisted Queries support, and is an exchange that can be used alongside the default `fetchExchange` or `@urql/exchange-multipart-fetch`. It's still experimental, just like `@urql/exchange-multipart-fetch`, so please test it with care and report any bugs you find. ================================================ FILE: exchanges/persisted/README.md ================================================ # @urql/exchange-persisted The `persistedExchange` is an exchange that allows other terminating exchanges to support Persisted Queries, and is as such placed in front of either the default `fetchExchange` or other terminating exchanges. ## Quick Start Guide First install `@urql/exchange-persisted` alongside `urql`: ```sh yarn add @urql/exchange-persisted # or npm install --save @urql/exchange-persisted ``` You'll then need to add the `persistedExchange` function, that this package exposes, to your `exchanges`. ```js import { createClient, fetchExchange, cacheExchange } from 'urql'; import { persistedExchange } from '@urql/exchange-persisted'; const client = createClient({ url: 'http://localhost:1234/graphql', exchanges: [ cacheExchange, persistedExchange({ /* optional config */ }), fetchExchange, ], }); ``` The `persistedExchange` supports three configuration options: - `preferGetForPersistedQueries`: Enforce `GET` method to be used by the default `fetchExchange` for persisted queries - `enforcePersistedQueries`: This disables _automatic persisted queries_ and disables any retry logic for how the API responds to persisted queries. Instead it's assumed that they'll always succeed. - `generateHash`: A function that takes a GraphQL query and returns the hashed result. This defaults to the `window.crypto` API in the browser and the `crypto` module in Node. - `enableForMutation`: By default, the exchange only handles `query` operations, but enabling this allows it to handle mutations as well. ## Avoid hashing during runtime If you want to generate hashes at build-time you can use a [webpack-loader](https://github.com/leoasis/graphql-persisted-document-loader) to achieve this, when using this all you need to do in this exchange is the following: ```js import { createClient, fetchExchange, cacheExchange } from 'urql'; import { persistedExchange } from '@urql/exchange-persisted'; const client = createClient({ url: 'http://localhost:1234/graphql', exchanges: [ cacheExchange, persistedExchange({ generateHash: (_, document) => document.documentId, }), fetchExchange, ], }); ``` ================================================ FILE: exchanges/persisted/jsr.json ================================================ { "name": "@urql/exchange-persisted", "version": "5.0.1", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/persisted/package.json ================================================ { "name": "@urql/exchange-persisted", "version": "5.0.1", "description": "An exchange that allows for persisted queries support when fetching queries", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/persisted" }, "keywords": [ "urql", "graphql", "persisted queries", "exchanges" ], "main": "dist/urql-exchange-persisted", "module": "dist/urql-exchange-persisted.mjs", "types": "dist/urql-exchange-persisted.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-persisted.d.ts", "import": "./dist/urql-exchange-persisted.mjs", "require": "./dist/urql-exchange-persisted.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist extras", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "peerDependencies": { "@urql/core": "^6.0.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "devDependencies": { "@urql/core": "workspace:*", "graphql": "^16.0.0" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/persisted/src/index.ts ================================================ export * from './persistedExchange'; ================================================ FILE: exchanges/persisted/src/persistedExchange.test.ts ================================================ import { Source, pipe, fromValue, fromArray, toPromise, delay, take, tap, map, } from 'wonka'; import { Client, Operation, OperationResult, CombinedError } from '@urql/core'; import { vi, expect, it } from 'vitest'; import { queryResponse, queryOperation, } from '../../../packages/core/src/test-utils'; import { persistedExchange } from './persistedExchange'; const makeExchangeArgs = () => { const operations: Operation[] = []; const result = vi.fn( (operation: Operation): OperationResult => ({ ...queryResponse, operation }) ); return { operations, result, exchangeArgs: { forward: (op$: Source) => pipe( op$, tap(op => operations.push(op)), map(result) ), client: new Client({ url: '/api', exchanges: [] }), } as any, }; }; it('adds the APQ extensions correctly', async () => { const { exchangeArgs } = makeExchangeArgs(); const res = await pipe( fromValue(queryOperation), persistedExchange()(exchangeArgs), take(1), toPromise ); expect(res.operation.context.persistAttempt).toBe(true); expect(res.operation.extensions).toEqual({ persistedQuery: { version: 1, sha256Hash: expect.any(String), miss: undefined, }, }); }); it('retries query when persisted query resulted in miss', async () => { const { result, operations, exchangeArgs } = makeExchangeArgs(); result.mockImplementationOnce(operation => ({ ...queryResponse, operation, error: new CombinedError({ graphQLErrors: [{ message: 'PersistedQueryNotFound' }], }), })); const res = await pipe( fromValue(queryOperation), persistedExchange()(exchangeArgs), take(1), toPromise ); expect(res.operation.context.persistAttempt).toBe(true); expect(operations.length).toBe(2); expect(operations[1].extensions).toEqual({ persistedQuery: { version: 1, sha256Hash: expect.any(String), miss: true, }, }); }); it('retries query persisted query resulted in unsupported', async () => { const { result, operations, exchangeArgs } = makeExchangeArgs(); result.mockImplementationOnce(operation => ({ ...queryResponse, operation, error: new CombinedError({ graphQLErrors: [{ message: 'PersistedQueryNotSupported' }], }), })); await pipe( fromArray([queryOperation, queryOperation]), delay(0), persistedExchange()(exchangeArgs), take(2), toPromise ); expect(operations.length).toBe(3); expect(operations[1].extensions).toEqual({ persistedQuery: undefined, }); expect(operations[2].extensions).toEqual(undefined); }); it('fails gracefully when an invalid result with `PersistedQueryNotFound` is always delivered', async () => { const { result, operations, exchangeArgs } = makeExchangeArgs(); result.mockImplementation(operation => ({ ...queryResponse, operation, error: new CombinedError({ graphQLErrors: [{ message: 'PersistedQueryNotFound' }], }), })); const res = await pipe( fromValue(queryOperation), persistedExchange()(exchangeArgs), take(1), toPromise ); expect(res.operation.context.persistAttempt).toBe(true); expect(operations.length).toBe(2); expect(operations[1].extensions).toEqual({ persistedQuery: { version: 1, sha256Hash: expect.any(String), miss: true, }, }); expect(console.warn).toHaveBeenLastCalledWith( expect.stringMatching(/two misses/i) ); }); it('skips operation when generateHash returns a nullish value', async () => { const { result, operations, exchangeArgs } = makeExchangeArgs(); result.mockImplementationOnce(operation => ({ ...queryResponse, operation, data: null, })); const res = await pipe( fromValue(queryOperation), persistedExchange({ generateHash: async () => null })(exchangeArgs), take(1), toPromise ); expect(res.operation.context.persistAttempt).toBe(true); expect(operations.length).toBe(1); expect(operations[0]).not.toHaveProperty('extensions.persistedQuery'); }); it.each([true, false, 'force', 'within-url-limit'] as const)( 'sets `context.preferGetMethod` to %s when `options.preferGetForPersistedQueries` is %s', async preferGetMethodValue => { const { exchangeArgs } = makeExchangeArgs(); const res = await pipe( fromValue(queryOperation), persistedExchange({ preferGetForPersistedQueries: preferGetMethodValue })( exchangeArgs ), take(1), toPromise ); expect(res.operation.context.preferGetMethod).toBe(preferGetMethodValue); } ); ================================================ FILE: exchanges/persisted/src/persistedExchange.ts ================================================ import { map, makeSubject, fromPromise, filter, merge, mergeMap, takeUntil, pipe, } from 'wonka'; import type { PersistedRequestExtensions, TypedDocumentNode, OperationResult, CombinedError, Exchange, Operation, OperationContext, } from '@urql/core'; import { makeOperation, stringifyDocument } from '@urql/core'; import { hash } from './sha256'; const isPersistedMiss = (error: CombinedError): boolean => error.graphQLErrors.some(x => x.message === 'PersistedQueryNotFound'); const isPersistedUnsupported = (error: CombinedError): boolean => error.graphQLErrors.some(x => x.message === 'PersistedQueryNotSupported'); /** Input parameters for the {@link persistedExchange}. */ export interface PersistedExchangeOptions { /** Controls whether GET method requests will be made for Persisted Queries. * * @remarks * When set to `true` or `'within-url-limit'`, the `persistedExchange` * will use GET requests on persisted queries when the request URL * doesn't exceed the 2048 character limit. * * When set to `force`, the `persistedExchange` will set * `OperationContext.preferGetMethod` to `'force'` on persisted queries, * which will force requests to be made using a GET request. * * GET requests are frequently used to make GraphQL requests more * cacheable on CDNs. * * @defaultValue `within-url-limit` - Use GET requests for persisted queries within the URL limit. */ preferGetForPersistedQueries?: OperationContext['preferGetMethod']; /** Enforces non-automatic persisted queries by ignoring APQ errors. * * @remarks * When enabled, the `persistedExchange` will ignore `PersistedQueryNotFound` * and `PersistedQueryNotSupported` errors and assume that all persisted * queries are already known to the API. * * This is used to switch from Automatic Persisted Queries to * Persisted Queries. This is commonly used to obfuscate GraphQL * APIs. */ enforcePersistedQueries?: boolean; /** Custom hashing function for persisted queries. * * @remarks * By default, `persistedExchange` will create a SHA-256 hash for * persisted queries automatically. If you're instead generating * hashes at compile-time, or need to use a custom SHA-256 function, * you may pass one here. * * If `generateHash` returns either `null` or `undefined`, the * operation will not be treated as a persisted operation, which * essentially skips this exchange’s logic for a given operation. * * Hint: The default SHA-256 function uses the WebCrypto API. This * API is unavailable on React Native, which may require you to * pass a custom function here. */ generateHash?( query: string, document: TypedDocumentNode ): Promise; /** Enables persisted queries to be used for mutations. * * @remarks * When enabled, the `persistedExchange` will also use the persisted queries * logic for mutation operations. * * This is disabled by default, but often used on APIs that obfuscate * their GraphQL APIs. */ enableForMutation?: boolean; /** Enables persisted queries to be used for subscriptions. * * @remarks * When enabled, the `persistedExchange` will also use the persisted queries * logic for subscription operations. * * This is disabled by default, but often used on APIs that obfuscate * their GraphQL APIs. */ enableForSubscriptions?: boolean; } /** Exchange factory that adds support for Persisted Queries. * * @param options - A {@link PersistedExchangeOptions} configuration object. * @returns the created persisted queries {@link Exchange}. * * @remarks * The `persistedExchange` adds support for (Automatic) Persisted Queries * to any `fetchExchange`, `subscriptionExchange`, or other API exchanges * following it. * * It does so by adding the `persistedQuery` extensions field to GraphQL * requests and handles `PersistedQueryNotFound` and * `PersistedQueryNotSupported` errors. * * @example * ```ts * import { Client, cacheExchange, fetchExchange } from '@urql/core'; * import { persistedExchange } from '@urql/exchange-persisted'; * * const client = new Client({ * url: 'URL', * exchanges: [ * cacheExchange, * persistedExchange({ * preferGetForPersistedQueries: true, * }), * fetchExchange * ], * }); * ``` */ export const persistedExchange = (options?: PersistedExchangeOptions): Exchange => ({ forward }) => { if (!options) options = {}; const preferGetForPersistedQueries = options.preferGetForPersistedQueries != null ? options.preferGetForPersistedQueries : 'within-url-limit'; const enforcePersistedQueries = !!options.enforcePersistedQueries; const hashFn = options.generateHash || hash; const enableForMutation = !!options.enableForMutation; const enableForSubscriptions = !!options.enableForSubscriptions; let supportsPersistedQueries = true; const operationFilter = (operation: Operation) => supportsPersistedQueries && !operation.context.persistAttempt && ((enableForMutation && operation.kind === 'mutation') || (enableForSubscriptions && operation.kind === 'subscription') || operation.kind === 'query'); const getPersistedOperation = async (operation: Operation) => { const persistedOperation = makeOperation(operation.kind, operation, { ...operation.context, persistAttempt: true, }); const sha256Hash = await hashFn( stringifyDocument(operation.query), operation.query ); if (sha256Hash) { persistedOperation.extensions = { ...persistedOperation.extensions, persistedQuery: { version: 1, sha256Hash, }, }; if (persistedOperation.kind === 'query') { persistedOperation.context.preferGetMethod = preferGetForPersistedQueries; } } return persistedOperation; }; return operations$ => { const retries = makeSubject(); const forwardedOps$ = pipe( operations$, filter(operation => !operationFilter(operation)) ); const persistedOps$ = pipe( operations$, filter(operationFilter), mergeMap(operation => { const persistedOperation$ = getPersistedOperation(operation); return pipe( fromPromise(persistedOperation$), takeUntil( pipe( operations$, filter(op => op.kind === 'teardown' && op.key === operation.key) ) ) ); }) ); return pipe( merge([persistedOps$, forwardedOps$, retries.source]), forward, map(result => { if ( !enforcePersistedQueries && result.operation.extensions && result.operation.extensions.persistedQuery ) { if (result.error && isPersistedUnsupported(result.error)) { // Disable future persisted queries if they're not enforced supportsPersistedQueries = false; // Update operation with unsupported attempt const followupOperation = makeOperation( result.operation.kind, result.operation ); if (followupOperation.extensions) delete followupOperation.extensions.persistedQuery; retries.next(followupOperation); return null; } else if (result.error && isPersistedMiss(result.error)) { if (result.operation.extensions.persistedQuery.miss) { if (process.env.NODE_ENV !== 'production') { console.warn( 'persistedExchange()’s results include two misses for the same operation.\n' + 'This is not expected as it means a persisted error has been delivered for a non-persisted query!\n' + 'Another exchange with a cache may be delivering an outdated result. For example, a server-side ssrExchange() may be caching an errored result.\n' + 'Try moving the persistedExchange() in past these exchanges, for example in front of your fetchExchange.' ); } return result; } // Update operation with unsupported attempt const followupOperation = makeOperation( result.operation.kind, result.operation ); // Mark as missed persisted query followupOperation.extensions = { ...followupOperation.extensions, persistedQuery: { ...(followupOperation.extensions || {}).persistedQuery, miss: true, } as PersistedRequestExtensions, }; retries.next(followupOperation); return null; } } return result; }), filter((result): result is OperationResult => !!result) ); }; }; ================================================ FILE: exchanges/persisted/src/sha256.ts ================================================ const webCrypto = ( typeof window !== 'undefined' ? window.crypto : typeof self !== 'undefined' ? self.crypto : null ) as typeof globalThis.crypto | null; let nodeCrypto: Promise | void; const getNodeCrypto = async (): Promise => { if (!nodeCrypto) { // Indirect eval'd require/import to guarantee no side-effects in module scope // (optimization for minifiers) try { nodeCrypto = new Function('require', 'return require("crypto")')(require); } catch (_error) { try { nodeCrypto = new Function('return import("crypto")')(); } catch (_error) {} } } return nodeCrypto; }; export const hash = async (query: string): Promise => { if (webCrypto && webCrypto.subtle) { const digest = await webCrypto.subtle.digest( { name: 'SHA-256' }, new TextEncoder().encode(query) ); return new Uint8Array(digest).reduce( (prev, byte) => prev + byte.toString(16).padStart(2, '0'), '' ); } else if (await getNodeCrypto()) { // Node.js support return (await nodeCrypto)!.createHash('sha256').update(query).digest('hex'); } if (process.env.NODE_ENV !== 'production') { console.warn( '[@urql/exchange-persisted-fetch]: The Node Crypto and Web Crypto APIs are not available.\n' + 'This is an unexpected error. Please report it by filing a GitHub Issue.' ); } return ''; }; ================================================ FILE: exchanges/persisted/src/test-utils.ts ================================================ import type { GraphQLRequest, OperationContext, Operation } from '@urql/core'; import { gql, makeOperation } from '@urql/core'; const context: OperationContext = { fetchOptions: { method: 'POST', }, requestPolicy: 'cache-first', url: 'http://localhost:3000/graphql', }; const queryGql: GraphQLRequest = { key: 2, query: gql` query getUser($name: String) { user(name: $name) { id firstName lastName } } `, variables: { name: 'Clara', }, }; export const mutationGql: GraphQLRequest = { key: 2, query: gql` mutation AddUser($name: String) { addUser(name: $name) { name } } `, variables: { name: 'Clara', }, }; export const queryOperation: Operation = makeOperation( 'query', queryGql, context ); export const mutationOperation: Operation = makeOperation( 'mutation', mutationGql, context ); ================================================ FILE: exchanges/persisted/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/persisted/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: exchanges/populate/CHANGELOG.md ================================================ # @urql/exchange-populate ## 2.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 1.2.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 1.2.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 1.1.2 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 1.1.1 ### Patch Changes - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs for all exchanges, documenting API internals Submitted by [@kitten](https://github.com/kitten) (See [#3072](https://github.com/urql-graphql/urql/pull/3072)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 1.1.0 ### Minor Changes - Introduce `maxDepth` and `skipType` into the `populateExchange`, these options allow you to specify the maximum depth a mutation should be populated as well as which types should not be counted towards this depth Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3023](https://github.com/urql-graphql/urql/pull/3023)) ### Patch Changes - Updated dependencies (See [#3007](https://github.com/urql-graphql/urql/pull/3007), [#2962](https://github.com/urql-graphql/urql/pull/2962), [#3007](https://github.com/urql-graphql/urql/pull/3007), [#3015](https://github.com/urql-graphql/urql/pull/3015), and [#3022](https://github.com/urql-graphql/urql/pull/3022)) - @urql/core@3.2.0 ## 1.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Minor Changes - Remove the `babel-plugin-modular-graphql` helper, this because the graphql package hasn't converted to ESM yet which gives issues in node environments, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2551](https://github.com/FormidableLabs/urql/pull/2551)) ### Patch Changes - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 0.2.3 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 0.2.2 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 0.2.1 ### Patch Changes - Add missing `.mjs` extension to all imports from `graphql` to fix Webpack 5 builds, which require extension-specific import paths for ESM bundles and packages. **This change allows you to safely upgrade to Webpack 5.**, by [@kitten](https://github.com/kitten) (See [#1094](https://github.com/FormidableLabs/urql/pull/1094)) - Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was previously confusing as `operationName` was effectively referring to two different things. You can safely upgrade to this new version, however to mute all deprecation warnings you will have to **upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please use [the new `makeOperation` helper function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead, by [@bkonkle](https://github.com/bkonkle) (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - Updated dependencies (See [#1094](https://github.com/FormidableLabs/urql/pull/1094) and [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - @urql/core@1.14.0 ## 0.2.0 ### Minor Changes - Support interfaces and nested interfaces, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#963](https://github.com/FormidableLabs/urql/pull/963)) ### Patch Changes - Updated dependencies (See [#1011](https://github.com/FormidableLabs/urql/pull/1011)) - @urql/core@1.13.1 ## 0.1.8 ### Patch Changes - Upgrade to a minimum version of wonka@^4.0.14 to work around issues with React Native's minification builds, which use uglify-es and could lead to broken bundles, by [@kitten](https://github.com/kitten) (See [#842](https://github.com/FormidableLabs/urql/pull/842)) - Updated dependencies (See [#838](https://github.com/FormidableLabs/urql/pull/838) and [#842](https://github.com/FormidableLabs/urql/pull/842)) - @urql/core@1.12.0 ## 0.1.7 ### Patch Changes - Add a `"./package.json"` entry to the `package.json`'s `"exports"` field for Node 14. This seems to be required by packages like `rollup-plugin-svelte` to function properly, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - ⚠️ Fix `visitWithTypeInfo` import, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - Updated dependencies (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - @urql/core@1.11.6 ## 0.1.6 ### Patch Changes - ⚠️ Fix @urql/exchange-populate visitWithTypeInfo import by bumping babel-plugin-modular-graphql, by [@kitten](https://github.com/kitten) (See [#709](https://github.com/FormidableLabs/urql/pull/709)) ## 0.1.5 ### Patch Changes - Pick modules from graphql package, instead of importing from graphql/index.mjs, by [@kitten](https://github.com/kitten) (See [#700](https://github.com/FormidableLabs/urql/pull/700)) - Updated dependencies (See [#700](https://github.com/FormidableLabs/urql/pull/700)) - @urql/core@1.10.9 ## 0.1.4 ### Patch Changes - Add graphql@^15.0.0 to peer dependency range, by [@kitten](https://github.com/kitten) (See [#688](https://github.com/FormidableLabs/urql/pull/688)) - Updated dependencies (See [#688](https://github.com/FormidableLabs/urql/pull/688) and [#678](https://github.com/FormidableLabs/urql/pull/678)) - @urql/core@1.10.8 ## 0.1.3 ### Patch Changes - ⚠️ Fix node resolution when using Webpack, which experiences a bug where it only resolves `package.json:main` instead of `module` when an `.mjs` file imports a package, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - Updated dependencies (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - @urql/core@1.10.4 ## 0.1.2 ### Patch Changes - ⚠️ Fix Node.js Module support for v13 (experimental-modules) and v14. If your bundler doesn't support `.mjs` files and fails to resolve the new version, please double check your configuration for Webpack, or similar tools, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - Updated dependencies (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - @urql/core@1.10.3 ## 0.1.1 ### Patch Changes - Remove the shared package, this will fix the types file generation for graphcache, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#579](https://github.com/FormidableLabs/urql/pull/579)) - Updated dependencies (See [#577](https://github.com/FormidableLabs/urql/pull/577)) - @urql/core@1.9.2 ## 0.1.0 ### Initial release - Moved the `populateExchange` from `@urql/exchange-graphcache` to its own package. ================================================ FILE: exchanges/populate/README.md ================================================ # @urql/exchange-populate `populate` is an exchange for auto-populating fields in your mutations. [Read more about the `populateExchange` on our docs!](https://formidable.com/open-source/urql/docs/advanced/auto-populate-mutations) ## Quick Start Guide First install `@urql/exchange-populate` alongside `urql`: ```sh yarn add @urql/exchange-populate # or npm install --save @urql/exchange-populate ``` You'll then need to add the `populateExchange`, that this package exposes. ```js import { createClient, cacheExchange, fetchExchange } from 'urql'; import { populateExchange } from '@urql/exchange-populate'; const client = createClient({ url: 'http://localhost:1234/graphql', exchanges: [populateExchange({ schema }), cacheExchange, fetchExchange], }); ``` The `schema` option is the introspection result for your backend graphql schema, more information about how to get your schema can be found [in the "Schema Awareness" Page of the Graphcache documentation.](https://formidable.com/open-source/urql/docs/graphcache/schema-awareness/#getting-your-schema). ## Example usage Consider the following queries which have been requested in other parts of your application: ```graphql # Query 1 { todos { id name } } # Query 2 { todos { id createdAt } } ``` Without the `populateExchange` you may write a mutation like the following which returns a newly created todo item: ```graphql # Without populate mutation addTodo(id: ID!) { addTodo(id: $id) { id # To update Query 1 & 2 name # To update Query 1 createdAt # To update Query 2 } } ``` By using `populateExchange`, you no longer need to manually specify the selection set required to update your other queries. Instead you can just add the `@populate` directive. ```graphql # With populate mutation addTodo(id: ID!) { addTodo(id: $id) @populate } ``` > Note: The above two mutations produce an identical GraphQL request. ================================================ FILE: exchanges/populate/jsr.json ================================================ { "name": "@urql/exchange-populate", "version": "2.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/populate/package.json ================================================ { "name": "@urql/exchange-populate", "version": "2.0.0", "description": "An exchange that automaticcally populates the mutation selection body", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/advanced/auto-populate-mutations", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/populate" }, "keywords": [ "urql", "graphql", "exchanges" ], "main": "dist/urql-exchange-populate", "module": "dist/urql-exchange-populate.mjs", "types": "dist/urql-exchange-populate.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-populate.d.ts", "import": "./dist/urql-exchange-populate.mjs", "require": "./dist/urql-exchange-populate.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/", "extras/" ], "scripts": { "test": "vitest", "clean": "rimraf dist extras", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "peerDependencies": { "@urql/core": "^6.0.0", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" }, "devDependencies": { "@urql/core": "workspace:*", "graphql": "^16.0.0" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/populate/src/helpers/help.ts ================================================ // These are guards that are used throughout the codebase to warn or error on // unexpected behaviour or conditions. // Every warning and error comes with a number that uniquely identifies them. // You can read more about the messages themselves in `docs/graphcache/errors.md` export type ErrorCode = | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19; // URL unfurls to https://formidable.com/open-source/urql/docs/graphcache/errors/ const helpUrl = '\nhttps://bit.ly/2XbVrpR#'; const cache = new Set(); export function warn(message: string, code: ErrorCode) { if (!cache.has(message)) { console.warn(message + helpUrl + code); cache.add(message); } } ================================================ FILE: exchanges/populate/src/helpers/node.ts ================================================ import type { NameNode, GraphQLOutputType, GraphQLWrappingType } from 'graphql'; import { isWrappingType, Kind } from 'graphql'; export type GraphQLFlatType = Exclude; /** Returns the name of a given node */ export const getName = (node: { name: NameNode }): string => node.name.value; export const unwrapType = ( type: null | undefined | GraphQLOutputType ): GraphQLFlatType | null => { if (isWrappingType(type)) { return unwrapType(type.ofType); } return type || null; }; export function createNameNode(value: string): NameNode { return { kind: Kind.NAME, value, }; } ================================================ FILE: exchanges/populate/src/helpers/traverse.ts ================================================ import type { SelectionNode, ASTNode, DefinitionNode, GraphQLSchema, GraphQLFieldMap, FragmentDefinitionNode, FragmentSpreadNode, } from 'graphql'; import { Kind, isAbstractType } from 'graphql'; import { unwrapType, getName } from './node'; export function traverse( node: ASTNode, enter?: (n: ASTNode) => ASTNode | void, exit?: (n: ASTNode) => ASTNode | void ): any { if (enter) { node = enter(node) || node; } switch (node.kind) { case Kind.DOCUMENT: { node = { ...node, definitions: node.definitions.map( n => traverse(n, enter, exit) as DefinitionNode ), }; break; } case Kind.OPERATION_DEFINITION: case Kind.FIELD: case Kind.FRAGMENT_DEFINITION: { if (node.selectionSet) { node = { ...node, selectionSet: { ...node.selectionSet, selections: node.selectionSet.selections.map( n => traverse(n, enter, exit) as SelectionNode ), }, }; } break; } } if (exit) { node = exit(node) || node; } return node; } export function resolveFields( schema: GraphQLSchema, visits: string[] ): GraphQLFieldMap { let currentFields = schema.getQueryType()!.getFields(); for (let i = 0; i < visits.length; i++) { const t = unwrapType(currentFields[visits[i]].type); if (isAbstractType(t)) { currentFields = {}; schema.getPossibleTypes(t).forEach(implementedType => { currentFields = { ...currentFields, // @ts-ignore TODO: proper casting ...schema.getType(implementedType.name)!.toConfig().fields, }; }); } else { // @ts-ignore TODO: proper casting currentFields = schema.getType(t!.name)!.toConfig().fields; } } return currentFields; } /** Get fragment names referenced by node. */ export function getUsedFragmentNames(node: FragmentDefinitionNode) { const names: string[] = []; traverse(node, n => { if (n.kind === Kind.FRAGMENT_SPREAD) { names.push(getName(n as FragmentSpreadNode)); } }); return names; } ================================================ FILE: exchanges/populate/src/index.ts ================================================ export * from './populateExchange'; ================================================ FILE: exchanges/populate/src/populateExchange.test.ts ================================================ import { buildSchema, print, introspectionFromSchema, visit, DocumentNode, ASTKindToNode, Kind, } from 'graphql'; import { vi, expect, it, describe } from 'vitest'; import { fromValue, pipe, fromArray, toArray } from 'wonka'; import { gql, Client, Operation, OperationContext, makeOperation, } from '@urql/core'; import { populateExchange } from './populateExchange'; const schemaDef = ` interface Node { id: ID! } type User implements Node { id: ID! name: String! age: Int! todos: [Todo] } type Todo implements Node { id: ID! text: String! createdAt(timezone: String): String! creator: User! } union UnionType = User | Todo interface Product { id: ID! name: String! price: Int! } interface Store { id: ID! name: String! } type PhysicalStore implements Store { id: ID! name: String! address: String } type OnlineStore implements Store { id: ID! name: String! website: String } type SimpleProduct implements Product { id: ID! name: String! price: Int! store: PhysicalStore } type ComplexProduct implements Product { id: ID! name: String! price: Int! tax: Int! store: OnlineStore } type Company { id: String employees: [User] } type Query { todos: [Todo!] users: [User!]! products: [Product]! company: Company } type Mutation { addTodo: [Todo] removeTodo: [Node] updateTodo: [UnionType] addProduct: Product removeCompany: Company } `; const context = {} as OperationContext; const getNodesByType = ( query: DocumentNode, type: T ) => { let result: N[] = []; visit(query, { [type]: n => { result = [...result, n]; }, }); return result; }; const schema = introspectionFromSchema(buildSchema(schemaDef)); const exchangeArgs = { forward: a => a as any, client: {} as Client, dispatchDebug: vi.fn(), }; describe('on mutation', () => { const operation = makeOperation( 'mutation', { key: 1234, variables: undefined, query: gql` mutation MyMutation { addTodo @populate } `, }, context ); describe('mutation query', () => { it('matches snapshot', async () => { const response = pipe( fromValue(operation), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[0].query)).toMatchInlineSnapshot(` "mutation MyMutation { addTodo { __typename } }" `); }); }); }); describe('on query -> mutation', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { todos { id text creator { id name } } users { todos { text } } } `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { addTodo @populate } `, }, context ); describe('mutation query', () => { it('matches snapshot', async () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { addTodo { __typename id text creator { __typename id name } } }" `); }); }); }); describe('on query -> mutation', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { todos { id text createdAt(timezone: "GMT+1") } } `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { addTodo @populate } `, }, context ); describe('mutation query', () => { it('matches snapshot', async () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { addTodo { __typename id text createdAt(timezone: "GMT+1") } }" `); }); }); }); describe('on (query w/ fragment) -> mutation', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { todos { ...TodoFragment creator { ...CreatorFragment } } } fragment TodoFragment on Todo { id text } fragment CreatorFragment on User { id name } `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { addTodo @populate { ...TodoFragment } } fragment TodoFragment on Todo { id text } `, }, context ); describe('mutation query', () => { it('matches snapshot', async () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { addTodo { ...TodoFragment __typename id text creator { __typename id name } } } fragment TodoFragment on Todo { id text }" `); }); }); }); describe('on (query w/ unused fragment) -> mutation', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { todos { id text } users { ...UserFragment } } fragment UserFragment on User { id name } `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { addTodo @populate } `, }, context ); describe('mutation query', () => { it('matches snapshot', async () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { addTodo { __typename id text } }" `); }); it('excludes user fragment', () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); const fragments = getNodesByType( response[1].query, Kind.FRAGMENT_DEFINITION ); expect( fragments.filter(f => 'name' in f && f.name.value === 'UserFragment') ).toHaveLength(0); }); }); }); describe('on query -> (mutation w/ interface return type)', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { todos { id name } users { id text } } `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { removeTodo @populate } `, }, context ); describe('mutation query', () => { it('matches snapshot', async () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { removeTodo { ... on User { __typename id } ... on Todo { __typename id } } }" `); }); }); }); describe('on query -> (mutation w/ union return type)', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { todos { id text } users { id name } } `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { updateTodo @populate } `, }, context ); describe('mutation query', () => { it('matches snapshot', async () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { updateTodo { ... on User { __typename id name } ... on Todo { __typename id text } } }" `); }); }); }); // TODO: figure out how to behave with teardown, just removing and // not requesting fields feels kinda incorrect as we would start having // stale cache values here describe.skip('on query -> teardown -> mutation', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { todos { id text } } `, }, context ); const teardownOp = makeOperation('teardown', queryOp, context); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { addTodo @populate } `, }, context ); describe('mutation query', () => { it('matches snapshot', async () => { const response = pipe( fromArray([queryOp, teardownOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[2].query)).toMatchInlineSnapshot(` "mutation MyMutation { addTodo { __typename } }" `); }); it('only requests __typename', () => { const response = pipe( fromArray([queryOp, teardownOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); getNodesByType(response[2].query, Kind.FIELD).forEach(field => { expect((field as any).name.value).toMatch(/addTodo|__typename/); }); }); }); }); describe('interface returned in mutation', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { products { id text price tax } } `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { addProduct @populate } `, }, context ); it('should correctly make the inline-fragments', () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { addProduct { ... on SimpleProduct { __typename id price } ... on ComplexProduct { __typename id price tax } } }" `); }); }); describe('nested interfaces', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { products { id text price tax store { id name address website } } } `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { addProduct @populate } `, }, context ); it('should correctly make the inline-fragments', () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { addProduct { ... on SimpleProduct { __typename id price store { __typename id name address } } ... on ComplexProduct { __typename id price tax store { __typename id name website } } } }" `); }); }); describe('nested fragment', () => { const fragment = gql` fragment TodoFragment on Todo { id author { id } } `; const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { todos { ...TodoFragment } } ${fragment} `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { updateTodo @populate } `, }, context ); it('should work with nested fragments', () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { updateTodo { ... on Todo { __typename id } } }" `); }); }); describe('respects max-depth', () => { const queryOp = makeOperation( 'query', { key: 1234, variables: undefined, query: gql` query { company { id employees { id todos { id } } } } `, }, context ); const mutationOp = makeOperation( 'mutation', { key: 5678, variables: undefined, query: gql` mutation MyMutation { removeCompany @populate } `, }, context ); describe('mutation query', () => { it('matches snapshot', async () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema, options: { maxDepth: 1 } })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { removeCompany { __typename id employees { __typename id } } }" `); }); it('respects skip syntax', async () => { const response = pipe( fromArray([queryOp, mutationOp]), populateExchange({ schema, options: { maxDepth: 1, skipType: /User/ }, })(exchangeArgs), toArray ); expect(print(response[1].query)).toMatchInlineSnapshot(` "mutation MyMutation { removeCompany { __typename id employees { __typename id todos { __typename id } } } }" `); }); }); }); ================================================ FILE: exchanges/populate/src/populateExchange.ts ================================================ import type { FragmentDefinitionNode, IntrospectionQuery, SelectionNode, GraphQLInterfaceType, FieldNode, InlineFragmentNode, FragmentSpreadNode, ArgumentNode, } from 'graphql'; import { buildClientSchema, isAbstractType, Kind, GraphQLObjectType, valueFromASTUntyped, GraphQLScalarType, } from 'graphql'; import { pipe, tap, map } from 'wonka'; import type { Exchange, Operation } from '@urql/core'; import { stringifyVariables } from '@urql/core'; import type { GraphQLFlatType } from './helpers/node'; import { getName, unwrapType } from './helpers/node'; import { traverse } from './helpers/traverse'; /** Configuration options for the {@link populateExchange}'s behaviour */ export interface Options { /** Prevents populating fields for matching types. * * @remarks * `skipType` may be set to a regular expression that, when matching, * prevents fields to be added automatically for the given type by the * `populateExchange`. * * @defaultValue `/^PageInfo|(Connection|Edge)$/` - Omit Relay pagination fields */ skipType?: RegExp; /** Specifies a maximum depth for populated fields. * * @remarks * `maxDepth` may be set to a maximum depth at which fields are populated. * This may prevent the `populateExchange` from adding infinitely deep * recursive fields or simply too many fields. * * @defaultValue `2` - Omit fields past a depth of 2. */ maxDepth?: number; } /** Input parameters for the {@link populateExchange}. */ export interface PopulateExchangeOpts { /** Introspection data for an API’s schema. * * @remarks * `schema` must be passed Schema Introspection data for the GraphQL API * this exchange is applied for. * You may use the `@urql/introspection` package to generate this data. * * @see {@link https://spec.graphql.org/October2021/#sec-Schema-Introspection} for the Schema Introspection spec. */ schema: IntrospectionQuery; /** Configuration options for the {@link populateExchange}'s behaviour */ options?: Options; } const makeDict = (): any => Object.create(null); /** stores information per each type it finds */ type TypeKey = GraphQLObjectType | GraphQLInterfaceType; /** stores all known fields per each type key */ type FieldValue = Record; type TypeFields = Map; /** Describes information about a given field, i.e. type (owner), arguments, how many operations use this field */ interface FieldUsage { type: TypeKey; args: null | { [key: string]: { value: any; kind: any } }; fieldName: string; } type FragmentMap = Record; const SKIP_COUNT_TYPE = /^PageInfo|(Connection|Edge)$/; /** Creates an `Exchange` handing automatic mutation selection-set population based on the * query selection-sets seen. * * @param options - A {@link PopulateExchangeOpts} configuration object. * @returns the created populate {@link Exchange}. * * @remarks * The `populateExchange` will create an exchange that monitors queries and * extracts fields and types so it knows what is currently observed by your * application. * When a mutation comes in with the `@populate` directive it will fill the * selection-set based on these prior queries. * * This Exchange can ease up the transition from documentCache to graphCache * * @example * ```ts * populateExchange({ * schema, * options: { * maxDepth: 3, * skipType: /Todo/ * }, * }); * * const query = gql` * mutation { addTodo @popualte } * `; * ``` */ export const populateExchange = ({ schema: ogSchema, options }: PopulateExchangeOpts): Exchange => ({ forward }) => { const maxDepth = (options && options.maxDepth) || 2; const skipType = (options && options.skipType) || SKIP_COUNT_TYPE; const schema = buildClientSchema(ogSchema); /** List of operation keys that have already been parsed. */ const parsedOperations = new Set(); /** List of operation keys that have not been torn down. */ const activeOperations = new Set(); /** Collection of fragments used by the user. */ const userFragments: FragmentMap = makeDict(); // State of the global types & their fields const typeFields: TypeFields = new Map(); let currentVariables: object = {}; /** Handle mutation and inject selections + fragments. */ const handleIncomingMutation = (op: Operation) => { if (op.kind !== 'mutation') { return op; } const document = traverse(op.query, node => { if (node.kind === Kind.FIELD) { if (!node.directives) return; const directives = node.directives.filter( d => getName(d) !== 'populate' ); if (directives.length === node.directives.length) return; const field = schema.getMutationType()!.getFields()[node.name.value]; if (!field) return; const type = unwrapType(field.type); if (!type) { return { ...node, selectionSet: { kind: Kind.SELECTION_SET, selections: [ { kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename', }, }, ], }, directives, }; } const visited = new Set(); const populateSelections = ( type: GraphQLFlatType, selections: Array< FieldNode | InlineFragmentNode | FragmentSpreadNode >, depth: number ) => { let possibleTypes: readonly string[] = []; let isAbstract = false; if (isAbstractType(type)) { isAbstract = true; possibleTypes = schema.getPossibleTypes(type).map(x => x.name); } else { possibleTypes = [type.name]; } possibleTypes.forEach(typeName => { const fieldsForType = typeFields.get(typeName); if (!fieldsForType) { if (possibleTypes.length === 1) { selections.push({ kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename', }, }); } return; } let typeSelections: Array< FieldNode | InlineFragmentNode | FragmentSpreadNode > = selections; if (isAbstract) { typeSelections = [ { kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename', }, }, ]; selections.push({ kind: Kind.INLINE_FRAGMENT, typeCondition: { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: typeName, }, }, selectionSet: { kind: Kind.SELECTION_SET, selections: typeSelections, }, }); } else { typeSelections.push({ kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename', }, }); } Object.keys(fieldsForType).forEach(key => { const value = fieldsForType[key]; if (value.type instanceof GraphQLScalarType) { const args = value.args ? Object.keys(value.args).map(k => { const v = value.args![k]; return { kind: Kind.ARGUMENT, value: { kind: v.kind, value: v.value, }, name: { kind: Kind.NAME, value: k, }, } as ArgumentNode; }) : []; const field: FieldNode = { kind: Kind.FIELD, arguments: args, name: { kind: Kind.NAME, value: value.fieldName, }, }; typeSelections.push(field); } else if ( value.type instanceof GraphQLObjectType && !visited.has(value.type.name) && depth < maxDepth ) { visited.add(value.type.name); const fieldSelections: Array = []; populateSelections( value.type, fieldSelections, skipType.test(value.type.name) ? depth : depth + 1 ); const args = value.args ? Object.keys(value.args).map(k => { const v = value.args![k]; return { kind: Kind.ARGUMENT, value: { kind: v.kind, value: v.value, }, name: { kind: Kind.NAME, value: k, }, } as ArgumentNode; }) : []; const field: FieldNode = { kind: Kind.FIELD, selectionSet: { kind: Kind.SELECTION_SET, selections: fieldSelections, }, arguments: args, name: { kind: Kind.NAME, value: value.fieldName, }, }; typeSelections.push(field); } }); }); }; visited.add(type.name); const selections: Array< FieldNode | InlineFragmentNode | FragmentSpreadNode > = node.selectionSet ? [...node.selectionSet.selections] : []; populateSelections(type, selections, 0); return { ...node, selectionSet: { kind: Kind.SELECTION_SET, selections, }, directives, }; } }); return { ...op, query: document, }; }; const readFromSelectionSet = ( type: GraphQLObjectType | GraphQLInterfaceType, selections: readonly SelectionNode[], seenFields: Record = {} ) => { if (isAbstractType(type)) { // TODO: should we add this to typeParents/typeFields as well? schema.getPossibleTypes(type).forEach(t => { readFromSelectionSet(t, selections); }); } else { const fieldMap = type.getFields(); let args: null | Record = null; for (let i = 0; i < selections.length; i++) { const selection = selections[i]; if (selection.kind === Kind.FRAGMENT_SPREAD) { const fragmentName = getName(selection); const fragment = userFragments[fragmentName]; if (fragment) { readFromSelectionSet(type, fragment.selectionSet.selections); } continue; } if (selection.kind === Kind.INLINE_FRAGMENT) { readFromSelectionSet(type, selection.selectionSet.selections); continue; } if (selection.kind !== Kind.FIELD) continue; const fieldName = selection.name.value; if (!fieldMap[fieldName]) continue; const ownerType = seenFields[fieldName] || (seenFields[fieldName] = type); let fields = typeFields.get(ownerType.name); if (!fields) typeFields.set(type.name, (fields = {})); const childType = unwrapType( fieldMap[fieldName].type ) as GraphQLObjectType; if (selection.arguments && selection.arguments.length) { args = {}; for (let j = 0; j < selection.arguments.length; j++) { const argNode = selection.arguments[j]; args[argNode.name.value] = { value: valueFromASTUntyped( argNode.value, currentVariables as any ), kind: argNode.value.kind, }; } } const fieldKey = args ? `${fieldName}:${stringifyVariables(args)}` : fieldName; if (!fields[fieldKey]) { fields[fieldKey] = { type: childType, args, fieldName, }; } if (selection.selectionSet) { readFromSelectionSet(childType, selection.selectionSet.selections); } } } }; /** Handle query and extract fragments. */ const handleIncomingQuery = ({ key, kind, query, variables, }: Operation) => { if (kind !== 'query') { return; } activeOperations.add(key); if (parsedOperations.has(key)) { return; } parsedOperations.add(key); currentVariables = variables || {}; for (let i = query.definitions.length; i--; ) { const definition = query.definitions[i]; if (definition.kind === Kind.FRAGMENT_DEFINITION) { userFragments[getName(definition)] = definition; } else if (definition.kind === Kind.OPERATION_DEFINITION) { const type = schema.getQueryType()!; readFromSelectionSet( unwrapType(type) as GraphQLObjectType, definition.selectionSet.selections! ); } } }; const handleIncomingTeardown = ({ key, kind }: Operation) => { // TODO: we might want to remove fields here, the risk becomes // that data in the cache would become stale potentially if (kind === 'teardown') { activeOperations.delete(key); } }; return ops$ => { return pipe( ops$, tap(handleIncomingQuery), tap(handleIncomingTeardown), map(handleIncomingMutation), forward ); }; }; ================================================ FILE: exchanges/populate/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/populate/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: exchanges/refocus/CHANGELOG.md ================================================ # Changelog ## 2.1.0 ### Minor Changes - Add `minimumTime` to `refocusExchange` to throttle query reexecution Submitted by [@ThaUnknown](https://github.com/ThaUnknown) (See [#3825](https://github.com/urql-graphql/urql/pull/3825)) ## 2.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 1.1.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 1.1.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 1.0.2 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 1.0.1 ### Patch Changes - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs for all exchanges, documenting API internals Submitted by [@kitten](https://github.com/kitten) (See [#3072](https://github.com/urql-graphql/urql/pull/3072)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 1.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Patch Changes - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 0.2.5 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 0.2.4 ### Patch Changes - ⚠️ Fix use context of the reexecuting operation, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2104](https://github.com/FormidableLabs/urql/pull/2104)) ## 0.2.3 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 0.2.2 ### Patch Changes - Prevent the refocus Exchange from being used on a Node env, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1430](https://github.com/FormidableLabs/urql/pull/1430)) ## 0.2.1 ### Patch Changes - Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was previously confusing as `operationName` was effectively referring to two different things. You can safely upgrade to this new version, however to mute all deprecation warnings you will have to **upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please use [the new `makeOperation` helper function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead, by [@bkonkle](https://github.com/bkonkle) (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - Updated dependencies (See [#1094](https://github.com/FormidableLabs/urql/pull/1094) and [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - @urql/core@1.14.0 ## 0.2.0 ### Minor Changes - Switch from a `focus-event` triggering the refetch to a change in [`page-visbility`](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API). This means that interacting with an `iframe` and then going back to the page won't trigger a refetch, interacting with Devtools won't cause refetches and a bubbled `focusEvent` won't trigger a refetch, by [@tatchi](https://github.com/tatchi) (See [#1077](https://github.com/FormidableLabs/urql/pull/1077)) ## v0.1.0 **Initial Release** ================================================ FILE: exchanges/refocus/README.md ================================================ # @urql/exchange-refocus `@urql/exchange-refocus` is an exchange for the [`urql`](../../README.md) GraphQL client that tracks currently active operations and redispatches them when the window regains focus ## Quick Start Guide First install `@urql/exchange-refocus` alongside `urql`: ```sh yarn add @urql/exchange-refocus # or npm install --save @urql/exchange-refocus ``` Then add it to your `Client`, preferably before the `cacheExchange` and in front of any asynchronous exchanges, like the `fetchExchange`: ```js import { createClient, cacheExchange, fetchExchange } from 'urql'; import { refocusExchange } from '@urql/exchange-refocus'; const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [refocusExchange({ // The minimum time in milliseconds to wait before another refocus can trigger. Default value is 0. minimumTime: 2000 }), cacheExchange, fetchExchange], }); ``` ================================================ FILE: exchanges/refocus/jsr.json ================================================ { "name": "@urql/exchange-refocus", "version": "2.1.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/refocus/package.json ================================================ { "name": "@urql/exchange-refocus", "version": "2.1.0", "description": "An exchange that dispatches active operations when the window regains focus", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/refocus" }, "keywords": [ "urql", "graphql client", "graphql", "exchanges", "react", "focus" ], "main": "dist/urql-exchange-refocus", "module": "dist/urql-exchange-refocus.mjs", "types": "dist/urql-exchange-refocus.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-refocus.d.ts", "import": "./dist/urql-exchange-refocus.mjs", "require": "./dist/urql-exchange-refocus.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@types/react": "^17.0.4", "@urql/core": "workspace:*", "graphql": "^16.0.0" }, "peerDependencies": { "@urql/core": "^6.0.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/refocus/src/index.ts ================================================ export { refocusExchange } from './refocusExchange'; ================================================ FILE: exchanges/refocus/src/refocusExchange.test.ts ================================================ // @vitest-environment jsdom import { pipe, map, makeSubject, publish, tap } from 'wonka'; import { vi, expect, it, beforeEach } from 'vitest'; import { gql, createClient, Operation, OperationResult, ExchangeIO, } from '@urql/core'; import { queryResponse } from '../../../packages/core/src/test-utils'; import { refocusExchange } from './refocusExchange'; const dispatchDebug = vi.fn(); const queryOne = gql` { author { id name } } `; const queryOneData = { __typename: 'Query', author: { __typename: 'Author', id: '123', name: 'Author', }, }; let client, op, ops$, next; beforeEach(() => { client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); op = client.createRequestOperation('query', { key: 1, query: queryOne, }); ({ source: ops$, next } = makeSubject()); }); it(`attaches a listener and redispatches queries on call`, () => { const response = vi.fn((forwardOp: Operation): OperationResult => { return { ...queryResponse, operation: forwardOp, data: queryOneData, }; }); let listener; const spy = vi .spyOn(window, 'addEventListener') .mockImplementation((_keyword, fn) => { listener = fn; }); const reexecuteSpy = vi .spyOn(client, 'reexecuteOperation') .mockImplementation(() => ({})); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; pipe( refocusExchange()({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); expect(spy).toBeCalledTimes(1); expect(spy).toBeCalledWith('visibilitychange', expect.anything()); next(op); listener(); expect(reexecuteSpy).toBeCalledTimes(1); expect(reexecuteSpy).toBeCalledWith({ context: expect.anything(), key: 1, query: queryOne, kind: 'query', }); }); ================================================ FILE: exchanges/refocus/src/refocusExchange.ts ================================================ import { pipe, tap } from 'wonka'; import type { Exchange, Operation } from '@urql/core'; export interface RefocusOptions { /** The minimum time in milliseconds to wait before another refocus can trigger. * @defaultValue `0` */ minimumTime?: number; } /** Exchange factory that reexecutes operations after a user returns to the tab. * * @param opts - A {@link RefocusOptions} configuration object. * * @returns a new refocus {@link Exchange}. * * @remarks * The `refocusExchange` will reexecute `Operation`s with the `cache-and-network` * policy when a user switches back to your application's browser tab. This can * effectively update all on-screen data when a user has stayed inactive for a * long time. * * The `cache-and-network` policy will refetch data in the background, but will * only refetch queries that are currently active. */ export const refocusExchange = (opts: RefocusOptions = {}): Exchange => { const { minimumTime = 0 } = opts; return ({ client, forward }) => ops$ => { if (typeof window === 'undefined') { return forward(ops$); } const watchedOperations = new Map(); const observedOperations = new Map(); let lastHidden = 0; window.addEventListener('visibilitychange', () => { const state = typeof document !== 'object' ? 'visible' : document.visibilityState; if (state === 'visible') { if (Date.now() - lastHidden < minimumTime) return; watchedOperations.forEach(op => { client.reexecuteOperation( client.createRequestOperation('query', op, { ...op.context, requestPolicy: 'cache-and-network', }) ); }); } else { lastHidden = Date.now(); } }); const processIncomingOperation = (op: Operation) => { if (op.kind === 'query' && !observedOperations.has(op.key)) { observedOperations.set(op.key, 1); watchedOperations.set(op.key, op); } if (op.kind === 'teardown' && observedOperations.has(op.key)) { observedOperations.delete(op.key); watchedOperations.delete(op.key); } }; return forward(pipe(ops$, tap(processIncomingOperation))); }; }; ================================================ FILE: exchanges/refocus/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/refocus/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: exchanges/request-policy/CHANGELOG.md ================================================ # Changelog ## 2.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 1.2.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 1.2.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 1.1.0 ### Minor Changes - Change the request-policy exchange not to rely on OperationMeta set by the cache exchanges Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3521](https://github.com/urql-graphql/urql/pull/3521)) ## 1.0.2 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 1.0.1 ### Patch Changes - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs for all exchanges, documenting API internals Submitted by [@kitten](https://github.com/kitten) (See [#3072](https://github.com/urql-graphql/urql/pull/3072)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 1.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Patch Changes - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 0.1.5 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 0.1.4 ### Patch Changes - Do not set the TTL unless cache outcome is "miss". Previously we set the TTL on cache "miss" if it was the first time an operation returned a result, now the TTL is only set on cache miss results. This allows the request policy exchange to work when using persisted caching, by [@Mookiies](https://github.com/Mookiies) (See [#1742](https://github.com/FormidableLabs/urql/pull/1742)) - Updated dependencies (See [#1776](https://github.com/FormidableLabs/urql/pull/1776) and [#1755](https://github.com/FormidableLabs/urql/pull/1755)) - @urql/core@2.1.5 ## 0.1.3 ### Patch Changes - ⚠️ Fix TTL being updated to a newer timestamp when a cached result comes in, and prevent TTL from being deleted on our React binding's cache probes. Instead we now never delete the TTL and update it on incoming cache miss results, by [@kitten](https://github.com/kitten) (See [#1641](https://github.com/FormidableLabs/urql/pull/1641)) - Updated dependencies (See [#1634](https://github.com/FormidableLabs/urql/pull/1634) and [#1638](https://github.com/FormidableLabs/urql/pull/1638)) - @urql/core@2.1.2 ## 0.1.2 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 0.1.1 ### Patch Changes - ⚠️ Fix non-query operations being upgraded by `requestPolicyExchange` and time being stored by last issuance rather than last result, by [@kitten](https://github.com/kitten) (See [#1377](https://github.com/FormidableLabs/urql/pull/1377)) - Updated dependencies (See [#1374](https://github.com/FormidableLabs/urql/pull/1374), [#1357](https://github.com/FormidableLabs/urql/pull/1357), and [#1375](https://github.com/FormidableLabs/urql/pull/1375)) - @urql/core@2.0.0 ## v0.1.0 **Initial Release** ================================================ FILE: exchanges/request-policy/README.md ================================================ # @urql/exchange-request-policy (Exchange factory) `@urql/exchange-request-policy` is an exchange for the [`urql`](../../README.md) GraphQL client that will automatically upgrade operation request-policies on a time-to-live basis. ## Quick Start Guide First install `@urql/exchange-request-policy` alongside `urql`: ```sh yarn add @urql/exchange-request-policy # or npm install --save @urql/exchange-request-policy ``` Then add it to your client. ```js import { createClient, cacheExchange, fetchExchange } from 'urql'; import { requestPolicyExchange } from '@urql/exchange-request-policy'; const client = createClient({ url: 'http://localhost:1234/graphql', exchanges: [ requestPolicyExchange({ // The amount of time in ms that has to go by before upgrading, default is 5 minutes. ttl: 60 * 1000, // 1 minute. // An optional function that allows you to specify whether an operation should be upgraded. shouldUpgrade: operation => operation.context.requestPolicy !== 'cache-only', }), cacheExchange, fetchExchange, ], }); ``` Now when the exchange sees a `cache-first` operation that hasn't been seen in ttl amount of time it will upgrade the `requestPolicy` to `cache-and-network`. ================================================ FILE: exchanges/request-policy/jsr.json ================================================ { "name": "@urql/exchange-request-policy", "version": "2.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/request-policy/package.json ================================================ { "name": "@urql/exchange-request-policy", "version": "2.0.0", "description": "An exchange for operation request-policy upgrading in urql", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/request-policy" }, "keywords": [ "urql", "graphql client", "graphql", "exchanges", "request-policy" ], "main": "dist/urql-exchange-request-policy", "module": "dist/urql-exchange-request-policy.mjs", "types": "dist/urql-exchange-request-policy.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-request-policy.d.ts", "import": "./dist/urql-exchange-request-policy.mjs", "require": "./dist/urql-exchange-request-policy.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@urql/core": "workspace:*", "graphql": "^16.0.0" }, "peerDependencies": { "@urql/core": "^6.0.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/request-policy/src/index.ts ================================================ export { requestPolicyExchange } from './requestPolicyExchange'; ================================================ FILE: exchanges/request-policy/src/requestPolicyExchange.test.ts ================================================ import { pipe, map, makeSubject, publish, tap } from 'wonka'; import { vi, expect, it, beforeEach } from 'vitest'; import { gql, createClient, Operation, OperationResult, ExchangeIO, } from '@urql/core'; import { queryResponse } from '../../../packages/core/src/test-utils'; import { requestPolicyExchange } from './requestPolicyExchange'; const dispatchDebug = vi.fn(); const mockOptions = { ttl: 5, }; const queryOne = gql` { author { id name } } `; const queryOneData = { __typename: 'Query', author: { __typename: 'Author', id: '123', name: 'Author', }, }; let client, op, ops$, next; beforeEach(() => { client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); op = client.createRequestOperation('query', { key: 1, query: queryOne, }); ({ source: ops$, next } = makeSubject()); }); it(`upgrades to cache-and-network`, async () => { const response = vi.fn((forwardOp: Operation): OperationResult => { return { ...queryResponse, operation: forwardOp, data: queryOneData, }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; pipe( requestPolicyExchange(mockOptions)({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); expect(response).toHaveBeenCalledTimes(1); expect(response.mock.calls[0][0].context.requestPolicy).toEqual( 'cache-and-network' ); expect(result).toHaveBeenCalledTimes(1); await new Promise(res => { setTimeout(() => { next(op); expect(response).toHaveBeenCalledTimes(2); expect(response.mock.calls[1][0].context.requestPolicy).toEqual( 'cache-and-network' ); expect(result).toHaveBeenCalledTimes(2); res(null); }, 10); }); }); it(`doesn't upgrade when shouldUpgrade returns false`, async () => { const response = vi.fn((forwardOp: Operation): OperationResult => { return { ...queryResponse, operation: forwardOp, data: queryOneData, }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; const shouldUpgrade = vi.fn(() => false); pipe( requestPolicyExchange({ ...mockOptions, shouldUpgrade })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); expect(response).toHaveBeenCalledTimes(1); expect(response.mock.calls[0][0].context.requestPolicy).toEqual( 'cache-first' ); expect(result).toHaveBeenCalledTimes(1); await new Promise(res => { setTimeout(() => { next(op); expect(response).toHaveBeenCalledTimes(2); expect(response.mock.calls[1][0].context.requestPolicy).toEqual( 'cache-first' ); expect(result).toHaveBeenCalledTimes(2); expect(shouldUpgrade).toBeCalledTimes(2); res(null); }, 10); }); }); ================================================ FILE: exchanges/request-policy/src/requestPolicyExchange.ts ================================================ import type { Operation, OperationResult, Exchange } from '@urql/core'; import { makeOperation } from '@urql/core'; import { pipe, tap, map } from 'wonka'; const defaultTTL = 5 * 60 * 1000; /** Input parameters for the {@link requestPolicyExchange}. */ export interface Options { /** Predicate allowing you to selectively not upgrade `Operation`s. * * @remarks * When `shouldUpgrade` is set, it may be used to selectively return a boolean * per `Operation`. This allows certain `Operation`s to not be upgraded to a * `cache-and-network` policy, when `false` is returned. * * By default, all `Operation`s are subject to be upgraded. * operation to "cache-and-network". */ shouldUpgrade?: (op: Operation) => boolean; /** The time-to-live (TTL) for which a request policy won't be upgraded. * * @remarks * The `ttl` defines the time frame in which the `Operation` won't be updated * with a `cache-and-network` request policy. If an `Operation` is sent again * and the `ttl` time period has expired, the policy is upgraded. * * @defaultValue `300_000` - 5min */ ttl?: number; } /** Exchange factory that upgrades request policies to `cache-and-network` for queries outside of a defined `ttl`. * * @param options - An {@link Options} configuration object. * @returns the created request-policy {@link Exchange}. * * @remarks * The `requestPolicyExchange` upgrades query operations based on {@link Options.ttl}. * The `ttl` defines a timeframe outside of which a query's request policy is set to * `cache-and-network` to refetch it in the background. * * You may define a {@link Options.shouldUpgrade} function to selectively ignore some * operations by returning `false` there. * * @example * ```ts * requestPolicyExchange({ * // Upgrade when we haven't seen this operation for 1 second * ttl: 1000, * // and only upgrade operations that query the `todos` field. * shouldUpgrade: op => op.kind === 'query' && op.query.definitions[0].name?.value === 'todos' * }); * ``` */ export const requestPolicyExchange = (options: Options): Exchange => ({ forward }) => { const operations = new Map(); const TTL = (options || {}).ttl || defaultTTL; const dispatched = new Map(); let counter = 0; const processIncomingOperation = (operation: Operation): Operation => { if ( operation.kind !== 'query' || (operation.context.requestPolicy !== 'cache-first' && operation.context.requestPolicy !== 'cache-only') ) { return operation; } const currentTime = new Date().getTime(); // When an operation passes by we track the current time dispatched.set(operation.key, counter); queueMicrotask(() => { counter = (counter + 1) | 0; }); const lastOccurrence = operations.get(operation.key) || 0; if ( currentTime - lastOccurrence > TTL && (!options.shouldUpgrade || options.shouldUpgrade(operation)) ) { return makeOperation(operation.kind, operation, { ...operation.context, requestPolicy: 'cache-and-network', }); } return operation; }; const processIncomingResults = (result: OperationResult): void => { // When we get a result for the operation we check whether it resolved // synchronously by checking whether the counter is different from the // dispatched counter. const lastDispatched = dispatched.get(result.operation.key) || 0; if (counter !== lastDispatched) { // We only delete in the case of a miss to ensure that cache-and-network // is properly taken care of dispatched.delete(result.operation.key); operations.set(result.operation.key, new Date().getTime()); } }; return ops$ => { return pipe( forward(pipe(ops$, map(processIncomingOperation))), tap(processIncomingResults) ); }; }; ================================================ FILE: exchanges/request-policy/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/request-policy/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: exchanges/retry/CHANGELOG.md ================================================ # Changelog ## 2.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 1.3.2 ### Patch Changes - Mark options argument as optional (`retryExchange()`) Submitted by [@jtomaszewski](https://github.com/jtomaszewski) (See [#3775](https://github.com/urql-graphql/urql/pull/3775)) - Updated dependencies (See [#3773](https://github.com/urql-graphql/urql/pull/3773), [#3767](https://github.com/urql-graphql/urql/pull/3767), [#3730](https://github.com/urql-graphql/urql/pull/3730), and [#3770](https://github.com/urql-graphql/urql/pull/3770)) - @urql/core@5.1.2 ## 1.3.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 1.3.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 1.2.1 ### Patch Changes --- Fixed the delay amount not increasing as retry count increases Submitted by [@DoisKoh](https://github.com/DoisKoh) (See [#3478](https://github.com/urql-graphql/urql/pull/3478)) ## 1.2.0 ### Minor Changes - Reset `retryExchange`’s previous attempts and delay if an operation succeeds. This prevents the exchange from keeping its old retry count and delay if the operation delivered a result in the meantime. This is important for it to help recover from failing subscriptions Submitted by [@kitten](https://github.com/kitten) (See [#3229](https://github.com/urql-graphql/urql/pull/3229)) ## 1.1.1 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 1.1.0 ### Minor Changes - Update exchanges to drop redundant `share` calls, since `@urql/core`’s `composeExchanges` utility now automatically does so for us Submitted by [@kitten](https://github.com/kitten) (See [#3082](https://github.com/urql-graphql/urql/pull/3082)) ### Patch Changes - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs for all exchanges, documenting API internals Submitted by [@kitten](https://github.com/kitten) (See [#3072](https://github.com/urql-graphql/urql/pull/3072)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 1.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Patch Changes - make `randomDelay` work correctly, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2615](https://github.com/FormidableLabs/urql/pull/2615)) - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 0.3.3 ### Patch Changes - Export `RetryExchangeOption` type from top level export, by [@KoltonG](https://github.com/KoltonG) (See [#2351](https://github.com/FormidableLabs/urql/pull/2351)) - Updated dependencies (See [#2384](https://github.com/FormidableLabs/urql/pull/2384) and [#2386](https://github.com/FormidableLabs/urql/pull/2386)) - @urql/core@2.4.4 ## 0.3.2 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 0.3.1 ### Patch Changes - ⚠️ Fix operations sometimes not being executed after a retry is supposed to be triggered, due to a `setTimeout` reordering issue when the timer isn't as predictable as it should be, by [@kitten](https://github.com/kitten) (See [#2124](https://github.com/FormidableLabs/urql/pull/2124)) ## 0.3.0 ### Minor Changes - Add a new `retryWith` option which allows operations to be updated when a request is being retried, by [@kitten](https://github.com/kitten) (See [#1881](https://github.com/FormidableLabs/urql/pull/1881)) ### Patch Changes - Updated dependencies (See [#1870](https://github.com/FormidableLabs/urql/pull/1870) and [#1880](https://github.com/FormidableLabs/urql/pull/1880)) - @urql/core@2.3.1 ## 0.2.1 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 0.2.0 ### Minor Changes - Add a second `Operation` input argument to the `retryIf` predicate, so that retrying can be actively avoided for specific types of operations, e.g. mutations or subscriptions, in certain user-defined cases, by [@kitten](https://github.com/kitten) (See [#1117](https://github.com/FormidableLabs/urql/pull/1117)) ### Patch Changes - Updated dependencies (See [#1119](https://github.com/FormidableLabs/urql/pull/1119), [#1113](https://github.com/FormidableLabs/urql/pull/1113), [#1104](https://github.com/FormidableLabs/urql/pull/1104), and [#1123](https://github.com/FormidableLabs/urql/pull/1123)) - @urql/core@1.15.0 ## 0.1.10 ### Patch Changes - ⚠️ Fix the production build overwriting the development build. Specifically in the previous release we mistakenly replaced all development bundles with production bundles. This doesn't have any direct influence on how these packages work, but prevented development warnings from being logged or full errors from being thrown, by [@kitten](https://github.com/kitten) (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - Updated dependencies (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - @urql/core@1.14.1 ## 0.1.9 ### Patch Changes - Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was previously confusing as `operationName` was effectively referring to two different things. You can safely upgrade to this new version, however to mute all deprecation warnings you will have to **upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please use [the new `makeOperation` helper function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead, by [@bkonkle](https://github.com/bkonkle) (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - Updated dependencies (See [#1094](https://github.com/FormidableLabs/urql/pull/1094) and [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - @urql/core@1.14.0 ## 0.1.8 ### Patch Changes - Upgrade to a minimum version of wonka@^4.0.14 to work around issues with React Native's minification builds, which use uglify-es and could lead to broken bundles, by [@kitten](https://github.com/kitten) (See [#842](https://github.com/FormidableLabs/urql/pull/842)) - Updated dependencies (See [#838](https://github.com/FormidableLabs/urql/pull/838) and [#842](https://github.com/FormidableLabs/urql/pull/842)) - @urql/core@1.12.0 ## 0.1.7 ### Patch Changes - Add `source` debug name to all `dispatchDebug` calls during build time to identify events by which exchange dispatched them, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#780](https://github.com/FormidableLabs/urql/pull/780)) - Updated dependencies (See [#780](https://github.com/FormidableLabs/urql/pull/780)) - @urql/core@1.11.7 ## 0.1.6 ### Patch Changes - Add a `"./package.json"` entry to the `package.json`'s `"exports"` field for Node 14. This seems to be required by packages like `rollup-plugin-svelte` to function properly, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - Updated dependencies (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - @urql/core@1.11.6 ## 0.1.5 ### Patch Changes - Add debugging events to exchanges that add more detailed information on what is happening internally, which will be displayed by devtools like the urql [Chrome / Firefox extension](https://github.com/FormidableLabs/urql-devtools), by [@andyrichardson](https://github.com/andyrichardson) (See [#608](https://github.com/FormidableLabs/urql/pull/608)) - Updated dependencies (See [#608](https://github.com/FormidableLabs/urql/pull/608), [#718](https://github.com/FormidableLabs/urql/pull/718), and [#722](https://github.com/FormidableLabs/urql/pull/722)) - @urql/core@1.11.0 ## 0.1.4 ### Patch Changes - Add graphql@^15.0.0 to peer dependency range, by [@kitten](https://github.com/kitten) (See [#688](https://github.com/FormidableLabs/urql/pull/688)) - Updated dependencies (See [#688](https://github.com/FormidableLabs/urql/pull/688) and [#678](https://github.com/FormidableLabs/urql/pull/678)) - @urql/core@1.10.8 ## 0.1.3 ### Patch Changes - ⚠️ Fix node resolution when using Webpack, which experiences a bug where it only resolves `package.json:main` instead of `module` when an `.mjs` file imports a package, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - Updated dependencies (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - @urql/core@1.10.4 ## 0.1.2 ### Patch Changes - ⚠️ Fix Node.js Module support for v13 (experimental-modules) and v14. If your bundler doesn't support `.mjs` files and fails to resolve the new version, please double check your configuration for Webpack, or similar tools, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - Updated dependencies (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - @urql/core@1.10.3 ## 0.1.1 ### Patch Changes - ⚠️ Fix Rollup bundle output being written to .es.js instead of .esm.js, by [@kitten](https://github.com/kitten) (See [#609](https://github.com/FormidableLabs/urql/pull/609)) - Updated dependencies (See [#609](https://github.com/FormidableLabs/urql/pull/609)) - @urql/core@1.10.1 ## v0.1.0 **Initial Release** ================================================ FILE: exchanges/retry/README.md ================================================ # @urql/exchange-retry (Exchange factory) `@urql/exchange-retry` is an exchange for the [`urql`](../../README.md) GraphQL client that allows operations (queries, mutations, subscriptions) to be retried based on an `options` parameter. ## Quick Start Guide First install `@urql/exchange-retry` alongside `urql`: ```sh yarn add @urql/exchange-retry # or npm install --save @urql/exchange-retry ``` Read more about the [retry exchange](https://formidable.com/open-source/urql/docs/advanced/retry-operations). ================================================ FILE: exchanges/retry/jsr.json ================================================ { "name": "@urql/exchange-retry", "version": "2.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/retry/package.json ================================================ { "name": "@urql/exchange-retry", "version": "2.0.0", "description": "An exchange for operation retry support in urql", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/retry" }, "keywords": [ "urql", "graphql client", "graphql", "exchanges", "retry" ], "main": "dist/urql-exchange-retry", "module": "dist/urql-exchange-retry.mjs", "types": "dist/urql-exchange-retry.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-retry.d.ts", "import": "./dist/urql-exchange-retry.mjs", "require": "./dist/urql-exchange-retry.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@urql/core": "workspace:*", "graphql": "^16.0.0" }, "peerDependencies": { "@urql/core": "^6.0.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/retry/src/index.ts ================================================ export { retryExchange } from './retryExchange'; export type { RetryExchangeOptions } from './retryExchange'; ================================================ FILE: exchanges/retry/src/retryExchange.test.ts ================================================ import { Source, pipe, map, makeSubject, mergeMap, fromValue, fromArray, publish, tap, } from 'wonka'; import { vi, expect, it, beforeEach, afterEach } from 'vitest'; import { gql, createClient, makeOperation, Operation, OperationResult, ExchangeIO, } from '@urql/core'; import { retryExchange } from './retryExchange'; const dispatchDebug = vi.fn(); beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); const mockOptions = { initialDelayMs: 50, maxDelayMs: 500, randomDelay: true, maxNumberAttempts: 10, retryIf: () => true, }; const queryOne = gql` { author { id name } } `; const queryOneData = { __typename: 'Query', author: { __typename: 'Author', id: '123', name: 'Author', }, }; const queryOneError = { name: 'error', message: 'scary error', }; let client, op, ops$, next; beforeEach(() => { client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); op = client.createRequestOperation('query', { key: 1, query: queryOne, }); ({ source: ops$, next } = makeSubject()); }); it('retries if it hits an error and works for multiple concurrent operations', () => { const queryTwo = gql` { films { id name } } `; const queryTwoError = { name: 'error2', message: 'scary error2', }; const opTwo = client.createRequestOperation('query', { key: 2, query: queryTwo, }); const response = vi.fn((forwardOp: Operation): OperationResult => { expect( forwardOp.key === op.key || forwardOp.key === opTwo.key ).toBeTruthy(); return { operation: forwardOp, // @ts-ignore error: forwardOp.key === 2 ? queryTwoError : queryOneError, }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; const mockRetryIf = vi.fn(() => true); pipe( retryExchange({ ...mockOptions, retryIf: mockRetryIf, })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); expect(mockRetryIf).toHaveBeenCalledTimes(1); expect(mockRetryIf).toHaveBeenCalledWith(queryOneError as any, op); vi.runAllTimers(); expect(mockRetryIf).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts); expect(response).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts); // result should only ever be called once per operation expect(result).toHaveBeenCalledTimes(1); next(opTwo); vi.runAllTimers(); expect(mockRetryIf).toHaveBeenCalledWith(queryTwoError as any, opTwo); // max number of retries for each op expect(response).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts * 2); expect(result).toHaveBeenCalledTimes(2); }); it('should retry x number of times and then return the successful result', () => { const numberRetriesBeforeSuccess = 3; const response = vi.fn((forwardOp: Operation): OperationResult => { expect(forwardOp.key).toBe(op.key); // @ts-ignore return { operation: forwardOp, ...(forwardOp.context.retry?.count >= numberRetriesBeforeSuccess ? { data: queryOneData } : { error: queryOneError }), }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; const mockRetryIf = vi.fn(() => true); pipe( retryExchange({ ...mockOptions, retryIf: mockRetryIf, })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); vi.runAllTimers(); expect(mockRetryIf).toHaveBeenCalledTimes(numberRetriesBeforeSuccess); expect(mockRetryIf).toHaveBeenCalledWith(queryOneError as any, op); // one for original source, one for retry expect(response).toHaveBeenCalledTimes(1 + numberRetriesBeforeSuccess); expect(result).toHaveBeenCalledTimes(1); }); it('should reset the retry counter if an operation succeeded first', () => { let call = 0; const response = vi.fn((forwardOp: Operation): Source => { expect(forwardOp.key).toBe(op.key); if (call === 0) { call++; return fromValue({ operation: forwardOp, error: queryOneError, } as any); } else if (call === 1) { call++; return fromArray([ { operation: forwardOp, error: queryOneError, } as any, { operation: forwardOp, data: queryOneData, } as any, ]); } else { expect(forwardOp.context.retry).toEqual({ count: 0, delay: null }); return fromValue({ operation: forwardOp, data: queryOneData, } as any); } }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, mergeMap(response)); }; const mockRetryIf = vi.fn(() => true); pipe( retryExchange({ ...mockOptions, retryIf: mockRetryIf, })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); vi.runAllTimers(); expect(mockRetryIf).toHaveBeenCalledTimes(2); expect(mockRetryIf).toHaveBeenCalledWith(queryOneError as any, op); expect(response).toHaveBeenCalledTimes(3); expect(result).toHaveBeenCalledTimes(2); }); it(`should still retry if retryIf undefined but there is a networkError`, () => { const errorWithNetworkError = { ...queryOneError, networkError: 'scary network error', }; const response = vi.fn((forwardOp: Operation): OperationResult => { expect(forwardOp.key).toBe(op.key); return { operation: forwardOp, // @ts-ignore error: errorWithNetworkError, }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; pipe( retryExchange({ ...mockOptions, retryIf: undefined, })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); vi.runAllTimers(); // max number of retries, plus original call expect(response).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts); expect(result).toHaveBeenCalledTimes(1); }); it('should allow retryWhen to return falsy value and act as replacement of retryIf', () => { const errorWithNetworkError = { ...queryOneError, networkError: 'scary network error', }; const response = vi.fn((forwardOp: Operation): OperationResult => { expect(forwardOp.key).toBe(op.key); return { operation: forwardOp, // @ts-ignore error: errorWithNetworkError, }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; const retryWith = vi.fn(() => null); pipe( retryExchange({ ...mockOptions, retryIf: undefined, retryWith, })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); vi.runAllTimers(); // max number of retries, plus original call expect(retryWith).toHaveBeenCalledTimes(1); expect(response).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(1); }); it('should allow retryWhen to return new operations when retrying', () => { const errorWithNetworkError = { ...queryOneError, networkError: 'scary network error', }; const response = vi.fn((forwardOp: Operation): OperationResult => { expect(forwardOp.key).toBe(op.key); return { operation: forwardOp, // @ts-ignore error: errorWithNetworkError, }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; const retryWith = vi.fn((_error, operation) => { return makeOperation(operation.kind, operation, { ...operation.context, counter: (operation.context?.counter || 0) + 1, }); }); pipe( retryExchange({ ...mockOptions, retryIf: undefined, retryWith, })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); vi.runAllTimers(); // max number of retries, plus original call expect(retryWith).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts - 1); expect(response).toHaveBeenCalledTimes(mockOptions.maxNumberAttempts); expect(result).toHaveBeenCalledTimes(1); expect(response.mock.calls[1][0]).toHaveProperty('context.counter', 1); expect(response.mock.calls[2][0]).toHaveProperty('context.counter', 2); }); it('should increase retries by initialDelayMs for each subsequent failure', () => { const errorWithNetworkError = { ...queryOneError, networkError: 'scary network error', }; const response = vi.fn((forwardOp: Operation): OperationResult => { expect(forwardOp.key).toBe(op.key); return { operation: forwardOp, // @ts-ignore error: errorWithNetworkError, }; }); const result = vi.fn(); const forward: ExchangeIO = ops$ => { return pipe(ops$, map(response)); }; const retryWith = vi.fn((_error, operation) => { return makeOperation(operation.kind, operation, { ...operation.context, counter: (operation.context?.counter || 0) + 1, }); }); const fixedDelayMs = 50; const fixedDelayOptions = { ...mockOptions, randomDelay: false, initialDelayMs: fixedDelayMs, }; pipe( retryExchange({ ...fixedDelayOptions, retryIf: undefined, retryWith, })({ forward, client, dispatchDebug, })(ops$), tap(result), publish ); next(op); // delay between each call should be increased by initialDelayMs // (e.g. if initialDelayMs is 5s, first retry is waits 5 seconds, second retry waits 10 seconds) for (let i = 1; i <= fixedDelayOptions.maxNumberAttempts; i++) { expect(response).toHaveBeenCalledTimes(i); vi.advanceTimersByTime(i * fixedDelayOptions.initialDelayMs); } }); ================================================ FILE: exchanges/retry/src/retryExchange.ts ================================================ import { makeSubject, pipe, merge, filter, fromValue, debounce, mergeMap, takeUntil, } from 'wonka'; import type { Exchange, Operation, CombinedError } from '@urql/core'; import { makeOperation } from '@urql/core'; /** Input parameters for the {@link retryExchange}. */ export interface RetryExchangeOptions { /** Specify the minimum time to wait until retrying. * * @remarks * `initialDelayMs` specifies the minimum time (in milliseconds) to wait * until a failed operation is retried. * * @defaultValue `1_000` - one second */ initialDelayMs?: number; /** Specifies the maximum time to wait until retrying. * * @remarks * `maxDelayMs` specifies the maximum time (in milliseconds) to wait * until a failed operation is retried. While `initialDelayMs` * specifies the minimum amount of time, `randomDelay` may cause * the delay to increase over multiple attempts. * * @defaultValue `15_000` - 15 seconds */ maxDelayMs?: number; /** Enables a random exponential backoff to increase the delay over multiple retries. * * @remarks * `randomDelay`, unless disabled, increases the time until a failed * operation is retried over multiple attempts. It increases the time * starting at `initialDelayMs` by 1.5x with an added factor of 0–1, * until `maxDelayMs` is reached. * * @defaultValue `true` - enables random exponential backoff */ randomDelay?: boolean; /** Specifies the maximum number of attempts, including the initial request. * * @remarks * `maxNumberAttempts` defines the total number of attempts before it's * considered failed. * * @defaultValue `2` - Retry once, i.e. two attempts */ maxNumberAttempts?: number; /** Predicate allowing you to selectively not retry `Operation`s. * * @remarks * `retryIf` is called with a {@link CombinedError} and the {@link Operation} that * failed. If this function returns false the failed `Operation` is not retried. * * @defaultValue `(error) => !!error.networkError` - retries only on network errors. */ retryIf?(error: CombinedError, operation: Operation): boolean; /** Transform function allowing you to selectively replace a retried `Operation` or return nullish value. * * @remarks * `retryWhen` is called with a {@link CombinedError} and the {@link Operation} that * failed. If this function returns an `Operation`, `retryExchange` will replace the * failed `Operation` and retry. It won't retry the `Operation` if a nullish value * is returned. * * The `retryIf` function, if defined, takes precedence and overrides this option. */ retryWith?( error: CombinedError, operation: Operation ): Operation | null | undefined; } interface RetryState { count: number; delay: number | null; } /** Exchange factory that retries failed operations. * * @param options - A {@link RetriesExchangeOptions} configuration object. * @returns the created retry {@link Exchange}. * * @remarks * The `retryExchange` retries failed operations with specified delays * and exponential backoff. * * You may define a {@link RetryExchangeOptions.retryIf} or * {@link RetryExchangeOptions.retryWhen} function to only retry * certain kinds of operations, e.g. only queries. * * @example * ```ts * retryExchange({ * initialDelayMs: 1000, * maxDelayMs: 15000, * randomDelay: true, * maxNumberAttempts: 2, * retryIf: err => err && err.networkError, * }); * ``` */ export const retryExchange = (options: RetryExchangeOptions = {}): Exchange => { const { retryIf, retryWith } = options; const MIN_DELAY = options.initialDelayMs || 1000; const MAX_DELAY = options.maxDelayMs || 15_000; const MAX_ATTEMPTS = options.maxNumberAttempts || 2; const RANDOM_DELAY = options.randomDelay != null ? !!options.randomDelay : true; return ({ forward, dispatchDebug }) => operations$ => { const { source: retry$, next: nextRetryOperation } = makeSubject(); const retryWithBackoff$ = pipe( retry$, mergeMap((operation: Operation) => { const retry: RetryState = operation.context.retry || { count: 0, delay: null, }; const retryCount = ++retry.count; let delayAmount = retry.delay || MIN_DELAY; const backoffFactor = Math.random() + 1.5; if (RANDOM_DELAY) { // if randomDelay is enabled and it won't exceed the max delay, apply a random // amount to the delay to avoid thundering herd problem if (delayAmount * backoffFactor < MAX_DELAY) { delayAmount *= backoffFactor; } else { delayAmount = MAX_DELAY; } } else { // otherwise, increase the delay proportionately by the initial delay delayAmount = Math.min(retryCount * MIN_DELAY, MAX_DELAY); } // ensure the delay is carried over to the next context retry.delay = delayAmount; // We stop the retries if a teardown event for this operation comes in // But if this event comes through regularly we also stop the retries, since it's // basically the query retrying itself, no backoff should be added! const teardown$ = pipe( operations$, filter(op => { return ( (op.kind === 'query' || op.kind === 'teardown') && op.key === operation.key ); }) ); dispatchDebug({ type: 'retryAttempt', message: `The operation has failed and a retry has been triggered (${retryCount} / ${MAX_ATTEMPTS})`, operation, data: { retryCount, delayAmount, }, }); // Add new retryDelay and retryCount to operation return pipe( fromValue( makeOperation(operation.kind, operation, { ...operation.context, retry, }) ), debounce(() => delayAmount), // Stop retry if a teardown comes in takeUntil(teardown$) ); }) ); return pipe( merge([operations$, retryWithBackoff$]), forward, filter(res => { const retry = res.operation.context.retry as RetryState | undefined; // Only retry if the error passes the conditional retryIf function (if passed) // or if the error contains a networkError if ( !res.error || (retryIf ? !retryIf(res.error, res.operation) : !retryWith && !res.error.networkError) ) { // Reset the delay state for a successful operation if (retry) { retry.count = 0; retry.delay = null; } return true; } const maxNumberAttemptsExceeded = ((retry && retry.count) || 0) >= MAX_ATTEMPTS - 1; if (!maxNumberAttemptsExceeded) { const operation = retryWith ? retryWith(res.error, res.operation) : res.operation; if (!operation) return true; // Send failed responses to be retried by calling next on the retry$ subject // Exclude operations that have been retried more than the specified max nextRetryOperation(operation); return false; } dispatchDebug({ type: 'retryExhausted', message: 'Maximum number of retries has been reached. No further retries will be performed.', operation: res.operation, }); return true; }) ); }; }; ================================================ FILE: exchanges/retry/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/retry/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: exchanges/throw-on-error/CHANGELOG.md ================================================ # @urql/exchange-throw-on-error ## 1.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 0.1.2 ### Patch Changes - Update `graphql-toe` and add more detail to README Submitted by [@benjie](https://github.com/benjie) (See [#3765](https://github.com/urql-graphql/urql/pull/3765)) ## 0.1.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 0.1.0 ### Minor Changes - Initial release Submitted by [@XiNiHa](https://github.com/XiNiHa) (See [#3677](https://github.com/urql-graphql/urql/pull/3677)) ================================================ FILE: exchanges/throw-on-error/README.md ================================================ # @urql/exchange-throw-on-error (Exchange factory) `@urql/exchange-throw-on-error` is an exchange for the [`urql`](https://github.com/urql-graphql/urql) GraphQL client that throws on field access to errored fields. It is built on top of the [`graphql-toe`](https://github.com/graphile/graphql-toe) package - please see that package for more information. ## Quick Start Guide First install `@urql/exchange-throw-on-error` alongside `urql`: ```sh yarn add @urql/exchange-throw-on-error # or npm install --save @urql/exchange-throw-on-error ``` Then add the `throwOnErrorExchange`, to your client: ```js import { createClient, cacheExchange, fetchExchange } from 'urql'; import { throwOnErrorExchange } from '@urql/exchange-throw-on-error'; const client = createClient({ url: '/graphql', exchanges: [cacheExchange, throwOnErrorExchange(), fetchExchange], }); ``` ================================================ FILE: exchanges/throw-on-error/jsr.json ================================================ { "name": "@urql/exchange-throw-on-error", "version": "1.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: exchanges/throw-on-error/package.json ================================================ { "name": "@urql/exchange-throw-on-error", "version": "1.0.0", "description": "An exchange for throw-on-error support in urql", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "exchanges/throw-on-error" }, "keywords": [ "urql", "graphql client", "graphql", "exchanges", "throw on error" ], "main": "dist/urql-exchange-throw-on-error", "module": "dist/urql-exchange-throw-on-error.mjs", "types": "dist/urql-exchange-throw-on-error.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-exchange-throw-on-error.d.ts", "import": "./dist/urql-exchange-throw-on-error.mjs", "require": "./dist/urql-exchange-throw-on-error.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@urql/core": "workspace:*", "graphql": "^16.0.0" }, "peerDependencies": { "@urql/core": "^6.0.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "graphql-toe": "^1.0.0-rc.0", "wonka": "^6.3.2" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: exchanges/throw-on-error/src/index.ts ================================================ export { throwOnErrorExchange } from './throwOnErrorExchange'; ================================================ FILE: exchanges/throw-on-error/src/throwOnErrorExchange.test.ts ================================================ import { pipe, map, fromValue, toPromise, take } from 'wonka'; import { vi, expect, it, beforeEach } from 'vitest'; import { GraphQLError } from 'graphql'; import { gql, createClient, Operation, ExchangeIO, Client, CombinedError, } from '@urql/core'; import { throwOnErrorExchange } from './throwOnErrorExchange'; const dispatchDebug = vi.fn(); const query = gql` { topLevel topLevelList object { inner } objectList { inner } } `; const mockData = { topLevel: 'topLevel', topLevelList: ['topLevelList'], object: { inner: 'inner' }, objectList: [{ inner: 'inner' }], }; let client: Client, op: Operation; beforeEach(() => { client = createClient({ url: 'http://0.0.0.0', exchanges: [], }); op = client.createRequestOperation('query', { key: 1, query, variables: {} }); }); it('throws on top level field error', async () => { const forward: ExchangeIO = ops$ => pipe( ops$, map( operation => ({ operation, data: { ...mockData, topLevel: null, }, error: new CombinedError({ graphQLErrors: [ new GraphQLError('top level error', { path: ['topLevel'] }), ], }), }) as any ) ); const res = await pipe( fromValue(op), throwOnErrorExchange()({ forward, client, dispatchDebug }), take(1), toPromise ); expect(() => res.data?.topLevel).toThrow('top level error'); expect(() => res.data).not.toThrow(); expect(() => res.data?.topLevelList[0]).not.toThrow(); }); it('throws on top level list element error', async () => { const forward: ExchangeIO = ops$ => pipe( ops$, map( operation => ({ operation, data: { ...mockData, topLevelList: ['topLevelList', null], }, error: new CombinedError({ graphQLErrors: [ new GraphQLError('top level list error', { path: ['topLevelList', 1], }), ], }), }) as any ) ); const res = await pipe( fromValue(op), throwOnErrorExchange()({ forward, client, dispatchDebug }), take(1), toPromise ); expect(() => res.data?.topLevelList[1]).toThrow('top level list error'); expect(() => res.data).not.toThrow(); expect(() => res.data?.topLevelList[0]).not.toThrow(); }); it('throws on object field error', async () => { const forward: ExchangeIO = ops$ => pipe( ops$, map( operation => ({ operation, data: { ...mockData, object: null, }, error: new CombinedError({ graphQLErrors: [ new GraphQLError('object field error', { path: ['object'] }), ], }), }) as any ) ); const res = await pipe( fromValue(op), throwOnErrorExchange()({ forward, client, dispatchDebug }), take(1), toPromise ); expect(() => res.data?.object).toThrow('object field error'); expect(() => res.data?.object.inner).toThrow('object field error'); expect(() => res.data).not.toThrow(); expect(() => res.data?.topLevel).not.toThrow(); }); it('throws on object inner field error', async () => { const forward: ExchangeIO = ops$ => pipe( ops$, map( operation => ({ operation, data: { ...mockData, object: { inner: null, }, }, error: new CombinedError({ graphQLErrors: [ new GraphQLError('object inner field error', { path: ['object', 'inner'], }), ], }), }) as any ) ); const res = await pipe( fromValue(op), throwOnErrorExchange()({ forward, client, dispatchDebug }), take(1), toPromise ); expect(() => res.data?.object.inner).toThrow('object inner field error'); expect(() => res.data).not.toThrow(); expect(() => res.data?.object).not.toThrow(); }); it('throws on object list field error', async () => { const forward: ExchangeIO = ops$ => pipe( ops$, map( operation => ({ operation, data: { ...mockData, objectList: null, }, error: new CombinedError({ graphQLErrors: [ new GraphQLError('object list field error', { path: ['objectList'], }), ], }), }) as any ) ); const res = await pipe( fromValue(op), throwOnErrorExchange()({ forward, client, dispatchDebug }), take(1), toPromise ); expect(() => res.data?.objectList).toThrow('object list field error'); expect(() => res.data?.objectList[0]).toThrow('object list field error'); expect(() => res.data?.objectList[0].inner).toThrow( 'object list field error' ); expect(() => res.data).not.toThrow(); expect(() => res.data?.topLevel).not.toThrow(); }); it('throws on object inner field error', async () => { const forward: ExchangeIO = ops$ => pipe( ops$, map( operation => ({ operation, data: { ...mockData, objectList: [{ inner: 'inner' }, { inner: null }], }, error: new CombinedError({ graphQLErrors: [ new GraphQLError('object list inner field error', { path: ['objectList', 1, 'inner'], }), ], }), }) as any ) ); const res = await pipe( fromValue(op), throwOnErrorExchange()({ forward, client, dispatchDebug }), take(1), toPromise ); expect(() => res.data?.objectList[1].inner).toThrow( 'object list inner field error' ); expect(() => res.data).not.toThrow(); expect(() => res.data?.objectList[0].inner).not.toThrow(); }); ================================================ FILE: exchanges/throw-on-error/src/throwOnErrorExchange.ts ================================================ import type { Exchange } from '@urql/core'; import { mapExchange } from '@urql/core'; import { toe } from 'graphql-toe'; /** Exchange factory that maps the fields of the data to throw an error on access if the field was errored. * * @returns the created throw-on-error {@link Exchange}. */ export const throwOnErrorExchange = (): Exchange => { return mapExchange({ onResult(result) { if (result.data) { const errors = result.error && result.error.graphQLErrors; result.data = toe({ data: result.data, errors }); } return result; }, }); }; ================================================ FILE: exchanges/throw-on-error/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: exchanges/throw-on-error/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: package.json ================================================ { "private": true, "scripts": { "test": "test -z $CI && vitest || vitest", "check": "tsc", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "node ./scripts/actions/build-all.mjs", "postinstall": "node ./scripts/prepare/postinstall.js", "pack": "node ./scripts/actions/pack-all.mjs", "changeset:version": "node ./scripts/changesets/version.mjs", "changeset:publish": "changeset publish && pnpm jsr", "jsr": "pnpm --filter @urql/core jsr", "jsr:dryrun": "pnpm --filter @urql/core jsr --dry-run" }, "eslintConfig": { "root": true, "extends": [ "./scripts/eslint/preset.js" ] }, "prettier": { "singleQuote": true, "arrowParens": "avoid", "trailingComma": "es5" }, "lint-staged": { "*.{js,jsx,ts,tsx}": "eslint -c scripts/eslint/preset.js --fix", "*.json": "prettier --write", "*.md": "prettier --write" }, "husky": { "hooks": { "pre-commit": "lint-staged --quiet --relative" } }, "pnpm": { "peerDependencyRules": { "ignoreMissing": [ "react-native" ], "allowedVersions": { "styled-components": "5" } }, "overrides": { "graphql": "^16.6.0", "styled-components": "^5.2.3", "wonka": "^6.3.2" } }, "devDependencies": { "@0no-co/graphql.web": "^1.0.13", "@actions/artifact": "^2.3.2", "@actions/core": "^1.11.1", "@babel/core": "^7.25.2", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-typescript": "^7.25.2", "@changesets/cli": "^2.29.6", "@changesets/get-github-info": "0.6.0", "@npmcli/arborist": "^7.5.4", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-terser": "^0.4.4", "@rollup/pluginutils": "^5.1.0", "@types/node": "^18.19.50", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "cypress": "^12.17.4", "dotenv": "^16.4.5", "eslint": "^8.57.0", "eslint-config-prettier": "^8.10.0", "eslint-plugin-es5": "^1.5.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.36.1", "eslint-plugin-react-hooks": "^4.6.2", "execa": "^8.0.0", "glob": "^9.3.5", "graphql": "^16.9.0", "husky-v4": "^4.3.8", "invariant": "^2.2.4", "jsdom": "^25.0.0", "jsr": "^0.13.2", "lint-staged": "^13.3.0", "npm-packlist": "^8.0.2", "npm-run-all": "^4.1.5", "prettier": "^3.3.3", "react": "^17.0.2", "react-dom": "^17.0.2", "react-is": "^17.0.2", "rimraf": "^6.0.1", "rollup": "^3.29.4", "rollup-plugin-cjs-check": "^1.0.3", "rollup-plugin-dts": "^5.3.1", "rollup-plugin-visualizer": "^5.12.0", "tar": "^7.4.3", "terser": "^5.32.0", "typescript": "^5.6.2", "vite": "^5.4.5", "vite-tsconfig-paths": "^4.3.2", "vitest": "^2.1.1" }, "dependencies": { "@actions/github": "^6.0.0", "node-fetch": "^3.3.2" }, "engines": { "pnpm": ">=9.0.0", "node": ">=18.0.0" } } ================================================ FILE: packages/core/.gitignore ================================================ /internal ================================================ FILE: packages/core/CHANGELOG.md ================================================ # @urql/core ## 6.0.1 ### Patch Changes - Use nullish coalescing for `preferGetMethod` and `preferGetForPersistedQueries` so that `false` is kept if set Submitted by [@dargmuesli](https://github.com/dargmuesli) (See [#3812](https://github.com/urql-graphql/urql/pull/3812)) ## 6.0.0 ### Major Changes - By default leverage GET for queries where the query-string + variables comes down to less than 2048 characters. When upgrading it's important to see whether your server supports `GET`, if it doesn't ideally adding support for it or alternatively setting `preferGetMethod` in the `createClient` method as well as `preferGetForPersistedQueries` for the persisted exchange to `false` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3789](https://github.com/urql-graphql/urql/pull/3789)) ### Minor Changes - Fix type definition for `GraphQLRequestParams` to make `variables` field optional if and only if **all** fields are optional or nullish Submitted by [@arkandias](https://github.com/arkandias) (See [#3807](https://github.com/urql-graphql/urql/pull/3807)) ## 5.2.0 ### Minor Changes - export the getOperationName utility function Submitted by [@giacomocerquone](https://github.com/giacomocerquone) (See [#3785](https://github.com/urql-graphql/urql/pull/3785)) ## 5.1.2 ### Patch Changes - Correct typo in cacheHit debug message of the `debugExchange` Submitted by [@jorrit](https://github.com/jorrit) (See [#3773](https://github.com/urql-graphql/urql/pull/3773)) - ⚠️ Fix `fetchSource` not text-decoding response chunks as streams, which could cause UTF-8 decoding to break Submitted by [@i110](https://github.com/i110) (See [#3767](https://github.com/urql-graphql/urql/pull/3767)) - ⚠️ Fix compatibility with Typescript >5.5 (See: https://github.com/0no-co/graphql.web/pull/49) Submitted by [@andreisergiu98](https://github.com/andreisergiu98) (See [#3730](https://github.com/urql-graphql/urql/pull/3730)) - Change debug log verbosity to `console.debug` rather than `console.log` Submitted by [@kitten](https://github.com/kitten) (See [#3770](https://github.com/urql-graphql/urql/pull/3770)) ## 5.1.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) ## 5.1.0 ### Minor Changes - Remove `addMetadata` transform where we'd strip out metadata for production environments, this particularly affects `OperationResult.context.metadata.cacheOutcome` Submitted by [@alpavlove](https://github.com/alpavlove) (See [#3714](https://github.com/urql-graphql/urql/pull/3714)) ## 5.0.8 ### Patch Changes - ⚠️ Fix `deepMerge` regression on array values Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3696](https://github.com/urql-graphql/urql/pull/3696)) ## 5.0.7 ### Patch Changes - Remove `for-of` syntax from `@urql/core` helpers for JSC memory reduction Submitted by [@kitten](https://github.com/kitten) (See [#3690](https://github.com/urql-graphql/urql/pull/3690)) ## 5.0.6 ### Patch Changes - Allow empty error messages when re-hydrating GraphQL errors Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3650](https://github.com/urql-graphql/urql/pull/3650)) ## 5.0.5 ### Patch Changes - Removes double serialization of `data` in `ssrExchange` Submitted by [@negezor](https://github.com/negezor) (See [#3632](https://github.com/urql-graphql/urql/pull/3632)) ## 5.0.4 ### Patch Changes - Change how we calculate the `OperationKey` to take files into account, before we would encode them to `null` resulting in every mutation with the same variables (excluding the files) to have the same key. This resulted in mutations that upload different files at the same time to share a result in GraphCache Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3601](https://github.com/urql-graphql/urql/pull/3601)) ## 5.0.3 ### Patch Changes - Use `documentId` from persisted documents for document keys, when it's available Submitted by [@kitten](https://github.com/kitten) (See [#3575](https://github.com/urql-graphql/urql/pull/3575)) ## 5.0.2 ### Patch Changes - ⚠️ Fix issue where a reexecute on an in-flight operation would lead to multiple network-requests. For example, this issue presents itself when Graphcache is concurrently updating multiple, inter-dependent queries with shared entities. One query completing while others are still in-flight may lead to duplicate operations being issued Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3573](https://github.com/urql-graphql/urql/pull/3573)) ## 5.0.1 ### Patch Changes - ⚠️ Fix `@ts-ignore` on TypeScript peer dependency import in typings not being applied due to a leading `!` character Submitted by [@kitten](https://github.com/kitten) (See [#3567](https://github.com/urql-graphql/urql/pull/3567)) ## 5.0.0 ### Major Changes - Remove deprecated `dedupExchange` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3520](https://github.com/urql-graphql/urql/pull/3520)) - Remove deprecated `maskTypename` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3520](https://github.com/urql-graphql/urql/pull/3520)) ### Patch Changes - Upgrade `@0no-co/graphql.web` to `1.0.5` Submitted by [@kitten](https://github.com/kitten) (See [#3553](https://github.com/urql-graphql/urql/pull/3553)) ## 4.3.0 ### Minor Changes - Support [Apollo Federation's format](https://www.apollographql.com/docs/router/executing-operations/subscription-multipart-protocol/) for subscription results in `multipart/mixed` responses (result properties essentially are namespaced on a `payload` key) Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3499](https://github.com/urql-graphql/urql/pull/3499)) - Add support for sending persisted documents. Any `DocumentNode` with no/empty definitions and a `documentId` property is considered a persisted document. When this is detected a `documentId` parameter rather than a `query` string is sent to the GraphQL API, similar to Automatic Persisted Queries (APQs). However, APQs are only supported via `@urql/exchange-persisted`, while support for `documentId` is now built-in Submitted by [@kitten](https://github.com/kitten) (See [#3515](https://github.com/urql-graphql/urql/pull/3515)) ### Patch Changes - Allow `url` to be a plain, non-URL pathname (i.e. `/api/graphql`) to be used with `preferGetMethod` Submitted by [@akrantz01](https://github.com/akrantz01) (See [#3514](https://github.com/urql-graphql/urql/pull/3514)) - Correctly support the `Headers` class being used in `fetchOptions` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3505](https://github.com/urql-graphql/urql/pull/3505)) ## 4.2.3 ### Patch Changes - Add back our cache-outcome on the document-cache, this was behind a development flag however in our normalized cache we always add it already Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3464](https://github.com/urql-graphql/urql/pull/3464)) ## 4.2.2 ### Patch Changes - ⚠️ Fix the default `cacheExchange` crashing on `cache-only` request policies with cache misses due to `undefined` results Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3459](https://github.com/urql-graphql/urql/pull/3459)) ## 4.2.1 ### Patch Changes - ⚠️ Fix incorrect JSON stringification of objects from different JS contexts. This could lead to invalid variables being generated in the Vercel Edge runtime specifically Submitted by [@SoraKumo001](https://github.com/SoraKumo001) (See [#3453](https://github.com/urql-graphql/urql/pull/3453)) ## 4.2.0 ### Minor Changes - Try to parse `text/plain` content-type as JSON before bailing out with an error Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3430](https://github.com/urql-graphql/urql/pull/3430)) ## 4.1.4 ### Patch Changes - Implement new `@defer` / `@stream` transport protocol spec changes Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3389](https://github.com/urql-graphql/urql/pull/3389)) - Support non spec-compliant error bodies, i.e. the Shopify API does return `errors` but as an object. Adding a check whether we are really dealing with an Array of errors enables this Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3395](https://github.com/urql-graphql/urql/pull/3395)) - ⚠️ Fix `preferGetMethod: 'force' | 'within-url-limit'` not being applied correctly by the `Client` Submitted by [@Burbenog](https://github.com/Burbenog) (See [#3403](https://github.com/urql-graphql/urql/pull/3403)) ## 4.1.3 ### Patch Changes - ⚠️ Fix missing `teardown` operation handling in the `ssrExchange`. This could lead to duplicate network operations being executed Submitted by [@kitten](https://github.com/kitten) (See [#3386](https://github.com/urql-graphql/urql/pull/3386)) ## 4.1.2 ### Patch Changes - Explicitly unblock `client.reexecuteOperation` calls to allow stalled operations from continuing and re-executing. Previously, this could cause `@urql/exchange-graphcache` to stall if an optimistic mutation led to a cache miss Submitted by [@kitten](https://github.com/kitten) (See [#3363](https://github.com/urql-graphql/urql/pull/3363)) ## 4.1.1 ### Patch Changes - Add case for `subscriptionExchange` to handle `GraphQLError[]` received in the `error` observer callback. **Note:** This doesn't strictly check for the `GraphQLError` shape and only checks for arrays and receiving errors in the `ExecutionResult` on the `next` observer callback is preferred and recommended for transports Submitted by [@kitten](https://github.com/kitten) (See [#3346](https://github.com/urql-graphql/urql/pull/3346)) ## 4.1.0 ### Minor Changes - Update `formatDocument` to output `FormattedNode` type mapping. The formatter will now annotate added `__typename` fields with `_generated: true`, place selection nodes' directives onto a `_directives` dictionary, and will filter directives to not include `"_"` underscore prefixed directives in the final query. This prepares us for a feature that allows enhanced client-side directives in Graphcache Submitted by [@kitten](https://github.com/kitten) (See [#3317](https://github.com/urql-graphql/urql/pull/3317)) ### Patch Changes - Add `OperationContext.optimistic` flag as an internal indication on whether a mutation triggered an optimistic update in `@urql/exchange-graphcache`'s `cacheExchange` Submitted by [@kitten](https://github.com/kitten) (See [#3308](https://github.com/urql-graphql/urql/pull/3308)) ## 4.0.11 ### Patch Changes - Re-order `maskTypename` to apply masking earlier in the chain Submitted by [@kitten](https://github.com/kitten) (See [#3298](https://github.com/urql-graphql/urql/pull/3298)) - ⚠️ Fix `ssrExchange` not formatting query documents using `formatDocument`. Without this call we'd run the risk of not having `__typename` available on the client-side when rehydrating Submitted by [@kitten](https://github.com/kitten) (See [#3288](https://github.com/urql-graphql/urql/pull/3288)) - Add deprecation notice for `maskTypename` option. Masking typenames in a result is no longer recommended. It’s only useful when multiple pre-conditions are applied and inferior to mapping to an input object manually Submitted by [@kitten](https://github.com/kitten) (See [#3299](https://github.com/urql-graphql/urql/pull/3299)) ## 4.0.10 ### Patch Changes - Add missing `fetchSubscriptions` entry to `OperationContext`. The Client’s `fetchSubscriptions` now works properly and can be used to execute subscriptions as multipart/event-stream requests Submitted by [@kitten](https://github.com/kitten) (See [#3244](https://github.com/urql-graphql/urql/pull/3244)) - ⚠️ Fix `fetchSource` not working for subscriptions since `hasNext` isn’t necessarily set Submitted by [@kitten](https://github.com/kitten) (See [#3244](https://github.com/urql-graphql/urql/pull/3244)) ## 4.0.9 ### Patch Changes - Return `AbortController` invocation to previous behaviour where it used to be more forceful. It will now properly abort outside of when our generator yields results, and hence now also cancels requests again that have already delivered headers but are currently awaiting a response body Submitted by [@kitten](https://github.com/kitten) (See [#3239](https://github.com/urql-graphql/urql/pull/3239)) ## 4.0.8 ### Patch Changes - Respect `additionalTypenames` on subscriptions and re-execute queries for them as well, as one would intuitively expect Submitted by [@kitten](https://github.com/kitten) (See [#3230](https://github.com/urql-graphql/urql/pull/3230)) - Update build process to generate correct source maps Submitted by [@kitten](https://github.com/kitten) (See [#3201](https://github.com/urql-graphql/urql/pull/3201)) - Don't allow `isSubscriptionOperation` option in `subscriptionExchange` to include `teardown` operations, to avoid confusion Submitted by [@kitten](https://github.com/kitten) (See [#3206](https://github.com/urql-graphql/urql/pull/3206)) ## 4.0.7 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 4.0.6 ### Patch Changes - Handle `multipart/mixed` variations starting with boundary rather than CRLF and a boundary Submitted by [@kitten](https://github.com/kitten) (See [#3172](https://github.com/urql-graphql/urql/pull/3172)) - ⚠️ Fix regression which would disallow `network-only` operations after `cache-and-network` completed Submitted by [@kitten](https://github.com/kitten) (See [#3174](https://github.com/urql-graphql/urql/pull/3174)) ## 4.0.5 ### Patch Changes - Replace `File` and `Blob` objects with `null` in variables if multipart request will be started Submitted by [@kitten](https://github.com/kitten) (See [#3169](https://github.com/urql-graphql/urql/pull/3169)) - Strictly deduplicate `cache-and-network` and `network-only` operations, while a non-stale response is being waited for Submitted by [@kitten](https://github.com/kitten) (See [#3157](https://github.com/urql-graphql/urql/pull/3157)) - ⚠️ Fix boundary stopping `multipart/mixed` streams when it randomly occurs in response payloads Submitted by [@kitten](https://github.com/kitten) (See [#3155](https://github.com/urql-graphql/urql/pull/3155)) - Improve dispatching of arbitrary operations using `reexecuteOperation` Submitted by [@kitten](https://github.com/kitten) (See [#3159](https://github.com/urql-graphql/urql/pull/3159)) ## 4.0.4 ### Patch Changes - ⚠️ Fix `hasNext` being defaulted to `false` when a new subscription event is received on the `subscriptionExchange` that doesn't have `hasNext` set Submitted by [@kitten](https://github.com/kitten) (See [#3137](https://github.com/urql-graphql/urql/pull/3137)) ## 4.0.3 ### Patch Changes - Handle `fetch` rejections in `makeFetchSource` and properly hand them over to `CombinedError`s Submitted by [@kitten](https://github.com/kitten) (See [#3131](https://github.com/urql-graphql/urql/pull/3131)) ## 4.0.2 ### Patch Changes - ⚠️ Fix incremental delivery payloads not merging data correctly, or not handling patches on root results Submitted by [@kitten](https://github.com/kitten) (See [#3124](https://github.com/urql-graphql/urql/pull/3124)) ## 4.0.1 ### Patch Changes - ⚠️ Fix format of `map` form data field on multipart upload requests. This was erroneously set to a string rather than a string tuple Submitted by [@kitten](https://github.com/kitten) (See [#3118](https://github.com/urql-graphql/urql/pull/3118)) ## 4.0.0 ### Major Changes - Remove `defaultExchanges` from `@urql/core` and make `exchanges` a required property on `Client` construction. In doing so we make the `urql` package more tree-shakeable as the three default exchanges are in no code paths meaning they can be removed if not used. A migration would look as follows if you are currently creating a client without exchanges ```js import { createClient, cacheExchange, fetchExchange } from '@urql/core'; const client = createClient({ url: '', exchanges: [cacheExchange, fetchExchange], }); ``` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3033](https://github.com/urql-graphql/urql/pull/3033)) - Update `subscriptionExchange` to receive `FetchBody` instead. In the usual usage of `subscriptionExchange` (for instance with `graphql-ws`) you can expect no breaking changes. However, the `key` and `extensions` field has been removed and instead the `forwardSubscription` function receives the full `Operation` as a second argument Submitted by [@kitten](https://github.com/kitten) (See [#3054](https://github.com/urql-graphql/urql/pull/3054)) - Remove dependence on `graphql` package and replace it with `@0no-co/graphql.web`, which reduces the default bundlesize impact of `urql` packages to a minimum. All types should remain compatible, even if you use `graphql` elsewhere in your app, and if other dependencies are using `graphql` you may alias it to `graphql-web-lite` Submitted by [@kitten](https://github.com/kitten) (See [#3097](https://github.com/urql-graphql/urql/pull/3097)) - Update `OperationResult.hasNext` and `OperationResult.stale` to be required fields. If you have a custom exchange creating results, you'll have to add these fields or use the `makeResult`, `mergeResultPatch`, or `makeErrorResult` helpers Submitted by [@kitten](https://github.com/kitten) (See [#3061](https://github.com/urql-graphql/urql/pull/3061)) - Remove `getOperationName` export from `@urql/core` Submitted by [@kitten](https://github.com/kitten) (See [#3062](https://github.com/urql-graphql/urql/pull/3062)) ### Minor Changes - Return a new `OperationResultSource` from all `Client` methods (which replaces `PromisifiedSource` on shortcut methods). This allows not only `toPromise()` to be called, but it can also be used as an awaitable `PromiseLike` and has a `.subscribe(onResult)` method aliasing the subscribe utility from `wonka` Submitted by [@kitten](https://github.com/kitten) (See [#3060](https://github.com/urql-graphql/urql/pull/3060)) - Update `subscriptionExchange` to support incremental results out of the box. If a subscription proactively completes, results are also now updated with `hasNext: false` Submitted by [@kitten](https://github.com/kitten) (See [#3055](https://github.com/urql-graphql/urql/pull/3055)) - Implement `text/event-stream` response support. This generally adheres to the GraphQL SSE protocol and GraphQL Yoga push responses, and is an alternative to `multipart/mixed` Submitted by [@kitten](https://github.com/kitten) (See [#3050](https://github.com/urql-graphql/urql/pull/3050)) - Implement GraphQL Multipart Request support in `@urql/core`. This adds the File/Blob upload support to `@urql/core`, which effectively deprecates `@urql/exchange-multipart-fetch` Submitted by [@kitten](https://github.com/kitten) (See [#3051](https://github.com/urql-graphql/urql/pull/3051)) - Support `GraphQLRequest.extensions` as spec-extensions input to GraphQL requests Submitted by [@kitten](https://github.com/kitten) (See [#3054](https://github.com/urql-graphql/urql/pull/3054)) - Allow subscriptions to be handled by the `fetchExchange` when `fetchSubscriptions` is turned on Submitted by [@kitten](https://github.com/kitten) (See [#3106](https://github.com/urql-graphql/urql/pull/3106)) - Deprecate the `dedupExchange`. The functionality of deduplicating queries and subscriptions has now been moved into and absorbed by the `Client`. Previously, the `Client` already started doing some work to share results between queries, and to avoid dispatching operations as needed. It now only dispatches operations strictly when the `dedupExchange` would allow so as well, moving its logic into the `Client` Submitted by [@kitten](https://github.com/kitten) (See [#3058](https://github.com/urql-graphql/urql/pull/3058)) ### Patch Changes - Deduplicate operations as the `dedupExchange` did; by filtering out duplicate operations until either the original operation has been cancelled (teardown) or a first result (without `hasNext: true`) has come in Submitted by [@kitten](https://github.com/kitten) (See [#3101](https://github.com/urql-graphql/urql/pull/3101)) - ⚠️ Fix source maps included with recently published packages, which lost their `sourcesContent`, including additional source files, and had incorrect paths in some of them Submitted by [@kitten](https://github.com/kitten) (See [#3053](https://github.com/urql-graphql/urql/pull/3053)) - Allow `makeOperation` to be called with a partial `OperationContext` when it’s called to copy an operation. When it receives an `Operation` as a second argument now, the third argument, the context, will be spread into the prior `operation.context` Submitted by [@kitten](https://github.com/kitten) (See [#3081](https://github.com/urql-graphql/urql/pull/3081)) - Move `multipart/mixed` to end of `Accept` header to avoid cauing Yoga to unnecessarily use it Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3039](https://github.com/urql-graphql/urql/pull/3039)) - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Update `Exchange` contract and `composeExchanges` utility to remove the need to manually call `share` on either incoming `Source` or `forward()`’s `Source`. This is now taken care of internally in `composeExchanges` and should make it easier for you to create custom exchanges and for us to explain them Submitted by [@kitten](https://github.com/kitten) (See [#3082](https://github.com/urql-graphql/urql/pull/3082)) - Add support for `graphql`’s built-in `TypedQueryDocumentNode` typings for type inference Submitted by [@kitten](https://github.com/kitten) (See [#3085](https://github.com/urql-graphql/urql/pull/3085)) - Add missing type exports of SSR-related types (`SerializedResult`, `SSRExchangeParams`, `SSRExchange`, and `SSRData`) to `@urql/core`'s type exports Submitted by [@kitten](https://github.com/kitten) (See [#3079](https://github.com/urql-graphql/urql/pull/3079)) - Allow any object fitting the `GraphQLError` shape to rehydrate without passing through a `GraphQLError` constructor in `CombinedError` Submitted by [@kitten](https://github.com/kitten) (See [#3087](https://github.com/urql-graphql/urql/pull/3087)) - Add missing `hasNext` and `stale` passthroughs on caching exchanges Submitted by [@kitten](https://github.com/kitten) (See [#3059](https://github.com/urql-graphql/urql/pull/3059)) - ⚠️ Fix incremental results not merging `errors` from subsequent non-incremental results Submitted by [@kitten](https://github.com/kitten) (See [#3055](https://github.com/urql-graphql/urql/pull/3055)) - Add logic for `request.extensions.persistedQuery` to `@urql/core` to omit sending `query` as needed Submitted by [@kitten](https://github.com/kitten) (See [#3057](https://github.com/urql-graphql/urql/pull/3057)) - ⚠️ Fix incorrect operation name being picked from queries that contain multiple operations Submitted by [@kitten](https://github.com/kitten) (See [#3062](https://github.com/urql-graphql/urql/pull/3062)) - Replace fetch source implementation with async generator implementation, based on Wonka's `fromAsyncIterable`. This also further hardens our support for the "Incremental Delivery" specification and refactors its implementation and covers more edge cases Submitted by [@kitten](https://github.com/kitten) (See [#3043](https://github.com/urql-graphql/urql/pull/3043)) - Ensure network errors are always issued with `CombinedError`s, while downstream errors are re-thrown Submitted by [@kitten](https://github.com/kitten) (See [#3063](https://github.com/urql-graphql/urql/pull/3063)) - Refactor `Client` result source construction code and allow multiple mutation results, if `result.hasNext` on a mutation result is set to `true`, indicating deferred or streamed results Submitted by [@kitten](https://github.com/kitten) (See [#3102](https://github.com/urql-graphql/urql/pull/3102)) - Remove dependence on `import { visit } from 'graphql';` with smaller but functionally equivalent alternative Submitted by [@kitten](https://github.com/kitten) (See [#3097](https://github.com/urql-graphql/urql/pull/3097)) ## 3.2.2 ### Patch Changes - ⚠️ Fix generated empty `Variables` type as passed to generics, that outputs a type of `{ [var: string]: never; }`. A legacy/unsupported version of `typescript-urql`, which wraps `urql`'s React hooks, generates empty variables types as the following code snippet, which is not detected: ```ts type Exact = { [K in keyof T]: T[K] }; type Variables = Exact<{ [key: string]: never }>; ``` Submitted by [@kitten](https://github.com/kitten) (See [#3029](https://github.com/urql-graphql/urql/pull/3029)) ## 3.2.1 ### Patch Changes - Bump to `@urql/core@3.2.1` due to invalid `3.2.0` release Submitted by [@kitten](https://github.com/kitten) (See [`a84268db`](https://github.com/urql-graphql/urql/commit/a84268db98789b2fd23de009c7b5e1c09fca7103)) ## 3.2.0 ### Minor Changes - Update support for the "Incremental Delivery" payload specification, accepting the new `incremental` property on execution results, as per the specification. This will expand support for newer APIs implementing the more up-to-date specification Submitted by [@kitten](https://github.com/kitten) (See [#3007](https://github.com/urql-graphql/urql/pull/3007)) - Update default `Accept` header to include `multipart/mixed` and `application/graphql-response+json`. The former seems to now be a defactor standard-accepted indication for support of the "Incremental Delivery" GraphQL over HTTP spec addition/RFC, and the latter is an updated form of the older `Content-Type` of GraphQL responses, so both the old and new one should now be included Submitted by [@kitten](https://github.com/kitten) (See [#3007](https://github.com/urql-graphql/urql/pull/3007)) ### Patch Changes - Add TSDoc annotations to all external `@urql/core` APIs Submitted by [@kitten](https://github.com/kitten) (See [#2962](https://github.com/urql-graphql/urql/pull/2962)) - ⚠️ Fix subscriptions not being duplicated when `hasNext` isn't set. The `hasNext` field is an upcoming "Incremental Delivery" field. When a subscription result doesn't set it we now set it to `true` manually. This indicates to the `dedupExchange` that no duplicate subscription operations should be started Submitted by [@kitten](https://github.com/kitten) (See [#3015](https://github.com/urql-graphql/urql/pull/3015)) - Expose consistent `GraphQLRequestParams` utility type from which `GraphQLRequest`s are created in all bindings Submitted by [@kitten](https://github.com/kitten) (See [#3022](https://github.com/urql-graphql/urql/pull/3022)) ## 3.1.1 ### Patch Changes - Correctly mark cache-hits from the ssr-exchange, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2872](https://github.com/urql-graphql/urql/pull/2872)) - ⚠️ Fix type-generation, with a change in TS/Rollup the type generation took the paths as src and resolved them into the types dir, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2870](https://github.com/urql-graphql/urql/pull/2870)) - ⚠️ Fix regression in `@urql/core`'s `stringifyDocument` that caused some formatted documents to not be reprinted, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2871](https://github.com/urql-graphql/urql/pull/2871)) ## 3.1.0 ### Minor Changes - Implement `mapExchange`, which replaces `errorExchange`, allowing `onOperation` and `onResult` to be called to either react to or replace operations and results. For backwards compatibility, this exchange is also exported as `errorExchange` and supports `onError`, by [@kitten](https://github.com/kitten) (See [#2846](https://github.com/urql-graphql/urql/pull/2846)) ### Patch Changes - Move remaining `Variables` generics over from `object` default to `Variables extends AnyVariables = AnyVariables`. This has been introduced previously in [#2607](https://github.com/urql-graphql/urql/pull/2607) but some missing ports have been missed due to TypeScript not catching them previously. Depending on your TypeScript version the `object` default is incompatible with `AnyVariables`, by [@kitten](https://github.com/kitten) (See [#2843](https://github.com/urql-graphql/urql/pull/2843)) - Reuse output of `stringifyDocument` in place of repeated `print`. This will mean that we now prevent calling `print` repeatedly for identical operations and are instead only reusing the result once. This change has a subtle consequence of our internals. Operation keys will change due to this refactor and we will no longer sanitise strip newlines from queries that `@urql/core` has printed, by [@kitten](https://github.com/kitten) (See [#2847](https://github.com/urql-graphql/urql/pull/2847)) - Update to `wonka@^6.1.2` to fix memory leak in `fetch` caused in Node.js by a lack of clean up after initiating a request, by [@kitten](https://github.com/kitten) (See [#2850](https://github.com/urql-graphql/urql/pull/2850)) ## 3.0.5 ### Patch Changes - Update typings of the client to encompass the changes of https://github.com/FormidableLabs/urql/pull/2692, by [@c-schwan](https://github.com/c-schwan) (See [#2758](https://github.com/FormidableLabs/urql/pull/2758)) - ⚠️ Fix case where our transform-debug-target babel plugin would override the root dispatchDebug in `compose.ts` with the latest found exchange, in this case `fetchExchange`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2762](https://github.com/FormidableLabs/urql/pull/2762)) ## 3.0.4 ### Patch Changes - ⚠️ Fix `ssrExchange` bug which prevented `staleWhileRevalidate` from sending off requests as network-only requests, and caused unrelated `network-only` operations to be dropped, by [@kitten](https://github.com/kitten) (See [#2691](https://github.com/FormidableLabs/urql/pull/2691)) - Allow URL limit for GET requests to be bypassed using `preferGetMethod: 'force'` rather than the default `true` or `'within-url-limit'` value, by [@kitten](https://github.com/kitten) (See [#2692](https://github.com/FormidableLabs/urql/pull/2692)) - ⚠️ Fix operation identities preventing users from deeply cloning operation contexts. Instead, we now use a client-wide counter (rolling over as needed). While this changes an internal data structure in `@urql/core` only, this change also affects the `offlineExchange` in `@urql/exchange-graphcache` due to it relying on the identity being previously an object rather than an integer, by [@kitten](https://github.com/kitten) (See [#2732](https://github.com/FormidableLabs/urql/pull/2732)) ## 3.0.3 ### Patch Changes - ⚠️ Fix variable types in core makeOperation, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2665](https://github.com/FormidableLabs/urql/pull/2665)) ## 3.0.2 ### Patch Changes - ⚠️ Fix case where `maskTypename` would not traverse down when the root query-field does not contain a `__typename`, by [@mlecoq](https://github.com/mlecoq) (See [#2643](https://github.com/FormidableLabs/urql/pull/2643)) ## 3.0.1 ### Patch Changes - ⚠️ fix setting a client default requestPolicy, we set `context.requestPolicy: undefined` from our bindings which makes a spread override the client-set default, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2634](https://github.com/FormidableLabs/urql/pull/2634)) ## 3.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Remove support for options on the `Client` and `Client.createOperationContext`. We've noticed that there's no real need for `createOperationContext` or the options on the `Client` and that it actually encourages modifying properties on the `Client` that are really meant to be modified dynamically via exchanges, by [@kitten](https://github.com/kitten) (See [#2619](https://github.com/FormidableLabs/urql/pull/2619)) - Implement stricter variables types, which require variables to always be passed and match TypeScript types when the generic is set or inferred. This is a breaking change for TypeScript users potentially, unless all types are adhered to, by [@kitten](https://github.com/kitten) (See [#2607](https://github.com/FormidableLabs/urql/pull/2607)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Minor Changes - Remove the `babel-plugin-modular-graphql` helper, this because the graphql package hasn't converted to ESM yet which gives issues in node environments, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2551](https://github.com/FormidableLabs/urql/pull/2551)) ## 2.6.1 ### Patch Changes - ⚠️ Fix missing React updates after an incoming response that schedules a mount. We now prevent dispatched operations from continuing to flush synchronously when the original source that runs the queue has terminated. This is important for the React bindings, because an update (e.g. `setState`) may recursively schedule a mount, which then disabled other `setState` updates from being processed. Previously we assumed that React used a trampoline scheduler for updates, however it appears that `setState` can recursively start more React work, by [@kitten](https://github.com/kitten) (See [#2556](https://github.com/FormidableLabs/urql/pull/2556)) ## 2.6.0 ### Minor Changes - Allow providing a custom `isSubscriptionOperation` implementation, by [@n1ru4l](https://github.com/n1ru4l) (See [#2534](https://github.com/FormidableLabs/urql/pull/2534)) ## 2.5.0 ### Minor Changes - Add `Accept` header to GraphQL HTTP requests. This complies to the specification but doesn't go as far as sending `Content-Type` which would throw a lot of APIs off. Instead, we'll now be sending an accept header for `application/graphql+json, application/json` to indicate that we comply with the GraphQL over HTTP protocol. This also fixes headers merging to allow overriding `Accept` and `Content-Type` regardless of the user options' casing, by [@kitten](https://github.com/kitten) (See [#2457](https://github.com/FormidableLabs/urql/pull/2457)) ### Patch Changes - Support aborting in `withPromise` cases, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2446](https://github.com/FormidableLabs/urql/pull/2446)) - Passthrough responses with content type of `text/*` as error messages, by [@kitten](https://github.com/kitten) (See [#2456](https://github.com/FormidableLabs/urql/pull/2456)) ## 2.4.4 ### Patch Changes - cut off `url` when using the GET method at 2048 characters (lowest url-size coming from chromium), by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2384](https://github.com/FormidableLabs/urql/pull/2384)) - ⚠️ Fix issue where a synchronous `toPromise()` return would not result in the stream tearing down, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2386](https://github.com/FormidableLabs/urql/pull/2386)) ## 2.4.3 ### Patch Changes - Prevent ignored characters in GraphQL queries from being replaced inside strings and block strings. Previously we accepted sanitizing strings via regular expressions causing duplicate hashes as acceptable, since it'd only be caused when a string wasn't extracted into variables. This is fixed now however, by [@kitten](https://github.com/kitten) (See [#2295](https://github.com/FormidableLabs/urql/pull/2295)) ## 2.4.2 ### Patch Changes - Undo logic to catch errors from incremental fetching and forking the response stream, introduce logic to detect results, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2287](https://github.com/FormidableLabs/urql/pull/2287)) ## 2.4.1 ### Patch Changes - ⚠️ Fix mutation operation being used as compared identity and instead add a stand-in comparison, by [@kitten](https://github.com/kitten) (See [#2228](https://github.com/FormidableLabs/urql/pull/2228)) ## 2.4.0 ### Minor Changes - Allow for repeated mutations that have similar inputs which results in the same key, this is for instance the case with file uploads, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2189](https://github.com/FormidableLabs/urql/pull/2189)) ### Patch Changes - Bump `@graphql-typed-document-node/core` to 3.1.1 for `graphql@16` support, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2153](https://github.com/FormidableLabs/urql/pull/2153)) - ⚠️ Fix error bubbling, when an error happened in the exchange-pipeline we would treat it as a GraphQL-error, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2210](https://github.com/FormidableLabs/urql/pull/2210)) - Filter `network-only` requests from the `ssrExchange`, this is to enable `staleWhileRevalidated` queries to successfully dispatch their queries, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2198](https://github.com/FormidableLabs/urql/pull/2198)) ## 2.3.6 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) ## 2.3.5 ### Patch Changes - ⚠️ Fix issue where `maskTypename` would ignore array shapes, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2074](https://github.com/FormidableLabs/urql/pull/2074)) ## 2.3.4 ### Patch Changes - Prevent `Buffer` from being polyfilled by an automatic detection in Webpack. Instead of referencing the `Buffer` global we now simply check the constructor name, by [@kitten](https://github.com/kitten) (See [#2027](https://github.com/FormidableLabs/urql/pull/2027)) - ⚠️ Fix error-type of an `ExecutionResult` to line up with subscription-libs, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1998](https://github.com/FormidableLabs/urql/pull/1998)) ## 2.3.3 ### Patch Changes - Adding option to `ssrExchange` to include the `extensions` field of operation results in the cache, by [@dios-david](https://github.com/dios-david) (See [#1985](https://github.com/FormidableLabs/urql/pull/1985)) ## 2.3.2 ### Patch Changes - ⚠️ Fix issue where the ssr-exchange would loop due to checking network-only revalidations, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1944](https://github.com/FormidableLabs/urql/pull/1944)) ## 2.3.1 ### Patch Changes - ⚠️ Fix mark `query.__key` as non-enumerable so `formatDocument` does not restore previous invocations when cloning the gql-ast, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1870](https://github.com/FormidableLabs/urql/pull/1870)) - ⚠️ Fix: update toPromise to exclude `hasNext` results. This change ensures that when we call toPromise() on a query we wont serve an incomplete result, the user will expect to receive a non-stale full-result when using toPromise(), by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1880](https://github.com/FormidableLabs/urql/pull/1880)) ## 2.3.0 ### Minor Changes - Add **experimental** support for `@defer` and `@stream` responses for GraphQL. This implements the ["GraphQL Defer and Stream Directives"](https://github.com/graphql/graphql-spec/blob/4fd39e0/rfcs/DeferStream.md) and ["Incremental Delivery over HTTP"](https://github.com/graphql/graphql-over-http/blob/290b0e2/rfcs/IncrementalDelivery.md) specifications. If a GraphQL API supports `multipart/mixed` responses for deferred and streamed delivery of GraphQL results, `@urql/core` (and all its derived fetch implementations) will attempt to stream results. This is _only supported_ on browsers [supporting streamed fetch responses](https://developer.mozilla.org/en-US/docs/Web/API/Response/body), which excludes IE11. The implementation of streamed multipart responses is derived from [`meros` by `@maraisr`](https://github.com/maraisr/meros), and is subject to change if the RFCs end up changing, by [@kitten](https://github.com/kitten) (See [#1854](https://github.com/FormidableLabs/urql/pull/1854)) ## 2.2.0 ### Minor Changes - Add a `staleWhileRevalidate` option to the `ssrExchange`, which allows the client to immediately refetch a new result on hydration, which may be used for cached / stale SSR or SSG pages. This is different from using `cache-and-network` by default (which isn't recommended) as the `ssrExchange` typically acts like a "replacement fetch request", by [@kitten](https://github.com/kitten) (See [#1852](https://github.com/FormidableLabs/urql/pull/1852)) ### Patch Changes - ⚠️ Fix prevent mangling embedded strings in queries sent using the `GET` method, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1851](https://github.com/FormidableLabs/urql/pull/1851)) - The [single-source behavior previously](https://github.com/FormidableLabs/urql/pull/1515) wasn't effective for implementations like React, where the issue presents itself when the state of an operation is first polled. This led to the operation being torn down erroneously. We now ensure that operations started at the same time still use a shared single-source, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1850](https://github.com/FormidableLabs/urql/pull/1850)) ## 2.1.6 ### Patch Changes - Warn for invalid operation passed to query/subscription/mutation, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1829](https://github.com/FormidableLabs/urql/pull/1829)) ## 2.1.5 ### Patch Changes - Prevent `ssrExchange().restoreData()` from adding results to the exchange that have already been invalidated. This may happen when `restoreData()` is called repeatedly, e.g. per page. When a prior run has already invalidated an SSR result then the result is 'migrated' to the user's `cacheExchange`, which means that `restoreData()` should never attempt to re-add it again, by [@kitten](https://github.com/kitten) (See [#1776](https://github.com/FormidableLabs/urql/pull/1776)) - ⚠️ Fix accidental change in passive `stale: true`, where a `cache-first` operation issued by Graphcache wouldn't yield an affected query and update its result to reflect the loading state with `stale: true`. This is a regression from `v2.1.0` and mostly becomes unexpected when `cache.invalidate(...)` is used, by [@kitten](https://github.com/kitten) (See [#1755](https://github.com/FormidableLabs/urql/pull/1755)) ## 2.1.4 ### Patch Changes - Prevent stale results from being emitted by promisified query sources, e.g. `client.query(...).toPromise()` yielding a partial result with `stale: true` set. Instead, `.toPromise()` will now filter out stale results, by [@kitten](https://github.com/kitten) (See [#1709](https://github.com/FormidableLabs/urql/pull/1709)) ## 2.1.3 ### Patch Changes - Treat empty variables the same as no variables in `@urql/core`'s `createRequest`, by [@kitten](https://github.com/kitten) (See [#1695](https://github.com/FormidableLabs/urql/pull/1695)) ## 2.1.2 ### Patch Changes - ⚠️ Fix a condition under which the `Client` would hang when a query is started and consumed with `toPromise()`, by [@kitten](https://github.com/kitten) (See [#1634](https://github.com/FormidableLabs/urql/pull/1634)) - Refactor `Client` to hide some implementation details and to reduce size, by [@kitten](https://github.com/kitten) (See [#1638](https://github.com/FormidableLabs/urql/pull/1638)) ## 2.1.1 ### Patch Changes - ⚠️ Fix a regression in `@urql/core@2.1.1` that prevented concurrent operations from being dispatched with differing request policies, which for instance prevented the explicit `executeQuery` calls on bindings to fail, by [@kitten](https://github.com/kitten) (See [#1626](https://github.com/FormidableLabs/urql/pull/1626)) ## 2.1.0 ### Minor Changes - With the "single-source behavior" the `Client` will now also avoid executing an operation if it's already active, has a previous result available, and is either run with the `cache-first` or `cache-only` request policies. This is similar to a "short circuiting" behavior, where unnecessary work is avoided by not issuing more operations into the exchange pipeline than expected, by [@kitten](https://github.com/kitten) (See [#1600](https://github.com/FormidableLabs/urql/pull/1600)) - Add consistent "single-source behavior" which makes the `Client` more forgiving when duplicate sources are used, which previously made it difficult to use the same operation across an app together with `cache-and-network`; This was a rare use-case, and it isn't recommended to overfetch data across an app, however, the new `Client` implementation of shared sources ensures that when an operation is active that the `Client` distributes the last known result for the active operation to any new usages of it (which is called “replaying stale results”) (See [#1515](https://github.com/FormidableLabs/urql/pull/1515)) ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - ⚠️ Fix inconsistency in generating keys for `DocumentNode`s, especially when using GraphQL Code Generator, which could cause SSR serialization to fail, by [@zenflow](https://github.com/zenflow) (See [#1509](https://github.com/FormidableLabs/urql/pull/1509)) ## 2.0.0 ### Major Changes - **Breaking**: Remove `pollInterval` feature from `OperationContext`. Instead consider using a source that uses `Wonka.interval` and `Wonka.switchMap` over `client.query()`'s source, by [@kitten](https://github.com/kitten) (See [#1374](https://github.com/FormidableLabs/urql/pull/1374)) - Remove deprecated `operationName` property from `Operation`s. The new `Operation.kind` property is now preferred. If you're creating new operations you may also use the `makeOperation` utility instead. When upgrading `@urql/core` please ensure that your package manager didn't install any duplicates of it. You may deduplicate it manually using `npx yarn-deduplicate` (for Yarn) or `npm dedupe` (for npm), by [@kitten](https://github.com/kitten) (See [#1357](https://github.com/FormidableLabs/urql/pull/1357)) ### Minor Changes - Reemit an `OperationResult` as `stale: true` if it's being reexecuted as `network-only` operation to give bindings immediate feedback on background refetches, by [@kitten](https://github.com/kitten) (See [#1375](https://github.com/FormidableLabs/urql/pull/1375)) ## 1.16.2 ### Patch Changes - Add a workaround for `graphql-tag/loader`, which provides filtered query documents (where the original document contains multiple operations) without updating or providing a correct `document.loc.source.body` string, by [@kitten](https://github.com/kitten) (See [#1315](https://github.com/FormidableLabs/urql/pull/1315)) ## 1.16.1 ### Patch Changes - Add fragment deduplication to `gql` tag. Identical fragments can now be interpolated multiple times without a warning being triggered or them being duplicated in `gql`'s output, by [@kitten](https://github.com/kitten) (See [#1225](https://github.com/FormidableLabs/urql/pull/1225)) ## 1.16.0 ### Minor Changes - Add a built-in `gql` tag function helper to `@urql/core`. This behaves similarly to `graphql-tag` but only warns about _locally_ duplicated fragment names rather than globally. It also primes `@urql/core`'s key cache with the parsed `DocumentNode`, by [@kitten](https://github.com/kitten) (See [#1187](https://github.com/FormidableLabs/urql/pull/1187)) ### Patch Changes - ⚠️ Fix edge case in `formatDocument`, which fails to add a `__typename` field if it has been aliased to a different name, by [@kitten](https://github.com/kitten) (See [#1186](https://github.com/FormidableLabs/urql/pull/1186)) - Cache results of `formatDocument` by the input document's key, by [@kitten](https://github.com/kitten) (See [#1186](https://github.com/FormidableLabs/urql/pull/1186)) ## 1.15.2 ### Patch Changes - Don't add `undefined` to any property of the `ssrExchange`'s serialized results, as this would crash in Next.js, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1168](https://github.com/FormidableLabs/urql/pull/1168)) ## 1.15.1 ### Patch Changes - Export `getOperationName` from `@urql/core` and use it in `@urql/exchange-execute`, fixing several imports, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1135](https://github.com/FormidableLabs/urql/pull/1135)) ## 1.15.0 ### Minor Changes - Improve the Suspense implementation, which fixes edge-cases when Suspense is used with subscriptions, partially disabled, or _used on the client-side_. It has now been ensured that client-side suspense functions without the deprecated `suspenseExchange` and uncached results are loaded consistently. As part of this work, the `Client` itself does now never throw Suspense promises anymore, which is functionality that either way has no place outside of the React/Preact bindings, by [@kitten](https://github.com/kitten) (See [#1123](https://github.com/FormidableLabs/urql/pull/1123)) ### Patch Changes - Use `Record` over `object` type for subscription operation variables. The `object` type is currently hard to use ([see this issue](https://github.com/microsoft/TypeScript/issues/21732)), by [@enisdenjo](https://github.com/enisdenjo) (See [#1119](https://github.com/FormidableLabs/urql/pull/1119)) - Add support for `TypedDocumentNode` to infer the type of the `OperationResult` and `Operation` for all methods, functions, and hooks that either directly or indirectly accept a `DocumentNode`. See [`graphql-typed-document-node` and the corresponding blog post for more information.](https://github.com/dotansimha/graphql-typed-document-node), by [@kitten](https://github.com/kitten) (See [#1113](https://github.com/FormidableLabs/urql/pull/1113)) - Refactor `useSource` hooks which powers `useQuery` and `useSubscription` to improve various edge case behaviour. This will not change the behaviour of these hooks dramatically but avoid unnecessary state updates when any updates are obviously equivalent and the hook will furthermore improve continuation from mount to effects, which will fix cases where the state between the mounting and effect phase may slightly change, by [@kitten](https://github.com/kitten) (See [#1104](https://github.com/FormidableLabs/urql/pull/1104)) ## 1.14.1 ### Patch Changes - ⚠️ Fix the production build overwriting the development build. Specifically in the previous release we mistakenly replaced all development bundles with production bundles. This doesn't have any direct influence on how these packages work, but prevented development warnings from being logged or full errors from being thrown, by [@kitten](https://github.com/kitten) (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) ## 1.14.0 This version of `@urql/core` renames `Operation.operationName` to `Operation.kind`. For now the old property is merely deprecated and will issue a warning if it's used directly. That said, all exchanges that are released today also need this new version of `@urql/core@>=1.14.0`, so if you upgrade to any of the following packages, you will also need to upgrade `@urql/core`. If you upgrade and see the deprecation warning, check whether all following exchanges have been upgraded: - `@urql/exchange-auth@0.1.2` - `@urql/exchange-execute@1.0.2` - `@urql/exchange-graphcache@3.1.8` - `@urql/exchange-multipart-fetch@0.1.10` - `@urql/exchange-persisted-fetch@1.2.2` - `@urql/exchange-populate@0.2.1` - `@urql/exchange-refocus@0.2.1` - `@urql/exchange-retry@0.1.9` - `@urql/exchange-suspense@1.9.2` Once you've upgraded `@urql/core` please also ensure that your package manager hasn't accidentally duplicated the `@urql/core` package. If you're using `npm` you can do so by running `npm dedupe`, and if you use `yarn` you can do so by running `yarn-deduplicate`. If you have a custom exchange, you can mute the deprecation warning by using `Operation.kind` rather than `Operation.operationName`. If these exchanges are copying or altering operations by spreading them this will also trigger the warning, which you can fix by using [the new `makeOperation` helper function.](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) ### Minor Changes - Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was previously confusing as `operationName` was effectively referring to two different things. You can safely upgrade to this new version, however to mute all deprecation warnings you will have to **upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please use [the new `makeOperation` helper function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead, by [@bkonkle](https://github.com/bkonkle) (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) ### Patch Changes - Add missing `.mjs` extension to all imports from `graphql` to fix Webpack 5 builds, which require extension-specific import paths for ESM bundles and packages. **This change allows you to safely upgrade to Webpack 5.**, by [@kitten](https://github.com/kitten) (See [#1094](https://github.com/FormidableLabs/urql/pull/1094)) ## 1.13.1 ### Patch Changes - Allow `client.reexecuteOperation` to be called with mutations which skip the active operation minimums, by [@kitten](https://github.com/kitten) (See [#1011](https://github.com/FormidableLabs/urql/pull/1011)) ## 1.13.0 Please note that this release changes the data structure of the `ssrExchange`'s output. We don't treat this as a breaking change, since this data is considered a private structure, but if your tests or other code relies on this, please check the type changes and update it. ### Minor Changes - Adds an error exchange to urql-core. This allows tapping into all graphql errors within the urql client. Useful for logging, debugging, handling authentication errors etc, by [@kadikraman](https://github.com/kadikraman) (See [#947](https://github.com/FormidableLabs/urql/pull/947)) ### Patch Changes - ⚠️ Fix condition where mutated result data would be picked up by the `ssrExchange`, for instance as a result of mutations by Graphcache. Instead the `ssrExchange` now serializes data early, by [@kitten](https://github.com/kitten) (See [#962](https://github.com/FormidableLabs/urql/pull/962)) - Omit the `Content-Type: application/json` HTTP header when using GET in the `fetchExchange`, `persistedFetchExchange`, or `multipartFetchExchange`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#957](https://github.com/FormidableLabs/urql/pull/957)) ## 1.12.3 ### Patch Changes - Remove whitespace and comments from string-queries, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#911](https://github.com/FormidableLabs/urql/pull/911)) - Remove redundant whitespaces when using GET for graphql queries, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#908](https://github.com/FormidableLabs/urql/pull/908)) ## 1.12.2 ### Patch Changes - ⚠️ Fix `formatDocument` mutating parts of the `DocumentNode` which may be shared by other documents and queries. Also ensure that a formatted document will always generate the same key in `createRequest` as the original document, by [@kitten](https://github.com/kitten) (See [#880](https://github.com/FormidableLabs/urql/pull/880)) - ⚠️ Fix `ssrExchange` invalidating results on the client-side too eagerly, by delaying invalidation by a tick, by [@kitten](https://github.com/kitten) (See [#885](https://github.com/FormidableLabs/urql/pull/885)) ## 1.12.1 ### Patch Changes - ⚠️ Fix timing for out-of-band `client.reexecuteOperation` calls. This would surface in asynchronous caching scenarios, where no result would be delivered by the cache synchronously, while it still calls `client.reexecuteOperation` for e.g. a `network-only` request, which happens for `cache-and-network`. This issue becomes especially obvious in highly synchronous frameworks like Svelte, by [@kitten](https://github.com/kitten) (See [#860](https://github.com/FormidableLabs/urql/pull/860)) - Replace unnecessary `scheduleTask` polyfill with inline `Promise.resolve().then(fn)` calls, by [@kitten](https://github.com/kitten) (See [#861](https://github.com/FormidableLabs/urql/pull/861)) ## 1.12.0 As always, please ensure that you deduplicate `@urql/core` when upgrading. Additionally deduplicating the versions of `wonka` that you have installed may also reduce your bundlesize. ### Minor Changes - Expose a `client.subscription` shortcut method, similar to `client.query` and `client.mutation`, by [@FredyC](https://github.com/FredyC) (See [#838](https://github.com/FormidableLabs/urql/pull/838)) ### Patch Changes - Upgrade to a minimum version of wonka@^4.0.14 to work around issues with React Native's minification builds, which use uglify-es and could lead to broken bundles, by [@kitten](https://github.com/kitten) (See [#842](https://github.com/FormidableLabs/urql/pull/842)) ## 1.11.8 ### Patch Changes - Add operationName to GET queries, by [@jakubriedl](https://github.com/jakubriedl) (See [#798](https://github.com/FormidableLabs/urql/pull/798)) ## 1.11.7 ### Patch Changes - Add `source` debug name to all `dispatchDebug` calls during build time to identify events by which exchange dispatched them, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#780](https://github.com/FormidableLabs/urql/pull/780)) ## 1.11.6 ### Patch Changes - Add a `"./package.json"` entry to the `package.json`'s `"exports"` field for Node 14. This seems to be required by packages like `rollup-plugin-svelte` to function properly, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#771](https://github.com/FormidableLabs/urql/pull/771)) ## 1.11.5 ### Patch Changes - Hoist variables in unminified build output for Metro Bundler builds which otherwise fails for `process.env.NODE_ENV` if-clauses, by [@kitten](https://github.com/kitten) (See [#737](https://github.com/FormidableLabs/urql/pull/737)) - Add a babel-plugin that removes empty imports from the final build output, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#735](https://github.com/FormidableLabs/urql/pull/735)) ## 1.11.4 ### Patch Changes Sorry for the many updates; Please only upgrade to `>=1.11.4` and don't use the deprecated `1.11.3` and `1.11.2` release. - ⚠️ Fix nested package path for @urql/core/internal and @urql/exchange-graphcache/extras, by [@kitten](https://github.com/kitten) (See [#734](https://github.com/FormidableLabs/urql/pull/734)) ## 1.11.3 ### Patch Changes - Make the extension of the main export unknown, which fixes a Webpack issue where the resolver won't pick `module` fields in `package.json` files once it's importing from another `.mjs` file, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#733](https://github.com/FormidableLabs/urql/pull/733)) ## 1.11.1 ### Patch Changes - ⚠️ Fix missing `@urql/core/internal` entrypoint in the npm-release, which was previously not included, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#731](https://github.com/FormidableLabs/urql/pull/731)) ## 1.11.0 ### Minor Changes - Add debugging events to exchanges that add more detailed information on what is happening internally, which will be displayed by devtools like the urql [Chrome / Firefox extension](https://github.com/FormidableLabs/urql-devtools), by [@andyrichardson](https://github.com/andyrichardson) (See [#608](https://github.com/FormidableLabs/urql/pull/608)) - Add @urql/core/internal entrypoint for internally shared utilities and start sharing fetchExchange-related code, by [@kitten](https://github.com/kitten) (See [#722](https://github.com/FormidableLabs/urql/pull/722)) ### Patch Changes - ⚠️ Fix stringifyVariables breaking on x.toJSON scalars, by [@kitten](https://github.com/kitten) (See [#718](https://github.com/FormidableLabs/urql/pull/718)) ## 1.10.9 ### Patch Changes - Pick modules from graphql package, instead of importing from graphql/index.mjs, by [@kitten](https://github.com/kitten) (See [#700](https://github.com/FormidableLabs/urql/pull/700)) ## 1.10.8 ### Patch Changes - Add graphql@^15.0.0 to peer dependency range, by [@kitten](https://github.com/kitten) (See [#688](https://github.com/FormidableLabs/urql/pull/688)) - ⚠️ Fix non-2xx results never being parsed as GraphQL results. This can result in valid GraphQLErrors being hidden, which should take precedence over generic HTTP NetworkErrors, by [@kitten](https://github.com/kitten) (See [#678](https://github.com/FormidableLabs/urql/pull/678)) ## 1.10.7 ### Patch Changes - ⚠️ Fix oversight in edge case for #662. The operation queue wasn't marked as being active which caused `stale` results and `cache-and-network` operations from reissuing operations immediately (unqueued essentially) which would then be filtered out by the `dedupExchange`, by [@kitten](https://github.com/kitten) (See [#669](https://github.com/FormidableLabs/urql/pull/669)) ## 1.10.6 ### Patch Changes - ⚠️ Fix critical bug in operation queueing that can lead to unexpected teardowns and swallowed operations. This would happen when a teardown operation kicks off the queue, by [@kitten](https://github.com/kitten) (See [#662](https://github.com/FormidableLabs/urql/pull/662)) ## 1.10.5 ### Patch Changes - Refactor a couple of core helpers for minor bundlesize savings, by [@kitten](https://github.com/kitten) (See [#658](https://github.com/FormidableLabs/urql/pull/658)) - Add support for variables that contain non-plain objects without any enumerable keys, e.g. `File` or `Blob`. In this case `stringifyVariables` will now use a stable (but random) key, which means that mutations containing `File`s — or other objects like this — will now be distinct, as they should be, by [@kitten](https://github.com/kitten) (See [#650](https://github.com/FormidableLabs/urql/pull/650)) ## 1.10.4 ### Patch Changes - ⚠️ Fix node resolution when using Webpack, which experiences a bug where it only resolves `package.json:main` instead of `module` when an `.mjs` file imports a package, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#642](https://github.com/FormidableLabs/urql/pull/642)) ## 1.10.3 ### Patch Changes - ⚠️ Fix Node.js Module support for v13 (experimental-modules) and v14. If your bundler doesn't support `.mjs` files and fails to resolve the new version, please double check your configuration for Webpack, or similar tools, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#637](https://github.com/FormidableLabs/urql/pull/637)) ## 1.10.2 ### Patch Changes - Add a guard to "maskTypenames" so a null value isn't considered an object, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#621](https://github.com/FormidableLabs/urql/pull/621)) ## 1.10.1 ### Patch Changes - ⚠️ Fix Rollup bundle output being written to .es.js instead of .esm.js, by [@kitten](https://github.com/kitten) (See [#609](https://github.com/FormidableLabs/urql/pull/609)) ## 1.10.0 ### Minor Changes - Add `additionalTypenames` to the `OperationContext`, which allows the document cache to invalidate efficiently when the `__typename` is unknown at the initial fetch, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#601](https://github.com/FormidableLabs/urql/pull/601)) [You can learn more about this change in our docs.](https://formidable.com/open-source/urql/docs/basics/document-caching/#adding-typenames) ### Patch Changes - Add missing GraphQLError serialization for extensions and path field to ssrExchange, by [@kitten](https://github.com/kitten) (See [#607](https://github.com/FormidableLabs/urql/pull/607)) ## 1.9.2 ### Patch Changes - Prevent active teardowns for queries on subscriptionExchange, by [@kitten](https://github.com/kitten) (See [#577](https://github.com/FormidableLabs/urql/pull/577)) ## 1.9.1 ### Patch Changes - ⚠️ Fix `cache-only` operations being forwarded and triggering fetch requests, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#551](https://github.com/FormidableLabs/urql/pull/551)) - Adds a one-tick delay to the subscriptionExchange to prevent unnecessary early tear downs, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#542](https://github.com/FormidableLabs/urql/pull/542)) - Add enableAllOperations option to subscriptionExchange to let it handle queries and mutations as well, by [@kitten](https://github.com/kitten) (See [#544](https://github.com/FormidableLabs/urql/pull/544)) ## 1.9.0 ### Minor Changes - Adds the `maskTypename` export to urql-core, this deeply masks typenames from the given payload. Masking `__typename` properties is also available as a `maskTypename` option on the `Client`. Setting this to true will strip typenames from results, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#533](https://github.com/FormidableLabs/urql/pull/533)) - Add support for sending queries using GET instead of POST method (See [#519](https://github.com/FormidableLabs/urql/pull/519)) - Add client.readQuery method (See [#518](https://github.com/FormidableLabs/urql/pull/518)) ### Patch Changes - ⚠️ Fix ssrExchange not serialising networkError on CombinedErrors correctly. (See [#515](https://github.com/FormidableLabs/urql/pull/515)) - Add explicit error when creating Client without a URL in development. (See [#512](https://github.com/FormidableLabs/urql/pull/512)) ================================================ FILE: packages/core/README.md ================================================ # @urql/core > The shared core for the highly customizable and versatile GraphQL client, urql More documentation is available at [formidable.com/open-source/urql](https://formidable.com/open-source/urql/). ================================================ FILE: packages/core/jsr.json ================================================ { "name": "@urql/core", "version": "6.0.1", "exports": { ".": "./src/index.ts", "./internal": "./src/internal/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/core/package.json ================================================ { "name": "@urql/core", "version": "6.0.1", "description": "The shared core for the highly customizable and versatile GraphQL client", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/core" }, "keywords": [ "graphql", "graphql client", "state management", "cache", "exchanges" ], "main": "dist/urql-core", "module": "dist/urql-core.mjs", "types": "dist/urql-core.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-core.d.ts", "import": "./dist/urql-core.mjs", "require": "./dist/urql-core.js", "source": "./src/index.ts" }, "./package.json": "./package.json", "./internal": { "types": "./dist/urql-core-internal.d.ts", "import": "./dist/urql-core-internal.mjs", "require": "./dist/urql-core-internal.js", "source": "./src/internal/index.ts" } }, "files": [ "LICENSE", "README.md", "dist/", "internal/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build", "jsr": "jsr publish" }, "dependencies": { "@0no-co/graphql.web": "^1.0.13", "wonka": "^6.3.2" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: packages/core/src/__snapshots__/client.test.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`createClient / Client > passes snapshot 1`] = ` Client2 { "createRequestOperation": [Function], "executeMutation": [Function], "executeQuery": [Function], "executeRequestOperation": [Function], "executeSubscription": [Function], "mutation": [Function], "operations$": [Function], "query": [Function], "readQuery": [Function], "reexecuteOperation": [Function], "subscribeToDebugTarget": [Function], "subscription": [Function], "suspense": false, } `; ================================================ FILE: packages/core/src/client.test.ts ================================================ import { print } from '@0no-co/graphql.web'; import { vi, expect, it, beforeEach, describe, afterEach } from 'vitest'; /** NOTE: Testing in this file is designed to test both the client and its interaction with default Exchanges */ import { Source, delay, map, never, pipe, merge, subscribe, publish, filter, share, toArray, toPromise, onPush, tap, take, fromPromise, fromValue, mergeMap, } from 'wonka'; import { gql } from './gql'; import { Exchange, Operation, OperationResult } from './types'; import { makeOperation } from './utils'; import { Client, createClient } from './client'; import { mutationOperation, queryOperation, subscriptionOperation, } from './test-utils'; const url = 'https://hostname.com'; describe('createClient / Client', () => { it('creates an instance of Client', () => { expect(createClient({ url, exchanges: [] }) instanceof Client).toBeTruthy(); expect(new Client({ url, exchanges: [] }) instanceof Client).toBeTruthy(); }); it('passes snapshot', () => { const client = createClient({ url, exchanges: [] }); expect(client).toMatchSnapshot(); }); }); const query = { key: 1, query: gql` { todos { id } } `, variables: { example: 1234 }, }; const mutation = { key: 1, query: gql` mutation { todos { id } } `, variables: { example: 1234 }, }; const subscription = { key: 1, query: gql` subscription { todos { id } } `, variables: { example: 1234 }, }; let receivedOps: Operation[] = []; let client = createClient({ url: '1234', exchanges: [], }); const receiveMock = vi.fn((s: Source) => pipe( s, tap(op => (receivedOps = [...receivedOps, op])), map(op => ({ operation: op })) ) ); const exchangeMock = vi.fn(() => receiveMock); beforeEach(() => { receivedOps = []; exchangeMock.mockClear(); receiveMock.mockClear(); client = createClient({ url, exchanges: [exchangeMock] as any[], requestPolicy: 'cache-and-network', }); }); describe('exchange args', () => { it('receives forward function', () => { // @ts-ignore expect(typeof exchangeMock.mock.calls[0][0].forward).toBe('function'); }); it('receives client', () => { // @ts-ignore expect(exchangeMock.mock.calls[0][0]).toHaveProperty('client', client); }); }); describe('promisified methods', () => { it('query', () => { const queryResult = client .query( gql` { todos { id } } `, { example: 1234 }, { requestPolicy: 'cache-only' } ) .toPromise(); const received = receivedOps[0]; expect(print(received.query)).toEqual(print(query.query)); expect(received.key).toBeDefined(); expect(received.variables).toEqual({ example: 1234 }); expect(received.kind).toEqual('query'); expect(received.context).toEqual({ url: 'https://hostname.com', requestPolicy: 'cache-only', fetchOptions: undefined, fetch: undefined, suspense: false, preferGetMethod: 'within-url-limit', }); expect(queryResult).toHaveProperty('then'); }); it('mutation', () => { const mut = gql` mutation { todos { id } } `; const mutationResult = client.mutation(mut, { example: 1234 }).toPromise(); const received = receivedOps[0]; expect(print(received.query)).toEqual(print(mut)); expect(received.key).toBeDefined(); expect(received.variables).toEqual({ example: 1234 }); expect(received.kind).toEqual('mutation'); expect(received.context).toMatchObject({ url: 'https://hostname.com', requestPolicy: 'cache-and-network', fetchOptions: undefined, fetch: undefined, suspense: false, preferGetMethod: 'within-url-limit', }); expect(mutationResult).toHaveProperty('then'); }); }); describe('synchronous methods', () => { it('readQuery', () => { const result = client.readQuery( gql` { todos { id } } `, { example: 1234 } ); expect(receivedOps.length).toBe(2); expect(receivedOps[0].kind).toBe('query'); expect(receivedOps[1].kind).toBe('teardown'); expect(result).toEqual({ operation: { ...query, context: expect.anything(), key: expect.any(Number), kind: 'query', }, }); }); }); describe('executeQuery', () => { it('passes query string exchange', () => { pipe( client.executeQuery(query), subscribe(x => x) ); const receivedQuery = receivedOps[0].query; expect(print(receivedQuery)).toBe(print(query.query)); }); it('should throw when passing in a mutation', () => { try { client.executeQuery(mutation); expect(true).toBeFalsy(); } catch (e: any) { expect(e.message).toMatchInlineSnapshot( `"Expected operation of type "query" but found "mutation""` ); } }); it('passes variables type to exchange', () => { pipe( client.executeQuery(query), subscribe(x => x) ); expect(receivedOps[0]).toHaveProperty('variables', query.variables); }); it('passes requestPolicy to exchange', () => { pipe( client.executeQuery(query), subscribe(x => x) ); expect(receivedOps[0].context).toHaveProperty( 'requestPolicy', 'cache-and-network' ); }); it('allows overriding the requestPolicy', () => { pipe( client.executeQuery(query, { requestPolicy: 'cache-first' }), subscribe(x => x) ); expect(receivedOps[0].context).toHaveProperty( 'requestPolicy', 'cache-first' ); }); it('passes kind type to exchange', () => { pipe( client.executeQuery(query), subscribe(x => x) ); expect(receivedOps[0]).toHaveProperty('kind', 'query'); }); it('passes url (from context) to exchange', () => { pipe( client.executeQuery(query), subscribe(x => x) ); expect(receivedOps[0]).toHaveProperty('context.url', url); }); }); describe('executeMutation', () => { it('passes query string exchange', async () => { pipe( client.executeMutation(mutation), subscribe(x => x) ); const receivedQuery = receivedOps[0].query; expect(print(receivedQuery)).toBe(print(mutation.query)); }); it('passes variables type to exchange', () => { pipe( client.executeMutation(mutation), subscribe(x => x) ); expect(receivedOps[0]).toHaveProperty('variables', query.variables); }); it('passes kind type to exchange', () => { pipe( client.executeMutation(mutation), subscribe(x => x) ); expect(receivedOps[0]).toHaveProperty('kind', 'mutation'); }); it('passes url (from context) to exchange', () => { pipe( client.executeMutation(mutation), subscribe(x => x) ); expect(receivedOps[0]).toHaveProperty('context.url', url); }); }); describe('executeSubscription', () => { it('passes query string exchange', async () => { pipe( client.executeSubscription(subscription), subscribe(x => x) ); const receivedQuery = receivedOps[0].query; expect(print(receivedQuery)).toBe(print(subscription.query)); }); it('passes variables type to exchange', () => { pipe( client.executeSubscription(subscription), subscribe(x => x) ); expect(receivedOps[0]).toHaveProperty('variables', subscription.variables); }); it('passes kind type to exchange', () => { pipe( client.executeSubscription(subscription), subscribe(x => x) ); expect(receivedOps[0]).toHaveProperty('kind', 'subscription'); }); }); describe('queuing behavior', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('queues reexecuteOperation, which dispatchOperation consumes', () => { const output: Array = []; const exchange: Exchange = ({ client }) => ops$ => { return pipe( ops$, filter(op => op.kind !== 'teardown'), tap(op => { output.push(op); if ( op.key === queryOperation.key && op.context.requestPolicy !== 'network-only' ) { client.reexecuteOperation({ ...op, context: { ...op.context, requestPolicy: 'network-only', }, }); } }), map(op => ({ stale: false, hasNext: false, data: op.key, operation: op, })) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const shared = pipe( client.executeRequestOperation(queryOperation), onPush(result => output.push(result)), share ); const results = pipe(shared, toArray); pipe(shared, publish); expect(output.length).toBe(8); expect(results.length).toBe(2); expect(output[0]).toHaveProperty('key', queryOperation.key); expect(output[0]).toHaveProperty('context.requestPolicy', 'cache-first'); expect(output[1]).toHaveProperty('operation.key', queryOperation.key); expect(output[1]).toHaveProperty( 'operation.context.requestPolicy', 'cache-first' ); expect(output[2]).toHaveProperty('key', queryOperation.key); expect(output[2]).toHaveProperty('context.requestPolicy', 'network-only'); expect(output[3]).toHaveProperty('operation.key', queryOperation.key); expect(output[3]).toHaveProperty( 'operation.context.requestPolicy', 'network-only' ); expect(output[1]).toBe(results[0]); expect(output[3]).toBe(results[1]); }); it('reemits previous results as stale if the operation is reexecuted as network-only', async () => { const output: OperationResult[] = []; const exchange: Exchange = () => { let countRes = 0; return ops$ => { return pipe( ops$, filter(op => op.kind !== 'teardown'), map(op => ({ hasNext: false, stale: false, data: ++countRes, operation: op, })), delay(1) ); }; }; const client = createClient({ url: 'test', exchanges: [exchange], }); const { unsubscribe } = pipe( client.executeRequestOperation(queryOperation), subscribe(result => { output.push(result); }) ); vi.advanceTimersByTime(1); expect(output.length).toBe(1); expect(output[0]).toHaveProperty('data', 1); expect(output[0]).toHaveProperty('operation.key', queryOperation.key); expect(output[0]).toHaveProperty( 'operation.context.requestPolicy', 'cache-first' ); client.reexecuteOperation( makeOperation(queryOperation.kind, queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }) ); await Promise.resolve(); expect(output.length).toBe(2); expect(output[1]).toHaveProperty('data', 1); expect(output[1]).toHaveProperty('stale', true); expect(output[1]).toHaveProperty('operation.key', queryOperation.key); expect(output[1]).toHaveProperty( 'operation.context.requestPolicy', 'cache-first' ); vi.advanceTimersByTime(1); expect(output.length).toBe(3); expect(output[2]).toHaveProperty('data', 2); expect(output[2]).toHaveProperty('stale', false); expect(output[2]).toHaveProperty('operation.key', queryOperation.key); expect(output[2]).toHaveProperty( 'operation.context.requestPolicy', 'network-only' ); unsubscribe(); }); }); describe('deduplication behavior', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('deduplicates operations when no result has been sent yet', () => { const onOperation = vi.fn(); const exchange: Exchange = () => ops$ => { let i = 0; return pipe( ops$, onPush(onOperation), map(op => ({ hasNext: false, stale: false, data: ++i, operation: op, })), delay(1) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const resultOne = vi.fn(); const resultTwo = vi.fn(); const operationOne = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'cache-first', }); const operationTwo = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); pipe(client.executeRequestOperation(operationOne), subscribe(resultOne)); pipe(client.executeRequestOperation(operationTwo), subscribe(resultTwo)); expect(resultOne).toHaveBeenCalledTimes(0); expect(resultTwo).toHaveBeenCalledTimes(0); vi.advanceTimersByTime(1); expect(resultOne).toHaveBeenCalledTimes(1); expect(resultTwo).toHaveBeenCalledTimes(1); expect(onOperation).toHaveBeenCalledTimes(1); }); it('deduplicates operations when hasNext: true is set', () => { const onOperation = vi.fn(); const exchange: Exchange = () => ops$ => { let i = 0; return pipe( ops$, onPush(onOperation), map(op => ({ hasNext: true, stale: false, data: ++i, operation: op, })) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const resultOne = vi.fn(); const resultTwo = vi.fn(); const operationOne = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'cache-first', }); const operationTwo = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); pipe(client.executeRequestOperation(operationOne), subscribe(resultOne)); pipe(client.executeRequestOperation(operationTwo), subscribe(resultTwo)); expect(resultOne).toHaveBeenCalledTimes(1); expect(resultTwo).toHaveBeenCalledTimes(1); vi.advanceTimersByTime(1); expect(resultOne).toHaveBeenCalledTimes(1); expect(resultTwo).toHaveBeenCalledTimes(1); expect(onOperation).toHaveBeenCalledTimes(1); }); it('deduplicates otherwise if operation has already been sent', () => { const onOperation = vi.fn(); const onResult = vi.fn(); let hasSent = false; const exchange: Exchange = () => ops$ => pipe( ops$, onPush(onOperation), map(op => ({ hasNext: false, stale: false, data: 'test', operation: op, })), filter(() => { return hasSent ? false : (hasSent = true); }), delay(1) ); const client = createClient({ url: 'test', exchanges: [exchange], }); const operationOne = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'cache-first', }); const operationTwo = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); const operationThree = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); pipe(client.executeRequestOperation(operationOne), subscribe(onResult)); pipe(client.executeRequestOperation(operationTwo), subscribe(onResult)); pipe(client.executeRequestOperation(operationThree), subscribe(onResult)); vi.advanceTimersByTime(1); expect(onOperation).toHaveBeenCalledTimes(1); expect(onResult).toHaveBeenCalledTimes(3); }); it('does not deduplicate cache-and-network’s follow-up operations', () => { const onOperation = vi.fn(); const onResult = vi.fn(); const operationOne = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'cache-and-network', }); const operationTwo = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); let shouldSend = true; const exchange: Exchange = () => ops$ => pipe( ops$, onPush(onOperation), map(op => ({ hasNext: false, stale: true, data: 'test', operation: op, })), filter(() => { if (shouldSend) { shouldSend = false; client.reexecuteOperation(operationTwo); return true; } else { return false; } }) ); const client = createClient({ url: 'test', exchanges: [exchange], }); const operationThree = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); pipe(client.executeRequestOperation(operationOne), subscribe(onResult)); pipe(client.executeRequestOperation(operationThree), subscribe(onResult)); expect(onOperation).toHaveBeenCalledTimes(2); }); it('unblocks mutation operations on call to reexecuteOperation', async () => { const onOperation = vi.fn(); const onResult = vi.fn(); let hasSent = false; const exchange: Exchange = () => ops$ => pipe( ops$, onPush(onOperation), map(op => ({ hasNext: false, stale: false, data: 'test', operation: op, })), filter(() => hasSent || !(hasSent = true)) ); const client = createClient({ url: 'test', exchanges: [exchange], }); const operation = makeOperation('mutation', mutationOperation, { ...mutationOperation.context, requestPolicy: 'cache-first', }); pipe(client.executeRequestOperation(operation), subscribe(onResult)); expect(onOperation).toHaveBeenCalledTimes(1); expect(onResult).toHaveBeenCalledTimes(0); client.reexecuteOperation(operation); await Promise.resolve(); expect(onOperation).toHaveBeenCalledTimes(2); expect(onResult).toHaveBeenCalledTimes(1); }); // See https://github.com/urql-graphql/urql/issues/3254 it('unblocks stale operations', async () => { const onOperation = vi.fn(); const onResult = vi.fn(); let sends = 0; const exchange: Exchange = () => ops$ => pipe( ops$, onPush(onOperation), map(op => ({ hasNext: false, stale: sends++ ? false : true, data: 'test', operation: op, })) ); const client = createClient({ url: 'test', exchanges: [exchange], }); const operation = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'cache-first', }); pipe(client.executeRequestOperation(operation), subscribe(onResult)); expect(onOperation).toHaveBeenCalledTimes(1); expect(onResult).toHaveBeenCalledTimes(1); client.reexecuteOperation(operation); await Promise.resolve(); expect(onOperation).toHaveBeenCalledTimes(2); expect(onResult).toHaveBeenCalledTimes(2); }); // See https://github.com/urql-graphql/urql/issues/3565 it('blocks reexecuting operations that are in-flight', async () => { const onOperation = vi.fn(); const onResult = vi.fn(); let resolve; const exchange: Exchange = ({ client }) => ops$ => pipe( ops$, onPush(onOperation), mergeMap(op => { if (op.key === queryOperation.key) { const promise = new Promise(res => { resolve = res; }); return fromPromise( promise.then(() => { return { hasNext: false, stale: false, data: 'test', operation: op, }; }) ); } else { client.reexecuteOperation(queryOperation); return fromValue({ hasNext: false, stale: false, data: 'test', operation: op, }); } }) ); const client = createClient({ url: 'test', exchanges: [exchange], }); const operation = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'cache-first', }); const mutation = makeOperation('mutation', mutationOperation, { ...mutationOperation.context, requestPolicy: 'cache-first', }); pipe(client.executeRequestOperation(operation), subscribe(onResult)); expect(onOperation).toHaveBeenCalledTimes(1); expect(onResult).toHaveBeenCalledTimes(0); pipe(client.executeRequestOperation(mutation), subscribe(onResult)); await Promise.resolve(); expect(onOperation).toHaveBeenCalledTimes(2); expect(onResult).toHaveBeenCalledTimes(1); resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); expect(onOperation).toHaveBeenCalledTimes(2); expect(onResult).toHaveBeenCalledTimes(2); }); }); describe('shared sources behavior', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('replays results from prior operation result as needed (cache-first)', async () => { const exchange: Exchange = () => ops$ => { let i = 0; return pipe( ops$, map(op => ({ hasNext: false, stale: false, data: ++i, operation: op, })), delay(1) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const resultOne = vi.fn(); const resultTwo = vi.fn(); pipe(client.executeRequestOperation(queryOperation), subscribe(resultOne)); expect(resultOne).toHaveBeenCalledTimes(0); vi.advanceTimersByTime(1); expect(resultOne).toHaveBeenCalledTimes(1); expect(resultOne).toHaveBeenCalledWith({ data: 1, operation: queryOperation, stale: false, hasNext: false, }); pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo)); expect(resultTwo).toHaveBeenCalledWith({ data: 1, operation: queryOperation, stale: true, hasNext: false, }); vi.advanceTimersByTime(1); // With cache-first we don't expect a new operation to be issued expect(resultTwo).toHaveBeenCalledTimes(2); }); it('dispatches the correct request policy on subsequent sources', async () => { const exchange: Exchange = () => ops$ => { let i = 0; return pipe( ops$, map(op => ({ hasNext: false, stale: false, data: ++i, operation: op, })), delay(1) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const resultOne = vi.fn(); const resultTwo = vi.fn(); const operationOne = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'cache-first', }); const operationTwo = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); pipe(client.executeRequestOperation(operationOne), subscribe(resultOne)); expect(resultOne).toHaveBeenCalledTimes(0); vi.advanceTimersByTime(1); expect(resultOne).toHaveBeenCalledTimes(1); expect(resultOne).toHaveBeenCalledWith({ data: 1, operation: operationOne, hasNext: false, stale: false, }); pipe(client.executeRequestOperation(operationTwo), subscribe(resultTwo)); expect(resultTwo).toHaveBeenCalledWith({ data: 1, operation: operationOne, stale: true, hasNext: false, }); vi.advanceTimersByTime(1); expect(resultTwo).toHaveBeenCalledWith({ data: 2, operation: operationTwo, stale: false, hasNext: false, }); }); it('replays results from prior operation result as needed (network-only)', async () => { const exchange: Exchange = () => ops$ => { let i = 0; return pipe( ops$, map(op => ({ hasNext: false, stale: false, data: ++i, operation: op, })), delay(1) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const operation = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); const resultOne = vi.fn(); const resultTwo = vi.fn(); pipe(client.executeRequestOperation(operation), subscribe(resultOne)); expect(resultOne).toHaveBeenCalledTimes(0); vi.advanceTimersByTime(1); expect(resultOne).toHaveBeenCalledTimes(1); expect(resultOne).toHaveBeenCalledWith({ data: 1, operation, stale: false, hasNext: false, }); pipe(client.executeRequestOperation(operation), subscribe(resultTwo)); expect(resultTwo).toHaveBeenCalledWith({ data: 1, operation, stale: true, hasNext: false, }); expect(resultOne).toHaveBeenCalledWith({ data: 1, operation, stale: true, hasNext: false, }); expect(resultTwo).toHaveBeenCalledTimes(1); expect(resultOne).toHaveBeenCalledTimes(2); vi.advanceTimersByTime(1); // With network-only we expect a new operation to be issued, hence a new result expect(resultTwo).toHaveBeenCalledTimes(2); expect(resultOne).toHaveBeenCalledTimes(3); expect(resultTwo).toHaveBeenCalledWith({ data: 2, operation, stale: false, hasNext: false, }); expect(resultOne).toHaveBeenCalledWith({ data: 2, operation, stale: false, hasNext: false, }); }); it('does not replay values from a past subscription', async () => { const exchange: Exchange = () => ops$ => { let i = 0; return pipe( ops$, filter(op => op.kind !== 'teardown'), map(op => ({ hasNext: false, stale: false, data: ++i, operation: op, })), delay(1) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); // We keep the source in-memory const source = client.executeRequestOperation(queryOperation); const resultOne = vi.fn(); let subscription; subscription = pipe(source, subscribe(resultOne)); expect(resultOne).toHaveBeenCalledTimes(0); vi.advanceTimersByTime(1); expect(resultOne).toHaveBeenCalledWith({ data: 1, operation: queryOperation, hasNext: false, stale: false, }); subscription.unsubscribe(); const resultTwo = vi.fn(); subscription = pipe(source, subscribe(resultTwo)); expect(resultTwo).toHaveBeenCalledTimes(0); vi.advanceTimersByTime(1); expect(resultTwo).toHaveBeenCalledWith({ data: 2, operation: queryOperation, stale: false, hasNext: false, }); }); it('replayed results are not emitted on the shared source', () => { const exchange: Exchange = () => ops$ => { let i = 0; return pipe( ops$, map(op => ({ data: ++i, operation: op, hasNext: false, stale: false, })), take(1) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const operation = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); const resultOne = vi.fn(); const resultTwo = vi.fn(); pipe(client.executeRequestOperation(operation), subscribe(resultOne)); pipe(client.executeRequestOperation(operation), subscribe(resultTwo)); expect(resultTwo).toHaveBeenCalledTimes(1); expect(resultTwo).toHaveBeenCalledWith({ data: 1, operation, stale: true, hasNext: false, }); }); it('does nothing when no operation result has been emitted yet', () => { const dispatched = vi.fn(); const exchange: Exchange = () => ops$ => { return pipe( ops$, map(op => { dispatched(op); return { hasNext: false, stale: false, data: 1, operation: op }; }), filter(() => false) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const resultOne = vi.fn(); const resultTwo = vi.fn(); pipe(client.executeRequestOperation(queryOperation), subscribe(resultOne)); pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo)); expect(resultOne).toHaveBeenCalledTimes(0); expect(resultTwo).toHaveBeenCalledTimes(0); expect(dispatched).toHaveBeenCalledTimes(1); }); it('skips replaying results when a result is emitted immediately (network-only)', () => { const exchange: Exchange = () => ops$ => { let i = 0; return pipe( ops$, map(op => ({ hasNext: false, stale: false, data: ++i, operation: op })) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const operation = makeOperation('query', queryOperation, { ...queryOperation.context, requestPolicy: 'network-only', }); const resultOne = vi.fn(); const resultTwo = vi.fn(); pipe(client.executeRequestOperation(operation), subscribe(resultOne)); expect(resultOne).toHaveBeenCalledWith({ data: 1, operation, hasNext: false, stale: false, }); pipe(client.executeRequestOperation(operation), subscribe(resultTwo)); expect(resultTwo).toHaveBeenCalledWith({ data: 2, operation, hasNext: false, stale: false, }); expect(resultOne).toHaveBeenCalledWith({ data: 2, operation, hasNext: false, stale: false, }); }); it('replays stale results as needed', () => { const exchange: Exchange = () => ops$ => { return pipe( ops$, map(op => ({ hasNext: false, stale: true, data: 1, operation: op })), take(1) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const resultOne = vi.fn(); const resultTwo = vi.fn(); pipe(client.executeRequestOperation(queryOperation), subscribe(resultOne)); expect(resultOne).toHaveBeenCalledWith({ data: 1, operation: queryOperation, stale: true, hasNext: false, }); pipe(client.executeRequestOperation(queryOperation), subscribe(resultTwo)); expect(resultTwo).toHaveBeenCalledWith({ data: 1, operation: queryOperation, stale: true, hasNext: false, }); }); it('does nothing when operation is a subscription has been emitted yet', () => { const exchange: Exchange = () => ops$ => { return merge([ pipe( ops$, map(op => ({ hasNext: true, data: 1, operation: op })), take(1) ), never, ]); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const resultOne = vi.fn(); const resultTwo = vi.fn(); pipe( client.executeRequestOperation(subscriptionOperation), subscribe(resultOne) ); expect(resultOne).toHaveBeenCalledTimes(1); pipe( client.executeRequestOperation(subscriptionOperation), subscribe(resultTwo) ); expect(resultTwo).toHaveBeenCalledTimes(0); }); it('supports promisified sources', async () => { const exchange: Exchange = () => ops$ => { return pipe( ops$, map(op => ({ hasNext: false, stale: true, data: 1, operation: op })) ); }; const client = createClient({ url: 'test', exchanges: [exchange], }); const resultOne = await pipe( client.executeRequestOperation(queryOperation), take(1), toPromise ); expect(resultOne).toEqual({ data: 1, operation: queryOperation, stale: true, hasNext: false, }); }); }); ================================================ FILE: packages/core/src/client.ts ================================================ /* eslint-disable @typescript-eslint/no-use-before-define */ import type { Source, Subscription } from 'wonka'; import { lazy, filter, makeSubject, onEnd, onPush, onStart, pipe, share, take, takeUntil, takeWhile, publish, subscribe, switchMap, fromValue, merge, map, } from 'wonka'; import { composeExchanges } from './exchanges'; import { fallbackExchange } from './exchanges/fallback'; import type { DocumentInput, AnyVariables, Exchange, ExchangeInput, GraphQLRequest, Operation, OperationInstance, OperationContext, OperationResult, OperationResultSource, OperationType, RequestPolicy, DebugEvent, } from './types'; import { createRequest, withPromise, noop, makeOperation, getOperationType, } from './utils'; /** Configuration options passed when creating a new {@link Client}. * * @remarks * The `ClientOptions` are passed when creating a new {@link Client}, and * are used to instantiate the pipeline of {@link Exchange | Exchanges}, configure * options used to initialize {@link OperationContext | OperationContexts}, or to * change the general behaviour of the {@link Client}. */ export interface ClientOptions { /** Target URL used by fetch exchanges to make GraphQL API requests to. * * @remarks * This is the URL that fetch exchanges will call to make GraphQL API requests. * This value is copied to {@link OperationContext.url}. */ url: string; /** Additional options used by fetch exchanges that'll be passed to the `fetch` call on API requests. * * @remarks * The options in this object or an object returned by a callback function will be merged into the * {@link RequestInit} options passed to the `fetch` call. * * Hint: If you're trying to implement more complex changes per {@link Operation}, it's worth considering * to use the {@link mapExchange} instead, which allows you to change `Operation`s and `OperationResult`s. * * Hint: If you're trying to use this as a function for authentication, consider checking out * `@urql/exchange-auth` instead, which allows you to handle refresh auth flows, and more * complex auth flows. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch} for a description of this object. */ fetchOptions?: RequestInit | (() => RequestInit); /** A `fetch` function polyfill used by fetch exchanges to make API calls. * * @remarks * This is the fetch polyfill used by any fetch exchange to make an API request. By default, when this * option isn't set, any fetch exchange will attempt to use the globally available `fetch` function * to make a request instead. * * It's recommended to only pass a polyfill, if any of the environments you're running the {@link Client} * in don't support the Fetch API natively. * * Hint: If you're using the "Incremental Delivery" multipart spec, for instance with `@defer` directives, * you're better off using the native `fetch` function, or must ensure that your polyfill supports streamed * results. However, a "Streaming requests unsupported" error will be thrown, to let you know that your `fetch` * API doesn't support incrementally streamed responses, if this mode is used. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} for the Fetch API spec. */ fetch?: typeof fetch; /** Allows a subscription to be executed using a `fetch` API request. * * @remarks * If your API supports the `text/event-stream` and/or `multipart/mixed` response protocol, and you use * this protocol to handle subscriptions, then you may switch this flag to `true`. * * This means you won’t have to create a {@link subscriptionExchange} to handle subscriptions with an * external transport, and will instead be able to use GraphQL over HTTP transports. */ fetchSubscriptions?: boolean; /** A list of `Exchange`s that will be used to create the `Client`'s execution pipeline. * * @remarks * The {@link Client} accepts and composes a list of {@link Exchange | Exchanges} into an “exchange pipeline” * which receive a stream of {@link Operation | Operations} the `Client` wishes to execute, and return a stream * of {@link OperationResult | OperationResults}. * * This is the basis for how `urql` handles GraphQL operations, and exchanges handle the creation, execution, * and control flow of exchanges for the `Client`. * * To easily get started you should consider using the {@link cacheExchange} and {@link fetchExchange} * these are all exported from the core package. * * @see {@link https://urql.dev/goto/docs/architecture/#the-client-and-exchanges} for more information * on what `Exchange`s are and how they work. */ exchanges: Exchange[]; /** A configuration flag indicating whether support for "Suspense" is activated. * * @remarks * This configuration flag is only relevant for using `urql` with the React or Preact bindings. * When activated it allows `useQuery` to "suspend" instead of returning a loading state, which * will stop updates in a querying component and instead cascade * to a higher suspense boundary for a loading state. * * Hint: While, when this option is enabled, by default all `useQuery` hooks will suspense, you can * disable Suspense selectively for each hook. * * @see {@link https://beta.reactjs.org/blog/2022/03/29/react-v18#new-suspense-features} for more information on React Suspense. */ suspense?: boolean; /** The request and caching strategy that all `Operation`s on this `Client` will use by default. * * @remarks * The {@link RequestPolicy} instructs cache exchanges how to use and treat their cached results. * By default `cache-first` is set and used, which will use cache results, and only make an API request * on a cache miss. * * The `requestPolicy` can be overriden per operation, since it's added to the {@link OperationContext}, * which allows you to change the policy per `Operation`, rather than changing it by default here. * * Hint: We don’t recommend changing this from the default `cache-first` option, unless you know what * you‘re doing. Setting this to `cache-and-network` is not recommend and may not lead to the behaviour * you expect. If you’re looking to always update your cache frequently, use `@urql/exchange-request-policy` * instead. */ requestPolicy?: RequestPolicy; /** Instructs fetch exchanges to use a GET request. * * @remarks * This changes the {@link OperationContext.preferGetMethod} option, which tells fetch exchanges * to use GET requests for queries instead of POST requests. * * When set to `true` or `'within-url-limit'`, built-in fetch exchanges will always attempt to send query * operations as GET requests, unless the resulting URL exceeds a length of 2,048 characters. * If you want to bypass this restriction, set this option to `'force'` instead, to always send GET. * requests for queries. */ preferGetMethod?: boolean | 'force' | 'within-url-limit'; } /** The `Client` is the central hub for your GraphQL operations and holds `urql`'s state. * * @remarks * The `Client` manages your active GraphQL operations and their state, and contains the * {@link Exchange} pipeline to execute your GraphQL operations. * * It contains methods that allow you to execute GraphQL operations manually, but the `Client` * is also interacted with by bindings (for React, Preact, Vue, Svelte, etc) to execute GraphQL * operations. * * While {@link Exchange | Exchanges} are ultimately responsible for the control flow of operations, * sending API requests, and caching, the `Client` still has the important responsibility for * creating operations, managing consumers of active operations, sharing results for operations, * and more tasks as a “central hub”. * * @see {@link https://urql.dev/goto/docs/architecture/#requests-and-operations-on-the-client} for more information * on what the `Client` is and does. */ export interface Client { new (options: ClientOptions): Client; /** Exposes the stream of `Operation`s that is passed to the `Exchange` pipeline. * * @remarks * This is a Wonka {@link Source} that issues the {@link Operation | Operations} going into * the exchange pipeline. * @internal */ operations$: Source; /** Flag indicating whether support for “Suspense” is activated. * * @remarks * This flag indicates whether support for “Suspense” has been activated via the * {@link ClientOptions.suspense} flag. * * When this is enabled, the {@link Client} itself doesn’t function any differently, and the flag * only serves as an instructions for the React/Preact bindings to change their behaviour. * * @see {@link ClientOptions.suspense} for more information. * @internal */ suspense: boolean; /** Dispatches an `Operation` to the `Exchange` pipeline, if this `Operation` is active. * * @remarks * This method is frequently used in {@link Exchange | Exchanges}, for instance caches, to reexecute * an operation. It’s often either called because an `Operation` will need to be queried against the * cache again, if a cache result has changed or been invalidated, or it’s called with an {@link Operation}'s * {@link RequestPolicy} set to `network-only` to issue a network request. * * This method will only dispatch an {@link Operation} if it has active consumers, meaning, * active subscribers to the sources of {@link OperationResult}. For instance, if no bindings * (e.g. `useQuery`) is subscribed to the `Operation`, then `reexecuteOperation` will do nothing. * * All operations are put onto a queue and executed after a micro-tick. The queue of operations is * emptied eagerly and synchronously, similar to a trampoline scheduler. */ reexecuteOperation(operation: Operation): void; /** Subscribe method to add an event listener to debug events. * * @param onEvent - A callback called with new debug events, each time an `Exchange` issues them. * @returns A Wonka {@link Subscription} which is used to optionally terminate the event listener. * * @remarks * This is a method that's only available in development, and allows the `urql-devtools` to receive * to debug events that are issued by exchanges, giving the devtools more information about the flow * and execution of {@link Operation | Operations}. * * @see {@link DebugEventTypes} for a description of all debug events. * @internal */ subscribeToDebugTarget?(onEvent: (event: DebugEvent) => void): Subscription; /** Creates an `Operation` from a `GraphQLRequest` and optionally, overriding `OperationContext` options. * * @param kind - The {@link OperationType} of GraphQL operation, i.e. `query`, `mutation`, or `subscription`. * @param request - A {@link GraphQLRequest} created prior to calling this method. * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}. * @returns An {@link Operation} created from the parameters. * * @remarks * This method is expected to be called with a `kind` set to the `OperationType` of the GraphQL operation. * In development, this is enforced by checking that the GraphQL document's operation matches this `kind`. * * Hint: While bindings will use this method combined with {@link Client.executeRequestOperation}, if * you’re executing operations manually, you can use one of the other convenience methods instead. * * @see {@link Client.executeRequestOperation} for the method used to execute operations. * @see {@link createRequest} which creates a `GraphQLRequest` from a `DocumentNode` and variables. */ createRequestOperation< Data = any, Variables extends AnyVariables = AnyVariables, >( kind: OperationType, request: GraphQLRequest, opts?: Partial | undefined ): Operation; /** Creates a `Source` that executes the `Operation` and issues `OperationResult`s for this `Operation`. * * @param operation - {@link Operation} that will be executed. * @returns A Wonka {@link Source} of {@link OperationResult | OperationResults} for the passed `Operation`. * * @remarks * The {@link Operation} will be dispatched to the pipeline of {@link Exchange | Exchanges} when * subscribing to the returned {@link Source}, which issues {@link OperationResult | OperationResults} * belonging to this `Operation`. * * Internally, {@link OperationResult | OperationResults} are filtered and deliverd to this source by * comparing the {@link Operation.key} on the operation and the {@link OperationResult.operation}. * For mutations, the {@link OperationContext._instance | `OperationContext._instance`} will additionally be compared, since two mutations * with, even given the same variables, will have two distinct results and will be executed separately. * * The {@link Client} dispatches the {@link Operation} when we subscribe to the returned {@link Source} * and will from then on consider the `Operation` as “active” until we unsubscribe. When all consumers unsubscribe * from an `Operation` and it becomes “inactive” a `teardown` signal will be dispatched to the * {@link Exchange | Exchanges}. * * Hint: While bindings will use this method, if you’re executing operations manually, you can use one * of the other convenience methods instead, like {@link Client.executeQuery} et al. */ executeRequestOperation< Data = any, Variables extends AnyVariables = AnyVariables, >( operation: Operation ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL query operation created from the passed parameters. * * @param query - a GraphQL document containing the query operation that will be executed. * @param variables - the variables used to execute the operation. * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}. * @returns A {@link OperationResultSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation. * * @remarks * The `Client.query` method is useful to programmatically create and issue a GraphQL query operation. * It automatically calls {@link createRequest}, {@link client.createRequestOperation}, and * {@link client.executeRequestOperation} for you, and is a convenience method. * * Since it returns a {@link OperationResultSource} it may be chained with a `toPromise()` call to only * await a single result in an async function. * * Hint: This is the recommended way to create queries programmatically when not using the bindings, * or when you’re trying to get a single, promisified result. * * @example * ```ts * const getBookQuery = gql` * query GetBook($id: ID!) { * book(id: $id) { * id * name * author { * name * } * } * } * `; * * async function getBook(id) { * const result = await client.query(getBookQuery, { id }).toPromise(); * if (result.error) { * throw result.error; * } * * return result.data.book; * } * ``` */ query( query: DocumentInput, variables: Variables, context?: Partial ): OperationResultSource>; /** Returns the first synchronous result a `Client` provides for a given operation. * * @param query - a GraphQL document containing the query operation that will be executed. * @param variables - the variables used to execute the operation. * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}. * @returns An {@link OperationResult} if one became available synchronously or `null`. * * @remarks * The `Client.readQuery` method returns a result synchronously or defaults to `null`. This is useful * as it limits the result for a query operation to whatever the cache {@link Exchange} of a {@link Client} * had stored and available at that moment. * * In `urql`, it's expected that cache exchanges return their results synchronously. The bindings * and this method exploit this by using synchronous results, like these, to check what data is already * in the cache. * * This method is similar to what all bindings do to synchronously provide the initial state for queries, * regardless of whether effects afterwards that subscribe to the query operation update this state synchronously * or asynchronously. */ readQuery( query: DocumentInput, variables: Variables, context?: Partial ): OperationResult | null; /** Creates a `Source` that executes the GraphQL query operation for the passed `GraphQLRequest`. * * @param query - a {@link GraphQLRequest} * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}. * @returns A {@link PromisifiedSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation. * * @remarks * The `Client.executeQuery` method is used to programmatically issue a GraphQL query operation. * It automatically calls {@link client.createRequestOperation} and {@link client.executeRequestOperation} for you, * but requires you to create a {@link GraphQLRequest} using {@link createRequest} yourself first. * * @see {@link Client.query} for a method that doesn't require calling {@link createRequest} yourself. */ executeQuery( query: GraphQLRequest, opts?: Partial | undefined ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL subscription operation created from the passed parameters. * * @param query - a GraphQL document containing the subscription operation that will be executed. * @param variables - the variables used to execute the operation. * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}. * @returns A Wonka {@link Source} issuing the {@link OperationResult | OperationResults} for the GraphQL operation. * * @remarks * The `Client.subscription` method is useful to programmatically create and issue a GraphQL subscription operation. * It automatically calls {@link createRequest}, {@link client.createRequestOperation}, and * {@link client.executeRequestOperation} for you, and is a convenience method. * * Hint: This is the recommended way to create subscriptions programmatically when not using the bindings. * * @example * ```ts * import { pipe, subscribe } from 'wonka'; * * const getNewsSubscription = gql` * subscription GetNews { * breakingNews { * id * text * createdAt * } * } * `; * * function subscribeToBreakingNews() { * const subscription = pipe( * client.subscription(getNewsSubscription, {}), * subscribe(result => { * if (result.data) { * console.log(result.data.breakingNews.text); * } * }) * ); * * return subscription.unsubscribe; * } * ``` */ subscription( query: DocumentInput, variables: Variables, context?: Partial ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL subscription operation for the passed `GraphQLRequest`. * * @param query - a {@link GraphQLRequest} * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}. * @returns A {@link PromisifiedSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation. * * @remarks * The `Client.executeSubscription` method is used to programmatically issue a GraphQL subscription operation. * It automatically calls {@link client.createRequestOperation} and {@link client.executeRequestOperation} for you, * but requires you to create a {@link GraphQLRequest} using {@link createRequest} yourself first. * * @see {@link Client.subscription} for a method that doesn't require calling {@link createRequest} yourself. */ executeSubscription< Data = any, Variables extends AnyVariables = AnyVariables, >( query: GraphQLRequest, opts?: Partial | undefined ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL mutation operation created from the passed parameters. * * @param query - a GraphQL document containing the mutation operation that will be executed. * @param variables - the variables used to execute the operation. * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}. * @returns A {@link PromisifiedSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation. * * @remarks * The `Client.mutation` method is useful to programmatically create and issue a GraphQL mutation operation. * It automatically calls {@link createRequest}, {@link client.createRequestOperation}, and * {@link client.executeRequestOperation} for you, and is a convenience method. * * Since it returns a {@link PromisifiedSource} it may be chained with a `toPromise()` call to only * await a single result in an async function. Since mutations will only typically issue one result, * using this method is recommended. * * Hint: This is the recommended way to create mutations programmatically when not using the bindings, * or when you’re trying to get a single, promisified result. * * @example * ```ts * const createPostMutation = gql` * mutation CreatePost($text: String!) { * createPost(text: $text) { * id * text * } * } * `; * * async function createPost(text) { * const result = await client.mutation(createPostMutation, { * text, * }).toPromise(); * if (result.error) { * throw result.error; * } * * return result.data.createPost; * } * ``` */ mutation( query: DocumentInput, variables: Variables, context?: Partial ): OperationResultSource>; /** Creates a `Source` that executes the GraphQL mutation operation for the passed `GraphQLRequest`. * * @param query - a {@link GraphQLRequest} * @param opts - {@link OperationContext} options that'll override and be merged with options from the {@link ClientOptions}. * @returns A {@link PromisifiedSource} issuing the {@link OperationResult | OperationResults} for the GraphQL operation. * * @remarks * The `Client.executeMutation` method is used to programmatically issue a GraphQL mutation operation. * It automatically calls {@link client.createRequestOperation} and {@link client.executeRequestOperation} for you, * but requires you to create a {@link GraphQLRequest} using {@link createRequest} yourself first. * * @see {@link Client.mutation} for a method that doesn't require calling {@link createRequest} yourself. */ executeMutation( query: GraphQLRequest, opts?: Partial | undefined ): OperationResultSource>; } export const Client: new (opts: ClientOptions) => Client = function Client( this: Client | {}, opts: ClientOptions ) { if (process.env.NODE_ENV !== 'production' && !opts.url) { throw new Error('You are creating an urql-client without a url.'); } let ids = 0; const replays = new Map(); const active: Map> = new Map(); const dispatched = new Set(); const queue: Operation[] = []; const baseOpts = { url: opts.url, fetchSubscriptions: opts.fetchSubscriptions, fetchOptions: opts.fetchOptions, fetch: opts.fetch, preferGetMethod: opts.preferGetMethod != null ? opts.preferGetMethod : 'within-url-limit', requestPolicy: opts.requestPolicy || 'cache-first', }; // This subject forms the input of operations; executeOperation may be // called to dispatch a new operation on the subject const operations = makeSubject(); function nextOperation(operation: Operation) { if ( operation.kind === 'mutation' || operation.kind === 'teardown' || !dispatched.has(operation.key) ) { if (operation.kind === 'teardown') { dispatched.delete(operation.key); } else if (operation.kind !== 'mutation') { dispatched.add(operation.key); } operations.next(operation); } } // We define a queued dispatcher on the subject, which empties the queue when it's // activated to allow `reexecuteOperation` to be trampoline-scheduled let isOperationBatchActive = false; function dispatchOperation(operation?: Operation | void) { if (operation) nextOperation(operation); if (!isOperationBatchActive) { isOperationBatchActive = true; while (isOperationBatchActive && (operation = queue.shift())) nextOperation(operation); isOperationBatchActive = false; } } /** Defines how result streams are created */ const makeResultSource = (operation: Operation) => { let result$ = pipe( results$, // Filter by matching key (or _instance if it’s set) filter( (res: OperationResult) => res.operation.kind === operation.kind && res.operation.key === operation.key && (!res.operation.context._instance || res.operation.context._instance === operation.context._instance) ), // End the results stream when an active teardown event is sent takeUntil( pipe( operations.source, filter(op => op.kind === 'teardown' && op.key === operation.key) ) ) ); if (operation.kind !== 'query') { // Interrupt subscriptions and mutations when they have no more results result$ = pipe( result$, takeWhile(result => !!result.hasNext, true) ); } else { result$ = pipe( result$, // Add `stale: true` flag when a new operation is sent for queries switchMap(result => { const value$ = fromValue(result); return result.stale || result.hasNext ? value$ : merge([ value$, pipe( operations.source, filter(op => op.key === operation.key), take(1), map(() => { result.stale = true; return result; }) ), ]); }) ); } if (operation.kind !== 'mutation') { result$ = pipe( result$, // Store replay result onPush(result => { if (result.stale) { if (!result.hasNext) { // we are dealing with an optimistic mutation or a partial result dispatched.delete(operation.key); } else { // If the current result has queued up an operation of the same // key, then `stale` refers to it for (let i = 0; i < queue.length; i++) { const operation = queue[i]; if (operation.key === result.operation.key) { dispatched.delete(operation.key); break; } } } } else if (!result.hasNext) { dispatched.delete(operation.key); } replays.set(operation.key, result); }), // Cleanup active states on end of source onEnd(() => { // Delete the active operation handle dispatched.delete(operation.key); replays.delete(operation.key); active.delete(operation.key); // Interrupt active queue isOperationBatchActive = false; // Delete all queued up operations of the same key on end for (let i = queue.length - 1; i >= 0; i--) if (queue[i].key === operation.key) queue.splice(i, 1); // Dispatch a teardown signal for the stopped operation nextOperation( makeOperation('teardown', operation, operation.context) ); }) ); } else { result$ = pipe( result$, // Send mutation operation on start onStart(() => { nextOperation(operation); }) ); } return share(result$); }; const instance: Client = this instanceof Client ? this : Object.create(Client.prototype); const client: Client = Object.assign(instance, { suspense: !!opts.suspense, operations$: operations.source, reexecuteOperation(operation: Operation) { // Reexecute operation only if any subscribers are still subscribed to the // operation's exchange results if (operation.kind === 'teardown') { dispatchOperation(operation); } else if (operation.kind === 'mutation') { queue.push(operation); Promise.resolve().then(dispatchOperation); } else if (active.has(operation.key)) { let queued = false; for (let i = 0; i < queue.length; i++) { if (queue[i].key === operation.key) { queue[i] = operation; queued = true; } } if ( !queued && (!dispatched.has(operation.key) || operation.context.requestPolicy === 'network-only') ) { queue.push(operation); Promise.resolve().then(dispatchOperation); } else { dispatched.delete(operation.key); Promise.resolve().then(dispatchOperation); } } }, createRequestOperation(kind, request, opts) { if (!opts) opts = {}; let requestOperationType: string | undefined; if ( process.env.NODE_ENV !== 'production' && kind !== 'teardown' && (requestOperationType = getOperationType(request.query)) !== kind ) { throw new Error( `Expected operation of type "${kind}" but found "${requestOperationType}"` ); } return makeOperation(kind, request, { _instance: kind === 'mutation' ? ((ids = (ids + 1) | 0) as OperationInstance) : undefined, ...baseOpts, ...opts, requestPolicy: opts.requestPolicy || baseOpts.requestPolicy, suspense: opts.suspense || (opts.suspense !== false && client.suspense), }); }, executeRequestOperation(operation) { if (operation.kind === 'mutation') { return withPromise(makeResultSource(operation)); } return withPromise( lazy(() => { let source = active.get(operation.key); if (!source) { active.set(operation.key, (source = makeResultSource(operation))); } source = pipe( source, onStart(() => { dispatchOperation(operation); }) ); const replay = replays.get(operation.key); if ( operation.kind === 'query' && replay && (replay.stale || replay.hasNext) ) { return pipe( merge([ source, pipe( fromValue(replay), filter(replay => replay === replays.get(operation.key)) ), ]), switchMap(fromValue) ); } else { return source; } }) ); }, executeQuery(query, opts) { const operation = client.createRequestOperation('query', query, opts); return client.executeRequestOperation(operation); }, executeSubscription(query, opts) { const operation = client.createRequestOperation( 'subscription', query, opts ); return client.executeRequestOperation(operation); }, executeMutation(query, opts) { const operation = client.createRequestOperation('mutation', query, opts); return client.executeRequestOperation(operation); }, readQuery(query, variables, context) { let result: OperationResult | null = null; pipe( client.query(query, variables, context), subscribe(res => { result = res; }) ).unsubscribe(); return result; }, query(query, variables, context) { return client.executeQuery(createRequest(query, variables), context); }, subscription(query, variables, context) { return client.executeSubscription( createRequest(query, variables), context ); }, mutation(query, variables, context) { return client.executeMutation(createRequest(query, variables), context); }, } as Client); let dispatchDebug: ExchangeInput['dispatchDebug'] = noop; if (process.env.NODE_ENV !== 'production') { const { next, source } = makeSubject(); client.subscribeToDebugTarget = (onEvent: (e: DebugEvent) => void) => pipe(source, subscribe(onEvent)); dispatchDebug = next as ExchangeInput['dispatchDebug']; } // All exchange are composed into a single one and are called using the constructed client // and the fallback exchange stream const composedExchange = composeExchanges(opts.exchanges); // All exchanges receive inputs using which they can forward operations to the next exchange // and receive a stream of results in return, access the client, or dispatch debugging events // All operations then run through the Exchange IOs in a pipeline-like fashion const results$ = share( composedExchange({ client, dispatchDebug, forward: fallbackExchange({ dispatchDebug }), })(operations.source) ); // Prevent the `results$` exchange pipeline from being closed by active // cancellations cascading up from components pipe(results$, publish); return client; } as any; /** Accepts `ClientOptions` and creates a `Client`. * @param opts - A {@link ClientOptions} objects with options for the `Client`. * @returns A {@link Client} instantiated with `opts`. */ export const createClient = Client as any as (opts: ClientOptions) => Client; ================================================ FILE: packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`on error > returns error data 1`] = ` { "data": undefined, "error": [CombinedError: [Network] No Content], "extensions": undefined, "hasNext": false, "operation": { "context": { "fetchOptions": { "method": "POST", }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 2, "kind": "query", "query": { "__key": -2395444236, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "getUser", }, "operation": "query", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "name", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "id", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "firstName", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "lastName", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 92, "source": { "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "name": "Clara", }, }, "stale": false, } `; exports[`on error > returns error data with status 400 and manual redirect mode 1`] = ` { "data": undefined, "error": [CombinedError: [Network] No Content], "extensions": undefined, "hasNext": false, "operation": { "context": { "fetchOptions": [MockFunction spy] { "calls": [ [], ], "results": [ { "type": "return", "value": { "redirect": "manual", }, }, ], }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 2, "kind": "query", "query": { "__key": -2395444236, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "getUser", }, "operation": "query", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "name", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "id", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "firstName", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "lastName", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 92, "source": { "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "name": "Clara", }, }, "stale": false, } `; exports[`on success > returns response data 1`] = ` { "data": { "data": { "user": 1200, }, }, "error": undefined, "extensions": undefined, "hasNext": false, "operation": { "context": { "fetchOptions": [MockFunction spy] { "calls": [ [], ], "results": [ { "type": "return", "value": {}, }, ], }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 2, "kind": "query", "query": { "__key": -2395444236, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "getUser", }, "operation": "query", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "name", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "id", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "firstName", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "lastName", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 92, "source": { "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "name": "Clara", }, }, "stale": false, } `; exports[`on success > returns response data 2`] = `"{"operationName":"getUser","query":"query getUser($name: String) {\\n user(name: $name) {\\n id\\n firstName\\n lastName\\n }\\n}","variables":{"name":"Clara"}}"`; ================================================ FILE: packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`should return response data from forwardSubscription observable 1`] = ` { "data": {}, "error": undefined, "extensions": undefined, "hasNext": true, "operation": { "context": { "fetchOptions": { "method": "POST", }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 4, "kind": "subscription", "query": { "__key": 7623921801, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "subscribeToUser", }, "operation": "subscription", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "user", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "user", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "name", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "user", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 82, "source": { "body": "subscription subscribeToUser($user: String) { user(user: $user) { name } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "user": "colin", }, }, "stale": false, } `; ================================================ FILE: packages/core/src/exchanges/cache.test.ts ================================================ import { makeSubject, map, pipe, publish, Source, Subject, forEach, scan, toPromise, } from 'wonka'; import { vi, expect, it, beforeEach, describe } from 'vitest'; import { Client } from '../client'; import { mutationOperation, mutationResponse, queryOperation, queryResponse, subscriptionOperation, subscriptionResult, undefinedQueryResponse, } from '../test-utils'; import { Operation, OperationResult, ExchangeInput } from '../types'; import { cacheExchange } from './cache'; const reexecuteOperation = vi.fn(); const dispatchDebug = vi.fn(); let response; let exchangeArgs: ExchangeInput; let forwardedOperations: Operation[]; let input: Subject; beforeEach(() => { response = queryResponse; forwardedOperations = []; input = makeSubject(); // Collect all forwarded operations const forward = (s: Source) => { return pipe( s, map(op => { forwardedOperations.push(op); return response; }) ); }; const client = { reexecuteOperation: reexecuteOperation as any, } as Client; exchangeArgs = { forward, client, dispatchDebug }; }); describe('on query', () => { it('forwards to next exchange when no cache hit', () => { const { source: ops$, next, complete } = input; const exchange = cacheExchange(exchangeArgs)(ops$); publish(exchange); next(queryOperation); complete(); expect(forwardedOperations.length).toBe(1); expect(reexecuteOperation).not.toBeCalled(); }); it('caches results', () => { const { source: ops$, next, complete } = input; const exchange = cacheExchange(exchangeArgs)(ops$); publish(exchange); next(queryOperation); next(queryOperation); complete(); expect(forwardedOperations.length).toBe(1); expect(reexecuteOperation).not.toBeCalled(); }); it('respects cache-and-network', () => { const { source: ops$, next, complete } = input; const result = vi.fn(); const exchange = cacheExchange(exchangeArgs)(ops$); pipe(exchange, forEach(result)); next(queryOperation); next({ ...queryOperation, context: { ...queryOperation.context, requestPolicy: 'cache-and-network', }, }); complete(); expect(forwardedOperations.length).toBe(1); expect(reexecuteOperation).toHaveBeenCalledTimes(1); expect(result).toHaveBeenCalledTimes(2); expect(result.mock.calls[1][0].stale).toBe(true); expect(reexecuteOperation.mock.calls[0][0]).toEqual({ ...queryOperation, context: { ...queryOperation.context, requestPolicy: 'network-only' }, }); }); it('respects cache-only', () => { const { source: ops$, next, complete } = input; const exchange = cacheExchange(exchangeArgs)(ops$); publish(exchange); next({ ...queryOperation, context: { ...queryOperation.context, requestPolicy: 'cache-only', }, }); complete(); expect(forwardedOperations.length).toBe(0); expect(reexecuteOperation).not.toBeCalled(); }); describe('cache hit', () => { it('is miss when operation is forwarded', () => { const { source: ops$, next, complete } = input; const exchange = cacheExchange(exchangeArgs)(ops$); publish(exchange); next(queryOperation); complete(); expect(forwardedOperations[0].context).toHaveProperty( 'meta.cacheOutcome', 'miss' ); }); it('is true when cached response is returned', async () => { const { source: ops$, next, complete } = input; const exchange = cacheExchange(exchangeArgs)(ops$); const results$ = pipe( exchange, scan((acc, x) => [...acc, x], [] as OperationResult[]), toPromise ); publish(exchange); next(queryOperation); next(queryOperation); complete(); const results = await results$; expect(results[1].operation.context).toHaveProperty( 'meta.cacheOutcome', 'hit' ); }); }); }); describe('on mutation', () => { it('does not cache', () => { response = mutationResponse; const { source: ops$, next, complete } = input; const exchange = cacheExchange(exchangeArgs)(ops$); publish(exchange); next(mutationOperation); next(mutationOperation); complete(); expect(forwardedOperations.length).toBe(2); expect(reexecuteOperation).not.toBeCalled(); }); }); describe('on subscription', () => { it('forwards subscriptions', () => { response = subscriptionResult; const { source: ops$, next, complete } = input; const exchange = cacheExchange(exchangeArgs)(ops$); publish(exchange); next(subscriptionOperation); next(subscriptionOperation); complete(); expect(forwardedOperations.length).toBe(2); expect(reexecuteOperation).not.toBeCalled(); }); }); // Empty query response implies the data propertys is undefined describe('on empty query response', () => { beforeEach(() => { response = undefinedQueryResponse; forwardedOperations = []; input = makeSubject(); // Collect all forwarded operations const forward = (s: Source) => { return pipe( s, map(op => { forwardedOperations.push(op); return response; }) ); }; const client = { reexecuteOperation: reexecuteOperation as any, } as Client; exchangeArgs = { forward, client, dispatchDebug }; }); it('does not cache response', () => { const { source: ops$, next, complete } = input; const exchange = cacheExchange(exchangeArgs)(ops$); publish(exchange); next(queryOperation); next(queryOperation); complete(); // 2 indicates it's not cached. expect(forwardedOperations.length).toBe(2); expect(reexecuteOperation).not.toBeCalled(); }); }); ================================================ FILE: packages/core/src/exchanges/cache.ts ================================================ /* eslint-disable @typescript-eslint/no-use-before-define */ import { filter, map, merge, pipe, tap } from 'wonka'; import type { Client } from '../client'; import type { Exchange, Operation, OperationResult } from '../types'; import { makeOperation, addMetadata, collectTypenames, formatDocument, makeResult, } from '../utils'; type ResultCache = Map; type OperationCache = Map>; const shouldSkip = ({ kind }: Operation) => kind !== 'mutation' && kind !== 'query'; /** Adds unique typenames to query (for invalidating cache entries) */ export const mapTypeNames = (operation: Operation): Operation => { const query = formatDocument(operation.query); if (query !== operation.query) { const formattedOperation = makeOperation(operation.kind, operation); formattedOperation.query = query; return formattedOperation; } else { return operation; } }; /** Default document cache exchange. * * @remarks * The default document cache in `urql` avoids sending the same GraphQL request * multiple times by caching it using the {@link Operation.key}. It will invalidate * query results automatically whenever it sees a mutation responses with matching * `__typename`s in their responses. * * The document cache will get the introspected `__typename` fields by modifying * your GraphQL operation documents using the {@link formatDocument} utility. * * This automatic invalidation strategy can fail if your query or mutation don’t * contain matching typenames, for instance, because the query contained an * empty list. * You can manually add hints for this exchange by specifying a list of * {@link OperationContext.additionalTypenames} for queries and mutations that * should invalidate one another. * * @see {@link https://urql.dev/goto/docs/basics/document-caching} for more information on this cache. */ export const cacheExchange: Exchange = ({ forward, client, dispatchDebug }) => { const resultCache: ResultCache = new Map(); const operationCache: OperationCache = new Map(); const isOperationCached = (operation: Operation) => operation.kind === 'query' && operation.context.requestPolicy !== 'network-only' && (operation.context.requestPolicy === 'cache-only' || resultCache.has(operation.key)); return ops$ => { const cachedOps$ = pipe( ops$, filter(op => !shouldSkip(op) && isOperationCached(op)), map(operation => { const cachedResult = resultCache.get(operation.key); dispatchDebug({ operation, ...(cachedResult ? { type: 'cacheHit', message: 'The result was successfully retrieved from the cache', } : { type: 'cacheMiss', message: 'The result could not be retrieved from the cache', }), }); let result: OperationResult = cachedResult || makeResult(operation, { data: null, }); result = { ...result, operation: addMetadata(operation, { cacheOutcome: cachedResult ? 'hit' : 'miss', }), }; if (operation.context.requestPolicy === 'cache-and-network') { result.stale = true; reexecuteOperation(client, operation); } return result; }) ); const forwardedOps$ = pipe( merge([ pipe( ops$, filter(op => !shouldSkip(op) && !isOperationCached(op)), map(mapTypeNames) ), pipe( ops$, filter(op => shouldSkip(op)) ), ]), map(op => addMetadata(op, { cacheOutcome: 'miss' })), filter( op => op.kind !== 'query' || op.context.requestPolicy !== 'cache-only' ), forward, tap(response => { let { operation } = response; if (!operation) return; let typenames = operation.context.additionalTypenames || []; // NOTE: For now, we only respect `additionalTypenames` from subscriptions to // avoid unexpected breaking changes // We'd expect live queries or other update mechanisms to be more suitable rather // than using subscriptions as “signals” to reexecute queries. However, if they’re // just used as signals, it’s intuitive to hook them up using `additionalTypenames` if (response.operation.kind !== 'subscription') { typenames = collectTypenames(response.data).concat(typenames); } // Invalidates the cache given a mutation's response if ( response.operation.kind === 'mutation' || response.operation.kind === 'subscription' ) { const pendingOperations = new Set(); dispatchDebug({ type: 'cacheInvalidation', message: `The following typenames have been invalidated: ${typenames}`, operation, data: { typenames, response }, }); for (let i = 0; i < typenames.length; i++) { const typeName = typenames[i]; let operations = operationCache.get(typeName); if (!operations) operationCache.set(typeName, (operations = new Set())); for (const key of operations.values()) pendingOperations.add(key); operations.clear(); } for (const key of pendingOperations.values()) { if (resultCache.has(key)) { operation = (resultCache.get(key) as OperationResult).operation; resultCache.delete(key); reexecuteOperation(client, operation); } } } else if (operation.kind === 'query' && response.data) { resultCache.set(operation.key, response); for (let i = 0; i < typenames.length; i++) { const typeName = typenames[i]; let operations = operationCache.get(typeName); if (!operations) operationCache.set(typeName, (operations = new Set())); operations.add(operation.key); } } }) ); return merge([cachedOps$, forwardedOps$]); }; }; /** Reexecutes an `Operation` with the `network-only` request policy. * @internal */ export const reexecuteOperation = (client: Client, operation: Operation) => { return client.reexecuteOperation( makeOperation(operation.kind, operation, { requestPolicy: 'network-only', }) ); }; ================================================ FILE: packages/core/src/exchanges/compose.test.ts ================================================ import { empty, Source } from 'wonka'; import { vi, expect, it, beforeEach, describe } from 'vitest'; import { Exchange } from '../types'; import { composeExchanges } from './compose'; import { noop } from '../utils'; const mockClient = {} as any; const forward = vi.fn(); const noopExchange: Exchange = ({ forward }) => ops$ => forward(ops$); beforeEach(() => { vi.spyOn(Date, 'now').mockReturnValue(1234); }); it('composes exchanges correctly', () => { let counter = 0; const firstExchange: Exchange = ({ client, forward }) => { expect(client).toBe(mockClient); expect(counter++).toBe(1); return ops$ => { expect(counter++).toBe(2); return forward(ops$); }; }; const secondExchange: Exchange = ({ client, forward }) => { expect(client).toBe(mockClient); expect(counter++).toBe(0); return ops$ => { expect(counter++).toBe(3); return forward(ops$); }; }; const exchange = composeExchanges([firstExchange, secondExchange]); const outerFw = vi.fn(() => noopExchange) as any; exchange({ client: mockClient, forward: outerFw, dispatchDebug: noop })( empty as Source ); expect(outerFw).toHaveBeenCalled(); expect(counter).toBe(4); }); describe('on dispatchDebug', () => { it('dispatches debug event with exchange source name', () => { const dispatchDebug = vi.fn(); const debugArgs = { type: 'test', message: 'Hello', } as any; const testExchange: Exchange = ({ dispatchDebug }) => { dispatchDebug(debugArgs); return () => empty as Source; }; composeExchanges([testExchange])({ client: mockClient, forward, dispatchDebug, }); expect(dispatchDebug).toBeCalledTimes(1); expect(dispatchDebug).toBeCalledWith({ ...debugArgs, timestamp: Date.now(), source: 'testExchange', }); }); }); ================================================ FILE: packages/core/src/exchanges/compose.ts ================================================ import { share } from 'wonka'; import type { ExchangeIO, Exchange, ExchangeInput } from '../types'; /** Composes an array of Exchanges into a single one. * * @param exchanges - An array of {@link Exchange | Exchanges}. * @returns - A composed {@link Exchange}. * * @remarks * `composeExchanges` returns an {@link Exchange} that when instantiated * composes the array of passed `Exchange`s into one, calling them from * right to left, with the prior `Exchange`’s {@link ExchangeIO} function * as the {@link ExchangeInput.forward} input. * * This simply merges all exchanges into one and is used by the {@link Client} * to merge the `exchanges` option it receives. * * @throws * In development, if {@link ExchangeInput.forward} is called repeatedly * by an {@link Exchange} an error is thrown, since `forward()` must only * be called once per `Exchange`. */ export const composeExchanges = (exchanges: Exchange[]): Exchange => ({ client, forward, dispatchDebug }: ExchangeInput): ExchangeIO => exchanges.reduceRight((forward, exchange) => { let forwarded = false; return exchange({ client, forward(operations$) { if (process.env.NODE_ENV !== 'production') { if (forwarded) throw new Error( 'forward() must only be called once in each Exchange.' ); forwarded = true; } return share(forward(share(operations$))); }, dispatchDebug(event) { dispatchDebug({ timestamp: Date.now(), source: exchange.name, ...event, }); }, }); }, forward); ================================================ FILE: packages/core/src/exchanges/debug.test.ts ================================================ import { makeSubject, map, pipe, publish, Source, Subject } from 'wonka'; import { vi, expect, it, beforeEach, describe, afterEach } from 'vitest'; import { Client } from '../client'; import { queryOperation, queryResponse } from '../test-utils'; import { Operation } from '../types'; import { debugExchange } from './debug'; let exchangeArgs; let forwardedOperations: Operation[]; let input: Subject; beforeEach(() => { forwardedOperations = []; input = makeSubject(); // Collect all forwarded operations const forward = (s: Source) => { return pipe( s, map(op => { forwardedOperations.push(op); return queryResponse; }) ); }; exchangeArgs = { forward, subject: {} as Client }; }); it('forwards query operations correctly', async () => { vi.spyOn(globalThis.console, 'debug').mockImplementation(() => { /** Do NOthing */ }); const { source: ops$, next, complete } = input; const exchange = debugExchange(exchangeArgs)(ops$); publish(exchange); next(queryOperation); complete(); // eslint-disable-next-line no-console expect(console.debug).toBeCalled(); // eslint-disable-next-line no-console expect(console.debug).toBeCalledTimes(2); }); describe('production', () => { beforeEach(() => { process.env.NODE_ENV = 'production'; }); afterEach(() => { process.env.NODE_ENV = 'test'; }); it('is a noop in production', () => { const { source: ops$ } = input; debugExchange({ forward: ops => { expect(ops).toBe(ops$); }, } as any)(ops$); }); }); ================================================ FILE: packages/core/src/exchanges/debug.ts ================================================ import { pipe, tap } from 'wonka'; import type { Exchange } from '../types'; /** Simple log debugger exchange. * * @remarks * An exchange that logs incoming {@link Operation | Operations} and * {@link OperationResult | OperationResults} in development. * * This exchange is a no-op in production and often used in issue reporting * to understand certain usage patterns of `urql` without having access to * the original source code. * * Hint: When you report an issue you’re having with `urql`, adding * this as your first exchange and posting its output can speed up * issue triaging a lot! */ export const debugExchange: Exchange = ({ forward }) => { if (process.env.NODE_ENV === 'production') { return ops$ => forward(ops$); } else { return ops$ => pipe( ops$, // eslint-disable-next-line no-console tap(op => console.debug('[Exchange debug]: Incoming operation: ', op)), forward, tap(result => // eslint-disable-next-line no-console console.debug('[Exchange debug]: Completed operation: ', result) ) ); } }; ================================================ FILE: packages/core/src/exchanges/fallback.test.ts ================================================ import { forEach, fromValue, pipe } from 'wonka'; import { vi, expect, it, beforeEach, afterAll } from 'vitest'; import { queryOperation, teardownOperation } from '../test-utils'; import { fallbackExchange } from './fallback'; const consoleWarn = console.warn; const dispatchDebug = vi.fn(); beforeEach(() => { console.warn = vi.fn(); }); afterAll(() => { console.warn = consoleWarn; }); it('filters all results and warns about input', () => { const res: any[] = []; pipe( fallbackExchange({ dispatchDebug })(fromValue(queryOperation)), forEach(x => res.push(x)) ); expect(res.length).toBe(0); expect(console.warn).toHaveBeenCalled(); }); it('filters all results and does not warn about teardown operations', () => { const res: any[] = []; pipe( fallbackExchange({ dispatchDebug })(fromValue(teardownOperation)), forEach(x => res.push(x)) ); expect(res.length).toBe(0); expect(console.warn).not.toHaveBeenCalled(); }); ================================================ FILE: packages/core/src/exchanges/fallback.ts ================================================ import { filter, pipe, tap } from 'wonka'; import type { ExchangeIO, ExchangeInput } from '../types'; /** Used by the `Client` as the last exchange to warn about unhandled operations. * * @remarks * In a normal setup, some operations may go unhandled when a {@link Client} isn’t set up * with the right exchanges. * For instance, a `Client` may be missing a fetch exchange, or an exchange handling subscriptions. * This {@link Exchange} is added by the `Client` automatically to log warnings about unhandled * {@link Operaiton | Operations} in development. */ export const fallbackExchange: ({ dispatchDebug, }: Pick) => ExchangeIO = ({ dispatchDebug }) => ops$ => { if (process.env.NODE_ENV !== 'production') { ops$ = pipe( ops$, tap(operation => { if ( operation.kind !== 'teardown' && process.env.NODE_ENV !== 'production' ) { const message = `No exchange has handled operations of kind "${operation.kind}". Check whether you've added an exchange responsible for these operations.`; dispatchDebug({ type: 'fallbackCatch', message, operation, }); console.warn(message); } }) ); } // All operations that skipped through the entire exchange chain should be filtered from the output return filter((_x): _x is never => false)(ops$); }; ================================================ FILE: packages/core/src/exchanges/fetch.test.ts ================================================ import { empty, fromValue, pipe, Source, subscribe, toPromise } from 'wonka'; import { vi, expect, it, beforeEach, describe, beforeAll, Mock, afterEach, afterAll, } from 'vitest'; import { Client } from '../client'; import { makeOperation } from '../utils'; import { queryOperation } from '../test-utils'; import { OperationResult } from '../types'; import { fetchExchange } from './fetch'; const fetch = (globalThis as any).fetch as Mock; const abort = vi.fn(); const abortError = new Error(); abortError.name = 'AbortError'; beforeAll(() => { (globalThis as any).AbortController = function AbortController() { this.signal = undefined; this.abort = abort; }; }); afterEach(() => { fetch.mockClear(); abort.mockClear(); }); afterAll(() => { (globalThis as any).AbortController = undefined; }); const response = JSON.stringify({ status: 200, data: { data: { user: 1200, }, }, }); const exchangeArgs = { dispatchDebug: vi.fn(), forward: () => empty as Source, client: { debugTarget: { dispatchEvent: vi.fn(), }, } as any as Client, }; describe('on success', () => { beforeEach(() => { fetch.mockResolvedValue({ status: 200, headers: { get: () => 'application/json' }, text: vi.fn().mockResolvedValue(response), }); }); it('returns response data', async () => { const fetchOptions = vi.fn().mockReturnValue({}); const data = await pipe( fromValue({ ...queryOperation, context: { ...queryOperation.context, fetchOptions, }, }), fetchExchange(exchangeArgs), toPromise ); expect(data).toMatchSnapshot(); expect(fetchOptions).toHaveBeenCalled(); expect(fetch.mock.calls[0][1].body).toMatchSnapshot(); }); }); describe('on error', () => { beforeEach(() => { fetch.mockResolvedValue({ status: 400, headers: { get: () => 'application/json' }, text: vi.fn().mockResolvedValue(JSON.stringify({})), }); }); it('returns error data', async () => { const data = await pipe( fromValue(queryOperation), fetchExchange(exchangeArgs), toPromise ); expect(data).toMatchSnapshot(); }); it('returns error data with status 400 and manual redirect mode', async () => { const fetchOptions = vi.fn().mockReturnValue({ redirect: 'manual' }); const data = await pipe( fromValue({ ...queryOperation, context: { ...queryOperation.context, fetchOptions, }, }), fetchExchange(exchangeArgs), toPromise ); expect(data).toMatchSnapshot(); }); it('ignores the error when a result is available', async () => { fetch.mockResolvedValue({ status: 400, headers: { get: () => 'application/json' }, text: vi.fn().mockResolvedValue(response), }); const data = await pipe( fromValue(queryOperation), fetchExchange(exchangeArgs), toPromise ); expect(data.data).toEqual(JSON.parse(response).data); }); }); describe('on teardown', () => { const fail = () => { expect(true).toEqual(false); }; it('does not start the outgoing request on immediate teardowns', async () => { fetch.mockImplementation(async () => { await new Promise(() => { /*noop*/ }); }); const { unsubscribe } = pipe( fromValue(queryOperation), fetchExchange(exchangeArgs), subscribe(fail) ); unsubscribe(); // NOTE: We can only observe the async iterator's final run after a macro tick await new Promise(resolve => setTimeout(resolve)); expect(fetch).toHaveBeenCalledTimes(0); expect(abort).toHaveBeenCalledTimes(1); }); it('aborts the outgoing request', async () => { fetch.mockResolvedValue({ status: 200, headers: new Map([['Content-Type', 'application/json']]), text: vi.fn().mockResolvedValue('{ "data": null }'), }); const { unsubscribe } = pipe( fromValue(queryOperation), fetchExchange(exchangeArgs), subscribe(() => { /*noop*/ }) ); await new Promise(resolve => setTimeout(resolve)); unsubscribe(); // NOTE: We can only observe the async iterator's final run after a macro tick await new Promise(resolve => setTimeout(resolve)); expect(fetch).toHaveBeenCalledTimes(1); expect(abort).toHaveBeenCalledTimes(1); }); it('does not call the query', () => { fetch.mockResolvedValue(new Response('text', { status: 200 })); pipe( fromValue( makeOperation('teardown', queryOperation, queryOperation.context) ), fetchExchange(exchangeArgs), subscribe(fail) ); expect(fetch).toHaveBeenCalledTimes(0); expect(abort).toHaveBeenCalledTimes(0); }); }); ================================================ FILE: packages/core/src/exchanges/fetch.ts ================================================ /* eslint-disable @typescript-eslint/no-use-before-define */ import { filter, merge, mergeMap, pipe, takeUntil, onPush } from 'wonka'; import type { Exchange } from '../types'; import { makeFetchBody, makeFetchURL, makeFetchOptions, makeFetchSource, } from '../internal'; /** Default GraphQL over HTTP fetch exchange. * * @remarks * The default fetch exchange in `urql` supports sending GraphQL over HTTP * requests, can optionally send GraphQL queries as GET requests, and * handles incremental multipart responses. * * This exchange does not handle persisted queries or multipart uploads. * Support for the former can be added using `@urql/exchange-persisted-fetch` * and the latter using `@urql/exchange-multipart-fetch`. * * Hint: The `fetchExchange` and the two other exchanges all use the built-in fetch * utilities in `@urql/core/internal`, which you can also use to implement * a customized fetch exchange. * * @see {@link makeFetchSource} for the shared utility calling the Fetch API. */ export const fetchExchange: Exchange = ({ forward, dispatchDebug }) => { return ops$ => { const fetchResults$ = pipe( ops$, filter(operation => { return ( operation.kind !== 'teardown' && (operation.kind !== 'subscription' || !!operation.context.fetchSubscriptions) ); }), mergeMap(operation => { const body = makeFetchBody(operation); const url = makeFetchURL(operation, body); const fetchOptions = makeFetchOptions(operation, body); dispatchDebug({ type: 'fetchRequest', message: 'A fetch request is being executed.', operation, data: { url, fetchOptions, }, }); const source = pipe( makeFetchSource(operation, url, fetchOptions), takeUntil( pipe( ops$, filter(op => op.kind === 'teardown' && op.key === operation.key) ) ) ); if (process.env.NODE_ENV !== 'production') { return pipe( source, onPush(result => { const error = !result.data ? result.error : undefined; dispatchDebug({ type: error ? 'fetchError' : 'fetchSuccess', message: `A ${ error ? 'failed' : 'successful' } fetch response has been returned.`, operation, data: { url, fetchOptions, value: error || result, }, }); }) ); } return source; }) ); const forward$ = pipe( ops$, filter(operation => { return ( operation.kind === 'teardown' || (operation.kind === 'subscription' && !operation.context.fetchSubscriptions) ); }), forward ); return merge([fetchResults$, forward$]); }; }; ================================================ FILE: packages/core/src/exchanges/index.ts ================================================ export { ssrExchange } from './ssr'; export { cacheExchange } from './cache'; export { subscriptionExchange } from './subscription'; export { debugExchange } from './debug'; export { fetchExchange } from './fetch'; export { composeExchanges } from './compose'; export type { SerializedResult, SSRExchangeParams, SSRExchange, SSRData, } from './ssr'; export type { SubscriptionOperation, SubscriptionForwarder, SubscriptionExchangeOpts, } from './subscription'; export { mapExchange, mapExchange as errorExchange } from './map'; export type { MapExchangeOpts } from './map'; ================================================ FILE: packages/core/src/exchanges/map.test.ts ================================================ import { map, tap, pipe, fromValue, toArray, toPromise } from 'wonka'; import { vi, expect, describe, it } from 'vitest'; import { Client } from '../client'; import { queryResponse, queryOperation } from '../test-utils'; import { Operation } from '../types'; import { mapExchange } from './map'; import { makeOperation, makeErrorResult, makeResult, CombinedError, } from '../utils'; describe('onOperation', () => { it('triggers and maps on operations', () => { const mockOperation = makeOperation('query', queryOperation, { ...queryOperation.context, mock: true, }); const onOperation = vi.fn().mockReturnValue(mockOperation); const onExchangeResult = vi.fn(); const exchangeArgs = { forward: op$ => pipe( op$, tap(onExchangeResult), map((operation: Operation) => makeResult(operation, { data: null })) ), client: {} as Client, dispatchDebug: () => null, }; pipe( fromValue(queryOperation), mapExchange({ onOperation })(exchangeArgs), toArray ); expect(onOperation).toBeCalledTimes(1); expect(onOperation).toBeCalledWith(queryOperation); expect(onExchangeResult).toBeCalledTimes(1); expect(onExchangeResult).toBeCalledWith(mockOperation); }); it('triggers and forwards identity when returning undefined', () => { const onOperation = vi.fn().mockReturnValue(undefined); const onExchangeResult = vi.fn(); const exchangeArgs = { forward: op$ => pipe( op$, tap(onExchangeResult), map((operation: Operation) => makeResult(operation, { data: null })) ), client: {} as Client, dispatchDebug: () => null, }; pipe( fromValue(queryOperation), mapExchange({ onOperation })(exchangeArgs), toArray ); expect(onOperation).toBeCalledTimes(1); expect(onOperation).toBeCalledWith(queryOperation); expect(onExchangeResult).toBeCalledTimes(1); expect(onExchangeResult).toBeCalledWith(queryOperation); }); it('awaits returned promises as needed', async () => { const mockOperation = makeOperation('query', queryOperation, { ...queryOperation.context, mock: true, }); const onOperation = vi.fn().mockResolvedValue(mockOperation); const onExchangeResult = vi.fn(); const exchangeArgs = { forward: op$ => pipe( op$, tap(onExchangeResult), map((operation: Operation) => makeResult(operation, { data: null })) ), client: {} as Client, dispatchDebug: () => null, }; await pipe( fromValue(queryOperation), mapExchange({ onOperation })(exchangeArgs), toPromise ); expect(onOperation).toBeCalledTimes(1); expect(onOperation).toBeCalledWith(queryOperation); expect(onExchangeResult).toBeCalledTimes(1); expect(onExchangeResult).toBeCalledWith(mockOperation); }); }); describe('onResult', () => { it('triggers and maps on results', async () => { const mockOperation = makeOperation('query', queryOperation, { ...queryOperation.context, mock: true, }); const mockResult = makeErrorResult(mockOperation, new Error('Mock')); const onResult = vi.fn().mockReturnValue(mockResult); const onExchangeResult = vi.fn(); const exchangeArgs = { forward: op$ => pipe( op$, map((operation: Operation) => makeResult(operation, { data: null })) ), client: {} as Client, dispatchDebug: () => null, }; pipe( fromValue(queryOperation), mapExchange({ onResult })(exchangeArgs), tap(onExchangeResult), toArray ); expect(onResult).toBeCalledTimes(1); expect(onResult).toBeCalledWith(makeResult(queryOperation, { data: null })); expect(onExchangeResult).toBeCalledTimes(1); expect(onExchangeResult).toBeCalledWith(mockResult); }); it('triggers and forwards identity when returning undefined', async () => { const onResult = vi.fn().mockReturnValue(undefined); const onExchangeResult = vi.fn(); const exchangeArgs = { forward: op$ => pipe( op$, map((operation: Operation) => makeResult(operation, { data: null })) ), client: {} as Client, dispatchDebug: () => null, }; pipe( fromValue(queryOperation), mapExchange({ onResult })(exchangeArgs), tap(onExchangeResult), toArray ); const result = makeResult(queryOperation, { data: null }); expect(onResult).toBeCalledTimes(1); expect(onResult).toBeCalledWith(result); expect(onExchangeResult).toBeCalledTimes(1); expect(onExchangeResult).toBeCalledWith(result); }); it('awaits returned promises as needed', async () => { const mockOperation = makeOperation('query', queryOperation, { ...queryOperation.context, mock: true, }); const mockResult = makeErrorResult(mockOperation, new Error('Mock')); const onResult = vi.fn().mockResolvedValue(mockResult); const onExchangeResult = vi.fn(); const exchangeArgs = { forward: op$ => pipe( op$, map((operation: Operation) => makeResult(operation, { data: null })) ), client: {} as Client, dispatchDebug: () => null, }; await pipe( fromValue(queryOperation), mapExchange({ onResult })(exchangeArgs), tap(onExchangeResult), toPromise ); expect(onResult).toBeCalledTimes(1); expect(onResult).toBeCalledWith(makeResult(queryOperation, { data: null })); expect(onExchangeResult).toBeCalledTimes(1); expect(onExchangeResult).toBeCalledWith(mockResult); }); }); describe('onError', () => { it('does not trigger when there are no errors', async () => { const onError = vi.fn(); const exchangeArgs = { forward: op$ => pipe( op$, map((operation: Operation) => ({ ...queryResponse, operation })) ), client: {} as Client, dispatchDebug: () => null, }; pipe( fromValue(queryOperation), mapExchange({ onError })(exchangeArgs), toArray ); expect(onError).toBeCalledTimes(0); }); it('triggers correctly when the operations has an error', async () => { const onError = vi.fn(); const error = new Error('Sad times'); const exchangeArgs = { forward: op$ => pipe( op$, map((operation: Operation) => makeErrorResult(operation, error)) ), client: {} as Client, dispatchDebug: () => null, }; pipe( fromValue(queryOperation), mapExchange({ onError })(exchangeArgs), toArray ); expect(onError).toBeCalledTimes(1); expect(onError).toBeCalledWith( new CombinedError({ networkError: error }), queryOperation ); }); }); ================================================ FILE: packages/core/src/exchanges/map.ts ================================================ import { mergeMap, fromValue, fromPromise, pipe } from 'wonka'; import type { Operation, OperationResult, Exchange } from '../types'; import type { CombinedError } from '../utils'; /** Options for the `mapExchange` allowing it to react to incoming operations, results, or errors. */ export interface MapExchangeOpts { /** Accepts a callback for incoming `Operation`s. * * @param operation - An {@link Operation} that the {@link mapExchange} received. * @returns optionally a new {@link Operation} replacing the original. * * @remarks * You may return new {@link Operation | Operations} from this function replacing * the original that the {@link mapExchange} received. * It’s recommended that you use the {@link makeOperation} utility to create a copy * of the original when you do this. (However, this isn’t required) * * Hint: The callback may also be promisified and return a new {@link Operation} asynchronously, * provided you place your {@link mapExchange} after all synchronous {@link Exchange | Exchanges}, * like after your `cacheExchange`. */ onOperation?(operation: Operation): Promise | Operation | void; /** Accepts a callback for incoming `OperationResult`s. * * @param result - An {@link OperationResult} that the {@link mapExchange} received. * @returns optionally a new {@link OperationResult} replacing the original. * * @remarks * This callback may optionally return a new {@link OperationResult} that replaces the original, * which you can use to modify incoming API results. * * Hint: The callback may also be promisified and return a new {@link Operation} asynchronously, * provided you place your {@link mapExchange} after all synchronous {@link Exchange | Exchanges}, * like after your `cacheExchange`. */ onResult?( result: OperationResult ): Promise | OperationResult | void; /** Accepts a callback for incoming `CombinedError`s. * * @param error - A {@link CombinedError} that an incoming {@link OperationResult} contained. * @param operation - The {@link Operation} of the incoming {@link OperationResult}. * * @remarks * The callback may also be promisified and return a new {@link Operation} asynchronously, * provided you place your {@link mapExchange} after all synchronous {@link Exchange | Exchanges}, * like after your `cacheExchange`. */ onError?(error: CombinedError, operation: Operation): void; } /** Creates an `Exchange` mapping over incoming operations, results, and/or errors. * * @param opts - A {@link MapExchangeOpts} configuration object, containing the callbacks the `mapExchange` will use. * @returns the created {@link Exchange} * * @remarks * The `mapExchange` may be used to react to or modify incoming {@link Operation | Operations} * and {@link OperationResult | OperationResults}. Optionally, it can also modify these * asynchronously, when a promise is returned from the callbacks. * * This is useful to, for instance, add an authentication token to a given request, when * the `@urql/exchange-auth` package would be overkill. * * It can also accept an `onError` callback, which can be used to react to incoming * {@link CombinedError | CombinedErrors} on results, and trigger side-effects. * */ export const mapExchange = ({ onOperation, onResult, onError, }: MapExchangeOpts): Exchange => { return ({ forward }) => ops$ => { return pipe( pipe( ops$, mergeMap(operation => { const newOperation = (onOperation && onOperation(operation)) || operation; return 'then' in newOperation ? fromPromise(newOperation) : fromValue(newOperation); }) ), forward, mergeMap(result => { if (onError && result.error) onError(result.error, result.operation); const newResult = (onResult && onResult(result)) || result; return 'then' in newResult ? fromPromise(newResult) : fromValue(newResult); }) ); }; }; ================================================ FILE: packages/core/src/exchanges/ssr.test.ts ================================================ import { makeSubject, pipe, map, publish, forEach, Subject } from 'wonka'; import { vi, expect, it, beforeEach, afterEach } from 'vitest'; import { Client } from '../client'; import { queryOperation, queryResponse } from '../test-utils'; import { ExchangeIO, Operation, OperationResult } from '../types'; import { CombinedError, formatDocument } from '../utils'; import { ssrExchange } from './ssr'; let forward: ExchangeIO; let exchangeInput; let client: Client; let input: Subject; let output; const serializedQueryResponse = { ...queryResponse, data: JSON.stringify(queryResponse.data), }; beforeEach(() => { input = makeSubject(); output = vi.fn(operation => ({ operation })); forward = ops$ => pipe(ops$, map(output)); client = { suspense: true } as any; exchangeInput = { forward, client }; }); afterEach(() => { output.mockClear(); }); it('caches query results correctly', () => { output.mockReturnValueOnce(queryResponse); const ssr = ssrExchange(); const { source: ops$, next } = input; const exchange = ssr(exchangeInput)(ops$); publish(exchange); next(queryOperation); const data = ssr.extractData(); expect(Object.keys(data)).toEqual(['' + queryOperation.key]); expect(data).toEqual({ [queryOperation.key]: { data: serializedQueryResponse.data, error: undefined, hasNext: false, }, }); }); it('serializes query results quickly', () => { const result: OperationResult = { ...queryResponse, operation: queryOperation, data: { user: { name: 'Clive', }, }, }; const serializedQueryResponse = { ...result, data: JSON.stringify(result.data), }; output.mockReturnValueOnce(result); const ssr = ssrExchange(); const { source: ops$, next } = input; const exchange = ssr(exchangeInput)(ops$); publish(exchange); next(queryOperation); result.data.user.name = 'Not Clive'; const data = ssr.extractData(); expect(Object.keys(data)).toEqual(['' + queryOperation.key]); expect(data).toEqual({ [queryOperation.key]: { data: serializedQueryResponse.data, error: undefined, hasNext: false, }, }); }); it('caches errored query results correctly', () => { output.mockReturnValueOnce({ ...queryResponse, data: null, error: new CombinedError({ graphQLErrors: ['Oh no!'], }), }); const ssr = ssrExchange(); const { source: ops$, next } = input; const exchange = ssr(exchangeInput)(ops$); publish(exchange); next(queryOperation); const data = ssr.extractData(); expect(Object.keys(data)).toEqual(['' + queryOperation.key]); expect(data).toEqual({ [queryOperation.key]: { data: 'null', error: { graphQLErrors: [ { extensions: {}, message: 'Oh no!', path: undefined, }, ], networkError: undefined, }, hasNext: false, }, }); }); it('caches extensions when includeExtensions=true', () => { output.mockReturnValueOnce({ ...queryResponse, extensions: { foo: 'bar', }, }); const ssr = ssrExchange({ includeExtensions: true, }); const { source: ops$, next } = input; const exchange = ssr(exchangeInput)(ops$); publish(exchange); next(queryOperation); const data = ssr.extractData(); expect(Object.keys(data)).toEqual(['' + queryOperation.key]); expect(data).toEqual({ [queryOperation.key]: { data: '{"user":{"name":"Clive"}}', extensions: '{"foo":"bar"}', hasNext: false, }, }); }); it('caches complex GraphQLErrors in query results correctly', () => { output.mockReturnValueOnce({ ...queryResponse, data: null, error: new CombinedError({ graphQLErrors: [ { message: 'Oh no!', path: ['Query'], extensions: { test: true }, }, ], }), }); const ssr = ssrExchange(); const { source: ops$, next } = input; const exchange = ssr(exchangeInput)(ops$); publish(exchange); next(queryOperation); const error = ssr.extractData()[queryOperation.key]!.error; expect(error).toHaveProperty('graphQLErrors.0.message', 'Oh no!'); expect(error).toHaveProperty('graphQLErrors.0.path', ['Query']); expect(error).toHaveProperty('graphQLErrors.0.extensions.test', true); }); it('resolves cached query results correctly', () => { const onPush = vi.fn(); const ssr = ssrExchange({ initialState: { [queryOperation.key]: serializedQueryResponse as any }, }); const { source: ops$, next } = input; const exchange = ssr(exchangeInput)(ops$); pipe(exchange, forEach(onPush)); next(queryOperation); const data = ssr.extractData(); expect(Object.keys(data).length).toBe(1); expect(output).not.toHaveBeenCalled(); expect(onPush).toHaveBeenCalledWith({ ...queryResponse, stale: false, hasNext: false, operation: { ...queryResponse.operation, context: { ...queryResponse.operation.context, meta: { cacheOutcome: 'hit', }, }, }, }); }); it('resolves deferred, cached query results correctly', () => { const onPush = vi.fn(); const ssr = ssrExchange({ isClient: true, initialState: { [queryOperation.key]: { ...(serializedQueryResponse as any), hasNext: true, }, }, }); const { source: ops$, next } = input; const exchange = ssr(exchangeInput)(ops$); pipe(exchange, forEach(onPush)); next(queryOperation); const data = ssr.extractData(); expect(Object.keys(data).length).toBe(1); expect(output).toHaveBeenCalledTimes(1); expect(onPush).toHaveBeenCalledTimes(2); expect(onPush.mock.calls[1][0]).toEqual({ ...queryResponse, hasNext: true, stale: false, operation: { ...queryResponse.operation, context: { ...queryResponse.operation.context, meta: { cacheOutcome: 'hit', }, }, }, }); expect(output.mock.calls[0][0].query).toBe( formatDocument(queryOperation.query) ); }); it('deletes cached results in non-suspense environments', async () => { client.suspense = false; const onPush = vi.fn(); const ssr = ssrExchange(); ssr.restoreData({ [queryOperation.key]: serializedQueryResponse as any }); expect(Object.keys(ssr.extractData()).length).toBe(1); const { source: ops$, next } = input; const exchange = ssr(exchangeInput)(ops$); pipe(exchange, forEach(onPush)); next(queryOperation); await Promise.resolve(); expect(Object.keys(ssr.extractData()).length).toBe(0); expect(onPush).toHaveBeenCalledWith({ ...queryResponse, stale: false, hasNext: false, operation: { ...queryResponse.operation, context: { ...queryResponse.operation.context, meta: { cacheOutcome: 'hit', }, }, }, }); // NOTE: The operation should not be duplicated expect(output).not.toHaveBeenCalled(); }); it('never allows restoration of invalidated results', async () => { client.suspense = false; const onPush = vi.fn(); const initialState = { [queryOperation.key]: serializedQueryResponse as any }; const ssr = ssrExchange({ isClient: true, initialState: { ...initialState }, }); const { source: ops$, next } = input; const exchange = ssr(exchangeInput)(ops$); pipe(exchange, forEach(onPush)); next(queryOperation); await Promise.resolve(); expect(Object.keys(ssr.extractData()).length).toBe(0); expect(onPush).toHaveBeenCalledTimes(1); expect(output).not.toHaveBeenCalled(); ssr.restoreData(initialState); expect(Object.keys(ssr.extractData()).length).toBe(0); next(queryOperation); expect(onPush).toHaveBeenCalledTimes(2); expect(output).toHaveBeenCalledTimes(1); }); ================================================ FILE: packages/core/src/exchanges/ssr.ts ================================================ import type { GraphQLError } from '../utils/graphql'; import { pipe, filter, merge, map, tap } from 'wonka'; import type { Exchange, OperationResult, Operation } from '../types'; import { addMetadata, CombinedError } from '../utils'; import { reexecuteOperation, mapTypeNames } from './cache'; /** A serialized version of an {@link OperationResult}. * * @remarks * All properties are serialized separately as JSON strings, except for the * {@link CombinedError} to speed up JS parsing speed, even if a result doesn’t * end up being used. * * @internal */ export interface SerializedResult { hasNext?: boolean; /** JSON-serialized version of {@link OperationResult.data}. */ data?: string | undefined; // JSON string of data /** JSON-serialized version of {@link OperationResult.extensions}. */ extensions?: string | undefined; /** JSON version of {@link CombinedError}. */ error?: { graphQLErrors: Array | string>; networkError?: string; }; } /** A dictionary of {@link Operation.key} keys to serializable {@link SerializedResult} objects. * * @remarks * It’s not recommended to modify the serialized data manually, however, multiple payloads of * this dictionary may safely be merged and combined. */ export interface SSRData { [key: string]: SerializedResult; } /** Options for the `ssrExchange` allowing it to either operate on the server- or client-side. */ export interface SSRExchangeParams { /** Indicates to the {@link SSRExchange} whether it's currently in server-side or client-side mode. * * @remarks * Depending on this option, the {@link SSRExchange} will either capture or replay results. * When `true`, it’s in client-side mode and results will be serialized. When `false`, it’ll * use its deserialized data and replay results from it. */ isClient?: boolean; /** May be used on the client-side to pass the {@link SSRExchange} serialized data from the server-side. * * @remarks * Alternatively, {@link SSRExchange.restoreData} may be called to imperatively add serialized data to * the exchange. * * Hint: This method also works on the server-side to add to the initial serialized data, which enables * you to combine multiple {@link SSRExchange} results, as needed. */ initialState?: SSRData; /** Forces a new API request to be sent in the background after replaying the deserialized result. * * @remarks * Similarly to the `cache-and-network` {@link RequestPolicy}, this option tells the {@link SSRExchange} * to send a new API request for the {@link Operation} after replaying a serialized result. * * Hint: This is useful when you're caching SSR results and need the client-side to update itself after * rendering the initial serialized SSR results. */ staleWhileRevalidate?: boolean; /** Forces {@link OperationResult.extensions} to be serialized alongside the rest of a result. * * @remarks * Entries in the `extension` object of a GraphQL result are often non-standard metdata, and many * APIs use it for data that changes between every request. As such, the {@link SSRExchange} will * not serialize this data by default, unless this flag is set. */ includeExtensions?: boolean; } /** An `SSRExchange` either in server-side mode, serializing results, or client-side mode, deserializing and replaying results.. * * @remarks * This same {@link Exchange} is used in your code both for the client-side and server-side as it’s “universal” * and can be put into either client-side or server-side mode using the {@link SSRExchangeParams.isClient} flag. * * In server-side mode, the `ssrExchange` will “record” results it sees from your API and provide them for you * to send to the client-side using the {@link SSRExchange.extractData} method. * * In client-side mode, the `ssrExchange` will use these serialized results, rehydrated either using * {@link SSRExchange.restoreData} or {@link SSRexchangeParams.initialState}, to replay results the * server-side has seen and sent before. * * Each serialized result will only be replayed once, as it’s assumed that your cache exchange will have the * results cached afterwards. */ export interface SSRExchange extends Exchange { /** Client-side method to add serialized results to the {@link SSRExchange}. * @param data - {@link SSRData}, */ restoreData(data: SSRData): void; /** Server-side method to get all serialized results the {@link SSRExchange} has captured. * @returns an {@link SSRData} dictionary. */ extractData(): SSRData; } /** Serialize an OperationResult to plain JSON */ const serializeResult = ( result: OperationResult, includeExtensions: boolean ): SerializedResult => { const serialized: SerializedResult = { hasNext: result.hasNext, }; if (result.data !== undefined) { serialized.data = JSON.stringify(result.data); } if (includeExtensions && result.extensions !== undefined) { serialized.extensions = JSON.stringify(result.extensions); } if (result.error) { serialized.error = { graphQLErrors: result.error.graphQLErrors.map(error => { if (!error.path && !error.extensions) return error.message; return { message: error.message, path: error.path, extensions: error.extensions, }; }), }; if (result.error.networkError) { serialized.error.networkError = '' + result.error.networkError; } } return serialized; }; /** Deserialize plain JSON to an OperationResult * @internal */ const deserializeResult = ( operation: Operation, result: SerializedResult, includeExtensions: boolean ): OperationResult => ({ operation, data: result.data ? JSON.parse(result.data) : undefined, extensions: includeExtensions && result.extensions ? JSON.parse(result.extensions) : undefined, error: result.error ? new CombinedError({ networkError: result.error.networkError ? new Error(result.error.networkError) : undefined, graphQLErrors: result.error.graphQLErrors, }) : undefined, stale: false, hasNext: !!result.hasNext, }); const revalidated = new Set(); /** Creates a server-side rendering `Exchange` that either captures responses on the server-side or replays them on the client-side. * * @param params - An {@link SSRExchangeParams} configuration object. * @returns the created {@link SSRExchange} * * @remarks * When dealing with server-side rendering, we essentially have two {@link Client | Clients} making requests, * the server-side client, and the client-side one. The `ssrExchange` helps implementing a tiny cache on both * sides that: * * - captures results on the server-side which it can serialize, * - replays results on the client-side that it deserialized from the server-side. * * Hint: The `ssrExchange` is basically an exchange that acts like a replacement for any fetch exchange * temporarily. As such, you should place it after your cache exchange but in front of any fetch exchange. */ export const ssrExchange = (params: SSRExchangeParams = {}): SSRExchange => { const staleWhileRevalidate = !!params.staleWhileRevalidate; const includeExtensions = !!params.includeExtensions; const data: Record = {}; // On the client-side, we delete results from the cache as they're resolved // this is delayed so that concurrent queries don't delete each other's data const invalidateQueue: number[] = []; const invalidate = (result: OperationResult) => { invalidateQueue.push(result.operation.key); if (invalidateQueue.length === 1) { Promise.resolve().then(() => { let key: number | void; while ((key = invalidateQueue.shift())) { data[key] = null; } }); } }; // The SSR Exchange is a temporary cache that can populate results into data for suspense // On the client it can be used to retrieve these temporary results from a rehydrated cache const ssr: SSRExchange = ({ client, forward }) => ops$ => { // params.isClient tells us whether we're on the client-side // By default we assume that we're on the client if suspense-mode is disabled const isClient = params && typeof params.isClient === 'boolean' ? !!params.isClient : !client.suspense; let forwardedOps$ = pipe( ops$, filter( operation => operation.kind === 'teardown' || !data[operation.key] || !!data[operation.key]!.hasNext || operation.context.requestPolicy === 'network-only' ), map(mapTypeNames), forward ); // NOTE: Since below we might delete the cached entry after accessing // it once, cachedOps$ needs to be merged after forwardedOps$ let cachedOps$ = pipe( ops$, filter( operation => operation.kind !== 'teardown' && !!data[operation.key] && operation.context.requestPolicy !== 'network-only' ), map(op => { const serialized = data[op.key]!; const cachedResult = deserializeResult( op, serialized, includeExtensions ); if (staleWhileRevalidate && !revalidated.has(op.key)) { cachedResult.stale = true; revalidated.add(op.key); reexecuteOperation(client, op); } const result: OperationResult = { ...cachedResult, operation: addMetadata(op, { cacheOutcome: 'hit', }), }; return result; }) ); if (!isClient) { // On the server we cache results in the cache as they're resolved forwardedOps$ = pipe( forwardedOps$, tap((result: OperationResult) => { const { operation } = result; if (operation.kind !== 'mutation') { const serialized = serializeResult(result, includeExtensions); data[operation.key] = serialized; } }) ); } else { // On the client we delete results from the cache as they're resolved cachedOps$ = pipe(cachedOps$, tap(invalidate)); } return merge([forwardedOps$, cachedOps$]); }; ssr.restoreData = (restore: SSRData) => { for (const key in restore) { // We only restore data that hasn't been previously invalidated if (data[key] !== null) { data[key] = restore[key]; } } }; ssr.extractData = () => { const result: SSRData = {}; for (const key in data) if (data[key] != null) result[key] = data[key]!; return result; }; if (params && params.initialState) { ssr.restoreData(params.initialState); } return ssr; }; ================================================ FILE: packages/core/src/exchanges/subscription.test.ts ================================================ import { vi, expect, it } from 'vitest'; import { empty, publish, fromValue, pipe, Source, take, toPromise, } from 'wonka'; import { Client } from '../client'; import { subscriptionOperation, subscriptionResult } from '../test-utils'; import { stringifyDocument } from '../utils'; import { OperationResult } from '../types'; import { subscriptionExchange, SubscriptionForwarder } from './subscription'; it('should return response data from forwardSubscription observable', async () => { const exchangeArgs = { dispatchDebug: vi.fn(), forward: () => empty as Source, client: {} as Client, }; const unsubscribe = vi.fn(); const forwardSubscription: SubscriptionForwarder = operation => { expect(operation.query).toBe( stringifyDocument(subscriptionOperation.query) ); expect(operation.variables).toBe(subscriptionOperation.variables); return { subscribe(observer) { Promise.resolve().then(() => { observer.next(subscriptionResult); }); return { unsubscribe }; }, }; }; const data = await pipe( fromValue(subscriptionOperation), subscriptionExchange({ forwardSubscription })(exchangeArgs), take(1), toPromise ); expect(data).toMatchSnapshot(); expect(unsubscribe).toHaveBeenCalled(); }); it('should tear down the operation if the source subscription ends', async () => { const reexecuteOperation = vi.fn(); const unsubscribe = vi.fn(); const exchangeArgs = { dispatchDebug: vi.fn(), forward: () => empty as Source, client: { reexecuteOperation: reexecuteOperation as any } as Client, }; const forwardSubscription: SubscriptionForwarder = () => ({ subscribe(observer) { observer.complete(); return { unsubscribe }; }, }); pipe( fromValue(subscriptionOperation), subscriptionExchange({ forwardSubscription })(exchangeArgs), publish ); await Promise.resolve(); expect(unsubscribe).not.toHaveBeenCalled(); expect(reexecuteOperation).toHaveBeenCalled(); }); it('should allow providing a custom isSubscriptionOperation implementation', async () => { const exchangeArgs = { dispatchDebug: vi.fn(), forward: () => empty as Source, client: {} as Client, }; const isSubscriptionOperation = vi.fn(() => true); const forwardSubscription: SubscriptionForwarder = () => ({ subscribe(observer) { observer.next(subscriptionResult); return { unsubscribe: vi.fn() }; }, }); await pipe( fromValue(subscriptionOperation), subscriptionExchange({ forwardSubscription, isSubscriptionOperation })( exchangeArgs ), take(1), toPromise ); expect(isSubscriptionOperation).toHaveBeenCalled(); }); ================================================ FILE: packages/core/src/exchanges/subscription.ts ================================================ import type { Subscription, Source } from 'wonka'; import { filter, make, merge, mergeMap, pipe, takeUntil } from 'wonka'; import { makeResult, mergeResultPatch, makeErrorResult, makeOperation, } from '../utils'; import type { Exchange, ExecutionResult, Operation, OperationResult, } from '../types'; import type { FetchBody } from '../internal'; import { makeFetchBody } from '../internal'; /** An abstract observer-like interface. * * @remarks * Observer-like interfaces are passed to {@link ObservableLike.subscribe} to provide them * with callbacks for their events. * * @see {@link https://github.com/tc39/proposal-observable} for the full TC39 Observable proposal. */ export interface ObserverLike { /** Callback for values an {@link ObservableLike} emits. */ next: (value: T) => void; /** Callback for an error an {@link ObservableLike} emits, which ends the subscription. */ error: (err: any) => void; /** Callback for the completion of an {@link ObservableLike}, which ends the subscription. */ complete: () => void; } /** An abstract observable-like interface. * * @remarks * Observable, or Observable-like interfaces, are often used by GraphQL transports to abstract * how they send {@link ExecutionResult | ExecutionResults} to consumers. These generally contain * a `subscribe` method accepting an {@link ObserverLike} structure. * * @see {@link https://github.com/tc39/proposal-observable} for the full TC39 Observable proposal. */ export interface ObservableLike { /** Start the Observable-like subscription and returns a subscription handle. * * @param observer - an {@link ObserverLike} object with result, error, and completion callbacks. * @returns a subscription handle providing an `unsubscribe` method to stop the subscription. */ subscribe(observer: ObserverLike): { unsubscribe: () => void; }; } /** A more cross-compatible version of the {@link GraphQLRequest} structure. * {@link FetchBody} for more details */ export type SubscriptionOperation = FetchBody; /** A subscription forwarding function, which must accept a {@link SubscriptionOperation}. * * @param operation - A {@link SubscriptionOperation} * @returns An {@link ObservableLike} object issuing {@link ExecutionResult | ExecutionResults}. */ export type SubscriptionForwarder = ( request: FetchBody, operation: Operation ) => ObservableLike; /** This is called to create a subscription and needs to be hooked up to a transport client. */ export interface SubscriptionExchangeOpts { /** A subscription forwarding function, which must accept a {@link SubscriptionOperation}. * * @param operation - A {@link SubscriptionOperation} * @returns An {@link ObservableLike} object issuing {@link ExecutionResult | ExecutionResults}. * * @remarks * This callback is called for each {@link Operation} that this `subscriptionExchange` will * handle. It receives the {@link SubscriptionOperation}, which is a more compatible version * of the raw {@link Operation} objects and must return an {@link ObservableLike} of results. */ forwardSubscription: SubscriptionForwarder; /** Flag to enable this exchange to handle all types of GraphQL operations. * * @remarks * When you aren’t using fetch exchanges and GraphQL over HTTP as a transport for your GraphQL requests, * or you have a third-party GraphQL transport implementation, which must also be used for queries and * mutations, this flag may be used to allow this exchange to handle all kinds of GraphQL operations. * * By default, this flag is `false` and the exchange will only handle GraphQL subscription operations. */ enableAllOperations?: boolean; /** A predicate function that causes an operation to be handled by this `subscriptionExchange` if `true` is returned. * * @param operation - an {@link Operation} * @returns true when the operation is handled by this exchange. * * @remarks * In some cases, a `subscriptionExchange` will be used to only handle some {@link Operation | Operations}, * e.g. all that contain `@live` directive. For these cases, this function may be passed to precisely * determine which `Operation`s this exchange should handle, instead of forwarding. * * When specified, the {@link SubscriptionExchangeOpts.enableAllOperations} flag is disregarded. */ isSubscriptionOperation?: (operation: Operation) => boolean; } /** Generic subscription exchange factory used to either create an exchange handling just subscriptions or all operation kinds. * * @remarks * `subscriptionExchange` can be used to create an {@link Exchange} that either * handles just GraphQL subscription operations, or optionally all operations, * when the {@link SubscriptionExchangeOpts.enableAllOperations} flag is passed. * * The {@link SubscriptionExchangeOpts.forwardSubscription} function must * be provided and provides a generic input that's based on {@link Operation} * but is compatible with many libraries implementing GraphQL request or * subscription interfaces. */ export const subscriptionExchange = ({ forwardSubscription, enableAllOperations, isSubscriptionOperation, }: SubscriptionExchangeOpts): Exchange => ({ client, forward }) => { const createSubscriptionSource = ( operation: Operation ): Source => { const observableish = forwardSubscription( makeFetchBody(operation), operation ); return make(observer => { let isComplete = false; let sub: Subscription | void; let result: OperationResult | void; function nextResult(value: ExecutionResult) { observer.next( (result = result ? mergeResultPatch(result, value) : makeResult(operation, value)) ); } Promise.resolve().then(() => { if (isComplete) return; sub = observableish.subscribe({ next: nextResult, error(error) { if (Array.isArray(error)) { // NOTE: This is an exception for transports that deliver `GraphQLError[]`, as part // of the observer’s error callback (may happen as part of `graphql-ws`). // We only check for arrays here, as this is an extremely “unexpected” case as the // shape of `ExecutionResult` is instead strictly defined. nextResult({ errors: error }); } else { observer.next(makeErrorResult(operation, error)); } observer.complete(); }, complete() { if (!isComplete) { isComplete = true; if (operation.kind === 'subscription') { client.reexecuteOperation( makeOperation('teardown', operation, operation.context) ); } if (result && result.hasNext) { nextResult({ hasNext: false }); } observer.complete(); } }, }); }); return () => { isComplete = true; if (sub) sub.unsubscribe(); }; }); }; const isSubscriptionOperationFn = isSubscriptionOperation || (operation => operation.kind === 'subscription' || (!!enableAllOperations && (operation.kind === 'query' || operation.kind === 'mutation'))); return ops$ => { const subscriptionResults$ = pipe( ops$, filter( operation => operation.kind !== 'teardown' && isSubscriptionOperationFn(operation) ), mergeMap(operation => { const { key } = operation; const teardown$ = pipe( ops$, filter(op => op.kind === 'teardown' && op.key === key) ); return pipe( createSubscriptionSource(operation), takeUntil(teardown$) ); }) ); const forward$ = pipe( ops$, filter( operation => operation.kind === 'teardown' || !isSubscriptionOperationFn(operation) ), forward ); return merge([subscriptionResults$, forward$]); }; }; ================================================ FILE: packages/core/src/gql.test.ts ================================================ import { parse, print } from '@0no-co/graphql.web'; import { vi, expect, it, beforeEach, MockInstance } from 'vitest'; import { gql } from './gql'; import { keyDocument } from './utils'; let warn: MockInstance; beforeEach(() => { warn = vi.spyOn(console, 'warn'); warn.mockClear(); }); it('parses GraphQL Documents', () => { const doc = gql` { gql testing } `; expect(doc.definitions).toEqual( parse('{ gql testing }', { noLocation: true }).definitions ); expect(doc).toBe(keyDocument('{\n gql\n testing\n}')); expect(doc.loc).toEqual({ start: 0, end: 19, source: expect.anything(), }); }); it('deduplicates fragments', () => { const frag = gql` fragment Test on Test { testField } `; const doc = gql` query { ...Test } ${frag} ${frag} `; expect(doc.definitions.length).toBe(2); expect(warn).not.toHaveBeenCalled(); }); it('warns on duplicate fragment names with different sources', () => { const frag = gql` fragment Test on Test { testField } `; const duplicate = gql` fragment Test on Test { otherField } `; const doc = gql` query { ...Test } ${frag} ${duplicate} `; expect(warn).toHaveBeenCalledTimes(1); expect(doc.definitions.length).toBe(2); }); it('interpolates nested GraphQL Documents', () => { expect( print(gql` query { ...Query } ${gql` fragment Query on Query { field } `} `) ).toMatchInlineSnapshot(` "{ ...Query } fragment Query on Query { field }" `); }); it('interpolates strings', () => { expect( print( gql` query { ${'field'} } ` ) ).toMatchInlineSnapshot(` "{ field }" `); }); ================================================ FILE: packages/core/src/gql.ts ================================================ /* eslint-disable prefer-rest-params */ import { Kind } from '@0no-co/graphql.web'; import type { DocumentNode, DefinitionNode } from './utils/graphql'; import type { AnyVariables, TypedDocumentNode } from './types'; import { keyDocument, stringifyDocument } from './utils'; /** A GraphQL parse function, which may be called as a tagged template literal, returning a parsed {@link DocumentNode}. * * @remarks * The `gql` tag or function is used to parse a GraphQL query document into a {@link DocumentNode}. * * When used as a tagged template, `gql` will automatically merge fragment definitions into the resulting * document and deduplicate them. * * It enforces that all fragments have a unique name. When fragments with different definitions share a name, * it will log a warning in development. * * Hint: It’s recommended to use this `gql` function over other GraphQL parse functions, since it puts the parsed * results directly into `@urql/core`’s internal caches and prevents further unnecessary work. * * @example * ```ts * const AuthorFragment = gql` * fragment AuthorDisplayComponent on Author { * id * name * } * `; * * const BookFragment = gql` * fragment ListBookComponent on Book { * id * title * author { * ...AuthorDisplayComponent * } * } * * ${AuthorFragment} * `; * * const BookQuery = gql` * query Book($id: ID!) { * book(id: $id) { * ...BookFragment * } * } * * ${BookFragment} * `; * ``` */ function gql( strings: TemplateStringsArray, ...interpolations: Array ): TypedDocumentNode; function gql( string: string ): TypedDocumentNode; function gql(parts: string | TemplateStringsArray /* arguments */) { const fragmentNames = new Map(); const definitions: DefinitionNode[] = []; const source: DocumentNode[] = []; // Apply the entire tagged template body's definitions let body: string = Array.isArray(parts) ? parts[0] : parts || ''; for (let i = 1; i < arguments.length; i++) { const value = arguments[i]; if (value && value.definitions) { source.push(value); } else { body += value; } body += arguments[0][i]; } source.unshift(keyDocument(body)); for (let i = 0; i < source.length; i++) { for (let j = 0; j < source[i].definitions.length; j++) { const definition = source[i].definitions[j]; if (definition.kind === Kind.FRAGMENT_DEFINITION) { const name = definition.name.value; const value = stringifyDocument(definition); // Fragments will be deduplicated according to this Map if (!fragmentNames.has(name)) { fragmentNames.set(name, value); definitions.push(definition); } else if ( process.env.NODE_ENV !== 'production' && fragmentNames.get(name) !== value ) { // Fragments with the same names is expected to have the same contents console.warn( '[WARNING: Duplicate Fragment] A fragment with name `' + name + '` already exists in this document.\n' + 'While fragment names may not be unique across your source, each name must be unique per document.' ); } } else { definitions.push(definition); } } } return keyDocument({ kind: Kind.DOCUMENT, definitions, }); } export { gql }; ================================================ FILE: packages/core/src/index.ts ================================================ export { gql } from './gql'; export * from './client'; export * from './exchanges'; export * from './types'; export { CombinedError, stringifyVariables, stringifyDocument, createRequest, makeResult, makeErrorResult, mergeResultPatch, formatDocument, makeOperation, getOperationName, } from './utils'; ================================================ FILE: packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`on error > ignores the error when a result is available 1`] = ` { "data": undefined, "error": [CombinedError: [Network] Forbidden], "extensions": undefined, "hasNext": false, "operation": { "context": { "fetchOptions": { "method": "POST", }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 2, "kind": "query", "query": { "__key": -2395444236, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "getUser", }, "operation": "query", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "name", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "id", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "firstName", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "lastName", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 92, "source": { "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "name": "Clara", }, }, "stale": false, } `; exports[`on error > returns error data 1`] = ` { "data": undefined, "error": [CombinedError: [Network] Forbidden], "extensions": undefined, "hasNext": false, "operation": { "context": { "fetchOptions": { "method": "POST", }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 2, "kind": "query", "query": { "__key": -2395444236, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "getUser", }, "operation": "query", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "name", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "id", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "firstName", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "lastName", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 92, "source": { "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "name": "Clara", }, }, "stale": false, } `; exports[`on error > returns error data with status 400 and manual redirect mode 1`] = ` { "data": undefined, "error": [CombinedError: [Network] Forbidden], "extensions": undefined, "hasNext": false, "operation": { "context": { "fetchOptions": { "method": "POST", }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 2, "kind": "query", "query": { "__key": -2395444236, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "getUser", }, "operation": "query", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "name", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "id", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "firstName", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "lastName", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 92, "source": { "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "name": "Clara", }, }, "stale": false, } `; exports[`on error with non spec-compliant body > handles network errors 1`] = ` { "data": undefined, "error": [CombinedError: [Network] Forbidden], "extensions": undefined, "hasNext": false, "operation": { "context": { "fetchOptions": { "method": "POST", }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 2, "kind": "query", "query": { "__key": -2395444236, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "getUser", }, "operation": "query", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "name", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "id", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "firstName", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "lastName", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 92, "source": { "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "name": "Clara", }, }, "stale": false, } `; exports[`on success > returns response data 1`] = ` { "data": { "data": { "user": 1200, }, }, "error": undefined, "extensions": undefined, "hasNext": false, "operation": { "context": { "fetchOptions": { "method": "POST", }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 2, "kind": "query", "query": { "__key": -2395444236, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "getUser", }, "operation": "query", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "name", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "id", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "firstName", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "lastName", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 92, "source": { "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "name": "Clara", }, }, "stale": false, } `; exports[`on success > uses the mock fetch if given 1`] = ` { "data": { "data": { "user": 1200, }, }, "error": undefined, "extensions": undefined, "hasNext": false, "operation": { "context": { "fetch": [MockFunction spy] { "calls": [ [ "https://test.com/graphql", { "signal": undefined, }, ], ], "results": [ { "type": "return", "value": Promise {}, }, ], }, "fetchOptions": { "method": "POST", }, "requestPolicy": "cache-first", "url": "http://localhost:3000/graphql", }, "key": 2, "kind": "query", "query": { "__key": -2395444236, "definitions": [ { "directives": undefined, "kind": "OperationDefinition", "name": { "kind": "Name", "value": "getUser", }, "operation": "query", "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": [ { "kind": "Argument", "name": { "kind": "Name", "value": "name", }, "value": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "user", }, "selectionSet": { "kind": "SelectionSet", "selections": [ { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "id", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "firstName", }, "selectionSet": undefined, }, { "alias": undefined, "arguments": undefined, "directives": undefined, "kind": "Field", "name": { "kind": "Name", "value": "lastName", }, "selectionSet": undefined, }, ], }, }, ], }, "variableDefinitions": [ { "defaultValue": undefined, "directives": undefined, "kind": "VariableDefinition", "type": { "kind": "NamedType", "name": { "kind": "Name", "value": "String", }, }, "variable": { "kind": "Variable", "name": { "kind": "Name", "value": "name", }, }, }, ], }, ], "kind": "Document", "loc": { "end": 92, "source": { "body": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "locationOffset": { "column": 1, "line": 1, }, "name": "gql", }, "start": 0, }, }, "variables": { "name": "Clara", }, }, "stale": false, } `; ================================================ FILE: packages/core/src/internal/fetchOptions.test.ts ================================================ // @vitest-environment jsdom import { expect, describe, it } from 'vitest'; import { Kind } from '@0no-co/graphql.web'; import { makeOperation } from '../utils/operation'; import { queryOperation, mutationOperation } from '../test-utils'; import { makeFetchBody, makeFetchURL, makeFetchOptions } from './fetchOptions'; describe('makeFetchBody', () => { it('creates a fetch body', () => { const body = makeFetchBody(queryOperation); expect(body).toMatchInlineSnapshot(` { "documentId": undefined, "extensions": undefined, "operationName": "getUser", "query": "query getUser($name: String) { user(name: $name) { id firstName lastName } }", "variables": { "name": "Clara", }, } `); }); it('omits the query property when APQ is set', () => { const apqOperation = makeOperation(queryOperation.kind, queryOperation); apqOperation.extensions = { ...apqOperation.extensions, persistedQuery: { version: 1, sha256Hash: '[test]', }, }; expect(makeFetchBody(apqOperation).query).toBe(undefined); apqOperation.extensions.persistedQuery!.miss = true; expect(makeFetchBody(apqOperation).query).not.toBe(undefined); }); it('omits the query property when query is a persisted document', () => { // A persisted documents is one that carries a `documentId` property and // has no definitions const persistedOperation = makeOperation(queryOperation.kind, { ...queryOperation, query: { kind: Kind.DOCUMENT, definitions: [], documentId: 'TestDocumentId', }, }); expect(makeFetchBody(persistedOperation).query).toBe(undefined); expect(makeFetchBody(persistedOperation).documentId).toBe('TestDocumentId'); }); }); describe('makeFetchURL', () => { it('returns the URL by default', () => { const body = makeFetchBody(queryOperation); expect(makeFetchURL(queryOperation, body)).toBe( 'http://localhost:3000/graphql' ); }); it('returns the URL by default when only a path is provided', () => { const operation = makeOperation(queryOperation.kind, queryOperation, { url: '/graphql', }); const body = makeFetchBody(operation); expect(makeFetchURL(operation, body)).toBe('/graphql'); }); it('returns a query parameter URL when GET is preferred', () => { const operation = makeOperation(queryOperation.kind, queryOperation, { ...queryOperation.context, preferGetMethod: true, }); const body = makeFetchBody(operation); expect(makeFetchURL(operation, body)).toMatchInlineSnapshot( '"http://localhost:3000/graphql?query=query+getUser%28%24name%3A+String%29+%7B%0A++user%28name%3A+%24name%29+%7B%0A++++id%0A++++firstName%0A++++lastName%0A++%7D%0A%7D&operationName=getUser&variables=%7B%22name%22%3A%22Clara%22%7D"' ); }); it('returns a query parameter URL when GET is preferred and only a path is provided', () => { const operation = makeOperation(queryOperation.kind, queryOperation, { url: '/graphql', preferGetMethod: true, }); const body = makeFetchBody(operation); expect(makeFetchURL(operation, body)).toMatchInlineSnapshot( '"/graphql?query=query+getUser%28%24name%3A+String%29+%7B%0A++user%28name%3A+%24name%29+%7B%0A++++id%0A++++firstName%0A++++lastName%0A++%7D%0A%7D&operationName=getUser&variables=%7B%22name%22%3A%22Clara%22%7D"' ); }); it('returns the URL without query parameters when it exceeds given length', () => { const operation = makeOperation(queryOperation.kind, queryOperation, { ...queryOperation.context, preferGetMethod: true, }); operation.variables = { ...operation.variables, test: 'x'.repeat(2048), }; const body = makeFetchBody(operation); expect(makeFetchURL(operation, body)).toBe('http://localhost:3000/graphql'); // Resets the `preferGetMethod` field expect(operation.context.preferGetMethod).toBe(false); }); it('returns the URL without query parameters for mutations', () => { const operation = makeOperation(mutationOperation.kind, mutationOperation, { ...mutationOperation.context, preferGetMethod: true, }); const body = makeFetchBody(operation); expect(makeFetchURL(operation, body)).toBe('http://localhost:3000/graphql'); }); }); describe('makeFetchOptions', () => { it('creates a JSON request by default', () => { const body = makeFetchBody(queryOperation); expect(makeFetchOptions(queryOperation, body)).toMatchInlineSnapshot(` { "body": "{"operationName":"getUser","query":"query getUser($name: String) {\\n user(name: $name) {\\n id\\n firstName\\n lastName\\n }\\n}","variables":{"name":"Clara"}}", "headers": { "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed", "content-type": "application/json", }, "method": "POST", } `); }); it('handles the Headers object', () => { const headers = new Headers(); headers.append('x-test', 'true'); const operation = makeOperation(queryOperation.kind, queryOperation, { ...queryOperation.context, fetchOptions: { headers, }, }); const body = makeFetchBody(operation); expect(makeFetchOptions(operation, body)).toMatchInlineSnapshot(` { "body": "{"operationName":"getUser","query":"query getUser($name: String) {\\n user(name: $name) {\\n id\\n firstName\\n lastName\\n }\\n}","variables":{"name":"Clara"}}", "headers": { "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed", "content-type": "application/json", "x-test": "true", }, "method": "POST", } `); }); it('handles an Array headers init', () => { const operation = makeOperation(queryOperation.kind, queryOperation, { ...queryOperation.context, fetchOptions: { headers: [['x-test', 'true']], }, }); const body = makeFetchBody(operation); expect(makeFetchOptions(operation, body)).toMatchInlineSnapshot(` { "body": "{"operationName":"getUser","query":"query getUser($name: String) {\\n user(name: $name) {\\n id\\n firstName\\n lastName\\n }\\n}","variables":{"name":"Clara"}}", "headers": { "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed", "content-type": "application/json", "x-test": "true", }, "method": "POST", } `); }); it('creates a GET request when preferred for query operations', () => { const operation = makeOperation(queryOperation.kind, queryOperation, { ...queryOperation.context, preferGetMethod: 'force', }); const body = makeFetchBody(operation); expect(makeFetchOptions(operation, body)).toMatchInlineSnapshot(` { "body": undefined, "headers": { "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed", }, "method": "GET", } `); }); it('creates a POST multipart request when a file is detected', () => { const operation = makeOperation(mutationOperation.kind, mutationOperation); operation.variables = { ...operation.variables, file: new Blob(), }; const body = makeFetchBody(operation); const options = makeFetchOptions(operation, body); expect(options).toMatchInlineSnapshot(` { "body": FormData {}, "headers": { "accept": "application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed", }, "method": "POST", } `); expect(options.body).toBeInstanceOf(FormData); const form = options.body as FormData; expect(JSON.parse(form.get('operations') as string)).toEqual({ ...body, variables: { ...body.variables, file: null, }, }); expect(form.get('map')).toMatchInlineSnapshot(`"{"0":["variables.file"]}"`); expect(form.get('0')).toBeInstanceOf(Blob); }); }); ================================================ FILE: packages/core/src/internal/fetchOptions.ts ================================================ import { stringifyDocument, getOperationName, stringifyVariables, extractFiles, } from '../utils'; import type { AnyVariables, GraphQLRequest, Operation } from '../types'; /** Abstract definition of the JSON data sent during GraphQL HTTP POST requests. */ export interface FetchBody { query?: string; documentId?: string; operationName: string | undefined; variables: undefined | Record; extensions: undefined | Record; } /** Creates a GraphQL over HTTP compliant JSON request body. * @param request - An object containing a `query` document and `variables`. * @returns A {@link FetchBody} * @see {@link https://github.com/graphql/graphql-over-http} for the GraphQL over HTTP spec. */ export function makeFetchBody< Data = any, Variables extends AnyVariables = AnyVariables, >(request: Omit, 'key'>): FetchBody { const body: FetchBody = { query: undefined, documentId: undefined, operationName: getOperationName(request.query), variables: request.variables || undefined, extensions: request.extensions, }; if ( 'documentId' in request.query && request.query.documentId && // NOTE: We have to check that the document will definitely be sent // as a persisted document to avoid breaking changes (!request.query.definitions || !request.query.definitions.length) ) { body.documentId = request.query.documentId; } else if ( !request.extensions || !request.extensions.persistedQuery || !!request.extensions.persistedQuery.miss ) { body.query = stringifyDocument(request.query); } return body; } /** Creates a URL that will be called for a GraphQL HTTP request. * * @param operation - An {@link Operation} for which to make the request. * @param body - A {@link FetchBody} which may be replaced with a URL. * * @remarks * Creates the URL that’ll be called as part of a GraphQL HTTP request. * Built-in fetch exchanges support sending GET requests, even for * non-persisted full requests, which this function supports by being * able to serialize GraphQL requests into the URL. */ export const makeFetchURL = ( operation: Operation, body?: FetchBody ): string => { const useGETMethod = operation.kind === 'query' && operation.context.preferGetMethod; if (!useGETMethod || !body) return operation.context.url; const urlParts = splitOutSearchParams(operation.context.url); for (const key in body) { const value = body[key]; if (value) { urlParts[1].set( key, typeof value === 'object' ? stringifyVariables(value) : value ); } } const finalUrl = urlParts.join('?'); if (finalUrl.length > 2047 && useGETMethod !== 'force') { operation.context.preferGetMethod = false; return operation.context.url; } return finalUrl; }; const splitOutSearchParams = ( url: string ): readonly [string, URLSearchParams] => { const start = url.indexOf('?'); return start > -1 ? [url.slice(0, start), new URLSearchParams(url.slice(start + 1))] : [url, new URLSearchParams()]; }; /** Serializes a {@link FetchBody} into a {@link RequestInit.body} format. */ const serializeBody = ( operation: Operation, body?: FetchBody ): FormData | string | undefined => { const omitBody = operation.kind === 'query' && !!operation.context.preferGetMethod; if (body && !omitBody) { const json = stringifyVariables(body); const files = extractFiles(body.variables); if (files.size) { const form = new FormData(); form.append('operations', json); form.append( 'map', stringifyVariables({ ...[...files.keys()].map(value => [value]), }) ); let index = 0; for (const file of files.values()) form.append(`${index++}`, file); return form; } return json; } }; const isHeaders = (headers: HeadersInit): headers is Headers => 'has' in headers && !Object.keys(headers).length; /** Creates a `RequestInit` object for a given `Operation`. * * @param operation - An {@link Operation} for which to make the request. * @param body - A {@link FetchBody} which is added to the options, if the request isn’t a GET request. * * @remarks * Creates the fetch options {@link RequestInit} object that’ll be passed to the Fetch API * as part of a GraphQL over HTTP request. It automatically sets a default `Content-Type` * header. * * @see {@link https://github.com/graphql/graphql-over-http} for the GraphQL over HTTP spec. * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} for the Fetch API spec. */ export const makeFetchOptions = ( operation: Operation, body?: FetchBody ): RequestInit => { const headers: HeadersInit = { accept: operation.kind === 'subscription' ? 'text/event-stream, multipart/mixed' : 'application/graphql-response+json, application/graphql+json, application/json, text/event-stream, multipart/mixed', }; const extraOptions = (typeof operation.context.fetchOptions === 'function' ? operation.context.fetchOptions() : operation.context.fetchOptions) || {}; if (extraOptions.headers) { if (isHeaders(extraOptions.headers)) { extraOptions.headers.forEach((value, key) => { headers[key] = value; }); } else if (Array.isArray(extraOptions.headers)) { (extraOptions.headers as Array<[string, string]>).forEach( (value, key) => { if (Array.isArray(value)) { if (headers[value[0]]) { headers[value[0]] = `${headers[value[0]]},${value[1]}`; } else { headers[value[0]] = value[1]; } } else { headers[key] = value; } } ); } else { for (const key in extraOptions.headers) { headers[key.toLowerCase()] = extraOptions.headers[key]; } } } const serializedBody = serializeBody(operation, body); if (typeof serializedBody === 'string' && !headers['content-type']) headers['content-type'] = 'application/json'; return { ...extraOptions, method: serializedBody ? 'POST' : 'GET', body: serializedBody, headers, }; }; ================================================ FILE: packages/core/src/internal/fetchSource.test.ts ================================================ import { pipe, scan, subscribe, toPromise } from 'wonka'; import { vi, expect, it, beforeEach, describe, beforeAll, Mock, afterAll, } from 'vitest'; import { queryOperation, context } from '../test-utils'; import { makeFetchSource } from './fetchSource'; import { gql } from '../gql'; import { OperationResult, Operation } from '../types'; import { makeOperation } from '../utils'; const fetch = (globalThis as any).fetch as Mock; const abort = vi.fn(); beforeAll(() => { (globalThis as any).AbortController = function AbortController() { this.signal = undefined; this.abort = abort; }; }); beforeEach(() => { fetch.mockClear(); abort.mockClear(); }); afterAll(() => { (globalThis as any).AbortController = undefined; }); const response = JSON.stringify({ status: 200, data: { data: { user: 1200, }, }, }); describe('on success', () => { beforeEach(() => { fetch.mockResolvedValue({ status: 200, headers: { get: () => 'application/json' }, text: vi.fn().mockResolvedValue(response), }); }); it('returns response data', async () => { const fetchOptions = {}; const data = await pipe( makeFetchSource(queryOperation, 'https://test.com/graphql', fetchOptions), toPromise ); expect(data).toMatchSnapshot(); expect(fetch).toHaveBeenCalled(); expect(fetch.mock.calls[0][0]).toBe('https://test.com/graphql'); expect(fetch.mock.calls[0][1]).toBe(fetchOptions); }); it('uses the mock fetch if given', async () => { const fetchOptions = {}; const fetcher = vi.fn().mockResolvedValue({ status: 200, headers: { get: () => 'application/json' }, text: vi.fn().mockResolvedValue(response), }); const data = await pipe( makeFetchSource( { ...queryOperation, context: { ...queryOperation.context, fetch: fetcher, }, }, 'https://test.com/graphql', fetchOptions ), toPromise ); expect(data).toMatchSnapshot(); expect(fetch).not.toHaveBeenCalled(); expect(fetcher).toHaveBeenCalled(); }); }); describe('on error', () => { beforeEach(() => { fetch.mockResolvedValue({ status: 400, statusText: 'Forbidden', headers: { get: () => 'application/json' }, text: vi.fn().mockResolvedValue('{}'), }); }); it('handles network errors', async () => { const error = new Error('test'); fetch.mockRejectedValue(error); const fetchOptions = {}; const data = await pipe( makeFetchSource(queryOperation, 'https://test.com/graphql', fetchOptions), toPromise ); expect(data).toHaveProperty('error.networkError', error); }); it('returns error data', async () => { const fetchOptions = {}; const data = await pipe( makeFetchSource(queryOperation, 'https://test.com/graphql', fetchOptions), toPromise ); expect(data).toMatchSnapshot(); }); it('returns error data with status 400 and manual redirect mode', async () => { const data = await pipe( makeFetchSource(queryOperation, 'https://test.com/graphql', { redirect: 'manual', }), toPromise ); expect(data).toMatchSnapshot(); }); it('ignores the error when a result is available', async () => { const data = await pipe( makeFetchSource(queryOperation, 'https://test.com/graphql', {}), toPromise ); expect(data).toMatchSnapshot(); }); }); describe('on unexpected plain text responses', () => { beforeEach(() => { fetch.mockResolvedValue({ status: 200, headers: new Map([['Content-Type', 'text/plain']]), text: vi.fn().mockResolvedValue('Some Error Message'), }); }); it('returns error data', async () => { const fetchOptions = {}; const result = await pipe( makeFetchSource(queryOperation, 'https://test.com/graphql', fetchOptions), toPromise ); expect(result.error).toMatchObject({ message: '[Network] Some Error Message', }); }); }); describe('on error with non spec-compliant body', () => { beforeEach(() => { fetch.mockResolvedValue({ status: 400, statusText: 'Forbidden', headers: { get: () => 'application/json' }, text: vi.fn().mockResolvedValue('{"errors":{"detail":"Bad Request"}}'), }); }); it('handles network errors', async () => { const data = await pipe( makeFetchSource(queryOperation, 'https://test.com/graphql', {}), toPromise ); expect(data).toMatchSnapshot(); expect(data).toHaveProperty('error.networkError.message', 'Forbidden'); }); }); describe('on teardown', () => { const fail = () => { expect(true).toEqual(false); }; it('does not start the outgoing request on immediate teardowns', async () => { fetch.mockImplementation(async () => { await new Promise(() => { /*noop*/ }); }); const { unsubscribe } = pipe( makeFetchSource(queryOperation, 'https://test.com/graphql', {}), subscribe(fail) ); unsubscribe(); // NOTE: We can only observe the async iterator's final run after a macro tick await new Promise(resolve => setTimeout(resolve)); expect(fetch).toHaveBeenCalledTimes(0); expect(abort).toHaveBeenCalledTimes(1); }); it('aborts the outgoing request', async () => { fetch.mockResolvedValue({ status: 200, headers: new Map([['Content-Type', 'application/json']]), text: vi.fn().mockResolvedValue('{ "data": null }'), }); const { unsubscribe } = pipe( makeFetchSource(queryOperation, 'https://test.com/graphql', {}), subscribe(() => { /*noop*/ }) ); await new Promise(resolve => setTimeout(resolve)); unsubscribe(); // NOTE: We can only observe the async iterator's final run after a macro tick await new Promise(resolve => setTimeout(resolve)); expect(fetch).toHaveBeenCalledTimes(1); expect(abort).toHaveBeenCalledTimes(1); }); }); describe('on multipart/mixed', () => { const wrap = (json: object) => '\r\n' + 'Content-Type: application/json; charset=utf-8\r\n\r\n' + JSON.stringify(json) + '\r\n---'; it('listens for more streamed responses', async () => { fetch.mockResolvedValue({ status: 200, headers: { get() { return 'multipart/mixed'; }, }, body: { getReader: function () { let cancelled = false; const results = [ { done: false, value: Buffer.from('\r\n---'), }, { done: false, value: Buffer.from( wrap({ hasNext: true, data: { author: { id: '1', __typename: 'Author', }, }, }) ), }, { done: false, value: Buffer.from( wrap({ incremental: [ { path: ['author'], data: { name: 'Steve' }, }, ], hasNext: true, }) ), }, { done: false, value: Buffer.from(wrap({ hasNext: false }) + '--'), }, { done: true }, ]; let count = 0; return { cancel: function () { cancelled = true; }, read: function () { if (cancelled) throw new Error('No'); return Promise.resolve(results[count++]); }, }; }, }, }); const AuthorFragment = gql` fragment authorFields on Author { name } `; const streamedQueryOperation: Operation = makeOperation( 'query', { query: gql` query { author { id ...authorFields @defer } } ${AuthorFragment} `, variables: {}, key: 1, }, context ); const chunks: OperationResult[] = await pipe( makeFetchSource(streamedQueryOperation, 'https://test.com/graphql', {}), scan((prev: OperationResult[], item) => [...prev, item], []), toPromise ); expect(chunks.length).toEqual(3); expect(chunks[0].data).toEqual({ author: { id: '1', __typename: 'Author', }, }); expect(chunks[1].data).toEqual({ author: { id: '1', name: 'Steve', __typename: 'Author', }, }); expect(chunks[2].data).toEqual({ author: { id: '1', name: 'Steve', __typename: 'Author', }, }); }); }); describe('on text/event-stream', () => { const wrap = (json: object) => 'data: ' + JSON.stringify(json) + '\n\n'; it('listens for streamed responses', async () => { fetch.mockResolvedValue({ status: 200, headers: { get() { return 'text/event-stream'; }, }, body: { getReader: function () { let cancelled = false; const results = [ { done: false, value: Buffer.from( wrap({ hasNext: true, data: { author: { id: '1', __typename: 'Author', }, }, }) ), }, { done: false, value: Buffer.from( wrap({ incremental: [ { path: ['author'], data: { name: 'Steve' }, }, ], hasNext: true, }) ), }, { done: false, value: Buffer.from(wrap({ hasNext: false })), }, { done: true }, ]; let count = 0; return { cancel: function () { cancelled = true; }, read: function () { if (cancelled) throw new Error('No'); return Promise.resolve(results[count++]); }, }; }, }, }); const AuthorFragment = gql` fragment authorFields on Author { name } `; const streamedQueryOperation: Operation = makeOperation( 'query', { query: gql` query { author { id ...authorFields @defer } } ${AuthorFragment} `, variables: {}, key: 1, }, context ); const chunks: OperationResult[] = await pipe( makeFetchSource(streamedQueryOperation, 'https://test.com/graphql', {}), scan((prev: OperationResult[], item) => [...prev, item], []), toPromise ); expect(chunks.length).toEqual(3); expect(chunks[0].data).toEqual({ author: { id: '1', __typename: 'Author', }, }); expect(chunks[1].data).toEqual({ author: { id: '1', name: 'Steve', __typename: 'Author', }, }); expect(chunks[2].data).toEqual({ author: { id: '1', name: 'Steve', __typename: 'Author', }, }); }); it('merges deferred results on the root-type', async () => { fetch.mockResolvedValue({ status: 200, headers: { get() { return 'text/event-stream'; }, }, body: { getReader: function () { let cancelled = false; const results = [ { done: false, value: Buffer.from( wrap({ hasNext: true, data: { author: { id: '1', __typename: 'Author', }, }, }) ), }, { done: false, value: Buffer.from( wrap({ incremental: [ { path: [], data: { author: { name: 'Steve' } }, }, ], hasNext: true, }) ), }, { done: false, value: Buffer.from(wrap({ hasNext: false })), }, { done: true }, ]; let count = 0; return { cancel: function () { cancelled = true; }, read: function () { if (cancelled) throw new Error('No'); return Promise.resolve(results[count++]); }, }; }, }, }); const AuthorFragment = gql` fragment authorFields on Query { author { name } } `; const streamedQueryOperation: Operation = makeOperation( 'query', { query: gql` query { author { id ...authorFields @defer } } ${AuthorFragment} `, variables: {}, key: 1, }, context ); const chunks: OperationResult[] = await pipe( makeFetchSource(streamedQueryOperation, 'https://test.com/graphql', {}), scan((prev: OperationResult[], item) => [...prev, item], []), toPromise ); expect(chunks.length).toEqual(3); expect(chunks[0].data).toEqual({ author: { id: '1', __typename: 'Author', }, }); expect(chunks[1].data).toEqual({ author: { id: '1', name: 'Steve', __typename: 'Author', }, }); expect(chunks[2].data).toEqual({ author: { id: '1', name: 'Steve', __typename: 'Author', }, }); }); }); ================================================ FILE: packages/core/src/internal/fetchSource.ts ================================================ /* Summary: This file handles the HTTP transport via GraphQL over HTTP * See: https://graphql.github.io/graphql-over-http/draft/ * * `@urql/core`, by default, implements several RFC'd protocol extensions * on top of this. As such, this implementation supports: * - [Incremental Delivery](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md) * - [GraphQL over SSE](https://github.com/graphql/graphql-over-http/blob/main/rfcs/GraphQLOverSSE.md) * * This also supports the "Defer Stream" payload format. * See: https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md * Implementation for this is located in `../utils/result.ts` in `mergeResultPatch` * * And; this also supports the GraphQL Multipart spec for file uploads. * See: https://github.com/jaydenseric/graphql-multipart-request-spec * Implementation for this is located in `../utils/variables.ts` in `extractFiles`, * and `./fetchOptions.ts` in `serializeBody`. * * And; this also supports GET requests (and hence; automatic persisted queries) * via the `@urql/exchange-persisted` package. * * This implementation DOES NOT support Batching. * See: https://github.com/graphql/graphql-over-http/blob/main/rfcs/Batching.md * Which is deemed out-of-scope, as it's sufficiently unnecessary given * modern handling of HTTP requests being in parallel. * * The implementation in this file needs to make certain accommodations for: * - The Web Fetch API * - Non-browser or polyfill Fetch APIs * - Node.js-like Fetch implementations * * GraphQL over SSE has a reference implementation, which supports non-HTTP/2 * modes and is a faithful implementation of the spec. * See: https://github.com/enisdenjo/graphql-sse * * GraphQL Inremental Delivery (aka “GraphQL Multipart Responses”) has a * reference implementation, which a prior implementation of this file heavily * leaned on (See prior attribution comments) * See: https://github.com/maraisr/meros * * This file merges support for all three GraphQL over HTTP response formats * via async generators and Wonka’s `fromAsyncIterable`. As part of this, `streamBody` * and `split` are the common, cross-compatible base implementations. */ import type { Source } from 'wonka'; import { fromAsyncIterable, onEnd, filter, pipe } from 'wonka'; import type { Operation, OperationResult, ExecutionResult } from '../types'; import { makeResult, makeErrorResult, mergeResultPatch } from '../utils'; const boundaryHeaderRe = /boundary="?([^=";]+)"?/i; const eventStreamRe = /data: ?([^\n]+)/; type ChunkData = Buffer | Uint8Array; async function* streamBody( response: Response ): AsyncIterableIterator { if (response.body![Symbol.asyncIterator]) { for await (const chunk of response.body! as any) yield chunk as ChunkData; } else { const reader = response.body!.getReader(); let result: ReadableStreamReadResult; try { while (!(result = await reader.read()).done) yield result.value; } finally { reader.cancel(); } } } async function* streamToBoundedChunks( chunks: AsyncIterableIterator, boundary: string ): AsyncIterableIterator { const decoder = typeof TextDecoder !== 'undefined' ? new TextDecoder() : null; let buffer = ''; let boundaryIndex: number; for await (const chunk of chunks) { // NOTE: We're avoiding referencing the `Buffer` global here to prevent // auto-polyfilling in Webpack buffer += chunk.constructor.name === 'Buffer' ? (chunk as Buffer).toString() : decoder!.decode(chunk as ArrayBuffer, { stream: true }); while ((boundaryIndex = buffer.indexOf(boundary)) > -1) { yield buffer.slice(0, boundaryIndex); buffer = buffer.slice(boundaryIndex + boundary.length); } } } async function* parseJSON( response: Response ): AsyncIterableIterator { yield JSON.parse(await response.text()); } async function* parseEventStream( response: Response ): AsyncIterableIterator { let payload: any; for await (const chunk of streamToBoundedChunks( streamBody(response), '\n\n' )) { const match = chunk.match(eventStreamRe); if (match) { const chunk = match[1]; try { yield (payload = JSON.parse(chunk)); } catch (error) { if (!payload) throw error; } if (payload && payload.hasNext === false) break; } } if (payload && payload.hasNext !== false) { yield { hasNext: false }; } } async function* parseMultipartMixed( contentType: string, response: Response ): AsyncIterableIterator { const boundaryHeader = contentType.match(boundaryHeaderRe); const boundary = '--' + (boundaryHeader ? boundaryHeader[1] : '-'); let isPreamble = true; let payload: any; for await (let chunk of streamToBoundedChunks( streamBody(response), '\r\n' + boundary )) { if (isPreamble) { isPreamble = false; const preambleIndex = chunk.indexOf(boundary); if (preambleIndex > -1) { chunk = chunk.slice(preambleIndex + boundary.length); } else { continue; } } try { yield (payload = JSON.parse(chunk.slice(chunk.indexOf('\r\n\r\n') + 4))); } catch (error) { if (!payload) throw error; } if (payload && payload.hasNext === false) break; } if (payload && payload.hasNext !== false) { yield { hasNext: false }; } } async function* parseMaybeJSON( response: Response ): AsyncIterableIterator { const text = await response.text(); try { const result = JSON.parse(text); if (process.env.NODE_ENV !== 'production') { console.warn( `Found response with content-type "text/plain" but it had a valid "application/json" response.` ); } yield result; } catch (e) { throw new Error(text); } } async function* fetchOperation( operation: Operation, url: string, fetchOptions: RequestInit ) { let networkMode = true; let result: OperationResult | null = null; let response: Response | undefined; try { // Delay for a tick to give the Client a chance to cancel the request // if a teardown comes in immediately yield await Promise.resolve(); response = await (operation.context.fetch || fetch)(url, fetchOptions); const contentType = response.headers.get('Content-Type') || ''; let results: AsyncIterable; if (/multipart\/mixed/i.test(contentType)) { results = parseMultipartMixed(contentType, response); } else if (/text\/event-stream/i.test(contentType)) { results = parseEventStream(response); } else if (!/text\//i.test(contentType)) { results = parseJSON(response); } else { results = parseMaybeJSON(response); } let pending: ExecutionResult['pending']; for await (const payload of results) { if (payload.pending && !result) { pending = payload.pending; } else if (payload.pending) { pending = [...pending!, ...payload.pending]; } result = result ? mergeResultPatch(result, payload, response, pending) : makeResult(operation, payload, response); networkMode = false; yield result; networkMode = true; } if (!result) { yield (result = makeResult(operation, {}, response)); } } catch (error: any) { if (!networkMode) { throw error; } yield makeErrorResult( operation, response && (response.status < 200 || response.status >= 300) && response.statusText ? new Error(response.statusText) : error, response ); } } /** Makes a GraphQL HTTP request to a given API by wrapping around the Fetch API. * * @param operation - The {@link Operation} that should be sent via GraphQL over HTTP. * @param url - The endpoint URL for the GraphQL HTTP API. * @param fetchOptions - The {@link RequestInit} fetch options for the request. * @returns A Wonka {@link Source} of {@link OperationResult | OperationResults}. * * @remarks * This utility defines how all built-in fetch exchanges make GraphQL HTTP requests, * supporting multipart incremental responses, cancellation and other smaller * implementation details. * * If you’re implementing a modified fetch exchange for a GraphQL over HTTP API * it’s recommended you use this utility. * * Hint: This function does not use the passed `operation` to create or modify the * `fetchOptions` and instead expects that the options have already been created * using {@link makeFetchOptions} and modified as needed. * * @throws * If the `fetch` polyfill or globally available `fetch` function doesn’t support * streamed multipart responses while trying to handle a `multipart/mixed` GraphQL response, * the source will throw “Streaming requests unsupported”. * This shouldn’t happen in modern browsers and Node.js. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} for the Fetch API spec. */ export function makeFetchSource( operation: Operation, url: string, fetchOptions: RequestInit ): Source { let abortController: AbortController | void; if (typeof AbortController !== 'undefined') { fetchOptions.signal = (abortController = new AbortController()).signal; } return pipe( fromAsyncIterable(fetchOperation(operation, url, fetchOptions)), filter((result): result is OperationResult => !!result), onEnd(() => { if (abortController) abortController.abort(); }) ); } ================================================ FILE: packages/core/src/internal/index.ts ================================================ export * from './fetchOptions'; export * from './fetchSource'; ================================================ FILE: packages/core/src/test-utils/index.ts ================================================ export * from './samples'; ================================================ FILE: packages/core/src/test-utils/samples.ts ================================================ import { gql } from '../gql'; import type { ExecutionResult, GraphQLRequest, Operation, OperationContext, OperationResult, } from '../types'; import { makeOperation } from '../utils'; export const context: OperationContext = { fetchOptions: { method: 'POST', }, requestPolicy: 'cache-first', url: 'http://localhost:3000/graphql', }; export const queryGql: GraphQLRequest = { key: 2, query: gql` query getUser($name: String) { user(name: $name) { id firstName lastName } } `, variables: { name: 'Clara', }, }; export const mutationGql: GraphQLRequest = { key: 3, query: gql` mutation AddUser($name: String) { addUser(name: $name) { name } } `, variables: { name: 'Clara', }, }; export const subscriptionGql: GraphQLRequest = { key: 4, query: gql` subscription subscribeToUser($user: String) { user(user: $user) { name } } `, variables: { user: 'colin', }, }; export const queryOperation: Operation = makeOperation( 'query', { query: queryGql.query, variables: queryGql.variables, key: queryGql.key, }, context ); export const teardownOperation: Operation = makeOperation( 'teardown', { query: queryOperation.query, variables: queryOperation.variables, key: queryOperation.key, }, context ); export const mutationOperation: Operation = makeOperation( 'mutation', { query: mutationGql.query, variables: mutationGql.variables, key: mutationGql.key, }, context ); export const subscriptionOperation: Operation = makeOperation( 'subscription', { query: subscriptionGql.query, variables: subscriptionGql.variables, key: subscriptionGql.key, }, context ); export const undefinedQueryResponse: OperationResult = { operation: queryOperation, stale: false, hasNext: false, }; export const queryResponse: OperationResult = { operation: queryOperation, data: { user: { name: 'Clive', }, }, stale: false, hasNext: false, }; export const mutationResponse: OperationResult = { operation: mutationOperation, data: {}, stale: false, hasNext: false, }; export const subscriptionResult: ExecutionResult = { data: {}, }; ================================================ FILE: packages/core/src/types.ts ================================================ import type { GraphQLError, DocumentNode } from './utils/graphql'; import type { Kind, DirectiveNode, ValueNode, TypeNode, } from '@0no-co/graphql.web'; import type { Subscription, Source } from 'wonka'; import type { Client } from './client'; import type { CombinedError } from './utils/error'; /** A GraphQL persisted document will contain `documentId` that replaces its definitions */ export interface PersistedDocument extends DocumentNode { documentId?: string; } /** A GraphQL `DocumentNode` with attached generics for its result data and variables. * * @remarks * A GraphQL {@link DocumentNode} defines both the variables it accepts on request and the `data` * shape it delivers on a response in the GraphQL query language. * * To bridge the gap to TypeScript, tools may be used to generate TypeScript types that define the shape * of `data` and `variables` ahead of time. These types are then attached to GraphQL documents using this * `TypedDocumentNode` type. * * Using a `DocumentNode` that is typed like this will cause any `urql` API to type its input `variables` * and resulting `data` using the types provided. * * @privateRemarks * For compatibility reasons this type has been copied and internalized from: * https://github.com/dotansimha/graphql-typed-document-node/blob/3711b12/packages/core/src/index.ts#L3-L10 * * @see {@link https://github.com/dotansimha/graphql-typed-document-node} for more information. */ export type TypedDocumentNode< Result = { [key: string]: any }, Variables = { [key: string]: any }, > = DocumentNode & { /** Type to support `@graphql-typed-document-node/core` * @internal */ __apiType?: (variables: Variables) => Result; /** Type to support `TypedQueryDocumentNode` from `graphql` * @internal */ __ensureTypesOfVariablesAndResultMatching?: (variables: Variables) => Result; }; /** GraphQL nodes with added `_directives` dictionary on nodes with directives. * * @remarks * The {@link formatDocument} utility processes documents to add `__typename` * fields to them. It additionally provides additional directives processing * and outputs this type. * * When applied, every node with non-const directives, will have an additional * `_directives` dictionary added to it, and filter directives starting with * a leading `_` underscore from the directives array. */ export type FormattedNode = Node extends readonly (infer Child)[] ? readonly FormattedNode[] : Node extends ValueNode | TypeNode ? Node : Node extends { kind: Kind } ? { [K in Exclude]: FormattedNode< Node[K] >; } extends infer Node ? Node extends { kind: Kind.FIELD | Kind.INLINE_FRAGMENT | Kind.FRAGMENT_SPREAD; } ? Node & { _generated?: boolean; _directives?: Record | undefined; } : Node : Node : Node; /** Any GraphQL `DocumentNode` or query string input. * * @remarks * Wherever any `urql` bindings or API expect a query, it accepts either a query string, * a `DocumentNode`, or a {@link TypedDocumentNode}. */ export type DocumentInput< Result = { [key: string]: any }, Variables = { [key: string]: any }, > = string | DocumentNode | TypedDocumentNode; /** A list of errors on {@link ExecutionResult | ExecutionResults}. * @see {@link https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format} for the GraphQL Error Result format spec. */ export type ErrorLike = Partial | Error; /** Extensions which may be placed on {@link ExecutionResult | ExecutionResults}. * @see {@link https://spec.graphql.org/draft/#sel-EAPHJCAACCoGu9J} for the GraphQL Error Result format spec. */ type Extensions = Record; /** Extensions sub-property on `persistedQuery` for Automatic Persisted Queries. * * @remarks * This is part of the Automatic Persisted Query defacto standard and allows an API * request to omit the `query`, instead sending this `sha256Hash`. */ export interface PersistedRequestExtensions { version?: 1; sha256Hash: string; /** Set when a `sha256Hash` previously experienced a miss which will force `query` to be sent. */ miss?: boolean; } /** Extensions which may be palced on {@link GraphQLRequest | GraphQLRequests}. * @see {@link https://github.com/graphql/graphql-over-http/blob/1928447/spec/GraphQLOverHTTP.md#request-parameters} for the GraphQL over HTTP spec */ export interface RequestExtensions { persistedQuery?: PersistedRequestExtensions; [extension: string]: any; } type Path = readonly (string | number)[]; /** Incremental Payloads sent as part of "Incremental Delivery" patching prior result data. * * @remarks * "Incremental Delivery" works by allowing APIs to stream patches to the client, whih update * prior results at the specified `path`. * * @see {@link https://github.com/graphql/graphql-spec/blob/94363c9/spec/Section%207%20--%20Response.md#incremental} for the incremental payload spec */ export interface IncrementalPayload { /** Optional label for the incremental payload that corresponds to directives' labels. * * @remarks * All incremental payloads are labelled by the label that `@stream` or `@defer` directives * specified, to identify which directive they originally belonged to. */ label?: string | null; /** JSON patch at which to apply the `data` patch or append the `items`. * * @remarks * The `path` indicates the JSON path of a prior result’s `data` structure at which * to insert the patch’s data. * When `items` is set instead, which represents a list of items to insert, the last * entry of the `path` will be an index number at which to start setting the range of * items. */ path?: Path; /** An id pointing at an entry in the "pending" set of deferred results * * @remarks * When we resolve this id it will give us the path to the deferred Fragment, this * can be afterwards combined with the subPath to get the eventual location of the data. */ id?: string; /** A path array from the defer/stream fragment to the location of our data. */ subPath?: Path; /** Data to patch into the result data at the given `path`. * * @remarks * This `data`, when set, is merged into the object at the given `path` of the last * result that has been delivered. * This isn't set when `items` is set. */ data?: Record | null; /** List of items to patch into the result data at the given `path`. * * @remarks * The `items`, when provided, is set onto a range in an array, at the given JSON * `path`. The start index is the last entry of the `path` and the end index is * the length of the `items` list added to this index. * This isn't set when `data` is set. */ items?: readonly unknown[] | null; /** Contains a list of errors raised by incremental payloads. * * @remarks * The list of `errors` on `incremental` payloads is merged into the list of prior * results’ errors. * * @see {@link https://spec.graphql.org/October2021/#sec-Errors} for the GraphQL Errors Response spec */ errors?: ErrorLike[] | readonly ErrorLike[]; /** Additional metadata that a GraphQL API may choose to send that is out of spec. * @see {@link https://spec.graphql.org/October2021/#sel-EAPHJCAACCoGu9J} for the GraphQL Response spec */ extensions?: Extensions; } type PendingIncrementalResult = { path: Path; id: string; label?: string; }; export interface ExecutionResult { /** Payloads we are still waiting for from the server. * * @remarks * This was nely introduced in the defer/stream spec iteration of June 2023 https://github.com/graphql/defer-stream-wg/discussions/69 * Pending can be present on both Incremental as well as normal execution results, the presence of pending on an incremental * result points at a nested deferred/streamed fragment. */ pending?: readonly PendingIncrementalResult[]; /** Incremental patches to be applied to a previous result as part of "Incremental Delivery". * * @remarks * When this is set `data` and `errors` is typically not set on the result. Instead, the incremental payloads * are applied as patches to a prior result's `data`. * * @see {@link https://github.com/graphql/graphql-spec/blob/94363c9/spec/Section%207%20--%20Response.md#incremental} for the incremental payload spec */ incremental?: IncrementalPayload[]; /** The result of the execution of the GraphQL operation. * @see {@link https://spec.graphql.org/October2021/#sec-Data} for the GraphQL Data Response spec */ data?: null | Record; /** Contains a list of errors raised by fields or the request itself. * @see {@link https://spec.graphql.org/October2021/#sec-Errors} for the GraphQL Errors Response spec */ errors?: ErrorLike[] | readonly ErrorLike[]; /** Additional metadata that a GraphQL API may choose to send that is out of spec. * @see {@link https://spec.graphql.org/October2021/#sel-EAPHJCAACCoGu9J} for the GraphQL Response spec */ extensions?: Extensions; /** Flag indicating whether a future, incremental response may update this response. * @see {@link https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md#payload-format} for the DeferStream spec */ hasNext?: boolean; payload?: Omit; } /** A source of {@link OperationResult | OperationResults}, convertable to a promise, subscribable, or Wonka Source. * * @remarks * The {@link Client} will often return a `OperationResultSource` to provide a more flexible Wonka {@link Source}. * * While a {@link Source} may require you to import helpers to convert it to a `Promise` for a single result, or * to subscribe to it, the `OperationResultSource` is a `PromiseLike` and has methods to convert it to a promise, * or to subscribe to it with a single method call. */ export type OperationResultSource = Source & PromiseLike & { /** Returns the first non-stale, settled results of the source. * @remarks * The `toPromise` method gives you the first result of an `OperationResultSource` * that has `hasNext: false` and `stale: false` set as a `Promise`. * * Hint: If you're trying to get updates for your results, this won't work. * This gives you only a single, promisified result, so it won't receive * cache or other updates. */ toPromise(): Promise; /** Alias for Wonka's `subscribe` and calls `onResult` when subscribed to for each new `OperationResult`. */ subscribe(onResult: (value: T) => void): Subscription; }; /** A type of Operation, either a GraphQL `query`, `mutation`, or `subscription`; or a `teardown` signal. * * @remarks * Internally, {@link Operation | Operations} instruct the {@link Client} to perform a certain action on its exchanges. * Any of the three GraphQL operations tell it to execute these operations, and the `teardown` signal instructs it that * the operations are cancelled and/or have ended. * * The `teardown` signal is sent when nothing is subscribed to an operation anymore and no longer interested in its results * or any updates. */ export type OperationType = 'subscription' | 'query' | 'mutation' | 'teardown'; /** The request and caching strategy that is used by exchanges to retrive cached results. * * @remarks * The `RequestPolicy` is used by cache exchanges to decide how a query operation may be resolved with cached results.h * A cache exchange may behave differently depending on which policy is returned. * * - `cache-first` (the default) prefers cached results and falls back to sending an API request. * - `cache-and-network` returns cached results but also always sends an API request in the background. * - `network-only` will ignore any cached results and send an API request. * - `cache-only` will always return cached results and prevent API requests. */ export type RequestPolicy = | 'cache-first' | 'cache-and-network' | 'network-only' | 'cache-only'; /** A metadata flag set by cache exchanges to indicate whether a cache miss, a cache hit, or a partial cache hit has occurred. * * @remarks * A cache exchange may update {@link OperationDebugMeta.cacheOutcome} on {@link OperationContext.meta} to indicate whether * an operation has been resolved from the cache. * * A cache hit is considered a result that has fully come from a cache. A partial result is a result that has come from a cache * but is incomplete, which may trigger another API request. A cache miss means a result must be requested from the API as no * cache result has been delivered. */ export type CacheOutcome = 'miss' | 'partial' | 'hit'; /** A default type for variables. * * @remarks * While {@link TypedDocumentNode} can be used by generators to add TypeScript types for a GraphQL operation’s * variables and result, when this isn’t the case this type is used as a fallback for the `Variables` generic. */ export type AnyVariables = { [prop: string]: any } | void | undefined; /** A GraphQL request representing a single execution in GraphQL. * * @remarks * A `GraphQLRequest` is a single executable request that may be used by a cache or a GraphQL API to deliver a result. * A request contains a `DocumentNode` for the query document of a GraphQL operation and the `variables` for the given * request. * * A unique `key` is generated to identify the request internally by `urql`. Two requests with the same query and * variables will share the same `key`. * * The `Data` and `Variables` generics may be provided by a {@link TypedDocumentNode}, adding TypeScript types for what * the result shape and variables shape are. * * @see {@link https://spec.graphql.org/October2021/#sec-Executing-Requests} for more information on GraphQL reuqests. */ export interface GraphQLRequest< Data = any, Variables extends AnyVariables = AnyVariables, > { /** Unique identifier for the `GraphQLRequest`. * * @remarks * This is a key that combines the unique key of the `query` and the `variables` into a unique * `key` for the `GraphQLRequest`. Any request with the same query and variables will have a unique * `key` by which results and requests can be identified as identical. * * Internally, a stable, cached `key` is generated for the `DocumentNode` and for the `variables` and * both will be combined into a combined `key` which is set here, based on a DJB2 hash, * * The `variables` will change the key even if they contain a non-JSON reference. If you pass a custom * class instance to `variables` that doesn't contain a `toString` or `toJSON` method, a stable but random * identifier will replace this class to generate a key. */ key: number; /** A GraphQL document to execute against a cache or API. * * @remarks * A `GraphQLRequest` is executed against an operation in a GraphQL document. * In `urql`, we expect a document to only contain a single operation that is executed rather than * multiple ones by convention. */ query: DocumentNode | PersistedDocument | TypedDocumentNode; /** Variables used to execute the `query` document. * * @remarks * The `variables`, based either on the {@link AnyVariables} type or the {@link TypedDocumentNode}'s provided * generic, are sent to the GraphQL API to execute a request. */ variables: Variables; /** Additional metadata that a GraphQL API may accept for spec extensions. * @see {@link https://github.com/graphql/graphql-over-http/blob/1928447/spec/GraphQLOverHTTP.md#request-parameters} for the GraphQL over HTTP spec */ extensions?: RequestExtensions | undefined; } /** Parameters from which {@link GraphQLRequest | GraphQLRequests} are created from. * * @remarks * A `GraphQLRequest` is a single executable request with a generated `key` to identify * their results, whereas `GraphQLRequestParams` is a utility type used to generate * inputs for `urql` to create requests from, i.e. it only contains a `query` and * `variables`. The type conditionally makes the `variables` property completely * optional. * * @privateRemarks * The wrapping union type is needed for passthrough or wrapper utilities that wrap * functions like `useQuery` with generics. */ export type GraphQLRequestParams< Data = any, Variables extends AnyVariables = AnyVariables, > = | ({ query: DocumentInput; } & (Variables extends void ? { variables?: Variables; } : Variables extends { [P in keyof Variables]: undefined extends Variables[P] ? unknown : null extends Variables[P] ? unknown : void extends Variables[P] ? unknown : never; } ? { variables?: Variables; } : { variables: Variables; })) | { query: DocumentInput; variables: Variables; }; /** Metadata used to annotate an `Operation` in development for the `urql-devtools`. * * @remarks * The `OperationDebugMeta` is found on {@link OperationContext.meta} only in development, * and is used to send additional metadata to the `urql-devtools` about the {@link Operation}. * * In production, most of this metadata will be missing, and it must not be used outside * of development, and should only be used by the `urql-devtools`. */ export interface OperationDebugMeta { /** A label for the source of the `Operation`. * * @remarks * The `source` string indicates a human readable originator for the `Operation`. * This may be set to a component name or function name to indicate what originally * triggered the `Operation`. */ source?: string; /** A type of caching outcome set by cache exchanges on `OperationResult`s. * * @remarks * The `cacheOutcome` flag is set to a {@link CacheOutcome} on {@link Operation | Operations} * after they passed through the cache exchange. This flag indicates whether a cache hit, miss, * or partial cache hit has occurred. */ cacheOutcome?: CacheOutcome; /** Reserved to indicate the time it took for a GraphQL request to receive a response from a GraphQL API. * * @remarks * The `networkLatency` may be set to the time it took (in ms) for a GraphQL API to respond to a request * and deliver a result. * @internal */ networkLatency?: number; /** Reserved to indicate the timestamp at which a GraphQL request was sent to a GraphQL API. * * @remarks * The `startTime` is set to an epoch timestamp (in ms) at which a GraphQL request was started * and sent to a GraphQL API. * @internal */ startTime?: number; } /** A unique identity for GraphQL mutations. * * @remarks * GraphQL mutations not only use {@link GraphQLRequest.key} to identify a result, but instead use * an identity to mark which result belongs to them. * * While two GraphQL queries and subscriptions sharing the same variables and the same operation * (i.e. `DocumentNode`) are considered identical, two mutations are not. * Two GraphQL queries or subscription results with the same {@link GraphQLRequest.key} can be used * to resolve any {@link GraphQLRequest} with this same `key`. * This is because identical queries and subscriptions are idempotent. * * However, two mutations with the same variables may receive different results from a GraphQL API, * since they may trigger side-effects. * This means that `urql` needs an additional identifier to differentiate between two mutations with * the same `DocumentNode`s and `variables`. */ export type OperationInstance = number & { /** Marker to indicate that an `OperationInstance` may not be created by a user. * * @remarks * The {@link Client} creates `OperationInstance` indentities automatically and uses them internally * to identify mutations results as belonging to mutation operations. These are just integers (numbers), * however, they're used as if they are objects (e.g. `{}`). However, since instances of arrays and * objects are not serialisable numbers are used instead. * * Because these are internal, the TypeScript type is marked using a `unique symbol` because they're * created opaquely and privately. * * @internal */ readonly _opaque: unique symbol; }; /** Additional metadata for an `Operation` used to execute it. * * @remarks * The `OperationContext` is found on {@link Operation.context} and gives exchanges additional metadata * and options used to execute the operation. * * The context can often be changed on a per-operation basis, meaning, APIs on the {@link Client} or * bindings can pass a partial context that alters these options for a single operation. * * The `OperationContext` is populated mostly from the initial options passed to the `Client` at its * time of creation, but may also be modified by exchanges when an {@link Operation} is passed through * to the next exchange or when a result is returned. */ export interface OperationContext { /** A unique identity for GraphQL mutations. * * @remarks * This is an internal property set by the `Client` to an identity of type {@link OperationInstance}. * An `OperationInstance` is an identifier that's used to tell two mutation operations with identical * `query` documents and `variables` apart from one another. * @internal */ readonly _instance?: OperationInstance | undefined; /** Additional cache tags for `@urql/core`'s document `cacheExchange`. * * @remarks * The built-in {@link cacheExchange} in `@urql/core` is a document cache that uses `__typename`s in * mutation results to invalidate past, cached queries. * The `additionalTypenames` array may be set to the list of custom typenames whenever a result may * not deliver `__typename` properties, e.g. when an empty array may be sent. * * By providing a list of custom typenames you may "tag" a result as containing a certain type, which * helps the document cache associate mutations with queries when either don't actually contain a * `__typename` in the JSON result. */ additionalTypenames?: string[]; /** The `fetch` function used to make API calls. * * @remarks * This is the fetch polyfill used by any fetch exchange to make an API request. By default, when this * option isn't set, any fetch exchange will attempt to use the globally available `fetch` function * to make a request instead. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API} for the Fetch API spec. */ fetch?: typeof fetch; /** The `url` passed to the `fetch` call on API requests. * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch} for a description of the `fetch` calls. */ url: string; /** Additional options passed to the `fetch` call on API requests. * * @remarks * The options in this object or an object returned by a callback function will be merged into the * {@link RequestInit} options passed to the `fetch` call. * * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch} for a description of this object. */ fetchOptions?: RequestInit | (() => RequestInit); /** Allows the `fetchExchange` to handle subscriptions. * * @remarks * When set to `true`, subscriptions are allowed to be handled by the {@link fetchExchange} and will * be sent using a `fetch` call as GraphQL over HTTP requests. * This may be enabled on {@link ClientOptions.fetchSubscriptions} when your API supports the * `text/event-stream` and `multipart/mixed` response protocols and is able to use them to * respond with subscription results. */ fetchSubscriptions?: boolean; /** The request and caching strategy instructing cache exchanges how to treat cached results. * * @remarks * The {@link RequestPolicy} instructing cache exchanges how to use and treat their cached results. * By default `cache-first` is set and used, which will use cache results, and only make an API request * on a cache miss. */ requestPolicy: RequestPolicy; /** Metadata that annotates an `Operation` in development for the `urql-devtools`. * * @remarks * This is metadata that is used by the `urql-devtools` to get more information about `Operation`s and * `OperationResult`s and is filled in by exchanges across the codebase in development. * * This data is not for production use and hence shouldn't be used or relied upon directly. * In production, this may not be set by default exchanges. */ meta?: OperationDebugMeta; /** Instructs fetch exchanges to use a GET request. * * @remarks * By default, GraphQL over HTTP requests are always sent as POST requests with a JSON body. * However, sometimes it's preferable to send a GET request instead, for instance, for caching with or * without persisted queries. * * When set to `true`, the `preferGetMethod` instructs fetch exchanges to instead send a GET request * for query operations. * * Additionally, `urql`'s built-in fetch exchanges will default to `'within-url-limit'` and not send a GET request * when the resulting URL would be 2,048 characters or longer. This can be forced and circumvented by setting * this option to `'force'`. */ preferGetMethod?: boolean | 'force' | 'within-url-limit'; /** A configuration flag indicating whether this operation may trigger "Suspense". * * @remarks * This configuration flag is reserved for `urql` (`react-urql`) and `@urql/preact` to activate or * deactivate support for Suspense, and is ignored in other bindings. * When activated here and on {@link `Client.suspense`} it allows the bindings to "suspend" instead * of returning a loading state, which will stop updates in a querying component and instead cascade * to a higher suspense boundary for a loading state. * * @see {@link https://beta.reactjs.org/blog/2022/03/29/react-v18#new-suspense-features} for more information on React Suspense. */ suspense?: boolean; /** A metdata flag indicating whether this operation triggered optimistic updates. * * @remarks * This configuration flag is reserved for `@urql/exchange-graphcache` and is flipped * when an operation triggerd optimistic updates. */ optimistic?: boolean; [key: string]: any; } /** The inputs to `urql`'s Exchange pipeline to instruct them to execute a GraphQL operation. * * @remarks * An `Operation`, in `urql`, starts a {@link GraphQLRequest} and are events. The `kind` of an `Operation` can * be set to any operation kind of GraphQL, namely `query`, `mutation`, or `subscription`. To terminate an * operation, once it's cancelled, a `teardown` kind event is sent. * * The {@link ExchangeIO} type describes how {@link Exchange | Exchanges} receive `Operation`s and return * `OperationResults`, using `teardown` `Operation`s to cancel ongoing operations. * * @see {@link https://urql.dev/goto/docs/architecture/#the-client-and-exchanges} for more information * on the flow of Exchanges. */ export interface Operation< Data = any, Variables extends AnyVariables = AnyVariables, > extends GraphQLRequest { /** The `OperationType` describing the kind of `Operation`. * * @remarks * This is used to describe what to do with the {@link GraphQLRequest} of an {@link Operation} and is set * to a GraphQL operation type (`query`, `mutation`, or `subscription`) to start an `Operation`; and to * `teardown` to cancel an operation, which either terminates it early or lets exchanges know that no * consumer is interested in this operation any longer. */ readonly kind: OperationType; /** Holds additional metadata for an `Operation` used to execute it. * * @remarks * The {@link OperationContext} is created by the {@link Client} but may also be modified by * {@link Exchange | Exchanges} and is used as metadata by them. */ context: OperationContext; } /** A result for an `Operation` carrying a full GraphQL response. * * @remarks * An `OperationResult` is the result of an {@link Operation} and carry a description of the full response * on them. The {@link OperationResult.operation} is set to the `Operation` that this result fulfils. * * Unlike {@link ExecutionResult}, an `OperationResult` will never be an incremental result and will * always match the fully merged type of a GraphQL request. It essentially is a postprocessed version * of a GraphQL API response. */ export interface OperationResult< Data = any, Variables extends AnyVariables = AnyVariables, > { /** The [operation]{@link Operation} which has been executed. */ /** The `Operation` which this `OperationResult` is for. * * @remarks * The `operation` property is set to the {@link Operation} that this result is. At the time the * {@link OperationResult} is constructed (either from the cache or an API response) the original * `Operation` that the exchange delivering this result has received will be added. * * The {@link Client} uses this to identify which {@link Operation} this {@link OperationResult} is * for and to filter and deliver this result to the right place and consumers. */ operation: Operation; /** The result of the execution of the GraphQL operation. * @see {@link https://spec.graphql.org/October2021/#sec-Data} for the GraphQL Data Response spec */ data?: Data; /** Contains a description of errors raised by GraphQL fields or the request itself by the API. * * @remarks * The `error` of an `OperationResult` is set to a {@link CombinedError} if the GraphQL API response * contained any GraphQL errors. * * GraphQL errors occur when either a GraphQL request was prevented from executing entirely * (at which point `data: undefined` is set) or when one or more fields of a GraphQL request * failed to execute. Due to the latter, you may receive partial data when a GraphQL request * partially failed. */ error?: CombinedError; /** Additional metadata that a GraphQL API may choose to send that is out of spec. * @see {@link https://spec.graphql.org/October2021/#sel-EAPHJCAACCoGu9J} for the GraphQL Response spec */ extensions?: Record; /** Indicates that an `OperationResult` is not fresh and a new result will follow. * * @remarks * The `stale` flag indicates whether a result is expected to be superseded by a new result soon. * This flag is set whenever a new result is being awaited and will be deliverd as soon as the API responds. * * It may be set by the {@link Client} when the `Operation` was already active, at which point * the {@link Client} asks the {@link Exchange | Exchanges} to request a new API response, or * by cache exchanges when a temporary, incomplete, or initial cache result has been deliverd, and * a new API request has been started in the background. (For partial cache results) * * Most commonly, this flag is set for a cached result when the operation is executed using the * `cache-and-network` {@link RequestPolicy}. */ stale: boolean; /** Indicates that the GraphQL response is streamed and updated results will follow. * * @remarks * Due to incremental delivery, an API may deliver multiple {@link ExecutionResult | ExecutionResults} for a * single GraphQL request. This can happen for `@defer`, `@stream`, or `@live` queries, which allow an API * to update an initial GraphQL response over time, like a subscription. * * For GraphQL subscriptions, this flag will always be set to `true`. */ hasNext: boolean; } /** The input parameters a `Client` passes to an `Exchange` when it's created. * * @remarks * When instantiated, a {@link Client} passes these inputs parameters to an {@link Exchange}. * * This input contains the `Client` itself, a `dispatchDebug` function for the `urql-devtools`, and * `forward`, which is set to the next exchange's {@link ExchangeIO} function in the exchange pipeline. */ export interface ExchangeInput { /** The `Client` that is using this `Exchange`. * * @remarks * The {@link Client} instantiating the {@link Exchange} will call it with the `ExchangeInput` object, * while setting `client` to itself. * * Exchanges use methods like {@link Client.reexecuteOperation} to issue {@link Operation | Operations} * themselves, and communicate with the exchange pipeline as a whole. */ client: Client; /** The next `Exchange`'s {@link ExchangeIO} function in the pipeline. * * @remarks * `Exchange`s are like middleware function, and are henced composed like a recursive pipeline. * Each `Exchange` will receive the next `Exchange`'s {@link ExchangeIO} function which they * then call to compose each other. * * Since each `Exchange` calls the next, this creates a pipeline where operations are forwarded * in sequence and `OperationResult`s from the next `Exchange` are combined with the current. */ forward: ExchangeIO; /** Issues a debug event to the `urql-devtools`. * * @remarks * If `@urql/devtools` are set up, this dispatch function issues events to the `urql-devtools`. * These events give the devtools more granular insights on what's going on in exchanges asynchronously, * since `Operation`s and `OperationResult`s only signify the “start” and “end” of a request. */ dispatchDebug( t: DebugEventArg ): void; } /** `Exchange`s are both extensions for a `Client` and part of the control-flow executing `Operation`s. * * @remarks * `Exchange`s are responsible for the pipeline in `urql` that accepts {@link Operation | Operations} and * returns {@link OperationResult | OperationResults}. They take care of adding functionality to a {@link Client}, * like deduplication, caching, and fetching (i.e. making GraphQL requests). * * When passed to the `Client`, they're instantiated with the {@link ExchangeInput} object and return an {@link ExchangeIO} * function, which is a mapping function that receives a stream of `Operation`s and returns a stream of `OperationResult`s. * * Like middleware, exchanges are composed, calling each other in a pipeline-like fashion, which is facilitated by exchanges * calling {@link ExchangeInput.forward}, which is set to the next exchange's {@link ExchangeIO} function in the pipeline. * * @see {@link https://urql.dev/goto/docs/architecture/#the-client-and-exchanges} for more information on Exchanges. * @see {@link https://urql.dev/goto/docs/advanced/authoring-exchanges} on how Exchanges are authored. * * @example * ```ts * import { pipe, onPush } from 'wonka'; * import { Exchange } from '@urql/core'; * * const debugExchange: Exchange => { * return ops$ => pipe( * ops$, * onPush(operation => console.log(operation)), * forward, * onPush(result => console.log(result)), * ); * }; * ``` */ export type Exchange = (input: ExchangeInput) => ExchangeIO; /** Returned by `Exchange`s, the `ExchangeIO` function are the composed pipeline functions. * * @remarks * An {@link Exchange} must return an `ExchangeIO` function, which accepts a stream of {@link Operation | Operations} which * this exchange handles and returns a stream of {@link OperationResult | OperationResults}. These streams are Wonka {@link Source | Sources}. * * An exchange may enhance the incoming stream of `Operation`s to add, filter, map (change), or remove `Operation`s, before * forwarding those to {@link ExchangeInput.forward}, using Wonka's operators, and may add or remove `OperationResult`s from * the returned stream. * * Generally, the stream of `OperationResult` returned by {@link ExchangeInput.forward} is always merged and combined with * the `Exchange`'s own stream of results if the `Exchange` creates and delivers results of its own. * * @see {@link https://urql.dev/goto/docs/advanced/authoring-exchanges} on how Exchanges are authored. */ export type ExchangeIO = (ops$: Source) => Source; /** A mapping type of debug event types to their payloads. * * @remarks * These are the debug events that {@link ExchangeInput.dispatchDebug} accepts mapped to the payloads these events carry. * Debug events are only used in development and only consumed by the `urql-devtools`. */ export interface DebugEventTypes { /** Signals to the devtools that a cache exchange will deliver a cached result. */ cacheHit: { value: any }; /** Signals to the devtools that a cache exchange will invalidate a cached result. */ cacheInvalidation: { typenames: string[]; response: OperationResult; }; /** Signals to the devtools that a fetch exchange will make a GraphQL API request. */ fetchRequest: { url: string; fetchOptions: RequestInit; }; /** Signals to the devtools that a fetch exchange has received a GraphQL API response successfully. */ fetchSuccess: { url: string; fetchOptions: RequestInit; value: object; }; /** Signals to the devtools that a fetch exchange has failed to execute a GraphQL API request. */ fetchError: { url: string; fetchOptions: RequestInit; value: Error; }; /** Signals to the devtools that a retry exchange will retry an Operation. */ retryRetrying: { retryCount: number; }; } /** Utility type that maps a debug event type to its payload. * * @remarks * This is a utility type that determines the required payload for a given debug event, which is * sent to the `urql-devtools`. * * The payloads for known debug events are defined by the {@link DebugEventTypes} type, and * each event additionally carries a human readable `message` with it, and the {@link Operation} * for which the event is. * * @internal */ export type DebugEventArg = { type: T; message: string; operation: Operation; } & (T extends keyof DebugEventTypes ? { data: DebugEventTypes[T] } : { data?: any }); /** Utility type of the full payload that is sent to the `urql-devtools`. * * @remarks * While the {@link DebugEventArg} defines the payload that {@link ExchangeInput.dispatchDebug} * accepts, each debug event then receives additional properties which are sent to the `urql-devtools`, * which this type defines. * @internal */ export type DebugEvent = DebugEventArg & { timestamp: number; source: string; }; ================================================ FILE: packages/core/src/utils/__snapshots__/error.test.ts.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`CombinedError > behaves like a normal Error 1`] = `"[Network] test"`; ================================================ FILE: packages/core/src/utils/collectTypenames.test.ts ================================================ import { describe, it, expect } from 'vitest'; import { collectTypenames } from './collectTypenames'; describe('collectTypenames', () => { it('returns all typenames included in a response as an array', () => { const typeNames = collectTypenames({ todos: [ { id: 1, __typename: 'Todo', }, ], }); expect(typeNames).toEqual(['Todo']); }); it('does not duplicate typenames', () => { const typeNames = collectTypenames({ todos: [ { id: 1, __typename: 'Todo', }, { id: 3, __typename: 'Todo', }, ], }); expect(typeNames).toEqual(['Todo']); }); it('returns multiple different typenames', () => { const typeNames = collectTypenames({ todos: [ { id: 1, __typename: 'Todo', }, { id: 3, __typename: 'Avocado', }, ], }); expect(typeNames).toEqual(['Todo', 'Avocado']); }); it('works on nested objects', () => { const typeNames = collectTypenames({ todos: [ { id: 1, __typename: 'Todo', }, { id: 2, subTask: { id: 3, __typename: 'SubTask', }, }, ], }); expect(typeNames).toEqual(['Todo', 'SubTask']); }); it('traverses nested arrays of objects', () => { const typenames = collectTypenames({ todos: [ { id: 1, authors: [ [ { name: 'Phil', __typename: 'Author', }, ], ], __typename: 'Todo', }, ], }); expect(typenames).toEqual(['Author', 'Todo']); }); }); ================================================ FILE: packages/core/src/utils/collectTypenames.ts ================================================ interface EntityLike { [key: string]: EntityLike | EntityLike[] | any; __typename: string | null | void; } const collectTypes = (obj: EntityLike | EntityLike[], types: Set) => { if (Array.isArray(obj)) { for (let i = 0, l = obj.length; i < l; i++) { collectTypes(obj[i], types); } } else if (typeof obj === 'object' && obj !== null) { for (const key in obj) { if (key === '__typename' && typeof obj[key] === 'string') { types.add(obj[key] as string); } else { collectTypes(obj[key], types); } } } return types; }; /** Finds and returns a list of `__typename` fields found in response data. * * @privateRemarks * This is used by `@urql/core`’s document `cacheExchange` to find typenames * in a given GraphQL response’s data. */ export const collectTypenames = (response: object): string[] => [ ...collectTypes(response as EntityLike, new Set()), ]; ================================================ FILE: packages/core/src/utils/error.test.ts ================================================ import { describe, it, expect } from 'vitest'; import { CombinedError } from './error'; describe('CombinedError', () => { it('can be instantiated with graphQLErrors', () => { const err = new CombinedError({ graphQLErrors: [], }); expect(err.name).toBe('CombinedError'); }); it('behaves like a normal Error', () => { const err = new CombinedError({ networkError: new Error('test'), }); expect(err).toBeInstanceOf(CombinedError); expect(err).toBeInstanceOf(Error); expect('' + err).toMatchSnapshot(); }); it('accepts graphQLError messages and generates a single message from them', () => { const graphQLErrors = ['Error Message A', 'Error Message B']; const err = new CombinedError({ graphQLErrors }); expect(err.message).toBe( ` [GraphQL] Error Message A [GraphQL] Error Message B `.trim() ); expect(err.graphQLErrors).toEqual(graphQLErrors.map(x => new Error(x))); }); it('accepts a network error and generates a message from it', () => { const networkError = new Error('Network Shenanigans'); const err = new CombinedError({ networkError }); expect(err.message).toBe(`[Network] ${networkError.message}`); }); it('accepts actual errors for graphQLError', () => { const graphQLErrors = [ new Error('Error Message A'), new Error('Error Message B'), ]; const err = new CombinedError({ graphQLErrors }); expect(err.message).toBe( ` [GraphQL] Error Message A [GraphQL] Error Message B `.trim() ); expect(err.graphQLErrors).toEqual(graphQLErrors); }); it('accepts empty string errors for graphQLError', () => { const graphQLErrors = [new Error('')]; const err = new CombinedError({ graphQLErrors }); expect(err.message).toBe('[GraphQL] '); expect(err.graphQLErrors).toEqual(graphQLErrors); }); it('accepts a response that is attached to the resulting error', () => { const response = {}; const err = new CombinedError({ graphQLErrors: [], response, }); expect(err.response).toBe(response); }); }); ================================================ FILE: packages/core/src/utils/error.ts ================================================ import { GraphQLError } from '@0no-co/graphql.web'; import type { ErrorLike } from '../types'; const generateErrorMessage = ( networkErr?: Error, graphQlErrs?: GraphQLError[] ) => { let error = ''; if (networkErr) return `[Network] ${networkErr.message}`; if (graphQlErrs) { for (let i = 0, l = graphQlErrs.length; i < l; i++) { if (error) error += '\n'; error += `[GraphQL] ${graphQlErrs[i].message}`; } } return error; }; const rehydrateGraphQlError = (error: any): GraphQLError => { if ( error && typeof error.message === 'string' && (error.extensions || error.name === 'GraphQLError') ) { return error; } else if (typeof error === 'object' && typeof error.message === 'string') { return new GraphQLError( error.message, error.nodes, error.source, error.positions, error.path, error, error.extensions || {} ); } else { return new GraphQLError(error as any); } }; /** An abstracted `Error` that provides either a `networkError` or `graphQLErrors`. * * @remarks * During a GraphQL request, either the request can fail entirely, causing a network error, * or the GraphQL execution or fields can fail, which will cause an {@link ExecutionResult} * to contain an array of GraphQL errors. * * The `CombinedError` abstracts and normalizes both failure cases. When {@link OperationResult.error} * is set to this error, the `CombinedError` abstracts all errors, making it easier to handle only * a subset of error cases. * * @see {@link https://urql.dev/goto/docs/basics/errors} for more information on handling * GraphQL errors and the `CombinedError`. */ export class CombinedError extends Error { public name: string; public message: string; /** A list of GraphQL errors rehydrated from a {@link ExecutionResult}. * * @remarks * If an {@link ExecutionResult} received from the API contains a list of errors, * the `CombinedError` will rehydrate them, normalize them to * {@link GraphQLError | GraphQLErrors} and list them here. * An empty list indicates that no GraphQL error has been sent by the API. */ public graphQLErrors: GraphQLError[]; /** Set to an error, if a GraphQL request has failed outright. * * @remarks * A GraphQL over HTTP request may fail and not reach the API. Any error that * prevents a GraphQl request outright, will be considered a “network error” and * set here. */ public networkError?: Error; /** Set to the {@link Response} object a fetch exchange received. * * @remarks * If a built-in fetch {@link Exchange} is used in `urql`, this may * be set to the {@link Response} object of the Fetch API response. * However, since `urql` doesn’t assume that all users will use HTTP * as the only or exclusive transport for GraphQL this property is * neither typed nor guaranteed and may be re-used for other purposes * by non-fetch exchanges. * * Hint: It can be useful to use `response.status` here, however, if * you plan on relying on this being a {@link Response} in your app, * which it is by default, then make sure you add some extra checks * before blindly assuming so! */ public response?: any; constructor(input: { networkError?: Error; graphQLErrors?: Array; response?: any; }) { const normalizedGraphQLErrors = (input.graphQLErrors || []).map( rehydrateGraphQlError ); const message = generateErrorMessage( input.networkError, normalizedGraphQLErrors ); super(message); this.name = 'CombinedError'; this.message = message; this.graphQLErrors = normalizedGraphQLErrors; this.networkError = input.networkError; this.response = input.response; } toString(): string { return this.message; } } ================================================ FILE: packages/core/src/utils/formatDocument.test.ts ================================================ import { Kind, parse, print } from '@0no-co/graphql.web'; import { describe, it, expect } from 'vitest'; import { createRequest } from './request'; import { formatDocument } from './formatDocument'; const formatTypeNames = (query: string) => { const typedNode = formatDocument(parse(query)); return print(typedNode); }; describe('formatDocument', () => { it('creates a new instance when adding typenames', () => { const doc = parse(`{ id todos { id } }`) as any; const newDoc = formatDocument(doc) as any; expect(doc).not.toBe(newDoc); expect(doc.definitions).not.toBe(newDoc.definitions); expect(doc.definitions[0]).not.toBe(newDoc.definitions[0]); expect(doc.definitions[0].selectionSet).not.toBe( newDoc.definitions[0].selectionSet ); expect(doc.definitions[0].selectionSet.selections).not.toBe( newDoc.definitions[0].selectionSet.selections ); // Here we're equal again: expect(doc.definitions[0].selectionSet.selections[0]).toBe( newDoc.definitions[0].selectionSet.selections[0] ); // Not equal again: expect(doc.definitions[0].selectionSet.selections[1]).not.toBe( newDoc.definitions[0].selectionSet.selections[1] ); expect(doc.definitions[0].selectionSet.selections[1].selectionSet).not.toBe( newDoc.definitions[0].selectionSet.selections[1].selectionSet ); // Equal again: expect( doc.definitions[0].selectionSet.selections[1].selectionSet.selections[0] ).toBe( newDoc.definitions[0].selectionSet.selections[1].selectionSet .selections[0] ); }); it('preserves the hashed key of the resulting query', () => { const doc = parse(`{ id todos { id } }`) as any; const expectedKey = createRequest(doc, undefined).key; const formattedDoc = formatDocument(doc); expect(formattedDoc).not.toBe(doc); const actualKey = createRequest(formattedDoc, undefined).key; expect(expectedKey).toBe(actualKey); }); it('does not preserve the referential integrity with a cloned object', () => { const doc = parse(`{ id todos { id } }`); const formattedDoc = formatDocument(doc); expect(formattedDoc).not.toBe(doc); const query = { ...formattedDoc }; const reformattedDoc = formatDocument(query); expect(reformattedDoc).not.toBe(doc); }); it('preserves custom properties', () => { const doc = parse(`{ todos { id } }`) as any; doc.documentId = '123'; expect((formatDocument(doc) as any).documentId).toBe(doc.documentId); }); it('adds typenames to a query string', () => { expect(formatTypeNames(`{ todos { id } }`)).toMatchInlineSnapshot(` "{ todos { id __typename } }" `); }); it('does not duplicate typenames', () => { expect( formatTypeNames(`{ todos { id __typename } }`) ).toMatchInlineSnapshot(` "{ todos { id __typename } }" `); }); it('does add typenames when it is aliased', () => { expect( formatTypeNames(`{ todos { id typename: __typename } }`) ).toMatchInlineSnapshot(` "{ todos { id typename: __typename __typename } }" `); }); it('processes directives', () => { const document = ` { todos @skip { id @_test } } `; const node = formatDocument(parse(document)); expect(node).toHaveProperty( 'definitions.0.selectionSet.selections.0.selectionSet.selections.0._directives', { test: { kind: Kind.DIRECTIVE, arguments: undefined, name: { kind: Kind.NAME, value: '_test', }, }, } ); expect(formatTypeNames(document)).toMatchInlineSnapshot(` "{ todos @skip { id __typename } }" `); }); }); ================================================ FILE: packages/core/src/utils/formatDocument.ts ================================================ import type { FieldNode, SelectionNode, DefinitionNode, DirectiveNode, } from '@0no-co/graphql.web'; import { Kind } from '@0no-co/graphql.web'; import type { KeyedDocumentNode } from './request'; import { keyDocument } from './request'; import type { FormattedNode, TypedDocumentNode } from '../types'; const formatNode = < T extends SelectionNode | DefinitionNode | TypedDocumentNode, >( node: T ): FormattedNode => { if ('definitions' in node) { const definitions: FormattedNode[] = []; for (let i = 0, l = node.definitions.length; i < l; i++) { const newDefinition = formatNode(node.definitions[i]); definitions.push(newDefinition); } return { ...node, definitions } as FormattedNode; } if ('directives' in node && node.directives && node.directives.length) { const directives: DirectiveNode[] = []; const _directives = {}; for (let i = 0, l = node.directives.length; i < l; i++) { const directive = node.directives[i]; let name = directive.name.value; if (name[0] !== '_') { directives.push(directive); } else { name = name.slice(1); } _directives[name] = directive; } node = { ...node, directives, _directives }; } if ('selectionSet' in node) { const selections: FormattedNode[] = []; let hasTypename = node.kind === Kind.OPERATION_DEFINITION; if (node.selectionSet) { for (let i = 0, l = node.selectionSet.selections.length; i < l; i++) { const selection = node.selectionSet.selections[i]; hasTypename = hasTypename || (selection.kind === Kind.FIELD && selection.name.value === '__typename' && !selection.alias); const newSelection = formatNode(selection); selections.push(newSelection); } if (!hasTypename) { selections.push({ kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename', }, _generated: true, } as FormattedNode); } return { ...node, selectionSet: { ...node.selectionSet, selections }, } as FormattedNode; } } return node as FormattedNode; }; const formattedDocs: Map = new Map< number, KeyedDocumentNode >(); /** Formats a GraphQL document to add `__typename` fields and process client-side directives. * * @param node - a {@link DocumentNode}. * @returns a {@link FormattedDocument} * * @remarks * Cache {@link Exchange | Exchanges} will require typename introspection to * recognize types in a GraphQL response. To retrieve these typenames, * this function is used to add the `__typename` fields to non-root * selection sets of a GraphQL document. * * Additionally, this utility will process directives, filter out client-side * directives starting with an `_` underscore, and place a `_directives` dictionary * on selection nodes. * * This utility also preserves the internally computed key of the * document as created by {@link createRequest} to avoid any * formatting from being duplicated. * * @see {@link https://spec.graphql.org/October2021/#sec-Type-Name-Introspection} for more information * on typename introspection via the `__typename` field. */ export const formatDocument = >( node: T ): FormattedNode => { const query = keyDocument(node); let result = formattedDocs.get(query.__key); if (!result) { formattedDocs.set( query.__key, (result = formatNode(query) as KeyedDocumentNode) ); // Ensure that the hash of the resulting document won't suddenly change // we are marking __key as non-enumerable so when external exchanges use visit // to manipulate a document we won't restore the previous query due to the __key // property. Object.defineProperty(result, '__key', { value: query.__key, enumerable: false, }); } return result as FormattedNode; }; ================================================ FILE: packages/core/src/utils/graphql.ts ================================================ import type * as GraphQLWeb from '@0no-co/graphql.web'; import type * as GraphQL from 'graphql'; type OrNever = void extends T ? never : T; export type GraphQLError = | GraphQLWeb.GraphQLError | OrNever; export type DocumentNode = | GraphQLWeb.DocumentNode | OrNever; export type DefinitionNode = | GraphQLWeb.DefinitionNode | OrNever; ================================================ FILE: packages/core/src/utils/hash.test.ts ================================================ import { HashValue, phash } from './hash'; import { expect, it } from 'vitest'; it('hashes given strings', () => { expect(phash('hello')).toMatchInlineSnapshot('261238937'); }); it('hashes given strings and seeds', () => { let hash: HashValue; expect((hash = phash('hello'))).toMatchInlineSnapshot('261238937'); expect((hash = phash('world', hash))).toMatchInlineSnapshot('-152191'); expect((hash = phash('!', hash))).toMatchInlineSnapshot('-5022270'); expect(typeof hash).toBe('number'); }); ================================================ FILE: packages/core/src/utils/hash.ts ================================================ /** A hash value as computed by {@link phash}. * * @remarks * Typically `HashValue`s are used as hashes and keys of GraphQL documents, * variables, and combined, for GraphQL requests. */ export type HashValue = number & { /** Marker to indicate that a `HashValue` may not be created by a user. * * @remarks * `HashValue`s are created by {@link phash} and are marked as such to not mix them * up with other numbers and prevent them from being created or used outside of this * hashing function. * * @internal */ readonly _opaque: unique symbol; }; /** Computes a djb2 hash of the given string. * * @param x - the string to be hashed * @param seed - optionally a prior hash for progressive hashing * @returns a hash value, i.e. a number * * @remark * This is the hashing function used throughout `urql`, primarily to compute * {@link Operation.key}. * * @see {@link http://www.cse.yorku.ca/~oz/hash.html#djb2} for a further description of djb2. */ export const phash = (x: string, seed?: HashValue): HashValue => { let h = (seed || 5381) | 0; for (let i = 0, l = x.length | 0; i < l; i++) h = (h << 5) + h + x.charCodeAt(i); return h as HashValue; }; ================================================ FILE: packages/core/src/utils/index.ts ================================================ export * from './error'; export * from './request'; export * from './result'; export * from './variables'; export * from './collectTypenames'; export * from './formatDocument'; export * from './streamUtils'; export * from './operation'; export const noop = () => { /* noop */ }; ================================================ FILE: packages/core/src/utils/operation.ts ================================================ import type { AnyVariables, GraphQLRequest, Operation, OperationContext, OperationType, } from '../types'; /** Creates a {@link Operation} from the given parameters. * * @param kind - The {@link OperationType} of GraphQL operation, i.e. `query`, `mutation`, or `subscription`. * @param request - The {@link GraphQLRequest} or {@link Operation} used as a template for the new `Operation`. * @param context - The {@link OperationContext} `context` data for the `Operation`. * @returns A new {@link Operation}. * * @remarks * This method is both used to create new {@link Operation | Operations} as well as copy and modify existing * operations. While it’s not required to use this function to copy an `Operation`, it is recommended, in case * additional dynamic logic is added to them in the future. * * Hint: When an {@link Operation} is passed to the `request` argument, the `context` argument does not have to be * a complete {@link OperationContext} and will instead be combined with passed {@link Operation.context}. * * @example * An example of copying an existing `Operation` to modify its `context`: * * ```ts * makeOperation( * operation.kind, * operation, * { requestPolicy: 'cache-first' }, * ); * ``` */ function makeOperation< Data = any, Variables extends AnyVariables = AnyVariables, >( kind: OperationType, request: GraphQLRequest, context: OperationContext ): Operation; function makeOperation< Data = any, Variables extends AnyVariables = AnyVariables, >( kind: OperationType, request: Operation, context?: Partial ): Operation; function makeOperation(kind, request, context) { return { ...request, kind, context: request.context ? { ...request.context, ...context, } : context || request.context, }; } export { makeOperation }; /** Adds additional metadata to an `Operation`'s `context.meta` property while copying it. * @see {@link OperationDebugMeta} for more information on the {@link OperationContext.meta} property. */ export const addMetadata = ( operation: Operation, meta: OperationContext['meta'] ) => { return makeOperation(operation.kind, operation, { meta: { ...operation.context.meta, ...meta, }, }); }; ================================================ FILE: packages/core/src/utils/request.test.ts ================================================ import { expect, it, describe } from 'vitest'; import { parse, print } from '@0no-co/graphql.web'; import { gql } from '../gql'; import { createRequest, stringifyDocument } from './request'; import { formatDocument } from './formatDocument'; describe('createRequest', () => { it('should hash identical queries identically', () => { const reqA = createRequest('{ test }', undefined); const reqB = createRequest('{ test }', undefined); expect(reqA.key).toBe(reqB.key); }); it('should hash identical queries identically', () => { const reqA = createRequest('{ test }', undefined); const reqB = createRequest('{ test }', undefined); expect(reqA.key).toBe(reqB.key); }); it('should hash identical DocumentNodes identically', () => { const reqA = createRequest(parse('{ testB }'), undefined); const reqB = createRequest(parse('{ testB }'), undefined); expect(reqA.key).toBe(reqB.key); expect(reqA.query).toBe(reqB.query); }); it('should use the hash from a key if available', () => { const doc = parse('{ testC }'); (doc as any).__key = 1234; const req = createRequest(doc, undefined); expect(req.key).toBe(1234); }); it('should hash DocumentNodes and strings identically', () => { const docA = parse('{ field }'); const docB = print(docA); const reqA = createRequest(docA, undefined); const reqB = createRequest(docB, undefined); expect(reqA.key).toBe(reqB.key); expect(reqA.query).toBe(reqB.query); }); it('should hash graphql-tag documents correctly', () => { const doc = gql` { testD } `; createRequest(doc, undefined); expect((doc as any).__key).not.toBe(undefined); }); it('should return a valid query object', () => { const doc = gql` { testE } `; const val = createRequest(doc, undefined); expect(val).toMatchObject({ key: expect.any(Number), query: expect.any(Object), variables: {}, }); }); it('should return a valid query object with variables', () => { const doc = print(gql` { testF } `); const val = createRequest(doc, { test: 5 }); expect(print(val.query)).toBe(doc); expect(val).toMatchObject({ key: expect.any(Number), query: expect.any(Object), variables: { test: 5 }, }); }); it('should hash persisted documents consistently', () => { const doc = parse('{ testG }'); const docPersisted = parse('{ testG }'); (docPersisted as any).documentId = 'testG'; const req = createRequest(doc, undefined); const reqPersisted = createRequest(docPersisted, undefined); expect(req.key).not.toBe(reqPersisted.key); }); }); describe('stringifyDocument ', () => { it('should reprint formatted documents', () => { const doc = parse('{ test { field } }'); const formatted = formatDocument(doc); expect(stringifyDocument(formatted)).toBe(print(formatted)); }); it('should reprint request documents', () => { const request = createRequest(`query { test { field } }`, {}); const formatted = formatDocument(request.query); expect(print(formatted)).toMatchInlineSnapshot(` "{ test { field __typename } }" `); expect(stringifyDocument(formatted)).toBe(print(formatted)); }); it('should reprint gql documents', () => { const request = createRequest( gql` query { test { field } } `, {} ); const formatted = formatDocument(request.query); expect(print(formatted)).toMatchInlineSnapshot(` "{ test { field __typename } }" `); expect(stringifyDocument(formatted)).toBe(print(formatted)); }); it('should remove comments', () => { const doc = ` { #query # broken test } `; expect(stringifyDocument(createRequest(doc, undefined).query)) .toMatchInlineSnapshot(` "{ test }" `); }); it('should remove duplicate spaces', () => { const doc = ` { abc ,, test } `; expect(stringifyDocument(createRequest(doc, undefined).query)) .toMatchInlineSnapshot(` "{ abc test }" `); }); it('should not sanitize within strings', () => { const doc = ` { field(arg: "test #1") } `; expect(stringifyDocument(createRequest(doc, undefined).query)) .toMatchInlineSnapshot(` "{ field(arg: "test #1") }" `); }); it('should not sanitize within block strings', () => { const doc = ` { field( arg: """ hello #hello """ ) } `; expect(stringifyDocument(createRequest(doc, undefined).query)) .toMatchInlineSnapshot(` "{ field(arg: """ hello #hello """) }" `); }); }); ================================================ FILE: packages/core/src/utils/request.ts ================================================ import { Kind, parse, print } from '@0no-co/graphql.web'; import type { DocumentNode, DefinitionNode } from './graphql'; import type { HashValue } from './hash'; import { phash } from './hash'; import { stringifyVariables } from './variables'; import type { DocumentInput, TypedDocumentNode, AnyVariables, GraphQLRequest, RequestExtensions, } from '../types'; type PersistedDocumentNode = TypedDocumentNode & { documentId?: string; }; /** A `DocumentNode` annotated with its hashed key. * @internal */ export type KeyedDocumentNode = TypedDocumentNode & { __key: HashValue; }; const SOURCE_NAME = 'gql'; const GRAPHQL_STRING_RE = /("{3}[\s\S]*"{3}|"(?:\\.|[^"])*")/g; const REPLACE_CHAR_RE = /(?:#[^\n\r]+)?(?:[\r\n]+|$)/g; const replaceOutsideStrings = (str: string, idx: number): string => idx % 2 === 0 ? str.replace(REPLACE_CHAR_RE, '\n') : str; /** Sanitizes a GraphQL document string by replacing comments and redundant newlines in it. */ const sanitizeDocument = (node: string): string => node.split(GRAPHQL_STRING_RE).map(replaceOutsideStrings).join('').trim(); const prints: Map = new Map< DocumentNode | DefinitionNode, string >(); const docs: Map = new Map< HashValue, KeyedDocumentNode >(); /** A cached printing function for GraphQL documents. * * @param node - A string of a document or a {@link DocumentNode} * @returns A normalized printed string of the passed GraphQL document. * * @remarks * This function accepts a GraphQL query string or {@link DocumentNode}, * then prints and sanitizes it. The sanitizer takes care of removing * comments, which otherwise alter the key of the document although the * document is otherwise equivalent to another. * * When a {@link DocumentNode} is passed to this function, it caches its * output by modifying the `loc.source.body` property on the GraphQL node. */ export const stringifyDocument = ( node: string | DefinitionNode | DocumentNode ): string => { let printed: string; if (typeof node === 'string') { printed = sanitizeDocument(node); } else if (node.loc && docs.get((node as KeyedDocumentNode).__key) === node) { printed = node.loc.source.body; } else { printed = prints.get(node) || sanitizeDocument(print(node)); prints.set(node, printed); } if (typeof node !== 'string' && !node.loc) { (node as any).loc = { start: 0, end: printed.length, source: { body: printed, name: SOURCE_NAME, locationOffset: { line: 1, column: 1 }, }, }; } return printed; }; /** Computes the hash for a document's string using {@link stringifyDocument}'s output. * * @param node - A string of a document or a {@link DocumentNode} * @returns A {@link HashValue} * * @privateRemarks * This function adds the operation name of the document to the hash, since sometimes * a merged document with multiple operations may be used. Although `urql` requires a * `DocumentNode` to only contain a single operation, when the cached `loc.source.body` * of a `DocumentNode` is used, this string may still contain multiple operations and * the resulting hash should account for only one at a time. */ const hashDocument = ( node: string | DefinitionNode | DocumentNode ): HashValue => { let key: HashValue; if ((node as PersistedDocumentNode).documentId) { key = phash((node as PersistedDocumentNode).documentId!); } else { key = phash(stringifyDocument(node)); // Add the operation name to the produced hash if ((node as DocumentNode).definitions) { const operationName = getOperationName(node as DocumentNode); if (operationName) key = phash(`\n# ${operationName}`, key); } } return key; }; /** Returns a canonical version of the passed `DocumentNode` with an added hash key. * * @param node - A string of a document or a {@link DocumentNode} * @returns A {@link KeyedDocumentNode} * * @remarks * `urql` will always avoid unnecessary work, no matter whether a user passes `DocumentNode`s * or strings of GraphQL documents to its APIs. * * This function will return a canonical version of a {@link KeyedDocumentNode} no matter * which kind of input is passed, avoiding parsing or hashing of passed data as needed. */ export const keyDocument = (node: string | DocumentNode): KeyedDocumentNode => { let key: HashValue; let query: DocumentNode; if (typeof node === 'string') { key = hashDocument(node); query = docs.get(key) || parse(node, { noLocation: true }); } else { key = (node as KeyedDocumentNode).__key || hashDocument(node); query = docs.get(key) || node; } // Add location information if it's missing if (!query.loc) stringifyDocument(query); (query as KeyedDocumentNode).__key = key; docs.set(key, query as KeyedDocumentNode); return query as KeyedDocumentNode; }; /** Creates a `GraphQLRequest` from the passed parameters. * * @param q - A string of a document or a {@link DocumentNode} * @param variables - A variables object for the defined GraphQL operation. * @returns A {@link GraphQLRequest} * * @remarks * `createRequest` creates a {@link GraphQLRequest} from the passed parameters, * while replacing the document as needed with a canonical version of itself, * to avoid parsing, printing, or hashing the same input multiple times. * * If no variables are passed, canonically it'll default to an empty object, * which is removed from the resulting hash key. */ export const createRequest = < Data = any, Variables extends AnyVariables = AnyVariables, >( _query: DocumentInput, _variables: Variables, extensions?: RequestExtensions | undefined ): GraphQLRequest => { const variables = _variables || ({} as Variables); const query = keyDocument(_query); const printedVars = stringifyVariables(variables, true); let key = query.__key; if (printedVars !== '{}') key = phash(printedVars, key); return { key, query, variables, extensions }; }; /** Returns the name of the `DocumentNode`'s operation, if any. * @param query - A {@link DocumentNode} * @returns the operation's name contained within the document, or `undefined` */ export const getOperationName = (query: DocumentNode): string | undefined => { for (let i = 0, l = query.definitions.length; i < l; i++) { const node = query.definitions[i]; if (node.kind === Kind.OPERATION_DEFINITION) { return node.name ? node.name.value : undefined; } } }; /** Returns the type of the `DocumentNode`'s operation, if any. * @param query - A {@link DocumentNode} * @returns the operation's type contained within the document, or `undefined` */ export const getOperationType = (query: DocumentNode): string | undefined => { for (let i = 0, l = query.definitions.length; i < l; i++) { const node = query.definitions[i]; if (node.kind === Kind.OPERATION_DEFINITION) { return node.operation; } } }; ================================================ FILE: packages/core/src/utils/result.test.ts ================================================ import { describe, it, expect } from 'vitest'; import { OperationResult } from '../types'; import { queryOperation, subscriptionOperation } from '../test-utils'; import { makeResult, mergeResultPatch } from './result'; import { GraphQLError } from '@0no-co/graphql.web'; import { CombinedError } from './error'; describe('makeResult', () => { it('adds extensions and errors correctly', () => { const origResult = { data: undefined, errors: ['error message'], extensions: { extensionKey: 'extensionValue', }, }; const result = makeResult(queryOperation, origResult); expect(result.hasNext).toBe(false); expect(result.operation).toBe(queryOperation); expect(result.data).toBe(undefined); expect(result.extensions).toEqual(origResult.extensions); expect(result.error).toMatchInlineSnapshot( `[CombinedError: [GraphQL] error message]` ); }); it('default hasNext to true for subscriptions', () => { const origResult = { data: undefined, errors: ['error message'], extensions: { extensionKey: 'extensionValue', }, }; const result = makeResult(subscriptionOperation, origResult); expect(result.hasNext).toBe(true); }); }); describe('mergeResultPatch (defer/stream latest', () => { it('should read pending and append the result', () => { const pending = [{ id: '0', path: [] }]; const prevResult: OperationResult = { operation: queryOperation, stale: false, hasNext: true, data: { f2: { a: 'a', b: 'b', c: { d: 'd', e: 'e', f: { h: 'h', i: 'i' }, }, }, }, }; const merged = mergeResultPatch( prevResult, { incremental: [ { id: '0', data: { MyFragment: 'Query' } }, { id: '0', subPath: ['f2', 'c', 'f'], data: { j: 'j' } }, ], // TODO: not sure if we need this but it's part of the spec // completed: [{ id: '0' }], hasNext: false, }, undefined, pending ); expect(merged.data).toEqual({ MyFragment: 'Query', f2: { a: 'a', b: 'b', c: { d: 'd', e: 'e', f: { h: 'h', i: 'i', j: 'j' }, }, }, }); }); it('should read pending and append the result w/ overlapping fields', () => { const pending = [ { id: '0', path: [], label: 'D1' }, { id: '1', path: ['f2', 'c', 'f'], label: 'D2' }, ]; const prevResult: OperationResult = { operation: queryOperation, stale: false, hasNext: true, data: { f2: { a: 'A', b: 'B', c: { d: 'D', e: 'E', f: { h: 'H', i: 'I', }, }, }, }, }; const merged = mergeResultPatch( prevResult, { incremental: [ { id: '0', subPath: ['f2', 'c', 'f'], data: { j: 'J', k: 'K' } }, ], pending: [{ id: '1', path: ['f2', 'c', 'f'], label: 'D2' }], hasNext: true, }, undefined, pending ); const merged2 = mergeResultPatch( merged, { incremental: [{ id: '1', data: { l: 'L', m: 'M' } }], hasNext: false, }, undefined, pending ); expect(merged2.data).toEqual({ f2: { a: 'A', b: 'B', c: { d: 'D', e: 'E', f: { h: 'H', i: 'I', j: 'J', k: 'K', l: 'L', m: 'M', }, }, }, }); }); }); describe('mergeResultPatch (defer/stream pre June-2023)', () => { it('should default hasNext to true if the last result was set to true', () => { const prevResult: OperationResult = { operation: subscriptionOperation, data: { __typename: 'Subscription', event: 1, }, stale: false, hasNext: true, }; const merged = mergeResultPatch(prevResult, { data: { __typename: 'Subscription', event: 2, }, }); expect(merged.data).not.toBe(prevResult.data); expect(merged.data.event).toBe(2); expect(merged.hasNext).toBe(true); }); it('should work with the payload property', () => { const prevResult: OperationResult = { operation: subscriptionOperation, data: { __typename: 'Subscription', event: 1, }, stale: false, hasNext: true, }; const merged = mergeResultPatch(prevResult, { payload: { data: { __typename: 'Subscription', event: 2, }, }, }); expect(merged.data).not.toBe(prevResult.data); expect(merged.data.event).toBe(2); expect(merged.hasNext).toBe(true); }); it('should work with the payload property and errors', () => { const prevResult: OperationResult = { operation: subscriptionOperation, data: { __typename: 'Subscription', event: 1, }, stale: false, hasNext: true, }; const merged = mergeResultPatch(prevResult, { payload: { data: { __typename: 'Subscription', event: 2, }, }, errors: [new GraphQLError('Something went horribly wrong')], }); expect(merged.data).not.toBe(prevResult.data); expect(merged.data.event).toBe(2); expect(merged.error).toEqual( new CombinedError({ graphQLErrors: [new GraphQLError('Something went horribly wrong')], }) ); expect(merged.hasNext).toBe(true); }); it('should ignore invalid patches', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', items: [ { __typename: 'Item', id: 'id', }, ], }, stale: false, hasNext: true, }; const merged = mergeResultPatch(prevResult, { incremental: [ { data: undefined, path: ['a'], }, { items: null, path: ['b'], }, ], }); expect(merged.data).toStrictEqual({ __typename: 'Query', items: [ { __typename: 'Item', id: 'id', }, ], }); }); it('should apply incremental defer patches', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', items: [ { __typename: 'Item', id: 'id', child: undefined, }, ], }, stale: false, hasNext: true, }; const patch = { __typename: 'Child' }; const merged = mergeResultPatch(prevResult, { incremental: [ { data: patch, path: ['items', 0, 'child'], }, ], }); expect(merged.data.items[0]).not.toBe(prevResult.data.items[0]); expect(merged.data.items[0].child).toBe(patch); expect(merged.data).toStrictEqual({ __typename: 'Query', items: [ { __typename: 'Item', id: 'id', child: patch, }, ], }); }); it('should handle null incremental defer patches', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', item: undefined, }, stale: false, hasNext: true, }; const merged = mergeResultPatch(prevResult, { incremental: [ { data: null, path: ['item'], }, ], }); expect(merged.data).not.toBe(prevResult.data); expect(merged.data.item).toBe(null); }); it('should apply incremental stream patches', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', items: [{ __typename: 'Item' }], }, stale: false, hasNext: true, }; const patch = { __typename: 'Item' }; const merged = mergeResultPatch(prevResult, { incremental: [ { items: [patch], path: ['items', 1], }, ], }); expect(merged.data.items).not.toBe(prevResult.data.items); expect(merged.data.items[0]).toBe(prevResult.data.items[0]); expect(merged.data.items[1]).toBe(patch); expect(merged.data).toStrictEqual({ __typename: 'Query', items: [{ __typename: 'Item' }, { __typename: 'Item' }], }); }); it('should apply incremental stream patches deeply', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', test: [ { __typename: 'Test', }, ], }, stale: false, hasNext: true, }; const patch = { name: 'Test' }; const merged = mergeResultPatch(prevResult, { incremental: [ { items: [patch], path: ['test', 0], }, ], }); expect(merged.data).toStrictEqual({ __typename: 'Query', test: [ { __typename: 'Test', name: 'Test', }, ], }); }); it('should handle null incremental stream patches', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', items: [{ __typename: 'Item' }], }, stale: false, hasNext: true, }; const merged = mergeResultPatch(prevResult, { incremental: [ { items: null, path: ['items', 1], }, ], }); expect(merged.data.items).not.toBe(prevResult.data.items); expect(merged.data.items[0]).toBe(prevResult.data.items[0]); expect(merged.data).toStrictEqual({ __typename: 'Query', items: [{ __typename: 'Item' }], }); }); it('should handle root incremental stream patches', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', item: { test: true, }, }, stale: false, hasNext: true, }; const merged = mergeResultPatch(prevResult, { incremental: [ { data: { item: { test2: false } }, path: [], }, ], }); expect(merged.data).toStrictEqual({ __typename: 'Query', item: { test: true, test2: false, }, }); }); it('should merge extensions from each patch', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', }, extensions: { base: true, }, stale: false, hasNext: true, }; const merged = mergeResultPatch(prevResult, { incremental: [ { data: null, path: ['item'], extensions: { patch: true, }, }, ], }); expect(merged.extensions).toStrictEqual({ base: true, patch: true, }); }); it('should combine errors from each patch', () => { const prevResult: OperationResult = makeResult(queryOperation, { errors: ['base'], }); const merged = mergeResultPatch(prevResult, { incremental: [ { data: null, path: ['item'], errors: ['patch'], }, ], }); expect(merged.error).toMatchInlineSnapshot(` [CombinedError: [GraphQL] base [GraphQL] patch] `); }); it('should preserve all data for noop patches', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', }, extensions: { base: true, }, stale: false, hasNext: true, }; const merged = mergeResultPatch(prevResult, { hasNext: false, }); expect(merged.data).toStrictEqual({ __typename: 'Query', }); }); it('handles the old version of the incremental payload spec (DEPRECATED)', () => { const prevResult: OperationResult = { operation: queryOperation, data: { __typename: 'Query', items: [ { __typename: 'Item', id: 'id', child: undefined, }, ], }, stale: false, hasNext: true, }; const patch = { __typename: 'Child' }; const merged = mergeResultPatch(prevResult, { data: patch, path: ['items', 0, 'child'], } as any); expect(merged.data.items[0]).not.toBe(prevResult.data.items[0]); expect(merged.data.items[0].child).toBe(patch); expect(merged.data).toStrictEqual({ __typename: 'Query', items: [ { __typename: 'Item', id: 'id', child: patch, }, ], }); }); }); ================================================ FILE: packages/core/src/utils/result.ts ================================================ import type { ExecutionResult, Operation, OperationResult, IncrementalPayload, } from '../types'; import { CombinedError } from './error'; /** Converts the `ExecutionResult` received for a given `Operation` to an `OperationResult`. * * @param operation - The {@link Operation} for which the API’s result is for. * @param result - The GraphQL API’s {@link ExecutionResult}. * @param response - Optionally, a raw object representing the API’s result (Typically a {@link Response}). * @returns An {@link OperationResult}. * * @remarks * This utility can be used to create {@link OperationResult | OperationResults} in the shape * that `urql` expects and defines, and should be used rather than creating the results manually. * * @throws * If no data, or errors are contained within the result, or the result is instead an incremental * response containing a `path` property, a “No Content” error is thrown. * * @see {@link ExecutionResult} for the type definition of GraphQL API results. */ export const makeResult = ( operation: Operation, result: ExecutionResult, response?: any ): OperationResult => { if ( !('data' in result) && (!('errors' in result) || !Array.isArray(result.errors)) ) { throw new Error('No Content'); } const defaultHasNext = operation.kind === 'subscription'; return { operation, data: result.data, error: Array.isArray(result.errors) ? new CombinedError({ graphQLErrors: result.errors, response, }) : undefined, extensions: result.extensions ? { ...result.extensions } : undefined, hasNext: result.hasNext == null ? defaultHasNext : result.hasNext, stale: false, }; }; const deepMerge = (target: any, source: any): any => { if (typeof target === 'object' && target != null) { if (Array.isArray(target)) { target = [...target]; for (let i = 0, l = source.length; i < l; i++) target[i] = deepMerge(target[i], source[i]); return target; } if (!target.constructor || target.constructor === Object) { target = { ...target }; for (const key in source) target[key] = deepMerge(target[key], source[key]); return target; } } return source; }; /** Merges an incrementally delivered `ExecutionResult` into a previous `OperationResult`. * * @param prevResult - The {@link OperationResult} that preceded this result. * @param path - The GraphQL API’s {@link ExecutionResult} that should be patching the `prevResult`. * @param response - Optionally, a raw object representing the API’s result (Typically a {@link Response}). * @returns A new {@link OperationResult} patched with the incremental result. * * @remarks * This utility should be used to merge subsequent {@link ExecutionResult | ExecutionResults} of * incremental responses into a prior {@link OperationResult}. * * When directives like `@defer`, `@stream`, and `@live` are used, GraphQL may deliver new * results that modify previous results. In these cases, it'll set a `path` property to modify * the result it sent last. This utility is built to handle these cases and merge these payloads * into existing {@link OperationResult | OperationResults}. * * @see {@link ExecutionResult} for the type definition of GraphQL API results. */ export const mergeResultPatch = ( prevResult: OperationResult, nextResult: ExecutionResult, response?: any, pending?: ExecutionResult['pending'] ): OperationResult => { let errors = prevResult.error ? prevResult.error.graphQLErrors : []; let hasExtensions = !!prevResult.extensions || !!(nextResult.payload || nextResult).extensions; const extensions = { ...prevResult.extensions, ...(nextResult.payload || nextResult).extensions, }; let incremental = nextResult.incremental; // NOTE: We handle the old version of the incremental delivery payloads as well if ('path' in nextResult) { incremental = [nextResult as IncrementalPayload]; } const withData = { data: prevResult.data }; if (incremental) { for (let i = 0, l = incremental.length; i < l; i++) { const patch = incremental[i]; if (Array.isArray(patch.errors)) { errors.push(...(patch.errors as any)); } if (patch.extensions) { Object.assign(extensions, patch.extensions); hasExtensions = true; } let prop: string | number = 'data'; let part: Record | Array = withData; let path: readonly (string | number)[] = []; if (patch.path) { path = patch.path; } else if (pending) { const res = pending.find(pendingRes => pendingRes.id === patch.id); if (patch.subPath) { path = [...res!.path, ...patch.subPath]; } else { path = res!.path; } } for (let i = 0, l = path.length; i < l; prop = path[i++]) { part = part[prop] = Array.isArray(part[prop]) ? [...part[prop]] : { ...part[prop] }; } if (patch.items) { const startIndex = +prop >= 0 ? (prop as number) : 0; for (let i = 0, l = patch.items.length; i < l; i++) part[startIndex + i] = deepMerge( part[startIndex + i], patch.items[i] ); } else if (patch.data !== undefined) { part[prop] = deepMerge(part[prop], patch.data); } } } else { withData.data = (nextResult.payload || nextResult).data || prevResult.data; errors = (nextResult.errors as any[]) || (nextResult.payload && nextResult.payload.errors) || errors; } return { operation: prevResult.operation, data: withData.data, error: errors.length ? new CombinedError({ graphQLErrors: errors, response }) : undefined, extensions: hasExtensions ? extensions : undefined, hasNext: nextResult.hasNext != null ? nextResult.hasNext : prevResult.hasNext, stale: false, }; }; /** Creates an `OperationResult` containing a network error for requests that encountered unexpected errors. * * @param operation - The {@link Operation} for which the API’s result is for. * @param error - The network-like error that prevented an API result from being delivered. * @param response - Optionally, a raw object representing the API’s result (Typically a {@link Response}). * @returns An {@link OperationResult} containing only a {@link CombinedError}. * * @remarks * This utility can be used to create {@link OperationResult | OperationResults} in the shape * that `urql` expects and defines, and should be used rather than creating the results manually. * This function should be used for when the {@link CombinedError.networkError} property is * populated and no GraphQL execution actually occurred. */ export const makeErrorResult = ( operation: Operation, error: Error, response?: any ): OperationResult => ({ operation, data: undefined, error: new CombinedError({ networkError: error, response, }), extensions: undefined, hasNext: false, stale: false, }); ================================================ FILE: packages/core/src/utils/streamUtils.ts ================================================ import type { Sink, Source } from 'wonka'; import { subscribe, take, filter, toPromise, pipe } from 'wonka'; import type { OperationResult, OperationResultSource } from '../types'; /** Patches a `toPromise` method onto the `Source` passed to it. * @param source$ - the Wonka {@link Source} to patch. * @returns The passed `source$` with a patched `toPromise` method as a {@link PromisifiedSource}. * @internal */ export function withPromise( _source$: Source ): OperationResultSource { const source$ = ((sink: Sink) => _source$(sink)) as OperationResultSource; source$.toPromise = () => pipe( source$, filter(result => !result.stale && !result.hasNext), take(1), toPromise ); source$.then = (onResolve, onReject) => source$.toPromise().then(onResolve, onReject); source$.subscribe = onResult => subscribe(onResult)(source$); return source$; } ================================================ FILE: packages/core/src/utils/variables.test.ts ================================================ // @vitest-environment jsdom import { stringifyVariables, extractFiles } from './variables'; import { describe, it, expect } from 'vitest'; import { Script } from 'vm'; describe('stringifyVariables', () => { it('stringifies objects stabily', () => { expect(stringifyVariables({ b: 'b', a: 'a' })).toBe('{"a":"a","b":"b"}'); expect(stringifyVariables({ x: { b: 'b', a: 'a' } })).toBe( '{"x":{"a":"a","b":"b"}}' ); }); it('stringifies arrays', () => { expect(stringifyVariables([1, 2])).toBe('[1,2]'); expect(stringifyVariables({ x: [1, 2] })).toBe('{"x":[1,2]}'); }); it('stringifies scalars', () => { expect(stringifyVariables(1)).toBe('1'); expect(stringifyVariables('test')).toBe('"test"'); expect(stringifyVariables(null)).toBe('null'); expect(stringifyVariables(undefined)).toBe(''); expect(stringifyVariables(Infinity)).toBe('null'); expect(stringifyVariables(1 / 0)).toBe('null'); }); it('returns null for circular structures', () => { const x = { x: null } as any; x.x = x; expect(stringifyVariables(x)).toBe('{"x":null}'); }); it('stringifies dates correctly', () => { const date = new Date('2019-12-11T04:20:00'); expect(stringifyVariables(date)).toBe(`"${date.toJSON()}"`); }); it('stringifies dictionaries (Object.create(null)) correctly', () => { expect(stringifyVariables(Object.create(null))).toBe('{}'); }); it('recovers if the root object is a dictionary (Object.create(null)) and nests a plain object', () => { const root = Object.create(null); root.data = { test: true }; expect(stringifyVariables(root)).toBe('{"data":{"test":true}}'); }); it('recovers if the root object contains a dictionary (Object.create(null))', () => { const data = Object.create(null); data.test = true; const root = { data }; expect(stringifyVariables(root)).toBe('{"data":{"test":true}}'); }); it('replaces non-plain objects at the root with keyed replacements', () => { expect(stringifyVariables(new (class Test {})())).toMatch( /^{"__key":"\w+"}$/ ); expect(stringifyVariables(new Map())).toMatch(/^{"__key":"\w+"}$/); }); it('stringifies files correctly', () => { const file = new File([0] as any, 'test.js'); const str = stringifyVariables(file); expect(str).toBe('null'); }); it('stringifies plain objects from foreign JS contexts correctly', () => { const scriptGlobal: typeof globalThis = new Script( 'exports = globalThis' ).runInNewContext({}).exports; const plain = new scriptGlobal.Function('return { test: true }')(); expect(stringifyVariables(plain)).toBe('{"test":true}'); const data = new scriptGlobal.Function('return new (class Test {})')(); expect(stringifyVariables(data)).toMatch(/^{"__key":"\w+"}$/); }); }); describe('extractFiles', () => { it('extracts files from nested objects', () => { const file = new Blob(); expect(extractFiles({ files: { a: file } })).toEqual( new Map([['variables.files.a', file]]) ); }); it('extracts files from nested arrays', () => { const file = new Blob(); expect(extractFiles({ files: [file] })).toEqual( new Map([['variables.files.0', file]]) ); }); }); ================================================ FILE: packages/core/src/utils/variables.ts ================================================ export type FileMap = Map; const seen: Set = new Set(); const cache: WeakMap = new WeakMap(); const stringify = (x: any, includeFiles: boolean): string => { if (x === null || seen.has(x)) { return 'null'; } else if (typeof x !== 'object') { return JSON.stringify(x) || ''; } else if (x.toJSON) { return stringify(x.toJSON(), includeFiles); } else if (Array.isArray(x)) { let out = '['; for (let i = 0, l = x.length; i < l; i++) { if (out.length > 1) out += ','; out += stringify(x[i], includeFiles) || 'null'; } out += ']'; return out; } else if ( !includeFiles && ((FileConstructor !== NoopConstructor && x instanceof FileConstructor) || (BlobConstructor !== NoopConstructor && x instanceof BlobConstructor)) ) { return 'null'; } const keys = Object.keys(x).sort(); if ( !keys.length && x.constructor && Object.getPrototypeOf(x).constructor !== Object.prototype.constructor ) { const key = cache.get(x) || Math.random().toString(36).slice(2); cache.set(x, key); return stringify({ __key: key }, includeFiles); } seen.add(x); let out = '{'; for (let i = 0, l = keys.length; i < l; i++) { const value = stringify(x[keys[i]], includeFiles); if (value) { if (out.length > 1) out += ','; out += stringify(keys[i], includeFiles) + ':' + value; } } seen.delete(x); out += '}'; return out; }; const extract = (map: FileMap, path: string, x: any): void => { if (x == null || typeof x !== 'object' || x.toJSON || seen.has(x)) { /*noop*/ } else if (Array.isArray(x)) { for (let i = 0, l = x.length; i < l; i++) extract(map, `${path}.${i}`, x[i]); } else if (x instanceof FileConstructor || x instanceof BlobConstructor) { map.set(path, x as File | Blob); } else { seen.add(x); for (const key in x) extract(map, `${path}.${key}`, x[key]); } }; /** A stable stringifier for GraphQL variables objects. * * @param x - any JSON-like data. * @return A JSON string. * * @remarks * This utility creates a stable JSON string from any passed data, * and protects itself from throwing. * * The JSON string is stable insofar as objects’ keys are sorted, * and instances of non-plain objects are replaced with random keys * replacing their values, which remain stable for the objects’ * instance. */ export const stringifyVariables = (x: any, includeFiles?: boolean): string => { seen.clear(); return stringify(x, includeFiles || false); }; class NoopConstructor {} const FileConstructor = typeof File !== 'undefined' ? File : NoopConstructor; const BlobConstructor = typeof Blob !== 'undefined' ? Blob : NoopConstructor; export const extractFiles = (x: any): FileMap => { const map: FileMap = new Map(); if ( FileConstructor !== NoopConstructor || BlobConstructor !== NoopConstructor ) { seen.clear(); extract(map, 'variables', x); } return map; }; ================================================ FILE: packages/core/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/core/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: packages/introspection/CHANGELOG.md ================================================ # @urql/introspection ## 1.2.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) ## 1.2.0 ### Minor Changes - Add oneOf support Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3743](https://github.com/urql-graphql/urql/pull/3743)) ## 1.1.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ## 1.0.3 ### Patch Changes - ⚠️ Fix `Any` type being included, even when it isn’t needed Submitted by [@kitten](https://github.com/kitten) (See [#3481](https://github.com/urql-graphql/urql/pull/3481)) ## 1.0.2 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 1.0.1 ### Patch Changes - Add TSDocs to `@urql/*` packages Submitted by [@kitten](https://github.com/kitten) (See [#3079](https://github.com/urql-graphql/urql/pull/3079)) ## 1.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ## 0.3.3 ### Patch Changes - Avoid making the imports of `@urql/introspection` more specific than they need to be, this because we aren't optimizing for bundle size and in pure node usage this can confuse Node as `import x from 'graphql'` won't share the same module scope as `import x from 'graphql/x/y.mjs'`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2529](https://github.com/FormidableLabs/urql/pull/2529)) ## 0.3.2 ### Patch Changes - ⚠️ Fix import of `executeSync` rather than `execute` causing an incompatibility when several `.mjs` imports and a main `import { executeSync } from 'graphql'` are causing two different modules to be instantiated, by [@kitten](https://github.com/kitten) (See [#2251](https://github.com/FormidableLabs/urql/pull/2251)) ## 0.3.1 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) ## 0.3.0 ### Minor Changes - Add options to `@urql/introspection`'s `minifyIntrospectionQuery` allowing the inclusion of more information into the minified schema as needed, namely `includeScalars`, `includeEnums`, `includeInputs`, and `includeDirectives`, by [@kitten](https://github.com/kitten) (See [#1578](https://github.com/FormidableLabs/urql/pull/1578)) ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) ## 0.2.0 ### Minor Changes - Update `minifyIntrospectionQuery` utility to remove additional information on arguments and to filter out schema metadata types, like `__Field` and others, by [@kitten](https://github.com/kitten) (See [#1351](https://github.com/FormidableLabs/urql/pull/1351)) ## 0.1.2 ### Patch Changes - ⚠️ Fix the `graphql` dependency being postfixed with `.mjs` when building the package, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1204](https://github.com/FormidableLabs/urql/pull/1204)) ## 0.1.1 ### Patch Changes - Add missing `.mjs` extension to all imports from `graphql` to fix Webpack 5 builds, which require extension-specific import paths for ESM bundles and packages. **This change allows you to safely upgrade to Webpack 5.**, by [@kitten](https://github.com/kitten) (See [#1094](https://github.com/FormidableLabs/urql/pull/1094)) ## 0.1.0 **Initial Release** ================================================ FILE: packages/introspection/README.md ================================================

@urql/introspection

Utilities for dealing with Introspection Queries and Client Schemas

================================================ FILE: packages/introspection/jsr.json ================================================ { "name": "@urql/introspection", "version": "1.2.1", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/introspection/package.json ================================================ { "name": "@urql/introspection", "version": "1.2.1", "description": "Utilities for dealing with Introspection Queries and Client Schemas", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/introspection" }, "keywords": [ "graphql", "graphql client", "graphql schema", "schema" ], "main": "dist/urql-introspection", "module": "dist/urql-introspection.mjs", "types": "dist/urql-introspection.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-introspection.d.ts", "import": "./dist/urql-introspection.mjs", "require": "./dist/urql-introspection.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "README.md", "dist/" ], "scripts": { "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "graphql": "^16.0.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: packages/introspection/src/getIntrospectedSchema.ts ================================================ import type { IntrospectionQuery, GraphQLSchema } from 'graphql'; import { parse, buildSchema, execute, getIntrospectionQuery } from 'graphql'; /** Returns an {@link IntrospectionQuery} result for a given GraphQL schema. * * @param input - A GraphQL schema, either as an SDL string, or a {@link GraphQLSchema} object. * @returns an {@link IntrospectionQuery} result. * * @remarks * `getIntrospectedSchema` can be used to get a Schema Introspection result from * a given GraphQL schema. The schema can be passed as an SDL string or a * {@link GraphQLSchema} object. If an {@link IntrospectionQuery} object is * passed, it'll be passed through. * * @throws * If `input` cannot be parsed or converted into a {@link GraphQLSchema} then * a {@link TypeError} will be thrown. */ export const getIntrospectedSchema = ( input: string | IntrospectionQuery | GraphQLSchema ): IntrospectionQuery => { if (typeof input === 'string') { try { input = JSON.parse(input); } catch (_error) { input = buildSchema(input as string); } } if (typeof input === 'object' && '__schema' in input) { return input; } const initialIntrospection: any = execute({ document: parse(getIntrospectionQuery({ descriptions: false })), schema: input as GraphQLSchema, }); if (!initialIntrospection.data || !initialIntrospection.data.__schema) { throw new TypeError( 'GraphQL could not generate an IntrospectionQuery from the given schema.' ); } return initialIntrospection.data as IntrospectionQuery; }; ================================================ FILE: packages/introspection/src/index.ts ================================================ export * from './getIntrospectedSchema'; export * from './minifyIntrospectionQuery'; ================================================ FILE: packages/introspection/src/minifyIntrospectionQuery.ts ================================================ import type { IntrospectionQuery, IntrospectionType, IntrospectionTypeRef, IntrospectionInputValue, IntrospectionDirective, } from 'graphql'; let _includeScalars = false; let _includeEnums = false; let _includeInputs = false; let _hasAnyType = false; const anyType: IntrospectionTypeRef = { kind: 'SCALAR', name: 'Any', }; const mapType = (fromType: any): IntrospectionTypeRef => { switch (fromType.kind) { case 'NON_NULL': case 'LIST': return { kind: fromType.kind, ofType: mapType(fromType.ofType), }; case 'SCALAR': if (_includeScalars) { return fromType; } else { _hasAnyType = true; return anyType; } case 'INPUT_OBJECT': if (_includeInputs) { return fromType; } else { _hasAnyType = true; return anyType; } case 'ENUM': if (_includeEnums) { return fromType; } else { _hasAnyType = true; return anyType; } case 'OBJECT': case 'INTERFACE': case 'UNION': return fromType; default: throw new TypeError( `Unrecognized type reference of type: ${(fromType as any).kind}.` ); } }; const minifyIntrospectionType = ( type: IntrospectionType ): IntrospectionType => { switch (type.kind) { case 'SCALAR': return { kind: 'SCALAR', name: type.name, }; case 'ENUM': return { kind: 'ENUM', name: type.name, enumValues: type.enumValues.map( value => ({ name: value.name, }) as any ), }; case 'INPUT_OBJECT': { return { kind: 'INPUT_OBJECT', name: type.name, isOneOf: type.isOneOf, inputFields: type.inputFields.map( field => ({ name: field.name, type: mapType(field.type), defaultValue: field.defaultValue || undefined, }) as IntrospectionInputValue ), }; } case 'OBJECT': return { kind: 'OBJECT', name: type.name, fields: type.fields.map( field => ({ name: field.name, type: field.type && mapType(field.type), args: field.args && field.args.map(arg => ({ name: arg.name, type: mapType(arg.type), })), }) as any ), interfaces: type.interfaces && type.interfaces.map(int => ({ kind: 'INTERFACE', name: int.name, })), }; case 'INTERFACE': return { kind: 'INTERFACE', name: type.name, fields: type.fields.map( field => ({ name: field.name, type: field.type && mapType(field.type), args: field.args && field.args.map(arg => ({ name: arg.name, type: mapType(arg.type), })), }) as any ), interfaces: type.interfaces && type.interfaces.map(int => ({ kind: 'INTERFACE', name: int.name, })), possibleTypes: type.possibleTypes && type.possibleTypes.map(type => ({ kind: type.kind, name: type.name, })), }; case 'UNION': return { kind: 'UNION', name: type.name, possibleTypes: type.possibleTypes.map(type => ({ kind: type.kind, name: type.name, })), }; default: return type; } }; /** Input parameters for the {@link minifyIntrospectionQuery} function. */ export interface MinifySchemaOptions { /** Includes scalars instead of removing them. * * @remarks * By default, all scalars will be replaced by a single scalar called `Any` * in the output, unless this option is set to `true`. */ includeScalars?: boolean; /** Includes enums instead of removing them. * * @remarks * By default, all enums will be replaced by a single scalar called `Any` * in the output, unless this option is set to `true`. */ includeEnums?: boolean; /** Includes inputs instead of removing them. * * @remarks * By default, all inputs will be replaced by a single scalar called `Any` * in the output, unless this option is set to `true`. */ includeInputs?: boolean; /** Includes directives instead of removing them. */ includeDirectives?: boolean; } /** Minifies an {@link IntrospectionQuery} for use with Graphcache or the `populateExchange`. * * @param schema - An {@link IntrospectionQuery} object to be minified. * @param opts - An optional {@link MinifySchemaOptions} configuration object. * @returns the minified {@link IntrospectionQuery} object. * * @remarks * `minifyIntrospectionQuery` reduces the size of an {@link IntrospectionQuery} by * removing data and information that a client-side consumer, like Graphcache or the * `populateExchange`, may not require. * * At the very least, it will remove system types, descriptions, depreactions, * and source locations. Unless disabled via the options passed, it will also * by default remove all scalars, enums, inputs, and directives. * * @throws * If `schema` receives an object that isn’t an {@link IntrospectionQuery}, a * {@link TypeError} will be thrown. */ export const minifyIntrospectionQuery = ( schema: IntrospectionQuery, opts: MinifySchemaOptions = {} ): IntrospectionQuery => { if (!schema || !('__schema' in schema)) { throw new TypeError('Expected to receive an IntrospectionQuery.'); } _hasAnyType = false; _includeScalars = !!opts.includeScalars; _includeEnums = !!opts.includeEnums; _includeInputs = !!opts.includeInputs; const { __schema: { queryType, mutationType, subscriptionType, types, directives }, } = schema; const minifiedTypes = types .filter(type => { switch (type.name) { case '__Directive': case '__DirectiveLocation': case '__EnumValue': case '__InputValue': case '__Field': case '__Type': case '__TypeKind': case '__Schema': return false; default: return ( (_includeScalars && type.kind === 'SCALAR') || (_includeEnums && type.kind === 'ENUM') || (_includeInputs && type.kind === 'INPUT_OBJECT') || type.kind === 'OBJECT' || type.kind === 'INTERFACE' || type.kind === 'UNION' ); } }) .map(minifyIntrospectionType); if (_hasAnyType) { minifiedTypes.push({ kind: 'SCALAR', name: anyType.name }); } let minifiedDirectives: IntrospectionDirective[] = []; if (opts.includeDirectives) { minifiedDirectives = (directives || []).map(directive => ({ name: directive.name, isRepeatable: directive.isRepeatable ? true : undefined, locations: directive.locations, args: directive.args.map( arg => ({ name: arg.name, type: mapType(arg.type), defaultValue: arg.defaultValue || undefined, }) as IntrospectionInputValue ), })); } return { __schema: { queryType, mutationType, subscriptionType, types: minifiedTypes, directives: minifiedDirectives, }, }; }; ================================================ FILE: packages/introspection/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/next-urql/.gitignore ================================================ /rsc ================================================ FILE: packages/next-urql/CHANGELOG.md ================================================ # Changelog ## 2.0.0 ### Patch Changes - Updated dependencies - urql@5.0.0 ## 1.1.5 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) ## 1.1.4 ### Patch Changes - Update Provider TSDoc to reflect our advice on instantiating the client within a React component Submitted by [@y-hsgw](https://github.com/y-hsgw) (See [#3748](https://github.com/urql-graphql/urql/pull/3748)) ## 1.1.3 ### Patch Changes - Add type for hasNext to the query-state in urql-next Submitted by [@isy](https://github.com/isy) (See [#3707](https://github.com/urql-graphql/urql/pull/3707)) ## 1.1.2 ### Patch Changes - export SSRContext from provider Submitted by [@ccummings](https://github.com/ccummings) (See [#3659](https://github.com/urql-graphql/urql/pull/3659)) ## 1.1.1 ### Patch Changes - ⚠️ Fix `CVE-2024-24556`, addressing an XSS vulnerability, where `@urql/next` failed to escape HTML characters in JSON payloads injected into RSC hydration bodies. When an attacker is able to manipulate strings in the JSON response in RSC payloads, this could cause HTML to be evaluated via a typical XSS vulnerability (See [`GHSA-qhjf-hm5j-335w`](https://github.com/urql-graphql/urql/security/advisories/GHSA-qhjf-hm5j-335w) for details.) Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [`4b7011b7`](https://github.com/urql-graphql/urql/commit/4b7011b70d5718728ff912d02a4dbdc7f703540d)) ## 1.1.0 ### Minor Changes - Support a `nonce` prop on `DataHydrationContextProvider` that passes it onto its script tags' attributes Submitted by [@Enalmada](https://github.com/Enalmada) (See [#3398](https://github.com/urql-graphql/urql/pull/3398)) ### Patch Changes - ⚠️ Fix invalid CJS by importing react with import-all semantics Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3405](https://github.com/urql-graphql/urql/pull/3405)) ## 1.0.0 ### Major Changes - Create `@urql/next` which is a package meant to support Next 13 and the React 18 features contained within. For server components we have `@urql/next/rsc` and for client components just `@urql/next` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3214](https://github.com/urql-graphql/urql/pull/3214)) ## 5.0.2 ### Patch Changes - Switch `react` imports to namespace imports, and update build process for CommonJS outputs to interoperate with `__esModule` marked modules again Submitted by [@kitten](https://github.com/kitten) (See [#3251](https://github.com/urql-graphql/urql/pull/3251)) ## 5.0.1 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 5.0.0 ### Patch Changes - Add TSDocs to `@urql/*` packages Submitted by [@kitten](https://github.com/kitten) (See [#3079](https://github.com/urql-graphql/urql/pull/3079)) - Updated dependencies (See [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3095](https://github.com/urql-graphql/urql/pull/3095), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3103](https://github.com/urql-graphql/urql/pull/3103), and [#3079](https://github.com/urql-graphql/urql/pull/3079)) - urql@4.0.0 ## 4.0.3 ### Patch Changes - Add `pageProps: {}` entry to props on app components, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2909](https://github.com/urql-graphql/urql/pull/2909)) ## 4.0.2 ### Patch Changes - ⚠️ Fix type-generation, with a change in TS/Rollup the type generation took the paths as src and resolved them into the types dir, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2870](https://github.com/urql-graphql/urql/pull/2870)) ## 4.0.1 ### Patch Changes - Change import for `createClient` to `@urql/core`, which helps Next not depend on `urql` and hence not cause `createContext` to be called when the import is treeshaken away, by [@SleeplessOne1917](https://github.com/SleeplessOne1917) (See [#2833](https://github.com/urql-graphql/urql/pull/2833)) ## 4.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Patch Changes - Updated dependencies (See [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - urql@3.0.0 ## 3.3.3 ### Patch Changes - ⚠️ Fix Node.js ESM re-export detection for `@urql/core` in `urql` package and CommonJS output for all other CommonJS-first packages. This ensures that Node.js' `cjs-module-lexer` can correctly identify re-exports and report them properly. Otherwise, this will lead to a runtime error, by [@kitten](https://github.com/kitten) (See [#2485](https://github.com/FormidableLabs/urql/pull/2485)) ## 3.3.2 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) ## 3.3.1 ### Patch Changes - ⚠️ Fix bail when the `getInitialProps` call indicates we've finished the response, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2101](https://github.com/FormidableLabs/urql/pull/2101)) ## 3.3.0 ### Minor Changes - Support forwarding the getLayout function from pages, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2065](https://github.com/FormidableLabs/urql/pull/2065)) ## 3.2.1 ### Patch Changes - ⚠️ Fix issue where the `renderToString` pass would keep looping due to reexecuting operations on the server, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1895](https://github.com/FormidableLabs/urql/pull/1895)) ## 3.2.0 ### Minor Changes - Add new `staleWhileRevalidate` option from the `ssrExchange` addition to `withUrqlClient`'s options. This is useful when Next.js is used in static site generation (SSG) mode, by [@kitten](https://github.com/kitten) (See [#1852](https://github.com/FormidableLabs/urql/pull/1852)) ### Patch Changes - Use the built-in `next` types for next-urql HOC return values, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1812](https://github.com/FormidableLabs/urql/pull/1812)) ## 3.1.1 ### Patch Changes - ⚠️ Fix `resetUrqlClient` not resetting the SSR cache itself and instead restoring data when all data related to this `Client` and session should've been deleted, by [@Biboswan](https://github.com/Biboswan) (See [#1715](https://github.com/FormidableLabs/urql/pull/1715)) ## 3.1.0 ### Minor Changes - Allow subsequent static-pages to hydrate the cache, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1602](https://github.com/FormidableLabs/urql/pull/1602)) ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) ## 3.0.1 ### Patch Changes - Ensure `urqlState` is hydrated onto the client when a user opts out of `ssr` and uses the `getServerSideProps` or `getStaticProps` on a page-level and `withUrqlClient` is wrapped on an `_app` level. Examples: - [getStaticProps](https://codesandbox.io/s/urql-get-static-props-dmjch?file=/pages/index.js) - [getServerSideProps](https://codesandbox.io/s/urql-get-static-props-forked-xfbrs?file=/pages/index.js), by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1501](https://github.com/FormidableLabs/urql/pull/1501)) ## 3.0.0 ### Patch Changes - Updated dependencies (See [#1335](https://github.com/FormidableLabs/urql/pull/1335), [#1357](https://github.com/FormidableLabs/urql/pull/1357), and [#1374](https://github.com/FormidableLabs/urql/pull/1374)) - urql@2.0.0 ## 2.2.0 ### Minor Changes - Fix, update Next integration types so that they work with the newer `NextPage` typings, by [@wgolledge](https://github.com/wgolledge) (See [#1294](https://github.com/FormidableLabs/urql/pull/1294)) ### Patch Changes - ⚠️ Fix `withUrqlClient` fast-refresh detection, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1302](https://github.com/FormidableLabs/urql/pull/1302)) ## 2.1.1 ### Patch Changes - ⚠️ Fix the production build overwriting the development build. Specifically in the previous release we mistakenly replaced all development bundles with production bundles. This doesn't have any direct influence on how these packages work, but prevented development warnings from being logged or full errors from being thrown, by [@kitten](https://github.com/kitten) (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - Updated dependencies - urql@1.10.3 ## 2.1.0 ### Minor Changes - Update `next-urql` types to be free-standing and not depend on the types from the `next` packages, by [@kitten](https://github.com/kitten) (See [#1095](https://github.com/FormidableLabs/urql/pull/1095)) ### Patch Changes - Updated dependencies (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - urql@1.10.2 ## 2.0.0 This release moves `urql` from being in `dependencies` to `peerDependencies`. Please install it explicitly, as you may have already in the past, and ensure that both `urql` and `@urql/core` are not duplicated with either `npm dedupe` or `npx yarn-deduplicate`. ```sh npm i --save urql # or yarn add urql ``` ### Major Changes - Move the `urql` dependency to a peer dependency. - Remove the automatic polyfilling of `fetch` since this is done automatically starting at [`Next v9.4`](https://nextjs.org/blog/next-9-4#improved-built-in-fetch-support) If you are using a version before 9.4 you can upgrade by installing [`isomorphic-unfetch`](https://www.npmjs.com/package/isomorphic-unfetch) and importing it to polyfill the behavior, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1018](https://github.com/FormidableLabs/urql/pull/1018)) ## 1.2.0 ### Minor Changes - Add option called `neverSuspend` to disable `React.Suspense` on next.js, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#923](https://github.com/FormidableLabs/urql/pull/923)) - Expose `initUrqlClient` function so that a `Client` can be created manually for use in Next's newer SSR methods manually, such as `getServerSideProps`, by [@sunpietro](https://github.com/sunpietro) (See [#993](https://github.com/FormidableLabs/urql/pull/993)) ## 1.1.0 ### Minor Changes - Add the option to reset the client on a next-urql application, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#894](https://github.com/FormidableLabs/urql/pull/894)) ### Patch Changes - Updated dependencies (See [#924](https://github.com/FormidableLabs/urql/pull/924) and [#904](https://github.com/FormidableLabs/urql/pull/904)) - urql@1.10.0 ## 1.0.2 ### Patch Changes - Disable suspense on the `Client` when we aren't using `react-ssr-prepass`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#884](https://github.com/FormidableLabs/urql/pull/884)) ## 1.0.1 ### Patch Cho 0nges - Prevent serialization of the `Client` for `withUrqlClient` even if the target component doesn't have a `getInitialProps` method. Before this caused the client to not be initialised correctly on the client-side, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#857](https://github.com/FormidableLabs/urql/pull/857)) ## 1.0.0 To migrate to the new version, you will now have to pass a single function argument, instead of two arguments to the `withUrqlClient` HOC helper. For instance, you would have to transform this: ```js export default withUrqlClient( ctx => ({ url: '', }), ssrExchange => [dedupExchange, cacheExchange, ssrExchange, fetchExchange] ); ``` To look like the following: ```js export default withUrqlClient( (ssrExchange, ctx) => ({ url: '', exchanges: [dedupExchange, cacheExchange, ssrExchange, fetchExchange], }), { ssr: true } ); ``` The second argument may now be used to pass `{ ssr: true }` explicitly, when you are wrapping a page without another `getInitialProps` method. This gives you better support when you implement custom methods like `getStaticProps`. ### Major Changes - Change `getInitialProps` to be applied when the wrapped page `getInitialProps` or when `{ ssr: true }` is passed as a second options object. This is to better support alternative methods like `getStaticProps`. By [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#797](https://github.com/FormidableLabs/urql/pull/797)) - Update the `withUrqlClient` function to remove the second argument formerly called `mergeExchanges` and merges it with the first argument. ### Patch Changes - Reuse the ssrExchange when there is one present on the client-side, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#855](https://github.com/FormidableLabs/urql/pull/855)) - Updated dependencies (See [#842](https://github.com/FormidableLabs/urql/pull/842)) - urql@1.9.8 ## 0.3.8 ### Patch Changes - Bump `react-ssr-prepass` so it can get eliminated in the client-side bundle, this because the 1.2.1 version added "sideEffects:false", by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#809](https://github.com/FormidableLabs/urql/pull/809)) ## 0.3.7 ### Patch Changes - Ensure that the Next.js context is available during all stages of SSR. Previously a missing check in `useMemo` on the server-side caused `clientConfig` from being called repeatedly, and another issue may have caused the client from being serialized to initial props, by [@parkerziegler](https://github.com/parkerziegler) (See [#719](https://github.com/FormidableLabs/urql/pull/719)) ## 0.3.6 ### Patch Changes - ⚠️ Fix bundling for packages depending on React, as it doesn't have native ESM bundles, by [@kitten](https://github.com/kitten) (See [#646](https://github.com/FormidableLabs/urql/pull/646)) - Updated dependencies (See [#646](https://github.com/FormidableLabs/urql/pull/646)) - urql@1.9.4 ## 0.3.5 ### Patch Changes - ⚠️ Fix node resolution when using Webpack, which experiences a bug where it only resolves `package.json:main` instead of `module` when an `.mjs` file imports a package, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - Updated dependencies (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - urql@1.9.3 ## 0.3.4 ### Patch Changes - ⚠️ Fix Node.js Module support for v13 (experimental-modules) and v14. If your bundler doesn't support `.mjs` files and fails to resolve the new version, please double check your configuration for Webpack, or similar tools, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - Updated dependencies (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - urql@1.9.2 ## 0.3.3 ### Patch Changes - ⚠️ Fix Rollup bundle output being written to .es.js instead of .esm.js, by [@kitten](https://github.com/kitten) (See [#609](https://github.com/FormidableLabs/urql/pull/609)) ## 0.3.2 ### Patch Changes - Pass the `Client` down in `withUrqlClient.getInitialProps` to prevent it from being created twice, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#589](https://github.com/FormidableLabs/urql/pull/589)) - Add missing GraphQLError serialization for extensions and path field to ssrExchange, by [@kitten](https://github.com/kitten) (See [#607](https://github.com/FormidableLabs/urql/pull/607)) - Enable users to configure the `suspense` option and clean up suspense warning message, by [@ryan-gilb](https://github.com/ryan-gilb) (See [#603](https://github.com/FormidableLabs/urql/pull/603)) ## 0.3.1 ### Patch Changes - Remove type import from internal urql package file that has been removed, by [@parkerziegler](https://github.com/parkerziegler) (See [#557](https://github.com/FormidableLabs/urql/pull/557)) - Ensure empty object gets returned in withUrqlClient's getInitialProps. Update next-urql examples to run in the urql monorepo, by [@parkerziegler](https://github.com/parkerziegler) (See [#563](https://github.com/FormidableLabs/urql/pull/563)) All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## 0.3.0 This release adds support for different `urql` Client configurations between the client-side and the server-side when using `next-urql`. **Warning:** To support this, access to Next's context object, `ctx`, **can only happen on the server**. ### Added - An example showing how to use a custom exchange with `next-urql`. PR by @ryan-gilb [here](https://github.com/FormidableLabs/next-urql/pull/32). - Instructions for using `next-urql` with ReasonML. PR by @parkerziegler [here](https://github.com/FormidableLabs/next-urql/pull/28). ### Fixed - `clientOptions` are no longer serialized inside of `withUrql`'s `getInitialProps` method. This ensures that users can use different Client configurations between the client and server. PR by @parkerziegler [here](https://github.com/FormidableLabs/next-urql/pull/33). - Proper support for forwarding `pageProps` when using `withUrqlClient` with an `_app.js` component. The `urql` Client instance is also attached to `ctx` for `_app.js` `getInitialProps`. PR by @parkerziegler [here](https://github.com/FormidableLabs/next-urql/pull/38). - `react-ssr-prepass` dependency upgraded to `1.1.2` to support `urql` `>= 1.9.0`. PR by @JoviDeCroock [here](https://github.com/FormidableLabs/next-urql/pull/37). ### Diff https://github.com/FormidableLabs/next-urql/compare/v0.2.5...v0.3.0 ## 0.2.5 This release encompasses small changes to our TypeScript definitions for `next-urql`, with an upgrade to using `next@9` as the basis for new type definitions in lieu of `@types/next`. The examples were also converted over to TypeScript from JavaScript. ### Added - All example projects now use TypeScript 🎉 PRs by @ryan-gilb [here](https://github.com/FormidableLabs/next-urql/pull/19) and [here](https://github.com/FormidableLabs/next-urql/pull/21). This gives us stronger guarantees around library types. ### Fixed - Upgraded type definitions to use types from `next@9`. PR by @ryan-gilb [here](https://github.com/FormidableLabs/next-urql/pull/22). If accessing the `NextContextWithAppTree` `interface`, the name has changed to `NextUrqlContext`. ### Diff https://github.com/FormidableLabs/next-urql/compare/v0.2.4...v0.2.5 ## 0.2.4 This release adds support for accessing the `urqlClient` instance off of Next's context object. ### Added - `urqlClient` is now added to Next's context object, `ctx`, such that it can be accessed by other components lower in the tree. PR by @BjoernRave [here](https://github.com/FormidableLabs/next-urql/pull/15). ### Diff https://github.com/FormidableLabs/next-urql/compare/v0.2.3...v0.2.4 ## 0.2.3 This release fixes support for using `withUrqlClient` with `_app.js`. ### Added - Examples are now separated into an `examples` directory. The first, `1-with-urql-client`, shows recommended usage by wrapping a Page component, while the second, `2-with-_app.js` shows how to set up `next-urql` with `_app.js`. ### Fixed - Be sure to check for `urqlClient` in both direct props and `pageProps` to handle `_app.js` usage with `withUrqlClient`. PR by @bmathews and @parkerziegler [here](https://github.com/FormidableLabs/next-urql/pull/13). ### Diff https://github.com/FormidableLabs/next-urql/compare/v0.2.2...v0.2.3 ## 0.2.2 This release fixes a small discrepancy in the types used by `withUrqlClient` and the public API defined by our `index.d.ts` file. ### Fixed - Use `NextUrqlClientConfig` in lieu of `NextUrqlClientOptions` in `index.d.ts` to match implementation of `withUrqlClient`. PR by @kylealwyn [here](https://github.com/FormidableLabs/next-urql/pull/9). ### Diff https://github.com/FormidableLabs/next-urql/compare/v0.2.1...v0.2.2 ## 0.2.1 This release fixes a regression introduced in 0.2.0 involving circular structures created by `withUrqlClient`'s `getInitialProps` method. ### Fixed - Amend circular structure in `withUrqlClient` caused by returning `ctx` in `getInitialProps`. PR by @parkerziegler [here](https://github.com/FormidableLabs/next-urql/pull/7). - Fix dependency resolution issues in the `example` project. Update `example` documentation. PR by @parkerziegler [here](https://github.com/FormidableLabs/next-urql/pull/7). ### Diff https://github.com/FormidableLabs/next-urql/compare/v0.2.0...v0.2.1 ## 0.2.0 [Deprecated] This release adds support for accessing Next's context object, `ctx`, to instantiate your `urql` Client instance. ### Added - Support for accessing Next's context object, `ctx`, when initializing `withUrqlClient` and creating client options. This should assist users who need to access some data stored in `ctx` to instantiate their `urql` Client instance. PR by @parkerziegler [here](https://github.com/FormidableLabs/next-urql/pull/4). ### Diff https://github.com/FormidableLabs/next-urql/compare/v0.1.1...v0.2.0 ## 0.1.1 This release adds TypeScript definitions to `next-urql`, alongside important pieces like a License (MIT), and improved documentation for users and contributors. ### Added - TypeScript definitions for the public API of `next-urql` now ship with the library. PR by @parkerziegler [here](https://github.com/FormidableLabs/next-urql/pull/2). - MIT License. - Improved README documentation around `withUrqlClient` usage. - CONTRIBUTING.md to help new contributors to the project get involved. ### Diff https://github.com/FormidableLabs/next-urql/compare/v0.1.0...v0.1.1 ## 0.1.0 This is the initial release of `next-urql` in its Beta API. The package is not meant to be consumed yet, and this purely serves as a prerelease for local testing. ================================================ FILE: packages/next-urql/README.md ================================================ ## `next-urql` A set of convenience utilities for using `urql` with Next.js. More documentation is available at https://urql.dev/goto/docs/advanced/server-side-rendering/#nextjs Examples can be found at https://github.com/urql-graphql/urql/tree/main/examples/with-next ================================================ FILE: packages/next-urql/jsr.json ================================================ { "name": "@urql/next", "version": "2.0.0", "exports": { ".": "./src/index.ts", "./rsc": "./src/rsc.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/next-urql/package.json ================================================ { "name": "@urql/next", "version": "2.0.0", "description": "Convenience wrappers for using urql with NextJS.", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/next-urql" }, "main": "dist/urql-next", "module": "dist/urql-next.mjs", "types": "dist/urql-next.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-next.d.ts", "import": "./dist/urql-next.mjs", "require": "./dist/urql-next.js", "source": "./src/index.ts" }, "./package.json": "./package.json", "./rsc": { "types": "./dist/urql-next-rsc.d.ts", "import": "./dist/urql-next-rsc.mjs", "require": "./dist/urql-next-rsc.js", "source": "./src/rsc.ts" } }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "rsc/", "dist/" ], "scripts": { "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@urql/core": "workspace:*", "urql": "workspace:*", "@types/react": "^18.3.8", "@types/react-dom": "^18.3.0", "graphql": "^16.0.0", "next": "^13.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" }, "peerDependencies": { "next": ">=13.0.0", "react": ">=18.0.0", "urql": "^5.0.0" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: packages/next-urql/src/DataHydrationContext.ts ================================================ import * as React from 'react'; import { ServerInsertedHTMLContext } from 'next/navigation'; import type { UrqlResult } from './useUrqlValue'; import { htmlEscapeJsonString } from './htmlescape'; interface DataHydrationValue { isInjecting: boolean; operationValuesByKey: Record; RehydrateScript: () => | React.DetailedReactHTMLElement< { dangerouslySetInnerHTML: { __html: string } }, HTMLElement > | React.FunctionComponentElement; } const DataHydrationContext = React.createContext< DataHydrationValue | undefined >(undefined); function transportDataToJS(data: any) { const key = 'urql_transport'; return `(window[Symbol.for("${key}")] ??= []).push(${htmlEscapeJsonString( JSON.stringify(data) )})`; } export const DataHydrationContextProvider = ({ nonce, children, }: React.PropsWithChildren<{ nonce?: string }>) => { const dataHydrationContext = React.useRef(); if (typeof window == 'undefined') { if (!dataHydrationContext.current) dataHydrationContext.current = buildContext({ nonce }); } return React.createElement( DataHydrationContext.Provider, { value: dataHydrationContext.current }, children ); }; export function useDataHydrationContext(): DataHydrationValue | undefined { const dataHydrationContext = React.useContext(DataHydrationContext); const insertHtml = React.useContext(ServerInsertedHTMLContext as any) as ( cb: () => any ) => any; if (typeof window !== 'undefined') return; if (insertHtml && dataHydrationContext && !dataHydrationContext.isInjecting) { dataHydrationContext.isInjecting = true; insertHtml(() => React.createElement(dataHydrationContext.RehydrateScript, {}) ); } return dataHydrationContext; } let key = 0; function buildContext({ nonce }: { nonce?: string }): DataHydrationValue { const dataHydrationContext: DataHydrationValue = { isInjecting: false, operationValuesByKey: {}, RehydrateScript() { dataHydrationContext.isInjecting = false; if (!Object.keys(dataHydrationContext.operationValuesByKey).length) return React.createElement(React.Fragment); const __html = transportDataToJS({ rehydrate: { ...dataHydrationContext.operationValuesByKey }, }); dataHydrationContext.operationValuesByKey = {}; return React.createElement('script', { key: key++, nonce: nonce, dangerouslySetInnerHTML: { __html }, }); }, }; return dataHydrationContext; } ================================================ FILE: packages/next-urql/src/Provider.ts ================================================ 'use client'; import * as React from 'react'; import type { SSRExchange, Client } from 'urql'; import { Provider } from 'urql'; import { DataHydrationContextProvider } from './DataHydrationContext'; export const SSRContext = React.createContext( undefined ); /** Provider for `@urql/next` during non-rsc interactions. * * @remarks * `Provider` accepts a {@link Client} and provides it to all GraphQL hooks, it * also accepts an {@link SSRExchange} to distribute data when re-hydrating * on the client. * * @example * ```tsx * import { useMemo } from 'react'; * import { * UrqlProvider, * ssrExchange, * cacheExchange, * fetchExchange, * createClient, * } from '@urql/next'; * * export default function Layout({ children }: React.PropsWithChildren) { * const [client, ssr] = useMemo(() => { * const ssr = ssrExchange(); * const client = createClient({ * url: 'https://trygql.formidable.dev/graphql/web-collections', * exchanges: [cacheExchange, ssr, fetchExchange], * suspense: true, * }); * * return [client, ssr]; * }, []); * * return ( * * {children} * * ); * } * * ``` */ export function UrqlProvider({ children, ssr, client, nonce, }: React.PropsWithChildren<{ ssr: SSRExchange; client: Client; nonce?: string; }>) { return React.createElement( Provider, { value: client }, React.createElement( SSRContext.Provider, { value: ssr }, React.createElement(DataHydrationContextProvider, { nonce }, children) ) ); } ================================================ FILE: packages/next-urql/src/htmlescape.ts ================================================ // See: https://github.com/vercel/next.js/blob/6bc07792a4462a4bf921a72ab30dc4ab2c4e1bda/packages/next/src/server/htmlescape.ts // License: https://github.com/vercel/next.js/blob/6bc07792a4462a4bf921a72ab30dc4ab2c4e1bda/packages/next/license.md // This utility is based on https://github.com/zertosh/htmlescape // License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE const ESCAPE_LOOKUP: { [match: string]: string } = { '&': '\\u0026', '>': '\\u003e', '<': '\\u003c', '\u2028': '\\u2028', '\u2029': '\\u2029', }; export const ESCAPE_REGEX = /[&><\u2028\u2029]/g; export function htmlEscapeJsonString(str: string): string { return str.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); } ================================================ FILE: packages/next-urql/src/index.ts ================================================ export * from 'urql'; export { useQuery } from './useQuery'; export { UrqlProvider, SSRContext } from './Provider'; ================================================ FILE: packages/next-urql/src/rsc.ts ================================================ import * as React from 'react'; import type { Client } from '@urql/core'; /** Function to cache an urql-client across React Server Components. * * @param makeClient - A function that creates an urql-client. * @returns an object containing a getClient method. * * @example * ```ts * import { cacheExchange, createClient, fetchExchange, gql } from '@urql/core'; * import { registerUrql } from '@urql/next/rsc'; * const makeClient = () => { * return createClient({ * url: 'https://trygql.formidable.dev/graphql/basic-pokedex', * exchanges: [cacheExchange, fetchExchange], * }); * }; * * const { getClient } = registerUrql(makeClient); * ``` */ export function registerUrql(makeClient: () => Client): { getClient: () => Client; } { // @ts-ignore you exist don't worry const getClient = React.cache(makeClient); return { getClient, }; } ================================================ FILE: packages/next-urql/src/useQuery.ts ================================================ 'use client'; import type { AnyVariables, CombinedError, GraphQLRequestParams, Operation, OperationContext, RequestPolicy, } from 'urql'; import { createRequest, useQuery as orig_useQuery } from 'urql'; import { useUrqlValue } from './useUrqlValue'; /** Input arguments for the {@link useQuery} hook. * * @param query - The GraphQL query that `useQuery` executes. * @param variables - The variables for the GraphQL query that `useQuery` executes. */ export type UseQueryArgs< Variables extends AnyVariables = AnyVariables, Data = any, > = { /** Updates the {@link RequestPolicy} for the executed GraphQL query operation. * * @remarks * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation * that `useQuery` executes, and indicates a caching strategy for cache exchanges. * * For example, when set to `'cache-and-network'`, {@link useQuery} will * receive a cached result with `stale: true` and an API request will be * sent in the background. * * @see {@link OperationContext.requestPolicy} for where this value is set. */ requestPolicy?: RequestPolicy; /** Updates the {@link OperationContext} for the executed GraphQL query operation. * * @remarks * `context` may be passed to {@link useQuery}, to update the {@link OperationContext} * of a query operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * Hint: This should be wrapped in a `useMemo` hook, to make sure that your * component doesn’t infinitely update. * * @example * ```ts * const [result, reexecute] = useQuery({ * query, * context: useMemo(() => ({ * additionalTypenames: ['Item'], * }), []) * }); * ``` */ context?: Partial; /** Prevents {@link useQuery} from automatically executing GraphQL query operations. * * @remarks * `pause` may be set to `true` to stop {@link useQuery} from executing * automatically. The hook will stop receiving updates from the {@link Client} * and won’t execute the query operation, until either it’s set to `false` * or the {@link UseQueryExecute} function is called. * * @see {@link https://urql.dev/goto/docs/basics/react-preact/#pausing-usequery} for * documentation on the `pause` option. */ pause?: boolean; } & GraphQLRequestParams; /** State of the current query, your {@link useQuery} hook is executing. * * @remarks * `UseQueryState` is returned (in a tuple) by {@link useQuery} and * gives you the updating {@link OperationResult} of GraphQL queries. * * Even when the query and variables passed to {@link useQuery} change, * this state preserves the prior state and sets the `fetching` flag to * `true`. * This allows you to display the previous state, while implementing * a separate loading indicator separately. */ export interface UseQueryState< Data = any, Variables extends AnyVariables = AnyVariables, > { /** Indicates whether `useQuery` is waiting for a new result. * * @remarks * When `useQuery` is passed a new query and/or variables, it will * start executing the new query operation and `fetching` is set to * `true` until a result arrives. * * Hint: This is subtly different than whether the query is actually * fetching, and doesn’t indicate whether a query is being re-executed * in the background. For this, see {@link UseQueryState.stale}. */ fetching: boolean; /** Indicates that the state is not fresh and a new result will follow. * * @remarks * The `stale` flag is set to `true` when a new result for the query * is expected and `useQuery` is waiting for it. This may indicate that * a new request is being requested in the background. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed query. */ data?: Data; /** The {@link OperationResult.error} for the executed query. */ error?: CombinedError; /** The {@link OperationResult.hasNext} for the executed query. */ hasNext: boolean; /** The {@link OperationResult.extensions} for the executed query. */ extensions?: Record; /** The {@link Operation} that the current state is for. * * @remarks * This is the {@link Operation} that is currently being executed. * When {@link UseQueryState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; } /** Triggers {@link useQuery} to execute a new GraphQL query operation. * * @param opts - optionally, context options that will be merged with the hook's * {@link UseQueryArgs.context} options and the `Client`’s options. * * @remarks * When called, {@link useQuery} will re-execute the GraphQL query operation * it currently holds, even if {@link UseQueryArgs.pause} is set to `true`. * * This is useful for executing a paused query or re-executing a query * and get a new network result, by passing a new request policy. * * ```ts * const [result, reexecuteQuery] = useQuery({ query }); * * const refresh = () => { * // Re-execute the query with a network-only policy, skipping the cache * reexecuteQuery({ requestPolicy: 'network-only' }); * }; * ``` */ export type UseQueryExecute = (opts?: Partial) => void; /** Result tuple returned by the {@link useQuery} hook. * * @remarks * Similarly to a `useState` hook’s return value, * the first element is the {@link useQuery}’s result and state, * a {@link UseQueryState} object, * and the second is used to imperatively re-execute the query * via a {@link UseQueryExecute} function. */ export type UseQueryResponse< Data = any, Variables extends AnyVariables = AnyVariables, > = [UseQueryState, UseQueryExecute]; /** Hook to run a GraphQL query and get updated GraphQL results. * * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link UseQueryResponse} tuple of a {@link UseQueryState} result, and re-execute function. * * @remarks * `useQuery` allows GraphQL queries to be defined and executed. * Given {@link UseQueryArgs.query}, it executes the GraphQL query with the * context’s {@link Client}. * * The returned result updates when the `Client` has new results * for the query, and changes when your input `args` change. * * Additionally, if the `suspense` option is enabled on the `Client`, * the `useQuery` hook will suspend instead of indicating that it’s * waiting for a result via {@link UseQueryState.fetching}. * * @see {@link https://urql.dev/goto/urql/docs/basics/react-preact/#queries} for `useQuery` docs. * * @example * ```ts * import { gql, useQuery } from 'urql'; * * const TodosQuery = gql` * query { todos { id, title } } * `; * * const Todos = () => { * const [result, reexecuteQuery] = useQuery({ * query: TodosQuery, * variables: {}, * }); * // ... * }; * ``` */ export function useQuery< Data = any, Variables extends AnyVariables = AnyVariables, >( args: UseQueryArgs ): UseQueryResponse { const request = createRequest( args.query, (args.variables || {}) as AnyVariables ); useUrqlValue(request.key); const [result, execute] = orig_useQuery(args); useUrqlValue(request.key); return [result, execute]; } ================================================ FILE: packages/next-urql/src/useUrqlValue.ts ================================================ 'use client'; import * as React from 'react'; import { useDataHydrationContext } from './DataHydrationContext'; import { SSRContext } from './Provider'; export const symbolString = 'urql_transport'; export const urqlTransportSymbol = Symbol.for(symbolString); export type UrqlResult = { data?: any; error?: any; extensions?: any }; export function useUrqlValue(operationKey: number): void { const ssrExchange = React.useContext(SSRContext); const rehydrationContext = useDataHydrationContext(); if (!ssrExchange) { throw new Error( 'Missing "UrqlProvider" component as a parent or did not pass in an "ssrExchange" to the Provider.' ); } if (typeof window == 'undefined') { const data = ssrExchange.extractData(); if (rehydrationContext && data[operationKey]) { const res = data[operationKey]; const parsed = { ...res, extensions: res.extensions ? JSON.parse(res.extensions) : res.extensions, data: res.data ? JSON.parse(res.data) : res.data, error: res.error, }; rehydrationContext.operationValuesByKey[operationKey] = parsed; } } else { const stores = (window[urqlTransportSymbol as any] || []) as unknown as Array<{ rehydrate: Record; }>; const store = stores.find( x => x && x.rehydrate && x.rehydrate[operationKey] ); if (store) { const result = store.rehydrate && store.rehydrate[operationKey]; if (result) { delete store.rehydrate[operationKey]; ssrExchange.restoreData({ [operationKey]: { extensions: JSON.stringify(result.extensions), data: JSON.stringify(result.data), error: result.error, }, }); delete store.rehydrate[operationKey]; } } } } ================================================ FILE: packages/next-urql/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/next-urql/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: packages/preact-urql/CHANGELOG.md ================================================ # @urql/preact ## 5.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 4.1.2 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 4.1.1 ### Patch Changes - Add type for `hasNext` to the query and mutation results Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3703](https://github.com/urql-graphql/urql/pull/3703)) ## 4.1.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ### Patch Changes - ⚠️ Fix subscription handlers to not receive `null` values Submitted by [@kitten](https://github.com/kitten) (See [#3581](https://github.com/urql-graphql/urql/pull/3581)) ## 4.0.5 ### Patch Changes - Updated dependencies (See [#3520](https://github.com/urql-graphql/urql/pull/3520), [#3553](https://github.com/urql-graphql/urql/pull/3553), and [#3520](https://github.com/urql-graphql/urql/pull/3520)) - @urql/core@5.0.0 ## 4.0.4 ### Patch Changes - Prioritise `context.suspense` and fallback to checking `client.suspense` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3427](https://github.com/urql-graphql/urql/pull/3427)) - Updated dependencies (See [#3430](https://github.com/urql-graphql/urql/pull/3430)) - @urql/core@4.2.0 ## 4.0.3 ### Patch Changes - Apply shallow difference patch from React bindings to `@urql/preact` (See: #3195) Submitted by [@kitten](https://github.com/kitten) (See [#3266](https://github.com/urql-graphql/urql/pull/3266)) ## 4.0.2 ### Patch Changes - Update build process to generate correct source maps Submitted by [@kitten](https://github.com/kitten) (See [#3201](https://github.com/urql-graphql/urql/pull/3201)) ## 4.0.1 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 4.0.0 ### Major Changes - Remove the default `Client` from `Context`. Previously, `urql` kept a legacy default client in its context, with default exchanges and calling an API at `/graphql`. This has now been removed and you will have to create your own `Client` if you were relying on this behaviour Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3033](https://github.com/urql-graphql/urql/pull/3033)) ### Minor Changes - Allow mutations to update their results in bindings when `hasNext: true` is set, which indicates deferred or streamed results Submitted by [@kitten](https://github.com/kitten) (See [#3103](https://github.com/urql-graphql/urql/pull/3103)) ### Patch Changes - ⚠️ Fix source maps included with recently published packages, which lost their `sourcesContent`, including additional source files, and had incorrect paths in some of them Submitted by [@kitten](https://github.com/kitten) (See [#3053](https://github.com/urql-graphql/urql/pull/3053)) - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs to all `urql` bindings packages Submitted by [@kitten](https://github.com/kitten) (See [#3079](https://github.com/urql-graphql/urql/pull/3079)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 3.0.3 ### Patch Changes - ⚠️ Fix type utilities turning the `variables` properties optional when a type from `TypedDocumentNode` has no `Variables` or all optional `Variables`. Previously this would break for wrappers, e.g. in code generators, or when the type didn't quite match what we'd expect Submitted by [@kitten](https://github.com/kitten) (See [#3022](https://github.com/urql-graphql/urql/pull/3022)) - Updated dependencies (See [#3007](https://github.com/urql-graphql/urql/pull/3007), [#2962](https://github.com/urql-graphql/urql/pull/2962), [#3007](https://github.com/urql-graphql/urql/pull/3007), [#3015](https://github.com/urql-graphql/urql/pull/3015), and [#3022](https://github.com/urql-graphql/urql/pull/3022)) - @urql/core@3.2.0 ## 3.0.2 ### Patch Changes - Update generics for components, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2663](https://github.com/FormidableLabs/urql/pull/2663)) - Updated dependencies (See [#2665](https://github.com/FormidableLabs/urql/pull/2665)) - @urql/core@3.0.3 ## 3.0.1 ### Patch Changes - Tweak the variables type for when generics only contain nullable keys, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2623](https://github.com/FormidableLabs/urql/pull/2623)) ## 3.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Implement stricter variables types, which require variables to always be passed and match TypeScript types when the generic is set or inferred. This is a breaking change for TypeScript users potentially, unless all types are adhered to, by [@kitten](https://github.com/kitten) (See [#2607](https://github.com/FormidableLabs/urql/pull/2607)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Patch Changes - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 2.0.4 ### Patch Changes - ⚠️ Fix Node.js ESM re-export detection for `@urql/core` in `urql` package and CommonJS output for all other CommonJS-first packages. This ensures that Node.js' `cjs-module-lexer` can correctly identify re-exports and report them properly. Otherwise, this will lead to a runtime error, by [@kitten](https://github.com/kitten) (See [#2485](https://github.com/FormidableLabs/urql/pull/2485)) ## 2.0.3 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 2.0.2 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 2.0.1 ### Patch Changes - Add a displayName to the Provider, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1431](https://github.com/FormidableLabs/urql/pull/1431)) ## 2.0.0 ### Major Changes - **Breaking**: Remove `pollInterval` option from `useQuery`. Instead please consider using `useEffect` calling `executeQuery` on an interval, by [@kitten](https://github.com/kitten) (See [#1374](https://github.com/FormidableLabs/urql/pull/1374)) ### Minor Changes - Remove deprecated `operationName` property from `Operation`s. The new `Operation.kind` property is now preferred. If you're creating new operations you may also use the `makeOperation` utility instead. When upgrading `@urql/core` please ensure that your package manager didn't install any duplicates of it. You may deduplicate it manually using `npx yarn-deduplicate` (for Yarn) or `npm dedupe` (for npm), by [@kitten](https://github.com/kitten) (See [#1357](https://github.com/FormidableLabs/urql/pull/1357)) ### Patch Changes - Updated dependencies (See [#1374](https://github.com/FormidableLabs/urql/pull/1374), [#1357](https://github.com/FormidableLabs/urql/pull/1357), and [#1375](https://github.com/FormidableLabs/urql/pull/1375)) - @urql/core@2.0.0 ## 1.4.4 ### Patch Changes - ⚠️ Fix Suspense when results share data, this would return partial results for graphCache and not update to the eventual data, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1282](https://github.com/FormidableLabs/urql/pull/1282)) ## 1.4.3 ### Patch Changes - Add a built-in `gql` tag function helper to `@urql/core`. This behaves similarly to `graphql-tag` but only warns about _locally_ duplicated fragment names rather than globally. It also primes `@urql/core`'s key cache with the parsed `DocumentNode`, by [@kitten](https://github.com/kitten) (See [#1187](https://github.com/FormidableLabs/urql/pull/1187)) - Add `suspense: false` to options when `executeQuery` is called explicitly, by [@kitten](https://github.com/kitten) (See [#1181](https://github.com/FormidableLabs/urql/pull/1181)) - Updated dependencies (See [#1187](https://github.com/FormidableLabs/urql/pull/1187), [#1186](https://github.com/FormidableLabs/urql/pull/1186), and [#1186](https://github.com/FormidableLabs/urql/pull/1186)) - @urql/core@1.16.0 ## 1.4.2 ### Patch Changes - ⚠️ Fix regression in client-side Suspense behaviour. This has been fixed in `urql@1.11.0` and `@urql/preact@1.4.0` but regressed in the patches afterwards that were aimed at fixing server-side Suspense, by [@kitten](https://github.com/kitten) (See [#1142](https://github.com/FormidableLabs/urql/pull/1142)) ## 1.4.1 ### Patch Changes - ⚠️ Fix server-side rendering by disabling the new Suspense cache on the server-side and clear it for prepasses, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1138](https://github.com/FormidableLabs/urql/pull/1138)) - Updated dependencies (See [#1135](https://github.com/FormidableLabs/urql/pull/1135)) - @urql/core@1.15.1 ## 1.4.0 ### Minor Changes - Improve the Suspense implementation, which fixes edge-cases when Suspense is used with subscriptions, partially disabled, or _used on the client-side_. It has now been ensured that client-side suspense functions without the deprecated `suspenseExchange` and uncached results are loaded consistently. As part of this work, the `Client` itself does now never throw Suspense promises anymore, which is functionality that either way has no place outside of the React/Preact bindings, by [@kitten](https://github.com/kitten) (See [#1123](https://github.com/FormidableLabs/urql/pull/1123)) ### Patch Changes - Add support for `TypedDocumentNode` to infer the type of the `OperationResult` and `Operation` for all methods, functions, and hooks that either directly or indirectly accept a `DocumentNode`. See [`graphql-typed-document-node` and the corresponding blog post for more information.](https://github.com/dotansimha/graphql-typed-document-node), by [@kitten](https://github.com/kitten) (See [#1113](https://github.com/FormidableLabs/urql/pull/1113)) - Refactor `useSource` hooks which powers `useQuery` and `useSubscription` to improve various edge case behaviour. This will not change the behaviour of these hooks dramatically but avoid unnecessary state updates when any updates are obviously equivalent and the hook will furthermore improve continuation from mount to effects, which will fix cases where the state between the mounting and effect phase may slightly change, by [@kitten](https://github.com/kitten) (See [#1104](https://github.com/FormidableLabs/urql/pull/1104)) - Updated dependencies (See [#1119](https://github.com/FormidableLabs/urql/pull/1119), [#1113](https://github.com/FormidableLabs/urql/pull/1113), [#1104](https://github.com/FormidableLabs/urql/pull/1104), and [#1123](https://github.com/FormidableLabs/urql/pull/1123)) - @urql/core@1.15.0 ## 1.3.2 ### Patch Changes - ⚠️ Fix the production build overwriting the development build. Specifically in the previous release we mistakenly replaced all development bundles with production bundles. This doesn't have any direct influence on how these packages work, but prevented development warnings from being logged or full errors from being thrown, by [@kitten](https://github.com/kitten) (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - Updated dependencies (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - @urql/core@1.14.1 ## 1.3.1 ### Patch Changes - Add missing `.mjs` extension to all imports from `graphql` to fix Webpack 5 builds, which require extension-specific import paths for ESM bundles and packages. **This change allows you to safely upgrade to Webpack 5.**, by [@kitten](https://github.com/kitten) (See [#1094](https://github.com/FormidableLabs/urql/pull/1094)) - Updated dependencies (See [#1094](https://github.com/FormidableLabs/urql/pull/1094) and [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - @urql/core@1.14.0 ## 1.3.0 ### Minor Changes - Update `@urql/preact` implementation to match `urql` React implementation. Internally these changes should align behaviour and updates slightly, but outwardly no changes should be apparent apart from how some updates are scheduled, by [@kitten](https://github.com/kitten) (See [#1008](https://github.com/FormidableLabs/urql/pull/1008)) ### Patch Changes - Updated dependencies (See [#1011](https://github.com/FormidableLabs/urql/pull/1011)) - @urql/core@1.13.1 ## 1.2.1 ### Patch Changes - Handle a bug in Preact where the current request might be `null`, by [@jlengstorf](https://github.com/jlengstorf) (See [#944](https://github.com/FormidableLabs/urql/pull/944)) - Updated dependencies (See [#947](https://github.com/FormidableLabs/urql/pull/947), [#962](https://github.com/FormidableLabs/urql/pull/962), and [#957](https://github.com/FormidableLabs/urql/pull/957)) - @urql/core@1.13.0 ## 1.2.0 ### Minor Changes - Add the operation to the query, mutation and subscription result, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#924](https://github.com/FormidableLabs/urql/pull/924)) ### Patch Changes - Updated dependencies (See [#911](https://github.com/FormidableLabs/urql/pull/911) and [#908](https://github.com/FormidableLabs/urql/pull/908)) - @urql/core@1.12.3 ## 1.1.8 ### Patch Changes - Upgrade to a minimum version of wonka@^4.0.14 to work around issues with React Native's minification builds, which use uglify-es and could lead to broken bundles, by [@kitten](https://github.com/kitten) (See [#842](https://github.com/FormidableLabs/urql/pull/842)) - Updated dependencies (See [#838](https://github.com/FormidableLabs/urql/pull/838) and [#842](https://github.com/FormidableLabs/urql/pull/842)) - @urql/core@1.12.0 ## 1.1.7 ### Patch Changes - Add a `"./package.json"` entry to the `package.json`'s `"exports"` field for Node 14. This seems to be required by packages like `rollup-plugin-svelte` to function properly, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - Updated dependencies (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - @urql/core@1.11.6 ## 1.1.6 ### Patch Changes - Bump @urql/core to ensure exchanges have dispatchDebug, this could formerly result in a crash, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#726](https://github.com/FormidableLabs/urql/pull/726)) ## 1.1.5 ### Patch Changes - Add graphql@^15.0.0 to peer dependency range, by [@kitten](https://github.com/kitten) (See [#688](https://github.com/FormidableLabs/urql/pull/688)) - Forcefully bump @urql/core package in all bindings and in @urql/exchange-graphcache. We're aware that in some cases users may not have upgraded to @urql/core, even though that's within the typical patch range. Since the latest @urql/core version contains a patch that is required for `cache-and-network` to work, we're pushing another patch that now forcefully bumps everyone to the new version that includes this fix, by [@kitten](https://github.com/kitten) (See [#684](https://github.com/FormidableLabs/urql/pull/684)) - Updated dependencies (See [#688](https://github.com/FormidableLabs/urql/pull/688) and [#678](https://github.com/FormidableLabs/urql/pull/678)) - @urql/core@1.10.8 ## 1.1.4 ### Patch Changes - ⚠️ Fix node resolution when using Webpack, which experiences a bug where it only resolves `package.json:main` instead of `module` when an `.mjs` file imports a package, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - Updated dependencies (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - @urql/core@1.10.4 ## 1.1.3 ### Patch Changes - ⚠️ Fix Node.js Module support for v13 (experimental-modules) and v14. If your bundler doesn't support `.mjs` files and fails to resolve the new version, please double check your configuration for Webpack, or similar tools, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - Updated dependencies (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - @urql/core@1.10.3 ## 1.1.2 ### Patch Changes - Bumps the `@urql/core` dependency minor version to ^1.10.1 for React, Preact and Svelte, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#623](https://github.com/FormidableLabs/urql/pull/623)) - Updated dependencies (See [#621](https://github.com/FormidableLabs/urql/pull/621)) - @urql/core@1.10.2 ## 1.1.1 ### Patch Changes - Switch over to using @urql/core package (See [`75323c0`](https://github.com/FormidableLabs/urql/commit/75323c0)) - Updated dependencies (See [#533](https://github.com/FormidableLabs/urql/pull/533), [#519](https://github.com/FormidableLabs/urql/pull/519), [#515](https://github.com/FormidableLabs/urql/pull/515), [#512](https://github.com/FormidableLabs/urql/pull/512), and [#518](https://github.com/FormidableLabs/urql/pull/518)) - @urql/core@1.9.0 ## 1.1.0 - Update urql to 1.8.0 - Update wonka to 4.0.0 (and incorporate breaking changes) ================================================ FILE: packages/preact-urql/README.md ================================================ ## Installation ```sh yarn add @urql/preact urql graphql # or npm install --save @urql/preact urql graphql ``` ## Usage The usage is a 1:1 mapping of the React usage found [here](https://formidable.com/open-source/urql/docs) small example: ```jsx import { createClient, cacheExchange, fetchExchange, Provider, useQuery } from '@urql/preact'; const client = createClient({ url: 'https://myHost/graphql', exchanges: [cacheExchange, fetchExchange], }); const App = () => ( ); const Dogs = () => { const [result] = useQuery({ query: `{ dogs { id name } }`, }); if (result.fetching) return

Loading...

; if (result.error) return

Oh no...

; return result.data.dogs.map(dog =>

{dog.name} is a good boy!

); }; ``` ================================================ FILE: packages/preact-urql/jsr.json ================================================ { "name": "@urql/preact", "version": "5.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/preact-urql/package.json ================================================ { "name": "@urql/preact", "version": "5.0.0", "description": "A highly customizable and versatile GraphQL client for Preact", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/preact-urql" }, "keywords": [ "graphql client", "state management", "cache", "graphql", "exchanges", "preact" ], "main": "dist/urql-preact", "module": "dist/urql-preact.mjs", "types": "dist/urql-preact.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-preact.d.ts", "import": "./dist/urql-preact.mjs", "require": "./dist/urql-preact.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@testing-library/preact": "^2.0.0", "@urql/core": "workspace:*", "graphql": "^16.0.0", "preact": "^10.13.0" }, "peerDependencies": { "@urql/core": "^6.0.0", "preact": ">= 10.0.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: packages/preact-urql/src/components/Mutation.test.tsx ================================================ // @vitest-environment jsdom import { h } from 'preact'; import { act, cleanup, render } from '@testing-library/preact'; import { pipe, fromValue, delay } from 'wonka'; import { vi, expect, it, beforeEach, describe, afterEach, Mock } from 'vitest'; import { Provider } from '../context'; import { Mutation } from './Mutation'; const mock = { executeMutation: vi.fn(() => pipe(fromValue({ data: 1, error: 2, extensions: { i: 1 } }), delay(200)) ), }; const client = mock as { executeMutation: Mock }; const query = 'mutation Example { example }'; describe('Mutation', () => { beforeEach(() => { vi.useFakeTimers(); vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothing }); }); afterEach(() => { cleanup(); }); it('Should execute the mutation', () => { // eslint-disable-next-line let execute = () => {}, props = {}; const Test = () => h('p', {}, 'hi'); const App = () => { // @ts-ignore return h(Provider, { value: client, children: [ h( Mutation as any, { query }, ({ data, fetching, error, executeMutation }) => { execute = executeMutation; props = { data, fetching, error }; // @ts-ignore return h(Test, {}); } ), ], }); }; render(h(App, {})); expect(client.executeMutation).toBeCalledTimes(0); expect(props).toStrictEqual({ data: undefined, fetching: false, error: undefined, }); act(() => { execute(); }); expect(props).toStrictEqual({ data: undefined, fetching: true, error: undefined, }); act(() => { vi.advanceTimersByTime(400); }); expect(props).toStrictEqual({ data: 1, fetching: false, error: 2 }); }); }); ================================================ FILE: packages/preact-urql/src/components/Mutation.ts ================================================ import type { VNode } from 'preact'; import type { AnyVariables, DocumentInput } from '@urql/core'; import type { UseMutationState, UseMutationExecute } from '../hooks'; import { useMutation } from '../hooks'; /** Props accepted by {@link Mutation}. * * @remarks * `MutationProps` are the props accepted by the {@link Mutation} component. * * The result, the {@link MutationState} object, will be passed to * a {@link MutationProps.children} function, passed as children * to the `Mutation` component. */ export interface MutationProps< Data = any, Variables extends AnyVariables = AnyVariables, > { /* The GraphQL mutation document that {@link useMutation} will execute. */ query: DocumentInput; children(arg: MutationState): VNode; } /** Object that {@link MutationProps.children} is called with. * * @remarks * This is an extented {@link UseMutationstate} with an added * {@link MutationState.executeMutation} method, which is usually * part of a tuple returned by {@link useMutation}. */ export interface MutationState< Data = any, Variables extends AnyVariables = AnyVariables, > extends UseMutationState { /** Alias to {@link useMutation}’s `executeMutation` function. */ executeMutation: UseMutationExecute; } /** Component Wrapper around {@link useMutation} to run a GraphQL query. * * @remarks * `Mutation` is a component wrapper around the {@link useMutation} hook * that calls the {@link MutationProps.children} prop, as a function, * with the {@link MutationState} object. */ export function Mutation< Data = any, Variables extends AnyVariables = AnyVariables, >(props: MutationProps): VNode { const mutation = useMutation(props.query); return props.children({ ...mutation[0], executeMutation: mutation[1] }); } ================================================ FILE: packages/preact-urql/src/components/Query.test.tsx ================================================ // @vitest-environment jsdom import { h } from 'preact'; import { cleanup, render } from '@testing-library/preact'; import { map, interval, pipe } from 'wonka'; import { vi, expect, it, beforeEach, describe, afterEach } from 'vitest'; import { Query } from './Query'; import { Provider } from '../context'; const query = '{ example }'; const variables = { myVar: 1234, }; const client = { executeQuery: vi.fn(() => pipe( interval(200), map((i: number) => ({ data: i, error: i + 1 })) ) ), }; describe('Query', () => { beforeEach(() => { vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothing }); }); afterEach(() => { cleanup(); }); it('Should execute the query', async () => { let props = {}; const Test = () => h('p', {}, 'hi'); const App = () => { // @ts-ignore return h(Provider, { value: client, children: [ // @ts-ignore h(Query, { query, variables }, ({ data, fetching, error }) => { props = { data, fetching, error }; // @ts-ignore return h(Test, {}); }), ], }); }; render(h(App, {})); expect(props).toStrictEqual({ data: undefined, fetching: true, error: undefined, }); await new Promise(res => { setTimeout(() => { expect(props).toStrictEqual({ data: 0, fetching: false, error: 1 }); res(null); }, 250); }); }); }); ================================================ FILE: packages/preact-urql/src/components/Query.ts ================================================ import type { VNode } from 'preact'; import type { AnyVariables } from '@urql/core'; import type { UseQueryArgs, UseQueryState, UseQueryExecute } from '../hooks'; import { useQuery } from '../hooks'; /** Props accepted by {@link Query}. * * @remarks * `QueryProps` are the props accepted by the {@link Query} component, * which is identical to {@link UseQueryArgs}. * * The result, the {@link QueryState} object, will be passed to * a {@link QueryProps.children} function, passed as children * to the `Query` component. */ export type QueryProps< Data = any, Variables extends AnyVariables = AnyVariables, > = UseQueryArgs & { children(arg: QueryState): VNode; }; /** Object that {@link QueryProps.children} is called with. * * @remarks * This is an extented {@link UseQueryState} with an added * {@link QueryState.executeQuery} method, which is usually * part of a tuple returned by {@link useQuery}. */ export interface QueryState< Data = any, Variables extends AnyVariables = AnyVariables, > extends UseQueryState { /** Alias to {@link useQuery}’s `executeQuery` function. */ executeQuery: UseQueryExecute; } /** Component Wrapper around {@link useQuery} to run a GraphQL query. * * @remarks * `Query` is a component wrapper around the {@link useQuery} hook * that calls the {@link QueryProps.children} prop, as a function, * with the {@link QueryState} object. */ export function Query< Data = any, Variables extends AnyVariables = AnyVariables, >(props: QueryProps): VNode { const query = useQuery(props); return props.children({ ...query[0], executeQuery: query[1] }); } ================================================ FILE: packages/preact-urql/src/components/Subscription.test.tsx ================================================ // @vitest-environment jsdom import { h } from 'preact'; import { cleanup, render, act } from '@testing-library/preact'; import { map, interval, pipe } from 'wonka'; import { vi, expect, it, beforeEach, describe, afterEach } from 'vitest'; import { Provider } from '../context'; import { Subscription } from './Subscription'; const query = 'subscription Example { example }'; const client = { executeSubscription: vi.fn(() => { return pipe( interval(200), map((i: number) => ({ data: i, error: i + 1 })) ); }), }; describe('Subscription', () => { beforeEach(() => { vi.useFakeTimers(); vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothing }); }); afterEach(() => { cleanup(); }); it('Should execute the subscription', () => { let props = {}; const Test = () => h('p', {}, 'hi'); const App = () => { // @ts-ignore return h(Provider, { value: client, children: [ // @ts-ignore h(Subscription, { query }, ({ data, fetching, error }) => { props = { data, fetching, error }; // @ts-ignore return h(Test, {}); }), ], }); }; render(h(App, {})); expect(props).toStrictEqual({ data: undefined, fetching: true, error: undefined, }); act(() => { vi.advanceTimersByTime(200); }); expect(props).toStrictEqual({ data: 0, fetching: true, error: 1 }); }); }); ================================================ FILE: packages/preact-urql/src/components/Subscription.ts ================================================ import type { VNode } from 'preact'; import type { AnyVariables } from '@urql/core'; import type { UseSubscriptionArgs, UseSubscriptionState, UseSubscriptionExecute, SubscriptionHandler, } from '../hooks'; import { useSubscription } from '../hooks'; /** Props accepted by {@link Subscription}. * * @remarks * `SubscriptionProps` are the props accepted by the {@link Subscription} component, * which is identical to {@link UseSubscriptionArgs} with an added * {@link SubscriptionProps.handler} prop, which {@link useSubscription} usually * accepts as an additional argument. * * The result, the {@link SubscriptionState} object, will be passed to * a {@link SubscriptionProps.children} function, passed as children * to the `Subscription` component. */ export type SubscriptionProps< Data = any, Result = Data, Variables extends AnyVariables = AnyVariables, > = UseSubscriptionArgs & { /** Accepts the {@link SubscriptionHandler} as a prop. */ handler?: SubscriptionHandler; children(arg: SubscriptionState): VNode; }; /** Object that {@link SubscriptionProps.children} is called with. * * @remarks * This is an extented {@link UseSubscriptionState} with an added * {@link SubscriptionState.executeSubscription} method, which is usually * part of a tuple returned by {@link useSubscription}. */ export interface SubscriptionState< Data = any, Variables extends AnyVariables = AnyVariables, > extends UseSubscriptionState { /** Alias to {@link useSubscription}’s `executeMutation` function. */ executeSubscription: UseSubscriptionExecute; } /** Component Wrapper around {@link useSubscription} to run a GraphQL subscription. * * @remarks * `Subscription` is a component wrapper around the {@link useSubscription} hook * that calls the {@link SubscriptionProps.children} prop, as a function, * with the {@link SubscriptionState} object. */ export function Subscription< Data = any, Result = Data, Variables extends AnyVariables = AnyVariables, >(props: SubscriptionProps): VNode { const subscription = useSubscription( props, props.handler ); return props.children({ ...subscription[0], executeSubscription: subscription[1], }); } ================================================ FILE: packages/preact-urql/src/components/index.ts ================================================ export * from './Mutation'; export * from './Query'; export * from './Subscription'; ================================================ FILE: packages/preact-urql/src/context.ts ================================================ import { createContext } from 'preact'; import { useContext } from 'preact/hooks'; import type { Client } from '@urql/core'; const OBJ = {}; /** `@urql/preact`'s Preact Context. * * @remarks * The Preact Context that `urql`’s {@link Client} will be provided with. * You may use the reexported {@link Provider} to provide a `Client` as well. */ export const Context: import('preact').Context = createContext(OBJ); /** Provider for `urql`’s {@link Client} to GraphQL hooks. * * @remarks * `Provider` accepts a {@link Client} and provides it to all GraphQL hooks, * and {@link useClient}. * * You should make sure to create a {@link Client} and provide it with the * `Provider` to parts of your component tree that use GraphQL hooks. * * @example * ```tsx * import { Provider } from '@urql/preact'; * // All of `@urql/core` is also re-exported by `@urql/preact`: * import { Client, cacheExchange, fetchExchange } from '@urql/core'; * * const client = new Client({ * url: 'https://API', * exchanges: [cacheExchange, fetchExchange], * }); * * const App = () => ( * * * * ); * ``` */ export const Provider: import('preact').Provider = Context.Provider; /** Preact Consumer component, providing the {@link Client} provided on a parent component. * @remarks * This is an alias for {@link Context.Consumer}. */ export const Consumer: import('preact').Consumer = Context.Consumer; Context.displayName = 'UrqlContext'; /** Hook returning a {@link Client} from {@link Context}. * * @remarks * `useClient` is a convenience hook, which accesses `@urql/preact`'s {@link Context} * and returns the {@link Client} defined on it. * * This will be the {@link Client} you passed to a {@link Provider} * you wrapped your elements containing this hook with. * * @throws * In development, if the component you call `useClient()` in is * not wrapped in a {@link Provider}, an error is thrown. */ export const useClient = (): Client => { const client = useContext(Context); if (client === OBJ && process.env.NODE_ENV !== 'production') { const error = "No client has been specified using urql's Provider. please create a client and add a Provider."; console.error(error); throw new Error(error); } return client as Client; }; ================================================ FILE: packages/preact-urql/src/hooks/constants.ts ================================================ export const initialState = { fetching: false, stale: false, hasNext: false, error: undefined, data: undefined, extensions: undefined, operation: undefined, }; ================================================ FILE: packages/preact-urql/src/hooks/index.ts ================================================ export * from './useQuery'; export * from './useMutation'; export * from './useSubscription'; ================================================ FILE: packages/preact-urql/src/hooks/useMutation.test.tsx ================================================ // @vitest-environment jsdom import { FunctionalComponent as FC, h } from 'preact'; import { render, cleanup, act } from '@testing-library/preact'; import { print } from 'graphql'; import { gql } from '@urql/core'; import { fromValue, delay, pipe } from 'wonka'; import { vi, expect, it, beforeEach, describe, beforeAll, afterEach, Mock, } from 'vitest'; import { useMutation } from './useMutation'; import { Provider } from '../context'; const mock = { executeMutation: vi.fn(() => pipe(fromValue({ data: 1, error: 2, extensions: { i: 1 } }), delay(200)) ), }; const client = mock as { executeMutation: Mock }; const props = { query: 'mutation Example { example }', }; let state: any; let execute: any; const MutationUser: FC = ({ query }) => { [state, execute] = useMutation(query); return h('p', {}, state.data); }; beforeAll(() => { vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothing }); }); describe('useMutation', () => { beforeEach(() => { client.executeMutation.mockClear(); state = undefined; execute = undefined; }); afterEach(() => cleanup()); it('does not execute subscription', () => { render( h(Provider, { value: client as any, children: [h(MutationUser, { ...props })], }) ); expect(client.executeMutation).toBeCalledTimes(0); }); it('executes mutation', () => { render( h(Provider, { value: client as any, children: [h(MutationUser, { ...props })], }) ); const vars = { test: 1234 }; act(() => { execute(vars); }); const call = client.executeMutation.mock.calls[0][0]; expect(state).toHaveProperty('fetching', true); expect(client.executeMutation).toBeCalledTimes(1); expect(print(call.query)).toBe(print(gql(props.query))); expect(call).toHaveProperty('variables', vars); }); it('respects context changes', () => { render( h(Provider, { value: client as any, children: [h(MutationUser, { ...props })], }) ); const vars = { test: 1234 }; act(() => { execute(vars, { url: 'test' }); }); const call = client.executeMutation.mock.calls[0][1]; expect(call.url).toBe('test'); }); describe('on sub update', () => { const vars = { test: 1234 }; it('receives data', async () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(MutationUser, { ...props })], }) ); await execute(vars); rerender( h(Provider, { value: client as any, children: [h(MutationUser, { ...props })], }) ); expect(state).toHaveProperty('data', 1); expect(state).toHaveProperty('error', 2); expect(state).toHaveProperty('extensions', { i: 1 }); expect(state).toHaveProperty('fetching', false); }); }); }); ================================================ FILE: packages/preact-urql/src/hooks/useMutation.ts ================================================ import { useState, useCallback, useRef, useEffect } from 'preact/hooks'; import { pipe, onPush, filter, toPromise, take } from 'wonka'; import type { AnyVariables, DocumentInput, OperationResult, OperationContext, CombinedError, Operation, } from '@urql/core'; import { createRequest } from '@urql/core'; import { useClient } from '../context'; import { initialState } from './constants'; /** State of the last mutation executed by your {@link useMutation} hook. * * @remarks * `UseMutationState` is returned (in a tuple) by {@link useMutation} and * gives you the {@link OperationResult} of the last mutation executed * with {@link UseMutationExecute}. * * Even if the mutation document passed to {@link useMutation} changes, * the state isn’t reset, so you can keep displaying the previous result. */ export interface UseMutationState< Data = any, Variables extends AnyVariables = AnyVariables, > { /** Indicates whether `useMutation` is currently executing a mutation. */ fetching: boolean; /** Indicates that the mutation result is not fresh. * * @remarks * The `stale` flag is set to `true` when a new result for the mutation * is expected. * This is mostly unused for mutations and will rarely affect you, and * is more relevant for queries. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed mutation. */ data?: Data; /** The {@link OperationResult.error} for the executed mutation. */ error?: CombinedError; /** The {@link OperationResult.hasNext} for the executed query. */ hasNext: boolean; /** The {@link OperationResult.extensions} for the executed mutation. */ extensions?: Record; /** The {@link Operation} that the current state is for. * * @remarks * This is the mutation {@link Operation} that has last been executed. * When {@link UseQueryState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; } /** Triggers {@link useMutation} to execute its GraphQL mutation operation. * * @param variables - variables using which the mutation will be executed. * @param context - optionally, context options that will be merged with the hook's * {@link UseQueryArgs.context} options and the `Client`’s options. * @returns the {@link OperationResult} of the mutation. * * @remarks * When called, {@link useMutation} will start the GraphQL mutation * it currently holds and use the `variables` passed to it. * * Once the mutation response comes back from the API, its * returned promise will resolve to the mutation’s {@link OperationResult} * and the {@link UseMutationState} will be updated with the result. * * @example * ```ts * const [result, executeMutation] = useMutation(UpdateTodo); * const start = async ({ id, title }) => { * const result = await executeMutation({ id, title }); * }; */ export type UseMutationExecute< Data = any, Variables extends AnyVariables = AnyVariables, > = ( variables: Variables, context?: Partial ) => Promise>; /** Result tuple returned by the {@link useMutation} hook. * * @remarks * Similarly to a `useState` hook’s return value, * the first element is the {@link useMutation}’s state, updated * as mutations are executed with the second value, which is * used to start mutations and is a {@link UseMutationExecute} * function. */ export type UseMutationResponse< Data = any, Variables extends AnyVariables = AnyVariables, > = [UseMutationState, UseMutationExecute]; /** Hook to create a GraphQL mutation, run by passing variables to the returned execute function. * * @param query - a GraphQL mutation document which `useMutation` will execute. * @returns a {@link UseMutationResponse} tuple of a {@link UseMutationState} result, * and an execute function to start the mutation. * * @remarks * `useMutation` allows GraphQL mutations to be defined and keeps its state * after the mutation is started with the returned execute function. * * Given a GraphQL mutation document it returns state to keep track of the * mutation state and a {@link UseMutationExecute} function, which accepts * variables for the mutation to be executed. * Once called, the mutation executes and the state will be updated with * the mutation’s result. * * @see {@link https://urql.dev/goto/docs/basics/react-preact/#mutations} for `useMutation` docs. * * @example * ```ts * import { gql, useMutation } from '@urql/preact'; * * const UpdateTodo = gql` * mutation ($id: ID!, $title: String!) { * updateTodo(id: $id, title: $title) { * id, title * } * } * `; * * const UpdateTodo = () => { * const [result, executeMutation] = useMutation(UpdateTodo); * const start = async ({ id, title }) => { * const result = await executeMutation({ id, title }); * }; * // ... * }; * ``` */ export function useMutation< Data = any, Variables extends AnyVariables = AnyVariables, >(query: DocumentInput): UseMutationResponse { const isMounted = useRef(true); const client = useClient(); const [state, setState] = useState>(initialState); const executeMutation = useCallback( (variables: Variables, context?: Partial) => { setState({ ...initialState, fetching: true }); return pipe( client.executeMutation( createRequest(query, variables), context || {} ), onPush(result => { if (isMounted.current) { setState({ fetching: false, stale: result.stale, data: result.data, hasNext: result.hasNext, error: result.error, extensions: result.extensions, operation: result.operation, }); } }), filter(result => !result.hasNext), take(1), toPromise ); }, // eslint-disable-next-line react-hooks/exhaustive-deps [client, query, setState] ); useEffect(() => { return () => { isMounted.current = false; }; }, []); return [state, executeMutation]; } ================================================ FILE: packages/preact-urql/src/hooks/useQuery.test.tsx ================================================ // @vitest-environment jsdom import { FunctionalComponent as FC, h } from 'preact'; import { render, cleanup, act } from '@testing-library/preact'; import { OperationContext } from '@urql/core'; import { map, interval, pipe, never, onStart, onEnd, empty } from 'wonka'; import { vi, expect, it, beforeEach, describe, afterEach, Mock } from 'vitest'; import { useQuery, UseQueryArgs, UseQueryState } from './useQuery'; import { Provider } from '../context'; const mock = { executeQuery: vi.fn(() => pipe( interval(400), map((i: number) => ({ data: i, error: i + 1, extensions: { i: 1 } })) ) ), }; const client = mock as { executeQuery: Mock }; const props: UseQueryArgs<{ myVar: number }> = { query: '{ example }', variables: { myVar: 1234, }, pause: false, }; let state: UseQueryState | undefined; let execute: ((_opts?: Partial) => void) | undefined; const QueryUser: FC> = ({ query, variables, pause, }) => { [state, execute] = useQuery({ query, variables, pause }); return h('p', {}, state.data); }; beforeEach(() => { vi.useFakeTimers(); vi.spyOn(globalThis.console, 'error'); }); describe('useQuery', () => { beforeEach(() => { client.executeQuery.mockClear(); state = undefined; execute = undefined; }); afterEach(() => cleanup()); it('executes subscription', () => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); expect(client.executeQuery).toBeCalledTimes(1); }); it('passes query and vars to executeQuery', () => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); expect(client.executeQuery).toBeCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: props.variables, }, expect.objectContaining({ requestPolicy: undefined, }) ); }); it('sets fetching to true', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); expect(state).toHaveProperty('fetching', true); }); it('forwards data response', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { vi.advanceTimersByTime(400); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(state).toHaveProperty('data', 0); }); it('forwards error response', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { vi.advanceTimersByTime(400); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(state).toHaveProperty('error', 1); }); it('forwards extensions response', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { vi.advanceTimersByTime(400); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(state).toHaveProperty('extensions', { i: 1 }); }); it('sets fetching to false', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { vi.advanceTimersByTime(400); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(state).toHaveProperty('fetching', false); }); describe('on change', () => { const q = 'query NewQuery { example }'; it('new query executes subscription', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props, query: q })], }) ); act(() => { rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props, query: q })], }) ); }); expect(client.executeQuery).toBeCalledTimes(2); }); }); describe('on unmount', () => { const start = vi.fn(); const unsubscribe = vi.fn(); beforeEach(() => { client.executeQuery.mockReturnValue( pipe(never, onStart(start), onEnd(unsubscribe)) ); }); it('unsubscribe is called', () => { const { unmount } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => { unmount(); }); expect(start).toBeCalledTimes(2); expect(unsubscribe).toBeCalledTimes(2); }); }); describe('active teardown', () => { it('sets fetching to false when the source ends', () => { client.executeQuery.mockReturnValueOnce(empty); act(() => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); }); expect(client.executeQuery).toHaveBeenCalled(); expect(state).toMatchObject({ fetching: false }); }); }); describe('execute query', () => { it('triggers query execution', () => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); act(() => execute && execute()); expect(client.executeQuery).toBeCalledTimes(2); }); }); describe('pause', () => { it('skips executing the query if pause is true', () => { render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props, pause: true })], }) ); expect(client.executeQuery).not.toBeCalled(); }); it('skips executing queries if pause updates to true', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(QueryUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(QueryUser, { ...props, pause: true })], }) ); expect(client.executeQuery).toBeCalledTimes(1); }); }); }); ================================================ FILE: packages/preact-urql/src/hooks/useQuery.ts ================================================ import { useEffect, useCallback, useMemo } from 'preact/hooks'; import type { Source } from 'wonka'; import { pipe, share, takeWhile, concat, fromValue, switchMap, map, scan, } from 'wonka'; import type { Client, GraphQLRequestParams, AnyVariables, CombinedError, OperationContext, RequestPolicy, OperationResult, Operation, } from '@urql/core'; import { useClient } from '../context'; import { useSource } from './useSource'; import { useRequest } from './useRequest'; import { initialState } from './constants'; /** Input arguments for the {@link useQuery} hook. * * @param query - The GraphQL query that `useQuery` executes. * @param variables - The variables for the GraphQL query that `useQuery` executes. */ export type UseQueryArgs< Variables extends AnyVariables = AnyVariables, Data = any, > = { /** Updates the {@link RequestPolicy} for the executed GraphQL query operation. * * @remarks * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation * that `useQuery` executes, and indicates a caching strategy for cache exchanges. * * For example, when set to `'cache-and-network'`, {@link useQuery} will * receive a cached result with `stale: true` and an API request will be * sent in the background. * * @see {@link OperationContext.requestPolicy} for where this value is set. */ requestPolicy?: RequestPolicy; /** Updates the {@link OperationContext} for the executed GraphQL query operation. * * @remarks * `context` may be passed to {@link useQuery}, to update the {@link OperationContext} * of a query operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * Hint: This should be wrapped in a `useMemo` hook, to make sure that your * component doesn’t infinitely update. * * @example * ```ts * const [result, reexecute] = useQuery({ * query, * context: useMemo(() => ({ * additionalTypenames: ['Item'], * }), []) * }); * ``` */ context?: Partial; /** Prevents {@link useQuery} from automatically executing GraphQL query operations. * * @remarks * `pause` may be set to `true` to stop {@link useQuery} from executing * automatically. The hook will stop receiving updates from the {@link Client} * and won’t execute the query operation, until either it’s set to `false` * or the {@link UseQueryExecute} function is called. * * @see {@link https://urql.dev/goto/docs/basics/react-preact/#pausing-usequery} for * documentation on the `pause` option. */ pause?: boolean; } & GraphQLRequestParams; /** State of the current query, your {@link useQuery} hook is executing. * * @remarks * `UseQueryState` is returned (in a tuple) by {@link useQuery} and * gives you the updating {@link OperationResult} of GraphQL queries. * * Even when the query and variables passed to {@link useQuery} change, * this state preserves the prior state and sets the `fetching` flag to * `true`. * This allows you to display the previous state, while implementing * a separate loading indicator separately. */ export interface UseQueryState< Data = any, Variables extends AnyVariables = AnyVariables, > { /** Indicates whether `useQuery` is waiting for a new result. * * @remarks * When `useQuery` is passed a new query and/or variables, it will * start executing the new query operation and `fetching` is set to * `true` until a result arrives. * * Hint: This is subtly different than whether the query is actually * fetching, and doesn’t indicate whether a query is being re-executed * in the background. For this, see {@link UseQueryState.stale}. */ fetching: boolean; /** Indicates that the state is not fresh and a new result will follow. * * @remarks * The `stale` flag is set to `true` when a new result for the query * is expected and `useQuery` is waiting for it. This may indicate that * a new request is being requested in the background. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed query. */ data?: Data; /** The {@link OperationResult.error} for the executed query. */ error?: CombinedError; /** The {@link OperationResult.extensions} for the executed query. */ extensions?: Record; /** The {@link Operation} that the current state is for. * * @remarks * This is the {@link Operation} that is currently being executed. * When {@link UseQueryState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; /** The {@link OperationResult.hasNext} for the executed query. */ hasNext: boolean; } /** Triggers {@link useQuery} to execute a new GraphQL query operation. * * @remarks * When called, {@link useQuery} will re-execute the GraphQL query operation * it currently holds, even if {@link UseQueryArgs.pause} is set to `true`. * * This is useful for executing a paused query or re-executing a query * and get a new network result, by passing a new request policy. * * ```ts * const [result, reexecuteQuery] = useQuery({ query }); * * const refresh = () => { * // Re-execute the query with a network-only policy, skipping the cache * reexecuteQuery({ requestPolicy: 'network-only' }); * }; * ``` */ export type UseQueryExecute = (opts?: Partial) => void; /** Result tuple returned by the {@link useQuery} hook. * * @remarks * Similarly to a `useState` hook’s return value, * the first element is the {@link useQuery}’s result and state, * a {@link UseQueryState} object, * and the second is used to imperatively re-execute the query * via a {@link UseQueryExecute} function. */ export type UseQueryResponse< Data = any, Variables extends AnyVariables = AnyVariables, > = [UseQueryState, UseQueryExecute]; /** Convert the Source to a React Suspense source on demand * @internal */ function toSuspenseSource(source: Source): Source { const shared = share(source); let cache: T | void; let resolve: (value: T) => void; return sink => { let hasSuspended = false; pipe( shared, takeWhile(result => { // The first result that is received will resolve the suspense // promise after waiting for a microtick if (cache === undefined) Promise.resolve(result).then(resolve); cache = result; return !hasSuspended; }) )(sink); // If we haven't got a previous result then start suspending // otherwise issue the last known result immediately if (cache !== undefined) { const signal = [cache] as [T] & { tag: 1 }; signal.tag = 1; sink(signal); } else { hasSuspended = true; sink(0 /* End */); throw new Promise(_resolve => { resolve = _resolve; }); } }; } const isSuspense = (client: Client, context?: Partial) => context && context.suspense !== undefined ? !!context.suspense : client.suspense; const sources = new Map>(); /** Hook to run a GraphQL query and get updated GraphQL results. * * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link UseQueryResponse} tuple of a {@link UseQueryState} result, and re-execute function. * * @remarks * `useQuery` allows GraphQL queries to be defined and executed. * Given {@link UseQueryArgs.query}, it executes the GraphQL query with the * context’s {@link Client}. * * The returned result updates when the `Client` has new results * for the query, and changes when your input `args` change. * * Additionally, if the `suspense` option is enabled on the `Client`, * the `useQuery` hook will suspend instead of indicating that it’s * waiting for a result via {@link UseQueryState.fetching}. * * @see {@link https://urql.dev/goto/docs/basics/react-preact/#queries} for `useQuery` docs. * * @example * ```ts * import { gql, useQuery } from '@urql/preact'; * * const TodosQuery = gql` * query { todos { id, title } } * `; * * const Todos = () => { * const [result, reexecuteQuery] = useQuery({ * query: TodosQuery, * variables: {}, * }); * // ... * }; * ``` */ export function useQuery< Data = any, Variables extends AnyVariables = AnyVariables, >(args: UseQueryArgs): UseQueryResponse { const client = useClient(); // This creates a request which will keep a stable reference // if request.key doesn't change const request = useRequest(args.query, args.variables as Variables); // Create a new query-source from client.executeQuery const makeQuery$ = useCallback( (opts?: Partial) => { // Determine whether suspense is enabled for the given operation const suspense = isSuspense(client, args.context); let source: Source | void = suspense ? sources.get(request.key) : undefined; if (!source) { source = client.executeQuery(request, { requestPolicy: args.requestPolicy, ...args.context, ...opts, }); // Create a suspense source and cache it for the given request if (suspense) { source = toSuspenseSource(source); if (typeof window !== 'undefined') { sources.set(request.key, source); } } } return source; }, [client, request, args.requestPolicy, args.context] ); const query$ = useMemo(() => { return args.pause ? null : makeQuery$(); }, [args.pause, makeQuery$]); const [state, update] = useSource( query$, useCallback((query$$, prevState?: UseQueryState) => { return pipe( query$$, switchMap(query$ => { if (!query$) return fromValue({ fetching: false, stale: false, hasNext: false }); return concat([ // Initially set fetching to true fromValue({ fetching: true, stale: false }), pipe( query$, map(({ stale, data, error, extensions, operation, hasNext }) => ({ fetching: false, stale: !!stale, hasNext, data, error, operation, extensions, })) ), // When the source proactively closes, fetching is set to false fromValue({ fetching: false, stale: false, hasNext: false }), ]); }), // The individual partial results are merged into each previous result scan( (result: UseQueryState, partial) => ({ ...result, ...partial, }), prevState || initialState ) ); }, []) ); // This is the imperative execute function passed to the user const executeQuery = useCallback( (opts?: Partial) => { update(makeQuery$({ suspense: false, ...opts })); }, [update, makeQuery$] ); useEffect(() => { sources.delete(request.key); // Delete any cached suspense source if (!isSuspense(client, args.context)) update(query$); }, [update, client, query$, request, args.context]); if (isSuspense(client, args.context)) { update(query$); } return [state, executeQuery]; } ================================================ FILE: packages/preact-urql/src/hooks/useRequest.ts ================================================ import type { DocumentNode } from 'graphql'; import { useRef, useMemo } from 'preact/hooks'; import type { AnyVariables, TypedDocumentNode, GraphQLRequest, } from '@urql/core'; import { createRequest } from '@urql/core'; /** Creates a request from a query and variables but preserves reference equality if the key isn't changing * @internal */ export function useRequest< Data = any, Variables extends AnyVariables = AnyVariables, >( query: string | DocumentNode | TypedDocumentNode, variables: Variables ): GraphQLRequest { const prev = useRef>(undefined); return useMemo(() => { const request = createRequest(query, variables); // We manually ensure reference equality if the key hasn't changed if (prev.current !== undefined && prev.current.key === request.key) { return prev.current; } else { prev.current = request; return request; } }, [query, variables]); } ================================================ FILE: packages/preact-urql/src/hooks/useSource.ts ================================================ /* eslint-disable react-hooks/exhaustive-deps */ import { useMemo, useEffect, useState } from 'preact/hooks'; import type { Source } from 'wonka'; import { fromValue, makeSubject, pipe, concat, subscribe } from 'wonka'; type Updater = (input: T) => void; let currentInit = false; // Two operations are considered equal if they have the same key const areOperationsEqual = ( a: { key: number } | undefined, b: { key: number } | undefined ) => { return a === b || !!(a && b && a.key === b.key); }; const isShallowDifferent = (a: any, b: any) => { if (typeof a != 'object' || typeof b != 'object') return a !== b; for (const x in a) if (!(x in b)) return true; for (const key in b) { if ( key === 'operation' ? !areOperationsEqual(a[key], b[key]) : a[key] !== b[key] ) { return true; } } return false; }; export function useSource( input: T, transform: (input$: Source, initial?: R) => Source ): [R, Updater] { const [input$, updateInput] = useMemo((): [Source, (value: T) => void] => { const subject = makeSubject(); const source = concat([fromValue(input), subject.source]); const updateInput = (nextInput: T) => { if (nextInput !== input) subject.next((input = nextInput)); }; return [source, updateInput]; }, []); const [state, setState] = useState(() => { currentInit = true; let state: R; try { pipe( transform(fromValue(input)), subscribe(value => { state = value; }) ).unsubscribe(); } finally { currentInit = false; } return state!; }); useEffect(() => { return pipe( transform(input$, state), subscribe(value => { if (!currentInit) { setState(prevValue => { return isShallowDifferent(prevValue, value) ? value : prevValue; }); } }) ).unsubscribe; }, [input$ /* `state` is only an initialiser */]); return [state, updateInput]; } ================================================ FILE: packages/preact-urql/src/hooks/useSubscription.test.tsx ================================================ // @vitest-environment jsdom import { FunctionalComponent as FC, h } from 'preact'; import { render, cleanup, act } from '@testing-library/preact'; import { OperationContext } from '@urql/core'; import { vi, expect, it, beforeEach, describe, beforeAll, Mock, afterEach, } from 'vitest'; import { merge, fromValue, never, empty } from 'wonka'; import { useSubscription, UseSubscriptionState } from './useSubscription'; import { Provider } from '../context'; const data = { data: 1234, error: 5678 }; const mock = { // @ts-ignore executeSubscription: vi.fn(() => merge([fromValue(data), never])), }; const client = mock as { executeSubscription: Mock }; const query = 'subscription Example { example }'; let state: UseSubscriptionState | undefined; let execute: ((_opts?: Partial) => void) | undefined; const SubscriptionUser: FC<{ q: string; handler?: (_prev: any, _data: any) => any; context?: Partial; pause?: boolean; }> = ({ q, handler, context, pause = false }) => { [state, execute] = useSubscription({ query: q, context, pause }, handler); return h('p', {}, state.data); }; beforeAll(() => { vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothing }); }); describe('useSubscription', () => { beforeEach(() => { client.executeSubscription.mockClear(); state = undefined; execute = undefined; }); const props = { q: query }; afterEach(() => cleanup()); it('executes subscription', () => { render( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props })], }) ); expect(client.executeSubscription).toBeCalledTimes(1); }); it('should support setting context in useSubscription params', () => { render( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props, context: { url: 'test' } })], }) ); expect(client.executeSubscription).toBeCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: {}, }, { url: 'test', } ); }); describe('on subscription', () => { it('forwards client response', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props })], }) ); /** * Have to call update (without changes) in order to see the * result of the state change. */ rerender( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props })], }) ); expect(state).toEqual({ ...data, hasNext: false, extensions: undefined, fetching: true, stale: false, }); }); }); it('calls handler', () => { const handler = vi.fn(); const { rerender } = render( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props, handler })], }) ); rerender( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props })], }) ); expect(handler).toBeCalledTimes(2); expect(handler).toBeCalledWith(undefined, 1234); }); describe('active teardown', () => { it('sets fetching to false when the source ends', () => { client.executeSubscription.mockReturnValueOnce(empty); render( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props })], }) ); expect(client.executeSubscription).toHaveBeenCalled(); expect(state).toMatchObject({ fetching: false }); }); }); describe('execute subscription', () => { it('triggers subscription execution', () => { render( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props })], }) ); act(() => execute && execute()); expect(client.executeSubscription).toBeCalledTimes(2); }); }); describe('pause', () => { const props = { q: query }; it('skips executing the query if pause is true', () => { render( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props, pause: true })], }) ); expect(client.executeSubscription).not.toBeCalled(); }); it('skips executing queries if pause updates to true', () => { const { rerender } = render( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props })], }) ); rerender( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props, pause: true })], }) ); rerender( h(Provider, { value: client as any, children: [h(SubscriptionUser, { ...props, pause: true })], }) ); expect(client.executeSubscription).toBeCalledTimes(1); expect(state).toMatchObject({ fetching: false }); }); }); }); ================================================ FILE: packages/preact-urql/src/hooks/useSubscription.ts ================================================ import { useEffect, useCallback, useRef, useMemo } from 'preact/hooks'; import { pipe, concat, fromValue, switchMap, map, scan } from 'wonka'; import type { AnyVariables, GraphQLRequestParams, CombinedError, OperationContext, Operation, } from '@urql/core'; import { useClient } from '../context'; import { useSource } from './useSource'; import { useRequest } from './useRequest'; import { initialState } from './constants'; /** Input arguments for the {@link useSubscription} hook. * * @param query - The GraphQL subscription document that `useSubscription` executes. * @param variables - The variables for the GraphQL subscription that `useSubscription` executes. */ export type UseSubscriptionArgs< Variables extends AnyVariables = AnyVariables, Data = any, > = { /** Prevents {@link useSubscription} from automatically starting GraphQL subscriptions. * * @remarks * `pause` may be set to `true` to stop {@link useSubscription} from starting its subscription * automatically. The hook will stop receiving updates from the {@link Client} * and won’t start the subscription operation, until either it’s set to `false` * or the {@link UseSubscriptionExecute} function is called. */ pause?: boolean; /** Updates the {@link OperationContext} for the executed GraphQL subscription operation. * * @remarks * `context` may be passed to {@link useSubscription}, to update the {@link OperationContext} * of a subscription operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * Hint: This should be wrapped in a `useMemo` hook, to make sure that your * component doesn’t infinitely update. * * @example * ```ts * const [result, reexecute] = useSubscription({ * query, * context: useMemo(() => ({ * additionalTypenames: ['Item'], * }), []) * }); * ``` */ context?: Partial; } & GraphQLRequestParams; /** Combines previous data with an incoming subscription result’s data. * * @remarks * A `SubscriptionHandler` may be passed to {@link useSubscription} to * aggregate subscription results into a combined {@link UseSubscriptionState.data} * value. * * This is useful when a subscription event delivers a single item, while * you’d like to display a list of events. * * @example * ```ts * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * const combineNotifications = (notifications = [], data) => { * return [...notifications, data.newNotification]; * }; * * const [result, executeSubscription] = useSubscription( * { query: NotificationsSubscription }, * combineNotifications, * ); * ``` */ export type SubscriptionHandler = (prev: R | undefined, data: T) => R; /** State of the current subscription, your {@link useSubscription} hook is executing. * * @remarks * `UseSubscriptionState` is returned (in a tuple) by {@link useSubscription} and * gives you the updating {@link OperationResult} of GraphQL subscriptions. * * If a {@link SubscriptionHandler} has been passed to `useSubscription` then * {@link UseSubscriptionState.data} is instead the updated data as returned * by the handler, otherwise it’s the latest result’s data. * * Hint: Even when the query and variables passed to {@link useSubscription} change, * this state preserves the prior state. */ export interface UseSubscriptionState< Data = any, Variables extends AnyVariables = AnyVariables, > { /** Indicates whether `useSubscription`’s subscription is active. * * @remarks * When `useSubscription` starts a subscription, the `fetching` flag * is set to `true` and will remain `true` until the subscription * completes on the API, or the {@link UseSubscriptionArgs.pause} * flag is set to `true`. */ fetching: boolean; /** Indicates that the subscription result is not fresh. * * @remarks * This is mostly unused for subscriptions and will rarely affect you, and * is more relevant for queries. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed subscription, or data returned by a handler. * * @remarks * `data` will be set to the last {@link OperationResult.data} value * received for the subscription. * * It will instead be set to the values that {@link SubscriptionHandler} * returned, if a handler has been passed to {@link useSubscription}. */ data?: Data; /** The {@link OperationResult.error} for the executed subscription. */ error?: CombinedError; /** The {@link OperationResult.extensions} for the executed mutation. */ extensions?: Record; /** The {@link Operation} that the current state is for. * * @remarks * This is the subscription {@link Operation} that is currently active. * When {@link UseSubscriptionState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; } /** Triggers {@link useSubscription} to reexecute a GraphQL subscription operation. * * @param opts - optionally, context options that will be merged with the hook's * {@link UseSubscriptionArgs.context} options and the `Client`’s options. * * @remarks * When called, {@link useSubscription} will restart the GraphQL subscription * operation it currently holds. If {@link UseSubscriptionArgs.pause} is set * to `true`, it will start executing the subscription. * * ```ts * const [result, executeSubscription] = useSubscription({ * query, * pause: true, * }); * * const start = () => { * executeSubscription(); * }; * ``` */ export type UseSubscriptionExecute = (opts?: Partial) => void; /** Result tuple returned by the {@link useSubscription} hook. * * @remarks * Similarly to a `useState` hook’s return value, * the first element is the {@link useSubscription}’s state, * a {@link UseSubscriptionState} object, * and the second is used to imperatively re-execute or start the subscription * via a {@link UseMutationExecute} function. */ export type UseSubscriptionResponse< Data = any, Variables extends AnyVariables = AnyVariables, > = [UseSubscriptionState, UseSubscriptionExecute]; /** Hook to run a GraphQL subscription and get updated GraphQL results. * * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options. * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results. * @returns a {@link UseSubscriptionResponse} tuple of a {@link UseSubscriptionState} result, and an execute function. * * @remarks * `useSubscription` allows GraphQL subscriptions to be defined and executed. * Given {@link UseSubscriptionArgs.query}, it executes the GraphQL subscription with the * context’s {@link Client}. * * The returned result updates when the `Client` has new results * for the subscription, and `data` is updated with the result’s data * or with the `data` that a `handler` returns. * * @example * ```ts * import { gql, useSubscription } from '@urql/preact'; * * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * const combineNotifications = (notifications = [], data) => { * return [...notifications, data.newNotification]; * }; * * const Notifications = () => { * const [result, executeSubscription] = useSubscription( * { query: NotificationsSubscription }, * combineNotifications, * ); * // ... * }; * ``` */ export function useSubscription< Data = any, Result = Data, Variables extends AnyVariables = AnyVariables, >( args: UseSubscriptionArgs, handler?: SubscriptionHandler ): UseSubscriptionResponse { const client = useClient(); // Update handler on constant ref, since handler changes shouldn't // trigger a new subscription run const handlerRef = useRef(handler); handlerRef.current = handler!; // This creates a request which will keep a stable reference // if request.key doesn't change const request = useRequest(args.query, args.variables as Variables); // Create a new subscription-source from client.executeSubscription const makeSubscription$ = useCallback( (opts?: Partial) => { return client.executeSubscription(request, { ...args.context, ...opts, }); }, [client, request, args.context] ); const subscription$ = useMemo(() => { return args.pause ? null : makeSubscription$(); }, [args.pause, makeSubscription$]); const [state, update] = useSource( subscription$, useCallback( (subscription$$, prevState?: UseSubscriptionState) => { return pipe( subscription$$, switchMap(subscription$ => { if (!subscription$) return fromValue({ fetching: false }); return concat([ // Initially set fetching to true fromValue({ fetching: true, stale: false }), pipe( subscription$, map(({ stale, data, error, extensions, operation }) => ({ fetching: true, stale: !!stale, data, error, extensions, operation, })) ), // When the source proactively closes, fetching is set to false fromValue({ fetching: false, stale: false }), ]); }), // The individual partial results are merged into each previous result scan( (result: UseSubscriptionState, partial: any) => { const { current: handler } = handlerRef; // If a handler has been passed, it's used to merge new data in const data = partial.data != null ? typeof handler === 'function' ? handler(result.data, partial.data) : partial.data : result.data; return { ...result, ...partial, data }; }, prevState || initialState ) ); }, [] ) ); // This is the imperative execute function passed to the user const executeSubscription = useCallback( (opts?: Partial) => update(makeSubscription$(opts)), [update, makeSubscription$] ); useEffect(() => { update(subscription$); }, [update, subscription$]); return [state, executeSubscription]; } ================================================ FILE: packages/preact-urql/src/index.ts ================================================ export * from '@urql/core'; export * from './hooks'; export * from './components'; export * from './context'; ================================================ FILE: packages/preact-urql/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/preact-urql/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: packages/react-urql/CHANGELOG.md ================================================ # urql ## 5.0.1 ### Patch Changes - Upgrade false-positive circumvention for internal React warning to support React 19 Submitted by [@kitten](https://github.com/kitten) (See [#3769](https://github.com/urql-graphql/urql/pull/3769)) ## 5.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 4.2.2 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 4.2.1 ### Patch Changes - Add type for `hasNext` to the query and mutation results Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3703](https://github.com/urql-graphql/urql/pull/3703)) ## 4.2.0 ### Minor Changes - Support use of defer with suspense Submitted by [@AndrewIngram](https://github.com/AndrewIngram) (See [#3687](https://github.com/urql-graphql/urql/pull/3687)) ## 4.1.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ### Patch Changes - ⚠️ Fix subscription handlers to not receive `null` values Submitted by [@kitten](https://github.com/kitten) (See [#3581](https://github.com/urql-graphql/urql/pull/3581)) ## 4.0.7 ### Patch Changes - Updated dependencies (See [#3520](https://github.com/urql-graphql/urql/pull/3520), [#3553](https://github.com/urql-graphql/urql/pull/3553), and [#3520](https://github.com/urql-graphql/urql/pull/3520)) - @urql/core@5.0.0 ## 4.0.6 ### Patch Changes - Prioritise `context.suspense` and fallback to checking `client.suspense` Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3427](https://github.com/urql-graphql/urql/pull/3427)) - Updated dependencies (See [#3430](https://github.com/urql-graphql/urql/pull/3430)) - @urql/core@4.2.0 ## 4.0.5 ### Patch Changes - ⚠️ Fix edge case that causes execute functions from `useQuery` and `useSubscription` to fail when they’re called in their state after a render that changes `pause`. This would previously cause internal dependencies to be outdated and the source to be discarded immediately in some cases Submitted by [@kitten](https://github.com/kitten) (See [#3323](https://github.com/urql-graphql/urql/pull/3323)) - Updated dependencies (See [#3317](https://github.com/urql-graphql/urql/pull/3317) and [#3308](https://github.com/urql-graphql/urql/pull/3308)) - @urql/core@4.1.0 ## 4.0.4 ### Patch Changes - Switch `react` imports to namespace imports, and update build process for CommonJS outputs to interoperate with `__esModule` marked modules again Submitted by [@kitten](https://github.com/kitten) (See [#3251](https://github.com/urql-graphql/urql/pull/3251)) ## 4.0.3 ### Patch Changes - Update build process to generate correct source maps Submitted by [@kitten](https://github.com/kitten) (See [#3201](https://github.com/urql-graphql/urql/pull/3201)) ## 4.0.2 ### Patch Changes - Avoid unnecessary re-render when two components use the same query but receive unchanging results, due to differing operations Submitted by [@nathan-knight](https://github.com/nathan-knight) (See [#3195](https://github.com/urql-graphql/urql/pull/3195)) ## 4.0.1 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 4.0.0 ### Major Changes - Remove the default `Client` from `Context`. Previously, `urql` kept a legacy default client in its context, with default exchanges and calling an API at `/graphql`. This has now been removed and you will have to create your own `Client` if you were relying on this behaviour Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3033](https://github.com/urql-graphql/urql/pull/3033)) ### Minor Changes - Allow mutations to update their results in bindings when `hasNext: true` is set, which indicates deferred or streamed results Submitted by [@kitten](https://github.com/kitten) (See [#3103](https://github.com/urql-graphql/urql/pull/3103)) ### Patch Changes - ⚠️ Fix source maps included with recently published packages, which lost their `sourcesContent`, including additional source files, and had incorrect paths in some of them Submitted by [@kitten](https://github.com/kitten) (See [#3053](https://github.com/urql-graphql/urql/pull/3053)) - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Silence "Cannot update a component (%s) while rendering a different component (%s)." warning forcefully Submitted by [@kitten](https://github.com/kitten) (See [#3095](https://github.com/urql-graphql/urql/pull/3095)) - Add TSDocs to all `urql` bindings packages Submitted by [@kitten](https://github.com/kitten) (See [#3079](https://github.com/urql-graphql/urql/pull/3079)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 3.0.4 ### Patch Changes - ⚠️ Fix type utilities turning the `variables` properties optional when a type from `TypedDocumentNode` has no `Variables` or all optional `Variables`. Previously this would break for wrappers, e.g. in code generators, or when the type didn't quite match what we'd expect Submitted by [@kitten](https://github.com/kitten) (See [#3022](https://github.com/urql-graphql/urql/pull/3022)) - Updated dependencies (See [#3007](https://github.com/urql-graphql/urql/pull/3007), [#2962](https://github.com/urql-graphql/urql/pull/2962), [#3007](https://github.com/urql-graphql/urql/pull/3007), [#3015](https://github.com/urql-graphql/urql/pull/3015), and [#3022](https://github.com/urql-graphql/urql/pull/3022)) - @urql/core@3.2.0 ## 3.0.3 ### Patch Changes - ⚠️ Fix `fetching` going to `false` after changing variables in a subscription, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2667](https://github.com/FormidableLabs/urql/pull/2667)) ## 3.0.2 ### Patch Changes - Update generics for components, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2663](https://github.com/FormidableLabs/urql/pull/2663)) - Updated dependencies (See [#2665](https://github.com/FormidableLabs/urql/pull/2665)) - @urql/core@3.0.3 ## 3.0.1 ### Patch Changes - Tweak the variables type for when generics only contain nullable keys, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2623](https://github.com/FormidableLabs/urql/pull/2623)) ## 3.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Implement stricter variables types, which require variables to always be passed and match TypeScript types when the generic is set or inferred. This is a breaking change for TypeScript users potentially, unless all types are adhered to, by [@kitten](https://github.com/kitten) (See [#2607](https://github.com/FormidableLabs/urql/pull/2607)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Patch Changes - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 2.2.3 ### Patch Changes - ⚠️ Fix missing React updates after an incoming response that schedules a mount. We now prevent dispatched operations from continuing to flush synchronously when the original source that runs the queue has terminated. This is important for the React bindings, because an update (e.g. `setState`) may recursively schedule a mount, which then disabled other `setState` updates from being processed. Previously we assumed that React used a trampoline scheduler for updates, however it appears that `setState` can recursively start more React work, by [@kitten](https://github.com/kitten) (See [#2556](https://github.com/FormidableLabs/urql/pull/2556)) - Updated dependencies (See [#2556](https://github.com/FormidableLabs/urql/pull/2556)) - @urql/core@2.6.1 ## 2.2.2 ### Patch Changes - ⚠️ Fix Node.js ESM re-export detection for `@urql/core` in `urql` package and CommonJS output for all other CommonJS-first packages. This ensures that Node.js' `cjs-module-lexer` can correctly identify re-exports and report them properly. Otherwise, this will lead to a runtime error, by [@kitten](https://github.com/kitten) (See [#2485](https://github.com/FormidableLabs/urql/pull/2485)) ## 2.2.1 ### Patch Changes - ⚠️ Fix issue where a paused subscription would execute with stale variables, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2463](https://github.com/FormidableLabs/urql/pull/2463)) ## 2.2.0 ### Minor Changes - Revert to the previous `useQuery` implementation, `use-sync-external-store` seems to be causing some unexpected timing issues, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2308](https://github.com/FormidableLabs/urql/pull/2308)) ### Patch Changes - Updated dependencies (See [#2295](https://github.com/FormidableLabs/urql/pull/2295)) - @urql/core@2.4.3 ## 2.1.3 ### Patch Changes - ⚠️ fix diff data correctly for the next state computing, this avoids having UI-flashes due to undefined data, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2238](https://github.com/FormidableLabs/urql/pull/2238)) - ⚠️ fix issue where the cache infinitely loops, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2237](https://github.com/FormidableLabs/urql/pull/2237)) ## 2.1.2 ### Patch Changes - Update `useQuery` implementation to avoid an aborted render on initial mount. We abort a render-on-update once when the state needs to be updated according to the `OperationResult` source we need to listen to and execute. However, we can avoid this on the initial mount as we've done in a prior version. This fix **does not** change any of the current behaviour, but simply avoids the confusing state transition on mount, by [@kitten](https://github.com/kitten) (See [#2227](https://github.com/FormidableLabs/urql/pull/2227)) - Updated dependencies (See [#2228](https://github.com/FormidableLabs/urql/pull/2228)) - @urql/core@2.4.1 ## 2.1.1 ### Patch Changes - pin version for `use-sync-external-store`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2223](https://github.com/FormidableLabs/urql/pull/2223)) ## 2.1.0 ### Minor Changes - Leverage the new `use-sync-external-store` package and `useSyncExternalStore` hook in `useQuery` implementation to bring the state synchronisation in React in line with React v18. While the current implementation works already with React Suspense and React Concurrent this will reduce the maintenance burden of our implementation and ensure certain guarantees so that React doesn't break us, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2164](https://github.com/FormidableLabs/urql/pull/2164)) ### Patch Changes - ⚠️ Fix `useMutation` not working correctly with React 18, by [@Dremora](https://github.com/Dremora) (See [#2158](https://github.com/FormidableLabs/urql/pull/2158)) - Updated dependencies (See [#2189](https://github.com/FormidableLabs/urql/pull/2189), [#2153](https://github.com/FormidableLabs/urql/pull/2153), [#2210](https://github.com/FormidableLabs/urql/pull/2210), and [#2198](https://github.com/FormidableLabs/urql/pull/2198)) - @urql/core@2.4.0 ## 2.0.6 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 2.0.5 ### Patch Changes - ⚠️ Fix issue where a paused query would not behave correctly when calling `executeQuery`, this scenario occured when the query has variables, there would be cases where on the first call it would think that the dependencies had changed (previous request vs current request) which made the source reset to null, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1982](https://github.com/FormidableLabs/urql/pull/1982)) - Updated dependencies (See [#1944](https://github.com/FormidableLabs/urql/pull/1944)) - @urql/core@2.3.2 ## 2.0.4 ### Patch Changes - ⚠️ Fix issue with `useQuery`'s `executeQuery` state updates, where some calls wouldn't trigger a source change and start a request when the hook was paused, by [@kitten](https://github.com/kitten) (See [#1722](https://github.com/FormidableLabs/urql/pull/1722)) - Updated dependencies (See [#1709](https://github.com/FormidableLabs/urql/pull/1709)) - @urql/core@2.1.4 ## 2.0.3 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 2.0.2 ### Patch Changes - Add a displayName to the Provider, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1431](https://github.com/FormidableLabs/urql/pull/1431)) ## 2.0.1 ### Patch Changes - ⚠️ Fix issue where `useSubscription` would endlessly loop when the callback wasn't memoized, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1384](https://github.com/FormidableLabs/urql/pull/1384)) - ⚠️ Fix case where identical `useQuery` calls would result in cross-component updates, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1383](https://github.com/FormidableLabs/urql/pull/1383)) ## 2.0.0 ### Major Changes - **Breaking**: Remove `pollInterval` option from `useQuery`. Instead please consider using `useEffect` calling `executeQuery` on an interval, by [@kitten](https://github.com/kitten) (See [#1374](https://github.com/FormidableLabs/urql/pull/1374)) ### Minor Changes - Reimplement `useQuery` to apply a consistent Suspense cache (torn down queries will still eliminate stale values) and support all Concurrent Mode edge cases. This work is based on `useMutableSource`'s mechanisms and allows React to properly fork lanes since no implicit state exists outside of `useState` in the implementation. The `useSubscription` hook has been updated similarly without a cache or retrieving values on mount, by [@kitten](https://github.com/kitten) (See [#1335](https://github.com/FormidableLabs/urql/pull/1335)) - Remove deprecated `operationName` property from `Operation`s. The new `Operation.kind` property is now preferred. If you're creating new operations you may also use the `makeOperation` utility instead. When upgrading `@urql/core` please ensure that your package manager didn't install any duplicates of it. You may deduplicate it manually using `npx yarn-deduplicate` (for Yarn) or `npm dedupe` (for npm), by [@kitten](https://github.com/kitten) (See [#1357](https://github.com/FormidableLabs/urql/pull/1357)) ### Patch Changes - Updated dependencies (See [#1374](https://github.com/FormidableLabs/urql/pull/1374), [#1357](https://github.com/FormidableLabs/urql/pull/1357), and [#1375](https://github.com/FormidableLabs/urql/pull/1375)) - @urql/core@2.0.0 ## 1.11.6 ### Patch Changes - ⚠️ Fix edge cases related to Suspense triggering on an update in Concurrent Mode. Previously it was possible for stale state to be preserved across the Suspense update instead of the new state showing up. This has been fixed by preventing the suspending query source from closing prematurely, by [@kitten](https://github.com/kitten) (See [#1308](https://github.com/FormidableLabs/urql/pull/1308)) ## 1.11.5 ### Patch Changes - ⚠️ Fix Suspense when results share data, this would return partial results for graphCache and not update to the eventual data, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1282](https://github.com/FormidableLabs/urql/pull/1282)) ## 1.11.4 ### Patch Changes - Add a built-in `gql` tag function helper to `@urql/core`. This behaves similarly to `graphql-tag` but only warns about _locally_ duplicated fragment names rather than globally. It also primes `@urql/core`'s key cache with the parsed `DocumentNode`, by [@kitten](https://github.com/kitten) (See [#1187](https://github.com/FormidableLabs/urql/pull/1187)) - Add `suspense: false` to options when `executeQuery` is called explicitly, by [@kitten](https://github.com/kitten) (See [#1181](https://github.com/FormidableLabs/urql/pull/1181)) - Updated dependencies (See [#1187](https://github.com/FormidableLabs/urql/pull/1187), [#1186](https://github.com/FormidableLabs/urql/pull/1186), and [#1186](https://github.com/FormidableLabs/urql/pull/1186)) - @urql/core@1.16.0 ## 1.11.3 ### Patch Changes - ⚠️ Fix in edge-case in client-side React Suspense, where after suspending due to an update a new state value is given to `useSource` in a render update. This was previously then causing us to subscribe to an outdated source in `useEffect` since the updated source would be ignored by the time we reach `useEffect` in `useSource`, by [@kitten](https://github.com/kitten) (See [#1157](https://github.com/FormidableLabs/urql/pull/1157)) ## 1.11.2 ### Patch Changes - ⚠️ Fix regression in client-side Suspense behaviour. This has been fixed in `urql@1.11.0` and `@urql/preact@1.4.0` but regressed in the patches afterwards that were aimed at fixing server-side Suspense, by [@kitten](https://github.com/kitten) (See [#1142](https://github.com/FormidableLabs/urql/pull/1142)) ## 1.11.1 ### Patch Changes - ⚠️ Fix server-side rendering by disabling the new Suspense cache on the server-side and clear it for prepasses, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1138](https://github.com/FormidableLabs/urql/pull/1138)) - Updated dependencies (See [#1135](https://github.com/FormidableLabs/urql/pull/1135)) - @urql/core@1.15.1 ## 1.11.0 ### Minor Changes - Improve the Suspense implementation, which fixes edge-cases when Suspense is used with subscriptions, partially disabled, or _used on the client-side_. It has now been ensured that client-side suspense functions without the deprecated `suspenseExchange` and uncached results are loaded consistently. As part of this work, the `Client` itself does now never throw Suspense promises anymore, which is functionality that either way has no place outside of the React/Preact bindings, by [@kitten](https://github.com/kitten) (See [#1123](https://github.com/FormidableLabs/urql/pull/1123)) ### Patch Changes - Add support for `TypedDocumentNode` to infer the type of the `OperationResult` and `Operation` for all methods, functions, and hooks that either directly or indirectly accept a `DocumentNode`. See [`graphql-typed-document-node` and the corresponding blog post for more information.](https://github.com/dotansimha/graphql-typed-document-node), by [@kitten](https://github.com/kitten) (See [#1113](https://github.com/FormidableLabs/urql/pull/1113)) - Refactor `useSource` hooks which powers `useQuery` and `useSubscription` to improve various edge case behaviour. This will not change the behaviour of these hooks dramatically but avoid unnecessary state updates when any updates are obviously equivalent and the hook will furthermore improve continuation from mount to effects, which will fix cases where the state between the mounting and effect phase may slightly change, by [@kitten](https://github.com/kitten) (See [#1104](https://github.com/FormidableLabs/urql/pull/1104)) - Updated dependencies (See [#1119](https://github.com/FormidableLabs/urql/pull/1119), [#1113](https://github.com/FormidableLabs/urql/pull/1113), [#1104](https://github.com/FormidableLabs/urql/pull/1104), and [#1123](https://github.com/FormidableLabs/urql/pull/1123)) - @urql/core@1.15.0 ## 1.10.3 ### Patch Changes - ⚠️ Fix the production build overwriting the development build. Specifically in the previous release we mistakenly replaced all development bundles with production bundles. This doesn't have any direct influence on how these packages work, but prevented development warnings from being logged or full errors from being thrown, by [@kitten](https://github.com/kitten) (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - Updated dependencies (See [#1097](https://github.com/FormidableLabs/urql/pull/1097)) - @urql/core@1.14.1 ## 1.10.2 ### Patch Changes - Deprecate the `Operation.operationName` property in favor of `Operation.kind`. This name was previously confusing as `operationName` was effectively referring to two different things. You can safely upgrade to this new version, however to mute all deprecation warnings you will have to **upgrade** all `urql` packages you use. If you have custom exchanges that spread operations, please use [the new `makeOperation` helper function](https://formidable.com/open-source/urql/docs/api/core/#makeoperation) instead, by [@bkonkle](https://github.com/bkonkle) (See [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - Updated dependencies (See [#1094](https://github.com/FormidableLabs/urql/pull/1094) and [#1045](https://github.com/FormidableLabs/urql/pull/1045)) - @urql/core@1.14.0 ## 1.10.1 ### Patch Changes - ⚠️ Fix React Fast Refresh beng broken due to an invalid effect, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#969](https://github.com/FormidableLabs/urql/pull/969)) ## 1.10.0 ### Minor Changes - Add the operation to the query, mutation and subscription result, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#924](https://github.com/FormidableLabs/urql/pull/924)) ### Patch Changes - Update hooks to be exported functions rather than exported block-scoped variables to provide TypeScript consumers with better access to their signature, by [@dotansimha](https://github.com/dotansimha) (See [#904](https://github.com/FormidableLabs/urql/pull/904)) - Updated dependencies (See [#911](https://github.com/FormidableLabs/urql/pull/911) and [#908](https://github.com/FormidableLabs/urql/pull/908)) - @urql/core@1.12.3 ## 1.9.8 ### Patch Changes - Upgrade to a minimum version of wonka@^4.0.14 to work around issues with React Native's minification builds, which use uglify-es and could lead to broken bundles, by [@kitten](https://github.com/kitten) (See [#842](https://github.com/FormidableLabs/urql/pull/842)) - Updated dependencies (See [#838](https://github.com/FormidableLabs/urql/pull/838) and [#842](https://github.com/FormidableLabs/urql/pull/842)) - @urql/core@1.12.0 ## 1.9.7 ### Patch Changes - Bump @urql/core to ensure exchanges have dispatchDebug, this could formerly result in a crash, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#726](https://github.com/FormidableLabs/urql/pull/726)) ## 1.9.6 ### Patch Changes - Add graphql@^15.0.0 to peer dependency range, by [@kitten](https://github.com/kitten) (See [#688](https://github.com/FormidableLabs/urql/pull/688)) - Forcefully bump @urql/core package in all bindings and in @urql/exchange-graphcache. We're aware that in some cases users may not have upgraded to @urql/core, even though that's within the typical patch range. Since the latest @urql/core version contains a patch that is required for `cache-and-network` to work, we're pushing another patch that now forcefully bumps everyone to the new version that includes this fix, by [@kitten](https://github.com/kitten) (See [#684](https://github.com/FormidableLabs/urql/pull/684)) - Updated dependencies (See [#688](https://github.com/FormidableLabs/urql/pull/688) and [#678](https://github.com/FormidableLabs/urql/pull/678)) - @urql/core@1.10.8 ## 1.9.5 ### Patch Changes - Avoid setting state on an unmounted component when useMutation is used, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#656](https://github.com/FormidableLabs/urql/pull/656)) - Updated dependencies (See [#658](https://github.com/FormidableLabs/urql/pull/658) and [#650](https://github.com/FormidableLabs/urql/pull/650)) - @urql/core@1.10.5 ## 1.9.4 ### Patch Changes - ⚠️ Fix bundling for packages depending on React, as it doesn't have native ESM bundles, by [@kitten](https://github.com/kitten) (See [#646](https://github.com/FormidableLabs/urql/pull/646)) ## 1.9.3 ### Patch Changes - ⚠️ Fix node resolution when using Webpack, which experiences a bug where it only resolves `package.json:main` instead of `module` when an `.mjs` file imports a package, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - Updated dependencies (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - @urql/core@1.10.4 ## 1.9.2 ### Patch Changes - ⚠️ Fix Node.js Module support for v13 (experimental-modules) and v14. If your bundler doesn't support `.mjs` files and fails to resolve the new version, please double check your configuration for Webpack, or similar tools, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - Updated dependencies (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - @urql/core@1.10.3 ## 1.9.1 ### Patch Changes - Bumps the `@urql/core` dependency minor version to ^1.10.1 for React, Preact and Svelte, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#623](https://github.com/FormidableLabs/urql/pull/623)) - Avoid React v16.13.0's "Warning: Cannot update a component" by preventing cross-hook updates during render or initial mount, by [@kitten](https://github.com/kitten) (See [#630](https://github.com/FormidableLabs/urql/pull/630)) - Updated dependencies (See [#621](https://github.com/FormidableLabs/urql/pull/621)) - @urql/core@1.10.2 ## 1.9.0 ### Patch Changes - ⚠️ Fix more concurrent-mode and strict-mode edge cases and bugs by switching to useSubscription. (See [#514](https://github.com/FormidableLabs/urql/pull/514)) - ⚠️ Fix client-side suspense support (as minimally as possible) by altering the useBehaviourSubject behaviour. (See [#512](https://github.com/FormidableLabs/urql/pull/521)) - Updated dependencies (See [#533](https://github.com/FormidableLabs/urql/pull/533), [#519](https://github.com/FormidableLabs/urql/pull/519), [#515](https://github.com/FormidableLabs/urql/pull/515), [#512](https://github.com/FormidableLabs/urql/pull/512), and [#518](https://github.com/FormidableLabs/urql/pull/518)) - @urql/core@1.9.0 ## 1.8.2 This patch fixes client-side suspense. While we wouldn't recommend its use anymore, since suspense lends itself to prerendering instead of a loading primitive, we'd like to ensure that suspense-mode works as expected in `urql`. Also, as mentioned in `v1.8.0`'s notes, please ensure that `urql` upgrades to use `wonka@^4.0.7` to avoid any issues. If your bundler or packager uses a lower version with `urql`, you will see runtime errors. - Clean up unnecessary `useMemo` for `useCallback` in hooks (see [#504](https://github.com/FormidableLabs/urql/pull/504)) - Fix synchronous, client-side suspense and simplify `toSuspenseSource` helper (see [#506](https://github.com/FormidableLabs/urql/pull/506)) ## 1.8.1 This patch fixes `urql` relying on a quirk in older versions of `wonka` where shared sources wouldn't cascade cancellations, which they now do. This meant that when an app goes from some queries/subscriptions to having none at all, the exchange pipeline would be stopped completely. - Fix exchange pipeline stalling when all queries end (see [#503](https://github.com/FormidableLabs/urql/pull/503)) ## 1.8.0 This release doesn't change any major feature aspects, but comes with bugfixes to our suspense and concurrent-mode handling. Due to an upgrade to `wonka@^4.0.0` this is a minor version though. In [v1.6.0](https://github.com/FormidableLabs/urql/blob/main/CHANGELOG.md#160) we believed to have solved all issues related to suspense and concurrent mode. However there were still some remaining cases where concurrent mode behaved incorrectly. With the new `useOperator` hook in [`react-wonka@2.0.0`](https://github.com/kitten/react-wonka) we believe to have now fixed all issues. The initial mount of `useQuery` and `useSubscription` will now synchronously reflect whatever `urql` returns, most of the times those will be cached results. Afterwards all subsequent updates and fetches will be scheduled cooperatively with React on an effect. If you're using `wonka` for an exchange with `urql` you may want to upgrade to `wonka@^4.0.5` soon. You can still use the older `v3.2.2` which will work with the new version (even in the same bundle), unless you're making use of its `subscribe`, `make`, or `makeSubject` exports. [A migration guide can be found in the `wonka` docs.](https://wonka.kitten.sh/migration) - Support concurrent mode with all edge cases fully (see [#496](https://github.com/FormidableLabs/urql/pull/496)) - Move to `react-wonka@2.0.0` with the prior fix in #496 (see [#499](https://github.com/FormidableLabs/urql/pull/499)) ## 1.7.0 This release splits our main package into two entrypoints. Importing from `urql` remains unchanged, but internally this entrypoint uses `urql/core`, which doesn't contain any React-related code. If you're building framework-agnostic libraries or apps without React, you can now use `urql/core` directly. - Fix `originalError` on `GraphQLError` instances (see [#470](https://github.com/FormidableLabs/urql/pull/470)) - Fix `stringifyVariables` not using `.toJSON()` which prevented Dates from being stringified, by [@BjoernRave](https://github.com/BjoernRave) (see [#485](https://github.com/FormidableLabs/urql/pull/485)) - Expose `urql/core` without any React code included (see [#424](https://github.com/FormidableLabs/urql/pull/424)) ## 1.6.3 - Fix suspense-mode being erroneously activated when using `client.query()` (see [#466](https://github.com/FormidableLabs/react-ssr-prepass/pull/21)) ## 1.6.2 This fixes a potentially critical bug, where a component would enter an infinite rerender loop, when another hook triggers an update. This may happen when multiple `useQuery` hooks are used in a single component or when another state hook triggers a synchronous update. - Add generic type-parameter to `client.query` and `client.mutation`, by [@ctrlplusb](https://github.com/ctrlplusb) (see [#456](https://github.com/FormidableLabs/urql/pull/456)) - ⚠️ Fix `useQuery` entering an infinite loop during SSR when an update is triggered (see [#459](https://github.com/FormidableLabs/urql/pull/459)) ## 1.6.1 - Fix hook updates not being propagated to potential context providers (see [#451](https://github.com/FormidableLabs/urql/pull/451)) ## 1.6.0 This release comes with stability improvements for the `useQuery` and `useSubscription` hooks when using suspense and concurrent mode. They should behave the same as before under normal circumstances and continue to deliver the correct state on initial mount and updates. The `useQuery` hook may however now trigger suspense updates when its inputs are changing, as it should, instead of erroneously throwing a promise in `useEffect`. The added `stale: boolean` flag on the hooks indicates whether a result is "stale". `useQuery` will expose `stale: true` on results that are cached but will be updated due to the use of `cache-and-network`. We've also made some changes so that `client.query()` won't throw a promise, when suspense mode is activated. - ✨ Add `stale` flag to `OperationResult` and hook results (see [#449](https://github.com/FormidableLabs/urql/pull/449)) - Replace `useImmeditateEffect` and `useImmediateState` with `react-wonka` derived state and effect (see [#447](https://github.com/FormidableLabs/urql/pull/447)) - Add (internal) `suspense` flag to `OperationContext` ## 1.5.1 - Replace `fast-json-stable-stringify` with embedded code (see [#426](https://github.com/FormidableLabs/urql/pull/426)) - ⚠ Prevent caching `null` data (see [#437](https://github.com/FormidableLabs/urql/pull/437)) ## 1.5.0 This release finally adds shortcuts to imperatively make queries and mutations. They make it easier to quickly use the client programmatically, either with a Wonka source-based or Promise-based call. ```js // Call .query or .mutation which return Source const source = client.query(doc, vars); const source = client.mutation(doc, vars); // Call .toPromise() on the source to get Promise const promise = client.query(doc, vars).toPromise(); const promise = client.mutation(doc, vars).toPromise(); ``` This version also adds a `useClient` hook as a shortcut for `useContext(Context)`. We provide a default client that makes requests to `/graphql`. Since that has confused users before, we now log a warning, when it's used. - ✨ Implement `client.query()` and `client.mutation()` (see [#405](https://github.com/FormidableLabs/urql/pull/405)) - Fix `useImmediateEffect` for concurrent mode (see [#418](https://github.com/FormidableLabs/urql/pull/418)) - Deconstruct `Wonka.pipe` using a Babel transform (see [#419](https://github.com/FormidableLabs/urql/pull/419)) - ⚠ Add `useClient` hook and warning when default client is used (see [#420](https://github.com/FormidableLabs/urql/pull/420)) ## 1.4.1 This release adds "active teardowns" for operations, which means that an exchange can now send a teardown to cancel ongoing operations. The `subscriptionsExchange` for instance now ends ongoing subscriptions proactively if the server says that they've completed! This is also reflected as `fetching: false` in the `useQuery` and `useSubscription` hook. We've also fixed a small issue with suspense and added all features from `useQuery` to `useSubscription`! This includes the `pause` argument and an `executeSubscription` function. - ✨ Implement active teardowns and add missing features to `useSubscription` (see [#410](https://github.com/FormidableLabs/urql/pull/410)) - Fix `UseMutationResponse` TypeScript type, by [@jbugman](https://github.com/jbugman) (see [#412](https://github.com/FormidableLabs/urql/pull/412)) - Exclude subscriptions from suspense source (see [#415](https://github.com/FormidableLabs/urql/pull/415)) ## 1.4.0 This release removes all metadata for the `@urql/devtools` extension from the core `urql` package. This data will now be generated internally in the devtools exchange itself. [Please also upgrade to the latest `@urql/devtools` version if you're using the extension.](https://github.com/FormidableLabs/urql-devtools/releases/tag/v0.0.3) This release has mainly been focused on minor refactors to keep the bundlesize low. But it also introduces new features, like specifying a default `requestPolicy` and a new polling option on `useQuery`! This release also exports `makeResult` and `makeErrorResult`, which will reduce the boilerplate code that you need for custom fetch exchanges. - Minor bundlesize optimizations and remove `debugExchange` in production (see [#375](https://github.com/FormidableLabs/urql/pull/375)) - ✨ Add `requestPolicy` option to `Client` to change the default request policy (see [#376](https://github.com/FormidableLabs/urql/pull/376)) - ⚠ Remove dependency on `graphql-tag` and improve `Operation.key` hashing (see [#383](https://github.com/FormidableLabs/urql/pull/383)) - Remove `networkLatency` and `source` metadata from context, and delete `useDevtoolsContext` (see [#387](https://github.com/FormidableLabs/urql/pull/387) and [#388](https://github.com/FormidableLabs/urql/pull/388)) - ✨ Add support for polling with `pollInterval` argument to `useQuery`, by [@mxstbr](https://github.com/mxstbr) (see [#397](https://github.com/FormidableLabs/urql/pull/397)) - ⚠ Prevent `__typename` from being added to the toplevel GraphQL documents (see [#399](https://github.com/FormidableLabs/urql/pull/399)) - Add `operationName` field to `fetch` request body (see [#401](https://github.com/FormidableLabs/urql/pull/401)) ## 1.3.0 This release comes with some important fixes and enhancements, which all address certain edge-cases when using `urql`. It fixes the `cache-and-network` request policy, which wouldn't always work correctly and issue another network request after resolving a response from the default cache. We also had a major bug in React Native environments where responses wouldn't ever be reflected in the `useQuery` hook's state. Lastly, you can now use `extensions` from your GraphQL servers and modify the `OperationContext` from the hooks options. - ✨ Add support for `extensions` key in GraphQL responses, by [@adamscybot](https://github.com/adamscybot) (see [#355](https://github.com/FormidableLabs/urql/pull/355)) - ⚠ Fix `cache-and-network` request policy by adding operation flushing to the client (see [#356](https://github.com/FormidableLabs/urql/pull/356)) - Add `fetch` option to the Client so it doesn't have to be polyfilled globally (see [#357](https://github.com/FormidableLabs/urql/pull/357) and [#359](https://github.com/FormidableLabs/urql/pull/359)) - ⚠ Fix `useImmediateState` for React Native environments (see [#358](https://github.com/FormidableLabs/urql/pull/358)) - ✨ Add `context` option to all hooks to allow `OperationContext` to be changed dynamically (see [#351](https://github.com/FormidableLabs/urql/pull/351)) - Add `isClient` option to `ssrExchange` in case `suspense` is activated on the client-side (see [#369](https://github.com/FormidableLabs/urql/pull/369)) ## 1.2.0 A release focused on improving developer experience (in preparation for the upcoming devtools) as well as minor documentation improvements and bug fixes. - Add metadata to operation context in development (see [#305](https://github.com/FormidableLabs/urql/pull/305), [#324](https://github.com/FormidableLabs/urql/pull/324), [#325](https://github.com/FormidableLabs/urql/pull/325) and [#329](https://github.com/FormidableLabs/urql/pull/329)) - Fix minor typename memory leak (see [#321](https://github.com/FormidableLabs/urql/pull/321)) - Fix types for react subscription components (see [#328](https://github.com/FormidableLabs/urql/pull/328)) - Fix displayName attributes not populated in examples (see [#330](https://github.com/FormidableLabs/urql/pull/330)) - Fix error in `collectTypes` method (see [#343](https://github.com/FormidableLabs/urql/pull/343)) - Fix HTTP status bounds check error (see [#348](https://github.com/FormidableLabs/urql/pull/348/files)) ## 1.1.3 This is a hotfix that patches a small regression from `1.1.2` where `useQuery` would crash due to an incorrect teardown function from pause. - Fix `executeQuery` dispose function when `pause` is set, by[@JoviDeCroock](https://github.com/JoviDeCroock) (see [#315](https://github.com/FormidableLabs/urql/pull/315)) ## 1.1.2 This patch fixes a small bug that usually manifests in development, where the initial state would be incorrect after a fast response from the GraphQL API. This used to lock the state into `fetching: true` indefinitely in some cases. - Export all TS types for components (see [#312](https://github.com/FormidableLabs/urql/pull/312)) - ⚠️ Fix state getting stuck on initial mount for fast responses (see [#310](https://github.com/FormidableLabs/urql/pull/310)) - Refactor build tooling to be driven only by Rollup (see [#306](https://github.com/FormidableLabs/urql/pull/306)) - Remove dev-only dependencies from `dependencies` (see [#304](https://github.com/FormidableLabs/urql/pull/304)) ## 1.1.1 This release comes with two small patches. One being a critical fix, where cancelled requests would be erroneously deduped, which meant a previously cancelled query would never be fetched. It also refactors our bundling process to transpile `Object.assign` to restore IE11 support and reduce the amount of duplicate helper in our bundles. - ⚠️ Fix torn-down requests being deduped forever (see [#281](https://github.com/FormidableLabs/urql/pull/281)) - Fix `useQuery`'s `pause` argument blocking explicit `executeQuery` calls (see [#278](https://github.com/FormidableLabs/urql/pull/278)) - Add `Object.assign` transpilation for IE11 and refactor bundling (see [#274](https://github.com/FormidableLabs/urql/pull/274)) ## 1.1.0 This release introduces support for **server-side rendering**. You can find out more about it by reading [the new Basics section on how to set it up.](https://github.com/FormidableLabs/urql/blob/master/docs/basics.md#server-side-rendering) This version now also requires a version of React supporting hooks! (>= 16.8.0) We unfortunately forgot to correct the `peerDependencies` entries in our v1.0.0 release. - ✨ Add **server-side rendering** support (see [#268](https://github.com/FormidableLabs/urql/pull/268)) - ✨ Ensure that state changes are applied immediately on mount (see [#256](https://github.com/FormidableLabs/urql/pull/256)) - Ensure that effects are run immediately on mount (see [#250](https://github.com/FormidableLabs/urql/pull/250)) - ⚠️ Remove `create-react-context` and bump React peer dependency (see [#252](https://github.com/FormidableLabs/urql/pull/252)) - Add generics to the `Query`, `Mutation`, and `Subscription` components - ⚠️ Fix issues where `useQuery` wouldn't update or teardown correctly (see [#243](https://github.com/FormidableLabs/urql/pull/243)) - ✨ Add support for `pause` prop/option to `useQuery` and `Query` (see [#237](https://github.com/FormidableLabs/urql/pull/237)) ## 1.0.5 - Export `MutationProps` types for TS typings, by [@mxstbr](https://github.com/mxstbr) (see [#236](https://github.com/FormidableLabs/urql/pull/236)) - Export `Use*Args` types for TS typings, by [@mxstbr](https://github.com/mxstbr) (see [#235](https://github.com/FormidableLabs/urql/pull/235)) - Export all hook response types for TS typings, by [@good-idea](https://github.com/good-idea) (see [#233](https://github.com/FormidableLabs/urql/pull/233)) - ⚠ Fix runtime error in `cachExchange` where already deleted keys where being accessed (see [#223](https://github.com/FormidableLabs/urql/pull/223)) - ⚠️ Fix `cacheExchange` not forwarding teardowns correctly, which lead to unnecessary/outdated queries being executed, by [@federicobadini](https://github.com/federicobadini) (see [#222](https://github.com/FormidableLabs/urql/pull/222)) - Change `GraphQLRequest` to always pass on a parsed GraphQL `DocumentNode` instead of just a string, which reduces work (see [#221](https://github.com/FormidableLabs/urql/pull/221)) - Fix incorrect TS types by using `Omit` (see [#220](https://github.com/FormidableLabs/urql/pull/220)) ## 1.0.4 - Fix `__typename` not being extracted from responses correctly, which broke caching - Fix `fetchOptions` being called in the client instead of the `fetchExchange` - Improve `CombinedError` to actually extend `Error` and rehydrate `GraphQLError` instances - Fix `executeMutation` prop not accepting any generics types ## 1.0.3 - Fix bug where `variables` were only compared using reference equality, leading to infinite rerenders ## 1.0.2 - Allow `graphql-tag` / `DocumentNode` usage; Operations' queries can now be `DocumentNode`s - Generating keys for queries has been optimized https://github.com/FormidableLabs/urql/compare/v1.0.4...v1.0.5 ## 1.0.0 > Since the entire library has been rewritten for v1.0.0, no changes > are listed here! `urql` v1 is more customisable than ever with "Exchanges", which allow you to change every aspect of how `urql` works. ================================================ FILE: packages/react-urql/README.md ================================================ # urql > A highly customizable and versatile GraphQL client **for React** More documentation is available at [formidable.com/open-source/urql](https://formidable.com/open-source/urql/). ================================================ FILE: packages/react-urql/core/index.d.ts ================================================ export * from '@urql/core'; ================================================ FILE: packages/react-urql/core/index.esm.js ================================================ export * from '@urql/core'; ================================================ FILE: packages/react-urql/core/index.js ================================================ module.exports = require('@urql/core'); ================================================ FILE: packages/react-urql/core/package.json ================================================ { "name": "urql-core", "private": true, "main": "index.js", "module": "index.esm.js", "types": "index.d.ts", "dependencies": { "@urql/core": "^1.7.0" } } ================================================ FILE: packages/react-urql/cypress/fixtures/example.json ================================================ { "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" } ================================================ FILE: packages/react-urql/cypress/support/component-index.html ================================================ Components App
================================================ FILE: packages/react-urql/cypress/support/component.js ================================================ // *********************************************************** // This example support/component.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** import { mount } from 'cypress/react'; Cypress.Commands.add('mount', mount); ================================================ FILE: packages/react-urql/cypress.config.js ================================================ // eslint-disable-next-line const { defineConfig } = require('cypress'); // eslint-disable-next-line const tsconfigPaths = require('vite-tsconfig-paths').default; module.exports = defineConfig({ video: false, e2e: { setupNodeEvents(_on, _config) { /*noop*/ }, supportFile: false, }, component: { specPattern: './**/e2e-tests/*spec.tsx', devServer: { framework: 'react', bundler: 'vite', viteConfig: { plugins: [tsconfigPaths()], server: { fs: { allow: ['..'], }, }, }, }, }, }); ================================================ FILE: packages/react-urql/e2e-tests/useQuery.spec.tsx ================================================ /// import * as React from 'react'; import { mount } from '@cypress/react'; import { delay, pipe } from 'wonka'; import { Provider, createClient, gql, useQuery, cacheExchange, fetchExchange, Exchange, } from '../src'; const delayExchange: Exchange = ({ forward }) => { return ops$ => { return pipe(ops$, forward, delay(250)); }; }; const Boundary = props => { return ( Loading...

}> {props.children}
); }; describe('Suspense', () => { let UrqlProvider; const PokemonsQuery = gql` query ($skip: Int!) { pokemons(limit: 10, skip: $skip) { id name } } `; const Pokemons = () => { const [skip, setSkip] = React.useState(0); const [result] = useQuery({ query: PokemonsQuery, variables: { skip } }); return (
    {result.data.pokemons.map(pokemon => (
  • {pokemon.id}. {pokemon.name}
  • ))}
{skip > 0 && ( )}
); }; beforeEach(() => { const client = createClient({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', suspense: true, exchanges: [cacheExchange, delayExchange, fetchExchange], }); // eslint-disable-next-line UrqlProvider = props => { return {props.children}; }; }); it('Suspends for a basic query', () => { mount( ); cy.get('#suspense').contains('Loading...'); cy.get('#pokemon-list > li').then(items => { expect(items.length).to.equal(10); }); }); it('Suspends when changing variables', () => { mount( ); cy.get('#suspense').contains('Loading...'); cy.get('#pokemon-list > li').then(items => { expect(items.length).to.equal(10); }); cy.get('#next-page').click(); cy.get('#suspense').contains('Loading...'); cy.get('#pokemon-list > li').then(items => { expect(items.length).to.equal(10); }); }); it('Does not suspend for pages that already have been cached', () => { mount( ); cy.get('#suspense').contains('Loading...'); cy.get('#pokemon-list > li').then(items => { expect(items.length).to.equal(10); }); cy.get('#next-page').click(); cy.get('#suspense').contains('Loading...'); cy.get('#pokemon-list > li').then(items => { expect(items.length).to.equal(10); }); cy.get('#previous-page').click(); cy.get('#suspense').should('not.exist'); cy.get('#pokemon-list > li').then(items => { expect(items.length).to.equal(10); }); }); it('does not cause an infinite loop with multiple components querying the same thing', () => { mount( ); cy.get('#suspense').contains('Loading...'); cy.get('ul').then(items => { expect(items.length).to.equal(3); }); }); }); describe('executeQuery', () => { let UrqlProvider; const PokemonsQuery = gql` query { pokemons(limit: 10) { id name } } `; const Pokemons = () => { const [result, excuteQuery] = useQuery({ query: PokemonsQuery }); if (result.fetching) return

Loading...

; return (
    {result.data.pokemons.map(pokemon => (
  • {pokemon.id}. {pokemon.name}
  • ))}
); }; beforeEach(() => { const client = createClient({ url: 'https://trygql.formidable.dev/graphql/basic-pokedex', suspense: false, exchanges: [cacheExchange, delayExchange, fetchExchange], }); // eslint-disable-next-line UrqlProvider = props => { return {props.children}; }; }); it('should set "fetching" to true when reexecuting', () => { mount( ); cy.get('#loading').contains('Loading...'); cy.get('#pokemon-list > li').then(items => { expect(items.length).to.equal(10); }); cy.get('#refetch').click(); cy.get('#loading').contains('Loading...'); cy.get('#pokemon-list > li').then(items => { expect(items.length).to.equal(10); }); }); }); ================================================ FILE: packages/react-urql/jsr.json ================================================ { "name": "urql", "version": "5.0.1", "exports": "src/index.ts", "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/react-urql/package.json ================================================ { "name": "urql", "version": "5.0.1", "description": "A highly customizable and versatile GraphQL client for React", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/react-urql" }, "keywords": [ "graphql client", "state management", "cache", "graphql", "exchanges", "react" ], "main": "dist/urql.js", "module": "dist/urql.es.js", "types": "dist/urql.d.ts", "source": "src/index.ts", "files": [ "LICENSE", "CHANGELOG.md", "README.md", "core/", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@cypress/react": "^8.0.2", "@cypress/vite-dev-server": "^5.2.0", "@testing-library/react": "^16.0.1", "@types/react": "^18.3.8", "@types/react-test-renderer": "^17.0.1", "@urql/core": "workspace:*", "cypress": "^13.14.0", "graphql": "^16.6.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-is": "^18.3.1", "react-ssr-prepass": "^1.5.0", "react-test-renderer": "^18.3.1" }, "peerDependencies": { "@urql/core": "^6.0.0", "react": ">= 16.8.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "publishConfig": { "provenance": true } } ================================================ FILE: packages/react-urql/src/components/Mutation.test.tsx ================================================ // @vitest-environment jsdom import { vi, expect, it, beforeEach, describe, Mock, afterEach } from 'vitest'; vi.mock('../context', async () => { const { delay, fromValue, pipe } = await vi.importActual('wonka'); const mock = { executeMutation: vi.fn(() => pipe(fromValue({ data: 1, error: 2 }), delay(200)) ), }; return { useClient: () => mock, }; }); import * as React from 'react'; import { act, cleanup, render } from '@testing-library/react'; import { Mutation } from './Mutation'; import { useClient } from '../context'; // @ts-ignore const client = useClient() as { executeMutation: Mock }; const query = 'mutation Example { example }'; describe('Mutation', () => { beforeEach(() => { vi.useFakeTimers(); // TODO: Fix use of act() vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothing }); }); afterEach(() => { cleanup(); }); it('Should execute the mutation', () => { let execute = () => { /* noop */ }, props = {}; const Test = () =>

Hi

; const App = () => { return ( // @ts-ignore {({ data, fetching, error, executeMutation }) => { execute = executeMutation; props = { data, fetching, error }; return ; }} ); }; render(); expect(client.executeMutation).toBeCalledTimes(0); expect(props).toStrictEqual({ data: undefined, fetching: false, error: undefined, }); act(() => { execute(); }); expect(props).toStrictEqual({ data: undefined, fetching: true, error: undefined, }); act(() => { vi.advanceTimersByTime(400); }); expect(props).toStrictEqual({ data: 1, fetching: false, error: 2 }); }); }); ================================================ FILE: packages/react-urql/src/components/Mutation.ts ================================================ import type { ReactElement } from 'react'; import type { AnyVariables, DocumentInput } from '@urql/core'; import type { UseMutationState, UseMutationExecute } from '../hooks'; import { useMutation } from '../hooks'; /** Props accepted by {@link Mutation}. * * @remarks * `MutationProps` are the props accepted by the {@link Mutation} component. * * The result, the {@link MutationState} object, will be passed to * a {@link MutationProps.children} function, passed as children * to the `Mutation` component. */ export interface MutationProps< Data = any, Variables extends AnyVariables = AnyVariables, > { /* The GraphQL mutation document that {@link useMutation} will execute. */ query: DocumentInput; children(arg: MutationState): ReactElement; } /** Object that {@link MutationProps.children} is called with. * * @remarks * This is an extented {@link UseMutationstate} with an added * {@link MutationState.executeMutation} method, which is usually * part of a tuple returned by {@link useMutation}. */ export interface MutationState< Data = any, Variables extends AnyVariables = AnyVariables, > extends UseMutationState { /** Alias to {@link useMutation}’s `executeMutation` function. */ executeMutation: UseMutationExecute; } /** Component Wrapper around {@link useMutation} to run a GraphQL query. * * @remarks * `Mutation` is a component wrapper around the {@link useMutation} hook * that calls the {@link MutationProps.children} prop, as a function, * with the {@link MutationState} object. */ export function Mutation< Data = any, Variables extends AnyVariables = AnyVariables, >(props: MutationProps): ReactElement { const mutation = useMutation(props.query); return props.children({ ...mutation[0], executeMutation: mutation[1] }); } ================================================ FILE: packages/react-urql/src/components/Query.test.tsx ================================================ // @vitest-environment jsdom import { vi, expect, it, beforeEach, describe, afterEach } from 'vitest'; vi.mock('../context', async () => { const { map, interval, pipe } = await vi.importActual('wonka'); const mock = { executeQuery: vi.fn(() => pipe( interval(150), map((i: number) => ({ data: i, error: i + 1 })) ) ), }; return { useClient: () => mock, }; }); import * as React from 'react'; import { cleanup, render } from '@testing-library/react'; import { Query } from './Query'; // @ts-ignore const query = '{ example }'; const variables = { myVar: 1234, }; describe('Query', () => { beforeEach(() => { // TODO: Fix use of act() vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothing }); }); afterEach(() => { cleanup(); }); it('Should execute the query', async () => { let props = {}; const Test = () =>

Hi

; const App = () => { return ( // @ts-ignore {({ data, fetching, error }) => { props = { data, fetching, error }; return ; }} ); }; render(); expect(props).toStrictEqual({ data: undefined, fetching: true, error: undefined, }); await new Promise(res => { setTimeout(() => { expect(props).toStrictEqual({ data: 0, fetching: false, error: 1 }); res(null); }, 200); }); }); }); ================================================ FILE: packages/react-urql/src/components/Query.ts ================================================ import type { ReactElement } from 'react'; import type { AnyVariables } from '@urql/core'; import type { UseQueryArgs, UseQueryState, UseQueryExecute } from '../hooks'; import { useQuery } from '../hooks'; /** Props accepted by {@link Query}. * * @remarks * `QueryProps` are the props accepted by the {@link Query} component, * which is identical to {@link UseQueryArgs}. * * The result, the {@link QueryState} object, will be passed to * a {@link QueryProps.children} function, passed as children * to the `Query` component. */ export type QueryProps< Data = any, Variables extends AnyVariables = AnyVariables, > = UseQueryArgs & { children(arg: QueryState): ReactElement; }; /** Object that {@link QueryProps.children} is called with. * * @remarks * This is an extented {@link UseQueryState} with an added * {@link QueryState.executeQuery} method, which is usually * part of a tuple returned by {@link useQuery}. */ export interface QueryState< Data = any, Variables extends AnyVariables = AnyVariables, > extends UseQueryState { /** Alias to {@link useQuery}’s `executeQuery` function. */ executeQuery: UseQueryExecute; } /** Component Wrapper around {@link useQuery} to run a GraphQL query. * * @remarks * `Query` is a component wrapper around the {@link useQuery} hook * that calls the {@link QueryProps.children} prop, as a function, * with the {@link QueryState} object. */ export function Query< Data = any, Variables extends AnyVariables = AnyVariables, >(props: QueryProps): ReactElement { const query = useQuery(props); return props.children({ ...query[0], executeQuery: query[1] }); } ================================================ FILE: packages/react-urql/src/components/Subscription.ts ================================================ import type { ReactElement } from 'react'; import type { AnyVariables } from '@urql/core'; import type { UseSubscriptionArgs, UseSubscriptionState, UseSubscriptionExecute, SubscriptionHandler, } from '../hooks'; import { useSubscription } from '../hooks'; /** Props accepted by {@link Subscription}. * * @remarks * `SubscriptionProps` are the props accepted by the {@link Subscription} component, * which is identical to {@link UseSubscriptionArgs} with an added * {@link SubscriptionProps.handler} prop, which {@link useSubscription} usually * accepts as an additional argument. * * The result, the {@link SubscriptionState} object, will be passed to * a {@link SubscriptionProps.children} function, passed as children * to the `Subscription` component. */ export type SubscriptionProps< Data = any, Result = Data, Variables extends AnyVariables = AnyVariables, > = UseSubscriptionArgs & { handler?: SubscriptionHandler; children(arg: SubscriptionState): ReactElement; }; /** Object that {@link SubscriptionProps.children} is called with. * * @remarks * This is an extented {@link UseSubscriptionState} with an added * {@link SubscriptionState.executeSubscription} method, which is usually * part of a tuple returned by {@link useSubscription}. */ export interface SubscriptionState< Data = any, Variables extends AnyVariables = AnyVariables, > extends UseSubscriptionState { /** Alias to {@link useSubscription}’s `executeMutation` function. */ executeSubscription: UseSubscriptionExecute; } /** Component Wrapper around {@link useSubscription} to run a GraphQL subscription. * * @remarks * `Subscription` is a component wrapper around the {@link useSubscription} hook * that calls the {@link SubscriptionProps.children} prop, as a function, * with the {@link SubscriptionState} object. */ export function Subscription< Data = any, Result = Data, Variables extends AnyVariables = AnyVariables, >(props: SubscriptionProps): ReactElement { const subscription = useSubscription( props, props.handler ); return props.children({ ...subscription[0], executeSubscription: subscription[1], }); } ================================================ FILE: packages/react-urql/src/components/index.ts ================================================ export * from './Mutation'; export * from './Query'; export * from './Subscription'; ================================================ FILE: packages/react-urql/src/context.ts ================================================ import * as React from 'react'; import type { Client } from '@urql/core'; const OBJ = {}; /** `urql`'s React Context. * * @remarks * The React Context that `urql`’s {@link Client} will be provided with. * You may use the reexported {@link Provider} to provide a `Client` as well. */ export const Context: import('react').Context = React.createContext(OBJ); /** Provider for `urql`'s {@link Client} to GraphQL hooks. * * @remarks * `Provider` accepts a {@link Client} and provides it to all GraphQL hooks, * and {@link useClient}. * * You should make sure to create a {@link Client} and provide it with the * `Provider` to parts of your component tree that use GraphQL hooks. * * @example * ```tsx * import { Provider } from 'urql'; * // All of `@urql/core` is also re-exported by `urql`: * import { Client, cacheExchange, fetchExchange } from '@urql/core'; * * const client = new Client({ * url: 'https://API', * exchanges: [cacheExchange, fetchExchange], * }); * * const App = () => ( * * * * ); * ``` */ export const Provider: React.Provider = Context.Provider; /** React Consumer component, providing the {@link Client} provided on a parent component. * @remarks * This is an alias for {@link Context.Consumer}. */ export const Consumer: React.Consumer = Context.Consumer; Context.displayName = 'UrqlContext'; /** Hook returning a {@link Client} from {@link Context}. * * @remarks * `useClient` is a convenience hook, which accesses `urql`'s {@link Context} * and returns the {@link Client} defined on it. * * This will be the {@link Client} you passed to a {@link Provider} * you wrapped your elements containing this hook with. * * @throws * In development, if the component you call `useClient()` in is * not wrapped in a {@link Provider}, an error is thrown. */ export const useClient = (): Client => { const client = React.useContext(Context); if (client === OBJ && process.env.NODE_ENV !== 'production') { const error = "No client has been specified using urql's Provider. please create a client and add a Provider."; console.error(error); throw new Error(error); } return client as Client; }; ================================================ FILE: packages/react-urql/src/hooks/__snapshots__/useMutation.test.tsx.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`on initial useEffect > initialises default state 1`] = ` { "data": undefined, "error": undefined, "extensions": undefined, "fetching": false, "hasNext": false, "operation": undefined, "stale": false, } `; ================================================ FILE: packages/react-urql/src/hooks/__snapshots__/useQuery.test.tsx.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`on initial useEffect > initialises default state 1`] = ` { "data": undefined, "error": undefined, "extensions": undefined, "fetching": true, "hasNext": false, "operation": undefined, "stale": false, } `; ================================================ FILE: packages/react-urql/src/hooks/__snapshots__/useSubscription.test.tsx.snap ================================================ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`on initial useEffect > initialises default state 1`] = ` { "data": undefined, "error": undefined, "extensions": undefined, "fetching": true, "hasNext": false, "operation": undefined, "stale": false, } `; ================================================ FILE: packages/react-urql/src/hooks/cache.ts ================================================ import { pipe, subscribe } from 'wonka'; import type { Client, OperationResult } from '@urql/core'; type CacheEntry = OperationResult | Promise | undefined; interface Cache { get(key: number): CacheEntry; set(key: number, value: CacheEntry): void; dispose(key: number): void; } interface ClientWithCache extends Client { _react?: Cache; } export const getCacheForClient = (client: Client): Cache => { if (!(client as ClientWithCache)._react) { const reclaim = new Set(); const map = new Map(); if (client.operations$ /* not available in mocks */) { pipe( client.operations$, subscribe(operation => { if (operation.kind === 'teardown' && reclaim.has(operation.key)) { reclaim.delete(operation.key); map.delete(operation.key); } }) ); } (client as ClientWithCache)._react = { get(key) { return map.get(key); }, set(key, value) { reclaim.delete(key); map.set(key, value); }, dispose(key) { reclaim.add(key); }, }; } return (client as ClientWithCache)._react!; }; ================================================ FILE: packages/react-urql/src/hooks/index.ts ================================================ export * from './useMutation'; export * from './useQuery'; export * from './useSubscription'; ================================================ FILE: packages/react-urql/src/hooks/state.ts ================================================ import type { Dispatch } from 'react'; export const initialState = { fetching: false, stale: false, hasNext: false, error: undefined, data: undefined, extensions: undefined, operation: undefined, }; // Two operations are considered equal if they have the same key const areOperationsEqual = ( a: { key: number } | undefined, b: { key: number } | undefined ) => { return a === b || !!(a && b && a.key === b.key); }; /** * Checks if two objects are shallowly different with a special case for * 'operation' where it compares the key if they are not the otherwise equal */ const isShallowDifferent = >(a: T, b: T) => { for (const key in a) if (!(key in b)) return true; for (const key in b) { if ( key === 'operation' ? !areOperationsEqual(a[key], b[key]) : a[key] !== b[key] ) { return true; } } return false; }; interface Stateish { data?: any; error?: any; hasNext: boolean; fetching: boolean; stale: boolean; } export const computeNextState = ( prevState: T, result: Partial ): T => { const newState: T = { ...prevState, ...result, data: result.data !== undefined || result.error ? result.data : prevState.data, fetching: !!result.fetching, stale: !!result.stale, }; return isShallowDifferent(prevState, newState) ? newState : prevState; }; export const hasDepsChanged = (a: T, b: T) => { for (let i = 0, l = b.length; i < l; i++) if (a[i] !== b[i]) return true; return false; }; let isDispatching = false; function deferDispatch>( setState: F, value: F extends Dispatch ? State : void ): void; function deferDispatch>(setState: F): ReturnType; function deferDispatch>( setState: F, value?: F extends Dispatch ? State : void ): any { if (!isDispatching || value === undefined) { try { isDispatching = true; return setState(value); } finally { isDispatching = false; } } else { Promise.resolve(value).then(setState); } } export { deferDispatch }; ================================================ FILE: packages/react-urql/src/hooks/useMutation.test.tsx ================================================ // @vitest-environment jsdom import { vi, expect, it, beforeEach, describe, Mock } from 'vitest'; // Note: Testing for hooks is not yet supported in Enzyme - https://github.com/airbnb/enzyme/issues/2011 vi.mock('../context', async () => { const { delay, fromValue, pipe } = await vi.importActual('wonka'); const mock = { executeMutation: vi.fn(() => pipe(fromValue({ data: 1, error: 2, extensions: { i: 1 } }), delay(200)) ), }; return { useClient: () => mock, }; }); import { print } from 'graphql'; import { gql } from '@urql/core'; import React from 'react'; import renderer, { act } from 'react-test-renderer'; import { useClient } from '../context'; import { useMutation } from './useMutation'; // @ts-ignore const client = useClient() as { executeMutation: Mock }; const props = { query: 'mutation Example { example }', }; let state: any; let execute: any; const MutationUser = ({ query }: { query: any }) => { const [s, e] = useMutation(query); state = s; execute = e; return

{s.data}

; }; beforeEach(() => { vi.useFakeTimers(); vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothing }); client.executeMutation.mockClear(); state = undefined; execute = undefined; }); describe('on initial useEffect', () => { it('initialises default state', () => { renderer.create(); expect(state).toMatchSnapshot(); }); it('does not execute subscription', () => { renderer.create(); expect(client.executeMutation).toBeCalledTimes(0); }); }); describe('on execute', () => { const vars = { test: 1234 }; it('sets fetching to true', () => { renderer.create(); act(() => { execute(vars); }); expect(state).toHaveProperty('fetching', true); }); it('calls executeMutation', () => { renderer.create(); act(() => { execute(vars); }); expect(client.executeMutation).toBeCalledTimes(1); }); it('calls executeMutation with query', () => { renderer.create(); act(() => { execute(vars); }); const call = client.executeMutation.mock.calls[0][0]; expect(print(call.query)).toBe(print(gql(props.query))); }); it('calls executeMutation with variables', () => { renderer.create(); act(() => { execute(vars); }); expect(client.executeMutation.mock.calls[0][0]).toHaveProperty( 'variables', vars ); }); it('can adjust context in executeMutation', () => { renderer.create(); act(() => { execute(vars, { url: 'test' }); }); expect(client.executeMutation.mock.calls[0][1].url).toBe('test'); }); }); describe('on subscription update', () => { it('forwards data response', () => { const wrapper = renderer.create(); execute(); act(() => { vi.advanceTimersByTime(200); wrapper.update(); }); expect(state).toHaveProperty('data', 1); }); it('forwards error response', () => { const wrapper = renderer.create(); execute(); act(() => { vi.advanceTimersByTime(200); wrapper.update(); }); expect(state).toHaveProperty('error', 2); }); it('forwards extensions response', () => { const wrapper = renderer.create(); execute(); act(() => { vi.advanceTimersByTime(200); wrapper.update(); }); expect(state).toHaveProperty('extensions', { i: 1 }); }); it('sets fetching to false', () => { const wrapper = renderer.create(); wrapper.update(); execute(); act(() => { vi.advanceTimersByTime(200); wrapper.update(); }); expect(state).toHaveProperty('fetching', false); }); }); ================================================ FILE: packages/react-urql/src/hooks/useMutation.ts ================================================ import * as React from 'react'; import { pipe, onPush, filter, toPromise, take } from 'wonka'; import type { AnyVariables, DocumentInput, OperationResult, OperationContext, CombinedError, Operation, } from '@urql/core'; import { createRequest } from '@urql/core'; import { useClient } from '../context'; import { deferDispatch, initialState } from './state'; /** State of the last mutation executed by your {@link useMutation} hook. * * @remarks * `UseMutationState` is returned (in a tuple) by {@link useMutation} and * gives you the {@link OperationResult} of the last mutation executed * with {@link UseMutationExecute}. * * Even if the mutation document passed to {@link useMutation} changes, * the state isn’t reset, so you can keep displaying the previous result. */ export interface UseMutationState< Data = any, Variables extends AnyVariables = AnyVariables, > { /** Indicates whether `useMutation` is currently executing a mutation. */ fetching: boolean; /** Indicates that the mutation result is not fresh. * * @remarks * The `stale` flag is set to `true` when a new result for the mutation * is expected. * This is mostly unused for mutations and will rarely affect you, and * is more relevant for queries. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed mutation. */ data?: Data; /** The {@link OperationResult.error} for the executed mutation. */ error?: CombinedError; /** The {@link OperationResult.extensions} for the executed mutation. */ extensions?: Record; /** The {@link OperationResult.hasNext} for the executed query. */ hasNext: boolean; /** The {@link Operation} that the current state is for. * * @remarks * This is the mutation {@link Operation} that has last been executed. * When {@link UseQueryState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; } /** Triggers {@link useMutation} to execute its GraphQL mutation operation. * * @param variables - variables using which the mutation will be executed. * @param context - optionally, context options that will be merged with the hook's * {@link UseQueryArgs.context} options and the `Client`’s options. * @returns the {@link OperationResult} of the mutation. * * @remarks * When called, {@link useMutation} will start the GraphQL mutation * it currently holds and use the `variables` passed to it. * * Once the mutation response comes back from the API, its * returned promise will resolve to the mutation’s {@link OperationResult} * and the {@link UseMutationState} will be updated with the result. * * @example * ```ts * const [result, executeMutation] = useMutation(UpdateTodo); * const start = async ({ id, title }) => { * const result = await executeMutation({ id, title }); * }; */ export type UseMutationExecute< Data = any, Variables extends AnyVariables = AnyVariables, > = ( variables: Variables, context?: Partial ) => Promise>; /** Result tuple returned by the {@link useMutation} hook. * * @remarks * Similarly to a `useState` hook’s return value, * the first element is the {@link useMutation}’s state, updated * as mutations are executed with the second value, which is * used to start mutations and is a {@link UseMutationExecute} * function. */ export type UseMutationResponse< Data = any, Variables extends AnyVariables = AnyVariables, > = [UseMutationState, UseMutationExecute]; /** Hook to create a GraphQL mutation, run by passing variables to the returned execute function. * * @param query - a GraphQL mutation document which `useMutation` will execute. * @returns a {@link UseMutationResponse} tuple of a {@link UseMutationState} result, * and an execute function to start the mutation. * * @remarks * `useMutation` allows GraphQL mutations to be defined and keeps its state * after the mutation is started with the returned execute function. * * Given a GraphQL mutation document it returns state to keep track of the * mutation state and a {@link UseMutationExecute} function, which accepts * variables for the mutation to be executed. * Once called, the mutation executes and the state will be updated with * the mutation’s result. * * @see {@link https://urql.dev/goto/urql/docs/basics/react-preact/#mutations} for `useMutation` docs. * * @example * ```ts * import { gql, useMutation } from 'urql'; * * const UpdateTodo = gql` * mutation ($id: ID!, $title: String!) { * updateTodo(id: $id, title: $title) { * id, title * } * } * `; * * const UpdateTodo = () => { * const [result, executeMutation] = useMutation(UpdateTodo); * const start = async ({ id, title }) => { * const result = await executeMutation({ id, title }); * }; * // ... * }; * ``` */ export function useMutation< Data = any, Variables extends AnyVariables = AnyVariables, >(query: DocumentInput): UseMutationResponse { const isMounted = React.useRef(true); const client = useClient(); const [state, setState] = React.useState>(initialState); const executeMutation = React.useCallback( (variables: Variables, context?: Partial) => { deferDispatch(setState, { ...initialState, fetching: true }); return pipe( client.executeMutation( createRequest(query, variables), context || {} ), onPush(result => { if (isMounted.current) { deferDispatch(setState, { fetching: false, stale: result.stale, data: result.data, error: result.error, extensions: result.extensions, operation: result.operation, hasNext: result.hasNext, }); } }), filter(result => !result.hasNext), take(1), toPromise ); }, // eslint-disable-next-line react-hooks/exhaustive-deps [client, query, setState] ); React.useEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); return [state, executeMutation]; } ================================================ FILE: packages/react-urql/src/hooks/useQuery.spec.ts ================================================ // @vitest-environment jsdom import { renderHook, act } from '@testing-library/react'; import { interval, map, pipe } from 'wonka'; import { RequestPolicy } from '@urql/core'; import { vi, expect, it, beforeEach, describe, beforeAll, Mock } from 'vitest'; import { useClient } from '../context'; import { useQuery } from './useQuery'; vi.mock('../context', () => { const mock = { executeQuery: vi.fn(() => pipe( interval(1000 / 60), map(i => ({ data: i, error: i + 1 })) ) ), }; return { useClient: () => mock, }; }); // @ts-ignore const client = useClient() as { executeQuery: Mock }; const mockQuery = ` query todo($id: ID!) { todo(id: $id) { id text completed } } `; const mockVariables = { id: 1, }; describe('useQuery', () => { beforeAll(() => { // TODO: Fix use of act() vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothing }); }); beforeEach(() => { client.executeQuery.mockClear(); }); it('should set fetching to true and run effect on first mount', () => { const { result } = renderHook( ({ query, variables }) => useQuery({ query, variables }), { initialProps: { query: mockQuery, variables: mockVariables } } ); const [state] = result.current; expect(state).toEqual({ fetching: true, stale: false, hasNext: false, extensions: undefined, error: undefined, data: undefined, }); }); it('should support setting context in useQuery params', () => { const context = { url: 'test' }; renderHook( ({ query, variables }) => useQuery({ query, variables, context }), { initialProps: { query: mockQuery, variables: mockVariables } } ); expect(client.executeQuery).toBeCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: mockVariables, }, { requestPolicy: undefined, url: 'test', } ); }); it('should execute the subscription', async () => { renderHook(({ query, variables }) => useQuery({ query, variables }), { initialProps: { query: mockQuery, variables: mockVariables }, }); expect(client.executeQuery).toBeCalledTimes(1); }); it('should pass query and variables to executeQuery', async () => { renderHook(({ query, variables }) => useQuery({ query, variables }), { initialProps: { query: mockQuery, variables: mockVariables }, }); expect(client.executeQuery).toBeCalledTimes(1); expect(client.executeQuery).toBeCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: mockVariables, }, expect.objectContaining({ requestPolicy: undefined, }) ); }); it('should return data from executeQuery', async () => { const { result } = renderHook( ({ query, variables }) => useQuery({ query, variables }), { initialProps: { query: mockQuery, variables: mockVariables } } ); await new Promise(res => setTimeout(res, 30)); const [state] = result.current; expect(state).toEqual({ fetching: false, stale: false, extensions: undefined, hasNext: false, error: 1, data: 0, }); }); it('should update if a new query is received', async () => { const { rerender } = renderHook< any, { query: string; variables: { id?: number } } >(({ query, variables }) => useQuery({ query, variables }), { initialProps: { query: mockQuery, variables: mockVariables }, }); expect(client.executeQuery).toBeCalledTimes(1); const newQuery = ` query places { id address } `; rerender({ query: newQuery, variables: {} }); expect(client.executeQuery).toBeCalledTimes(2); expect(client.executeQuery).toHaveBeenNthCalledWith( 2, { key: expect.any(Number), query: expect.any(Object), variables: {}, }, expect.objectContaining({ requestPolicy: undefined, }) ); }); it('should update if new variables are received', async () => { const { rerender } = renderHook( ({ query, variables }) => useQuery({ query, variables }), { initialProps: { query: mockQuery, variables: mockVariables }, } ); expect(client.executeQuery).toBeCalledTimes(1); const newVariables = { id: 2, }; rerender({ query: mockQuery, variables: newVariables }); expect(client.executeQuery).toBeCalledTimes(2); expect(client.executeQuery).toHaveBeenNthCalledWith( 2, { key: expect.any(Number), query: expect.any(Object), variables: newVariables, }, expect.objectContaining({ requestPolicy: undefined, }) ); }); it('should not update if query and variables are unchanged', async () => { const { rerender } = renderHook( ({ query, variables }) => useQuery({ query, variables }), { initialProps: { query: mockQuery, variables: mockVariables }, } ); expect(client.executeQuery).toBeCalledTimes(1); rerender({ query: mockQuery, variables: mockVariables }); expect(client.executeQuery).toBeCalledTimes(1); }); it('should update if a new requestPolicy is provided', async () => { const { rerender } = renderHook( ({ query, variables, requestPolicy }) => useQuery({ query, variables, requestPolicy }), { initialProps: { query: mockQuery, variables: mockVariables, requestPolicy: 'cache-first' as RequestPolicy, }, } ); expect(client.executeQuery).toBeCalledTimes(1); expect(client.executeQuery).toHaveBeenNthCalledWith( 1, { key: expect.any(Number), query: expect.any(Object), variables: mockVariables, }, expect.objectContaining({ requestPolicy: 'cache-first', }) ); rerender({ query: mockQuery, variables: mockVariables, requestPolicy: 'network-only', }); expect(client.executeQuery).toBeCalledTimes(2); expect(client.executeQuery).toHaveBeenNthCalledWith( 2, { key: expect.any(Number), query: expect.any(Object), variables: mockVariables, }, expect.objectContaining({ requestPolicy: 'network-only', }) ); }); it('should provide an executeQuery function to be imperatively executed', async () => { const { result } = renderHook( ({ query, variables }) => useQuery({ query, variables }), { initialProps: { query: mockQuery, variables: mockVariables } } ); expect(client.executeQuery).toBeCalledTimes(1); const [, executeQuery] = result.current; act(() => executeQuery()); expect(client.executeQuery).toBeCalledTimes(2); }); it('should pause executing the query if pause is true', () => { renderHook( ({ query, variables, pause }) => useQuery({ query, variables, pause }), { initialProps: { query: mockQuery, variables: mockVariables, pause: true, }, } ); expect(client.executeQuery).not.toBeCalled(); }); it('should pause executing the query if pause updates to true', async () => { const { rerender } = renderHook( props => { const { query, variables, pause } = props; return useQuery({ query, variables, pause }); }, { initialProps: { query: mockQuery, variables: mockVariables, pause: false, }, } ); expect(client.executeQuery).toBeCalledTimes(1); rerender({ query: mockQuery, variables: mockVariables, pause: true }); expect(client.executeQuery).toBeCalledTimes(1); }); }); ================================================ FILE: packages/react-urql/src/hooks/useQuery.test.tsx ================================================ // @vitest-environment jsdom import { vi, expect, it, beforeEach, describe, Mock } from 'vitest'; // Note: Testing for hooks is not yet supported in Enzyme - https://github.com/airbnb/enzyme/issues/2011 vi.mock('../context', async () => { const { map, interval, pipe } = await vi.importActual('wonka'); const mock = { executeQuery: vi.fn(() => pipe( interval(400), map((i: number) => ({ data: i, error: i + 1, extensions: { i: 1 } })) ) ), }; return { useClient: () => mock, }; }); import React from 'react'; import renderer, { act } from 'react-test-renderer'; import { pipe, onStart, onEnd, never } from 'wonka'; import { OperationContext } from '@urql/core'; import { useQuery, UseQueryArgs, UseQueryState } from './useQuery'; import { useClient } from '../context'; // @ts-ignore const client = useClient() as { executeQuery: Mock }; const props: UseQueryArgs<{ myVar: number }> = { query: '{ example }', variables: { myVar: 1234, }, pause: false, }; let state: UseQueryState | undefined; let execute: ((_opts?: Partial) => void) | undefined; const QueryUser = ({ query, variables, pause, }: UseQueryArgs<{ myVar: number }>) => { const [s, e] = useQuery({ query, variables, pause }); state = s; execute = e; return

{s.data}

; }; beforeEach(() => { vi.useFakeTimers(); // TODO: Fix use of act() vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // do nothings }); client.executeQuery.mockClear(); state = undefined; execute = undefined; }); describe('on initial useEffect', () => { it('initialises default state', () => { renderer.create(); expect(state).toMatchSnapshot(); }); it('executes subscription', () => { renderer.create(); expect(client.executeQuery).toBeCalledTimes(1); }); it('passes query and vars to executeQuery', () => { renderer.create(); expect(client.executeQuery).toBeCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: props.variables, }, expect.objectContaining({ requestPolicy: undefined, }) ); }); }); describe('on subscription', () => { it('sets fetching to true', () => { const wrapper = renderer.create(); wrapper.update(); expect(state).toHaveProperty('fetching', true); }); }); describe('on subscription update', () => { it('forwards data response', () => { const wrapper = renderer.create(); /** * Have to call update (without changes) in order to see the * result of the state change. */ wrapper.update(); act(() => { vi.advanceTimersByTime(400); wrapper.update(); }); expect(state).toHaveProperty('data', 0); }); it('forwards error response', () => { const wrapper = renderer.create(); wrapper.update(); act(() => { vi.advanceTimersByTime(400); wrapper.update(); }); expect(state).toHaveProperty('error', 1); }); it('forwards extensions response', () => { const wrapper = renderer.create(); wrapper.update(); act(() => { vi.advanceTimersByTime(400); wrapper.update(); }); expect(state).toHaveProperty('extensions', { i: 1 }); }); it('sets fetching to false', () => { const wrapper = renderer.create(); wrapper.update(); act(() => { vi.advanceTimersByTime(400); wrapper.update(); }); expect(state).toHaveProperty('fetching', false); }); }); describe('on change', () => { const q = 'query NewQuery { example }'; it('new query executes subscription', () => { const wrapper = renderer.create(); wrapper.update(); act(() => { wrapper.update(); }); expect(client.executeQuery).toBeCalledTimes(2); }); }); describe('on unmount', () => { const start = vi.fn(); const unsubscribe = vi.fn(); beforeEach(() => { client.executeQuery.mockReturnValue( pipe(never, onStart(start), onEnd(unsubscribe)) ); }); it('unsubscribe is called', () => { const wrapper = renderer.create(); act(() => wrapper.unmount()); expect(start).toHaveBeenCalled(); expect(unsubscribe).toHaveBeenCalled(); }); }); describe('execute query', () => { it('triggers query execution', () => { renderer.create(); act(() => execute && execute()); expect(client.executeQuery).toBeCalledTimes(2); }); }); describe('pause', () => { it('skips executing the query if pause is true', () => { renderer.create(); expect(client.executeQuery).not.toBeCalled(); }); it('skips executing queries if pause updates to true', () => { const wrapper = renderer.create(); wrapper.update(); act(() => { wrapper.update(); }); expect(client.executeQuery).toBeCalledTimes(1); }); }); ================================================ FILE: packages/react-urql/src/hooks/useQuery.ts ================================================ /* eslint-disable react-hooks/exhaustive-deps */ import type { Source } from 'wonka'; import { pipe, subscribe, onEnd, onPush, takeWhile } from 'wonka'; import * as React from 'react'; import type { GraphQLRequestParams, AnyVariables, Client, CombinedError, OperationContext, RequestPolicy, OperationResult, Operation, } from '@urql/core'; import { useClient } from '../context'; import { useRequest } from './useRequest'; import { getCacheForClient } from './cache'; import { deferDispatch, initialState, computeNextState, hasDepsChanged, } from './state'; /** Input arguments for the {@link useQuery} hook. * * @param query - The GraphQL query that `useQuery` executes. * @param variables - The variables for the GraphQL query that `useQuery` executes. */ export type UseQueryArgs< Variables extends AnyVariables = AnyVariables, Data = any, > = { /** Updates the {@link RequestPolicy} for the executed GraphQL query operation. * * @remarks * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation * that `useQuery` executes, and indicates a caching strategy for cache exchanges. * * For example, when set to `'cache-and-network'`, {@link useQuery} will * receive a cached result with `stale: true` and an API request will be * sent in the background. * * @see {@link OperationContext.requestPolicy} for where this value is set. */ requestPolicy?: RequestPolicy; /** Updates the {@link OperationContext} for the executed GraphQL query operation. * * @remarks * `context` may be passed to {@link useQuery}, to update the {@link OperationContext} * of a query operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * Hint: This should be wrapped in a `useMemo` hook, to make sure that your * component doesn’t infinitely update. * * @example * ```ts * const [result, reexecute] = useQuery({ * query, * context: useMemo(() => ({ * additionalTypenames: ['Item'], * }), []) * }); * ``` */ context?: Partial; /** Prevents {@link useQuery} from automatically executing GraphQL query operations. * * @remarks * `pause` may be set to `true` to stop {@link useQuery} from executing * automatically. The hook will stop receiving updates from the {@link Client} * and won’t execute the query operation, until either it’s set to `false` * or the {@link UseQueryExecute} function is called. * * @see {@link https://urql.dev/goto/docs/basics/react-preact/#pausing-usequery} for * documentation on the `pause` option. */ pause?: boolean; } & GraphQLRequestParams; /** State of the current query, your {@link useQuery} hook is executing. * * @remarks * `UseQueryState` is returned (in a tuple) by {@link useQuery} and * gives you the updating {@link OperationResult} of GraphQL queries. * * Even when the query and variables passed to {@link useQuery} change, * this state preserves the prior state and sets the `fetching` flag to * `true`. * This allows you to display the previous state, while implementing * a separate loading indicator separately. */ export interface UseQueryState< Data = any, Variables extends AnyVariables = AnyVariables, > { /** Indicates whether `useQuery` is waiting for a new result. * * @remarks * When `useQuery` is passed a new query and/or variables, it will * start executing the new query operation and `fetching` is set to * `true` until a result arrives. * * Hint: This is subtly different than whether the query is actually * fetching, and doesn’t indicate whether a query is being re-executed * in the background. For this, see {@link UseQueryState.stale}. */ fetching: boolean; /** Indicates that the state is not fresh and a new result will follow. * * @remarks * The `stale` flag is set to `true` when a new result for the query * is expected and `useQuery` is waiting for it. This may indicate that * a new request is being requested in the background. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed query. */ data?: Data; /** The {@link OperationResult.error} for the executed query. */ error?: CombinedError; /** The {@link OperationResult.hasNext} for the executed query. */ hasNext: boolean; /** The {@link OperationResult.extensions} for the executed query. */ extensions?: Record; /** The {@link Operation} that the current state is for. * * @remarks * This is the {@link Operation} that is currently being executed. * When {@link UseQueryState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; } /** Triggers {@link useQuery} to execute a new GraphQL query operation. * * @param opts - optionally, context options that will be merged with the hook's * {@link UseQueryArgs.context} options and the `Client`’s options. * * @remarks * When called, {@link useQuery} will re-execute the GraphQL query operation * it currently holds, even if {@link UseQueryArgs.pause} is set to `true`. * * This is useful for executing a paused query or re-executing a query * and get a new network result, by passing a new request policy. * * ```ts * const [result, reexecuteQuery] = useQuery({ query }); * * const refresh = () => { * // Re-execute the query with a network-only policy, skipping the cache * reexecuteQuery({ requestPolicy: 'network-only' }); * }; * ``` */ export type UseQueryExecute = (opts?: Partial) => void; /** Result tuple returned by the {@link useQuery} hook. * * @remarks * Similarly to a `useState` hook’s return value, * the first element is the {@link useQuery}’s result and state, * a {@link UseQueryState} object, * and the second is used to imperatively re-execute the query * via a {@link UseQueryExecute} function. */ export type UseQueryResponse< Data = any, Variables extends AnyVariables = AnyVariables, > = [UseQueryState, UseQueryExecute]; const isSuspense = (client: Client, context?: Partial) => context && context.suspense !== undefined ? !!context.suspense : client.suspense; /** Hook to run a GraphQL query and get updated GraphQL results. * * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link UseQueryResponse} tuple of a {@link UseQueryState} result, and re-execute function. * * @remarks * `useQuery` allows GraphQL queries to be defined and executed. * Given {@link UseQueryArgs.query}, it executes the GraphQL query with the * context’s {@link Client}. * * The returned result updates when the `Client` has new results * for the query, and changes when your input `args` change. * * Additionally, if the `suspense` option is enabled on the `Client`, * the `useQuery` hook will suspend instead of indicating that it’s * waiting for a result via {@link UseQueryState.fetching}. * * @see {@link https://urql.dev/goto/urql/docs/basics/react-preact/#queries} for `useQuery` docs. * * @example * ```ts * import { gql, useQuery } from 'urql'; * * const TodosQuery = gql` * query { todos { id, title } } * `; * * const Todos = () => { * const [result, reexecuteQuery] = useQuery({ * query: TodosQuery, * variables: {}, * }); * // ... * }; * ``` */ export function useQuery< Data = any, Variables extends AnyVariables = AnyVariables, >(args: UseQueryArgs): UseQueryResponse { const client = useClient(); const cache = getCacheForClient(client); const suspense = isSuspense(client, args.context); const request = useRequest(args.query, args.variables as Variables); const source = React.useMemo(() => { if (args.pause) return null; const source = client.executeQuery(request, { requestPolicy: args.requestPolicy, ...args.context, }); return suspense ? pipe( source, onPush(result => { cache.set(request.key, result); }) ) : source; }, [ cache, client, request, suspense, args.pause, args.requestPolicy, args.context, ]); const getSnapshot = React.useCallback( ( source: Source> | null, suspense: boolean ): Partial> => { if (!source) return { fetching: false }; let result = cache.get(request.key); if (!result) { let resolve: (value: unknown) => void; const subscription = pipe( source, takeWhile( () => (suspense && !resolve) || !result || ('hasNext' in result && result.hasNext) ), subscribe(_result => { result = _result; if (resolve) resolve(result); }) ); if (result == null && suspense) { const promise = new Promise(_resolve => { resolve = _resolve; }); cache.set(request.key, promise); throw promise; } else { subscription.unsubscribe(); } } else if (suspense && result != null && 'then' in result) { throw result; } return (result as OperationResult) || { fetching: true }; }, [cache, request] ); const deps = [ client, request, args.requestPolicy, args.context, args.pause, ] as const; const [state, setState] = React.useState( () => [ source, computeNextState( initialState, deferDispatch(() => getSnapshot(source, suspense)) ), deps, ] as const ); let currentResult = state[1]; if (source !== state[0] && hasDepsChanged(state[2], deps)) { setState([ source, (currentResult = computeNextState( state[1], deferDispatch(() => getSnapshot(source, suspense)) )), deps, ]); } React.useEffect(() => { const source = state[0]; const request = state[2][1]; let hasResult = false; const updateResult = (result: Partial>) => { hasResult = true; deferDispatch(setState, state => { const nextResult = computeNextState(state[1], result); return state[1] !== nextResult ? [state[0], nextResult, state[2]] : state; }); }; if (source) { const subscription = pipe( source, onEnd(() => { updateResult({ fetching: false }); }), subscribe(updateResult) ); if (!hasResult) updateResult({ fetching: true }); return () => { cache.dispose(request.key); subscription.unsubscribe(); }; } else { updateResult({ fetching: false }); } }, [cache, state[0], state[2][1]]); const executeQuery = React.useCallback( (opts?: Partial) => { const context = { requestPolicy: args.requestPolicy, ...args.context, ...opts, }; deferDispatch(setState, state => { const source = suspense ? pipe( client.executeQuery(request, context), onPush(result => { cache.set(request.key, result); }) ) : client.executeQuery(request, context); return [source, state[1], deps]; }); }, [ client, cache, request, suspense, args.requestPolicy, args.context, args.pause, ] ); return [currentResult, executeQuery]; } ================================================ FILE: packages/react-urql/src/hooks/useRequest.test.ts ================================================ // @vitest-environment jsdom import { gql } from '@urql/core'; import { renderHook } from '@testing-library/react'; import { it, expect } from 'vitest'; import { useRequest } from './useRequest'; it('preserves instance of request when key has not changed', () => { const query = gql` query getUser($name: String) { user(name: $name) { id firstName lastName } } `; let variables = { name: 'Clara', }; const { result, rerender } = renderHook( ({ query, variables }) => useRequest(query, variables), { initialProps: { query, variables } } ); const resultA = result.current; expect(resultA).toEqual({ key: expect.any(Number), query: expect.anything(), variables: variables, }); variables = { ...variables }; // Change reference rerender({ query, variables }); const resultB = result.current; expect(resultA).toBe(resultB); variables = { ...variables, test: true } as any; // Change values rerender({ query, variables }); const resultC = result.current; expect(resultA).not.toBe(resultC); }); ================================================ FILE: packages/react-urql/src/hooks/useRequest.ts ================================================ import * as React from 'react'; import type { AnyVariables, DocumentInput, GraphQLRequest } from '@urql/core'; import { createRequest } from '@urql/core'; /** Creates a request from a query and variables but preserves reference equality if the key isn't changing * @internal */ export function useRequest< Data = any, Variables extends AnyVariables = AnyVariables, >( query: DocumentInput, variables: Variables ): GraphQLRequest { const prev = React.useRef>( undefined ); return React.useMemo(() => { const request = createRequest(query, variables); // We manually ensure reference equality if the key hasn't changed if (prev.current !== undefined && prev.current.key === request.key) { return prev.current; } else { prev.current = request; return request; } }, [query, variables]); } ================================================ FILE: packages/react-urql/src/hooks/useSubscription.test.tsx ================================================ // @vitest-environment jsdom import { vi, expect, it, beforeEach, describe, Mock } from 'vitest'; // Note: Testing for hooks is not yet supported in Enzyme - https://github.com/airbnb/enzyme/issues/2011 vi.mock('../context', async () => { const d = { data: 1234, error: 5678 }; const { merge, fromValue, never } = await vi.importActual('wonka'); const mock = { executeSubscription: vi.fn(() => merge([fromValue(d), never])), }; return { useClient: () => mock, }; }); import React from 'react'; import renderer, { act } from 'react-test-renderer'; import { OperationContext } from '@urql/core'; import { useSubscription, UseSubscriptionState } from './useSubscription'; import { useClient } from '../context'; // @ts-ignore const client = useClient() as { executeSubscription: Mock }; const query = 'subscription Example { example }'; let state: UseSubscriptionState | undefined; let execute: ((_opts?: Partial) => void) | undefined; const SubscriptionUser = ({ q, handler, context, pause = false, }: { q: string; handler?: (_prev: any, _data: any) => any; context?: Partial; pause?: boolean; }) => { [state, execute] = useSubscription({ query: q, context, pause }, handler); return

{state.data}

; }; beforeEach(() => { client.executeSubscription.mockClear(); state = undefined; }); describe('on initial useEffect', () => { it('initialises default state', () => { renderer.create(); expect(state).toMatchSnapshot(); }); it('executes subscription', () => { renderer.create(); expect(client.executeSubscription).toBeCalledTimes(1); }); }); it('should support setting context in useSubscription params', () => { const context = { url: 'test' }; act(() => { renderer.create(); }); expect(client.executeSubscription).toBeCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: {}, }, { url: 'test', } ); }); it('calls handler', () => { const handler = vi.fn(); const wrapper = renderer.create( ); wrapper.update(); expect(handler).toBeCalledWith(undefined, 1234); }); describe('execute subscription', () => { it('triggers subscription execution', () => { renderer.create(); act(() => execute && execute()); expect(client.executeSubscription).toBeCalledTimes(2); }); }); describe('pause', () => { const props = { q: query }; it('skips executing the query if pause is true', () => { renderer.create(); expect(client.executeSubscription).not.toBeCalled(); }); it('skips executing queries if pause updates to true', () => { const wrapper = renderer.create(); wrapper.update(); wrapper.update(); expect(client.executeSubscription).toBeCalledTimes(1); expect(state).toMatchObject({ fetching: false }); }); }); ================================================ FILE: packages/react-urql/src/hooks/useSubscription.ts ================================================ /* eslint-disable react-hooks/exhaustive-deps */ import { pipe, subscribe, onEnd } from 'wonka'; import * as React from 'react'; import type { GraphQLRequestParams, AnyVariables, CombinedError, OperationContext, Operation, } from '@urql/core'; import { useClient } from '../context'; import { useRequest } from './useRequest'; import { deferDispatch, initialState, computeNextState, hasDepsChanged, } from './state'; /** Input arguments for the {@link useSubscription} hook. * * @param query - The GraphQL subscription document that `useSubscription` executes. * @param variables - The variables for the GraphQL subscription that `useSubscription` executes. */ export type UseSubscriptionArgs< Variables extends AnyVariables = AnyVariables, Data = any, > = { /** Prevents {@link useSubscription} from automatically starting GraphQL subscriptions. * * @remarks * `pause` may be set to `true` to stop {@link useSubscription} from starting its subscription * automatically. The hook will stop receiving updates from the {@link Client} * and won’t start the subscription operation, until either it’s set to `false` * or the {@link UseSubscriptionExecute} function is called. */ pause?: boolean; /** Updates the {@link OperationContext} for the executed GraphQL subscription operation. * * @remarks * `context` may be passed to {@link useSubscription}, to update the {@link OperationContext} * of a subscription operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * Hint: This should be wrapped in a `useMemo` hook, to make sure that your * component doesn’t infinitely update. * * @example * ```ts * const [result, reexecute] = useSubscription({ * query, * context: useMemo(() => ({ * additionalTypenames: ['Item'], * }), []) * }); * ``` */ context?: Partial; } & GraphQLRequestParams; /** Combines previous data with an incoming subscription result’s data. * * @remarks * A `SubscriptionHandler` may be passed to {@link useSubscription} to * aggregate subscription results into a combined {@link UseSubscriptionState.data} * value. * * This is useful when a subscription event delivers a single item, while * you’d like to display a list of events. * * @example * ```ts * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * const combineNotifications = (notifications = [], data) => { * return [...notifications, data.newNotification]; * }; * * const [result, executeSubscription] = useSubscription( * { query: NotificationsSubscription }, * combineNotifications, * ); * ``` */ export type SubscriptionHandler = (prev: R | undefined, data: T) => R; /** State of the current subscription, your {@link useSubscription} hook is executing. * * @remarks * `UseSubscriptionState` is returned (in a tuple) by {@link useSubscription} and * gives you the updating {@link OperationResult} of GraphQL subscriptions. * * If a {@link SubscriptionHandler} has been passed to `useSubscription` then * {@link UseSubscriptionState.data} is instead the updated data as returned * by the handler, otherwise it’s the latest result’s data. * * Hint: Even when the query and variables passed to {@link useSubscription} change, * this state preserves the prior state. */ export interface UseSubscriptionState< Data = any, Variables extends AnyVariables = AnyVariables, > { /** Indicates whether `useSubscription`’s subscription is active. * * @remarks * When `useSubscription` starts a subscription, the `fetching` flag * is set to `true` and will remain `true` until the subscription * completes on the API, or the {@link UseSubscriptionArgs.pause} * flag is set to `true`. */ fetching: boolean; /** Indicates that the subscription result is not fresh. * * @remarks * This is mostly unused for subscriptions and will rarely affect you, and * is more relevant for queries. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed subscription, or data returned by a handler. * * @remarks * `data` will be set to the last {@link OperationResult.data} value * received for the subscription. * * It will instead be set to the values that {@link SubscriptionHandler} * returned, if a handler has been passed to {@link useSubscription}. */ data?: Data; /** The {@link OperationResult.error} for the executed subscription. */ error?: CombinedError; /** The {@link OperationResult.extensions} for the executed mutation. */ extensions?: Record; /** The {@link Operation} that the current state is for. * * @remarks * This is the subscription {@link Operation} that is currently active. * When {@link UseSubscriptionState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; } /** Triggers {@link useSubscription} to reexecute a GraphQL subscription operation. * * @param opts - optionally, context options that will be merged with the hook's * {@link UseSubscriptionArgs.context} options and the `Client`’s options. * * @remarks * When called, {@link useSubscription} will restart the GraphQL subscription * operation it currently holds. If {@link UseSubscriptionArgs.pause} is set * to `true`, it will start executing the subscription. * * ```ts * const [result, executeSubscription] = useSubscription({ * query, * pause: true, * }); * * const start = () => { * executeSubscription(); * }; * ``` */ export type UseSubscriptionExecute = (opts?: Partial) => void; /** Result tuple returned by the {@link useSubscription} hook. * * @remarks * Similarly to a `useState` hook’s return value, * the first element is the {@link useSubscription}’s state, * a {@link UseSubscriptionState} object, * and the second is used to imperatively re-execute or start the subscription * via a {@link UseMutationExecute} function. */ export type UseSubscriptionResponse< Data = any, Variables extends AnyVariables = AnyVariables, > = [UseSubscriptionState, UseSubscriptionExecute]; /** Hook to run a GraphQL subscription and get updated GraphQL results. * * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options. * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results. * @returns a {@link UseSubscriptionResponse} tuple of a {@link UseSubscriptionState} result, and an execute function. * * @remarks * `useSubscription` allows GraphQL subscriptions to be defined and executed. * Given {@link UseSubscriptionArgs.query}, it executes the GraphQL subscription with the * context’s {@link Client}. * * The returned result updates when the `Client` has new results * for the subscription, and `data` is updated with the result’s data * or with the `data` that a `handler` returns. * * @example * ```ts * import { gql, useSubscription } from 'urql'; * * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * const combineNotifications = (notifications = [], data) => { * return [...notifications, data.newNotification]; * }; * * const Notifications = () => { * const [result, executeSubscription] = useSubscription( * { query: NotificationsSubscription }, * combineNotifications, * ); * // ... * }; * ``` */ export function useSubscription< Data = any, Result = Data, Variables extends AnyVariables = AnyVariables, >( args: UseSubscriptionArgs, handler?: SubscriptionHandler ): UseSubscriptionResponse { const client = useClient(); const request = useRequest(args.query, args.variables as Variables); const handlerRef = React.useRef< SubscriptionHandler | undefined >(handler); handlerRef.current = handler; const source = React.useMemo( () => !args.pause ? client.executeSubscription(request, args.context) : null, [client, request, args.pause, args.context] ); const deps = [client, request, args.context, args.pause] as const; const [state, setState] = React.useState( () => [source, { ...initialState, fetching: !!source }, deps] as const ); let currentResult = state[1]; if (source !== state[0] && hasDepsChanged(state[2], deps)) { setState([ source, (currentResult = computeNextState(state[1], { fetching: !!source })), deps, ]); } React.useEffect(() => { const updateResult = ( result: Partial> ) => { deferDispatch(setState, state => { const nextResult = computeNextState(state[1], result); if (state[1] === nextResult) return state; if ( handlerRef.current && nextResult.data != null && state[1].data !== nextResult.data ) { nextResult.data = handlerRef.current( state[1].data, nextResult.data ) as any; } return [state[0], nextResult as any, state[2]]; }); }; if (state[0]) { return pipe( state[0], onEnd(() => { updateResult({ fetching: !!source }); }), subscribe(updateResult) ).unsubscribe; } else { updateResult({ fetching: false }); } }, [state[0]]); // This is the imperative execute function passed to the user const executeSubscription = React.useCallback( (opts?: Partial) => { const source = client.executeSubscription(request, { ...args.context, ...opts, }); deferDispatch(setState, state => [source, state[1], deps]); }, [client, request, args.context, args.pause] ); return [currentResult, executeSubscription]; } ================================================ FILE: packages/react-urql/src/index.ts ================================================ export * from '@urql/core'; export * from './context'; export * from './components'; export * from './hooks'; ================================================ FILE: packages/react-urql/src/test-utils/ssr.test.tsx ================================================ import React from 'react'; import prepass from 'react-ssr-prepass'; import { never, publish, filter, delay, pipe, map } from 'wonka'; import { describe, it, beforeEach, expect } from 'vitest'; import { gql, Client, Exchange, cacheExchange, ssrExchange, OperationContext, GraphQLRequest, Operation, OperationResult, makeOperation, } from '@urql/core'; import { Provider } from '../context'; import { useQuery } from '../hooks'; const context: OperationContext = { fetchOptions: { method: 'POST', }, requestPolicy: 'cache-first', url: 'http://localhost:3000/graphql', suspense: true, }; export const queryGql: GraphQLRequest = { key: 2, query: gql` query getUser($name: String) { user(name: $name) { id firstName lastName } } `, variables: { name: 'Clara', }, }; const teardownOperation: Operation = makeOperation( 'teardown', { query: queryGql.query, variables: queryGql.variables, key: queryGql.key, }, context ); const queryOperation: Operation = makeOperation( 'query', { query: teardownOperation.query, variables: teardownOperation.variables, key: teardownOperation.key, }, context ); const queryResponse: OperationResult = { operation: queryOperation, data: { user: { name: 'Clive', }, }, stale: false, hasNext: false, }; const url = 'https://hostname.com'; describe('server-side rendering', () => { let ssr; let client: Client; beforeEach(() => { const fetchExchange: Exchange = () => ops$ => { return pipe( ops$, filter(x => x.kind === 'query'), delay(100), map(operation => ({ ...queryResponse, operation })) ); }; ssr = ssrExchange(); client = new Client({ url, // We include the SSR exchange after the cache exchanges: [cacheExchange, ssr, fetchExchange], suspense: true, }); }); it('works for an actual component tree', async () => { const Query = () => { useQuery({ query: queryOperation.query, variables: queryOperation.variables, }); return null; }; const Element = Provider as any; const App = () => ( ); await prepass(); const data = ssr.extractData(); expect(Object.keys(data).length).toBe(1); }); }); describe('client-side rehydration', () => { let ssr; let client; beforeEach(() => { const fetchExchange: Exchange = () => () => never as any; ssr = ssrExchange(); client = new Client({ url, // We include the SSR exchange after the cache exchanges: [cacheExchange, ssr, fetchExchange], suspense: false, }); }); it('can rehydrate results on the client', async () => { ssr.restoreData({ [queryOperation.key]: { ...queryResponse, data: JSON.stringify(queryResponse.data), }, }); expect(() => { pipe(client.executeRequestOperation(queryOperation), publish); }).not.toThrow(); await Promise.resolve(); const data = ssr.extractData(); expect(Object.keys(data).length).toBe(0); }); }); ================================================ FILE: packages/react-urql/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/react-urql/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, { test: { environment: 'jsdom', }, }); ================================================ FILE: packages/site/CHANGELOG.md ================================================ # urql-docs ## 1.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/urql-graphql/urql/pull/2504)) ================================================ FILE: packages/site/package.json ================================================ { "name": "urql-docs", "version": "1.0.0", "description": "Documentation site for urql", "private": true, "scripts": { "start": "react-static start", "build": "NODE_OPTIONS=--openssl-legacy-provider react-static build", "lint": "eslint --ext=js,jsx .", "clean": "rimraf dist", "prepublishOnly": "run-s clean build", "stage:build": "pnpm build --staging", "prod:build": "pnpm build" }, "babel": { "presets": [ "react-static/babel-preset" ], "plugins": [ "babel-plugin-styled-components" ] }, "dependencies": { "@babel/runtime": "^7.20.1", "@mdx-js/react": "^1.6.22", "formidable-oss-badges": "0.3.5", "fuse.js": "^6.4.6", "history": "^4.7.2", "path": "^0.12.7", "preact": "^10.5.13", "prism-react-renderer": "^1.1.0", "prop-types": "^15.6.2", "react": "^17.0.2", "react-dom": "^17.0.2", "react-ga": "^3.3.0", "react-gtm-module": "^2.0.11", "react-inlinesvg": "^1.2.0", "react-is": "^17.0.2", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-router-ga": "^1.2.3", "react-scroll": "^1.8.1", "react-static": "7.3.0", "react-static-plugin-md-pages": "^0.3.3", "styled-components": "^5.2.3" }, "devDependencies": { "@babel/core": "^7.2.0", "@mdx-js/mdx": "^1.5.7", "@octokit/plugin-request-log": "1.0.0", "babel-plugin-universal-import": "^3.1.3", "lodash": "^4.17.19", "react-hot-loader": "^4.12.20", "react-static-plugin-sitemap": "7.2.2", "react-static-plugin-styled-components": "7.2.2", "resolve-from": "^3.0.0", "surge": "^0.21.3", "webpack": ">=4.4.6" }, "engines": { "node": ">=18.0.0" } } ================================================ FILE: packages/site/plugins/assets-fix/node.api.js ================================================ export default () => ({ webpack(config) { const rules = config.module.rules[0].oneOf; for (let i = 0; i < rules.length; i++) { const rule = rules[i]; if (rule.loader === 'url-loader') { delete rule.options; rule.query = { limit: 10000, name: 'static/[name].[hash:8].[ext]', }; } } return config; }, }); ================================================ FILE: packages/site/plugins/monorepo-fix/node.api.js ================================================ import { silent as resolveFrom } from 'resolve-from'; const NODE_MODULES_JS_RE = /node_modules[/\\].*\.js$/; const REACT_STATIC_RE = /node_modules[/\\]react-static/; export default () => ({ webpack: (config, { stage }) => { if (stage === 'node') { config.externals = [ ...config.externals, (context, request, callback) => { if (/^[./]/.test(request)) { return callback(); } const res = resolveFrom(`${context}/`, request); if ( res && NODE_MODULES_JS_RE.test(res) && !REACT_STATIC_RE.test(res) ) { return callback(null, `commonjs ${request}`); } else { return callback(); } }, ]; } return config; }, }); ================================================ FILE: packages/site/plugins/preact/node.api.js ================================================ export default () => ({ webpack: config => { config.resolve.alias = { ...(config.resolve.alias || {}), react: 'preact/compat', 'react-dom': 'preact/compat', }; return config; }, }); ================================================ FILE: packages/site/plugins/react-router/browser.api.js ================================================ /* eslint-disable react/display-name */ /* eslint-disable react-hooks/rules-of-hooks */ import React from 'react'; import { useBasepath, useStaticInfo } from 'react-static'; import { BrowserRouter, StaticRouter, withRouter } from 'react-router-dom'; const Location = withRouter(({ children, location }) => children(location)); const ReactRouterPlugin = ({ RouterProps: userRouterProps = {} }) => ({ Root: PreviousRoot => ({ children }) => { const routerProps = { basename: useBasepath() || '' }; if (routerProps.basename) routerProps.basename = `/${routerProps.basename}`; const staticInfo = useStaticInfo(); // Test for document to detect the node stage let Router; if (typeof document !== 'undefined') { // NOTE: React Router is inconsistent in how it handles base paths // This will need a trailing slash while the StaticRouter does not if (routerProps.basename) routerProps.basename += '/'; // If in the browser, just use the browser router Router = BrowserRouter; } else { Router = StaticRouter; routerProps.location = staticInfo.path; // Required routerProps.context = {}; // Required } return ( {children} ); }, Routes: PreviousRoutes => props => ( {location => } ), }); export default ReactRouterPlugin; ================================================ FILE: packages/site/public/browserconfig.xml ================================================ #fff ================================================ FILE: packages/site/public/site.webmanifest ================================================ { "name": "urql Documentation", "short_name": "urql", "icons": [ { "src": "./favicon/favicon-32.png", "sizes": "16x16", "type": "image/png" }, { "src": "./favicon/favicon-32.png", "sizes": "32x32", "type": "image/png" }, { "src": "./favicon/favicon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "./favicon/favicon-32.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" } ================================================ FILE: packages/site/src/analytics.js ================================================ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; let GoogleAnalytics = null; if (typeof window !== 'undefined') { GoogleAnalytics = require('react-router-ga'); if (GoogleAnalytics.default) GoogleAnalytics = GoogleAnalytics.default; } export const Analytics = props => !GoogleAnalytics ? ( ) : ( {props.children} ); Analytics.propTypes = { children: PropTypes.element, }; ================================================ FILE: packages/site/src/app.js ================================================ // eslint-disable-next-line react/no-multi-comp import React, { useEffect } from 'react'; import { Root, Routes } from 'react-static'; import { ThemeProvider } from 'styled-components'; import constants from './constants'; import { GlobalStyle } from './styles/global'; import * as theme from './styles/theme'; import Analytics from './google-analytics'; import { initGoogleTagManager } from './google-tag-manager'; import { Loading } from './components/loading'; const App = () => { useEffect(() => { initGoogleTagManager(); }, []); return ( }> ); }; export default App; ================================================ FILE: packages/site/src/assets/anchor.js ================================================ import React from 'react'; const SvgAnchor = props => ( ); export default SvgAnchor; ================================================ FILE: packages/site/src/assets/chevron.js ================================================ import React from 'react'; const SvgChevron = props => ( ); export default SvgChevron; ================================================ FILE: packages/site/src/components/body-copy.js ================================================ import styled from 'styled-components'; export const BodyCopy = styled.p` font-size: 1.4rem; line-height: 2.2rem; width: 100%; text-align: center; ${p => p.noMargin && 'margin: 0'}; @media (min-width: 768px) { font-size: 1.5rem; line-height: 2.4rem; text-align: left; } `; ================================================ FILE: packages/site/src/components/button.js ================================================ import React from 'react'; import styled, { css } from 'styled-components'; export const buttonLinkStyling = css` background: white; color: #383838; font-weight: normal; font-size: 1.4rem; font-style: normal; font-stretch: normal; height: 4rem; line-height: 4rem; padding: 0 2rem; letter-spacing: 0.01rem; text-align: center; text-transform: uppercase; transition: opacity 0.4s ease-out; &:hover { opacity: 0.8; } &:active { opacity: 0.6; } `; const ButtonNoBorder = styled.button` border: none; `; export const Button = styled(props => ( {props.children} ))` ${buttonLinkStyling} `; ================================================ FILE: packages/site/src/components/footer.js ================================================ import React from 'react'; import { Wrapper } from './wrapper'; import styled from 'styled-components'; import logoFormidableWhite from '../assets/logo_formidable_white.svg'; const Container = styled.footer` background: #1f1f1f; color: white; display: flex; flex-direction: column; height: auto; padding: 9rem 0; align-items: center; `; const FooterDescription = styled.p` flex: 2; font-size: 1.4rem; line-height: 1.6; margin: 2rem 0 0; max-width: 56rem; text-align: left; @media (min-width: 768px) { font-size: 1.5rem; margin: 0; min-width: auto; } & a { color: white; transition: opacity 0.4s; } & a:hover { opacity: 0.7; } & a:visited { color: white; } `; const FooterLeft = styled.div` display: flex; flex: 1; padding: 0 4rem 0 0; text-align: left; `; const FooterLogo = styled.img` width: 100px; `; const FooterLinks = styled.ul` font-size: 1.4rem; list-style: none; padding: 0px 8px; text-transform: uppercase; & li { margin: 0.2rem 0; } & a { color: white; letter-spacing: 0.05em; transition: opacity 0.4s; } & a:hover { opacity: 0.7; } & a:visited { color: white; } `; export const Footer = () => (
  • Contact
  • Careers
  • Formidable is a global design and engineering consultancy and open source software organization, specializing in React.js, React Native, GraphQL, Node.js, and the extended JavaScript ecosystem. We have locations in Seattle, London, Toronto, Denver, and Phoenix with remote consultants worldwide. For more information please visit{' '} formidable.com.
    ); ================================================ FILE: packages/site/src/components/header.js ================================================ import React from 'react'; import styled from 'styled-components'; import PropTypes from 'prop-types'; import Hero from '../screens/home/hero'; import logoFormidableWhite from '../assets/logo_formidable_white.svg'; import LeftTriangles from '../assets/left-triangles.svg'; import RightTriangles from '../assets/right-triangles.svg'; const Container = styled.header` background: rgb(109, 117, 153); background: linear-gradient( 225deg, rgba(109, 117, 153, 1) 0%, rgba(41, 45, 55, 1) 100% ); background-size: 100% 100%; color: white; height: 100%; padding: 0 0 4rem; width: 100%; display: flex; justify-content: center; position: relative; overflow: hidden; `; const HeaderContainer = styled.a` display: flex; position: absolute; left: 0.25rem; top: 0.25rem; width: 12rem; flex-direction: column; color: #ffffff; text-decoration: none; z-index: 2; `; const HeaderText = styled.p` text-transform: uppercase; font-size: 1.5rem; margin-left: 14px; line-height: 1.9rem; margin-bottom: 0; `; const HeaderLogo = styled.img` width: 70px; z-index: 1; `; const LeftTrianglesImg = styled.img` position: absolute; display: block; left: 0; top: 0; height: 80%; max-width: none; `; const RightTrianglesImg = styled.img` position: absolute; right: 0; bottom: 0; display: none; height: 45%; @media (min-width: 768px) { display: block; } `; export const Header = ({ content }) => ( Another oss project by ); Header.propTypes = { content: PropTypes.shape({ hero: PropTypes.shape({ copyText: PropTypes.string }), }).isRequired, }; ================================================ FILE: packages/site/src/components/link.js ================================================ import React from 'react'; import styled from 'styled-components'; import { Link as ReactRouterLink } from 'react-router-dom'; import { buttonLinkStyling } from './button'; export const Link = styled(({ isExternal, ...rest }) => isExternal ? ( {rest.children} ) : ( ) )` ${buttonLinkStyling} `; ================================================ FILE: packages/site/src/components/loading.js ================================================ import React from 'react'; import styled, { keyframes } from 'styled-components'; import Docs from '../screens/docs'; const Container = styled.div` height: 100vh; width: 100%; `; const Loader = styled.div` position: relative; margin: 0 auto; width: ${p => p.theme.spacing.xl}; top: calc(50% - ${p => p.theme.spacing.xl}); &:before { content: ''; display: block; padding-top: 100%; } `; const rotate = keyframes` 100% { transform: rotate(360deg); } `; const dash = keyframes` 0% { stroke-dasharray: 1, 200; stroke-dashoffset: 0; } 50% { stroke-dasharray: 89, 200; stroke-dashoffset: -35px; } 100% { stroke-dasharray: 89, 200; stroke-dashoffset: -124px; } `; const Svg = styled.svg` animation: ${rotate} 2s linear infinite; height: 100%; transform-origin: center center; width: 100%; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; `; const Circle = styled.circle` stroke: ${p => p.theme.colors.accent}; stroke-dasharray: 1, 200; stroke-dashoffset: 0; animation: ${dash} 1.5s ease-in-out infinite; stroke-linecap: round; `; export const Loading = () => ( ); ================================================ FILE: packages/site/src/components/markdown.js ================================================ import styled from 'styled-components'; export const Markdown = styled.article` width: 60vw; @media (max-width: 768px) { width: 75vw; } h1 { font-size: 3.4rem; margin: 0 0 2rem; @media (min-width: 1024px) { font-size: 4.8rem; } @media (max-width: 768px) { margin: 6rem 0 2rem; } } h2 { font-size: 2.8rem; margin: 6rem 0 2rem; @media (min-width: 1024px) { font-size: 2.5rem; } } h3 { font-size: 1.8rem; margin: 2rem 0; @media (min-width: 1024px) { font-size: 2rem; } } table { border-collapse: collapse; } td { height: 50px; text-align: left; } td, th { padding: 15px; } th { text-align: center; } table, th, td { font-size: 1.7rem; border: 1px solid lightgrey; tr:nth-child(even) { background-color: #f2f2f2; } } pre { background-color: #efefef; padding: 2rem; color: #333; } pre > code { color: #333; } p { font-size: 1.7rem; line-height: 1.3; } p code { border: 1px solid lightgrey; opacity: 0.8; padding: 0.5rem; font-size: 1.5rem; margin: 0 0.5rem 0 0.5rem; } blockquote { margin: 0 0.2rem; padding: 0 1.8rem; border-left: 3px solid #255db0; } li { font-size: 1.7rem; line-height: 1.3; padding: 0.5rem; @media (max-width: 768px) { margin-left: -2rem; } } li code { border: 1px solid lightgrey; opacity: 0.8; padding: 0.5rem; font-size: 1.5rem; margin: 0 0.5rem 0 0.5rem; } a { color: #895160; &:target { display: block; position: relative; top: -60px; visibility: hidden; } &:hover { color: black; } } `; ================================================ FILE: packages/site/src/components/mdx.js ================================================ import React from 'react'; import styled, { css } from 'styled-components'; import { MDXProvider } from '@mdx-js/react'; import { Link } from 'react-router-dom'; import Highlight, { Prism } from 'prism-react-renderer'; import nightOwlLight from 'prism-react-renderer/themes/nightOwlLight'; import AnchorSvg from '../assets/anchor'; const getLanguage = className => { const res = className.match(/language-(\w+)/); return res ? res[1] : null; }; const Pre = styled.pre` background: ${p => p.theme.colors.codeBg}; border: 1px solid ${p => p.theme.colors.border}; border-radius: ${p => p.theme.spacing.xs}; font-size: ${p => p.theme.fontSizes.code}; line-height: ${p => p.theme.lineHeights.code}; max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; padding: ${p => p.theme.spacing.sm}; position: relative; white-space: pre; `; const Code = styled.code` display: block; font-family: ${p => p.theme.fonts.code}; color: ${p => p.theme.colors.code}; font-variant-ligatures: none; font-feature-settings: normal; white-space: pre; hyphens: initial; `; const InlineCode = styled(props => { const children = props.children.replace(/\\\|/g, '|'); return {children}; })` background: ${p => p.theme.colors.codeBg}; color: ${p => p.theme.colors.code}; font-family: ${p => p.theme.fonts.code}; font-size: ${p => p.theme.fontSizes.small}; border-radius: ${p => p.theme.spacing.xs}; display: inline-block; vertical-align: baseline; font-variant-ligatures: none; font-feature-settings: normal; padding: 0 0.2em; margin: 0; a > & { text-decoration: underline; } `; const InlineImage = styled.img` display: inline-block; margin: 0 ${p => p.theme.spacing.sm} ${p => p.theme.spacing.md} 0; padding: ${p => p.theme.spacing.xs} ${p => p.theme.spacing.sm}; border: 1px solid ${p => p.theme.colors.border}; border-radius: ${p => p.theme.spacing.xs}; `; const ImageWrapper = styled.div` margin: ${p => p.theme.spacing.md} 0; border: 1px solid ${p => p.theme.colors.border}; border-radius: ${p => p.theme.spacing.xs}; background: ${p => p.theme.colors.bg}; display: flex; flex-direction: column; & > img { padding: ${p => p.theme.spacing.md}; align-self: center; max-height: 40vh; } `; const ImageAlt = styled.span.attrs(() => ({ 'aria-hidden': true, // This is just duplicating alt }))` display: block; padding: ${p => p.theme.spacing.xs} ${p => p.theme.spacing.sm}; border-top: 1px solid ${p => p.theme.colors.border}; background: ${p => p.theme.colors.codeBg}; font-size: ${p => p.theme.fontSizes.small}; `; const Image = props => { const { height, width, alt, src } = props; if (height || width) return ; return ( {alt} {alt} ); }; const HighlightCode = ({ className = '', children }) => { const language = getLanguage(className); return ( {({ className, style, tokens, getLineProps, getTokenProps }) => ( {tokens.map((line, i) => (
    {line.map((token, key) => ( ))}
    ))}
    )}
    ); }; const Blockquote = styled.blockquote` margin: ${p => p.theme.spacing.md} 0; padding: 0 0 0 ${p => p.theme.spacing.md}; border-left: 0.5rem solid ${p => p.theme.colors.border}; font-size: ${p => p.theme.fontSizes.small}; & > * { margin: ${p => p.theme.spacing.sm} 0; } `; const sharedTableCellStyling = css` padding: ${p => p.theme.spacing.xs} ${p => p.theme.spacing.sm}; border-left: 1px solid ${p => p.theme.colors.passiveBg}; border-bottom: 1px solid ${p => p.theme.colors.passiveBg}; & > ${InlineCode} { white-space: pre-wrap; display: inline; } `; const TableHeader = styled.th` text-align: left; white-space: nowrap; ${sharedTableCellStyling} `; const TableCell = styled.td` ${sharedTableCellStyling} ${p => { const isCodeOnly = React.Children.toArray(p.children).every( x => x.props && x.props.mdxType === 'inlineCode' ); return ( isCodeOnly && css` background-color: ${p.theme.colors.codeBg}; && > ${InlineCode} { background: none; padding: 0; margin: 0; white-space: pre; display: block; } ` ); }} &:first-child { width: min-content; min-width: 25rem; } @media ${p => p.theme.media.md} { &:not(:first-child) { overflow-wrap: break-word; } } `; const TableScrollContainer = styled.div` overflow-x: auto; @media ${p => p.theme.media.maxmd} { overflow-x: scroll; -webkit-overflow-scrolling: touch; } `; const Table = styled.table` border: 1px solid ${p => p.theme.colors.passiveBg}; border-collapse: collapse; overflow-x: auto; @media ${p => p.theme.media.maxmd} { overflow-x: scroll; overflow-wrap: initial; word-wrap: initial; word-break: initial; hyphens: initial; } `; const TableScroll = props => ( ); const MdLink = ({ href, children }) => { if (!/^\w+:/.test(href) && !href.startsWith('#')) { return {children}; } return ( {children} ); }; const HeadingText = styled.h1` &:target:before { content: ''; display: block; height: 1.5em; margin: -1.5em 0 0; } `; const AnchorLink = styled.a` display: inline-block; color: ${p => p.theme.colors.accent}; padding-right: 0.5rem; width: 2rem; @media ${({ theme }) => theme.media.sm} { margin-left: -2rem; display: none; ${HeadingText}:hover > & { display: inline-block; } } `; const AnchorIcon = styled(AnchorSvg)` height: 100%; `; const Header = tag => { const HeaderComponent = ({ id, children }) => ( {children} ); HeaderComponent.displayName = `Header(${tag})`; return HeaderComponent; }; const components = { pre: Pre, img: Image, blockquote: Blockquote, inlineCode: InlineCode, code: HighlightCode, table: TableScroll, th: TableHeader, td: TableCell, a: MdLink, h1: HeadingText, h2: Header('h2'), h3: Header('h3'), }; export const MDXComponents = ({ children }) => ( {children} ); ================================================ FILE: packages/site/src/components/navigation.js ================================================ import styled from 'styled-components'; import { NavLink } from 'react-router-dom'; import ChevronIcon from '../assets/chevron'; export const SidebarContainer = styled.div` display: ${p => (p.hidden ? 'none' : 'block')}; position: absolute; left: 0; right: 0; bottom: 0; min-height: 100%; width: ${p => p.theme.layout.sidebar}; @media ${({ theme }) => theme.media.sm} { display: block; position: relative; margin-left: calc(2 * ${p => p.theme.layout.stripes}); } `; export const SideBarStripes = styled.div` border-left: ${p => p.theme.layout.stripes} solid #8196ff; border-right: ${p => p.theme.layout.stripes} solid #bcc6fa; position: absolute; height: 100%; width: 0; left: 0; top: 0; bottom: 0; `; export const SidebarWrapper = styled.aside` position: fixed; bottom: 0; top: ${p => p.theme.layout.header}; -webkit-overflow-scrolling: touch; overflow-y: scroll; display: flex; flex-direction: column; z-index: 1; overflow-y: scroll; min-height: 100%; line-height: ${p => p.theme.lineHeights.body}; font-size: ${p => p.theme.fontSizes.small}; padding: ${p => p.theme.spacing.sm} ${p => p.theme.spacing.md}; background-color: ${p => p.theme.colors.bg}; border-right: 1px solid ${p => p.theme.colors.border}; border-top: 1px solid ${p => p.theme.colors.border}; width: ${p => p.theme.layout.sidebar}; @media ${({ theme }) => theme.media.sm} { border: none; background: none; padding-top: ${p => p.theme.spacing.md}; } `; export const SidebarNavItem = styled(NavLink).attrs(() => ({ activeClassName: 'active', }))` display: block; margin: ${p => p.theme.spacing.xs} 0; color: ${p => p.theme.colors.text}; font-weight: ${p => p.theme.fontWeights.heading}; text-decoration: none; width: 100%; &:hover { color: ${p => p.theme.colors.accent}; } &.active { color: ${p => p.theme.colors.accent}; } `; export const ChevronItem = styled(ChevronIcon).attrs(() => ({ 'aria-hidden': 'true', }))` display: inline-block; color: inherit; vertical-align: baseline; margin-top: 0.08em; margin-left: 0.3em; padding: 0.08em; width: 1em; height: 1em; position: relative; top: 0.16em; ${SidebarNavItem}.active & { transform: rotate(180deg); } `; export const SidebarNavSubItemWrapper = styled.div` padding-left: ${p => p.theme.spacing.sm}; margin-bottom: ${p => p.theme.spacing.xs}; `; export const SidebarNavSubItem = styled(NavLink).attrs(() => ({}))` display: block; color: ${p => p.theme.colors.passive}; font-weight: ${p => p.theme.fontWeights.body}; text-decoration: none; margin: ${p => `${p.theme.spacing.xs} 0 0 ${p.nested ? p.theme.spacing.sm : 0}`}; &:first-child { margin-top: 0; } &:hover { color: ${p => p.theme.colors.accent}; } &.active { color: ${p => p.theme.colors.accent}; font-weight: ${p => p.theme.fontWeights.heading}; } `; ================================================ FILE: packages/site/src/components/panel.js ================================================ import React from 'react'; import styled, { css } from 'styled-components'; import constants from '../constants'; const dark = css` background-color: #0d1129; `; const light = css` background: ${constants.color}; border-bottom: 1rem solid rgba(0, 0, 0, 0.4); box-shadow: inset 0 -1rem 0 rgba(0, 0, 0, 0.2); `; export const FullWidthContainer = styled.div` color: #e3eef8; display: flex; justify-content: center; ${p => (p.isLight ? light : dark)}; ${p => p.background && `background: ${p.background}`} `; export const SectionWrapper = styled.div` flex-direction: column; align-items: center; display: flex; padding: 8rem 4rem; width: 100%; @media (min-width: 768px) { flex-direction: column; margin: 0 8rem; padding: 8rem 8rem; } `; export const PanelSectionWrapper = ({ children, isLight, background, ...rest }) => ( {children} ); ================================================ FILE: packages/site/src/components/scroll-to-top.js ================================================ import React, { useEffect, useRef } from 'react'; import { useLocation } from 'react-router-dom'; import { useMarkdownPage } from 'react-static-plugin-md-pages'; const parsePathname = pathname => { const match = pathname && pathname.match(/#[a-z|-]+/); return match && match[0]; }; export const ScrollToTop = () => { const inputRef = useRef(null); const location = useLocation(); const md = useMarkdownPage(); const hash = location.hash || parsePathname(location.pathname); useEffect(() => { if (hash && md) { inputRef.current.click(); } else { window.scrollTo(0, 0); } }, [hash, md]); return ; }; ================================================ FILE: packages/site/src/components/secondary-title.js ================================================ import styled from 'styled-components'; export const SecondaryTitle = styled.h3` color: white; font-size: 2rem; line-height: 2.4rem; margin: 2rem auto 1rem; text-align: center; @media (min-width: 768px) { font-size: 2.2rem; line-height: 2.6rem; } `; ================================================ FILE: packages/site/src/components/section-title.js ================================================ import styled from 'styled-components'; export const SectionTitle = styled.h2` color: #fff; font-size: 3.5rem; flex: auto; line-height: 1.3; margin: 0 0 3rem; width: 100%; text-align: center; @media (min-width: 768px) { font-size: 4.5rem; margin: 0 0 6rem; } `; ================================================ FILE: packages/site/src/components/sidebar-search-input.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; const StyledInput = styled.input` background-color: rgba(255, 255, 255, 0.8); border: none; border-radius: 0.5rem; color: ${p => p.theme.colors.text}; font-family: ${p => p.theme.fonts.body}; font-size: 1.6rem; line-height: 2.3rem; letter-spacing: -0.6px; margin: ${p => `${p.theme.spacing.sm} 0 ${p.theme.spacing.sm} calc(${p.theme.spacing.xs} * -1.5)`}; padding: ${p => `${p.theme.spacing.xs} calc(${p.theme.spacing.xs} * 1.5) ${p.theme.spacing.xs}`}; width: calc(100% + 1.8rem); background-color: ${p => p.theme.colors.passiveBg}; @media ${p => p.theme.media.sm} { background-color: ${p => p.theme.colors.bg}; } `; const SidebarSearchInput = ({ value, onHandleInputChange }) => ( ); SidebarSearchInput.propTypes = { value: PropTypes.string, onHandleInputChange: PropTypes.func, }; export default SidebarSearchInput; ================================================ FILE: packages/site/src/components/sidebar.js ================================================ /* eslint-disable react-hooks/rules-of-hooks */ import React, { Fragment, useMemo, useState } from 'react'; import styled from 'styled-components'; import Fuse from 'fuse.js'; import { Link, useLocation } from 'react-router-dom'; import { useMarkdownTree } from 'react-static-plugin-md-pages'; import { SidebarNavItem, SidebarNavSubItem, SidebarNavSubItemWrapper, SidebarContainer, SidebarWrapper, SideBarStripes, ChevronItem, } from './navigation'; import SidebarSearchInput from './sidebar-search-input'; import logoSidebar from '../assets/sidebar-badge.svg'; const HeroLogoLink = styled(Link)` display: none; flex-direction: row; justify-content: center; margin-bottom: ${p => p.theme.spacing.sm}; align-self: center; @media ${p => p.theme.media.sm} { display: flex; } `; const HeroLogo = styled.img.attrs(() => ({ src: logoSidebar, alt: 'urql', }))` width: ${p => p.theme.layout.logo}; height: ${p => p.theme.layout.logo}; `; const ContentWrapper = styled.div` display: flex; flex-direction: column; padding-bottom: ${p => p.theme.spacing.lg}; `; export const SidebarStyling = ({ children, sidebarOpen }) => ( <> ); const getMatchTree = (() => { const sortByRefIndex = (a, b) => a.refIndex - b.refIndex; const options = { distance: 100, findAllMatches: true, includeMatches: true, keys: [ 'frontmatter.title', `children.frontmatter.title`, 'children.headings.value', ], threshold: 0.2, }; return (children, pattern) => { // Filter any nested heading with a depth greater than 2 const childrenMaxH3 = children.map(child => ({ ...child, children: child.children && child.children.map(child => ({ ...child, headings: child.headings.filter(heading => heading.depth == 2), })), })); const fuse = new Fuse(childrenMaxH3, options); let matches = fuse.search(pattern); // For every matching section, include only matching headers return matches .reduce((matches, match) => { const matchesMap = new Map(); match.matches.forEach(individualMatch => { matchesMap.set(individualMatch.value, { indices: individualMatch.indices, }); }); // Add the top level heading but don't add subheadings unless they match const currentItem = { ...match.item, matchedIndices: match.indices, refIndex: match.refIndex, }; // For every child of the currently matched section, add all appplicable // H2 and H3 headers plus their indices if (currentItem.children) { currentItem.children = currentItem.children.reduce( (children, child) => { const newChild = { ...child }; newChild.headings = newChild.headings.reduce( (headings, header) => { const match = matchesMap.get(header.value); if (match) { headings.push({ ...header, matchedIndices: match.indices, }); } return headings; }, [] ); const match = matchesMap.get(newChild.frontmatter.title); if (match) { newChild.matchedIndices = match.indices; } if (match || newChild.headings.length > 0) { children.push(newChild); } return children; }, [] ); } return [...matches, currentItem]; }, []) .sort(sortByRefIndex); }; })(); // Wrap matching substrings in const highlightText = (text, indices) => ( <> {indices.map(([startIndex, endIndex], i) => { const isLastIndex = !indices[i + 1]; const prevEndIndex = indices[i - 1] ? indices[i - 1][1] : -1; return ( <> {startIndex != 0 ? text.slice(prevEndIndex + 1, startIndex) : ''} {text.slice(startIndex, endIndex + 1)} {isLastIndex && endIndex < text.length ? text.slice(endIndex + 1, text.length) : ''} ); })} ); const Sidebar = ({ closeSidebar, ...props }) => { const [filterTerm, setFilterTerm] = useState(''); const location = useLocation(); const tree = useMarkdownTree(); const sidebarItems = useMemo(() => { let pathname = location.pathname.match(/docs\/?(.+)?/); if (!pathname || !tree || !tree.children || !location) { return null; } pathname = pathname[0]; const trimmedPathname = pathname.replace(/(\/$)|(\/#.+)/, ''); let children = tree.children; if (tree.frontmatter && tree.originalPath) { children = [{ ...tree, children: undefined }, ...children]; } if (filterTerm) { children = getMatchTree(children, filterTerm); } return children.map(page => { const pageChildren = page.children || []; const isActive = pageChildren.length ? trimmedPathname.startsWith(page.path) : !!page.path.match(new RegExp(`${trimmedPathname}$`, 'g')); const showSubItems = !!filterTerm || (pageChildren.length && isActive); return ( isActive} onClick={closeSidebar} > {page.matchedIndices ? highlightText(page.frontmatter.title, page.matchedIndices) : page.frontmatter.title} {pageChildren.length ? : null} {showSubItems ? ( {pageChildren.map(childPage => ( !!childPage.path.match( new RegExp(`${trimmedPathname}$`, 'g') ) } to={`/${childPage.path}/`} > {childPage.matchedIndices ? highlightText( childPage.frontmatter.title, childPage.matchedIndices ) : childPage.frontmatter.title} {/* Only Show H3 items if there is a search applied */} {filterTerm ? childPage.headings.map(heading => ( {highlightText(heading.value, heading.matchedIndices)} )) : null} ))} ) : null} ); }); }, [location, tree, filterTerm, closeSidebar]); return ( setFilterTerm(e.target.value)} value={filterTerm} /> {sidebarItems} ); }; export default Sidebar; ================================================ FILE: packages/site/src/components/wrapper.js ================================================ import styled from 'styled-components'; export const Wrapper = styled.div` display: flex; flex-direction: column; flex-wrap: wrap; justify-content: space-between; margin: 0; padding: ${props => (props.noPadding ? '0 4rem' : '4rem')}; text-align: center; width: 100%; @media (min-width: 768px) { flex-direction: row; max-width: 116rem; padding: ${props => (props.noPadding ? '0 4rem' : '4rem 8rem')}; } @media (max-width: 768px) { padding: ${props => (props.noPadding ? '0 4rem' : '0 8rem')}; text-align: center; img { max-width: 240px; } } `; ================================================ FILE: packages/site/src/constants.js ================================================ const constants = { docsTitle: 'URQL', githubIssues: 'https://www.github.com/urql-graphql/urql/issues', github: 'https://www.github.com/urql-graphql/urql', readme: 'https://github.com/urql-graphql/urql/blob/main/README.md', color: '#6B78B8', googleAnalyticsId: 'UA-43290258-1', }; export default constants; ================================================ FILE: packages/site/src/google-analytics.js ================================================ import React from 'react'; import { useBasepath } from 'react-static'; import PropTypes from 'prop-types'; let Analytics = {}; if (typeof document !== 'undefined') { Analytics = require('react-router-ga').default; } else { Analytics = React.Fragment; } const GoogleAnalytics = ({ children, ...rest }) => { const basename = `/${useBasepath() || ''}`; if (typeof document !== 'undefined') { // fragment doesn't like it when you try to give it attributes return ( {children} ); } return {children}; }; GoogleAnalytics.propTypes = { children: PropTypes.element, }; export default GoogleAnalytics; ================================================ FILE: packages/site/src/google-tag-manager.js ================================================ /** * Google Tag Manager */ const TagManager = require('react-gtm-module'); export const initGoogleTagManager = () => { if (typeof document === 'undefined') { return {}; } else { return TagManager.initialize({ gtmId: 'GTM-MD32945' }); } }; ================================================ FILE: packages/site/src/html.js ================================================ import React from 'react'; const Document = ({ Html, Head, Body, children }) => ( urql Documentation {children} ); export default Document; ================================================ FILE: packages/site/src/index.js ================================================ import React from 'react'; import { render, hydrate } from 'react-dom'; import { AppContainer } from 'react-hot-loader'; import App from './app'; export default App; // Render your app if (typeof document !== 'undefined') { const renderMethod = module.hot ? render : hydrate; const mount = Comp => { renderMethod( , document.getElementById('root') ); }; mount(App); if (module.hot) { module.hot.accept('./app', () => mount(require('./app').default)); } } ================================================ FILE: packages/site/src/screens/404/404.js ================================================ import React from 'react'; const NotFound = () => { return

    404! That page does not exist :(

    ; }; export default NotFound; ================================================ FILE: packages/site/src/screens/404/index.js ================================================ import React from 'react'; import Docs from '../docs'; import NotFoundPage from './404'; const NotFound = () => { return ( ); }; export default NotFound; ================================================ FILE: packages/site/src/screens/docs/article.js ================================================ /* eslint-disable react-hooks/rules-of-hooks */ import React from 'react'; import { Head } from 'react-static'; import styled from 'styled-components'; import { useMarkdownPage } from 'react-static-plugin-md-pages'; import { ScrollToTop } from '../../components/scroll-to-top'; import { MDXComponents } from '../../components/mdx'; const Container = styled.main.attrs(() => ({ className: 'page-content', }))` flex: 1; width: 100%; display: flex; flex-direction: row-reverse; align-items: flex-start; `; const Content = styled.article.attrs(() => ({ id: 'page-content', }))` flex: 1; min-height: calc(100vh - ${p => p.theme.layout.header}); background: ${p => p.theme.colors.bg}; padding: ${p => p.theme.spacing.md}; @media ${p => p.theme.media.lg} { padding: ${p => p.theme.spacing.lg}; } overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; hyphens: auto; `; const Legend = styled.aside` display: none; @media ${({ theme }) => theme.media.lg} { display: block; position: sticky; top: ${p => p.theme.layout.header}; width: 100%; max-width: ${p => p.theme.layout.legend}; padding: ${p => p.theme.spacing.lg} ${p => p.theme.spacing.md}; margin: 0; overflow: auto; height: calc(100vh - ${p => p.theme.layout.header}); } `; const LegendTitle = styled.h3` font-size: ${p => p.theme.fontSizes.body}; font-weight: ${p => p.theme.fontWeights.heading}; margin-bottom: ${p => p.theme.spacing.sm}; `; const HeadingList = styled.ul` list-style-type: none; margin: 0; padding: 0; `; const HeadingItem = styled.li` line-height: ${p => p.theme.lineHeights.heading}; margin-bottom: ${p => p.theme.spacing.xs}; margin-left: ${p => (p.depth >= 3 ? p.theme.spacing.sm : 0)}; > a { font-size: ${p => p.theme.fontSizes.small}; font-weight: ${p => p.depth < 3 ? p.theme.fontWeights.links : p.theme.fontWeights.body}; color: ${p => p.theme.colors.passive}; text-decoration: none; } `; const SectionList = () => { const page = useMarkdownPage(); if (!page || !page.headings) return null; const title = (page.frontmatter && page.frontmatter.title) || null; const headings = page.headings.filter(x => x.depth > 1); if (headings.length === 0) return null; return ( <> {title && ( {title} | urql Documentation )} In this section {headings.map(heading => (
    {heading.value} ))} ); }; export const ArticleStyling = ({ children, SectionList }) => ( {SectionList && } {children} ); const Article = ({ children }) => ( <> {children} ); export default Article; ================================================ FILE: packages/site/src/screens/docs/header.js ================================================ import React from 'react'; import styled from 'styled-components'; import { Link } from 'react-router-dom'; import formidableLogo from '../../assets/logos/logo-formidable.svg'; const Fixed = styled.header` position: fixed; top: 0; left: 0; right: 0; width: 100%; z-index: 1; box-sizing: border-box; height: ${p => p.theme.layout.header}; background: ${p => p.theme.colors.bg}; border-bottom: 1px solid ${p => p.theme.colors.border}; padding: 0 ${p => p.theme.spacing.md}; box-shadow: ${p => p.theme.shadows.header}; `; const Wrapper = styled.div` width: 100%; height: 100%; max-width: ${p => p.theme.layout.page}; margin: 0 auto; padding-top: 2px; display: flex; flex-direction: row; align-items: center; `; const BlockLink = styled.a` display: flex; color: inherit; text-decoration: none; `; const ProjectWording = styled(Link)` display: flex; text-decoration: none; font-family: ${p => p.theme.fonts.code}; color: ${p => p.theme.colors.accent}; margin-left: 0.6ch; font-size: 1.9rem; `; const FormidableLogo = styled.img.attrs(() => ({ src: formidableLogo, }))` height: 2.8rem; position: relative; top: -0.1rem; `; const Header = () => ( urql ); export default Header; ================================================ FILE: packages/site/src/screens/docs/index.js ================================================ import React, { useState, useCallback } from 'react'; import styled from 'styled-components'; import Article, { ArticleStyling } from './article'; import Header from './header'; import Sidebar from '../../components/sidebar'; import burger from '../../assets/burger.svg'; import closeButton from '../../assets/close.svg'; const Container = styled.div` position: relative; display: flex; flex-direction: row; width: 100%; max-width: ${p => p.theme.layout.page}; margin: 0 auto; margin-top: ${p => p.theme.layout.header}; `; const OpenCloseSidebar = styled.img.attrs(props => ({ src: props.sidebarOpen ? closeButton : burger, }))` cursor: pointer; display: block; margin: ${p => p.theme.spacing.sm} ${p => p.theme.spacing.md}; position: fixed; right: 0; top: 0; z-index: 1; @media ${p => p.theme.media.sm} { display: none; } `; const Docs = ({ isLoading, children }) => { const [sidebarOpen, setSidebarOpen] = useState(false); const closeSidebar = useCallback(() => { setSidebarOpen(false); }, [setSidebarOpen]); return ( <>
    setSidebarOpen(prev => !prev)} /> {/* load just the styles if Suspense fallback in use */} {isLoading ? ( {children} ) : (
    {children}
    )}
    ); }; export default Docs; ================================================ FILE: packages/site/src/screens/home/_content.js ================================================ const content = { header: { hero: { copyText: 'npm install urql graphql', }, }, features: [ { title: 'Performant and functional', description: 'Lightweight, powerful, and easy to use; urql is a great alternative to bulky GraphQL clients.', icon: require('../../assets/gql-tile.svg'), }, { title: 'Extensible library that grows with you', description: 'Want to change how you fetch, cache, or subscribe to data? The urql exchanges allow you to customize your data layer to suit your needs.', icon: require('../../assets/eagle-tile.svg'), }, { title: 'Logical default behavior and caching', description: 'Adding urql enables you to rapidly use GraphQL in your apps without complex configuration or large API overhead.', icon: require('../../assets/clock-tile.svg'), }, ], preview: { description: '', media: '', }, getStarted: { description: `With its intuitive set of lightweight API's, getting started with urql is a breeze. Dive into the documentation to get up and running in minutes.`, link: '/docs', }, oss: [ { title: 'Victory', description: 'An ecosystem of modular data visualization components for React. Friendly and flexible.', logo: require('../../assets/badge_victory.svg'), link: 'https://formidable.com/open-source/victory', }, { title: 'urql', description: 'Universal React Query Library is a blazing-fast GraphQL client, exposed as a set of ReactJS components.', logo: require('../../assets/sidebar-badge.svg'), link: 'https://formidable.com/open-source/urql/', }, { title: 'Spectacle', description: 'A React.js based library for creating sleek presentations using JSX syntax that gives you the ability to live demo your code.', logo: require('../../assets/badge_spectacle.svg'), link: 'https://formidable.com/open-source/spectacle/', }, { title: 'Runpkg', description: 'The online package explorer. Runpkg turns any npm package into an interactive and informative browsing experience', logo: require('../../assets/badge_runpkg.svg'), link: 'https://www.runpkg.com/', }, ], }; export default content; ================================================ FILE: packages/site/src/screens/home/features.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { BodyCopy } from '../../components/body-copy'; import { SecondaryTitle } from '../../components/secondary-title'; import { SectionTitle } from '../../components/section-title'; import { PanelSectionWrapper } from '../../components/panel'; const FeatureWrapper = styled.div` display: flex; justify-content: space-between; flex-direction: column; @media (min-width: 768px) { flex-direction: row; } `; const FeatureCard = styled.div` display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 28rem; text-align: center; > img { width: 100%; max-width: 28rem; box-shadow: -20px 20px 0 0 rgba(0, 0, 0, 0.5); } &:not(:last-child) { margin: 0 0 4rem; } @media (min-width: 768px) { margin: 0; width: calc(1 / 3 * 100% - (1 - 1 / 3) * 40px); align-items: flex-start; text-align: left; } @media (min-width: 1024px) { width: calc(1 / 3 * 100% - (1 - 1 / 3) * 80px); } `; const SectionTitleStyled = styled(SectionTitle)` margin-top: 0; margin-bottom: 4rem; @media (min-width: 768px) { margin-top: 0; margin-bottom: 6rem; } `; const SecondaryTitleStyled = styled(SecondaryTitle)` @media (min-width: 768px) { margin-left: 0; margin-right: 0; } `; class Features extends React.Component { render() { return ( Features {this.props.featureArray.map(feature => ( {feature.title} {feature.description} ))} ); } } Features.propTypes = { featureArray: PropTypes.array, }; export default Features; ================================================ FILE: packages/site/src/screens/home/get-started.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { BodyCopy } from '../../components/body-copy'; import { Link } from '../../components/link'; import { SectionTitle } from '../../components/section-title'; import { PanelSectionWrapper } from '../../components/panel'; const GetStartedWrapper = styled.div` align-items: center; display: flex; flex-direction: column; max-width: 55rem; p { text-align: center; } h2 { margin-top: 0; } `; const GetStartedTitle = styled(SectionTitle)` margin: 2rem 0 4rem; `; const ButtonsWrapper = styled.div` display: flex; align-items: center; margin-top: 6rem; flex-direction: column; @media (min-width: 768px) { flex-direction: row; } `; class GetStarted extends React.Component { render() { const { content } = this.props; return ( Get Started {content.description} Quick Start Guide ); } } GetStarted.propTypes = { content: PropTypes.object, }; export default GetStarted; ================================================ FILE: packages/site/src/screens/home/hero.js ================================================ import React, { useCallback } from 'react'; import { Link as ReactRouterLink } from 'react-router-dom'; import { Wrapper } from '../../components/wrapper'; import { Button } from '../../components/button'; import { Link } from '../../components/link'; import styled from 'styled-components'; import badge from '../../assets/sidebar-badge.svg'; const WrapperStyled = styled(Wrapper)` z-index: 1; `; const HeroContent = styled.div` align-items: center; display: flex; flex-direction: column; flex-wrap: wrap; margin-top: 5rem; padding: 0; position: relative; text-align: left; width: 100%; @media (min-width: 768px) { flex-direction: row; margin-top: 20rem; padding-left: 32rem; } `; const HeroTitle = styled.h1` font-size: 5rem; margin: 0 0 2rem; text-align: center; text-transform: uppercase; width: 100%; color: #fff; @media (min-width: 768px) { font-size: 5.8rem; margin: 4rem 0 2rem; text-align: left; } `; const HeroBody = styled.p` font-size: 2rem; line-height: 3rem; text-align: left; width: 100%; margin-top: 0; margin-bottom: 0; @media (min-width: 768px) { margin: 0 0 6rem; max-width: 50rem; } `; const HeroLogoContainer = styled.div` display: flex; width: 100%; margin: 6rem 0; height: 160px; @media (min-width: 768px) { height: auto; display: block; width: inherit; margin: 0; } `; const HeroLogo = styled.img` width: 20rem; margin: auto; @media (min-width: 768px) { left: -3rem; max-width: 32rem; position: absolute; top: 0; width: 100%; } `; const HeroButtonsWrapper = styled.div` max-width: 100%; flex-direction: column; justify-content: center; display: flex; @media (min-width: 1024px) { flex-direction: row; } @media (max-width: 768px) { align-items: center; } `; const HeroNPMWrapper = styled.div` flex-direction: row; justify-content: center; display: none; width: 30rem; @media (min-width: 768px) { display: flex; } @media (min-width: 1024px) { width: 28rem; } `; const HeroNPMCopy = styled.p` width: 22rem; height: 4rem; color: #383838; background-color: #d5d5d5; color: black; text-align: left; padding: 0.33rem 1.5rem; line-height: 3.44rem; font-size: 14px; margin: 0; `; const HeroNPMButton = styled(Button)` width: 8rem; cursor: copy; text-decoration: none; `; export const HeroDocsButton = styled(Link)` width: 30rem; margin-top: 4rem; @media (min-width: 768px) { margin-top: 2rem; width: 30rem; } @media (min-width: 1024px) { margin-top: 0; margin-left: 2rem; width: 18rem; } `; const HeroNavList = styled.ul` border-top: 2px solid #707070; margin-top: 2rem; display: flex; flex-direction: row; list-style: none; padding: 2rem 0 0; text-align: center; width: 100%; justify-content: space-around; & li { display: inline-block; margin-right: 33px; } & li:last-child { margin-right: 0; } & li a { color: white; display: inline-block; font-size: 1.7rem; transition: opacity 0.4s; text-transform: uppercase; text-decoration: none; } & li a:hover { color: #8196ff; } @media (min-width: 768px) { display: inline-block; border-top: 2px solid #ffffff; padding-top: 4rem; margin: 4rem 0 0 0; & li { margin-right: 66px; } } @media (min-width: 1024px) { width: 48rem; margin: 4rem 0 0 32rem; } `; const copyFallBack = copyText => { const copyTextArea = document.createElement('textArea'); copyTextArea.value = copyText; document.body.appendChild(copyTextArea); copyTextArea.focus(); copyTextArea.select(); document.execCommand('copy'); copyTextArea.remove(); }; const Hero = props => { const handleCopy = useCallback( e => { if (!navigator.clipboard) { copyFallBack(props.content.copyText); e.preventDefault(); return; } navigator.clipboard.writeText(props.content.copyText); }, [props.content.copyText] ); return ( urql The highly customizable and versatile GraphQL client for React, Svelte, Vue, Solid or plain JavaScript, with which you add on features like normalized caching as you grow. {props.content.copyText} copy Documentation
  • Docs
  • Issues
  • GitHub
  • ); }; export default Hero; ================================================ FILE: packages/site/src/screens/home/index.js ================================================ import React from 'react'; import styled from 'styled-components'; import { usePrefetch } from 'react-static'; import { useMarkdownTree } from 'react-static-plugin-md-pages'; import Features from './features'; import GetStarted from './get-started'; import MoreOSS from './more-oss'; import content from './_content'; import { Header } from '../../components/header'; import { Footer } from '../../components/footer'; const Container = styled.div` width: 100%; `; const Home = () => { const ref = usePrefetch('docs'); useMarkdownTree(); return (
    ); }; export default Home; ================================================ FILE: packages/site/src/screens/home/more-oss.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; import { BodyCopy } from '../../components/body-copy'; import { Link } from '../../components/link'; import { PanelSectionWrapper } from '../../components/panel'; import { SectionTitle } from '../../components/section-title'; import { SecondaryTitle } from '../../components/secondary-title'; const OSSCardContainer = styled.div` display: grid; grid-template-columns: 1fr; grid-template-rows: repeat(4, 1fr); grid-gap: 4rem; width: calc(100% - 4rem); max-width: 75%; margin: auto auto 4rem auto; @media (min-width: 768px) { grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(2, 1fr); max-width: 116rem; } `; const OSSCard = styled.div` text-align: left; display: flex; flex-direction: column; align-items: center; > * { margin-top: 0; margin-bottom: 0; } > * + * { margin-top: 1rem; } @media (min-width: 768px) { flex-direction: row; justify-content: space-between; > * { margin-left: 0; margin-right: 0; } > * + * { margin-top: 0; margin-left: 3rem; } } `; const OSSImage = styled.img` flex: 0 0 15rem; height: 15rem; `; const OSSCopyContainer = styled.div` display: flex; flex-direction: column; > * { margin-top: 0; margin-bottom: 0; } > * + * { margin-top: 2rem; } `; const OSSTitle = styled(SecondaryTitle)` transition: opacity 0.3s ease-out; margin: 0; &:hover { opacity: 0.7; } @media (min-width: 768px) { text-align: left; } `; const OSSDescription = styled(BodyCopy)` @media (min-width: 768px) { text-align: left; } `; const MoreOSS = ({ oss }) => ( More Open Source from Formidable {oss.map(card => { return ( {card.title} {card.description} ); })} View All ); MoreOSS.propTypes = { oss: PropTypes.arrayOf( PropTypes.shape({ title: PropTypes.string.isRequired, link: PropTypes.string.isRequired, description: PropTypes.string.isRequired, logo: PropTypes.string.isRequired, }).isRequired ).isRequired, }; export default MoreOSS; ================================================ FILE: packages/site/src/styles/global.js ================================================ import { createGlobalStyle } from 'styled-components'; export const GlobalStyle = createGlobalStyle` * { box-sizing: inherit; min-width: 0; } html { box-sizing: border-box; font-size: 62.5%; overflow-x: hidden; } body { background: ${p => p.theme.colors.passiveBg}; color: ${p => p.theme.colors.text}; font-family: ${p => p.theme.fonts.body}; line-height: ${p => p.theme.lineHeights.body}; font-weight: ${p => p.theme.fontWeights.body}; text-rendering: optimizeLegibility; margin: 0; padding: 0; font-size: ${p => p.theme.fontSizes.bodySmall}; @media ${p => p.theme.media.lg} { font-size: ${p => p.theme.fontSizes.body}; } } a { color: ${p => p.theme.colors.accent}; font-weight: ${p => p.theme.fontWeights.links}; } table, pre, p, h1, h2, h3 { margin: 0 0 ${p => p.theme.spacing.md} 0; } h1, h2, h3 { font-family: ${p => p.theme.fonts.heading}; font-weight: ${p => p.theme.fontWeights.heading}; line-height: ${p => p.theme.lineHeights.heading}; color: ${p => p.theme.colors.heading}; } h1 { font-size: ${p => p.theme.fontSizes.h1}; } h2 { font-size: ${p => p.theme.fontSizes.h2}; } h3 { font-size: ${p => p.theme.fontSizes.h3}; } img { max-width: 100%; } `; ================================================ FILE: packages/site/src/styles/theme.js ================================================ const systemFonts = [ '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans', 'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', ]; export const colors = { passiveBg: '#f2f2f2', codeBg: '#f0f7fb', bg: '#ffffff', border: '#ececec', activeBorder: '#a2b1ff', text: '#000000', heading: '#444444', accent: '#566ac8', code: '#403f53', passive: '#444444', }; export const layout = { page: '144rem', header: '4.8rem', stripes: '0.7rem', sidebar: '28rem', legend: '22rem', logo: '12rem', }; export const fonts = { heading: systemFonts.join(', '), body: systemFonts.join(', '), code: 'Space Mono, monospace', }; export const fontSizes = { small: '0.9em', body: '1.8rem', bodySmall: '1.5rem', code: '0.8em', h1: '3.45em', h2: '2.11em', h3: '1.64em', }; export const fontWeights = { body: '400', links: '500', heading: '600', }; export const lineHeights = { body: '1.5', heading: '1.1', code: '1.2', }; export const shadows = { header: 'rgba(0, 0, 0, 0.09) 0px 2px 10px -3px', input: 'rgba(0, 0, 0, 0.09) 0px 2px 10px -3px', }; export const mediaSizes = { sm: 700, md: 960, lg: 1200, }; export const media = { maxmd: `(max-width: ${mediaSizes.md - 1}px)`, sm: `(min-width: ${mediaSizes.sm}px)`, md: `(min-width: ${mediaSizes.md}px)`, lg: `(min-width: ${mediaSizes.lg}px)`, }; export const spacing = { xs: '0.6rem', sm: '1.5rem', md: '2.75rem', lg: '4.75rem', xl: '8.2rem', }; ================================================ FILE: packages/site/static.config.js ================================================ import * as os from 'os'; import { resolve } from 'path'; import constants from './src/constants'; import Document from './src/html'; const basePath = process.env.VERCEL_ENV === 'preview' ? '.' : 'open-source/urql'; const isStaging = process.env.REACT_STATIC_ENV === 'staging'; const isProduction = process.env.REACT_STATIC_ENV === 'production'; export default { plugins: [ resolve(__dirname, 'plugins/assets-fix/'), resolve(__dirname, 'plugins/monorepo-fix/'), resolve(__dirname, 'plugins/react-router/'), (isStaging || isProduction) && resolve(__dirname, 'plugins/preact/'), [ 'react-static-plugin-md-pages', { location: '../../docs', template: './src/screens/docs', pathPrefix: 'docs', }, ], 'react-static-plugin-styled-components', 'react-static-plugin-sitemap', ].filter(Boolean), paths: { src: 'src', dist: `dist/${basePath}`, buildArtifacts: 'node_modules/.cache/react-static/artifacts/', devDist: 'node_modules/.cache/react-static/dist/', temp: 'node_modules/.cache/react-static/temp/', assetsPath: 'static', public: 'public', // The public directory (files copied to dist during build) }, basePath, stagingBasePath: basePath, devBasePath: basePath, Document, getSiteData: () => ({ title: constants.docsTitle, }), maxThreads: Math.min(8, os.cpus().length / 2), getRoutes: async () => [ { path: '/', template: require.resolve('./src/screens/home'), }, { path: '/docs/concepts/core-package', redirect: '/docs/basics/core', }, { path: '/docs/basics/getting-started', redirect: '/docs/basics', }, { path: '/docs/basics/mutations', redirect: '/docs/basics', }, { path: '/docs/basics/queries', redirect: '/docs/basics', }, { path: '404', template: require.resolve('./src/screens/404'), }, { path: '/docs/graphcache/custom-updates', redirect: '/docs/graphcache/cache-updates', }, { path: '/docs/graphcache/computed-queries', redirect: '/docs/graphcache/local-resolvers', }, { path: '/docs/graphcache/under-the-hood', redirect: '/docs/graphcache/normalized-caching', }, { path: '/docs/concepts/document-caching', redirect: '/docs/basics/document-caching', }, { path: '/docs/concepts/errors', redirect: '/docs/basics/errors', }, { path: '/docs/concepts', redirect: '/docs/architecture', }, { path: '/docs/concepts/stream-patterns', redirect: '/docs/architecture', }, { path: '/docs/concepts/philosophy', redirect: '/docs/architecture', }, { path: '/docs/concepts/exchanges', redirect: '/docs/advanced/authoring-exchanges', }, ], }; ================================================ FILE: packages/site/vercel.json ================================================ { "github": { "silent": true } } ================================================ FILE: packages/solid-start-urql/CHANGELOG.md ================================================ # @urql/solid-start ## 0.1.0 ### Minor Changes - Initial release of `@urql/solid-start` - URQL integration built with SolidStart's native primitives. Get started with: - **`createQuery`** - GraphQL queries using SolidStart's `query()` and `createAsync()` - **`createMutation`** - GraphQL mutations using SolidStart's `action()` and `useAction()` - **`createSubscription`** - Real-time GraphQL subscriptions - **`Provider`** and **`useClient`** - Context-based client access - **Reactive variables** - All parameters accept signals/accessors for automatic re-execution - **Full SSR support** - Works seamlessly with SolidStart's server-side rendering - **TypeScript support** - Complete type safety with GraphQL types - **Uses `@solid-primitives/utils`** - Leverages standard Solid ecosystem utilities Submitted by [@davedbase](https://github.com/davedbase) (See [#3837](https://github.com/urql-graphql/urql/pull/3837)) ### Patch Changes - Updated dependencies (See [#3837](https://github.com/urql-graphql/urql/pull/3837)) - @urql/solid@1.0.1 ================================================ FILE: packages/solid-start-urql/README.md ================================================ # @urql/solid-start `@urql/solid-start` provides URQL integration for [SolidStart](https://start.solidjs.com/), built with SolidStart's native primitives like `query`, `action`, and `createAsync`. > **Note:** This package is specifically designed for SolidStart applications with SSR. If you're building a client-side only SolidJS app, use [`@urql/solid`](https://github.com/urql-graphql/urql/tree/main/packages/solid-urql) instead. See [SolidJS vs SolidStart](#solidjs-vs-solidstart) for key differences. ## Features - 🎯 **SolidStart Native** - Built with `query`, `action`, `createAsync`, and `useAction` - 🚀 **Automatic SSR** - Works seamlessly with SolidStart's server-side rendering - 🔄 **Reactive Variables** - Query variables can be signals that automatically trigger re-execution - 📡 **Real-time Subscriptions** - Full GraphQL subscription support - 🎨 **Type Safe** - Complete TypeScript support with GraphQL types - 🪶 **Lightweight** - Minimal wrapper over URQL core ## Installation ```bash npm install @urql/solid-start @urql/solid @urql/core graphql # or pnpm add @urql/solid-start @urql/solid @urql/core graphql # or yarn add @urql/solid-start @urql/solid @urql/core graphql ``` > **Note:** `@urql/solid` is a peer dependency required for subscriptions. ## Quick Start ### 1. Set up the Provider Wrap your app with the `Provider` to make the URQL client and router primitives available: ```tsx // src/app.tsx import { Router, action, query } from '@solidjs/router'; import { FileRoutes } from '@solidjs/start/router'; import { Provider } from '@urql/solid-start'; import { createClient, cacheExchange, fetchExchange } from '@urql/core'; const client = createClient({ url: 'https://api.example.com/graphql', exchanges: [cacheExchange, fetchExchange], }); export default function App() { return ( {props.children}}> ); } ``` > **Note:** The Provider accepts `client`, `query`, and `action` so `createQuery` and `createMutation` can use SolidStart APIs via context. ### 2. Use Queries ```tsx // src/routes/todos.tsx import { createQuery } from '@urql/solid-start'; import { createAsync } from '@solidjs/router'; import { gql } from '@urql/core'; import { For, Show, Suspense } from 'solid-js'; const TodosQuery = gql` query { todos { id title completed } } `; export default function TodosPage() { const queryTodos = createQuery(TodosQuery, 'todos-list'); const todos = createAsync(() => queryTodos()); return (

    Todos

    Loading...
    }>
      {todo => (
    • {todo.title}
    • )}

    Error: {todos()!.error.message}

    ); } ``` ### 3. Use Mutations ```tsx // src/components/AddTodoForm.tsx import { createMutation } from '@urql/solid-start'; import { useAction, useSubmission } from '@solidjs/router'; import { gql } from '@urql/core'; import { Show } from 'solid-js'; const AddTodoMutation = gql` mutation AddTodo($title: String!) { addTodo(title: $title) { id title } } `; export function AddTodoForm() { const addTodoAction = createMutation(AddTodoMutation, 'add-todo'); const addTodo = useAction(addTodoAction); const submission = useSubmission(addTodoAction); let inputRef: HTMLInputElement | undefined; const handleSubmit = async (e: Event) => { e.preventDefault(); if (!inputRef?.value) return; const result = await addTodo({ title: inputRef.value }); if (result.data) { inputRef.value = ''; } }; return (

    Error: {submission.result!.error.message}

    ); } ``` ### 4. Use Subscriptions ```tsx // src/components/LiveMessages.tsx import { createSubscription } from '@urql/solid-start'; import { gql } from '@urql/core'; import { For } from 'solid-js'; const MessagesSubscription = gql` subscription { messageAdded { id text createdAt } } `; export function LiveMessages() { const [messages] = createSubscription( { query: MessagesSubscription }, // Optional handler to accumulate messages (prev = [], data) => [...prev, data.messageAdded] ); return (

    Live Messages

    {msg =>
    {msg.text}
    }
    ); } ``` ## API Reference ### `createQuery(queryDocument, key, options?)` Creates a GraphQL query using SolidStart's `query` and `createAsync` primitives. The `query` function is automatically retrieved from context. **Parameters:** - `queryDocument: DocumentInput` - GraphQL query document - `key: string` - Cache key for SolidStart's router - `options?: object` - Optional configuration - `variables?: Variables` - Query variables - `requestPolicy?: RequestPolicy` - Cache policy - `context?: Partial` - Additional context **Returns:** A query function that can be used with `createAsync` **Basic Example:** ```tsx import { createAsync } from '@solidjs/router'; import { createQuery } from '@urql/solid-start'; export default function TodosPage() { const queryTodos = createQuery(TodosQuery, 'todos-list'); const todos = createAsync(() => queryTodos()); return
    {/* ... */}
    ; } ``` **Example with variables:** ```tsx import { createAsync } from '@solidjs/router'; import { createQuery } from '@urql/solid-start'; export default function UserPage() { const queryUser = createQuery(UserQuery, 'user-details', { variables: { id: 1 }, }); const user = createAsync(() => queryUser()); return
    {/* ... */}
    ; } ``` **Example with custom client:** ```tsx import { createAsync } from '@solidjs/router'; import { createQuery } from '@urql/solid-start'; import { createClient } from '@urql/core'; const customClient = createClient({ url: 'https://api.example.com/graphql' }); export default function CustomPage() { const queryTodos = createQuery(TodosQuery, 'todos-list'); const todos = createAsync(() => queryTodos(customClient)); return
    {/* ... */}
    ; } ``` > **Note:** `createQuery` must be called inside a component where it has access to the URQL context. The query function from `@solidjs/router` is automatically retrieved from the Provider. ### `createMutation(mutation, key)` Creates a GraphQL mutation action using SolidStart's `action` primitive. **Args:** - `mutation: DocumentInput` - GraphQL mutation document - `key: string` - Cache key for SolidStart's router **Returns:** `Action` - A SolidStart action that can be used with `useAction()` and `useSubmission()` **Example:** ```tsx import { createMutation } from '@urql/solid-start'; import { useAction, useSubmission } from '@solidjs/router'; const updateUserAction = createMutation(UpdateUserMutation, 'update-user'); const updateUser = useAction(updateUserAction); const submission = useSubmission(updateUserAction); // Call the mutation const result = await updateUser({ id: 1, name: 'Alice' }); // Access submission state console.log(submission.pending); // boolean console.log(submission.result); // OperationResult ``` ### `createSubscription(args, handler?)` Creates a GraphQL subscription for real-time updates. **Args:** ```ts { query: DocumentInput; variables?: MaybeAccessor; context?: MaybeAccessor>; pause?: MaybeAccessor; } ``` **Handler:** Optional function to accumulate/transform subscription data ```ts (previousData: Data | undefined, newData: Data) => Data; ``` **Returns:** `[State, ExecuteFunction]` **Example with handler:** ```tsx const [messages] = createSubscription({ query: MessagesSubscription }, (prev = [], data) => [ ...prev, data.messageAdded, ]); ``` ### `Provider` Context provider for the URQL client and SolidStart router primitives. **Props:** - `value: { client: Client; query: typeof query; action: typeof action }` - Object containing the URQL client and SolidStart router primitives **Example:** ```tsx import { action, query } from '@solidjs/router'; import { Provider } from '@urql/solid-start'; ; ``` ### `useClient()` Hook to access the URQL client from context. **Returns:** `Client` ## Request Policies Control caching behavior with `requestPolicy`: - `cache-first` (default) - Use cache if available, otherwise fetch - `cache-only` - Only use cached data, never fetch - `network-only` - Always fetch, ignore cache - `cache-and-network` - Return cache immediately, then fetch in background ```tsx const todos = createQuery({ query: TodosQuery, requestPolicy: 'cache-and-network', }); ``` ## Dynamic Queries For dynamic queries that change based on reactive values, you can pass variables to the query function: ```tsx import { createSignal } from 'solid-js'; import { createAsync } from '@solidjs/router'; import { createQuery } from '@urql/solid-start'; export default function UserPage() { const [userId, setUserId] = createSignal(1); // Create the query function const queryUser = createQuery(UserQuery, 'user-details', { variables: { id: userId() }, }); // Wrap with createAsync to get reactive data const user = createAsync(() => queryUser()); return (

    {user()!.data.user.name}

    ); } ``` Note: For fully reactive variables that trigger re-fetches, you may need to create the query function inside a `createEffect` or recreate it when dependencies change. ## Cache Keys Cache keys are required for both queries and mutations to enable SolidStart's caching and revalidation features: ```tsx // Query with cache key const queryTodos = createQuery(TodosQuery, 'todos-list', query); // Mutation with cache key const [state, addTodo] = createMutation(AddTodoMutation, 'add-todo'); ``` Choose descriptive cache keys that: - Are unique within your application - Describe the data being cached (e.g., 'user-profile', 'todos-list') - Make debugging easier by being human-readable ## Advanced: Custom Exchanges Add custom exchanges for authentication, error handling, etc: ```tsx import { createClient, cacheExchange, fetchExchange } from '@urql/core'; import { authExchange } from '@urql/exchange-auth'; const client = createClient({ url: 'https://api.example.com/graphql', exchanges: [ cacheExchange, authExchange(async utils => { return { addAuthToOperation(operation) { const token = localStorage.getItem('token'); if (!token) return operation; return utils.appendHeaders(operation, { Authorization: `Bearer ${token}`, }); }, didAuthError(error) { return error.graphQLErrors.some(e => e.extensions?.code === 'UNAUTHENTICATED'); }, async refreshAuth() { // Refresh token logic }, }; }), fetchExchange, ], }); ``` ## TypeScript Full type safety with GraphQL types: ```tsx import { createQuery } from '@urql/solid-start'; import { gql, type TypedDocumentNode } from '@urql/core'; interface Todo { id: string; title: string; completed: boolean; } interface TodosData { todos: Todo[]; } const TodosQuery: TypedDocumentNode = gql` query { todos { id title completed } } `; // Fully typed! const todos = createQuery({ query: TodosQuery }); // ^? Accessor | undefined> ``` ## How It Works `@urql/solid-start` integrates URQL with SolidStart's primitives: - **`createQuery`** wraps SolidStart's `query()` function to execute URQL queries with automatic SSR and caching. The `query` function is automatically retrieved from the URQL context, eliminating the need for manual injection. - **`createMutation`** creates SolidStart `action()` primitives that integrate with `useAction()` and `useSubmission()` for form handling and progressive enhancement - **`createSubscription`** uses `@urql/solid-start` context so it works with the same Provider as queries and mutations This means you get: - ✅ Automatic server-side rendering - ✅ Request deduplication via SolidStart's query caching - ✅ Streaming responses - ✅ Progressive enhancement with actions - ✅ Full fine-grained reactivity ## SolidJS vs SolidStart ### When to Use Each Package | Use Case | Package | Why | | ------------------ | ------------------- | ---------------------------------------------------------------- | | Client-side SPA | `@urql/solid` | Optimized for client-only apps, uses SolidJS reactivity patterns | | SolidStart SSR App | `@urql/solid-start` | Integrates with SolidStart's routing, SSR, and action system | ### Key Differences #### Queries **@urql/solid** (Client-side): ```tsx import { createQuery } from '@urql/solid'; const [result] = createQuery({ query: TodosQuery }); // Returns: [Accessor, Accessor] ``` **@urql/solid-start** (SSR): ```tsx import { createQuery } from '@urql/solid-start'; import { createAsync } from '@solidjs/router'; const queryTodos = createQuery(TodosQuery, 'todos'); const todos = createAsync(() => queryTodos()); // Returns: Accessor // Works with SSR and SolidStart's caching ``` #### Mutations **@urql/solid** (Client-side): ```tsx import { createMutation } from '@urql/solid'; const [result, executeMutation] = createMutation(AddTodoMutation); await executeMutation({ title: 'New Todo' }); // Returns: [Accessor, ExecuteMutation] ``` **@urql/solid-start** (SSR with Actions): ```tsx import { createMutation } from '@urql/solid-start'; import { useAction, useSubmission } from '@solidjs/router'; const addTodoAction = createMutation(AddTodoMutation, 'add-todo'); const addTodo = useAction(addTodoAction); const submission = useSubmission(addTodoAction); await addTodo({ title: 'New Todo' }); // Integrates with SolidStart's action system for progressive enhancement ``` ### Why Different APIs? - **SSR Support**: SolidStart queries run on the server and stream to the client - **Router Integration**: Automatic caching and invalidation with SolidStart's router - **Progressive Enhancement**: Actions work without JavaScript enabled - **Suspense**: Native support for SolidJS Suspense boundaries ### Migration If you're moving from a SolidJS SPA to SolidStart: 1. Change imports from `@urql/solid` to `@urql/solid-start` 2. Wrap queries with `createAsync()` 3. Update mutations to use the action pattern with `useAction()` and `useSubmission()` ## Resources - [SolidStart Documentation](https://start.solidjs.com/) - [URQL Documentation](https://formidable.com/open-source/urql/docs/) - [Solid Primitives](https://primitives.solidjs.community/) ## License MIT ================================================ FILE: packages/solid-start-urql/jsr.json ================================================ { "name": "@urql/solid-start", "version": "0.1.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/solid-start-urql/package.json ================================================ { "name": "@urql/solid-start", "version": "0.1.0", "description": "A highly customizable and versatile GraphQL client for SolidStart", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/solid-start-urql" }, "keywords": [ "graphql client", "state management", "cache", "graphql", "exchanges", "solid", "solidstart", "ssr" ], "main": "dist/urql-solid-start", "module": "dist/urql-solid-start.mjs", "types": "dist/urql-solid-start.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-solid-start.d.ts", "import": "./dist/urql-solid-start.mjs", "require": "./dist/urql-solid-start.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@solidjs/testing-library": "^0.8.10", "@solidjs/start": "^1.2.1", "@urql/core": "workspace:*", "graphql": "^16.0.0", "jsdom": "^22.1.0", "vite-plugin-solid": "^2.11.10" }, "peerDependencies": { "@solidjs/router": ">=0.15.4", "@solidjs/start": ">=1.2.1", "@urql/core": "^6.0.0", "@urql/solid": "^1.0.0", "solid-js": "^1.9.10" }, "dependencies": { "@solid-primitives/utils": "^6.2.1", "@urql/core": "workspace:^6.0.1", "@urql/solid": "workspace:^1.0.1", "wonka": "^6.3.2" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: packages/solid-start-urql/src/context.test.tsx ================================================ // @vitest-environment jsdom /* @jsxImportSource solid-js */ import { expect, it, describe } from 'vitest'; import { Provider, useAction, useClient } from './context'; import { renderHook } from '@solidjs/testing-library'; import { createClient } from '@urql/core'; describe('context', () => { it('should provide client through context', () => { const client = createClient({ url: '/graphql', exchanges: [], }); // Mock query function that matches the expected type const mockQuery = (fn: any) => fn; const mockAction = (fn: any) => fn; const wrapper = (props: { children: any }) => { return ( {props.children} ); }; const { result } = renderHook(() => useClient(), { wrapper }); expect(result).toBe(client); }); it('should throw error when client is not provided', () => { const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'development'; expect(() => { renderHook(() => useClient()); }).toThrow(); process.env.NODE_ENV = originalEnv; }); it('should provide action through context', () => { const client = createClient({ url: '/graphql', exchanges: [], }); const mockQuery = (fn: any) => fn; const mockAction = (fn: any) => fn; const wrapper = (props: { children: any }) => { return ( {props.children} ); }; const { result } = renderHook(() => useAction(), { wrapper }); expect(result).toBe(mockAction); }); }); ================================================ FILE: packages/solid-start-urql/src/context.ts ================================================ import type { Client } from '@urql/core'; import { createContext, useContext } from 'solid-js'; import type { query as defaultQuery, action as defaultAction, } from '@solidjs/router'; export interface UrqlContext { client: Client; query: typeof defaultQuery; action: typeof defaultAction; } export const Context = createContext(); export const Provider = Context.Provider; const hasContext = ( context: UrqlContext | undefined, type: 'client' | 'context' | 'action' ) => { if (process.env.NODE_ENV !== 'production' && context === undefined) { const error = `No ${type} has been specified using urql's Provider. Please create a context and add a Provider.`; console.error(error); throw new Error(error); } }; export type UseClient = () => Client; export const useClient: UseClient = () => { const context = useContext(Context); hasContext(context, 'client'); return context!.client; }; export type UseQuery = () => typeof defaultQuery; export const useQuery: UseQuery = () => { const context = useContext(Context); hasContext(context, 'context'); return context!.query; }; export type UseAction = () => typeof defaultAction; export const useAction: UseAction = () => { const context = useContext(Context); hasContext(context, 'action'); return context!.action; }; ================================================ FILE: packages/solid-start-urql/src/createMutation.test.ts ================================================ // @vitest-environment jsdom import { expect, it, describe, vi } from 'vitest'; import { createMutation } from './createMutation'; import { createClient } from '@urql/core'; import { makeSubject } from 'wonka'; import { OperationResult } from '@urql/core'; const client = createClient({ url: '/graphql', exchanges: [], }); vi.mock('./context', () => { const action = (fn: any, _key?: string) => fn; return { useClient: () => client, useAction: () => action, }; }); describe('createMutation', () => { it('should create a mutation action', () => { const mutationAction = createMutation( 'mutation AddTodo($title: String!) { addTodo(title: $title) { id } }', 'add-todo' ); expect(mutationAction).toBeDefined(); expect(typeof mutationAction).toBe('function'); }); it('should execute a mutation through the action', async () => { const subject = makeSubject>(); const mutationSpy = vi .spyOn(client, 'executeMutation') .mockImplementation(() => subject.source as any); const mutationAction = createMutation< { addTodo: { id: number } }, { title: string } >( 'mutation AddTodo($title: String!) { addTodo(title: $title) { id } }', 'add-todo' ); const promise = mutationAction({ title: 'Test Todo' }); // Emit result subject.next({ data: { addTodo: { id: 1 } }, stale: false, hasNext: false, } as any); const result = await promise; expect(mutationSpy).toHaveBeenCalled(); expect(result.data).toEqual({ addTodo: { id: 1 } }); mutationSpy.mockRestore(); }); it('should handle mutation errors', async () => { const subject = makeSubject>(); const mutationSpy = vi .spyOn(client, 'executeMutation') .mockImplementation(() => subject.source as any); const mutationAction = createMutation('mutation { test }', 'test'); const promise = mutationAction({}); // Emit error subject.next({ data: undefined, error: { message: 'Error', graphQLErrors: [], networkError: undefined, } as any, stale: false, hasNext: false, } as any); const result = await promise; expect(result.error).toBeDefined(); expect(result.error?.message).toEqual('Error'); mutationSpy.mockRestore(); }); it('should pass context to mutation', async () => { const subject = makeSubject>(); const mutationSpy = vi .spyOn(client, 'executeMutation') .mockImplementation(() => subject.source as any); const mutationAction = createMutation('mutation { test }', 'test'); const customContext = { requestPolicy: 'network-only' as const }; const promise = mutationAction({}, customContext); // Emit result subject.next({ data: { test: 'success' }, stale: false, hasNext: false, } as any); await promise; expect(mutationSpy).toHaveBeenCalledWith(expect.anything(), customContext); mutationSpy.mockRestore(); }); }); ================================================ FILE: packages/solid-start-urql/src/createMutation.ts ================================================ import { type AnyVariables, type DocumentInput, type OperationContext, type OperationResult, createRequest, } from '@urql/core'; import type { Action } from '@solidjs/router'; import { pipe, filter, take, toPromise } from 'wonka'; import { useAction, useClient } from './context'; export type CreateMutationAction< Data = any, Variables extends AnyVariables = AnyVariables, > = Action< [variables: Variables, context?: Partial], OperationResult >; /** * Creates a GraphQL mutation action for SolidStart. * * @remarks * This uses SolidStart's `action()` primitive to create an action that can be * used with `useAction()` and `useSubmission()` in components for form handling and * progressive enhancement. * * IMPORTANT: Must be called inside a component where it has access to the URQL context. * * @param mutation - The GraphQL mutation document * @param key - Cache key for SolidStart's router * * @example * ```tsx * import { createMutation } from '@urql/solid-start'; * import { useAction, useSubmission } from '@solidjs/router'; * import { gql } from '@urql/core'; * * function AddTodoForm() { * const addTodoAction = createMutation( * gql`mutation AddTodo($title: String!) { * addTodo(title: $title) { id title } * }`, * 'add-todo' * ); * * const addTodo = useAction(addTodoAction); * const submission = useSubmission(addTodoAction); * * const handleSubmit = async (e: Event) => { * e.preventDefault(); * const result = await addTodo({ title: 'New Todo' }); * if (result.data) { * console.log('Todo added:', result.data.addTodo); * } * }; * * return ( *
    * * {submission.result?.error &&

    Error: {submission.result.error.message}

    } * * ); * } * ``` */ export function createMutation< Data = any, Variables extends AnyVariables = AnyVariables, >( mutation: DocumentInput, key: string ): CreateMutationAction { const client = useClient(); const action = useAction(); return action( async (variables: Variables, context?: Partial) => { const request = createRequest(mutation, variables); return pipe( client.executeMutation(request, context), filter(result => !result.hasNext), take(1), toPromise ); }, key ); } ================================================ FILE: packages/solid-start-urql/src/createQuery.test.tsx ================================================ // @vitest-environment jsdom import { expect, it, describe, vi } from 'vitest'; import { createQuery } from './createQuery'; import { renderHook } from '@solidjs/testing-library'; import { createClient } from '@urql/core'; import { createSignal } from 'solid-js'; import { makeSubject, pipe, toPromise } from 'wonka'; import { OperationResult, OperationResultSource } from '@urql/core'; const client = createClient({ url: '/graphql', exchanges: [], }); vi.mock('./context', () => { const useClient = () => { return client!; }; const useQuery = () => { // Return a mock query function that wraps with SolidStart's query primitive return (fn: any, _key: string) => { // Store the query function for later execution const queryFn = fn; // Return a function that executes the query return () => queryFn(); }; }; return { useClient, useQuery }; }); vi.mock('@solidjs/router', () => { return { query: (fn: any) => fn, createAsync: (fn: any) => { const [data, setData] = createSignal(); fn().then(setData); return data; }, }; }); describe('createQuery', () => { it('should execute a query', async () => { const subject = makeSubject, 'data'>>(); const executeQuery = vi .spyOn(client, 'executeQuery') .mockImplementation(() => { const source = subject.source as OperationResultSource; // Return an object with toPromise method return { toPromise: () => pipe(source, toPromise), } as any; }); const result = renderHook(() => createQuery<{ test: boolean }>('{ test }', 'test-query') ); // Trigger the query subject.next({ data: { test: true } }); await vi.waitFor(() => { const data = result.result(); expect(data).toBeDefined(); }); executeQuery.mockRestore(); }); it('should respect pause option', () => { const executeQuery = vi.spyOn(client, 'executeQuery'); renderHook(() => createQuery('{ test }', 'test-query-pause', { pause: true, } as any) ); expect(executeQuery).not.toHaveBeenCalled(); executeQuery.mockRestore(); }); it.skip('should re-execute when reactive variables change', async () => { // This test is skipped because SolidStart's query() primitive doesn't // automatically re-execute when variables change. This would require // using createAsync with a reactive dependency or manually refetching. // This is expected behavior for SolidStart. }); }); ================================================ FILE: packages/solid-start-urql/src/createQuery.ts ================================================ import { type AnyVariables, type DocumentInput, type OperationContext, type RequestPolicy, createRequest, type Client, } from '@urql/core'; import { useClient, useQuery } from './context'; /** * Creates a cached query function using SolidStart's query primitive. * * @remarks * This function creates a reusable query function that executes a GraphQL query. * It uses SolidStart's query primitive for caching and deduplication. * Call this at module level, then use the returned function with createAsync in your component. * * @example * ```tsx * import { createQuery } from '@urql/solid-start'; * import { createAsync } from '@solidjs/router'; * import { gql } from '@urql/core'; * * const POKEMONS_QUERY = gql` * query Pokemons { * pokemons(limit: 10) { * id * name * } * } * `; * * const queryPokemons = createQuery(POKEMONS_QUERY, 'list-pokemons'); * * export default function PokemonList() { * const client = useClient(); * const pokemons = createAsync(() => queryPokemons(client)); * * return ( * * * {pokemon =>
  • {pokemon.name}
  • } *
    *
    * ); * } * ``` */ export function createQuery< Data = any, Variables extends AnyVariables = AnyVariables, >( queryDocument: DocumentInput, key: string, options?: { variables?: Variables; requestPolicy?: RequestPolicy; context?: Partial; } ) { // Get the query function from context const queryFn = useQuery(); // Return the result of calling the query function return queryFn( async ( clientOrVariables?: Client | Variables, variablesOrContext?: Variables | Partial, contextOverride?: Partial ) => { // Determine if first arg is client or variables let client: Client; let variables: Variables | undefined; let context: Partial | undefined; if ( clientOrVariables && typeof (clientOrVariables as any).executeQuery === 'function' ) { // First arg is client client = clientOrVariables as Client; variables = variablesOrContext as Variables | undefined; context = contextOverride; } else { // First arg is variables (or nothing), use useClient client = useClient(); variables = clientOrVariables as Variables | undefined; context = variablesOrContext as Partial | undefined; } const finalVariables = variables !== undefined ? variables : options && options.variables; const request = createRequest(queryDocument, finalVariables as Variables); const finalContext: Partial = { requestPolicy: options && options.requestPolicy, ...(options && options.context), ...context, }; return await client.executeQuery(request, finalContext).toPromise(); }, key ); } ================================================ FILE: packages/solid-start-urql/src/createSubscription.test.ts ================================================ // @vitest-environment jsdom import { renderHook } from '@solidjs/testing-library'; import { OperationResult, OperationResultSource, createClient, gql, } from '@urql/core'; import { expect, it, describe, vi } from 'vitest'; import { makeSubject } from 'wonka'; import { createSubscription } from './index'; const QUERY = gql` subscription { value } `; const client = createClient({ url: '/graphql', exchanges: [], suspense: false, }); vi.mock('./context', () => { const useClient = () => client; return { useClient }; }); describe('createSubscription', () => { it('should execute against solid-start context client', () => { const subject = makeSubject, 'data'>>(); const executeSubscription = vi .spyOn(client, 'executeSubscription') .mockImplementation( () => subject.source as OperationResultSource ); const { result: [state], } = renderHook(() => createSubscription<{ value: number }, { variable: number }>({ query: QUERY, }) ); expect(executeSubscription).toHaveBeenCalledOnce(); subject.next({ data: { value: 1 } }); expect(state.data).toEqual({ value: 1 }); }); }); ================================================ FILE: packages/solid-start-urql/src/createSubscription.ts ================================================ import { type MaybeAccessor, asAccessor } from './utils'; import { type AnyVariables, type DocumentInput, type Operation, type OperationContext, type OperationResult, type CombinedError, createRequest, } from '@urql/core'; import { useClient } from './context'; import { createStore, produce, reconcile } from 'solid-js/store'; import { batch, createComputed, createSignal, onCleanup, untrack, } from 'solid-js'; import { type Source, onEnd, pipe, subscribe } from 'wonka'; export type CreateSubscriptionExecute = ( opts?: Partial ) => void; export type CreateSubscriptionArgs< Data, Variables extends AnyVariables = AnyVariables, > = { query: DocumentInput; variables?: MaybeAccessor; context?: MaybeAccessor>; pause?: MaybeAccessor; }; export type CreateSubscriptionState< Data = any, Variables extends AnyVariables = AnyVariables, > = { fetching: boolean; stale: boolean; data?: Data; error?: CombinedError; extensions?: Record; operation?: Operation; }; export type SubscriptionHandler = (prev: R | undefined, data: T) => R; export type CreateSubscriptionResult< Data, Variables extends AnyVariables = AnyVariables, > = [CreateSubscriptionState, CreateSubscriptionExecute]; export const createSubscription = < Data, Result = Data, Variables extends AnyVariables = AnyVariables, >( args: CreateSubscriptionArgs, handler?: SubscriptionHandler ): CreateSubscriptionResult => { const getContext = asAccessor(args.context); const getPause = asAccessor(args.pause); const getVariables = asAccessor(args.variables); const client = useClient(); const request = createRequest(args.query, getVariables() as Variables); const operation = client.createRequestOperation( 'subscription', request, getContext() ); const initialState: CreateSubscriptionState = { operation, fetching: false, data: undefined, error: undefined, extensions: undefined, stale: false, }; const [source, setSource] = createSignal< Source> | undefined >(undefined, { equals: false }); const [state, setState] = createStore>(initialState); createComputed(() => { if (getPause() === true) { setSource(undefined); return; } const context = getContext(); const request = createRequest(args.query, getVariables() as Variables); setSource(() => client.executeSubscription(request, context)); }); createComputed(() => { const s = source(); if (s === undefined) { setState('fetching', false); return; } setState('fetching', true); onCleanup( pipe( s, onEnd(() => { setState( produce(draft => { draft.fetching = false; }) ); }), subscribe(res => { batch(() => { if (res.data !== undefined) { const newData = typeof handler === 'function' ? handler( untrack(() => state.data), res.data ) : (res.data as Result); setState('data', reconcile(newData)); } setState( produce(draft => { draft.stale = !!res.stale; draft.fetching = true; draft.error = res.error; draft.operation = res.operation; draft.extensions = res.extensions; }) ); }); }) ).unsubscribe ); }); const executeSubscription = (opts?: Partial) => { const context: Partial = { ...getContext(), ...opts, }; const request = createRequest(args.query, getVariables() as Variables); setSource(() => client.executeSubscription(request, context)); }; return [state, executeSubscription]; }; ================================================ FILE: packages/solid-start-urql/src/index.ts ================================================ // Re-export everything from @urql/core export * from '@urql/core'; // Context exports export { type UseAction, type UseClient, type UseQuery, type UrqlContext, } from './context'; export { useAction, useClient, useQuery, Provider } from './context'; // Query exports export { createQuery } from './createQuery'; // Mutation exports export { type CreateMutationAction } from './createMutation'; export { createMutation } from './createMutation'; // Subscription exports export { type CreateSubscriptionArgs, type CreateSubscriptionState, type CreateSubscriptionExecute, type CreateSubscriptionResult, type SubscriptionHandler, } from './createSubscription'; export { createSubscription } from './createSubscription'; // Utility exports export { type MaybeAccessor } from './utils'; ================================================ FILE: packages/solid-start-urql/src/utils.ts ================================================ // Re-export utility types and functions from @solid-primitives/utils export { access, asAccessor, type MaybeAccessor, } from '@solid-primitives/utils'; ================================================ FILE: packages/solid-start-urql/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"], "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js" } } ================================================ FILE: packages/solid-start-urql/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import solidPlugin from 'vite-plugin-solid'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, { plugins: [solidPlugin({ hot: false })], esbuild: { jsxImportSource: 'solid-js', }, }); ================================================ FILE: packages/solid-urql/CHANGELOG.md ================================================ # @urql/solid ## 1.0.1 ### Patch Changes - Use `@solid-primitives/utils` for `access` and `MaybeAccessor` utilities instead of custom implementations. This aligns the package with standard Solid ecosystem conventions Submitted by [@davedbase](https://github.com/davedbase) (See [#3837](https://github.com/urql-graphql/urql/pull/3837)) ## 1.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 0.1.2 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 0.1.1 ### Patch Changes - Add type for `hasNext` to the query and mutation results Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3703](https://github.com/urql-graphql/urql/pull/3703)) ## 0.1.0 ### Minor Changes - Initial release Submitted by [@stefanmaric](https://github.com/stefanmaric) (See [#3607](https://github.com/urql-graphql/urql/pull/3607)) ### Patch Changes - Export Provider from the entry Submitted by [@XiNiHa](https://github.com/XiNiHa) (See [#3670](https://github.com/urql-graphql/urql/pull/3670)) - Correctly track query data reads with suspense Submitted by [@XiNiHa](https://github.com/XiNiHa) (See [#3672](https://github.com/urql-graphql/urql/pull/3672)) - feat(solid): reconcile data updates Submitted by [@XiNiHa](https://github.com/XiNiHa) (See [#3674](https://github.com/urql-graphql/urql/pull/3674)) ================================================ FILE: packages/solid-urql/README.md ================================================ # @urql/solid A highly customizable and versatile GraphQL client for SolidJS. > **Note:** This package is for client-side SolidJS applications. If you're building a SolidStart application with SSR, use [`@urql/solid-start`](../solid-start-urql) instead. See the [comparison section](../solid-start-urql#solidjs-vs-solidstart) in the SolidStart package for key differences. ## Installation ```bash npm install @urql/solid @urql/core graphql # or pnpm add @urql/solid @urql/core graphql # or yarn add @urql/solid @urql/core graphql ``` ## Documentation Full documentation is available at [formidable.com/open-source/urql/docs/](https://formidable.com/open-source/urql/docs/). ## Quick Start ```tsx import { createClient, Provider, createQuery } from '@urql/solid'; import { cacheExchange, fetchExchange } from '@urql/core'; import { gql } from '@urql/core'; const client = createClient({ url: 'https://api.example.com/graphql', exchanges: [cacheExchange, fetchExchange], }); const TodosQuery = gql` query { todos { id title } } `; function App() { return ( ); } function TodoList() { const [result] = createQuery({ query: TodosQuery }); return (
    {result().data?.todos.map(todo => (
    {todo.title}
    ))}
    ); } ``` ## When to Use @urql/solid vs @urql/solid-start | Use Case | Package | |----------|---------| | Client-side SPA | `@urql/solid` | | SolidStart with SSR | `@urql/solid-start` | For a detailed comparison of the APIs and when to use each package, see the [SolidJS vs SolidStart comparison](../solid-start-urql#solidjs-vs-solidstart) in the `@urql/solid-start` documentation. ## License MIT ================================================ FILE: packages/solid-urql/jsr.json ================================================ { "name": "@urql/solid", "version": "1.0.1", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/solid-urql/package.json ================================================ { "name": "@urql/solid", "version": "1.0.1", "description": "A highly customizable and versatile GraphQL client for Solid", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/solid-urql" }, "keywords": [ "graphql client", "state management", "cache", "graphql", "exchanges", "solid" ], "main": "dist/urql-solid", "module": "dist/urql-solid.mjs", "types": "dist/urql-solid.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-solid.d.ts", "import": "./dist/urql-solid.mjs", "require": "./dist/urql-solid.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@solidjs/testing-library": "^0.8.2", "@urql/core": "workspace:*", "graphql": "^16.0.0", "jsdom": "^22.1.0", "vite-plugin-solid": "^2.7.0" }, "peerDependencies": { "@urql/core": "^6.0.0", "solid-js": "^1.7.7" }, "dependencies": { "@solid-primitives/utils": "^6.2.1", "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: packages/solid-urql/src/context.ts ================================================ import type { Client } from '@urql/core'; import { createContext, useContext } from 'solid-js'; export const Context = createContext(); export const Provider = Context.Provider; export type UseClient = () => Client; export const useClient: UseClient = () => { const client = useContext(Context); if (process.env.NODE_ENV !== 'production' && client === undefined) { const error = "No client has been specified using urql's Provider. please create a client and add a Provider."; console.error(error); throw new Error(error); } return client!; }; ================================================ FILE: packages/solid-urql/src/createMutation.test.ts ================================================ // @vitest-environment jsdom import { testEffect } from '@solidjs/testing-library'; import { expect, it, describe, vi } from 'vitest'; import { CreateMutationState, createMutation } from './createMutation'; import { OperationResult, OperationResultSource, createClient, gql, } from '@urql/core'; import { makeSubject } from 'wonka'; import { createEffect } from 'solid-js'; const QUERY = gql` mutation { test } `; const client = createClient({ url: '/graphql', exchanges: [], suspense: false, }); vi.mock('./context', () => { const useClient = () => { return client!; }; return { useClient }; }); // Given that it is not possible to directly listen to all store changes it is necessary // to access all relevant parts on which `createEffect` should listen on const markStateDependencies = (state: CreateMutationState) => { state.data; state.error; state.extensions; state.fetching; state.operation; state.stale; }; describe('createMutation', () => { it('should have expected state before and after finish', () => { const subject = makeSubject(); const clientMutation = vi .spyOn(client, 'executeMutation') .mockImplementation( () => subject.source as OperationResultSource ); return testEffect(done => { const [state, execute] = createMutation< { test: boolean }, { variable: number } >(QUERY); createEffect((run: number = 0) => { markStateDependencies(state); switch (run) { case 0: { expect(state).toMatchObject({ data: undefined, stale: false, fetching: false, error: undefined, extensions: undefined, operation: undefined, }); execute({ variable: 1 }); break; } case 1: { expect(state).toMatchObject({ data: undefined, stale: false, fetching: true, error: undefined, extensions: undefined, operation: undefined, }); expect(clientMutation).toHaveBeenCalledTimes(1); subject.next({ data: { test: true }, stale: false }); break; } case 2: { expect(state).toMatchObject({ data: { test: true }, stale: false, fetching: false, error: undefined, extensions: undefined, }); done(); break; } } return run + 1; }); }); }); }); ================================================ FILE: packages/solid-urql/src/createMutation.ts ================================================ import { createStore, reconcile } from 'solid-js/store'; import { type AnyVariables, type DocumentInput, type OperationContext, type Operation, type OperationResult, type CombinedError, createRequest, } from '@urql/core'; import { useClient } from './context'; import { pipe, onPush, filter, take, toPromise } from 'wonka'; import { batch } from 'solid-js'; export type CreateMutationState< Data = any, Variables extends AnyVariables = AnyVariables, > = { /** Indicates whether `createMutation` is currently executing a mutation. */ fetching: boolean; /** Indicates that the mutation result is not fresh. * * @remarks * The `stale` flag is set to `true` when a new result for the mutation * is expected. * This is mostly unused for mutations and will rarely affect you, and * is more relevant for queries. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed mutation. */ data?: Data; /** The {@link OperationResult.error} for the executed mutation. */ error?: CombinedError; /** The {@link OperationResult.extensions} for the executed mutation. */ extensions?: Record; /** The {@link Operation} that the current state is for. * * @remarks * This is the mutation {@link Operation} that has last been executed. * When {@link CreateMutationState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; /** The {@link OperationResult.hasNext} for the executed query. */ hasNext: boolean; }; /** Triggers {@link createMutation} to execute its GraphQL mutation operation. * * @param variables - variables using which the mutation will be executed. * @param context - optionally, context options that will be merged with the hook's * context options and the `Client`’s options. * @returns the {@link OperationResult} of the mutation. * * @remarks * When called, {@link createMutation} will start the GraphQL mutation * it currently holds and use the `variables` passed to it. * * Once the mutation response comes back from the API, its * returned promise will resolve to the mutation’s {@link OperationResult} * and the {@link CreateMutationState} will be updated with the result. * * @example * ```ts * const [result, executeMutation] = createMutation(UpdateTodo); * const start = async ({ id, title }) => { * const result = await executeMutation({ id, title }); * }; */ export type CreateMutationExecute< Data = any, Variables extends AnyVariables = AnyVariables, > = ( variables: Variables, context?: Partial ) => Promise>; /** Result tuple returned by the {@link createMutation} hook. * * @remarks * Similarly to a `createSignal` hook’s return value, * the first element is the {@link createMutation}’s state, updated * as mutations are executed with the second value, which is * used to start mutations and is a {@link CreateMutationExecute} * function. */ export type CreateMutationResult< Data = any, Variables extends AnyVariables = AnyVariables, > = [ CreateMutationState, CreateMutationExecute, ]; /** Hook to create a GraphQL mutation, run by passing variables to the returned execute function. * * @param query - a GraphQL mutation document which `createMutation` will execute. * @returns a {@link CreateMutationResult} tuple of a {@link CreateMutationState} result, * and an execute function to start the mutation. * * @remarks * `createMutation` allows GraphQL mutations to be defined and keeps its state * after the mutation is started with the returned execute function. * * Given a GraphQL mutation document it returns state to keep track of the * mutation state and a {@link CreateMutationExecute} function, which accepts * variables for the mutation to be executed. * Once called, the mutation executes and the state will be updated with * the mutation’s result. * * @example * ```ts * import { gql, createMutation } from '@urql/solid'; * * const UpdateTodo = gql` * mutation ($id: ID!, $title: String!) { * updateTodo(id: $id, title: $title) { * id, title * } * } * `; * * const UpdateTodo = () => { * const [result, executeMutation] = createMutation(UpdateTodo); * const start = async ({ id, title }) => { * const result = await executeMutation({ id, title }); * }; * // ... * }; * ``` */ export const createMutation = < Data = any, Variables extends AnyVariables = AnyVariables, >( query: DocumentInput ): CreateMutationResult => { const client = useClient(); const initialResult: CreateMutationState = { operation: undefined, fetching: false, hasNext: false, stale: false, data: undefined, error: undefined, extensions: undefined, }; const [state, setState] = createStore>(initialResult); const execute = ( variables: Variables, context?: Partial ) => { setState({ ...initialResult, fetching: true }); const request = createRequest(query, variables); return pipe( client.executeMutation(request, context), onPush(result => { batch(() => { setState('data', reconcile(result.data)); setState({ fetching: false, stale: result.stale, error: result.error, extensions: result.extensions, operation: result.operation, hasNext: result.hasNext, }); }); }), filter(result => !result.hasNext), take(1), toPromise ); }; return [state, execute]; }; ================================================ FILE: packages/solid-urql/src/createQuery.test.tsx ================================================ // @vitest-environment jsdom import { expect, it, describe, vi } from 'vitest'; import { CreateQueryState, createQuery } from './createQuery'; import { renderHook, testEffect } from '@solidjs/testing-library'; import { createClient } from '@urql/core'; import { createEffect, createSignal } from 'solid-js'; import { makeSubject } from 'wonka'; import { OperationResult, OperationResultSource } from '@urql/core'; const client = createClient({ url: '/graphql', exchanges: [], suspense: false, }); vi.mock('./context', () => { const useClient = () => { return client!; }; return { useClient }; }); // Given that it is not possible to directly listen to all store changes it is necessary // to access all relevant parts on which `createEffect` should listen on const markStateDependencies = (state: CreateQueryState) => { state.data; state.error; state.extensions; state.fetching; state.operation; state.stale; }; describe('createQuery', () => { it('should fetch when query is resumed', () => { const subject = makeSubject, 'data'>>(); const executeQuery = vi .spyOn(client, 'executeQuery') .mockImplementation( () => subject.source as OperationResultSource ); return testEffect(done => { const [pause, setPause] = createSignal(true); const [state] = createQuery<{ test: boolean }, { variable: number }>({ query: '{ test }', pause: pause, }); createEffect((run: number = 0) => { markStateDependencies(state); switch (run) { case 0: { expect(state.fetching).toEqual(false); expect(executeQuery).not.toHaveBeenCalled(); setPause(false); break; } case 1: { expect(state.fetching).toEqual(true); expect(executeQuery).toHaveBeenCalledOnce(); subject.next({ data: { test: true } }); break; } case 2: { expect(state.fetching).toEqual(false); expect(state.data).toStrictEqual({ test: true }); done(); break; } } return run + 1; }); }); }); it('should override pause when execute via refetch', () => { const subject = makeSubject, 'data'>>(); const executeQuery = vi .spyOn(client, 'executeQuery') .mockImplementation( () => subject.source as OperationResultSource ); return testEffect(done => { const [state, refetch] = createQuery< { test: boolean }, { variable: number } >({ query: '{ test }', pause: true, }); createEffect((run: number = 0) => { markStateDependencies(state); switch (run) { case 0: { expect(state.fetching).toEqual(false); expect(executeQuery).not.toBeCalled(); refetch(); break; } case 1: { expect(state.fetching).toEqual(true); expect(executeQuery).toHaveBeenCalledOnce(); subject.next({ data: { test: true } }); break; } case 2: { expect(state.fetching).toEqual(false); expect(state.data).toStrictEqual({ test: true }); done(); break; } } return run + 1; }); }); }); it('should trigger refetch on variables change', () => { const subject = makeSubject, 'data'>>(); const executeQuery = vi .spyOn(client, 'executeQuery') .mockImplementation( () => subject.source as OperationResultSource ); return testEffect(done => { const [variables, setVariables] = createSignal<{ variable: number }>({ variable: 1, }); const [state] = createQuery<{ test: boolean }, { variable: number }>({ query: '{ test }', variables: variables, }); createEffect((run: number = 0) => { markStateDependencies(state); switch (run) { case 0: { expect(state.fetching).toEqual(true); subject.next({ data: { test: true } }); break; } case 1: { expect(state.fetching).toEqual(false); expect(state.data).toEqual({ test: true }); setVariables({ variable: 2 }); break; } case 2: { expect(state.fetching).toEqual(true); expect(executeQuery).toHaveBeenCalledTimes(2); subject.next({ data: { test: false } }); break; } case 3: { expect(state.fetching).toEqual(false); expect(state.data).toEqual({ test: false }); done(); break; } } return run + 1; }); }); }); it('should receive data', () => { const subject = makeSubject, 'data'>>(); const executeQuery = vi .spyOn(client, 'executeQuery') .mockImplementation( () => subject.source as OperationResultSource ); return testEffect(done => { const [state] = createQuery<{ test: boolean }, { variable: number }>({ query: '{ test }', }); createEffect((run: number = 0) => { markStateDependencies(state); switch (run) { case 0: { expect(state.fetching).toEqual(true); expect(state.data).toBeUndefined(); subject.next({ data: { test: true } }); break; } case 1: { expect(state.fetching).toEqual(false); expect(state.data).toStrictEqual({ test: true }); expect(executeQuery).toHaveBeenCalledTimes(1); done(); break; } } return run + 1; }); }); }); it('should unsubscribe on teardown', () => { const subject = makeSubject, 'data'>>(); vi.spyOn(client, 'executeQuery').mockImplementation( () => subject.source as OperationResultSource ); const { result: [state], cleanup, } = renderHook(() => createQuery<{ test: number }, { variable: number }>({ query: '{ test }', }) ); return testEffect(done => { markStateDependencies(state); createEffect((run: number = 0) => { switch (run) { case 0: { expect(state.fetching).toEqual(true); cleanup(); break; } case 1: { expect(state.fetching).toEqual(false); done(); break; } } return run + 1; }); }); }); }); ================================================ FILE: packages/solid-urql/src/createQuery.ts ================================================ import { type AnyVariables, type OperationContext, type DocumentInput, type OperationResult, type RequestPolicy, createRequest, } from '@urql/core'; import { batch, createComputed, createMemo, createResource, createSignal, onCleanup, } from 'solid-js'; import { createStore, produce, reconcile } from 'solid-js/store'; import { useClient } from './context'; import { type MaybeAccessor, asAccessor } from './utils'; import type { Source, Subscription } from 'wonka'; import { onEnd, pipe, subscribe } from 'wonka'; /** Triggers {@link createQuery} to execute a new GraphQL query operation. * * @remarks * When called, {@link createQuery} will re-execute the GraphQL query operation * it currently holds, even if {@link CreateQueryArgs.pause} is set to `true`. * * This is useful for executing a paused query or re-executing a query * and get a new network result, by passing a new request policy. * * ```ts * const [result, reExecuteQuery] = createQuery({ query }); * * const refresh = () => { * // Re-execute the query with a network-only policy, skipping the cache * reExecuteQuery({ requestPolicy: 'network-only' }); * }; * ``` * */ export type CreateQueryExecute = (opts?: Partial) => void; /** State of the current query, your {@link createQuery} hook is executing. * * @remarks * `CreateQueryState` is returned (in a tuple) by {@link createQuery} and * gives you the updating {@link OperationResult} of GraphQL queries. * * Even when the query and variables passed to {@link createQuery} change, * this state preserves the prior state and sets the `fetching` flag to * `true`. * This allows you to display the previous state, while implementing * a separate loading indicator separately. */ export type CreateQueryState< Data = any, Variables extends AnyVariables = AnyVariables, > = OperationResult & { /** Indicates whether `createQuery` is waiting for a new result. * * @remarks * When `createQuery` is passed a new query and/or variables, it will * start executing the new query operation and `fetching` is set to * `true` until a result arrives. * * Hint: This is subtly different than whether the query is actually * fetching, and doesn’t indicate whether a query is being re-executed * in the background. For this, see {@link CreateQueryState.stale}. */ fetching: boolean; }; /** * Input arguments for the {@link createQuery} hook. */ export type CreateQueryArgs< Data = any, Variables extends AnyVariables = AnyVariables, > = { /** The GraphQL query that `createQuery` executes. */ query: DocumentInput; /** The variables for the GraphQL {@link CreateQueryArgs.query} that `createQuery` executes. */ variables?: MaybeAccessor; /** Updates the {@link RequestPolicy} for the executed GraphQL query operation. * * @remarks * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation * that `createQuery` executes, and indicates a caching strategy for cache exchanges. * * For example, when set to `'cache-and-network'`, {@link createQuery} will * receive a cached result with `stale: true` and an API request will be * sent in the background. * * @see {@link OperationContext.requestPolicy} for where this value is set. */ requestPolicy?: MaybeAccessor; /** Updates the {@link OperationContext} for the executed GraphQL query operation. * * @remarks * `context` may be passed to {@link createQuery}, to update the {@link OperationContext} * of a query operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * In order to re-execute query on context change pass {@link Accessor} instead * of raw value. */ context?: MaybeAccessor>; /** Prevents {@link createQuery} from automatically executing GraphQL query operations. * * @remarks * `pause` may be set to `true` to stop {@link createQuery} from executing * automatically. The hook will stop receiving updates from the {@link Client} * and won’t execute the query operation, until either it’s set to `false` * or the {@link CreateQueryExecute} function is called. */ pause?: MaybeAccessor; }; /** Result tuple returned by the {@link createQuery} hook. * * @remarks * the first element is the {@link createQuery}’s result and state, * a {@link CreateQueryState} object, * and the second is used to imperatively re-execute the query * via a {@link CreateQueryExecute} function. */ export type CreateQueryResult< Data = any, Variables extends AnyVariables = AnyVariables, > = [CreateQueryState, CreateQueryExecute]; /** Hook to run a GraphQL query and get updated GraphQL results. * * @param args - a {@link CreateQueryArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link CreateQueryResult} tuple of a {@link CreateQueryState} result, and re-execute function. * * @remarks * `createQuery` allows GraphQL queries to be defined and executed. * Given {@link CreateQueryArgs.query}, it executes the GraphQL query with the * context’s {@link Client}. * * The returned result updates when the `Client` has new results * for the query, and changes when your input `args` change. * * Additionally, if the `suspense` option is enabled on the `Client`, * the `createQuery` hook will suspend instead of indicating that it’s * waiting for a result via {@link CreateQueryState.fetching}. * * @example * ```tsx * import { gql, createQuery } from '@urql/solid'; * * const TodosQuery = gql` * query { todos { id, title } } * `; * * const Todos = () => { * const [result, reExecuteQuery] = createQuery({ * query: TodosQuery, * }); * // ... * }; * ``` */ export const createQuery = < Data = any, Variables extends AnyVariables = AnyVariables, >( args: CreateQueryArgs ): CreateQueryResult => { const client = useClient(); const getContext = asAccessor(args.context); const getPause = asAccessor(args.pause); const getRequestPolicy = asAccessor(args.requestPolicy); const getVariables = asAccessor(args.variables); const [source, setSource] = createSignal< Source> | undefined >(undefined, { equals: false }); // Combine suspense param coming from context and client with context being priority const isSuspense = createMemo(() => { const ctx = getContext(); if (ctx !== undefined && ctx.suspense !== undefined) { return ctx.suspense; } return client.suspense; }); const request = createRequest(args.query, getVariables() as any); const context: Partial = { requestPolicy: getRequestPolicy(), ...getContext(), }; const operation = client.createRequestOperation('query', request, context); const initialResult: CreateQueryState = { operation: operation, fetching: false, data: undefined, error: undefined, extensions: undefined, hasNext: false, stale: false, }; const [result, setResult] = createStore>(initialResult); createComputed(() => { if (getPause() === true) { setSource(undefined); return; } const request = createRequest(args.query, getVariables() as any); const context: Partial = { requestPolicy: getRequestPolicy(), ...getContext(), }; setSource(() => client.executeQuery(request, context)); }); createComputed(() => { const s = source(); if (s === undefined) { setResult( produce(draft => { draft.fetching = false; draft.stale = false; draft.hasNext = false; }) ); return; } setResult( produce(draft => { draft.fetching = true; draft.stale = false; draft.hasNext = false; }) ); onCleanup( pipe( s, onEnd(() => { setResult( produce(draft => { draft.fetching = false; draft.stale = false; draft.hasNext = false; }) ); }), subscribe(res => { batch(() => { setResult('data', reconcile(res.data)); setResult( produce(draft => { draft.stale = !!res.stale; draft.fetching = false; draft.error = res.error; draft.operation = res.operation; draft.extensions = res.extensions; draft.hasNext = res.hasNext; }) ); }); }) ).unsubscribe ); }); const [dataResource, { refetch }] = createResource< CreateQueryState, Source> | undefined >(source, source => { let sub: Subscription | void; if (source === undefined) { return Promise.resolve(result); } return new Promise>(resolve => { let hasResult = false; sub = pipe( source, subscribe(() => { if (!result.fetching && !result.stale) { if (sub) sub.unsubscribe(); hasResult = true; resolve(result); } }) ); if (hasResult) { sub.unsubscribe(); } }); }); const executeQuery: CreateQueryExecute = opts => { const request = createRequest(args.query, getVariables() as any); const context: Partial = { requestPolicy: getRequestPolicy(), ...getContext(), ...opts, }; setSource(() => client.executeQuery(request, context)); if (isSuspense()) { refetch(); } }; const handler = { get( target: CreateQueryState, prop: keyof CreateQueryState ): any { if (isSuspense() && prop === 'data') { const resource = dataResource(); if (resource === undefined) return undefined; } return Reflect.get(target, prop); }, }; const proxy = new Proxy(result, handler); return [proxy, executeQuery]; }; ================================================ FILE: packages/solid-urql/src/createSubscription.test.ts ================================================ // @vitest-environment jsdom import { renderHook, testEffect } from '@solidjs/testing-library'; import { OperationResult, OperationResultSource, createClient, createRequest, gql, } from '@urql/core'; import { expect, it, describe, vi } from 'vitest'; import { makeSubject } from 'wonka'; import { CreateSubscriptionState, createSubscription, } from './createSubscription'; import { createEffect, createSignal } from 'solid-js'; const QUERY = gql` subscription { value } `; const client = createClient({ url: '/graphql', exchanges: [], suspense: false, }); vi.mock('./context', () => { const useClient = () => { return client!; }; return { useClient }; }); // Given that it is not possible to directly listen to all store changes it is necessary // to access all relevant parts on which `createEffect` should listen on const markStateDependencies = (state: CreateSubscriptionState) => { state.data; state.error; state.extensions; state.fetching; state.operation; state.stale; }; describe('createSubscription', () => { it('should receive data', () => { return testEffect(done => { const subject = makeSubject, 'data'>>(); const executeQuery = vi .spyOn(client, 'executeSubscription') .mockImplementation( () => subject.source as OperationResultSource ); const request = createRequest(QUERY, undefined); const operation = client.createRequestOperation('subscription', request); const [state] = createSubscription< { value: number }, { value: number }, { variable: number } >({ query: QUERY, }); createEffect((run: number = 0) => { markStateDependencies(state); switch (run) { case 0: { expect(state).toMatchObject({ data: undefined, stale: false, operation: operation, error: undefined, extensions: undefined, fetching: true, }); expect(executeQuery).toEqual(expect.any(Function)); expect(executeQuery).toHaveBeenCalledOnce(); expect(client.executeSubscription).toBeCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: {}, }, undefined ); subject.next({ data: { value: 0 } }); break; } case 1: { expect(state.data).toEqual({ value: 0 }); subject.next({ data: { value: 1 } }); break; } case 2: { expect(state.data).toEqual({ value: 1 }); // expect(state.fetching).toEqual(true); subject.complete(); break; } case 3: { expect(state.fetching).toEqual(false); expect(state.data).toEqual({ value: 1 }); done(); } } return run + 1; }); }); }); it('should call handler', () => { const handler = vi.fn(); const subject = makeSubject, 'data'>>(); vi.spyOn(client, 'executeSubscription').mockImplementation( () => subject.source as OperationResultSource ); return testEffect(done => { const [state] = createSubscription< { value: number }, { value: number }, { variable: number } >( { query: QUERY, }, handler ); createEffect((run: number = 0) => { markStateDependencies(state); switch (run) { case 0: { expect(state.fetching).toEqual(true); subject.next({ data: { value: 0 } }); break; } case 1: { expect(handler).toHaveBeenCalledOnce(); expect(handler).toBeCalledWith(undefined, { value: 0 }); done(); break; } } return run + 1; }); }); }); it('should unsubscribe on teardown', () => { const subject = makeSubject, 'data'>>(); vi.spyOn(client, 'executeSubscription').mockImplementation( () => subject.source as OperationResultSource ); const { result: [state], cleanup, } = renderHook(() => createSubscription<{ value: number }, { variable: number }>({ query: QUERY, }) ); return testEffect(done => createEffect((run: number = 0) => { if (run === 0) { expect(state.fetching).toEqual(true); cleanup(); } if (run === 1) { expect(state.fetching).toEqual(false); done(); } return run + 1; }) ); }); it('should skip executing query when paused', () => { const subject = makeSubject, 'data'>>(); vi.spyOn(client, 'executeSubscription').mockImplementation( () => subject.source as OperationResultSource ); return testEffect(done => { const [pause, setPause] = createSignal(true); const [state] = createSubscription< { value: number }, { value: number }, { variable: number } >({ query: QUERY, pause: pause }); createEffect((run: number = 0) => { switch (run) { case 0: { expect(state.fetching).toBe(false); setPause(false); break; } case 1: { expect(state.fetching).toBe(true); expect(state.data).toBeUndefined(); subject.next({ data: { value: 1 } }); break; } case 2: { expect(state.data).toStrictEqual({ value: 1 }); done(); break; } } return run + 1; }); }); }); it('should override pause when execute executeSubscription', () => { const subject = makeSubject, 'data'>>(); const executeQuery = vi .spyOn(client, 'executeSubscription') .mockImplementation( () => subject.source as OperationResultSource ); return testEffect(done => { const [state, executeSubscription] = createSubscription< { value: number }, { value: number }, { variable: number } >({ query: QUERY, pause: true, }); createEffect((run: number = 0) => { markStateDependencies(state); switch (run) { case 0: { expect(state.fetching).toEqual(false); expect(executeQuery).not.toBeCalled(); executeSubscription(); break; } case 1: { expect(state.fetching).toEqual(true); expect(executeQuery).toHaveBeenCalledOnce(); subject.next({ data: { value: 1 } }); break; } case 2: { expect(state.data).toStrictEqual({ value: 1 }); done(); break; } } return run + 1; }); }); }); it('should aggregate results', () => { const subject = makeSubject, 'data'>>(); vi.spyOn(client, 'executeSubscription').mockImplementation( () => subject.source as OperationResultSource ); return testEffect(done => { const [state] = createSubscription< { value: number }, { merged: number }, { variable: number } >( { query: QUERY, }, (prev, next) => { if (prev === undefined) { return { merged: 0 + next.value, }; } return { merged: prev.merged + next.value }; } ); createEffect((run: number = 0) => { markStateDependencies(state); switch (run) { case 0: { expect(state.fetching).toEqual(true); subject.next({ data: { value: 1 } }); break; } case 1: { expect(state.data).toEqual({ merged: 1 }); subject.next({ data: { value: 2 } }); break; } case 2: { expect(state.data).toEqual({ merged: 3 }); done(); break; } } return run + 1; }); }); }); }); ================================================ FILE: packages/solid-urql/src/createSubscription.ts ================================================ import { type MaybeAccessor, asAccessor } from './utils'; import { type AnyVariables, type DocumentInput, type Operation, type OperationContext, type OperationResult, type CombinedError, createRequest, } from '@urql/core'; import { useClient } from './context'; import { createStore, produce, reconcile } from 'solid-js/store'; import { batch, createComputed, createSignal, onCleanup, untrack, } from 'solid-js'; import { type Source, onEnd, pipe, subscribe } from 'wonka'; /** Triggers {@link createSubscription} to re-execute a GraphQL subscription operation. * * @param opts - optionally, context options that will be merged with the hook's * {@link CreateSubscriptionArgs.context} options and the `Client`’s options. * * @remarks * When called, {@link createSubscription} will restart the GraphQL subscription * operation it currently holds. If {@link CreateSubscriptionArgs.pause} is set * to `true`, it will start executing the subscription. * * ```ts * const [result, executeSubscription] = createSubscription({ * query, * pause: true, * }); * * const start = () => { * executeSubscription(); * }; * ``` */ export type CreateSubscriptionExecute = ( opts?: Partial ) => void; /** Input arguments for the {@link createSubscription} hook. */ export type CreateSubscriptionArgs< Data, Variables extends AnyVariables = AnyVariables, > = { /** The GraphQL subscription document that `createSubscription` executes. */ query: DocumentInput; /** The variables for the GraphQL subscription that `createSubscription` executes. */ variables?: MaybeAccessor; /** Updates the {@link OperationContext} for the executed GraphQL subscription operation. * * @remarks * `context` may be passed to {@link createSubscription}, to update the {@link OperationContext} * of a subscription operation. This may be used to update the `context` that exchanges * will receive for a single hook. */ context?: MaybeAccessor>; /** Prevents {@link createSubscription} from automatically starting GraphQL subscriptions. * * @remarks * `pause` may be set to `true` to stop {@link createSubscription} from starting its subscription * automatically. The hook will stop receiving updates from the {@link Client} * and won’t start the subscription operation, until either it’s set to `false` * or the {@link CreateSubscriptionExecute} function is called. */ pause?: MaybeAccessor; }; export type CreateSubscriptionState< Data = any, Variables extends AnyVariables = AnyVariables, > = { /** Indicates whether `createSubscription`’s subscription is active. * * @remarks * When `createSubscription` starts a subscription, the `fetching` flag * is set to `true` and will remain `true` until the subscription * completes on the API, or the {@link CreateSubscriptionArgs.pause} * flag is set to `true`. */ fetching: boolean; /** Indicates that the subscription result is not fresh. * * @remarks * This is mostly unused for subscriptions and will rarely affect you, and * is more relevant for queries. * * @see {@link OperationResult.stale} for the source of this value. */ stale: boolean; /** The {@link OperationResult.data} for the executed subscription, or data returned by a handler. * * @remarks * `data` will be set to the last {@link OperationResult.data} value * received for the subscription. * * It will instead be set to the values that {@link SubscriptionHandler} * returned, if a handler has been passed to {@link CreateSubscription}. */ data?: Data; /** The {@link OperationResult.error} for the executed subscription. */ error?: CombinedError; /** The {@link OperationResult.extensions} for the executed mutation. */ extensions?: Record; /** The {@link Operation} that the current state is for. * * @remarks * This is the subscription {@link Operation} that is currently active. * When {@link CreateSubscriptionState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation?: Operation; }; /** Combines previous data with an incoming subscription result’s data. * * @remarks * A `SubscriptionHandler` may be passed to {@link createSubscription} to * aggregate subscription results into a combined {@link CreateSubscriptionState.data} * value. * * This is useful when a subscription event delivers a single item, while * you’d like to display a list of events. * * @example * ```ts * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * const combineNotifications = (notifications = [], data) => { * return [...notifications, data.newNotification]; * }; * * const [result, executeSubscription] = createSubscription( * { query: NotificationsSubscription }, * combineNotifications, * ); * ``` */ export type SubscriptionHandler = (prev: R | undefined, data: T) => R; /** Result tuple returned by the {@link createSubscription} hook. * * @remarks * Similarly to a `createSignal` hook’s return value, * the first element is the {@link createSubscription}’s state, * a {@link CreateSubscriptionState} object, * and the second is used to imperatively re-execute or start the subscription * via a {@link CreateMutationExecute} function. */ export type CreateSubscriptionResult< Data, Variables extends AnyVariables = AnyVariables, > = [CreateSubscriptionState, CreateSubscriptionExecute]; /** Hook to run a GraphQL subscription and get updated GraphQL results. * * @param args - a {@link CreateSubscriptionArgs} object, to pass a `query`, `variables`, and options. * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results. * @returns a {@link CreateSubscriptionResponse} tuple of a {@link CreateSubscriptionState} result, * and an execute function. * * @remarks * `createSubscription` allows GraphQL subscriptions to be defined and executed. * Given {@link CreateSubscriptionArgs.query}, it executes the GraphQL subscription with the * context’s {@link Client}. * * The returned result updates when the `Client` has new results * for the subscription, and `data` is updated with the result’s data * or with the `data` that a `handler` returns. * * @example * ```ts * import { gql, createSubscription } from '@urql/solid'; * * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * const combineNotifications = (notifications = [], data) => { * return [...notifications, data.newNotification]; * }; * * const Notifications = () => { * const [result, executeSubscription] = createSubscription( * { query: NotificationsSubscription }, * combineNotifications, * ); * // ... * }; * ``` */ export const createSubscription = < Data, Result = Data, Variables extends AnyVariables = AnyVariables, >( args: CreateSubscriptionArgs, handler?: SubscriptionHandler ): CreateSubscriptionResult => { const getContext = asAccessor(args.context); const getPause = asAccessor(args.pause); const getVariables = asAccessor(args.variables); const client = useClient(); const request = createRequest(args.query, getVariables() as Variables); const operation = client.createRequestOperation( 'subscription', request, getContext() ); const initialState: CreateSubscriptionState = { operation, fetching: false, data: undefined, error: undefined, extensions: undefined, stale: false, }; const [source, setSource] = createSignal< Source> | undefined >(undefined, { equals: false }); const [state, setState] = createStore>(initialState); createComputed(() => { if (getPause() === true) { setSource(undefined); return; } const context = getContext(); const request = createRequest(args.query, getVariables() as Variables); setSource(() => client.executeSubscription(request, context)); }); createComputed(() => { const s = source(); if (s === undefined) { setState('fetching', false); return; } setState('fetching', true); onCleanup( pipe( s, onEnd(() => { setState( produce(draft => { draft.fetching = false; }) ); }), subscribe(res => { batch(() => { if (res.data !== undefined) { const newData = typeof handler === 'function' ? handler( untrack(() => state.data), res.data ) : (res.data as Result); setState('data', reconcile(newData)); } setState( produce(draft => { draft.stale = !!res.stale; draft.fetching = true; draft.error = res.error; draft.operation = res.operation; draft.extensions = res.extensions; }) ); }); }) ).unsubscribe ); }); const executeSubscription = (opts?: Partial) => { const context: Partial = { ...getContext(), ...opts, }; const request = createRequest(args.query, getVariables() as Variables); setSource(() => client.executeSubscription(request, context)); }; return [state, executeSubscription]; }; ================================================ FILE: packages/solid-urql/src/index.ts ================================================ export * from '@urql/core'; export { type UseClient } from './context'; export { useClient, Provider } from './context'; export { type CreateMutationState, type CreateMutationExecute, type CreateMutationResult, } from './createMutation'; export { createMutation } from './createMutation'; export { type CreateQueryArgs, type CreateQueryState, type CreateQueryExecute, type CreateQueryResult, } from './createQuery'; export { createQuery } from './createQuery'; export { type CreateSubscriptionArgs, type CreateSubscriptionState, type CreateSubscriptionExecute, type CreateSubscriptionResult, type SubscriptionHandler, } from './createSubscription'; export { createSubscription } from './createSubscription'; ================================================ FILE: packages/solid-urql/src/suspense.test.tsx ================================================ /** @jsxImportSource solid-js */ // @vitest-environment jsdom import { describe, it, vi } from 'vitest'; import { OperationResult, OperationResultSource, createClient, } from '@urql/core'; import { createQuery } from './createQuery'; import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library'; import { Provider } from './context'; import { Suspense } from 'solid-js'; import { makeSubject } from 'wonka'; describe('createQuery suspense', () => { it('should not suspend', async () => { const client = createClient({ url: '/graphql', exchanges: [], suspense: false, }); const subject = makeSubject, 'data'>>(); vi.spyOn(client, 'executeQuery').mockImplementation( () => subject.source as OperationResultSource ); const Page = () => { const [state, refetch] = createQuery< { test: boolean }, { variable: number } >({ query: '{ test }', }); return (
    ); }; render(() => ( )); subject.next({ data: { test: true } }); await waitFor(() => screen.getByText('data: true')); fireEvent.click(screen.getByTestId('refetch')); subject.next({ data: { test: false } }); await waitFor(() => screen.getByText('data: false')); }); it('should suspend', async () => { const client = createClient({ url: '/graphql', exchanges: [], suspense: true, }); const subject = makeSubject, 'data'>>(); vi.spyOn(client, 'executeQuery').mockImplementation( () => subject.source as OperationResultSource ); const Page = () => { const [state] = createQuery<{ test: boolean }, { variable: number }>({ query: '{ test }', }); return
    data: {String(state.data?.test)}
    ; }; render(() => ( )); await waitFor(() => screen.getByText('loading')); subject.next({ data: { test: true } }); await waitFor(() => screen.getByText('data: true')); }); it('context suspend should override client suspend', async () => { const client = createClient({ url: '/graphql', exchanges: [], suspense: false, }); const subject = makeSubject, 'data'>>(); vi.spyOn(client, 'executeQuery').mockImplementation( () => subject.source as OperationResultSource ); const Page = () => { const [state] = createQuery<{ test: boolean }, { variable: number }>({ query: '{ test }', context: { suspense: true, }, }); return
    data: {String(state.data?.test)}
    ; }; render(() => ( )); await waitFor(() => screen.getByText('loading')); subject.next({ data: { test: true } }); await waitFor(() => screen.getByText('data: true')); }); }); ================================================ FILE: packages/solid-urql/src/utils.ts ================================================ // Re-export utility types and functions from @solid-primitives/utils export { access, asAccessor, type MaybeAccessor, } from '@solid-primitives/utils'; ================================================ FILE: packages/solid-urql/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"], "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js" } } ================================================ FILE: packages/solid-urql/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import solidPlugin from 'vite-plugin-solid'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, { plugins: [solidPlugin({ hot: false })], }); ================================================ FILE: packages/storage-rn/CHANGELOG.md ================================================ # Changelog ## 1.1.2 ### Patch Changes - Add support for `@react-native-async-storage/async-storage` v2.0.0 Submitted by [@nhangeland](https://github.com/nhangeland) (See [#3836](https://github.com/urql-graphql/urql/pull/3836)) ## 1.1.1 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) ## 1.1.0 ### Minor Changes - Bump peer-dependency of `@react-native-community/netinfo` to allow v11 Submitted by [@albinhubsch](https://github.com/albinhubsch) (See [#3485](https://github.com/urql-graphql/urql/pull/3485)) ## 1.0.3 ### Patch Changes - Switch `react` imports to namespace imports, and update build process for CommonJS outputs to interoperate with `__esModule` marked modules again Submitted by [@kitten](https://github.com/kitten) (See [#3251](https://github.com/urql-graphql/urql/pull/3251)) ## 1.0.2 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 1.0.1 ### Patch Changes - Add TSDocs to `@urql/*` packages Submitted by [@kitten](https://github.com/kitten) (See [#3079](https://github.com/urql-graphql/urql/pull/3079)) ## 1.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ## 0.1.1 ### Patch Changes - ⚠️ Fix issue where the in-memory cache would not be cleared, by [@benmechen](https://github.com/benmechen) (See [#2222](https://github.com/FormidableLabs/urql/pull/2222)) ## v0.1.0 **Initial Release** ================================================ FILE: packages/storage-rn/LICENCE ================================================ MIT License Copyright (c) 2018–2020 Formidable Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/storage-rn/README.md ================================================ # @urql/storage-rn `@urql/storage-rn` is a Graphcache offline storage for React Native. It is compatible for both plain React Native and Expo apps (including managed workflow), but it has a two peer dependencies - [Async Storage](https://react-native-async-storage.github.io/async-storage/) and [NetInfo](https://github.com/react-native-netinfo/react-native-netinfo) - which must be installed separately. AsyncStorage will be used to persist the data, and NetInfo will be used to determine when the app is online and offline. ## Quick Start Guide Install NetInfo ([RN](https://github.com/react-native-netinfo/react-native-netinfo) | [Expo](https://docs.expo.dev/versions/latest/sdk/netinfo/)) and AsyncStorage ([RN](https://react-native-async-storage.github.io/async-storage/docs/install) | [Expo](https://docs.expo.dev/versions/v42.0.0/sdk/async-storage/)). Install `@urql/storage-rn` alongside `urql` and `@urql/exchange-graphcache`: ```sh yarn add @urql/storage-rn # or npm install --save @urql/storage-rn ``` Then add it to the offline exchange: ```js import { createClient, fetchExchange } from 'urql'; import { offlineExchange } from '@urql/exchange-graphcache'; import { makeAsyncStorage } from '@urql/storage-rn'; const storage = makeAsyncStorage({ dataKey: 'graphcache-data', // The AsyncStorage key used for the data (defaults to graphcache-data) metadataKey: 'graphcache-metadata', // The AsyncStorage key used for the metadata (defaults to graphcache-metadata) maxAge: 7, // How long to persist the data in storage (defaults to 7 days) }); const cache = offlineExchange({ schema, storage, updates: { /* ... */ }, optimistic: { /* ... */ }, }); const client = createClient({ url: 'http://localhost:3000/graphql', exchanges: [cache, fetchExchange], }); ``` ================================================ FILE: packages/storage-rn/jsr.json ================================================ { "name": "@urql/storage-rn", "version": "1.1.2", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/storage-rn/package.json ================================================ { "name": "@urql/storage-rn", "version": "1.1.2", "sideEffects": false, "description": "Graphcache offline storage for React Native", "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/storage-rn" }, "keywords": [ "urql", "graphql client", "graphql", "exchanges", "react native", "offline", "storage" ], "main": "dist/urql-storage-rn", "module": "dist/urql-storage-rn.mjs", "types": "dist/urql-storage-rn.d.ts", "source": "src/index.ts", "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "exports": { ".": { "types": "./dist/urql-storage-rn.d.ts", "import": "./dist/urql-storage-rn.mjs", "require": "./dist/urql-storage-rn.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "scripts": { "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "peerDependencies": { "@react-native-async-storage/async-storage": "^1.15.5 || ^2.0.0", "@react-native-community/netinfo": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^11.0.0", "@urql/exchange-graphcache": ">=5.0.0" }, "devDependencies": { "@react-native-async-storage/async-storage": "^2.2.0", "@react-native-community/netinfo": "^11.2.1", "@urql/core": "workspace:*", "@urql/exchange-graphcache": "workspace:*" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: packages/storage-rn/src/index.ts ================================================ export { makeAsyncStorage } from './makeAsyncStorage'; ================================================ FILE: packages/storage-rn/src/makeAsyncStorage.test.ts ================================================ import { vi, expect, it, describe } from 'vitest'; vi.mock('@react-native-community/netinfo', () => ({ addEventListener: () => 'addEventListener', default: { addEventListener: () => 'addEventListener', }, })); vi.mock('@react-native-async-storage/async-storage', () => ({ default: { setItem: () => 'setItem', getItem: () => 'getItem', getAllKeys: () => 'getAllKeys', removeItem: () => 'removeItem', }, setItem: () => 'setItem', getItem: () => 'getItem', getAllKeys: () => 'getAllKeys', removeItem: () => 'removeItem', })); import AsyncStorage from '@react-native-async-storage/async-storage'; import NetInfo from '@react-native-community/netinfo'; import { makeAsyncStorage } from './makeAsyncStorage'; const request = [ { query: 'something something', variables: { foo: 'bar' }, }, ]; const serializedRequest = '[{"query":"something something","variables":{"foo":"bar"}}]'; const entires = { hello: 'world', }; const serializedEntries = '{"hello":"world"}'; describe('makeAsyncStorage', () => { describe('writeMetadata', () => { it('writes metadata to async storage', async () => { const setItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'setItem').mockImplementationOnce(setItemSpy); const storage = makeAsyncStorage(); if (storage && storage.writeMetadata) { await storage.writeMetadata(request); } expect(setItemSpy).toHaveBeenCalledWith( 'graphcache-metadata', serializedRequest ); }); it('writes metadata using a custom key', async () => { const setItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'setItem').mockImplementationOnce(setItemSpy); const storage = makeAsyncStorage({ metadataKey: 'my-custom-key' }); if (storage && storage.writeMetadata) { await storage.writeMetadata(request); } expect(setItemSpy).toHaveBeenCalledWith( 'my-custom-key', serializedRequest ); }); }); describe('readMetadata', () => { it('returns an empty array if no metadata is found', async () => { const getItemSpy = vi.fn().mockResolvedValue(null); vi.spyOn(AsyncStorage, 'getItem').mockImplementationOnce(getItemSpy); const storage = makeAsyncStorage(); if (storage && storage.readMetadata) { const result = await storage.readMetadata(); expect(getItemSpy).toHaveBeenCalledWith('graphcache-metadata'); expect(result).toEqual([]); } }); it('returns the parsed JSON correctly', async () => { const getItemSpy = vi.fn().mockResolvedValue(serializedRequest); vi.spyOn(AsyncStorage, 'getItem').mockImplementationOnce(getItemSpy); const storage = makeAsyncStorage(); if (storage && storage.readMetadata) { const result = await storage.readMetadata(); expect(getItemSpy).toHaveBeenCalledWith('graphcache-metadata'); expect(result).toEqual(request); } }); it('reads metadata using a custom key', async () => { const getItemSpy = vi.fn().mockResolvedValue(serializedRequest); vi.spyOn(AsyncStorage, 'getItem').mockImplementationOnce(getItemSpy); const storage = makeAsyncStorage({ metadataKey: 'my-custom-key' }); if (storage && storage.readMetadata) { const result = await storage.readMetadata(); expect(getItemSpy).toHaveBeenCalledWith('my-custom-key'); expect(result).toEqual(request); } }); it('returns an empty array if json.parse errors', async () => { const getItemSpy = vi.fn().mockResolvedValue('surprise!'); vi.spyOn(AsyncStorage, 'getItem').mockImplementationOnce(getItemSpy); const storage = makeAsyncStorage(); if (storage && storage.readMetadata) { const result = await storage.readMetadata(); expect(getItemSpy).toHaveBeenCalledWith('graphcache-metadata'); expect(result).toEqual([]); } }); }); describe('writeData', () => { it('writes data to async storage', async () => { vi.spyOn(Date.prototype, 'valueOf').mockReturnValueOnce(1632209690641); const dayStamp = 18891; const setItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'setItem').mockImplementationOnce(setItemSpy); const storage = makeAsyncStorage(); if (storage && storage.writeData) { await storage.writeData(entires); } expect(setItemSpy).toHaveBeenCalledWith( 'graphcache-data', `{"${dayStamp}":${serializedEntries}}` ); }); it('writes data to async storage using custom key', async () => { vi.spyOn(Date.prototype, 'valueOf').mockReturnValueOnce(1632209690641); const dayStamp = 18891; const setItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'setItem').mockImplementationOnce(setItemSpy); const storage = makeAsyncStorage({ dataKey: 'my-custom-key' }); if (storage && storage.writeData) { await storage.writeData(entires); } expect(setItemSpy).toHaveBeenCalledWith( 'my-custom-key', `{"${dayStamp}":${serializedEntries}}` ); }); it('merges previous writes', async () => { vi.spyOn(Date.prototype, 'valueOf').mockReturnValueOnce(1632209690641); const dayStamp = 18891; const setItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'setItem').mockImplementationOnce(setItemSpy); const storage = makeAsyncStorage(); // write once if (storage && storage.writeData) { await storage.writeData(entires); } expect(setItemSpy).toHaveBeenCalledWith( 'graphcache-data', `{"${dayStamp}":${serializedEntries}}` ); // write twice const secondSetItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'setItem').mockImplementationOnce( secondSetItemSpy ); if (storage && storage.writeData) { storage.writeData({ foo: 'bar' }); } expect(secondSetItemSpy).toHaveBeenCalledWith( 'graphcache-data', `{"${dayStamp}":${JSON.stringify({ hello: 'world', foo: 'bar' })}}` ); }); it('keeps items from previous days', async () => { vi.spyOn(Date.prototype, 'valueOf').mockReturnValueOnce(1632209690641); const dayStamp = 18891; const oldDayStamp = 18857; vi.spyOn(AsyncStorage, 'getItem').mockResolvedValueOnce( JSON.stringify({ [oldDayStamp]: { foo: 'bar' } }) ); const setItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'setItem').mockImplementationOnce(setItemSpy); const storage = makeAsyncStorage(); if (storage && storage.writeData) { await storage.writeData(entires); } expect(setItemSpy).toHaveBeenCalledWith( 'graphcache-data', JSON.stringify({ [oldDayStamp]: { foo: 'bar' }, [dayStamp]: entires }) ); }); it('propagates deleted keys to previous days', async () => { vi.spyOn(Date.prototype, 'valueOf').mockReturnValueOnce(1632209690641); const dayStamp = 18891; vi.spyOn(AsyncStorage, 'getItem').mockResolvedValueOnce( JSON.stringify({ [dayStamp]: { foo: 'bar', hello: 'world' }, [dayStamp - 1]: { foo: 'bar', hello: 'world' }, [dayStamp - 2]: { foo: 'bar', hello: 'world' }, }) ); const setItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'setItem').mockImplementationOnce(setItemSpy); const storage = makeAsyncStorage(); if (storage && storage.writeData) { await storage.writeData({ foo: 'new', hello: undefined }); } expect(setItemSpy).toHaveBeenCalledWith( 'graphcache-data', JSON.stringify({ [dayStamp]: { foo: 'new' }, [dayStamp - 1]: { foo: 'bar' }, [dayStamp - 2]: { foo: 'bar' }, }) ); }); }); describe('readData', () => { it('returns an empty object if no data is found', async () => { const getItemSpy = vi.fn().mockResolvedValue(null); vi.spyOn(AsyncStorage, 'getItem').mockImplementationOnce(getItemSpy); const storage = makeAsyncStorage(); if (storage && storage.readData) { const result = await storage.readData(); expect(getItemSpy).toHaveBeenCalledWith('graphcache-data'); expect(result).toEqual({}); } }); it("returns today's data correctly", async () => { vi.spyOn(Date.prototype, 'valueOf').mockReturnValueOnce(1632209690641); const dayStamp = 18891; const mockData = JSON.stringify({ [dayStamp]: entires }); const getItemSpy = vi.fn().mockResolvedValue(mockData); vi.spyOn(AsyncStorage, 'getItem').mockImplementationOnce(getItemSpy); const storage = makeAsyncStorage(); if (storage && storage.readData) { const result = await storage.readData(); expect(getItemSpy).toHaveBeenCalledWith('graphcache-data'); expect(result).toEqual(entires); } }); it('merges data from past days correctly', async () => { vi.spyOn(Date.prototype, 'valueOf').mockReturnValueOnce(1632209690641); const dayStamp = 18891; const mockData = JSON.stringify({ [dayStamp]: { one: 'one' }, [dayStamp - 1]: { two: 'two' }, [dayStamp - 3]: { three: 'three' }, [dayStamp - 4]: { two: 'old' }, }); const getItemSpy = vi.fn().mockResolvedValue(mockData); vi.spyOn(AsyncStorage, 'getItem').mockImplementationOnce(getItemSpy); const storage = makeAsyncStorage(); if (storage && storage.readData) { const result = await storage.readData(); expect(getItemSpy).toHaveBeenCalledWith('graphcache-data'); expect(result).toEqual({ one: 'one', two: 'two', three: 'three', }); } }); it('cleans up old data', async () => { vi.spyOn(Date.prototype, 'valueOf').mockReturnValueOnce(1632209690641); const dayStamp = 18891; const maxAge = 5; const mockData = JSON.stringify({ [dayStamp]: entires, // should be kept [dayStamp - maxAge + 1]: entires, // should be kept [dayStamp - maxAge - 1]: { old: 'data' }, // should get deleted }); vi.spyOn(AsyncStorage, 'getItem').mockResolvedValueOnce(mockData); const setItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'setItem').mockImplementationOnce(setItemSpy); const storage = makeAsyncStorage({ maxAge }); if (storage && storage.readData) { const result = await storage.readData(); expect(result).toEqual(entires); expect(setItemSpy).toBeCalledWith( 'graphcache-data', JSON.stringify({ [dayStamp]: entires, [dayStamp - maxAge + 1]: entires, }) ); } }); }); describe('onOnline', () => { it('sets up an event listener for the network change event', () => { const addEventListenerSpy = vi.fn(); vi.spyOn(NetInfo, 'addEventListener').mockImplementationOnce( addEventListenerSpy ); const storage = makeAsyncStorage(); if (storage && storage.onOnline) { storage.onOnline(() => null); } expect(addEventListenerSpy).toBeCalledTimes(1); }); it('calls the callback when the device comes online', () => { const callbackSpy = vi.fn(); let networkCallback; vi.spyOn(NetInfo, 'addEventListener').mockImplementationOnce(callback => { networkCallback = callback; return () => null; }); const storage = makeAsyncStorage(); if (storage && storage.onOnline) { storage.onOnline(callbackSpy); } networkCallback({ isConnected: true }); expect(callbackSpy).toBeCalledTimes(1); }); it('does not call the callback when the device is offline', () => { const callbackSpy = vi.fn(); let networkCallback; vi.spyOn(NetInfo, 'addEventListener').mockImplementationOnce(callback => { networkCallback = callback; return () => null; }); const storage = makeAsyncStorage(); if (storage && storage.onOnline) { storage.onOnline(callbackSpy); } networkCallback({ isConnected: false }); expect(callbackSpy).toBeCalledTimes(0); }); }); describe('clear', () => { it('clears all data and metadata', async () => { const removeItemSpy = vi.fn(); const secondRemoveItemSpy = vi.fn(); vi.spyOn(AsyncStorage, 'removeItem') .mockImplementationOnce(removeItemSpy) .mockImplementationOnce(secondRemoveItemSpy); const storage = makeAsyncStorage({ dataKey: 'my-data', metadataKey: 'my-metadata', }); if (storage && storage.clear) { await storage.clear(); } expect(removeItemSpy).toHaveBeenCalledWith('my-data'); expect(secondRemoveItemSpy).toHaveBeenCalledWith('my-metadata'); }); }); }); ================================================ FILE: packages/storage-rn/src/makeAsyncStorage.ts ================================================ import type { StorageAdapter } from '@urql/exchange-graphcache'; import AsyncStorage from '@react-native-async-storage/async-storage'; import NetInfo from '@react-native-community/netinfo'; export interface StorageOptions { /** Name of the `AsyncStorage` key that’s used for persisted data. * @defaultValue `'graphcache-data'` */ dataKey?: string; /** Name of the `AsyncStorage` key that’s used for persisted metadata. * @defaultValue `'graphcache-metadata'` */ metadataKey?: string; /** Maximum age of cache entries (in days) after which data is discarded. * @defaultValue `7` days */ maxAge?: number; } const parseData = (persistedData: any, fallback: any) => { try { if (persistedData) { return JSON.parse(persistedData); } } catch (_err) {} return fallback; }; let disconnect; /** React Native storage adapter persisting to `AsyncStorage`. */ export interface DefaultAsyncStorage extends StorageAdapter { /** Clears the entire `AsyncStorage`. */ clear(): Promise; } /** Creates a {@link StorageAdapter} which uses React Native’s `AsyncStorage`. * * @param opts - A {@link StorageOptions} configuration object. * @returns the created {@link DefaultAsyncStorage} adapter. * * @remarks * `makeAsyncStorage` creates a storage adapter for React Native, * which persisted to `AsyncStorage` via the `@react-native-async-storage/async-storage` * package. * * Note: We have no data on stability of this storage and our Offline Support * for large APIs or longterm use. Proceed with caution. */ export const makeAsyncStorage: ( ops?: StorageOptions ) => DefaultAsyncStorage = ({ dataKey = 'graphcache-data', metadataKey = 'graphcache-metadata', maxAge = 7, } = {}) => { const todayDayStamp = Math.floor( new Date().valueOf() / (1000 * 60 * 60 * 24) ); let allData = {}; return { readData: async () => { if (!Object.keys(allData).length) { let persistedData: string | null = null; try { persistedData = await AsyncStorage.getItem(dataKey); } catch (_err) {} const parsed = parseData(persistedData, {}); Object.assign(allData, parsed); } // clean up old data let syncNeeded = false; Object.keys(allData).forEach(dayStamp => { if (todayDayStamp - Number(dayStamp) > maxAge) { syncNeeded = true; delete allData[dayStamp]; } }); if (syncNeeded) { try { await AsyncStorage.setItem(dataKey, JSON.stringify(allData)); } catch (_err) {} } return Object.assign( {}, ...Object.keys(allData).map(key => allData[key]) ); }, writeData: async delta => { if (!Object.keys(allData).length) { let persistedData: string | null = null; try { persistedData = await AsyncStorage.getItem(dataKey); } catch (_err) {} const parsed = parseData(persistedData, {}); Object.assign(allData, parsed); } const deletedKeys = {}; Object.keys(delta).forEach(key => { if (delta[key] === undefined) { deletedKeys[key] = undefined; } }); for (const key in allData) { allData[key] = Object.assign(allData[key], deletedKeys); } allData[todayDayStamp] = Object.assign( allData[todayDayStamp] || {}, delta ); try { await AsyncStorage.setItem(dataKey, JSON.stringify(allData)); } catch (_err) {} }, writeMetadata: async data => { try { await AsyncStorage.setItem(metadataKey, JSON.stringify(data)); } catch (_err) {} }, readMetadata: async () => { let persistedData: string | null = null; try { persistedData = await AsyncStorage.getItem(metadataKey); } catch (_err) {} return parseData(persistedData, []); }, onOnline: cb => { if (disconnect) { disconnect(); disconnect = undefined; } disconnect = NetInfo.addEventListener(({ isConnected }) => { if (isConnected) { cb(); } }); }, clear: async () => { try { allData = {}; await AsyncStorage.removeItem(dataKey); await AsyncStorage.removeItem(metadataKey); } catch (_err) {} }, }; }; ================================================ FILE: packages/storage-rn/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/storage-rn/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: packages/svelte-urql/CHANGELOG.md ================================================ # @urql/svelte ## 5.0.0 ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 4.2.3 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 4.2.2 ### Patch Changes - Add type for `hasNext` to the query and mutation results Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3703](https://github.com/urql-graphql/urql/pull/3703)) ## 4.2.1 ### Patch Changes - add support for Svelte 5 in the `peerDependencies` Submitted by [@itssumitrai](https://github.com/itssumitrai) (See [#3634](https://github.com/urql-graphql/urql/pull/3634)) ## 4.2.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ### Patch Changes - ⚠️ Fix subscription handlers to not receive `null` values Submitted by [@kitten](https://github.com/kitten) (See [#3581](https://github.com/urql-graphql/urql/pull/3581)) ## 4.1.1 ### Patch Changes - Updated dependencies (See [#3520](https://github.com/urql-graphql/urql/pull/3520), [#3553](https://github.com/urql-graphql/urql/pull/3553), and [#3520](https://github.com/urql-graphql/urql/pull/3520)) - @urql/core@5.0.0 ## 4.1.0 ### Minor Changes - Add back the `reexecute` function Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3472](https://github.com/urql-graphql/urql/pull/3472)) ### Patch Changes - Updated dependencies (See [#3514](https://github.com/urql-graphql/urql/pull/3514), [#3505](https://github.com/urql-graphql/urql/pull/3505), [#3499](https://github.com/urql-graphql/urql/pull/3499), and [#3515](https://github.com/urql-graphql/urql/pull/3515)) - @urql/core@4.3.0 ## 4.0.4 ### Patch Changes - ⚠️ Fix `queryStore` and `subscriptionStore` not subscribing when `writable` calls its `StartStopNotifier`. This caused both stores to be inactive and become unresponsive when they’ve been unsubscribed from once, as they wouldn’t be able to restart their subscriptions to the `Client` Submitted by [@kitten](https://github.com/kitten) (See [#3331](https://github.com/urql-graphql/urql/pull/3331)) ## 4.0.3 ### Patch Changes - Updated peer dependency range to include support for Svelte `^4.0.0` Submitted by [@ategen3rt](https://github.com/ategen3rt) (See [#3302](https://github.com/urql-graphql/urql/pull/3302)) ## 4.0.2 ### Patch Changes - Update build process to generate correct source maps Submitted by [@kitten](https://github.com/kitten) (See [#3201](https://github.com/urql-graphql/urql/pull/3201)) ## 4.0.1 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 4.0.0 ### Major Changes - Update `OperationResult.hasNext` and `OperationResult.stale` to be required fields. If you have a custom exchange creating results, you'll have to add these fields or use the `makeResult`, `mergeResultPatch`, or `makeErrorResult` helpers Submitted by [@kitten](https://github.com/kitten) (See [#3061](https://github.com/urql-graphql/urql/pull/3061)) - Move `handler`, which combines subscription events, from `mutationStore` to `subscriptionStore`. It’s accidentally been defined and implemented on the wrong store and was meant to be on `subscriptionStore` Submitted by [@kitten](https://github.com/kitten) (See [#3078](https://github.com/urql-graphql/urql/pull/3078)) ### Minor Changes - Allow mutations to update their results in bindings when `hasNext: true` is set, which indicates deferred or streamed results Submitted by [@kitten](https://github.com/kitten) (See [#3103](https://github.com/urql-graphql/urql/pull/3103)) ### Patch Changes - ⚠️ Fix source maps included with recently published packages, which lost their `sourcesContent`, including additional source files, and had incorrect paths in some of them Submitted by [@kitten](https://github.com/kitten) (See [#3053](https://github.com/urql-graphql/urql/pull/3053)) - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs to all `urql` bindings packages Submitted by [@kitten](https://github.com/kitten) (See [#3079](https://github.com/urql-graphql/urql/pull/3079)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 3.0.4 ### Patch Changes - ⚠️ Fix type utilities turning the `variables` properties optional when a type from `TypedDocumentNode` has no `Variables` or all optional `Variables`. Previously this would break for wrappers, e.g. in code generators, or when the type didn't quite match what we'd expect Submitted by [@kitten](https://github.com/kitten) (See [#3022](https://github.com/urql-graphql/urql/pull/3022)) - Updated dependencies (See [#3007](https://github.com/urql-graphql/urql/pull/3007), [#2962](https://github.com/urql-graphql/urql/pull/2962), [#3007](https://github.com/urql-graphql/urql/pull/3007), [#3015](https://github.com/urql-graphql/urql/pull/3015), and [#3022](https://github.com/urql-graphql/urql/pull/3022)) - @urql/core@3.2.0 ## 3.0.3 ### Patch Changes - ⚠️ Fix type-generation, with a change in TS/Rollup the type generation took the paths as src and resolved them into the types dir, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2870](https://github.com/urql-graphql/urql/pull/2870)) - Updated dependencies (See [#2872](https://github.com/urql-graphql/urql/pull/2872), [#2870](https://github.com/urql-graphql/urql/pull/2870), and [#2871](https://github.com/urql-graphql/urql/pull/2871)) - @urql/core@3.1.1 ## 3.0.2 ### Patch Changes - Move remaining `Variables` generics over from `object` default to `Variables extends AnyVariables = AnyVariables`. This has been introduced previously in [#2607](https://github.com/urql-graphql/urql/pull/2607) but some missing ports have been missed due to TypeScript not catching them previously. Depending on your TypeScript version the `object` default is incompatible with `AnyVariables`, by [@kitten](https://github.com/kitten) (See [#2843](https://github.com/urql-graphql/urql/pull/2843)) - Updated dependencies (See [#2843](https://github.com/urql-graphql/urql/pull/2843), [#2847](https://github.com/urql-graphql/urql/pull/2847), [#2850](https://github.com/urql-graphql/urql/pull/2850), and [#2846](https://github.com/urql-graphql/urql/pull/2846)) - @urql/core@3.1.0 ## 3.0.1 ### Patch Changes - Tweak the variables type for when generics only contain nullable keys, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2623](https://github.com/FormidableLabs/urql/pull/2623)) ## 3.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Implement stricter variables types, which require variables to always be passed and match TypeScript types when the generic is set or inferred. This is a breaking change for TypeScript users potentially, unless all types are adhered to, by [@kitten](https://github.com/kitten) (See [#2607](https://github.com/FormidableLabs/urql/pull/2607)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Patch Changes - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 2.0.2 ### Patch Changes - Made variables optional for all operations, by [@mpiorowski](https://github.com/mpiorowski) (See [#2496](https://github.com/FormidableLabs/urql/pull/2496)) - ⚠️ Fix issue with `subscriptionStore` and `queryStore` eagerly terminating the subscription due to `derived`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2514](https://github.com/FormidableLabs/urql/pull/2514)) ## 2.0.1 ### Patch Changes - ⚠️ Fix Node.js ESM re-export detection for `@urql/core` in `urql` package and CommonJS output for all other CommonJS-first packages. This ensures that Node.js' `cjs-module-lexer` can correctly identify re-exports and report them properly. Otherwise, this will lead to a runtime error, by [@kitten](https://github.com/kitten) (See [#2485](https://github.com/FormidableLabs/urql/pull/2485)) ## 2.0.0 ### Major Changes - Reimplement Svelte with functional-only API. We've gotten plenty of feedback and issues from the Svelte community about our prior Svelte bindings. These bindings favoured a Store singleton to read and write to, and a separate signal to start an operation. Svelte usually however calls for a lot more flexibility, so we're returning the API to a functional-only API again that serves to only create stores, which is more similar to the original implementation, by [@jonathanstanley](https://github.com/jonathanstanley) (See [#2370](https://github.com/FormidableLabs/urql/pull/2370)) ### Patch Changes - Updated dependencies (See [#2446](https://github.com/FormidableLabs/urql/pull/2446), [#2456](https://github.com/FormidableLabs/urql/pull/2456), and [#2457](https://github.com/FormidableLabs/urql/pull/2457)) - @urql/core@2.5.0 ## 1.3.3 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 1.3.2 ### Patch Changes - ⚠️ Fix initialize `operationStore` with `fetching: false`, the invocation of `query` or any other operation will mark it as `true` when deemed appropriate, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2048](https://github.com/FormidableLabs/urql/pull/2048)) - Updated dependencies (See [#2027](https://github.com/FormidableLabs/urql/pull/2027) and [#1998](https://github.com/FormidableLabs/urql/pull/1998)) - @urql/core@2.3.4 ## 1.3.1 ### Patch Changes - Add missing `pause` on the `operationStore` return value, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1925](https://github.com/FormidableLabs/urql/pull/1925)) - Updated dependencies (See [#1944](https://github.com/FormidableLabs/urql/pull/1944)) - @urql/core@2.3.2 ## 1.3.0 ### Minor Changes - Improve granularity of `operationStore` updates when `query`, `variables`, and `context` are changed. This also adds an `operationStore(...).reexecute()` method, which optionally accepts a new context value and forces an update on the store, so that a query may reexecute, by [@kitten](https://github.com/kitten) (See [#1780](https://github.com/FormidableLabs/urql/pull/1780)) ### Patch Changes - Loosen `subscription(...)` type further to allow any `operationStore` input, regardless of the `Result` produced, by [@kitten](https://github.com/kitten) (See [#1779](https://github.com/FormidableLabs/urql/pull/1779)) - Updated dependencies (See [#1776](https://github.com/FormidableLabs/urql/pull/1776) and [#1755](https://github.com/FormidableLabs/urql/pull/1755)) - @urql/core@2.1.5 ## 1.2.3 ### Patch Changes - Improve `OperationStore` and `subscription` types to allow for result types of `data` that differ from the original `Data` type, which may be picked up from `TypedDocumentNode`, by [@kitten](https://github.com/kitten) (See [#1731](https://github.com/FormidableLabs/urql/pull/1731)) - Use client.executeMutation rather than client.mutation, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1732](https://github.com/FormidableLabs/urql/pull/1732)) - Updated dependencies (See [#1709](https://github.com/FormidableLabs/urql/pull/1709)) - @urql/core@2.1.4 ## 1.2.2 ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 1.2.1 ### Patch Changes - Allow `mutation` to accept a more partial `GraphQLRequest` object without a `key` or `variables`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1473](https://github.com/FormidableLabs/urql/pull/1473)) ## 1.2.0 ### Minor Changes - Remove deprecated `operationName` property from `Operation`s. The new `Operation.kind` property is now preferred. If you're creating new operations you may also use the `makeOperation` utility instead. When upgrading `@urql/core` please ensure that your package manager didn't install any duplicates of it. You may deduplicate it manually using `npx yarn-deduplicate` (for Yarn) or `npm dedupe` (for npm), by [@kitten](https://github.com/kitten) (See [#1357](https://github.com/FormidableLabs/urql/pull/1357)) ### Patch Changes - Updated dependencies (See [#1374](https://github.com/FormidableLabs/urql/pull/1374), [#1357](https://github.com/FormidableLabs/urql/pull/1357), and [#1375](https://github.com/FormidableLabs/urql/pull/1375)) - @urql/core@2.0.0 ## 1.1.4 ### Patch Changes - Add a built-in `gql` tag function helper to `@urql/core`. This behaves similarly to `graphql-tag` but only warns about _locally_ duplicated fragment names rather than globally. It also primes `@urql/core`'s key cache with the parsed `DocumentNode`, by [@kitten](https://github.com/kitten) (See [#1187](https://github.com/FormidableLabs/urql/pull/1187)) - Updated dependencies (See [#1187](https://github.com/FormidableLabs/urql/pull/1187), [#1186](https://github.com/FormidableLabs/urql/pull/1186), and [#1186](https://github.com/FormidableLabs/urql/pull/1186)) - @urql/core@1.16.0 ## 1.1.3 ### Patch Changes - Add support for `TypedDocumentNode` to infer the type of the `OperationResult` and `Operation` for all methods, functions, and hooks that either directly or indirectly accept a `DocumentNode`. See [`graphql-typed-document-node` and the corresponding blog post for more information.](https://github.com/dotansimha/graphql-typed-document-node), by [@kitten](https://github.com/kitten) (See [#1113](https://github.com/FormidableLabs/urql/pull/1113)) - Updated dependencies (See [#1119](https://github.com/FormidableLabs/urql/pull/1119), [#1113](https://github.com/FormidableLabs/urql/pull/1113), [#1104](https://github.com/FormidableLabs/urql/pull/1104), and [#1123](https://github.com/FormidableLabs/urql/pull/1123)) - @urql/core@1.15.0 ## 1.1.2 ### Patch Changes - Replace `void` union types with `undefined` in `OperationStore` to allow nullish property access in TypeScript, by [@kitten](https://github.com/kitten) (See [#1053](https://github.com/FormidableLabs/urql/pull/1053)) ## 1.1.1 ### Patch Changes - ⚠️ Fix missing `stale` flag updates on query results, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1044](https://github.com/FormidableLabs/urql/pull/1044)) ## 1.1.0 ### Minor Changes - Support passing `pause` to stop executing queries or subscriptions, by [@kitten](https://github.com/kitten) (See [#1046](https://github.com/FormidableLabs/urql/pull/1046)) ### Patch Changes - ⚠️ Fix an issue where updated `context` options wouldn't cause a new query to be executed, or updates to the store would erroneously throw a debug error, by [@kitten](https://github.com/kitten) (See [#1046](https://github.com/FormidableLabs/urql/pull/1046)) ## 1.0.1 ### Patch Changes - ⚠️ Fix `stale` keeping a `truthy` value on a `cache-and-network` operation, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1032](https://github.com/FormidableLabs/urql/pull/1032)) ## 1.0.0 The new `@urql/svelte` API features the `query`, `mutation`, and `subscription` utilities, which are called as part of a component's normal lifecycle and accept `operationStore` stores. These are writable stores that encapsulate both a GraphQL operation's inputs and outputs (the result)! Learn more about how to use `@urql/svelte` [in our new API docs](https://formidable.com/open-source/urql/docs/api/svelte/) or starting from the [Basics pages.](https://formidable.com/open-source/urql/docs/basics/) ### Major Changes - Reimplement the `@urql/svelte` API, which is now marked as stable, by [@kitten](https://github.com/kitten) (See [#1016](https://github.com/FormidableLabs/urql/pull/1016)) ## 0.4.0 ### Minor Changes - Add the operation to the query, mutation and subscription result, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#924](https://github.com/FormidableLabs/urql/pull/924)) ### Patch Changes - Updated dependencies (See [#911](https://github.com/FormidableLabs/urql/pull/911) and [#908](https://github.com/FormidableLabs/urql/pull/908)) - @urql/core@1.12.3 ## 0.3.0 ### Minor Changes - Refactor all operations to allow for more use-cases which preserve state and allow all modes of Svelte to be applied to urql. ``` // Standard Usage: mutate({ query, variables })() // Subscribable Usage: $: result = mutate({ query, variables }); // Curried Usage const executeMutation = mutate({ query, variables }); const onClick = () => executeMutation(); // Curried Usage with overrides const executeMutation = mutate({ query }); const onClick = () => await executeMutation({ variables }); // Subscribable Usage (as before): $: result = query({ query: TestQuery, variables }); // Subscribable Usage which preserves state over time: const testQuery = query({ query: TestQuery }); // - this preserves the state even when the variables change! $: result = testQuery({ variables }); // Promise-based callback usage: const testQuery = query({ query: TestQuery }); const doQuery = async () => await testQuery; // Promise-based usage updates the subscribables! const testQuery = query({ query: TestQuery }); const doQuery = async () => await testQuery; // - doQuery will also update this result $: result = query({ query: TestQuery, variables }); ``` ### Patch Changes - Updated dependencies (See [#860](https://github.com/FormidableLabs/urql/pull/860) and [#861](https://github.com/FormidableLabs/urql/pull/861)) - @urql/core@1.12.1 ## 0.2.4 ### Patch Changes - Upgrade to a minimum version of wonka@^4.0.14 to work around issues with React Native's minification builds, which use uglify-es and could lead to broken bundles, by [@kitten](https://github.com/kitten) (See [#842](https://github.com/FormidableLabs/urql/pull/842)) - Updated dependencies (See [#838](https://github.com/FormidableLabs/urql/pull/838) and [#842](https://github.com/FormidableLabs/urql/pull/842)) - @urql/core@1.12.0 ## 0.2.3 ### Patch Changes - Add a `"./package.json"` entry to the `package.json`'s `"exports"` field for Node 14. This seems to be required by packages like `rollup-plugin-svelte` to function properly, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - Updated dependencies (See [#771](https://github.com/FormidableLabs/urql/pull/771)) - @urql/core@1.11.6 ## 0.2.2 ### Patch Changes - Update `mutate` helper to return a Promise directly rather than a lazy Promise-like object, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#758](https://github.com/FormidableLabs/urql/pull/758)) ## 0.2.1 ### Patch Changes - Bump @urql/core to ensure exchanges have dispatchDebug, this could formerly result in a crash, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#726](https://github.com/FormidableLabs/urql/pull/726)) ## 0.2.0 ### Minor Changes - Update `mutate()` API to accept an options argument, instead of separate arguments, to increase consistency, by [@kitten](https://github.com/kitten) (See [#705](https://github.com/FormidableLabs/urql/pull/705)) ## 0.1.3 ### Patch Changes - Add graphql@^15.0.0 to peer dependency range, by [@kitten](https://github.com/kitten) (See [#688](https://github.com/FormidableLabs/urql/pull/688)) - Forcefully bump @urql/core package in all bindings and in @urql/exchange-graphcache. We're aware that in some cases users may not have upgraded to @urql/core, even though that's within the typical patch range. Since the latest @urql/core version contains a patch that is required for `cache-and-network` to work, we're pushing another patch that now forcefully bumps everyone to the new version that includes this fix, by [@kitten](https://github.com/kitten) (See [#684](https://github.com/FormidableLabs/urql/pull/684)) - Updated dependencies (See [#688](https://github.com/FormidableLabs/urql/pull/688) and [#678](https://github.com/FormidableLabs/urql/pull/678)) - @urql/core@1.10.8 ## 0.1.2 ### Patch Changes - ⚠️ Fix node resolution when using Webpack, which experiences a bug where it only resolves `package.json:main` instead of `module` when an `.mjs` file imports a package, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - Updated dependencies (See [#642](https://github.com/FormidableLabs/urql/pull/642)) - @urql/core@1.10.4 ## 0.1.1 ### Patch Changes - ⚠️ Fix Node.js Module support for v13 (experimental-modules) and v14. If your bundler doesn't support `.mjs` files and fails to resolve the new version, please double check your configuration for Webpack, or similar tools, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - Updated dependencies (See [#637](https://github.com/FormidableLabs/urql/pull/637)) - @urql/core@1.10.3 ## 0.1.0 ### Patch Changes - Bumps the `@urql/core` dependency minor version to ^1.10.1 for React, Preact and Svelte, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#623](https://github.com/FormidableLabs/urql/pull/623)) - Updated dependencies (See [#621](https://github.com/FormidableLabs/urql/pull/621)) - @urql/core@1.10.2 ## 0.1.0-alpha.0 Initial Alpha Release ================================================ FILE: packages/svelte-urql/README.md ================================================ # @urql/svelte > A highly customizable and versatile GraphQL client **for Svelte** More documentation is available at [formidable.com/open-source/urql](https://formidable.com/open-source/urql/). ================================================ FILE: packages/svelte-urql/jsr.json ================================================ { "name": "@urql/svelte", "version": "5.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/svelte-urql/package.json ================================================ { "name": "@urql/svelte", "version": "5.0.0", "description": "A highly customizable and versatile GraphQL client for Svelte", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/svelte-urql" }, "keywords": [ "graphql client", "state management", "cache", "graphql", "exchanges", "svelte" ], "main": "dist/urql-svelte", "module": "dist/urql-svelte.mjs", "types": "dist/urql-svelte.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-svelte.d.ts", "import": "./dist/urql-svelte.mjs", "require": "./dist/urql-svelte.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "peerDependencies": { "@urql/core": "^6.0.0", "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "devDependencies": { "@urql/core": "workspace:*", "graphql": "^16.0.0", "svelte": "^3.20.0" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: packages/svelte-urql/src/common.ts ================================================ import type { Readable, Writable } from 'svelte/store'; import type { AnyVariables, OperationResult } from '@urql/core'; import type { Source } from 'wonka'; import { make } from 'wonka'; /** An {@link OperationResult} with an added {@link OperationResultState.fetching} flag. * * @remarks * Stores will contain a readable state based on {@link OperationResult | OperationResults} * they received. */ export interface OperationResultState< Data = any, Variables extends AnyVariables = AnyVariables, > extends OperationResult { /** Indicates whether the store is waiting for a new {@link OperationResult}. * * @remarks * When a store starts executing a GraphQL operation, `fetching` is * set to `true` until a result arrives. * * Hint: This is subtly different than whether the operation is actually * fetching, and doesn’t indicate whether an operation is being re-executed * in the background. For this, see {@link OperationResult.stale}. */ fetching: boolean; } /** A Readable store of {@link OperationResultState}. */ export type OperationResultStore< Data = any, Variables extends AnyVariables = AnyVariables, > = Readable>; /** Consumes a {@link Readable} as a {@link Source}. * @internal */ export const fromStore = (store$: Readable): Source => make(observer => store$.subscribe(observer.next)); export const initialResult = { operation: undefined, fetching: false, data: undefined, error: undefined, extensions: undefined, hasNext: false, stale: false, }; /** A pausable Svelte store. * * @remarks * The {@link queryStore} and {@link useSubscription} store allow * you to pause execution and resume it later on, which is managed * by a `pause` option passed to them. * * A `Pauseable` allows execution of GraphQL operations to be paused, * which means a {@link OperationResultStore} won’t update with new * results or execute new operations, and to be resumed later on. */ export interface Pausable { /** Indicates whether a store is currently paused. * * @remarks * When a {@link OperationResultStore} has been paused, it will stop * receiving updates from the {@link Client} and won’t execute GraphQL * operations, until this writable becomes `true` or * {@link Pausable.resume} is called. * * @see {@link https://urql.dev/goto/docs/basics/svelte#pausing-queries} for * documentation on the `Pausable`. */ isPaused$: Writable; /** Pauses a GraphQL operation to stop it from executing. * * @remarks * Pauses an {@link OperationResultStore}’s GraphQL operation, which * stops it from receiving updates from the {@link Client} and to stop * an ongoing operation. * * @see {@link https://urql.dev/goto/docs/basics/svelte#pausing-queries} for * documentation on the `Pausable`. */ pause(): void; /** Resumes a paused GraphQL operation if it’s currently paused. * * @remarks * Resumes or starts {@link OperationResultStore}’s GraphQL operation, * if it’s currently paused. * * @see {@link https://urql.dev/goto/docs/basics/svelte#pausing-queries} for * documentation on the `Pausable`. */ resume(): void; } /** Creates a {@link Pausable}. * @internal */ export const createPausable = (isPaused$: Writable): Pausable => ({ isPaused$, pause() { isPaused$.set(true); }, resume() { isPaused$.set(false); }, }); ================================================ FILE: packages/svelte-urql/src/context.ts ================================================ import { setContext, getContext } from 'svelte'; import type { ClientOptions } from '@urql/core'; import { Client } from '@urql/core'; const _contextKey = '$$_urql'; /** Returns a provided {@link Client}. * * @remarks * `getContextClient` returns the {@link Client} that’s previously * been provided on Svelte’s context with {@link setContextClient}. * * This is useful to create a `Client` on Svelte’s context once, and * then pass it to all GraphQL store functions without importing it * from a singleton export. * * @throws * In development, if `getContextClient` can’t get a {@link Client} * from Svelte’s context, an error will be thrown. */ export const getContextClient = (): Client => { const client = getContext(_contextKey); if (process.env.NODE_ENV !== 'production' && !client) { throw new Error( 'No urql Client was found in Svelte context. Did you forget to call setContextClient?' ); } return client as Client; }; /** Provides a {@link Client} to a component’s children. * * @remarks * `setContextClient` updates the Svelte context to provide * a {@link Client} to be later retrieved using the * {@link getContextClient} function. */ export const setContextClient = (client: Client): void => { setContext(_contextKey, client); }; /** Creates a {@link Client} and provides it to a component’s children. * * @param args - a {@link ClientOptions} object to create a `Client` with. * @returns the created {@link Client}. * * @remarks * `initContextClient` is a convenience wrapper around * `setContextClient` that accepts {@link ClientOptions}, * creates a {@link Client} and provides it to be later * retrieved using the {@link getContextClient} function. */ export const initContextClient = (args: ClientOptions): Client => { const client = new Client(args); setContextClient(client); return client; }; ================================================ FILE: packages/svelte-urql/src/index.ts ================================================ export * from '@urql/core'; export type { Pausable, OperationResultStore, OperationResultState, } from './common'; export * from './queryStore'; export * from './mutationStore'; export * from './subscriptionStore'; export * from './context'; ================================================ FILE: packages/svelte-urql/src/mutationStore.test.ts ================================================ import { print } from 'graphql'; import { createClient } from '@urql/core'; import { get } from 'svelte/store'; import { vi, expect, it, describe } from 'vitest'; import { mutationStore } from './mutationStore'; describe('mutationStore', () => { const client = createClient({ url: 'noop', exchanges: [], }); const variables = {}; const context = {}; const query = 'mutation ($input: Example!) { doExample(input: $input) { id } }'; const store = mutationStore({ client, query, variables, context, }); it('creates a svelte store', () => { const subscriber = vi.fn(); store.subscribe(subscriber); expect(subscriber).toHaveBeenCalledTimes(1); }); it('fills the store with correct values', () => { expect(get(store).operation.kind).toBe('mutation'); expect(get(store).operation.context.url).toBe('noop'); expect(get(store).operation.variables).toBe(variables); expect(print(get(store).operation.query)).toMatchInlineSnapshot(` "mutation ($input: Example!) { doExample(input: $input) { id } }" `); }); }); ================================================ FILE: packages/svelte-urql/src/mutationStore.ts ================================================ import { pipe, map, scan, subscribe } from 'wonka'; import { derived, writable } from 'svelte/store'; import type { AnyVariables, GraphQLRequestParams, Client, OperationContext, } from '@urql/core'; import { createRequest } from '@urql/core'; import type { OperationResultState, OperationResultStore } from './common'; import { initialResult } from './common'; /** Input arguments for the {@link mutationStore} function. * * @param query - The GraphQL mutation that the `mutationStore` executes. * @param variables - The variables for the GraphQL mutation that `mutationStore` executes. */ export type MutationArgs< Data = any, Variables extends AnyVariables = AnyVariables, > = { /** The {@link Client} using which the subscription will be started. * * @remarks * If you’ve previously provided a {@link Client} on Svelte’s context * this can be set to {@link getContextClient}’s return value. */ client: Client; /** Updates the {@link OperationContext} for the GraphQL mutation operation. * * @remarks * `context` may be passed to {@link mutationStore}, to update the * {@link OperationContext} of a mutation operation. This may be used to update * the `context` that exchanges will receive for a single hook. * * @example * ```ts * mutationStore({ * query, * context: { * additionalTypenames: ['Item'], * }, * }); * ``` */ context?: Partial; } & GraphQLRequestParams; /** Function to create a `mutationStore` that runs a GraphQL mutation and updates with a GraphQL result. * * @param args - a {@link MutationArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link OperationResultStore} of the mutation’s result. * * @remarks * `mutationStore` allows a GraphQL mutation to be defined as a Svelte store. * Given {@link MutationArgs.query}, it executes the GraphQL mutation on the * {@link MutationArgs.client}. * * The returned store updates with an {@link OperationResult} when * the `Client` returns a result for the mutation. * * Hint: It’s often easier to use {@link Client.mutation} if you’re * creating a mutation imperatively and don’t need a store. * * @see {@link https://urql.dev/goto/docs/basics/svelte#mutations} for * `mutationStore` docs. * * @example * ```ts * import { mutationStore, gql, getContextClient } from '@urql/svelte'; * * const client = getContextClient(); * * let result; * function updateTodo({ id, title }) { * result = queryStore({ * client, * query: gql` * mutation($id: ID!, $title: String!) { * updateTodo(id: $id, title: $title) { id, title } * } * `, * variables: { id, title }, * }); * } * ``` */ export function mutationStore< Data = any, Variables extends AnyVariables = AnyVariables, >(args: MutationArgs): OperationResultStore { const request = createRequest(args.query, args.variables as Variables); const operation = args.client.createRequestOperation( 'mutation', request, args.context ); const initialState: OperationResultState = { ...initialResult, operation, fetching: true, }; const result$ = writable(initialState); const subscription = pipe( pipe( args.client.executeRequestOperation(operation), map(({ stale, data, error, extensions, operation, hasNext }) => ({ fetching: false, stale, data, error, operation, extensions, hasNext, })) ), scan( (result: OperationResultState, partial) => ({ ...result, ...partial, }), initialState ), subscribe(result => { result$.set(result); }) ); return derived(result$, (result, set) => { set(result); return subscription.unsubscribe; }); } ================================================ FILE: packages/svelte-urql/src/queryStore.test.ts ================================================ import { createClient } from '@urql/core'; import { vi, expect, it, describe } from 'vitest'; import { get } from 'svelte/store'; import { queryStore } from './queryStore'; describe('queryStore', () => { const client = createClient({ url: 'https://example.com', exchanges: [], }); const variables = {}; const context = {}; const query = '{ test }'; const store = queryStore({ client, query, variables, context }); it('creates a svelte store', () => { const subscriber = vi.fn(); store.subscribe(subscriber); expect(subscriber).toHaveBeenCalledTimes(1); }); it('fills the store with correct values', () => { expect(get(store).operation.kind).toBe('query'); expect(get(store).operation.context.url).toBe('https://example.com'); expect(get(store).operation.variables).toBe(variables); expect(get(store).operation.query.loc?.source.body).toMatchInlineSnapshot(` "{ test }" `); }); it('adds pause handles', () => { expect(get(store.isPaused$)).toBe(false); store.pause(); expect(get(store.isPaused$)).toBe(true); store.resume(); expect(get(store.isPaused$)).toBe(false); }); }); ================================================ FILE: packages/svelte-urql/src/queryStore.ts ================================================ import type { Client, GraphQLRequestParams, AnyVariables, OperationContext, RequestPolicy, } from '@urql/core'; import { createRequest } from '@urql/core'; import type { Source } from 'wonka'; import { pipe, map, fromValue, switchMap, subscribe, concat, scan, never, } from 'wonka'; import { derived, writable } from 'svelte/store'; import type { OperationResultState, OperationResultStore, Pausable, } from './common'; import { initialResult, createPausable, fromStore } from './common'; /** Input arguments for the {@link queryStore} function. * * @param query - The GraphQL query that the `queryStore` executes. * @param variables - The variables for the GraphQL query that `queryStore` executes. */ export type QueryArgs< Data = any, Variables extends AnyVariables = AnyVariables, > = { /** The {@link Client} using which the query will be executed. * * @remarks * If you’ve previously provided a {@link Client} on Svelte’s context * this can be set to {@link getContextClient}’s return value. */ client: Client; /** Updates the {@link OperationContext} for the executed GraphQL query operation. * * @remarks * `context` may be passed to {@link queryStore}, to update the {@link OperationContext} * of a query operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * @example * ```ts * queryStore({ * query, * context: { * additionalTypenames: ['Item'], * }, * }); * ``` */ context?: Partial; /** Sets the {@link RequestPolicy} for the executed GraphQL query operation. * * @remarks * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation * that the {@link queryStore} executes, and indicates a caching strategy for cache exchanges. * * For example, when set to `'cache-and-network'`, the `queryStore` will * receive a cached result with `stale: true` and an API request will be * sent in the background. * * @see {@link OperationContext.requestPolicy} for where this value is set. */ requestPolicy?: RequestPolicy; /** Prevents the {@link queryStore} from automatically executing GraphQL query operations. * * @remarks * `pause` may be set to `true` to stop the {@link queryStore} from executing * automatically. The store will stop receiving updates from the {@link Client} * and won’t execute the query operation, until either it’s set to `false` * or {@link Pausable.resume} is called. * * @see {@link https://urql.dev/goto/docs/basics/svelte#pausing-queries} for * documentation on the `pause` option. */ pause?: boolean; } & GraphQLRequestParams; /** Function to create a `queryStore` that runs a GraphQL query and updates with GraphQL results. * * @param args - a {@link QueryArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link OperationResultStore} of query results, which implements {@link Pausable}. * * @remarks * `queryStore` allows GraphQL queries to be defined as Svelte stores. * Given {@link QueryArgs.query}, it executes the GraphQL query on the * {@link QueryArgs.client}. * * The returned store updates with {@link OperationResult} values when * the `Client` has new results for the query. * * @see {@link https://urql.dev/goto/docs/basics/svelte#queries} for `queryStore` docs. * * @example * ```ts * import { queryStore, gql, getContextClient } from '@urql/svelte'; * * const todos = queryStore({ * client: getContextClient(), * query: gql`{ todos { id, title } }`, * }); * ``` */ export function queryStore< Data = any, Variables extends AnyVariables = AnyVariables, >( args: QueryArgs ): OperationResultStore & Pausable & { reexecute: (context: Partial) => void } { const request = createRequest(args.query, args.variables as Variables); const context: Partial = { requestPolicy: args.requestPolicy, ...args.context, }; const operation = args.client.createRequestOperation( 'query', request, context ); const operation$ = writable(operation); const initialState: OperationResultState = { ...initialResult, operation, }; const isPaused$ = writable(!!args.pause); const result$ = writable(initialState, () => { return pipe( fromStore(isPaused$), switchMap( (isPaused): Source>> => { if (isPaused) { return never as any; } return pipe( fromStore(operation$), switchMap(operation => { return concat>>([ fromValue({ fetching: true, stale: false, hasNext: false }), pipe( args.client.executeRequestOperation(operation), map( ({ stale, data, error, extensions, operation, hasNext, }) => ({ fetching: false, stale: !!stale, data, error, operation, extensions, hasNext, }) ) ), fromValue({ fetching: false, hasNext: false }), ]); }) ); } ), scan( (result: OperationResultState, partial) => ({ ...result, ...partial, }), initialState ), subscribe(result => { result$.set(result); }) ).unsubscribe; }); const reexecute = (context: Partial) => { const newContext = { ...context, ...args.context }; const operation = args.client.createRequestOperation( 'query', request, newContext ); isPaused$.set(false); operation$.set(operation); }; return { ...derived(result$, (result, set) => { set(result); }), ...createPausable(isPaused$), reexecute, }; } ================================================ FILE: packages/svelte-urql/src/subscriptionStore.test.ts ================================================ import { createClient } from '@urql/core'; import { get } from 'svelte/store'; import { vi, expect, it, describe } from 'vitest'; import { subscriptionStore } from './subscriptionStore'; describe('subscriptionStore', () => { const client = createClient({ url: 'https://example.com', exchanges: [], }); const variables = {}; const context = {}; const query = `subscription ($input: ExampleInput) { exampleSubscribe(input: $input) { data } }`; const store = subscriptionStore({ client, query, variables, context, }); it('creates a svelte store', () => { const subscriber = vi.fn(); store.subscribe(subscriber); expect(subscriber).toHaveBeenCalledTimes(1); }); it('fills the store with correct values', () => { expect(get(store).operation.kind).toBe('subscription'); expect(get(store).operation.context.url).toBe('https://example.com'); expect(get(store).operation.variables).toBe(variables); expect(get(store).operation.query.loc?.source.body).toMatchInlineSnapshot(` "subscription ($input: ExampleInput) { exampleSubscribe(input: $input) { data } }" `); }); it('adds pause handles', () => { expect(get(store.isPaused$)).toBe(false); store.pause(); expect(get(store.isPaused$)).toBe(true); store.resume(); expect(get(store.isPaused$)).toBe(false); }); }); ================================================ FILE: packages/svelte-urql/src/subscriptionStore.ts ================================================ import type { AnyVariables, GraphQLRequestParams, Client, OperationContext, } from '@urql/core'; import { createRequest } from '@urql/core'; import type { Source } from 'wonka'; import { pipe, map, fromValue, switchMap, subscribe, concat, scan, never, } from 'wonka'; import { derived, writable } from 'svelte/store'; import type { OperationResultState, OperationResultStore, Pausable, } from './common'; import { initialResult, createPausable, fromStore } from './common'; /** Combines previous data with an incoming subscription result’s data. * * @remarks * A `SubscriptionHandler` may be passed to {@link subscriptionStore} to * aggregate subscription results into a combined `data` value on the * {@link OperationResultStore}. * * This is useful when a subscription event delivers a single item, while * you’d like to display a list of events. * * @example * ```ts * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * subscriptionStore( * { query: NotificationsSubscription }, * function combineNotifications(notifications = [], data) { * return [...notifications, data.newNotification]; * }, * ); * ``` */ export type SubscriptionHandler = (prev: R | undefined, data: T) => R; /** Input arguments for the {@link subscriptionStore} function. * * @param query - The GraphQL subscription that the `subscriptionStore` executes. * @param variables - The variables for the GraphQL subscription that `subscriptionStore` executes. */ export type SubscriptionArgs< Data = any, Variables extends AnyVariables = AnyVariables, > = { /** The {@link Client} using which the subscription will be started. * * @remarks * If you’ve previously provided a {@link Client} on Svelte’s context * this can be set to {@link getContextClient}’s return value. */ client: Client; /** Updates the {@link OperationContext} for the GraphQL subscription operation. * * @remarks * `context` may be passed to {@link subscriptionStore}, to update the * {@link OperationContext} of a subscription operation. This may be used to update * the `context` that exchanges will receive for a single hook. * * @example * ```ts * subscriptionStore({ * query, * context: { * additionalTypenames: ['Item'], * }, * }); * ``` */ context?: Partial; /** Prevents the {@link subscriptionStore} from automatically starting the GraphQL subscription. * * @remarks * `pause` may be set to `true` to stop the {@link subscriptionStore} from starting * its subscription automatically. The store won't execute the subscription operation, * until either it’s set to `false` or {@link Pausable.resume} is called. */ pause?: boolean; } & GraphQLRequestParams; /** Function to create a `subscriptionStore` that starts a GraphQL subscription. * * @param args - a {@link QueryArgs} object, to pass a `query`, `variables`, and options. * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results. * @returns a {@link OperationResultStore} of subscription results, which implements {@link Pausable}. * * @remarks * `subscriptionStore` allows GraphQL subscriptions to be defined as Svelte stores. * Given {@link SubscriptionArgs.query}, it executes the GraphQL subsription on the * {@link SubscriptionArgs.client}. * * The returned store updates with {@link OperationResult} values when * the `Client` has new results for the subscription. * * @see {@link https://urql.dev/goto/docs/advanced/subscriptions#svelte} for * `subscriptionStore` docs. * * @example * ```ts * import { subscriptionStore, gql, getContextClient } from '@urql/svelte'; * * const todos = subscriptionStore({ * client: getContextClient(), * query: gql` * subscription { * newNotification { id, text } * } * `, * }, * function combineNotifications(notifications = [], data) { * return [...notifications, data.newNotification]; * }, * ); * ``` */ export function subscriptionStore< Data, Result = Data, Variables extends AnyVariables = AnyVariables, >( args: SubscriptionArgs, handler?: SubscriptionHandler ): OperationResultStore & Pausable { const request = createRequest(args.query, args.variables as Variables); const operation = args.client.createRequestOperation( 'subscription', request, args.context ); const initialState: OperationResultState = { ...initialResult, operation, fetching: true, }; const isPaused$ = writable(!!args.pause); const result$ = writable(initialState, () => { return pipe( fromStore(isPaused$), switchMap( (isPaused): Source>> => { if (isPaused) { return never as any; } return concat>>([ fromValue({ fetching: true, stale: false }), pipe( args.client.executeRequestOperation(operation), map(({ stale, data, error, extensions, operation }) => ({ fetching: true, stale: !!stale, data, error, operation, extensions, })) ), fromValue({ fetching: false }), ]); } ), scan((result: OperationResultState, partial) => { const data = partial.data != null ? typeof handler === 'function' ? handler(result.data, partial.data) : partial.data : result.data; return { ...result, ...partial, data, } as OperationResultState; }, initialState), subscribe(result => { result$.set(result); }) ).unsubscribe; }); return { ...derived(result$, (result, set) => { set(result); }), ...createPausable(isPaused$), }; } ================================================ FILE: packages/svelte-urql/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/svelte-urql/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: packages/vue-urql/CHANGELOG.md ================================================ # @urql/vue ## 2.0.0 ### Major Changes - Bump Vue to 3.2+ and replace getCurrentInstance with getCurrentScope Submitted by [@arkandias](https://github.com/arkandias) (See [#3806](https://github.com/urql-graphql/urql/pull/3806)) ### Minor Changes - Fix regression breaking `variables` typing Submitted by [@arkandias](https://github.com/arkandias) (See [#3734](https://github.com/urql-graphql/urql/pull/3734)) ### Patch Changes - Updated dependencies (See [#3789](https://github.com/urql-graphql/urql/pull/3789) and [#3807](https://github.com/urql-graphql/urql/pull/3807)) - @urql/core@6.0.0 ## 1.4.3 ### Patch Changes - Omit minified files and sourcemaps' `sourcesContent` in published packages Submitted by [@kitten](https://github.com/kitten) (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - Updated dependencies (See [#3755](https://github.com/urql-graphql/urql/pull/3755)) - @urql/core@5.1.1 ## 1.4.2 ### Patch Changes - Add type for `hasNext` to the query and mutation results Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3703](https://github.com/urql-graphql/urql/pull/3703)) ## 1.4.1 ### Patch Changes - Use `shallowRef` for data variable to avoid extra overhead for heavy objects Submitted by [@yurks](https://github.com/yurks) (See [#3641](https://github.com/urql-graphql/urql/pull/3641)) ## 1.4.0 ### Minor Changes - Refactor composable functions with a focus on avoiding memory leaks and Vue best practices Submitted by [@yurks](https://github.com/yurks) (See [#3619](https://github.com/urql-graphql/urql/pull/3619)) ## 1.3.2 ### Patch Changes - ⚠️ Fix deep options reactivity for subscriptions Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3616](https://github.com/urql-graphql/urql/pull/3616)) ## 1.3.1 ### Patch Changes - ⚠️ Fix variables losing reactivity Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#3614](https://github.com/urql-graphql/urql/pull/3614)) ## 1.3.0 ### Minor Changes - Use `shallowRef` to avoid creating deeply reactive objects for heavy objects Submitted by [@negezor](https://github.com/negezor) (See [#3611](https://github.com/urql-graphql/urql/pull/3611)) - Remove wrapping request `args` in `reactive` to fix memory leak Submitted by [@negezor](https://github.com/negezor) (See [#3612](https://github.com/urql-graphql/urql/pull/3612)) ## 1.2.2 ### Patch Changes - ⚠️ Fix reactaive typings for `variables` (See [#3605](https://github.com/urql-graphql/urql/pull/3605)) Submitted by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [`118d74b2`](https://github.com/urql-graphql/urql/commit/118d74b238007264cacfb91fc12de74370d5766e)) - Restore the possibility to use a getter for the pause property Submitted by [@arkandias](https://github.com/arkandias) (See [#3598](https://github.com/urql-graphql/urql/pull/3598)) ## 1.2.1 ### Patch Changes - ⚠️ Fix regression causing `pause` argument on `useQuery` and `useSubscription` to not be reactive Submitted by [@arkandias](https://github.com/arkandias) (See [#3595](https://github.com/urql-graphql/urql/pull/3595)) ## 1.2.0 ### Minor Changes - Mark `@urql/core` as a peer dependency as well as a regular dependency Submitted by [@kitten](https://github.com/kitten) (See [#3579](https://github.com/urql-graphql/urql/pull/3579)) ### Patch Changes - ⚠️ Fix subscription handlers to not receive `null` values Submitted by [@kitten](https://github.com/kitten) (See [#3581](https://github.com/urql-graphql/urql/pull/3581)) - Add missing support for getter functions as input arguments values to `useQuery`, `useSubscription`, and `useMutation` Submitted by [@kitten](https://github.com/kitten) (See [#3582](https://github.com/urql-graphql/urql/pull/3582)) ## 1.1.3 ### Patch Changes - Updated dependencies (See [#3520](https://github.com/urql-graphql/urql/pull/3520), [#3553](https://github.com/urql-graphql/urql/pull/3553), and [#3520](https://github.com/urql-graphql/urql/pull/3520)) - @urql/core@5.0.0 ## 1.1.2 ### Patch Changes - Update build process to generate correct source maps Submitted by [@kitten](https://github.com/kitten) (See [#3201](https://github.com/urql-graphql/urql/pull/3201)) - Prevent multiple operations being executed in a row when multiple inputs change simultaneously (e.g. `isPaused` and query inputs) Submitted by [@kitten](https://github.com/kitten) (See [#3231](https://github.com/urql-graphql/urql/pull/3231)) ## 1.1.1 ### Patch Changes - Publish with npm provenance Submitted by [@kitten](https://github.com/kitten) (See [#3180](https://github.com/urql-graphql/urql/pull/3180)) ## 1.1.0 ### Minor Changes - Allow mutations to update their results in bindings when `hasNext: true` is set, which indicates deferred or streamed results Submitted by [@kitten](https://github.com/kitten) (See [#3103](https://github.com/urql-graphql/urql/pull/3103)) ### Patch Changes - ⚠️ Fix source maps included with recently published packages, which lost their `sourcesContent`, including additional source files, and had incorrect paths in some of them Submitted by [@kitten](https://github.com/kitten) (See [#3053](https://github.com/urql-graphql/urql/pull/3053)) - Upgrade to `wonka@^6.3.0` Submitted by [@kitten](https://github.com/kitten) (See [#3104](https://github.com/urql-graphql/urql/pull/3104)) - Add TSDocs to all `urql` bindings packages Submitted by [@kitten](https://github.com/kitten) (See [#3079](https://github.com/urql-graphql/urql/pull/3079)) - Updated dependencies (See [#3101](https://github.com/urql-graphql/urql/pull/3101), [#3033](https://github.com/urql-graphql/urql/pull/3033), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3053](https://github.com/urql-graphql/urql/pull/3053), [#3060](https://github.com/urql-graphql/urql/pull/3060), [#3081](https://github.com/urql-graphql/urql/pull/3081), [#3039](https://github.com/urql-graphql/urql/pull/3039), [#3104](https://github.com/urql-graphql/urql/pull/3104), [#3082](https://github.com/urql-graphql/urql/pull/3082), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3061](https://github.com/urql-graphql/urql/pull/3061), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3085](https://github.com/urql-graphql/urql/pull/3085), [#3079](https://github.com/urql-graphql/urql/pull/3079), [#3087](https://github.com/urql-graphql/urql/pull/3087), [#3059](https://github.com/urql-graphql/urql/pull/3059), [#3055](https://github.com/urql-graphql/urql/pull/3055), [#3057](https://github.com/urql-graphql/urql/pull/3057), [#3050](https://github.com/urql-graphql/urql/pull/3050), [#3062](https://github.com/urql-graphql/urql/pull/3062), [#3051](https://github.com/urql-graphql/urql/pull/3051), [#3043](https://github.com/urql-graphql/urql/pull/3043), [#3063](https://github.com/urql-graphql/urql/pull/3063), [#3054](https://github.com/urql-graphql/urql/pull/3054), [#3102](https://github.com/urql-graphql/urql/pull/3102), [#3097](https://github.com/urql-graphql/urql/pull/3097), [#3106](https://github.com/urql-graphql/urql/pull/3106), [#3058](https://github.com/urql-graphql/urql/pull/3058), and [#3062](https://github.com/urql-graphql/urql/pull/3062)) - @urql/core@4.0.0 ## 1.0.5 ### Patch Changes - Allow a `Client` provided using `provideClient` to be used in the same component it's been provided in Submitted by [@kitten](https://github.com/kitten) (See [#3018](https://github.com/urql-graphql/urql/pull/3018)) - ⚠️ Fix type utilities turning the `variables` properties optional when a type from `TypedDocumentNode` has no `Variables` or all optional `Variables`. Previously this would break for wrappers, e.g. in code generators, or when the type didn't quite match what we'd expect Submitted by [@kitten](https://github.com/kitten) (See [#3022](https://github.com/urql-graphql/urql/pull/3022)) - Updated dependencies (See [#3007](https://github.com/urql-graphql/urql/pull/3007), [#2962](https://github.com/urql-graphql/urql/pull/2962), [#3007](https://github.com/urql-graphql/urql/pull/3007), [#3015](https://github.com/urql-graphql/urql/pull/3015), and [#3022](https://github.com/urql-graphql/urql/pull/3022)) - @urql/core@3.2.0 ## 1.0.4 ### Patch Changes - ⚠️ Fix type-generation, with a change in TS/Rollup the type generation took the paths as src and resolved them into the types dir, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2870](https://github.com/urql-graphql/urql/pull/2870)) - Updated dependencies (See [#2872](https://github.com/urql-graphql/urql/pull/2872), [#2870](https://github.com/urql-graphql/urql/pull/2870), and [#2871](https://github.com/urql-graphql/urql/pull/2871)) - @urql/core@3.1.1 ## 1.0.3 ### Patch Changes - Move remaining `Variables` generics over from `object` default to `Variables extends AnyVariables = AnyVariables`. This has been introduced previously in [#2607](https://github.com/urql-graphql/urql/pull/2607) but some missing ports have been missed due to TypeScript not catching them previously. Depending on your TypeScript version the `object` default is incompatible with `AnyVariables`, by [@kitten](https://github.com/kitten) (See [#2843](https://github.com/urql-graphql/urql/pull/2843)) - Updated dependencies (See [#2843](https://github.com/urql-graphql/urql/pull/2843), [#2847](https://github.com/urql-graphql/urql/pull/2847), [#2850](https://github.com/urql-graphql/urql/pull/2850), and [#2846](https://github.com/urql-graphql/urql/pull/2846)) - @urql/core@3.1.0 ## 1.0.2 ### Patch Changes - ⚠️ Fix an issue that caused `useQuery` to fail for promise-based access, if a result is delivered by the `Client` immediately, by [@kitten](https://github.com/kitten) (See [#2629](https://github.com/FormidableLabs/urql/pull/2629)) ## 1.0.1 ### Patch Changes - Tweak the variables type for when generics only contain nullable keys, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2623](https://github.com/FormidableLabs/urql/pull/2623)) ## 1.0.0 ### Major Changes - **Goodbye IE11!** 👋 This major release removes support for IE11. All code that is shipped will be transpiled much less and will _not_ be ES5-compatible anymore, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - Implement stricter variables types, which require variables to always be passed and match TypeScript types when the generic is set or inferred. This is a breaking change for TypeScript users potentially, unless all types are adhered to, by [@kitten](https://github.com/kitten) (See [#2607](https://github.com/FormidableLabs/urql/pull/2607)) - Upgrade to [Wonka v6](https://github.com/0no-co/wonka) (`wonka@^6.0.0`), which has no breaking changes but is built to target ES2015 and comes with other minor improvements. The library has fully been migrated to TypeScript which will hopefully help with making contributions easier!, by [@kitten](https://github.com/kitten) (See [#2504](https://github.com/FormidableLabs/urql/pull/2504)) ### Patch Changes - Support nested refs in variables, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2608](https://github.com/FormidableLabs/urql/pull/2608)) - Updated dependencies (See [#2551](https://github.com/FormidableLabs/urql/pull/2551), [#2504](https://github.com/FormidableLabs/urql/pull/2504), [#2619](https://github.com/FormidableLabs/urql/pull/2619), [#2607](https://github.com/FormidableLabs/urql/pull/2607), and [#2504](https://github.com/FormidableLabs/urql/pull/2504)) - @urql/core@3.0.0 ## 0.6.4 ### Patch Changes - Allow Vue 2.7 as peer dependency to prevent peer dependency errors e.g. with pnpm, by [@dargmuesli](https://github.com/dargmuesli) (See [#2561](https://github.com/FormidableLabs/urql/pull/2561)) ## 0.6.3 ### Patch Changes - ⚠️ Fix Node.js ESM re-export detection for `@urql/core` in `urql` package and CommonJS output for all other CommonJS-first packages. This ensures that Node.js' `cjs-module-lexer` can correctly identify re-exports and report them properly. Otherwise, this will lead to a runtime error, by [@kitten](https://github.com/kitten) (See [#2485](https://github.com/FormidableLabs/urql/pull/2485)) ## 0.6.2 ### Patch Changes - ⚠️ Fix wait for the first non-stale result when using `await executeQuery`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2410](https://github.com/FormidableLabs/urql/pull/2410)) ## 0.6.1 ### Patch Changes - Extend peer dependency range of `graphql` to include `^16.0.0`. As always when upgrading across many packages of `urql`, especially including `@urql/core` we recommend you to deduplicate dependencies after upgrading, using `npm dedupe` or `npx yarn-deduplicate`, by [@kitten](https://github.com/kitten) (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - Updated dependencies (See [#2133](https://github.com/FormidableLabs/urql/pull/2133)) - @urql/core@2.3.6 ## 0.6.0 ### Minor Changes - Provide the client as a ref so it can observe changes. This change is potentially breaking for anyone using the `useClient` import as it will now return a `Ref` rather than a `Client`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#2047](https://github.com/FormidableLabs/urql/pull/2047)) ### Patch Changes - Updated dependencies (See [#2027](https://github.com/FormidableLabs/urql/pull/2027) and [#1998](https://github.com/FormidableLabs/urql/pull/1998)) - @urql/core@2.3.4 ## 0.5.0 ### Minor Changes - Allow passing in a Ref of client to `provideClient` and `install`, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1962](https://github.com/FormidableLabs/urql/pull/1962)) ### Patch Changes - Updated dependencies (See [#1944](https://github.com/FormidableLabs/urql/pull/1944)) - @urql/core@2.3.2 ## 0.4.3 ### Patch Changes - Unwrap the `variables` proxy before we send it into the client, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1810](https://github.com/FormidableLabs/urql/pull/1810)) ## 0.4.2 ### Patch Changes - Refactor `useQuery` implementation to utilise the single-source implementation of `@urql/core@2.1.0`. This should improve the stability of promisified `useQuery()` calls and prevent operations from not being issued in some edge cases, by [@kitten](https://github.com/kitten) (See [#1758](https://github.com/FormidableLabs/urql/pull/1758)) - Updated dependencies (See [#1776](https://github.com/FormidableLabs/urql/pull/1776) and [#1755](https://github.com/FormidableLabs/urql/pull/1755)) - @urql/core@2.1.5 ## 0.4.1 ### Patch Changes - Use client.executeMutation rather than client.mutation in useMutation, by [@JoviDeCroock](https://github.com/JoviDeCroock) (See [#1680](https://github.com/FormidableLabs/urql/pull/1680)) - Updated dependencies (See [#1709](https://github.com/FormidableLabs/urql/pull/1709)) - @urql/core@2.1.4 ## 0.4.0 ### Minor Changes - A `useClientHandle()` function has been added. This creates a `handle` on which all `use*` hooks can be called, like `await handle.useQuery(...)` or `await handle.useSubscription(...)` which is useful for sequentially chaining hook calls in an `async setup()` function or preserve the right instance of a `Client` across lifecycle hooks, by [@kitten](https://github.com/kitten) (See [#1599](https://github.com/FormidableLabs/urql/pull/1599)) ### Patch Changes - Remove closure-compiler from the build step (See [#1570](https://github.com/FormidableLabs/urql/pull/1570)) - The `useClient()` function will now throw a more helpful error when it's called outside of any lifecycle hooks, by [@kitten](https://github.com/kitten) (See [#1599](https://github.com/FormidableLabs/urql/pull/1599)) - Updated dependencies (See [#1570](https://github.com/FormidableLabs/urql/pull/1570), [#1509](https://github.com/FormidableLabs/urql/pull/1509), [#1600](https://github.com/FormidableLabs/urql/pull/1600), and [#1515](https://github.com/FormidableLabs/urql/pull/1515)) - @urql/core@2.1.0 ## 0.3.0 ### Minor Changes - **Breaking**: Remove `pollInterval` option from `useQuery`. Please consider adding an interval manually calling `executeQuery()`, by [@kitten](https://github.com/kitten) (See [#1374](https://github.com/FormidableLabs/urql/pull/1374)) - Remove deprecated `operationName` property from `Operation`s. The new `Operation.kind` property is now preferred. If you're creating new operations you may also use the `makeOperation` utility instead. When upgrading `@urql/core` please ensure that your package manager didn't install any duplicates of it. You may deduplicate it manually using `npx yarn-deduplicate` (for Yarn) or `npm dedupe` (for npm), by [@kitten](https://github.com/kitten) (See [#1357](https://github.com/FormidableLabs/urql/pull/1357)) ### Patch Changes - Updated dependencies (See [#1374](https://github.com/FormidableLabs/urql/pull/1374), [#1357](https://github.com/FormidableLabs/urql/pull/1357), and [#1375](https://github.com/FormidableLabs/urql/pull/1375)) - @urql/core@2.0.0 ## 0.2.1 ### Patch Changes - Add a built-in `gql` tag function helper to `@urql/core`. This behaves similarly to `graphql-tag` but only warns about _locally_ duplicated fragment names rather than globally. It also primes `@urql/core`'s key cache with the parsed `DocumentNode`, by [@kitten](https://github.com/kitten) (See [#1187](https://github.com/FormidableLabs/urql/pull/1187)) - Updated dependencies (See [#1187](https://github.com/FormidableLabs/urql/pull/1187), [#1186](https://github.com/FormidableLabs/urql/pull/1186), and [#1186](https://github.com/FormidableLabs/urql/pull/1186)) - @urql/core@1.16.0 ## 0.2.0 ### Minor Changes - Export a Vue plugin function as the default export, by [@LinusBorg](https://github.com/LinusBorg) (See [#1152](https://github.com/FormidableLabs/urql/pull/1152)) - Refactor `useQuery` to resolve the lazy promise for Vue Suspense to the latest result that has been requested as per the input to `useQuery`, by [@kitten](https://github.com/kitten) (See [#1162](https://github.com/FormidableLabs/urql/pull/1162)) ### Patch Changes - ⚠️ Fix pausing feature of `useQuery` by turning `isPaused` into a ref again, by [@LinusBorg](https://github.com/LinusBorg) (See [#1155](https://github.com/FormidableLabs/urql/pull/1155)) - ⚠️ Fix implementation of Vue's Suspense feature by making the lazy `PromiseLike` on the returned state passive, by [@kitten](https://github.com/kitten) (See [#1159](https://github.com/FormidableLabs/urql/pull/1159)) ## 0.1.0 Initial release ================================================ FILE: packages/vue-urql/README.md ================================================ ## Installation ```sh yarn add @urql/vue graphql # or npm install --save @urql/vue graphql ``` > **Note:** `@urql/vue` has a peer dependency on `vue@^3.0.0` (Not v2) and doesn't currently plan to > be backwards compatible to Vue 2. ================================================ FILE: packages/vue-urql/jsr.json ================================================ { "name": "@urql/vue", "version": "2.0.0", "exports": { ".": "./src/index.ts" }, "exclude": [ "node_modules", "cypress", "**/*.test.*", "**/*.spec.*", "**/*.test.*.snap", "**/*.spec.*.snap" ] } ================================================ FILE: packages/vue-urql/package.json ================================================ { "name": "@urql/vue", "version": "2.0.0", "description": "A highly customizable and versatile GraphQL client for vue", "sideEffects": false, "homepage": "https://formidable.com/open-source/urql/docs/", "bugs": "https://github.com/urql-graphql/urql/issues", "license": "MIT", "author": "urql GraphQL Contributors", "repository": { "type": "git", "url": "https://github.com/urql-graphql/urql.git", "directory": "packages/vue-urql" }, "keywords": [ "graphql client", "state management", "cache", "graphql", "exchanges", "vue" ], "main": "dist/urql-vue", "module": "dist/urql-vue.mjs", "types": "dist/urql-vue.d.ts", "source": "src/index.ts", "exports": { ".": { "types": "./dist/urql-vue.d.ts", "import": "./dist/urql-vue.mjs", "require": "./dist/urql-vue.js", "source": "./src/index.ts" }, "./package.json": "./package.json" }, "files": [ "LICENSE", "CHANGELOG.md", "README.md", "dist/" ], "scripts": { "test": "vitest", "clean": "rimraf dist", "check": "tsc --noEmit", "lint": "eslint --ext=js,jsx,ts,tsx .", "build": "rollup -c ../../scripts/rollup/config.mjs", "prepare": "node ../../scripts/prepare/index.js", "prepublishOnly": "run-s clean build" }, "devDependencies": { "@urql/core": "workspace:*", "@vue/test-utils": "^2.3.0", "graphql": "^16.0.0", "vue": "^3.2.47" }, "peerDependencies": { "@urql/core": "^6.0.0", "vue": "^3.2.0" }, "dependencies": { "@urql/core": "workspace:^6.0.1", "wonka": "^6.3.2" }, "publishConfig": { "access": "public", "provenance": true } } ================================================ FILE: packages/vue-urql/src/index.ts ================================================ export * from '@urql/core'; export * from './useClientHandle'; export { install, provideClient } from './useClient'; export { useQuery } from './useQuery'; export type { UseQueryArgs, UseQueryResponse, UseQueryState } from './useQuery'; export { useSubscription } from './useSubscription'; export type { UseSubscriptionArgs, UseSubscriptionResponse, SubscriptionHandlerArg, SubscriptionHandler, } from './useSubscription'; export { useMutation } from './useMutation'; export type { UseMutationResponse } from './useMutation'; import { install } from './useClient'; export default install; ================================================ FILE: packages/vue-urql/src/useClient.test.ts ================================================ // @vitest-environment jsdom import { expect, it, describe } from 'vitest'; import { defineComponent, effectScope, h } from 'vue'; import { mount } from '@vue/test-utils'; import { Client } from '@urql/core'; import { useClient, provideClient } from './useClient'; describe('provideClient and useClient', () => { it('provides client to current component instance', async () => { const TestComponent = defineComponent({ setup() { provideClient( new Client({ url: 'test', exchanges: [], }) ); const client = useClient(); expect(client).toBeDefined(); return null; }, }); mount(TestComponent); }); it('provides client to child components via provide/inject', async () => { const ChildComponent = defineComponent({ setup() { const client = useClient(); expect(client).toBeDefined(); return () => null; }, }); const ParentComponent = defineComponent({ components: { ChildComponent }, setup() { provideClient( new Client({ url: 'test', exchanges: [], }) ); return () => h(ChildComponent); }, }); mount(ParentComponent); }); it('works in effect scopes outside components', () => { const scope = effectScope(); scope.run(() => { provideClient( new Client({ url: 'test', exchanges: [], }) ); const client = useClient(); expect(client).toBeDefined(); }); }); it('throws error when no client is provided', () => { expect(() => { const TestComponent = defineComponent({ setup() { // No provideClient called useClient(); // Should throw return null; }, }); mount(TestComponent); }).toThrow('No urql Client was provided'); }); it('throws error when called outside reactive context', () => { expect(() => { // Called outside any component or scope useClient(); }).toThrow('reactive context'); }); }); ================================================ FILE: packages/vue-urql/src/useClient.ts ================================================ import { type App, getCurrentScope, type Ref } from 'vue'; import { inject, provide, isRef, shallowRef } from 'vue'; import type { ClientOptions } from '@urql/core'; import { Client } from '@urql/core'; // WeakMap to store client instances as fallback when client is provided and used in the same component const clientsPerScope = new WeakMap<{}, Ref>(); /** Provides a {@link Client} to a component and it’s children. * * @param opts - {@link ClientOptions}, a {@link Client}, or a reactive ref object of a `Client`. * * @remarks * `provideClient` provides a {@link Client} to `@urql/vue`’s GraphQL * functions in children components. * * Hint: GraphQL functions and {@link useClient} will see the * provided `Client`, even if `provideClient` has been called * in the same component’s `setup` function. * * @example * ```ts * * ``` */ export function provideClient(opts: ClientOptions | Client | Ref) { let client: Ref; if (!isRef(opts)) { client = shallowRef(opts instanceof Client ? opts : new Client(opts)); } else { client = opts; } const scope = getCurrentScope(); if (scope) { clientsPerScope.set(scope, client); } provide('$urql', client); return client.value; } /** Provides a {@link Client} to a Vue app. * * @param app - the Vue {@link App} * @param opts - {@link ClientOptions}, a {@link Client}, or a reactive ref object of a `Client`. * * @remarks * `install` provides a {@link Client} to `@urql/vue`’s GraphQL * functions in a Vue app. * * @example * ```ts * import * as urql from '@urql/vue'; * // All of `@urql/core` is also re-exported by `@urql/vue`: * import { cacheExchange, fetchExchange } from '@urql/core'; * * import { createApp } from 'vue'; * import Root from './App.vue'; * * const app = createApp(Root); * app.use(urql, { * url: 'http://localhost:3000/graphql', * exchanges: [cacheExchange, fetchExchange], * }); * ``` */ export function install(app: App, opts: ClientOptions | Client | Ref) { let client: Ref; if (!isRef(opts)) { client = shallowRef(opts instanceof Client ? opts : new Client(opts)); } else { client = opts; } app.provide('$urql', client); } /** Returns a provided reactive ref object of a {@link Client}. * * @remarks * `useClient` may be called in a reactive context to retrieve a * reactive ref object of a {@link Client} that’s previously been * provided with {@link provideClient} in the current or a parent’s * `setup` function. * * @throws * In development, if `useClient` is called outside of a reactive context * or no {@link Client} was provided, an error will be thrown. */ export function useClient(): Ref { const scope = getCurrentScope(); if (process.env.NODE_ENV !== 'production' && !scope) { throw new Error( 'use* function must be called within a reactive context (component setup, composable, or effect scope).' ); } let client = inject('$urql') as Ref | undefined; if (!client) { client = clientsPerScope.get(scope!); } if (process.env.NODE_ENV !== 'production' && !client) { throw new Error( 'No urql Client was provided. Did you forget to install the plugin or call `provideClient` in a parent?' ); } return client!; } ================================================ FILE: packages/vue-urql/src/useClientHandle.ts ================================================ import type { AnyVariables, Client, DocumentInput } from '@urql/core'; import type { WatchStopHandle } from 'vue'; import { getCurrentInstance, onMounted, onBeforeUnmount } from 'vue'; import { useClient } from './useClient'; import type { UseQueryArgs, UseQueryResponse } from './useQuery'; import { callUseQuery } from './useQuery'; import type { UseMutationResponse } from './useMutation'; import { callUseMutation } from './useMutation'; import type { UseSubscriptionArgs, SubscriptionHandlerArg, UseSubscriptionResponse, } from './useSubscription'; import { callUseSubscription } from './useSubscription'; /** Handle to create GraphQL operations outside of Vue’s `setup` functions. * * @remarks * The `ClientHandle` object is created inside a Vue `setup` function but * allows its methods to be called outside of `setup` functions, delaying * the creation of GraphQL operations, as an alternative to pausing queries * or subscriptions. * * This is also important when chaining multiple functions inside an * `async setup()` function. * * Hint: If you only need a single, non-updating result and want to execute * queries programmatically, it may be easier to call the {@link Client.query} * method. */ export interface ClientHandle { /** The {@link Client} that’ll be used to execute GraphQL operations. */ client: Client; /** Calls {@link useQuery} outside of a synchronous Vue `setup` function. * * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link UseQueryResponse} object. * * @remarks * Creates a {@link UseQueryResponse} outside of a synchronous Vue `setup` * function or when chained in an `async setup()` function. */ useQuery( args: UseQueryArgs ): UseQueryResponse; /** Calls {@link useSubscription} outside of a synchronous Vue `setup` function. * * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options. * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results. * @returns a {@link UseSubscriptionResponse} object. * * @remarks * Creates a {@link UseSubscriptionResponse} outside of a synchronous Vue `setup` * function or when chained in an `async setup()` function. */ useSubscription( args: UseSubscriptionArgs, handler?: SubscriptionHandlerArg ): UseSubscriptionResponse; /** Calls {@link useMutation} outside of a synchronous Vue `setup` function. * * @param query - a GraphQL mutation document which `useMutation` will execute. * @returns a {@link UseMutationResponse} object. * * @remarks * Creates a {@link UseMutationResponse} outside of a synchronous Vue `setup` * function or when chained in an `async setup()` function. */ useMutation( query: DocumentInput ): UseMutationResponse; } /** Creates a {@link ClientHandle} inside a Vue `setup` function. * * @remarks * `useClientHandle` creates and returns a {@link ClientHandle} * when called in a Vue `setup` function, which allows queries, * mutations, and subscriptions to be created _outside_ of * `setup` functions. * * This is also important when chaining multiple functions inside an * `async setup()` function. * * {@link useQuery} and other GraphQL functions must usually * be created in Vue `setup` functions so they can stop GraphQL * operations when your component unmounts. However, while they * queries and subscriptions can be paused, sometimes it’s easier * to delay the creation of their response objects. * * * @example * ```ts * import { ref, computed } from 'vue'; * import { gql, useClientHandle } from '@urql/vue'; * * export default { * async setup() { * const handle = useClientHandle(); * * const pokemons = await handle.useQuery({ * query: gql`{ pokemons(limit: 10) { id, name } }`, * }); * * const index = ref(0); * * // The `handle` allows another `useQuery` call to now be setup again * const pokemon = await handle.useQuery({ * query: gql` * query ($id: ID!) { * pokemon(id: $id) { id, name } * } * `, * variables: computed(() => ({ * id: pokemons.data.value.pokemons[index.value].id, * }), * }); * } * }; * ``` */ export function useClientHandle(): ClientHandle { const client = useClient(); const stops: WatchStopHandle[] = []; onBeforeUnmount(() => { let stop: WatchStopHandle | void; while ((stop = stops.shift())) stop(); }); const handle: ClientHandle = { client: client.value, useQuery( args: UseQueryArgs ): UseQueryResponse { return callUseQuery(args, client, stops); }, useSubscription( args: UseSubscriptionArgs, handler?: SubscriptionHandlerArg ): UseSubscriptionResponse { return callUseSubscription(args, handler, client, stops); }, useMutation( query: DocumentInput ): UseMutationResponse { return callUseMutation(query, client); }, }; if (process.env.NODE_ENV !== 'production') { onMounted(() => { Object.assign(handle, { useQuery( args: UseQueryArgs ): UseQueryResponse { if (process.env.NODE_ENV !== 'production' && !getCurrentInstance()) { throw new Error( '`handle.useQuery()` should only be called in the `setup()` or a lifecycle hook.' ); } return callUseQuery(args, client, stops); }, useSubscription( args: UseSubscriptionArgs, handler?: SubscriptionHandlerArg ): UseSubscriptionResponse { if (process.env.NODE_ENV !== 'production' && !getCurrentInstance()) { throw new Error( '`handle.useSubscription()` should only be called in the `setup()` or a lifecycle hook.' ); } return callUseSubscription(args, handler, client, stops); }, }); }); } return handle; } ================================================ FILE: packages/vue-urql/src/useMutation.test.ts ================================================ import { OperationResult, OperationResultSource } from '@urql/core'; import { readonly } from 'vue'; import { vi, expect, it, beforeEach, describe } from 'vitest'; vi.mock('./useClient.ts', async () => { const { ref } = await vi.importActual('vue'); return { __esModule: true, ...((await vi.importActual('./useClient.ts')) as object), useClient: () => ref(client), }; }); import { makeSubject } from 'wonka'; import { createClient, gql } from '@urql/core'; import { useMutation } from './useMutation'; const client = createClient({ url: '/graphql', exchanges: [] }); beforeEach(() => { vi.resetAllMocks(); }); describe('useMutation', () => { it('provides an execute method that resolves a promise', async () => { const subject = makeSubject(); const clientMutation = vi .spyOn(client, 'executeMutation') .mockImplementation( () => subject.source as OperationResultSource ); const mutation = useMutation(gql` mutation { test } `); expect(readonly(mutation)).toMatchObject({ data: undefined, stale: false, fetching: false, error: undefined, extensions: undefined, operation: undefined, executeMutation: expect.any(Function), }); const promise = mutation.executeMutation({ test: true }); expect(mutation.fetching.value).toBe(true); expect(mutation.stale.value).toBe(false); expect(mutation.error.value).toBe(undefined); expect(clientMutation).toHaveBeenCalledTimes(1); subject.next({ data: { test: true }, stale: false }); await promise; expect(mutation.fetching.value).toBe(false); expect(mutation.stale.value).toBe(false); expect(mutation.error.value).toBe(undefined); expect(mutation.data.value).toHaveProperty('test', true); }); }); ================================================ FILE: packages/vue-urql/src/useMutation.ts ================================================ /* eslint-disable react-hooks/rules-of-hooks */ import type { Ref } from 'vue'; import { shallowRef } from 'vue'; import { pipe, onPush, filter, toPromise, take } from 'wonka'; import type { Client, AnyVariables, CombinedError, Operation, OperationContext, OperationResult, DocumentInput, } from '@urql/core'; import { useClient } from './useClient'; import { createRequestWithArgs, type MaybeRefOrGetter, useRequestState, } from './utils'; /** State of the last mutation executed by {@link useMutation}. * * @remarks * `UseMutationResponse` is returned by {@link useMutation} and * gives you the {@link OperationResult} of the last executed mutation, * and a {@link UseMutationResponse.executeMutation} method to * start mutations. * * Even if the mutation document passed to {@link useMutation} changes, * the state isn’t reset, so you can keep displaying the previous result. */ export interface UseMutationResponse { /** Indicates whether `useMutation` is currently executing a mutation. */ fetching: Ref; /** Indicates that the mutation result is not fresh. * * @remarks * The `stale` flag is set to `true` when a new result for the mutation * is expected. * This is mostly unused for mutations and will rarely affect you, and * is more relevant for queries. * * @see {@link OperationResult.stale} for the source of this value. */ stale: Ref; /** Reactive {@link OperationResult.data} for the executed mutation. */ data: Ref; /** Reactive {@link OperationResult.error} for the executed mutation. */ error: Ref; /** Reactive {@link OperationResult.extensions} for the executed mutation. */ extensions: Ref | undefined>; /** Reactive {@link Operation} that the current state is for. * * @remarks * This is the mutation {@link Operation} that has last been executed. * When {@link UseQueryState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation: Ref | undefined>; /** The {@link OperationResult.hasNext} for the executed query. */ hasNext: Ref; /** Triggers {@link useMutation} to execute its GraphQL mutation operation. * * @param variables - variables using which the mutation will be executed. * @param context - optionally, context options that will be merged with * the `Client`’s options. * @returns the {@link OperationResult} of the mutation. * * @remarks * When called, {@link useMutation} will start the GraphQL mutation * it currently holds and use the `variables` passed to it. * * Once the mutation response comes back from the API, its * returned promise will resolve to the mutation’s {@link OperationResult} * and the {@link UseMutationResponse} will be updated with the result. * * @example * ```ts * const result = useMutation(UpdateTodo); * const start = async ({ id, title }) => { * const result = await result.executeMutation({ id, title }); * }; */ executeMutation( variables: V, context?: Partial ): Promise>; } /** Function to create a GraphQL mutation, run by passing variables to {@link UseMutationResponse.executeMutation} * * @param query - a GraphQL mutation document which `useMutation` will execute. * @returns a {@link UseMutationResponse} object. * * @remarks * `useMutation` allows GraphQL mutations to be defined inside Vue `setup` functions, * and keeps its state after the mutation is started. Mutations can be started by calling * {@link UseMutationResponse.executeMutation} with variables. * * The returned result updates when a mutation is executed and keeps * track of the last mutation result. * * @see {@link https://urql.dev/goto/docs/basics/vue#mutations} for `useMutation` docs. * * @example * ```ts * import { gql, useMutation } from '@urql/vue'; * * const UpdateTodo = gql` * mutation ($id: ID!, $title: String!) { * updateTodo(id: $id, title: $title) { * id, title * } * } * `; * * export default { * setup() { * const result = useMutation(UpdateTodo); * const start = async ({ id, title }) => { * const result = await result.executeMutation({ id, title }); * }; * // ... * }, * }; * ``` */ export function useMutation( query: DocumentInput ): UseMutationResponse { return callUseMutation(query); } export function callUseMutation( query: MaybeRefOrGetter>, client: Ref = useClient() ): UseMutationResponse { const data: Ref = shallowRef(); const { fetching, operation, extensions, stale, error, hasNext } = useRequestState(); return { data, stale, fetching, error, operation, extensions, hasNext, executeMutation( variables: V, context?: Partial ): Promise> { fetching.value = true; return pipe( client.value.executeMutation( createRequestWithArgs({ query, variables }), context || {} ), onPush(result => { data.value = result.data; stale.value = result.stale; fetching.value = false; error.value = result.error; operation.value = result.operation; extensions.value = result.extensions; hasNext.value = result.hasNext; }), filter(result => !result.hasNext), take(1), toPromise ); }, }; } ================================================ FILE: packages/vue-urql/src/useQuery.test.ts ================================================ import { OperationResult, OperationResultSource, RequestPolicy, } from '@urql/core'; import { computed, nextTick, reactive, readonly, ref } from 'vue'; import { vi, expect, it, describe } from 'vitest'; vi.mock('./useClient.ts', async () => ({ __esModule: true, ...(await vi.importActual('./useClient.ts')), useClient: () => ref(client), })); import { pipe, makeSubject, fromValue, delay } from 'wonka'; import { createClient } from '@urql/core'; import { useQuery, UseQueryArgs } from './useQuery'; const client = createClient({ url: '/graphql', exchanges: [] }); const createQuery = (args: UseQueryArgs) => { const executeQuery = vi .spyOn(client, 'executeQuery') .mockImplementation(request => { return pipe( fromValue({ operation: request, data: { test: true } }), delay(1) ) as any; }); const query$ = useQuery(args); return { query$, executeQuery, }; }; describe('useQuery', () => { it('runs a query and updates data', async () => { const subject = makeSubject(); const executeQuery = vi .spyOn(client, 'executeQuery') .mockImplementation( () => subject.source as OperationResultSource ); const query = useQuery({ query: `{ test }`, }); expect(readonly(query)).toMatchObject({ data: undefined, stale: false, fetching: true, error: undefined, extensions: undefined, operation: undefined, isPaused: false, pause: expect.any(Function), resume: expect.any(Function), executeQuery: expect.any(Function), then: expect.any(Function), }); expect(executeQuery).toHaveBeenCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: {}, context: undefined, }, { requestPolicy: undefined, } ); expect(query.fetching.value).toBe(true); subject.next({ data: { test: true } }); expect(query.fetching.value).toBe(false); expect(query.data.value).toHaveProperty('test', true); }); it('runs queries as a promise-like that resolves when used', async () => { const executeQuery = vi .spyOn(client, 'executeQuery') .mockImplementation(() => { return pipe(fromValue({ data: { test: true } }), delay(1)) as any; }); const query = await useQuery({ query: `{ test }`, }); expect(executeQuery).toHaveBeenCalledTimes(1); expect(query.fetching.value).toBe(false); expect(query.data.value).toEqual({ test: true }); }); it('runs queries as a promise-like that resolves even when the query changes', async () => { const doc = ref('{ test }'); const { executeQuery, query$ } = createQuery({ query: doc, }); doc.value = '{ test2 }'; await query$; expect(executeQuery).toHaveBeenCalledTimes(2); expect(query$.fetching.value).toBe(false); expect(query$.data.value).toEqual({ test: true }); expect(query$.operation.value).toHaveProperty( 'query.definitions.0.selectionSet.selections.0.name.value', 'test2' ); }); it('reacts to ref variables changing', async () => { const variables = ref({ prop: 1 }); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), variables, }); await query$; expect(executeQuery).toHaveBeenCalledTimes(1); expect(query$.operation.value).toHaveProperty('variables.prop', 1); variables.value.prop++; await query$; expect(executeQuery).toHaveBeenCalledTimes(2); expect(query$.operation.value).toHaveProperty('variables.prop', 2); variables.value = { prop: 3 }; await query$; expect(executeQuery).toHaveBeenCalledTimes(3); expect(query$.operation.value).toHaveProperty('variables.prop', 3); }); it('reacts to ref variables changing', async () => { const foo = ref(1); const bar = ref(1); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), variables: ref({ prop: foo, nested: { prop: bar, }, }), }); await query$; expect(executeQuery).toHaveBeenCalledTimes(1); expect(query$.operation.value).toHaveProperty('variables.prop', 1); foo.value++; await query$; expect(executeQuery).toHaveBeenCalledTimes(2); expect(query$.operation.value).toHaveProperty('variables.prop', 2); bar.value++; await query$; expect(executeQuery).toHaveBeenCalledTimes(3); expect(query$.operation.value).toHaveProperty('variables.nested.prop', 2); }); it('reacts to getter variables changing', async () => { const foo = ref(1); const bar = ref(1); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), variables: () => ({ prop: foo.value, nested: { prop: bar.value, }, }), }); await query$; expect(executeQuery).toHaveBeenCalledTimes(1); expect(query$.operation.value).toHaveProperty('variables.prop', 1); foo.value++; await query$; expect(executeQuery).toHaveBeenCalledTimes(2); expect(query$.operation.value).toHaveProperty('variables.prop', 2); bar.value++; await query$; expect(executeQuery).toHaveBeenCalledTimes(3); expect(query$.operation.value).toHaveProperty('variables.nested.prop', 2); }); it('reacts to reactive variables changing', async () => { const prop = ref(1); const variables = reactive({ prop: 1, deep: { nested: { prop } } }); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), variables, }); await query$; expect(executeQuery).toHaveBeenCalledTimes(1); expect(query$.operation.value).toHaveProperty('variables.prop', 1); variables.prop++; await query$; expect(executeQuery).toHaveBeenCalledTimes(2); expect(query$.operation.value).toHaveProperty('variables.prop', 2); prop.value++; await query$; expect(executeQuery).toHaveBeenCalledTimes(3); expect(query$.operation.value).toHaveProperty( 'variables.deep.nested.prop', 2 ); }); it('reacts to computed variables changing', async () => { const foo = ref(1); const bar = ref(1); const variables = computed(() => ({ prop: foo.value, deep: { nested: { prop: bar.value } }, })); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), variables, }); await query$; expect(executeQuery).toHaveBeenCalledTimes(1); expect(query$.operation.value).toHaveProperty('variables.prop', 1); foo.value++; await query$; expect(executeQuery).toHaveBeenCalledTimes(2); expect(query$.operation.value).toHaveProperty('variables.prop', 2); bar.value++; await query$; expect(executeQuery).toHaveBeenCalledTimes(3); expect(query$.operation.value).toHaveProperty( 'variables.deep.nested.prop', 2 ); }); it('reacts to reactive context argument', async () => { const context = ref<{ requestPolicy: RequestPolicy }>({ requestPolicy: 'cache-only', }); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), context, }); await query$; expect(executeQuery).toHaveBeenCalledTimes(1); context.value.requestPolicy = 'network-only'; await query$; expect(executeQuery).toHaveBeenCalledTimes(2); }); it('reacts to callback context argument', async () => { const requestPolicy = ref('cache-only'); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), context: () => ({ requestPolicy: requestPolicy.value, }), }); await query$; expect(executeQuery).toHaveBeenCalledTimes(1); requestPolicy.value = 'network-only'; await query$; expect(executeQuery).toHaveBeenCalledTimes(2); }); it('pauses query when asked to do so', async () => { const subject = makeSubject(); const executeQuery = vi .spyOn(client, 'executeQuery') .mockImplementation( () => subject.source as OperationResultSource ); const query = useQuery({ query: `{ test }`, pause: true, }); expect(executeQuery).not.toHaveBeenCalled(); query.resume(); await nextTick(); expect(query.fetching.value).toBe(true); subject.next({ data: { test: true } }); expect(query.fetching.value).toBe(false); expect(query.data.value).toHaveProperty('test', true); }); it('pauses query with ref variable', async () => { const pause = ref(true); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), pause, }); await query$; expect(executeQuery).not.toHaveBeenCalled(); pause.value = false; await query$; expect(executeQuery).toHaveBeenCalledTimes(1); query$.pause(); query$.resume(); await query$; expect(executeQuery).toHaveBeenCalledTimes(2); }); it('pauses query with computed variable', async () => { const pause = ref(true); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), pause: computed(() => pause.value), }); await query$; expect(executeQuery).not.toHaveBeenCalled(); pause.value = false; await query$; expect(executeQuery).toHaveBeenCalledTimes(1); query$.pause(); query$.resume(); await query$; // this shouldn't be called, as pause/resume functionality should works in sync with passed `pause` variable, e.g.: // if we pass readonly computed variable, then we want to make sure that its value fully controls the state of the request. expect(executeQuery).toHaveBeenCalledTimes(1); }); it('pauses query with callback', async () => { const pause = ref(true); const { executeQuery, query$ } = createQuery({ query: ref('{ test }'), pause: () => pause.value, }); await query$; expect(executeQuery).not.toHaveBeenCalled(); pause.value = false; await query$; expect(executeQuery).toHaveBeenCalledTimes(1); query$.pause(); query$.resume(); await query$; // the same as computed variable example - user has full control over the request state if using callback expect(executeQuery).toHaveBeenCalledTimes(1); }); }); ================================================ FILE: packages/vue-urql/src/useQuery.ts ================================================ /* eslint-disable react-hooks/rules-of-hooks */ import type { Ref, WatchStopHandle } from 'vue'; import { shallowRef, watchEffect } from 'vue'; import type { Subscription } from 'wonka'; import { pipe, subscribe, onEnd } from 'wonka'; import type { Client, AnyVariables, GraphQLRequestParams, CombinedError, OperationContext, RequestPolicy, Operation, } from '@urql/core'; import { useClient } from './useClient'; import type { MaybeRefOrGetter, MaybeRefOrGetterObj } from './utils'; import { useRequestState, useClientState } from './utils'; /** Input arguments for the {@link useQuery} function. * * @param query - The GraphQL query that `useQuery` executes. * @param variables - The variables for the GraphQL query that `useQuery` executes. */ export type UseQueryArgs< Data = any, Variables extends AnyVariables = AnyVariables, > = { /** Updates the {@link RequestPolicy} for the executed GraphQL query operation. * * @remarks * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation * that `useQuery` executes, and indicates a caching strategy for cache exchanges. * * For example, when set to `'cache-and-network'`, {@link useQuery} will * receive a cached result with `stale: true` and an API request will be * sent in the background. * * @see {@link OperationContext.requestPolicy} for where this value is set. */ requestPolicy?: MaybeRefOrGetter; /** Updates the {@link OperationContext} for the executed GraphQL query operation. * * @remarks * `context` may be passed to {@link useQuery}, to update the {@link OperationContext} * of a query operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * @example * ```ts * const result = useQuery({ * query, * context: { * additionalTypenames: ['Item'], * }, * }); * ``` */ context?: MaybeRefOrGetter>; /** Prevents {@link useQuery} from automatically executing GraphQL query operations. * * @remarks * `pause` may be set to `true` to stop {@link useQuery} from executing * automatically. This will pause the query until {@link UseQueryState.resume} * is called, or, if `pause` is a reactive ref of a boolean, until this * ref changes to `true`. * * @see {@link https://urql.dev/goto/docs/basics/vue#pausing-usequery} for * documentation on the `pause` option. */ pause?: MaybeRefOrGetter; } & MaybeRefOrGetterObj>; /** State of the current query, your {@link useQuery} function is executing. * * @remarks * `UseQueryState` is returned by {@link useQuery} and * gives you the updating {@link OperationResult} of * GraphQL queries. * * Each value that is part of the result is wrapped in a reactive ref * and updates as results come in. * * Hint: Even when the query and variables update, the previous state of * the last result is preserved, which allows you to display the * previous state, while implementing a loading indicator separately. */ export interface UseQueryState { /** Indicates whether `useQuery` is waiting for a new result. * * @remarks * When `useQuery` receives a new query and/or variables, it will * start executing the new query operation and `fetching` is set to * `true` until a result arrives. * * Hint: This is subtly different than whether the query is actually * fetching, and doesn’t indicate whether a query is being re-executed * in the background. For this, see {@link UseQueryState.stale}. */ fetching: Ref; /** Indicates that the state is not fresh and a new result will follow. * * @remarks * The `stale` flag is set to `true` when a new result for the query * is expected and `useQuery` is waiting for it. This may indicate that * a new request is being requested in the background. * * @see {@link OperationResult.stale} for the source of this value. */ stale: Ref; /** Reactive {@link OperationResult.data} for the executed query. */ data: Ref; /** Reactive {@link OperationResult.error} for the executed query. */ error: Ref; /** Reactive {@link OperationResult.extensions} for the executed query. */ extensions: Ref | undefined>; /** Reactive {@link Operation} that the current state is for. * * @remarks * This is the {@link Operation} that is currently being executed. * When {@link UseQueryState.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation: Ref | undefined>; /** Indicates whether {@link useQuery} is currently paused. * * @remarks * When `useQuery` has been paused, it will stop receiving updates * from the {@link Client} and won’t execute query operations, until * {@link UseQueryArgs.pause} becomes `true` or {@link UseQueryState.resume} * is called. * * @see {@link https://urql.dev/goto/docs/basics/vue#pausing-usequery} for * documentation on the `pause` option. */ isPaused: Ref; /** The {@link OperationResult.hasNext} for the executed query. */ hasNext: Ref; /** Resumes {@link useQuery} if it’s currently paused. * * @remarks * Resumes or starts {@link useQuery}’s query, if it’s currently paused. * * @see {@link https://urql.dev/goto/docs/basics/vue#pausing-usequery} for * documentation on the `pause` option. */ resume(): void; /** Pauses {@link useQuery} to stop it from executing the query. * * @remarks * Pauses {@link useQuery}’s query, which stops it from receiving updates * from the {@link Client} and to stop the ongoing query operation. * * @see {@link https://urql.dev/goto/docs/basics/vue#pausing-usequery} for * documentation on the `pause` option. */ pause(): void; /** Triggers {@link useQuery} to execute a new GraphQL query operation. * * @param opts - optionally, context options that will be merged with * {@link UseQueryArgs.context} and the `Client`’s options. * * @remarks * When called, {@link useQuery} will re-execute the GraphQL query operation * it currently holds, unless it’s currently paused. * * This is useful for re-executing a query and get a new network result, * by passing a new request policy. * * ```ts * const result = useQuery({ query }); * * const refresh = () => { * // Re-execute the query with a network-only policy, skipping the cache * result.executeQuery({ requestPolicy: 'network-only' }); * }; * ``` */ executeQuery(opts?: Partial): UseQueryResponse; } /** Return value of {@link useQuery}, which is an awaitable {@link UseQueryState}. * * @remarks * {@link useQuery} returns a {@link UseQueryState} but may also be * awaited inside a Vue `async setup()` function. If it’s awaited * the query is executed before resolving. */ export type UseQueryResponse< T, V extends AnyVariables = AnyVariables, > = UseQueryState & PromiseLike>; /** Function to run a GraphQL query and get reactive GraphQL results. * * @param args - a {@link UseQueryArgs} object, to pass a `query`, `variables`, and options. * @returns a {@link UseQueryResponse} object. * * @remarks * `useQuery` allows GraphQL queries to be defined and executed inside * Vue `setup` functions. * Given {@link UseQueryArgs.query}, it executes the GraphQL query with the * provided {@link Client}. * * The returned result’s reactive values update when the `Client` has * new results for the query, and changes when your input `args` change. * * Additionally, `useQuery` may also be awaited inside an `async setup()` * function to use Vue’s Suspense feature. * * @see {@link https://urql.dev/goto/docs/basics/vue#queries} for `useQuery` docs. * * @example * ```ts * import { gql, useQuery } from '@urql/vue'; * * const TodosQuery = gql` * query { todos { id, title } } * `; * * export default { * setup() { * const result = useQuery({ query: TodosQuery }); * return { data: result.data }; * }, * }; * ``` */ export function useQuery( args: UseQueryArgs ): UseQueryResponse { return callUseQuery(args); } export function callUseQuery( args: UseQueryArgs, client: Ref = useClient(), stops?: WatchStopHandle[] ): UseQueryResponse { const data: Ref = shallowRef(); const { fetching, operation, extensions, stale, error, hasNext } = useRequestState(); const { isPaused, source, pause, resume, execute, teardown } = useClientState< T, V >(args, client, 'executeQuery'); const teardownQuery = watchEffect( onInvalidate => { if (source.value) { fetching.value = true; stale.value = false; onInvalidate( pipe( source.value, onEnd(() => { fetching.value = false; stale.value = false; hasNext.value = false; }), subscribe(res => { data.value = res.data; stale.value = !!res.stale; fetching.value = false; error.value = res.error; operation.value = res.operation; extensions.value = res.extensions; hasNext.value = res.hasNext; }) ).unsubscribe ); } else { fetching.value = false; stale.value = false; hasNext.value = false; } }, { // NOTE: This part of the query pipeline is only initialised once and will need // to do so synchronously flush: 'sync', } ); stops && stops.push(teardown, teardownQuery); const then: UseQueryResponse['then'] = (onFulfilled, onRejected) => { let sub: Subscription | void; const promise = new Promise>(resolve => { if (!source.value) { return resolve(state); } let hasResult = false; sub = pipe( source.value, subscribe(() => { if (!state.fetching.value && !state.stale.value) { if (sub) sub.unsubscribe(); hasResult = true; resolve(state); } }) ); if (hasResult) sub.unsubscribe(); }); return promise.then(onFulfilled, onRejected); }; const state: UseQueryState = { data, stale, error, operation, extensions, fetching, isPaused, hasNext, pause, resume, executeQuery(opts?: Partial): UseQueryResponse { execute(opts); return { ...state, then }; }, }; return { ...state, then }; } ================================================ FILE: packages/vue-urql/src/useSubscription.test.ts ================================================ // @vitest-environment jsdom import { OperationResult, OperationResultSource } from '@urql/core'; import { nextTick, readonly, ref } from 'vue'; import { vi, expect, it, describe } from 'vitest'; vi.mock('./useClient.ts', async () => ({ __esModule: true, ...(await vi.importActual('./useClient.ts')), useClient: () => ref(client), })); import { makeSubject } from 'wonka'; import { createClient } from '@urql/core'; import { useSubscription } from './useSubscription'; const client = createClient({ url: '/graphql', exchanges: [] }); describe('useSubscription', () => { it('subscribes to a subscription and updates data', async () => { const subject = makeSubject(); const executeQuery = vi .spyOn(client, 'executeSubscription') .mockImplementation( () => subject.source as OperationResultSource ); const sub = useSubscription({ query: `{ test }`, }); expect(readonly(sub)).toMatchObject({ data: undefined, stale: false, fetching: true, error: undefined, extensions: undefined, operation: undefined, isPaused: false, pause: expect.any(Function), resume: expect.any(Function), executeSubscription: expect.any(Function), }); expect(executeQuery).toHaveBeenCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: {}, }, expect.any(Object) ); expect(sub.fetching.value).toBe(true); subject.next({ data: { test: true } }); expect(sub.data.value).toHaveProperty('test', true); subject.complete(); expect(sub.fetching.value).toBe(false); }); it('updates the executed subscription when inputs change', async () => { const subject = makeSubject(); const executeSubscription = vi .spyOn(client, 'executeSubscription') .mockImplementation( () => subject.source as OperationResultSource ); const variables = ref({}); const sub = useSubscription({ query: `{ test }`, variables, }); expect(executeSubscription).toHaveBeenCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: {}, }, expect.any(Object) ); subject.next({ data: { test: true } }); expect(sub.data.value).toHaveProperty('test', true); variables.value = { test: true }; await nextTick(); expect(executeSubscription).toHaveBeenCalledTimes(2); expect(executeSubscription).toHaveBeenCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: { test: true }, }, expect.any(Object) ); expect(sub.fetching.value).toBe(true); expect(sub.data.value).toHaveProperty('test', true); }); it('supports a custom scanning handler', async () => { const subject = makeSubject(); const executeSubscription = vi .spyOn(client, 'executeSubscription') .mockImplementation( () => subject.source as OperationResultSource ); const scanHandler = (currentState: any, nextState: any) => ({ counter: (currentState ? currentState.counter : 0) + nextState.counter, }); const sub = useSubscription( { query: `subscription { counter }`, }, scanHandler ); expect(executeSubscription).toHaveBeenCalledWith( { key: expect.any(Number), query: expect.any(Object), variables: {}, }, expect.any(Object) ); subject.next({ data: { counter: 1 } }); expect(sub.data.value).toHaveProperty('counter', 1); subject.next({ data: { counter: 2 } }); expect(sub.data.value).toHaveProperty('counter', 3); }); }); ================================================ FILE: packages/vue-urql/src/useSubscription.ts ================================================ /* eslint-disable react-hooks/rules-of-hooks */ import { pipe, subscribe, onEnd } from 'wonka'; import type { Ref, WatchStopHandle } from 'vue'; import { shallowRef, isRef, watchEffect } from 'vue'; import type { Client, GraphQLRequestParams, AnyVariables, CombinedError, OperationContext, Operation, } from '@urql/core'; import { useClient } from './useClient'; import type { MaybeRefOrGetter, MaybeRefOrGetterObj } from './utils'; import { useRequestState, useClientState } from './utils'; /** Input arguments for the {@link useSubscription} function. * * @param query - The GraphQL subscription document that `useSubscription` executes. * @param variables - The variables for the GraphQL subscription that `useSubscription` executes. */ export type UseSubscriptionArgs< Data = any, Variables extends AnyVariables = AnyVariables, > = { /** Prevents {@link useSubscription} from automatically executing GraphQL subscription operations. * * @remarks * `pause` may be set to `true` to stop {@link useSubscription} from starting * its subscription automatically. This will pause the subscription until * {@link UseSubscriptionResponse.resume} is called, or, if `pause` is a reactive * ref of a boolean, until this ref changes to `true`. */ pause?: MaybeRefOrGetter; /** Updates the {@link OperationContext} for the executed GraphQL subscription operation. * * @remarks * `context` may be passed to {@link useSubscription}, to update the {@link OperationContext} * of a subscription operation. This may be used to update the `context` that exchanges * will receive for a single hook. * * @example * ```ts * const result = useQuery({ * query, * context: { * additionalTypenames: ['Item'], * }, * }); * ``` */ context?: MaybeRefOrGetter>; } & MaybeRefOrGetterObj>; /** Combines previous data with an incoming subscription result’s data. * * @remarks * A `SubscriptionHandler` may be passed to {@link useSubscription} to * aggregate subscription results into a combined {@link UseSubscriptionResponse.data} * value. * * This is useful when a subscription event delivers a single item, while * you’d like to display a list of events. * * @example * ```ts * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * const combineNotifications = (notifications = [], data) => { * return [...notifications, data.newNotification]; * }; * * const result = useSubscription( * { query: NotificationsSubscription }, * combineNotifications, * ); * ``` */ export type SubscriptionHandler = (prev: R | undefined, data: T) => R; /** A {@link SubscriptionHandler} or a reactive ref of one. */ export type SubscriptionHandlerArg = | Ref> | SubscriptionHandler; /** State of the current query, your {@link useSubscription} function is executing. * * @remarks * `UseSubscriptionResponse` is returned by {@link useSubscription} and * gives you the updating {@link OperationResult} of GraphQL subscriptions. * * Each value that is part of the result is wrapped in a reactive ref * and updates as results come in. * * Hint: Even when the query and variables update, the prior state of * the last result is preserved. */ export interface UseSubscriptionResponse< T = any, R = T, V extends AnyVariables = AnyVariables, > { /** Indicates whether `useSubscription`’s subscription is active. * * @remarks * When `useSubscription` starts a subscription, the `fetching` flag * is set to `true` and will remain `true` until the subscription * completes on the API, or `useSubscription` is paused. */ fetching: Ref; /** Indicates that the subscription result is not fresh. * * @remarks * This is mostly unused for subscriptions and will rarely affect you, and * is more relevant for queries. * * @see {@link OperationResult.stale} for the source of this value. */ stale: Ref; /** Reactive {@link OperationResult.data} for the executed subscription, or data returned by the handler. * * @remarks * `data` will be set to the last {@link OperationResult.data} value * received for the subscription. * * It will instead be set to the values that {@link SubscriptionHandler} * returned, if a handler has been passed to {@link useSubscription}. */ data: Ref; /** Reactive {@link OperationResult.error} for the executed subscription. */ error: Ref; /** Reactive {@link OperationResult.extensions} for the executed mutation. */ extensions: Ref | undefined>; /** Reactive {@link Operation} that the current state is for. * * @remarks * This is the subscription {@link Operation} that is currently active. * When {@link UseSubscriptionResponse.fetching} is `true`, this is the * last `Operation` that the current state was for. */ operation: Ref | undefined>; /** Indicates whether {@link useSubscription} is currently paused. * * @remarks * When `useSubscription` has been paused, it will stop receiving updates * from the {@link Client} and won’t execute the subscription, until * {@link UseSubscriptionArgs.pause} becomes true or * {@link UseSubscriptionResponse.resume} is called. */ isPaused: Ref; /** Resumes {@link useSubscription} if it’s currently paused. * * @remarks * Resumes or starts {@link useSubscription}’s subscription, if it’s currently paused. */ resume(): void; /** Pauses {@link useSubscription} to stop the subscription. * * @remarks * Pauses {@link useSubscription}’s subscription, which stops it * from receiving updates from the {@link Client} and to stop executing * the subscription operation. */ pause(): void; /** Triggers {@link useQuery} to reexecute a GraphQL subscription operation. * * @param opts - optionally, context options that will be merged with * {@link UseQueryArgs.context} and the `Client`’s options. * * @remarks * When called, {@link useSubscription} will re-execute the GraphQL subscription * operation it currently holds, unless it’s currently paused. */ executeSubscription(opts?: Partial): void; } /** Function to run a GraphQL subscription and get reactive GraphQL results. * * @param args - a {@link UseSubscriptionArgs} object, to pass a `query`, `variables`, and options. * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results. * @returns a {@link UseSubscriptionResponse} object. * * @remarks * `useSubscription` allows GraphQL subscriptions to be defined and executed inside * Vue `setup` functions. * Given {@link UseSubscriptionArgs.query}, it executes the GraphQL subscription with the * provided {@link Client}. * * The returned result updates when the `Client` has new results * for the subscription, and `data` is updated with the result’s data * or with the `data` that a `handler` returns. * * @example * ```ts * import { gql, useSubscription } from '@urql/vue'; * * const NotificationsSubscription = gql` * subscription { newNotification { id, text } } * `; * * export default { * setup() { * const result = useSubscription( * { query: NotificationsSubscription }, * function combineNotifications(notifications = [], data) { * return [...notifications, data.newNotification]; * }, * ); * // ... * }, * }; * ``` */ export function useSubscription< T = any, R = T, V extends AnyVariables = AnyVariables, >( args: UseSubscriptionArgs, handler?: SubscriptionHandlerArg ): UseSubscriptionResponse { return callUseSubscription(args, handler); } export function callUseSubscription< T = any, R = T, V extends AnyVariables = AnyVariables, >( args: UseSubscriptionArgs, handler?: SubscriptionHandlerArg, client: Ref = useClient(), stops?: WatchStopHandle[] ): UseSubscriptionResponse { const data: Ref = shallowRef(); const { fetching, operation, extensions, stale, error } = useRequestState< T, V >(); const { isPaused, source, pause, resume, execute, teardown } = useClientState< T, V >(args, client, 'executeSubscription'); const teardownSubscription = watchEffect(onInvalidate => { if (source.value) { fetching.value = true; onInvalidate( pipe( source.value, onEnd(() => { fetching.value = false; }), subscribe(result => { fetching.value = true; error.value = result.error; extensions.value = result.extensions; stale.value = !!result.stale; operation.value = result.operation; if (result.data != null && handler) { const cb = isRef(handler) ? handler.value : handler; if (typeof cb === 'function') { data.value = cb(data.value, result.data); return; } } data.value = result.data as R; }) ).unsubscribe ); } else { fetching.value = false; } }); stops && stops.push(teardown, teardownSubscription); const state: UseSubscriptionResponse = { data, stale, error, operation, extensions, fetching, isPaused, pause, resume, executeSubscription( opts?: Partial ): UseSubscriptionResponse { execute(opts); return state; }, }; return state; } ================================================ FILE: packages/vue-urql/src/utils.ts ================================================ import type { AnyVariables, Client, CombinedError, DocumentInput, GraphQLRequest, Operation, OperationContext, OperationResult, OperationResultSource, } from '@urql/core'; import { createRequest } from '@urql/core'; import { type Ref, unref } from 'vue'; import { watchEffect, isReadonly, computed, ref, shallowRef, isRef } from 'vue'; import type { UseSubscriptionArgs } from './useSubscription'; import type { UseQueryArgs } from './useQuery'; export type MaybeRefOrGetter = T | (() => T) | Ref; export type MaybeRefOrGetterObj = T extends Record ? T : { [K in keyof T]: MaybeRefOrGetter }; const isFunction = (val: MaybeRefOrGetter): val is () => T => typeof val === 'function'; const toValue = (source: MaybeRefOrGetter): T => isFunction(source) ? source() : unref(source); export const createRequestWithArgs = < T = any, V extends AnyVariables = AnyVariables, >( args: | UseQueryArgs | UseSubscriptionArgs | { query: MaybeRefOrGetter>; variables: V } ): GraphQLRequest => { const _args = toValue(args); return createRequest( toValue(_args.query), toValue(_args.variables as MaybeRefOrGetter) ); }; export const useRequestState = < T = any, V extends AnyVariables = AnyVariables, >() => { const hasNext: Ref = ref(false); const stale: Ref = ref(false); const fetching: Ref = ref(false); const error: Ref = shallowRef(); const operation: Ref | undefined> = shallowRef(); const extensions: Ref | undefined> = shallowRef(); return { hasNext, stale, fetching, error, operation, extensions, }; }; export function useClientState( args: UseQueryArgs | UseSubscriptionArgs, client: Ref, method: keyof Pick ) { const source: Ref> | undefined> = shallowRef(); const isPaused: Ref = isRef(args.pause) ? args.pause : typeof args.pause === 'function' ? computed(args.pause) : ref(!!args.pause); const request = computed(() => createRequestWithArgs(args)); const requestOptions = computed(() => { return 'requestPolicy' in args ? { requestPolicy: toValue(args.requestPolicy), ...toValue(args.context), } : { ...toValue(args.context), }; }); const pause = () => { if (!isReadonly(isPaused)) { isPaused.value = true; } }; const resume = () => { if (!isReadonly(isPaused)) { isPaused.value = false; } }; const executeRaw = (opts?: Partial) => { return client.value[method](request.value, { ...requestOptions.value, ...opts, }); }; const execute = (opts?: Partial) => { source.value = executeRaw(opts); }; // it's important to use `watchEffect()` here instead of `watch()` // because it listening for reactive variables inside `executeRaw()` function const teardown = watchEffect(() => { source.value = !isPaused.value ? executeRaw() : undefined; }); return { source, isPaused, pause, resume, execute, teardown, }; } ================================================ FILE: packages/vue-urql/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/vue-urql/vitest.config.ts ================================================ import { mergeConfig } from 'vitest/config'; import baseConfig from '../../vitest.config'; export default mergeConfig(baseConfig, {}); ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - 'packages/*' - 'exchanges/*' - '!examples/*' ================================================ FILE: scripts/actions/build-all.mjs ================================================ #!/usr/bin/env node import { listPackages } from './lib/packages.mjs'; import { buildPackage } from './lib/commands.mjs'; (async () => { try { const packages = await listPackages(); const builds = packages.map(buildPackage); await Promise.all(builds); } catch (e) { console.error(e.message); process.exit(1); } })(); ================================================ FILE: scripts/actions/lib/commands.mjs ================================================ import * as stream from 'stream'; import * as path from 'path'; import * as fs from 'fs'; import { promisify } from 'util'; import * as tar from 'tar'; import { execa, execaNode } from 'execa'; import Arborist from '@npmcli/arborist'; import packlist from 'npm-packlist'; import { workspaceRoot, require } from './constants.mjs'; import { getPackageManifest, getPackageArtifact } from './packages.mjs'; const pipeline = promisify(stream.pipeline); const buildPackage = async cwd => { const manifest = getPackageManifest(cwd); console.log('> Building', manifest.name); try { await execa('run-s', ['build'], { preferLocal: true, localDir: workspaceRoot, cwd, }); } catch (error) { console.error('> Build failed', manifest.name); throw error; } }; const preparePackage = async cwd => { const manifest = getPackageManifest(cwd); console.log('> Preparing', manifest.name); try { await execaNode(require.resolve('../../prepare/index.js'), { cwd }); } catch (error) { console.error('> Preparing failed', manifest.name); throw error; } }; const packPackage = async cwd => { const manifest = getPackageManifest(cwd); const artifact = getPackageArtifact(cwd); console.log('> Packing', manifest.name); const arborist = new Arborist({ path: cwd }); const tree = await arborist.loadActual(); try { await pipeline( tar.create( { cwd, prefix: 'package/', portable: true, gzip: true, }, (await packlist(tree)).map(f => `./${f}`) ), fs.createWriteStream(path.resolve(cwd, artifact)) ); } catch (error) { console.error('> Packing failed', manifest.name); throw error; } }; export { buildPackage, preparePackage, packPackage }; ================================================ FILE: scripts/actions/lib/constants.mjs ================================================ import * as url from 'url'; import * as path from 'path'; import { createRequire } from 'node:module'; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); export const workspaceRoot = path.resolve(__dirname, '../../../'); export const workspaces = ['packages/*', 'exchanges/*']; export const require = createRequire(import.meta.url); ================================================ FILE: scripts/actions/lib/github.mjs ================================================ import * as path from 'path'; import { getPackageManifest, getPackageArtifact } from './packages.mjs'; import { DefaultArtifactClient } from '@actions/artifact'; export const uploadArtifact = async cwd => { const manifest = getPackageManifest(cwd); const artifact = getPackageArtifact(cwd); console.log('> Uploading', manifest.name); try { const client = new DefaultArtifactClient(); await client.uploadArtifact(artifact, [path.resolve(cwd, artifact)], cwd, { continueOnError: false, }); } catch (error) { console.error('> Uploading failed', manifest.name); throw error; } }; ================================================ FILE: scripts/actions/lib/packages.mjs ================================================ import * as path from 'path'; import * as fs from 'fs/promises'; import glob from 'glob'; import { workspaceRoot, workspaces, require } from './constants.mjs'; const getPackageManifest = cwd => require(path.resolve(cwd, 'package.json')); const updatePackageManifest = async (cwd, manifest) => { const sortDependencies = dependencies => { if (dependencies == null) return undefined; return Object.keys(dependencies) .sort() .reduce((acc, key) => { acc[key] = dependencies[key]; return acc; }, {}); }; try { if (!!getPackageManifest(cwd)) { manifest.dependencies = sortDependencies(manifest.dependencies); manifest.devDependencies = sortDependencies(manifest.devDependencies); await fs.writeFile( path.resolve(cwd, 'package.json'), JSON.stringify(manifest, null, 2) + '\n' ); } } catch (_error) { throw new Error('package.json does not exist in: ' + cwd); } }; const getPackageArtifact = cwd => { const pkg = getPackageManifest(cwd); const name = pkg.name[0] === '@' ? pkg.name.slice(1).replace(/\//g, '-') : pkg.name; return `${name}-v${pkg.version}.tgz`; }; const listPackages = async () => { let manifests = await Promise.all( workspaces.map(dir => glob(`${dir}/package.json`)) ); manifests = manifests.reduce((acc, manifests) => { acc.push(...manifests); return acc; }, []); let packages = manifests .filter(pkg => !require(path.join(workspaceRoot, pkg)).private) .map(pkg => path.resolve(pkg, '../')); if (process.env.NODE_TOTAL) { const nodeTotal = parseInt(process.env.NODE_TOTAL, 10) || 1; const nodeIndex = parseInt(process.env.NODE_INDEX, 10) % nodeTotal; packages = packages.filter((_, i) => i % nodeTotal === nodeIndex); console.log(`> Node ${nodeIndex + 1} of ${nodeTotal}.`); } return packages; }; const listArtifacts = async () => { return (await listPackages()).map(cwd => { const artifact = getPackageArtifact(cwd); return path.resolve(cwd, artifact); }); }; export { getPackageManifest, updatePackageManifest, getPackageArtifact, listPackages, listArtifacts, }; ================================================ FILE: scripts/actions/pack-all.mjs ================================================ #!/usr/bin/env node import { listPackages } from './lib/packages.mjs'; import { preparePackage, packPackage } from './lib/commands.mjs'; import { uploadArtifact } from './lib/github.mjs'; (async () => { try { const isPR = process.env.GITHUB_EVENT_NAME === 'pull_request'; const packages = await listPackages(); const packs = packages.map(async cwd => { await preparePackage(cwd); await packPackage(cwd); if (isPR) { await uploadArtifact(cwd); } }); await Promise.all(packs); } catch (e) { console.error(e.message); process.exit(1); } })(); ================================================ FILE: scripts/babel/transform-debug-target.mjs ================================================ const visited = 'visitedByDebugTargetTransformer'; const warningDevCheckTemplate = ` process.env.NODE_ENV !== 'production' ? NODE : undefined `.trim(); const plugin = ({ template, types: t }) => { const wrapWithDevCheck = template.expression(warningDevCheckTemplate, { placeholderPattern: /^NODE$/, }); let name = 'unknownExchange'; return { visitor: { ExportNamedDeclaration(path) { if ( path.node.declaration && path.node.declaration.declarations && path.node.declaration.declarations[0] && path.node.declaration.declarations[0].id ) { const exportName = path.node.declaration.declarations[0].id.name; if (/Exchange$/i.test(exportName)) name = exportName; } }, CallExpression(path, meta) { if (path.node[visited] || !path.node.callee) return; if (path.node.callee.name === 'dispatchDebug') { path.node[visited] = true; if ( t.isObjectExpression(path.node.arguments[0]) && !meta.filename.endsWith('compose.ts') ) { path.node.arguments[0].properties.push( t.objectProperty(t.stringLiteral('source'), t.stringLiteral(name)) ); } path.replaceWith(wrapWithDevCheck({ NODE: path.node })); } }, }, }; }; export default plugin; ================================================ FILE: scripts/babel/transform-invariant-warning.mjs ================================================ const visited = 'visitedByInvariantWarningTransformer'; const warningDevCheckTemplate = ` if (process.env.NODE_ENV !== 'production') { NODE; } `.trim(); const plugin = ({ template, types: t }) => { const wrapWithDevCheck = template(warningDevCheckTemplate, { placeholderPattern: /^NODE$/, }); return { visitor: { CallExpression(path) { const { name } = path.node.callee; if ( (name === 'warn' || name === 'deprecationWarning') && !path.node[visited] ) { path.node[visited] = true; // The production-check may be hoisted if the parent // is already an if-statement only containing the // warn call let p = path; while (t.isExpressionStatement(p.parentPath.node)) { if ( t.isBlockStatement(p.parentPath.parentPath.node) && p.parentPath.parentPath.node.body.length === 1 && p.parentPath.parentPath.node.body[0] === path.parentPath.node && t.isIfStatement(p.parentPath.parentPath.parentPath.node) && p.parentPath.parentPath.parentPath.node.consequent === p.parentPath.parentPath.node && !p.parentPath.parentPath.node.alternate ) { p = p.parentPath.parentPath.parentPath; } else if ( t.isIfStatement(p.parentPath.parentPath.node) && p.parentPath.parentPath.node.consequent === p.parentPath.node && !p.parentPath.parentPath.node.alternate ) { p = path.parentPath.parentPath; } else { break; } } p.replaceWith(wrapWithDevCheck({ NODE: p.node })); } else if (name === 'invariant' && !path.node[visited]) { path.node[visited] = true; const formerNode = path.node.arguments[1]; path.node.arguments[1] = t.conditionalExpression( t.binaryExpression( '!==', t.memberExpression( t.memberExpression( t.identifier('process'), t.identifier('env') ), t.identifier('NODE_ENV') ), t.stringLiteral('production') ), formerNode, t.stringLiteral('') ); } }, }, }; }; export default plugin; ================================================ FILE: scripts/babel/transform-pipe.mjs ================================================ const pipeExpression = (t, pipeline) => { let x = pipeline[0]; for (let i = 1; i < pipeline.length; i++) x = t.callExpression(pipeline[i], [x]); return x; }; const pipePlugin = ({ types: t }) => ({ visitor: { ImportDeclaration(path, state) { if (path.node.source.value === 'wonka') { const { specifiers } = path.node; const pipeSpecifierIndex = specifiers.findIndex(spec => { return spec.imported.name === 'pipe'; }); if (pipeSpecifierIndex > -1) { const pipeSpecifier = specifiers[pipeSpecifierIndex]; state.pipeName = pipeSpecifier.local.name; if (specifiers.length > 1) { path.node.specifiers.splice(pipeSpecifierIndex, 1); } else { path.remove(); } } } }, CallExpression(path, state) { if (state.pipeName) { const callee = path.node.callee; const args = path.node.arguments; if (callee.name !== state.pipeName) { return; } else if (args.length === 0) { path.replaceWith(t.identifier('undefined')); } else { path.replaceWith(pipeExpression(t, args)); } } }, }, }); export default pipePlugin; ================================================ FILE: scripts/changesets/changelog.js ================================================ const { config } = require('dotenv'); const { getInfo } = require('@changesets/get-github-info'); config(); const REPO = 'urql-graphql/urql'; const SEE_LINE = /^See:\s*(.*)/i; const TRAILING_CHAR = /[.;:]$/g; const listFormatter = new Intl.ListFormat('en-US'); const getSummaryLines = cs => { let lines = cs.summary.trim().split(/\r?\n/); if (!lines.some(line => /```/.test(line))) { lines = lines.map(l => l.trim()).filter(Boolean); const size = lines.length; if (size > 0) { lines[size - 1] = lines[size - 1].replace(TRAILING_CHAR, ''); } } return lines; }; /** Creates a "(See X)" string from a template */ const templateSeeRef = links => { const humanReadableLinks = links.filter(Boolean).map(link => { if (typeof link === 'string') return link; return link.pull || link.commit; }); const size = humanReadableLinks.length; if (size === 0) return ''; const str = listFormatter.format(humanReadableLinks); return `(See ${str})`; }; const changelogFunctions = { getDependencyReleaseLine: async (changesets, dependenciesUpdated) => { if (dependenciesUpdated.length === 0) return ''; const dependenciesLinks = await Promise.all( changesets.map(async cs => { if (!cs.commit) return undefined; const lines = getSummaryLines(cs); const prLine = lines.find(line => SEE_LINE.test(line)); if (prLine) { const match = prLine.match(SEE_LINE); return (match && match[1].trim()) || undefined; } const { links } = await getInfo({ repo: REPO, commit: cs.commit, }); return links; }) ); let changesetLink = '- Updated dependencies'; const seeRef = templateSeeRef(dependenciesLinks); if (seeRef) changesetLink += ` ${seeRef}`; const detailsLinks = dependenciesUpdated.map(dep => { return ` - ${dep.name}@${dep.newVersion}`; }); return [changesetLink, ...detailsLinks].join('\n'); }, getReleaseLine: async (changeset, type) => { let pull, commit, user; const lines = getSummaryLines(changeset); const prLineIndex = lines.findIndex(line => SEE_LINE.test(line)); if (prLineIndex > -1) { const match = lines[prLineIndex].match(SEE_LINE); pull = (match && match[1].trim()) || undefined; lines.splice(prLineIndex, 1); } const [firstLine, ...futureLines] = lines; if (changeset.commit && !pull) { const { links } = await getInfo({ repo: REPO, commit: changeset.commit, }); pull = links.pull || undefined; commit = links.commit || undefined; user = links.user || undefined; } let annotation = ''; if (type === 'patch' && /^\s*fix/i.test(firstLine)) { annotation = '⚠️ '; } let str = `- ${annotation}${firstLine}`; if (futureLines.length > 0) { str += `\n${futureLines.map(l => ` ${l}`).join('\n')}`; } const endsWithParagraph = /(?<=(?:[!;?.]|```) *)$/g; if (user && !endsWithParagraph) { str += `, by ${user}`; } else { str += `\nSubmitted by ${user}`; } if (pull || commit) { const seeRef = templateSeeRef([pull || commit]); if (seeRef) str += ` ${seeRef}`; } return str; }, }; module.exports = { ...changelogFunctions, default: changelogFunctions, }; ================================================ FILE: scripts/changesets/jsr.mjs ================================================ #!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; import { getPackageManifest, listPackages } from '../actions/lib/packages.mjs'; const getExports = exports => { const exportNames = Object.keys(exports); const eventualExports = {}; for (const exportName of exportNames) { if (exportName.includes('package.json')) continue; const exp = exports[exportName]; eventualExports[exportName] = exp.source; } return eventualExports; }; export const updateJsr = async () => { (await listPackages()).forEach(dir => { const manifest = getPackageManifest(dir); const jsrManifest = { name: manifest.name, version: manifest.version, exports: manifest.exports ? getExports(manifest.exports) : manifest.source, exclude: [ 'node_modules', 'cypress', '**/*.test.*', '**/*.spec.*', '**/*.test.*.snap', '**/*.spec.*.snap', ], }; fs.writeFileSync( path.resolve(dir, 'jsr.json'), JSON.stringify(jsrManifest, undefined, 2) ); }); }; ================================================ FILE: scripts/changesets/version.mjs ================================================ #!/usr/bin/env node import glob from 'glob'; import { execa } from 'execa'; import { getPackageManifest, updatePackageManifest, listPackages, } from '../actions/lib/packages.mjs'; import { updateJsr } from './jsr.mjs'; const versionRe = /^\d+\.\d+\.\d+/i; const execaOpts = { stdio: 'inherit' }; await execa('changeset', ['version'], execaOpts); await execa('pnpm', ['install', '--lockfile-only'], execaOpts); const packages = (await listPackages()).reduce((map, dir) => { const manifest = getPackageManifest(dir); const versionMatch = manifest.version.match(versionRe); if (versionMatch) { const { name } = manifest; const version = `^${versionMatch[0]}`; map[name] = version; } return map; }, {}); const examples = (await glob('./examples/*/')).filter( x => !/node_modules$/.test(x) ); for (const example of examples) { let hadMatch = false; const manifest = getPackageManifest(example); if (manifest.dependencies) { for (const name in manifest.dependencies) { hadMatch = hadMatch || !!packages[name]; if (packages[name] && packages[name] !== manifest.dependencies) manifest.dependencies[name] = packages[name]; } } if (manifest.devDependencies) { for (const name in manifest.devDependencies) { hadMatch = hadMatch || !!packages[name]; if (packages[name] && packages[name] !== manifest.devDependencies) manifest.devDependencies[name] = packages[name]; } } if ( hadMatch && !(manifest.devDependencies || {})['@urql/core'] && !(manifest.dependencies || {})['@urql/core'] ) { (manifest.dependencies || manifest.devDependencies || {})['@urql/core'] = packages['@urql/core']; } await updatePackageManifest(example, manifest); } await updateJsr(); ================================================ FILE: scripts/eslint/preset.js ================================================ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 9, sourceType: 'module', ecmaFeatures: { modules: true, jsx: true, }, }, ignorePatterns: [ 'node_modules/', 'dist/', 'dist-prod/', 'build/', 'coverage/', 'benchmark/', 'docs/', ], settings: { react: { version: 'detect', }, }, extends: ['eslint:recommended', 'prettier'], plugins: ['@typescript-eslint', 'prettier', 'es5'], rules: { 'no-undef': 'off', 'no-empty': 'off', 'sort-keys': 'off', 'no-console': ['error', { allow: ['warn', 'error'] }], 'prefer-arrow/prefer-arrow-functions': 'off', 'es5/no-for-of': 'off', 'es5/no-generators': 'off', 'es5/no-typeof-symbol': 'warn', 'no-unused-vars': [ 'warn', { argsIgnorePattern: '^_', }, ], 'prettier/prettier': [ 'error', { singleQuote: true, arrowParens: 'avoid', trailingComma: 'es5', }, ], }, overrides: [ { extends: ['plugin:@typescript-eslint/recommended'], files: ['*.ts', '*.tsx'], rules: { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/ban-types': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/member-ordering': 'off', '@typescript-eslint/explicit-member-accessibility': 'off', '@typescript-eslint/no-object-literal-type-assertion': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-misused-new': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/array-type': 'off', 'import/no-internal-modules': 'off', 'no-restricted-syntax': [ 'error', { selector: 'PropertyDefinition[value]', message: 'Property definitions with value initializers aren’t transpiled', }, { selector: 'MemberExpression[optional=true]', message: 'Optional chaining (?.) operator is outside of specified browser support', }, { selector: 'LogicalExpression[operator="??"]', message: 'Nullish coalescing (??) operator is outside of specified browser support', }, { selector: 'AssignmentExpression[operator="??="]', message: 'Nullish coalescing assignment (??=) is outside of specified browser support', }, { selector: 'SequenceExpression', message: 'Sequence expressions are to be avoided since they can be confusing', }, { selector: ':not(ForStatement) > VariableDeclaration[declarations.length>1]', message: 'Only one variable declarator per variable declaration is preferred', }, ], '@typescript-eslint/no-import-type-side-effects': 'error', '@typescript-eslint/consistent-type-imports': [ 'error', { disallowTypeAnnotations: false, fixStyle: 'separate-type-imports', }, ], '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_', }, ], }, }, { extends: ['plugin:react/recommended'], files: ['*.tsx'], plugins: ['react-hooks'], rules: { 'react-hooks/rules-of-hooks': 'error', 'react-hooks/exhaustive-deps': 'warn', 'react/react-in-jsx-scope': 'off', 'react/prop-types': 'off', 'react/no-children-prop': 'off', }, }, { files: ['*.test.ts', '*.test.tsx', '*.spec.ts', '*.spec.tsx'], globals: { vi: true }, rules: { 'no-restricted-syntax': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/no-import-type-side-effects': 'off', '@typescript-eslint/consistent-type-imports': 'off', 'react-hooks/rules-of-hooks': 'off', 'react-hooks/exhaustive-deps': 'off', 'es5/no-for-of': 'off', 'es5/no-generators': 'off', 'es5/no-typeof-symbol': 'off', }, }, { files: ['*.js'], rules: { '@typescript-eslint/no-var-requires': 'off', 'consistent-return': 'warn', 'no-magic-numbers': 'off', 'es5/no-es6-methods': 'off', }, }, { files: ['*.jsx'], extends: ['plugin:react/recommended'], rules: { 'consistent-return': 'warn', 'no-magic-numbers': 'off', 'react/jsx-key': 'off', 'react/jsx-handler-names': 'off', 'es5/no-es6-methods': 'off', }, }, { files: ['examples/**/*.jsx'], rules: { 'react/prop-types': 'off', }, }, ], }; ================================================ FILE: scripts/prepare/index.js ================================================ #!/usr/bin/env node const invariant = require('invariant'); const path = require('path'); const fs = require('fs'); const cwd = process.cwd(); const workspaceRoot = path.resolve(__dirname, '../../'); const pkg = require(path.resolve(cwd, 'package.json')); const hasReact = [ 'dependencies', 'optionalDependencies', 'peerDependencies', ].some(dep => pkg[dep] && pkg[dep].react); const hasNext = [ 'dependencies', 'optionalDependencies', 'peerDependencies', ].some(dep => pkg[dep] && pkg[dep].next); const normalize = name => name .replace(/[@\s/.]+/g, ' ') .trim() .replace(/\s+/, '-') .toLowerCase(); const name = normalize(pkg.name); const posixPath = x => path.normalize(x).split(path.sep).join('/'); const is = (a, b) => posixPath(a) === posixPath(b); invariant( pkg.publishConfig.provenance === true, 'package.json:publishConfig.provenance must be set to true' ); if (pkg.name.startsWith('@urql/')) { invariant( pkg.publishConfig.access === 'public', 'package.json:publishConfig.access must be set to public for @urql/* packages' ); } invariant(!is(cwd, workspaceRoot), 'prepare-pkg must be run in a package.'); invariant( fs.existsSync(pkg.source || 'src/index.ts'), 'package.json:source must exist' ); if (hasReact && !hasNext) { invariant( is(pkg.main, path.join('dist', `${name}.js`)), 'package.json:main path must end in `.js` for packages depending on React.' ); invariant( is(pkg.module, path.join('dist', `${name}.es.js`)), 'package.json:module path must end in `.es.js` for packages depending on React.' ); } else { invariant( is(pkg.main, path.join('dist', `${name}`)), 'package.json:main path must be valid and have no extension' ); invariant( is(pkg.module, path.join('dist', `${name}.mjs`)), 'package.json:module path must be valid and ending in .mjs' ); } invariant( is(pkg.types, path.join('dist', `${name}.d.ts`)), 'package.json:types path must be valid' ); invariant( is(pkg.repository.directory, path.relative(workspaceRoot, cwd)), 'package.json:repository.directory path is invalid' ); invariant(pkg.sideEffects === false, 'package.json:sideEffects must be false'); invariant(!!pkg.author, 'package.json:author must be defined'); invariant(pkg.license === 'MIT', 'package.json:license must be "MIT"'); invariant( Array.isArray(pkg.files) && pkg.files.some(x => path.normalize(x).startsWith('dist')) && pkg.files.some(x => path.normalize(x) === 'LICENSE'), 'package.json:files must include "dist" and "LICENSE"' ); if (pkg.dependencies && pkg.dependencies['@urql/core']) { invariant( !!pkg.peerDependencies && !!pkg.peerDependencies['@urql/core'], 'package.json:peerDependencies must contain @urql/core.' ); } if (pkg.peerDependencies && pkg.peerDependencies['@urql/core']) { invariant( !!pkg.dependencies && !!pkg.dependencies['@urql/core'], 'package.json:dependencies must contain @urql/core.' ); } for (const key in pkg.peerDependencies || {}) { const dependency = pkg.peerDependencies[key]; invariant( key !== 'react' || key !== 'preact' || !dependency.includes('>='), `Peer Dependency "${key}" must not contain ">=" (greater than) range` ); } if (hasReact && !hasNext) { invariant( !pkg.exports, 'package.json:exports must not be added for packages depending on React.' ); } else { invariant( !!pkg.exports, 'package.json:exports must be added and have a "." entry' ); invariant(!!pkg.exports['.'], 'package.json:exports must have a "." entry'); invariant( !!pkg.exports['./package.json'], 'package.json:exports must have a "./package.json" entry' ); for (const key in pkg.exports) { const entry = pkg.exports[key]; if (entry === './package.json') continue; const entryName = normalize(key); const bundleName = entryName ? `${name}-${entryName}` : name; invariant( fs.existsSync(entry.source), `package.json:exports["${key}"].source must exist` ); invariant( is(entry.require, `./dist/${bundleName}.js`), `package.json:exports["${key}"].require must be valid` ); invariant( is(entry.import, `./dist/${bundleName}.mjs`), `package.json:exports["${key}"].import must be valid` ); invariant( is(entry.types, `./dist/${bundleName}.d.ts`), 'package.json:types path must be valid' ); invariant( Object.keys(entry)[0] === 'types', 'package.json:types must come first' ); } } fs.copyFileSync( path.resolve(workspaceRoot, 'LICENSE'), path.resolve(cwd, 'LICENSE'), 0 ); ================================================ FILE: scripts/prepare/postinstall.js ================================================ const path = require('path'); const fs = require('fs'); try { const hookSource = path.resolve( __dirname, '../../node_modules/husky-v4/sh/husky.sh' ); const hook = path.resolve(__dirname, '../../.git/hooks/husky.sh'); const localHook = path.resolve(__dirname, '../../.git/hooks/husky.local.sh'); const gitConfig = path.resolve(__dirname, '../../.git/config'); let script = fs.readFileSync(hookSource, { encoding: 'utf-8' }); script = script.replace(`$(basename "$0")`, `$(basename "$0" .sh)`); let config = fs.readFileSync(gitConfig, { encoding: 'utf-8' }); config = config.replace(/\s*hooksPath\s*=\s*\.husky\n?/g, '\n'); fs.writeFileSync(hook, script); fs.writeFileSync(gitConfig, config); fs.writeFileSync(localHook, 'packageManager=yarn\n' + 'cd "."\n'); } catch { // Ignore errors in ci etc } ================================================ FILE: scripts/rollup/cleanup-plugin.mjs ================================================ import { createFilter } from '@rollup/pluginutils'; function cleanup() { const emptyImportRe = /import\s+(?:'[^']+'|"[^"]+")\s*;?/g; const gqlImportRe = /(import\s+(?:[*\s{}\w\d]+)\s*from\s*'graphql';?)/g; const dtsFilter = createFilter(/\.d\.ts(\.map)?$/, null, { resolve: false }); return { name: 'cleanup', renderChunk(input, chunk) { if (dtsFilter(chunk.fileName)) { return input .replace(emptyImportRe, '') .replace(gqlImportRe, x => '/*@ts-ignore*/\n' + x); } }, }; } export default cleanup; ================================================ FILE: scripts/rollup/config.mjs ================================================ import dts from 'rollup-plugin-dts'; import * as fs from 'fs/promises'; import { relative, join, dirname, basename } from 'path'; import { makePlugins, makeBasePlugins, makeOutputPlugins } from './plugins.mjs'; import cleanup from './cleanup-plugin.mjs'; import * as settings from './settings.mjs'; const plugins = makePlugins(); const isCI = !!process.env.CI; const chunkFileNames = extension => { let hasDynamicChunk = false; return chunkInfo => { if ( chunkInfo.isDynamicEntry || chunkInfo.isEntry || chunkInfo.isImplicitEntry ) { return `[name]${extension}`; } else if (!hasDynamicChunk) { hasDynamicChunk = true; return `${settings.name}-chunk${extension}`; } else { return `[name]-chunk${extension}`; } }; }; const input = settings.sources.reduce((acc, source) => { acc[source.name] = source.source; if (source.name !== settings.name) { const rel = relative(source.dir, process.cwd()); plugins.push({ async writeBundle() { const packageJson = JSON.stringify( { name: source.name, private: true, version: '0.0.0', main: join(rel, dirname(source.main), basename(source.main, '.js')), module: join(rel, source.module), types: join(rel, source.types), source: join(rel, source.source), exports: { '.': { types: join(rel, source.types), import: join(rel, source.module), require: join(rel, source.main), source: join(rel, source.source), }, './package.json': './package.json', }, }, null, 2 ).trim(); await fs.mkdir(source.dir, { recursive: true }); await fs.writeFile( join(source.dir, 'package.json'), packageJson + '\n' ); }, }); } return acc; }, {}); const output = ({ format, isProduction }) => { if (typeof isProduction !== 'boolean') throw new Error('Invalid option `isProduction` at output({ ... })'); if (format !== 'cjs' && format !== 'esm') throw new Error('Invalid option `format` at output({ ... })'); let extension = format === 'esm' ? settings.hasReact && !settings.hasNext ? '.es.js' : '.mjs' : '.js'; if (isProduction) { extension = '.min' + extension; } return { entryFileNames: `[name]${extension}`, chunkFileNames: chunkFileNames(extension), dir: './dist', exports: 'named', sourcemap: true, banner: chunk => (chunk.name === 'urql-next' ? '"use client"' : undefined), sourcemapExcludeSources: isCI, hoistTransitiveImports: false, indent: false, freeze: false, strict: false, format, plugins: makeOutputPlugins({ isProduction, extension: format === 'esm' ? '.mjs' : '.js', }), // NOTE: All below settings are important for cjs-module-lexer to detect the export // When this changes (and terser mangles the output) this will interfere with Node.js ESM intercompatibility esModule: format !== 'esm', externalLiveBindings: format !== 'esm', interop(id) { if (format === 'esm') { return 'esModule'; } else if (id === 'react') { return 'esModule'; } else { return 'auto'; } }, generatedCode: { preset: 'es5', reservedNamesAsProps: false, objectShorthand: false, constBindings: false, }, }; }; const commonConfig = { input, external: settings.isExternal, onwarn() {}, treeshake: { unknownGlobalSideEffects: false, tryCatchDeoptimization: false, moduleSideEffects: false, }, }; export default [ { ...commonConfig, plugins, output: [ output({ format: 'cjs', isProduction: false }), output({ format: 'esm', isProduction: false }), !isCI && output({ format: 'cjs', isProduction: true }), !isCI && output({ format: 'esm', isProduction: true }), ].filter(Boolean), }, { ...commonConfig, plugins: [ ...makeBasePlugins(), dts({ compilerOptions: { preserveSymlinks: false, }, }), ], output: { minifyInternalExports: false, entryFileNames: '[name].d.ts', chunkFileNames: chunkFileNames('.d.ts'), dir: './dist', plugins: [cleanup()], }, }, ]; ================================================ FILE: scripts/rollup/plugins.mjs ================================================ import * as path from 'path'; import * as React from 'react'; import commonjs from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; import replace from '@rollup/plugin-replace'; import babel from '@rollup/plugin-babel'; import visualizer from 'rollup-plugin-visualizer'; import terser from '@rollup/plugin-terser'; import cjsCheck from 'rollup-plugin-cjs-check'; import cleanup from './cleanup-plugin.mjs'; import babelPluginTransformPipe from '../babel/transform-pipe.mjs'; import babelPluginTransformInvariant from '../babel/transform-invariant-warning.mjs'; import babelPluginTransformDebugTarget from '../babel/transform-debug-target.mjs'; import * as settings from './settings.mjs'; export const makeBasePlugins = () => [ resolve({ dedupe: settings.externalModules, extensions: ['.js', '.ts', '.tsx'], mainFields: ['module', 'jsnext', 'main'], preferBuiltins: false, browser: true, }), commonjs({ ignoreGlobal: true, include: /\/node_modules\//, }), ]; export const makePlugins = () => [ ...makeBasePlugins(), babel({ babelrc: false, babelHelpers: 'bundled', extensions: ['js', 'jsx', 'ts', 'tsx'], exclude: 'node_modules/**', presets: [], plugins: [ '@babel/plugin-transform-typescript', '@babel/plugin-transform-block-scoping', babelPluginTransformDebugTarget, babelPluginTransformPipe, babelPluginTransformInvariant, settings.hasReact && [ '@babel/plugin-transform-react-jsx', { pragma: 'React.createElement', pragmaFrag: 'React.Fragment', useBuiltIns: true, }, ], !settings.hasReact && settings.hasPreact && [ '@babel/plugin-transform-react-jsx', { pragma: 'h', useBuiltIns: true, }, ], ].filter(Boolean), }), ]; export const makeOutputPlugins = ({ isProduction, extension }) => { if (typeof isProduction !== 'boolean') throw new Error( 'Missing option `isProduction` on makeOutputPlugins({ ... })' ); if (extension !== '.mjs' && extension !== '.js') throw new Error('Missing option `extension` on makeOutputPlugins({ ... })'); return [ isProduction && replace({ 'process.env.NODE_ENV': JSON.stringify('production'), }), cjsCheck({ extension }), cleanup(), isProduction ? terserMinified : extension !== '.js' ? terserPretty : null, isProduction && settings.isAnalyze && visualizer({ filename: path.resolve( settings.cwd, 'node_modules/.cache/analyze.html' ), sourcemap: true, }), ].filter(Boolean); }; const terserPretty = terser({ warnings: true, ecma: 2015, keep_fnames: true, ie8: false, compress: { pure_getters: true, toplevel: true, booleans_as_integers: false, keep_fnames: true, keep_fargs: true, if_return: false, ie8: false, sequences: false, loops: false, conditionals: false, join_vars: false, }, mangle: { module: true, keep_fnames: true, }, output: { comments: false, beautify: true, braces: true, indent_level: 2, }, }); const terserMinified = terser({ warnings: true, ecma: 2015, ie8: false, toplevel: true, compress: { keep_infinity: true, pure_getters: true, passes: 10, }, mangle: { module: true, }, output: { comments: false, }, }); ================================================ FILE: scripts/rollup/settings.mjs ================================================ import { readFileSync } from 'fs'; import path from 'path'; export const cwd = process.cwd(); export const pkg = JSON.parse( readFileSync(path.resolve(cwd, './package.json'), 'utf-8') ); export const types = path.resolve(cwd, 'dist/types/'); const normalize = name => name .replace(/[@\s\/\.]+/g, ' ') .trim() .replace(/\s+/, '-') .toLowerCase(); export const name = normalize(pkg.name); export const sources = pkg.exports ? Object.keys(pkg.exports) .map(entry => { if (entry === './package.json') return undefined; const exports = pkg.exports[entry]; const dir = normalize(entry); return { name: dir ? `${name}-${dir}` : name, dir: dir || '.', main: exports.require, module: exports.import, types: exports.types, source: exports.source, }; }) .filter(Boolean) : [ { name, source: pkg.source || './src/index.ts', }, ]; export const externalModules = ['dns', 'fs', 'path', 'url']; if (pkg.peerDependencies) externalModules.push(...Object.keys(pkg.peerDependencies)); if (pkg.devDependencies) externalModules.push(...Object.keys(pkg.devDependencies)); if (pkg.dependencies) externalModules.push(...Object.keys(pkg.dependencies)); if (pkg.optionalDependencies) externalModules.push(...Object.keys(pkg.optionalDependencies)); const prodDependencies = new Set([ ...Object.keys(pkg.peerDependencies || {}), ...Object.keys(pkg.dependencies || {}), ]); const externalPredicate = new RegExp(`^(${externalModules.join('|')})($|/)`); export const isExternal = id => { if (id === 'babel-plugin-transform-async-to-promises/helpers') return false; return externalPredicate.test(id); }; export const hasNext = prodDependencies.has('next'); export const hasReact = prodDependencies.has('react'); export const hasPreact = prodDependencies.has('preact'); export const hasSvelte = prodDependencies.has('svelte'); export const hasVue = prodDependencies.has('vue'); export const mayReexport = hasReact || hasPreact || hasSvelte || hasVue; export const isAnalyze = !!process.env.ANALYZE; ================================================ FILE: scripts/vitest/setup.js ================================================ // This script is run before each `.test.ts` file. globalThis.AbortController = undefined; globalThis.fetch = vi.fn(); process.on('unhandledRejection', error => { throw error; }); const originalConsole = console; globalThis.console = { ...originalConsole, warn: (vi.SpyInstance = () => { /* noop */ }), error: (vi.SpyInstance = message => { throw new Error(message); }), }; vi.spyOn(console, 'log'); vi.spyOn(console, 'warn'); vi.spyOn(console, 'error'); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "baseUrl": "./", "paths": { "urql": ["node_modules/urql/src", "packages/react-urql/src"], "*-urql": ["node_modules/*-urql/src", "packages/*-urql/src"], "@urql/exchange-*": [ "node_modules/@urql/exchange-*/src", "exchanges/*/src" ], "@urql/core/*": ["node_modules/@urql/core/src/*", "packages/core/src/*"], "@urql/devtools": ["node_modules/@urql/devtools"], "@urql/*": [ "node_modules/@urql/*/src", "packages/*-urql/src", "packages/*/src" ] }, "esModuleInterop": true, "isolatedModules": true, "forceConsistentCasingInFileNames": true, "noUnusedLocals": true, "noEmit": true, "lib": ["dom", "esnext"], "jsx": "react", "module": "es2015", "moduleResolution": "node", "target": "esnext", "strict": true, "noImplicitAny": false, "noUnusedParameters": true, "skipLibCheck": true }, "include": ["packages", "exchanges"], "exclude": [ "**/e2e-tests", "**/examples", "**/dist", "**/node_modules", "node_modules", "scripts" ] } ================================================ FILE: vercel.json ================================================ { "github": { "silent": true } } ================================================ FILE: vitest.config.ts ================================================ import { resolve } from 'path'; import { defineConfig } from 'vitest/config'; import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ resolve: { alias: { 'preact/hooks': __dirname + '/packages/preact-urql/node_modules/preact/hooks/dist/hooks.js', preact: __dirname + '/packages/preact-urql/node_modules/preact/dist/preact.js', }, }, plugins: [tsconfigPaths()], test: { globals: true, setupFiles: [resolve(__dirname, 'scripts/vitest/setup.js')], clearMocks: true, exclude: [ 'packages/solid-urql/**', 'packages/solid-start-urql/**', '**/node_modules/**', '**/dist/**', '**/cypress/**', '**/e2e-tests/**', '**/.{idea,git,cache,output,temp}/**', '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress}.config.*', ], }, });