Skip to content

Commit df4f39b

Browse files
authored
feat: json patch deflate/inflate live query results adapters (#243)
* add prototype * refactor: simply logic and json payload * refactor: adjust api accorsing to feedback * fix: update lock file * refactor: simplify logic * refactor: restructure code * update example apps to use the live query patch inflator/deflator for smaller payloads over the wire 🎉 * update readme * update docs
1 parent f9720b6 commit df4f39b

32 files changed

+876
-199
lines changed

.changeset/large-taxis-act.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@n1ru4l/socket-io-graphql-client": minor
3+
---
4+
5+
Return AsyncIterableIterator for the execution result instead of taking a sink as an argument.

.changeset/red-gorillas-kiss.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@n1ru4l/socket-io-graphql-client": patch
3+
---
4+
5+
Correctly re-execute active operations after being offline.

.vscode/settings.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{
2-
"editor.formatOnSave": true
3-
}
2+
"editor.formatOnSave": true,
3+
"editor.defaultFormatter": "esbenp.prettier-vscode"
4+
}

README.md

+9-7
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414

1515
## Packages in this Repository
1616

17-
| Package | Description | Stats |
18-
| --------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
19-
| [`@n1ru4l/in-memory-live-query-store`](packages/in-memory-live-query-store) | Live query implementation. | [![npm version](https://img.shields.io/npm/v/@n1ru4l/in-memory-live-query-store.svg)](https://www.npmjs.com/package/@n1ru4l/in-memory-live-query-store) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/in-memory-live-query-store.svg)](https://www.npmjs.com/package/@n1ru4l/in-memory-live-query-store) |
20-
| [`@n1ru4l/graphql-live-query`](packages/graphql-live-query) | Utilities for live query implementations. | [![npm version](https://img.shields.io/npm/v/@n1ru4l/graphql-live-query.svg)](https://www.npmjs.com/package/@n1ru4l/graphql-live-query) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/graphql-live-query.svg)](https://www.npmjs.com/package/@n1ru4l/graphql-live-query) |
21-
| [`@n1ru4l/socket-io-graphql-server`](packages/socket-io-graphql-server) | GraphQL over Socket.io - Server Middleware | [![npm version](https://img.shields.io/npm/v/@n1ru4l/socket-io-graphql-server.svg)](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-server) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/socket-io-graphql-server.svg)](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-server) |
22-
| [`@n1ru4l/socket-io-graphql-client`](packages/socket-io-graphql-client) | GraphQL over Socket.io - Client | [![npm version](https://img.shields.io/npm/v/@n1ru4l/socket-io-graphql-client.svg)](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-client) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/socket-io-graphql-client.svg)](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-client) |
23-
| [`todo-example-app`](packages/todo-example) | Todo App with state sync across clients. | - |
17+
| Package | Description | Stats |
18+
| --------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
19+
| [`@n1ru4l/in-memory-live-query-store`](packages/in-memory-live-query-store) | Live query implementation. | [![npm version](https://img.shields.io/npm/v/@n1ru4l/in-memory-live-query-store.svg)](https://www.npmjs.com/package/@n1ru4l/in-memory-live-query-store) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/in-memory-live-query-store.svg)](https://www.npmjs.com/package/@n1ru4l/in-memory-live-query-store) |
20+
| [`@n1ru4l/graphql-live-query`](packages/graphql-live-query) | Utilities for live query implementations. | [![npm version](https://img.shields.io/npm/v/@n1ru4l/graphql-live-query.svg)](https://www.npmjs.com/package/@n1ru4l/graphql-live-query) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/graphql-live-query.svg)](https://www.npmjs.com/package/@n1ru4l/graphql-live-query) |
21+
| [`@n1ru4l/graphql-live-query-patch`](packages/graphql-live-query-patch) | Reduce live query payload size with JSON patches | [![npm version](https://img.shields.io/npm/v/@n1ru4l/graphql-live-query-patch.svg)](https://www.npmjs.com/package/@n1ru4l/graphql-live-query-patch) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/graphql-live-query-patch.svg)](https://www.npmjs.com/package/@n1ru4l/graphql-live-query-patch) |
22+
| [`todo-example-app`](packages/todo-example) | Todo App with state sync across clients. |
23+
| [`@n1ru4l/socket-io-graphql-server`](packages/socket-io-graphql-server) | GraphQL over Socket.io - Server Middleware | [![npm version](https://img.shields.io/npm/v/@n1ru4l/socket-io-graphql-server.svg)](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-server) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/socket-io-graphql-server.svg)](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-server) |
24+
| [`@n1ru4l/socket-io-graphql-client`](packages/socket-io-graphql-client) | GraphQL over Socket.io - Client | [![npm version](https://img.shields.io/npm/v/@n1ru4l/socket-io-graphql-client.svg)](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-client) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/socket-io-graphql-client.svg)](https://www.npmjs.com/package/@n1ru4l/socket-io-graphql-client) |
25+
| [`todo-example-app`](packages/todo-example) | Todo App with state sync across clients. | - |
2426

2527
## Motivation
2628

packages/example/client/src/createRelayEnvironment.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SocketIOGraphQLClient } from "@n1ru4l/socket-io-graphql-client";
2+
import { applyAsyncIterableIteratorToSink } from "@n1ru4l/push-pull-async-iterable-iterator";
23
import {
34
Environment,
45
Network,
@@ -51,19 +52,19 @@ const attachNotifyGarbageCollectionBehaviourToStore = (store: Store): Store => {
5152
};
5253

5354
export const createRelayEnvironment = (
54-
networkInterface: SocketIOGraphQLClient<GraphQLResponse, Error>
55+
networkInterface: SocketIOGraphQLClient<GraphQLResponse>
5556
) => {
5657
const execute = (request: RequestParameters, variables: Variables) => {
5758
if (!request.text) throw new Error("Missing document.");
5859
const { text: operation, name } = request;
5960

6061
return Observable.create<GraphQLResponse>((sink) =>
61-
networkInterface.execute(
62-
{
62+
applyAsyncIterableIteratorToSink(
63+
networkInterface.execute({
6364
operation,
6465
variables,
6566
operationName: name,
66-
},
67+
}),
6768
sink
6869
)
6970
);

packages/example/client/src/index.tsx

+15-9
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { Global, css } from "@emotion/core";
99
import { io } from "socket.io-client";
1010
import {
1111
createSocketIOGraphQLClient,
12-
Sink,
1312
SocketIOGraphQLClient,
1413
} from "@n1ru4l/socket-io-graphql-client";
14+
import { applyAsyncIterableIteratorToSink } from "@n1ru4l/push-pull-async-iterable-iterator";
1515
import { ChatApplication } from "./ChatApplication";
1616
import { createRelayEnvironment } from "./createRelayEnvironment";
1717
import { Fetcher, FetcherResult } from "graphiql/dist/components/GraphiQL";
@@ -22,9 +22,15 @@ const socket = io();
2222

2323
const socketIOGraphQLClient = createSocketIOGraphQLClient(socket);
2424
const relayEnvironment = createRelayEnvironment(
25-
socketIOGraphQLClient as SocketIOGraphQLClient<GraphQLResponse, Error>
25+
socketIOGraphQLClient as SocketIOGraphQLClient<GraphQLResponse>
2626
);
2727

28+
export type Sink<TValue = unknown, TError = unknown> = {
29+
next: (value: TValue) => void;
30+
error: (error: TError) => void;
31+
complete: () => void;
32+
};
33+
2834
const fetcher: Fetcher = ({ query: operation, ...restGraphQLParams }) =>
2935
({
3036
subscribe: (
@@ -36,13 +42,13 @@ const fetcher: Fetcher = ({ query: operation, ...restGraphQLParams }) =>
3642
? { next: sinkOrNext, error: args[0], complete: args[1] }
3743
: sinkOrNext;
3844

39-
const unsubscribe = (socketIOGraphQLClient as SocketIOGraphQLClient<
40-
FetcherResult
41-
>).execute(
42-
{
43-
operation,
44-
...restGraphQLParams,
45-
},
45+
const unsubscribe = applyAsyncIterableIteratorToSink(
46+
(socketIOGraphQLClient as SocketIOGraphQLClient<FetcherResult>).execute(
47+
{
48+
operation,
49+
...restGraphQLParams,
50+
}
51+
),
4652
sink
4753
);
4854

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# @n1ru4l/graphql-live-query-patch
2+
3+
[![npm version](https://img.shields.io/npm/v/@n1ru4l/graphql-live-query-patch.svg)](https://www.npmjs.com/package/@n1ru4l/graphql-live-query-patch) [![npm downloads](https://img.shields.io/npm/dm/@n1ru4l/graphql-live-query-patch.svg)](https://www.npmjs.com/package/@n1ru4l/graphql-live-query-patch)
4+
5+
Make your live query payload smaller with json patches.
6+
7+
If you have big live query operations you might opt into only sending JSON patches to your clients. This requires the server to always store the latest execution result. When a new execution result is published a JSON patch is generated by diffing those execution results.
8+
The resulting patch operations are then sent to the client where they are applied to the initial execution result.
9+
10+
For example usage check out the todo-example client & server code.
11+
12+
## Install Instructions
13+
14+
```bash
15+
yarn add -E @n1ru4l/graphql-live-query-patch
16+
```
17+
18+
## API
19+
20+
### `createLiveQueryPatchDeflator`
21+
22+
```ts
23+
import { execute } from "graphql";
24+
import { createLiveQueryPatchDeflator } from "@n1ru4l/graphql-live-query-patch";
25+
import { schema } from "./schema";
26+
27+
execute({
28+
schema,
29+
operationDocument: parse(/* GraphQL */ `
30+
query todosQuery @live {
31+
todos {
32+
id
33+
content
34+
isComplete
35+
}
36+
}
37+
`),
38+
rootValue: rootValue,
39+
contextValue: {},
40+
variableValues: null,
41+
operationName: "todosQuery",
42+
}).then(async (result) => {
43+
if (isAsyncIterable(result)) {
44+
for (const value of createLiveQueryPatchDeflator(result)) {
45+
console.log(value);
46+
}
47+
}
48+
});
49+
```
50+
51+
### `applyLiveQueryPatchDeflator`
52+
53+
Convenience wrapper for applying `createLiveQueryPatchDeflator` on the `execute` return value.
54+
55+
```ts
56+
import { execute } from "graphql";
57+
import { applyLiveQueryPatchDeflator } from "@n1ru4l/graphql-live-query-patch";
58+
import { schema } from "./schema";
59+
60+
const result = applyLiveQueryPatchDeflator(
61+
execute({
62+
schema,
63+
operationDocument: parse(/* GraphQL */ `
64+
query todosQuery @live {
65+
todos {
66+
id
67+
content
68+
isComplete
69+
}
70+
}
71+
`),
72+
rootValue: rootValue,
73+
contextValue: {},
74+
variableValues: null,
75+
operationName: "todosQuery",
76+
})
77+
);
78+
```
79+
80+
### `createLiveQueryPatchInflator`
81+
82+
Inflate the execution result on the client side.
83+
84+
```ts
85+
import { createLiveQueryPatchInflator } from "@n1ru4l/graphql-live-query-patch";
86+
87+
const asyncIterable = createLiveQueryPatchInflator(
88+
networkLayer.execute({
89+
operation: /* GraphQL */ `
90+
query todosQuery @live {
91+
todos {
92+
id
93+
content
94+
isComplete
95+
}
96+
}
97+
`,
98+
})
99+
);
100+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "@n1ru4l/graphql-live-query-patch",
3+
"version": "0.0.1",
4+
"author": "n1ru4l <[email protected]>",
5+
"license": "MIT",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/n1ru4l/graphql-live-queries.git",
9+
"directory": "packages/graphql-live-query-patch"
10+
},
11+
"bugs": {
12+
"url": "https://github.com/n1ru4l/graphql-live-queries/issues"
13+
},
14+
"homepage": "https://github.com/n1ru4l/graphql-live-queries#readme",
15+
"keywords": [
16+
"graphql",
17+
"query",
18+
"live",
19+
"real-time"
20+
],
21+
"scripts": {
22+
"prepack": "bob prepack"
23+
},
24+
"dependencies": {
25+
"fast-json-patch": "3.0.0-1"
26+
},
27+
"devDependencies": {
28+
"graphql": "15.4.0-experimental-stream-defer.1",
29+
"@n1ru4l/graphql-live-query": "*",
30+
"@n1ru4l/in-memory-live-query-store": "*"
31+
},
32+
"peerDependencies": {
33+
"graphql": "^15.4.0"
34+
},
35+
"main": "dist/index.cjs.js",
36+
"module": "dist/index.esm.js",
37+
"exports": {
38+
"require": "./dist/index.cjs.js",
39+
"default": "./dist/index.mjs"
40+
},
41+
"typings": "dist/index.d.ts",
42+
"typescript": {
43+
"definition": "dist/index.d.ts"
44+
},
45+
"buildOptions": {
46+
"input": "./src/index.ts"
47+
},
48+
"publishConfig": {
49+
"directory": "dist",
50+
"access": "public"
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Operation } from "fast-json-patch";
2+
import { ExecutionResult } from "graphql";
3+
4+
export type ExecutionLivePatchResult = {
5+
errors?: ExecutionResult["errors"];
6+
/* data must be included in the first result */
7+
data?: ExecutionResult["data"];
8+
/* patch must be present in the next results */
9+
patch?: Operation[];
10+
revision?: number;
11+
extensions?: ExecutionResult["extensions"];
12+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { ExecutionResult } from "graphql";
2+
import type { LiveExecutionResult } from "@n1ru4l/graphql-live-query";
3+
import { createLiveQueryPatchDeflator } from "./createLiveQueryPatchDeflator";
4+
type MaybePromise<T> = T | Promise<T>;
5+
6+
const isAsyncIterable = (value: unknown): value is AsyncIterable<unknown> => {
7+
return (
8+
typeof value === "object" && value !== null && Symbol.asyncIterator in value
9+
);
10+
};
11+
12+
type LiveQueryDeflatorExecutionResult = MaybePromise<
13+
AsyncIterableIterator<ExecutionResult | LiveExecutionResult> | ExecutionResult
14+
>;
15+
16+
/**
17+
* "afterware" for "execute" for applying deflation.
18+
*/
19+
export const applyLiveQueryPatchDeflator = (
20+
executionResult: LiveQueryDeflatorExecutionResult
21+
): LiveQueryDeflatorExecutionResult => {
22+
const handler = (
23+
result:
24+
| AsyncIterableIterator<ExecutionResult | LiveExecutionResult>
25+
| ExecutionResult
26+
) => {
27+
if (isAsyncIterable(result)) {
28+
return createLiveQueryPatchDeflator(result);
29+
}
30+
return result;
31+
};
32+
33+
if (executionResult instanceof Promise) {
34+
return executionResult.then(handler);
35+
}
36+
37+
return handler(executionResult);
38+
};

0 commit comments

Comments
 (0)