@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/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 iteratee from 'lodash-es/iteratee';
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
- * Special-cased proxy to lodash's `matches` method.
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 !!iteratee(attrs)(this.attributes);
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 }))) return false;
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
- const result = this.sync('read', this, options);
541
- if (promise) {
542
- Promise.resolve(result).then(
543
- // A sync that resolves its promise without invoking options.success:
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 && getResolveablePromise();
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
- if (attrs && !wait) {
583
- if (!this.set(attrs, options)) return false;
584
- } else if (!this._validate(attrs || {}, options)) {
585
- return false;
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)) return false;
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
- const xhr = this.sync(method, this, options);
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
- if (return_promise) {
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 (name: string) => StorageDriver;
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