Skip to content

Commit ff67109

Browse files
committed
Live Queries with @live
1 parent e9b6c89 commit ff67109

File tree

12 files changed

+1010
-85
lines changed

12 files changed

+1010
-85
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@graphprotocol/client-cli": patch
3+
---
4+
5+
### Dependencies Updates
6+
7+
- Updated dependency ([`@graphql-mesh/[email protected]` ↗︎](https://www.npmjs.com/package/@graphql-mesh/cli/v/0.75.7)) (was `0.75.6`, in `dependencies`)

.changeset/config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"react-query-example",
1616
"nextjs-example",
1717
"cross-chain-sdk",
18-
"cross-chain-extension"
18+
"cross-chain-extension",
19+
"live-queries-example"
1920
],
2021
"access": "public",
2122
"baseBranch": "main",

.changeset/metal-books-live.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphprotocol/client-polling-live': major
3+
'@graphprotocol/client-cli': patch
4+
---
5+
6+
New Polling-based Live Queries plugin

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ examples/**/node_modules
1717

1818
*.tsbuildinfo
1919
coverage/
20-
.bob
20+
.bob
21+
.env
22+
.graphclient
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
sources:
2+
- name: Sushi
3+
handler:
4+
graphql:
5+
endpoint: https://api.thegraph.com/subgraphs/name/sushiswap/exchange
6+
7+
plugins:
8+
- pollingLive:
9+
defaultInterval: 1000
10+
11+
documents:
12+
- ./example-query.graphql
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
query ExampleQuery @live {
2+
transactions(first: 2, orderBy: timestamp, orderDirection: desc) {
3+
id
4+
blockNumber
5+
timestamp
6+
}
7+
}

examples/live-queries/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "live-queries-example",
3+
"private": true,
4+
"version": "0.0.0",
5+
"scripts": {
6+
"build-client": "graphclient build",
7+
"check": "tsc --pretty --noEmit",
8+
"start": "graphclient serve-dev"
9+
},
10+
"dependencies": {
11+
"@graphprotocol/client-cli": "2.1.1",
12+
"@graphprotocol/client-polling-live": "0.0.0",
13+
"graphql": "16.5.0"
14+
}
15+
}

packages/polling-live/package.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"name": "@graphprotocol/client-polling-live",
3+
"version": "0.0.0",
4+
"description": "",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://github.com/graphprotocol/graph-client.git",
8+
"directory": "packages/polling-live"
9+
},
10+
"scripts": {
11+
"prepack": "bob prepack",
12+
"check": "tsc --pretty --noEmit"
13+
},
14+
"keywords": [
15+
"thegraph",
16+
"graphql",
17+
"client"
18+
],
19+
"license": "MIT",
20+
"sideEffects": false,
21+
"main": "dist/cjs/index.js",
22+
"module": "dist/esm/index.js",
23+
"typings": "dist/typings/index.d.ts",
24+
"typescript": {
25+
"definition": "dist/typings/index.d.ts"
26+
},
27+
"exports": {
28+
".": {
29+
"require": {
30+
"types": "./dist/typings/index.d.cts",
31+
"default": "./dist/cjs/index.js"
32+
},
33+
"import": {
34+
"types": "./dist/typings/index.d.ts",
35+
"default": "./dist/esm/index.js"
36+
},
37+
"default": {
38+
"types": "./dist/typings/index.d.ts",
39+
"default": "./dist/esm/index.js"
40+
}
41+
},
42+
"./package.json": "./package.json"
43+
},
44+
"publishConfig": {
45+
"directory": "dist",
46+
"access": "public"
47+
},
48+
"dependencies": {
49+
"@repeaterjs/repeater": "^3.0.4",
50+
"tslib": "2.4.0"
51+
},
52+
"peerDependencies": {
53+
"graphql": "^15.2.0 || ^16.0.0",
54+
"@graphql-tools/merge": "^8.3.1",
55+
"@envelop/core": "^2.4.2"
56+
},
57+
"type": "module"
58+
}

