shelving 1.65.1 → 1.68.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/db/Database.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import type { Validators, ValidatorType } from "../util/validate.js";
2
- import type { Key, Datas } from "../util/data.js";
2
+ import type { Key, Datas, Entity } from "../util/data.js";
3
3
  import type { Provider } from "../provider/Provider.js";
4
- import type { SortKeys } from "../query/Sort.js";
5
- import type { FilterProps } from "../query/Filter.js";
6
- import { Entity } from "../util/data.js";
4
+ import type { QueryProps } from "../query/Query.js";
7
5
  import { DocumentReference, QueryReference } from "./Reference.js";
8
6
  /**
9
7
  * Combines a database model and a provider.
@@ -17,7 +15,7 @@ export declare class Database<V extends Validators<Datas> = Validators<Datas>> {
17
15
  readonly provider: Provider;
18
16
  constructor(validators: V, provider: Provider);
19
17
  /** Create a query on a collection in this model. */
20
- query<K extends Key<V>>(collection: K, filters?: FilterProps<Entity<ValidatorType<V[K]>>>, sorts?: SortKeys<Entity<ValidatorType<V[K]>>>, limit?: number | null): QueryReference<ValidatorType<V[K]>>;
18
+ query<K extends Key<V>>(collection: K, query?: QueryProps<Entity<ValidatorType<V[K]>>>): QueryReference<ValidatorType<V[K]>>;
21
19
  /** Reference a document in a collection in this model. */
22
20
  doc<K extends Key<V>>(collection: K, id: string): DocumentReference<ValidatorType<V[K]>>;
23
21
  /**
package/db/Database.js CHANGED
@@ -1,5 +1,3 @@
1
- import { Filters } from "../query/Filters.js";
2
- import { Sorts } from "../query/Sorts.js";
3
1
  import { DocumentReference, QueryReference } from "./Reference.js";
4
2
  /**
5
3
  * Combines a database model and a provider.
@@ -15,8 +13,9 @@ export class Database {
15
13
  this.provider = provider;
16
14
  }
17
15
  /** Create a query on a collection in this model. */
