@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.
@@ -31,19 +31,6 @@ function __values(o) {
31
31
  throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
32
32
  }
33
33
 
34
- function __classPrivateFieldGet(receiver, state, kind, f) {
35
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
36
- 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");
37
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
38
- }
39
-
40
- function __classPrivateFieldSet(receiver, state, value, kind, f) {
41
- if (kind === "m") throw new TypeError("Private method is not writable");
42
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
43
- 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");
44
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
45
- }
46
-
47
34
  typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
48
35
  var e = new Error(message);
49
36
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
@@ -7792,6 +7779,16 @@ function guid() {
7792
7779
  // Generate a pseudo-GUID by concatenating random hexadecimal.
7793
7780
  return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
7794
7781
  }
7782
+ // Helpers
7783
+ // -------
7784
+ /**
7785
+ * Whether `value` is a thenable (a promise, or a promise-like object such as
7786
+ * a jqXHR). Decides whether a `sync` return value can be bridged to the
7787
+ * promise that `fetch`/`save` hand back.
7788
+ */
7789
+ function isThenable(value) {
7790
+ return value != null && typeof value.then === 'function';
7791
+ }
7795
7792
  function getResolveablePromise() {
7796
7793
  const wrapper = {
7797
7794
  isResolved: false,
@@ -7836,6 +7833,65 @@ function wrapError(model, options, promise) {
7836
7833
  promise && promise.reject && promise.reject(resp);
7837
7834
  };
7838
7835
  }
7836
+ /**
7837
+ * Run a `sync` and make sure `promise` settles from its outcome, however the
7838
+ * sync reports it.
7839
+ *
7840
+ * A `sync` may invoke `options.success`/`options.error` (the callback
7841
+ * contract), or settle the promise it returns without calling them: a
7842
+ * promise-only custom sync, or an async sync whose promise rejects because a
7843
+ * `'sync'`/`'error'` listener threw after the callbacks ran. `options.success`
7844
+ * and `options.error` must already be wired to resolve/reject `promise`. This
7845
+ * wraps them to record that they ran, calls `run()`, and bridges a thenable
7846
+ * result to them so the promise never hangs:
7847
+ *
7848
+ * - fulfilment with the callbacks unrun is delivered to `options.success`, so
7849
+ * the response is still merged into the model or collection;
7850
+ * - rejection with the callbacks unrun is delivered to `options.error`, so the
7851
+ * caller's `error` callback and the `'error'` event still fire;
7852
+ * - rejection after the callbacks ran rejects `promise` directly: a no-op if
7853
+ * they already settled it, and the only way to settle it if a listener threw
7854
+ * before they could;
7855
+ * - a throw from either delivery (a throwing `success`/`error` callback or
7856
+ * listener) rejects `promise` instead of surfacing as an unhandled rejection.
7857
+ *
7858
+ * A non-thenable result (a callback-only sync that returns nothing, or an
7859
+ * XHR) is left alone: such a sync settles `promise` through its callbacks, and
7860
+ * bridging it would fire `success` on the next microtask, before the sync is
7861
+ * done. Without a `promise`, `run()` is called as is.
7862
+ */
7863
+ function bridgeSyncToPromise(options, promise, run) {
7864
+ if (!promise)
7865
+ return run();
7866
+ let settled = false;
7867
+ const success = options.success;
7868
+ const error = options.error;
7869
+ options.success = (...args) => {
7870
+ settled = true;
7871
+ return success && success(...args);
7872
+ };
7873
+ options.error = (...args) => {
7874
+ settled = true;
7875
+ return error && error(...args);
7876
+ };
7877
+ const result = run();
7878
+ if (isThenable(result)) {
7879
+ Promise.resolve(result)
7880
+ .then((value) => {
7881
+ if (!settled)
7882
+ options.success(value);
7883
+ }, (e) => {
7884
+ if (settled) {
7885
+ promise.reject(e);
7886
+ }
7887
+ else {
7888
+ options.error(e);
7889
+ }
7890
+ })
7891
+ .catch((e) => promise.reject(e));
7892
+ }
7893
+ return result;
7894
+ }
7839
7895
  // Map from CRUD to HTTP for our default `sync` implementation.
7840
7896
  const methodMap = {
7841
7897
  create: 'POST',
@@ -7885,43 +7941,77 @@ function getSyncMethod(model) {
7885
7941
  return store ? store.sync() : sync;
7886
7942
  }
7887
7943
  /**
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
7944
+ * The error the default `sync` hands to `options.error`, and rejects with, for
7945
+ * a response outside the 2xx range.
7946
+ * @public
7898
7947
  */
7948
+ class HTTPError extends Error {
7949
+ constructor(response, method, url) {
7950
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
7951
+ super(`${method} ${url} failed with HTTP ${response.status}${statusText}`);
7952
+ this.name = 'HTTPError';
7953
+ this.status = response.status;
7954
+ this.response = response;
7955
+ }
7956
+ }
7899
7957
  /**
7958
+ * The default `sync`: a JSON request over `fetch()` to the model's `url()`.
7959
+ * Override it (or `Model.sync`/`Collection.sync`) to change how models are
7960
+ * persisted, for instance to batch rapid-fire updates with `setTimeout`, to
7961
+ * talk over WebSockets, or to write to browser storage.
7962
+ *
7963
+ * It honours both contracts a `sync` may use. On a 2xx response the JSON body
7964
+ * (`null` when the body is empty) is passed to `options.success` and the
7965
+ * returned promise resolves with it. On a non-2xx response, a network failure
7966
+ * or an unparsable body, `options.error` receives the error (an `HTTPError`
7967
+ * for the first) and the promise rejects with it.
7968
+ *
7969
+ * `options.data` is sent as query parameters on GET, which may not carry a
7970
+ * body, and as a JSON body otherwise. For create, update and patch the body
7971
+ * defaults to `options.attrs` or the model's `toJSON()`.
7900
7972
  * @public
7901
7973
  */
7902
7974
  function sync(method, model, options = {}) {
7975
+ const type = methodMap[method];
7903
7976
  let data = options.data;
7904
7977
  // Ensure that we have the appropriate request data.
7905
7978
  if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
7906
7979
  data = options.attrs || model.toJSON();
7907
7980
  }
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;
7981
+ let url = options.url || result(model, 'url') || urlError();
7982
+ const params = { method: type };
7983
+ if (data != null) {
7984
+ if (type === 'GET') {
7985
+ url += (url.includes('?') ? '&' : '?') + new URLSearchParams(data).toString();
7986
+ }
7987
+ else {
7988
+ params.body = JSON.stringify(data);
7989
+ params.headers = { 'Content-Type': 'application/json' };
7990
+ }
7922
7991
  }
7992
+ const xhr = fetch(url, params);
7993
+ options.xhr = xhr;
7923
7994
  model.trigger('request', model, xhr, { ...params, xhr });
7924
- return xhr;
7995
+ return Promise.resolve(xhr)
7996
+ .then(async (response) => {
7997
+ if (!response.ok)
7998
+ throw new HTTPError(response, type, url);
7999
+ const text = await response.text();
8000
+ return text ? JSON.parse(text) : null;
8001
+ })
8002
+ .then(
8003
+ // The callbacks run before the returned promise settles, so a caller that
8004
+ // bridges this promise (`Model.fetch`/`save`) sees the outcome as already
8005
+ // reported. A throw from `success` (a `'sync'` listener, say) is not
8006
+ // routed to `error`: it rejects the promise, which is what the bridge
8007
+ // expects once the callbacks have run.
8008
+ (resp) => {
8009
+ options.success?.(resp, options);
8010
+ return resp;
8011
+ }, (e) => {
8012
+ options.error?.(e);
8013
+ throw e;
8014
+ });
7925
8015
  }
7926
8016
 
7927
8017
  /**
@@ -10115,6 +10205,48 @@ var keyBy = createAggregator(function(result, value, key) {
10115
10205
  baseAssignValue(result, key, value);
10116
10206
  });
10117
10207
 
10208
+ /** Used to compose bitmasks for cloning. */
10209
+ var CLONE_DEEP_FLAG$1 = 1;
10210
+
10211
+ /**
10212
+ * Creates a function that performs a partial deep comparison between a given
10213
+ * object and `source`, returning `true` if the given object has equivalent
10214
+ * property values, else `false`.
10215
+ *
10216
+ * **Note:** The created function is equivalent to `_.isMatch` with `source`
10217
+ * partially applied.
10218
+ *
10219
+ * Partial comparisons will match empty array and empty object `source`
10220
+ * values against any array or object value, respectively. See `_.isEqual`
10221
+ * for a list of supported value comparisons.
10222
+ *
10223
+ * **Note:** Multiple values can be checked by combining several matchers
10224
+ * using `_.overSome`
10225
+ *
10226
+ * @static
10227
+ * @memberOf _
10228
+ * @since 3.0.0
10229
+ * @category Util
10230
+ * @param {Object} source The object of property values to match.
10231
+ * @returns {Function} Returns the new spec function.
10232
+ * @example
10233
+ *
10234
+ * var objects = [
10235
+ * { 'a': 1, 'b': 2, 'c': 3 },
10236
+ * { 'a': 4, 'b': 5, 'c': 6 }
10237
+ * ];
10238
+ *
10239
+ * _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
10240
+ * // => [{ 'a': 4, 'b': 5, 'c': 6 }]
10241
+ *
10242
+ * // Checking for several possible values
10243
+ * _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
10244
+ * // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
10245
+ */
10246
+ function matches(source) {
10247
+ return baseMatches(baseClone(source, CLONE_DEEP_FLAG$1));
10248
+ }
10249
+
10118
10250
  /** Built-in value references. */
10119
10251
  var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
10120
10252
 
@@ -10678,53 +10810,36 @@ function isEqual(value, other) {
10678
10810
  return baseIsEqual(value, other);
10679
10811
  }
10680
10812
 
10681
- /** Used to compose bitmasks for cloning. */
10682
- var CLONE_DEEP_FLAG$1 = 1;
10683
-
10684
10813
  /**
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`.
10814
+ * Performs a partial deep comparison between `object` and `source` to
10815
+ * determine if `object` contains equivalent property values.
10816
+ *
10817
+ * **Note:** This method is equivalent to `_.matches` when `source` is
10818
+ * partially applied.
10819
+ *
10820
+ * Partial comparisons will match empty array and empty object `source`
10821
+ * values against any array or object value, respectively. See `_.isEqual`
10822
+ * for a list of supported value comparisons.
10690
10823
  *
10691
10824
  * @static
10692
- * @since 4.0.0
10693
10825
  * @memberOf _
10694
- * @category Util
10695
- * @param {*} [func=_.identity] The value to convert to a callback.
10696
- * @returns {Function} Returns the callback.
10826
+ * @since 3.0.0
10827
+ * @category Lang
10828
+ * @param {Object} object The object to inspect.
10829
+ * @param {Object} source The object of property values to match.
10830
+ * @returns {boolean} Returns `true` if `object` is a match, else `false`.
10697
10831
  * @example
10698
10832
  *
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 }]
10833
+ * var object = { 'a': 1, 'b': 2 };
10711
10834
  *
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
- * });
10835
+ * _.isMatch(object, { 'b': 2 });
10836
+ * // => true
10722
10837
  *
10723
- * _.filter(['abc', 'def'], /ef/);
10724
- * // => ['def']
10838
+ * _.isMatch(object, { 'b': 1 });
10839
+ * // => false
10725
10840
  */
10726
- function iteratee(func) {
10727
- return baseIteratee(typeof func == 'function' ? func : baseClone(func, CLONE_DEEP_FLAG$1));
10841
+ function isMatch(object, source) {
10842
+ return object === source || baseIsMatch(object, source, getMatchData(source));
10728
10843
  }
10729
10844
 
10730
10845
  /**
@@ -11029,7 +11144,47 @@ var pick = flatRest(function(object, paths) {
11029
11144
  return object == null ? {} : basePick(object, paths);
11030
11145
  });
11031
11146
 
11032
- var _Model_instances, _Model_computedCache, _Model_computedDefs, _Model_state, _Model_hydrate, _Model_doAutoSave, _Model_updateComputed, _Model_initComputed;
11147
+ const INTERNALS = new WeakMap();
11148
+ function internals(model) {
11149
+ let priv = INTERNALS.get(model);
11150
+ if (!priv) {
11151
+ priv = { state: 'constructing' };
11152
+ INTERNALS.set(model, priv);
11153
+ }
11154
+ return priv;
11155
+ }
11156
+ /** Load the model from storage, marking it ready whether or not the read succeeds. */
11157
+ async function hydrate$1(model, priv) {
11158
+ ensureUnloadListener(PersistentStorage);
11159
+ try {
11160
+ await model.fetch({ fromStorage: true, promise: true });
11161
+ }
11162
+ finally {
11163
+ priv.state = 'ready';
11164
+ }
11165
+ }
11166
+ /** Evaluate the model's declared computed properties once, seeding the cache. */
11167
+ function initComputed(model, priv) {
11168
+ const defs = result(model, 'computed');
11169
+ if (!defs || isEmpty(defs))
11170
+ return;
11171
+ priv.computedDefs = defs;
11172
+ priv.computedCache = {};
11173
+ for (const [key, def] of Object.entries(defs)) {
11174
+ priv.computedCache[key] = def.fn(model);
11175
+ }
11176
+ }
11177
+ /** Recompute one computed property, firing `change:key` when its value changed. */
11178
+ function updateComputed(model, priv, key, def, silent, options) {
11179
+ const newVal = def.fn(model);
11180
+ if (!isEqual(priv.computedCache[key], newVal)) {
11181
+ priv.computedCache[key] = newVal;
11182
+ if (!silent) {
11183
+ model.changed[key] = newVal;
11184
+ model.trigger('change:' + key, model, newVal, options);
11185
+ }
11186
+ }
11187
+ }
11033
11188
  /**
11034
11189
  * @public
11035
11190
  * **Models** are the basic data object in the framework --
@@ -11044,15 +11199,12 @@ class Model extends EventEmitterObject {
11044
11199
  */
11045
11200
  constructor(attributes, options) {
11046
11201
  super();
11047
- _Model_instances.add(this);
11048
- _Model_computedCache.set(this, void 0);
11049
- _Model_computedDefs.set(this, void 0);
11050
- _Model_state.set(this, 'constructing');
11051
11202
  this._changing = false;
11052
11203
  this._pending = false;
11053
11204
  this._url = '';
11054
11205
  this.changed = {};
11055
11206
  this.validationError = null;
11207
+ const priv = internals(this);
11056
11208
  let attrs = attributes || {};
11057
11209
  options = options || {};
11058
11210
  this.preinitialize.apply(this, arguments);
@@ -11065,11 +11217,12 @@ class Model extends EventEmitterObject {
11065
11217
  const default_attrs = result(this, 'defaults');
11066
11218
  attrs = defaults(Object.assign({}, default_attrs, attrs), default_attrs);
11067
11219
  this.set(attrs, options);
11068
- __classPrivateFieldGet(this, _Model_instances, "m", _Model_initComputed).call(this);
11220
+ initComputed(this, priv);
11069
11221
  this.attrs = new Proxy(this.attributes, {
11070
11222
  get: (_target, key) => {
11071
- if (typeof key !== 'symbol' && __classPrivateFieldGet(this, _Model_computedCache, "f") && key in __classPrivateFieldGet(this, _Model_computedCache, "f")) {
11072
- return __classPrivateFieldGet(this, _Model_computedCache, "f")[key];
11223
+ const cache = priv.computedCache;
11224
+ if (typeof key !== 'symbol' && cache && key in cache) {
11225
+ return cache[key];
11073
11226
  }
11074
11227
  return this.attributes[key];
11075
11228
  },
@@ -11087,16 +11240,16 @@ class Model extends EventEmitterObject {
11087
11240
  // otherwise it's already ready and resolves immediately.
11088
11241
  if (this.autoSync) {
11089
11242
  if (getStorage(this) && !this.isNew()) {
11090
- __classPrivateFieldSet(this, _Model_state, 'hydrating', "f");
11091
- this.hydrated = __classPrivateFieldGet(this, _Model_instances, "m", _Model_hydrate).call(this);
11243
+ priv.state = 'hydrating';
11244
+ this.hydrated = hydrate$1(this, priv);
11092
11245
  }
11093
11246
  else {
11094
- __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11247
+ priv.state = 'ready';
11095
11248
  this.hydrated = Promise.resolve();
11096
11249
  }
11097
11250
  }
11098
11251
  else {
11099
- __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11252
+ priv.state = 'ready';
11100
11253
  }
11101
11254
  }
11102
11255
  /**
@@ -11195,8 +11348,9 @@ class Model extends EventEmitterObject {
11195
11348
  * Get the value of an attribute, or a computed property.
11196
11349
  */
11197
11350
  get(attr) {
11198
- if (__classPrivateFieldGet(this, _Model_computedCache, "f") && attr in __classPrivateFieldGet(this, _Model_computedCache, "f")) {
11199
- return __classPrivateFieldGet(this, _Model_computedCache, "f")[attr];
11351
+ const cache = internals(this).computedCache;
11352
+ if (cache && attr in cache) {
11353
+ return cache[attr];
11200
11354
  }
11201
11355
  return this.attributes[attr];
11202
11356
  }
@@ -11238,10 +11392,20 @@ class Model extends EventEmitterObject {
11238
11392
  return this.get(attr) != null;
11239
11393
  }
11240
11394
  /**
11241
- * Special-cased proxy to lodash's `matches` method.
11395
+ * Returns `true` if the model's attributes satisfy `attrs`.
11396
+ *
11397
+ * An attributes object is compared with lodash's `isMatch`: a partial, deep
11398
+ * comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
11399
+ * that has `type: 'hidden'` alongside other keys. A function is called with
11400
+ * the attributes and its result coerced to a boolean.
11401
+ *
11402
+ * To scan a collection, prefer `Collection#find`, `filter` or `where`. They
11403
+ * build their matcher once per scan instead of once per model.
11242
11404
  */
11243
11405
  matches(attrs) {
11244
- return !!iteratee(attrs)(this.attributes);
11406
+ if (typeof attrs === 'function')
11407
+ return !!attrs(this.attributes);
11408
+ return isMatch(this.attributes, attrs);
11245
11409
  }
11246
11410
  /**
11247
11411
  * Set a hash of model attributes on the object, firing `"change"`. This is
@@ -11262,9 +11426,10 @@ class Model extends EventEmitterObject {
11262
11426
  attrs[key] = val;
11263
11427
  }
11264
11428
  options = options || {};
11429
+ const priv = internals(this);
11265
11430
  // Guard must come after arg normalization: when called as set(obj, opts),
11266
11431
  // fromStorage lives in `val` (second arg), not `options` (third arg).
11267
- if (this.autoSync && __classPrivateFieldGet(this, _Model_state, "f") === 'hydrating' && !options.fromStorage) {
11432
+ if (this.autoSync && priv.state === 'hydrating' && !options.fromStorage) {
11268
11433
  throw new Error(`Skeletor: set() called on an autoSync model before hydration resolved. Await model.hydrated first.`);
11269
11434
  }
11270
11435
  // Run validation.
@@ -11308,10 +11473,10 @@ class Model extends EventEmitterObject {
11308
11473
  }
11309
11474
  }
11310
11475
  // Recalculate computed properties whose deps changed.
11311
- if (changes.length && __classPrivateFieldGet(this, _Model_computedDefs, "f")) {
11312
- for (const [key, def] of Object.entries(__classPrivateFieldGet(this, _Model_computedDefs, "f"))) {
11476
+ if (changes.length && priv.computedDefs) {
11477
+ for (const [key, def] of Object.entries(priv.computedDefs)) {
11313
11478
  if (def.deps.some((dep) => changes.includes(dep))) {
11314
- __classPrivateFieldGet(this, _Model_instances, "m", _Model_updateComputed).call(this, key, def, silent, options);
11479
+ updateComputed(this, priv, key, def, silent, options);
11315
11480
  }
11316
11481
  }
11317
11482
  }
@@ -11331,13 +11496,13 @@ class Model extends EventEmitterObject {
11331
11496
  // Auto-save: schedule a debounced write when autoSync is on and there
11332
11497
  // were actual changes that originated from application code (not a
11333
11498
  // storage read or an explicit noAutoSave call).
11334
- if (__classPrivateFieldGet(this, _Model_state, "f") === 'ready' &&
11499
+ if (priv.state === 'ready' &&
11335
11500
  this.autoSync &&
11336
11501
  getStorage(this) &&
11337
11502
  changes.length &&
11338
11503
  !options.fromStorage &&
11339
11504
  !options.noAutoSave) {
11340
- scheduleAutoSave(this, () => __classPrivateFieldGet(this, _Model_instances, "m", _Model_doAutoSave).call(this), this.autoSyncDelay);
11505
+ scheduleAutoSave(this, () => this.save(null, { noAutoSave: true }), this.autoSyncDelay);
11341
11506
  }
11342
11507
  return this;
11343
11508
  }
@@ -11414,47 +11579,25 @@ class Model extends EventEmitterObject {
11414
11579
  options = Object.assign({ parse: true }, options);
11415
11580
  const success = options.success;
11416
11581
  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
11582
  options.success = (resp) => {
11424
- settled = true;
11425
11583
  const serverAttrs = options.parse ? this.parse(resp, options) : resp;
11426
11584
  // fromStorage prevents the set() from triggering auto-save on reads
11427
- if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
11585
+ if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
11586
+ // The fetched attributes failed validation (`'invalid'` has fired).
11587
+ // Settle the promise with the error rather than leave it pending.
11588
+ promise && promise.reject(this.validationError);
11428
11589
  return false;
11590
+ }
11429
11591
  if (success)
11430
11592
  success.call(options.context, this, resp, options);
11431
11593
  this.trigger('sync', this, resp, options);
11432
11594
  promise && promise.resolve(resp);
11433
11595
  };
11434
11596
  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;
11597
+ // With `promise`, the sync's outcome is bridged to it however the sync
11598
+ // reports it: through the callbacks, or through the promise it returns.
11599
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
11600
+ return promise ?? result;
11458
11601
  }
11459
11602
  /**
11460
11603
  * Set a hash of model attributes, and sync the model to the server.
@@ -11475,16 +11618,19 @@ class Model extends EventEmitterObject {
11475
11618
  options = Object.assign({ validate: true, parse: true }, options);
11476
11619
  const wait = options.wait;
11477
11620
  const return_promise = options.promise;
11478
- const promise = return_promise && getResolveablePromise();
11621
+ const promise = return_promise ? getResolveablePromise() : undefined;
11479
11622
  // If we're not waiting and attributes exist, save acts as
11480
11623
  // `set(attr).save(null, opts)` with validation. Otherwise, check if
11481
11624
  // the model will be valid when the attributes, if any, are set.
11482
- if (attrs && !wait) {
11483
- if (!this.set(attrs, options))
11625
+ const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
11626
+ if (!valid) {
11627
+ // Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
11628
+ // rejected promise rather than `false`, so the promise contract holds
11629
+ // for every outcome.
11630
+ if (!promise)
11484
11631
  return false;
11485
- }
11486
- else if (!this._validate(attrs || {}, options)) {
11487
- return false;
11632
+ promise.reject(this.validationError);
11633
+ return promise;
11488
11634
  }
11489
11635
  // After a successful server-side save, the client is (optionally)
11490
11636
  // updated with the server-side state.
@@ -11497,8 +11643,12 @@ class Model extends EventEmitterObject {
11497
11643
  let serverAttrs = options.parse ? this.parse(resp, options) : resp;
11498
11644
  if (wait)
11499
11645
  serverAttrs = Object.assign({}, attrs, serverAttrs);
11500
- if (serverAttrs && !this.set(serverAttrs, options))
11646
+ if (serverAttrs && !this.set(serverAttrs, options)) {
11647
+ // The server attributes failed validation (`'invalid'` has fired).
11648
+ // Settle the promise with the error rather than leave it pending.
11649
+ return_promise && promise.reject(this.validationError);
11501
11650
  return false;
11651
+ }
11502
11652
  if (success)
11503
11653
  success.call(options.context, this, resp, options);
11504
11654
  this.trigger('sync', this, resp, options);
@@ -11515,15 +11665,12 @@ class Model extends EventEmitterObject {
11515
11665
  const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
11516
11666
  if (method === 'patch' && !options.attrs)
11517
11667
  options.attrs = attrs;
11518
- const xhr = this.sync(method, this, options);
11668
+ // With `promise`, the sync's outcome is bridged to it however the sync
11669
+ // reports it: through the callbacks, or through the promise it returns.
11670
+ const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
11519
11671
  // Restore attributes.
11520
11672
  this.attributes = attributes;
11521
- if (return_promise) {
11522
- return promise;
11523
- }
11524
- else {
11525
- return xhr;
11526
- }
11673
+ return promise ?? xhr;
11527
11674
  }
11528
11675
  /**
11529
11676
  * Destroy this model on the server if it was already persisted.
@@ -11636,40 +11783,40 @@ class Model extends EventEmitterObject {
11636
11783
  return false;
11637
11784
  }
11638
11785
  }
11639
- _Model_computedCache = new WeakMap(), _Model_computedDefs = new WeakMap(), _Model_state = new WeakMap(), _Model_instances = new WeakSet(), _Model_hydrate = async function _Model_hydrate() {
11640
- ensureUnloadListener(PersistentStorage);
11641
- try {
11642
- await this.fetch({ fromStorage: true, promise: true });
11643
- }
11644
- finally {
11645
- __classPrivateFieldSet(this, _Model_state, 'ready', "f");
11646
- }
11647
- }, _Model_doAutoSave = function _Model_doAutoSave() {
11648
- return this.save(null, { noAutoSave: true });
11649
- }, _Model_updateComputed = function _Model_updateComputed(key, def, silent, options) {
11650
- const newVal = def.fn(this);
11651
- if (!isEqual(__classPrivateFieldGet(this, _Model_computedCache, "f")[key], newVal)) {
11652
- __classPrivateFieldGet(this, _Model_computedCache, "f")[key] = newVal;
11653
- if (!silent) {
11654
- this.changed[key] = newVal;
11655
- this.trigger('change:' + key, this, newVal, options);
11656
- }
11657
- }
11658
- }, _Model_initComputed = function _Model_initComputed() {
11659
- const defs = result(this, 'computed');
11660
- if (!defs || isEmpty(defs))
11661
- return;
11662
- __classPrivateFieldSet(this, _Model_computedDefs, defs, "f");
11663
- __classPrivateFieldSet(this, _Model_computedCache, {}, "f");
11664
- for (const [key, def] of Object.entries(defs)) {
11665
- __classPrivateFieldGet(this, _Model_computedCache, "f")[key] = def.fn(this);
11666
- }
11667
- };
11668
11786
 
11669
- var _Collection_instances, _Collection_hydrate, _a;
11787
+ var _a;
11670
11788
  // Default options for `Collection#set`.
11671
11789
  const setOptions = { add: true, remove: true, merge: true };
11672
11790
  const addOptions = { add: true, remove: false };
11791
+ /**
11792
+ * Normalizes the predicate forms accepted by the query methods into one
11793
+ * `(model) => value` function. A function is used as-is, a string reads that
11794
+ * attribute, and an attributes object becomes a lodash `matches` matcher.
11795
+ *
11796
+ * The matcher is built here, once per scan. Calling `Model#matches` per model
11797
+ * instead rebuilds the match data for every model visited, which made
11798
+ * `findWhere` an order of magnitude slower than a hand-written predicate.
11799
+ */
11800
+ function toModelPredicate(pred) {
11801
+ if (isFunction(pred))
11802
+ return pred;
11803
+ if (isString(pred))
11804
+ return (m) => m.get(pred);
11805
+ const matcher = matches(pred);
11806
+ return (m) => matcher(m.attributes);
11807
+ }
11808
+ /**
11809
+ * Load the collection's members from storage. A module-level function rather
11810
+ * than a `#private` method so the published declarations carry no private
11811
+ * members, which TypeScript 6 rejects in anonymously extended classes (TS4094).
11812
+ */
11813
+ async function hydrate(collection) {
11814
+ ensureUnloadListener(PersistentStorage);
11815
+ // An empty store is a normal first-run state: findAll() returns [] and a
11816
+ // missing model record resolves to null, so hydration resolves rather
11817
+ // than rejecting.
11818
+ await collection.fetch({ fromStorage: true, promise: true });
11819
+ }
11673
11820
  /**
11674
11821
  * @public
11675
11822
  * If models tend to represent a single row of data, a Collection is
@@ -11687,7 +11834,6 @@ class Collection extends EventEmitterObject {
11687
11834
  */
11688
11835
  constructor(models, options) {
11689
11836
  super();
11690
- _Collection_instances.add(this);
11691
11837
  this._url = '';
11692
11838
  /**
11693
11839
  * @public
@@ -11709,7 +11855,7 @@ class Collection extends EventEmitterObject {
11709
11855
  // uniformly `await collection.hydrated`; it resolves immediately when
11710
11856
  // there's no storage to hydrate from.
11711
11857
  if (this.autoSync) {
11712
- this.hydrated = getStorage(this) ? __classPrivateFieldGet(this, _Collection_instances, "m", _Collection_hydrate).call(this) : Promise.resolve();
11858
+ this.hydrated = getStorage(this) ? hydrate(this) : Promise.resolve();
11713
11859
  }
11714
11860
  }
11715
11861
  get comparator() {
@@ -11998,17 +12144,13 @@ class Collection extends EventEmitterObject {
11998
12144
  return this.models.slice(start, end);
11999
12145
  }
12000
12146
  filter(callback, thisArg) {
12001
- return this.models.filter(isFunction(callback)
12002
- ? callback
12003
- : (m) => m.matches(callback), thisArg);
12147
+ return this.models.filter(toModelPredicate(callback), thisArg);
12004
12148
  }
12005
12149
  every(pred) {
12006
12150
  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));
12151
+ return this.models.every((m) => pred(m.attributes));
12011
12152
  }
12153
+ return this.models.every(toModelPredicate(pred));
12012
12154
  }
12013
12155
  difference(values) {
12014
12156
  return this.models.filter((m) => !values.includes(m));
@@ -12024,16 +12166,12 @@ class Collection extends EventEmitterObject {
12024
12166
  }
12025
12167
  some(pred) {
12026
12168
  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));
12169
+ return this.models.some((m) => pred(m.attributes));
12031
12170
  }
12171
+ return this.models.some(toModelPredicate(pred));
12032
12172
  }
12033
12173
  sortBy(iteratee) {
12034
- return sortBy(this.models, isFunction(iteratee)
12035
- ? iteratee
12036
- : (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
12174
+ return sortBy(this.models, toModelPredicate(iteratee));
12037
12175
  }
12038
12176
  isEmpty() {
12039
12177
  return !this.models.length;
@@ -12054,26 +12192,22 @@ class Collection extends EventEmitterObject {
12054
12192
  return this.models.length;
12055
12193
  }
12056
12194
  countBy(f) {
12057
- return countBy(this.models, isFunction(f) ? f : (m) => (isString(f) ? m.get(f) : m.matches(f)));
12195
+ return countBy(this.models, toModelPredicate(f));
12058
12196
  }
12059
12197
  groupBy(pred) {
12060
- return groupBy(this.models, isFunction(pred) ? pred : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12198
+ return groupBy(this.models, toModelPredicate(pred));
12061
12199
  }
12062
12200
  indexOf(model, fromIndex) {
12063
12201
  return this.models.indexOf(model, fromIndex);
12064
12202
  }
12065
12203
  findLastIndex(pred, fromIndex) {
12066
- return this.models.findLastIndex(isFunction(pred)
12067
- ? pred
12068
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
12204
+ return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
12069
12205
  }
12070
12206
  lastIndexOf(model, fromIndex) {
12071
12207
  return this.models.lastIndexOf(model, fromIndex);
12072
12208
  }
12073
12209
  findIndex(pred) {
12074
- return this.models.findIndex(isFunction(pred)
12075
- ? pred
12076
- : (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
12210
+ return this.models.findIndex(toModelPredicate(pred));
12077
12211
  }
12078
12212
  last() {
12079
12213
  const length = this.models == null ? 0 : this.models.length;
@@ -12086,9 +12220,7 @@ class Collection extends EventEmitterObject {
12086
12220
  return this.head();
12087
12221
  }
12088
12222
  map(cb, thisArg) {
12089
- return this.models.map(isFunction(cb)
12090
- ? cb
12091
- : (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
12223
+ return this.models.map(toModelPredicate(cb), thisArg);
12092
12224
  }
12093
12225
  reduce(callback, initialValue) {
12094
12226
  return this.models.reduce(callback, initialValue || this.models[0]);
@@ -12139,10 +12271,7 @@ class Collection extends EventEmitterObject {
12139
12271
  return this.where(attrs, true);
12140
12272
  }
12141
12273
  find(predicate, fromIndex) {
12142
- const pred = isFunction(predicate)
12143
- ? predicate
12144
- : (m) => m.matches(predicate);
12145
- return this.models.find(pred, fromIndex);
12274
+ return this.models.find(toModelPredicate(predicate), fromIndex);
12146
12275
  }
12147
12276
  /**
12148
12277
  * Force the collection to re-sort itself. You don't need to call this under
@@ -12185,46 +12314,22 @@ class Collection extends EventEmitterObject {
12185
12314
  // eslint-disable-next-line @typescript-eslint/no-this-alias
12186
12315
  const collection = this;
12187
12316
  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
12317
  options.success = function (resp) {
12195
- settled = true;
12196
12318
  const method = options.reset ? 'reset' : 'set';
12197
12319
  // fromStorage prevents individual model set() calls from triggering auto-save on reads
12198
12320
  collection[method](resp, Object.assign({}, options, { fromStorage: true }));
12199
12321
  if (success)
12200
12322
  success.call(options.context, collection, resp, options);
12201
- promise && promise.resolve();
12323
+ // Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
12324
+ // listener rejects the promise instead of being lost.
12202
12325
  collection.trigger('sync', collection, resp, options);
12326
+ promise && promise.resolve();
12203
12327
  };
12204
12328
  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;
12329
+ // With `promise`, the sync's outcome is bridged to it however the sync
12330
+ // reports it: through the callbacks, or through the promise it returns.
12331
+ const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
12332
+ return promise ?? result;
12228
12333
  }
12229
12334
  /**
12230
12335
  * Create a new instance of a model in this collection. Add the model to the
@@ -12236,9 +12341,22 @@ class Collection extends EventEmitterObject {
12236
12341
  const wait = options.wait;
12237
12342
  const return_promise = options.promise;
12238
12343
  const promise = return_promise && getResolveablePromise();
12344
+ // `_prepareModel` reports attributes that fail validation by firing
12345
+ // `'invalid'` and returning null. With `promise`, capture that error so a
12346
+ // rejected promise is handed back rather than `null`.
12347
+ let validationError;
12348
+ const captureInvalid = (_c, err) => (validationError = err);
12349
+ if (return_promise)
12350
+ this.on('invalid', captureInvalid);
12239
12351
  const preparedModel = this._prepareModel(model, options);
12240
- if (!preparedModel)
12241
- return null;
12352
+ if (return_promise)
12353
+ this.off('invalid', captureInvalid);
12354
+ if (!preparedModel) {
12355
+ if (!return_promise)
12356
+ return null;
12357
+ promise.reject(validationError);
12358
+ return promise;
12359
+ }
12242
12360
  if (!wait)
12243
12361
  this.add(preparedModel, options);
12244
12362
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -12260,8 +12378,15 @@ class Collection extends EventEmitterObject {
12260
12378
  error && error.call(options.context, model, e, options);
12261
12379
  return_promise && promise.reject(e);
12262
12380
  };
12263
- preparedModel.save(null, Object.assign(options, { 'promise': false }));
12381
+ // `options.promise` is passed through, so `save` installs its own bridge
12382
+ // from the sync's returned promise to its callbacks: a sync that resolves
12383
+ // without calling them still reaches the wired `options.success` above,
12384
+ // which resolves our promise with the model. Only a rejection that bypassed
12385
+ // the callbacks (a rejecting sync, or a throwing `'sync'`/`'error'`
12386
+ // listener) still needs bridging to ours.
12387
+ const result = preparedModel.save(null, options);
12264
12388
  if (return_promise) {
12389
+ Promise.resolve(result).catch((e) => promise.reject(e));
12265
12390
  return promise;
12266
12391
  }
12267
12392
  else {
@@ -12413,13 +12538,7 @@ class Collection extends EventEmitterObject {
12413
12538
  this.trigger.apply(this, arguments);
12414
12539
  }
12415
12540
  }
12416
- _Collection_instances = new WeakSet(), _Collection_hydrate = async function _Collection_hydrate() {
12417
- ensureUnloadListener(PersistentStorage);
12418
- // An empty store is a normal first-run state: findAll() returns [] and a
12419
- // missing model record resolves to null, so hydration resolves rather
12420
- // than rejecting.
12421
- await this.fetch({ fromStorage: true, promise: true });
12422
- }, _a = Symbol.iterator;
12541
+ _a = Symbol.iterator;
12423
12542
  // This "enum" defines the three possible kinds of values which can be emitted
12424
12543
  // by a CollectionIterator that correspond to the values(), keys() and entries()
12425
12544
  // methods on Collection, respectively.
@@ -12487,6 +12606,7 @@ const skeletor = {
12487
12606
  EventEmitter,
12488
12607
  Model,
12489
12608
  sync,
12609
+ HTTPError,
12490
12610
  };
12491
12611
  // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
12492
12612
  // We use `self` instead of `window` for `WebWorker` support.
@@ -12524,5 +12644,5 @@ const BrowserStorage = PersistentStorage;
12524
12644
  // their bundles entirely.
12525
12645
  PersistentStorage.nodeStorage = NodeSQLiteStorage;
12526
12646
 
12527
- export { BrowserStorage, Collection, EventEmitter, Model, NodeSQLiteStorage, PersistentStorage, skeletor as default, getStorage, noConflict, sync };
12647
+ export { BrowserStorage, Collection, EventEmitter, HTTPError, Model, NodeSQLiteStorage, PersistentStorage, skeletor as default, getStorage, noConflict, sync };
12528
12648
  //# sourceMappingURL=skeletor.node.mjs.map