@converse/skeletor 3.1.6 → 4.0.1

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
@@ -1674,19 +1674,6 @@
1674
1674
  throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
1675
1675
  }
1676
1676
 
1677
- function __classPrivateFieldGet(receiver, state, kind, f) {
1678
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
1679
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
1680
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
1681
- }
1682
-
1683
- function __classPrivateFieldSet(receiver, state, value, kind, f) {
1684
- if (kind === "m") throw new TypeError("Private method is not writable");
1685
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
1686
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
1687
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
1688
- }
1689
-
1690
1677
  typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
1691
1678
  var e = new Error(message);
1692
1679
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
@@ -8477,6 +8464,16 @@
8477
8464
  // Generate a pseudo-GUID by concatenating random hexadecimal.
8478
8465
  return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
8479
8466
  }
8467
+ // Helpers
8468
+ // -------
8469
+ /**
8470
+ * Whether `value` is a thenable (a promise, or a promise-like object such as
8471
+ * a jqXHR). Decides whether a `sync` return value can be bridged to the
8472
+ * promise that `fetch`/`save` hand back.
8473
+ */
8474
+ function isThenable(value) {
8475
+ return value != null && typeof value.then === 'function';
8476
+ }
8480
8477
  function getResolveablePromise() {
8481
8478
  const wrapper = {
8482
8479
  isResolved: false,
@@ -8521,6 +8518,65 @@
8521
8518
  promise && promise.reject && promise.reject(resp);
8522
8519
  };
8523
8520
  }
8521
+ /**
8522
+ * Run a `sync` and make sure `promise` settles from its outcome, however the
8523
+ * sync reports it.
8524
+ *
8525
+ * A `sync` may invoke `options.success`/`options.error` (the callback
8526
+ * contract), or settle the promise it returns without calling them: a
8527
+ * promise-only custom sync, or an async sync whose promise rejects because a
8528
+ * `'sync'`/`'error'` listener threw after the callbacks ran. `options.success`
8529
+ * and `options.error` must already be wired to resolve/reject `promise`. This
8530
+ * wraps them to record that they ran, calls `run()`, and bridges a thenable
8531
+ * result to them so the promise never hangs:
8532
+ *
8533
+ * - fulfilment with the callbacks unrun is delivered to `options.success`, so
8534
+ * the response is still merged into the model or collection;
8535
+ * - rejection with the callbacks unrun is delivered to `options.error`, so the
8536
+ * caller's `error` callback and the `'error'` event still fire;
8537
+ * - rejection after the callbacks ran rejects `promise` directly: a no-op if
8538
+ * they already settled it, and the only way to settle it if a listener threw
8539
+ * before they could;
8540
+ * - a throw from either delivery (a throwing `success`/`error` callback or
8541
+ * listener) rejects `promise` instead of surfacing as an unhandled rejection.
8542
+ *
8543
+ * A non-thenable result (a callback-only sync that returns nothing, or an
8544
+ * XHR) is left alone: such a sync settles `promise` through its callbacks, and
8545
+ * bridging it would fire `success` on the next microtask, before the sync is
8546
+ * done. Without a `promise`, `run()` is called as is.
8547
+ */
8548
+ function bridgeSyncToPromise(options, promise, run) {
8549
+ if (!promise)
8550
+ return run();
8551
+ let settled = false;
8552
+ const success = options.success;
8553
+ const error = options.error;
8554
+ options.success = (...args) => {
8555
+ settled = true;
8556
+ return success && success(...args);
8557
+ };
8558
+ options.error = (...args) => {
8559
+ settled = true;
8560
+ return error && error(...args);
8561
+ };
8562
+ const result = run();
8563
+ if (isThenable(result)) {
8564
+ Promise.resolve(result)
8565
+ .then((value) => {
8566
+ if (!settled)
8567
+ options.success(value);
8568
+ }, (e) => {
8569
+ if (settled) {
8570
+ promise.reject(e);
8571
+ }
8572
+ else {
8573
+ options.error(e);
8574
+ }
8575
+ })
8576
+ .catch((e) => promise.reject(e));
8577
+ }
8578
+ return result;
8579
+ }
8524
8580
  // Map from CRUD to HTTP for our default `sync` implementation.
8525
8581
  const methodMap = {
8526
8582
  create: 'POST',
@@ -8570,43 +8626,77 @@
8570
8626
  return store ? store.sync() : sync;
8571
8627
  }
8572
8628
  /**
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
8629
+ * The error the default `sync` hands to `options.error`, and rejects with, for
8630
+ * a response outside the 2xx range.
8631
+ * @public
8583
8632
  */
