GraphQL Summit
8 November 2018
curl
:
curl \
-X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ posts { title } }" }' \
https://1jzxrj179.lp.gql.zone/graphql
curl
:
curl \
-X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{ posts { title } }" }' \
https://1jzxrj179.lp.gql.zone/graphql
{
"data": {
"posts": [{
"title": "Introduction to GraphQL"
}, {
"title": "Welcome to Apollo"
}, {
"title": "Advanced GraphQL"
}]
}
}
fetch
:
await fetch('https://1jzxrj179.lp.gql.zone/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: '{ posts { title } }' }),
}).then(res => res.json())
const typeDefs = gql`
type Query {
hello: String,
document: Document
}
type Document {
id: String!
currentVersion: Version!
}
type Version {
id: String!
downloadUrl: String!
editors: [User]
}
type User {
id: String!
name: String!
}
`;
const resolvers = {
Query: {
hello: () => 'Hello world!',
document: () => {
return {
id: 'foo'
}
}
},
Document: {
currentVersion: async () => ({
id: uuid()
})
},
Version: {
downloadUrl: () => 'http://example.com',
editors: () => [{
id: '1',
name: 'Foo Bar'
}, {
id: '2',
name: 'Bar Baz'
}]
}
};
const documentQuery = gql`
query {
document {
id
currentVersion {
id
downloadUrl
}
}
}
`;
const editorsQuery = gql`
query {
document {
id
currentVersion {
id
editors {
name
}
}
}
}
`;
const resolvers = {
Query: {
hello: () => 'Hello world!',
document: () => {
return {
id: 'foo'
}
}
},
Document: {
currentVersion: async () => ({
id: uuid()
})
},
Version: {
downloadUrl: () => 'http://example.com',
editors: () => [{
id: '1',
name: 'Foo Bar'
}, {
id: '2',
name: 'Bar Baz'
}]
}
};
Image credit: Dhaivat Pandya
So that logically related (structurally similar) queries mutually benefit from the cache
And active queries can be updated automatically when new data arrive, based on logical dependencies
Further reading: GraphQL Concepts Visualized
ApolloCache
interface, and they have!This cache maintains an immutable & normalized graph of the values received from your GraphQL server. It enables the cache to return direct references to the cache, in order to satisfy queries. As a result, reads from the cache require minimal work (and can be optimized to constant time lookups in some cases). The tradeoff is that rather than receiving only the fields selected by a GraphQL query, there may be additional fields.
See yesterday's talk by Ian MacLeod for more details!
Bouncing back from a distant last place, the @apollographql
InMemoryCache
is now neck-and-neck with Hermes on @convoyteam 's own benchmark suite, which means the two fastest #GraphQL caching systems are both implementations of theApolloCache
interface:
Results worm their way into other parts of the system in spooky and indirect ways
readQuery
is called, the cache must extract a tree-shaped query result from the normalized graphbroadcastQueries
re-reads every watched query after any update
broadcastQueries
with lots of query observers would be a lot less expensive
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
console.log(fib(78)); // ???
import { wrap } from "optimism"
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
console.log(fib(78)); // ???
import { wrap } from "optimism"
const fib = wrap(function (n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
console.log(fib(78)); // ???
import { wrap } from "optimism"
const fib = wrap(function (n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
});
console.log(fib(7)); // 13
console.log(fib(8)); // 21
console.log(fib(9)); // 34
console.log(fib(10)); // 55
console.log(fib(78)); // 8944394323791464
function hashDirectory(dirPath) {
const hashesByFile = {};
readDirectory(dirPath).forEach(file => {
const absPath = path.join(dirPath, file);
if (isFile(absPath)) {
hashesByFile[file] = hashFile(absPath);
} else if (isDirectory(absPath)) {
hashesByFile[file] = hashDirectory(absPath);
}
});
return crypto.createHash("sha1")
.update(JSON.stringify(hashesByFile))
.digest("hex");
}
import { wrap } from "optimism"
function hashDirectory(dirPath) {
const hashesByFile = {};
readDirectory(dirPath).forEach(file => {
const absPath = path.join(dirPath, file);
if (isFile(absPath)) {
hashesByFile[file] = hashFile(absPath);
} else if (isDirectory(absPath)) {
hashesByFile[file] = hashDirectory(absPath);
}
});
return crypto.createHash("sha1")
.update(JSON.stringify(hashesByFile))
.digest("hex");
}
import { wrap } from "optimism"
const hashDirectory = wrap(function (dirPath) {
const hashesByFile = {};
readDirectory(dirPath).forEach(file => {
const absPath = path.join(dirPath, file);
if (isFile(absPath)) {
hashesByFile[file] = hashFile(absPath);
} else if (isDirectory(absPath)) {
hashesByFile[file] = hashDirectory(absPath);
}
});
return crypto.createHash("sha1")
.update(JSON.stringify(hashesByFile))
.digest("hex");
});
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
const cachedFunction = wrap(function originalFunction(a, b) {
// Original function body...
}, {
});
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
const cachedFunction = wrap(function originalFunction(a, b) {
// Original function body...
}, {
// Maximum number of cached values to retain.
max: Math.pow(2, 16),
});
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
const cachedFunction = wrap(function originalFunction(a, b) {
// Original function body...
}, {
// Maximum number of cached values to retain.
max: Math.pow(2, 16),
// Optional function that can be used to simplify (or reduce
// dimensionality) of the input arguments.
makeCacheKey(a, b) {
// Return a value that could be used as a key in a Map.
// Returning nothing (undefined) forces the actual function
// to be called, skipping all caching logic.
// The default behavior works even if a or b are objects:
return defaultMakeCacheKey(a, b);
},
});
optimism
API
import { wrap, defaultMakeCacheKey } from "optimism"
const cachedFunction = wrap(function originalFunction(a, b) {
// Original function body...
}, {
// Maximum number of cached values to retain.
max: Math.pow(2, 16),
// Optional function that can be used to simplify (or reduce
// dimensionality) of the input arguments.
makeCacheKey(a, b) {
// Return a value that could be used as a key in a Map.
// Returning nothing (undefined) forces the actual function
// to be called, skipping all caching logic.
// The default behavior works even if a or b are objects:
return defaultMakeCacheKey(a, b);
},
// Optional function that can be used to observe changes that
// might affect the validity of the cached value.
subscribe(a, b) {
const watcher = fs.watch(a, () => {
// Invalidates the cached value for these arguments, after
// transforming them with makeCacheKey. Idempotent.
cachedFunction.dirty(a, b);
};
return () => watcher.close();
}
});
optimism
function readDirectory(dir) {
return fs.readdirSync(dir);
}
optimism
function readDirectory(dir) {
return fs.readdirSync(dir);
}
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
optimism
function readDirectory(dir) {
return fs.readdirSync(dir);
}
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
optimism
function readDirectory(dir) {
return fs.readdirSync(dir);
}
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
optimism
import { wrap } from "optimism"
function readDirectory(dir) {
return fs.readdirSync(dir);
}
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
});
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
}
});
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
}
});
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
return () => watcher.close();
}
});
function isDirectory(path) {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
return () => watcher.close();
}
});
const isDirectory = wrap(path => {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}, {
subscribe(path) { ... }
});
function isFile(path) {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
optimism
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
return () => watcher.close();
}
});
const isDirectory = wrap(path => {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}, {
subscribe(path) { ... }
});
const isFile = wrap(path => {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}, {
subscribe(path) {
const watcher = fs.watch(path, () => {
isFile.dirty(path);
});
return () => watcher.close();
}
});
function hashFile(path) {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}
import { wrap } from "optimism"
const readDirectory = wrap(dir => {
return fs.readdirSync(dir);
}, {
subscribe(dir) {
const watcher = fs.watch(dir, () => {
readDirectory.dirty(dir);
});
return () => watcher.close();
}
});
const isDirectory = wrap(path => {
try {
return fs.statSync(path).isDirectory();
} catch (e) {
return false;
}
}, {
subscribe(path) { ... }
});
const isFile = wrap(path => {
try {
return fs.statSync(path).isFile();
} catch (e) {
return false;
}
}, {
subscribe(path) {
const watcher = fs.watch(path, () => {
isFile.dirty(path);
});
return () => watcher.close();
}
});
const hashFile = wrap(path => {
return crypto.createHash("sha1")
.update(fs.readFileSync(path))
.digest("hex");
}, {
subscribe(path) { ... }
});
optimism
What happens when hashFile.dirty(path)
is called?
apollo-cache-inmemory
?
export class DepTrackingCache implements NormalizedCache {
apollo-cache-inmemory
?import { wrap } from "optimism";
export class DepTrackingCache implements NormalizedCache {
apollo-cache-inmemory
?import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
apollo-cache-inmemory
?import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
apollo-cache-inmemory
?import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
}
apollo-cache-inmemory
?import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
apollo-cache-inmemory
?import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
public get(dataId: string): StoreObject {
this.depend(dataId);
return this.data[dataId];
}
apollo-cache-inmemory
?import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
public set(dataId: string, value: StoreObject) {
const oldValue = this.data[dataId];
if (value !== oldValue) {
this.data[dataId] = value;
this.depend.dirty(dataId);
}
}
apollo-cache-inmemory
?import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
public delete(dataId: string): void {
if (Object.prototype.hasOwnProperty.call(this.data, dataId)) {
delete this.data[dataId];
this.depend.dirty(dataId);
}
}
apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
Nothing changes in a
DepTrackingCache
without these methods being called
apollo-cache-inmemory
?
import { wrap } from "optimism";
type OptimisticWrapperFunction<
T = (...args: any[]) => any
> = T & { dirty: T };
export class DepTrackingCache implements NormalizedCache {
private depend: OptimisticWrapperFunction<(dataId: string) => StoreObject>;
constructor(private data: NormalizedCacheObject = Object.create(null)) {
this.depend = wrap((dataId: string) => this.data[dataId], {
makeCacheKey(dataId: string) {
return dataId;
}
});
}
Which makes DepTrackingCache
an extremely convenient bottleneck for dependency tracking
import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
private executeSelectionSet({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions): ExecResult {
const {
fragmentMap,
contextValue,
variableValues: variables,
} = execContext;
const object: StoreObject = contextValue.store.get(rootValue.id);
const finalResult: ExecResult = {
result: {},
};
// Recursively populate finalResult.result...
return finalResult;
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
});
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
}
});
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
);
}
});
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
);
}
});
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
);
}
});
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
selectionSet,
);
}
});
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
selectionSet,
JSON.stringify(execContext.variableValues),
);
}
});
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
selectionSet,
JSON.stringify(execContext.variableValues),
rootValue.id,
);
}
});
}
apollo-cache-inmemory
?import { wrap, defaultMakeCacheKey } from "optimism"
export class StoreReader {
constructor() {
const { executeSelectionSet } = this;
this.executeSelectionSet = wrap((options: ExecSelectionSetOptions) => {
return executeSelectionSet.call(this, options);
}, {
makeCacheKey({
selectionSet,
rootValue,
execContext,
}: ExecSelectionSetOptions) {
if (execContext.contextValue.store instanceof DepTrackingCache) {
return defaultMakeCacheKey(
execContext.contextValue.store,
execContext.query,
selectionSet,
JSON.stringify(execContext.variableValues),
rootValue.id,
);
}
}
});
}
apollo-cache-inmemory
?executeSelectionSet
but not executeField
QueryKeyMaker
)
===
equality
React.PureComponent
or React.memo
or
shouldComponentUpdate
can help
If you must modify result objects from the cache, be sure to write them back immediately