@nymphjs/client 1.0.0-beta.13 → 1.0.0-beta.131

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +552 -0
  2. package/README.md +12 -8
  3. package/asyncitertest.js +53 -0
  4. package/dist/Entity.d.ts +158 -0
  5. package/dist/Entity.js +643 -0
  6. package/dist/Entity.js.map +1 -0
  7. package/dist/Entity.types.d.ts +216 -0
  8. package/dist/Entity.types.js +2 -0
  9. package/{lib → dist}/EntityWeakCache.d.ts +1 -1
  10. package/{lib → dist}/EntityWeakCache.js +5 -9
  11. package/dist/EntityWeakCache.js.map +1 -0
  12. package/{lib → dist}/HttpRequester.d.ts +18 -4
  13. package/dist/HttpRequester.js +365 -0
  14. package/dist/HttpRequester.js.map +1 -0
  15. package/{lib → dist}/Nymph.d.ts +48 -11
  16. package/{lib → dist}/Nymph.js +172 -56
  17. package/dist/Nymph.js.map +1 -0
  18. package/dist/Nymph.types.d.ts +227 -0
  19. package/dist/Nymph.types.js +45 -0
  20. package/dist/Nymph.types.js.map +1 -0
  21. package/{lib → dist}/PubSub.d.ts +16 -10
  22. package/{lib → dist}/PubSub.js +207 -135
  23. package/dist/PubSub.js.map +1 -0
  24. package/{lib → dist}/PubSub.types.d.ts +19 -6
  25. package/dist/PubSub.types.js +2 -0
  26. package/{lib → dist}/entityRefresh.d.ts +1 -1
  27. package/{lib → dist}/entityRefresh.js +21 -13
  28. package/dist/entityRefresh.js.map +1 -0
  29. package/dist/index.d.ts +13 -0
  30. package/dist/index.js +13 -2
  31. package/dist/index.js.map +1 -1
  32. package/{lib → dist}/utils.d.ts +2 -1
  33. package/dist/utils.js +145 -0
  34. package/dist/utils.js.map +1 -0
  35. package/jest.config.js +31 -5
  36. package/package.json +36 -32
  37. package/src/Entity.ts +361 -247
  38. package/src/Entity.types.ts +37 -57
  39. package/src/EntityWeakCache.ts +8 -6
  40. package/src/HttpRequester.ts +268 -31
  41. package/src/Nymph.ts +203 -96
  42. package/src/Nymph.types.ts +98 -2
  43. package/src/PubSub.ts +264 -181
  44. package/src/PubSub.types.ts +33 -19
  45. package/src/entityRefresh.ts +6 -6
  46. package/src/index.ts +10 -10
  47. package/src/utils.ts +67 -14
  48. package/tsconfig.json +7 -4
  49. package/typedoc.json +4 -0
  50. package/dist/index.js.LICENSE.txt +0 -8
  51. package/lib/Entity.d.ts +0 -51
  52. package/lib/Entity.js +0 -500
  53. package/lib/Entity.js.map +0 -1
  54. package/lib/Entity.types.d.ts +0 -65
  55. package/lib/Entity.types.js +0 -3
  56. package/lib/EntityWeakCache.js.map +0 -1
  57. package/lib/HttpRequester.js +0 -190
  58. package/lib/HttpRequester.js.map +0 -1
  59. package/lib/Nymph.js.map +0 -1
  60. package/lib/Nymph.types.d.ts +0 -98
  61. package/lib/Nymph.types.js +0 -3
  62. package/lib/Nymph.types.js.map +0 -1
  63. package/lib/PubSub.js.map +0 -1
  64. package/lib/PubSub.types.js +0 -3
  65. package/lib/entityRefresh.js.map +0 -1
  66. package/lib/index.d.ts +0 -13
  67. package/lib/index.js +0 -34
  68. package/lib/index.js.map +0 -1
  69. package/lib/utils.js +0 -86
  70. package/lib/utils.js.map +0 -1
  71. package/webpack.config.js +0 -28
  72. /package/{lib → dist}/Entity.types.js.map +0 -0
  73. /package/{lib → dist}/PubSub.types.js.map +0 -0
package/src/Entity.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { difference, isEqual } from 'lodash';
1
+ import { difference, isEqual } from 'lodash-es';
2
2
 
3
- import type Nymph from './Nymph';
3
+ import type Nymph from './Nymph.js';
4
4
  import {
5
5
  EntityConstructor,
6
6
  EntityData,
@@ -8,21 +8,25 @@ import {
8
8
  EntityJson,
9
9
  EntityPatch,
10
10
  EntityReference,
11
- } from './Entity.types';
11
+ } from './Entity.types.js';
12
12
  import {
13
13
  uniqueStrings,
14
14
  entitiesToReferences,
15
15
  referencesToEntities,
16
16
  sortObj,
17
- } from './utils';
17
+ entityConstructorsToClassNames,
18
+ classNamesToEntityConstructors,
19
+ } from './utils.js';
18
20
 