8633
+ class HTTPError extends Error {
8634
+ constructor(response, method, url) {
8635
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
8636
+ super(`${method} ${url} failed with HTTP ${response.status}${statusText}`);
8637
+ this.name = 'HTTPError';
8638
+ this.status = response.status;
8639
+ this.response = response;
8640
+ }
8641
+ }
8584
8642
  /**
8643
+ * The default `sync`: a JSON request over `fetch()` to the model's `url()`.
8644
+ * Override it (or `Model.sync`/`Collection.sync`) to change how models are
8645
+ * persisted, for instance to batch rapid-fire updates with `setTimeout`, to
8646
+ * talk over WebSockets, or to write to browser storage.
8647
+ *
8648
+ * It honours both contracts a `sync` may use. On a 2xx response the JSON body
8649
+ * (`null` when the body is empty) is passed to `options.success` and the
8650
+ * returned promise resolves with it. On a non-2xx response, a network failure
8651
+ * or an unparsable body, `options.error` receives the error (an `HTTPError`
8652
+ * for the first) and the promise rejects with it.
8653
+ *
8654
+ * `options.data` is sent as query parameters on GET, which may not carry a
8655
+ * body, and as a JSON body otherwise. For create, update and patch the body
8656
+ * defaults to `options.attrs` or the model's `toJSON()`.
8585
8657
  * @public
8586
8658
  */
8587
8659
  function sync(method, model, options = {}) {
8660
+ const type = methodMap[method];
8588
8661
  let data = options.data;
8589
8662
  // Ensure that we have the appropriate request data.
8590
8663
  if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
8591
8664
  data = options.attrs || model.toJSON();
8592
8665
  }
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;
8666
+ let url = options.url || result(model, 'url') || urlError();
8667
+ const params = { method: type };
8668
+ if (data != null) {
8669
+ if (type === 'GET') {
8670
+ url += (url.includes('?') ? '&' : '?') + new URLSearchParams(data).toString();
8671
+ }
8672
+ else {
8673
+ params.body = JSON.stringify(data);
8674
+ params.headers = { 'Content-Type': 'application/json' };
8675
+ }
8607
8676
  }
8677
+ const xhr = fetch(url, params);
8678
+ options.xhr = xhr;
8608
8679
  model.trigger('request', model, xhr, { ...params, xhr });
8609
- return xhr;
8680
+ return Promise.resolve(xhr)
8681
+ .then(async (response) => {
8682
+ if (!response.ok)
8683
+ throw new HTTPError(response, type, url);
8684
+ const text = await response.text();
8685
+ return text ? JSON.parse(text) : null;
8686
+ })
8687
+ .then(
8688
+ // The callbacks run before the returned promise settles, so a caller that
8689
+ // bridges this promise (`Model.fetch`/`save`) sees the outcome as already
8690
+ // reported. A throw from `success` (a `'sync'` listener, say) is not
8691
+ // routed to `error`: it rejects the promise, which is what the bridge
8692
+ // expects once the callbacks have run.
8693
+ (resp) => {
8694
+ options.success?.(resp, options);
8695
+ return resp;
8696
+ }, (e) => {
8697
+ options.error?.(e);
8698
+ throw e;
8699
+ });
8610
8700
  }
8611
8701
 
8612
8702
  /**
@@ -9972,6 +10062,48 @@
9972
10062
  baseAssignValue(result, key, value);
9973
10063
  });
9974
10064
 
10065
+ /** Used to compose bitmasks for cloning. */
10066
+ var CLONE_DEEP_FLAG$1 = 1;
10067
+
10068
+ /**
10069
+ * Creates a function that performs a partial deep comparison between a given
10070
+ * object and `source`, returning `true` if the given object has equivalent
10071
+ * property values, else `false`.
10072
+ *
10073
+ * **Note:** The created function is equivalent to `_.isMatch` with `source`
10074
+ * partially applied.
10075
+ *
10076
+ * Partial comparisons will match empty array and empty object `source`
10077
+ * values against any array or object value, respectively. See `_.isEqual`
10078
+ * for a list of supported value comparisons.
10079
+ *
10080
+ * **Note:** Multiple values can be checked by combining several matchers
10081
+ * using `_.overSome`
10082
+ *
10083
+ * @static
10084
+ * @memberOf _
10085
+ * @since 3.0.0
10086
+ * @category Util
10087
+ * @param {Object} source The object of property values to match.
10088
+ * @returns {Function} Returns the new spec function.
10089
+ * @example
10090
+ *
10091
+ * var objects = [
10092
+ * { 'a': 1, 'b': 2, 'c': 3 },
10093
+ * { 'a': 4, 'b': 5, 'c': 6 }
10094
+ * ];
10095
+ *
10096
+ * _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
10097
+ * // => [{ 'a': 4, 'b': 5, 'c': 6 }]
10098
+ *
10099
+ * // Checking for several possible values
10100
+ * _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
10101
+ * // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
10102
+ */
10103
+ function matches(source) {
10104
+ return baseMatches(baseClone(source, CLONE_DEEP_FLAG$1));
10105
+ }
10106
+
9975
10107
  /** Built-in value references. */
9976
10108
  var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
9977
10109
 
@@ -10535,53 +10667,36 @@
10535
10667
  return baseIsEqual(value, other);
10536
10668
  }
10537
10669
 
