@converse/skeletor 0.0.8 → 1.0.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,846 @@
1
+ import clone from 'lodash-es/clone';
2
+ import countBy from 'lodash-es/countBy';
3
+ import groupBy from 'lodash-es/groupBy';
4
+ import isFunction from 'lodash-es/isFunction';
5
+ import isString from 'lodash-es/isString';
6
+ import keyBy from 'lodash-es/keyBy';
7
+ import sortBy from 'lodash-es/sortBy';
8
+ import {EventEmitterObject} from './eventemitter';
9
+ import type Storage from './storage';
10
+ import {getResolveablePromise, getSyncMethod, wrapError} from './helpers';
11
+ import {Model} from './model';
12
+ import {
13
+ CollectionOptions,
14
+ Comparator,
15
+ ModelAttributes,
16
+ ModelOptions,
17
+ ObjectWithId,
18
+ Options,
19
+ SyncOperation,
20
+ } from './types';
21
+
22
+ // Default options for `Collection#set`.
23
+ const setOptions = {add: true, remove: true, merge: true};
24
+ const addOptions = {add: true, remove: false};
25
+
26
+ /**
27
+ * @public
28
+ * If models tend to represent a single row of data, a Collection is
29
+ * more analogous to a table full of data ... or a small slice or page of that
30
+ * table, or a collection of rows that belong together for a particular reason
31
+ * -- all of the messages in this particular folder, all of the documents
32
+ * belonging to this particular author, and so on. Collections maintain
33
+ * indexes of their models, both in order, and for lookup by `id`.
34
+ */
35
+ export class Collection<T extends Model = Model> extends EventEmitterObject {
36
+ [key: symbol]: () => CollectionIterator<T>;
37
+ _browserStorage?: Storage;
38
+ _comparator?: Comparator<T>;
39
+ _url: string = '';
40
+ models: T[];
41
+ protected _byId: Record<string, T>;
42
+ protected _model?: new (attributes?: Partial<ModelAttributes>, options?: ModelOptions) => T;
43
+
44
+ /**
45
+ * Create a new **Collection**, perhaps to contain a specific type of `model`.
46
+ * If a `comparator` is specified, the Collection will maintain
47
+ * its models in sort order, as they're added and removed.
48
+ */
49
+ constructor(models?: T[] | ModelAttributes[] | T | ModelAttributes, options?: CollectionOptions<T>) {
50
+ super();
51
+ options = options || {};
52
+ this.preinitialize.apply(this, arguments as any);
53
+ if (options.model) this._model = options.model;
54
+ if (options.comparator !== undefined) this.comparator = options.comparator;
55
+ this._reset();
56
+ this.initialize.apply(this, arguments as any);
57
+ if (models) this.reset(models, Object.assign({silent: true}, options));
58
+
59
+ this[Symbol.iterator] = this.values;
60
+ }
61
+
62
+ get comparator(): Comparator<T> {
63
+ return this._comparator;
64
+ }
65
+
66
+ set comparator(c: Comparator<T>) {
67
+ this._comparator = c;
68
+ }
69
+
70
+ set browserStorage(storage: Storage) {
71
+ this._browserStorage = storage;
72
+ }
73
+
74
+ get browserStorage(): Storage | undefined {
75
+ return this._browserStorage;
76
+ }
77
+
78
+ /**
79
+ * The default model for a collection is just a **Model**.
80
+ * This should be overridden in most cases.
81
+ */
82
+ get model(): new (attributes?: Partial<ModelAttributes>, options?: ModelOptions) => T | Model {
83
+ return this._model ?? Model;
84
+ }
85
+
86
+ set model(model: new (attributes?: Partial<ModelAttributes>, options?: ModelOptions) => T) {
87
+ this._model = model;
88
+ }
89
+
90
+ get length(): number {
91
+ return this.models.length;
92
+ }
93
+
94
+ get url(): string {
95
+ return this._url;
96
+ }
97
+
98
+ set url(url: string) {
99
+ this._url = url;
100
+ }
101
+
102
+ /**
103
+ * preinitialize is an empty function by default. You can override it with a function
104
+ * or object. preinitialize will run before any instantiation logic is run in the Collection.
105
+ */
106
+ preinitialize(..._args: any[]): void {}
107
+
108
+ /**
109
+ * Initialize is an empty function by default. Override it with your own
110
+ * initialization logic.
111
+ */
112
+ initialize(..._args: any[]): void {}
113
+
114
+ /**
115
+ * The JSON representation of a Collection is an array of the
116
+ * models' attributes.
117
+ */
118
+ toJSON(): any[] {
119
+ return this.map(function (model) {
120
+ return model.toJSON();
121
+ });
122
+ }
123
+
124
+ sync(method: SyncOperation, model: Model | Collection<any>, options?: Options): any {
125
+ return getSyncMethod(this)(method, model, options);
126
+ }
127
+
128
+ /**
129
+ * Add a model, or list of models to the set. `models` may be
130
+ * Models or raw JavaScript objects to be converted to Models, or any
131
+ * combination of the two.
132
+ */
133
+ add(models: T[] | T | ModelAttributes | ModelAttributes[], options?: Options): T | T[] {
134
+ return this.set(models, Object.assign({merge: false}, options, addOptions));
135
+ }
136
+
137
+ /**
138
+ * Remove a model, or a list of models from the set.
139
+ */
140
+ remove(models: T | ObjectWithId | (T | ObjectWithId)[], options?: Options): T | T[] {
141
+ options = Object.assign({}, options);
142
+ const singular = !Array.isArray(models);
143
+ const modelsArray = singular ? [models] : (models as T[]).slice();
144
+ const removed = this._removeModels(modelsArray, options);
145
+ if (!options.silent && removed.length) {
146
+ options.changes = {added: [], merged: [], removed: removed};
147
+ this.trigger('update', this, options);
148
+ }
149
+ return singular ? removed[0] : removed;
150
+ }
151
+
152
+ /**
153
+ * Update a collection by `set`-ing a new list of models, adding new ones,
154
+ * removing models that are no longer present, and merging models that
155
+ * already exist in the collection, as necessary. Similar to **Model#set**,
156
+ * the core operation for updating the data contained by the collection.
157
+ */
158
+ set(models: T[] | T | ModelAttributes | ModelAttributes[], options?: Options): T | T[] {
159
+ if (models == null) return;
160
+
161
+ options = Object.assign({}, setOptions, options);
162
+ if (options.parse && !this._isModel(models)) {
163
+ models = this.parse(models, options) || [];
164
+ }
165
+
166
+ const singular = !Array.isArray(models);
167
+ models = singular ? [models] : (models as T[] | ModelAttributes[]).slice();
168
+
169
+ let at = options.at;
170
+ if (at != null) at = +at;
171
+ if (at > this.length) at = this.length;
172
+ if (at < 0) at += this.length + 1;
173
+
174
+ const set = [];
175
+ const toAdd = [];
176
+ const toMerge = [];
177
+ const toRemove = [];
178
+ const modelMap = {};
179
+
180
+ const add = options.add;
181
+ const merge = options.merge;
182
+ const remove = options.remove;
183
+
184
+ let sort = false;
185
+ const sortable = this.comparator && at == null && options.sort !== false;
186
+ const sortAttr = isString(this.comparator) ? this.comparator : null;
187
+
188
+ // Turn bare objects into model references, and prevent invalid models
189
+ // from being added.
190
+ let model: T, i: number;
191
+ for (i = 0; i < models.length; i++) {
192
+ model = models[i];
193
+
194
+ // If a duplicate is found, prevent it from being added and
195
+ // optionally merge it into the existing model.
196
+ const existing = this.get(model);
197
+ if (existing) {
198
+ if (merge && model !== existing) {
199
+ let attrs = this._isModel(model) ? model.attributes : model;
200
+ if (options.parse) attrs = existing.parse(attrs, options) as Partial<ModelAttributes>;
201
+ existing.set(attrs, options);
202
+ toMerge.push(existing);
203
+
204
+ if (sortable && !sort) sort = existing.hasChanged(sortAttr as string);
205
+ }
206
+ if (!modelMap[existing.cid]) {
207
+ modelMap[existing.cid] = true;
208
+ set.push(existing);
209
+ }
210
+ models[i] = existing;
211
+
212
+ // If this is a new, valid model, push it to the `toAdd` list.
213
+ } else if (add) {
214
+ model = models[i] = this._prepareModel(model, options);
215
+ if (model) {
216
+ toAdd.push(model);
217
+ this._addReference(model, options);
218
+ modelMap[model.cid] = true;
219
+ set.push(model);
220
+ }
221
+ }
222
+ }
223
+
224
+ // Remove stale models.
225
+ if (remove) {
226
+ for (i = 0; i < this.length; i++) {
227
+ model = this.models[i];
228
+ if (!modelMap[model.cid]) toRemove.push(model);
229
+ }
230
+ if (toRemove.length) this._removeModels(toRemove, options);
231
+ }
232
+
233
+ // See if sorting is needed, update `length` and splice in new models.
234
+ let orderChanged = false;
235
+ const replace = !sortable && add && remove;
236
+
237
+ if (set.length && replace) {
238
+ orderChanged = this.length !== set.length || this.models.some((m, idx) => m !== set[idx]);
239
+ this.models.length = 0;
240
+ this.models.splice(0, 0, ...set);
241
+ } else if (toAdd.length) {
242
+ if (sortable) sort = true;
243
+ let idx = at == null ? this.length : at;
244
+ idx = Math.min(Math.max(idx, 0), this.models.length);
245
+ this.models.splice(idx, 0, ...toAdd);
246
+ }
247
+
248
+ // Silently sort the collection if appropriate.
249
+ if (sort) this.sort({silent: true});
250
+
251
+ // Unless silenced, it's time to fire all appropriate add/sort/update events.
252
+ if (!options.silent) {
253
+ for (i = 0; i < toAdd.length; i++) {
254
+ if (at != null) options.index = at + i;
255
+ model = toAdd[i];
256
+ model.trigger('add', model, this, options);
257
+ }
258
+ if (sort || orderChanged) this.trigger('sort', this, options);
259
+ if (toAdd.length || toRemove.length || toMerge.length) {
260
+ options.changes = {
261
+ added: toAdd,
262
+ removed: toRemove,
263
+ merged: toMerge,
264
+ };
265
+ this.trigger('update', this, options);
266
+ }
267
+ }
268
+
269
+ // Return the added (or merged) model (or models).
270
+ return singular ? models[0] : (models as T);
271
+ }
272
+
273
+ async clearStore(options: Options = {}, filter: (model: T) => boolean = () => true): Promise<void> {
274
+ await Promise.all(
275
+ this.models.filter(filter).map((m) => {
276
+ return new Promise<void>((resolve) => {
277
+ m.destroy(
278
+ Object.assign(options, {
279
+ 'success': resolve,
280
+ 'error': (_m: T, e: any) => {
281
+ console.error(e);
282
+ resolve();
283
+ },
284
+ })
285
+ );
286
+ });
287
+ })
288
+ );
289
+ await this.browserStorage?.clear();
290
+ this.reset();
291
+ }
292
+
293
+ /**
294
+ * When you have more items than you want to add or remove individually,
295
+ * you can reset the entire set with a new list of models, without firing
296
+ * any granular `add` or `remove` events. Fires `reset` when finished.
297
+ * Useful for bulk operations and optimizations.
298
+ */
299
+ reset(models?: T[] | T | ModelAttributes | ModelAttributes[], options?: Options): T | T[] {
300
+ options = options ? clone(options) : {};
301
+ for (let i = 0; i < this.models.length; i++) {
302
+ this._removeReference(this.models[i], options);
303
+ }
304
+ options.previousModels = this.models;
305
+ this._reset();
306
+ models = this.add(models, Object.assign({silent: true}, options));
307
+ if (!options.silent) this.trigger('reset', this, options);
308
+ return models as T | T[];
309
+ }
310
+
311
+ /**
312
+ * Add a model to the end of the collection.
313
+ */
314
+ push(model: T | ModelAttributes, options?: Options): T {
315
+ return this.add(model, Object.assign({at: this.length}, options)) as T;
316
+ }
317
+
318
+ /**
319
+ * Remove a model from the end of the collection.
320
+ */
321
+ pop(options?: Options): T | undefined {
322
+ const model = this.at(this.length - 1);
323
+ return this.remove(model, options) as T | undefined;
324
+ }
325
+
326
+ /**
327
+ * Add a model to the beginning of the collection.
328
+ */
329
+ unshift(model: T | ModelAttributes, options?: Options): T {
330
+ return this.add(model, Object.assign({at: 0}, options)) as T;
331
+ }
332
+
333
+ /**
334
+ * Remove a model from the beginning of the collection.
335
+ */
336
+ shift(options?: Options): T | undefined {
337
+ const model = this.at(0);
338
+ return this.remove(model, options) as T | undefined;
339
+ }
340
+
341
+ /** Slice out a sub-array of models from the collection. */
342
+ slice(start?: number, end?: number): T[] {
343
+ return this.models.slice(start, end);
344
+ }
345
+
346
+ filter(callback: ((model: T) => boolean) | string | Partial<ModelAttributes>, thisArg?: any): T[] {
347
+ return this.models.filter(
348
+ isFunction(callback)
349
+ ? (callback as (model: T) => boolean)
350
+ : (m) => m.matches(callback as Partial<ModelAttributes>),
351
+ thisArg
352
+ );
353
+ }
354
+
355
+ every(pred: ((attrs: ModelAttributes) => boolean) | Options): boolean {
356
+ if (isFunction(pred)) {
357
+ return this.models.map((m) => m.attributes).every(pred as (attrs: ModelAttributes) => boolean);
358
+ } else {
359
+ return this.models.every((m) => m.matches(pred));
360
+ }
361
+ }
362
+
363
+ difference(values: T[]): T[] {
364
+ return this.models.filter((m) => !values.includes(m));
365
+ }
366
+
367
+ max(): number {
368
+ return Math.max.apply(Math, this.models as any);
369
+ }
370
+
371
+ min(): number {
372
+ return Math.min.apply(Math, this.models as any);
373
+ }
374
+
375
+ drop(n: number = 1): T[] {
376
+ return this.models.slice(n);
377
+ }
378
+
379
+ some(pred: ((attrs: ModelAttributes) => boolean) | Options): boolean {
380
+ if (isFunction(pred)) {
381
+ return this.models.map((m) => m.attributes).some(pred as (attrs: ModelAttributes) => boolean);
382
+ } else {
383
+ return this.models.some((m) => m.matches(pred));
384
+ }
385
+ }
386
+
387
+ sortBy(iteratee: string | ((model: T) => any)): T[] {
388
+ return sortBy(
389
+ this.models,
390
+ isFunction(iteratee)
391
+ ? iteratee
392
+ : (m: T) => (isString(iteratee) ? m.get(iteratee as string) : m.matches(iteratee as Partial<ModelAttributes>))
393
+ );
394
+ }
395
+
396
+ isEmpty(): boolean {
397
+ return !this.models.length;
398
+ }
399
+
400
+ keyBy(iteratee: string | ((model: T) => string)): Record<string, T> {
401
+ return keyBy(this.models, iteratee);
402
+ }
403
+
404
+ each(callback: (model: T, index: number, array: T[]) => void, thisArg?: any): void {
405
+ return this.forEach(callback, thisArg);
406
+ }
407
+
408
+ forEach(callback: (model: T, index: number, array: T[]) => void, thisArg?: any): void {
409
+ return this.models.forEach(callback, thisArg);
410
+ }
411
+
412
+ includes(item: T): boolean {
413
+ return this.models.includes(item);
414
+ }
415
+
416
+ size(): number {
417
+ return this.models.length;
418
+ }
419
+
420
+ countBy(f: string | ((model: T) => string) | Partial<ModelAttributes>): Record<string, number> {
421
+ return countBy(this.models, isFunction(f) ? f : (m) => (isString(f) ? m.get(f) : m.matches(f)));
422
+ }
423
+
424
+ groupBy(pred: string | ((model: T) => string | number)): Record<string, T[]> {
425
+ return groupBy(this.models, isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
426
+ }
427
+
428
+ indexOf(model: T, fromIndex?: number): number {
429
+ return this.models.indexOf(model, fromIndex);
430
+ }
431
+
432
+ findLastIndex(pred: ((model: T) => boolean) | string | Partial<ModelAttributes>, fromIndex?: number): number {
433
+ return this.models.findLastIndex(
434
+ isFunction(pred)
435
+ ? (pred as (model: T) => boolean)
436
+ : (m) => (isString(pred) ? m.get(pred as string) : m.matches(pred as Partial<ModelAttributes>)),
437
+ fromIndex
438
+ );
439
+ }
440
+
441
+ lastIndexOf(model: T, fromIndex?: number): number {
442
+ return this.models.lastIndexOf(model, fromIndex);
443
+ }
444
+
445
+ findIndex(pred: ((model: T) => boolean) | string | Partial<ModelAttributes>): number {
446
+ return this.models.findIndex(
447
+ isFunction(pred)
448
+ ? (pred as (model: T) => boolean)
449
+ : (m) => (isString(pred) ? m.get(pred as string) : m.matches(pred as Partial<ModelAttributes>))
450
+ );
451
+ }
452
+
453
+ last(): T | undefined {
454
+ const length = this.models == null ? 0 : this.models.length;
455
+ return length ? this.models[length - 1] : undefined;
456
+ }
457
+
458
+ head(): T | undefined {
459
+ return this.models[0];
460
+ }
461
+
462
+ first(): T | undefined {
463
+ return this.head();
464
+ }
465
+
466
+ map<U>(cb: string | ((model: T) => U) | Partial<ModelAttributes>, thisArg?: any): U[] {
467
+ return this.models.map(
468
+ isFunction(cb)
469
+ ? (cb as (model: T) => U)
470
+ : (m) => (isString(cb) ? m.get(cb as string) : m.matches(cb as Partial<ModelAttributes>)),
471
+ thisArg
472
+ );
473
+ }
474
+
475
+ reduce(callback: (accumulator: T, model: T, index: number, array: T[]) => T, initialValue?: T): T {
476
+ return this.models.reduce(callback, initialValue || this.models[0]);
477
+ }
478
+
479
+ reduceRight(callback: (accumulator: T, model: T, index: number, array: T[]) => T, initialValue?: T): T {
480
+ return this.models.reduceRight(callback, initialValue || this.models[0]);
481
+ }
482
+
483
+ toArray(): T[] {
484
+ return Array.from(this.models);
485
+ }
486
+
487
+ /**
488
+ * Get a model from the set by id, cid, model object with id or cid
489
+ * properties, or an attributes object that is transformed through modelId.
490
+ */
491
+ get(obj?: string | number | ModelAttributes | T | null): T | undefined {
492
+ if (obj == null) return undefined;
493
+ return (
494
+ this._byId[obj as string] ||
495
+ this._byId[this.modelId(this._isModel(obj) ? (obj as T).attributes : (obj as ModelAttributes)) as string] ||
496
+ ((obj as T).cid && this._byId[(obj as T).cid])
497
+ );
498
+ }
499
+
500
+ /**
501
+ * Returns `true` if the model is in the collection.
502
+ */
503
+ has(obj: string | number | ModelAttributes | T | null): boolean {
504
+ return this.get(obj) != null;
505
+ }
506
+
507
+ /**
508
+ * Get the model at the given index.
509
+ */
510
+ at(index: number): T | undefined {
511
+ if (index < 0) index += this.length;
512
+ return this.models[index];
513
+ }
514
+
515
+ /**
516
+ * Return models with matching attributes. Useful for simple cases of
517
+ * `filter`.
518
+ */
519
+ where(attrs: ModelAttributes | Partial<ModelAttributes>, first?: boolean): T[] | T | undefined {
520
+ return this[first ? 'find' : 'filter'](attrs);
521
+ }
522
+
523
+ /**
524
+ * Return the first model with matching attributes. Useful for simple cases
525
+ * of `find`.
526
+ */
527
+ findWhere(attrs: ModelAttributes): T | undefined {
528
+ return this.where(attrs, true) as T | undefined;
529
+ }
530
+
531
+ find(predicate: ((model: T) => boolean) | Partial<ModelAttributes> | string, fromIndex?: number): T | undefined {
532
+ const pred = isFunction(predicate)
533
+ ? (predicate as (model: T) => boolean)
534
+ : (m: T) => m.matches(predicate as Partial<ModelAttributes>);
535
+ return this.models.find(pred, fromIndex);
536
+ }
537
+
538
+ /**
539
+ * Force the collection to re-sort itself. You don't need to call this under
540
+ * normal circumstances, as the set will maintain sort order as each item
541
+ * is added.
542
+ */
543
+ sort(options?: Options): this {
544
+ let comparator = this.comparator;
545
+ if (!comparator) throw new Error('Cannot sort a set without a comparator');
546
+ options = options || {};
547
+
548
+ const length = isFunction(comparator) ? comparator.length : 0;
549
+ if (isFunction(comparator)) comparator = (comparator as (a: T, b: T) => number).bind(this);
550
+
551
+ // Run sort based on type of `comparator`.
552
+ if (length === 1 || isString(comparator)) {
553
+ this.models = this.sortBy(comparator as string | ((model: T) => any));
554
+ } else {
555
+ this.models.sort(comparator as (a: T, b: T) => number);
556
+ }
557
+ if (!options.silent) this.trigger('sort', this, options);
558
+ return this;
559
+ }
560
+
561
+ /**
562
+ * Pluck an attribute from each model in the collection.
563
+ */
564
+ pluck(attr: string): any[] {
565
+ return this.map(attr + '');
566
+ }
567
+
568
+ /**
569
+ * Fetch the default set of models for this collection, resetting the
570
+ * collection when they arrive. If `reset: true` is passed, the response
571
+ * data will be passed through the `reset` method instead of `set`.
572
+ */
573
+ fetch(options?: Options): Promise<any> | any {
574
+ options = Object.assign({parse: true}, options);
575
+ const success = options.success;
576
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
577
+ const collection = this;
578
+ const promise = options.promise && getResolveablePromise();
579
+ options.success = function (resp: any) {
580
+ const method = options.reset ? 'reset' : 'set';
581
+ collection[method](resp, options);
582
+ if (success) success.call(options.context, collection, resp, options);
583
+ promise && promise.resolve();
584
+ collection.trigger('sync', collection, resp, options);
585
+ };
586
+ wrapError(this, options);
587
+ return promise ? promise : this.sync('read', this, options);
588
+ }
589
+
590
+ /**
591
+ * Create a new instance of a model in this collection. Add the model to the
592
+ * collection immediately, unless `wait: true` is passed, in which case we
593
+ * wait for the server to agree.
594
+ */
595
+ create(model: T | ModelAttributes, options?: Options): Promise<T> | T | false {
596
+ options = options ? clone(options) : {};
597
+ const wait = options.wait;
598
+ const return_promise = options.promise;
599
+ const promise = return_promise && getResolveablePromise();
600
+
601
+ const preparedModel = this._prepareModel(model, options);
602
+ if (!preparedModel) return false;
603
+ if (!wait) this.add(preparedModel, options);
604
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
605
+ const collection = this;
606
+ const success = options.success;
607
+ const error = options.error;
608
+ options.success = function (m: T, resp: any, callbackOpts: Options) {
609
+ if (wait) {
610
+ collection.add(m, callbackOpts);
611
+ }
612
+ if (success) {
613
+ success.call(callbackOpts.context, m, resp, callbackOpts);
614
+ }
615
+ if (return_promise) {
616
+ promise.resolve(m);
617
+ }
618
+ };
619
+ options.error = function (model: T, e: any, options: Options) {
620
+ error && error.call(options.context, model, e, options);
621
+ return_promise && promise.reject(e);
622
+ };
623
+
624
+ preparedModel.save(null, Object.assign(options, {'promise': false}));
625
+ if (return_promise) {
626
+ return promise;
627
+ } else {
628
+ return preparedModel;
629
+ }
630
+ }
631
+
632
+ /**
633
+ * **parse** converts a response into a list of models to be added to the
634
+ * collection. The default implementation is just to pass it through.
635
+ */
636
+ parse(resp: any, _options?: Options): any {
637
+ return resp;
638
+ }
639
+
640
+ /**
641
+ * Define how to uniquely identify models in the collection.
642
+ */
643
+ modelId(attrs: ModelAttributes): string | number | undefined {
644
+ return attrs[this.model.prototype?.idAttribute || 'id'];
645
+ }
646
+
647
+ /** Get an iterator of all models in this collection. */
648
+ values(): CollectionIterator<T> {
649
+ return new CollectionIterator(this, ITERATOR_VALUES);
650
+ }
651
+
652
+ /**
653
+ * @public
654
+ * Enable for...of iteration over the collection.
655
+ */
656
+ [Symbol.iterator] = this.values;
657
+
658
+ /** Get an iterator of all model IDs in this collection. */
659
+ keys(): CollectionIterator<T> {
660
+ return new CollectionIterator(this, ITERATOR_KEYS);
661
+ }
662
+
663
+ /** Get an iterator of all [ID, model] tuples in this collection. */
664
+ entries(): CollectionIterator<T> {
665
+ return new CollectionIterator(this, ITERATOR_KEYSVALUES);
666
+ }
667
+
668
+ /**
669
+ * Private method to reset all internal state. Called when the collection
670
+ * is first initialized or reset.
671
+ */
672
+ _reset(): void {
673
+ this.models = [];
674
+ this._byId = {};
675
+ }
676
+
677
+ createModel(attrs: ModelAttributes, options?: Options): T {
678
+ const Klass = this.model;
679
+ return new Klass(attrs, options) as T;
680
+ }
681
+
682
+ /**
683
+ * Prepare a hash of attributes (or other model) to be added to this
684
+ * collection.
685
+ */
686
+ _prepareModel(attrs: ModelAttributes | T, options?: Options): T | null {
687
+ if (this._isModel(attrs)) {
688
+ if (!(attrs as T).collection) (attrs as T).collection = this;
689
+ return attrs as T;
690
+ }
691
+ options = options ? clone(options) : {};
692
+ options.collection = this;
693
+ const model = this.createModel(attrs as ModelAttributes, options);
694
+ if (!model.validationError) return model;
695
+ this.trigger('invalid', this, model.validationError, options);
696
+ return null;
697
+ }
698
+
699
+ /**
700
+ * Internal method called by both remove and set.
701
+ */
702
+ _removeModels(models: (T | ObjectWithId)[], options?: Options): T[] {
703
+ const removed: T[] = [];
704
+ for (let i = 0; i < models.length; i++) {
705
+ const model = this.get(models[i]);
706
+ if (!model) continue;
707
+
708
+ const index = this.indexOf(model);
709
+ this.models.splice(index, 1);
710
+
711
+ // Remove references before triggering 'remove' event to prevent an
712
+ // infinite loop. #3693
713
+ delete this._byId[model.cid];
714
+ const id = this.modelId(model.attributes);
715
+ if (id != null) delete this._byId[id];
716
+
717
+ if (!options?.silent) {
718
+ options = options || {};
719
+ options.index = index;
720
+ model.trigger('remove', model, this, options);
721
+ }
722
+
723
+ removed.push(model);
724
+ this._removeReference(model, options);
725
+ }
726
+ return removed;
727
+ }
728
+
729
+ /**
730
+ * Method for checking whether an object should be considered a model for
731
+ * the purposes of adding to the collection.
732
+ */
733
+ _isModel(model: any): model is T {
734
+ return model instanceof Model;
735
+ }
736
+
737
+ /**
738
+ * Internal method to create a model's ties to a collection.
739
+ */
740
+ _addReference(model: T, _options?: Options): void {
741
+ this._byId[model.cid] = model;
742
+ const id = this.modelId(model.attributes);
743
+ if (id != null) this._byId[id as string] = model;
744
+ model.on('all', this._onModelEvent, this);
745
+ }
746
+
747
+ /**
748
+ * Internal method to sever a model's ties to a collection.
749
+ */
750
+ _removeReference(model: T, _options?: Options): void {
751
+ delete this._byId[model.cid];
752
+ const id = this.modelId(model.attributes);
753
+ if (id != null) delete this._byId[id as string];
754
+ if (this === model.collection) delete model.collection;
755
+ model.off('all', this._onModelEvent, this);
756
+ }
757
+
758
+ /**
759
+ * Internal method called every time a model in the set fires an event.
760
+ * Sets need to update their indexes when models change ids. All other
761
+ * events simply proxy through. "add" and "remove" events that originate
762
+ * in other collections are ignored.
763
+ */
764
+ _onModelEvent(event: string, model: T, collection: Collection<T>, options?: Options): void {
765
+ if (model) {
766
+ if ((event === 'add' || event === 'remove') && collection !== this) return;
767
+ if (event === 'destroy') this.remove(model, options);
768
+ if (event === 'change') {
769
+ const prevId = this.modelId(model.previousAttributes());
770
+ const id = this.modelId(model.attributes);
771
+ if (prevId !== id) {
772
+ if (prevId != null) delete this._byId[prevId as string];
773
+ if (id != null) this._byId[id as string] = model;
774
+ }
775
+ }
776
+ }
777
+ this.trigger.apply(this, arguments as any);
778
+ }
779
+ }
780
+
781
+ // This "enum" defines the three possible kinds of values which can be emitted
782
+ // by a CollectionIterator that correspond to the values(), keys() and entries()
783
+ // methods on Collection, respectively.
784
+ const ITERATOR_VALUES = 1;
785
+ const ITERATOR_KEYS = 2;
786
+ const ITERATOR_KEYSVALUES = 3;
787
+
788
+ /**
789
+ * @public
790
+ */
791
+ export class CollectionIterator<T extends Model> {
792
+ private _collection: Collection<T> | undefined;
793
+ private _kind: number;
794
+ private _index: number;
795
+
796
+ /**
797
+ * A CollectionIterator implements JavaScript's Iterator protocol, allowing the
798
+ * use of `for of` loops in modern browsers and interoperation between
799
+ * Collection and other JavaScript functions and third-party libraries
800
+ * which can operate on Iterables.
801
+ */
802
+ constructor(collection: Collection<T>, kind: number) {
803
+ this._collection = collection;
804
+ this._kind = kind;
805
+ this._index = 0;
806
+ }
807
+
808
+ next(): IteratorResult<any> {
809
+ if (this._collection) {
810
+ // Only continue iterating if the iterated collection is long enough.
811
+ if (this._index < this._collection.length) {
812
+ const model = this._collection.at(this._index);
813
+ this._index++;
814
+
815
+ if (!model) {
816
+ return {value: undefined, done: true};
817
+ }
818
+
819
+ // Construct a value depending on what kind of values should be iterated.
820
+ let value: T | string | number | [string | number, T];
821
+ if (this._kind === ITERATOR_VALUES) {
822
+ value = model;
823
+ } else {
824
+ const id = this._collection.modelId(model.attributes);
825
+ if (this._kind === ITERATOR_KEYS) {
826
+ value = id;
827
+ } else {
828
+ // ITERATOR_KEYSVALUES
829
+ value = [id, model];
830
+ }
831
+ }
832
+ return {value, done: false};
833
+ }
834
+
835
+ // Once exhausted, remove the reference to the collection so future
836
+ // calls to the next method always return done.
837
+ this._collection = undefined;
838
+ }
839
+
840
+ return {value: undefined, done: true};
841
+ }
842
+
843
+ [Symbol.iterator](): IterableIterator<any> {
844
+ return this;
845
+ }
846
+ }