@converse/skeletor 3.1.6 → 4.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.
package/dist/skeletor.js CHANGED
@@ -8477,6 +8477,16 @@
8477
8477
  // Generate a pseudo-GUID by concatenating random hexadecimal.
8478
8478
  return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
8479
8479
  }
8480
+ // Helpers
8481
+ // -------
8482
+ /**
8483
+ * Whether `value` is a thenable (a promise, or a promise-like object such as
8484
+ * a jqXHR). Decides whether a `sync` return value can be bridged to the
8485
+ * promise that `fetch`/`save` hand back.
8486
+ */
8487
+ function isThenable(value) {
8488
+ return value != null && typeof value.then === 'function';
8489
+ }
8480
8490
  function getResolveablePromise() {
8481
8491
  const wrapper = {
8482
8492
  isResolved: false,
@@ -8521,6 +8531,65 @@
8521
8531
  promise && promise.reject && promise.reject(resp);
8522
8532
  };
8523
8533
  }
8534
+ /**
8535
+ * Run a `sync` and make sure `promise` settles from its outcome, however the
8536
+ * sync reports it.
8537
+ *
8538
+ * A `sync` may invoke `options.success`/`options.error` (the callback
8539
+ * contract), or settle the promise it returns without calling them: a
8540
+ * promise-only custom sync, or an async sync whose promise rejects because a
8541
+ * `'sync'`/`'error'` listener threw after the callbacks ran. `options.success`
8542
+ * and `options.error` must already be wired to resolve/reject `promise`. This
8543
+ * wraps them to record that they ran, calls `run()`, and bridges a thenable
8544
+ * result to them so the promise never hangs:
8545
+ *
8546
+ * - fulfilment with the callbacks unrun is delivered to `options.success`, so
8547
+ * the response is still merged into the model or collection;
8548
+ * - rejection with the callbacks unrun is delivered to `options.error`, so the
8549
+ * caller's `error` callback and the `'error'` event still fire;
8550
+ * - rejection after the callbacks ran rejects `promise` directly: a no-op if
8551
+ * they already settled it, and the only way to settle it if a listener threw
8552
+ * before they could;
8553
+ * - a throw from either delivery (a throwing `success`/`error` callback or
8554
+ * listener) rejects `promise` instead of surfacing as an unhandled rejection.
8555
+ *
8556
+ * A non-thenable result (a callback-only sync that returns nothing, or an
8557
+ * XHR) is left alone: such a sync settles `promise` through its callbacks, and
8558
+ * bridging it would fire `success` on the next microtask, before the sync is
8559
+ * done. Without a `promise`, `run()` is called as is.
8560
+ */
8561
+ function bridgeSyncToPromise(options, promise, run) {
8562
+ if (!promise)
8563
+ return run();
8564
+ let settled = false;
8565
+ const success = options.success;
8566
+ const error = options.error;
8567
+ options.success = (...args) => {
8568
+ settled = true;
8569
+ return success && success(...args);
8570
+ };
8571
+ options.error = (...args) => {
8572
+ settled = true;
8573
+ return error && error(...args);
8574
+ };
8575
+ const result = run();
8576
+ if (isThenable(result)) {
8577
+ Promise.resolve(result)
8578
+ .then((value) => {
8579
+ if (!settled)
8580
+ options.success(value);
8581
+ }, (e) => {
8582
+ if (settled) {
8583
+ promise.reject(e);
8584
+ }
8585
+ else {
8586
+ options.error(e);
8587
+ }
8588
+ })
8589
+ .catch((e) => promise.reject(e));
8590
+ }
8591
+ return result;
8592
+ }
8524
8593
  // Map from CRUD to HTTP for our default `sync` implementation.
8525
8594
  const methodMap = {
8526
8595
  create: 'POST',
@@ -8570,43 +8639,77 @@
8570
8639
  return store ? store.sync() : sync;
8571
8640
  }
8572
8641
  /**
8573
- * Override this function to change the manner in which Backbone persists
8574
- * models to the server. You will be passed the type of request, and the
8575
- * model in question. By default makes a `fetch()` API call
8576
- * to the model's `url()`.
8577
- *
8578
- * Some possible customizations could be:
8579
- *
8580
- * - Use `setTimeout` to batch rapid-fire updates into a single request.
8581
- * - Persist models via WebSockets instead of Ajax.
8582
- * - Persist models to browser storage
8642
+ * The error the default `sync` hands to `options.error`, and rejects with, for
8643
+ * a response outside the 2xx range.
8644
+ * @public
8583
8645
  */
