@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.
@@ -8471,6 +8471,16 @@ function guid() {
8471
8471
  // Generate a pseudo-GUID by concatenating random hexadecimal.
8472
8472
  return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
8473
8473
  }
8474
+ // Helpers
8475
+ // -------
8476
+ /**
8477
+ * Whether `value` is a thenable (a promise, or a promise-like object such as
8478
+ * a jqXHR). Decides whether a `sync` return value can be bridged to the
8479
+ * promise that `fetch`/`save` hand back.
8480
+ */
8481
+ function isThenable(value) {
8482
+ return value != null && typeof value.then === 'function';
8483
+ }
8474
8484
  function getResolveablePromise() {
8475
8485
  const wrapper = {
8476
8486
  isResolved: false,
@@ -8515,6 +8525,65 @@ function wrapError(model, options, promise) {
8515
8525
  promise && promise.reject && promise.reject(resp);
8516
8526
  };
8517
8527
  }
8528
+ /**
8529
+ * Run a `sync` and make sure `promise` settles from its outcome, however the
8530
+ * sync reports it.
8531
+ *
8532
+ * A `sync` may invoke `options.success`/`options.error` (the callback
8533
+ * contract), or settle the promise it returns without calling them: a
8534
+ * promise-only custom sync, or an async sync whose promise rejects because a
8535
+ * `'sync'`/`'error'` listener threw after the callbacks ran. `options.success`
8536
+ * and `options.error` must already be wired to resolve/reject `promise`. This
8537
+ * wraps them to record that they ran, calls `run()`, and bridges a thenable
8538
+ * result to them so the promise never hangs:
8539
+ *
8540
+ * - fulfilment with the callbacks unrun is delivered to `options.success`, so
8541
+ * the response is still merged into the model or collection;
8542
+ * - rejection with the callbacks unrun is delivered to `options.error`, so the
8543
+ * caller's `error` callback and the `'error'` event still fire;
8544
+ * - rejection after the callbacks ran rejects `promise` directly: a no-op if
8545
+ * they already settled it, and the only way to settle it if a listener threw
8546
+ * before they could;
8547
+ * - a throw from either delivery (a throwing `success`/`error` callback or
8548
+ * listener) rejects `promise` instead of surfacing as an unhandled rejection.
8549
+ *
8550
+ * A non-thenable result (a callback-only sync that returns nothing, or an
8551
+ * XHR) is left alone: such a sync settles `promise` through its callbacks, and
8552
+ * bridging it would fire `success` on the next microtask, before the sync is
8553
+ * done. Without a `promise`, `run()` is called as is.
8554
+ */
8555
+ function bridgeSyncToPromise(options, promise, run) {
8556
+ if (!promise)
8557
+ return run();
8558
+ let settled = false;
8559
+ const success = options.success;
8560
+ const error = options.error;
8561
+ options.success = (...args) => {
8562
+ settled = true;
8563
+ return success && success(...args);
8564
+ };
8565
+ options.error = (...args) => {
8566
+ settled = true;
8567
+ return error && error(...args);
8568
+ };
8569
+ const result = run();
8570
+ if (isThenable(result)) {
8571
+ Promise.resolve(result)
8572
+ .then((value) => {
8573
+ if (!settled)
8574
+ options.success(value);
8575
+ }, (e) => {
8576
+ if (settled) {
8577
+ promise.reject(e);
8578
+ }
8579
+ else {
8580
+ options.error(e);
8581
+ }
8582
+ })
8583
+ .catch((e) => promise.reject(e));
8584
+ }
8585
+ return result;
8586
+ }
8518
8587
  // Map from CRUD to HTTP for our default `sync` implementation.
8519
8588
  const methodMap = {
8520
8589
  create: 'POST',
@@ -8564,43 +8633,77 @@ function getSyncMethod(model) {
8564
8633
  return store ? store.sync() : sync;
8565
8634
  }
8566
8635
  /**
8567
- * Override this function to change the manner in which Backbone persists
8568
- * models to the server. You will be passed the type of request, and the
8569
- * model in question. By default makes a `fetch()` API call
8570
- * to the model's `url()`.
8571
- *
8572
- * Some possible customizations could be:
8573
- *
8574
- * - Use `setTimeout` to batch rapid-fire updates into a single request.
8575
- * - Persist models via WebSockets instead of Ajax.
8576
- * - Persist models to browser storage
8636
+ * The error the default `sync` hands to `options.error`, and rejects with, for
8637
+ * a response outside the 2xx range.
8638
+ * @public
8577
8639
  */
8640
+ class HTTPError extends Error {
8641
+ constructor(response, method, url) {
8642
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
8643
+ super(`${method} ${url} failed with HTTP ${response.status}${statusText}`);
8644
+ this.name = 'HTTPError';
8645
+ this.status = response.status;
8646
+ this.response = response;
8647
+ }
8648
+ }
8578
8649
  /**
8650
+ * The default `sync`: a JSON request over `fetch()` to the model's `url()`.
8651
+ * Override it (or `Model.sync`/`Collection.sync`) to change how models are
8652
+ * persisted, for instance to batch rapid-fire updates with `setTimeout`, to
8653
+ * talk over WebSockets, or to write to browser storage.
8654
+ *
8655
+ * It honours both contracts a `sync` may use. On a 2xx response the JSON body
8656
+ * (`null` when the body is empty) is passed to `options.success` and the
8657
+ * returned promise resolves with it. On a non-2xx response, a network failure
8658
+ * or an unparsable body, `options.error` receives the error (an `HTTPError`
8659
+ * for the first) and the promise rejects with it.
8660
+ *
8661
+ * `options.data` is sent as query parameters on GET, which may not carry a
8662
+ * body, and as a JSON body otherwise. For create, update and patch the body
8663
+ * defaults to `options.attrs` or the model's `toJSON()`.
8579
8664
  * @public
8580
8665
  */
8581
8666
  function sync(method, model, options = {}) {
8667
+ const type = methodMap[method];
8582
8668
  let data = options.data;
8583
8669
  // Ensure that we have the appropriate request data.
8584
8670
  if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
8585
8671
  data = options.attrs || model.toJSON();
8586
8672
  }
8587
- const type = methodMap[method];
8588
- const params = {
8589
- method: type,
8590
- body: data ? JSON.stringify(data) : '',
8591
- headers: {
8592
- 'Content-Type': 'application/json',
8593
- },
8594
- success: options.success,
8595
- error: options.error,
8596
- };
8597
- const url = options.url || result(model, 'url') || urlError();
8598
- const xhr = fetch(url, params);
8599
- if (options) {
8600
- options.xhr = xhr;
8673
+ let url = options.url || result(model, 'url') || urlError();
8674
+ const params = { method: type };
8675
+ if (data != null) {
8676
+ if (type === 'GET') {
8677
+ url += (url.includes('?') ? '&' : '?') + new URLSearchParams(data).toString();
8678
+ }
8679
+ else {
8680
+ params.body = JSON.stringify(data);
8681
+ params.headers = { 'Content-Type': 'application/json' };
8682
+ }
8601
8683
  }
8684
+ const xhr = fetch(url, params);
8685
+ options.xhr = xhr;
8602
8686
  model.trigger('request', model, xhr, { ...params, xhr });
8603
- return xhr;
8687
+ return Promise.resolve(xhr)
8688
+ .then(async (response) => {
8689
+ if (!response.ok)
8690
+ throw new HTTPError(response, type, url);
8691
+ const text = await response.text();
8692
+ return text ? JSON.parse(text) : null;
8693
+ })
8694
+ .then(
8695
+ // The callbacks run before the returned promise settles, so a caller that
8696
+ // bridges this promise (`Model.fetch`/`save`) sees the outcome as already
8697
+ // reported. A throw from `success` (a `'sync'` listener, say) is not
8698
+ // routed to `error`: it rejects the promise, which is what the bridge
8699
+ // expects once the callbacks have run.
8700
+ (resp) => {
8701
+ options.success?.(resp, options);
8702
+ return resp;
8703
+ }, (e) => {
8704
+ options.error?.(e);
8705
+ throw e;
8706
+ });
8604
8707
  }
8605
8708
 
8606
8709
  /**
@@ -9966,6 +10069,48 @@ var keyBy = createAggregator(function(result, value, key) {
9966
10069
  baseAssignValue(result, key, value);
9967
10070
  });
9968
10071
 
10072
+ /** Used to compose bitmasks for cloning. */
10073
+ var CLONE_DEEP_FLAG$1 = 1;
10074
+
10075
+ /**
10076
+ * Creates a function that performs a partial deep comparison between a given
10077
+ * object and `source`, returning `true` if the given object has equivalent
10078
+ * property values, else `false`.
10079
+ *
10080
+ * **Note:** The created function is equivalent to `_.isMatch` with `source`
10081
+ * partially applied.
10082
+ *
10083
+ * Partial comparisons will match empty array and empty object `source`
10084
+ * values against any array or object value, respectively. See `_.isEqual`
10085
+ * for a list of supported value comparisons.
10086
+ *
10087
+ * **Note:** Multiple values can be checked by combining several matchers
10088
+ * using `_.overSome`
10089
+ *
10090
+ * @static
10091
+ * @memberOf _
10092
+ * @since 3.0.0
10093
+ * @category Util
10094
+ * @param {Object} source The object of property values to match.
10095
+ * @returns {Function} Returns the new spec function.
10096
+ * @example
10097
+ *
10098
+ * var objects = [
10099
+ * { 'a': 1, 'b': 2, 'c': 3 },
10100
+ * { 'a': 4, 'b': 5, 'c': 6 }
10101
+ * ];
10102
+ *
10103
+ * _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
10104
+ * // => [{ 'a': 4, 'b': 5, 'c': 6 }]
10105
+ *
10106
+ * // Checking for several possible values
10107
+ * _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
10108
+ * // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
10109
+ */
10110
+ function matches(source) {
10111
+ return baseMatches(baseClone(source, CLONE_DEEP_FLAG$1));
10112
+ }
10113
+
9969
10114
  /** Built-in value references. */
9970
10115
  var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
9971
10116
 
@@ -10529,53 +10674,36 @@ function isEqual(value, other) {
10529
10674
  return baseIsEqual(value, other);
10530
10675
  }
10531
10676
 
10532
- /** Used to compose bitmasks for cloning. */
10533
- var CLONE_DEEP_FLAG$1 = 1;
10534
-
10535
10677
  /**
10536
- * Creates a function that invokes `func` with the arguments of the created
10537
- * function. If `func` is a property name, the created function returns the
10538
- * property value for a given element. If `func` is an array or object, the
10539
- * created function returns `true` for elements that contain the equivalent
10540
- * source properties, otherwise it returns `false`.
10678
+ * Performs a partial deep comparison between `object` and `source` to
10679
+ * determine if `object` contains equivalent property values.
10680
+ *
10681
+ * **Note:** This method is equivalent to `_.matches` when `source` is
10682
+ * partially applied.
10683
+ *
10684
+ * Partial comparisons will match empty array and empty object `source`
10685
+ * values against any array or object value, respectively. See `_.isEqual`
10686
+ * for a list of supported value comparisons.
10541
10687
  *
10542
10688
  * @static
10543
- * @since 4.0.0
10544
10689
  * @memberOf _
10545
- * @category Util
10546
- * @param {*} [func=_.identity] The value to convert to a callback.
10547
- * @returns {Function} Returns the callback.
10690
+ * @since 3.0.0
10691
+ * @category Lang
10692
+ * @param {Object} object The object to inspect.
10693
+ * @param {Object} source The object of property values to match.
10694
+ * @returns {boolean} Returns `true` if `object` is a match, else `false`.
10548
10695
  * @example
10549
10696
  *
10550
- * var users = [
10551
- * { 'user': 'barney', 'age': 36, 'active': true },
10552
- * { 'user': 'fred', 'age': 40, 'active': false }
10553
- * ];
10554
- *
10555
- * // The `_.matches` iteratee shorthand.
10556
- * _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
10557
- * // => [{ 'user': 'barney', 'age': 36, 'active': true }]
10558
- *
10559
- * // The `_.matchesProperty` iteratee shorthand.
10560
- * _.filter(users, _.iteratee(['user', 'fred']));
10561
- * // => [{ 'user': 'fred', 'age': 40 }]
10697
+ * var object = { 'a': 1, 'b': 2 };
10562
10698
  *
10563
- * // The `_.property` iteratee shorthand.
10564
- * _.map(users, _.iteratee('user'));
10565
- * // => ['barney', 'fred']
10566
- *
10567
- * // Create custom iteratee shorthands.
10568
- * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
10569
- * return !_.isRegExp(func) ? iteratee(func) : function(string) {
10570
- * return func.test(string);
10571
- * };
10572
- * });
10699
+ * _.isMatch(object, { 'b': 2 });
10700
+ * // => true
10573
10701
  *
10574
- * _.filter(['abc', 'def'], /ef/);
10575
- * // => ['def']
10702
+ * _.isMatch(object, { 'b': 1 });
10703
+ * // => false
10576
10704
  */
10577
- function iteratee(func) {
10578
- return baseIteratee(typeof func == 'function' ? func : baseClone(func, CLONE_DEEP_FLAG$1));
10705
+ function isMatch(object, source) {
10706
+ return object === source || baseIsMatch(object, source, getMatchData(source));
10579
10707
  }
10580
10708
 
10581
10709
  /**
@@ -11089,10 +11217,20 @@ class Model extends EventEmitterObject {
11089
11217
  return this.get(attr) != null;
11090
11218
  }
11091
11219
  /**
11092
- * Special-cased proxy to lodash's `matches` method.
11220
+ * Returns `true` if the model's attributes satisfy `attrs`.
11221
+ *
11222
+ * An attributes object is compared with lodash's `isMatch`: a partial, deep
11223
+ * comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
11224
+ * that has `type: 'hidden'` alongside other keys. A function is called with
11225
+ * the attributes and its result coerced to a boolean.
11226
+ *
11227
+ * To scan a collection, prefer `Collection#find`, `filter` or `where`. They
11228
+ * build their matcher once per scan instead of once per model.
11093
11229
  */
11094
11230
  matches(attrs) {
11095
- return !!iteratee(attrs)(this.attributes);
11231
+ if (typeof attrs === 'function')
11232
+ return !!attrs(this.attributes);
11233
+ return isMatch(this.attributes, attrs);
11096
11234
  }
11097
11235
  /**
11098
11236
  * Set a hash of model attributes on the object, firing `"change"`. This is
@@ -11265,47 +11403,25 @@ class Model extends EventEmitterObject {
11265
11403
  options = Object.assign({ parse: true }, options);
11266
11404
  const success = options.success;
11267
11405
  const promise = options.promise ? getResolveablePromise() : undefined;
11268
- // Tracks whether sync invoked one of the callbacks. A sync may instead
11269
- // settle its returned promise; the `.then`/`.catch` below bridges that to
11270
- // the callbacks so the fetch promise never hangs. The flag is set
11271
- // synchronously inside each callback (before sync's promise resolves), so
11272
- // it's reliable by the time those handlers run.
11273
- let settled = false;
11274
11406
  options.success = (resp) => {
11275
- settled = true;
11276
11407
  const serverAttrs = options.parse ? this.parse(resp, options) : resp;
11277
11408
  // fromStorage prevents the set() from triggering auto-save on reads
11278
- if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
11409
+ if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
11410
+ // The fetched attributes failed validation (`'invalid'` has fired).
11411
+ // Settle the promise with the error rather than leave it pending.
11412
+ promise && promise.reject(this.validationError);
11279
11413
  return false;
11414
+ }
11280
11415
  if (success)
11281
11416
  success.call(options.context, this, resp, options);
11282
11417
  this.trigger('sync', this, resp, options);
11283
11418
  promise && promise.resolve(resp);
11284
11419
  };
11285
11420
  wrapError(this, options, promise);
11286
- if (promise) {
11287
- const onError = options.error;
11288
- options.error = (resp) => {
11289
- settled = true;
11290
- onError(resp);
11291
- };
11292
- }
11293
- const result = this.sync('read', this, options);
11294
- if (promise) {
11295
- Promise.resolve(result).then(
11296
- // A sync that resolves its promise without invoking options.success:
11297
- // treat the resolved value as the response, so the model is still
11298
- // merged and the promise settles instead of hanging.
11299
- (value) => {
11300
- if (!settled)
11301
- options.success(value);
11302
- },
11303
- // A sync that rejects instead of invoking options.error; settling
11304
- // twice is a harmless no-op.
11305
- (e) => promise.reject(e));
11306
- return promise;
11307
- }
11308
- return result;
11421
+ // With `promise`, the sync's outcome is bridged to it however the sync
11422
+ // reports it: through the callbacks, or through the promise it returns.
11423
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
11424
+ return promise ?? result;
11309
11425
  }
11310
11426
  /**
11311
11427
  * Set a hash of model attributes, and sync the model to the server.
@@ -11326,16 +11442,19 @@ class Model extends EventEmitterObject {
11326
11442
  options = Object.assign({ validate: true, parse: true }, options);
11327
11443
  const wait = options.wait;
11328
11444
  const return_promise = options.promise;
11329
- const promise = return_promise && getResolveablePromise();
11445
+ const promise = return_promise ? getResolveablePromise() : undefined;
11330
11446
  // If we're not waiting and attributes exist, save acts as
11331
11447
  // `set(attr).save(null, opts)` with validation. Otherwise, check if
11332
11448
  // the model will be valid when the attributes, if any, are set.
11333
- if (attrs && !wait) {
11334
- if (!this.set(attrs, options))
11449
+ const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
11450
+ if (!valid) {
11451
+ // Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
11452
+ // rejected promise rather than `false`, so the promise contract holds
11453
+ // for every outcome.
11454
+ if (!promise)
11335
11455
  return false;
11336
- }
11337
- else if (!this._validate(attrs || {}, options)) {
11338
- return false;
11456
+ promise.reject(this.validationError);
11457
+ return promise;
11339
11458
  }
11340
11459
  // After a successful server-side save, the client is (optionally)
11341
11460
  // updated with the server-side state.
@@ -11348,8 +11467,12 @@ class Model extends EventEmitterObject {
11348
11467
  let serverAttrs = options.parse ? this.parse(resp, options) : resp;
11349
11468
  if (wait)
11350
11469
  serverAttrs = Object.assign({}, attrs, serverAttrs);
11351
- if (serverAttrs && !this.set(serverAttrs, options))
11470
+ if (serverAttrs && !this.set(serverAttrs, options)) {
11471
+ // The server attributes failed validation (`'invalid'` has fired).
11472
+ // Settle the promise with the error rather than leave it pending.
11473
+ return_promise && promise.reject(this.validationError);
11352
11474
  return false;
11475
+ }
11353
11476
  if (success)
11354
11477
  success.call(options.context, this, resp, options);
11355
11478
  this.trigger('sync', this, resp, options);
@@ -11366,15 +11489,12 @@ class Model extends EventEmitterObject {
11366
11489
  const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
11367
11490
  if (method === 'patch' && !options.attrs)
11368
11491
  options.attrs = attrs;
11369
- const xhr = this.sync(method, this, options);
11492
+ // With `promise`, the sync's outcome is bridged to it however the sync
11493
+ // reports it: through the callbacks, or through the promise it returns.
11494
+ const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
11370
11495
  // Restore attributes.
11371
11496
  this.attributes = attributes;
11372
- if (return_promise) {
11373
- return promise;
11374
- }
11375
- else {
11376
- return xhr;
11377
- }
11497
+ return promise ?? xhr;
11378
11498
  }
11379
11499
  /**
11380
11500
  * Destroy this model on the server if it was already persisted.
@@ -11521,6 +11641,23 @@ var _Collection_instances, _Collection_hydrate, _a;
11521
11641
  // Default options for `Collection#set`.
11522
11642
  const setOptions = { add: true, remove: true, merge: true };
11523
11643
  const addOptions = { add: true, remove: false };
11644
+ /**
11645
+ * Normalizes the predicate forms accepted by the query methods into one
11646
+ * `(model) => value` function. A function is used as-is, a string reads that
11647
+ * attribute, and an attributes object becomes a lodash `matches` matcher.
11648
+ *
11649
+ * The matcher is built here, once per scan. Calling `Model#matches` per model
11650
+ * instead rebuilds the match data for every model visited, which made
11651
+ * `findWhere` an order of magnitude slower than a hand-written predicate.
11652
+ */
11653
+ function toModelPredicate(pred) {
11654
+ if (isFunction(pred))
11655
+ return pred;
11656
+ if (isString(pred))
11657
+ return (m) => m.get(pred);
11658
+ const matcher = matches(pred);
11659
+ return (m) => matcher(m.attributes);
11660
+ }
11524
11661
  /**
11525
11662
  * @public
11526
11663
  * If models tend to represent a single row of data, a Collection is
@@ -11849,17 +11986,13 @@ class Collection extends EventEmitterObject {
11849
11986
  return this.models.slice(start, end);
11850
11987
  }
11851
11988
  filter(callback, thisArg) {
11852
- return this.models.filter(isFunction(callback)
11853
- ? callback
11854
- : (m) => m.matches(callback), thisArg);
11989
+ return this.models.filter(toModelPredicate(callback), thisArg);
11855
11990
  }
11856
11991
  every(pred) {
11857
11992
  if (isFunction(pred)) {
11858
- return this.models.map((m) => m.attributes).every(pred);
11859
- }
11860
- else {
11861
- return this.models.every((m) => m.matches(pred));
11993
+ return this.models.every((m) => pred(m.attributes));
11862
11994
  }
11995
+ return this.models.every(toModelPredicate(pred));
11863
11996
  }
11864
11997
  difference(values) {
11865
11998
  return this.models.filter((m) => !values.includes(m));
@@ -11875,16 +12008,12 @@ class Collection extends EventEmitterObject {
11875
12008
  }
11876
12009
  some(pred) {
11877
12010
  if (isFunction(pred)) {
11878
- return this.models.map((m) => m.attributes).some(pred);
11879
- }
11880
- else {
11881
- return this.models.some((m) => m.matches(pred));
12011
+ return this.models.some((m) => pred(m.attributes));
11882
12012
  }
12013
+ return this.models.some(toModelPredicate(pred));
11883
12014
  }
11884
12015
  sortBy(iteratee) {
11885
- return sortBy(this.models, isFunction(iteratee)
11886
- ? iteratee
11887
- : (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
12016
+ return sortBy(this.models, toModelPredicate(iteratee));
11888
12017
  }
11889
12018
  isEmpty() {
11890
12019
  return !this.models.length;
@@ -11905,26 +12034,22 @@ class Collection extends EventEmitterObject {
11905
12034
  return this.models.length;
11906
12035
  }
11907
12036
  countBy(f) {
11908
- return countBy(this.models, isFunction(f) ? f : (m) => (isString(f) ? m.get(f) : m.matches(f)));
12037
+ return countBy(this.models, toModelPredicate(f));
11909
12038
  }
11910
12039
  groupBy(pred) {
11911
- return groupBy(this.models, isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12040
+ return groupBy(this.models, toModelPredicate(pred));
11912
12041
  }
11913
12042
  indexOf(model, fromIndex) {
11914
12043
  return this.models.indexOf(model, fromIndex);
11915
12044
  }
11916
12045
  findLastIndex(pred, fromIndex) {
11917
- return this.models.findLastIndex(isFunction(pred)
11918
- ? pred
11919
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
12046
+ return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
11920
12047
  }
11921
12048
  lastIndexOf(model, fromIndex) {
11922
12049
  return this.models.lastIndexOf(model, fromIndex);
11923
12050
  }
11924
12051
  findIndex(pred) {
11925
- return this.models.findIndex(isFunction(pred)
11926
- ? pred
11927
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12052
+ return this.models.findIndex(toModelPredicate(pred));
11928
12053
  }
11929
12054
  last() {
11930
12055
  const length = this.models == null ? 0 : this.models.length;
@@ -11937,9 +12062,7 @@ class Collection extends EventEmitterObject {
11937
12062
  return this.head();
11938
12063
  }
11939
12064
  map(cb, thisArg) {
11940
- return this.models.map(isFunction(cb)
11941
- ? cb
11942
- : (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
12065
+ return this.models.map(toModelPredicate(cb), thisArg);
11943
12066
  }
11944
12067
  reduce(callback, initialValue) {
11945
12068
  return this.models.reduce(callback, initialValue || this.models[0]);
@@ -11990,10 +12113,7 @@ class Collection extends EventEmitterObject {
11990
12113
  return this.where(attrs, true);
11991
12114
  }
11992
12115
  find(predicate, fromIndex) {
11993
- const pred = isFunction(predicate)
11994
- ? predicate
11995
- : (m) => m.matches(predicate);
11996
- return this.models.find(pred, fromIndex);
12116
+ return this.models.find(toModelPredicate(predicate), fromIndex);
11997
12117
  }
11998
12118
  /**
11999
12119
  * Force the collection to re-sort itself. You don't need to call this under
@@ -12036,46 +12156,22 @@ class Collection extends EventEmitterObject {
12036
12156
  // eslint-disable-next-line @typescript-eslint/no-this-alias
12037
12157
  const collection = this;
12038
12158
  const promise = options.promise ? getResolveablePromise() : undefined;
12039
- // Tracks whether sync invoked one of the callbacks. A sync may instead
12040
- // settle its returned promise; the `.then`/`.catch` below bridges that to
12041
- // the callbacks so the fetch promise never hangs. The flag is set
12042
- // synchronously inside each callback (before sync's promise resolves), so
12043
- // it's reliable by the time those handlers run.
12044
- let settled = false;
12045
12159
  options.success = function (resp) {
12046
- settled = true;
12047
12160
  const method = options.reset ? 'reset' : 'set';
12048
12161
  // fromStorage prevents individual model set() calls from triggering auto-save on reads
12049
12162
  collection[method](resp, Object.assign({}, options, { fromStorage: true }));
12050
12163
  if (success)
12051
12164
  success.call(options.context, collection, resp, options);
12052
- promise && promise.resolve();
12165
+ // Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
12166
+ // listener rejects the promise instead of being lost.
12053
12167
  collection.trigger('sync', collection, resp, options);
12168
+ promise && promise.resolve();
12054
12169
  };
12055
12170
  wrapError(this, options, promise);
12056
- if (promise) {
12057
- const onError = options.error;
12058
- options.error = (resp) => {
12059
- settled = true;
12060
- onError(resp);
12061
- };
12062
- }
12063
- const result = this.sync('read', this, options);
12064
- if (promise) {
12065
- Promise.resolve(result).then(
12066
- // A sync that resolves its promise without invoking options.success:
12067
- // treat the resolved value as the response, so the collection is still
12068
- // populated and the promise settles instead of hanging.
12069
- (value) => {
12070
- if (!settled)
12071
- options.success(value);
12072
- },
12073
- // A sync that rejects instead of invoking options.error; settling
12074
- // twice is a harmless no-op.
12075
- (e) => promise.reject(e));
12076
- return promise;
12077
- }
12078
- return result;
12171
+ // With `promise`, the sync's outcome is bridged to it however the sync
12172
+ // reports it: through the callbacks, or through the promise it returns.
12173
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
12174
+ return promise ?? result;
12079
12175
  }
12080
12176
  /**
12081
12177
  * Create a new instance of a model in this collection. Add the model to the
@@ -12087,9 +12183,22 @@ class Collection extends EventEmitterObject {
12087
12183
  const wait = options.wait;
12088
12184
  const return_promise = options.promise;
12089
12185
  const promise = return_promise && getResolveablePromise();
12186
+ // `_prepareModel` reports attributes that fail validation by firing
12187
+ // `'invalid'` and returning null. With `promise`, capture that error so a
12188
+ // rejected promise is handed back rather than `null`.
12189
+ let validationError;
12190
+ const captureInvalid = (_c, err) => (validationError = err);
12191
+ if (return_promise)
12192
+ this.on('invalid', captureInvalid);
12090
12193
  const preparedModel = this._prepareModel(model, options);
12091
- if (!preparedModel)
12092
- return null;
12194
+ if (return_promise)
12195
+ this.off('invalid', captureInvalid);
12196
+ if (!preparedModel) {
12197
+ if (!return_promise)
12198
+ return null;
12199
+ promise.reject(validationError);
12200
+ return promise;
12201
+ }
12093
12202
  if (!wait)
12094
12203
  this.add(preparedModel, options);
12095
12204
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -12111,8 +12220,15 @@ class Collection extends EventEmitterObject {
12111
12220
  error && error.call(options.context, model, e, options);
12112
12221
  return_promise && promise.reject(e);
12113
12222
  };
12114
- preparedModel.save(null, Object.assign(options, { 'promise': false }));
12223
+ // `options.promise` is passed through, so `save` installs its own bridge
12224
+ // from the sync's returned promise to its callbacks: a sync that resolves
12225
+ // without calling them still reaches the wired `options.success` above,
12226
+ // which resolves our promise with the model. Only a rejection that bypassed
12227
+ // the callbacks (a rejecting sync, or a throwing `'sync'`/`'error'`
12228
+ // listener) still needs bridging to ours.
12229
+ const result = preparedModel.save(null, options);
12115
12230
  if (return_promise) {
12231
+ Promise.resolve(result).catch((e) => promise.reject(e));
12116
12232
  return promise;
12117
12233
  }
12118
12234
  else {
@@ -12338,6 +12454,7 @@ const skeletor = {
12338
12454
  EventEmitter,
12339
12455
  Model,
12340
12456
  sync,
12457
+ HTTPError,
12341
12458
  };
12342
12459
  // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
12343
12460
  // We use `self` instead of `window` for `WebWorker` support.
@@ -12365,5 +12482,5 @@ root.Skeletor = skeletor;
12365
12482
  */
12366
12483
  const BrowserStorage = PersistentStorage;
12367
12484
 
12368
- export { BrowserStorage, Collection, EventEmitter, Model, PersistentStorage, skeletor as default, getStorage, noConflict, sync };
12485
+ export { BrowserStorage, Collection, EventEmitter, HTTPError, Model, PersistentStorage, skeletor as default, getStorage, noConflict, sync };
12369
12486
  //# sourceMappingURL=skeletor.esm.js.map