@ember-data/store 4.6.1 → 4.7.0

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 (45) hide show
  1. package/addon/-debug/index.js +35 -13
  2. package/addon/-private/{identifier-cache.ts → caches/identifier-cache.ts} +148 -73
  3. package/addon/-private/caches/instance-cache.ts +690 -0
  4. package/addon/-private/{record-data-for.ts → caches/record-data-for.ts} +2 -7
  5. package/addon/-private/index.ts +44 -24
  6. package/addon/-private/{model → legacy-model-support}/record-reference.ts +15 -13
  7. package/addon/-private/{schema-definition-service.ts → legacy-model-support/schema-definition-service.ts} +13 -9
  8. package/addon/-private/{model → legacy-model-support}/shim-model-class.ts +18 -11
  9. package/addon/-private/managers/record-array-manager.ts +377 -0
  10. package/addon/-private/managers/record-data-manager.ts +845 -0
  11. package/addon/-private/managers/record-data-store-wrapper.ts +421 -0
  12. package/addon/-private/managers/record-notification-manager.ts +109 -0
  13. package/addon/-private/network/fetch-manager.ts +567 -0
  14. package/addon/-private/{finders.js → network/finders.js} +14 -17
  15. package/addon/-private/{request-cache.ts → network/request-cache.ts} +21 -18
  16. package/addon/-private/{snapshot-record-array.ts → network/snapshot-record-array.ts} +14 -31
  17. package/addon/-private/{snapshot.ts → network/snapshot.ts} +40 -49
  18. package/addon/-private/{promise-proxies.ts → proxies/promise-proxies.ts} +76 -15
  19. package/addon/-private/{promise-proxy-base.js → proxies/promise-proxy-base.js} +0 -0
  20. package/addon/-private/record-arrays/identifier-array.ts +924 -0
  21. package/addon/-private/{core-store.ts → store-service.ts} +574 -215
  22. package/addon/-private/{coerce-id.ts → utils/coerce-id.ts} +1 -1
  23. package/addon/-private/{common.js → utils/common.js} +1 -2
  24. package/addon/-private/utils/construct-resource.ts +2 -2
  25. package/addon/-private/{identifer-debug-consts.ts → utils/identifer-debug-consts.ts} +0 -0
  26. package/addon/-private/utils/is-non-empty-string.ts +1 -1
  27. package/addon/-private/{normalize-model-name.ts → utils/normalize-model-name.ts} +1 -3
  28. package/addon/-private/utils/promise-record.ts +5 -6
  29. package/addon/-private/{serializer-response.ts → utils/serializer-response.ts} +2 -2
  30. package/addon/-private/utils/uuid-polyfill.ts +73 -0
  31. package/package.json +12 -8
  32. package/addon/-private/backburner.js +0 -25
  33. package/addon/-private/errors-utils.js +0 -146
  34. package/addon/-private/fetch-manager.ts +0 -597
  35. package/addon/-private/identity-map.ts +0 -54
  36. package/addon/-private/instance-cache.ts +0 -387
  37. package/addon/-private/internal-model-factory.ts +0 -359
  38. package/addon/-private/internal-model-map.ts +0 -121
  39. package/addon/-private/model/internal-model.ts +0 -602
  40. package/addon/-private/record-array-manager.ts +0 -444
  41. package/addon/-private/record-arrays/adapter-populated-record-array.ts +0 -130
  42. package/addon/-private/record-arrays/record-array.ts +0 -318
  43. package/addon/-private/record-data-store-wrapper.ts +0 -243
  44. package/addon/-private/record-notification-manager.ts +0 -73
  45. package/addon/-private/weak-cache.ts +0 -125
