graphql-http 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{LICENSE → LICENSE.md} +2 -2
- package/README.md +643 -30
- package/lib/client.d.ts +147 -0
- package/lib/client.js +189 -0
- package/lib/client.mjs +170 -0
- package/lib/common.d.ts +98 -0
- package/lib/common.js +19 -0
- package/lib/common.mjs +15 -0
- package/lib/handler.d.ts +155 -0
- package/lib/handler.js +314 -0
- package/lib/handler.mjs +310 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +18 -184
- package/lib/index.mjs +3 -0
- package/lib/utils.d.ts +7 -0
- package/lib/utils.js +13 -0
- package/lib/utils.mjs +9 -0
- package/package.json +87 -25
- package/umd/graphql-http.js +206 -0
- package/umd/graphql-http.min.js +1 -0
- package/umd/graphql-http.min.js.gz +0 -0
- package/.eslintrc.json +0 -8
- package/.npmignore +0 -33
- package/index.js +0 -1
- package/lib/__tests__/index.js +0 -66
package/lib/client.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* client
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import { ExecutionResult } from 'graphql';
|
|
7
|
+
import { RequestParams, Sink } from './common';
|
|
8
|
+
/** This file is the entry point for browsers, re-export common elements. */
|
|
9
|
+
export * from './common';
|
|
10
|
+
/** @category Client */
|
|
11
|
+
export interface ClientOptions {
|
|
12
|
+
/**
|
|
13
|
+
* URL of the GraphQL over HTTP server to connect.
|
|
14
|
+
*
|
|
15
|
+
* If the option is a function, it will be called on each request.
|
|
16
|
+
* Returning a Promise is supported too and the request will stall until it
|
|
17
|
+
* resolves.
|
|
18
|
+
*
|
|
19
|
+
* A good use-case for having a function is when using the URL for authentication,
|
|
20
|
+
* where subsequent requests (due to auth) may have a refreshed identity token.
|
|
21
|
+
*/
|
|
22
|
+
url: string | (() => Promise<string> | string);
|
|
23
|
+
/**
|
|
24
|
+
* Indicates whether the user agent should send cookies from the other domain in the case
|
|
25
|
+
* of cross-origin requests.
|
|
26
|
+
*
|
|
27
|
+
* Possible options are:
|
|
28
|
+
* - `omit`: Never send or receive cookies.
|
|
29
|
+
* - `same-origin`: Send user credentials (cookies, basic http auth, etc..) if the URL is on the same origin as the calling script.
|
|
30
|
+
* - `include`: Always send user credentials (cookies, basic http auth, etc..), even for cross-origin calls.
|
|
31
|
+
*
|
|
32
|
+
* @default same-origin
|
|
33
|
+
*/
|
|
34
|
+
credentials?: 'omit' | 'same-origin' | 'include';
|
|
35
|
+
/**
|
|
36
|
+
* A string specifying the referrer of the request. This can be a same-origin URL, about:client, or an empty string.
|
|
37
|
+
*
|
|
38
|
+
* @default undefined
|
|
39
|
+
*/
|
|
40
|
+
referrer?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Specifies the referrer policy to use for the request.
|
|
43
|
+
*
|
|
44
|
+
* Possible options are:
|
|
45
|
+
* - `no-referrer`: Does not send referrer information along with requests to any origin.
|
|
46
|
+
* - `no-referrer-when-downgrade`: Sends full referrerURL for requests: whose referrerURL and current URL are both potentially trustworthy URLs, or whose referrerURL is a non-potentially trustworthy URL.
|
|
47
|
+
* - `same-origin`: Sends full referrerURL as referrer information when making same-origin-referrer requests.
|
|
48
|
+
* - `origin`: Sends only the ASCII serialization of the request’s referrerURL when making both same-origin-referrer requests and cross-origin-referrer requests.
|
|
49
|
+
* - `strict-origin`: Sends the ASCII serialization of the origin of the referrerURL for requests: whose referrerURL and current URL are both potentially trustworthy URLs, or whose referrerURL is a non-potentially trustworthy URL
|
|
50
|
+
* - `origin-when-cross-origin`: Sends full referrerURL when making same-origin-referrer requests, and only the ASCII serialization of the origin of the request’s referrerURL is sent when making cross-origin-referrer requests
|
|
51
|
+
* - `strict-origin-when-cross-origin`: Sends full referrerURL when making same-origin-referrer requests, and only the ASCII serialization of the origin of the request’s referrerURL when making cross-origin-referrer requests: whose referrerURL and current URL are both potentially trustworthy URLs, or whose referrerURL is a non-potentially trustworthy URL.
|
|
52
|
+
* - `unsafe-url`: Sends full referrerURL along for both same-origin-referrer requests and cross-origin-referrer requests.
|
|
53
|
+
*
|
|
54
|
+
* @default undefined
|
|
55
|
+
*/
|
|
56
|
+
referrerPolicy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'same-origin' | 'origin' | 'strict-origin' | 'origin-when-cross-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url';
|
|
57
|
+
/**
|
|
58
|
+
* HTTP headers to pass along the request.
|
|
59
|
+
*
|
|
60
|
+
* If the option is a function, it will be called on each request.
|
|
61
|
+
* Returning a Promise is supported too and the request will stall until it
|
|
62
|
+
* resolves.
|
|
63
|
+
*
|
|
64
|
+
* A good use-case for having a function is when using the URL for authentication,
|
|
65
|
+
* where subsequent requests (due to auth) may have a refreshed identity token.
|
|
66
|
+
*/
|
|
67
|
+
headers?: Record<string, string> | (() => Promise<Record<string, string> | null | void> | Record<string, string> | null | void);
|
|
68
|
+
/**
|
|
69
|
+
* Control whether the network request error should be retried.
|
|
70
|
+
*
|
|
71
|
+
* Please note that you can **only** control network errors, all other
|
|
72
|
+
* errors are considered fatal and will be reported immediately.
|
|
73
|
+
*
|
|
74
|
+
* You may implement your own waiting strategy by timing the resolution of the returned promise.
|
|
75
|
+
*
|
|
76
|
+
* Useful for retrying requests that failed because the service is temporarely unavailable.
|
|
77
|
+
*
|
|
78
|
+
* `retries` argument counts actual retries, so it will begin with
|
|
79
|
+
* 0 after the first failed request.
|
|
80
|
+
*
|
|
81
|
+
* Returning `false` will report the `err` argument; however, throwing a different error from
|
|
82
|
+
* the `err` argument, will report it instead.
|
|
83
|
+
*
|
|
84
|
+
* @default '() => false'
|
|
85
|
+
*/
|
|
86
|
+
shouldRetry?: (err: NetworkError, retries: number) => Promise<boolean>;
|
|
87
|
+
/**
|
|
88
|
+
* The Fetch function to use.
|
|
89
|
+
*
|
|
90
|
+
* For NodeJS environments consider using [`node-fetch`](https://github.com/node-fetch/node-fetch).
|
|
91
|
+
*
|
|
92
|
+
* @default global.fetch
|
|
93
|
+
*/
|
|
94
|
+
fetchFn?: unknown;
|
|
95
|
+
/**
|
|
96
|
+
* The AbortController implementation to use.
|
|
97
|
+
*
|
|
98
|
+
* For NodeJS environments before v15 consider using [`node-abort-controller`](https://github.com/southpolesteve/node-abort-controller).
|
|
99
|
+
*
|
|
100
|
+
* @default global.AbortController
|
|
101
|
+
*/
|
|
102
|
+
abortControllerImpl?: unknown;
|
|
103
|
+
}
|
|
104
|
+
/** @category Client */
|
|
105
|
+
export interface Client {
|
|
106
|
+
/**
|
|
107
|
+
* Subscribes to receive a response by making an HTTP request.
|
|
108
|
+
*
|
|
109
|
+
* It uses the `sink` to emit the received data or errors. Returns a _dispose_
|
|
110
|
+
* function used for canceling active requests and cleaning up.
|
|
111
|
+
*/
|
|
112
|
+
subscribe<Data = Record<string, unknown>, Extensions = unknown>(request: RequestParams, sink: Sink<ExecutionResult<Data, Extensions>>): () => void;
|
|
113
|
+
/**
|
|
114
|
+
* Dispose of the client, cancel all active requests and clean up resources.
|
|
115
|
+
*/
|
|
116
|
+
dispose: () => void;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Creates a disposable GraphQL over HTTP client to transmit
|
|
120
|
+
* GraphQL operation results.
|
|
121
|
+
*
|
|
122
|
+
* @category Client
|
|
123
|
+
*/
|
|
124
|
+
export declare function createClient(options: ClientOptions): Client;
|
|
125
|
+
/**
|
|
126
|
+
* A network error caused by the client or an unexpected response from the server.
|
|
127
|
+
*
|
|
128
|
+
* To avoid bundling DOM typings (because the client can run in Node env too),
|
|
129
|
+
* you should supply the `Response` generic depending on your Fetch implementation.
|
|
130
|
+
*
|
|
131
|
+
* @category Client
|
|
132
|
+
*/
|
|
133
|
+
export declare class NetworkError<Response extends ResponseLike = ResponseLike> extends Error {
|
|
134
|
+
/**
|
|
135
|
+
* The underlyig response thats considered an error.
|
|
136
|
+
*
|
|
137
|
+
* Will be undefined when no response is received,
|
|
138
|
+
* instead an unexpected network error.
|
|
139
|
+
*/
|
|
140
|
+
response: Response | undefined;
|
|
141
|
+
constructor(msgOrErrOrResponse: string | Error | Response);
|
|
142
|
+
}
|
|
143
|
+
interface ResponseLike {
|
|
144
|
+
readonly ok: boolean;
|
|
145
|
+
readonly status: number;
|
|
146
|
+
readonly statusText: string;
|
|
147
|
+
}
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* client
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
19
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.NetworkError = exports.createClient = void 0;
|
|
23
|
+
const utils_1 = require("./utils");
|
|
24
|
+
/** This file is the entry point for browsers, re-export common elements. */
|
|
25
|
+
__exportStar(require("./common"), exports);
|
|
26
|
+
/**
|
|
27
|
+
* Creates a disposable GraphQL over HTTP client to transmit
|
|
28
|
+
* GraphQL operation results.
|
|
29
|
+
*
|
|
30
|
+
* @category Client
|
|
31
|
+
*/
|
|
32
|
+
function createClient(options) {
|
|
33
|
+
const { credentials = 'same-origin', referrer, referrerPolicy, shouldRetry = () => false, } = options;
|
|
34
|
+
const fetchFn = (options.fetchFn || fetch);
|
|
35
|
+
const AbortControllerImpl = (options.abortControllerImpl ||
|
|
36
|
+
AbortController);
|
|
37
|
+
// we dont use yet another AbortController here because of
|
|
38
|
+
// node's max EventEmitters listeners being only 10
|
|
39
|
+
const client = (() => {
|
|
40
|
+
let disposed = false;
|
|
41
|
+
const listeners = [];
|
|
42
|
+
return {
|
|
43
|
+
get disposed() {
|
|
44
|
+
return disposed;
|
|
45
|
+
},
|
|
46
|
+
onDispose(cb) {
|
|
47
|
+
if (disposed) {
|
|
48
|
+
// empty the call stack and then call the cb
|
|
49
|
+
setTimeout(() => cb(), 0);
|
|
50
|
+
return () => {
|
|
51
|
+
// noop
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
listeners.push(cb);
|
|
55
|
+
return () => {
|
|
56
|
+
listeners.splice(listeners.indexOf(cb), 1);
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
dispose() {
|
|
60
|
+
if (disposed)
|
|
61
|
+
return;
|
|
62
|
+
disposed = true;
|
|
63
|
+
// we copy the listeners so that onDispose unlistens dont "pull the rug under our feet"
|
|
64
|
+
for (const listener of [...listeners]) {
|
|
65
|
+
listener();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
})();
|
|
70
|
+
return {
|
|
71
|
+
subscribe(request, sink) {
|
|
72
|
+
if (client.disposed)
|
|
73
|
+
throw new Error('Client has been disposed');
|
|
74
|
+
const control = new AbortControllerImpl();
|
|
75
|
+
const unlisten = client.onDispose(() => {
|
|
76
|
+
unlisten();
|
|
77
|
+
control.abort();
|
|
78
|
+
});
|
|
79
|
+
(async () => {
|
|
80
|
+
var _a;
|
|
81
|
+
let retryingErr = null, retries = 0;
|
|
82
|
+
for (;;) {
|
|
83
|
+
if (retryingErr) {
|
|
84
|
+
const should = await shouldRetry(retryingErr, retries);
|
|
85
|
+
// requst might've been canceled while waiting for retry
|
|
86
|
+
if (control.signal.aborted)
|
|
87
|
+
return;
|
|
88
|
+
if (!should)
|
|
89
|
+
throw retryingErr;
|
|
90
|
+
retries++;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const url = typeof options.url === 'function'
|
|
94
|
+
? await options.url()
|
|
95
|
+
: options.url;
|
|
96
|
+
if (control.signal.aborted)
|
|
97
|
+
return;
|
|
98
|
+
const headers = typeof options.headers === 'function'
|
|
99
|
+
? await options.headers()
|
|
100
|
+
: (_a = options.headers) !== null && _a !== void 0 ? _a : {};
|
|
101
|
+
if (control.signal.aborted)
|
|
102
|
+
return;
|
|
103
|
+
let res;
|
|
104
|
+
try {
|
|
105
|
+
res = await fetchFn(url, {
|
|
106
|
+
signal: control.signal,
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: Object.assign(Object.assign({}, headers), { 'content-type': 'application/json; charset=utf-8', accept: 'application/graphql+json, application/json' }),
|
|
109
|
+
credentials,
|
|
110
|
+
referrer,
|
|
111
|
+
referrerPolicy,
|
|
112
|
+
body: JSON.stringify(request),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new NetworkError(err);
|
|
117
|
+
}
|
|
118
|
+
if (!res.ok)
|
|
119
|
+
throw new NetworkError(res);
|
|
120
|
+
if (!res.body)
|
|
121
|
+
throw new Error('Missing response body');
|
|
122
|
+
const contentType = res.headers.get('content-type');
|
|
123
|
+
if (!contentType)
|
|
124
|
+
throw new Error('Missing response content-type');
|
|
125
|
+
if (!contentType.includes('application/graphql+json') &&
|
|
126
|
+
!contentType.includes('application/json')) {
|
|
127
|
+
throw new Error(`Unsupported response content-type ${contentType}`);
|
|
128
|
+
}
|
|
129
|
+
const result = await res.json();
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
|
+
sink.next(result);
|
|
132
|
+
return control.abort();
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (control.signal.aborted)
|
|
136
|
+
return;
|
|
137
|
+
// all non-network errors are worth reporting immediately
|
|
138
|
+
if (!(err instanceof NetworkError))
|
|
139
|
+
throw err;
|
|
140
|
+
// try again
|
|
141
|
+
retryingErr = err;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
})()
|
|
145
|
+
.then(() => sink.complete())
|
|
146
|
+
.catch((err) => sink.error(err));
|
|
147
|
+
return () => control.abort();
|
|
148
|
+
},
|
|
149
|
+
dispose() {
|
|
150
|
+
client.dispose();
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
exports.createClient = createClient;
|
|
155
|
+
/**
|
|
156
|
+
* A network error caused by the client or an unexpected response from the server.
|
|
157
|
+
*
|
|
158
|
+
* To avoid bundling DOM typings (because the client can run in Node env too),
|
|
159
|
+
* you should supply the `Response` generic depending on your Fetch implementation.
|
|
160
|
+
*
|
|
161
|
+
* @category Client
|
|
162
|
+
*/
|
|
163
|
+
class NetworkError extends Error {
|
|
164
|
+
constructor(msgOrErrOrResponse) {
|
|
165
|
+
let message, response;
|
|
166
|
+
if (isResponseLike(msgOrErrOrResponse)) {
|
|
167
|
+
response = msgOrErrOrResponse;
|
|
168
|
+
message =
|
|
169
|
+
'Server responded with ' +
|
|
170
|
+
msgOrErrOrResponse.status +
|
|
171
|
+
': ' +
|
|
172
|
+
msgOrErrOrResponse.statusText;
|
|
173
|
+
}
|
|
174
|
+
else if (msgOrErrOrResponse instanceof Error)
|
|
175
|
+
message = msgOrErrOrResponse.message;
|
|
176
|
+
else
|
|
177
|
+
message = String(msgOrErrOrResponse);
|
|
178
|
+
super(message);
|
|
179
|
+
this.name = this.constructor.name;
|
|
180
|
+
this.response = response;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.NetworkError = NetworkError;
|
|
184
|
+
function isResponseLike(val) {
|
|
185
|
+
return ((0, utils_1.isObject)(val) &&
|
|
186
|
+
typeof val['ok'] === 'boolean' &&
|
|
187
|
+
typeof val['status'] === 'number' &&
|
|
188
|
+
typeof val['statusText'] === 'string');
|
|
189
|
+
}
|
package/lib/client.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* client
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import { isObject } from './utils.mjs';
|
|
7
|
+
/** This file is the entry point for browsers, re-export common elements. */
|
|
8
|
+
export * from './common.mjs';
|
|
9
|
+
/**
|
|
10
|
+
* Creates a disposable GraphQL over HTTP client to transmit
|
|
11
|
+
* GraphQL operation results.
|
|
12
|
+
*
|
|
13
|
+
* @category Client
|
|
14
|
+
*/
|
|
15
|
+
export function createClient(options) {
|
|
16
|
+
const { credentials = 'same-origin', referrer, referrerPolicy, shouldRetry = () => false, } = options;
|
|
17
|
+
const fetchFn = (options.fetchFn || fetch);
|
|
18
|
+
const AbortControllerImpl = (options.abortControllerImpl ||
|
|
19
|
+
AbortController);
|
|
20
|
+
// we dont use yet another AbortController here because of
|
|
21
|
+
// node's max EventEmitters listeners being only 10
|
|
22
|
+
const client = (() => {
|
|
23
|
+
let disposed = false;
|
|
24
|
+
const listeners = [];
|
|
25
|
+
return {
|
|
26
|
+
get disposed() {
|
|
27
|
+
return disposed;
|
|
28
|
+
},
|
|
29
|
+
onDispose(cb) {
|
|
30
|
+
if (disposed) {
|
|
31
|
+
// empty the call stack and then call the cb
|
|
32
|
+
setTimeout(() => cb(), 0);
|
|
33
|
+
return () => {
|
|
34
|
+
// noop
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
listeners.push(cb);
|
|
38
|
+
return () => {
|
|
39
|
+
listeners.splice(listeners.indexOf(cb), 1);
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
dispose() {
|
|
43
|
+
if (disposed)
|
|
44
|
+
return;
|
|
45
|
+
disposed = true;
|
|
46
|
+
// we copy the listeners so that onDispose unlistens dont "pull the rug under our feet"
|
|
47
|
+
for (const listener of [...listeners]) {
|
|
48
|
+
listener();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
return {
|
|
54
|
+
subscribe(request, sink) {
|
|
55
|
+
if (client.disposed)
|
|
56
|
+
throw new Error('Client has been disposed');
|
|
57
|
+
const control = new AbortControllerImpl();
|
|
58
|
+
const unlisten = client.onDispose(() => {
|
|
59
|
+
unlisten();
|
|
60
|
+
control.abort();
|
|
61
|
+
});
|
|
62
|
+
(async () => {
|
|
63
|
+
var _a;
|
|
64
|
+
let retryingErr = null, retries = 0;
|
|
65
|
+
for (;;) {
|
|
66
|
+
if (retryingErr) {
|
|
67
|
+
const should = await shouldRetry(retryingErr, retries);
|
|
68
|
+
// requst might've been canceled while waiting for retry
|
|
69
|
+
if (control.signal.aborted)
|
|
70
|
+
return;
|
|
71
|
+
if (!should)
|
|
72
|
+
throw retryingErr;
|
|
73
|
+
retries++;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const url = typeof options.url === 'function'
|
|
77
|
+
? await options.url()
|
|
78
|
+
: options.url;
|
|
79
|
+
if (control.signal.aborted)
|
|
80
|
+
return;
|
|
81
|
+
const headers = typeof options.headers === 'function'
|
|
82
|
+
? await options.headers()
|
|
83
|
+
: (_a = options.headers) !== null && _a !== void 0 ? _a : {};
|
|
84
|
+
if (control.signal.aborted)
|
|
85
|
+
return;
|
|
86
|
+
let res;
|
|
87
|
+
try {
|
|
88
|
+
res = await fetchFn(url, {
|
|
89
|
+
signal: control.signal,
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: Object.assign(Object.assign({}, headers), { 'content-type': 'application/json; charset=utf-8', accept: 'application/graphql+json, application/json' }),
|
|
92
|
+
credentials,
|
|
93
|
+
referrer,
|
|
94
|
+
referrerPolicy,
|
|
95
|
+
body: JSON.stringify(request),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
throw new NetworkError(err);
|
|
100
|
+
}
|
|
101
|
+
if (!res.ok)
|
|
102
|
+
throw new NetworkError(res);
|
|
103
|
+
if (!res.body)
|
|
104
|
+
throw new Error('Missing response body');
|
|
105
|
+
const contentType = res.headers.get('content-type');
|
|
106
|
+
if (!contentType)
|
|
107
|
+
throw new Error('Missing response content-type');
|
|
108
|
+
if (!contentType.includes('application/graphql+json') &&
|
|
109
|
+
!contentType.includes('application/json')) {
|
|
110
|
+
throw new Error(`Unsupported response content-type ${contentType}`);
|
|
111
|
+
}
|
|
112
|
+
const result = await res.json();
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
sink.next(result);
|
|
115
|
+
return control.abort();
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (control.signal.aborted)
|
|
119
|
+
return;
|
|
120
|
+
// all non-network errors are worth reporting immediately
|
|
121
|
+
if (!(err instanceof NetworkError))
|
|
122
|
+
throw err;
|
|
123
|
+
// try again
|
|
124
|
+
retryingErr = err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})()
|
|
128
|
+
.then(() => sink.complete())
|
|
129
|
+
.catch((err) => sink.error(err));
|
|
130
|
+
return () => control.abort();
|
|
131
|
+
},
|
|
132
|
+
dispose() {
|
|
133
|
+
client.dispose();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A network error caused by the client or an unexpected response from the server.
|
|
139
|
+
*
|
|
140
|
+
* To avoid bundling DOM typings (because the client can run in Node env too),
|
|
141
|
+
* you should supply the `Response` generic depending on your Fetch implementation.
|
|
142
|
+
*
|
|
143
|
+
* @category Client
|
|
144
|
+
*/
|
|
145
|
+
export class NetworkError extends Error {
|
|
146
|
+
constructor(msgOrErrOrResponse) {
|
|
147
|
+
let message, response;
|
|
148
|
+
if (isResponseLike(msgOrErrOrResponse)) {
|
|
149
|
+
response = msgOrErrOrResponse;
|
|
150
|
+
message =
|
|
151
|
+
'Server responded with ' +
|
|
152
|
+
msgOrErrOrResponse.status +
|
|
153
|
+
': ' +
|
|
154
|
+
msgOrErrOrResponse.statusText;
|
|
155
|
+
}
|
|
156
|
+
else if (msgOrErrOrResponse instanceof Error)
|
|
157
|
+
message = msgOrErrOrResponse.message;
|
|
158
|
+
else
|
|
159
|
+
message = String(msgOrErrOrResponse);
|
|
160
|
+
super(message);
|
|
161
|
+
this.name = this.constructor.name;
|
|
162
|
+
this.response = response;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isResponseLike(val) {
|
|
166
|
+
return (isObject(val) &&
|
|
167
|
+
typeof val['ok'] === 'boolean' &&
|
|
168
|
+
typeof val['status'] === 'number' &&
|
|
169
|
+
typeof val['statusText'] === 'string');
|
|
170
|
+
}
|
package/lib/common.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* common
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Concrete interface that the headers map should implement.
|
|
8
|
+
*
|
|
9
|
+
* @category Common
|
|
10
|
+
*/
|
|
11
|
+
export interface Headers {
|
|
12
|
+
accept?: string | undefined;
|
|
13
|
+
allow?: string | undefined;
|
|
14
|
+
'content-type'?: string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Always an array in Node. Duplicates are added to it.
|
|
17
|
+
* Not necessarily true for other environments, make sure
|
|
18
|
+
* to check the type during runtime.
|
|
19
|
+
*/
|
|
20
|
+
'set-cookie'?: string | string[] | undefined;
|
|
21
|
+
[key: string]: string | string[] | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Server agnostic request interface containing the raw request
|
|
25
|
+
* which is server dependant.
|
|
26
|
+
*
|
|
27
|
+
* @category Common
|
|
28
|
+
*/
|
|
29
|
+
export interface Request<RawRequest> {
|
|
30
|
+
readonly method: string;
|
|
31
|
+
readonly url: string;
|
|
32
|
+
readonly headers: Headers;
|
|
33
|
+
readonly body: string | Record<string, unknown> | null;
|
|
34
|
+
/**
|
|
35
|
+
* The raw request itself from the implementing server.
|
|
36
|
+
*
|
|
37
|
+
* For example: `express.Request` when using Express, or maybe
|
|
38
|
+
* `http.IncomingMessage` when just using Node with `http.createServer`.
|
|
39
|
+
*/
|
|
40
|
+
readonly raw: RawRequest;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parameters for GraphQL's request for execution.
|
|
44
|
+
*
|
|
45
|
+
* Reference: https://graphql.github.io/graphql-over-http/draft/#sec-Request-Parameters
|
|
46
|
+
*
|
|
47
|
+
* @category Common
|
|
48
|
+
*/
|
|
49
|
+
export interface RequestParams {
|
|
50
|
+
operationName?: string | undefined;
|
|
51
|
+
query: string;
|
|
52
|
+
variables?: Record<string, unknown> | undefined;
|
|
53
|
+
extensions?: Record<string, unknown> | undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Server agnostic response body returned from `graphql-http` needing
|
|
57
|
+
* to be coerced to the server implementation in use.
|
|
58
|
+
*
|
|
59
|
+
* @category Common
|
|
60
|
+
*/
|
|
61
|
+
export declare type ResponseBody = string;
|
|
62
|
+
/**
|
|
63
|
+
* Server agnostic response options (ex. status and headers) returned from
|
|
64
|
+
* `graphql-http` needing to be coerced to the server implementation in use.
|
|
65
|
+
*
|
|
66
|
+
* @category Common
|
|
67
|
+
*/
|
|
68
|
+
export interface ResponseInit {
|
|
69
|
+
readonly status: number;
|
|
70
|
+
readonly statusText?: string;
|
|
71
|
+
readonly headers?: Headers;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Server agnostic response returned from `graphql-http` containing the
|
|
75
|
+
* body and init options needing to be coerced to the server implementation in use.
|
|
76
|
+
*
|
|
77
|
+
* @category Common
|
|
78
|
+
*/
|
|
79
|
+
export declare type Response = readonly [body: ResponseBody | null, init: ResponseInit];
|
|
80
|
+
/**
|
|
81
|
+
* Checks whether the passed value is the `graphql-http` server agnostic response.
|
|
82
|
+
*
|
|
83
|
+
* @category Common
|
|
84
|
+
*/
|
|
85
|
+
export declare function isResponse(val: unknown): val is Response;
|
|
86
|
+
/**
|
|
87
|
+
* A representation of any set of values over any amount of time.
|
|
88
|
+
*
|
|
89
|
+
* @category Common
|
|
90
|
+
*/
|
|
91
|
+
export interface Sink<T = unknown> {
|
|
92
|
+
/** Next value arriving. */
|
|
93
|
+
next(value: T): void;
|
|
94
|
+
/** An error that has occured. This function "closes" the sink. */
|
|
95
|
+
error(error: unknown): void;
|
|
96
|
+
/** The sink has completed. This function "closes" the sink. */
|
|
97
|
+
complete(): void;
|
|
98
|
+
}
|
package/lib/common.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* common
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.isResponse = void 0;
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
/**
|
|
11
|
+
* Checks whether the passed value is the `graphql-http` server agnostic response.
|
|
12
|
+
*
|
|
13
|
+
* @category Common
|
|
14
|
+
*/
|
|
15
|
+
function isResponse(val) {
|
|
16
|
+
// TODO: make sure the contents of init match ResponseInit
|
|
17
|
+
return Array.isArray(val) && typeof val[0] === 'string' && (0, utils_1.isObject)(val[1]);
|
|
18
|
+
}
|
|
19
|
+
exports.isResponse = isResponse;
|
package/lib/common.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* common
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import { isObject } from './utils.mjs';
|
|
7
|
+
/**
|
|
8
|
+
* Checks whether the passed value is the `graphql-http` server agnostic response.
|
|
9
|
+
*
|
|
10
|
+
* @category Common
|
|
11
|
+
*/
|
|
12
|
+
export function isResponse(val) {
|
|
13
|
+
// TODO: make sure the contents of init match ResponseInit
|
|
14
|
+
return Array.isArray(val) && typeof val[0] === 'string' && isObject(val[1]);
|
|
15
|
+
}
|