@nymphjs/client 1.0.0-beta.7 → 1.0.0-beta.71

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,4 +1,5 @@
1
1
  import type Nymph from './Nymph';
2
+ import type Entity from './Entity';
2
3
 
3
4
  export type ServerCallResponse = {
4
5
  return: any;
@@ -166,14 +167,14 @@ export interface EntityInterface extends DataObjectInterface {
166
167
  *
167
168
  * @returns The entity.
168
169
  */
169
- $ready(): Promise<EntityInterface>;
170
+ $wake(): Promise<EntityInterface>;
170
171
  /**
171
172
  * Ready this entity's data, and the data of entity's within this one's.
172
173
  *
173
- * @param level The number of levels deep to ready. If undefined, it will keep going until there are no more entities. (Careful of infinite loops.)
174
+ * @param level The number of levels deep to wake. If undefined, it will keep going until there are no more entities. (Careful of infinite loops.)
174
175
  * @returns The entity.
175
176
  */
176
- $readyAll(level?: number): Promise<EntityInterface>;
177
+ $wakeAll(level?: number): Promise<EntityInterface>;
177
178
  /**
178
179
  * Remove one or more tags.
179
180
  *
@@ -195,7 +196,7 @@ export interface EntityInterface extends DataObjectInterface {
195
196
  $serverCall(
196
197
  method: string,
197
198
  params: Iterable<any>,
198
- stateless: boolean
199
+ stateless: boolean,
199
200
  ): Promise<any>;
200
201
  /**
201
202
  * Return a Nymph Entity Reference for this entity.
@@ -208,46 +209,9 @@ export interface EntityInterface extends DataObjectInterface {
208
209
  $toReference(): EntityReference | EntityInterface;
209
210
  }
210
211
 
211
- export type EntityConstructor = (new () => EntityInterface) & {
212
- /**
213
- * The instance of Nymph to use for queries.
214
- */
215
- nymph: Nymph;
216
- /**
217
- * The lookup name for this entity.
218
- *
219
- * This is used for reference arrays (and sleeping references) and client
220
- * requests.
221
- */
222
- class: string;
223
- /**
224
- * Create a new entity instance.
225
- *
226
- * @param guid An optional GUID to retrieve.
227
- */
228
- factory(guid?: string): Promise<EntityInterface>;
229
- /**
230
- * Create a new entity instance.
231
- *
232
- * @param guid An optional GUID to retrieve.
233
- */
234
- factorySync(guid?: string): EntityInterface;
235
- /**
236
- * Create a new sleeping reference instance.
237
- *
238
- * Sleeping references won't retrieve their data from the server until they
239
- * are readied with `ready()` or a parent's `readyAll()`.
240
- *
241
- * @param reference The Nymph Entity Reference to use to wake.
242
- * @returns The new instance.
243
- */
244
- factoryReference(reference: EntityReference): EntityInterface;
245
- /**
246
- * Call a static method on the server version of this entity.
247
- *
248
- * @param method The name of the method.
249
- * @param params The parameters to call the method with.
250
- * @returns The value that the method on the server returned.
251
- */
252
- serverCallStatic(method: string, params: Iterable<any>): Promise<any>;
212
+ export type EntityConstructor<
213
+ D extends EntityData = EntityData,
214
+ E extends Entity<D> = Entity<D>,
215
+ > = (new (...args: any[]) => E) & {
216
+ [k in keyof typeof Entity]: (typeof Entity)[k];
253
217
  };
@@ -1,15 +1,18 @@
1
1
  import { EntityConstructor, EntityInterface } from './Entity.types';
2
2
 
3
3
  export default class EntityWeakCache {
4
- private references: WeakMap<EntityConstructor, { [k: string]: any }> =
5
- new WeakMap();
4
+ private references: WeakMap<
5
+ EntityConstructor,
6
+ { [k: string]: WeakRef<EntityInterface> }
7
+ > = new WeakMap();
6
8
 
7
9
  get(EntityClass: EntityConstructor, guid: string): EntityInterface | null {
8
10
  const classMap = this.references.get(EntityClass);
9
11
  if (classMap && guid in classMap) {
10
12
  const weakRef = classMap[guid];
11
- if (weakRef && weakRef.deref() != null) {
12
- return weakRef.deref();
13
+ const deref = weakRef && weakRef.deref();
14
+ if (deref != null) {
15
+ return deref;
13
16
  } else {
14
17
  delete classMap[guid];
15
18
  }
@@ -22,7 +25,6 @@ export default class EntityWeakCache {
22
25
  return;
23
26
  }
24
27
 
25
- // @ts-ignore TS doesn't know about WeakRef.
26
28
  const weakRef = new WeakRef(entity);
27
29
 
28
30
  const classMap = this.references.get(EntityClass) || {};
@@ -1,25 +1,41 @@
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<T extends any = any>
30
+ extends AsyncIterable<T> {
31
+ abortController: AbortController;
32
+ }
33
+
18
34
  export default class HttpRequester {
19
35
  private fetch: WindowOrWorkerGlobalScope['fetch'];
20
- 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
49
- : never
65
+ : T extends 'iterator'
66
+ ? HttpRequesterIteratorCallback
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
70
- : never
90
+ : T extends 'iterator'
91
+ ? HttpRequesterIteratorCallback
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;
@@ -86,10 +110,6 @@ export default class HttpRequester {
86
110
  return true;
87
111
  }
88
112
 
89
- setXsrfToken(xsrfToken: string | null) {
90
- this.xsrfToken = xsrfToken;
91
- }
92
-
93
113
  async GET(opt: HttpRequesterRequestOptions) {
94
114
  return await this._httpRequest('GET', opt);
95
115
  }
@@ -98,6 +118,10 @@ export default class HttpRequester {
98
118
  return await this._httpRequest('POST', opt);
99
119
  }
100
120
 
121
+ async POST_ITERATOR(opt: HttpRequesterRequestOptions) {
122
+ return await this._iteratorRequest('POST', opt);
123
+ }
124
+
101
125
  async PUT(opt: HttpRequesterRequestOptions) {
102
126
  return await this._httpRequest('PUT', opt);
103
127
  }
@@ -112,7 +136,7 @@ export default class HttpRequester {
112
136
 
113
137
  async _httpRequest(
114
138
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
115
- opt: HttpRequesterRequestOptions
139
+ opt: HttpRequesterRequestOptions,
116
140
  ) {
117
141
  const dataString = JSON.stringify(opt.data);
118
142
  let url = opt.url;
@@ -123,7 +147,7 @@ export default class HttpRequester {
123
147
  }
124
148
  const options: RequestInit = {
125
149
  method,
126
- headers: {},
150
+ headers: opt.headers ?? {},
127
151
  credentials: 'include',
128
152
  };
129
153
 
@@ -137,18 +161,13 @@ export default class HttpRequester {
137
161
  this.requestCallbacks[i] && this.requestCallbacks[i](this, url, options);
138
162
  }
139
163
 
140
- if (this.xsrfToken !== null) {
141
- (options.headers as Record<string, string>)['X-Xsrf-Token'] =
142
- this.xsrfToken;
143
- }
144
-
145
164
  const response = await this.fetch(url, options);
146
165
  let text: string;
147
166
  try {
148
167
  text = await response.text();
149
168
  } catch (e: any) {
150
169
  throw new InvalidResponseError(
151
- 'Server response did not contain valid text body.'
170
+ 'Server response did not contain valid text body.',
152
171
  );
153
172
  }
154
173
  if (!response.ok) {
@@ -167,9 +186,15 @@ export default class HttpRequester {
167
186
  };
168
187
  }
169
188
  errObj.status = response.status;
170
- throw response.status < 500
171
- ? new ClientError(errObj)
172
- : new ServerError(errObj);
189
+ throw response.status < 200
190
+ ? new InformationalError(response, errObj)
191
+ : response.status < 300
192
+ ? new SuccessError(response, errObj)
193
+ : response.status < 400
194
+ ? new RedirectError(response, errObj)
195
+ : response.status < 500
196
+ ? new ClientError(response, errObj)
197
+ : new ServerError(response, errObj);
173
198
  }
174
199
  for (let i = 0; i < this.responseCallbacks.length; i++) {
175
200
  this.responseCallbacks[i] &&
@@ -186,13 +211,216 @@ export default class HttpRequester {
186
211
  throw e;
187
212
  }
188
213
  throw new InvalidResponseError(
189
- 'Server response was invalid: ' + JSON.stringify(text)
214
+ 'Server response was invalid: ' + JSON.stringify(text),
190
215
  );
191
216
  }
192
217
  } else {
193
218
  return text;
194
219
  }
195
220
  }
221
+
222
+ async _iteratorRequest(
223
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
224
+ opt: HttpRequesterRequestOptions,
225
+ ): Promise<AbortableAsyncIterator> {
226
+ const dataString = JSON.stringify(opt.data);
227
+ let url = opt.url;
228
+ if (method === 'GET') {
229
+ // TODO: what should this size be?
230
+ // && dataString.length < 1) {
231
+ url = HttpRequester.makeUrl(opt.url, opt.data);
232
+ }
233
+ const hasBody = method !== 'GET' && opt.data;
234
+ const headers: Record<string, string> = opt.headers ?? {};
235
+
236
+ if (hasBody) {
237
+ headers['Content-Type'] = 'application/json';
238
+ }
239
+
240
+ for (let i = 0; i < this.iteratorCallbacks.length; i++) {
241
+ this.iteratorCallbacks[i] &&
242
+ this.iteratorCallbacks[i](this, url, headers);
243
+ }
244
+
245
+ const responses: any[] = [];
246
+ let nextResponseResolve: (value: void) => void;
247
+ let nextResponseReadyPromise = new Promise<void>((res) => {
248
+ nextResponseResolve = res;
249
+ });
250
+ let responsesDone = false;
251
+ let serverResponse: Response;
252
+
253
+ const ctrl = new AbortController();
254
+
255
+ fetchEventSource(url, {
256
+ openWhenHidden: true,
257
+ fetch: this.fetch,
258
+
259
+ method,
260
+ headers,
261
+ credentials: 'include',
262
+ body: hasBody ? dataString : undefined,
263
+ signal: ctrl.signal,
264
+
265
+ async onopen(response) {
266
+ serverResponse = response;
267
+ if (response.ok) {
268
+ if (response.headers.get('content-type') === EventStreamContentType) {
269
+ throw new InvalidResponseError(
270
+ 'Server response is not an event stream.',
271
+ );
272
+ }
273
+
274
+ // Response is ok, wait for messages.
275
+ return;
276
+ }
277
+
278
+ let text: string = '';
279
+ try {
280
+ text = await response.text();
281
+ } catch (e: any) {
282
+ // Ignore error here.
283
+ }
284
+
285
+ let errObj;
286
+ try {
287
+ errObj = JSON.parse(text);
288
+ } catch (e: any) {
289
+ if (!(e instanceof SyntaxError)) {
290
+ throw e;
291
+ }
292
+ }
293
+
294
+ if (typeof errObj !== 'object') {
295
+ errObj = {
296
+ textStatus: response.statusText,
297
+ };
298
+ }
299
+ errObj.status = response.status;
300
+ throw response.status < 200
301
+ ? new InformationalError(response, errObj)
302
+ : response.status < 300
303
+ ? new SuccessError(response, errObj)
304
+ : response.status < 400
305
+ ? new RedirectError(response, errObj)
306
+ : response.status < 500
307
+ ? new ClientError(response, errObj)
308
+ : new ServerError(response, errObj);
309
+ },
310
+
311
+ onmessage(event) {
312
+ if (event.event === 'next') {
313
+ let text = event.data;
314
+
315
+ if (opt.dataType === 'json') {
316
+ if (!text.length) {
317
+ responses.push(
318
+ new InvalidResponseError('Server response was empty.'),
319
+ );
320
+ } else {
321
+ try {
322
+ responses.push(JSON.parse(text));
323
+ } catch (e: any) {
324
+ if (!(e instanceof SyntaxError)) {
325
+ responses.push(e);
326
+ } else {
327
+ responses.push(
328
+ new InvalidResponseError(
329
+ 'Server response was invalid: ' + JSON.stringify(text),
330
+ ),
331
+ );
332
+ }
333
+ }
334
+ }
335
+ } else {
336
+ responses.push(text);
337
+ }
338
+ } else if (event.event === 'error') {
339
+ let text = event.data;
340
+
341
+ let errObj;
342
+ try {
343
+ errObj = JSON.parse(text);
344
+ } catch (e: any) {
345
+ if (!(e instanceof SyntaxError)) {
346
+ throw e;
347
+ }
348
+ }
349
+
350
+ if (typeof errObj !== 'object') {
351
+ errObj = {
352
+ status: 500,
353
+ textStatus: 'Iterator Error',
354
+ };
355
+ }
356
+ responses.push(
357
+ errObj.status < 200
358
+ ? new InformationalError(serverResponse, errObj)
359
+ : errObj.status < 300
360
+ ? new SuccessError(serverResponse, errObj)
361
+ : errObj.status < 400
362
+ ? new RedirectError(serverResponse, errObj)
363
+ : errObj.status < 500
364
+ ? new ClientError(serverResponse, errObj)
365
+ : new ServerError(serverResponse, errObj),
366
+ );
367
+ } else if (event.event === 'finished') {
368
+ responsesDone = true;
369
+ } else if (event.event === 'ping') {
370
+ // Ignore keep-alive pings.
371
+ return;
372
+ }
373
+
374
+ const resolve = nextResponseResolve;
375
+ if (!responsesDone) {
376
+ nextResponseReadyPromise = new Promise<void>((res) => {
377
+ nextResponseResolve = res;
378
+ });
379
+ }
380
+
381
+ // Resolve the promise to continue any waiting iterator.
382
+ resolve();
383
+ },
384
+
385
+ onclose() {
386
+ responses.push(
387
+ new ConnectionClosedUnexpectedlyError(
388
+ 'The connection to the server was closed unexpectedly.',
389
+ ),
390
+ );
391
+
392
+ responsesDone = true;
393
+ nextResponseResolve();
394
+ },
395
+
396
+ onerror(err) {
397
+ // Rethrow to stop the operation.
398
+ throw err;
399
+ },
400
+ }).catch((err) => {
401
+ responses.push(
402
+ new ConnectionError('The connection could not be established: ' + err),
403
+ );
404
+
405
+ responsesDone = true;
406
+ nextResponseResolve();
407
+ });
408
+
409
+ const iterator: AbortableAsyncIterator = {
410
+ abortController: ctrl,
411
+ async *[Symbol.asyncIterator]() {
412
+ do {
413
+ await nextResponseReadyPromise;
414
+
415
+ while (responses.length) {
416
+ yield responses.shift();
417
+ }
418
+ } while (!responsesDone);
419
+ },
420
+ };
421
+
422
+ return iterator;
423
+ }
196
424
  }
197
425
 
198
426
  export class InvalidResponseError extends Error {
@@ -202,18 +430,63 @@ export class InvalidResponseError extends Error {
202
430
  }
203
431
  }
204
432
 
205
- export class ClientError extends Error {
206
- constructor(errObj: { textStatus: string }) {
207
- super(errObj.textStatus);
208
- this.name = 'ClientError';
209
- Object.assign(this, errObj);
433
+ export class ConnectionClosedUnexpectedlyError extends Error {
434
+ constructor(message: string) {
435
+ super(message);
436
+ this.name = 'ConnectionClosedUnexpectedlyError';
437
+ }
438
+ }
439
+
440
+ export class ConnectionError extends Error {
441
+ constructor(message: string) {
442
+ super(message);
443
+ this.name = 'ConnectionError';
210
444
  }
211
445
  }
212
446
 
213
- export class ServerError extends Error {
214
- constructor(errObj: { textStatus: string }) {
447
+ export class HttpError extends Error {
448
+ status: number;
449
+ statusText: string;
450
+
451
+ constructor(
452
+ name: string,
453
+ response: Response,
454
+ errObj: { textStatus: string },
455
+ ) {
215
456
  super(errObj.textStatus);
216
- this.name = 'ServerError';
457
+ this.name = name;
458
+ this.status = response.status;
459
+ this.statusText = response.statusText;
217
460
  Object.assign(this, errObj);
218
461
  }
219
462
  }
463
+
464
+ export class InformationalError extends HttpError {
465
+ constructor(response: Response, errObj: { textStatus: string }) {
466
+ super('InformationalError', response, errObj);
467
+ }
468
+ }
469
+
470
+ export class SuccessError extends HttpError {
471
+ constructor(response: Response, errObj: { textStatus: string }) {
472
+ super('SuccessError', response, errObj);
473
+ }
474
+ }
475
+
476
+ export class RedirectError extends HttpError {
477
+ constructor(response: Response, errObj: { textStatus: string }) {
478
+ super('RedirectError', response, errObj);
479
+ }
480
+ }
481
+
482
+ export class ClientError extends HttpError {
483
+ constructor(response: Response, errObj: { textStatus: string }) {
484
+ super('ClientError', response, errObj);
485
+ }
486
+ }
487
+
488
+ export class ServerError extends HttpError {
489
+ constructor(response: Response, errObj: { textStatus: string }) {
490
+ super('ServerError', response, errObj);
491
+ }
492
+ }