@@ -1,602 +0,0 @@
1
- import { assert } from '@ember/debug';
2
- import { _backburner as emberBackburner, cancel, run } from '@ember/runloop';
3
- import { DEBUG } from '@glimmer/env';
4
-
5
- import { HAS_MODEL_PACKAGE } from '@ember-data/private-build-infra';
6
- import type { DSModel } from '@ember-data/types/q/ds-model';
7
- import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
8
- import type { ChangedAttributesHash, RecordData } from '@ember-data/types/q/record-data';
9
- import type { JsonApiResource, JsonApiValidationError } from '@ember-data/types/q/record-data-json-api';
10
- import type { RecordInstance } from '@ember-data/types/q/record-instance';
11
-
12
- import type Store from '../core-store';
13
- import { errorsHashToArray } from '../errors-utils';
14
- import { internalModelFactoryFor } from '../internal-model-factory';
15
-
16
- /**
17
- @module @ember-data/store
18
- */
19
-
20
- function isDSModel(record: RecordInstance | null): record is DSModel {
21
- return (
22
- HAS_MODEL_PACKAGE &&
23
- !!record &&
24
- 'constructor' in record &&
25
- 'isModel' in record.constructor &&
26
- record.constructor.isModel === true
27
- );
28
- }
29
-
30
- export default class InternalModel {
31
- declare _id: string | null;
32
- declare modelName: string;
33
- declare clientId: string;
34
- declare hasRecordData: boolean;
35
- declare _isDestroyed: boolean;
36
- declare isError: boolean;
37
- declare _pendingRecordArrayManagerFlush: boolean;
38
- declare _isDematerializing: boolean;
39
- declare _doNotDestroy: boolean;
40
- declare isDestroying: boolean;
41
- declare _isUpdatingId: boolean;
42
- declare _deletedRecordWasNew: boolean;
43
-
44
- // Not typed yet
45
- declare _scheduledDestroy: any;
46
- declare _modelClass: any;
47
- declare __recordArrays: any;
48
- declare error: any;
49
- declare store: Store;
50
- declare identifier: StableRecordIdentifier;
51
- declare hasRecord: boolean;
52
-
53
- constructor(store: Store, identifier: StableRecordIdentifier) {
54
- this.store = store;
55
- this.identifier = identifier;
56
- this._id = identifier.id;
57
- this._isUpdatingId = false;
58
- this.modelName = identifier.type;
59
- this.clientId = identifier.lid;
60
- this.hasRecord = false;
61
-
62
- this.hasRecordData = false;
63
-
64
- this._isDestroyed = false;
65
- this._doNotDestroy = false;
66
- this.isError = false;
67
- this._pendingRecordArrayManagerFlush = false; // used by the recordArrayManager
68
-
69
- // During dematerialization we don't want to rematerialize the record. The
70
- // reason this might happen is that dematerialization removes records from
71
- // record arrays, and Ember arrays will always `objectAt(0)` and
72
- // `objectAt(len - 1)` to test whether or not `firstObject` or `lastObject`
73
- // have changed.
74
- this._isDematerializing = false;
75
- this._scheduledDestroy = null;
76
-
77
- this.error = null;
78
-
79
- // caches for lazy getters
80
- this._modelClass = null;
81
- this.__recordArrays = null;
82
-
83
- this.error = null;
84
- }
85
-
86
- get id(): string | null {
87
- return this.identifier.id;
88
- }
89
- set id(value: string | null) {
90
- if (value !== this._id) {
91
- let newIdentifier = { type: this.identifier.type, lid: this.identifier.lid, id: value };
92
- // TODO potentially this needs to handle merged result
93
- this.store.identifierCache.updateRecordIdentifier(this.identifier, newIdentifier);
94
- this.notifyPropertyChange('id');
95
- }
96
- }
97
-
98
- get modelClass() {
99
- if (this.store.modelFor) {
100
- return this._modelClass || (this._modelClass = this.store.modelFor(this.modelName));
101
- }
102
- }
103
-
104
- get _recordData(): RecordData {
105
- return this.store._instanceCache.getRecordData(this.identifier);
106
- }
107
-
108
- isHiddenFromRecordArrays() {
109
- // During dematerialization we don't want to rematerialize the record.
110
- // recordWasDeleted can cause other records to rematerialize because it
111
- // removes the internal model from the array and Ember arrays will always
112
- // `objectAt(0)` and `objectAt(len -1)` to check whether `firstObject` or
113
- // `lastObject` have changed. When this happens we don't want those
114
- // models to rematerialize their records.
115
-
116
- // eager checks to avoid instantiating record data if we are empty or loading
117
- if (this.isEmpty) {
118
- return true;
119
- }
120
-
121
- if (this.isLoading) {
122
- return false;
123
- }
124
-
125
- let isRecordFullyDeleted = this._isRecordFullyDeleted();
126
- return this._isDematerializing || this.hasScheduledDestroy() || this.isDestroyed || isRecordFullyDeleted;
127
- }
128
-
129
- _isRecordFullyDeleted(): boolean {
130
- if (this._recordData.isDeletionCommitted && this._recordData.isDeletionCommitted()) {
131
- return true;
132
- } else if (
133
- this._recordData.isNew &&
134
- this._recordData.isDeleted &&
135
- this._recordData.isNew() &&
136
- this._recordData.isDeleted()
137
- ) {
138
- return true;
139
- } else {
140
- return false;
141
- }
142
- }
143
-
144
- isDeleted(): boolean {
145
- if (this._recordData.isDeleted) {
146
- return this._recordData.isDeleted();
147
- } else {
148
- return false;
149
- }
150
- }
151
-
152
- isNew(): boolean {
153
- if (this.hasRecordData && this._recordData.isNew) {
154
- return this._recordData.isNew();
155
- } else {
156
- return false;
157
- }
158
- }
159
-
160
- get isEmpty(): boolean {
161
- return !this.hasRecordData || ((!this.isNew() || this.isDeleted()) && this._recordData.isEmpty?.()) || false;
162
- }
163
-
164
- get isLoading() {
165
- const req = this.store.getRequestStateService();
166
- const { identifier } = this;
167
- // const fulfilled = req.getLastRequestForRecord(identifier);
168
-
169
- return (
170
- !this.isLoaded &&
171
- // fulfilled === null &&
172
- req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query')
173
- );
174
- }
175
-
176
- get isLoaded() {
177
- // if we are new we must consider ourselves loaded
178
- if (this.isNew()) {
179
- return true;
180
- }
181
- // even if we have a past request, if we are now empty we are not loaded
182
- // typically this is true after an unloadRecord call
183
-
184
- // if we are not empty, not new && we have a fulfilled request then we are loaded
185
- // we should consider allowing for something to be loaded that is simply "not empty".
186
- // which is how RecordState currently handles this case; however, RecordState is buggy
187
- // in that it does not account for unloading.
188
- return !this.isEmpty;
189
- }
190
-
191
- dematerializeRecord() {
192
- this._isDematerializing = true;
193
-
194
- // TODO IGOR add a test that fails when this is missing, something that involves canceling a destroy
195
- // and the destroy not happening, and then later on trying to destroy
196
- this._doNotDestroy = false;
197
- // this has to occur before the internal model is removed
198
- // for legacy compat.
199
- const { identifier } = this;
200
- this.store._instanceCache.removeRecord(identifier);
201
-
202
- // move to an empty never-loaded state
203
- // ensure any record notifications happen prior to us
204
- // unseting the record but after we've triggered
205
- // destroy
206
- this.store._backburner.join(() => {
207
- this._recordData.unloadRecord();
208
- });
209
-
210
- this.hasRecord = false; // this must occur after relationship removal
211
- this.error = null;
212
- this.store.recordArrayManager.recordDidChange(this.identifier);
213
- }
214
-
215
- deleteRecord() {
216
- run(() => {
217
- const backburner = this.store._backburner;
218
- backburner.run(() => {
219
- if (this._recordData.setIsDeleted) {
220
- this._recordData.setIsDeleted(true);
221
- }
222
-
223
- if (this.isNew()) {
224
- // destroyRecord follows up deleteRecord with save(). This prevents an unecessary save for a new record
225
- this._deletedRecordWasNew = true;
226
- this.unloadRecord();
227
- }
228
- });
229
- });
230
- }
231
-
232
- /*
233
- Unload the record for this internal model. This will cause the record to be
234
- destroyed and freed up for garbage collection. It will also do a check
235
- for cleaning up internal models.
236
-
237
- This check is performed by first computing the set of related internal
238
- models. If all records in this set are unloaded, then the entire set is
239
- destroyed. Otherwise, nothing in the set is destroyed.
240
-
241
- This means that this internal model will be freed up for garbage collection
242
- once all models that refer to it via some relationship are also unloaded.
243
- */
244
- unloadRecord() {
245
- if (this.isDestroyed) {
246
- return;
247
- }
248
- if (DEBUG) {
249
- const requests = this.store.getRequestStateService().getPendingRequestsForRecord(this.identifier);
250
- if (
251
- requests.some((req) => {
252
- return req.type === 'mutation';
253
- })
254
- ) {
255
- assert('You can only unload a record which is not inFlight. `' + this + '`');
256
- }
257
- }
258
- this.dematerializeRecord();
259
- if (this._scheduledDestroy === null) {
260
- this._scheduledDestroy = emberBackburner.schedule('destroy', this, '_checkForOrphanedInternalModels');
261
- }
262
- }
263
-
264
- hasScheduledDestroy() {
265
- return !!this._scheduledDestroy;
266
- }
267
-
268
- cancelDestroy() {
269
- assert(
270
- `You cannot cancel the destruction of an InternalModel once it has already been destroyed`,
271
- !this.isDestroyed
272
- );
273
-
274
- this._doNotDestroy = true;
275
- this._isDematerializing = false;
276
- cancel(this._scheduledDestroy);
277
- this._scheduledDestroy = null;
278
- }
279
-
280
- // typically, we prefer to async destroy this lets us batch cleanup work.
281
- // Unfortunately, some scenarios where that is not possible. Such as:
282
- //
283
- // ```js
284
- // const record = store.findRecord(‘record’, 1);
285
- // record.unloadRecord();
286
- // store.createRecord(‘record’, 1);
287
- // ```
288
- //
289
- // In those scenarios, we make that model's cleanup work, sync.
290
- //
291
- destroySync() {
292
- if (this._isDematerializing) {
293
- this.cancelDestroy();
294
- }
295
- this._checkForOrphanedInternalModels();
296
- if (this.isDestroyed || this.isDestroying) {
297
- return;
298
- }
299
-
300
- // just in-case we are not one of the orphaned, we should still
301
- // still destroy ourselves
302
- this.destroy();
303
- }
304
-
305
- _checkForOrphanedInternalModels() {
306
- this._isDematerializing = false;
307
- this._scheduledDestroy = null;
308
- if (this.isDestroyed) {
309
- return;
310
- }
311
- }
312
-
313
- destroyFromRecordData() {
314
- if (this._doNotDestroy) {
315
- this._doNotDestroy = false;
316
- return;
317
- }
318
- this.destroy();
319
- }
320
-
321
- destroy() {
322
- let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' });
323
- assert(
324
- 'Cannot destroy an internalModel while its record is materialized',
325
- !record || record.isDestroyed || record.isDestroying
326
- );
327
- this.isDestroying = true;
328
-
329
- internalModelFactoryFor(this.store).remove(this);
330
- this._isDestroyed = true;
331
- }
332
-
333
- setupData(data) {
334
- if (this.isNew()) {
335
- this.store._notificationManager.notify(this.identifier, 'identity');
336
- }
337
- this._recordData.pushData(data, this.hasRecord);
338
- }
339
-
340
- notifyAttributes(keys: string[]): void {
341
- if (this.hasRecord) {
342
- let manager = this.store._notificationManager;
343
- let { identifier } = this;
344
-
345
- if (!keys || !keys.length) {
346
- manager.notify(identifier, 'attributes');
347
- } else {
348
- for (let i = 0; i < keys.length; i++) {
349
- manager.notify(identifier, 'attributes', keys[i]);
350
- }
351
- }
352
- }
353
- }
354
-
355
- get isDestroyed(): boolean {
356
- return this._isDestroyed;
357
- }
358
-
359
- hasChangedAttributes(): boolean {
360
- if (!this.hasRecordData) {
361
- // no need to calculate changed attributes when calling `findRecord`
362
- return false;
363
- }
364
- return this._recordData.hasChangedAttributes();
365
- }
366
-
367
- changedAttributes(): ChangedAttributesHash {
368
- if (!this.hasRecordData) {
369
- // no need to calculate changed attributes when calling `findRecord`
370
- return {};
371
- }
372
- return this._recordData.changedAttributes();
373
- }
374
-
375
- adapterWillCommit(): void {
376
- this._recordData.willCommit();
377
- let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' });
378
- if (record && isDSModel(record)) {
379
- record.errors.clear();
380
- }
381
- }
382
-
383
- notifyHasManyChange(key: string) {
384
- if (this.hasRecord) {
385
- this.store._notificationManager.notify(this.identifier, 'relationships', key);
386
- }
387
- }
388
-
389
- notifyBelongsToChange(key: string) {
390
- if (this.hasRecord) {
391
- this.store._notificationManager.notify(this.identifier, 'relationships', key);
392
- }
393
- }
394
-
395
- notifyPropertyChange(key: string) {
396
- if (this.hasRecord) {
397
- // TODO this should likely *mostly* be the `attributes` bucket
398
- // but it seems for local mutations we rely on computed updating
399
- // iteself when set. As we design our own thing we may need to change
400
- // that.
401
- this.store._notificationManager.notify(this.identifier, 'property', key);
402
- }
403
- }
404
-
405
- notifyStateChange(key?: string) {
406
- if (this.hasRecord) {
407
- this.store._notificationManager.notify(this.identifier, 'state');
408
- }
409
- if (!key || key === 'isDeletionCommitted') {
410
- this.store.recordArrayManager.recordDidChange(this.identifier);
411
- }
412
- }
413
-
414
- rollbackAttributes() {
415
- this.store._backburner.join(() => {
416
- let dirtyKeys = this._recordData.rollbackAttributes();
417
-
418
- let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' });
419
- if (record && isDSModel(record)) {
420
- record.errors.clear();
421
- }
422
-
423
- if (this.hasRecord && dirtyKeys && dirtyKeys.length > 0) {
424
- this.notifyAttributes(dirtyKeys);
425
- }
426
- });
427
- }
428
-
429
- removeFromInverseRelationships() {
430
- if (this.hasRecordData) {
431
- this.store._backburner.join(() => {
432
- this._recordData.removeFromInverseRelationships();
433
- });
434
- }
435
- }
436
-
437
- /*
438
- When a find request is triggered on the store, the user can optionally pass in
439
- attributes and relationships to be preloaded. These are meant to behave as if they
440
- came back from the server, except the user obtained them out of band and is informing
441
- the store of their existence. The most common use case is for supporting client side
442
- nested URLs, such as `/posts/1/comments/2` so the user can do
443
- `store.findRecord('comment', 2, { preload: { post: 1 } })` without having to fetch the post.
444
-
445
- Preloaded data can be attributes and relationships passed in either as IDs or as actual
446
- models.
447
- */
448
- preloadData(preload) {
449
- let jsonPayload: JsonApiResource = {};
450
- //TODO(Igor) consider the polymorphic case
451
- Object.keys(preload).forEach((key) => {
452
- let preloadValue = preload[key];
453
- let relationshipMeta = this.modelClass.metaForProperty(key);
454
- if (relationshipMeta.isRelationship) {
455
- if (!jsonPayload.relationships) {
456
- jsonPayload.relationships = {};
457
- }
458
- jsonPayload.relationships[key] = this._preloadRelationship(key, preloadValue);
459
- } else {
460
- if (!jsonPayload.attributes) {
461
- jsonPayload.attributes = {};
462
- }
463
- jsonPayload.attributes[key] = preloadValue;
464
- }
465
- });
466
- this._recordData.pushData(jsonPayload);
467
- }
468
-
469
- _preloadRelationship(key, preloadValue) {
470
- let relationshipMeta = this.modelClass.metaForProperty(key);
471
- let modelClass = relationshipMeta.type;
472
- let data;
473
- if (relationshipMeta.kind === 'hasMany') {
474
- assert('You need to pass in an array to set a hasMany property on a record', Array.isArray(preloadValue));
475
- data = preloadValue.map((value) => this._convertPreloadRelationshipToJSON(value, modelClass));
476
- } else {
477
- data = this._convertPreloadRelationshipToJSON(preloadValue, modelClass);
478
- }
479
- return { data };
480
- }
481
-
482
- _convertPreloadRelationshipToJSON(value, modelClass) {
483
- if (typeof value === 'string' || typeof value === 'number') {
484
- return { type: modelClass, id: value };
485
- }
486
- let internalModel;
487
- if (value._internalModel) {
488
- internalModel = value._internalModel;
489
- } else {
490
- internalModel = value;
491
- }
492
- // TODO IGOR DAVID assert if no id is present
493
- return { type: internalModel.modelName, id: internalModel.id };
494
- }
495
-
496
- /*
497
- * calling `InstanceCache.setRecordId` is necessary to update
498
- * the cache index for this record if we have changed.
499
- *
500
- * However, since the store is not aware of whether the update
501
- * is from us (via user set) or from a push of new data
502
- * it will also call us so that we can notify and update state.
503
- *
504
- * When it does so it calls with `fromCache` so that we can
505
- * short-circuit instead of cycling back.
506
- *
507
- * This differs from the short-circuit in the `_isUpdatingId`
508
- * case in that the the cache can originate the call to setId,
509
- * so on first entry we will still need to do our own update.
510
- */
511
- setId(id: string | null, fromCache: boolean = false) {
512
- if (this._isUpdatingId === true) {
513
- return;
514
- }
515
- this._isUpdatingId = true;
516
- let didChange = id !== this._id;
517
- this._id = id;
518
-
519
- if (didChange && id !== null) {
520
- if (!fromCache) {
521
- this.store._instanceCache.setRecordId(this.modelName, id, this.clientId);
522
- }
523
- // internal set of ID to get it to RecordData from DS.Model
524
- // if we are within create we may not have a recordData yet.
525
- if (this.hasRecordData && this._recordData.__setId) {
526
- this._recordData.__setId(id);
527
- }
528
- }
529
-
530
- if (didChange && this.hasRecord) {
531
- this.store._notificationManager.notify(this.identifier, 'identity');
532
- }
533
- this._isUpdatingId = false;
534
- }
535
-
536
- didError() {}
537
-
538
- /*
539
- If the adapter did not return a hash in response to a commit,
540
- merge the changed attributes and relationships into the existing
541
- saved data.
542
- */
543
- adapterDidCommit(data) {
544
- this._recordData.didCommit(data);
545
- this.store.recordArrayManager.recordDidChange(this.identifier);
546
- }
547
-
548
- hasErrors(): boolean {
549
- // TODO add assertion forcing consuming RecordData's to implement getErrors
550
- if (this._recordData.getErrors) {
551
- return this._recordData.getErrors(this.identifier).length > 0;
552
- } else {
553
- let record = this.store._instanceCache.peek({ identifier: this.identifier, bucket: 'record' });
554
- // we can't have errors if we never tried loading
555
- if (!record) {
556
- return false;
557
- }
558
- let errors = (record as DSModel).errors;
559
- return errors.length > 0;
560
- }
561
- }
562
-
563
- // FOR USE DURING COMMIT PROCESS
564
- adapterDidInvalidate(parsedErrors, error?) {
565
- // TODO @runspired this should be handled by RecordState
566
- // and errors should be dirtied but lazily fetch if at
567
- // all possible. We should only notify errors here.
568
- let attribute;
569
- if (error && parsedErrors) {
570
- // TODO add assertion forcing consuming RecordData's to implement getErrors
571
- if (!this._recordData.getErrors) {
572
- let record = this.store._instanceCache.getRecord(this.identifier) as DSModel;
573
- let errors = record.errors;
574
- for (attribute in parsedErrors) {
575
- if (Object.prototype.hasOwnProperty.call(parsedErrors, attribute)) {
576
- errors.add(attribute, parsedErrors[attribute]);
577
- }
578
- }
579
- }
580
-
581
- let jsonApiErrors: JsonApiValidationError[] = errorsHashToArray(parsedErrors);
582
- if (jsonApiErrors.length === 0) {
583
- jsonApiErrors = [{ title: 'Invalid Error', detail: '', source: { pointer: '/data' } }];
584
- }
585
- this._recordData.commitWasRejected(this.identifier, jsonApiErrors);
586
- } else {
587
- this._recordData.commitWasRejected(this.identifier);
588
- }
589
- }
590
-
591
- notifyErrorsChange() {
592
- this.store._notificationManager.notify(this.identifier, 'errors');
593
- }
594
-
595
- adapterDidError() {
596
- this._recordData.commitWasRejected();
597
- }
598
-
599
- toString() {
600
- return `<${this.modelName}:${this.id}>`;
601
- }
602
- }