@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.
@@ -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'