@converse/skeletor 3.1.5 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/skeletor.js CHANGED
@@ -8477,6 +8477,16 @@
8477
8477
  // Generate a pseudo-GUID by concatenating random hexadecimal.
8478
8478
  return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
8479
8479
  }
8480
+ // Helpers
8481
+ // -------
8482
+ /**
8483
+ * Whether `value` is a thenable (a promise, or a promise-like object such as
8484
+ * a jqXHR). Decides whether a `sync` return value can be bridged to the
8485
+ * promise that `fetch`/`save` hand back.
8486
+ */
8487
+ function isThenable(value) {
8488
+ return value != null && typeof value.then === 'function';
8489
+ }
8480
8490
  function getResolveablePromise() {
8481
8491
  const wrapper = {
8482
8492
  isResolved: false,
@@ -8521,6 +8531,65 @@
8521
8531
  promise && promise.reject && promise.reject(resp);
8522
8532
  };
8523
8533
  }
8534
+ /**
8535
+ * Run a `sync` and make sure `promise` settles from its outcome, however the
8536
+ * sync reports it.
8537
+ *
8538
+ * A `sync` may invoke `options.success`/`options.error` (the callback
8539
+ * contract), or settle the promise it returns without calling them: a
8540
+ * promise-only custom sync, or an async sync whose promise rejects because a
8541
+ * `'sync'`/`'error'` listener threw after the callbacks ran. `options.success`
8542
+ * and `options.error` must already be wired to resolve/reject `promise`. This
8543
+ * wraps them to record that they ran, calls `run()`, and bridges a thenable
8544
+ * result to them so the promise never hangs:
8545
+ *
8546
+ * - fulfilment with the callbacks unrun is delivered to `options.success`, so
8547
+ * the response is still merged into the model or collection;
8548
+ * - rejection with the callbacks unrun is delivered to `options.error`, so the
8549
+ * caller's `error` callback and the `'error'` event still fire;
8550
+ * - rejection after the callbacks ran rejects `promise` directly: a no-op if
8551
+ * they already settled it, and the only way to settle it if a listener threw
8552
+ * before they could;
8553
+ * - a throw from either delivery (a throwing `success`/`error` callback or
8554
+ * listener) rejects `promise` instead of surfacing as an unhandled rejection.
8555
+ *
8556
+ * A non-thenable result (a callback-only sync that returns nothing, or an
8557
+ * XHR) is left alone: such a sync settles `promise` through its callbacks, and
8558
+ * bridging it would fire `success` on the next microtask, before the sync is
8559
+ * done. Without a `promise`, `run()` is called as is.
8560
+ */
8561
+ function bridgeSyncToPromise(options, promise, run) {
8562
+ if (!promise)
8563
+ return run();
8564
+ let settled = false;
8565
+ const success = options.success;
8566
+ const error = options.error;
8567
+ options.success = (...args) => {
8568
+ settled = true;
8569
+ return success && success(...args);
8570
+ };
8571
+ options.error = (...args) => {
8572
+ settled = true;
8573
+ return error && error(...args);
8574
+ };
8575
+ const result = run();
8576
+ if (isThenable(result)) {
8577
+ Promise.resolve(result)
8578
+ .then((value) => {
8579
+ if (!settled)
8580
+ options.success(value);
8581
+ }, (e) => {
8582
+ if (settled) {
8583
+ promise.reject(e);
8584
+ }
8585
+ else {
8586
+ options.error(e);
8587
+ }
8588
+ })
8589
+ .catch((e) => promise.reject(e));
8590
+ }
8591
+ return result;
8592
+ }
8524
8593
  // Map from CRUD to HTTP for our default `sync` implementation.
8525
8594
  const methodMap = {
8526
8595
  create: 'POST',
@@ -8570,43 +8639,77 @@
8570
8639
  return store ? store.sync() : sync;
8571
8640
  }