8646
+ class HTTPError extends Error {
8647
+ constructor(response, method, url) {
8648
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
8649
+ super(`${method} ${url} failed with HTTP ${response.status}${statusText}`);
8650
+ this.name = 'HTTPError';
8651
+ this.status = response.status;
8652
+ this.response = response;
8653
+ }
8654
+ }
8584
8655
  /**
8656
+ * The default `sync`: a JSON request over `fetch()` to the model's `url()`.
8657
+ * Override it (or `Model.sync`/`Collection.sync`) to change how models are
8658
+ * persisted, for instance to batch rapid-fire updates with `setTimeout`, to
8659
+ * talk over WebSockets, or to write to browser storage.
8660
+ *
8661
+ * It honours both contracts a `sync` may use. On a 2xx response the JSON body
8662
+ * (`null` when the body is empty) is passed to `options.success` and the
8663
+ * returned promise resolves with it. On a non-2xx response, a network failure
8664
+ * or an unparsable body, `options.error` receives the error (an `HTTPError`
8665
+ * for the first) and the promise rejects with it.
8666
+ *
8667
+ * `options.data` is sent as query parameters on GET, which may not carry a
8668
+ * body, and as a JSON body otherwise. For create, update and patch the body
8669
+ * defaults to `options.attrs` or the model's `toJSON()`.
8585
8670
  * @public
8586
8671
  */
8587
8672
  function sync(method, model, options = {}) {
8673
+ const type = methodMap[method];
8588
8674
  let data = options.data;
8589
8675
  // Ensure that we have the appropriate request data.
8590
8676
  if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
8591
8677
  data = options.attrs || model.toJSON();
8592
8678
  }
8593
- const type = methodMap[method];
8594
- const params = {
8595
- method: type,
8596
- body: data ? JSON.stringify(data) : '',
8597
- headers: {
8598
- 'Content-Type': 'application/json',
8599
- },
8600
- success: options.success,
8601
- error: options.error,
8602
- };
8603
- const url = options.url || result(model, 'url') || urlError();
8604
- const xhr = fetch(url, params);
8605
- if (options) {
8606
- options.xhr = xhr;
8679
+ let url = options.url || result(model, 'url') || urlError();
8680
+ const params = { method: type };
8681
+ if (data != null) {
8682
+ if (type === 'GET') {
8683
+ url += (url.includes('?') ? '&' : '?') + new URLSearchParams(data).toString();
8684
+ }
8685
+ else {
8686
+ params.body = JSON.stringify(data);
8687
+ params.headers = { 'Content-Type': 'application/json' };
8688
+ }
8607
8689
  }
8690
+ const xhr = fetch(url, params);
8691
+ options.xhr = xhr;
8608
8692
  model.trigger('request', model, xhr, { ...params, xhr });
8609
- return xhr;
8693
+ return Promise.resolve(xhr)
8694
+ .then(async (response) => {
8695
+ if (!response.ok)
8696
+ throw new HTTPError(response, type, url);
8697
+ const text = await response.text();
8698
+ return text ? JSON.parse(text) : null;
8699
+ })
8700
+ .then(
8701
+ // The callbacks run before the returned promise settles, so a caller that
8702
+ // bridges this promise (`Model.fetch`/`save`) sees the outcome as already
8703
+ // reported. A throw from `success` (a `'sync'` listener, say) is not
8704
+ // routed to `error`: it rejects the promise, which is what the bridge
8705
+ // expects once the callbacks have run.
8706
+ (resp) => {
8707
+ options.success?.(resp, options);
8708
+ return resp;
8709
+ }, (e) => {
8710
+ options.error?.(e);
8711
+ throw e;
8712
+ });
8610
8713
  }
8611
8714
 
