@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.
@@ -7796,6 +7796,16 @@ function guid() {
7796
7796
  // Generate a pseudo-GUID by concatenating random hexadecimal.
7797
7797
  return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
7798
7798
  }
7799
+ // Helpers
7800
+ // -------
7801
+ /**
7802
+ * Whether `value` is a thenable (a promise, or a promise-like object such as
7803
+ * a jqXHR). Decides whether a `sync` return value can be bridged to the
7804
+ * promise that `fetch`/`save` hand back.
7805
+ */
7806
+ function isThenable(value) {
7807
+ return value != null && typeof value.then === 'function';
7808
+ }
7799
7809
  function getResolveablePromise() {
7800
7810
  const wrapper = {
7801
7811
  isResolved: false,
@@ -7840,6 +7850,65 @@ function wrapError(model, options, promise) {
7840
7850
  promise && promise.reject && promise.reject(resp);
7841
7851
  };
7842
7852
  }
7853
+ /**
7854
+ * Run a `sync` and make sure `promise` settles from its outcome, however the
7855
+ * sync reports it.
7856
+ *
7857
+ * A `sync` may invoke `options.success`/`options.error` (the callback
7858
+ * contract), or settle the promise it returns without calling them: a
7859
+ * promise-only custom sync, or an async sync whose promise rejects because a
7860
+ * `'sync'`/`'error'` listener threw after the callbacks ran. `options.success`
7861
+ * and `options.error` must already be wired to resolve/reject `promise`. This
7862
+ * wraps them to record that they ran, calls `run()`, and bridges a thenable
7863
+ * result to them so the promise never hangs:
7864
+ *
7865
+ * - fulfilment with the callbacks unrun is delivered to `options.success`, so
7866
+ * the response is still merged into the model or collection;
7867
+ * - rejection with the callbacks unrun is delivered to `options.error`, so the
7868
+ * caller's `error` callback and the `'error'` event still fire;
7869
+ * - rejection after the callbacks ran rejects `promise` directly: a no-op if
7870
+ * they already settled it, and the only way to settle it if a listener threw
7871
+ * before they could;
7872
+ * - a throw from either delivery (a throwing `success`/`error` callback or
7873
+ * listener) rejects `promise` instead of surfacing as an unhandled rejection.
7874
+ *
7875
+ * A non-thenable result (a callback-only sync that returns nothing, or an
7876
+ * XHR) is left alone: such a sync settles `promise` through its callbacks, and
7877
+ * bridging it would fire `success` on the next microtask, before the sync is
7878
+ * done. Without a `promise`, `run()` is called as is.
7879
+ */
7880
+ function bridgeSyncToPromise(options, promise, run) {
7881
+ if (!promise)
7882
+ return run();
7883
+ let settled = false;
7884
+ const success = options.success;
7885
+ const error = options.error;
7886
+ options.success = (...args) => {
7887
+ settled = true;
7888
+ return success && success(...args);
7889
+ };
7890
+ options.error = (...args) => {
7891
+ settled = true;
7892
+ return error && error(...args);
7893
+ };
7894
+ const result = run();
7895
+ if (isThenable(result)) {
7896
+ Promise.resolve(result)
7897
+ .then((value) => {
7898
+ if (!settled)
7899
+ options.success(value);
7900
+ }, (e) => {
7901
+ if (settled) {
7902
+ promise.reject(e);
7903
+ }
7904
+ else {
7905
+ options.error(e);
7906
+ }
7907
+ })
7908
+ .catch((e) => promise.reject(e));
7909
+ }
7910
+ return result;
7911
+ }
7843
7912
  // Map from CRUD to HTTP for our default `sync` implementation.
7844
7913
  const methodMap = {
7845
7914
  create: 'POST',
@@ -7889,43 +7958,77 @@ function getSyncMethod(model) {
7889
7958
  return store ? store.sync() : sync;
7890
7959
  }
7891
7960
  /**
7892
- * Override this function to change the manner in which Backbone persists
7893
- * models to the server. You will be passed the type of request, and the
7894
- * model in question. By default makes a `fetch()` API call
7895
- * to the model's `url()`.
7896
- *
7897
- * Some possible customizations could be:
7898
- *
7899
- * - Use `setTimeout` to batch rapid-fire updates into a single request.
7900
- * - Persist models via WebSockets instead of Ajax.
7901
- * - Persist models to browser storage
7961
+ * The error the default `sync` hands to `options.error`, and rejects with, for
7962
+ * a response outside the 2xx range.
7963
+ * @public
7902
7964
  */
7965
+ class HTTPError extends Error {
7966
+ constructor(response, method, url) {
7967
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
7968
+ super(`${method} ${url} failed with HTTP ${response.status}${statusText}`);
7969
+ this.name = 'HTTPError';
7970
+ this.status = response.status;
7971
+ this.response = response;
7972
+ }
7973
+ }
7903
7974
  /**
7975
+ * The default `sync`: a JSON request over `fetch()` to the model's `url()`.
7976
+ * Override it (or `Model.sync`/`Collection.sync`) to change how models are
7977
+ * persisted, for instance to batch rapid-fire updates with `setTimeout`, to
7978
+ * talk over WebSockets, or to write to browser storage.
7979
+ *
7980
+ * It honours both contracts a `sync` may use. On a 2xx response the JSON body
7981
+ * (`null` when the body is empty) is passed to `options.success` and the
7982
+ * returned promise resolves with it. On a non-2xx response, a network failure
7983
+ * or an unparsable body, `options.error` receives the error (an `HTTPError`
7984
+ * for the first) and the promise rejects with it.
7985
+ *
7986
+ * `options.data` is sent as query parameters on GET, which may not carry a
7987
+ * body, and as a JSON body otherwise. For create, update and patch the body
7988
+ * defaults to `options.attrs` or the model's `toJSON()`.
7904
7989
  * @public
7905
7990
  */
7906
7991
  function sync(method, model, options = {}) {
7992
+ const type = methodMap[method];
7907
7993
  let data = options.data;
7908
7994
  // Ensure that we have the appropriate request data.
7909
7995
  if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
7910
7996
  data = options.attrs || model.toJSON();
7911
7997
  }
7912
- const type = methodMap[method];
7913
- const params = {
7914
- method: type,
7915
- body: data ? JSON.stringify(data) : '',
7916
- headers: {
7917
- 'Content-Type': 'application/json',
7918
- },
7919
- success: options.success,
7920
- error: options.error,
7921
- };
7922
- const url = options.url || result(model, 'url') || urlError();
7923
- const xhr = fetch(url, params);
7924
- if (options) {
7925
- options.xhr = xhr;
7998
+ let url = options.url || result(model, 'url') || urlError();
7999
+ const params = { method: type };
8000
+ if (data != null) {
8001
+ if (type === 'GET') {
8002
+ url += (url.includes('?') ? '&' : '?') + new URLSearchParams(data).toString();
8003
+ }
8004
+ else {
8005
+ params.body = JSON.stringify(data);
8006
+ params.headers = { 'Content-Type': 'application/json' };
8007
+ }
7926
8008
  }
8009
+ const xhr = fetch(url, params);
8010
+ options.xhr = xhr;
7927
8011
  model.trigger('request', model, xhr, { ...params, xhr });
7928
- return xhr;
8012
+ return Promise.resolve(xhr)
8013
+ .then(async (response) => {
8014
+ if (!response.ok)
8015
+ throw new HTTPError(response, type, url);
8016
+ const text = await response.text();
8017
+ return text ? JSON.parse(text) : null;
8018
+ })
8019
+ .then(
8020
+ // The callbacks run before the returned promise settles, so a caller that
8021
+ // bridges this promise (`Model.fetch`/`save`) sees the outcome as already
8022
+ // reported. A throw from `success` (a `'sync'` listener, say) is not
8023
+ // routed to `error`: it rejects the promise, which is what the bridge
8024
+ // expects once the callbacks have run.
8025
+ (resp) => {
8026
+ options.success?.(resp, options);
8027
+ return resp;
8028
+ }, (e) => {
8029
+ options.error?.(e);
8030
+ throw e;
8031
+ });
7929
8032
  }
7930
8033
 
7931
8034
  /**
@@ -10119,6 +10222,48 @@ var keyBy = createAggregator(function(result, value, key) {
10119
10222
  baseAssignValue(result, key, value);
10120
10223
  });
10121
10224
 
10225
+ /** Used to compose bitmasks for cloning. */
10226
+ var CLONE_DEEP_FLAG$1 = 1;
10227
+
10228
+ /**
10229
+ * Creates a function that performs a partial deep comparison between a given
10230
+ * object and `source`, returning `true` if the given object has equivalent
10231
+ * property values, else `false`.
10232
+ *
10233
+ * **Note:** The created function is equivalent to `_.isMatch` with `source`
10234
+ * partially applied.
10235
+ *
10236
+ * Partial comparisons will match empty array and empty object `source`
10237
+ * values against any array or object value, respectively. See `_.isEqual`
10238
+ * for a list of supported value comparisons.
10239
+ *
10240
+ * **Note:** Multiple values can be checked by combining several matchers
10241
+ * using `_.overSome`
10242
+ *
10243
+ * @static
10244
+ * @memberOf _
10245
+ * @since 3.0.0
10246
+ * @category Util
10247
+ * @param {Object} source The object of property values to match.
10248
+ * @returns {Function} Returns the new spec function.
10249
+ * @example
10250
+ *
10251
+ * var objects = [
10252
+ * { 'a': 1, 'b': 2, 'c': 3 },
10253
+ * { 'a': 4, 'b': 5, 'c': 6 }
10254
+ * ];
10255
+ *
10256
+ * _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
10257
+ * // => [{ 'a': 4, 'b': 5, 'c': 6 }]
10258
+ *
10259
+ * // Checking for several possible values
10260
+ * _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
10261
+ * // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
10262
+ */
10263
+ function matches(source) {
10264
+ return baseMatches(baseClone(source, CLONE_DEEP_FLAG$1));
10265
+ }
10266
+
10122
10267
  /** Built-in value references. */
10123
10268
  var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
10124
10269
 
@@ -10682,53 +10827,36 @@ function isEqual(value, other) {
10682
10827
  return baseIsEqual(value, other);
10683
10828
  }
10684
10829
 
10685
- /** Used to compose bitmasks for cloning. */
10686
- var CLONE_DEEP_FLAG$1 = 1;
10687
-
10688
10830
  /**
10689
- * Creates a function that invokes `func` with the arguments of the created
10690
- * function. If `func` is a property name, the created function returns the
10691
- * property value for a given element. If `func` is an array or object, the
10692
- * created function returns `true` for elements that contain the equivalent
10693
- * source properties, otherwise it returns `false`.
10831
+ * Performs a partial deep comparison between `object` and `source` to
10832
+ * determine if `object` contains equivalent property values.
10833
+ *
10834
+ * **Note:** This method is equivalent to `_.matches` when `source` is
10835
+ * partially applied.
10836
+ *
10837
+ * Partial comparisons will match empty array and empty object `source`
10838
+ * values against any array or object value, respectively. See `_.isEqual`
10839
+ * for a list of supported value comparisons.
10694
10840
  *
10695
10841
  * @static
10696
- * @since 4.0.0
10697
10842
  * @memberOf _
10698
- * @category Util
10699
- * @param {*} [func=_.identity] The value to convert to a callback.
10700
- * @returns {Function} Returns the callback.
10843
+ * @since 3.0.0
10844
+ * @category Lang
10845
+ * @param {Object} object The object to inspect.
10846
+ * @param {Object} source The object of property values to match.
10847
+ * @returns {boolean} Returns `true` if `object` is a match, else `false`.
10701
10848
  * @example
10702
10849
  *
10703
- * var users = [
10704
- * { 'user': 'barney', 'age': 36, 'active': true },
10705
- * { 'user': 'fred', 'age': 40, 'active': false }
10706
- * ];
10707
- *
10708
- * // The `_.matches` iteratee shorthand.
10709
- * _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
10710
- * // => [{ 'user': 'barney', 'age': 36, 'active': true }]
10711
- *
10712
- * // The `_.matchesProperty` iteratee shorthand.
10713
- * _.filter(users, _.iteratee(['user', 'fred']));
10714
- * // => [{ 'user': 'fred', 'age': 40 }]
10850
+ * var object = { 'a': 1, 'b': 2 };
10715
10851
  *
10716
- * // The `_.property` iteratee shorthand.
10717
- * _.map(users, _.iteratee('user'));
10718
- * // => ['barney', 'fred']
10719
- *
10720
- * // Create custom iteratee shorthands.
10721
- * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
10722
- * return !_.isRegExp(func) ? iteratee(func) : function(string) {
10723
- * return func.test(string);
10724
- * };
10725
- * });
10852
+ * _.isMatch(object, { 'b': 2 });
10853
+ * // => true
10726
10854
  *
10727
- * _.filter(['abc', 'def'], /ef/);
10728
- * // => ['def']
10855
+ * _.isMatch(object, { 'b': 1 });
10856
+ * // => false
10729
10857
  */
10730
- function iteratee(func) {
10731
- return baseIteratee(typeof func == 'function' ? func : baseClone(func, CLONE_DEEP_FLAG$1));
10858
+ function isMatch(object, source) {
10859
+ return object === source || baseIsMatch(object, source, getMatchData(source));
10732
10860
  }
10733
10861
 
10734
10862
  /**
@@ -11242,10 +11370,20 @@ class Model extends EventEmitterObject {
11242
11370
  return this.get(attr) != null;
11243
11371
  }
11244
11372
  /**
11245
- * Special-cased proxy to lodash's `matches` method.
11373
+ * Returns `true` if the model's attributes satisfy `attrs`.
11374
+ *
11375
+ * An attributes object is compared with lodash's `isMatch`: a partial, deep
11376
+ * comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
11377
+ * that has `type: 'hidden'` alongside other keys. A function is called with
11378
+ * the attributes and its result coerced to a boolean.
11379
+ *
11380
+ * To scan a collection, prefer `Collection#find`, `filter` or `where`. They
11381
+ * build their matcher once per scan instead of once per model.
11246
11382
  */
11247
11383
  matches(attrs) {
11248
- return !!iteratee(attrs)(this.attributes);
11384
+ if (typeof attrs === 'function')
11385
+ return !!attrs(this.attributes);
11386
+ return isMatch(this.attributes, attrs);
11249
11387
  }
11250
11388
  /**
11251
11389
  * Set a hash of model attributes on the object, firing `"change"`. This is
@@ -11418,47 +11556,25 @@ class Model extends EventEmitterObject {
11418
11556
  options = Object.assign({ parse: true }, options);
11419
11557
  const success = options.success;
11420
11558
  const promise = options.promise ? getResolveablePromise() : undefined;
11421
- // Tracks whether sync invoked one of the callbacks. A sync may instead
11422
- // settle its returned promise; the `.then`/`.catch` below bridges that to
11423
- // the callbacks so the fetch promise never hangs. The flag is set
11424
- // synchronously inside each callback (before sync's promise resolves), so
11425
- // it's reliable by the time those handlers run.
11426
- let settled = false;
11427
11559
  options.success = (resp) => {
11428
- settled = true;
11429
11560
  const serverAttrs = options.parse ? this.parse(resp, options) : resp;
11430
11561
  // fromStorage prevents the set() from triggering auto-save on reads
11431
- if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
11562
+ if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
11563
+ // The fetched attributes failed validation (`'invalid'` has fired).
11564
+ // Settle the promise with the error rather than leave it pending.
11565
+ promise && promise.reject(this.validationError);
11432
11566
  return false;
11567
+ }
11433
11568
  if (success)
11434
11569
  success.call(options.context, this, resp, options);
11435
11570
  this.trigger('sync', this, resp, options);
11436
11571
  promise && promise.resolve(resp);
11437
11572
  };
11438
11573
  wrapError(this, options, promise);
11439
- if (promise) {
11440
- const onError = options.error;
11441
- options.error = (resp) => {
11442
- settled = true;
11443
- onError(resp);
11444
- };
11445
- }
11446
- const result = this.sync('read', this, options);
11447
- if (promise) {
11448
- Promise.resolve(result).then(
11449
- // A sync that resolves its promise without invoking options.success:
11450
- // treat the resolved value as the response, so the model is still
11451
- // merged and the promise settles instead of hanging.
11452
- (value) => {
11453
- if (!settled)
11454
- options.success(value);
11455
- },
11456
- // A sync that rejects instead of invoking options.error; settling
11457
- // twice is a harmless no-op.
11458
- (e) => promise.reject(e));
11459
- return promise;
11460
- }
11461
- return result;
11574
+ // With `promise`, the sync's outcome is bridged to it however the sync
11575
+ // reports it: through the callbacks, or through the promise it returns.
11576
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
11577
+ return promise ?? result;
11462
11578
  }
11463
11579
  /**
11464
11580
  * Set a hash of model attributes, and sync the model to the server.
@@ -11479,16 +11595,19 @@ class Model extends EventEmitterObject {
11479
11595
  options = Object.assign({ validate: true, parse: true }, options);
11480
11596
  const wait = options.wait;
11481
11597
  const return_promise = options.promise;
11482
- const promise = return_promise && getResolveablePromise();
11598
+ const promise = return_promise ? getResolveablePromise() : undefined;
11483
11599
  // If we're not waiting and attributes exist, save acts as
11484
11600
  // `set(attr).save(null, opts)` with validation. Otherwise, check if
11485
11601
  // the model will be valid when the attributes, if any, are set.
11486
- if (attrs && !wait) {
11487
- if (!this.set(attrs, options))
11602
+ const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
11603
+ if (!valid) {
11604
+ // Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
11605
+ // rejected promise rather than `false`, so the promise contract holds
11606
+ // for every outcome.
11607
+ if (!promise)
11488
11608
  return false;
11489
- }
11490
- else if (!this._validate(attrs || {}, options)) {
11491
- return false;
11609
+ promise.reject(this.validationError);
11610
+ return promise;
11492
11611
  }
11493
11612
  // After a successful server-side save, the client is (optionally)
11494
11613
  // updated with the server-side state.
@@ -11501,8 +11620,12 @@ class Model extends EventEmitterObject {
11501
11620
  let serverAttrs = options.parse ? this.parse(resp, options) : resp;
11502
11621
  if (wait)
11503
11622
  serverAttrs = Object.assign({}, attrs, serverAttrs);
11504
- if (serverAttrs && !this.set(serverAttrs, options))
11623
+ if (serverAttrs && !this.set(serverAttrs, options)) {
11624
+ // The server attributes failed validation (`'invalid'` has fired).
11625
+ // Settle the promise with the error rather than leave it pending.
11626
+ return_promise && promise.reject(this.validationError);
11505
11627
  return false;
11628
+ }
11506
11629
  if (success)
11507
11630
  success.call(options.context, this, resp, options);
11508
11631
  this.trigger('sync', this, resp, options);
@@ -11519,15 +11642,12 @@ class Model extends EventEmitterObject {
11519
11642
  const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
11520
11643
  if (method === 'patch' && !options.attrs)
11521
11644
  options.attrs = attrs;
11522
- const xhr = this.sync(method, this, options);
11645
+ // With `promise`, the sync's outcome is bridged to it however the sync
11646
+ // reports it: through the callbacks, or through the promise it returns.
11647
+ const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
11523
11648
  // Restore attributes.
11524
11649
  this.attributes = attributes;
11525
- if (return_promise) {
11526
- return promise;
11527
- }
11528
- else {
11529
- return xhr;
11530
- }
11650
+ return promise ?? xhr;
11531
11651
  }
11532
11652
  /**
11533
11653
  * Destroy this model on the server if it was already persisted.
@@ -11674,6 +11794,23 @@ var _Collection_instances, _Collection_hydrate, _a;
11674
11794
  // Default options for `Collection#set`.
11675
11795
  const setOptions = { add: true, remove: true, merge: true };
11676
11796
  const addOptions = { add: true, remove: false };
11797
+ /**
11798
+ * Normalizes the predicate forms accepted by the query methods into one
11799
+ * `(model) => value` function. A function is used as-is, a string reads that
11800
+ * attribute, and an attributes object becomes a lodash `matches` matcher.
11801
+ *
11802
+ * The matcher is built here, once per scan. Calling `Model#matches` per model
11803
+ * instead rebuilds the match data for every model visited, which made
11804
+ * `findWhere` an order of magnitude slower than a hand-written predicate.
11805
+ */
11806
+ function toModelPredicate(pred) {
11807
+ if (isFunction(pred))
11808
+ return pred;
11809
+ if (isString(pred))
11810
+ return (m) => m.get(pred);
11811
+ const matcher = matches(pred);
11812
+ return (m) => matcher(m.attributes);
11813
+ }
11677
11814
  /**
11678
11815
  * @public
11679
11816
  * If models tend to represent a single row of data, a Collection is
@@ -12002,17 +12139,13 @@ class Collection extends EventEmitterObject {
12002
12139
  return this.models.slice(start, end);
12003
12140
  }
12004
12141
  filter(callback, thisArg) {
12005
- return this.models.filter(isFunction(callback)
12006
- ? callback
12007
- : (m) => m.matches(callback), thisArg);
12142
+ return this.models.filter(toModelPredicate(callback), thisArg);
12008
12143
  }
12009
12144
  every(pred) {
12010
12145
  if (isFunction(pred)) {
12011
- return this.models.map((m) => m.attributes).every(pred);
12012
- }
12013
- else {
12014
- return this.models.every((m) => m.matches(pred));
12146
+ return this.models.every((m) => pred(m.attributes));
12015
12147
  }
12148
+ return this.models.every(toModelPredicate(pred));
12016
12149
  }
12017
12150
  difference(values) {
12018
12151
  return this.models.filter((m) => !values.includes(m));
@@ -12028,16 +12161,12 @@ class Collection extends EventEmitterObject {
12028
12161
  }
12029
12162
  some(pred) {
12030
12163
  if (isFunction(pred)) {
12031
- return this.models.map((m) => m.attributes).some(pred);
12032
- }
12033
- else {
12034
- return this.models.some((m) => m.matches(pred));
12164
+ return this.models.some((m) => pred(m.attributes));
12035
12165
  }
12166
+ return this.models.some(toModelPredicate(pred));
12036
12167
  }
12037
12168
  sortBy(iteratee) {
12038
- return sortBy(this.models, isFunction(iteratee)
12039
- ? iteratee
12040
- : (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
12169
+ return sortBy(this.models, toModelPredicate(iteratee));
12041
12170
  }
12042
12171
  isEmpty() {
12043
12172
  return !this.models.length;
@@ -12058,26 +12187,22 @@ class Collection extends EventEmitterObject {
12058
12187
  return this.models.length;
12059
12188
  }
12060
12189
  countBy(f) {
12061
- return countBy(this.models, isFunction(f) ? f : (m) => (isString(f) ? m.get(f) : m.matches(f)));
12190
+ return countBy(this.models, toModelPredicate(f));
12062
12191
  }
12063
12192
  groupBy(pred) {
12064
- return groupBy(this.models, isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12193
+ return groupBy(this.models, toModelPredicate(pred));
12065
12194
  }
12066
12195
  indexOf(model, fromIndex) {
12067
12196
  return this.models.indexOf(model, fromIndex);
12068
12197
  }
12069
12198
  findLastIndex(pred, fromIndex) {
12070
- return this.models.findLastIndex(isFunction(pred)
12071
- ? pred
12072
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
12199
+ return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
12073
12200
  }
12074
12201
  lastIndexOf(model, fromIndex) {
12075
12202
  return this.models.lastIndexOf(model, fromIndex);
12076
12203
  }
12077
12204
  findIndex(pred) {
12078
- return this.models.findIndex(isFunction(pred)
12079
- ? pred
12080
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12205
+ return this.models.findIndex(toModelPredicate(pred));
12081
12206
  }
12082
12207
  last() {
12083
12208
  const length = this.models == null ? 0 : this.models.length;
@@ -12090,9 +12215,7 @@ class Collection extends EventEmitterObject {
12090
12215
  return this.head();
12091
12216
  }
12092
12217
  map(cb, thisArg) {
12093
- return this.models.map(isFunction(cb)
12094
- ? cb
12095
- : (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
12218
+ return this.models.map(toModelPredicate(cb), thisArg);
12096
12219
  }
12097
12220
  reduce(callback, initialValue) {
12098
12221
  return this.models.reduce(callback, initialValue || this.models[0]);
@@ -12143,10 +12266,7 @@ class Collection extends EventEmitterObject {
12143
12266
  return this.where(attrs, true);
12144
12267
  }
12145
12268
  find(predicate, fromIndex) {
12146
- const pred = isFunction(predicate)
12147
- ? predicate
12148
- : (m) => m.matches(predicate);
12149
- return this.models.find(pred, fromIndex);
12269
+ return this.models.find(toModelPredicate(predicate), fromIndex);
12150
12270
  }
12151
12271
  /**
12152
12272
  * Force the collection to re-sort itself. You don't need to call this under
@@ -12189,46 +12309,22 @@ class Collection extends EventEmitterObject {
12189
12309
  // eslint-disable-next-line @typescript-eslint/no-this-alias
12190
12310
  const collection = this;
12191
12311
  const promise = options.promise ? getResolveablePromise() : undefined;
12192
- // Tracks whether sync invoked one of the callbacks. A sync may instead
12193
- // settle its returned promise; the `.then`/`.catch` below bridges that to
12194
- // the callbacks so the fetch promise never hangs. The flag is set
12195
- // synchronously inside each callback (before sync's promise resolves), so
12196
- // it's reliable by the time those handlers run.
12197
- let settled = false;
12198
12312
  options.success = function (resp) {
12199
- settled = true;
12200
12313
  const method = options.reset ? 'reset' : 'set';
12201
12314
  // fromStorage prevents individual model set() calls from triggering auto-save on reads
12202
12315
  collection[method](resp, Object.assign({}, options, { fromStorage: true }));
12203
12316
  if (success)
12204
12317
  success.call(options.context, collection, resp, options);
12205
- promise && promise.resolve();
12318
+ // Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
12319
+ // listener rejects the promise instead of being lost.
12206
12320
  collection.trigger('sync', collection, resp, options);
12321
+ promise && promise.resolve();
12207
12322
  };
12208
12323
  wrapError(this, options, promise);
12209
- if (promise) {
12210
- const onError = options.error;
12211
- options.error = (resp) => {
12212
- settled = true;
12213
- onError(resp);
12214
- };
12215
- }
12216
- const result = this.sync('read', this, options);
12217
- if (promise) {
12218
- Promise.resolve(result).then(
12219
- // A sync that resolves its promise without invoking options.success:
12220
- // treat the resolved value as the response, so the collection is still
12221
- // populated and the promise settles instead of hanging.
12222
- (value) => {
12223
- if (!settled)
12224
- options.success(value);
12225
- },
12226
- // A sync that rejects instead of invoking options.error; settling
12227
- // twice is a harmless no-op.
12228
- (e) => promise.reject(e));
12229
- return promise;
12230
- }
12231
- return result;
12324
+ // With `promise`, the sync's outcome is bridged to it however the sync
12325
+ // reports it: through the callbacks, or through the promise it returns.
12326
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
12327
+ return promise ?? result;
12232
12328
  }
12233
12329
  /**
12234
12330
  * Create a new instance of a model in this collection. Add the model to the
@@ -12240,9 +12336,22 @@ class Collection extends EventEmitterObject {
12240
12336
  const wait = options.wait;
12241
12337
  const return_promise = options.promise;
12242
12338
  const promise = return_promise && getResolveablePromise();
12339
+ // `_prepareModel` reports attributes that fail validation by firing
12340
+ // `'invalid'` and returning null. With `promise`, capture that error so a
12341
+ // rejected promise is handed back rather than `null`.
12342
+ let validationError;
12343
+ const captureInvalid = (_c, err) => (validationError = err);
12344
+ if (return_promise)
12345
+ this.on('invalid', captureInvalid);
12243
12346
  const preparedModel = this._prepareModel(model, options);
12244
- if (!preparedModel)
12245
- return null;
12347
+ if (return_promise)
12348
+ this.off('invalid', captureInvalid);
12349
+ if (!preparedModel) {
12350
+ if (!return_promise)
12351
+ return null;
12352
+ promise.reject(validationError);
12353
+ return promise;
12354
+ }
12246
12355
  if (!wait)
12247
12356
  this.add(preparedModel, options);
12248
12357
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -12264,8 +12373,15 @@ class Collection extends EventEmitterObject {
12264
12373
  error && error.call(options.context, model, e, options);
12265
12374
  return_promise && promise.reject(e);
12266
12375
  };
12267
- preparedModel.save(null, Object.assign(options, { 'promise': false }));
12376
+ // `options.promise` is passed through, so `save` installs its own bridge
12377
+ // from the sync's returned promise to its callbacks: a sync that resolves
12378
+ // without calling them still reaches the wired `options.success` above,
12379
+ // which resolves our promise with the model. Only a rejection that bypassed
12380
+ // the callbacks (a rejecting sync, or a throwing `'sync'`/`'error'`
12381
+ // listener) still needs bridging to ours.
12382
+ const result = preparedModel.save(null, options);
12268
12383
  if (return_promise) {
12384
+ Promise.resolve(result).catch((e) => promise.reject(e));
12269
12385
  return promise;
12270
12386
  }
12271
12387
  else {
@@ -12491,6 +12607,7 @@ const skeletor = {
12491
12607
  EventEmitter,
12492
12608
  Model,
12493
12609
  sync,
12610
+ HTTPError,
12494
12611
  };
12495
12612
  // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
12496
12613
  // We use `self` instead of `window` for `WebWorker` support.
@@ -12531,6 +12648,7 @@ PersistentStorage.nodeStorage = NodeSQLiteStorage;
12531
12648
  exports.BrowserStorage = BrowserStorage;
12532
12649
  exports.Collection = Collection;
12533
12650
  exports.EventEmitter = EventEmitter;
12651
+ exports.HTTPError = HTTPError;
12534
12652
  exports.Model = Model;
12535
12653
  exports.NodeSQLiteStorage = NodeSQLiteStorage;
12536
12654
  exports.PersistentStorage = PersistentStorage;