/
index.ts
173 lines (149 loc) Β· 4.98 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import { GraphQLError } from 'graphql';
import { ClientPluginContext, CombinedError, ClientPluginOperation, definePlugin, fetch as fetchPlugin } from 'villus';
import {
GraphQLResponse,
makeFetchOptions,
mergeFetchOpts,
ParsedResponse,
parseResponse,
resolveGlobalFetch,
} from '../../shared/src';
interface BatchOptions {
fetch?: typeof fetch;
timeout: number;
maxOperationCount: number;
exclude?: (op: ClientPluginOperation, ctx: ClientPluginContext) => boolean;
}
type BatchedGraphQLResponse = GraphQLResponse<unknown>[];
const defaultOpts = (): BatchOptions => ({
fetch: resolveGlobalFetch(),
timeout: 10,
maxOperationCount: 10,
});
export function batch(opts?: Partial<BatchOptions>) {
const { fetch, timeout, maxOperationCount } = { ...defaultOpts(), ...(opts || {}) };
const fetchPluginInstance = fetchPlugin({ fetch });
let operations: { resolveOp: (r: any, opIdx: number, err?: Error) => void; body: string }[] = [];
let scheduledConsume: any;
return definePlugin(function batchPlugin(ctx) {
const { useResult, opContext, operation } = ctx;
if (opts?.exclude?.(ctx.operation, ctx)) {
return fetchPluginInstance(ctx);
}
async function consume() {
const pending = operations;
const body = `[${operations.map(o => o.body).join(',')}]`;
const fetchOpts = mergeFetchOpts(opContext, { headers: {}, body });
operations = [];
if (!fetch) {
throw new Error('Could not resolve fetch, please provide a fetch function');
}
let response: ParsedResponse<unknown>;
try {
response = await fetch(opContext.url as string, fetchOpts).then(parseResponse);
ctx.response = response;
const resInit: Partial<Response> = {
ok: response.ok,
status: response.status,
statusText: response.statusText,
headers: response.headers,
};
pending.forEach(function unBatchResult(o, oIdx) {
const opResult = (response.body as unknown as BatchedGraphQLResponse | null)?.[oIdx];
// the server returned a non-json response or an empty one
if (!opResult) {
o.resolveOp(
{
...resInit,
body: response.body,
},
oIdx,
new Error('Received empty response for this operation from server'),
);
return;
}
o.resolveOp(
{
body: opResult,
...resInit,
},
oIdx,
);
});
} catch (err) {
// This usually mean a network fetch error which is limited to DNS lookup errors
// or the user may not be connected to the internet, so it's safe to assume no data is in the response
pending.forEach(function unBatchErrorResult(o, oIdx) {
o.resolveOp(undefined, oIdx, err as Error);
});
}
}
return new Promise(resolve => {
if (scheduledConsume) {
clearTimeout(scheduledConsume);
}
if (operations.length >= maxOperationCount) {
// consume the old array
consume();
}
if (!opContext.body) {
opContext.body = makeFetchOptions(operation, opContext).body;
}
operations.push({
resolveOp: (response: ParsedResponse<unknown>, opIdx, err) => {
resolve(undefined);
// Handle DNS errors
if (err) {
useResult(
{
data: null,
error: new CombinedError({
response,
networkError: err,
}),
},
true,
);
return;
}
const data = response.body?.data || null;
if (!response.ok || !response.body) {
const error = buildErrorObject(response, opIdx);
useResult(
{
data,
error,
},
true,
);
return;
}
useResult(
{
data,
error: response.body.errors ? new CombinedError({ response, graphqlErrors: response.body.errors }) : null,
},
true,
);
},
body: opContext.body as string,
});
scheduledConsume = setTimeout(consume, timeout);
});
});
}
function buildErrorObject(response: ParsedResponse<unknown>, opIdx: number) {
// It is possible than a non-200 response is returned with errors, it should be treated as GraphQL error
const ctorOptions: { response: typeof response; graphqlErrors?: GraphQLError[]; networkError?: Error } = {
response,
};
if (Array.isArray(response.body)) {
const opResponse = response.body[opIdx];
ctorOptions.graphqlErrors = opResponse?.errors;
} else if (response.body?.errors) {
ctorOptions.graphqlErrors = response.body.errors;
} else {
ctorOptions.networkError = new Error(response.statusText);
}
return new CombinedError(ctorOptions);
}