@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 CHANGED
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.0 (2026-10-24)
4
+
5
+ - Attribute-object queries on a collection (`findWhere`, `where`, and `find`, `filter`, `some`,
6
+ `every`, `map`, `sortBy`, `countBy`, `groupBy`, `findIndex` and `findLastIndex` when given an
7
+ object) now build their lodash matcher once per scan instead of once per model visited. Each
8
+ per-model build deep-cloned the attributes object and walked its keys, which made `findWhere`
9
+ roughly 8 to 13 times slower than `find` with a hand-written predicate on a few thousand models.
10
+ Matching semantics are unchanged: still lodash's partial, deep comparison, so nested lookups
11
+ such as `where({ FORM_TYPE: { type: 'hidden' } })` keep working. One alignment: `find` and
12
+ `filter` given a string now read that attribute through `Model.get`, like the other query
13
+ methods already did, instead of resolving it as a lodash property path over the raw attributes.
14
+
15
+ - `Model.matches` no longer deep-clones its argument on every call (it uses lodash's `isMatch`)
16
+ and its signature now admits the predicate function it has always accepted at runtime. The
17
+ undocumented lodash string and `[key, value]` shorthand forms are no longer supported.
18
+
19
+ - Fix `Model.save({ promise: true })` hanging forever, and leaking an unhandled rejection, when
20
+ its `sync` settles the returned promise instead of invoking the `success`/`error` callbacks
21
+ (a rejecting custom `sync`, or a throwing `'sync'`/`'error'` listener). `save` now bridges that
22
+ promise to the one it hands back, the way `fetch` already did. `Collection.create({ promise: true })`,
23
+ which delegates to `save`, is fixed the same way.
24
+
25
+ - Fix `fetch({ promise: true })` on a Model or Collection settling early when a callback-style `sync`
26
+ returns a non-thenable (nothing, or an XHR) and invokes `success`/`error` later. The bridge from the
27
+ `sync` return value to the returned promise now only engages for a thenable, so such a `sync` settles
28
+ the promise through its callbacks. The same guard applies to `save({ promise: true })`.
29
+
30
+ - The `{ promise: true }` paths of `fetch` and `save` now share one bridge from the `sync` outcome
31
+ to the returned promise, with two behaviour changes: a `sync` that rejects its promise without
32
+ calling the callbacks now also runs the `error` callback and fires the `'error'` event, and a
33
+ `'sync'`/`'error'` listener (or `success`/`error` callback) that throws now rejects the promise
34
+ instead of leaving it pending with an unhandled rejection. `Collection.fetch` fires `'sync'` before
35
+ resolving, as `Model.fetch`/`save` already did, so a throwing listener rejects there too.
36
+
37
+ - Fix `save({ promise: true })` and `fetch({ promise: true, validate: true })` leaving the promise
38
+ pending when the attributes returned by `sync` fail validation. The promise now rejects with
39
+ `validationError`; the `'invalid'` event fires as before.
40
+
41
+ - `save({ promise: true })` now returns a promise rejected with `validationError`, instead of `false`,
42
+ when the attributes passed to it fail validation; `create({ promise: true })` likewise returns a
43
+ rejected promise instead of `null`. The callback forms still return `false`/`null`.
44
+
45
+ - The default HTTP `sync` now honours both the callback and the promise contracts. It never invoked
46
+ `success`/`error` (it passed them to `fetch()` as init options, which ignores them), sent an empty
47
+ body on GET (which the Fetch API rejects outright), and resolved with the raw `Response`, which the
48
+ promise bridge then merged into the model as attributes. It now sends `data` as query parameters on
49
+ GET and as a JSON body otherwise, passes a parsed 2xx JSON body (`null` when empty) to `success` and
50
+ resolves with it, and on a non-2xx response or a network failure calls `error` with, and rejects
51
+ with, the error: an `HTTPError` (new export, carrying `status` and `response`) for the former.
52
+
53
+ ## 3.1.6 (2026-08-06)
54
+
55
+ - The `'node'` store type can now be told where to put its database files, via the new
56
+ `PersistentStorage.nodeStorageDir`. The driver has always taken a `storageDir` argument,
57
+ but nothing could reach it through the string store type, so `new PersistentStorage(id, 'node')`
58
+ was stuck with the driver's default. That default is relative, so it resolves against
59
+ `process.cwd()` and an app started from two directories keeps two separate sets of state.
60
+ Set an absolute path (`$XDG_STATE_HOME/<app>`, say) to pin it. The default is unchanged.
61
+
62
+ - Ship a separate `dist/skeletor.node.d.ts` for the `@converse/skeletor/node` entry point.
63
+ Both entry points pointed their `types` at the browser rollup.
64
+
3
65
  ## 3.1.5 (2026-07-22)
4
66
 
5
67
  - Fix every write through the Node.js SQLite driver failing with `Record Not Found` while
@@ -136,7 +136,7 @@ export declare class Collection<T extends Model = Model> extends EventEmitterObj
136
136
  min(): number;
137
137
  drop(n?: number): T[];
138
138
  some(pred: ((attrs: ModelAttributes) => boolean) | Options): boolean;
139
- sortBy(iteratee: string | ((model: T) => any)): T[];
139
+ sortBy(iteratee: string | ((model: T) => any) | Partial<ModelAttributes>): T[];
140
140
  isEmpty(): boolean;
141
141
  keyBy(iteratee: string | ((model: T) => string)): Record<string, T>;
142
142
  each(callback: (model: T, index: number, array: T[]) => void, thisArg?: any): void;
@@ -144,7 +144,7 @@ export declare class Collection<T extends Model = Model> extends EventEmitterObj
144
144
  includes(item: T): boolean;
145
145
  size(): number;
146
146
  countBy(f: string | ((model: T) => string) | Partial<ModelAttributes>): Record<string, number>;
147
- groupBy(pred: string | ((model: T) => string | number)): Record<string, T[]>;
147
+ groupBy(pred: string | ((model: T) => string | number) | Partial<ModelAttributes>): Record<string, T[]>;
148
148
  indexOf(model: T, fromIndex?: number): number;
149
149
  findLastIndex(pred: ((model: T) => boolean) | string | Partial<ModelAttributes>, fromIndex?: number): number;
150
150
  lastIndexOf(model: T, fromIndex?: number): number;
@@ -487,6 +487,17 @@ export declare type FetchOrCreateOptions = Options & {
487
487
  */
488
488
  export declare function getStorage(obj: Model<any> | Collection<any> | null | undefined): PersistentStorage | undefined;
489
489
 
490
+ /**
491
+ * The error the default `sync` hands to `options.error`, and rejects with, for
492
+ * a response outside the 2xx range.
493
+ * @public
494
+ */
495
+ export declare class HTTPError extends Error {
496
+ readonly status: number;
497
+ readonly response: Response;
498
+ constructor(response: Response, method: string, url: string);
499
+ }
500
+
490
501
  /**
491
502
  * @public
492
503
  */
@@ -634,9 +645,17 @@ export declare class Model<T extends ModelAttributes = ModelAttributes> extends
634
645
  */
635
646
  has(attr: keyof T): boolean;
636
647
  /**
637
- * Special-cased proxy to lodash's `matches` method.
648
+ * Returns `true` if the model's attributes satisfy `attrs`.
649
+ *
650
+ * An attributes object is compared with lodash's `isMatch`: a partial, deep
651
+ * comparison, so `{ form: { type: 'hidden' } }` matches a `form` attribute
652
+ * that has `type: 'hidden'` alongside other keys. A function is called with
653
+ * the attributes and its result coerced to a boolean.
654
+ *
655
+ * To scan a collection, prefer `Collection#find`, `filter` or `where`. They
656
+ * build their matcher once per scan instead of once per model.
638
657
  */
639
- matches(attrs: Partial<T>): boolean;
658
+ matches(attrs: Partial<T> | ((attrs: T) => any)): boolean;
640
659
  /**
641
660
  * Set a hash of model attributes on the object, firing `"change"`. This is
642
661
  * the core primitive operation of a model, updating the data and notifying
@@ -762,8 +781,9 @@ export declare function noConflict(): any;
762
781
  /**
763
782
  * Constructor for a Node.js-only storage driver. Registered by the
764
783
  * `@converse/skeletor/node` entry point via {@link PersistentStorage.nodeStorage}.
784
+ * @public
765
785
  */
766
- declare type NodeStorageConstructor = new (name: string) => StorageDriver;
786
+ export declare type NodeStorageConstructor = new (name: string, storageDir?: string, inMemory?: boolean) => StorageDriver;
767
787
 
768
788
  /**
769
789
  * @public
@@ -795,6 +815,18 @@ export declare class PersistentStorage {
795
815
  static sessionStorageInitialized: Promise<void>;
796
816
  static localForage: typeof localForage;
797
817
  static nodeStorage: NodeStorageConstructor | null;
818
+ /**
819
+ * Directory the `'node'` store type writes its database files to. The driver's
820
+ * own default is relative, so it resolves against `process.cwd()` and a process
821
+ * started from two directories ends up with two separate sets of state. Set an
822
+ * absolute path (`$XDG_STATE_HOME/<app>`, say) to pin it. Left `null`, the
823
+ * driver's default applies.
824
+ *
825
+ * Only the `'node'` string store type reads this; passing a driver instance to
826
+ * the constructor means you've already chosen a directory yourself.
827
+ * @public
828
+ */
829
+ static nodeStorageDir: string | null;
798
830
  /** Register a storage instance with a weak reference. @public */
799
831
  static register(instance: PersistentStorage): void;
800
832
  /**
@@ -843,6 +875,7 @@ export declare interface SkeletorType {
843
875
  EventEmitter: typeof EventEmitter;
844
876
  Model: typeof Model;
845
877
  sync: typeof sync;
878
+ HTTPError: typeof HTTPError;
846
879
  VERSION?: string;
847
880
  noConflict?: () => SkeletorType;
848
881
  }
@@ -880,18 +913,20 @@ export declare interface StorageDriver {
880
913
  export declare type StoreType = 'local' | 'session' | 'indexed' | 'in_memory' | 'node';
881
914
 
882
915
  /**
883
- * Override this function to change the manner in which Backbone persists
884
- * models to the server. You will be passed the type of request, and the
885
- * model in question. By default makes a `fetch()` API call
886
- * to the model's `url()`.
916
+ * The default `sync`: a JSON request over `fetch()` to the model's `url()`.
917
+ * Override it (or `Model.sync`/`Collection.sync`) to change how models are
918
+ * persisted, for instance to batch rapid-fire updates with `setTimeout`, to
919
+ * talk over WebSockets, or to write to browser storage.
887
920
  *
888
- * Some possible customizations could be:
921
+ * It honours both contracts a `sync` may use. On a 2xx response the JSON body
922
+ * (`null` when the body is empty) is passed to `options.success` and the
923
+ * returned promise resolves with it. On a non-2xx response, a network failure
924
+ * or an unparsable body, `options.error` receives the error (an `HTTPError`
925
+ * for the first) and the promise rejects with it.
889
926
  *
890
- * - Use `setTimeout` to batch rapid-fire updates into a single request.
891
- * - Persist models via WebSockets instead of Ajax.
892
- * - Persist models to browser storage
893
- */
894
- /**
927
+ * `options.data` is sent as query parameters on GET, which may not carry a
928
+ * body, and as a JSON body otherwise. For create, update and patch the body
929
+ * defaults to `options.attrs` or the model's `toJSON()`.
895
930
  * @public
896
931
  */
897
932
  export declare function sync(method: SyncOperation, model: Model | Collection<any>, options?: SyncOptions): Promise<any>;