@alwatr/nitrobase-reference 7.10.1 → 9.1.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.
@@ -0,0 +1,684 @@
1
+ import {delay} from '@alwatr/delay';
2
+ import {createLogger} from '@alwatr/logger';
3
+ import {getStoreId, getStorePath} from '@alwatr/nitrobase-helper';
4
+ import {
5
+ StoreFileType,
6
+ StoreFileExtension,
7
+ type StoreFileId,
8
+ type CollectionContext,
9
+ type CollectionItem,
10
+ type CollectionItemMeta,
11
+ type StoreFileMeta,
12
+ } from '@alwatr/nitrobase-types';
13
+
14
+ import {logger} from './logger.js';
15
+
16
+ __dev_mode__: logger.logFileModule?.('collection-reference');
17
+
18
+ /**
19
+ * Represents a reference to a collection of the AlwatrNitrobase.
20
+ * Provides methods to interact with the collection, such as retrieving, creating, updating, and deleting items.
21
+ *
22
+ * @template TItem - The data type of the collection items.
23
+ */
24
+ export class CollectionReference<TItem extends JsonObject = JsonObject> {
25
+ /**
26
+ * Alwatr nitrobase engine version string.
27
+ */
28
+ public static readonly version = __package_version__;
29
+
30
+ /**
31
+ * Alwatr nitrobase engine file format version number.
32
+ */
33
+ public static readonly fileFormatVersion = 3;
34
+
35
+ /**
36
+ * Creates new CollectionReference instance from stat.
37
+ *
38
+ * @param stat the collection stat.
39
+ * @param initialData the collection data.
40
+ * @param updatedCallback the callback to invoke when the collection changed.
41
+ * @template TItem The collection item data type.
42
+ * @returns A new collection reference class.
43
+ */
44
+ public static newRefFromData<TItem extends JsonObject>(
45
+ stat: StoreFileId,
46
+ updatedCallback: (from: CollectionReference<TItem>) => void,
47
+ debugDomain?: string,
48
+ ): CollectionReference<TItem> {
49
+ logger.logMethodArgs?.('col.newRefFromData', stat);
50
+
51
+ const now = Date.now();
52
+ const initialContext: CollectionContext<TItem> = {
53
+ ok: true,
54
+ meta: {
55
+ ...stat,
56
+ rev: 1,
57
+ updated: now,
58
+ created: now,
59
+ lastAutoId: 0,
60
+ type: StoreFileType.Collection,
61
+ extension: StoreFileExtension.Json,
62
+ fv: CollectionReference.fileFormatVersion,
63
+ extra: {},
64
+ },
65
+ data: {},
66
+ };
67
+
68
+ return new CollectionReference(initialContext, updatedCallback, debugDomain);
69
+ }
70
+
71
+ /**
72
+ * Creates new CollectionReference instance from CollectionContext.
73
+ *
74
+ * @param context the collection context.
75
+ * @param updatedCallback the callback to invoke when the collection changed.
76
+ * @template TItem The collection item data type.
77
+ * @returns A new collection reference class.
78
+ */
79
+ public static newRefFromContext<TItem extends JsonObject>(
80
+ context: CollectionContext<TItem>,
81
+ updatedCallback: (from: CollectionReference<TItem>) => void,
82
+ debugDomain?: string,
83
+ ): CollectionReference<TItem> {
84
+ logger.logMethodArgs?.('col.newRefFromContext', context.meta);
85
+ return new CollectionReference(context, updatedCallback, debugDomain);
86
+ }
87
+
88
+ /**
89
+ * Validates the collection context and try to migrate it to the latest version.
90
+ */
91
+ private validateContext__(): void {
92
+ this.logger__.logMethod?.('validateContext__');
93
+
94
+ if (this.context__.ok !== true) {
95
+ this.logger__.accident?.('validateContext__', 'store_not_ok');
96
+ throw new Error('store_not_ok', {cause: {context: this.context__}});
97
+ }
98
+
99
+ if (this.context__.meta === undefined) {
100
+ this.logger__.accident?.('validateContext__', 'store_meta_undefined');
101
+ throw new Error('store_meta_undefined', {cause: {context: this.context__}});
102
+ }
103
+
104
+ if (this.context__.meta.type !== StoreFileType.Collection) {
105
+ this.logger__.accident?.('validateContext__', 'collection_type_invalid', this.context__.meta);
106
+ throw new Error('collection_type_invalid', {cause: this.context__.meta});
107
+ }
108
+
109
+ if (this.context__.meta.fv !== CollectionReference.fileFormatVersion) {
110
+ this.logger__.incident?.('validateContext__', 'store_file_version_incompatible', {
111
+ old: this.context__.meta.fv,
112
+ new: CollectionReference.fileFormatVersion,
113
+ });
114
+ this.migrateContext__();
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Migrate the collection context to the latest.
120
+ */
121
+ private migrateContext__(): void {
122
+ if (this.context__.meta.fv === CollectionReference.fileFormatVersion) return;
123
+
124
+ this.logger__.logMethod?.('migrateContext__');
125
+
126
+ if (this.context__.meta.fv > CollectionReference.fileFormatVersion) {
127
+ this.logger__.accident('migrateContext__', 'store_version_incompatible', this.context__.meta);
128
+ throw new Error('store_version_incompatible', {cause: this.context__.meta});
129
+ }
130
+
131
+ if (this.context__.meta.fv === 1) {
132
+ // migrate from v1 to v2
133
+ // this.context__.meta.schemaVer = 0
134
+ this.context__.meta.fv = 2;
135
+ }
136
+
137
+ if (this.context__.meta.fv === 2) {
138
+ // migrate from v1 to v3
139
+ if (this.context__.meta.schemaVer === undefined || this.context__.meta.schemaVer === 0) {
140
+ this.context__.meta.schemaVer = 1;
141
+ }
142
+ delete (this.context__.meta as DictionaryOpt)['ver'];
143
+ this.context__.meta.extra ??= {};
144
+ this.context__.meta.fv = 3;
145
+ }
146
+
147
+ this.updated__();
148
+ }
149
+
150
+ /**
151
+ * The ID of the collection nitrobase file.
152
+ */
153
+ public readonly id: string;
154
+
155
+ /**
156
+ * The location path of the collection nitrobase file.
157
+ */
158
+ public readonly path: string;
159
+
160
+ /**
161
+ * Indicates whether the collection has unsaved changes.
162
+ */
163
+ public hasUnprocessedChanges_ = false;
164
+
165
+ /**
166
+ * Logger instance for this collection.
167
+ */
168
+ private logger__;
169
+
170
+ /**
171
+ * Collection reference have methods to get, set, update and save the Alwatr Nitrobase Collection.
172
+ * This class is dummy in saving and loading the collection from file.
173
+ * It's the responsibility of the Alwatr Nitrobase to save and load the collection.
174
+ *
175
+ * @param context__ Collection's context filled from the Alwatr Nitrobase (parent).
176
+ * @param updatedCallback__ updated callback to invoke when the collection is updated from the Alwatr Nitrobase (parent).
177
+ * @template TItem - Items data type.
178
+ * @example
179
+ * ```typescript
180
+ * const collectionRef = alwatrStore.col('blog/posts');
181
+ * ```
182
+ */
183
+ constructor(
184
+ private context__: CollectionContext<TItem>,
185
+ private updatedCallback__: (from: CollectionReference<TItem>) => void,
186
+ debugDomain?: string,
187
+ ) {
188
+ this.id = getStoreId(this.context__.meta);
189
+ this.path = getStorePath(this.context__.meta);
190
+
191
+ debugDomain ??= this.id.slice(0, 20);
192
+ this.logger__ = createLogger(`col:${debugDomain}`);
193
+
194
+ this.logger__.logMethodArgs?.('new', {id: this.id});
195
+
196
+ this.validateContext__();
197
+ }
198
+
199
+ /**
200
+ * Get nitrobase schema version
201
+ *
202
+ * @returns nitrobase schema version
203
+ */
204
+ public get schemaVer(): number {
205
+ return this.context__.meta.schemaVer ?? 1;
206
+ }
207
+
208
+ /**
209
+ * Set nitrobase schema version for migrate
210
+ */
211
+ public set schemaVer(ver: number) {
212
+ this.logger__.logMethodArgs?.('set schemaVer', {old: this.context__.meta.schemaVer, new: ver});
213
+ this.context__.meta.schemaVer = ver;
214
+ this.updated__();
215
+ }
216
+
217
+ /**
218
+ * Indicates whether the collection data is frozen and cannot be saved.
219
+ */
220
+ private _freeze = false;
221
+
222
+ /**
223
+ * Gets the freeze status of the collection data.
224
+ *
225
+ * @returns `true` if the collection data is frozen, `false` otherwise.
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * const isFrozen = collectionRef.freeze;
230
+ * console.log(isFrozen); // Output: false
231
+ * ```
232
+ */
233
+ public get freeze(): boolean {
234
+ return this._freeze;
235
+ }
236
+
237
+ /**
238
+ * Sets the freeze status of the collection data.
239
+ *
240
+ * @param value - The freeze status to set.
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * collectionRef.freeze = true;
245
+ * console.log(collectionRef.freeze); // Output: true
246
+ * ```
247
+ */
248
+ public set freeze(value: boolean) {
249
+ this.logger__.logMethodArgs?.('freeze changed', {value});
250
+ this._freeze = value;
251
+ }
252
+
253
+ /**
254
+ * Checks if an item exists in the collection.
255
+ *
256
+ * @param itemId - The ID of the item.
257
+ * @returns `true` if the item with the given ID exists in the collection, `false` otherwise.
258
+ *
259
+ * @example
260
+ * ```typescript
261
+ * const doesExist = collectionRef.hasItem('item1');
262
+ *
263
+ * if (doesExist) {
264
+ * collectionRef.addItem('item1', { key: 'value' });
265
+ * }
266
+ * ```
267
+ */
268
+ public hasItem(itemId: string | number): boolean {
269
+ const exists = Object.hasOwn(this.context__.data, itemId);
270
+ this.logger__.logMethodFull?.('hasItem', itemId, exists);
271
+ return exists;
272
+ }
273
+
274
+ /**
275
+ * Retrieves the metadata of the nitrobase file.
276
+ *
277
+ * @returns The metadata of the nitrobase file.
278
+ *
279
+ * @example
280
+ * ```typescript
281
+ * const metadata = collectionRef.getStoreMeta();
282
+ * ```
283
+ */
284
+ public getStoreMeta(): Readonly<StoreFileMeta> {
285
+ this.logger__.logMethod?.('getStoreMeta');
286
+ return this.context__.meta;
287
+ }
288
+
289
+ /**
290
+ * Retrieves an item from the collection. If the item does not exist, an error is thrown.
291
+ *
292
+ * @param itemId - The ID of the item.
293
+ * @returns The item with the given ID.
294
+ */
295
+ private item__(itemId: string | number): CollectionItem<TItem> {
296
+ const item = this.context__.data[itemId];
297
+ if (item === undefined) {
298
+ this.logger__.accident('item__', 'collection_item_not_found', {itemId});
299
+ throw new Error('collection_item_not_found', {cause: {itemId}});
300
+ }
301
+ return item;
302
+ }
303
+
304
+ /**
305
+ * Retrieves an item's metadata from the collection. If the item does not exist, an error is thrown.
306
+ *
307
+ * @param itemId - The ID of the item.
308
+ * @returns The metadata of the item with the given ID.
309
+ * @example
310
+ * ```typescript
311
+ * const itemMeta = collectionRef.getItemMeta('item1');
312
+ * ```
313
+ */
314
+ public getItemMeta(itemId: string | number): Readonly<CollectionItemMeta> {
315
+ const meta = this.item__(itemId).meta;
316
+ this.logger__.logMethodFull?.('getItemMeta', itemId, meta);
317
+ return meta;
318
+ }
319
+
320
+ /**
321
+ * Retrieves an item's data from the collection. If the item does not exist, an error is thrown.
322
+ *
323
+ * @param itemId - The ID of the item.
324
+ * @returns The data of the item with the given ID.
325
+ *
326
+ * @example
327
+ * ```typescript
328
+ * const itemData = collectionRef.getItemData('item1');
329
+ * ```
330
+ */
331
+ public getItemData(itemId: string | number): TItem {
332
+ this.logger__.logMethodArgs?.('getItemData', itemId);
333
+ return this.item__(itemId).data;
334
+ }
335
+
336
+ /**
337
+ * Direct access to an item.
338
+ * If the item does not exist, `undefined` is returned.
339
+ * **USE WITH CAUTION!**
340
+ *
341
+ * @param itemId - The ID of the item.
342
+ * @returns The data of the item with the given ID or `undefined` if the item does not exist.
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * collectionRef.getItemContext_('item1')?.data.name = 'test2';
347
+ * ```
348
+ */
349
+ public getItemContext_(itemId: string | number): CollectionItem<TItem> | undefined {
350
+ this.logger__.logMethodArgs?.('getItemContext_', itemId);
351
+ return this.context__.data[itemId];
352
+ }
353
+
354
+ /**
355
+ * Add a new item to the collection.
356
+ * If an item with the given ID already exists, an error is thrown.
357
+ *
358
+ * @param itemId - The ID of the item to create.
359
+ * @param data - The initial data of the item.
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * collectionRef.addItem('item1', { key: 'value' });
364
+ * ```
365
+ */
366
+ public addItem(itemId: string | number, data: TItem): void {
367
+ this.logger__.logMethodArgs?.('addItem', {itemId, data});
368
+ if (this.hasItem(itemId)) {
369
+ this.logger__.accident('addItem', 'collection_item_exist', {itemId});
370
+ throw new Error('collection_item_exist', {cause: {itemId}});
371
+ }
372
+
373
+ const now = Date.now();
374
+
375
+ this.context__.data[itemId] = {
376
+ meta: {
377
+ id: itemId,
378
+ // other prop calc in updateMeta__
379
+ rev: 0,
380
+ created: now,
381
+ updated: now,
382
+ },
383
+ data,
384
+ };
385
+ this.updated__(itemId);
386
+ }
387
+
388
+ /**
389
+ * Appends the given data to the collection with auto increment ID.
390
+ *
391
+ * @param data - The data to append.
392
+ * @returns The ID of the appended item.
393
+ *
394
+ * @example
395
+ * ```typescript
396
+ * const newId = collectionRef.appendItem({ key: 'value' });
397
+ * ```
398
+ */
399
+ public appendItem(data: TItem): string | number {
400
+ this.logger__.logMethodArgs?.('appendItem', data);
401
+ const id = this.nextAutoIncrementId__();
402
+ this.addItem(id, data);
403
+ return id;
404
+ }
405
+
406
+ /**
407
+ * Removes an item from the collection.
408
+ *
409
+ * @param itemId - The ID of the item to delete.
410
+ *
411
+ * @example
412
+ * ```typescript
413
+ * collectionRef.removeItem('item1');
414
+ * collectionRef.hasItem('item1'); // Output: false
415
+ * ```
416
+ */
417
+ public removeItem(itemId: string | number): void {
418
+ this.logger__.logMethodArgs?.('removeItem', itemId);
419
+ delete this.context__.data[itemId];
420
+ this.updated__();
421
+ }
422
+
423
+ /**
424
+ * Sets an item's data in the collection. Replaces the item's data with the given data.
425
+ *
426
+ * @param itemId - The ID of the item to set.
427
+ * @param data - The data to set for the item.
428
+ *
429
+ * @example
430
+ * ```typescript
431
+ * collectionRef.replaceItemData('item1', { a: 1, b: 2, c: 3 });
432
+ * ```
433
+ */
434
+ public replaceItemData(itemId: string | number, data: TItem): void {
435
+ this.logger__.logMethodArgs?.('replaceItemData', {itemId, data});
436
+ (this.item__(itemId).data as unknown) = data;
437
+ this.updated__(itemId);
438
+ }
439
+
440
+ /**
441
+ * Updates an item in the collection by merging a partial update into the item's data.
442
+ *
443
+ * @param itemId - The ID of the item to update.
444
+ * @param data - The part of data to merge into the item's data.
445
+ *
446
+ * @example
447
+ * ```typescript
448
+ * collectionRef.mergeItemData(itemId, partialUpdate);
449
+ * ```
450
+ */
451
+ public mergeItemData(itemId: string | number, data: Partial<TItem>): void {
452
+ this.logger__.logMethodArgs?.('mergeItemData', {itemId, data});
453
+ Object.assign(this.item__(itemId).data, data);
454
+ this.updated__(itemId);
455
+ }
456
+
457
+ /**
458
+ * Requests the Alwatr Nitrobase to save the collection.
459
+ * Saving may take some time in Alwatr Nitrobase due to the use of throttling.
460
+ *
461
+ * @example
462
+ * ```typescript
463
+ * collectionRef.save();
464
+ * ```
465
+ */
466
+ public save(itemId: string | number | null): void {
467
+ this.logger__.logMethod?.('save');
468
+ this.updated__(itemId, false);
469
+ }
470
+
471
+ /**
472
+ * Requests the Alwatr Nitrobase to save the collection immediately.
473
+ *
474
+ * @example
475
+ * ```typescript
476
+ * collectionRef.saveImmediate();
477
+ * ```
478
+ */
479
+ public saveImmediate(itemId: string | number | null): void {
480
+ this.logger__.logMethod?.('saveImmediate');
481
+ this.updated__(itemId, true);
482
+ }
483
+
484
+ /**
485
+ * Retrieves the IDs of all items in the collection in array.
486
+ * Impact performance if the collection is large, use `ids()` instead.
487
+ *
488
+ * @returns Array of IDs of all items in the collection.
489
+ * @example
490
+ * ```typescript
491
+ * const ids = collectionRef.keys();
492
+ * ```
493
+ */
494
+ public keys(): string[] {
495
+ this.logger__.logMethod?.('keys');
496
+ return Object.keys(this.context__.data);
497
+ }
498
+
499
+ /**
500
+ * Retrieves all items in the collection in array.
501
+ * Impact performance if the collection is large, use `items()` instead.
502
+ *
503
+ * @returns Array of all items in the collection.
504
+ * @example
505
+ * ```typescript
506
+ * const items = collectionRef.values();
507
+ * console.log('meta: %o', items[0].meta);
508
+ * console.log('data: %o', items[0].data);
509
+ * ```
510
+ */
511
+ public values(): CollectionItem<TItem>[] {
512
+ this.logger__.logMethod?.('values');
513
+ return Object.values(this.context__.data);
514
+ }
515
+
516
+ /**
517
+ * Retrieves the IDs of all items in the collection.
518
+ * Use this method instead of `keys()` if the collection is large.
519
+ * This method is a generator and can be used in `for...of` loops.
520
+ * @returns Generator of IDs of all items in the collection.
521
+ * @example
522
+ * ```typescript
523
+ * for (const id of collectionRef.ids()) {
524
+ * const doc = collectionRef.get(id);
525
+ * }
526
+ * ```
527
+ */
528
+ public *ids(): Generator<string, void, void> {
529
+ this.logger__.logMethod?.('ids');
530
+ for (const id in this.context__.data) {
531
+ yield id;
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Retrieves all items in the collection.
537
+ * Use this method instead of `values()` if the collection is large.
538
+ * This method is a generator and can be used in `for...of` loops.
539
+ * @returns Generator of all items in the collection.
540
+ * @example
541
+ * ```typescript
542
+ * for (const item of collectionRef.items()) {
543
+ * console.log(item.data);
544
+ * }
545
+ */
546
+ public *items(): Generator<CollectionItem<TItem>, void, void> {
547
+ this.logger__.logMethod?.('items');
548
+ for (const id in this.context__.data) {
549
+ yield this.context__.data[id];
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Retrieves the full context of the collection.
555
+ *
556
+ * @returns The full context of the collection.
557
+ *
558
+ * @example
559
+ * ```typescript
560
+ * const context = collectionRef.getFullContext_();
561
+ * ```
562
+ */
563
+ public getFullContext_(): Readonly<CollectionContext<TItem>> {
564
+ this.logger__.logMethod?.('getFullContext_');
565
+ return this.context__;
566
+ }
567
+
568
+ public updateDelayed_ = false;
569
+
570
+ /**
571
+ * Update the document metadata and invoke the updated callback.
572
+ * This method is throttled to prevent multiple updates in a short time.
573
+ *
574
+ * @param itemId - The ID of the item to update.
575
+ */
576
+ private async updated__(itemId: string | number | null = null, immediate = false): Promise<void> {
577
+ this.logger__.logMethodArgs?.('updated__', {id: itemId, immediate, delayed: this.updateDelayed_});
578
+
579
+ this.hasUnprocessedChanges_ = true;
580
+ if (itemId !== null) this.refreshMeta_(itemId); // meta must updated per item
581
+
582
+ if (immediate === false && this.updateDelayed_ === true) return;
583
+ // else
584
+
585
+ this.updateDelayed_ = true;
586
+
587
+ if (immediate === true || this.context__.meta.changeDebounce === undefined) {
588
+ await delay.nextMacrotask();
589
+ }
590
+ else {
591
+ await delay.by(this.context__.meta.changeDebounce);
592
+ }
593
+
594
+ if (this.updateDelayed_ !== true) return; // another parallel update finished!
595
+ this.updateDelayed_ = false;
596
+
597
+ if (itemId === null) this.refreshMeta_(itemId); // root meta not updated for null
598
+
599
+ if (this._freeze === true) return; // prevent save if frozen
600
+ this.updatedCallback__(this);
601
+ }
602
+
603
+ /**
604
+ * Refresh/recalculate the collection's metadata timestamp and revision.
605
+ *
606
+ * @param itemId - The ID of the item to update.
607
+ */
608
+ protected refreshMeta_(itemId: string | number | null): void {
609
+ this.logger__.logMethodArgs?.('refreshMeta_', {id: itemId});
610
+ const now = Date.now();
611
+ this.context__.meta.rev++;
612
+ this.context__.meta.updated = now;
613
+ if (itemId !== null) {
614
+ const itemMeta = this.item__(itemId).meta;
615
+ itemMeta.rev++;
616
+ itemMeta.updated = now;
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Generates the next auto increment ID.
622
+ *
623
+ * @returns The next auto increment ID.
624
+ * @example
625
+ * ```typescript
626
+ * const nextId = this.nextAutoIncrementId_();
627
+ * ```
628
+ */
629
+ private nextAutoIncrementId__(): number {
630
+ this.logger__.logMethod?.('nextAutoIncrementId__');
631
+ const meta = this.context__.meta as Required<StoreFileMeta>;
632
+ do {
633
+ meta.lastAutoId++;
634
+ } while (meta.lastAutoId in this.context__.data);
635
+ return meta.lastAutoId;
636
+ }
637
+
638
+ /**
639
+ * Retrieves the collection's extra metadata.
640
+ *
641
+ * @returns The collection's extra metadata.
642
+ *
643
+ * @example
644
+ * ```typescript
645
+ * const colExtraMeta = collectionRef.getExtraMeta();
646
+ * ```
647
+ */
648
+ public getExtraMeta<T extends JsonObject>(): T {
649
+ this.logger__.logMethod?.('getExtraMeta');
650
+ return this.context__.meta.extra as T;
651
+ }
652
+
653
+ /**
654
+ * Sets/replace the collection's extra metadata.
655
+ *
656
+ * @param extraMeta The new collection's extra metadata.
657
+ *
658
+ * @example
659
+ * ```typescript
660
+ * collectionRef.replaceExtraMeta({ a: 1, b: 2, c: 3 });
661
+ * ```
662
+ */
663
+ public replaceExtraMeta<T extends JsonObject>(extraMeta: T): void {
664
+ this.logger__.logMethodArgs?.('replaceExtraMeta', extraMeta);
665
+ this.context__.meta.extra = extraMeta;
666
+ this.updated__();
667
+ }
668
+
669
+ /**
670
+ * Updates collection's extra metadata by merging a partial update.
671
+ *
672
+ * @param extraMeta The part of extra metadata to merge into the collection's extra metadata.
673
+ *
674
+ * @example
675
+ * ```typescript
676
+ * collectionRef.mergeExtraMeta({ c: 4 });
677
+ * ```
678
+ */
679
+ public mergeExtraMeta<T extends JsonObject>(extraMeta: Partial<T>): void {
680
+ this.logger__.logMethodArgs?.('mergeExtraMeta', extraMeta);
681
+ Object.assign(this.context__.meta.extra, extraMeta);
682
+ this.updated__();
683
+ }
684
+ }