@nymphjs/client 1.0.0-beta.34 → 1.0.0-beta.35
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 +6 -0
- package/asyncitertest.js +53 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/lib/Entity.d.ts +1 -0
- package/lib/Entity.js +3 -0
- package/lib/Entity.js.map +1 -1
- package/lib/HttpRequester.d.ts +15 -2
- package/lib/HttpRequester.js +185 -1
- package/lib/HttpRequester.js.map +1 -1
- package/lib/Nymph.d.ts +2 -0
- package/lib/Nymph.js +31 -0
- package/lib/Nymph.js.map +1 -1
- package/package.json +13 -10
- package/src/Entity.ts +13 -0
- package/src/HttpRequester.ts +249 -0
- package/src/Nymph.ts +38 -0
package/src/HttpRequester.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
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,
|
|
@@ -9,17 +14,28 @@ export type HttpRequesterResponseCallback = (
|
|
|
9
14
|
response: Response,
|
|
10
15
|
text: string
|
|
11
16
|
) => void;
|
|
17
|
+
export type HttpRequesterIteratorCallback = (
|
|
18
|
+
requester: HttpRequester,
|
|
19
|
+
url: string,
|
|
20
|
+
headers: Record<string, string>
|
|
21
|
+
) => void;
|
|
12
22
|
export type HttpRequesterRequestOptions = {
|
|
13
23
|
url: string;
|
|
14
24
|
data: { [k: string]: any };
|
|
15
25
|
dataType: string;
|
|
16
26
|
};
|
|
17
27
|
|
|
28
|
+
export interface AbortableAsyncIterator<T extends any = any>
|
|
29
|
+
extends AsyncIterable<T> {
|
|
30
|
+
abortController: AbortController;
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
export default class HttpRequester {
|
|
19
34
|
private fetch: WindowOrWorkerGlobalScope['fetch'];
|
|
20
35
|
private xsrfToken: string | null = null;
|
|
21
36
|
private requestCallbacks: HttpRequesterRequestCallback[] = [];
|
|
22
37
|
private responseCallbacks: HttpRequesterResponseCallback[] = [];
|
|
38
|
+
private iteratorCallbacks: HttpRequesterIteratorCallback[] = [];
|
|
23
39
|
|
|
24
40
|
static makeUrl(url: string, data: { [k: string]: any }) {
|
|
25
41
|
if (!data) {
|
|
@@ -46,12 +62,16 @@ export default class HttpRequester {
|
|
|
46
62
|
? HttpRequesterRequestCallback
|
|
47
63
|
: T extends 'response'
|
|
48
64
|
? HttpRequesterResponseCallback
|
|
65
|
+
: T extends 'iterator'
|
|
66
|
+
? HttpRequesterIteratorCallback
|
|
49
67
|
: never
|
|
50
68
|
) {
|
|
51
69
|
const prop = (event + 'Callbacks') as T extends 'request'
|
|
52
70
|
? 'requestCallbacks'
|
|
53
71
|
: T extends 'response'
|
|
54
72
|
? 'responseCallbacks'
|
|
73
|
+
: T extends 'iterator'
|
|
74
|
+
? 'iteratorCallbacks'
|
|
55
75
|
: never;
|
|
56
76
|
if (!(prop in this)) {
|
|
57
77
|
throw new Error('Invalid event type.');
|
|
@@ -67,12 +87,16 @@ export default class HttpRequester {
|
|
|
67
87
|
? HttpRequesterRequestCallback
|
|
68
88
|
: T extends 'response'
|
|
69
89
|
? HttpRequesterResponseCallback
|
|
90
|
+
: T extends 'iterator'
|
|
91
|
+
? HttpRequesterIteratorCallback
|
|
70
92
|
: never
|
|
71
93
|
) {
|
|
72
94
|
const prop = (event + 'Callbacks') as T extends 'request'
|
|
73
95
|
? 'requestCallbacks'
|
|
74
96
|
: T extends 'response'
|
|
75
97
|
? 'responseCallbacks'
|
|
98
|
+
: T extends 'iterator'
|
|
99
|
+
? 'iteratorCallbacks'
|
|
76
100
|
: never;
|
|
77
101
|
if (!(prop in this)) {
|
|
78
102
|
return false;
|
|
@@ -98,6 +122,10 @@ export default class HttpRequester {
|
|
|
98
122
|
return await this._httpRequest('POST', opt);
|
|
99
123
|
}
|
|
100
124
|
|
|
125
|
+
async POST_ITERATOR(opt: HttpRequesterRequestOptions) {
|
|
126
|
+
return await this._iteratorRequest('POST', opt);
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
async PUT(opt: HttpRequesterRequestOptions) {
|
|
102
130
|
return await this._httpRequest('PUT', opt);
|
|
103
131
|
}
|
|
@@ -199,6 +227,213 @@ export default class HttpRequester {
|
|
|
199
227
|
return text;
|
|
200
228
|
}
|
|
201
229
|
}
|
|
230
|
+
|
|
231
|
+
async _iteratorRequest(
|
|
232
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
233
|
+
opt: HttpRequesterRequestOptions
|
|
234
|
+
): Promise<AbortableAsyncIterator> {
|
|
235
|
+
const dataString = JSON.stringify(opt.data);
|
|
236
|
+
let url = opt.url;
|
|
237
|
+
if (method === 'GET') {
|
|
238
|
+
// TODO: what should this size be?
|
|
239
|
+
// && dataString.length < 1) {
|
|
240
|
+
url = HttpRequester.makeUrl(opt.url, opt.data);
|
|
241
|
+
}
|
|
242
|
+
const hasBody = method !== 'GET' && opt.data;
|
|
243
|
+
const headers: Record<string, string> = {};
|
|
244
|
+
|
|
245
|
+
if (hasBody) {
|
|
246
|
+
headers['Content-Type'] = 'application/json';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (let i = 0; i < this.iteratorCallbacks.length; i++) {
|
|
250
|
+
this.iteratorCallbacks[i] &&
|
|
251
|
+
this.iteratorCallbacks[i](this, url, headers);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (this.xsrfToken !== null) {
|
|
255
|
+
headers['X-Xsrf-Token'] = this.xsrfToken;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const responses: any[] = [];
|
|
259
|
+
let nextResponseResolve: (value: void) => void;
|
|
260
|
+
let nextResponseReadyPromise = new Promise<void>((res) => {
|
|
261
|
+
nextResponseResolve = res;
|
|
262
|
+
});
|
|
263
|
+
let responsesDone = false;
|
|
264
|
+
let serverResponse: Response;
|
|
265
|
+
|
|
266
|
+
const ctrl = new AbortController();
|
|
267
|
+
|
|
268
|
+
fetchEventSource(url, {
|
|
269
|
+
openWhenHidden: true,
|
|
270
|
+
fetch: this.fetch,
|
|
271
|
+
|
|
272
|
+
method,
|
|
273
|
+
headers,
|
|
274
|
+
credentials: 'include',
|
|
275
|
+
body: hasBody ? dataString : undefined,
|
|
276
|
+
signal: ctrl.signal,
|
|
277
|
+
|
|
278
|
+
async onopen(response) {
|
|
279
|
+
serverResponse = response;
|
|
280
|
+
if (response.ok) {
|
|
281
|
+
if (response.headers.get('content-type') === EventStreamContentType) {
|
|
282
|
+
throw new InvalidResponseError(
|
|
283
|
+
'Server response is not an event stream.'
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Response is ok, wait for messages.
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let text: string = '';
|
|
292
|
+
try {
|
|
293
|
+
text = await response.text();
|
|
294
|
+
} catch (e: any) {
|
|
295
|
+
// Ignore error here.
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let errObj;
|
|
299
|
+
try {
|
|
300
|
+
errObj = JSON.parse(text);
|
|
301
|
+
} catch (e: any) {
|
|
302
|
+
if (!(e instanceof SyntaxError)) {
|
|
303
|
+
throw e;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (typeof errObj !== 'object') {
|
|
308
|
+
errObj = {
|
|
309
|
+
textStatus: response.statusText,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
errObj.status = response.status;
|
|
313
|
+
throw response.status < 200
|
|
314
|
+
? new InformationalError(response, errObj)
|
|
315
|
+
: response.status < 300
|
|
316
|
+
? new SuccessError(response, errObj)
|
|
317
|
+
: response.status < 400
|
|
318
|
+
? new RedirectError(response, errObj)
|
|
319
|
+
: response.status < 500
|
|
320
|
+
? new ClientError(response, errObj)
|
|
321
|
+
: new ServerError(response, errObj);
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
onmessage(event) {
|
|
325
|
+
if (event.event === 'next') {
|
|
326
|
+
let text = event.data;
|
|
327
|
+
|
|
328
|
+
if (opt.dataType === 'json') {
|
|
329
|
+
if (!text.length) {
|
|
330
|
+
responses.push(
|
|
331
|
+
new InvalidResponseError('Server response was empty.')
|
|
332
|
+
);
|
|
333
|
+
} else {
|
|
334
|
+
try {
|
|
335
|
+
responses.push(JSON.parse(text));
|
|
336
|
+
} catch (e: any) {
|
|
337
|
+
if (!(e instanceof SyntaxError)) {
|
|
338
|
+
responses.push(e);
|
|
339
|
+
} else {
|
|
340
|
+
responses.push(
|
|
341
|
+
new InvalidResponseError(
|
|
342
|
+
'Server response was invalid: ' + JSON.stringify(text)
|
|
343
|
+
)
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
responses.push(text);
|
|
350
|
+
}
|
|
351
|
+
} else if (event.event === 'error') {
|
|
352
|
+
let text = event.data;
|
|
353
|
+
|
|
354
|
+
let errObj;
|
|
355
|
+
try {
|
|
356
|
+
errObj = JSON.parse(text);
|
|
357
|
+
} catch (e: any) {
|
|
358
|
+
if (!(e instanceof SyntaxError)) {
|
|
359
|
+
throw e;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (typeof errObj !== 'object') {
|
|
364
|
+
errObj = {
|
|
365
|
+
status: 500,
|
|
366
|
+
textStatus: 'Iterator Error',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
responses.push(
|
|
370
|
+
errObj.status < 200
|
|
371
|
+
? new InformationalError(serverResponse, errObj)
|
|
372
|
+
: errObj.status < 300
|
|
373
|
+
? new SuccessError(serverResponse, errObj)
|
|
374
|
+
: errObj.status < 400
|
|
375
|
+
? new RedirectError(serverResponse, errObj)
|
|
376
|
+
: errObj.status < 500
|
|
377
|
+
? new ClientError(serverResponse, errObj)
|
|
378
|
+
: new ServerError(serverResponse, errObj)
|
|
379
|
+
);
|
|
380
|
+
} else if (event.event === 'finished') {
|
|
381
|
+
responsesDone = true;
|
|
382
|
+
} else if (event.event === 'ping') {
|
|
383
|
+
// Ignore keep-alive pings.
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const resolve = nextResponseResolve;
|
|
388
|
+
if (!responsesDone) {
|
|
389
|
+
nextResponseReadyPromise = new Promise<void>((res) => {
|
|
390
|
+
nextResponseResolve = res;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Resolve the promise to continue any waiting iterator.
|
|
395
|
+
resolve();
|
|
396
|
+
},
|
|
397
|
+
|
|
398
|
+
onclose() {
|
|
399
|
+
responses.push(
|
|
400
|
+
new ConnectionClosedUnexpectedlyError(
|
|
401
|
+
'The connection to the server was closed unexpectedly.'
|
|
402
|
+
)
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
responsesDone = true;
|
|
406
|
+
nextResponseResolve();
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
onerror(err) {
|
|
410
|
+
// Rethrow to stop the operation.
|
|
411
|
+
throw err;
|
|
412
|
+
},
|
|
413
|
+
}).catch((err) => {
|
|
414
|
+
responses.push(
|
|
415
|
+
new ConnectionError('The connection could not be established: ' + err)
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
responsesDone = true;
|
|
419
|
+
nextResponseResolve();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const iterator: AbortableAsyncIterator = {
|
|
423
|
+
abortController: ctrl,
|
|
424
|
+
async *[Symbol.asyncIterator]() {
|
|
425
|
+
do {
|
|
426
|
+
await nextResponseReadyPromise;
|
|
427
|
+
|
|
428
|
+
while (responses.length) {
|
|
429
|
+
yield responses.shift();
|
|
430
|
+
}
|
|
431
|
+
} while (!responsesDone);
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
return iterator;
|
|
436
|
+
}
|
|
202
437
|
}
|
|
203
438
|
|
|
204
439
|
export class InvalidResponseError extends Error {
|
|
@@ -208,6 +443,20 @@ export class InvalidResponseError extends Error {
|
|
|
208
443
|
}
|
|
209
444
|
}
|
|
210
445
|
|
|
446
|
+
export class ConnectionClosedUnexpectedlyError extends Error {
|
|
447
|
+
constructor(message: string) {
|
|
448
|
+
super(message);
|
|
449
|
+
this.name = 'ConnectionClosedUnexpectedlyError';
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export class ConnectionError extends Error {
|
|
454
|
+
constructor(message: string) {
|
|
455
|
+
super(message);
|
|
456
|
+
this.name = 'ConnectionError';
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
211
460
|
export class HttpError extends Error {
|
|
212
461
|
status: number;
|
|
213
462
|
statusText: string;
|
package/src/Nymph.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ServerCallStaticResponse,
|
|
8
8
|
} from './Entity.types';
|
|
9
9
|
import EntityWeakCache from './EntityWeakCache';
|
|
10
|
+
import type { AbortableAsyncIterator } from './HttpRequester';
|
|
10
11
|
import HttpRequester from './HttpRequester';
|
|
11
12
|
import type {
|
|
12
13
|
EventType,
|
|
@@ -521,6 +522,43 @@ export default class Nymph {
|
|
|
521
522
|
return this.initEntitiesFromData(data);
|
|
522
523
|
}
|
|
523
524
|
|
|
525
|
+
public async serverCallStaticIterator(
|
|
526
|
+
className: string,
|
|
527
|
+
method: string,
|
|
528
|
+
params: any[]
|
|
529
|
+
): Promise<AbortableAsyncIterator<ServerCallStaticResponse>> {
|
|
530
|
+
const iterable = await requester.POST_ITERATOR({
|
|
531
|
+
url: this.restUrl,
|
|
532
|
+
dataType: 'json',
|
|
533
|
+
data: {
|
|
534
|
+
action: 'method',
|
|
535
|
+
data: {
|
|
536
|
+
class: className,
|
|
537
|
+
static: true,
|
|
538
|
+
method: method,
|
|
539
|
+
iterator: true,
|
|
540
|
+
params: entitiesToReferences(entityConstructorsToClassNames(params)),
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const that = this;
|
|
546
|
+
const iterator: AbortableAsyncIterator = {
|
|
547
|
+
abortController: iterable.abortController,
|
|
548
|
+
async *[Symbol.asyncIterator]() {
|
|
549
|
+
for await (let response of iterable) {
|
|
550
|
+
if (response instanceof Error) {
|
|
551
|
+
yield response;
|
|
552
|
+
} else {
|
|
553
|
+
yield that.initEntitiesFromData(response);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
return iterator;
|
|
560
|
+
}
|
|
561
|
+
|
|
524
562
|
public on<T extends EventType>(
|
|
525
563
|
event: T,
|
|
526
564
|
callback: T extends 'request'
|