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