8612
8715
  /**
@@ -9972,6 +10075,48 @@
9972
10075
  baseAssignValue(result, key, value);
9973
10076
  });
9974
10077
 
10078
+ /** Used to compose bitmasks for cloning. */
10079
+ var CLONE_DEEP_FLAG$1 = 1;
10080
+
10081
+ /**
10082
+ * Creates a function that performs a partial deep comparison between a given
10083
+ * object and `source`, returning `true` if the given object has equivalent
10084
+ * property values, else `false`.
10085
+ *
10086
+ * **Note:** The created function is equivalent to `_.isMatch` with `source`
10087
+ * partially applied.
10088
+ *
10089
+ * Partial comparisons will match empty array and empty object `source`
10090
+ * values against any array or object value, respectively. See `_.isEqual`
10091
+ * for a list of supported value comparisons.
10092
+ *
10093
+ * **Note:** Multiple values can be checked by combining several matchers
10094
+ * using `_.overSome`
10095
+ *
10096
+ * @static
10097
+ * @memberOf _
10098
+ * @since 3.0.0
10099
+ * @category Util
10100
+ * @param {Object} source The object of property values to match.
10101
+ * @returns {Function} Returns the new spec function.
10102
+ * @example
10103
+ *
10104
+ * var objects = [
10105
+ * { 'a': 1, 'b': 2, 'c': 3 },
10106
+ * { 'a': 4, 'b': 5, 'c': 6 }
10107
+ * ];
10108
+ *
10109
+ * _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
10110
+ * // => [{ 'a': 4, 'b': 5, 'c': 6 }]
10111
+ *
10112
+ * // Checking for several possible values
10113
+ * _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
10114
+ * // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
10115
+ */
10116
+ function matches(source) {
10117
+ return baseMatches(baseClone(source, CLONE_DEEP_FLAG$1));
10118
+ }
10119
+
9975
10120
  /** Built-in value references. */
9976
10121
  var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
9977
10122
 
@@ -10535,53 +10680,36 @@
10535
10680
  return baseIsEqual(value, other);
10536
10681
  }
10537
10682
 
10538
- /** Used to compose bitmasks for cloning. */
10539
- var CLONE_DEEP_FLAG$1 = 1;
10540
-
10541
10683
  /**
10542
- * Creates a function that invokes `func` with the arguments of the created
10543
- * function. If `func` is a property name, the created function returns the
10544
- * property value for a given element. If `func` is an array or object, the
10545
- * created function returns `true` for elements that contain the equivalent
10546
- * source properties, otherwise it returns `false`.
10684
+ * Performs a partial deep comparison between `object` and `source` to
10685
+ * determine if `object` contains equivalent property values.
10686
+ *
10687
+ * **Note:** This method is equivalent to `_.matches` when `source` is
10688
+ * partially applied.
10689
+ *
10690
+ * Partial comparisons will match empty array and empty object `source`
10691
+ * values against any array or object value, respectively. See `_.isEqual`
10692
+ * for a list of supported value comparisons.
10547
10693
  *
10548
10694
  * @static
10549
- * @since 4.0.0
10550
10695
  * @memberOf _
10551
- * @category Util
10552
- * @param {*} [func=_.identity] The value to convert to a callback.
10553
- * @returns {Function} Returns the callback.
10696
+ * @since 3.0.0
10697
+ * @category Lang
10698
+ * @param {Object} object The object to inspect.
10699
+ * @param {Object} source The object of property values to match.
10700
+ * @returns {boolean} Returns `true` if `object` is a match, else `false`.
10554
10701
  * @example
10555
10702
  *
10556
- * var users = [
10557
- * { 'user': 'barney', 'age': 36, 'active': true },
10558
- * { 'user': 'fred', 'age': 40, 'active': false }
10559
- * ];
10560
- *
10561
- * // The `_.matches` iteratee shorthand.
10562
- * _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
10563
- * // => [{ 'user': 'barney', 'age': 36, 'active': true }]
10564
- *
10565
- * // The `_.matchesProperty` iteratee shorthand.
10566
- * _.filter(users, _.iteratee(['user', 'fred']));
10567
- * // => [{ 'user': 'fred', 'age': 40 }]
10703
+ * var object = { 'a': 1, 'b': 2 };
10568
10704
  *
10569
- * // The `_.property` iteratee shorthand.
10570
- * _.map(users, _.iteratee('user'));
10571
- * // => ['barney', 'fred']
10572
- *
10573
- * // Create custom iteratee shorthands.
10574
- * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
10575
- * return !_.isRegExp(func) ? iteratee(func) : function(string) {
10576
- * return func.test(string);
10577
- * };
10578
- * });
10705
+ * _.isMatch(object, { 'b': 2 });
10706
+ * // => true
10579
10707
  *
10580
- * _.filter(['abc', 'def'], /ef/);
10581
- * // => ['def']
10708
+ * _.isMatch(object, { 'b': 1 });
10709
+ * // => false
10582
10710
  */
