Skip to content

Commit cd52d74

Browse files
authored
feat: add redis example (#974)
1 parent 4013196 commit cd52d74

File tree

8 files changed

+409
-34
lines changed

8 files changed

+409
-34
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ liveQueryStore.invalidate([
9494

9595
Those invalidation calls could be done manually in the mutation resolvers or on more global reactive level e.g. as a listener on a database write log. The possibilities are infinite. 🤔
9696

97-
For scaling horizontally the independent `InMemoryLiveQueryStore` instances can be wired together via a PubSub system such as Redis.
97+
For scaling horizontally the independent `InMemoryLiveQueryStore` instances [can be wired together via a PubSub system such as Redis](./packages/example-redis).
9898

9999
### How are the updates sent/applied to the client?
100100

bob.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module.exports = {
99
"@n1ru4l/todo-example-server-ws",
1010
"@n1ru4l/todo-example-server-yoga",
1111
"@n1ru4l/end2end-tests",
12+
"@n1ru4l/example-redis",
1213
], // ignored packages
1314
base: "origin/main",
1415
};

packages/example-redis/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Redis Live Query Example
2+
3+
This example shows how you can distribute live query invalidation events across multiple live query store servers.
4+
5+
## Instructions
6+
7+
### Start Redis Container
8+
9+
```
10+
docker run -p "6379:6379" redis:7.0.2
11+
```
12+
13+
### Start GraphQL HTTP Server Instance 1
14+
15+
```
16+
npx cross-env PORT=3000 yarn start
17+
```
18+
19+
### Start GraphQL HTTP Server Instance 2
20+
21+
```
22+
npx cross-env PORT=3001 yarn start
23+
```
24+
25+
## Demo
26+
27+
Open the following links and execute the live query:
28+
29+
1. http://127.0.0.1:3001/graphql?query=query+%40live+%7B%0A++counter%0A%7D%0A
30+
31+
2. http://127.0.0.1:3000/graphql?query=query+%40live+%7B%0A++counter%0A%7D%0A
32+
33+
**Note:** Each of the live queries is executed on a different HTTP server (see the port)
34+
35+
Open http://127.0.0.1:3001/graphql?query=mutation+%7B%0A++increment%0A%7D
36+
37+
Execute the mutation operation.
38+
39+
See how all the live query results are updated automatically
40+
41+
**Bonus:**
42+
43+
1. Get the container id
44+
45+
```bash
46+
% docker ps
47+
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
48+
06239a997e72 redis:7.0.2 "docker-entrypoint.s…" 12 minutes ago Up 12 minutes 0.0.0.0:6379->6379/tcp gallant_mirzakhani
49+
```
50+
51+
2. Monitor the redis commands being executed
52+
53+
```bash
54+
% docker exec -ti 06239a997e72 redis-cli MONITOR
55+
OK
56+
```
57+
58+
3. Execute the mutation and observer the redis-cli output
59+
60+
```
61+
% docker exec -ti 06239a997e72 redis-cli MONITOR
62+
OK
63+
1663236183.245626 [0 172.17.0.1:62562] "incr" "counter"
64+
1663236183.247329 [0 172.17.0.1:62562] "publish" "live-query-invalidations" "Query.counter"
65+
1663236183.249530 [0 172.17.0.1:62562] "get" "counter"
66+
1663236183.249542 [0 172.17.0.1:62560] "get" "counter"
67+
```

packages/example-redis/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@n1ru4l/example-redis",
3+
"version": "0.1.2",
4+
"private": true,
5+
"dependencies": {
6+
"@n1ru4l/in-memory-live-query-store": "0.10.0",
7+
"@n1ru4l/graphql-live-query": "0.10.0",
8+
"graphql": "16.0.0-experimental-stream-defer.5",
9+
"@graphql-yoga/node": "2.13.13",
10+
"ioredis": "5.2.3"
11+
},
12+
"devDependencies": {
13+
"@types/node": "16.11.43",
14+
"ts-node-dev": "2.0.0",
15+
"typescript": "4.7.4"
16+
},
17+
"scripts": {
18+
"build": "tsc",
19+
"start": "ts-node-dev src/main.ts"
20+
},
21+
"bob": false
22+
}

packages/example-redis/src/main.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { InMemoryLiveQueryStore } from "@n1ru4l/in-memory-live-query-store";
2+
import { GraphQLLiveDirective } from "@n1ru4l/graphql-live-query";
3+
import { astFromDirective } from "@graphql-tools/utils";
4+
import { createServer, Plugin } from "@graphql-yoga/node";
5+
import Redis from "ioredis";
6+
import { execute as defaultExecute } from "graphql";
7+
8+
const httpPort = parseInt(process.env.PORT ?? "3000", 10);
9+
const redisUri = process.env.REDIS_URI ?? "redis://localhost:6379";
10+
11+
const inMemoryLiveQueryStore = new InMemoryLiveQueryStore();
12+
13+
const client = new Redis(redisUri);
14+
const subClient = new Redis(redisUri);
15+
16+
class RedisLiveQueryStore {
17+
pub: Redis;
18+
sub: Redis;
19+
channel: string;
20+
liveQueryStore: InMemoryLiveQueryStore;
21+
22+
constructor(
23+
pub: Redis,
24+
sub: Redis,
25+
channel: string,
26+
liveQueryStore: InMemoryLiveQueryStore
27+
) {
28+
this.pub = pub;
29+
this.sub = sub;
30+
this.liveQueryStore = liveQueryStore;
31+
this.channel = channel;
32+
33+
this.sub.subscribe(this.channel, (err) => {
34+
if (err) throw err;
35+
});
36+
37+
this.sub.on("message", (channel, resourceIdentifier) => {
38+
if (channel === this.channel && resourceIdentifier)
39+
this.liveQueryStore.invalidate(resourceIdentifier);
40+
});
41+
}
42+
43+
async invalidate(identifiers: Array<string> | string) {
44+
if (typeof identifiers === "string") {
45+
identifiers = [identifiers];
46+
}
47+
for (const identifier of identifiers) {
48+
this.pub.publish(this.channel, identifier);
49+
}
50+
}
51+
52+
makeExecute(execute: typeof defaultExecute) {
53+
return this.liveQueryStore.makeExecute(execute);
54+
}
55+
}
56+
57+
const liveQueryStore = new RedisLiveQueryStore(
58+
client,
59+
subClient,
60+
"live-query-invalidations",
61+
inMemoryLiveQueryStore
62+
);
63+
64+
const liveQueryPlugin: Plugin = {
65+
onExecute(params) {
66+
params.setExecuteFn(liveQueryStore.makeExecute(params.executeFn));
67+
},
68+
};
69+
70+
const server = createServer({
71+
context: () => ({
72+
liveQueryStore,
73+
redisClient: client,
74+
}),
75+
plugins: [liveQueryPlugin],
76+
port: httpPort,
77+
schema: {
78+
typeDefs: [
79+
/* GraphQL */ `
80+
type Query {
81+
counter: Int!
82+
}
83+
type Mutation {
84+
increment: Boolean
85+
}
86+
`,
87+
astFromDirective(GraphQLLiveDirective),
88+
],
89+
resolvers: {
90+
Query: {
91+
async counter(_, __, context) {
92+
const value = await context.redisClient.get("counter");
93+
return value == null ? 0 : parseInt(value, 10);
94+
},
95+
},
96+
Mutation: {
97+
async increment(_, __, context) {
98+
await context.redisClient.incr("counter");
99+
await context.liveQueryStore.invalidate(["Query.counter"]);
100+
},
101+
},
102+
},
103+
},
104+
});
105+
106+
server.start();

packages/example-redis/tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2019",
4+
"skipLibCheck": true,
5+
"esModuleInterop": true,
6+
"allowSyntheticDefaultImports": true,
7+
"strict": true,
8+
"module": "CommonJS",
9+
"moduleResolution": "node",
10+
"isolatedModules": true
11+
}
12+
}