8572
8641
  /**
8573
- * Override this function to change the manner in which Backbone persists
8574
- * models to the server. You will be passed the type of request, and the
8575
- * model in question. By default makes a `fetch()` API call
8576
- * to the model's `url()`.
8577
- *
8578
- * Some possible customizations could be:
8579
- *
8580
- * - Use `setTimeout` to batch rapid-fire updates into a single request.
8581
- * - Persist models via WebSockets instead of Ajax.
8582
- * - Persist models to browser storage
8642
+ * The error the default `sync` hands to `options.error`, and rejects with, for
8643
+ * a response outside the 2xx range.
8644
+ * @public
8583
8645
  */
8646
+ class HTTPError extends Error {
8647
+ constructor(response, method, url) {
8648
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
8649
+ super(`${method} ${url} failed with HTTP ${response.status}${statusText}`);
8650
+ this.name = 'HTTPError';
8651
+ this.status = response.status;
8652
+ this.response = response;
8653
+ }
8654
+ }
8584
8655
  /**
8656
+ * The default `sync`: a JSON request over `fetch()` to the model's `url()`.
8657
+ * Override it (or `Model.sync`/`Collection.sync`) to change how models are
8658
+ * persisted, for instance to batch rapid-fire updates with `setTimeout`, to
8659
+ * talk over WebSockets, or to write to browser storage.
8660
+ *
8661
+ * It honours both contracts a `sync` may use. On a 2xx response the JSON body
8662
+ * (`null` when the body is empty) is passed to `options.success` and the
8663
+ * returned promise resolves with it. On a non-2xx response, a network failure
8664
+ * or an unparsable body, `options.error` receives the error (an `HTTPError`
8665
+ * for the first) and the promise rejects with it.
8666
+ *
8667
+ * `options.data` is sent as query parameters on GET, which may not carry a
8668
+ * body, and as a JSON body otherwise. For create, update and patch the body
8669
+ * defaults to `options.attrs` or the model's `toJSON()`.
8585
8670
  * @public
8586
8671
  */
8587
8672
  function sync(method, model, options = {}) {
8673
+ const type = methodMap[method];
8588
8674
  let data = options.data;
8589
8675
  // Ensure that we have the appropriate request data.
8590
8676
  if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
8591
8677
  data = options.attrs || model.toJSON();
8592
8678
  }
8593
- const type = methodMap[method];
8594
- const params = {
8595
- method: type,
8596
- body: data ? JSON.stringify(data) : '',
8597
- headers: {
8598
- 'Content-Type': 'application/json',
8599
- },
8600
- success: options.success,
8601
- error: options.error,
8602
- };
8603
- const url = options.url || result(model, 'url') || urlError();
8604
- const xhr = fetch(url, params);
8605
- if (options) {
8606
- options.xhr = xhr;
8679
+ let url = options.url || result(model, 'url') || urlError();
8680
+ const params = { method: type };
8681
+ if (data != null) {
8682
+ if (type === 'GET') {
8683
+ url += (url.includes('?') ? '&' : '?') + new URLSearchParams(data).toString();
8684
+ }
8685
+ else {
8686
+ params.body = JSON.stringify(data);
8687
+ params.headers = { 'Content-Type': 'application/json' };
8688
+ }
8607
8689
  }
8690
+ const xhr = fetch(url, params);
8691
+ options.xhr = xhr;
8608
8692
  model.trigger('request', model, xhr, { ...params, xhr });
8609
- return xhr;
8693
+ return Promise.resolve(xhr)
8694
+ .then(async (response) => {
8695
+ if (!response.ok)
8696
+ throw new HTTPError(response, type, url);
8697
+ const text = await response.text();
8698
+ return text ? JSON.parse(text) : null;
8699
+ })
8700
+ .then(
8701
+ // The callbacks run before the returned promise settles, so a caller that
8702
+ // bridges this promise (`Model.fetch`/`save`) sees the outcome as already
8703
+ // reported. A throw from `success` (a `'sync'` listener, say) is not
8704
+ // routed to `error`: it rejects the promise, which is what the bridge
8705
+ // expects once the callbacks have run.
8706
+ (resp) => {
8707
+ options.success?.(resp, options);
8708
+ return resp;
8709
+ }, (e) => {
8710
+ options.error?.(e);
8711
+ throw e;
8712
+ });
8610
8713
  }