10538
- /** Used to compose bitmasks for cloning. */
10539
- var CLONE_DEEP_FLAG$1 = 1;
10540
-
10541
10670
  /**
10542
- * Creates a function that invokes `func` with the arguments of the created
10543
- * function. If `func` is a property name, the created function returns the
10544
- * property value for a given element. If `func` is an array or object, the
10545
- * created function returns `true` for elements that contain the equivalent
10546
- * source properties, otherwise it returns `false`.
10671
+ * Performs a partial deep comparison between `object` and `source` to
10672
+ * determine if `object` contains equivalent property values.
10673
+ *
10674
+ * **Note:** This method is equivalent to `_.matches` when `source` is
10675
+ * partially applied.
10676
+ *
10677
+ * Partial comparisons will match empty array and empty object `source`
10678
+ * values against any array or object value, respectively. See `_.isEqual`
10679
+ * for a list of supported value comparisons.
10547
10680
  *
10548
10681
  * @static
10549
- * @since 4.0.0
10550
10682
  * @memberOf _
10551
- * @category Util
10552
- * @param {*} [func=_.identity] The value to convert to a callback.
10553
- * @returns {Function} Returns the callback.
10683
+ * @since 3.0.0
10684
+ * @category Lang
10685
+ * @param {Object} object The object to inspect.
10686
+ * @param {Object} source The object of property values to match.
10687
+ * @returns {boolean} Returns `true` if `object` is a match, else `false`.
10554
10688
  * @example
10555
10689
  *
10556
- * var users = [
10557
- * { 'user': 'barney', 'age': 36, 'active': true },
10558
- * { 'user': 'fred', 'age': 40, 'active': false }
10559
- * ];
10560
- *
10561
- * // The `_.matches` iteratee shorthand.
10562
- * _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
10563
- * // => [{ 'user': 'barney', 'age': 36, 'active': true }]
10564
- *
10565
- * // The `_.matchesProperty` iteratee shorthand.
10566
- * _.filter(users, _.iteratee(['user', 'fred']));
10567
- * // => [{ 'user': 'fred', 'age': 40 }]
10690
+ * var object = { 'a': 1, 'b': 2 };
10568
10691
  *
10569
- * // The `_.property` iteratee shorthand.
10570
- * _.map(users, _.iteratee('user'));
10571
- * // => ['barney', 'fred']
10572
- *
10573
- * // Create custom iteratee shorthands.
10574
- * _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
10575
- * return !_.isRegExp(func) ? iteratee(func) : function(string) {
10576
- * return func.test(string);
10577
- * };
10578
- * });
10692
+ * _.isMatch(object, { 'b': 2 });
10693
+ * // => true
10579
10694
  *
10580
- * _.filter(['abc', 'def'], /ef/);
10581
- * // => ['def']
10695
+ * _.isMatch(object, { 'b': 1 });
10696
+ * // => false
10582
10697
  */