packages/in-memory-live-query-store/README.md

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -90,33 +90,41 @@ List of known and tested compatible transports/servers:
9090
### Using with Redis
9191

9292
You can use Redis to synchronize invalidations across multiple instances.
93+
[A full runnable example can be found here](../example-redis).
9394

9495
```ts
9596
import Redis from "ioredis";
96-
import {
97-
InMemoryLiveQueryStore,
98-
InMemoryLiveQueryStoreParameter,
99-
} from "@n1ru4l/in-memory-live-query-store";
100-
import { ExecutionArgs, execute as defaultExecute } from "graphql";
97+
import { InMemoryLiveQueryStore } from "@n1ru4l/in-memory-live-query-store";
98+
import { execute as defaultExecute } from "graphql";
10199

102-
const CHANNEL = "LIVE_QUERY_INVALIDATIONS";
100+
const inMemoryLiveQueryStore = new InMemoryLiveQueryStore();
103101

104-
export class RedisLiveQueryStore {
105-
pub: Redis.Redis;
106-
sub: Redis.Redis;
107-
liveQueryStore: InMemoryLiveQueryStore;
102+
const client = new Redis(redisUri);
103+
const subClient = new Redis(redisUri);
108104

109-
constructor(redisUrl: string, parameter?: InMemoryLiveQueryStoreParameter) {
110-
this.pub = new Redis(redisUrl);
111-
this.sub = new Redis(redisUrl);
112-
this.liveQueryStore = new InMemoryLiveQueryStore(parameter);
105+
class RedisLiveQueryStore {
106+
pub: Redis;
107+
sub: Redis;
108+
channel: string;
109+
liveQueryStore: InMemoryLiveQueryStore;
113110

114-
this.sub.subscribe(CHANNEL, (err) => {
111+
constructor(
112+
pub: Redis,
113+
sub: Redis,
114+
channel: string,
115+
liveQueryStore: InMemoryLiveQueryStore
116+
) {
117+
this.pub = pub;
118+
this.sub = sub;
119+
this.liveQueryStore = liveQueryStore;
120+
this.channel = channel;
121+
122+
this.sub.subscribe(this.channel, (err) => {
115123
if (err) throw err;
116124
});
117125

118126
this.sub.on("message", (channel, resourceIdentifier) => {
119-
if (channel === CHANNEL && resourceIdentifier)
127+
if (channel === this.channel && resourceIdentifier)
120128
this.liveQueryStore.invalidate(resourceIdentifier);
121129
});
122130
}
@@ -126,12 +134,19 @@ export class RedisLiveQueryStore {
126134
identifiers = [identifiers];
127135
}
128136
for (const identifier of identifiers) {
129-
this.pub.publish(CHANNEL, identifier);
137+
this.pub.publish(this.channel, identifier);
130138
}
131139
}
132140

133141
makeExecute(execute: typeof defaultExecute) {
134-
return this.liveQueryStore.makeExecute(args);
142+
return this.liveQueryStore.makeExecute(execute);
135143
}
136144
}
145+
146+
const liveQueryStore = new RedisLiveQueryStore(
147+
client,
148+
subClient,
149+
"live-query-invalidations",
150+
inMemoryLiveQueryStore
151+
);
137152
```

0 commit comments

Comments
 (0)