8611
8714
 
8612
8715
  /**
@@ -8705,7 +8808,7 @@
8705
8808
  throw new Error("Skeletor.storage: the 'node' storage type is only available in Node.js builds. " +
8706
8809
  "Import from '@converse/skeletor/node' instead of '@converse/skeletor'.");
8707
8810
  }
8708
- this.store = new NodeSQLiteStorage(this.name);
8811
+ this.store = new NodeSQLiteStorage(this.name, PersistentStorage.nodeStorageDir ?? undefined);
8709
8812
  if (batchedWrites) {
8710
8813
  this.store.debouncedSetItems = mergebounce((items) => this.store.setItems(items), 50, {
8711
8814
  'promise': true,
@@ -8909,6 +9012,18 @@
8909
9012
  // node-only driver (and its `node:sqlite` import) never enters the browser
8910
9013
  // bundle; the `@converse/skeletor/node` entry sets it.
8911
9014
  PersistentStorage.nodeStorage = null;
9015
+ /**
9016
+ * Directory the `'node'` store type writes its database files to. The driver's
9017
+ * own default is relative, so it resolves against `process.cwd()` and a process
9018
+ * started from two directories ends up with two separate sets of state. Set an
9019
+ * absolute path (`$XDG_STATE_HOME/<app>`, say) to pin it. Left `null`, the
9020
+ * driver's default applies.
9021
+ *
9022
+ * Only the `'node'` string store type reads this; passing a driver instance to
9023
+ * the constructor means you've already chosen a directory yourself.
9024
+ * @public
9025
+ */
9026
+ PersistentStorage.nodeStorageDir = null;
8912
9027
  PersistentStorage.sessionStorageInitialized = localForage.defineDriver(sessionStorageWrapper);
8913
9028
  PersistentStorage.localForage = localForage;
8914
9029
 
@@ -9960,6 +10075,48 @@
9960
10075
  baseAssignValue(result, key, value);
9961
10076
  });
9962
10077
 
10078
+ /** Used to compose bitmasks for cloning. */
10079
+ var CLONE_DEEP_FLAG$1 = 1;
10080
+
10081
+ /**
10082
+ * Creates a function that performs a partial deep comparison between a given
10083
+ * object and `source`, returning `true` if the given object has equivalent
10084
+ * property values, else `false`.
10085
+ *
10086
+ * **Note:** The created function is equivalent to `_.isMatch` with `source`
10087
+ * partially applied.
10088
+ *
10089
+ * Partial comparisons will match empty array and empty object `source`
10090
+ * values against any array or object value, respectively. See `_.isEqual`
10091
+ * for a list of supported value comparisons.
10092
+ *
10093
+ * **Note:** Multiple values can be checked by combining several matchers
10094
+ * using `_.overSome`
10095
+ *
10096
+ * @static
10097
+ * @memberOf _
10098
+ * @since 3.0.0
10099
+ * @category Util
10100
+ * @param {Object} source The object of property values to match.
10101
+ * @returns {Function} Returns the new spec function.
10102
+ * @example
10103
+ *
10104
+ * var objects = [
10105
+ * { 'a': 1, 'b': 2, 'c': 3 },
10106
+ * { 'a': 4, 'b': 5, 'c': 6 }
10107
+ * ];
10108
+ *
10109
+ * _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
10110
+ * // => [{ 'a': 4, 'b': 5, 'c': 6 }]
10111
+ *
10112
+ * // Checking for several possible values
10113
+ * _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
10114
+ * // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
10115
+ */
10116
+ function matches(source) {
10117
+ return baseMatches(baseClone(source, CLONE_DEEP_FLAG$1));
10118
+ }
10119
+
9963
10120
  /** Built-in value references. */
9964
10121
  var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
9965
10122
 
@@ -10523,53 +10680,36 @@
10523
10680
  return baseIsEqual(value, other);