18
- query(collection, filters, sorts, limit) {
19
- return new QueryReference(this, this.validators[collection], collection, filters && Filters.on(filters), sorts && Sorts.on(sorts), limit);
16
+ query(collection, query) {
17
+ const validator = this.validators[collection];
18
+ return new QueryReference(this, validator, collection, query);
20
19
  }
21
20
  /** Reference a document in a collection in this model. */
22
21
  doc(collection, id) {
package/db/Reference.d.ts CHANGED
@@ -1,15 +1,11 @@
1
1
  import type { Data, OptionalData, Entity, OptionalEntity, Entities } from "../util/data.js";
2
2
  import type { Dispatch } from "../util/function.js";
3
- import type { SortKeys } from "../query/Sort.js";
4
3
  import { ImmutableArray } from "../util/array.js";
5
4
  import type { PartialObserver } from "../observe/Observer.js";
6
5
  import type { Validator } from "../util/validate.js";
7
- import { Query } from "../query/Query.js";
8
- import { Filters } from "../query/Filters.js";
9
- import { Sorts } from "../query/Sorts.js";
6
+ import { Query, QueryProps } from "../query/Query.js";
10
7
  import { DataUpdate, PropUpdates } from "../update/DataUpdate.js";
11
- import { FilterProps } from "../query/Filter.js";
12
- import { Observable, Unsubscribe } from "../observe/Observable.js";
8
+ import type { Observable, Unsubscribe } from "../observe/Observable.js";
13
9
  import type { Database } from "./Database.js";
14
10
  /** A refence to a location in a database. */
15
11
  export interface Reference {
@@ -21,7 +17,7 @@ export declare class QueryReference<T extends Data = Data> extends Query<Entity<
21
17
  readonly db: Database;
22
18
  readonly validator: Validator<T>;
23
19
  readonly collection: string;
24
- constructor(db: Database, validator: Validator<T>, collection: string, filters?: Filters<Entity<T>>, sorts?: Sorts<Entity<T>>, limit?: number | null);
20
+ constructor(db: Database, validator: Validator<T>, collection: string, { sort, limit, ...filters }?: QueryProps<Entity<T>>);
25
21
  /** Reference a document in this query's collection. */
26
22
  doc(id: string): DocumentReference<T>;
27
23
  /**
@@ -104,7 +100,7 @@ export declare class DocumentReference<T extends Data = Data> implements Observa
104
100
  readonly id: string;
105
101
  constructor(db: Database, validator: Validator<T>, collection: string, id: string);
106
102
  /** Create a query on this document's collection. */
107
- query(filters?: FilterProps<Entity<T>>, sorts?: SortKeys<Entity<T>>, limit?: number | null): QueryReference<T>;
103
+ query(query?: QueryProps<Entity<T>>): QueryReference<T>;
108
104
  /** Get an 'optional' reference to this document (uses a `ModelQuery` with an `id` filter). */
109
105
  get optional(): QueryReference<T>;
110
106
  /**
package/db/Reference.js CHANGED
@@ -5,12 +5,11 @@ import { Sorts } from "../query/Sorts.js";
5
5
  import { callAsync } from "../util/async.js";
6
6
  import { countItems, hasItems } from "../util/iterate.js";
7
7
  import { DataUpdate } from "../update/DataUpdate.js";
8
- import { Filter } from "../query/Filter.js";
9
8
  import { RequiredError } from "../error/RequiredError.js";
10
9
  /** A query reference within a specific database. */
11
10
  export class QueryReference extends Query {
12
- constructor(db, validator, collection, filters, sorts, limit) {
13
- super(filters, sorts, limit);
11
+ constructor(db, validator, collection, { sort, limit, ...filters } = {}) {
12
+ super(Filters.on(filters), sort && Sorts.on(sort), limit);
14
13
  this.db = db;
15
14
  this.validator = validator;
16
15
  this.collection = collection;
@@ -127,12 +126,12 @@ export class DocumentReference {
127
126
  this.id = id;
128
127
  }
129
128
  /** Create a query on this document's collection. */
130
- query(filters, sorts, limit) {
131
- return new QueryReference(this.db, this.validator, this.collection, filters && Filters.on(filters), sorts && Sorts.on(sorts), limit);
129
+ query(query) {
130
+ return new QueryReference(this.db, this.validator, this.collection, query);
132
131
  }
133
132
  /** Get an 'optional' reference to this document (uses a `ModelQuery` with an `id` filter). */
134
133
  get optional() {
135
- return new QueryReference(this.db, this.validator, this.collection, new Filters(new Filter("id", "IS", this.id)));
134
+ return new QueryReference(this.db, this.validator, this.collection, { id: this.id, limit: 1 });
136
135
  }
137
136
  /**
138
137
  * Does this document exist?
package/observe/util.d.ts CHANGED
@@ -15,10 +15,10 @@ export declare function connected<T, C extends ConnectableObserver<T>>(source: S
15
15
  export declare function connected<T>(source: Subscribable<T>, target: ConnectableObserver<T>): Subject<T>;
16
16
  export declare function connected<T>(source: Subscribable<T>): Subject<T>;
17
17
  /** Connect a connectable to a source subscribable but transform the value using a transform, and return the connected connectable. */
18
- export declare function connectedDerived<T, TT, C extends ConnectableObserver<T>>(source: Subscribable<T>, transformer: Transformer<T, TT>, target: C): C;
19
- export declare function connectedDerived<T, TT>(source: Subscribable<T>, transformer: Transformer<T, TT>, target: ConnectableObserver<T>): ConnectableObserver<TT>;
18
+ export declare function connectedDerived<T, TT, C extends ConnectableObserver<TT>>(source: Subscribable<T>, transformer: Transformer<T, TT>, target: C): C;
19
+ export declare function connectedDerived<T, TT>(source: Subscribable<T>, transformer: Transformer<T, TT>, target: ConnectableObserver<TT>): ConnectableObserver<TT>;
20
20
  export declare function connectedDerived<T, TT>(source: Subscribable<T>, transformer: Transformer<T, TT>): Subject<TT>;
21
21
  /** Connect a connectable to a source subscribable but transform the value using an async transform, and return the connected connectable. */
22
22
  export declare function connectedAsyncDerived<T, TT, C extends ConnectableObserver<TT>>(source: Subscribable<T>, transformer: Transformer<T, PromiseLike<TT>>, target: C): C;
23
- export declare function connectedAsyncDerived<T, TT>(source: Subscribable<T>, transformer: Transformer<T, PromiseLike<TT>>, target: ConnectableObserver<T>): ConnectableObserver<TT>;
23
+ export declare function connectedAsyncDerived<T, TT>(source: Subscribable<T>, transformer: Transformer<T, PromiseLike<TT>>, target: ConnectableObserver<TT>): ConnectableObserver<TT>;
24
24
  export declare function connectedAsyncDerived<T, TT>(source: Subscribable<T>, transformer: Transformer<T, PromiseLike<TT>>): Subject<TT>;
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.65.1",
14
+ "version": "1.68.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
@@ -16,14 +16,14 @@ export declare class MemoryProvider extends Provider implements SynchronousProvi
16
16
  /** List of tables in `{ path: Table }` format. */
17
17
  private _tables;
18
18
  getTable<T extends Data>({ collection }: DocumentReference<T> | QueryReference<T>): MemoryTable<T>;
19
- getDocumentTime<T extends Data>(ref: DocumentReference<T>): number | undefined;
19
+ getDocumentTime<T extends Data>(ref: DocumentReference<T>): number | null;
20
20
  getDocument<T extends Data>(ref: DocumentReference<T>): OptionalEntity<T>;
21
21
  subscribeDocument<T extends Data>(ref: DocumentReference<T>, observer: PartialObserver<OptionalEntity<T>>): Unsubscribe;
22
22
  addDocument<T extends Data>(ref: QueryReference<T>, data: T): string;
23
23
  setDocument<T extends Data>(ref: DocumentReference<T>, data: T): void;
24
24
  updateDocument<T extends Data>(ref: DocumentReference<T>, update: DataUpdate<T>): void;
25
25
  deleteDocument<T extends Data>(ref: DocumentReference<T>): void;
26
- getQueryTime<T extends Data>(ref: QueryReference<T>): number | undefined;
26
+ getQueryTime<T extends Data>(ref: QueryReference<T>): number | null;
27
27
  getQuery<T extends Data>(ref: QueryReference<T>): Entities<T>;
28
28
  subscribeQuery<T extends Data>(ref: QueryReference<T>, observer: PartialObserver<Entities<T>>): Unsubscribe;
29
29
  setQuery<T extends Data>(ref: QueryReference<T>, data: T): number;
@@ -39,17 +39,21 @@ export declare class MemoryTable<T extends Data> extends Subject<void> {
39
39
  protected _times: Map<string, number>;
40
40
  protected _listeners: Set<Dispatch<[]>>;
41
41
  protected _firing: boolean;
42
- getDocumentTime(id: string): number | undefined;
42
+ getDocumentTime(id: string): number | null;
43
43
  getDocument(id: string): OptionalEntity<T>;
44
44
  subscribeDocument(id: string, observer: PartialObserver<OptionalEntity<T>>): Unsubscribe;
45
+ /** Subscribe to a query in this table, but only if the query has been explicitly set (and has a time). */
46
+ subscribeCachedDocument(id: string, observer: PartialObserver<OptionalEntity<T>>): Unsubscribe;
45
47
  addDocument(data: T): string;
46
48
  setEntity(entity: Entity<T>): void;
47
49
  setDocument(id: string, data: T): void;
48
50
  updateDocument(id: string, update: DataUpdate<T>): void;
49
51
  deleteDocument(id: string): void;
50
- getQueryTime(query: Query<Entity<T>>): number | undefined;
52
+ getQueryTime(query: Query<Entity<T>>): number | null;
51
53
  getQuery(query: Query<Entity<T>>): Entities<T>;
52
54
  subscribeQuery(query: Query<Entity<T>>, observer: PartialObserver<Entities<T>>): Unsubscribe;
55
+ /** Subscribe to a query in this table, but only if the query has been explicitly set (and has a time). */
56
+ subscribeCachedQuery(query: Query<Entity<T>>, observer: PartialObserver<Entities<T>>): Unsubscribe;
53
57
  protected _getWrites(query: Query<Entity<T>>): Iterable<Entity<T>>;
54
58
  setEntities(query: Query<Entity<T>>, entities: Entities<T>): number;
55
59
  setQuery(query: Query<Entity<T>>, data: T): number;
@@ -76,7 +76,7 @@ export class MemoryTable extends Subject {
76
76
  this._firing = false;
77
77
  }
78
78
  getDocumentTime(id) {
79
- return this._times.get(id);
79
+ return this._times.get(id) || null;
80
80
  }
81
81
  getDocument(id) {
82
82
  return this._data.get(id) || null;
@@ -94,6 +94,23 @@ export class MemoryTable extends Subject {
94
94
  }
95
95
  });
96
96
  }
97
+ /** Subscribe to a query in this table, but only if the query has been explicitly set (and has a time). */
98
+ subscribeCachedDocument(id, observer) {
99
+ // Call next() immediately with initial results.
100
+ let last = this.getDocument(id);
101
+ if (this._times.has(id))
102
+ dispatchNext(observer, last);
103
+ // Call next() every time the collection changes.
104
+ return this.subscribe(() => {
105
+ if (this._times.has(id)) {
106
+ const next = this.getDocument(id);
107
+ if (next !== last) {
108
+ last = next;
109
+ dispatchNext(observer, last);
110
+ }
111
+ }
112
+ });
113
+ }
97
114
  addDocument(data) {
98
115
  let id = getRandomKey();
99
116
  while (this._data.has(id))
@@ -122,7 +139,7 @@ export class MemoryTable extends Subject {
122
139
  this.next();
123
140
  }
124
141
  getQueryTime(query) {
125
- return this._times.get(_getQueryReference(query));
142
+ return this._times.get(_getQueryReference(query)) || null;
126
143
  }
127
144
  getQuery(query) {
128
145
  return getArray(query.transform(this._data.values()));
@@ -140,6 +157,24 @@ export class MemoryTable extends Subject {
140
157
  }
141
158
  });
142
159
  }
160
+ /** Subscribe to a query in this table, but only if the query has been explicitly set (and has a time). */
161
+ subscribeCachedQuery(query, observer) {
162
+ // Call next() immediately with initial results.
163
+ const ref = _getQueryReference(query);
164
+ let last = this.getQuery(query);
165
+ if (this._times.has(ref))
166
+ dispatchNext(observer, last);
167
+ // Call next() every time the collection changes.
168
+ return this.subscribe(() => {
169
+ if (this._times.has(ref)) {
170
+ const next = this.getQuery(query);
171
+ if (next !== last) {
172
+ last = next;
173
+ dispatchNext(observer, last);
174
+ }
175
+ }
176
+ });
177
+ }
143
178
  _getWrites(query) {
144
179
  // Queries that have no limit don't care about sorting either.
145
180
  // So sorting can be skipped for performance.
@@ -196,5 +231,5 @@ export class MemoryTable extends Subject {
196
231
  }
197
232
  function _getQueryReference(query) {
198
233
  // Queries that have no limit don't care about sorting either.
199
- return query.limit ? `filters=${query.filters.toString()}` : Query.prototype.toString.call(query);
234
+ return query.limit ? `{${query.filters.toString()}}` : Query.prototype.toString.call(query);
200
235
  }
@@ -32,4 +32,4 @@ export interface AsynchronousThroughProvider extends AsynchronousProvider {
32
32
  new (source: AsynchronousProvider): AsynchronousProvider;
33
33
  }
34
34
  /** Find a specific source provider in a database's provider stack. */
35
- export declare function findSourceProvider<P extends Provider>(provider: Provider, type: Class<P>): P;
35
+ export declare function findSourceProvider<P extends Provider>(provider: Provider, type: Class<P>): P | undefined;
@@ -1,4 +1,3 @@
1
- import { AssertionError } from "../error/AssertionError.js";
2
1
  import { Provider } from "./Provider.js";
3
2
  /**
4
3
  * Pass all reads and writes through to a source provider.
@@ -48,5 +47,4 @@ export function findSourceProvider(provider, type) {
48
47
  return provider;
49
48
  if (provider instanceof ThroughProvider)
50
49
  return findSourceProvider(provider.source, type);
51
- throw new AssertionError(`Source provider ${type.name} not found`, provider);
52
50
  }
package/query/Filter.js CHANGED
@@ -51,6 +51,26 @@ export class Filter extends Rule {
51
51
  return filterItems(items, this);
52
52
  }
53
53
  toString() {
54
- return `${this.key}:${this.operator}:${JSON.stringify(this.value)}`;
54
+ return `"${_formatKey(this)}":${JSON.stringify(this.value)}`;
55
+ }
56
+ }
57
+ /** Convert a `Filter` */
58
+ function _formatKey({ key, operator }) {
59
+ switch (operator) {
60
+ case "NOT":
61
+ case "OUT":
62
+ return `!${key}`;
63
+ case "CONTAINS":
64
+ return `${key}[]`;
65
+ case "LT":
66
+ return `${key}<`;
67
+ case "LTE":
68
+ return `${key}<=`;
69
+ case "GT":
70
+ return `${key}>`;
71
+ case "GTE":
72
+ return `${key}>=`;
73
+ default:
74
+ return key;
55
75
  }
56
76
  }
package/query/Query.d.ts CHANGED
@@ -5,6 +5,11 @@ import { Sortable, Sorts } from "./Sorts.js";
5
5
  import { Rule } from "./Rule.js";
6
6
  import { FilterProps } from "./Filter.js";
7
7
  import { SortKey, SortKeys } from "./Sort.js";
8
+ /** Set of props for a query defined as an object. */
9
+ export declare type QueryProps<T extends Data> = FilterProps<T> & {
10
+ readonly sort?: SortKey<T> | ImmutableArray<SortKey<T>>;
11
+ readonly limit?: number | null;
12
+ };
8
13
  /** Interface that combines Filterable, Sortable, Sliceable. */
9
14
  export interface Queryable<T extends Data> extends Filterable<T>, Sortable<T> {
10
15
  /**
@@ -26,11 +31,13 @@ export interface Queryable<T extends Data> extends Filterable<T>, Sortable<T> {
26
31
  readonly limit: number | null;
27
32
  /** Return a new instance of this class with a limit set. */
28
33
  max(max: number | null): this;
34
+ /** Return a new instance of this class with new filters, sorts, limits set. */
35
+ query(query: QueryProps<T>): this;
29
36
  }
30
37
  /** Allows filtering, sorting, and limiting on a set of results. */
31
38
  export declare class Query<T extends Data> extends Rule<T> implements Queryable<T> {
32
39
  /** Create a new `Query` object from a set of `QueryProps` */
33
- static on<X extends Data>(filters?: FilterProps<X>, sorts?: SortKey<X> | ImmutableArray<SortKey<X>>, limit?: number | null): Query<X>;
40
+ static on<X extends Data>({ sort, limit, ...filters }: QueryProps<X>): Query<X>;
34
41
  readonly filters: Filters<T>;
35
42
  readonly sorts: Sorts<T>;
36
43
  readonly limit: number | null;
@@ -44,10 +51,10 @@ export declare class Query<T extends Data> extends Rule<T> implements Queryable<
44
51
  sort(...keys: SortKeys<T>[]): this;
45
52
  get unsort(): this;
46
53
  rank(left: T, right: T): number;
47
- /** Return a new instance of this class with a limit defined. */
48
- max(limit: number | null): this;
49
54
  after(item: T): this;
50
55
  before(item: T): this;
56
+ max(limit: number | null): this;
57
+ query({ sort, limit, ...filters }: QueryProps<T>): this;
51
58
  transform(items: Iterable<T>): Iterable<T>;
52
59
  toString(): string;
53
60
  }
package/query/Query.js CHANGED
@@ -17,32 +17,44 @@ export class Query extends Rule {
17
17
  this.limit = limit;
18
18
  }
19
19
  /** Create a new `Query` object from a set of `QueryProps` */
20
- static on(filters, sorts, limit) {
21
- return new Query(filters && Filters.on(filters), sorts && Sorts.on(sorts), limit);
20
+ static on({ sort, limit, ...filters }) {
21
+ return new Query(filters && Filters.on(filters), sort && Sorts.on(sort), limit);
22
22
  }
23
23
  filter(input, value) {
24
- return { __proto__: Object.getPrototypeOf(this), ...this, filters: this.filters.filter(input, value) }; // eslint-disable-line @typescript-eslint/no-explicit-any
24
+ return {
25
+ __proto__: Object.getPrototypeOf(this),
26
+ ...this,
27
+ filters: this.filters.filter(input, value), // eslint-disable-line @typescript-eslint/no-explicit-any
28
+ };
25
29
  }
26
30
  get unfilter() {
27
- return { __proto__: Object.getPrototypeOf(this), ...this, filters: EMPTY_FILTERS };
31
+ return {
32
+ __proto__: Object.getPrototypeOf(this),
33
+ ...this,
34
+ filters: EMPTY_FILTERS,
35
+ };
28
36
  }
29
37
  match(item) {
30
38
  return this.filters.match(item);
31
39
  }
32
40
  // Implement `Sortable`
33
41
  sort(...keys) {
34
- return { __proto__: Object.getPrototypeOf(this), ...this, sorts: this.sorts.sort(...keys) };
42
+ return {
43
+ __proto__: Object.getPrototypeOf(this),
44
+ ...this,
45
+ sorts: this.sorts.sort(...keys),
46
+ };
35
47
  }
36
48
  get unsort() {
37
- return { __proto__: Object.getPrototypeOf(this), ...this, sorts: EMPTY_SORTS };
49
+ return {
50
+ __proto__: Object.getPrototypeOf(this),
51
+ ...this,
52
+ sorts: EMPTY_SORTS,
53
+ };
38
54
  }
39
55
  rank(left, right) {
40
56
  return this.sorts.rank(left, right);
41
57
  }
42
- /** Return a new instance of this class with a limit defined. */
43
- max(limit) {
44
- return limit === this.limit ? this : { __proto__: Object.getPrototypeOf(this), ...this, limit };
45
- }
46
58
  // Implement `Queryable`
47
59
  after(item) {
48
60
  const filters = [...this.filters];
@@ -52,7 +64,11 @@ export class Query extends Rule {
52
64
  const { key, direction } = sort;
53
65
  filters.push(new Filter(key, direction === "ASC" ? (sort === lastSort ? "GT" : "GTE") : sort === lastSort ? "LT" : "LTE", getProp(item, key)));
54
66
  }
55
- return { __proto__: Object.getPrototypeOf(this), ...this, filters: new Filters(...filters) };
67
+ return {
68
+ __proto__: Object.getPrototypeOf(this),
69
+ ...this,
70
+ filters: new Filters(...filters),
71
+ };
56
72
  }
57
73
  before(item) {
58
74
  const filters = [...this.filters];
@@ -62,7 +78,27 @@ export class Query extends Rule {
62
78
  const { key, direction } = sort;
63
79
  filters.push(new Filter(key, direction === "ASC" ? (sort === lastSort ? "LT" : "LTE") : sort === lastSort ? "GT" : "GTE", getProp(item, key)));
64
80
  }
65
- return { __proto__: Object.getPrototypeOf(this), ...this, filters: new Filters(...filters) };
81
+ return {
82
+ __proto__: Object.getPrototypeOf(this),
83
+ ...this,
84
+ filters: new Filters(...filters),
85
+ };
86
+ }
87
+ max(limit) {
88
+ return {
89
+ __proto__: Object.getPrototypeOf(this),
90
+ ...this,
91
+ limit,
92
+ };
93
+ }
94
+ query({ sort, limit, ...filters }) {
95
+ return {
96
+ __proto__: Object.getPrototypeOf(this),
97
+ ...this,
98
+ filters: this.filters.filter(filters),
99
+ sorts: sort ? this.sorts.sort(sort) : this.sorts,
100
+ limit: limit !== undefined ? limit : this.limit,
101
+ };
66
102
  }
67
103
  // Implement `Rule`
68
104
  transform(items) {
@@ -71,6 +107,8 @@ export class Query extends Rule {
71
107
  }
72
108
  // Implement toString()
73
109
  toString() {
74
- return `filters=${this.filters}&sorts=${this.sorts}${this.limit ? `&limit=${this.limit}` : ""}`;
110
+ const filters = this.filters.toString();
111
+ const sorts = this.sorts.toString();
112
+ return `{${filters}${sorts ? `${filters ? "," : ""}"sort":[${sorts}]` : ""}${typeof this.limit === "number" ? `${filters || sorts ? "," : ""}"limit":${this.limit}` : ""}}`;
75
113
  }
76
114
  }
package/query/Rules.js CHANGED
@@ -18,8 +18,9 @@ export class Rules extends Rule {
18
18
  get size() {
19
19
  return this._rules.length;
20
20
  }
21
+ // Override to join the strings from the rules together with `,` commas.
21
22
  toString() {
22
- return this._rules.map(toString).join(",");
23
+ return this._rules.map(String).join(",");
23
24
  }
24
25
  /** Clone this set of rules but add additional rules. */
25
26
  with(...rules) {
package/query/Sort.js CHANGED
@@ -19,6 +19,6 @@ export class Sort extends Rule {
19
19
  return sortItems(items, this);
20
20
  }
21
21
  toString() {
22
- return `${this.direction === "DESC" ? "!" : ""}${this.key}`;
22
+ return `"${this.direction === "DESC" ? "!" : ""}${this.key}"`;
23
23
  }
24
24
  }
@@ -0,0 +1,26 @@
1
+ /// <reference types="react" />
2
+ /**
3
+ * Controller that creates an independent cache.
4
+ * - The cache is a `Map` instance that stores indexed items.
5
+ */
6
+ export declare class CacheController<T> {
7
+ protected _context: import("react").Context<Map<string, T> | undefined>;
8
+ /** Use this cache in a component. */
9
+ readonly useCache: () => Map<string, T>;
10
+ /** Component that provides a cache of this type to its children. */
11
+ readonly Cache: ({ children }: {
12
+ children: React.ReactNode;
13
+ }) => React.ReactElement | null;
14
+ }
15
+ /**
16
+ * Default cache
17
+ * - This is a flexible generic cache intended to be the default.
18
+ * - Use this cache unless
19
+ */
20
+ export declare const CACHE: CacheController<any>;
21
+ /** Use a global cache in a component. */
22
+ export declare const useCache: () => Map<string, any>;
23
+ /** Component that provides a global cache to its children. */
24
+ export declare const Cache: ({ children }: {
25
+ children: React.ReactNode;
26
+ }) => React.ReactElement | null;
@@ -0,0 +1,36 @@
1
+ import { useContext, createContext, createElement } from "react";
2
+ import { ConditionError } from "../error/ConditionError.js";
3
+ import { useReduce } from "./useReduce.js";
4
+ /**
5
+ * Controller that creates an independent cache.
6
+ * - The cache is a `Map` instance that stores indexed items.
7
+ */
8
+ export class CacheController {
9
+ constructor() {
10
+ this._context = createContext(undefined);
11
+ /** Use this cache in a component. */
12
+ this.useCache = () => {
13
+ const cache = useContext(this._context);
14
+ if (!cache)
15
+ throw new ConditionError("useCache() must be used inside <Cache>");
16
+ return cache;
17
+ };
18
+ /** Component that provides a cache of this type to its children. */
19
+ this.Cache = ({ children }) => {
20
+ const cache = useReduce(_reduceMap);
21
+ return createElement(this._context.Provider, { children, value: cache });
22
+ };
23
+ }
24
+ }
25
+ /** Reducer that gets an existing `Map` instance or creates a new one. */
26
+ const _reduceMap = (previous) => previous || new Map();
27
+ /**
28
+ * Default cache
29
+ * - This is a flexible generic cache intended to be the default.
30
+ * - Use this cache unless
31
+ */
32
+ export const CACHE = new CacheController(); // eslint-disable-line @typescript-eslint/no-explicit-any
33
+ /** Use a global cache in a component. */
34
+ export const useCache = CACHE.useCache;
35
+ /** Component that provides a global cache to its children. */
36
+ export const Cache = CACHE.Cache;
@@ -1,35 +1,28 @@
1
1
  import type { Unsubscribe } from "../observe/Observable.js";
2
2
  import type { DocumentReference } from "../db/Reference.js";
3
- import type { Data, OptionalEntity } from "../util/data.js";
4
- import { Entity } from "../util/data.js";
3
+ import type { Data, OptionalEntity, Entity } from "../util/data.js";
5
4
  import { State } from "../state/State.js";
6
- import { MemoryTable } from "../provider/MemoryProvider.js";
7
5
  import { BooleanState } from "../state/BooleanState.js";
8
6
  /** Hold the current state of a document. */
9
7
  export declare class DocumentState<T extends Data> extends State<OptionalEntity<T>> {
10
8
  readonly ref: DocumentReference<T>;
11
9
  readonly busy: BooleanState;
12
- protected readonly _table: MemoryTable<T>;
13
- /** Time this state was last updated with a new value. */
14
- get time(): number | undefined;
15
- /** How old this state's value is (in milliseconds). */
16
- get age(): number;
10
+ /** Get the data of the document (throws `RequiredError` if document doesn't exist). */
17
11
  get data(): Entity<T>;
12
+ /** Does the document exist (i.e. its value isn't `null`)? */
13
+ get exists(): boolean;
18
14
  constructor(ref: DocumentReference<T>);
19
15
  /** Refresh this state from the source provider. */
20
- refresh(): Promise<void>;
16
+ readonly refresh: () => Promise<void>;
21
17
  /** Refresh this state if data in the cache is older than `maxAge` (in milliseconds). */
22
18
  refreshStale(maxAge: number): void;
23
19
  /** Subscribe this state to the source provider. */
24
20
  connectSource(): Unsubscribe;
21
+ /** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
22
+ connectCache(): Unsubscribe | void;
25
23
  protected _addFirstObserver(): void;
26
24
  protected _removeLastObserver(): void;
27
25
  }
28
- /**
29
- * Use a document in a React component.
30
- * - Use `useDocument(ref).data` to get the data of the document.
31
- * - Use `useDocument(ref).value` to get the data of the document or `null` if it doesn't exist.
32
- * - Use `useDocument(ref).exists` to check if the document is loaded before accessing `.data` or `.value`
33
- */
26
+ /** Use a document in a React component. */
34
27
  export declare function useDocument<T extends Data>(ref: DocumentReference<T>): DocumentState<T>;
35
28
  export declare function useDocument<T extends Data>(ref?: DocumentReference<T>): DocumentState<T> | undefined;
@@ -1,69 +1,72 @@
1
+ import { reduceMapItem } from "../util/map.js";
1
2
  import { getDocumentData, isSameReference } from "../db/Reference.js";
2
3
  import { CacheProvider } from "../provider/CacheProvider.js";
3
4
  import { findSourceProvider } from "../provider/ThroughProvider.js";
4
5
  import { State } from "../state/State.js";
5
- import { MatchObserver } from "../observe/MatchObserver.js";
6
6
  import { BooleanState } from "../state/BooleanState.js";
7
7
  import { ConditionError } from "../error/ConditionError.js";
8
- import { dispatch } from "../util/function.js";
9
- import { useReduce } from "./useReduce.js";
8
+ import { NOVALUE } from "../util/constants.js";
10
9
  import { useSubscribe } from "./useSubscribe.js";
10
+ import { useCache } from "./useCache.js";
11
11
  /** Hold the current state of a document. */
12
12
  export class DocumentState extends State {
13
13
  constructor(ref) {
14
- super();
14
+ var _a;
15
+ const table = (_a = findSourceProvider(ref.db.provider, CacheProvider)) === null || _a === void 0 ? void 0 : _a.memory.getTable(ref);
16
+ const time = table ? table.getDocumentTime(ref.id) : null;
17
+ const isCached = typeof time === "number";
18
+ super(table && isCached ? table.getDocument(ref.id) : NOVALUE);
15
19
  this.busy = new BooleanState();
16
- this._table = findSourceProvider(ref.db.provider, CacheProvider).memory.getTable(ref);
20
+ /** Refresh this state from the source provider. */
21
+ this.refresh = async () => {
22
+ if (this.closed)
23
+ throw new ConditionError("State is closed");
24
+ if (!this.busy.value) {
25
+ this.busy.next(true);
26
+ try {
27
+ const result = await this.ref.value;
28
+ this.next(result);
29
+ }
30
+ catch (thrown) {
31
+ this.error(thrown);
32
+ }
33
+ finally {
34
+ this.busy.next(false);
35
+ }
36
+ }
37
+ };
38
+ this._time = time;
17
39
  this.ref = ref;
18
- // If the result is cached use it as the initial value.
19
- const isCached = typeof this._table.getDocumentTime(ref.id) === "number";
20
- if (isCached)
21
- this.next(this._table.getDocument(ref.id)); // Use the existing cached value.
22
- else
23
- dispatch(this.refresh); // Queue a request to refresh the value.
24
- }
25
- /** Time this state was last updated with a new value. */
26
- get time() {
27
- return this._table.getDocumentTime(this.ref.id);
28
- }
29
- /** How old this state's value is (in milliseconds). */
30
- get age() {
31
- const time = this.time;
32
- return typeof time === "number" ? Date.now() - time : Infinity;
40
+ // Queue a request to refresh the value if it doesn't exist.
41
+ if (this.loading)
42
+ void this.refresh();
33
43
  }
44
+ /** Get the data of the document (throws `RequiredError` if document doesn't exist). */
34
45
  get data() {
35
46
  return getDocumentData(this.value, this.ref);
36
47
  }
37
- /** Refresh this state from the source provider. */
38
- async refresh() {
39
- if (this.closed)
40
- throw new ConditionError("State is closed");
41
- if (!this.busy.value) {
42
- try {
43
- this.busy.next(true);
44
- const result = await this.ref.value;
45
- this.busy.next(false);
46
- this.next(result);
47
- }
48
- catch (thrown) {
49
- this.error(thrown);
50
- }
51
- }
48
+ /** Does the document exist (i.e. its value isn't `null`)? */
49
+ get exists() {
50
+ return !!this.value;
52
51
  }
53
52
  /** Refresh this state if data in the cache is older than `maxAge` (in milliseconds). */
54
53
  refreshStale(maxAge) {
55
54
  if (this.age > maxAge)
56
- dispatch(this.refresh);
55
+ void this.refresh();
57
56
  }
58
57
  /** Subscribe this state to the source provider. */
59
58
  connectSource() {
60
59
  return this.connect(() => this.ref.subscribe({}));
61
60
  }
61
+ /** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
62
+ connectCache() {
63
+ var _a;
64
+ const table = (_a = findSourceProvider(this.ref.db.provider, CacheProvider)) === null || _a === void 0 ? void 0 : _a.memory.getTable(this.ref);
65
+ table && this.connect(() => table.subscribeCachedDocument(this.ref.id, this));
66
+ }
62
67
  // Override to subscribe to the cache when an observer is added.
63
68
  _addFirstObserver() {
64
- // Connect this state to the source.
65
- // Connect through a `MatchObserver` that only dispatches `next()` if the document is actually cached (it might just be `null` because no document has been cached yet).
66
- this.connect(() => this._table.subscribeDocument(this.ref.id, new MatchObserver(() => this._table.getDocumentTime(this.ref.id) !== undefined, this)));
69
+ this.connectCache();
67
70
  }
68
71
  // Override to unsubscribe from the cache when an observer is removed.
69
72
  _removeLastObserver() {
@@ -72,9 +75,10 @@ export class DocumentState extends State {
72
75
  }
73
76
  }
74
77
  /** Reuse the previous `DocumentState` or create a new one. */
75
- const _getDocumentState = (previous, ref) => !ref ? undefined : previous && isSameReference(previous.ref, ref) ? previous : new DocumentState(ref);
78
+ const _reduceDocumentState = (existing, ref) => (existing && isSameReference(existing.ref, ref) ? existing : new DocumentState(ref));
76
79
  export function useDocument(ref) {
77
- const state = useReduce(_getDocumentState, ref);
80
+ const cache = useCache();
81
+ const state = ref ? reduceMapItem(cache, ref.toString(), _reduceDocumentState, ref) : undefined;
78
82
  useSubscribe(state);
79
83
  return state;
80
84
  }
@@ -1,20 +1,13 @@
1
1
  import type { Unsubscribe } from "../observe/Observable.js";
2
2
  import type { QueryReference } from "../db/Reference.js";
3
- import type { Data, Entities, OptionalEntity } from "../util/data.js";
4
- import { Entity } from "../util/data.js";
3
+ import type { Data, Entities, OptionalEntity, Entity } from "../util/data.js";
5
4
  import { State } from "../state/State.js";
6
- import { MemoryTable } from "../provider/MemoryProvider.js";
7
5
  import { BooleanState } from "../state/BooleanState.js";
8
6
  /** Hold the current state of a query. */
9
7
  export declare class QueryState<T extends Data> extends State<Entities<T>> {
10
8
  readonly ref: QueryReference<T>;
11
9
  readonly busy: BooleanState;
12
10
  readonly limit: number;
13
- protected readonly _table: MemoryTable<T>;
14
- /** Time this state was last updated with a new value. */
15
- get time(): number | undefined;
16
- /** How old this state's value is (in milliseconds). */
17
- get age(): number;
18
11
  /** Can more items be loaded after the current result. */
19
12
  get hasMore(): boolean;
20
13
  protected _hasMore: boolean;
@@ -26,26 +19,27 @@ export declare class QueryState<T extends Data> extends State<Entities<T>> {
26
19
  get lastValue(): OptionalEntity<T>;
27
20
  /** Get the last document matched by this query. */
28
21
  get lastData(): Entity<T>;
22
+ /** Does the document have at least one result. */
23
+ get exists(): boolean;
24
+ /** Get the number of items matching this query. */
25
+ get count(): number;
29
26
  constructor(ref: QueryReference<T>);
30
27
  /** Refresh this state from the source provider. */
31
- refresh: () => Promise<void>;
28
+ readonly refresh: () => Promise<void>;
32
29
  /** Refresh this state if data in the cache is older than `maxAge` (in milliseconds). */
33
30
  refreshStale(maxAge: number): void;
34
31
  /** Subscribe this state to the source provider. */
35
32
  connectSource(): Unsubscribe;
33
+ /** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
34
+ connectCache(): Unsubscribe | void;
36
35
  protected _addFirstObserver(): void;
37
36
  protected _removeLastObserver(): void;
38
37
  /**
39
38
  * Load more items after the last once.
40
39
  * - Promise that needs to be handled.
41
40
  */
42
- loadMore: () => Promise<void>;
41
+ readonly loadMore: () => Promise<void>;
43
42
  }
44
- /**
45
- * Use a query in a React component.
46
- * - Use `useQuery(ref).data` to get the data of the query.
47
- * - Use `useQuery(ref).value` to get the data of the query or `null` if it doesn't exist.
48
- * - Use `useQuery(ref).exists` to check if the query is loaded before accessing `.data` or `.value`
49
- */
43
+ /** Use a query in a React component. */
50
44
  export declare function useQuery<T extends Data>(ref: QueryReference<T>): QueryState<T>;
51
45
  export declare function useQuery<T extends Data>(ref?: QueryReference<T>): QueryState<T> | undefined;
package/react/useQuery.js CHANGED
@@ -1,18 +1,21 @@
1
+ import { reduceMapItem } from "../util/map.js";
1
2
  import { getQueryFirstData, getQueryFirstValue, isSameReference } from "../db/Reference.js";
2
3
  import { CacheProvider } from "../provider/CacheProvider.js";
3
4
  import { findSourceProvider } from "../provider/ThroughProvider.js";
4
5
  import { State } from "../state/State.js";
5
- import { MatchObserver } from "../observe/MatchObserver.js";
6
6
  import { ConditionError } from "../error/ConditionError.js";
7
7
  import { BooleanState } from "../state/BooleanState.js";
8
- import { dispatch } from "../util/function.js";
9
- import { useReduce } from "./useReduce.js";
8
+ import { NOVALUE } from "../util/constants.js";
10
9
  import { useSubscribe } from "./useSubscribe.js";
10
+ import { useCache } from "./useCache.js";
11
11
  /** Hold the current state of a query. */
12
12
  export class QueryState extends State {
13
13
  constructor(ref) {
14
- var _a;
15
- super();
14
+ var _a, _b;
15
+ const table = (_a = findSourceProvider(ref.db.provider, CacheProvider)) === null || _a === void 0 ? void 0 : _a.memory.getTable(ref);
16
+ const time = table ? table.getQueryTime(ref) : null;
17
+ const isCached = typeof time === "number";
18
+ super(table && isCached ? table.getQuery(ref) : NOVALUE);
16
19
  this.busy = new BooleanState();
17
20
  this._hasMore = false;
18
21
  /** Refresh this state from the source provider. */
@@ -20,16 +23,18 @@ export class QueryState extends State {
20
23
  if (this.closed)
21
24
  throw new ConditionError("State is closed");
22
25
  if (!this.busy.value) {
26
+ this.busy.next(true);
23
27
  try {
24
- this.busy.next(true);
25
28
  const result = await this.ref.value;
26
29
  this._hasMore = result.length < this.limit;
27
- this.busy.next(false);
28
30
  this.next(result);
29
31
  }
30
32
  catch (thrown) {
31
33
  this.error(thrown);
32
34
  }
35
+ finally {
36
+ this.busy.next(false);
37
+ }
33
38
  }
34
39
  };
35
40
  /**
@@ -40,36 +45,26 @@ export class QueryState extends State {
40
45
  if (this.closed)
41
46
  throw new ConditionError("State is closed");
42
47
  if (!this.busy.value) {
48
+ this.busy.next(true);
43
49
  try {
44
- this.busy.next(true);
45
50
  const items = await this.ref.after(this.lastData).value;
46
51
  this.next([...this.value, ...items]);
47
52
  this._hasMore = items.length < this.limit;
48
- this.busy.next(false);
49
53
  }
50
54
  catch (thrown) {
51
55
  this.error(thrown);
52
56
  }
57
+ finally {
58
+ this.busy.next(false);
59
+ }
53
60
  }
54
61
  };
55
- this._table = findSourceProvider(ref.db.provider, CacheProvider).memory.getTable(ref);
62
+ this._time = time;
56
63
  this.ref = ref;
57
- this.limit = (_a = ref.limit) !== null && _a !== void 0 ? _a : Infinity;
58
- // If the result is cached use it as the initial value.
59
- const isCached = typeof this._table.getQueryTime(ref) === "number";
60
- if (isCached)
61
- this.next(this._table.getQuery(ref)); // Use the existing cached value.
62
- else
63
- dispatch(this.refresh); // Queue a request to refresh the value.
64
- }
65
- /** Time this state was last updated with a new value. */
66
- get time() {
67
- return this._table.getQueryTime(this.ref);
68
- }
69
- /** How old this state's value is (in milliseconds). */
70
- get age() {
71
- const time = this.time;
72
- return typeof time === "number" ? Date.now() - time : Infinity;
64
+ this.limit = (_b = ref.limit) !== null && _b !== void 0 ? _b : Infinity;
65
+ // Queue a request to refresh the value if it doesn't exist.
66
+ if (this.loading)
67
+ void this.refresh();
73
68
  }
74
69
  /** Can more items be loaded after the current result. */
75
70
  get hasMore() {
@@ -91,20 +86,32 @@ export class QueryState extends State {
91
86
  get lastData() {
92
87
  return getQueryFirstData(this.value, this.ref);
93
88
  }
89
+ /** Does the document have at least one result. */
90
+ get exists() {
91
+ return !!this.value.length;
92
+ }
93
+ /** Get the number of items matching this query. */
94
+ get count() {
95
+ return this.value.length;
96
+ }
94
97
  /** Refresh this state if data in the cache is older than `maxAge` (in milliseconds). */
95
98
  refreshStale(maxAge) {
96
- if (!this.busy.value && this.age > maxAge)
97
- dispatch(this.refresh);
99
+ if (this.age > maxAge)
100
+ void this.refresh();
98
101
  }
99
102
  /** Subscribe this state to the source provider. */
100
103
  connectSource() {
101
104
  return this.connect(() => this.ref.subscribe({}));
102
105
  }
106
+ /** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
107
+ connectCache() {
108
+ var _a;
109
+ const table = (_a = findSourceProvider(this.ref.db.provider, CacheProvider)) === null || _a === void 0 ? void 0 : _a.memory.getTable(this.ref);
110
+ return table && this.connect(() => table.subscribeCachedQuery(this.ref, this));
111
+ }
103
112
  // Override to subscribe to the cache when an observer is added.
104
113
  _addFirstObserver() {
105
- // Connect this state to the source.
106
- // Connect through a `MatchObserver` that only dispatches `next()` if the query is actually cached (it might just be `[]` because no query has been cached yet).
107
- this.connect(() => this._table.subscribeQuery(this.ref, new MatchObserver(() => this._table.getQueryTime(this.ref) !== undefined, this)));
114
+ this.connectCache();
108
115
  }
109
116
  // Override to unsubscribe from the cache when an observer is removed.
110
117
  _removeLastObserver() {
@@ -113,9 +120,10 @@ export class QueryState extends State {
113
120
  }
114
121
  }
115
122
  /** Reuse the previous `QueryState` or create a new one. */
116
- const _getQueryState = (previous, ref) => !ref ? undefined : previous && isSameReference(previous.ref, ref) ? previous : new QueryState(ref);
123
+ const _reduceQueryState = (existing, ref) => (existing && isSameReference(existing.ref, ref) ? existing : new QueryState(ref));
117
124
  export function useQuery(ref) {
118
- const state = useReduce(_getQueryState, ref);
125
+ const cache = useCache();
126
+ const state = ref ? reduceMapItem(cache, ref.toString(), _reduceQueryState, ref) : undefined;
119
127
  useSubscribe(state);
120
128
  return state;
121
129
  }
@@ -4,7 +4,7 @@ import { State } from "./State.js";
4
4
  export declare class ArrayState<T> extends State<ImmutableArray<T>> implements Iterable<T> {
5
5
  constructor(initial?: ImmutableArray<T>);
6
6
  /** Get the length of the current value of this state. */
7
- get length(): number;
7
+ get count(): number;
8
8
  /** Add an item to this array. */
9
9
  add(item: T): void;
10
10
  /** Remove an item from this array. */
@@ -6,7 +6,7 @@ export class ArrayState extends State {
6
6
  super(initial);
7
7
  }
8
8
  /** Get the length of the current value of this state. */
9
- get length() {
9
+ get count() {
10
10
  return this.value.length;
11
11
  }
12
12
  /** Add an item to this array. */
@@ -12,10 +12,10 @@ export declare class DataState<T extends Data> extends State<T> {
12
12
  }
13
13
  /** State that stores an optional data object and has additional methods to help with that. */
14
14
  export declare class OptionalDataState<T extends Data> extends State<OptionalData<T>> {
15
- /** Get the result value of this state. */
16
- get result(): OptionalData<T>;
17
15
  /** Get current data value of this state (or throw `Promise` that resolves to the next required value). */
18
16
  get data(): T;
17
+ /** Does the data exist or not? */
18
+ get exists(): boolean;
19
19
  /** Set a prop in this object to a new value. */
20
20
  set<K extends Key<T>>(key: K, value: T[K]): void;
21
21
  /** Update several props in this object. */
@@ -1,7 +1,5 @@
1
1
  import { getData, withProp } from "../util/data.js";
2
2
  import { transformData } from "../util/transform.js";
3
- import { awaitNext } from "../observe/util.js";
4
- import { NOERROR } from "../util/constants.js";
5
3
  import { State } from "./State.js";
6
4
  /** State that stores a data object and has additional methods to help with that. */
7
5
  export class DataState extends State {
@@ -20,18 +18,14 @@ export class DataState extends State {
20
18
  }
21
19
  /** State that stores an optional data object and has additional methods to help with that. */
22
20
  export class OptionalDataState extends State {
23
- /** Get the result value of this state. */
24
- get result() {
25
- return this.value;
26
- }
27
21
  /** Get current data value of this state (or throw `Promise` that resolves to the next required value). */
28
22
  get data() {
29
- if (this.reason !== NOERROR)
30
- throw this.reason;
31
- if (this.loading)
32
- throw awaitNext(this).then(getData);
33
23
  return getData(this.value);
34
24
  }
25
+ /** Does the data exist or not? */
26
+ get exists() {
27
+ return !!this.value;
28
+ }
35
29
  /** Set a prop in this object to a new value. */
36
30
  set(key, value) {
37
31
  this.next(withProp(this.data, key, value));
@@ -5,7 +5,7 @@ import { State } from "./State.js";
5
5
  export declare class ObjectState<T> extends State<ImmutableObject<T>> implements Iterable<Entry<T>> {
6
6
  constructor(initial?: ImmutableObject<T>);
7
7
  /** Get the length of the current value of this state. */
8
- get length(): number;
8
+ get count(): number;
9
9
  /** Remove a named entry from this object. */
10
10
  delete(key: string): void;
11
11
  /** Set a named entry in this object with a different value. */
@@ -6,7 +6,7 @@ export class ObjectState extends State {
6
6
  super(initial);
7
7
  }
8
8
  /** Get the length of the current value of this state. */
9
- get length() {
9
+ get count() {
10
10
  return Object.keys(this.value).length;
11
11
  }
12
12
  /** Remove a named entry from this object. */
package/state/State.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { NOERROR } from "../util/constants.js";
1
+ import { NOVALUE, NOERROR } from "../util/constants.js";
2
2
  import { Matchable } from "../util/match.js";
3
3
  import { Subject } from "../observe/Subject.js";
4
4
  import { Observer } from "../observe/Observer.js";
@@ -20,7 +20,14 @@ export declare class State<T> extends Subject<T> implements Matchable<T, void> {
20
20
  /** Most recently dispatched value (or throw `Promise` that resolves to the next value). */
21
21
  get value(): T;
22
22
  private _value;
23
+ /** Time this state was last updated with a new value. */
24
+ get time(): number | null;
25
+ protected _time: number | null;
26
+ /** How old this state's value is (in milliseconds). */
27
+ get age(): number;
23
28
  /** State is initiated with an initial state. */
29
+ constructor(initial: T | typeof NOVALUE);
30
+ constructor();
24
31
  constructor(...args: [] | [T]);
25
32
  /** Is there a current value, or is it still loading. */
26
33
  get loading(): boolean;
package/state/State.js CHANGED
@@ -14,12 +14,12 @@ import { dispatchComplete, dispatchError, dispatchNext } from "../observe/Observ
14
14
  * - To set the state to an explicit value, use that value or another `State` instance with a value.
15
15
  * */
16
16
  export class State extends Subject {
17
- /** State is initiated with an initial state. */
18
17
  constructor(...args) {
19
18
  super();
20
19
  /** Cached reason this state errored. */
21
20
  this.reason = NOERROR;
22
21
  this._value = args.length ? args[0] : NOVALUE;
22
+ this._time = args.length ? Date.now() : null;
23
23
  }
24
24
  /** Most recently dispatched value (or throw `Promise` that resolves to the next value). */
25
25
  get value() {
@@ -29,6 +29,15 @@ export class State extends Subject {
29
29
  throw awaitNext(this);
30
30
  return this._value;
31
31
  }
32
+ /** Time this state was last updated with a new value. */
33
+ get time() {
34
+ return this._time;
35
+ }
36
+ /** How old this state's value is (in milliseconds). */
37
+ get age() {
38
+ const time = this.time;
39
+ return time !== null ? Date.now() - time : Infinity;
40
+ }
32
41
  /** Is there a current value, or is it still loading. */
33
42
  get loading() {
34
43
  return this._value === NOVALUE;
@@ -59,6 +68,7 @@ export class State extends Subject {
59
68
  // Override to save value that is dispatched.
60
69
  _dispatch(value) {
61
70
  this._value = value;
71
+ this._time = Date.now();
62
72
  super._dispatch(value);
63
73
  }
64
74
  // Implement Matchable to see if a value matches the current value of this state.
package/util/map.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Entry } from "./entry.js";
2
+ import type { Arguments } from "./function.js";
2
3
  /**
3
4
  * `Map` with string keys that cannot be changed.
4
5
  * - Only allows keys to be `string`
@@ -20,3 +21,5 @@ export declare type PossibleMap<T> = ImmutableMap<T> | Iterable<Entry<T>>;
20
21
  /** Convert an iterable to a `Map` (if it's already a `Map` it passes through unchanged). */
21
22
  export declare function getMap<T>(iterable: ImmutableMap<T> | Iterable<Entry<T>>): ImmutableMap<T>;
22
23
  export declare function getMap<T>(iterable: PossibleMap<T>): ImmutableMap<T>;
24
+ /** Function that lets new items in a map be created and updated by calling a `reduce()` callback that receives the existing value. */
25
+ export declare function reduceMapItem<K, T, A extends Arguments = []>(map: Map<K, T>, key: K, reduce: (existing: T | undefined, ...a: A) => T, ...args: A): T;
package/util/map.js CHANGED
@@ -8,3 +8,11 @@ export function limitMap(map, limit) {
8
8
  export function getMap(iterable) {
9
9
  return iterable instanceof Map ? iterable : new Map(iterable);
10
10
  }
11
+ /** Function that lets new items in a map be created and updated by calling a `reduce()` callback that receives the existing value. */
12
+ export function reduceMapItem(map, key, reduce, ...args) {
13
+ const existing = map.get(key);
14
+ const next = reduce(existing, ...args);
15
+ if (existing !== next)
16
+ map.set(key, next);
17
+ return next;
18
+ }