10583
- function iteratee(func) {
10584
- return baseIteratee(typeof func == 'function' ? func : baseClone(func, CLONE_DEEP_FLAG$1));
10711
+ function isMatch(object, source) {
10712
+ return object === source || baseIsMatch(object, source, getMatchData(source));
10585
10713
  }
10586
10714
 
10587
10715
  /**
@@ -11095,10 +11223,20 @@
11095
11223
  return this.get(attr) != null;
11096
11224
  }
11097
11225
  /**
11098
- * Special-cased proxy to lodash's `matches` method.
11226
+ * Returns `true` if the model's attributes satisfy `attrs`.
11227
+ *
11228
+ * An attributes object is compared with lodash's `isMatch`: a partial, deep
11229
+ * comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
11230
+ * that has `type: 'hidden'` alongside other keys. A function is called with
11231
+ * the attributes and its result coerced to a boolean.
11232
+ *
11233
+ * To scan a collection, prefer `Collection#find`, `filter` or `where`. They
11234
+ * build their matcher once per scan instead of once per model.
11099
11235
  */
11100
11236
  matches(attrs) {
11101
- return !!iteratee(attrs)(this.attributes);
11237
+ if (typeof attrs === 'function')
11238
+ return !!attrs(this.attributes);
11239
+ return isMatch(this.attributes, attrs);
11102
11240
  }
11103
11241
  /**
11104
11242
  * Set a hash of model attributes on the object, firing `"change"`. This is
@@ -11271,47 +11409,25 @@
11271
11409
  options = Object.assign({ parse: true }, options);
11272
11410
  const success = options.success;
11273
11411
  const promise = options.promise ? getResolveablePromise() : undefined;
11274
- // Tracks whether sync invoked one of the callbacks. A sync may instead
11275
- // settle its returned promise; the `.then`/`.catch` below bridges that to
11276
- // the callbacks so the fetch promise never hangs. The flag is set
11277
- // synchronously inside each callback (before sync's promise resolves), so
11278
- // it's reliable by the time those handlers run.
11279
- let settled = false;
11280
11412
  options.success = (resp) => {
11281
- settled = true;
11282
11413
  const serverAttrs = options.parse ? this.parse(resp, options) : resp;
11283
11414
  // fromStorage prevents the set() from triggering auto-save on reads
11284
- if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
11415
+ if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
11416
+ // The fetched attributes failed validation (`'invalid'` has fired).
11417
+ // Settle the promise with the error rather than leave it pending.
11418
+ promise && promise.reject(this.validationError);
11285
11419
  return false;
11420
+ }
11286
11421
  if (success)
11287
11422
  success.call(options.context, this, resp, options);
11288
11423
  this.trigger('sync', this, resp, options);
11289
11424
  promise && promise.resolve(resp);
11290
11425
  };
11291
11426
  wrapError(this, options, promise);
11292
- if (promise) {
11293
- const onError = options.error;
11294
- options.error = (resp) => {
11295
- settled = true;
11296
- onError(resp);
11297
- };
11298
- }
11299
- const result = this.sync('read', this, options);
11300
- if (promise) {
11301
- Promise.resolve(result).then(
11302
- // A sync that resolves its promise without invoking options.success:
11303
- // treat the resolved value as the response, so the model is still
11304
- // merged and the promise settles instead of hanging.
11305
- (value) => {
11306
- if (!settled)
11307
- options.success(value);
11308
- },
11309
- // A sync that rejects instead of invoking options.error; settling
11310
- // twice is a harmless no-op.
11311
- (e) => promise.reject(e));
11312
- return promise;
11313
- }
11314
- return result;
11427
+ // With `promise`, the sync's outcome is bridged to it however the sync
11428
+ // reports it: through the callbacks, or through the promise it returns.
11429
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
11430
+ return promise ?? result;
11315
11431
  }
11316
11432
  /**
11317
11433
  * Set a hash of model attributes, and sync the model to the server.
@@ -11332,16 +11448,19 @@
11332
11448
  options = Object.assign({ validate: true, parse: true }, options);
11333
11449
  const wait = options.wait;
11334
11450
  const return_promise = options.promise;
11335
- const promise = return_promise && getResolveablePromise();
11451
+ const promise = return_promise ? getResolveablePromise() : undefined;
11336
11452
  // If we're not waiting and attributes exist, save acts as
11337
11453
  // `set(attr).save(null, opts)` with validation. Otherwise, check if
11338
11454
  // the model will be valid when the attributes, if any, are set.
11339
- if (attrs && !wait) {
11340
- if (!this.set(attrs, options))
11455
+ const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
11456
+ if (!valid) {
11457
+ // Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
11458
+ // rejected promise rather than `false`, so the promise contract holds
11459
+ // for every outcome.
11460
+ if (!promise)
11341
11461
  return false;
11342
- }
11343
- else if (!this._validate(attrs || {}, options)) {
11344
- return false;
11462
+ promise.reject(this.validationError);
11463
+ return promise;
11345
11464
  }
11346
11465
  // After a successful server-side save, the client is (optionally)
11347
11466
  // updated with the server-side state.
@@ -11354,8 +11473,12 @@
11354
11473
  let serverAttrs = options.parse ? this.parse(resp, options) : resp;
11355
11474
  if (wait)
11356
11475
  serverAttrs = Object.assign({}, attrs, serverAttrs);
11357
- if (serverAttrs && !this.set(serverAttrs, options))
11476
+ if (serverAttrs && !this.set(serverAttrs, options)) {
11477
+ // The server attributes failed validation (`'invalid'` has fired).
11478
+ // Settle the promise with the error rather than leave it pending.
11479
+ return_promise && promise.reject(this.validationError);
11358
11480
  return false;
11481
+ }
11359
11482
  if (success)
11360
11483
  success.call(options.context, this, resp, options);
11361
11484
  this.trigger('sync', this, resp, options);
@@ -11372,15 +11495,12 @@
11372
11495
  const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
11373
11496
  if (method === 'patch' && !options.attrs)
11374
11497
  options.attrs = attrs;
11375
- const xhr = this.sync(method, this, options);
11498
+ // With `promise`, the sync's outcome is bridged to it however the sync
11499
+ // reports it: through the callbacks, or through the promise it returns.
11500
+ const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
11376
11501
  // Restore attributes.
11377
11502
  this.attributes = attributes;
11378
- if (return_promise) {
11379
- return promise;
11380
- }
11381
- else {
11382
- return xhr;
11383
- }
11503
+ return promise ?? xhr;
11384
11504
  }
11385
11505
  /**
11386
11506
  * Destroy this model on the server if it was already persisted.
@@ -11527,6 +11647,23 @@
11527
11647
  // Default options for `Collection#set`.
11528
11648
  const setOptions = { add: true, remove: true, merge: true };
11529
11649
  const addOptions = { add: true, remove: false };
11650
+ /**
11651
+ * Normalizes the predicate forms accepted by the query methods into one
11652
+ * `(model) => value` function. A function is used as-is, a string reads that
11653
+ * attribute, and an attributes object becomes a lodash `matches` matcher.
11654
+ *
11655
+ * The matcher is built here, once per scan. Calling `Model#matches` per model
11656
+ * instead rebuilds the match data for every model visited, which made
11657
+ * `findWhere` an order of magnitude slower than a hand-written predicate.
11658
+ */
11659
+ function toModelPredicate(pred) {
11660
+ if (isFunction(pred))
11661
+ return pred;
11662
+ if (isString(pred))
11663
+ return (m) => m.get(pred);
11664
+ const matcher = matches(pred);
11665
+ return (m) => matcher(m.attributes);
11666
+ }
11530
11667
  /**
11531
11668
  * @public
11532
11669
  * If models tend to represent a single row of data, a Collection is
@@ -11855,17 +11992,13 @@
11855
11992
  return this.models.slice(start, end);
11856
11993
  }
11857
11994
  filter(callback, thisArg) {
11858
- return this.models.filter(isFunction(callback)
11859
- ? callback
11860
- : (m) => m.matches(callback), thisArg);
11995
+ return this.models.filter(toModelPredicate(callback), thisArg);
11861
11996
  }
11862
11997
  every(pred) {
11863
11998
  if (isFunction(pred)) {
11864
- return this.models.map((m) => m.attributes).every(pred);
11865
- }
11866
- else {
11867
- return this.models.every((m) => m.matches(pred));
11999
+ return this.models.every((m) => pred(m.attributes));
11868
12000
  }
12001
+ return this.models.every(toModelPredicate(pred));
11869
12002
  }
11870
12003
  difference(values) {
11871
12004
  return this.models.filter((m) => !values.includes(m));
@@ -11881,16 +12014,12 @@
11881
12014
  }
11882
12015
  some(pred) {
11883
12016
  if (isFunction(pred)) {
11884
- return this.models.map((m) => m.attributes).some(pred);
11885
- }
11886
- else {
11887
- return this.models.some((m) => m.matches(pred));
12017
+ return this.models.some((m) => pred(m.attributes));
11888
12018
  }
12019
+ return this.models.some(toModelPredicate(pred));
11889
12020
  }
11890
12021
  sortBy(iteratee) {
11891
- return sortBy(this.models, isFunction(iteratee)
11892
- ? iteratee
11893
- : (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
12022
+ return sortBy(this.models, toModelPredicate(iteratee));
11894
12023
  }
11895
12024
  isEmpty() {
11896
12025
  return !this.models.length;
@@ -11911,26 +12040,22 @@
11911
12040
  return this.models.length;
11912
12041
  }
11913
12042
  countBy(f) {
11914
- return countBy(this.models, isFunction(f) ? f : (m) => (isString(f) ? m.get(f) : m.matches(f)));
12043
+ return countBy(this.models, toModelPredicate(f));
11915
12044
  }
11916
12045
  groupBy(pred) {
11917
- return groupBy(this.models, isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12046
+ return groupBy(this.models, toModelPredicate(pred));
11918
12047
  }
11919
12048
  indexOf(model, fromIndex) {
11920
12049
  return this.models.indexOf(model, fromIndex);
11921
12050
  }
11922
12051
  findLastIndex(pred, fromIndex) {
11923
- return this.models.findLastIndex(isFunction(pred)
11924
- ? pred
11925
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
12052
+ return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
11926
12053
  }
11927
12054
  lastIndexOf(model, fromIndex) {
11928
12055
  return this.models.lastIndexOf(model, fromIndex);
11929
12056
  }
11930
12057
  findIndex(pred) {
11931
- return this.models.findIndex(isFunction(pred)
11932
- ? pred
11933
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12058
+ return this.models.findIndex(toModelPredicate(pred));
11934
12059
  }
11935
12060
  last() {
11936
12061
  const length = this.models == null ? 0 : this.models.length;
@@ -11943,9 +12068,7 @@
11943
12068
  return this.head();
11944
12069
  }
11945
12070
  map(cb, thisArg) {
11946
- return this.models.map(isFunction(cb)
11947
- ? cb
11948
- : (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
12071
+ return this.models.map(toModelPredicate(cb), thisArg);
11949
12072
  }
11950
12073
  reduce(callback, initialValue) {
11951
12074
  return this.models.reduce(callback, initialValue || this.models[0]);
@@ -11996,10 +12119,7 @@
11996
12119
  return this.where(attrs, true);
11997
12120
  }
11998
12121
  find(predicate, fromIndex) {
11999
- const pred = isFunction(predicate)
12000
- ? predicate
12001
- : (m) => m.matches(predicate);
12002
- return this.models.find(pred, fromIndex);
12122
+ return this.models.find(toModelPredicate(predicate), fromIndex);
12003
12123
  }
12004
12124
  /**
12005
12125
  * Force the collection to re-sort itself. You don't need to call this under
@@ -12042,46 +12162,22 @@
12042
12162
  // eslint-disable-next-line @typescript-eslint/no-this-alias
12043
12163
  const collection = this;
12044
12164
  const promise = options.promise ? getResolveablePromise() : undefined;
12045
- // Tracks whether sync invoked one of the callbacks. A sync may instead
12046
- // settle its returned promise; the `.then`/`.catch` below bridges that to
12047
- // the callbacks so the fetch promise never hangs. The flag is set
12048
- // synchronously inside each callback (before sync's promise resolves), so
12049
- // it's reliable by the time those handlers run.
12050
- let settled = false;
12051
12165
  options.success = function (resp) {
12052
- settled = true;
12053
12166
  const method = options.reset ? 'reset' : 'set';
12054
12167
  // fromStorage prevents individual model set() calls from triggering auto-save on reads
12055
12168
  collection[method](resp, Object.assign({}, options, { fromStorage: true }));
12056
12169
  if (success)
12057
12170
  success.call(options.context, collection, resp, options);
12058
- promise && promise.resolve();
12171
+ // Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
12172
+ // listener rejects the promise instead of being lost.
12059
12173
  collection.trigger('sync', collection, resp, options);
12174
+ promise && promise.resolve();
12060
12175
  };
12061
12176
  wrapError(this, options, promise);
12062
- if (promise) {
12063
- const onError = options.error;
12064
- options.error = (resp) => {
12065
- settled = true;
12066
- onError(resp);
12067
- };
12068
- }
12069
- const result = this.sync('read', this, options);
12070
- if (promise) {
12071
- Promise.resolve(result).then(
12072
- // A sync that resolves its promise without invoking options.success:
12073
- // treat the resolved value as the response, so the collection is still
12074
- // populated and the promise settles instead of hanging.
12075
- (value) => {
12076
- if (!settled)
12077
- options.success(value);
12078
- },
12079
- // A sync that rejects instead of invoking options.error; settling
12080
- // twice is a harmless no-op.
12081
- (e) => promise.reject(e));
12082
- return promise;
12083
- }
12084
- return result;
12177
+ // With `promise`, the sync's outcome is bridged to it however the sync
12178
+ // reports it: through the callbacks, or through the promise it returns.
12179
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
12180
+ return promise ?? result;
12085
12181
  }
12086
12182
  /**
12087
12183
  * Create a new instance of a model in this collection. Add the model to the
@@ -12093,9 +12189,22 @@
12093
12189
  const wait = options.wait;
12094
12190
  const return_promise = options.promise;
12095
12191
  const promise = return_promise && getResolveablePromise();
12192
+ // `_prepareModel` reports attributes that fail validation by firing
12193
+ // `'invalid'` and returning null. With `promise`, capture that error so a
12194
+ // rejected promise is handed back rather than `null`.
12195
+ let validationError;
12196
+ const captureInvalid = (_c, err) => (validationError = err);
12197
+ if (return_promise)
12198
+ this.on('invalid', captureInvalid);
12096
12199
  const preparedModel = this._prepareModel(model, options);
12097
- if (!preparedModel)
12098
- return null;
12200
+ if (return_promise)
12201
+ this.off('invalid', captureInvalid);
12202
+ if (!preparedModel) {
12203
+ if (!return_promise)
12204
+ return null;
12205
+ promise.reject(validationError);
12206
+ return promise;
12207
+ }
12099
12208
  if (!wait)
12100
12209
  this.add(preparedModel, options);
12101
12210
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -12117,8 +12226,15 @@
12117
12226
  error && error.call(options.context, model, e, options);
12118
12227
  return_promise && promise.reject(e);
12119
12228
  };
12120
- preparedModel.save(null, Object.assign(options, { 'promise': false }));
12229
+ // `options.promise` is passed through, so `save` installs its own bridge
12230
+ // from the sync's returned promise to its callbacks: a sync that resolves
12231
+ // without calling them still reaches the wired `options.success` above,
12232
+ // which resolves our promise with the model. Only a rejection that bypassed
12233
+ // the callbacks (a rejecting sync, or a throwing `'sync'`/`'error'`
12234
+ // listener) still needs bridging to ours.
12235
+ const result = preparedModel.save(null, options);
12121
12236
  if (return_promise) {
12237
+ Promise.resolve(result).catch((e) => promise.reject(e));
12122
12238
  return promise;
12123
12239
  }
12124
12240
  else {
@@ -12344,6 +12460,7 @@
12344
12460
  EventEmitter,
12345
12461
  Model,
12346
12462
  sync,
12463
+ HTTPError,
12347
12464
  };
12348
12465
  // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
12349
12466
  // We use `self` instead of `window` for `WebWorker` support.
@@ -12374,6 +12491,7 @@
12374
12491
  exports.BrowserStorage = BrowserStorage;
12375
12492
  exports.Collection = Collection;
12376
12493
  exports.EventEmitter = EventEmitter;
12494
+ exports.HTTPError = HTTPError;
12377
12495
  exports.Model = Model;
12378
12496
  exports.PersistentStorage = PersistentStorage;
12379
12497
  exports.default = skeletor;