@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.node.js
CHANGED
|
@@ -7796,6 +7796,16 @@ function guid() {
|
|
|
7796
7796
|
// Generate a pseudo-GUID by concatenating random hexadecimal.
|
|
7797
7797
|
return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
|
|
7798
7798
|
}
|
|
7799
|
+
// Helpers
|
|
7800
|
+
// -------
|
|
7801
|
+
/**
|
|
7802
|
+
* Whether `value` is a thenable (a promise, or a promise-like object such as
|
|
7803
|
+
* a jqXHR). Decides whether a `sync` return value can be bridged to the
|
|
7804
|
+
* promise that `fetch`/`save` hand back.
|
|
7805
|
+
*/
|
|
7806
|
+
function isThenable(value) {
|
|
7807
|
+
return value != null && typeof value.then === 'function';
|
|
7808
|
+
}
|
|
7799
7809
|
function getResolveablePromise() {
|
|
7800
7810
|
const wrapper = {
|
|
7801
7811
|
isResolved: false,
|
|
@@ -7840,6 +7850,65 @@ function wrapError(model, options, promise) {
|
|
|
7840
7850
|
promise && promise.reject && promise.reject(resp);
|
|
7841
7851
|
};
|
|
7842
7852
|
}
|
|
7853
|
+
/**
|
|
7854
|
+
* Run a `sync` and make sure `promise` settles from its outcome, however the
|
|
7855
|
+
* sync reports it.
|
|
7856
|
+
*
|
|
7857
|
+
* A `sync` may invoke `options.success`/`options.error` (the callback
|
|
7858
|
+
* contract), or settle the promise it returns without calling them: a
|
|
7859
|
+
* promise-only custom sync, or an async sync whose promise rejects because a
|
|
7860
|
+
* `'sync'`/`'error'` listener threw after the callbacks ran. `options.success`
|
|
7861
|
+
* and `options.error` must already be wired to resolve/reject `promise`. This
|
|
7862
|
+
* wraps them to record that they ran, calls `run()`, and bridges a thenable
|
|
7863
|
+
* result to them so the promise never hangs:
|
|
7864
|
+
*
|
|
7865
|
+
* - fulfilment with the callbacks unrun is delivered to `options.success`, so
|
|
7866
|
+
* the response is still merged into the model or collection;
|
|
7867
|
+
* - rejection with the callbacks unrun is delivered to `options.error`, so the
|
|
7868
|
+
* caller's `error` callback and the `'error'` event still fire;
|
|
7869
|
+
* - rejection after the callbacks ran rejects `promise` directly: a no-op if
|
|
7870
|
+
* they already settled it, and the only way to settle it if a listener threw
|
|
7871
|
+
* before they could;
|
|
7872
|
+
* - a throw from either delivery (a throwing `success`/`error` callback or
|
|
7873
|
+
* listener) rejects `promise` instead of surfacing as an unhandled rejection.
|
|
7874
|
+
*
|
|
7875
|
+
* A non-thenable result (a callback-only sync that returns nothing, or an
|
|
7876
|
+
* XHR) is left alone: such a sync settles `promise` through its callbacks, and
|
|
7877
|
+
* bridging it would fire `success` on the next microtask, before the sync is
|
|
7878
|
+
* done. Without a `promise`, `run()` is called as is.
|
|
7879
|
+
*/
|
|
7880
|
+
function bridgeSyncToPromise(options, promise, run) {
|
|
7881
|
+
if (!promise)
|
|
7882
|
+
return run();
|
|
7883
|
+
let settled = false;
|
|
7884
|
+
const success = options.success;
|
|
7885
|
+
const error = options.error;
|
|
7886
|
+
options.success = (...args) => {
|
|
7887
|
+
settled = true;
|
|
7888
|
+
return success && success(...args);
|
|
7889
|
+
};
|
|
7890
|
+
options.error = (...args) => {
|
|
7891
|
+
settled = true;
|
|
7892
|
+
return error && error(...args);
|
|
7893
|
+
};
|
|
7894
|
+
const result = run();
|
|
7895
|
+
if (isThenable(result)) {
|
|
7896
|
+
Promise.resolve(result)
|
|
7897
|
+
.then((value) => {
|
|
7898
|
+
if (!settled)
|
|
7899
|
+
options.success(value);
|
|
7900
|
+
}, (e) => {
|
|
7901
|
+
if (settled) {
|
|
7902
|
+
promise.reject(e);
|
|
7903
|
+
}
|
|
7904
|
+
else {
|
|
7905
|
+
options.error(e);
|
|
7906
|
+
}
|
|
7907
|
+
})
|
|
7908
|
+
.catch((e) => promise.reject(e));
|
|
7909
|
+
}
|
|
7910
|
+
return result;
|
|
7911
|
+
}
|
|
7843
7912
|
// Map from CRUD to HTTP for our default `sync` implementation.
|
|
7844
7913
|
const methodMap = {
|
|
7845
7914
|
create: 'POST',
|
|
@@ -7889,43 +7958,77 @@ function getSyncMethod(model) {
|
|
|
7889
7958
|
return store ? store.sync() : sync;
|
|
7890
7959
|
}
|
|
7891
7960
|
/**
|
|
7892
|
-
*
|
|
7893
|
-
*
|
|
7894
|
-
*
|
|
7895
|
-
* to the model's `url()`.
|
|
7896
|
-
*
|
|
7897
|
-
* Some possible customizations could be:
|
|
7898
|
-
*
|
|
7899
|
-
* - Use `setTimeout` to batch rapid-fire updates into a single request.
|
|
7900
|
-
* - Persist models via WebSockets instead of Ajax.
|
|
7901
|
-
* - Persist models to browser storage
|
|
7961
|
+
* The error the default `sync` hands to `options.error`, and rejects with, for
|
|
7962
|
+
* a response outside the 2xx range.
|
|
7963
|
+
* @public
|
|
7902
7964
|
*/
|
|
7965
|
+
class HTTPError extends Error {
|
|
7966
|
+
constructor(response, method, url) {
|
|
7967
|
+
const statusText = response.statusText ? ` ${response.statusText}` : '';
|
|
7968
|
+
super(`${method} ${url} failed with HTTP ${response.status}${statusText}`);
|
|
7969
|
+
this.name = 'HTTPError';
|
|
7970
|
+
this.status = response.status;
|
|
7971
|
+
this.response = response;
|
|
7972
|
+
}
|
|
7973
|
+
}
|
|
7903
7974
|
/**
|
|
7975
|
+
* The default `sync`: a JSON request over `fetch()` to the model's `url()`.
|
|
7976
|
+
* Override it (or `Model.sync`/`Collection.sync`) to change how models are
|
|
7977
|
+
* persisted, for instance to batch rapid-fire updates with `setTimeout`, to
|
|
7978
|
+
* talk over WebSockets, or to write to browser storage.
|
|
7979
|
+
*
|
|
7980
|
+
* It honours both contracts a `sync` may use. On a 2xx response the JSON body
|
|
7981
|
+
* (`null` when the body is empty) is passed to `options.success` and the
|
|
7982
|
+
* returned promise resolves with it. On a non-2xx response, a network failure
|
|
7983
|
+
* or an unparsable body, `options.error` receives the error (an `HTTPError`
|
|
7984
|
+
* for the first) and the promise rejects with it.
|
|
7985
|
+
*
|
|
7986
|
+
* `options.data` is sent as query parameters on GET, which may not carry a
|
|
7987
|
+
* body, and as a JSON body otherwise. For create, update and patch the body
|
|
7988
|
+
* defaults to `options.attrs` or the model's `toJSON()`.
|
|
7904
7989
|
* @public
|
|
7905
7990
|
*/
|
|
7906
7991
|
function sync(method, model, options = {}) {
|
|
7992
|
+
const type = methodMap[method];
|
|
7907
7993
|
let data = options.data;
|
|
7908
7994
|
// Ensure that we have the appropriate request data.
|
|
7909
7995
|
if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
|
7910
7996
|
data = options.attrs || model.toJSON();
|
|
7911
7997
|
}
|
|
7912
|
-
|
|
7913
|
-
const params = {
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
const url = options.url || result(model, 'url') || urlError();
|
|
7923
|
-
const xhr = fetch(url, params);
|
|
7924
|
-
if (options) {
|
|
7925
|
-
options.xhr = xhr;
|
|
7998
|
+
let url = options.url || result(model, 'url') || urlError();
|
|
7999
|
+
const params = { method: type };
|
|
8000
|
+
if (data != null) {
|
|
8001
|
+
if (type === 'GET') {
|
|
8002
|
+
url += (url.includes('?') ? '&' : '?') + new URLSearchParams(data).toString();
|
|
8003
|
+
}
|
|
8004
|
+
else {
|
|
8005
|
+
params.body = JSON.stringify(data);
|
|
8006
|
+
params.headers = { 'Content-Type': 'application/json' };
|
|
8007
|
+
}
|
|
7926
8008
|
}
|
|
8009
|
+
const xhr = fetch(url, params);
|
|
8010
|
+
options.xhr = xhr;
|
|
7927
8011
|
model.trigger('request', model, xhr, { ...params, xhr });
|
|
7928
|
-
return xhr
|
|
8012
|
+
return Promise.resolve(xhr)
|
|
8013
|
+
.then(async (response) => {
|
|
8014
|
+
if (!response.ok)
|
|
8015
|
+
throw new HTTPError(response, type, url);
|
|
8016
|
+
const text = await response.text();
|
|
8017
|
+
return text ? JSON.parse(text) : null;
|
|
8018
|
+
})
|
|
8019
|
+
.then(
|
|
8020
|
+
// The callbacks run before the returned promise settles, so a caller that
|
|
8021
|
+
// bridges this promise (`Model.fetch`/`save`) sees the outcome as already
|
|
8022
|
+
// reported. A throw from `success` (a `'sync'` listener, say) is not
|
|
8023
|
+
// routed to `error`: it rejects the promise, which is what the bridge
|
|
8024
|
+
// expects once the callbacks have run.
|
|
8025
|
+
(resp) => {
|
|
8026
|
+
options.success?.(resp, options);
|
|
8027
|
+
return resp;
|
|
8028
|
+
}, (e) => {
|
|
8029
|
+
options.error?.(e);
|
|
8030
|
+
throw e;
|
|
8031
|
+
});
|
|
7929
8032
|
}
|
|
7930
8033
|
|
|
7931
8034
|
/**
|
|
@@ -8024,7 +8127,7 @@ class PersistentStorage {
|
|
|
8024
8127
|
throw new Error("Skeletor.storage: the 'node' storage type is only available in Node.js builds. " +
|
|
8025
8128
|
"Import from '@converse/skeletor/node' instead of '@converse/skeletor'.");
|
|
8026
8129
|
}
|
|
8027
|
-
this.store = new NodeSQLiteStorage(this.name);
|
|
8130
|
+
this.store = new NodeSQLiteStorage(this.name, PersistentStorage.nodeStorageDir ?? undefined);
|
|
8028
8131
|
if (batchedWrites) {
|
|
8029
8132
|
this.store.debouncedSetItems = mergebounce((items) => this.store.setItems(items), 50, {
|
|
8030
8133
|
'promise': true,
|
|
@@ -8228,6 +8331,18 @@ class PersistentStorage {
|
|
|
8228
8331
|
// node-only driver (and its `node:sqlite` import) never enters the browser
|
|
8229
8332
|
// bundle; the `@converse/skeletor/node` entry sets it.
|
|
8230
8333
|
PersistentStorage.nodeStorage = null;
|
|
8334
|
+
/**
|
|
8335
|
+
* Directory the `'node'` store type writes its database files to. The driver's
|
|
8336
|
+
* own default is relative, so it resolves against `process.cwd()` and a process
|
|
8337
|
+
* started from two directories ends up with two separate sets of state. Set an
|
|
8338
|
+
* absolute path (`$XDG_STATE_HOME/<app>`, say) to pin it. Left `null`, the
|
|
8339
|
+
* driver's default applies.
|
|
8340
|
+
*
|
|
8341
|
+
* Only the `'node'` string store type reads this; passing a driver instance to
|
|
8342
|
+
* the constructor means you've already chosen a directory yourself.
|
|
8343
|
+
* @public
|
|
8344
|
+
*/
|
|
8345
|
+
PersistentStorage.nodeStorageDir = null;
|
|
8231
8346
|
PersistentStorage.sessionStorageInitialized = localForage.defineDriver(sessionStorageWrapper);
|
|
8232
8347
|
PersistentStorage.localForage = localForage;
|
|
8233
8348
|
|
|
@@ -8235,6 +8350,7 @@ const DEFAULT_BASE_DIR = '.skeletor-storage';
|
|
|
8235
8350
|
/**
|
|
8236
8351
|
* SQLite-backed implementation of StorageDriver.
|
|
8237
8352
|
* Uses a single table with key/value columns, storing values as JSON strings.
|
|
8353
|
+
* @public
|
|
8238
8354
|
*/
|
|
8239
8355
|
class NodeSQLiteStorage {
|
|
8240
8356
|
ready() {
|
|
@@ -10106,6 +10222,48 @@ var keyBy = createAggregator(function(result, value, key) {
|
|
|
10106
10222
|
baseAssignValue(result, key, value);
|
|
10107
10223
|
});
|
|
10108
10224
|
|
|
10225
|
+
/** Used to compose bitmasks for cloning. */
|
|
10226
|
+
var CLONE_DEEP_FLAG$1 = 1;
|
|
10227
|
+
|
|
10228
|
+
/**
|
|
10229
|
+
* Creates a function that performs a partial deep comparison between a given
|
|
10230
|
+
* object and `source`, returning `true` if the given object has equivalent
|
|
10231
|
+
* property values, else `false`.
|
|
10232
|
+
*
|
|
10233
|
+
* **Note:** The created function is equivalent to `_.isMatch` with `source`
|
|
10234
|
+
* partially applied.
|
|
10235
|
+
*
|
|
10236
|
+
* Partial comparisons will match empty array and empty object `source`
|
|
10237
|
+
* values against any array or object value, respectively. See `_.isEqual`
|
|
10238
|
+
* for a list of supported value comparisons.
|
|
10239
|
+
*
|
|
10240
|
+
* **Note:** Multiple values can be checked by combining several matchers
|
|
10241
|
+
* using `_.overSome`
|
|
10242
|
+
*
|
|
10243
|
+
* @static
|
|
10244
|
+
* @memberOf _
|
|
10245
|
+
* @since 3.0.0
|
|
10246
|
+
* @category Util
|
|
10247
|
+
* @param {Object} source The object of property values to match.
|
|
10248
|
+
* @returns {Function} Returns the new spec function.
|
|
10249
|
+
* @example
|
|
10250
|
+
*
|
|
10251
|
+
* var objects = [
|
|
10252
|
+
* { 'a': 1, 'b': 2, 'c': 3 },
|
|
10253
|
+
* { 'a': 4, 'b': 5, 'c': 6 }
|
|
10254
|
+
* ];
|
|
10255
|
+
*
|
|
10256
|
+
* _.filter(objects, _.matches({ 'a': 4, 'c': 6 }));
|
|
10257
|
+
* // => [{ 'a': 4, 'b': 5, 'c': 6 }]
|
|
10258
|
+
*
|
|
10259
|
+
* // Checking for several possible values
|
|
10260
|
+
* _.filter(objects, _.overSome([_.matches({ 'a': 1 }), _.matches({ 'a': 4 })]));
|
|
10261
|
+
* // => [{ 'a': 1, 'b': 2, 'c': 3 }, { 'a': 4, 'b': 5, 'c': 6 }]
|
|
10262
|
+
*/
|
|
10263
|
+
function matches(source) {
|
|
10264
|
+
return baseMatches(baseClone(source, CLONE_DEEP_FLAG$1));
|
|
10265
|
+
}
|
|
10266
|
+
|
|
10109
10267
|
/** Built-in value references. */
|
|
10110
10268
|
var spreadableSymbol = Symbol$1 ? Symbol$1.isConcatSpreadable : undefined;
|
|
10111
10269
|
|
|
@@ -10669,53 +10827,36 @@ function isEqual(value, other) {
|
|
|
10669
10827
|
return baseIsEqual(value, other);
|
|
10670
10828
|
}
|
|
10671
10829
|
|
|
10672
|
-
/** Used to compose bitmasks for cloning. */
|
|
10673
|
-
var CLONE_DEEP_FLAG$1 = 1;
|
|
10674
|
-
|
|
10675
10830
|
/**
|
|
10676
|
-
*
|
|
10677
|
-
*
|
|
10678
|
-
*
|
|
10679
|
-
*
|
|
10680
|
-
*
|
|
10831
|
+
* Performs a partial deep comparison between `object` and `source` to
|
|
10832
|
+
* determine if `object` contains equivalent property values.
|
|
10833
|
+
*
|
|
10834
|
+
* **Note:** This method is equivalent to `_.matches` when `source` is
|
|
10835
|
+
* partially applied.
|
|
10836
|
+
*
|
|
10837
|
+
* Partial comparisons will match empty array and empty object `source`
|
|
10838
|
+
* values against any array or object value, respectively. See `_.isEqual`
|
|
10839
|
+
* for a list of supported value comparisons.
|
|
10681
10840
|
*
|
|
10682
10841
|
* @static
|
|
10683
|
-
* @since 4.0.0
|
|
10684
10842
|
* @memberOf _
|
|
10685
|
-
* @
|
|
10686
|
-
* @
|
|
10687
|
-
* @
|
|
10843
|
+
* @since 3.0.0
|
|
10844
|
+
* @category Lang
|
|
10845
|
+
* @param {Object} object The object to inspect.
|
|
10846
|
+
* @param {Object} source The object of property values to match.
|
|
10847
|
+
* @returns {boolean} Returns `true` if `object` is a match, else `false`.
|
|
10688
10848
|
* @example
|
|
10689
10849
|
*
|
|
10690
|
-
* var
|
|
10691
|
-
* { 'user': 'barney', 'age': 36, 'active': true },
|
|
10692
|
-
* { 'user': 'fred', 'age': 40, 'active': false }
|
|
10693
|
-
* ];
|
|
10694
|
-
*
|
|
10695
|
-
* // The `_.matches` iteratee shorthand.
|
|
10696
|
-
* _.filter(users, _.iteratee({ 'user': 'barney', 'active': true }));
|
|
10697
|
-
* // => [{ 'user': 'barney', 'age': 36, 'active': true }]
|
|
10698
|
-
*
|
|
10699
|
-
* // The `_.matchesProperty` iteratee shorthand.
|
|
10700
|
-
* _.filter(users, _.iteratee(['user', 'fred']));
|
|
10701
|
-
* // => [{ 'user': 'fred', 'age': 40 }]
|
|
10850
|
+
* var object = { 'a': 1, 'b': 2 };
|
|
10702
10851
|
*
|
|
10703
|
-
*
|
|
10704
|
-
*
|
|
10705
|
-
* // => ['barney', 'fred']
|
|
10706
|
-
*
|
|
10707
|
-
* // Create custom iteratee shorthands.
|
|
10708
|
-
* _.iteratee = _.wrap(_.iteratee, function(iteratee, func) {
|
|
10709
|
-
* return !_.isRegExp(func) ? iteratee(func) : function(string) {
|
|
10710
|
-
* return func.test(string);
|
|
10711
|
-
* };
|
|
10712
|
-
* });
|
|
10852
|
+
* _.isMatch(object, { 'b': 2 });
|
|
10853
|
+
* // => true
|
|
10713
10854
|
*
|
|
10714
|
-
* _.
|
|
10715
|
-
* // =>
|
|
10855
|
+
* _.isMatch(object, { 'b': 1 });
|
|
10856
|
+
* // => false
|
|
10716
10857
|
*/
|
|
10717
|
-
function
|
|
10718
|
-
return
|
|
10858
|
+
function isMatch(object, source) {
|
|
10859
|
+
return object === source || baseIsMatch(object, source, getMatchData(source));
|
|
10719
10860
|
}
|
|
10720
10861
|
|
|
10721
10862
|
/**
|
|
@@ -11229,10 +11370,20 @@ class Model extends EventEmitterObject {
|
|
|
11229
11370
|
return this.get(attr) != null;
|
|
11230
11371
|
}
|
|
11231
11372
|
/**
|
|
11232
|
-
*
|
|
11373
|
+
* Returns `true` if the model's attributes satisfy `attrs`.
|
|
11374
|
+
*
|
|
11375
|
+
* An attributes object is compared with lodash's `isMatch`: a partial, deep
|
|
11376
|
+
* comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
|
|
11377
|
+
* that has `type: 'hidden'` alongside other keys. A function is called with
|
|
11378
|
+
* the attributes and its result coerced to a boolean.
|
|
11379
|
+
*
|
|
11380
|
+
* To scan a collection, prefer `Collection#find`, `filter` or `where`. They
|
|
11381
|
+
* build their matcher once per scan instead of once per model.
|
|
11233
11382
|
*/
|
|
11234
11383
|
matches(attrs) {
|
|
11235
|
-
|
|
11384
|
+
if (typeof attrs === 'function')
|
|
11385
|
+
return !!attrs(this.attributes);
|
|
11386
|
+
return isMatch(this.attributes, attrs);
|
|
11236
11387
|
}
|
|
11237
11388
|
/**
|
|
11238
11389
|
* Set a hash of model attributes on the object, firing `"change"`. This is
|
|
@@ -11405,47 +11556,25 @@ class Model extends EventEmitterObject {
|
|
|
11405
11556
|
options = Object.assign({ parse: true }, options);
|
|
11406
11557
|
const success = options.success;
|
|
11407
11558
|
const promise = options.promise ? getResolveablePromise() : undefined;
|
|
11408
|
-
// Tracks whether sync invoked one of the callbacks. A sync may instead
|
|
11409
|
-
// settle its returned promise; the `.then`/`.catch` below bridges that to
|
|
11410
|
-
// the callbacks so the fetch promise never hangs. The flag is set
|
|
11411
|
-
// synchronously inside each callback (before sync's promise resolves), so
|
|
11412
|
-
// it's reliable by the time those handlers run.
|
|
11413
|
-
let settled = false;
|
|
11414
11559
|
options.success = (resp) => {
|
|
11415
|
-
settled = true;
|
|
11416
11560
|
const serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
11417
11561
|
// fromStorage prevents the set() from triggering auto-save on reads
|
|
11418
|
-
if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
|
|
11562
|
+
if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
|
|
11563
|
+
// The fetched attributes failed validation (`'invalid'` has fired).
|
|
11564
|
+
// Settle the promise with the error rather than leave it pending.
|
|
11565
|
+
promise && promise.reject(this.validationError);
|
|
11419
11566
|
return false;
|
|
11567
|
+
}
|
|
11420
11568
|
if (success)
|
|
11421
11569
|
success.call(options.context, this, resp, options);
|
|
11422
11570
|
this.trigger('sync', this, resp, options);
|
|
11423
11571
|
promise && promise.resolve(resp);
|
|
11424
11572
|
};
|
|
11425
11573
|
wrapError(this, options, promise);
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
onError(resp);
|
|
11431
|
-
};
|
|
11432
|
-
}
|
|
11433
|
-
const result = this.sync('read', this, options);
|
|
11434
|
-
if (promise) {
|
|
11435
|
-
Promise.resolve(result).then(
|
|
11436
|
-
// A sync that resolves its promise without invoking options.success:
|
|
11437
|
-
// treat the resolved value as the response, so the model is still
|
|
11438
|
-
// merged and the promise settles instead of hanging.
|
|
11439
|
-
(value) => {
|
|
11440
|
-
if (!settled)
|
|
11441
|
-
options.success(value);
|
|
11442
|
-
},
|
|
11443
|
-
// A sync that rejects instead of invoking options.error; settling
|
|
11444
|
-
// twice is a harmless no-op.
|
|
11445
|
-
(e) => promise.reject(e));
|
|
11446
|
-
return promise;
|
|
11447
|
-
}
|
|
11448
|
-
return result;
|
|
11574
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
11575
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
11576
|
+
const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
|
|
11577
|
+
return promise ?? result;
|
|
11449
11578
|
}
|
|
11450
11579
|
/**
|
|
11451
11580
|
* Set a hash of model attributes, and sync the model to the server.
|
|
@@ -11466,16 +11595,19 @@ class Model extends EventEmitterObject {
|
|
|
11466
11595
|
options = Object.assign({ validate: true, parse: true }, options);
|
|
11467
11596
|
const wait = options.wait;
|
|
11468
11597
|
const return_promise = options.promise;
|
|
11469
|
-
const promise = return_promise
|
|
11598
|
+
const promise = return_promise ? getResolveablePromise() : undefined;
|
|
11470
11599
|
// If we're not waiting and attributes exist, save acts as
|
|
11471
11600
|
// `set(attr).save(null, opts)` with validation. Otherwise, check if
|
|
11472
11601
|
// the model will be valid when the attributes, if any, are set.
|
|
11473
|
-
|
|
11474
|
-
|
|
11602
|
+
const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
|
|
11603
|
+
if (!valid) {
|
|
11604
|
+
// Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
|
|
11605
|
+
// rejected promise rather than `false`, so the promise contract holds
|
|
11606
|
+
// for every outcome.
|
|
11607
|
+
if (!promise)
|
|
11475
11608
|
return false;
|
|
11476
|
-
|
|
11477
|
-
|
|
11478
|
-
return false;
|
|
11609
|
+
promise.reject(this.validationError);
|
|
11610
|
+
return promise;
|
|
11479
11611
|
}
|
|
11480
11612
|
// After a successful server-side save, the client is (optionally)
|
|
11481
11613
|
// updated with the server-side state.
|
|
@@ -11488,8 +11620,12 @@ class Model extends EventEmitterObject {
|
|
|
11488
11620
|
let serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
11489
11621
|
if (wait)
|
|
11490
11622
|
serverAttrs = Object.assign({}, attrs, serverAttrs);
|
|
11491
|
-
if (serverAttrs && !this.set(serverAttrs, options))
|
|
11623
|
+
if (serverAttrs && !this.set(serverAttrs, options)) {
|
|
11624
|
+
// The server attributes failed validation (`'invalid'` has fired).
|
|
11625
|
+
// Settle the promise with the error rather than leave it pending.
|
|
11626
|
+
return_promise && promise.reject(this.validationError);
|
|
11492
11627
|
return false;
|
|
11628
|
+
}
|
|
11493
11629
|
if (success)
|
|
11494
11630
|
success.call(options.context, this, resp, options);
|
|
11495
11631
|
this.trigger('sync', this, resp, options);
|
|
@@ -11506,15 +11642,12 @@ class Model extends EventEmitterObject {
|
|
|
11506
11642
|
const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
|
|
11507
11643
|
if (method === 'patch' && !options.attrs)
|
|
11508
11644
|
options.attrs = attrs;
|
|
11509
|
-
|
|
11645
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
11646
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
11647
|
+
const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
|
|
11510
11648
|
// Restore attributes.
|
|
11511
11649
|
this.attributes = attributes;
|
|
11512
|
-
|
|
11513
|
-
return promise;
|
|
11514
|
-
}
|
|
11515
|
-
else {
|
|
11516
|
-
return xhr;
|
|
11517
|
-
}
|
|
11650
|
+
return promise ?? xhr;
|
|
11518
11651
|
}
|
|
11519
11652
|
/**
|
|
11520
11653
|
* Destroy this model on the server if it was already persisted.
|
|
@@ -11661,6 +11794,23 @@ var _Collection_instances, _Collection_hydrate, _a;
|
|
|
11661
11794
|
// Default options for `Collection#set`.
|
|
11662
11795
|
const setOptions = { add: true, remove: true, merge: true };
|
|
11663
11796
|
const addOptions = { add: true, remove: false };
|
|
11797
|
+
/**
|
|
11798
|
+
* Normalizes the predicate forms accepted by the query methods into one
|
|
11799
|
+
* `(model) => value` function. A function is used as-is, a string reads that
|
|
11800
|
+
* attribute, and an attributes object becomes a lodash `matches` matcher.
|
|
11801
|
+
*
|
|
11802
|
+
* The matcher is built here, once per scan. Calling `Model#matches` per model
|
|
11803
|
+
* instead rebuilds the match data for every model visited, which made
|
|
11804
|
+
* `findWhere` an order of magnitude slower than a hand-written predicate.
|
|
11805
|
+
*/
|
|
11806
|
+
function toModelPredicate(pred) {
|
|
11807
|
+
if (isFunction(pred))
|
|
11808
|
+
return pred;
|
|
11809
|
+
if (isString(pred))
|
|
11810
|
+
return (m) => m.get(pred);
|
|
11811
|
+
const matcher = matches(pred);
|
|
11812
|
+
return (m) => matcher(m.attributes);
|
|
11813
|
+
}
|
|
11664
11814
|
/**
|
|
11665
11815
|
* @public
|
|
11666
11816
|
* If models tend to represent a single row of data, a Collection is
|
|
@@ -11989,17 +12139,13 @@ class Collection extends EventEmitterObject {
|
|
|
11989
12139
|
return this.models.slice(start, end);
|
|
11990
12140
|
}
|
|
11991
12141
|
filter(callback, thisArg) {
|
|
11992
|
-
return this.models.filter(
|
|
11993
|
-
? callback
|
|
11994
|
-
: (m) => m.matches(callback), thisArg);
|
|
12142
|
+
return this.models.filter(toModelPredicate(callback), thisArg);
|
|
11995
12143
|
}
|
|
11996
12144
|
every(pred) {
|
|
11997
12145
|
if (isFunction(pred)) {
|
|
11998
|
-
return this.models.
|
|
11999
|
-
}
|
|
12000
|
-
else {
|
|
12001
|
-
return this.models.every((m) => m.matches(pred));
|
|
12146
|
+
return this.models.every((m) => pred(m.attributes));
|
|
12002
12147
|
}
|
|
12148
|
+
return this.models.every(toModelPredicate(pred));
|
|
12003
12149
|
}
|
|
12004
12150
|
difference(values) {
|
|
12005
12151
|
return this.models.filter((m) => !values.includes(m));
|
|
@@ -12015,16 +12161,12 @@ class Collection extends EventEmitterObject {
|
|
|
12015
12161
|
}
|
|
12016
12162
|
some(pred) {
|
|
12017
12163
|
if (isFunction(pred)) {
|
|
12018
|
-
return this.models.
|
|
12019
|
-
}
|
|
12020
|
-
else {
|
|
12021
|
-
return this.models.some((m) => m.matches(pred));
|
|
12164
|
+
return this.models.some((m) => pred(m.attributes));
|
|
12022
12165
|
}
|
|
12166
|
+
return this.models.some(toModelPredicate(pred));
|
|
12023
12167
|
}
|
|
12024
12168
|
sortBy(iteratee) {
|
|
12025
|
-
return sortBy(this.models,
|
|
12026
|
-
? iteratee
|
|
12027
|
-
: (m) => (isString(iteratee) ? m.get(iteratee) : m.matches(iteratee)));
|
|
12169
|
+
return sortBy(this.models, toModelPredicate(iteratee));
|
|
12028
12170
|
}
|
|
12029
12171
|
isEmpty() {
|
|
12030
12172
|
return !this.models.length;
|
|
@@ -12045,26 +12187,22 @@ class Collection extends EventEmitterObject {
|
|
|
12045
12187
|
return this.models.length;
|
|
12046
12188
|
}
|
|
12047
12189
|
countBy(f) {
|
|
12048
|
-
return countBy(this.models,
|
|
12190
|
+
return countBy(this.models, toModelPredicate(f));
|
|
12049
12191
|
}
|
|
12050
12192
|
groupBy(pred) {
|
|
12051
|
-
return groupBy(this.models,
|
|
12193
|
+
return groupBy(this.models, toModelPredicate(pred));
|
|
12052
12194
|
}
|
|
12053
12195
|
indexOf(model, fromIndex) {
|
|
12054
12196
|
return this.models.indexOf(model, fromIndex);
|
|
12055
12197
|
}
|
|
12056
12198
|
findLastIndex(pred, fromIndex) {
|
|
12057
|
-
return this.models.findLastIndex(
|
|
12058
|
-
? pred
|
|
12059
|
-
: (m) => (isString(pred) ? m.get(pred) : m.matches(pred)), fromIndex);
|
|
12199
|
+
return this.models.findLastIndex(toModelPredicate(pred), fromIndex);
|
|
12060
12200
|
}
|
|
12061
12201
|
lastIndexOf(model, fromIndex) {
|
|
12062
12202
|
return this.models.lastIndexOf(model, fromIndex);
|
|
12063
12203
|
}
|
|
12064
12204
|
findIndex(pred) {
|
|
12065
|
-
return this.models.findIndex(
|
|
12066
|
-
? pred
|
|
12067
|
-
: (m) => (isString(pred) ? m.get(pred) : m.matches(pred)));
|
|
12205
|
+
return this.models.findIndex(toModelPredicate(pred));
|
|
12068
12206
|
}
|
|
12069
12207
|
last() {
|
|
12070
12208
|
const length = this.models == null ? 0 : this.models.length;
|
|
@@ -12077,9 +12215,7 @@ class Collection extends EventEmitterObject {
|
|
|
12077
12215
|
return this.head();
|
|
12078
12216
|
}
|
|
12079
12217
|
map(cb, thisArg) {
|
|
12080
|
-
return this.models.map(
|
|
12081
|
-
? cb
|
|
12082
|
-
: (m) => (isString(cb) ? m.get(cb) : m.matches(cb)), thisArg);
|
|
12218
|
+
return this.models.map(toModelPredicate(cb), thisArg);
|
|
12083
12219
|
}
|
|
12084
12220
|
reduce(callback, initialValue) {
|
|
12085
12221
|
return this.models.reduce(callback, initialValue || this.models[0]);
|
|
@@ -12130,10 +12266,7 @@ class Collection extends EventEmitterObject {
|
|
|
12130
12266
|
return this.where(attrs, true);
|
|
12131
12267
|
}
|
|
12132
12268
|
find(predicate, fromIndex) {
|
|
12133
|
-
|
|
12134
|
-
? predicate
|
|
12135
|
-
: (m) => m.matches(predicate);
|
|
12136
|
-
return this.models.find(pred, fromIndex);
|
|
12269
|
+
return this.models.find(toModelPredicate(predicate), fromIndex);
|
|
12137
12270
|
}
|
|
12138
12271
|
/**
|
|
12139
12272
|
* Force the collection to re-sort itself. You don't need to call this under
|
|
@@ -12176,46 +12309,22 @@ class Collection extends EventEmitterObject {
|
|
|
12176
12309
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
12177
12310
|
const collection = this;
|
|
12178
12311
|
const promise = options.promise ? getResolveablePromise() : undefined;
|
|
12179
|
-
// Tracks whether sync invoked one of the callbacks. A sync may instead
|
|
12180
|
-
// settle its returned promise; the `.then`/`.catch` below bridges that to
|
|
12181
|
-
// the callbacks so the fetch promise never hangs. The flag is set
|
|
12182
|
-
// synchronously inside each callback (before sync's promise resolves), so
|
|
12183
|
-
// it's reliable by the time those handlers run.
|
|
12184
|
-
let settled = false;
|
|
12185
12312
|
options.success = function (resp) {
|
|
12186
|
-
settled = true;
|
|
12187
12313
|
const method = options.reset ? 'reset' : 'set';
|
|
12188
12314
|
// fromStorage prevents individual model set() calls from triggering auto-save on reads
|
|
12189
12315
|
collection[method](resp, Object.assign({}, options, { fromStorage: true }));
|
|
12190
12316
|
if (success)
|
|
12191
12317
|
success.call(options.context, collection, resp, options);
|
|
12192
|
-
|
|
12318
|
+
// Fire 'sync' before resolving, as `Model.fetch`/`save` do, so a throwing
|
|
12319
|
+
// listener rejects the promise instead of being lost.
|
|
12193
12320
|
collection.trigger('sync', collection, resp, options);
|
|
12321
|
+
promise && promise.resolve();
|
|
12194
12322
|
};
|
|
12195
12323
|
wrapError(this, options, promise);
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
onError(resp);
|
|
12201
|
-
};
|
|
12202
|
-
}
|
|
12203
|
-
const result = this.sync('read', this, options);
|
|
12204
|
-
if (promise) {
|
|
12205
|
-
Promise.resolve(result).then(
|
|
12206
|
-
// A sync that resolves its promise without invoking options.success:
|
|
12207
|
-
// treat the resolved value as the response, so the collection is still
|
|
12208
|
-
// populated and the promise settles instead of hanging.
|
|
12209
|
-
(value) => {
|
|
12210
|
-
if (!settled)
|
|
12211
|
-
options.success(value);
|
|
12212
|
-
},
|
|
12213
|
-
// A sync that rejects instead of invoking options.error; settling
|
|
12214
|
-
// twice is a harmless no-op.
|
|
12215
|
-
(e) => promise.reject(e));
|
|
12216
|
-
return promise;
|
|
12217
|
-
}
|
|
12218
|
-
return result;
|
|
12324
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
12325
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
12326
|
+
const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
|
|
12327
|
+
return promise ?? result;
|
|
12219
12328
|
}
|
|
12220
12329
|
/**
|
|
12221
12330
|
* Create a new instance of a model in this collection. Add the model to the
|
|
@@ -12227,9 +12336,22 @@ class Collection extends EventEmitterObject {
|
|
|
12227
12336
|
const wait = options.wait;
|
|
12228
12337
|
const return_promise = options.promise;
|
|
12229
12338
|
const promise = return_promise && getResolveablePromise();
|
|
12339
|
+
// `_prepareModel` reports attributes that fail validation by firing
|
|
12340
|
+
// `'invalid'` and returning null. With `promise`, capture that error so a
|
|
12341
|
+
// rejected promise is handed back rather than `null`.
|
|
12342
|
+
let validationError;
|
|
12343
|
+
const captureInvalid = (_c, err) => (validationError = err);
|
|
12344
|
+
if (return_promise)
|
|
12345
|
+
this.on('invalid', captureInvalid);
|
|
12230
12346
|
const preparedModel = this._prepareModel(model, options);
|
|
12231
|
-
if (
|
|
12232
|
-
|
|
12347
|
+
if (return_promise)
|
|
12348
|
+
this.off('invalid', captureInvalid);
|
|
12349
|
+
if (!preparedModel) {
|
|
12350
|
+
if (!return_promise)
|
|
12351
|
+
return null;
|
|
12352
|
+
promise.reject(validationError);
|
|
12353
|
+
return promise;
|
|
12354
|
+
}
|
|
12233
12355
|
if (!wait)
|
|
12234
12356
|
this.add(preparedModel, options);
|
|
12235
12357
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
@@ -12251,8 +12373,15 @@ class Collection extends EventEmitterObject {
|
|
|
12251
12373
|
error && error.call(options.context, model, e, options);
|
|
12252
12374
|
return_promise && promise.reject(e);
|
|
12253
12375
|
};
|
|
12254
|
-
|
|
12376
|
+
// `options.promise` is passed through, so `save` installs its own bridge
|
|
12377
|
+
// from the sync's returned promise to its callbacks: a sync that resolves
|
|
12378
|
+
// without calling them still reaches the wired `options.success` above,
|
|
12379
|
+
// which resolves our promise with the model. Only a rejection that bypassed
|
|
12380
|
+
// the callbacks (a rejecting sync, or a throwing `'sync'`/`'error'`
|
|
12381
|
+
// listener) still needs bridging to ours.
|
|
12382
|
+
const result = preparedModel.save(null, options);
|
|
12255
12383
|
if (return_promise) {
|
|
12384
|
+
Promise.resolve(result).catch((e) => promise.reject(e));
|
|
12256
12385
|
return promise;
|
|
12257
12386
|
}
|
|
12258
12387
|
else {
|
|
@@ -12478,6 +12607,7 @@ const skeletor = {
|
|
|
12478
12607
|
EventEmitter,
|
|
12479
12608
|
Model,
|
|
12480
12609
|
sync,
|
|
12610
|
+
HTTPError,
|
|
12481
12611
|
};
|
|
12482
12612
|
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
|
|
12483
12613
|
// We use `self` instead of `window` for `WebWorker` support.
|
|
@@ -12518,6 +12648,7 @@ PersistentStorage.nodeStorage = NodeSQLiteStorage;
|
|
|
12518
12648
|
exports.BrowserStorage = BrowserStorage;
|
|
12519
12649
|
exports.Collection = Collection;
|
|
12520
12650
|
exports.EventEmitter = EventEmitter;
|
|
12651
|
+
exports.HTTPError = HTTPError;
|
|
12521
12652
|
exports.Model = Model;
|
|
12522
12653
|
exports.NodeSQLiteStorage = NodeSQLiteStorage;
|
|
12523
12654
|
exports.PersistentStorage = PersistentStorage;
|