10583
- function iteratee(func) {
10584
- return baseIteratee(typeof func == 'function' ? func : baseClone(func, CLONE_DEEP_FLAG$1));
10698
+ function isMatch(object, source) {
10699
+ return object === source || baseIsMatch(object, source, getMatchData(source));
10585
10700
  }
10586
10701
 
10587
10702
  /**
@@ -10886,7 +11001,47 @@
10886
11001
  return object == null ? {} : basePick(object, paths);
10887
11002
  });
10888
11003
 
10889
- var _Model_instances, _Model_computedCache, _Model_computedDefs, _Model_state, _Model_hydrate, _Model_doAutoSave, _Model_updateComputed, _Model_initComputed;
11004
+ const INTERNALS = new WeakMap();
11005
+ function internals(model) {
11006
+ let priv = INTERNALS.get(model);
11007
+ if (!priv) {
11008
+ priv = { state: 'constructing' };
11009
+ INTERNALS.set(model, priv);
11010
+ }
11011
+ return priv;
11012
+ }
11013
+ /** Load the model from storage, marking it ready whether or not the read succeeds. */
11014
+ async function hydrate$1(model, priv) {
11015
+ ensureUnloadListener(PersistentStorage);
11016
+ try {
11017
+ await model.fetch({ fromStorage: true, promise: true });
11018
+ }
11019
+ finally {
11020
+ priv.state = 'ready';
11021
+ }
11022
+ }
11023
+ /** Evaluate the model's declared computed properties once, seeding the cache. */
11024
+ function initComputed(model, priv) {
11025
+ const defs = result(model, 'computed');
11026
+ if (!defs || isEmpty(defs))
11027
+ return;
11028
+ priv.computedDefs = defs;
11029
+ priv.computedCache = {};
11030
+ for (const [key, def] of Object.entries(defs)) {
11031
+ priv.computedCache[key] = def.fn(model);
11032
+ }
11033
+ }
11034
+ /** Recompute one computed property, firing `change:key` when its value changed. */
11035
+ function updateComputed(model, priv, key, def, silent, options) {
11036
+ const newVal = def.fn(model);
11037
+ if (!isEqual(priv.computedCache[key], newVal)) {
11038
+ priv.computedCache[key] = newVal;
11039
+ if (!silent) {
11040
+ model.changed[key] = newVal;
11041
+ model.trigger('change:' + key, model, newVal, options);
11042
+ }
11043
+ }
11044
+ }
10890
11045
  /**
10891
11046
  * @public
10892
11047
  * **Models** are the basic data object in the framework --
@@ -10901,15 +11056,12 @@
10901
11056
  */
10902
11057
  constructor(attributes, options) {
10903
11058
  super();
10904
- _Model_instances.add(this);
10905
- _Model_computedCache.set(this, void 0);
10906
- _Model_computedDefs.set(this, void 0);
10907
- _Model_state.set(this, 'constructing');
10908
11059
  this._changing = false;
10909
11060
  this._pending = false;
10910
11061
  this._url = '';
10911
11062
  this.changed = {};
10912
11063
  this.validationError = null;
11064
+ const priv = internals(this);
10913
11065
  let attrs = attributes || {};
10914
11066
  options = options || {};
10915
11067
  this.preinitialize.apply(this, arguments);
@@ -10922,11 +11074,12 @@
10922
11074
  const default_attrs = result(this, 'defaults');
10923
11075
  attrs = defaults(Object.assign({}, default_attrs, attrs), default_attrs);
10924
11076
  this.set(attrs, options);
10925
- __classPrivateFieldGet(this, _Model_instances, "m", _Model_initComputed).call(this);
11077
+ initComputed(this, priv);
10926
11078
  this.attrs = new Proxy(this.attributes, {
10927
11079
  get: (_target, key) => {
10928
- if (typeof key !== 'symbol' && __classPrivateFieldGet(this, _Model_computedCache, "f") && key in __classPrivateFieldGet(this, _Model_computedCache, "f")) {
10929
- return __classPrivateFieldGet(this, _Model_computedCache, "f")[key];
11080
+ const cache = priv.computedCache;
11081
+ if (typeof key !== 'symbol' && cache && key in cache) {
11082
+ return cache[key];
10930
11083
  }
10931
11084
  return this.attributes[key];
10932
11085
  },
@@ -10944,16 +11097,16 @@
10944
11097
  // otherwise it's already ready and resolves immediately.
10945
11098
  if (this.autoSync) {
10946
11099
  if (getStorage(this) && !this.isNew()) {
10947
- __classPrivateFieldSet(this, _Model_state, 'hydrating', "f");
10948
- this.hydrated = __classPrivateFieldGet(this, _Model_instances, "m", _Model_hydrate).call(this);
11100
+ priv.state = 'hydrating';
11101
+ this.hydrated = hydrate$1(this, priv);
10949
11102
  }
10950
11103
  else {
10951
- __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11104
+ priv.state = 'ready';
10952
11105
  this.hydrated = Promise.resolve();
10953
11106
  }
10954
11107
  }
10955
11108
  else {
10956
- __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11109
+ priv.state = 'ready';
10957
11110
  }
10958
11111
  }
10959
11112
  /**
@@ -11052,8 +11205,9 @@
11052
11205
  * Get the value of an attribute, or a computed property.
11053
11206
  */
11054
11207
  get(attr) {
11055
- if (__classPrivateFieldGet(this, _Model_computedCache, "f") && attr in __classPrivateFieldGet(this, _Model_computedCache, "f")) {
11056
- return __classPrivateFieldGet(this, _Model_computedCache, "f")[attr];
11208
+ const cache = internals(this).computedCache;
11209
+ if (cache && attr in cache) {
11210
+ return cache[attr];
11057
11211
  }
11058
11212
  return this.attributes[attr];
11059
11213
  }
@@ -11095,10 +11249,20 @@
11095
11249
  return this.get(attr) != null;
11096
11250
  }
11097
11251
  /**
11098
- * Special-cased proxy to lodash's `matches` method.
11252
+ * Returns `true` if the model's attributes satisfy `attrs`.
11253
+ *
11254
+ * An attributes object is compared with lodash's `isMatch`: a partial, deep
11255
+ * comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
11256
+ * that has `type: 'hidden'` alongside other keys. A function is called with
11257
+ * the attributes and its result coerced to a boolean.
11258
+ *
11259
+ * To scan a collection, prefer `Collection#find`, `filter` or `where`. They
11260
+ * build their matcher once per scan instead of once per model.
11099
11261
  */
11100
11262
  matches(attrs) {
11101
- return !!iteratee(attrs)(this.attributes);
11263
+ if (typeof attrs === 'function')
11264
+ return !!attrs(this.attributes);
11265
+ return isMatch(this.attributes, attrs);
11102
11266
  }
11103
11267
  /**
11104
11268
  * Set a hash of model attributes on the object, firing `"change"`. This is
@@ -11119,9 +11283,10 @@
11119
11283
  attrs[key] = val;
11120
11284
  }
11121
11285
  options = options || {};
11286
+ const priv = internals(this);
11122
11287
  // Guard must come after arg normalization: when called as set(obj, opts),
11123
11288
  // fromStorage lives in `val` (second arg), not `options` (third arg).
11124
- if (this.autoSync && __classPrivateFieldGet(this, _Model_state, "f") === 'hydrating' && !options.fromStorage) {
11289
+ if (this.autoSync && priv.state === 'hydrating' && !options.fromStorage) {
11125
11290
  throw new Error(`Skeletor: set() called on an autoSync model before hydration resolved. Await model.hydrated first.`);
11126
11291
  }
11127
11292
  // Run validation.
@@ -11165,10 +11330,10 @@
11165
11330
  }
11166
11331
  }
11167
11332
  // Recalculate computed properties whose deps changed.
11168
- if (changes.length && __classPrivateFieldGet(this, _Model_computedDefs, "f")) {
11169
- for (const [key, def] of Object.entries(__classPrivateFieldGet(this, _Model_computedDefs, "f"))) {
11333
+ if (changes.length && priv.computedDefs) {
11334
+ for (const [key, def] of Object.entries(priv.computedDefs)) {
11170
11335
  if (def.deps.some((dep) => changes.includes(dep))) {
11171
- __classPrivateFieldGet(this, _Model_instances, "m", _Model_updateComputed).call(this, key, def, silent, options);
11336
+ updateComputed(this, priv, key, def, silent, options);
11172
11337
  }
11173
11338
  }
11174
11339
  }
@@ -11188,13 +11353,13 @@
11188
11353
  // Auto-save: schedule a debounced write when autoSync is on and there
11189
11354
  // were actual changes that originated from application code (not a
11190
11355
  // storage read or an explicit noAutoSave call).
11191
- if (__classPrivateFieldGet(this, _Model_state, "f") === 'ready' &&
11356
+ if (priv.state === 'ready' &&
11192
11357
  this.autoSync &&
11193
11358
  getStorage(this) &&
11194
11359
  changes.length &&
11195
11360
  !options.fromStorage &&
11196
11361
  !options.noAutoSave) {
11197
- scheduleAutoSave(this, () => __classPrivateFieldGet(this, _Model_instances, "m", _Model_doAutoSave).call(this), this.autoSyncDelay);
11362
+ scheduleAutoSave(this, () => this.save(null, { noAutoSave: true }), this.autoSyncDelay);
11198
11363
  }
11199
11364
  return this;
11200
11365
  }
@@ -11271,47 +11436,25 @@
11271
11436
  options = Object.assign({ parse: true }, options);
11272
11437
  const success = options.success;
11273
11438
  const promise = options.promise ? getResolveablePromise() : undefined;
11274
- // Tracks whether sync invoked one of the callbacks. A sync may instead
11275
- // settle its returned promise; the `.then`/`.catch` below bridges that to
11276
- // the callbacks so the fetch promise never hangs. The flag is set
11277
- // synchronously inside each callback (before sync's promise resolves), so
11278
- // it's reliable by the time those handlers run.
11279
- let settled = false;
11280
11439
  options.success = (resp) => {
11281
- settled = true;
11282
11440
  const serverAttrs = options.parse ? this.parse(resp, options) : resp;
11283
11441
  // fromStorage prevents the set() from triggering auto-save on reads
11284
- if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
11442
+ if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
11443
+ // The fetched attributes failed validation (`'invalid'` has fired).
11444
+ // Settle the promise with the error rather than leave it pending.
11445
+ promise && promise.reject(this.validationError);
11285
11446
  return false;
11447
+ }
11286
11448
  if (success)
11287
11449
  success.call(options.context, this, resp, options);
11288
11450
  this.trigger('sync', this, resp, options);
11289
11451
  promise && promise.resolve(resp);
11290
11452
  };
11291
11453
  wrapError(this, options, promise);
11292
- if (promise) {
11293
- const onError = options.error;
11294
- options.error = (resp) => {
11295
- settled = true;
11296
- onError(resp);
11297
- };
11298
- }
11299
- const result = this.sync('read', this, options);
11300
- if (promise) {
11301
- Promise.resolve(result).then(
11302
- // A sync that resolves its promise without invoking options.success:
11303
- // treat the resolved value as the response, so the model is still
11304
- // merged and the promise settles instead of hanging.
11305
- (value) => {
11306
- if (!settled)
11307
- options.success(value);
11308
- },
11309
- // A sync that rejects instead of invoking options.error; settling
11310
- // twice is a harmless no-op.
11311
- (e) => promise.reject(e));
11312
- return promise;
11313
- }
11314
- return result;
11454
+ // With `promise`, the sync's outcome is bridged to it however the sync
11455
+ // reports it: through the callbacks, or through the promise it returns.
11456
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
11457
+ return promise ?? result;
11315
11458
  }
11316
11459
  /**
11317
11460
  * Set a hash of model attributes, and sync the model to the server.
@@ -11332,16 +11475,19 @@
11332
11475
  options = Object.assign({ validate: true, parse: true }, options);
11333
11476
  const wait = options.wait;
11334
11477
  const return_promise = options.promise;
11335
- const promise = return_promise && getResolveablePromise();
11478
+ const promise = return_promise ? getResolveablePromise() : undefined;
11336
11479
  // If we're not waiting and attributes exist, save acts as
11337
11480
  // `set(attr).save(null, opts)` with validation. Otherwise, check if
11338
11481
  // the model will be valid when the attributes, if any, are set.
11339
- if (attrs && !wait) {
11340
- if (!this.set(attrs, options))
11482
+ const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
11483
+ if (!valid) {
11484
+ // Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
11485
+ // rejected promise rather than `false`, so the promise contract holds
11486
+ // for every outcome.
11487
+ if (!promise)
11341
11488
  return false;
11342
- }
11343
- else if (!this._validate(attrs || {}, options)) {
11344
- return false;
11489
+ promise.reject(this.validationError);
11490
+ return promise;
11345
11491
  }
11346
11492
  // After a successful server-side save, the client is (optionally)
11347
11493
  // updated with the server-side state.
@@ -11354,8 +11500,12 @@
11354
11500
  let serverAttrs = options.parse ? this.parse(resp, options) : resp;
11355
11501
  if (wait)
11356
11502
  serverAttrs = Object.assign({}, attrs, serverAttrs);
11357
- if (serverAttrs && !this.set(serverAttrs, options))
11503
+ if (serverAttrs && !this.set(serverAttrs, options)) {
11504
+ // The server attributes failed validation (`'invalid'` has fired).
11505
+ // Settle the promise with the error rather than leave it pending.
11506
+ return_promise && promise.reject(this.validationError);
11358
11507
  return false;
11508
+ }
11359
11509
  if (success)
11360
11510
  success.call(options.context, this, resp, options);
11361
11511
  this.trigger('sync', this, resp, options);
@@ -11372,15 +11522,12 @@
11372
11522
  const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
11373
11523
  if (method === 'patch' && !options.attrs)
11374
11524
  options.attrs = attrs;
11375
- const xhr = this.sync(method, this, options);
11525
+ // With `promise`, the sync's outcome is bridged to it however the sync
11526
+ // reports it: through the callbacks, or through the promise it returns.
11527
+ const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
11376
11528
  // Restore attributes.
11377
11529
  this.attributes = attributes;
11378
- if (return_promise) {
11379
- return promise;
11380
- }
11381
- else {
11382
- return xhr;
11383
- }
11530
+ return promise ?? xhr;
11384
11531
  }
11385
11532
  /**
11386
11533
  * Destroy this model on the server if it was already persisted.
@@ -11493,40 +11640,40 @@
11493
11640
  return false;
11494
11641
  }
11495
11642
  }
11496
- _Model_computedCache = new WeakMap(), _Model_computedDefs = new WeakMap(), _Model_state = new WeakMap(), _Model_instances = new WeakSet(), _Model_hydrate = async function _Model_hydrate() {
11497
- ensureUnloadListener(PersistentStorage);
11498
- try {
11499
- await this.fetch({ fromStorage: true, promise: true });
11500
- }
11501
- finally {
11502
- __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11503
- }
11504
- }, _Model_doAutoSave = function _Model_doAutoSave() {
11505
- return this.save(null, { noAutoSave: true });
11506
- }, _Model_updateComputed = function _Model_updateComputed(key, def, silent, options) {
11507
- const newVal = def.fn(this);
11508
- if (!isEqual(__classPrivateFieldGet(this, _Model_computedCache, "f")[key], newVal)) {
11509
- __classPrivateFieldGet(this, _Model_computedCache, "f")[key] = newVal;
11510
- if (!silent) {
11511
- this.changed[key] = newVal;
11512
- this.trigger('change:' + key, this, newVal, options);
11513
- }
11514
- }
11515
- }, _Model_initComputed = function _Model_initComputed() {
11516
- const defs = result(this, 'computed');
11517
- if (!defs || isEmpty(defs))
11518
- return;
11519
- __classPrivateFieldSet(this, _Model_computedDefs, defs, "f");
11520
- __classPrivateFieldSet(this, _Model_computedCache, {}, "f");
11521
- for (const [key, def] of Object.entries(defs)) {
11522
- __classPrivateFieldGet(this, _Model_computedCache, "f")[key] = def.fn(this);
11523
- }
11524
- };
11525
11643
 
11526
- var _Collection_instances, _Collection_hydrate, _a;
11644
+ var _a;
11527
11645
  // Default options for `Collection#set`.
11528
11646
  const setOptions = { add: true, remove: true, merge: true };
11529
11647
  const addOptions = { add: true, remove: false };
11648
+ /**
11649
+ * Normalizes the predicate forms accepted by the query methods into one
11650
+ * `(model) => value` function. A function is used as-is, a string reads that
11651
+ * attribute, and an attributes object becomes a lodash `matches` matcher.
11652
+ *
11653
+ * The matcher is built here, once per scan. Calling `Model#matches` per model
11654
+ * instead rebuilds the match data for every model visited, which made
11655
+ * `findWhere` an order of magnitude slower than a hand-written predicate.
11656
+ */
11657
+ function toModelPredicate(pred) {
11658
+ if (isFunction(pred))
11659
+ return pred;
11660
+ if (isString(pred))
11661
+ return (m) => m.get(pred);
11662
+ const matcher = matches(pred);
11663
+ return (m) => matcher(m.attributes);
11664
+ }
11665
+ /**
11666
+ * Load the collection's members from storage. A module-level function rather
11667
+ * than a `#private` method so the published declarations carry no private
11668
+ * members, which TypeScript 6 rejects in anonymously extended classes (TS4094).
11669
+ */
11670
+ async function hydrate(collection) {
11671
+ ensureUnloadListener(PersistentStorage);
11672
+ // An empty store is a normal first-run state: findAll() returns [] and a
11673
+ // missing model record resolves to null, so hydration resolves rather
11674
+ // than rejecting.
11675
+ await collection.fetch({ fromStorage: true, promise: true });
11676
+ }
11530
11677
  /**
11531
11678
  * @public
11532
11679
  * If models tend to represent a single row of data, a Collection is
@@ -11544,7 +11691,6 @@
11544
11691
  */
11545
11692
  constructor(models, options) {
11546
11693
  super();
11547
- _Collection_instances.add(this);
11548
11694
  this._url = '';
11549
11695
  /**
11550
11696
  * @public
@@ -11566,7 +11712,7 @@
11566
11712
  // uniformly `await collection.hydrated`; it resolves immediately when
11567
11713
  // there's no storage to hydrate from.
11568
11714
  if (this.autoSync) {
11569
- this.hydrated = getStorage(this) ? __classPrivateFieldGet(this, _Collection_instances, "m", _Collection_hydrate).call(this) : Promise.resolve();
11715
+ this.hydrated = getStorage(this) ? hydrate(this) : Promise.resolve();
11570
11716
  }
11571
11717
  }
11572
11718
  get comparator() {
@@ -11855,17 +12001,13 @@
11855
12001
  return this.models.slice(start, end);
11856
12002
  }
11857
12003
  filter(callback, thisArg) {
11858
- return this.models.filter(isFunction(callback)
11859
- ? callback
11860
- : (m) => m.matches(callback), thisArg);
12004
+ return this.models.filter(toModelPredicate(callback), thisArg);
11861
12005
  }
11862
12006
  every(pred) {
11863
12007
  if (isFunction(pred)) {
11864
- return this.models.map((m) => m.attributes).every(pred);
11865
- }
11866
- else {
11867
- return this.models.every((m) => m.matches(pred));
12008
+ return this.models.every((m) => pred(m.attributes));
11868
12009
  }
12010
+ return this.models.every(toModelPredicate(pred));
11869
12011
  }
11870
12012
  difference(values) {
11871
12013
  return this.models.filter((m) => !values.includes(m));
@@ -11881,16 +12023,12 @@
11881
12023
  }
11882
12024
  some(pred) {
11883
12025
  if (isFunction(pred)) {
11884
- return this.models.map((m) => m.attributes).some(pred);
11885
- }
11886
- else {
11887
- return this.models.some((m) => m.matches(pred));
12026
+ return this.models.some((m) => pred(m.attributes));
11888
12027
  }
12028
+ return this.models.some(toModelPredicate(pred));
11889
12029
  }
11890
12030
  sortBy(iteratee) {
11891
- return sortBy(this.models, isFunction(iteratee)
11892
- ? iteratee
11893
- : (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
12031
+ return sortBy(this.models, toModelPredicate(iteratee));
11894
12032
  }
11895
12033
  isEmpty() {
11896
12034
  return !this.models.length;
@@ -11911,26 +12049,22 @@
11911
12049
  return this.models.length;
11912
12050
  }
11913
12051
  countBy(f) {
11914
- return countBy(this.models, isFunction(f) ? f : (m) => (isString(f) ? m.get(f) : m.matches(f)));
12052
+ return countBy(this.models, toModelPredicate(f));
11915
12053
  }
11916
12054
  groupBy(pred) {
11917
- return groupBy(this.models, isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12055
+ return groupBy(this.models, toModelPredicate(pred));
11918
12056
  }
11919
12057
  indexOf(model, fromIndex) {
11920
12058
  return this.models.indexOf(model, fromIndex);
11921
12059
  }
11922
12060
  findLastIndex(pred, fromIndex) {
11923
- return this.models.findLastIndex(isFunction(pred)
11924
- ? pred
11925
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
12061
+ return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
11926
12062
  }
11927
12063
  lastIndexOf(model, fromIndex) {
11928
12064
  return this.models.lastIndexOf(model, fromIndex);
11929
12065
  }
11930
12066
  findIndex(pred) {
11931
- return this.models.findIndex(isFunction(pred)
11932
- ? pred
11933
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12067
+ return this.models.findIndex(toModelPredicate(pred));
11934
12068
  }
11935
12069
  last() {
11936
12070
  const length = this.models == null ? 0 : this.models.length;
@@ -11943,9 +12077,7 @@
11943
12077
  return this.head();
11944
12078
  }
11945
12079
  map(cb, thisArg) {
11946
- return this.models.map(isFunction(cb)
11947
- ? cb
11948
- : (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
12080
+ return this.models.map(toModelPredicate(cb), thisArg);
11949
12081
  }
11950
12082
  reduce(callback, initialValue) {
11951
12083
  return this.models.reduce(callback, initialValue || this.models[0]);
@@ -11996,10 +12128,7 @@
11996
12128
  return this.where(attrs, true);
11997
12129
  }
11998
12130
  find(predicate, fromIndex) {
11999
- const pred = isFunction(predicate)
12000
- ? predicate
12001
- : (m) => m.matches(predicate);
12002
- return this.models.find(pred, fromIndex);
12131
+ return this.models.find(toModelPredicate(predicate), fromIndex);
12003
12132
  }
12004
12133
  /**
12005
12134
  * Force the collection to re-sort itself. You don't need to call this under
@@ -12042,46 +12171,22 @@
12042
12171
  // eslint-disable-next-line @typescript-eslint/no-this-alias
12043
12172
  const collection = this;
12044
12173
  const promise = options.promise ? getResolveablePromise() : undefined;
12045
- // Tracks whether sync invoked one of the callbacks. A sync may instead
12046
- // settle its returned promise; the `.then`/`.catch` below bridges that to
12047
- // the callbacks so the fetch promise never hangs. The flag is set
12048
- // synchronously inside each callback (before sync's promise resolves), so
12049
- // it's reliable by the time those handlers run.
12050
- let settled = false;
12051
12174
  options.success = function (resp) {
12052
- settled = true;
12053
12175
  const method = options.reset ? 'reset' : 'set';
12054
12176
  // fromStorage prevents individual model set() calls from triggering auto-save on reads
12055
12177
  collection[method](resp, Object.assign({}, options, { fromStorage: true }));
12056
12178
  if (success)
12057
12179
  success.call(options.context, collection, resp, options);
12058
- promise && promise.resolve();
12180
+ // Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
12181
+ // listener rejects the promise instead of being lost.
12059
12182
  collection.trigger('sync', collection, resp, options);
12183
+ promise && promise.resolve();
12060
12184
  };
12061
12185
  wrapError(this, options, promise);
12062
- if (promise) {
12063
- const onError = options.error;
12064
- options.error = (resp) => {
12065
- settled = true;
12066
- onError(resp);
12067
- };
12068
- }
12069
- const result = this.sync('read', this, options);
12070
- if (promise) {
12071
- Promise.resolve(result).then(
12072
- // A sync that resolves its promise without invoking options.success:
12073
- // treat the resolved value as the response, so the collection is still
12074
- // populated and the promise settles instead of hanging.
12075
- (value) => {
12076
- if (!settled)
12077
- options.success(value);
12078
- },
12079
- // A sync that rejects instead of invoking options.error; settling
12080
- // twice is a harmless no-op.
12081
- (e) => promise.reject(e));
12082
- return promise;
12083
- }
12084
- return result;
12186
+ // With `promise`, the sync's outcome is bridged to it however the sync
12187
+ // reports it: through the callbacks, or through the promise it returns.
12188
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
12189
+ return promise ?? result;
12085
12190
  }
12086
12191
  /**
12087
12192
  * Create a new instance of a model in this collection. Add the model to the
@@ -12093,9 +12198,22 @@
12093
12198
  const wait = options.wait;
12094
12199
  const return_promise = options.promise;
12095
12200
  const promise = return_promise && getResolveablePromise();
12201
+ // `_prepareModel` reports attributes that fail validation by firing
12202
+ // `'invalid'` and returning null. With `promise`, capture that error so a
12203
+ // rejected promise is handed back rather than `null`.
12204
+ let validationError;
12205
+ const captureInvalid = (_c, err) => (validationError = err);
12206
+ if (return_promise)
12207
+ this.on('invalid', captureInvalid);
12096
12208
  const preparedModel = this._prepareModel(model, options);
12097
- if (!preparedModel)
12098
- return null;
12209
+ if (return_promise)
12210
+ this.off('invalid', captureInvalid);
12211
+ if (!preparedModel) {
12212
+ if (!return_promise)
12213
+ return null;
12214
+ promise.reject(validationError);
12215
+ return promise;
12216
+ }
12099
12217
  if (!wait)
12100
12218
  this.add(preparedModel, options);
12101
12219
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -12117,8 +12235,15 @@
12117
12235
  error && error.call(options.context, model, e, options);
12118
12236
  return_promise && promise.reject(e);
12119
12237
  };
12120
- preparedModel.save(null, Object.assign(options, { 'promise': false }));
12238
+ // `options.promise` is passed through, so `save` installs its own bridge
12239
+ // from the sync's returned promise to its callbacks: a sync that resolves
12240
+ // without calling them still reaches the wired `options.success` above,
12241
+ // which resolves our promise with the model. Only a rejection that bypassed
12242
+ // the callbacks (a rejecting sync, or a throwing `'sync'`/`'error'`
12243
+ // listener) still needs bridging to ours.
12244
+ const result = preparedModel.save(null, options);
12121
12245
  if (return_promise) {
12246
+ Promise.resolve(result).catch((e) => promise.reject(e));
12122
12247
  return promise;
12123
12248
  }
12124
12249
  else {
@@ -12270,13 +12395,7 @@
12270
12395
  this.trigger.apply(this, arguments);
12271
12396
  }
12272
12397
  }
12273
- _Collection_instances = new WeakSet(), _Collection_hydrate = async function _Collection_hydrate() {
12274
- ensureUnloadListener(PersistentStorage);
12275
- // An empty store is a normal first-run state: findAll() returns [] and a
12276
- // missing model record resolves to null, so hydration resolves rather
12277
- // than rejecting.
12278
- await this.fetch({ fromStorage: true, promise: true });
12279
- }, _a = Symbol.iterator;
12398
+ _a = Symbol.iterator;
12280
12399
  // This "enum" defines the three possible kinds of values which can be emitted
12281
12400
  // by a CollectionIterator that correspond to the values(), keys() and entries()
12282
12401
  // methods on Collection, respectively.
@@ -12344,6 +12463,7 @@
12344
12463
  EventEmitter,
12345
12464
  Model,
12346
12465
  sync,
12466
+ HTTPError,
12347
12467
  };
12348
12468
  // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
12349
12469
  // We use `self` instead of `window` for `WebWorker` support.
@@ -12374,6 +12494,7 @@
12374
12494
  exports.BrowserStorage = BrowserStorage;
12375
12495
  exports.Collection = Collection;
12376
12496
  exports.EventEmitter = EventEmitter;
12497
+ exports.HTTPError = HTTPError;
12377
12498
  exports.Model = Model;
12378
12499
  exports.PersistentStorage = PersistentStorage;
12379
12500
  exports.default = skeletor;