packages/polling-live/src/index.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { isAsyncIterable, Plugin } from '@envelop/core'
2+
import { DirectiveNode, GraphQLError, Kind, visit } from 'graphql'
3+
import { Repeater } from '@repeaterjs/repeater'
4+
import { mergeSchemas } from '@graphql-tools/schema'
5+
6+
export default function usePollingLive({ config: { defaultInterval = 1000 } = {} } = {}): Plugin<{}> {
7+
return {
8+
onSchemaChange({ schema, replaceSchema }) {
9+
if (!schema.getDirective('live')) {
10+
replaceSchema(
11+
mergeSchemas({
12+
schemas: [schema],
13+
typeDefs: /* GraphQL */ `
14+
directive @live(interval: Int) on QUERY
15+
`,
16+
}),
17+
)
18+
}
19+
},
20+
onExecute({ args, executeFn, setExecuteFn }) {
21+
let liveDirectiveNode: DirectiveNode | undefined
22+
args.document = visit(args.document, {
23+
OperationDefinition(node) {
24+
if (args.operationName != null && node.name?.value !== args.operationName) {
25+
return
26+
}
27+
const directives: DirectiveNode[] = []
28+
if (node.directives && node.operation === 'query') {
29+
for (const directive of node.directives) {
30+
if (directive.name.value === 'live') {
31+
liveDirectiveNode = directive
32+
} else {
33+
directives.push(directive)
34+
}
35+
}
36+
return {
37+
...node,
38+
directives,
39+
}
40+
}
41+
return node
42+
},
43+
})
44+
if (liveDirectiveNode) {
45+
const intervalArgNode = liveDirectiveNode.arguments?.find((argNode) => argNode.name.value === 'interval')
46+
let intervalMs = defaultInterval
47+
if (intervalArgNode?.value?.kind === Kind.INT) {
48+
intervalMs = parseInt(intervalArgNode.value.value)
49+
}
50+
51+
setExecuteFn(
52+
(args) =>
53+
new Repeater((push, stop) => {
54+
let finished = false
55+
async function pump() {
56+
if (finished) {
57+
return
58+
}
59+
const result: any = await executeFn(args)
60+
if (isAsyncIterable(result)) {
61+
push({
62+
data: null,
63+
errors: [new GraphQLError('Execution returned AsyncIterable which is not supported!')],
64+
isLive: true,
65+
})
66+
stop()
67+
return
68+
}
69+
result.isLive = true
70+
if (finished) {
71+
return
72+
}
73+
push(result)
74+
setTimeout(pump, intervalMs)
75+
}
76+
pump()
77+
stop.then(() => {
78+
finished = true
79+
})
80+
}) as any,
81+
)
82+
}
83+
},
84+
}
85+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { envelop, useSchema } from '@envelop/core'
2+
import { makeExecutableSchema } from '@graphql-tools/schema'
3+
import { parse } from 'graphql'
4+
import usePollingLive from '../src/index.js'
5+
6+
describe('Polling Live Queries', () => {
7+
const schema = makeExecutableSchema({
8+
typeDefs: /* GraphQL */ `
9+
type Query {
10+
timestamp: String
11+
}
12+
`,
13+
resolvers: {
14+
Query: {
15+
timestamp: () => new Date().toISOString(),
16+
},
17+
},
18+
})
19+
const getEnveloped = envelop({
20+
plugins: [useSchema(schema), usePollingLive()],
21+
})
22+
it('should create a live stream with the given argument as an interval', async () => {
23+
const enveloped = getEnveloped()
24+
25+
const result: any = await enveloped.execute({
26+
schema: enveloped.schema,
27+
document: parse(/* GraphQL */ `
28+
query TestQuery @live(interval: 500) {
29+
timestamp
30+
}
31+
`),
32+
operationName: 'TestQuery',
33+
})
34+
35+
expect(result[Symbol.asyncIterator]).toBeDefined()
36+
37+
let lastTimestamp
38+
let i = 0
39+
for await (const item of result) {
40+
expect(item?.data?.timestamp).toBeDefined()
41+
if (lastTimestamp) {
42+
expect(new Date(item?.data?.timestamp).getTime() - new Date(lastTimestamp).getTime()).toBeGreaterThan(0.49)
43+
}
44+
lastTimestamp = item?.data?.timestamp
45+
if (i > 2) {
46+
break
47+
}
48+
i++
49+
}
50+
expect.assertions(8)
51+
})
52+
it('should create a live stream with 1000 ms interval by default', async () => {
53+
const enveloped = getEnveloped()
54+
55+
const result: any = await enveloped.execute({
56+
schema: enveloped.schema,
57+
document: parse(/* GraphQL */ `
58+
query TestQuery @live {
59+
timestamp
60+
}
61+
`),
62+
operationName: 'TestQuery',
63+
})
64+
65+
expect(result[Symbol.asyncIterator]).toBeDefined()
66+
67+
let lastTimestamp
68+
let i = 0
69+
for await (const item of result) {
70+
expect(item?.data?.timestamp).toBeDefined()
71+
if (lastTimestamp) {
72+
expect(new Date(item?.data?.timestamp).getTime() - new Date(lastTimestamp).getTime()).toBeGreaterThan(0.99)
73+
}
74+
lastTimestamp = item?.data?.timestamp
75+
if (i > 2) {
76+
break
77+
}
78+
i++
79+
}
80+
expect.assertions(8)
81+
})
82+
it('should return regular results if there is no @live', async () => {
83+
const enveloped = getEnveloped()
84+
85+
const result = await enveloped.execute({
86+
schema: enveloped.schema,
87+
document: parse(/* GraphQL */ `
88+
query TestQuery {
89+
timestamp
90+
}
91+
`),
92+
operationName: 'TestQuery',
93+
})
94+
95+
expect(result?.data?.timestamp).toBeDefined()
96+
})
97+
})

0 commit comments

Comments
 (0)