@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/src/model.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
bridgeSyncToPromise,
|
|
2
3
|
getResolveablePromise,
|
|
3
4
|
getStorage,
|
|
4
5
|
getSyncMethod,
|
|
@@ -14,7 +15,7 @@ import has from 'lodash-es/has';
|
|
|
14
15
|
import invert from 'lodash-es/invert';
|
|
15
16
|
import isEmpty from 'lodash-es/isEmpty';
|
|
16
17
|
import isEqual from 'lodash-es/isEqual';
|
|
17
|
-
import
|
|
18
|
+
import isMatch from 'lodash-es/isMatch';
|
|
18
19
|
import omit from 'lodash-es/omit';
|
|
19
20
|
import pick from 'lodash-es/pick';
|
|
20
21
|
import result from 'lodash-es/result';
|
|
@@ -317,10 +318,19 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
/**
|
|
320
|
-
*
|
|
321
|
+
* Returns `true` if the model's attributes satisfy `attrs`.
|
|
322
|
+
*
|
|
323
|
+
* An attributes object is compared with lodash's `isMatch`: a partial, deep
|
|
324
|
+
* comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
|
|
325
|
+
* that has `type: 'hidden'` alongside other keys. A function is called with
|
|
326
|
+
* the attributes and its result coerced to a boolean.
|
|
327
|
+
*
|
|
328
|
+
* To scan a collection, prefer `Collection#find`, `filter` or `where`. They
|
|
329
|
+
* build their matcher once per scan instead of once per model.
|
|
321
330
|
*/
|
|
322
|
-
matches(attrs: Partial<T>): boolean {
|
|
323
|
-
return !!
|
|
331
|
+
matches(attrs: Partial<T> | ((attrs: T) => any)): boolean {
|
|
332
|
+
if (typeof attrs === 'function') return !!attrs(this.attributes);
|
|
333
|
+
return isMatch(this.attributes, attrs);
|
|
324
334
|
}
|
|
325
335
|
|
|
326
336
|
/**
|
|
@@ -511,48 +521,26 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
511
521
|
const success = options.success;
|
|
512
522
|
const promise = options.promise ? getResolveablePromise() : undefined;
|
|
513
523
|
|
|
514
|
-
// Tracks whether sync invoked one of the callbacks. A sync may instead
|
|
515
|
-
// settle its returned promise; the `.then`/`.catch` below bridges that to
|
|
516
|
-
// the callbacks so the fetch promise never hangs. The flag is set
|
|
517
|
-
// synchronously inside each callback (before sync's promise resolves), so
|
|
518
|
-
// it's reliable by the time those handlers run.
|
|
519
|
-
let settled = false;
|
|
520
|
-
|
|
521
524
|
options.success = (resp: any) => {
|
|
522
|
-
settled = true;
|
|
523
525
|
const serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
524
526
|
// fromStorage prevents the set() from triggering auto-save on reads
|
|
525
|
-
if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true })))
|
|
527
|
+
if (!this.set(serverAttrs, Object.assign({}, options, { fromStorage: true }))) {
|
|
528
|
+
// The fetched attributes failed validation (`'invalid'` has fired).
|
|
529
|
+
// Settle the promise with the error rather than leave it pending.
|
|
530
|
+
promise && promise.reject(this.validationError);
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
526
533
|
if (success) success.call(options.context, this, resp, options);
|
|
527
534
|
this.trigger('sync', this, resp, options);
|
|
528
535
|
promise && promise.resolve(resp);
|
|
529
536
|
};
|
|
530
537
|
|
|
531
538
|
wrapError(this, options, promise);
|
|
532
|
-
if (promise) {
|
|
533
|
-
const onError = options.error;
|
|
534
|
-
options.error = (resp: any) => {
|
|
535
|
-
settled = true;
|
|
536
|
-
onError(resp);
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
539
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
// treat the resolved value as the response, so the model is still
|
|
545
|
-
// merged and the promise settles instead of hanging.
|
|
546
|
-
(value) => {
|
|
547
|
-
if (!settled) options.success(value);
|
|
548
|
-
},
|
|
549
|
-
// A sync that rejects instead of invoking options.error; settling
|
|
550
|
-
// twice is a harmless no-op.
|
|
551
|
-
(e) => promise.reject(e),
|
|
552
|
-
);
|
|
553
|
-
return promise;
|
|
554
|
-
}
|
|
555
|
-
return result;
|
|
540
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
541
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
542
|
+
const result = bridgeSyncToPromise(options, promise, () => this.sync('read', this, options));
|
|
543
|
+
return promise ?? result;
|
|
556
544
|
}
|
|
557
545
|
|
|
558
546
|
/**
|
|
@@ -574,15 +562,19 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
574
562
|
options = Object.assign({ validate: true, parse: true }, options);
|
|
575
563
|
const wait = options.wait;
|
|
576
564
|
const return_promise = options.promise;
|
|
577
|
-
const promise = return_promise
|
|
565
|
+
const promise = return_promise ? getResolveablePromise() : undefined;
|
|
578
566
|
|
|
579
567
|
// If we're not waiting and attributes exist, save acts as
|
|
580
568
|
// `set(attr).save(null, opts)` with validation. Otherwise, check if
|
|
581
569
|
// the model will be valid when the attributes, if any, are set.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
570
|
+
const valid = attrs && !wait ? !!this.set(attrs, options) : this._validate(attrs || {}, options);
|
|
571
|
+
if (!valid) {
|
|
572
|
+
// Nothing is synced (`'invalid'` has fired). With `promise`, hand back a
|
|
573
|
+
// rejected promise rather than `false`, so the promise contract holds
|
|
574
|
+
// for every outcome.
|
|
575
|
+
if (!promise) return false;
|
|
576
|
+
promise.reject(this.validationError);
|
|
577
|
+
return promise;
|
|
586
578
|
}
|
|
587
579
|
|
|
588
580
|
// After a successful server-side save, the client is (optionally)
|
|
@@ -596,7 +588,12 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
596
588
|
this.attributes = attributes;
|
|
597
589
|
let serverAttrs = options.parse ? this.parse(resp, options) : resp;
|
|
598
590
|
if (wait) serverAttrs = Object.assign({}, attrs, serverAttrs);
|
|
599
|
-
if (serverAttrs && !this.set(serverAttrs, options))
|
|
591
|
+
if (serverAttrs && !this.set(serverAttrs, options)) {
|
|
592
|
+
// The server attributes failed validation (`'invalid'` has fired).
|
|
593
|
+
// Settle the promise with the error rather than leave it pending.
|
|
594
|
+
return_promise && promise.reject(this.validationError);
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
600
597
|
if (success) success.call(options.context, this, resp, options);
|
|
601
598
|
this.trigger('sync', this, resp, options);
|
|
602
599
|
return_promise && promise.resolve();
|
|
@@ -614,16 +611,14 @@ export class Model<T extends ModelAttributes = ModelAttributes> extends EventEmi
|
|
|
614
611
|
|
|
615
612
|
const method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
|
|
616
613
|
if (method === 'patch' && !options.attrs) options.attrs = attrs;
|
|
617
|
-
|
|
614
|
+
// With `promise`, the sync's outcome is bridged to it however the sync
|
|
615
|
+
// reports it: through the callbacks, or through the promise it returns.
|
|
616
|
+
const xhr = bridgeSyncToPromise(options, promise, () => this.sync(method, this, options));
|
|
618
617
|
|
|
619
618
|
// Restore attributes.
|
|
620
619
|
this.attributes = attributes;
|
|
621
620
|
|
|
622
|
-
|
|
623
|
-
return promise;
|
|
624
|
-
} else {
|
|
625
|
-
return xhr;
|
|
626
|
-
}
|
|
621
|
+
return promise ?? xhr;
|
|
627
622
|
}
|
|
628
623
|
|
|
629
624
|
/**
|
package/src/storage.ts
CHANGED
|
@@ -36,8 +36,13 @@ const registered = new WeakSet<PersistentStorage>();
|
|
|
36
36
|
/**
|
|
37
37
|
* Constructor for a Node.js-only storage driver. Registered by the
|
|
38
38
|
* `@converse/skeletor/node` entry point via {@link PersistentStorage.nodeStorage}.
|
|
39
|
+
* @public
|
|
39
40
|
*/
|
|
40
|
-
type NodeStorageConstructor = new (
|
|
41
|
+
export type NodeStorageConstructor = new (
|
|
42
|
+
name: string,
|
|
43
|
+
storageDir?: string,
|
|
44
|
+
inMemory?: boolean,
|
|
45
|
+
) => StorageDriver;
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* @public
|
|
@@ -60,6 +65,19 @@ class PersistentStorage {
|
|
|
60
65
|
// bundle; the `@converse/skeletor/node` entry sets it.
|
|
61
66
|
static nodeStorage: NodeStorageConstructor | null = null;
|
|
62
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Directory the `'node'` store type writes its database files to. The driver's
|
|
70
|
+
* own default is relative, so it resolves against `process.cwd()` and a process
|
|
71
|
+
* started from two directories ends up with two separate sets of state. Set an
|
|
72
|
+
* absolute path (`$XDG_STATE_HOME/<app>`, say) to pin it. Left `null`, the
|
|
73
|
+
* driver's default applies.
|
|
74
|
+
*
|
|
75
|
+
* Only the `'node'` string store type reads this; passing a driver instance to
|
|
76
|
+
* the constructor means you've already chosen a directory yourself.
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
static nodeStorageDir: string | null = null;
|
|
80
|
+
|
|
63
81
|
/** Register a storage instance with a weak reference. @public */
|
|
64
82
|
static register(instance: PersistentStorage): void {
|
|
65
83
|
if (registered.has(instance)) return;
|
|
@@ -128,7 +146,7 @@ class PersistentStorage {
|
|
|
128
146
|
"Import from '@converse/skeletor/node' instead of '@converse/skeletor'.",
|
|
129
147
|
);
|
|
130
148
|
}
|
|
131
|
-
this.store = new NodeSQLiteStorage(this.name);
|
|
149
|
+
this.store = new NodeSQLiteStorage(this.name, PersistentStorage.nodeStorageDir ?? undefined);
|
|
132
150
|
if (batchedWrites) {
|
|
133
151
|
this.store.debouncedSetItems = mergebounce((items: Record<string, any>) => this.store.setItems!(items), 50, {
|
|
134
152
|
'promise': true,
|
|
@@ -155,9 +173,11 @@ class PersistentStorage {
|
|
|
155
173
|
async clear(): Promise<void> {
|
|
156
174
|
await this.storeInitialized;
|
|
157
175
|
await this.store.removeItem(this.name).catch((e) => console.error(e));
|
|
176
|
+
|
|
158
177
|
const re = new RegExp(`^${this.name}-`);
|
|
159
178
|
const keys = await this.store.keys();
|
|
160
179
|
const removed_keys = keys.filter((k) => re.test(k));
|
|
180
|
+
|
|
161
181
|
await Promise.all(removed_keys.map((k) => this.store.removeItem(k).catch((e) => console.error(e))));
|
|
162
182
|
}
|
|
163
183
|
|