@nymphjs/client 1.0.0-beta.11 → 1.0.0-beta.111
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/CHANGELOG.md +456 -0
- package/README.md +6 -6
- package/asyncitertest.js +53 -0
- package/dist/Entity.d.ts +156 -0
- package/{lib → dist}/Entity.js +202 -104
- package/dist/Entity.js.map +1 -0
- package/dist/Entity.types.d.ts +218 -0
- package/dist/Entity.types.js +2 -0
- package/{lib → dist}/EntityWeakCache.d.ts +1 -1
- package/{lib → dist}/EntityWeakCache.js +5 -9
- package/dist/EntityWeakCache.js.map +1 -0
- package/{lib → dist}/HttpRequester.d.ts +18 -4
- package/dist/HttpRequester.js +365 -0
- package/dist/HttpRequester.js.map +1 -0
- package/{lib → dist}/Nymph.d.ts +48 -11
- package/{lib → dist}/Nymph.js +167 -51
- package/dist/Nymph.js.map +1 -0
- package/{lib → dist}/Nymph.types.d.ts +82 -2
- package/dist/Nymph.types.js +2 -0
- package/{lib → dist}/PubSub.d.ts +16 -10
- package/{lib → dist}/PubSub.js +172 -108
- package/dist/PubSub.js.map +1 -0
- package/{lib → dist}/PubSub.types.d.ts +8 -3
- package/dist/PubSub.types.js +2 -0
- package/{lib → dist}/entityRefresh.d.ts +1 -1
- package/{lib → dist}/entityRefresh.js +21 -13
- package/dist/entityRefresh.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/{lib → dist}/utils.d.ts +1 -1
- package/{lib → dist}/utils.js +28 -21
- package/dist/utils.js.map +1 -0
- package/jest.config.js +11 -2
- package/package.json +23 -27
- package/src/Entity.ts +173 -107
- package/src/Entity.types.ts +29 -47
- package/src/EntityWeakCache.ts +8 -6
- package/src/HttpRequester.ts +268 -31
- package/src/Nymph.ts +191 -88
- package/src/Nymph.types.ts +51 -2
- package/src/PubSub.ts +214 -141
- package/src/PubSub.types.ts +10 -5
- package/src/entityRefresh.ts +6 -6
- package/src/index.ts +10 -10
- package/src/utils.ts +12 -5
- package/tsconfig.json +6 -4
- package/typedoc.json +4 -0
- package/dist/index.js.LICENSE.txt +0 -8
- package/lib/Entity.d.ts +0 -51
- package/lib/Entity.js.map +0 -1
- package/lib/Entity.types.d.ts +0 -65
- package/lib/Entity.types.js +0 -3
- package/lib/EntityWeakCache.js.map +0 -1
- package/lib/HttpRequester.js +0 -190
- package/lib/HttpRequester.js.map +0 -1
- package/lib/Nymph.js.map +0 -1
- package/lib/Nymph.types.js +0 -3
- package/lib/PubSub.js.map +0 -1
- package/lib/PubSub.types.js +0 -3
- package/lib/entityRefresh.js.map +0 -1
- package/lib/index.d.ts +0 -13
- package/lib/index.js +0 -34
- package/lib/index.js.map +0 -1
- package/lib/utils.js.map +0 -1
- package/webpack.config.js +0 -28
- /package/{lib → dist}/Entity.types.js.map +0 -0
- /package/{lib → dist}/Nymph.types.js.map +0 -0
- /package/{lib → dist}/PubSub.types.js.map +0 -0
package/src/HttpRequester.ts
CHANGED
|
@@ -1,25 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchEventSource,
|
|
3
|
+
EventStreamContentType,
|
|
4
|
+
} from 'fetch-event-source-hperrin';
|
|
5
|
+
|
|
1
6
|
export type HttpRequesterEventType = 'request' | 'response';
|
|
2
7
|
export type HttpRequesterRequestCallback = (
|
|
3
8
|
requester: HttpRequester,
|
|
4
9
|
url: string,
|
|
5
|
-
options: RequestInit
|
|
10
|
+
options: RequestInit,
|
|
6
11
|
) => void;
|
|
7
12
|
export type HttpRequesterResponseCallback = (
|
|
8
13
|
requester: HttpRequester,
|
|
9
14
|
response: Response,
|
|
10
|
-
text: string
|
|
15
|
+
text: string,
|
|
16
|
+
) => void;
|
|
17
|
+
export type HttpRequesterIteratorCallback = (
|
|
18
|
+
requester: HttpRequester,
|
|
19
|
+
url: string,
|
|
20
|
+
headers: Record<string, string>,
|
|
11
21
|
) => void;
|
|
12
22
|
export type HttpRequesterRequestOptions = {
|
|
13
23
|
url: string;
|
|
24
|
+
headers?: { [k: string]: any };
|
|
14
25
|
data: { [k: string]: any };
|
|
15
26
|
dataType: string;
|
|
16
27
|
};
|
|
17
28
|
|
|
29
|
+
export interface AbortableAsyncIterator<
|
|
30
|
+
T extends any = any,
|
|
31
|
+
> extends AsyncIterable<T> {
|
|
32
|
+
abortController: AbortController;
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
export default class HttpRequester {
|
|
19
36
|
private fetch: WindowOrWorkerGlobalScope['fetch'];
|
|
20
|
-
private xsrfToken: string | null = null;
|
|
21
37
|
private requestCallbacks: HttpRequesterRequestCallback[] = [];
|
|
22
38
|
private responseCallbacks: HttpRequesterResponseCallback[] = [];
|
|
39
|
+
private iteratorCallbacks: HttpRequesterIteratorCallback[] = [];
|
|
23
40
|
|
|
24
41
|
static makeUrl(url: string, data: { [k: string]: any }) {
|
|
25
42
|
if (!data) {
|
|
@@ -45,14 +62,18 @@ export default class HttpRequester {
|
|
|
45
62
|
callback: T extends 'request'
|
|
46
63
|
? HttpRequesterRequestCallback
|
|
47
64
|
: T extends 'response'
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
? HttpRequesterResponseCallback
|
|
66
|
+
: T extends 'iterator'
|
|
67
|
+
? HttpRequesterIteratorCallback
|
|
68
|
+
: never,
|
|
50
69
|
) {
|
|
51
70
|
const prop = (event + 'Callbacks') as T extends 'request'
|
|
52
71
|
? 'requestCallbacks'
|
|
53
72
|
: T extends 'response'
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
? 'responseCallbacks'
|
|
74
|
+
: T extends 'iterator'
|
|
75
|
+
? 'iteratorCallbacks'
|
|
76
|
+
: never;
|
|
56
77
|
if (!(prop in this)) {
|
|
57
78
|
throw new Error('Invalid event type.');
|
|
58
79
|
}
|
|
@@ -66,14 +87,18 @@ export default class HttpRequester {
|
|
|
66
87
|
callback: T extends 'request'
|
|
67
88
|
? HttpRequesterRequestCallback
|
|
68
89
|
: T extends 'response'
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
? HttpRequesterResponseCallback
|
|
91
|
+
: T extends 'iterator'
|
|
92
|
+
? HttpRequesterIteratorCallback
|
|
93
|
+
: never,
|
|
71
94
|
) {
|
|
72
95
|
const prop = (event + 'Callbacks') as T extends 'request'
|
|
73
96
|
? 'requestCallbacks'
|
|
74
97
|
: T extends 'response'
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
? 'responseCallbacks'
|
|
99
|
+
: T extends 'iterator'
|
|
100
|
+
? 'iteratorCallbacks'
|
|
101
|
+
: never;
|
|
77
102
|
if (!(prop in this)) {
|
|
78
103
|
return false;
|
|
79
104
|
}
|
|
@@ -86,10 +111,6 @@ export default class HttpRequester {
|
|
|
86
111
|
return true;
|
|
87
112
|
}
|
|
88
113
|
|
|
89
|
-
setXsrfToken(xsrfToken: string | null) {
|
|
90
|
-
this.xsrfToken = xsrfToken;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
114
|
async GET(opt: HttpRequesterRequestOptions) {
|
|
94
115
|
return await this._httpRequest('GET', opt);
|
|
95
116
|
}
|
|
@@ -98,6 +119,10 @@ export default class HttpRequester {
|
|
|
98
119
|
return await this._httpRequest('POST', opt);
|
|
99
120
|
}
|
|
100
121
|
|
|
122
|
+
async POST_ITERATOR(opt: HttpRequesterRequestOptions) {
|
|
123
|
+
return await this._iteratorRequest('POST', opt);
|
|
124
|
+
}
|
|
125
|
+
|
|
101
126
|
async PUT(opt: HttpRequesterRequestOptions) {
|
|
102
127
|
return await this._httpRequest('PUT', opt);
|
|
103
128
|
}
|
|
@@ -112,7 +137,7 @@ export default class HttpRequester {
|
|
|
112
137
|
|
|
113
138
|
async _httpRequest(
|
|
114
139
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
115
|
-
opt: HttpRequesterRequestOptions
|
|
140
|
+
opt: HttpRequesterRequestOptions,
|
|
116
141
|
) {
|
|
117
142
|
const dataString = JSON.stringify(opt.data);
|
|
118
143
|
let url = opt.url;
|
|
@@ -123,7 +148,7 @@ export default class HttpRequester {
|
|
|
123
148
|
}
|
|
124
149
|
const options: RequestInit = {
|
|
125
150
|
method,
|
|
126
|
-
headers: {},
|
|
151
|
+
headers: opt.headers ?? {},
|
|
127
152
|
credentials: 'include',
|
|
128
153
|
};
|
|
129
154
|
|
|
@@ -137,18 +162,13 @@ export default class HttpRequester {
|
|
|
137
162
|
this.requestCallbacks[i] && this.requestCallbacks[i](this, url, options);
|
|
138
163
|
}
|
|
139
164
|
|
|
140
|
-
if (this.xsrfToken !== null) {
|
|
141
|
-
(options.headers as Record<string, string>)['X-Xsrf-Token'] =
|
|
142
|
-
this.xsrfToken;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
165
|
const response = await this.fetch(url, options);
|
|
146
166
|
let text: string;
|
|
147
167
|
try {
|
|
148
168
|
text = await response.text();
|
|
149
169
|
} catch (e: any) {
|
|
150
170
|
throw new InvalidResponseError(
|
|
151
|
-
'Server response did not contain valid text body.'
|
|
171
|
+
'Server response did not contain valid text body.',
|
|
152
172
|
);
|
|
153
173
|
}
|
|
154
174
|
if (!response.ok) {
|
|
@@ -170,12 +190,12 @@ export default class HttpRequester {
|
|
|
170
190
|
throw response.status < 200
|
|
171
191
|
? new InformationalError(response, errObj)
|
|
172
192
|
: response.status < 300
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
? new SuccessError(response, errObj)
|
|
194
|
+
: response.status < 400
|
|
195
|
+
? new RedirectError(response, errObj)
|
|
196
|
+
: response.status < 500
|
|
197
|
+
? new ClientError(response, errObj)
|
|
198
|
+
: new ServerError(response, errObj);
|
|
179
199
|
}
|
|
180
200
|
for (let i = 0; i < this.responseCallbacks.length; i++) {
|
|
181
201
|
this.responseCallbacks[i] &&
|
|
@@ -192,13 +212,216 @@ export default class HttpRequester {
|
|
|
192
212
|
throw e;
|
|
193
213
|
}
|
|
194
214
|
throw new InvalidResponseError(
|
|
195
|
-
'Server response was invalid: ' + JSON.stringify(text)
|
|
215
|
+
'Server response was invalid: ' + JSON.stringify(text),
|
|
196
216
|
);
|
|
197
217
|
}
|
|
198
218
|
} else {
|
|
199
219
|
return text;
|
|
200
220
|
}
|
|
201
221
|
}
|
|
222
|
+
|
|
223
|
+
async _iteratorRequest(
|
|
224
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
225
|
+
opt: HttpRequesterRequestOptions,
|
|
226
|
+
): Promise<AbortableAsyncIterator> {
|
|
227
|
+
const dataString = JSON.stringify(opt.data);
|
|
228
|
+
let url = opt.url;
|
|
229
|
+
if (method === 'GET') {
|
|
230
|
+
// TODO: what should this size be?
|
|
231
|
+
// && dataString.length < 1) {
|
|
232
|
+
url = HttpRequester.makeUrl(opt.url, opt.data);
|
|
233
|
+
}
|
|
234
|
+
const hasBody = method !== 'GET' && opt.data;
|
|
235
|
+
const headers: Record<string, string> = opt.headers ?? {};
|
|
236
|
+
|
|
237
|
+
if (hasBody) {
|
|
238
|
+
headers['Content-Type'] = 'application/json';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (let i = 0; i < this.iteratorCallbacks.length; i++) {
|
|
242
|
+
this.iteratorCallbacks[i] &&
|
|
243
|
+
this.iteratorCallbacks[i](this, url, headers);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const responses: any[] = [];
|
|
247
|
+
let nextResponseResolve: (value: void) => void;
|
|
248
|
+
let nextResponseReadyPromise = new Promise<void>((res) => {
|
|
249
|
+
nextResponseResolve = res;
|
|
250
|
+
});
|
|
251
|
+
let responsesDone = false;
|
|
252
|
+
let serverResponse: Response;
|
|
253
|
+
|
|
254
|
+
const ctrl = new AbortController();
|
|
255
|
+
|
|
256
|
+
fetchEventSource(url, {
|
|
257
|
+
openWhenHidden: true,
|
|
258
|
+
fetch: this.fetch,
|
|
259
|
+
|
|
260
|
+
method,
|
|
261
|
+
headers,
|
|
262
|
+
credentials: 'include',
|
|
263
|
+
body: hasBody ? dataString : undefined,
|
|
264
|
+
signal: ctrl.signal,
|
|
265
|
+
|
|
266
|
+
async onopen(response) {
|
|
267
|
+
serverResponse = response;
|
|
268
|
+
if (response.ok) {
|
|
269
|
+
if (response.headers.get('content-type') === EventStreamContentType) {
|
|
270
|
+
throw new InvalidResponseError(
|
|
271
|
+
'Server response is not an event stream.',
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Response is ok, wait for messages.
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let text: string = '';
|
|
280
|
+
try {
|
|
281
|
+
text = await response.text();
|
|
282
|
+
} catch (e: any) {
|
|
283
|
+
// Ignore error here.
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let errObj;
|
|
287
|
+
try {
|
|
288
|
+
errObj = JSON.parse(text);
|
|
289
|
+
} catch (e: any) {
|
|
290
|
+
if (!(e instanceof SyntaxError)) {
|
|
291
|
+
throw e;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (typeof errObj !== 'object') {
|
|
296
|
+
errObj = {
|
|
297
|
+
textStatus: response.statusText,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
errObj.status = response.status;
|
|
301
|
+
throw response.status < 200
|
|
302
|
+
? new InformationalError(response, errObj)
|
|
303
|
+
: response.status < 300
|
|
304
|
+
? new SuccessError(response, errObj)
|
|
305
|
+
: response.status < 400
|
|
306
|
+
? new RedirectError(response, errObj)
|
|
307
|
+
: response.status < 500
|
|
308
|
+
? new ClientError(response, errObj)
|
|
309
|
+
: new ServerError(response, errObj);
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
onmessage(event) {
|
|
313
|
+
if (event.event === 'next') {
|
|
314
|
+
let text = event.data;
|
|
315
|
+
|
|
316
|
+
if (opt.dataType === 'json') {
|
|
317
|
+
if (!text.length) {
|
|
318
|
+
responses.push(
|
|
319
|
+
new InvalidResponseError('Server response was empty.'),
|
|
320
|
+
);
|
|
321
|
+
} else {
|
|
322
|
+
try {
|
|
323
|
+
responses.push(JSON.parse(text));
|
|
324
|
+
} catch (e: any) {
|
|
325
|
+
if (!(e instanceof SyntaxError)) {
|
|
326
|
+
responses.push(e);
|
|
327
|
+
} else {
|
|
328
|
+
responses.push(
|
|
329
|
+
new InvalidResponseError(
|
|
330
|
+
'Server response was invalid: ' + JSON.stringify(text),
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
responses.push(text);
|
|
338
|
+
}
|
|
339
|
+
} else if (event.event === 'error') {
|
|
340
|
+
let text = event.data;
|
|
341
|
+
|
|
342
|
+
let errObj;
|
|
343
|
+
try {
|
|
344
|
+
errObj = JSON.parse(text);
|
|
345
|
+
} catch (e: any) {
|
|
346
|
+
if (!(e instanceof SyntaxError)) {
|
|
347
|
+
throw e;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (typeof errObj !== 'object') {
|
|
352
|
+
errObj = {
|
|
353
|
+
status: 500,
|
|
354
|
+
textStatus: 'Iterator Error',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
responses.push(
|
|
358
|
+
errObj.status < 200
|
|
359
|
+
? new InformationalError(serverResponse, errObj)
|
|
360
|
+
: errObj.status < 300
|
|
361
|
+
? new SuccessError(serverResponse, errObj)
|
|
362
|
+
: errObj.status < 400
|
|
363
|
+
? new RedirectError(serverResponse, errObj)
|
|
364
|
+
: errObj.status < 500
|
|
365
|
+
? new ClientError(serverResponse, errObj)
|
|
366
|
+
: new ServerError(serverResponse, errObj),
|
|
367
|
+
);
|
|
368
|
+
} else if (event.event === 'finished') {
|
|
369
|
+
responsesDone = true;
|
|
370
|
+
} else if (event.event === 'ping') {
|
|
371
|
+
// Ignore keep-alive pings.
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const resolve = nextResponseResolve;
|
|
376
|
+
if (!responsesDone) {
|
|
377
|
+
nextResponseReadyPromise = new Promise<void>((res) => {
|
|
378
|
+
nextResponseResolve = res;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Resolve the promise to continue any waiting iterator.
|
|
383
|
+
resolve();
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
onclose() {
|
|
387
|
+
responses.push(
|
|
388
|
+
new ConnectionClosedUnexpectedlyError(
|
|
389
|
+
'The connection to the server was closed unexpectedly.',
|
|
390
|
+
),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
responsesDone = true;
|
|
394
|
+
nextResponseResolve();
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
onerror(err) {
|
|
398
|
+
// Rethrow to stop the operation.
|
|
399
|
+
throw err;
|
|
400
|
+
},
|
|
401
|
+
}).catch((err) => {
|
|
402
|
+
responses.push(
|
|
403
|
+
new ConnectionError('The connection could not be established: ' + err),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
responsesDone = true;
|
|
407
|
+
nextResponseResolve();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const iterator: AbortableAsyncIterator = {
|
|
411
|
+
abortController: ctrl,
|
|
412
|
+
async *[Symbol.asyncIterator]() {
|
|
413
|
+
do {
|
|
414
|
+
await nextResponseReadyPromise;
|
|
415
|
+
|
|
416
|
+
while (responses.length) {
|
|
417
|
+
yield responses.shift();
|
|
418
|
+
}
|
|
419
|
+
} while (!responsesDone);
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return iterator;
|
|
424
|
+
}
|
|
202
425
|
}
|
|
203
426
|
|
|
204
427
|
export class InvalidResponseError extends Error {
|
|
@@ -208,6 +431,20 @@ export class InvalidResponseError extends Error {
|
|
|
208
431
|
}
|
|
209
432
|
}
|
|
210
433
|
|
|
434
|
+
export class ConnectionClosedUnexpectedlyError extends Error {
|
|
435
|
+
constructor(message: string) {
|
|
436
|
+
super(message);
|
|
437
|
+
this.name = 'ConnectionClosedUnexpectedlyError';
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export class ConnectionError extends Error {
|
|
442
|
+
constructor(message: string) {
|
|
443
|
+
super(message);
|
|
444
|
+
this.name = 'ConnectionError';
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
211
448
|
export class HttpError extends Error {
|
|
212
449
|
status: number;
|
|
213
450
|
statusText: string;
|
|
@@ -215,7 +452,7 @@ export class HttpError extends Error {
|
|
|
215
452
|
constructor(
|
|
216
453
|
name: string,
|
|
217
454
|
response: Response,
|
|
218
|
-
errObj: { textStatus: string }
|
|
455
|
+
errObj: { textStatus: string },
|
|
219
456
|
) {
|
|
220
457
|
super(errObj.textStatus);
|
|
221
458
|
this.name = name;
|