@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.esm.js
CHANGED
|
@@ -8471,6 +8471,16 @@ function guid() {
|
|
|
8471
8471
|
// Generate a pseudo-GUID by concatenating random hexadecimal.
|
|
8472
8472
|
return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
|
|
8473
8473
|
}
|
|
8474
|
+
// Helpers
|
|
8475
|
+
// -------
|
|
8476
|
+
/**
|
|
8477
|
+
* Whether `value` is a thenable (a promise, or a promise-like object such as
|
|
8478
|
+
* a jqXHR). Decides whether a `sync` return value can be bridged to the
|
|
8479
|
+
* promise that `fetch`/`save` hand back.
|
|
8480
|
+
*/
|
|
8481
|
+
function isThenable(value) {
|
|
8482
|
+
return value != null && typeof value.then === 'function';
|
|
8483
|
+
}
|
|
8474
8484
|
function getResolveablePromise() {
|
|
8475
8485
|
const wrapper = {
|
|
8476
8486
|
isResolved: false,
|
|
@@ -8515,6 +8525,65 @@ function wrapError(model, options, promise) {
|
|
|
8515
8525
|
promise && promise.reject && promise.reject(resp);
|
|
8516
8526
|
};
|
|
8517
8527
|
}
|
|
8528
|
+
/**
|
|
8529
|
+
* Run a `sync` and make sure `promise` settles from its outcome, however the
|
|
8530
|
+
* sync reports it.
|
|
8531
|
+
*
|
|
8532
|
+
* A `sync` may invoke `options.success`/`options.error` (the callback
|
|
8533
|
+
* contract), or settle the promise it returns without calling them: a
|
|
8534
|
+
* promise-only custom sync, or an async sync whose promise rejects because a
|
|
8535
|
+
* `'sync'`/`'error'` listener threw after the callbacks ran. `options.success`
|
|
8536
|
+
* and `options.error` must already be wired to resolve/reject `promise`. This
|
|
8537
|
+
* wraps them to record that they ran, calls `run()`, and bridges a thenable
|
|
8538
|
+
* result to them so the promise never hangs:
|
|
8539
|
+
*
|
|
8540
|
+
* - fulfilment with the callbacks unrun is delivered to `options.success`, so
|
|
8541
|
+
* the response is still merged into the model or collection;
|
|
8542
|
+
* - rejection with the callbacks unrun is delivered to `options.error`, so the
|
|
8543
|
+
* caller's `error` callback and the `'error'` event still fire;
|
|
8544
|
+
* - rejection after the callbacks ran rejects `promise` directly: a no-op if
|
|
8545
|
+
* they already settled it, and the only way to settle it if a listener threw
|
|
8546
|
+
* before they could;
|
|
8547
|
+
* - a throw from either delivery (a throwing `success`/`error` callback or
|
|
8548
|
+
* listener) rejects `promise` instead of surfacing as an unhandled rejection.
|
|
8549
|
+
*
|
|
8550
|
+
* A non-thenable result (a callback-only sync that returns nothing, or an
|
|
8551
|
+
* XHR) is left alone: such a sync settles `promise` through its callbacks, and
|
|
8552
|
+
* bridging it would fire `success` on the next microtask, before the sync is
|
|
8553
|
+
* done. Without a `promise`, `run()` is called as is.
|
|
8554
|
+
*/
|
|
8555
|
+
function bridgeSyncToPromise(options, promise, run) {
|
|
8556
|
+
if (!promise)
|
|
8557
|
+
return run();
|
|
8558
|
+
let settled = false;
|
|
8559
|
+
const success = options.success;
|
|
8560
|
+
const error = options.error;
|
|
8561
|
+
options.success = (...args) => {
|
|
8562
|
+
settled = true;
|
|
8563
|
+
return success && success(...args);
|
|
8564
|
+
};
|
|
8565
|
+
options.error = (...args) => {
|
|
8566
|
+
settled = true;
|
|
8567
|
+
return error && error(...args);
|
|
8568
|
+
};
|
|
8569
|
+
const result = run();
|
|
8570
|
+
if (isThenable(result)) {
|
|
8571
|
+
Promise.resolve(result)
|
|
8572
|
+
.then((value) => {
|
|
8573
|
+
if (!settled)
|
|
8574
|
+
options.success(value);
|
|
8575
|
+
}, (e) => {
|
|
8576
|
+
if (settled) {
|
|
8577
|
+
promise.reject(e);
|
|
8578
|
+
}
|
|
8579
|
+
else {
|
|
8580
|
+
options.error(e);
|
|
8581
|
+
}
|
|
8582
|
+
})
|
|
8583
|
+
.catch((e) => promise.reject(e));
|
|
8584
|
+
}
|
|
8585
|
+
return result;
|
|
8586
|
+
}
|
|
8518
8587
|
// Map from CRUD to HTTP for our default `sync` implementation.
|
|
8519
8588
|
const methodMap = {
|
|
8520
8589
|
create: 'POST',
|
|
@@ -8564,43 +8633,77 @@ function getSyncMethod(model) {
|
|
|
8564
8633
|
return store ? store.sync() : sync;
|
|
8565
8634
|
}
|
|
8566
8635
|
/**
|
|
8567
|
-
*
|
|
8568
|
-
*
|
|
8569
|
-
*
|
|
8570
|
-
* to the model's `url()`.
|
|
8571
|
-
*
|
|
8572
|
-
* Some possible customizations could be:
|
|
8573
|
-
*
|
|
8574
|
-
* - Use `setTimeout` to batch rapid-fire updates into a single request.
|
|
8575
|
-
* - Persist models via WebSockets instead of Ajax.
|
|
8576
|
-
* - Persist models to browser storage
|
|
8636
|
+
* The error the default `sync` hands to `options.error`, and rejects with, for
|
|
8637
|
+
* a response outside the 2xx range.
|
|
8638
|
+
* @public
|
|
8577
8639
|
*/
|
|
8640
|
+
class HTTPError extends Error {
|
|
8641
|
+
constructor(response, method, url) {
|
|
8642
|
+
const statusText = response.statusText ? ` ${response.statusText}` : '';
|
|
8643
|
+
super(`${method} ${url} failed with HTTP ${response.status}${statusText}`);
|
|
8644
|
+
this.name = 'HTTPError';
|
|
8645
|
+
this.status = response.status;
|
|
8646
|
+
this.response = response;
|
|
8647
|
+
}
|
|
8648
|
+
}
|
|
8578
8649
|
/**
|
|
8650
|
+
* The default `sync`: a JSON request over `fetch()` to the model's `url()`.
|
|
8651
|
+
* Override it (or `Model.sync`/`Collection.sync`) to change how models are
|
|
8652
|
+
* persisted, for instance to batch rapid-fire updates with `setTimeout`, to
|
|
8653
|
+
* talk over WebSockets, or to write to browser storage.
|
|
8654
|
+
*
|
|
8655
|
+
* It honours both contracts a `sync` may use. On a 2xx response the JSON body
|
|
8656
|
+
* (`null` when the body is empty) is passed to `options.success` and the
|
|
8657
|
+
* returned promise resolves with it. On a non-2xx response, a network failure
|
|
8658
|
+
* or an unparsable body, `options.error` receives the error (an `HTTPError`
|
|
8659
|
+
* for the first) and the promise rejects with it.
|
|
8660
|
+
*
|
|
8661
|
+
* `options.data` is sent as query parameters on GET, which may not carry a
|
|
8662
|
+
* body, and as a JSON body otherwise. For create, update and patch the body
|
|
8663
|
+
* defaults to `options.attrs` or the model's `toJSON()`.
|
|
8579
8664
|
* @public
|
|
8580
8665
|
*/
|
|
8581
8666
|
function sync(method, model, options = {}) {
|
|
8667
|
+
const type = methodMap[method];
|
|
8582
8668
|
let data = options.data;
|
|
8583
8669
|
// Ensure that we have the appropriate request data.
|
|
8584
8670
|
if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
|
8585
8671
|
data = options.attrs || model.toJSON();
|
|
8586
8672
|
}
|
|
8587
|
-
|
|
8588
|
-
const params = {
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
const url = options.url || result(model, 'url') || urlError();
|
|
8598
|
-
const xhr = fetch(url, params);
|
|
8599
|
-
if (options) {
|
|
8600
|
-
options.xhr = xhr;
|
|
8673
|
+
let url = options.url || result(model, 'url') || urlError();
|
|
8674
|
+
const params = { method: type };
|
|
8675
|
+
if (data != null) {
|
|
8676
|
+
if (type === 'GET') {
|
|
8677
|
+
url += (url.includes('?') ? '&' : '?') + new URLSearchParams(data).toString();
|
|
8678
|
+
}
|
|
8679
|
+
else {
|
|
8680
|
+
params.body = JSON.stringify(data);
|
|
8681
|
+
params.headers = { 'Content-Type': 'application/json' };
|
|
8682
|
+
}
|
|
8601
8683
|
}
|
|
8684
|
+
const xhr = fetch(url, params);
|
|
8685
|
+
options.xhr = xhr;
|
|
8602
8686
|
model.trigger('request', model, xhr, { ...params, xhr });
|
|
8603
|
-
return xhr
|
|
8687
|
+
return Promise.resolve(xhr)
|
|
8688
|
+
.then(async (response) => {
|
|
8689
|
+
if (!response.ok)
|
|
8690
|
+
throw new HTTPError(response, type, url);
|
|
8691
|
+
const text = await response.text();
|
|
8692
|
+
return text ? JSON.parse(text) : null;
|
|
8693
|
+
})
|
|
8694
|
+
.then(
|
|
8695
|
+
// The callbacks run before the returned promise settles, so a caller that
|
|
8696
|
+
// bridges this promise (`Model.fetch`/`save`) sees the outcome as already
|
|
8697
|
+
// reported. A throw from `success` (a `'sync'` listener, say) is not
|
|
8698
|
+
// routed to `error`: it rejects the promise, which is what the bridge
|
|
8699
|
+
// expects once the callbacks have run.
|
|
8700
|
+
(resp) => {
|
|
8701
|
+
options.success?.(resp, options);
|
|
8702
|
+
return resp;
|
|
8703
|
+
}, (e) => {
|
|
8704
|
+
options.error?.(e);
|
|
8705
|
+
throw e;
|
|
8706
|
+
});
|
|
8604
8707
|
}
|
|
8605
8708
|
|
|
8606
8709
|
/**
|
|
@@ -8699,7 +8802,7 @@ class PersistentStorage {
|
|
|
8699
8802
|
throw new Error("Skeletor.storage: the 'node' storage type is only available in Node.js builds. " +
|
|
8700
8803
|
"Import from '@converse/skeletor/node' instead of '@converse/skeletor'.");
|
|
8701
8804
|
}
|
|
8702
|
-
this.store = new NodeSQLiteStorage(this.name);
|
|
8805
|
+
this.store = new NodeSQLiteStorage(this.name, PersistentStorage.nodeStorageDir ?? undefined);
|
|
8703
8806
|
if (batchedWrites) {
|
|
8704
8807
|
this.store.debouncedSetItems = mergebounce((items) => this.store.setItems(items), 50, {
|
|
8705
8808
|
'promise': true,
|
|
@@ -8903,6 +9006,18 @@ class PersistentStorage {
|
|
|
8903
9006
|
// node-only driver (and its `node:sqlite` import) never enters the browser
|
|
8904
9007
|
// bundle; the `@converse/skeletor/node` entry sets it.
|
|
8905
9008
|
PersistentStorage.nodeStorage = null;
|
|
9009
|
+
/**
|
|
9010
|
+
* Directory the `'node'` store type writes its database files to. The driver's
|
|
9011
|
+
* own default is relative, so it resolves against `process.cwd()` and a process
|
|
9012
|
+
* started from two directories ends up with two separate sets of state. Set an
|
|
9013
|
+
* absolute path (`$XDG_STATE_HOME/<app>`, say) to pin it. Left `null`, the
|
|
9014
|
+
* driver's default applies.
|
|
9015
|
+
*
|
|
9016
|
+
* Only the `'node'` string store type reads this; passing a driver instance to
|
|
9017
|
+
* the constructor means you've already chosen a directory yourself.
|
|
9018
|
+
* @public
|
|
9019
|
+
*/
|
|
9020
|
+
PersistentStorage.nodeStorageDir = null;
|
|
8906
9021
|
PersistentStorage.sessionStorageInitialized = localForage.defineDriver(sessionStorageWrapper);
|
|
8907
9022
|
PersistentStorage.localForage = localForage;
|
|
8908
9023
|
|
|
@@ -9954,6 +10069,48 @@ var keyBy = createAggregator(function(result, value, key) {
|
|
|
9954
10069
|
baseAssignValue(result, key, value);
|
|
9955
10070
|
});
|
|
9956
10071
|
|
|
10072
|
+
/** Used to compose bitmasks for cloning. */
|
|
10073
|
+
var CLONE_DEEP_FLAG$1 = 1;
|
|
10074
|
+
|
|
10075
|
+
/**
|
|
10076
|
+
* Creates a function that performs a partial deep comparison between a given
|
|
10077
|
+
* object and `source`, returning `true` if the given object has equivalent
|
|
10078
|
+
* property values, else `false`.
|
|
10079
|
+
*
|
|
10080
|
+
* **Note:** The created function is equivalent to `_.isMatch` with `source`
|
|
10081
|
+
* partially applied.
|
|
10082
|
+
*
|
|
10083
|
+
* Partial comparisons will match empty array and empty object `source`
|
|
10084
|
+
* values against any array or object value, respectively. See `_.isEqual`
|
|
10085
|
+
* for a list of supported value comparisons.
|
|
10086
|
+
*
|
|
10087
|
+
* **Note:** Multiple values can be checked by combining several matchers
|
|
10088
|
+
* using `_.overSome`
|
|
10089
|
+
*
|
|
10090
|
+
* @static
|
|
10091
|
+
* @memberOf _
|
|
10092
|
+
* @since 3.0.0
|
|
10093
|
+
* @category Util
|
|
10094
|
+
* @param {Object} source The object of property values to match.
|
|
10095
|
+
* @returns {Function} Returns the new spec function.
|
|
10096
|
+
* @example
|
|
10097
|
+
*
|
|
10098
|
+
* var objects = [
|
|
10099
|
+
* { 'a': 1, 'b': 2, 'c': 3 },
|
|
10100
|
+
* { 'a': 4, 'b': 5, 'c': 6 }
|
|
10101
|
+
* ];
|
|
10102
|
+
*
|
|
10103
|
+
* _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
|
|
10104
|
+
* // => [{ 'a': 4, 'b': 5, 'c': 6 }]
|
|
10105
|
+
*
|
|
10106
|
+
* // Checking for several possible values
|
|
10107
|
+
* _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
|
|
10108
|
+
* // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
|
|
10109
|
+
*/
|
|
10110
|
+
function matches(source) {
|
|
10111
|
+
return baseMatches(baseClone(source, CLONE_DEEP_FLAG$1));
|
|
10112
|
+
}
|
|
10113
|
+
|
|
9957
10114
|
/** Built-in value references. */
|
|
9958
10115
|
var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
|
|
9959
10116
|
|
|
@@ -10517,53 +10674,36 @@ function isEqual(value, other) {
|
|
|
10517
10674
|
return baseIsEqual(value, other);
|
|
10518
10675
|
}
|
|
10519
10676
|
|
|
10520
|
-
/** Used to compose bitmasks for cloning. */
|
|
10521
|
-
var CLONE_DEEP_FLAG$1 = 1;
|
|
10522
|
-
|
|
10523
10677
|
/**
|
|
10524
|
-
*
|
|
10525
|
-
*
|
|
10526
|
-
*
|
|
10527
|
-
*
|
|
10528
|
-
*
|
|
10678
|
+
* Performs a partial deep comparison between `object` and `source` to
|
|
10679
|
+
* determine if `object` contains equivalent property values.
|
|
10680
|
+
*
|
|
10681
|
+
* **Note:** This method is equivalent to `_.matches` when `source` is
|
|
10682
|
+
* partially applied.
|
|
10683
|
+
*
|
|
10684
|
+
* Partial comparisons will match empty array and empty object `source`
|
|
10685
|
+
* values against any array or object value, respectively. See `_.isEqual`
|
|
10686
|
+
* for a list of supported value comparisons.
|
|
10529
10687
|
*
|
|
10530
10688
|
* @static
|
|
10531
|
-
* @since 4.0.0
|
|
10532
10689
|
* @memberOf _
|
|
10533
|
-
* @
|
|
10534
|
-
* @
|
|
10535
|
-
* @
|
|
10690
|
+
* @since 3.0.0
|
|
10691
|
+
* @category Lang
|
|
10692
|
+
* @param {Object} object The object to inspect.
|
|
10693
|
+
* @param {Object} source The object of property values to match.
|
|
10694
|
+
* @returns {boolean} Returns `true` if `object` is a match, else `false`.
|
|
10536
10695
|
* @example
|
|
10537
10696
|
*
|
|
10538
|
-
* var
|
|
10539
|
-
* { 'user': 'barney', 'age': 36, 'active': true },
|
|
10540
|
-
* { 'user': 'fred', 'age': 40, 'active': false }
|
|
10541
|
-
* ];
|
|
10542
|
-
*
|
|
10543
|
-
* // The `_.matches` iteratee shorthand.
|
|
10544
|
-
* _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
|
|
10545
|
-
* // => [{ 'user': 'barney', 'age': 36, 'active': true }]
|
|
10546
|
-
*
|
|
10547
|
-
* // The `_.matchesProperty` iteratee shorthand.
|
|
10548
|
-
* _.filter(users, _.iteratee(['user', 'fred']));
|
|
10549
|
-
* // => [{ 'user': 'fred', 'age': 40 }]
|
|
10697
|
+
* var object = { 'a': 1, 'b': 2 };
|
|
10550
10698
|
*
|
|
10551
|
-
*
|
|
10552
|
-
*
|
|
10553
|
-
* // => ['barney', 'fred']
|
|
10554
|
-
*
|
|
10555
|
-
* // Create custom iteratee shorthands.
|
|
10556
|
-
* _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
|
|
10557
|
-
* return !_.isRegExp(func) ? iteratee(func) : function(string) {
|
|
10558
|
-
* return func.test(string);
|
|
10559
|
-
* };
|
|
10560
|
-
* });
|
|
10699
|
+
* _.isMatch(object, { 'b': 2 });
|
|
10700
|
+
* // => true
|
|
10561
10701
|
*
|
|
10562
|
-
* _.
|
|
10563
|
-
* // =>
|
|
10702
|
+
* _.isMatch(object, { 'b': 1 });
|
|
10703
|
+
* // => false
|
|
10564
10704
|
*/
|
|
10565
|
-
function
|
|
10566
|
-
return
|
|
10705
|
+
function isMatch(object, source) {
|
|
10706
|
+
return object === source || baseIsMatch(object, source, getMatchData(source));
|
|
10567
10707
|
}
|
|
10568
10708
|
|
|
10569
10709
|
/**
|
|
@@ -11077,10 +11217,20 @@ class Model extends EventEmitterObject {
|
|
|
11077
11217
|
return this.get(attr) != null;
|
|
11078
11218
|
}
|
|
11079
11219
|
/**
|
|
11080
|
-
*
|
|
11220
|
+
* Returns `true` if the model's attributes satisfy `attrs`.
|
|
11221
|
+
*
|
|
11222
|
+
* An attributes object is compared with lodash's `isMatch`: a partial, deep
|
|
11223
|
+
* comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
|
|
11224
|
+
* that has `type: 'hidden'` alongside other keys. A function is called with
|
|
11225
|
+
* the attributes and its result coerced to a boolean.
|
|
11226
|
+
*
|
|
11227
|
+
* To scan a collection, prefer `Collection#find`, `filter` or `where`. They
|
|
11228
|
+
* build their matcher once per scan instead of once per model.
|
|
11081
11229
|
*/
|
|
11082
11230
|
matches(attrs) {
|
|
11083
|
-
|
|
11231
|
+
if (typeof attrs === 'function')
|
|
11232
|
+
return !!attrs(this.attributes);
|
|
11233
|
+
return isMatch(this.attributes, attrs);
|
|
11084
11234
|
}
|
|
11085
11235
|
/**
|
|
11086
11236
|
* Set a hash of model attributes on the object, firing `"change"`. This is
|
|
@@ -11253,47 +11403,25 @@ class Model extends EventEmitterObject {
|
|
|
11253
11403
|
options = Object.assign({ parse: true }, options);
|
|
11254
11404
|
const success = options.success;
|
|
11255
11405
|
const promise = options.promise ? getResolveablePromise() : undefined;
|
|
11256
|
-
// Tracks whether sync invoked one of the callbacks. A sync may instead
|
|
11257
|
-
// settle its returned promise; the `.then`/`.catch` below bridges that to
|
|
11258
|
-
// the callbacks so the fetch promise never hangs. The flag is set
|
|
11259
|
-
// synchronously inside each callback (before sync's promise resolves), so
|
|
11260
|
-
// it's reliable by the time those handlers run.
|
|
11261
|
-
let settled = false;
|
|
11262
11406
|
options.success = (resp) => {
|
|
11263
|
-
settled = true;
|
|
11264
11407
|
const serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
11265
11408
|
// fromStorage prevents the set() from triggering auto-save on reads
|
|
11266
|
-
if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
|
|
11409
|
+
if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
|
|
11410
|
+
// The fetched attributes failed validation (`'invalid'` has fired).
|
|
11411
|
+
// Settle the promise with the error rather than leave it pending.
|
|
11412
|
+
promise && promise.reject(this.validationError);
|
|
11267
11413
|
return false;
|
|
11414
|
+
}
|
|
11268
11415
|
if (success)
|
|
11269
11416
|
success.call(options.context, this, resp, options);
|
|
11270
11417
|
this.trigger('sync', this, resp, options);
|
|
11271
11418
|
promise && promise.resolve(resp);
|
|
11272
11419
|
};
|
|
11273
11420
|
wrapError(this, options, promise);
|
|
11274
|
-
|
|
11275
|
-
|
|
11276
|
-
|
|
11277
|
-
|
|
11278
|
-
onError(resp);
|
|
11279
|
-
};
|
|
11280
|
-
}
|
|
11281
|
-
const result = this.sync('read', this, options);
|
|
11282
|
-
if (promise) {
|
|
11283
|
-
Promise.resolve(result).then(
|
|
11284
|
-
// A sync that resolves its promise without invoking options.success:
|
|
11285
|
-
// treat the resolved value as the response, so the model is still
|
|
11286
|
-
// merged and the promise settles instead of hanging.
|
|
11287
|
-
(value) => {
|
|
11288
|
-
if (!settled)
|
|
11289
|
-
options.success(value);
|
|
11290
|
-
},
|
|
11291
|
-
// A sync that rejects instead of invoking options.error; settling
|
|
11292
|
-
// twice is a harmless no-op.
|
|
11293
|
-
(e) => promise.reject(e));
|
|
11294
|
-
return promise;
|
|
11295
|
-
}
|
|
11296
|
-
return result;
|
|
11421
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
11422
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
11423
|
+
const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
|
|
11424
|
+
return promise ?? result;
|
|
11297
11425
|
}
|
|
11298
11426
|
/**
|
|
11299
11427
|
* Set a hash of model attributes, and sync the model to the server.
|
|
@@ -11314,16 +11442,19 @@ class Model extends EventEmitterObject {
|
|
|
11314
11442
|
options = Object.assign({ validate: true, parse: true }, options);
|
|
11315
11443
|
const wait = options.wait;
|
|
11316
11444
|
const return_promise = options.promise;
|
|
11317
|
-
const promise = return_promise
|
|
11445
|
+
const promise = return_promise ? getResolveablePromise() : undefined;
|
|
11318
11446
|
// If we're not waiting and attributes exist, save acts as
|
|
11319
11447
|
// `set(attr).save(null, opts)` with validation. Otherwise, check if
|
|
11320
11448
|
// the model will be valid when the attributes, if any, are set.
|
|
11321
|
-
|
|
11322
|
-
|
|
11449
|
+
const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
|
|
11450
|
+
if (!valid) {
|
|
11451
|
+
// Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
|
|
11452
|
+
// rejected promise rather than `false`, so the promise contract holds
|
|
11453
|
+
// for every outcome.
|
|
11454
|
+
if (!promise)
|
|
11323
11455
|
return false;
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
return false;
|
|
11456
|
+
promise.reject(this.validationError);
|
|
11457
|
+
return promise;
|
|
11327
11458
|
}
|
|
11328
11459
|
// After a successful server-side save, the client is (optionally)
|
|
11329
11460
|
// updated with the server-side state.
|
|
@@ -11336,8 +11467,12 @@ class Model extends EventEmitterObject {
|
|
|
11336
11467
|
let serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
11337
11468
|
if (wait)
|
|
11338
11469
|
serverAttrs = Object.assign({}, attrs, serverAttrs);
|
|
11339
|
-
if (serverAttrs && !this.set(serverAttrs, options))
|
|
11470
|
+
if (serverAttrs && !this.set(serverAttrs, options)) {
|
|
11471
|
+
// The server attributes failed validation (`'invalid'` has fired).
|
|
11472
|
+
// Settle the promise with the error rather than leave it pending.
|
|
11473
|
+
return_promise && promise.reject(this.validationError);
|
|
11340
11474
|
return false;
|
|
11475
|
+
}
|
|
11341
11476
|
if (success)
|
|
11342
11477
|
success.call(options.context, this, resp, options);
|
|
11343
11478
|
this.trigger('sync', this, resp, options);
|
|
@@ -11354,15 +11489,12 @@ class Model extends EventEmitterObject {
|
|
|
11354
11489
|
const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
|
|
11355
11490
|
if (method === 'patch' && !options.attrs)
|
|
11356
11491
|
options.attrs = attrs;
|
|
11357
|
-
|
|
11492
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
11493
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
11494
|
+
const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
|
|
11358
11495
|
// Restore attributes.
|
|
11359
11496
|
this.attributes = attributes;
|
|
11360
|
-
|
|
11361
|
-
return promise;
|
|
11362
|
-
}
|
|
11363
|
-
else {
|
|
11364
|
-
return xhr;
|
|
11365
|
-
}
|
|
11497
|
+
return promise ?? xhr;
|
|
11366
11498
|
}
|
|
11367
11499
|
/**
|
|
11368
11500
|
* Destroy this model on the server if it was already persisted.
|
|
@@ -11509,6 +11641,23 @@ var _Collection_instances, _Collection_hydrate, _a;
|
|
|
11509
11641
|
// Default options for `Collection#set`.
|
|
11510
11642
|
const setOptions = { add: true, remove: true, merge: true };
|
|
11511
11643
|
const addOptions = { add: true, remove: false };
|
|
11644
|
+
/**
|
|
11645
|
+
* Normalizes the predicate forms accepted by the query methods into one
|
|
11646
|
+
* `(model) => value` function. A function is used as-is, a string reads that
|
|
11647
|
+
* attribute, and an attributes object becomes a lodash `matches` matcher.
|
|
11648
|
+
*
|
|
11649
|
+
* The matcher is built here, once per scan. Calling `Model#matches` per model
|
|
11650
|
+
* instead rebuilds the match data for every model visited, which made
|
|
11651
|
+
* `findWhere` an order of magnitude slower than a hand-written predicate.
|
|
11652
|
+
*/
|
|
11653
|
+
function toModelPredicate(pred) {
|
|
11654
|
+
if (isFunction(pred))
|
|
11655
|
+
return pred;
|
|
11656
|
+
if (isString(pred))
|
|
11657
|
+
return (m) => m.get(pred);
|
|
11658
|
+
const matcher = matches(pred);
|
|
11659
|
+
return (m) => matcher(m.attributes);
|
|
11660
|
+
}
|
|
11512
11661
|
/**
|
|
11513
11662
|
* @public
|
|
11514
11663
|
* If models tend to represent a single row of data, a Collection is
|
|
@@ -11837,17 +11986,13 @@ class Collection extends EventEmitterObject {
|
|
|
11837
11986
|
return this.models.slice(start, end);
|
|
11838
11987
|
}
|
|
11839
11988
|
filter(callback, thisArg) {
|
|
11840
|
-
return this.models.filter(
|
|
11841
|
-
? callback
|
|
11842
|
-
: (m) => m.matches(callback), thisArg);
|
|
11989
|
+
return this.models.filter(toModelPredicate(callback), thisArg);
|
|
11843
11990
|
}
|
|
11844
11991
|
every(pred) {
|
|
11845
11992
|
if (isFunction(pred)) {
|
|
11846
|
-
return this.models.
|
|
11847
|
-
}
|
|
11848
|
-
else {
|
|
11849
|
-
return this.models.every((m) => m.matches(pred));
|
|
11993
|
+
return this.models.every((m) => pred(m.attributes));
|
|
11850
11994
|
}
|
|
11995
|
+
return this.models.every(toModelPredicate(pred));
|
|
11851
11996
|
}
|
|
11852
11997
|
difference(values) {
|
|
11853
11998
|
return this.models.filter((m) => !values.includes(m));
|
|
@@ -11863,16 +12008,12 @@ class Collection extends EventEmitterObject {
|
|
|
11863
12008
|
}
|
|
11864
12009
|
some(pred) {
|
|
11865
12010
|
if (isFunction(pred)) {
|
|
11866
|
-
return this.models.
|
|
11867
|
-
}
|
|
11868
|
-
else {
|
|
11869
|
-
return this.models.some((m) => m.matches(pred));
|
|
12011
|
+
return this.models.some((m) => pred(m.attributes));
|
|
11870
12012
|
}
|
|
12013
|
+
return this.models.some(toModelPredicate(pred));
|
|
11871
12014
|
}
|
|
11872
12015
|
sortBy(iteratee) {
|
|
11873
|
-
return sortBy(this.models,
|
|
11874
|
-
? iteratee
|
|
11875
|
-
: (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
|
|
12016
|
+
return sortBy(this.models, toModelPredicate(iteratee));
|
|
11876
12017
|
}
|
|
11877
12018
|
isEmpty() {
|
|
11878
12019
|
return !this.models.length;
|
|
@@ -11893,26 +12034,22 @@ class Collection extends EventEmitterObject {
|
|
|
11893
12034
|
return this.models.length;
|
|
11894
12035
|
}
|
|
11895
12036
|
countBy(f) {
|
|
11896
|
-
return countBy(this.models,
|
|
12037
|
+
return countBy(this.models, toModelPredicate(f));
|
|
11897
12038
|
}
|
|
11898
12039
|
groupBy(pred) {
|
|
11899
|
-
return groupBy(this.models,
|
|
12040
|
+
return groupBy(this.models, toModelPredicate(pred));
|
|
11900
12041
|
}
|
|
11901
12042
|
indexOf(model, fromIndex) {
|
|
11902
12043
|
return this.models.indexOf(model, fromIndex);
|
|
11903
12044
|
}
|
|
11904
12045
|
findLastIndex(pred, fromIndex) {
|
|
11905
|
-
return this.models.findLastIndex(
|
|
11906
|
-
? pred
|
|
11907
|
-
: (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
|
|
12046
|
+
return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
|
|
11908
12047
|
}
|
|
11909
12048
|
lastIndexOf(model, fromIndex) {
|
|
11910
12049
|
return this.models.lastIndexOf(model, fromIndex);
|
|
11911
12050
|
}
|
|
11912
12051
|
findIndex(pred) {
|
|
11913
|
-
return this.models.findIndex(
|
|
11914
|
-
? pred
|
|
11915
|
-
: (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
|
|
12052
|
+
return this.models.findIndex(toModelPredicate(pred));
|
|
11916
12053
|
}
|
|
11917
12054
|
last() {
|
|
11918
12055
|
const length = this.models == null ? 0 : this.models.length;
|
|
@@ -11925,9 +12062,7 @@ class Collection extends EventEmitterObject {
|
|
|
11925
12062
|
return this.head();
|
|
11926
12063
|
}
|
|
11927
12064
|
map(cb, thisArg) {
|
|
11928
|
-
return this.models.map(
|
|
11929
|
-
? cb
|
|
11930
|
-
: (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
|
|
12065
|
+
return this.models.map(toModelPredicate(cb), thisArg);
|
|
11931
12066
|
}
|
|
11932
12067
|
reduce(callback, initialValue) {
|
|
11933
12068
|
return this.models.reduce(callback, initialValue || this.models[0]);
|
|
@@ -11978,10 +12113,7 @@ class Collection extends EventEmitterObject {
|
|
|
11978
12113
|
return this.where(attrs, true);
|
|
11979
12114
|
}
|
|
11980
12115
|
find(predicate, fromIndex) {
|
|
11981
|
-
|
|
11982
|
-
? predicate
|
|
11983
|
-
: (m) => m.matches(predicate);
|
|
11984
|
-
return this.models.find(pred, fromIndex);
|
|
12116
|
+
return this.models.find(toModelPredicate(predicate), fromIndex);
|
|
11985
12117
|
}
|
|
11986
12118
|
/**
|
|
11987
12119
|
* Force the collection to re-sort itself. You don't need to call this under
|
|
@@ -12024,46 +12156,22 @@ class Collection extends EventEmitterObject {
|
|
|
12024
12156
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
12025
12157
|
const collection = this;
|
|
12026
12158
|
const promise = options.promise ? getResolveablePromise() : undefined;
|
|
12027
|
-
// Tracks whether sync invoked one of the callbacks. A sync may instead
|
|
12028
|
-
// settle its returned promise; the `.then`/`.catch` below bridges that to
|
|
12029
|
-
// the callbacks so the fetch promise never hangs. The flag is set
|
|
12030
|
-
// synchronously inside each callback (before sync's promise resolves), so
|
|
12031
|
-
// it's reliable by the time those handlers run.
|
|
12032
|
-
let settled = false;
|
|
12033
12159
|
options.success = function (resp) {
|
|
12034
|
-
settled = true;
|
|
12035
12160
|
const method = options.reset ? 'reset' : 'set';
|
|
12036
12161
|
// fromStorage prevents individual model set() calls from triggering auto-save on reads
|
|
12037
12162
|
collection[method](resp, Object.assign({}, options, { fromStorage: true }));
|
|
12038
12163
|
if (success)
|
|
12039
12164
|
success.call(options.context, collection, resp, options);
|
|
12040
|
-
|
|
12165
|
+
// Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
|
|
12166
|
+
// listener rejects the promise instead of being lost.
|
|
12041
12167
|
collection.trigger('sync', collection, resp, options);
|
|
12168
|
+
promise && promise.resolve();
|
|
12042
12169
|
};
|
|
12043
12170
|
wrapError(this, options, promise);
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
onError(resp);
|
|
12049
|
-
};
|
|
12050
|
-
}
|
|
12051
|
-
const result = this.sync('read', this, options);
|
|
12052
|
-
if (promise) {
|
|
12053
|
-
Promise.resolve(result).then(
|
|
12054
|
-
// A sync that resolves its promise without invoking options.success:
|
|
12055
|
-
// treat the resolved value as the response, so the collection is still
|
|
12056
|
-
// populated and the promise settles instead of hanging.
|
|
12057
|
-
(value) => {
|
|
12058
|
-
if (!settled)
|
|
12059
|
-
options.success(value);
|
|
12060
|
-
},
|
|
12061
|
-
// A sync that rejects instead of invoking options.error; settling
|
|
12062
|
-
// twice is a harmless no-op.
|
|
12063
|
-
(e) => promise.reject(e));
|
|
12064
|
-
return promise;
|
|
12065
|
-
}
|
|
12066
|
-
return result;
|
|
12171
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
12172
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
12173
|
+
const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
|
|
12174
|
+
return promise ?? result;
|
|
12067
12175
|
}
|
|
12068
12176
|
/**
|
|
12069
12177
|
* Create a new instance of a model in this collection. Add the model to the
|
|
@@ -12075,9 +12183,22 @@ class Collection extends EventEmitterObject {
|
|
|
12075
12183
|
const wait = options.wait;
|
|
12076
12184
|
const return_promise = options.promise;
|
|
12077
12185
|
const promise = return_promise && getResolveablePromise();
|
|
12186
|
+
// `_prepareModel` reports attributes that fail validation by firing
|
|
12187
|
+
// `'invalid'` and returning null. With `promise`, capture that error so a
|
|
12188
|
+
// rejected promise is handed back rather than `null`.
|
|
12189
|
+
let validationError;
|
|
12190
|
+
const captureInvalid = (_c, err) => (validationError = err);
|
|
12191
|
+
if (return_promise)
|
|
12192
|
+
this.on('invalid', captureInvalid);
|
|
12078
12193
|
const preparedModel = this._prepareModel(model, options);
|
|
12079
|
-
if (
|
|
12080
|
-
|
|
12194
|
+
if (return_promise)
|
|
12195
|
+
this.off('invalid', captureInvalid);
|
|
12196
|
+
if (!preparedModel) {
|
|
12197
|
+
if (!return_promise)
|
|
12198
|
+
return null;
|
|
12199
|
+
promise.reject(validationError);
|
|
12200
|
+
return promise;
|
|
12201
|
+
}
|
|
12081
12202
|
if (!wait)
|
|
12082
12203
|
this.add(preparedModel, options);
|
|
12083
12204
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
@@ -12099,8 +12220,15 @@ class Collection extends EventEmitterObject {
|
|
|
12099
12220
|
error && error.call(options.context, model, e, options);
|
|
12100
12221
|
return_promise && promise.reject(e);
|
|
12101
12222
|
};
|
|
12102
|
-
|
|
12223
|
+
// `options.promise` is passed through, so `save` installs its own bridge
|
|
12224
|
+
// from the sync's returned promise to its callbacks: a sync that resolves
|
|
12225
|
+
// without calling them still reaches the wired `options.success` above,
|
|
12226
|
+
// which resolves our promise with the model. Only a rejection that bypassed
|
|
12227
|
+
// the callbacks (a rejecting sync, or a throwing `'sync'`/`'error'`
|
|
12228
|
+
// listener) still needs bridging to ours.
|
|
12229
|
+
const result = preparedModel.save(null, options);
|
|
12103
12230
|
if (return_promise) {
|
|
12231
|
+
Promise.resolve(result).catch((e) => promise.reject(e));
|
|
12104
12232
|
return promise;
|
|
12105
12233
|
}
|
|
12106
12234
|
else {
|
|
@@ -12326,6 +12454,7 @@ const skeletor = {
|
|
|
12326
12454
|
EventEmitter,
|
|
12327
12455
|
Model,
|
|
12328
12456
|
sync,
|
|
12457
|
+
HTTPError,
|
|
12329
12458
|
};
|
|
12330
12459
|
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
|
|
12331
12460
|
// We use `self` instead of `window` for `WebWorker` support.
|
|
@@ -12353,5 +12482,5 @@ root.Skeletor = skeletor;
|
|
|
12353
12482
|
*/
|
|
12354
12483
|
const BrowserStorage = PersistentStorage;
|
|
12355
12484
|
|
|
12356
|
-
export { BrowserStorage, Collection, EventEmitter, Model, PersistentStorage, skeletor as default, getStorage, noConflict, sync };
|
|
12485
|
+
export { BrowserStorage, Collection, EventEmitter, HTTPError, Model, PersistentStorage, skeletor as default, getStorage, noConflict, sync };
|
|
12357
12486
|
//# sourceMappingURL=skeletor.esm.js.map
|