Skip to content

Commit

Permalink
Add cache policy (HoudiniGraphql#157)
Browse files Browse the repository at this point in the history
* split up tests

* fix imports

* add utility for checking is selection is resolvable

* better tests for existing data check

* improved missing data test

* add hook to example that clears cache for requests

* cache should be disabled in the server hook to avoid any weird async issues

* added hook generator

* hook file is generated with init

* only generate the hook for kit

* disable the cache on the client

* add cache policy to internal schema and artifacts

* fetchPolicy uses artifact

* first pass at cache check

* implement rest of cache policies

* add missing import

* added test for happy path gc ticks

* more gc tests

* strengthen gc test

* tick garbage collector after queries

* fix closure over query variables when loading pages

* add gc ticks back

* removed unused log

* only clean up a record if it was on a field being deleted

* v0.10.6-alpha.0

* no need for hooks

* rename bufferSize to cacheBufferSize

* start documentation

* document cacheBufferSize

* clarify docs

* set CacheOrNetwork as default policy

* update snapshot

* update one more snapshot

* doc tweaks

* fix mock adapter

* import CachePolicy directly from types

* srcPath isn't used any more

* policy test shouldn't use default

* default cache policy can be overwritten

* add note about generating runtime before enum is defined for current users

* grammar tweak

* add cache sections to ToC

* pass routeQuery full payload

* updated snapshots

* CacheAndQuery calls refetch

* avoid double query from CacheAndNetwork if initial data was loaded over the network

* avoid double query in non-route components with CacheAndNetwork

* better non-route component refetch

* start pagination refetch

* first pass refetch for offsets

* refetch reloads current page

* remove refetch button from example

* fix CacheAndNetwork onMount logic

* update snapshot

* onMount is called onLoad

* NetworkOnly is default cache policy for now
  • Loading branch information
AlecAivazis committed Sep 9, 2021
1 parent d5b0e07 commit d20825d
Show file tree
Hide file tree
Showing 35 changed files with 4,927 additions and 3,637 deletions.
95 changes: 78 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@ for the generation of an incredibly lean GraphQL abstraction for your applicatio
1. [Fetching Data](#fetching-data)
1. [Query variables and page data](#query-variables-and-page-data)
1. [Loading State](#loading-state)
1. [Refetching Data](#refetching-data)
1. [Additional Logic](#additional-logic)
1. [Refetching Data](#refetching-data)
1. [Cache policy](#cache-policy)
1. [Data Retention](#data-retention)
1. [Changing default cache policy](#changing-default-cache-policy)
1. [What about load?](#what-about-load)
1. [Fragments](#fragments)
1. [Fragment Arguments](#fragment-arguments)
Expand Down Expand Up @@ -97,11 +100,11 @@ representation of your API's schema.
npx houdini init
```

> This will send a request to your API to download your schema definition. If you need
> headers to authenticate this request, you can pass them in with the `--pull-header`
> flag (abbreviated `-ph`). For example,
> `npx houdini init -ph Authorization="Bearer MyToken"`.
> You will also need to provide the same flag to `generate` when using the
> This will send a request to your API to download your schema definition. If you need
> headers to authenticate this request, you can pass them in with the `--pull-header`
> flag (abbreviated `-ph`). For example,
> `npx houdini init -ph Authorization="Bearer MyToken"`.
> You will also need to provide the same flag to `generate` when using the
> `--pull-schema` flag.
Finally, follow the steps appropriate for your framework.
Expand All @@ -116,7 +119,7 @@ import houdini from 'houdini-preprocess'

{
preprocess: [houdini()],

kit: {
vite: {
resolve: {
Expand Down Expand Up @@ -329,9 +332,9 @@ the result of query:

### Additional logic

Sometimes you will need to add additional logic to a component's query. For example, you might want to
Sometimes you will need to add additional logic to a component's query. For example, you might want to
check if the current session is valid before a query is sent to the server. In order to support this,
houdini will look for a function called `onLoad` defined in the module context which can be used to perform
houdini will look for a function called `onLoad` defined in the module context which can be used to perform
any logic you need. If you return a value from this function, it will be passed as props to your component:

```svelte
Expand All @@ -341,11 +344,11 @@ any logic you need. If you return a value from this function, it will be passed
if(!session.authenticated){
return this.redirect(302, '/login')
}
return {
message: "There are this many items"
}
}
}
</script>
<script>
Expand Down Expand Up @@ -393,6 +396,64 @@ Refetching data is done with the `refetch` function provided from the result of
<input type=checkbox bind:checked={completed}>
```

### Cache policy

By default, houdini will only try to load queries against its local cache when you indicate it is safe to do so.
This can be done with the `@cache` directive:

```graphql
query AllItems @cache(policy: CacheOrNetwork) {
items {
id
text
}
}
```

There are 3 different policies that can be specified:

- **CacheOrNetwork** will first check if a query can be resolved from the cache. If it can, it will return the cached value and only send a network request if data was missing.
- **CacheAndNetwork** will use cached data if it exists and always send a network request after the component has mounted to retrieve the latest data from the server
- **NetworkOnly** will never check if the data exists in the cache and always send a network request

#### Data Retention

Houdini will retain a query's data for a configurable number of queries (default 10).
For a concrete example, consider an example app that has 3 routes. If you load one of the
routes and then click between the other two 5 times, the first route's data will still be
resolvable (and the counter will reset if you visit it).
If you then toggle between the other routes 10 times and then try to load the first
route, a network request will be sent. This number is configurable with the
`cacheBufferSize` value in your config file:

```js
// houdini.config.js

export default {
// ...
cacheBufferSize: 5,
}
```

#### Changing default cache policy

As previously mentioned, the default cache policy is `CacheOrNetwork`. This can be changed
by setting the `defaultCachePolicy` config value:

```js
// houdini.config.js

import { CachePolicy } from '$houdini'

export default {
// ...

// note: if you are upgrading from a previous version of houdini, you might
// have to generate your runtime for this type to be defined.
defaultCachePolicy: CachePolicy.NetworkOnly,
}
```

### What about `load`?

Don't worry - that's where the preprocessor comes in. One of its responsibilities is moving the actual
Expand Down Expand Up @@ -976,7 +1037,7 @@ export default new Environment(async function ({ text, variables = {} }, session

## 🚦&nbsp;&nbsp;Persisted Queries

Sometimes you want to confine an API to only fire a set of pre-defined queries. This
Sometimes you want to confine an API to only fire a set of pre-defined queries. This
can be useful to not only reduce the amount of information transferred over the write
but also act as a list of approved queries, providing additional security. Regardless of
your motivation, the approach involves associating a known string with a particular query
Expand All @@ -985,10 +1046,10 @@ houdini passes a queries hash to the fetch function for you to use.

### Automatic Persisted Queries

An approach to Persisted Queries, popularized by Apollo, is known as
[Automatic Persisted Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/).
An approach to Persisted Queries, popularized by Apollo, is known as
[Automatic Persisted Queries](https://www.apollographql.com/docs/apollo-server/performance/apq/).
This involves first sending a queries hash and if its unrecognized, sending the full
query string. This might look something like:
query string. This might look something like:

```typescript
/// src/environment.ts
Expand Down Expand Up @@ -1024,7 +1085,7 @@ export default new Environment(async function({ text, variables = {}, hash }){
return response
}

// there were errors, send the hash and the query to associate the two for
// there were errors, send the hash and the query to associate the two for
// future requests
return await sendFetch.call(this, { variables, hash, text })
})
Expand All @@ -1045,7 +1106,7 @@ npx houdini generate -po ./path/to/persisted-queries.json

Once this map has been created, you will have to make it available to your server.

Now, instead of sending the full operation text with every request, you can now simply
Now, instead of sending the full operation text with every request, you can now simply
pass the hash under whatever field name you prefer:

```typescript
Expand Down
38 changes: 17 additions & 21 deletions example/src/routes/[filter].svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
completed: page.params.filter === 'completed',
}
}
</script>

<script lang="ts">
Expand All @@ -24,28 +23,29 @@
import { derived } from 'svelte/store'
// load the items
const { data, loadNextPage, pageInfo } = paginatedQuery<AllItems>(graphql`
query AllItems($completed: Boolean) {
filteredItems: items(completed: $completed, first: 2) @paginate(name: "Filtered_Items") {
edges {
node {
const { data, pageInfo, loadNextPage } = paginatedQuery<AllItems>(graphql`
query AllItems($completed: Boolean) @cache(policy: CacheAndNetwork) {
filteredItems: items(completed: $completed, first: 2)
@paginate(name: "Filtered_Items") {
edges {
node {
id
completed
...ItemEntry_item
}
}
}
allItems: items @list(name: "All_Items") {
edges {
node {
edges {
node {
id
completed
}
}
}
}
`)
// state and handler for the new item input
const addItem = mutation<AddItem>(graphql`
mutation AddItem($input: AddItemInput!) {
Expand All @@ -62,16 +62,14 @@
newItem {
item {
...All_Items_insert
...Filtered_Items_insert
@prepend(when_not: {completed: true})
...Filtered_Items_insert @prepend(when_not: { completed: true })
}
}
}
`)
$: numberOfItems = $data.allItems.edges.length
$: itemsLeft = $data.allItems.edges.filter(({node: item}) => !item.completed).length
$: itemsLeft = $data.allItems.edges.filter(({ node: item }) => !item.completed).length
// figure out the current page
const currentPage = derived(page, ($page) => {
Expand All @@ -93,14 +91,13 @@
inputValue = ''
}
}
</script>

<header class="header">
<a href="/">
<h1>todos</h1>
</a>
{#if $pageInfo.hasNextPage}
{#if $pageInfo.hasNextPage}
<nav>
<button on:click={() => loadNextPage()}>load more</button>
</nav>
Expand Down Expand Up @@ -138,23 +135,22 @@
</footer>
{/if}


<style>
nav {
nav {
position: absolute;
right: 0;
top: -30px;
}
button {
button {
border: 1px solid darkgray;
border-radius: 3px;
padding: 4px;
background: white;
cursor: pointer;
cursor: pointer;
}
button:active {
button:active {
background: #f6f6f6;
}
</style>
</style>
1 change: 0 additions & 1 deletion jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const config = testConfig()
expect.addSnapshotSerializer({
test: (val) => val && Object.keys(recast.types.namedTypes).includes(val.type),
serialize: (val) => {
console.log
return recast.print(val).code
},
})
Expand Down
21 changes: 20 additions & 1 deletion packages/houdini-common/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import fs from 'fs'
import path from 'path'
import mkdirp from 'mkdirp'
import os from 'os'
// locals
import { CachePolicy } from './types'

// the values we can take in from the config file
export type ConfigFile = {
Expand All @@ -17,6 +19,8 @@ export type ConfigFile = {
mode?: 'kit' | 'sapper'
framework?: 'kit' | 'sapper' | 'svelte'
module?: 'esm' | 'commonjs'
cacheBufferSize?: number
defaultCachePolicy?: CachePolicy
}

export type ScalarSpec = {
Expand Down Expand Up @@ -45,6 +49,8 @@ export class Config {
scalars?: ScalarMap
framework: 'sapper' | 'kit' | 'svelte' = 'sapper'
module: 'commonjs' | 'esm' = 'commonjs'
cacheBufferSize?: number
defaultCachePolicy: CachePolicy

constructor({
schema,
Expand All @@ -58,6 +64,8 @@ export class Config {
static: staticSite,
mode,
scalars,
cacheBufferSize,
defaultCachePolicy = CachePolicy.NetworkOnly,
}: ConfigFile & { filepath: string }) {
// make sure we got some kind of schema
if (!schema && !schemaPath) {
Expand Down Expand Up @@ -142,6 +150,8 @@ export class Config {
this.projectRoot = path.dirname(filepath)
this.static = staticSite
this.scalars = scalars
this.cacheBufferSize = cacheBufferSize
this.defaultCachePolicy = defaultCachePolicy

// if we are building a sapper project, we want to put the runtime in
// src/node_modules so that we can access @sapper/app and interact
Expand Down Expand Up @@ -303,6 +313,14 @@ export class Config {
return 'paginate'
}

get cacheDirective() {
return 'cache'
}

get cachePolicyArg() {
return 'policy'
}

paginationQueryName(documentName: string) {
return documentName + '_Pagination_Query'
}
Expand Down Expand Up @@ -347,6 +365,7 @@ export class Config {
this.argumentsDirective,
this.withDirective,
this.paginateDirective,
this.cacheDirective,
].includes(name.value) || this.isDeleteDirective(name.value)
)
}
Expand Down Expand Up @@ -538,7 +557,7 @@ export function testConfig(config: Partial<ConfigFile> = {}) {
cat: Cat
}
interface Node {
interface Node {
id: ID!
}
`,
Expand Down
7 changes: 7 additions & 0 deletions packages/houdini-common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ export type Script = {
start: number
end: number
}

export enum CachePolicy {
CacheOrNetwork = 'CacheOrNetwork',
CacheOnly = 'CacheOnly',
NetworkOnly = 'NetworkOnly',
CacheAndNetwork = 'CacheAndNetwork',
}
Loading

0 comments on commit d20825d

Please sign in to comment.