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