@converse/skeletor 3.1.6 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.md +50 -0
- package/dist/skeletor.d.ts +36 -14
- package/dist/skeletor.esm.js +291 -174
- package/dist/skeletor.esm.js.map +1 -1
- package/dist/skeletor.js +291 -173
- package/dist/skeletor.js.map +1 -1
- package/dist/skeletor.min.js +4 -4
- package/dist/skeletor.node.d.ts +36 -14
- package/dist/skeletor.node.js +291 -173
- package/dist/skeletor.node.mjs +291 -174
- package/package.json +1 -1
- package/src/collection.ts +68 -78
- package/src/helpers.ts +135 -25
- package/src/index.ts +4 -2
- package/src/model.ts +43 -48
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
|
-
*
|
|
8574
|
-
*
|
|
8575
|
-
*
|
|
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
|
-
|
|
8594
|
-
const params = {
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
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
|
/**
|
|
@@ -9972,6 +10075,48 @@
|
|
|
9972
10075
|
baseAssignValue(result, key, value);
|
|
9973
10076
|
});
|
|
9974
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
|
+
|
|
9975
10120
|
/** Built-in value references. */
|
|
9976
10121
|
var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
|
|
9977
10122
|
|
|
@@ -10535,53 +10680,36 @@
|
|
|
10535
10680
|
return baseIsEqual(value, other);
|
|
10536
10681
|
}
|
|
10537
10682
|
|
|
10538
|
-
/** Used to compose bitmasks for cloning. */
|
|
10539
|
-
var CLONE_DEEP_FLAG$1 = 1;
|
|
10540
|
-
|
|
10541
10683
|
/**
|
|
10542
|
-
*
|
|
10543
|
-
*
|
|
10544
|
-
*
|
|
10545
|
-
*
|
|
10546
|
-
*
|
|
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.
|
|
10547
10693
|
*
|
|
10548
10694
|
* @static
|
|
10549
|
-
* @since 4.0.0
|
|
10550
10695
|
* @memberOf _
|
|
10551
|
-
* @
|
|
10552
|
-
* @
|
|
10553
|
-
* @
|
|
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`.
|
|
10554
10701
|
* @example
|
|
10555
10702
|
*
|
|
10556
|
-
* var
|
|
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 }]
|
|
10703
|
+
* var object = { 'a': 1, 'b': 2 };
|
|
10568
10704
|
*
|
|
10569
|
-
*
|
|
10570
|
-
*
|
|
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
|
-
* });
|
|
10705
|
+
* _.isMatch(object, { 'b': 2 });
|
|
10706
|
+
* // => true
|
|
10579
10707
|
*
|
|
10580
|
-
* _.
|
|
10581
|
-
* // =>
|
|
10708
|
+
* _.isMatch(object, { 'b': 1 });
|
|
10709
|
+
* // => false
|
|
10582
10710
|
*/
|
|
10583
|
-
function
|
|
10584
|
-
return
|
|
10711
|
+
function isMatch(object, source) {
|
|
10712
|
+
return object === source || baseIsMatch(object, source, getMatchData(source));
|
|
10585
10713
|
}
|
|
10586
10714
|
|
|
10587
10715
|
/**
|
|
@@ -11095,10 +11223,20 @@
|
|
|
11095
11223
|
return this.get(attr) != null;
|
|
11096
11224
|
}
|
|
11097
11225
|
/**
|
|
11098
|
-
*
|
|
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.
|
|
11099
11235
|
*/
|
|
11100
11236
|
matches(attrs) {
|
|
11101
|
-
|
|
11237
|
+
if (typeof attrs === 'function')
|
|
11238
|
+
return !!attrs(this.attributes);
|
|
11239
|
+
return isMatch(this.attributes, attrs);
|
|
11102
11240
|
}
|
|
11103
11241
|
/**
|
|
11104
11242
|
* Set a hash of model attributes on the object, firing `"change"`. This is
|
|
@@ -11271,47 +11409,25 @@
|
|
|
11271
11409
|
options = Object.assign({ parse: true }, options);
|
|
11272
11410
|
const success = options.success;
|
|
11273
11411
|
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
11412
|
options.success = (resp) => {
|
|
11281
|
-
settled = true;
|
|
11282
11413
|
const serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
11283
11414
|
// fromStorage prevents the set() from triggering auto-save on reads
|
|
11284
|
-
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);
|
|
11285
11419
|
return false;
|
|
11420
|
+
}
|
|
11286
11421
|
if (success)
|
|
11287
11422
|
success.call(options.context, this, resp, options);
|
|
11288
11423
|
this.trigger('sync', this, resp, options);
|
|
11289
11424
|
promise && promise.resolve(resp);
|
|
11290
11425
|
};
|
|
11291
11426
|
wrapError(this, options, promise);
|
|
11292
|
-
|
|
11293
|
-
|
|
11294
|
-
|
|
11295
|
-
|
|
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;
|
|
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;
|
|
11315
11431
|
}
|
|
11316
11432
|
/**
|
|
11317
11433
|
* Set a hash of model attributes, and sync the model to the server.
|
|
@@ -11332,16 +11448,19 @@
|
|
|
11332
11448
|
options = Object.assign({ validate: true, parse: true }, options);
|
|
11333
11449
|
const wait = options.wait;
|
|
11334
11450
|
const return_promise = options.promise;
|
|
11335
|
-
const promise = return_promise
|
|
11451
|
+
const promise = return_promise ? getResolveablePromise() : undefined;
|
|
11336
11452
|
// If we're not waiting and attributes exist, save acts as
|
|
11337
11453
|
// `set(attr).save(null, opts)` with validation. Otherwise, check if
|
|
11338
11454
|
// the model will be valid when the attributes, if any, are set.
|
|
11339
|
-
|
|
11340
|
-
|
|
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)
|
|
11341
11461
|
return false;
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
return false;
|
|
11462
|
+
promise.reject(this.validationError);
|
|
11463
|
+
return promise;
|
|
11345
11464
|
}
|
|
11346
11465
|
// After a successful server-side save, the client is (optionally)
|
|
11347
11466
|
// updated with the server-side state.
|
|
@@ -11354,8 +11473,12 @@
|
|
|
11354
11473
|
let serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
11355
11474
|
if (wait)
|
|
11356
11475
|
serverAttrs = Object.assign({}, attrs, serverAttrs);
|
|
11357
|
-
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);
|
|
11358
11480
|
return false;
|
|
11481
|
+
}
|
|
11359
11482
|
if (success)
|
|
11360
11483
|
success.call(options.context, this, resp, options);
|
|
11361
11484
|
this.trigger('sync', this, resp, options);
|
|
@@ -11372,15 +11495,12 @@
|
|
|
11372
11495
|
const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
|
|
11373
11496
|
if (method === 'patch' && !options.attrs)
|
|
11374
11497
|
options.attrs = attrs;
|
|
11375
|
-
|
|
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));
|
|
11376
11501
|
// Restore attributes.
|
|
11377
11502
|
this.attributes = attributes;
|
|
11378
|
-
|
|
11379
|
-
return promise;
|
|
11380
|
-
}
|
|
11381
|
-
else {
|
|
11382
|
-
return xhr;
|
|
11383
|
-
}
|
|
11503
|
+
return promise ?? xhr;
|
|
11384
11504
|
}
|
|
11385
11505
|
/**
|
|
11386
11506
|
* Destroy this model on the server if it was already persisted.
|
|
@@ -11527,6 +11647,23 @@
|
|
|
11527
11647
|
// Default options for `Collection#set`.
|
|
11528
11648
|
const setOptions = { add: true, remove: true, merge: true };
|
|
11529
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
|
+
}
|
|
11530
11667
|
/**
|
|
11531
11668
|
* @public
|
|
11532
11669
|
* If models tend to represent a single row of data, a Collection is
|
|
@@ -11855,17 +11992,13 @@
|
|
|
11855
11992
|
return this.models.slice(start, end);
|
|
11856
11993
|
}
|
|
11857
11994
|
filter(callback, thisArg) {
|
|
11858
|
-
return this.models.filter(
|
|
11859
|
-
? callback
|
|
11860
|
-
: (m) => m.matches(callback), thisArg);
|
|
11995
|
+
return this.models.filter(toModelPredicate(callback), thisArg);
|
|
11861
11996
|
}
|
|
11862
11997
|
every(pred) {
|
|
11863
11998
|
if (isFunction(pred)) {
|
|
11864
|
-
return this.models.
|
|
11865
|
-
}
|
|
11866
|
-
else {
|
|
11867
|
-
return this.models.every((m) => m.matches(pred));
|
|
11999
|
+
return this.models.every((m) => pred(m.attributes));
|
|
11868
12000
|
}
|
|
12001
|
+
return this.models.every(toModelPredicate(pred));
|
|
11869
12002
|
}
|
|
11870
12003
|
difference(values) {
|
|
11871
12004
|
return this.models.filter((m) => !values.includes(m));
|
|
@@ -11881,16 +12014,12 @@
|
|
|
11881
12014
|
}
|
|
11882
12015
|
some(pred) {
|
|
11883
12016
|
if (isFunction(pred)) {
|
|
11884
|
-
return this.models.
|
|
11885
|
-
}
|
|
11886
|
-
else {
|
|
11887
|
-
return this.models.some((m) => m.matches(pred));
|
|
12017
|
+
return this.models.some((m) => pred(m.attributes));
|
|
11888
12018
|
}
|
|
12019
|
+
return this.models.some(toModelPredicate(pred));
|
|
11889
12020
|
}
|
|
11890
12021
|
sortBy(iteratee) {
|
|
11891
|
-
return sortBy(this.models,
|
|
11892
|
-
? iteratee
|
|
11893
|
-
: (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
|
|
12022
|
+
return sortBy(this.models, toModelPredicate(iteratee));
|
|
11894
12023
|
}
|
|
11895
12024
|
isEmpty() {
|
|
11896
12025
|
return !this.models.length;
|
|
@@ -11911,26 +12040,22 @@
|
|
|
11911
12040
|
return this.models.length;
|
|
11912
12041
|
}
|
|
11913
12042
|
countBy(f) {
|
|
11914
|
-
return countBy(this.models,
|
|
12043
|
+
return countBy(this.models, toModelPredicate(f));
|
|
11915
12044
|
}
|
|
11916
12045
|
groupBy(pred) {
|
|
11917
|
-
return groupBy(this.models,
|
|
12046
|
+
return groupBy(this.models, toModelPredicate(pred));
|
|
11918
12047
|
}
|
|
11919
12048
|
indexOf(model, fromIndex) {
|
|
11920
12049
|
return this.models.indexOf(model, fromIndex);
|
|
11921
12050
|
}
|
|
11922
12051
|
findLastIndex(pred, fromIndex) {
|
|
11923
|
-
return this.models.findLastIndex(
|
|
11924
|
-
? pred
|
|
11925
|
-
: (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
|
|
12052
|
+
return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
|
|
11926
12053
|
}
|
|
11927
12054
|
lastIndexOf(model, fromIndex) {
|
|
11928
12055
|
return this.models.lastIndexOf(model, fromIndex);
|
|
11929
12056
|
}
|
|
11930
12057
|
findIndex(pred) {
|
|
11931
|
-
return this.models.findIndex(
|
|
11932
|
-
? pred
|
|
11933
|
-
: (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
|
|
12058
|
+
return this.models.findIndex(toModelPredicate(pred));
|
|
11934
12059
|
}
|
|
11935
12060
|
last() {
|
|
11936
12061
|
const length = this.models == null ? 0 : this.models.length;
|
|
@@ -11943,9 +12068,7 @@
|
|
|
11943
12068
|
return this.head();
|
|
11944
12069
|
}
|
|
11945
12070
|
map(cb, thisArg) {
|
|
11946
|
-
return this.models.map(
|
|
11947
|
-
? cb
|
|
11948
|
-
: (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
|
|
12071
|
+
return this.models.map(toModelPredicate(cb), thisArg);
|
|
11949
12072
|
}
|
|
11950
12073
|
reduce(callback, initialValue) {
|
|
11951
12074
|
return this.models.reduce(callback, initialValue || this.models[0]);
|
|
@@ -11996,10 +12119,7 @@
|
|
|
11996
12119
|
return this.where(attrs, true);
|
|
11997
12120
|
}
|
|
11998
12121
|
find(predicate, fromIndex) {
|
|
11999
|
-
|
|
12000
|
-
? predicate
|
|
12001
|
-
: (m) => m.matches(predicate);
|
|
12002
|
-
return this.models.find(pred, fromIndex);
|
|
12122
|
+
return this.models.find(toModelPredicate(predicate), fromIndex);
|
|
12003
12123
|
}
|
|
12004
12124
|
/**
|
|
12005
12125
|
* Force the collection to re-sort itself. You don't need to call this under
|
|
@@ -12042,46 +12162,22 @@
|
|
|
12042
12162
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
12043
12163
|
const collection = this;
|
|
12044
12164
|
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
12165
|
options.success = function (resp) {
|
|
12052
|
-
settled = true;
|
|
12053
12166
|
const method = options.reset ? 'reset' : 'set';
|
|
12054
12167
|
// fromStorage prevents individual model set() calls from triggering auto-save on reads
|
|
12055
12168
|
collection[method](resp, Object.assign({}, options, { fromStorage: true }));
|
|
12056
12169
|
if (success)
|
|
12057
12170
|
success.call(options.context, collection, resp, options);
|
|
12058
|
-
|
|
12171
|
+
// Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
|
|
12172
|
+
// listener rejects the promise instead of being lost.
|
|
12059
12173
|
collection.trigger('sync', collection, resp, options);
|
|
12174
|
+
promise && promise.resolve();
|
|
12060
12175
|
};
|
|
12061
12176
|
wrapError(this, options, promise);
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
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;
|
|
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;
|
|
12085
12181
|
}
|
|
12086
12182
|
/**
|
|
12087
12183
|
* Create a new instance of a model in this collection. Add the model to the
|
|
@@ -12093,9 +12189,22 @@
|
|
|
12093
12189
|
const wait = options.wait;
|
|
12094
12190
|
const return_promise = options.promise;
|
|
12095
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);
|
|
12096
12199
|
const preparedModel = this._prepareModel(model, options);
|
|
12097
|
-
if (
|
|
12098
|
-
|
|
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
|
+
}
|
|
12099
12208
|
if (!wait)
|
|
12100
12209
|
this.add(preparedModel, options);
|
|
12101
12210
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
@@ -12117,8 +12226,15 @@
|
|
|
12117
12226
|
error && error.call(options.context, model, e, options);
|
|
12118
12227
|
return_promise && promise.reject(e);
|
|
12119
12228
|
};
|
|
12120
|
-
|
|
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);
|
|
12121
12236
|
if (return_promise) {
|
|
12237
|
+
Promise.resolve(result).catch((e) => promise.reject(e));
|
|
12122
12238
|
return promise;
|
|
12123
12239
|
}
|
|
12124
12240
|
else {
|
|
@@ -12344,6 +12460,7 @@
|
|
|
12344
12460
|
EventEmitter,
|
|
12345
12461
|
Model,
|
|
12346
12462
|
sync,
|
|
12463
|
+
HTTPError,
|
|
12347
12464
|
};
|
|
12348
12465
|
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
|
|
12349
12466
|
// We use `self` instead of `window` for `WebWorker` support.
|
|
@@ -12374,6 +12491,7 @@
|
|
|
12374
12491
|
exports.BrowserStorage = BrowserStorage;
|
|
12375
12492
|
exports.Collection = Collection;
|
|
12376
12493
|
exports.EventEmitter = EventEmitter;
|
|
12494
|
+
exports.HTTPError = HTTPError;
|
|
12377
12495
|
exports.Model = Model;
|
|
12378
12496
|
exports.PersistentStorage = PersistentStorage;
|
|
12379
12497
|
exports.default = skeletor;
|