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