19
- const sleepErr =
20
- 'This entity is in a sleeping reference state. ' +
21
- 'You must use .$ready() to wake it.';
21
+ export type EntityDataType<T> =
22
+ T extends Entity<infer DataType> ? DataType : never;
22
23
 
23
- export default class Entity<T extends EntityData = EntityData>
24
- implements EntityInterface
25
- {
24
+ export type EntityInstanceType<T extends EntityConstructor> =
25
+ T extends new () => infer E ? E & EntityDataType<E> : never;
26
+
27
+ export default class Entity<
28
+ T extends EntityData = EntityData,
29
+ > implements EntityInterface {
26
30
  /**
27
31
  * The instance of Nymph to use for queries.
28
32
  */
@@ -84,29 +88,26 @@ export default class Entity<T extends EntityData = EntityData>
84
88
  */
85
89
  protected $sleepingReference: EntityReference | null = null;
86
90
  /**
87
- * A promise that resolved when the entity's data is ready.
91
+ * A promise that resolved when the entity's data is wake.
88
92
  */
89
- protected $readyPromise: Promise<Entity<T>> | null = null;
93
+ protected $wakePromise: Promise<Entity<T>> | null = null;
90
94
 
91
95
  /**
92
- * Load an entity.
93
- * @param guid The ID of the entity to load, undefined for a new entity.
96
+ * Initialize an entity.
94
97
  */
95
- public constructor(guid?: string) {
98
+ public constructor(..._rest: any[]) {
96
99
  this.$nymph = (this.constructor as EntityConstructor).nymph;
97
100
  this.$dataHandler = {
98
101
  has: (data: EntityData, name: string) => {
99
- if (typeof name !== 'symbol' && this.$isASleepingReference) {
100
- console.error(`Tried to check data on a sleeping reference: ${name}`);
101
- return false;
102
+ if (typeof name !== 'symbol') {
103
+ this.$check();
102
104
  }
103
105
  return name in data;
104
106
  },
105
107
 
106
108
  get: (data: EntityData, name: string) => {
107
- if (typeof name !== 'symbol' && this.$isASleepingReference) {
108
- console.error(`Tried to get data on a sleeping reference: ${name}`);
109
- return undefined;
109
+ if (typeof name !== 'symbol') {
110
+ this.$check();
110
111
  }
111
112
  if (data.hasOwnProperty(name)) {
112
113
  return data[name];
@@ -115,9 +116,8 @@ export default class Entity<T extends EntityData = EntityData>
115
116
  },
116
117
 
117
118
  set: (data: EntityData, name: string, value: any) => {
118
- if (typeof name !== 'symbol' && this.$isASleepingReference) {
119
- console.error(`Tried to set data on a sleeping reference: ${name}`);
120
- return false;
119
+ if (typeof name !== 'symbol') {
120
+ this.$check();
121
121
  }
122
122
  if (typeof name !== 'symbol') {
123
123
  this.$dirty[name] = true;
@@ -127,11 +127,8 @@ export default class Entity<T extends EntityData = EntityData>
127
127
  },
128
128
 
129
129
  deleteProperty: (data: EntityData, name: string) => {
130
- if (typeof name !== 'symbol' && this.$isASleepingReference) {
131
- console.error(
132
- `Tried to delete data on a sleeping reference: ${name}`
133
- );
134
- return false;
130
+ if (typeof name !== 'symbol') {
131
+ this.$check();
135
132
  }
136
133
  if (data.hasOwnProperty(name)) {
137
134
  this.$dirty[name] = true;
@@ -143,13 +140,10 @@ export default class Entity<T extends EntityData = EntityData>
143
140
  defineProperty: (
144
141
  data: EntityData,
145
142
  name: string,
146
- descriptor: PropertyDescriptor
143
+ descriptor: PropertyDescriptor,
147
144
  ) => {
148
- if (typeof name !== 'symbol' && this.$isASleepingReference) {
149
- console.error(
150
- `Tried to define data on a sleeping reference: ${name}`
151
- );
152
- return false;
145
+ if (typeof name !== 'symbol') {
146
+ this.$check();
153
147
  }
154
148
  if (typeof name !== 'symbol') {
155
149
  this.$dirty[name] = true;
@@ -159,173 +153,258 @@ export default class Entity<T extends EntityData = EntityData>
159
153
  },
160
154
 
161
155
  getOwnPropertyDescriptor: (data: EntityData, name: string) => {
162
- if (typeof name !== 'symbol' && this.$isASleepingReference) {
163
- console.error(
164
- `Tried to get property descriptor on a sleeping reference: ${name}`
165
- );
166
- return undefined;
156
+ if (typeof name !== 'symbol') {
157
+ this.$check();
167
158
  }
168
159
  return Object.getOwnPropertyDescriptor(data, name);
169
160
  },
170
161
 
171
162
  ownKeys: (data: EntityData) => {
172
- if (this.$isASleepingReference) {
173
- console.error(`Tried to enumerate data on a sleeping reference.`);
174
- return undefined;
175
- }
163
+ this.$check();
176
164
  return Object.getOwnPropertyNames(data);
177
165
  },
178
166
  };
179
167
  this.$dataStore = {} as T;
180
168
  this.$data = new Proxy(this.$dataStore, this.$dataHandler);
181
169
 
182
- if (guid != null) {
183
- this.guid = guid;
184
- this.$isASleepingReference = true;
185
- this.$sleepingReference = [
186
- 'nymph_entity_reference',
187
- this.guid,
188
- (this.constructor as EntityConstructor).class,
189
- ];
190
- this.$ready();
191
- }
192
-
193
- return new Proxy(this, {
194
- has(entity: Entity, name: string) {
195
- if (
196
- typeof name !== 'string' ||
197
- name in entity ||
198
- name.substring(0, 1) === '$'
199
- ) {
200
- return name in entity;
201
- }
202
- return name in entity.$data;
203
- },
204
-
205
- get(entity: Entity, name: string) {
206
- if (
207
- typeof name !== 'string' ||
208
- name in entity ||
209
- name.substring(0, 1) === '$'
210
- ) {
211
- return (entity as EntityInterface)[name];
212
- }
213
- if (name in entity.$data) {
214
- return entity.$data[name];
215
- }
216
- return undefined;
217
- },
218
-
219
- set(entity: Entity, name: string, value: any) {
220
- if (
221
- typeof name !== 'string' ||
222
- name in entity ||
223
- name.substring(0, 1) === '$'
224
- ) {
225
- (entity as EntityInterface)[name] = value;
226
- } else {
227
- entity.$data[name] = value;
228
- }
229
- return true;
230
- },
231
-
232
- deleteProperty(entity: Entity, name: string) {
233
- if (name in entity) {
234
- return delete (entity as EntityInterface)[name];
235
- } else if (name in entity.$data) {
236
- return delete entity.$data[name];
237
- }
238
- return true;
239
- },
170
+ return this.$proxy();
171
+ }
240
172
 
241
- getPrototypeOf(entity: Entity) {
242
- return entity.constructor.prototype;
243
- },
173
+ private $existingProxy?: this & T;
174
+ protected $proxy() {
175
+ if (this.$existingProxy == null) {
176
+ this.$existingProxy = new Proxy(this, {
177
+ has: (entity: this, name: string) => {
178
+ if (
179
+ typeof name !== 'string' ||
180
+ name in entity ||
181
+ name.substring(0, 1) === '$'
182
+ ) {
183
+ return name in entity;
184
+ }
244
185
 
245
- defineProperty(
246
- entity: Entity,
247
- name: string,
248
- descriptor: PropertyDescriptor
249
- ) {
250
- if (
251
- typeof name !== 'string' ||
252
- name in entity ||
253
- name.substring(0, 1) === '$'
254
- ) {
255
- Object.defineProperty(entity, name, descriptor);
256
- } else {
257
- Object.defineProperty(entity.$data, name, descriptor);
258
- }
259
- return true;
260
- },
186
+ this.$check();
187
+ return name in entity.$data;
188
+ },
189
+
190
+ get: (entity: this, name: string) => {
191
+ if (
192
+ typeof name !== 'string' ||
193
+ name in entity ||
194
+ name.substring(0, 1) === '$'
195
+ ) {
196
+ if (name === 'tags' || name === 'cdate' || name === 'mdate') {
197
+ this.$check();
198
+ }
199
+ return (entity as any)[name];
200
+ }
261
201
 
262
- getOwnPropertyDescriptor(entity: Entity, name: string) {
263
- if (
264
- typeof name !== 'string' ||
265
- name in entity ||
266
- name.substring(0, 1) === '$'
267
- ) {
268
- return Object.getOwnPropertyDescriptor(entity, name);
269
- } else {
270
- return Object.getOwnPropertyDescriptor(entity.$data, name);
271
- }
272
- },
202
+ this.$check();
203
+ if (name in entity.$data) {
204
+ return entity.$data[name];
205
+ }
206
+ return undefined;
207
+ },
208
+
209
+ set: (entity: this, name: string, value: any) => {
210
+ if (
211
+ typeof name !== 'string' ||
212
+ name in entity ||
213
+ name.substring(0, 1) === '$'
214
+ ) {
215
+ if (name === 'tags' || name === 'cdate' || name === 'mdate') {
216
+ this.$check();
217
+ }
218
+ (entity as any)[name] = value;
219
+ } else {
220
+ this.$check();
221
+ (entity.$data as any)[name] = value;
222
+ }
223
+ return true;
224
+ },
225
+
226
+ deleteProperty: (entity: this, name: string) => {
227
+ if (name in entity) {
228
+ return delete (entity as any)[name];
229
+ } else if (name in entity.$data) {
230
+ this.$check();
231
+ return delete entity.$data[name];
232
+ }
233
+ return true;
234
+ },
235
+
236
+ getPrototypeOf: (entity: this) => {
237
+ return entity.constructor.prototype;
238
+ },
239
+
240
+ defineProperty: (
241
+ entity: this,
242
+ name: string,
243
+ descriptor: PropertyDescriptor,
244
+ ) => {
245
+ if (
246
+ typeof name !== 'string' ||
247
+ name in entity ||
248
+ name.substring(0, 1) === '$'
249
+ ) {
250
+ if (name === 'tags' || name === 'cdate' || name === 'mdate') {
251
+ this.$check();
252
+ }
253
+ Object.defineProperty(entity, name, descriptor);
254
+ } else {
255
+ this.$check();
256
+ Object.defineProperty(entity.$data, name, descriptor);
257
+ }
258
+ return true;
259
+ },
260
+
261
+ getOwnPropertyDescriptor: (entity: this, name: string) => {
262
+ if (
263
+ typeof name !== 'string' ||
264
+ name in entity ||
265
+ name.substring(0, 1) === '$'
266
+ ) {
267
+ if (name === 'tags' || name === 'cdate' || name === 'mdate') {
268
+ this.$check();
269
+ }
270
+ return Object.getOwnPropertyDescriptor(entity, name);
271
+ } else {
272
+ this.$check();
273
+ return Object.getOwnPropertyDescriptor(entity.$data, name);
274
+ }
275
+ },
273
276
 
274
- ownKeys(entity: Entity) {
275
- return Object.getOwnPropertyNames(entity).concat(
276
- Object.getOwnPropertyNames(entity.$data)
277
- );
278
- },
279
- }) as Entity<T>;
277
+ ownKeys: (entity: this) => {
278
+ this.$check();
279
+ return Object.getOwnPropertyNames(entity).concat(
280
+ Object.getOwnPropertyNames(entity.$data),
281
+ );
282
+ },
283
+ }) as this & T;
284
+ }
285
+ return this.$existingProxy;
280
286
  }
281
287
 
282
- public static async factory(guid?: string) {
288
+ /**
289
+ * Create or retrieve a new entity instance.
290
+ *
291
+ * Note that this will always return an entity, even if the GUID is not found.
292
+ *
293
+ * @param guid An optional GUID to retrieve.
294
+ */
295
+ public static async factory<E extends Entity>(
296
+ this: {
297
+ new (): E;
298
+ },
299
+ guid?: string,
300
+ ): Promise<E & EntityDataType<E>> {
283
301
  const cacheEntity = (
284
- guid ? this.nymph.getEntityFromCache(this, guid) : null
302
+ guid
303
+ ? (this as unknown as EntityConstructor).nymph.getEntityFromCache(
304
+ this as unknown as EntityConstructor,
305
+ guid,
306
+ )
307
+ : null
285
308
  ) as Entity | null;
286
- const entity = cacheEntity || new this(guid);
309
+ if (cacheEntity) {
310
+ return cacheEntity as E & EntityDataType<E>;
311
+ }
312
+ const entity = new this();
287
313
  if (guid != null) {
288
- await entity.$ready();
314
+ entity.guid = guid;
315
+ entity.$isASleepingReference = true;
316
+ entity.$sleepingReference = [
317
+ 'nymph_entity_reference',
318
+ guid,
319
+ (this as unknown as EntityConstructor).class,
320
+ ];
321
+ await entity.$wake();
289
322
  }
290
- return entity;
323
+ return entity as E & EntityDataType<E>;
291
324
  }
292
325
 
293
- public static factorySync(guid?: string) {
294
- const cacheEntity = (
295
- guid ? this.nymph.getEntityFromCache(this, guid) : null
296
- ) as Entity | null;
297
- return cacheEntity || new this(guid);
326
+ /**
327
+ * Create a new entity instance.
328
+ */
329
+ public static factorySync<E extends Entity>(this: {
330
+ new (): E;
331
+ }): E & EntityDataType<E> {
332
+ return new this() as E & EntityDataType<E>;
298
333
  }
299
334
 
300
- public static factoryReference(reference: EntityReference) {
335
+ /**
336
+ * Create a new sleeping reference instance.
337
+ *
338
+ * Sleeping references won't retrieve their data from the server until they
339
+ * are readied with `$wake()` or a parent's `$wakeAll()`.
340
+ *
341
+ * @param reference The Nymph Entity Reference to use to wake.
342
+ * @returns The new instance.
343
+ */
344
+ public static factoryReference<E extends Entity>(
345
+ this: {
346
+ new (): E;
347
+ },
348
+ reference: EntityReference,
349
+ ): E & EntityDataType<E> {
301
350
  const cacheEntity = (
302
- reference[1] ? this.nymph.getEntityFromCache(this, reference[1]) : null
351
+ reference[1]
352
+ ? (this as unknown as EntityConstructor).nymph.getEntityFromCache(
353
+ this as unknown as EntityConstructor,
354
+ reference[1],
355
+ )
356
+ : null
303
357
  ) as Entity | null;
304
358
 
305
359
  const entity = cacheEntity || new this();
306
360
  if (!cacheEntity) {
307
361
  entity.$referenceSleep(reference);
308
362
  }
309
- return entity;
363
+ return entity as E & EntityDataType<E>;
310
364
  }
311
365
 
366
+ /**
367
+ * Call a static method on the server version of this entity.
368
+ *
369
+ * @param method The name of the method.
370
+ * @param params The parameters to call the method with.
371
+ * @returns The value that the method on the server returned.
372
+ */
312
373
  public static async serverCallStatic(method: string, params: Iterable<any>) {
313
374
  const data = await this.nymph.serverCallStatic(
314
375
  this.class,
315
376
  method,
316
377
  // Turn the params into a real array, in case an arguments object was
317
378
  // passed.
318
- Array.prototype.slice.call(params)
379
+ Array.prototype.slice.call(params),
319
380
  );
320
381
  return data.return;
321
382
  }
322
383
 
384
+ /**
385
+ * Call a static iterator method on the server version of this entity.
386
+ *
387
+ * @param method The name of the method.
388
+ * @param params The parameters to call the method with.
389
+ * @returns An iterator that iterates over values that the method on the server yields.
390
+ */
391
+ public static async serverCallStaticIterator(
392
+ method: string,
393
+ params: Iterable<any>,
394
+ ) {
395
+ return await this.nymph.serverCallStaticIterator(
396
+ this.class,
397
+ method,
398
+ // Turn the params into a real array, in case an arguments object was
399
+ // passed.
400
+ Array.prototype.slice.call(params),
401
+ );
402
+ }
403
+
323
404
  public toJSON() {
324
- if (this.$isASleepingReference) {
325
- throw new EntityIsSleepingReferenceError(sleepErr);
326
- }
405
+ this.$check();
327
406
  const obj: EntityJson = {
328
- class: (this.constructor as any).class as string,
407
+ class: entityConstructorsToClassNames(this.constructor),
329
408
  guid: this.guid,
330
409
  cdate: this.cdate,
331
410
  mdate: this.mdate,
@@ -333,14 +412,16 @@ export default class Entity<T extends EntityData = EntityData>
333
412
  data: {},
334
413
  };
335
414
  for (let [key, value] of Object.entries(this.$dataStore)) {
336
- obj.data[key] = entitiesToReferences(value);
415
+ obj.data[key] = entityConstructorsToClassNames(
416
+ entitiesToReferences(value),
417
+ );
337
418
  }
338
419
  return obj;
339
420
  }
340
421
 
341
422
  public $init(entityJson: EntityJson | null) {
342
423
  if (entityJson == null) {
343
- return this;
424
+ return this.$proxy();
344
425
  }
345
426
 
346
427
  this.$isASleepingReference = false;
@@ -355,23 +436,27 @@ export default class Entity<T extends EntityData = EntityData>
355
436
  this.$dataStore = Object.entries(entityJson.data)
356
437
  .map(([key, value]) => {
357
438
  this.$dirty[key] = false;
358
- return { key, value: referencesToEntities(value, this.$nymph) };
439
+ return {
440
+ key,
441
+ value: classNamesToEntityConstructors(
442
+ this.$nymph,
443
+ referencesToEntities(value, this.$nymph),
444
+ ),
445
+ };
359
446
  })
360
447
  .reduce(
361
448
  (obj, { key, value }) => Object.assign(obj, { [key]: value }),
362
- {}
449
+ {},
363
450
  ) as T;
364
451
  this.$data = new Proxy(this.$dataStore, this.$dataHandler);
365
452
 
366
453
  this.$nymph.setEntityToCache(this.constructor as EntityConstructor, this);
367
454
 
368
- return this as Entity<T>;
455
+ return this.$proxy();
369
456
  }
370
457
 
371
458
  public $addTag(...tags: string[]) {
372
- if (this.$isASleepingReference) {
373
- throw new EntityIsSleepingReferenceError(sleepErr);
374
- }
459
+ this.$check();
375
460
 
376
461
  if (tags.length < 1) {
377
462
  return;
@@ -380,9 +465,7 @@ export default class Entity<T extends EntityData = EntityData>
380
465
  }
381
466
 
382
467
  public $arraySearch(array: any[], strict = false) {
383
- if (this.$isASleepingReference) {
384
- throw new EntityIsSleepingReferenceError(sleepErr);
385
- }
468
+ this.$check();
386
469
 
387
470
  if (!Array.isArray(array)) {
388
471
  return -1;
@@ -396,20 +479,18 @@ export default class Entity<T extends EntityData = EntityData>
396
479
  return -1;
397
480
  }
398
481
 
399
- public async $delete(): Promise<boolean> {
400
- if (this.$isASleepingReference) {
401
- throw new EntityIsSleepingReferenceError(sleepErr);
402
- }
482
+ public async $delete(): Promise<void> {
483
+ this.$check();
403
484
 
404
485
  const guid = this.guid;
405
486
 
406
- return (await this.$nymph.deleteEntity(this)) === guid;
487
+ if ((await this.$nymph.deleteEntity(this)) !== guid) {
488
+ throw new Error('Failed to delete entity.');
489
+ }
407
490
  }
408
491
 
409
492
  public $equals(object: any) {
410
- if (this.$isASleepingReference) {
411
- throw new EntityIsSleepingReferenceError(sleepErr);
412
- }
493
+ this.$check();
413
494
 
414
495
  if (!(object instanceof Entity)) {
415
496
  return false;
@@ -435,23 +516,21 @@ export default class Entity<T extends EntityData = EntityData>
435
516
  }
436
517
 
437
518
  public $getPatch(): EntityPatch {
438
- if (this.$isASleepingReference) {
439
- throw new EntityIsSleepingReferenceError(sleepErr);
440
- }
519
+ this.$check();
441
520
  if (this.guid == null) {
442
521
  throw new InvalidStateError(
443
- "You can't make a patch from an unsaved entity."
522
+ "You can't make a patch from an unsaved entity.",
444
523
  );
445
524
  }
446
525
  const patch: EntityPatch = {
447
526
  guid: this.guid,
448
527
  mdate: this.mdate,
449
- class: (this.constructor as EntityConstructor).class,
528
+ class: entityConstructorsToClassNames(this.constructor),
450
529
  addTags: this.tags.filter(
451
- (tag) => this.$originalTags.indexOf(tag) === -1
530
+ (tag) => this.$originalTags.indexOf(tag) === -1,
452
531
  ),
453
532
  removeTags: this.$originalTags.filter(
454
- (tag) => this.tags.indexOf(tag) === -1
533
+ (tag) => this.tags.indexOf(tag) === -1,
455
534
  ),
456
535
  unset: [],
457
536
  set: {},
@@ -460,7 +539,9 @@ export default class Entity<T extends EntityData = EntityData>
460
539
  for (let [key, dirty] of Object.entries(this.$dirty)) {
461
540
  if (dirty) {
462
541
  if (key in this.$data) {
463
- patch.set[key] = entitiesToReferences(this.$data[key]);
542
+ patch.set[key] = entityConstructorsToClassNames(
543
+ entitiesToReferences(this.$data[key]),
544
+ );
464
545
  } else {
465
546
  patch.unset.push(key);
466
547
  }
@@ -471,9 +552,7 @@ export default class Entity<T extends EntityData = EntityData>
471
552
  }
472
553
 
473
554
  public $hasTag(...tags: string[]) {
474
- if (this.$isASleepingReference) {
475
- throw new EntityIsSleepingReferenceError(sleepErr);
476
- }
555
+ this.$check();
477
556
 
478
557
  if (!tags.length) {
479
558
  return false;
@@ -486,14 +565,16 @@ export default class Entity<T extends EntityData = EntityData>
486
565
  return true;
487
566
  }
488
567
 
568
+ public $isDirty(property: string) {
569
+ return property in this.$dirty ? this.$dirty[property] : null;
570
+ }
571
+
489
572
  public $inArray(array: any[], strict = false) {
490
573
  return this.$arraySearch(array, strict) !== -1;
491
574
  }
492
575
 
493
576
  public $is(object: any) {
494
- if (this.$isASleepingReference) {
495
- throw new EntityIsSleepingReferenceError(sleepErr);
496
- }
577
+ this.$check();
497
578
 
498
579
  if (!(object instanceof Entity)) {
499
580
  return false;
@@ -514,31 +595,52 @@ export default class Entity<T extends EntityData = EntityData>
514
595
  }
515
596
 
516
597
  public async $patch() {
517
- if (this.$isASleepingReference) {
518
- throw new EntityIsSleepingReferenceError(sleepErr);
519
- }
598
+ this.$check();
520
599
 
521
600
  const mdate = this.mdate;
522
601
 
523
602
  await this.$nymph.patchEntity(this);
524
- return mdate !== this.mdate;
603
+ if (mdate === this.mdate) {
604
+ throw new Error('Failed to patch entity.');
605
+ }
606
+ }
607
+
608
+ /**
609
+ * Check if this is a sleeping reference and throw an error if so.
610
+ */
611
+ protected $check() {
612
+ if (this.$asleep()) {
613
+ throw new EntityIsSleepingReferenceError(
614
+ 'This entity is in a sleeping reference state. You must use .$wake() to wake it.',
615
+ );
616
+ }
617
+ }
618
+
619
+ /**
620
+ * Check if this is a sleeping reference.
621
+ */
622
+ public $asleep() {
623
+ return this.$isASleepingReference || this.$sleepingReference != null;
525
624
  }
526
625
 
527
- public $ready() {
528
- if (!this.$isASleepingReference) {
529
- this.$readyPromise = null;
530
- return Promise.resolve(this);
626
+ /**
627
+ * Wake from a sleeping reference.
628
+ */
629
+ public $wake() {
630
+ if (!this.$asleep()) {
631
+ this.$wakePromise = null;
632
+ return Promise.resolve(this.$proxy());
531
633
  }
532
634
  if (this.$sleepingReference?.[1] == null) {
533
635
  throw new InvalidStateError(
534
- 'Tried to ready a sleeping reference with no GUID.'
636
+ 'Tried to wake a sleeping reference with no GUID.',
535
637
  );
536
638
  }
537
- if (!this.$readyPromise) {
538
- this.$readyPromise = this.$nymph
639
+ if (!this.$wakePromise) {
640
+ this.$wakePromise = this.$nymph
539
641
  .getEntityData(
540
642
  { class: this.constructor as EntityConstructor },
541
- { type: '&', guid: this.$sleepingReference[1] }
643
+ { type: '&', guid: this.$sleepingReference[1] },
542
644
  )
543
645
  .then((data) => {
544
646
  if (data == null) {
@@ -548,56 +650,69 @@ export default class Entity<T extends EntityData = EntityData>
548
650
  return this.$init(data);
549
651
  })
550
652
  .finally(() => {
551
- this.$readyPromise = null;
653
+ this.$wakePromise = null;
552
654
  });
553
655
  }
554
- return this.$readyPromise;
656
+ return this.$wakePromise;
555
657
  }
556
658
 
557
- public $readyAll(level?: number) {
659
+ public $wakeAll(level?: number) {
558
660
  return new Promise((resolve, reject) => {
559
- // Run this once this entity is ready.
560
- const readyProps = () => {
561
- let newLevel;
661
+ // Run this once this entity is awake.
662
+ const wakeProps = () => {
663
+ let newLevel: number | undefined;
562
664
  // If level is undefined, keep going forever, otherwise, stop once we've
563
665
  // gone deep enough.
564
666
  if (level !== undefined) {
565
667
  newLevel = level - 1;
566
668
  }
567
669
  if (newLevel !== undefined && newLevel < 0) {
568
- resolve(this);
670
+ resolve(this.$proxy());
569
671
  return;
570
672
  }
571
- const promises = [];
572
- // Go through data looking for entities to ready.
573
- for (let [key, value] of Object.entries(this.$data)) {
574
- if (value instanceof Entity && value.$isASleepingReference) {
575
- promises.push(value.$readyAll(newLevel));
576
- } else if (Array.isArray(value)) {
577
- for (let i = 0; i < value.length; i++) {
578
- if (
579
- value[i] instanceof Entity &&
580
- value[i].$isASleepingReference
581
- ) {
582
- promises.push(value[i].$readyAll(newLevel));
583
- }
673
+ const promises: Promise<any>[] = [];
674
+ // Go through data looking for entities to wake.
675
+ const walkData = (data: any) => {
676
+ if (data == null) {
677
+ return;
678
+ } else if (
679
+ typeof data === 'function' &&
680
+ data.prototype instanceof Entity
681
+ ) {
682
+ // Ignore this, it is an entity class.
683
+ return;
684
+ } else if (data instanceof Entity) {
685
+ // Wake it up.
686
+ if (data.$isASleepingReference) {
687
+ promises.push(data.$wakeAll(newLevel));
688
+ }
689
+ } else if (Array.isArray(data)) {
690
+ // Recurse into lower arrays.
691
+ for (let i = 0; i < data.length; i++) {
692
+ walkData(data[i]);
693
+ }
694
+ } else if (data instanceof Object) {
695
+ // Recurse into objects.
696
+ for (let [key, value] of Object.entries(data)) {
697
+ walkData(value);
584
698
  }
585
699
  }
586
- }
700
+ };
701
+ walkData(this.$data);
587
702
  if (promises.length) {
588
703
  Promise.all(promises).then(
589
- () => resolve(this),
590
- (errObj) => reject(errObj)
704
+ () => resolve(this.$proxy()),
705
+ (errObj) => reject(errObj),
591
706
  );
592
707
  } else {
593
- resolve(this);
708
+ resolve(this.$proxy());
594
709
  }
595
710
  };
596
711
 
597
- if (this.$isASleepingReference) {
598
- this.$ready().then(readyProps, (errObj) => reject(errObj));
712
+ if (this.$asleep()) {
713
+ this.$wake().then(wakeProps, (errObj) => reject(errObj));
599
714
  } else {
600
- readyProps();
715
+ wakeProps();
601
716
  }
602
717
  }) as Promise<Entity<T>>;
603
718
  }
@@ -609,8 +724,8 @@ export default class Entity<T extends EntityData = EntityData>
609
724
  }
610
725
 
611
726
  public async $refresh() {
612
- if (this.$isASleepingReference) {
613
- await this.$ready();
727
+ if (this.$asleep()) {
728
+ await this.$wake();
614
729
  return true;
615
730
  }
616
731
 
@@ -624,37 +739,36 @@ export default class Entity<T extends EntityData = EntityData>
624
739
  {
625
740
  type: '&',
626
741
  guid: this.guid,
627
- }
742
+ },
628
743
  );
744
+ if (data == null) {
745
+ throw new Error("The entity's data could not be refreshed.");
746
+ }
629
747
  this.$init(data);
630
- return this.guid == null ? 0 : true;
748
+ return true;
631
749
  }
632
750
 
633
751
  public $removeTag(...tags: string[]) {
634
- if (this.$isASleepingReference) {
635
- throw new EntityIsSleepingReferenceError(sleepErr);
636
- }
752
+ this.$check();
637
753
 
638
754
  this.tags = difference(this.tags, tags);
639
755
  }
640
756
 
641
757
  public async $save() {
642
- if (this.$isASleepingReference) {
643
- throw new EntityIsSleepingReferenceError(sleepErr);
644
- }
758
+ this.$check();
645
759
 
646
760
  await this.$nymph.saveEntity(this);
647
- return !!this.guid;
761
+ if (!this.guid) {
762
+ throw new Error('Failed to save entity.');
763
+ }
648
764
  }
649
765
 
650
766
  public async $serverCall(
651
767
  method: string,
652
768
  params: Iterable<any>,
653
- stateless = false
769
+ stateless = false,
654
770
  ) {
655
- if (this.$isASleepingReference) {
656
- throw new EntityIsSleepingReferenceError(sleepErr);
657
- }
771
+ this.$check();
658
772
  // Turn the params into a real array, in case an arguments object was
659
773
  // passed.
660
774
  const paramArray = Array.prototype.slice.call(params);
@@ -662,7 +776,7 @@ export default class Entity<T extends EntityData = EntityData>
662
776
  this,
663
777
  method,
664
778
  paramArray,
665
- stateless
779
+ stateless,
666
780
  );
667
781
  if (!stateless && data.entity) {
668
782
  this.$init(data.entity);
@@ -670,13 +784,13 @@ export default class Entity<T extends EntityData = EntityData>
670
784
  return data.return;
671
785
  }
672
786
 
673
- public $toReference(): EntityReference | EntityInterface {
674
- if (this.$isASleepingReference && this.$sleepingReference) {
787
+ public $toReference() {
788
+ if (this.$isASleepingReference && this.$sleepingReference != null) {
675
789
  return this.$sleepingReference;
676
790
  }
677
791
 
678
792
  if (this.guid == null) {
679
- return this as EntityInterface;
793
+ return this.$proxy();
680
794
  }
681
795
  return [
682
796
  'nymph_entity_reference',