10524
10681
  }
10525
10682
 
10526
- /** Used to compose bitmasks for cloning. */
10527
- var CLONE_DEEP_FLAG$1 = 1;
10528
-
10529
10683
  /**
10530
- * Creates a function that invokes `func` with the arguments of the created
10531
- * function. If `func` is a property name, the created function returns the
10532
- * property value for a given element. If `func` is an array or object, the
10533
- * created function returns `true` for elements that contain the equivalent
10534
- * source properties, otherwise it returns `false`.
10684
+ * Performs a partial deep comparison between `object` and `source` to
10685
+ * determine if `object` contains equivalent property values.
10686
+ *
10687
+ * **Note:** This method is equivalent to `_.matches` when `source` is
10688
+ * partially applied.
10689
+ *
10690
+ * Partial comparisons will match empty array and empty object `source`
10691
+ * values against any array or object value, respectively. See `_.isEqual`
10692
+ * for a list of supported value comparisons.
10535
10693
  *
10536
10694
  * @static
10537
- * @since 4.0.0
10538
10695
  * @memberOf _
10539
- * @category Util
10540
- * @param {*} [func=_.identity] The value to convert to a callback.
10541
- * @returns {Function} Returns the callback.
10696
+ * @since 3.0.0
10697
+ * @category Lang
10698
+ * @param {Object} object The object to inspect.
10699
+ * @param {Object} source The object of property values to match.
10700
+ * @returns {boolean} Returns `true` if `object` is a match, else `false`.
10542
10701
  * @example
10543
10702
  *
10544
- * var users = [
10545
- * { 'user': 'barney', 'age': 36, 'active': true },
10546
- * { 'user': 'fred', 'age': 40, 'active': false }
10547
- * ];
10548
- *
10549
- * // The `_.matches` iteratee shorthand.
10550
- * _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
10551
- * // => [{ 'user': 'barney', 'age': 36, 'active': true }]
10552
- *
10553
- * // The `_.matchesProperty` iteratee shorthand.
10554
- * _.filter(users, _.iteratee(['user', 'fred']));
10555
- * // => [{ 'user': 'fred', 'age': 40 }]
10703
+ * var object = { 'a': 1, 'b': 2 };
10556
10704
  *
10557
- * // The `_.property` iteratee shorthand.
10558
- * _.map(users, _.iteratee('user'));
10559
- * // => ['barney', 'fred']
10560
- *
10561
- * // Create custom iteratee shorthands.
10562
- * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
10563
- * return !_.isRegExp(func) ? iteratee(func) : function(string) {
10564
- * return func.test(string);
10565
- * };
10566
- * });
10705
+ * _.isMatch(object, { 'b': 2 });
10706
+ * // => true
10567
10707
  *
10568
- * _.filter(['abc', 'def'], /ef/);
10569
- * // => ['def']
10708
+ * _.isMatch(object, { 'b': 1 });
10709
+ * // => false
10570
10710
  */
