@converse/skeletor 3.1.5 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.md +62 -0
- package/dist/skeletor.d.ts +50 -15
- package/dist/skeletor.esm.js +304 -175
- package/dist/skeletor.esm.js.map +1 -1
- package/dist/skeletor.js +304 -174
- package/dist/skeletor.js.map +1 -1
- package/dist/skeletor.min.js +4 -4
- package/dist/skeletor.node.d.ts +978 -0
- package/dist/skeletor.node.js +305 -174
- package/dist/skeletor.node.mjs +305 -175
- package/package.json +6 -4
- package/src/collection.ts +68 -78
- package/src/drivers/nodeSQLiteStorage.ts +1 -0
- package/src/helpers.ts +135 -25
- package/src/index.ts +5 -2
- package/src/model.ts +43 -48
- package/src/storage.ts +22 -2
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
|
/**
|
|
@@ -8705,7 +8808,7 @@
|
|
|
8705
8808
|
throw new Error("Skeletor.storage: the 'node' storage type is only available in Node.js builds. " +
|
|
8706
8809
|
"Import from '@converse/skeletor/node' instead of '@converse/skeletor'.");
|
|
8707
8810
|
}
|
|
8708
|
-
this.store = new NodeSQLiteStorage(this.name);
|
|
8811
|
+
this.store = new NodeSQLiteStorage(this.name, PersistentStorage.nodeStorageDir ?? undefined);
|
|
8709
8812
|
if (batchedWrites) {
|
|
8710
8813
|
this.store.debouncedSetItems = mergebounce((items) => this.store.setItems(items), 50, {
|
|
8711
8814
|
'promise': true,
|
|
@@ -8909,6 +9012,18 @@
|
|
|
8909
9012
|
// node-only driver (and its `node:sqlite` import) never enters the browser
|
|
8910
9013
|
// bundle; the `@converse/skeletor/node` entry sets it.
|
|
8911
9014
|
PersistentStorage.nodeStorage = null;
|
|
9015
|
+
/**
|
|
9016
|
+
* Directory the `'node'` store type writes its database files to. The driver's
|
|
9017
|
+
* own default is relative, so it resolves against `process.cwd()` and a process
|
|
9018
|
+
* started from two directories ends up with two separate sets of state. Set an
|
|
9019
|
+
* absolute path (`$XDG_STATE_HOME/<app>`, say) to pin it. Left `null`, the
|
|
9020
|
+
* driver's default applies.
|
|
9021
|
+
*
|
|
9022
|
+
* Only the `'node'` string store type reads this; passing a driver instance to
|
|
9023
|
+
* the constructor means you've already chosen a directory yourself.
|
|
9024
|
+
* @public
|
|
9025
|
+
*/
|
|
9026
|
+
PersistentStorage.nodeStorageDir = null;
|
|
8912
9027
|
PersistentStorage.sessionStorageInitialized = localForage.defineDriver(sessionStorageWrapper);
|
|
8913
9028
|
PersistentStorage.localForage = localForage;
|
|
8914
9029
|
|
|
@@ -9960,6 +10075,48 @@
|
|
|
9960
10075
|
baseAssignValue(result, key, value);
|
|
9961
10076
|
});
|
|
9962
10077
|
|
|
10078
|
+
/** Used to compose bitmasks for cloning. */
|
|
10079
|
+
var CLONE_DEEP_FLAG$1 = 1;
|
|
10080
|
+
|
|
10081
|
+
/**
|
|
10082
|
+
* Creates a function that performs a partial deep comparison between a given
|
|
10083
|
+
* object and `source`, returning `true` if the given object has equivalent
|
|
10084
|
+
* property values, else `false`.
|
|
10085
|
+
*
|
|
10086
|
+
* **Note:** The created function is equivalent to `_.isMatch` with `source`
|
|
10087
|
+
* partially applied.
|
|
10088
|
+
*
|
|
10089
|
+
* Partial comparisons will match empty array and empty object `source`
|
|
10090
|
+
* values against any array or object value, respectively. See `_.isEqual`
|
|
10091
|
+
* for a list of supported value comparisons.
|
|
10092
|
+
*
|
|
10093
|
+
* **Note:** Multiple values can be checked by combining several matchers
|
|
10094
|
+
* using `_.overSome`
|
|
10095
|
+
*
|
|
10096
|
+
* @static
|
|
10097
|
+
* @memberOf _
|
|
10098
|
+
* @since 3.0.0
|
|
10099
|
+
* @category Util
|
|
10100
|
+
* @param {Object} source The object of property values to match.
|
|
10101
|
+
* @returns {Function} Returns the new spec function.
|
|
10102
|
+
* @example
|
|
10103
|
+
*
|
|
10104
|
+
* var objects = [
|
|
10105
|
+
* { 'a': 1, 'b': 2, 'c': 3 },
|
|
10106
|
+
* { 'a': 4, 'b': 5, 'c': 6 }
|
|
10107
|
+
* ];
|
|
10108
|
+
*
|
|
10109
|
+
* _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
|
|
10110
|
+
* // => [{ 'a': 4, 'b': 5, 'c': 6 }]
|
|
10111
|
+
*
|
|
10112
|
+
* // Checking for several possible values
|
|
10113
|
+
* _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
|
|
10114
|
+
* // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
|
|
10115
|
+
*/
|
|
10116
|
+
function matches(source) {
|
|
10117
|
+
return baseMatches(baseClone(source, CLONE_DEEP_FLAG$1));
|
|
10118
|
+
}
|
|
10119
|
+
|
|
9963
10120
|
/** Built-in value references. */
|
|
9964
10121
|
var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
|
|
9965
10122
|
|
|
@@ -10523,53 +10680,36 @@
|
|
|
10523
10680
|
return baseIsEqual(value, other);
|
|
10524
10681
|
}
|
|
10525
10682
|
|
|
10526
|
-
/** Used to compose bitmasks for cloning. */
|
|
10527
|
-
var CLONE_DEEP_FLAG$1 = 1;
|
|
10528
|
-
|
|
10529
10683
|
/**
|
|
10530
|
-
*
|
|
10531
|
-
*
|
|
10532
|
-
*
|
|
10533
|
-
*
|
|
10534
|
-
*
|
|
10684
|
+
* Performs a partial deep comparison between `object` and `source` to
|
|
10685
|
+
* determine if `object` contains equivalent property values.
|
|
10686
|
+
*
|
|
10687
|
+
* **Note:** This method is equivalent to `_.matches` when `source` is
|
|
10688
|
+
* partially applied.
|
|
10689
|
+
*
|
|
10690
|
+
* Partial comparisons will match empty array and empty object `source`
|
|
10691
|
+
* values against any array or object value, respectively. See `_.isEqual`
|
|
10692
|
+
* for a list of supported value comparisons.
|
|
10535
10693
|
*
|
|
10536
10694
|
* @static
|
|
10537
|
-
* @since 4.0.0
|
|
10538
10695
|
* @memberOf _
|
|
10539
|
-
* @
|
|
10540
|
-
* @
|
|
10541
|
-
* @
|
|
10696
|
+
* @since 3.0.0
|
|
10697
|
+
* @category Lang
|
|
10698
|
+
* @param {Object} object The object to inspect.
|
|
10699
|
+
* @param {Object} source The object of property values to match.
|
|
10700
|
+
* @returns {boolean} Returns `true` if `object` is a match, else `false`.
|
|
10542
10701
|
* @example
|
|
10543
10702
|
*
|
|
10544
|
-
* var
|
|
10545
|
-
* { 'user': 'barney', 'age': 36, 'active': true },
|
|
10546
|
-
* { 'user': 'fred', 'age': 40, 'active': false }
|
|
10547
|
-
* ];
|
|
10548
|
-
*
|
|
10549
|
-
* // The `_.matches` iteratee shorthand.
|
|
10550
|
-
* _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
|
|
10551
|
-
* // => [{ 'user': 'barney', 'age': 36, 'active': true }]
|
|
10552
|
-
*
|
|
10553
|
-
* // The `_.matchesProperty` iteratee shorthand.
|
|
10554
|
-
* _.filter(users, _.iteratee(['user', 'fred']));
|
|
10555
|
-
* // => [{ 'user': 'fred', 'age': 40 }]
|
|
10703
|
+
* var object = { 'a': 1, 'b': 2 };
|
|
10556
10704
|
*
|
|
10557
|
-
*
|
|
10558
|
-
*
|
|
10559
|
-
* // => ['barney', 'fred']
|
|
10560
|
-
*
|
|
10561
|
-
* // Create custom iteratee shorthands.
|
|
10562
|
-
* _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
|
|
10563
|
-
* return !_.isRegExp(func) ? iteratee(func) : function(string) {
|
|
10564
|
-
* return func.test(string);
|
|
10565
|
-
* };
|
|
10566
|
-
* });
|
|
10705
|
+
* _.isMatch(object, { 'b': 2 });
|
|
10706
|
+
* // => true
|
|
10567
10707
|
*
|
|
10568
|
-
* _.
|
|
10569
|
-
* // =>
|
|
10708
|
+
* _.isMatch(object, { 'b': 1 });
|
|
10709
|
+
* // => false
|
|
10570
10710
|
*/
|
|
10571
|
-
function
|
|
10572
|
-
return
|
|
10711
|
+
function isMatch(object, source) {
|
|
10712
|
+
return object === source || baseIsMatch(object, source, getMatchData(source));
|
|
10573
10713
|
}
|
|
10574
10714
|
|
|
10575
10715
|
/**
|
|
@@ -11083,10 +11223,20 @@
|
|
|
11083
11223
|
return this.get(attr) != null;
|
|
11084
11224
|
}
|
|
11085
11225
|
/**
|
|
11086
|
-
*
|
|
11226
|
+
* Returns `true` if the model's attributes satisfy `attrs`.
|
|
11227
|
+
*
|
|
11228
|
+
* An attributes object is compared with lodash's `isMatch`: a partial, deep
|
|
11229
|
+
* comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
|
|
11230
|
+
* that has `type: 'hidden'` alongside other keys. A function is called with
|
|
11231
|
+
* the attributes and its result coerced to a boolean.
|
|
11232
|
+
*
|
|
11233
|
+
* To scan a collection, prefer `Collection#find`, `filter` or `where`. They
|
|
11234
|
+
* build their matcher once per scan instead of once per model.
|
|
11087
11235
|
*/
|
|
11088
11236
|
matches(attrs) {
|
|
11089
|
-
|
|
11237
|
+
if (typeof attrs === 'function')
|
|
11238
|
+
return !!attrs(this.attributes);
|
|
11239
|
+
return isMatch(this.attributes, attrs);
|
|
11090
11240
|
}
|
|
11091
11241
|
/**
|
|
11092
11242
|
* Set a hash of model attributes on the object, firing `"change"`. This is
|
|
@@ -11259,47 +11409,25 @@
|
|
|
11259
11409
|
options = Object.assign({ parse: true }, options);
|
|
11260
11410
|
const success = options.success;
|
|
11261
11411
|
const promise = options.promise ? getResolveablePromise() : undefined;
|
|
11262
|
-
// Tracks whether sync invoked one of the callbacks. A sync may instead
|
|
11263
|
-
// settle its returned promise; the `.then`/`.catch` below bridges that to
|
|
11264
|
-
// the callbacks so the fetch promise never hangs. The flag is set
|
|
11265
|
-
// synchronously inside each callback (before sync's promise resolves), so
|
|
11266
|
-
// it's reliable by the time those handlers run.
|
|
11267
|
-
let settled = false;
|
|
11268
11412
|
options.success = (resp) => {
|
|
11269
|
-
settled = true;
|
|
11270
11413
|
const serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
11271
11414
|
// fromStorage prevents the set() from triggering auto-save on reads
|
|
11272
|
-
if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
|
|
11415
|
+
if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
|
|
11416
|
+
// The fetched attributes failed validation (`'invalid'` has fired).
|
|
11417
|
+
// Settle the promise with the error rather than leave it pending.
|
|
11418
|
+
promise && promise.reject(this.validationError);
|
|
11273
11419
|
return false;
|
|
11420
|
+
}
|
|
11274
11421
|
if (success)
|
|
11275
11422
|
success.call(options.context, this, resp, options);
|
|
11276
11423
|
this.trigger('sync', this, resp, options);
|
|
11277
11424
|
promise && promise.resolve(resp);
|
|
11278
11425
|
};
|
|
11279
11426
|
wrapError(this, options, promise);
|
|
11280
|
-
|
|
11281
|
-
|
|
11282
|
-
|
|
11283
|
-
|
|
11284
|
-
onError(resp);
|
|
11285
|
-
};
|
|
11286
|
-
}
|
|
11287
|
-
const result = this.sync('read', this, options);
|
|
11288
|
-
if (promise) {
|
|
11289
|
-
Promise.resolve(result).then(
|
|
11290
|
-
// A sync that resolves its promise without invoking options.success:
|
|
11291
|
-
// treat the resolved value as the response, so the model is still
|
|
11292
|
-
// merged and the promise settles instead of hanging.
|
|
11293
|
-
(value) => {
|
|
11294
|
-
if (!settled)
|
|
11295
|
-
options.success(value);
|
|
11296
|
-
},
|
|
11297
|
-
// A sync that rejects instead of invoking options.error; settling
|
|
11298
|
-
// twice is a harmless no-op.
|
|
11299
|
-
(e) => promise.reject(e));
|
|
11300
|
-
return promise;
|
|
11301
|
-
}
|
|
11302
|
-
return result;
|
|
11427
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
11428
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
11429
|
+
const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
|
|
11430
|
+
return promise ?? result;
|
|
11303
11431
|
}
|
|
11304
11432
|
/**
|
|
11305
11433
|
* Set a hash of model attributes, and sync the model to the server.
|
|
@@ -11320,16 +11448,19 @@
|
|
|
11320
11448
|
options = Object.assign({ validate: true, parse: true }, options);
|
|
11321
11449
|
const wait = options.wait;
|
|
11322
11450
|
const return_promise = options.promise;
|
|
11323
|
-
const promise = return_promise
|
|
11451
|
+
const promise = return_promise ? getResolveablePromise() : undefined;
|
|
11324
11452
|
// If we're not waiting and attributes exist, save acts as
|
|
11325
11453
|
// `set(attr).save(null, opts)` with validation. Otherwise, check if
|
|
11326
11454
|
// the model will be valid when the attributes, if any, are set.
|
|
11327
|
-
|
|
11328
|
-
|
|
11455
|
+
const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
|
|
11456
|
+
if (!valid) {
|
|
11457
|
+
// Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
|
|
11458
|
+
// rejected promise rather than `false`, so the promise contract holds
|
|
11459
|
+
// for every outcome.
|
|
11460
|
+
if (!promise)
|
|
11329
11461
|
return false;
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
return false;
|
|
11462
|
+
promise.reject(this.validationError);
|
|
11463
|
+
return promise;
|
|
11333
11464
|
}
|
|
11334
11465
|
// After a successful server-side save, the client is (optionally)
|
|
11335
11466
|
// updated with the server-side state.
|
|
@@ -11342,8 +11473,12 @@
|
|
|
11342
11473
|
let serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
11343
11474
|
if (wait)
|
|
11344
11475
|
serverAttrs = Object.assign({}, attrs, serverAttrs);
|
|
11345
|
-
if (serverAttrs && !this.set(serverAttrs, options))
|
|
11476
|
+
if (serverAttrs && !this.set(serverAttrs, options)) {
|
|
11477
|
+
// The server attributes failed validation (`'invalid'` has fired).
|
|
11478
|
+
// Settle the promise with the error rather than leave it pending.
|
|
11479
|
+
return_promise && promise.reject(this.validationError);
|
|
11346
11480
|
return false;
|
|
11481
|
+
}
|
|
11347
11482
|
if (success)
|
|
11348
11483
|
success.call(options.context, this, resp, options);
|
|
11349
11484
|
this.trigger('sync', this, resp, options);
|
|
@@ -11360,15 +11495,12 @@
|
|
|
11360
11495
|
const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
|
|
11361
11496
|
if (method === 'patch' && !options.attrs)
|
|
11362
11497
|
options.attrs = attrs;
|
|
11363
|
-
|
|
11498
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
11499
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
11500
|
+
const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
|
|
11364
11501
|
// Restore attributes.
|
|
11365
11502
|
this.attributes = attributes;
|
|
11366
|
-
|
|
11367
|
-
return promise;
|
|
11368
|
-
}
|
|
11369
|
-
else {
|
|
11370
|
-
return xhr;
|
|
11371
|
-
}
|
|
11503
|
+
return promise ?? xhr;
|
|
11372
11504
|
}
|
|
11373
11505
|
/**
|
|
11374
11506
|
* Destroy this model on the server if it was already persisted.
|
|
@@ -11515,6 +11647,23 @@
|
|
|
11515
11647
|
// Default options for `Collection#set`.
|
|
11516
11648
|
const setOptions = { add: true, remove: true, merge: true };
|
|
11517
11649
|
const addOptions = { add: true, remove: false };
|
|
11650
|
+
/**
|
|
11651
|
+
* Normalizes the predicate forms accepted by the query methods into one
|
|
11652
|
+
* `(model) => value` function. A function is used as-is, a string reads that
|
|
11653
|
+
* attribute, and an attributes object becomes a lodash `matches` matcher.
|
|
11654
|
+
*
|
|
11655
|
+
* The matcher is built here, once per scan. Calling `Model#matches` per model
|
|
11656
|
+
* instead rebuilds the match data for every model visited, which made
|
|
11657
|
+
* `findWhere` an order of magnitude slower than a hand-written predicate.
|
|
11658
|
+
*/
|
|
11659
|
+
function toModelPredicate(pred) {
|
|
11660
|
+
if (isFunction(pred))
|
|
11661
|
+
return pred;
|
|
11662
|
+
if (isString(pred))
|
|
11663
|
+
return (m) => m.get(pred);
|
|
11664
|
+
const matcher = matches(pred);
|
|
11665
|
+
return (m) => matcher(m.attributes);
|
|
11666
|
+
}
|
|
11518
11667
|
/**
|
|
11519
11668
|
* @public
|
|
11520
11669
|
* If models tend to represent a single row of data, a Collection is
|
|
@@ -11843,17 +11992,13 @@
|
|
|
11843
11992
|
return this.models.slice(start, end);
|
|
11844
11993
|
}
|
|
11845
11994
|
filter(callback, thisArg) {
|
|
11846
|
-
return this.models.filter(
|
|
11847
|
-
? callback
|
|
11848
|
-
: (m) => m.matches(callback), thisArg);
|
|
11995
|
+
return this.models.filter(toModelPredicate(callback), thisArg);
|
|
11849
11996
|
}
|
|
11850
11997
|
every(pred) {
|
|
11851
11998
|
if (isFunction(pred)) {
|
|
11852
|
-
return this.models.
|
|
11853
|
-
}
|
|
11854
|
-
else {
|
|
11855
|
-
return this.models.every((m) => m.matches(pred));
|
|
11999
|
+
return this.models.every((m) => pred(m.attributes));
|
|
11856
12000
|
}
|
|
12001
|
+
return this.models.every(toModelPredicate(pred));
|
|
11857
12002
|
}
|
|
11858
12003
|
difference(values) {
|
|
11859
12004
|
return this.models.filter((m) => !values.includes(m));
|
|
@@ -11869,16 +12014,12 @@
|
|
|
11869
12014
|
}
|
|
11870
12015
|
some(pred) {
|
|
11871
12016
|
if (isFunction(pred)) {
|
|
11872
|
-
return this.models.
|
|
11873
|
-
}
|
|
11874
|
-
else {
|
|
11875
|
-
return this.models.some((m) => m.matches(pred));
|
|
12017
|
+
return this.models.some((m) => pred(m.attributes));
|
|
11876
12018
|
}
|
|
12019
|
+
return this.models.some(toModelPredicate(pred));
|
|
11877
12020
|
}
|
|
11878
12021
|
sortBy(iteratee) {
|
|
11879
|
-
return sortBy(this.models,
|
|
11880
|
-
? iteratee
|
|
11881
|
-
: (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
|
|
12022
|
+
return sortBy(this.models, toModelPredicate(iteratee));
|
|
11882
12023
|
}
|
|
11883
12024
|
isEmpty() {
|
|
11884
12025
|
return !this.models.length;
|
|
@@ -11899,26 +12040,22 @@
|
|
|
11899
12040
|
return this.models.length;
|
|
11900
12041
|
}
|
|
11901
12042
|
countBy(f) {
|
|
11902
|
-
return countBy(this.models,
|
|
12043
|
+
return countBy(this.models, toModelPredicate(f));
|
|
11903
12044
|
}
|
|
11904
12045
|
groupBy(pred) {
|
|
11905
|
-
return groupBy(this.models,
|
|
12046
|
+
return groupBy(this.models, toModelPredicate(pred));
|
|
11906
12047
|
}
|
|
11907
12048
|
indexOf(model, fromIndex) {
|
|
11908
12049
|
return this.models.indexOf(model, fromIndex);
|
|
11909
12050
|
}
|
|
11910
12051
|
findLastIndex(pred, fromIndex) {
|
|
11911
|
-
return this.models.findLastIndex(
|
|
11912
|
-
? pred
|
|
11913
|
-
: (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
|
|
12052
|
+
return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
|
|
11914
12053
|
}
|
|
11915
12054
|
lastIndexOf(model, fromIndex) {
|
|
11916
12055
|
return this.models.lastIndexOf(model, fromIndex);
|
|
11917
12056
|
}
|
|
11918
12057
|
findIndex(pred) {
|
|
11919
|
-
return this.models.findIndex(
|
|
11920
|
-
? pred
|
|
11921
|
-
: (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
|
|
12058
|
+
return this.models.findIndex(toModelPredicate(pred));
|
|
11922
12059
|
}
|
|
11923
12060
|
last() {
|
|
11924
12061
|
const length = this.models == null ? 0 : this.models.length;
|
|
@@ -11931,9 +12068,7 @@
|
|
|
11931
12068
|
return this.head();
|
|
11932
12069
|
}
|
|
11933
12070
|
map(cb, thisArg) {
|
|
11934
|
-
return this.models.map(
|
|
11935
|
-
? cb
|
|
11936
|
-
: (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
|
|
12071
|
+
return this.models.map(toModelPredicate(cb), thisArg);
|
|
11937
12072
|
}
|
|
11938
12073
|
reduce(callback, initialValue) {
|
|
11939
12074
|
return this.models.reduce(callback, initialValue || this.models[0]);
|
|
@@ -11984,10 +12119,7 @@
|
|
|
11984
12119
|
return this.where(attrs, true);
|
|
11985
12120
|
}
|
|
11986
12121
|
find(predicate, fromIndex) {
|
|
11987
|
-
|
|
11988
|
-
? predicate
|
|
11989
|
-
: (m) => m.matches(predicate);
|
|
11990
|
-
return this.models.find(pred, fromIndex);
|
|
12122
|
+
return this.models.find(toModelPredicate(predicate), fromIndex);
|
|
11991
12123
|
}
|
|
11992
12124
|
/**
|
|
11993
12125
|
* Force the collection to re-sort itself. You don't need to call this under
|
|
@@ -12030,46 +12162,22 @@
|
|
|
12030
12162
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
12031
12163
|
const collection = this;
|
|
12032
12164
|
const promise = options.promise ? getResolveablePromise() : undefined;
|
|
12033
|
-
// Tracks whether sync invoked one of the callbacks. A sync may instead
|
|
12034
|
-
// settle its returned promise; the `.then`/`.catch` below bridges that to
|
|
12035
|
-
// the callbacks so the fetch promise never hangs. The flag is set
|
|
12036
|
-
// synchronously inside each callback (before sync's promise resolves), so
|
|
12037
|
-
// it's reliable by the time those handlers run.
|
|
12038
|
-
let settled = false;
|
|
12039
12165
|
options.success = function (resp) {
|
|
12040
|
-
settled = true;
|
|
12041
12166
|
const method = options.reset ? 'reset' : 'set';
|
|
12042
12167
|
// fromStorage prevents individual model set() calls from triggering auto-save on reads
|
|
12043
12168
|
collection[method](resp, Object.assign({}, options, { fromStorage: true }));
|
|
12044
12169
|
if (success)
|
|
12045
12170
|
success.call(options.context, collection, resp, options);
|
|
12046
|
-
|
|
12171
|
+
// Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
|
|
12172
|
+
// listener rejects the promise instead of being lost.
|
|
12047
12173
|
collection.trigger('sync', collection, resp, options);
|
|
12174
|
+
promise && promise.resolve();
|
|
12048
12175
|
};
|
|
12049
12176
|
wrapError(this, options, promise);
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
onError(resp);
|
|
12055
|
-
};
|
|
12056
|
-
}
|
|
12057
|
-
const result = this.sync('read', this, options);
|
|
12058
|
-
if (promise) {
|
|
12059
|
-
Promise.resolve(result).then(
|
|
12060
|
-
// A sync that resolves its promise without invoking options.success:
|
|
12061
|
-
// treat the resolved value as the response, so the collection is still
|
|
12062
|
-
// populated and the promise settles instead of hanging.
|
|
12063
|
-
(value) => {
|
|
12064
|
-
if (!settled)
|
|
12065
|
-
options.success(value);
|
|
12066
|
-
},
|
|
12067
|
-
// A sync that rejects instead of invoking options.error; settling
|
|
12068
|
-
// twice is a harmless no-op.
|
|
12069
|
-
(e) => promise.reject(e));
|
|
12070
|
-
return promise;
|
|
12071
|
-
}
|
|
12072
|
-
return result;
|
|
12177
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
12178
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
12179
|
+
const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
|
|
12180
|
+
return promise ?? result;
|
|
12073
12181
|
}
|
|
12074
12182
|
/**
|
|
12075
12183
|
* Create a new instance of a model in this collection. Add the model to the
|
|
@@ -12081,9 +12189,22 @@
|
|
|
12081
12189
|
const wait = options.wait;
|
|
12082
12190
|
const return_promise = options.promise;
|
|
12083
12191
|
const promise = return_promise && getResolveablePromise();
|
|
12192
|
+
// `_prepareModel` reports attributes that fail validation by firing
|
|
12193
|
+
// `'invalid'` and returning null. With `promise`, capture that error so a
|
|
12194
|
+
// rejected promise is handed back rather than `null`.
|
|
12195
|
+
let validationError;
|
|
12196
|
+
const captureInvalid = (_c, err) => (validationError = err);
|
|
12197
|
+
if (return_promise)
|
|
12198
|
+
this.on('invalid', captureInvalid);
|
|
12084
12199
|
const preparedModel = this._prepareModel(model, options);
|
|
12085
|
-
if (
|
|
12086
|
-
|
|
12200
|
+
if (return_promise)
|
|
12201
|
+
this.off('invalid', captureInvalid);
|
|
12202
|
+
if (!preparedModel) {
|
|
12203
|
+
if (!return_promise)
|
|
12204
|
+
return null;
|
|
12205
|
+
promise.reject(validationError);
|
|
12206
|
+
return promise;
|
|
12207
|
+
}
|
|
12087
12208
|
if (!wait)
|
|
12088
12209
|
this.add(preparedModel, options);
|
|
12089
12210
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
@@ -12105,8 +12226,15 @@
|
|
|
12105
12226
|
error && error.call(options.context, model, e, options);
|
|
12106
12227
|
return_promise && promise.reject(e);
|
|
12107
12228
|
};
|
|
12108
|
-
|
|
12229
|
+
// `options.promise` is passed through, so `save` installs its own bridge
|
|
12230
|
+
// from the sync's returned promise to its callbacks: a sync that resolves
|
|
12231
|
+
// without calling them still reaches the wired `options.success` above,
|
|
12232
|
+
// which resolves our promise with the model. Only a rejection that bypassed
|
|
12233
|
+
// the callbacks (a rejecting sync, or a throwing `'sync'`/`'error'`
|
|
12234
|
+
// listener) still needs bridging to ours.
|
|
12235
|
+
const result = preparedModel.save(null, options);
|
|
12109
12236
|
if (return_promise) {
|
|
12237
|
+
Promise.resolve(result).catch((e) => promise.reject(e));
|
|
12110
12238
|
return promise;
|
|
12111
12239
|
}
|
|
12112
12240
|
else {
|
|
@@ -12332,6 +12460,7 @@
|
|
|
12332
12460
|
EventEmitter,
|
|
12333
12461
|
Model,
|
|
12334
12462
|
sync,
|
|
12463
|
+
HTTPError,
|
|
12335
12464
|
};
|
|
12336
12465
|
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
|
|
12337
12466
|
// We use `self` instead of `window` for `WebWorker` support.
|
|
@@ -12362,6 +12491,7 @@
|
|
|
12362
12491
|
exports.BrowserStorage = BrowserStorage;
|
|
12363
12492
|
exports.Collection = Collection;
|
|
12364
12493
|
exports.EventEmitter = EventEmitter;
|
|
12494
|
+
exports.HTTPError = HTTPError;
|
|
12365
12495
|
exports.Model = Model;
|
|
12366
12496
|
exports.PersistentStorage = PersistentStorage;
|
|
12367
12497
|
exports.default = skeletor;
|