10571
- function iteratee(func) {
10572
- return baseIteratee(typeof func == 'function' ? func : baseClone(func, CLONE_DEEP_FLAG$1));
10711
+ function isMatch(object, source) {
10712
+ return object === source || baseIsMatch(object, source, getMatchData(source));
10573
10713
  }
10574
10714
 
10575
10715
  /**
@@ -11083,10 +11223,20 @@
11083
11223
  return this.get(attr) != null;
11084
11224
  }
11085
11225
  /**
11086
- * Special-cased proxy to lodash's `matches` method.
11226
+ * Returns `true` if the model's attributes satisfy `attrs`.
11227
+ *
11228
+ * An attributes object is compared with lodash's `isMatch`: a partial, deep
11229
+ * comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
11230
+ * that has `type: 'hidden'` alongside other keys. A function is called with
11231
+ * the attributes and its result coerced to a boolean.
11232
+ *
11233
+ * To scan a collection, prefer `Collection#find`, `filter` or `where`. They
11234
+ * build their matcher once per scan instead of once per model.
11087
11235
  */
11088
11236
  matches(attrs) {
11089
- return !!iteratee(attrs)(this.attributes);
11237
+ if (typeof attrs === 'function')
11238
+ return !!attrs(this.attributes);
11239
+ return isMatch(this.attributes, attrs);
11090
11240
  }
11091
11241
  /**
11092
11242
  * Set a hash of model attributes on the object, firing `"change"`. This is
@@ -11259,47 +11409,25 @@
11259
11409
  options = Object.assign({ parse: true }, options);
11260
11410
  const success = options.success;
11261
11411
  const promise = options.promise ? getResolveablePromise() : undefined;
11262
- // Tracks whether sync invoked one of the callbacks. A sync may instead
11263
- // settle its returned promise; the `.then`/`.catch` below bridges that to
11264
- // the callbacks so the fetch promise never hangs. The flag is set
11265
- // synchronously inside each callback (before sync's promise resolves), so
11266
- // it's reliable by the time those handlers run.
11267
- let settled = false;
11268
11412
  options.success = (resp) => {
11269
- settled = true;
11270
11413
  const serverAttrs = options.parse ? this.parse(resp, options) : resp;
11271
11414
  // fromStorage prevents the set() from triggering auto-save on reads
11272
- if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
11415
+ if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
11416
+ // The fetched attributes failed validation (`'invalid'` has fired).
11417
+ // Settle the promise with the error rather than leave it pending.
11418
+ promise && promise.reject(this.validationError);
11273
11419
  return false;
11420
+ }
11274
11421
  if (success)
11275
11422
  success.call(options.context, this, resp, options);
11276
11423
  this.trigger('sync', this, resp, options);
11277
11424
  promise && promise.resolve(resp);
11278
11425
  };
11279
11426
  wrapError(this, options, promise);
11280
- if (promise) {
11281
- const onError = options.error;
11282
- options.error = (resp) => {
11283
- settled = true;
11284
- onError(resp);
11285
- };
11286
- }
11287
- const result = this.sync('read', this, options);
11288
- if (promise) {
11289
- Promise.resolve(result).then(
11290
- // A sync that resolves its promise without invoking options.success:
11291
- // treat the resolved value as the response, so the model is still
11292
- // merged and the promise settles instead of hanging.
11293
- (value) => {
11294
- if (!settled)
11295
- options.success(value);
11296
- },
11297
- // A sync that rejects instead of invoking options.error; settling
11298
- // twice is a harmless no-op.
11299
- (e) => promise.reject(e));
11300
- return promise;
11301
- }
11302
- return result;
11427
+ // With `promise`, the sync's outcome is bridged to it however the sync
11428
+ // reports it: through the callbacks, or through the promise it returns.
11429
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
11430
+ return promise ?? result;
11303
11431
  }
11304
11432
  /**
11305
11433
  * Set a hash of model attributes, and sync the model to the server.
@@ -11320,16 +11448,19 @@
11320
11448
  options = Object.assign({ validate: true, parse: true }, options);
11321
11449
  const wait = options.wait;
11322
11450
  const return_promise = options.promise;
11323
- const promise = return_promise && getResolveablePromise();
11451
+ const promise = return_promise ? getResolveablePromise() : undefined;
11324
11452
  // If we're not waiting and attributes exist, save acts as
11325
11453
  // `set(attr).save(null, opts)` with validation. Otherwise, check if
11326
11454
  // the model will be valid when the attributes, if any, are set.
11327
- if (attrs && !wait) {
11328
- if (!this.set(attrs, options))
11455
+ const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
11456
+ if (!valid) {
11457
+ // Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
11458
+ // rejected promise rather than `false`, so the promise contract holds
11459
+ // for every outcome.
11460
+ if (!promise)
11329
11461
  return false;
11330
- }
11331
- else if (!this._validate(attrs || {}, options)) {
11332
- return false;
11462
+ promise.reject(this.validationError);
11463
+ return promise;
11333
11464
  }
11334
11465
  // After a successful server-side save, the client is (optionally)
11335
11466
  // updated with the server-side state.
@@ -11342,8 +11473,12 @@
11342
11473
  let serverAttrs = options.parse ? this.parse(resp, options) : resp;
11343
11474
  if (wait)
11344
11475
  serverAttrs = Object.assign({}, attrs, serverAttrs);
11345
- if (serverAttrs && !this.set(serverAttrs, options))
11476
+ if (serverAttrs && !this.set(serverAttrs, options)) {
11477
+ // The server attributes failed validation (`'invalid'` has fired).
11478
+ // Settle the promise with the error rather than leave it pending.
11479
+ return_promise && promise.reject(this.validationError);
11346
11480
  return false;
11481
+ }
11347
11482
  if (success)
11348
11483
  success.call(options.context, this, resp, options);
11349
11484
  this.trigger('sync', this, resp, options);
@@ -11360,15 +11495,12 @@
11360
11495
  const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
11361
11496
  if (method === 'patch' && !options.attrs)
11362
11497
  options.attrs = attrs;
11363
- const xhr = this.sync(method, this, options);
11498
+ // With `promise`, the sync's outcome is bridged to it however the sync
11499
+ // reports it: through the callbacks, or through the promise it returns.
11500
+ const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
11364
11501
  // Restore attributes.
11365
11502
  this.attributes = attributes;
11366
- if (return_promise) {
11367
- return promise;
11368
- }
11369
- else {
11370
- return xhr;
11371
- }
11503
+ return promise ?? xhr;
11372
11504
  }
11373
11505
  /**
11374
11506
  * Destroy this model on the server if it was already persisted.
@@ -11515,6 +11647,23 @@
11515
11647
  // Default options for `Collection#set`.
11516
11648
  const setOptions = { add: true, remove: true, merge: true };
11517
11649
  const addOptions = { add: true, remove: false };
11650
+ /**
11651
+ * Normalizes the predicate forms accepted by the query methods into one
11652
+ * `(model) => value` function. A function is used as-is, a string reads that
11653
+ * attribute, and an attributes object becomes a lodash `matches` matcher.
11654
+ *
11655
+ * The matcher is built here, once per scan. Calling `Model#matches` per model
11656
+ * instead rebuilds the match data for every model visited, which made
11657
+ * `findWhere` an order of magnitude slower than a hand-written predicate.
11658
+ */
11659
+ function toModelPredicate(pred) {
11660
+ if (isFunction(pred))
11661
+ return pred;
11662
+ if (isString(pred))
11663
+ return (m) => m.get(pred);
11664
+ const matcher = matches(pred);
11665
+ return (m) => matcher(m.attributes);
11666
+ }
11518
11667
  /**
11519
11668
  * @public
11520
11669
  * If models tend to represent a single row of data, a Collection is
@@ -11843,17 +11992,13 @@
11843
11992
  return this.models.slice(start, end);
11844
11993
  }
11845
11994
  filter(callback, thisArg) {
11846
- return this.models.filter(isFunction(callback)
11847
- ? callback
11848
- : (m) => m.matches(callback), thisArg);
11995
+ return this.models.filter(toModelPredicate(callback), thisArg);
11849
11996
  }
11850
11997
  every(pred) {
11851
11998
  if (isFunction(pred)) {
11852
- return this.models.map((m) => m.attributes).every(pred);
11853
- }
11854
- else {
11855
- return this.models.every((m) => m.matches(pred));
11999
+ return this.models.every((m) => pred(m.attributes));
11856
12000
  }
12001
+ return this.models.every(toModelPredicate(pred));
11857
12002
  }
11858
12003
  difference(values) {
11859
12004
  return this.models.filter((m) => !values.includes(m));
@@ -11869,16 +12014,12 @@
11869
12014
  }
11870
12015
  some(pred) {
11871
12016
  if (isFunction(pred)) {
11872
- return this.models.map((m) => m.attributes).some(pred);
11873
- }
11874
- else {
11875
- return this.models.some((m) => m.matches(pred));
12017
+ return this.models.some((m) => pred(m.attributes));
11876
12018
  }
12019
+ return this.models.some(toModelPredicate(pred));
11877
12020
  }
11878
12021
  sortBy(iteratee) {
11879
- return sortBy(this.models, isFunction(iteratee)
11880
- ? iteratee
11881
- : (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
12022
+ return sortBy(this.models, toModelPredicate(iteratee));
11882
12023
  }
11883
12024
  isEmpty() {
11884
12025
  return !this.models.length;
@@ -11899,26 +12040,22 @@
11899
12040
  return this.models.length;
11900
12041
  }
11901
12042
  countBy(f) {
11902
- return countBy(this.models, isFunction(f) ? f : (m) => (isString(f) ? m.get(f) : m.matches(f)));
12043
+ return countBy(this.models, toModelPredicate(f));
11903
12044
  }
11904
12045
  groupBy(pred) {
11905
- return groupBy(this.models, isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12046
+ return groupBy(this.models, toModelPredicate(pred));
11906
12047
  }
11907
12048
  indexOf(model, fromIndex) {
11908
12049
  return this.models.indexOf(model, fromIndex);
11909
12050
  }
11910
12051
  findLastIndex(pred, fromIndex) {
11911
- return this.models.findLastIndex(isFunction(pred)
11912
- ? pred
11913
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
12052
+ return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
11914
12053
  }
11915
12054
  lastIndexOf(model, fromIndex) {
11916
12055
  return this.models.lastIndexOf(model, fromIndex);
11917
12056
  }
11918
12057
  findIndex(pred) {
11919
- return this.models.findIndex(isFunction(pred)
11920
- ? pred
11921
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12058
+ return this.models.findIndex(toModelPredicate(pred));
11922
12059
  }
11923
12060
  last() {
11924
12061
  const length = this.models == null ? 0 : this.models.length;
@@ -11931,9 +12068,7 @@
11931
12068
  return this.head();
11932
12069
  }
11933
12070
  map(cb, thisArg) {
11934
- return this.models.map(isFunction(cb)
11935
- ? cb
11936
- : (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
12071
+ return this.models.map(toModelPredicate(cb), thisArg);
11937
12072
  }
11938
12073
  reduce(callback, initialValue) {
11939
12074
  return this.models.reduce(callback, initialValue || this.models[0]);
@@ -11984,10 +12119,7 @@
11984
12119
  return this.where(attrs, true);
11985
12120
  }
11986
12121
  find(predicate, fromIndex) {
11987
- const pred = isFunction(predicate)
11988
- ? predicate
11989
- : (m) => m.matches(predicate);
11990
- return this.models.find(pred, fromIndex);
12122
+ return this.models.find(toModelPredicate(predicate), fromIndex);
11991
12123
  }
11992
12124
  /**
11993
12125
  * Force the collection to re-sort itself. You don't need to call this under
@@ -12030,46 +12162,22 @@
12030
12162
  // eslint-disable-next-line @typescript-eslint/no-this-alias
12031
12163
  const collection = this;
12032
12164
  const promise = options.promise ? getResolveablePromise() : undefined;
12033
- // Tracks whether sync invoked one of the callbacks. A sync may instead
12034
- // settle its returned promise; the `.then`/`.catch` below bridges that to
12035
- // the callbacks so the fetch promise never hangs. The flag is set
12036
- // synchronously inside each callback (before sync's promise resolves), so
12037
- // it's reliable by the time those handlers run.
12038
- let settled = false;
12039
12165
  options.success = function (resp) {
12040
- settled = true;
12041
12166
  const method = options.reset ? 'reset' : 'set';
12042
12167
  // fromStorage prevents individual model set() calls from triggering auto-save on reads
12043
12168
  collection[method](resp, Object.assign({}, options, { fromStorage: true }));
12044
12169
  if (success)
12045
12170
  success.call(options.context, collection, resp, options);
12046
- promise && promise.resolve();
12171
+ // Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
12172
+ // listener rejects the promise instead of being lost.
12047
12173
  collection.trigger('sync', collection, resp, options);
12174
+ promise && promise.resolve();
12048
12175
  };
12049
12176
  wrapError(this, options, promise);
12050
- if (promise) {
12051
- const onError = options.error;
12052
- options.error = (resp) => {
12053
- settled = true;
12054
- onError(resp);
12055
- };
12056
- }
12057
- const result = this.sync('read', this, options);
12058
- if (promise) {
12059
- Promise.resolve(result).then(
12060
- // A sync that resolves its promise without invoking options.success:
12061
- // treat the resolved value as the response, so the collection is still
12062
- // populated and the promise settles instead of hanging.
12063
- (value) => {
12064
- if (!settled)
12065
- options.success(value);
12066
- },
12067
- // A sync that rejects instead of invoking options.error; settling
12068
- // twice is a harmless no-op.
12069
- (e) => promise.reject(e));
12070
- return promise;
12071
- }
12072
- return result;
12177
+ // With `promise`, the sync's outcome is bridged to it however the sync
12178
+ // reports it: through the callbacks, or through the promise it returns.
12179
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
12180
+ return promise ?? result;
12073
12181
  }
12074
12182
  /**
12075
12183
  * Create a new instance of a model in this collection. Add the model to the
@@ -12081,9 +12189,22 @@
12081
12189
  const wait = options.wait;
12082
12190
  const return_promise = options.promise;
12083
12191
  const promise = return_promise && getResolveablePromise();
12192
+ // `_prepareModel` reports attributes that fail validation by firing
12193
+ // `'invalid'` and returning null. With `promise`, capture that error so a
12194
+ // rejected promise is handed back rather than `null`.
12195
+ let validationError;
12196
+ const captureInvalid = (_c, err) => (validationError = err);
12197
+ if (return_promise)
12198
+ this.on('invalid', captureInvalid);
12084
12199
  const preparedModel = this._prepareModel(model, options);
12085
- if (!preparedModel)
12086
- return null;
12200
+ if (return_promise)
12201
+ this.off('invalid', captureInvalid);
12202
+ if (!preparedModel) {
12203
+ if (!return_promise)
12204
+ return null;
12205
+ promise.reject(validationError);
12206
+ return promise;
12207
+ }
12087
12208
  if (!wait)
12088
12209
  this.add(preparedModel, options);
12089
12210
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -12105,8 +12226,15 @@
12105
12226
  error && error.call(options.context, model, e, options);
12106
12227
  return_promise && promise.reject(e);
12107
12228
  };
12108
- preparedModel.save(null, Object.assign(options, { 'promise': false }));
12229
+ // `options.promise` is passed through, so `save` installs its own bridge
12230
+ // from the sync's returned promise to its callbacks: a sync that resolves
12231
+ // without calling them still reaches the wired `options.success` above,
12232
+ // which resolves our promise with the model. Only a rejection that bypassed
12233
+ // the callbacks (a rejecting sync, or a throwing `'sync'`/`'error'`
12234
+ // listener) still needs bridging to ours.
12235
+ const result = preparedModel.save(null, options);
12109
12236
  if (return_promise) {
12237
+ Promise.resolve(result).catch((e) => promise.reject(e));
12110
12238
  return promise;
12111
12239
  }
12112
12240
  else {
@@ -12332,6 +12460,7 @@
12332
12460
  EventEmitter,
12333
12461
  Model,
12334
12462
  sync,
12463
+ HTTPError,
12335
12464
  };
12336
12465
  // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
12337
12466
  // We use `self` instead of `window` for `WebWorker` support.
@@ -12362,6 +12491,7 @@
12362
12491
  exports.BrowserStorage = BrowserStorage;
12363
12492
  exports.Collection = Collection;
12364
12493
  exports.EventEmitter = EventEmitter;
12494
+ exports.HTTPError = HTTPError;
12365
12495
  exports.Model = Model;
12366
12496
  exports.PersistentStorage = PersistentStorage;
12367
12497
  exports.default = skeletor;