shelving 1.66.0 → 1.68.1

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/index.js CHANGED
@@ -19,5 +19,8 @@ export * from "./update/index.js";
19
19
  export * from "./util/index.js";
20
20
  // Integrations.
21
21
  // export * from "./react/index.js"; // Not exported.
22
+ // export * from "./firestore/client/index.js"; // Not exported.
23
+ // export * from "./firestore/lite/index.js"; // Not exported.
24
+ // export * from "./firestore/server/index.js"; // Not exported.
22
25
  // Testing.
23
26
  // export * from "./test/index.js"; // Not exported.
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.66.0",
14
+ "version": "1.68.1",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
@@ -29,11 +29,12 @@
29
29
  "./firestore/lite": "./firestore/lite/index.js",
30
30
  "./firestore/server": "./firestore/server/index.js",
31
31
  "./markup": "./markup/index.js",
32
+ "./observe": "./observe/index.js",
32
33
  "./provider": "./provider/index.js",
33
34
  "./query": "./query/index.js",
34
35
  "./react": "./react/index.js",
35
36
  "./schema": "./schema/index.js",
36
- "./stream": "./stream/index.js",
37
+ "./state": "./state/index.js",
37
38
  "./test": "./test/index.js",
38
39
  "./update": "./update/index.js",
39
40
  "./util": "./util/index.js"
@@ -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
  }
package/react/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./useLazy.js";
2
2
  export * from "./useReduce.js";
3
3
  export * from "./useInstance.js";
4
+ export * from "./useCache.js";
4
5
  export * from "./useSubscribe.js";
5
6
  export * from "./useDocument.js";
6
7
  export * from "./useQuery.js";
package/react/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export * from "./useLazy.js";
3
3
  export * from "./useReduce.js";
4
4
  export * from "./useInstance.js";
5
+ export * from "./useCache.js";
5
6
  // Observables.
6
7
  export * from "./useSubscribe.js";
7
8
  // DB.
@@ -0,0 +1,46 @@
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 you want to cache a completely independent set of items without interference.
19
+ */
20
+ export declare const CACHE: CacheController<any>;
21
+ /**
22
+ * Use a global cache in a component.
23
+ * - Throws an error if used outside of `<Cache>`
24
+ */
25
+ export declare const useCache: () => Map<string, any>;
26
+ /**
27
+ * Component that provides a global cache to its children.
28
+ *
29
+ * Note: If mounted globally this cache will bloat over time, so you need a strategy to clear or reset the cache occasionally.
30
+ *
31
+ * A good strategy is to wrap a separate `<Cache>` around each page of your app.
32
+ * This means the cache can only grow to the size of each page and the memory is released when the user navigates to a new page.
33
+ * You might need to use `<Cache key="something unique to the page">` to ensure the cache component is destroyed and remounted for each page.
34
+ *
35
+ * Put a `<Suspense>` boundary _inside_ `<Cache>`
36
+ * - This prevents promises being thrown up through the cache causing it to be destroyed.
37
+ * - When the promise resolves and the render is tried again the data would not exist (because the cache was destroyed).
38
+ * - This will cause an infinite loading loop.
39
+ *
40
+ * Put your error boundary _outside_ your `<Cache>`
41
+ * - The error being thrown up through the cache causes it to be destroyed.
42
+ * - This means when the uses tells the error boundary to try again (if supported) all data on the page will be retried.
43
+ */
44
+ export declare const Cache: ({ children }: {
45
+ children: React.ReactNode;
46
+ }) => React.ReactElement | null;
@@ -0,0 +1,56 @@
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 you want to cache a completely independent set of items without interference.
31
+ */
32
+ export const CACHE = new CacheController(); // eslint-disable-line @typescript-eslint/no-explicit-any
33
+ /**
34
+ * Use a global cache in a component.
35
+ * - Throws an error if used outside of `<Cache>`
36
+ */
37
+ export const useCache = CACHE.useCache;
38
+ /**
39
+ * Component that provides a global cache to its children.
40
+ *
41
+ * Note: If mounted globally this cache will bloat over time, so you need a strategy to clear or reset the cache occasionally.
42
+ *
43
+ * A good strategy is to wrap a separate `<Cache>` around each page of your app.
44
+ * This means the cache can only grow to the size of each page and the memory is released when the user navigates to a new page.
45
+ * You might need to use `<Cache key="something unique to the page">` to ensure the cache component is destroyed and remounted for each page.
46
+ *
47
+ * Put a `<Suspense>` boundary _inside_ `<Cache>`
48
+ * - This prevents promises being thrown up through the cache causing it to be destroyed.
49
+ * - When the promise resolves and the render is tried again the data would not exist (because the cache was destroyed).
50
+ * - This will cause an infinite loading loop.
51
+ *
52
+ * Put your error boundary _outside_ your `<Cache>`
53
+ * - The error being thrown up through the cache causes it to be destroyed.
54
+ * - This means when the uses tells the error boundary to try again (if supported) all data on the page will be retried.
55
+ */
56
+ export const Cache = CACHE.Cache;
@@ -1,19 +1,12 @@
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;
17
10
  /** Get the data of the document (throws `RequiredError` if document doesn't exist). */
18
11
  get data(): Entity<T>;
19
12
  /** Does the document exist (i.e. its value isn't `null`)? */
@@ -25,14 +18,14 @@ export declare class DocumentState<T extends Data> extends State<OptionalEntity<
25
18
  refreshStale(maxAge: number): void;
26
19
  /** Subscribe this state to the source provider. */
27
20
  connectSource(): Unsubscribe;
21
+ /** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
22
+ connectCache(): Unsubscribe | void;
28
23
  protected _addFirstObserver(): void;
29
24
  protected _removeLastObserver(): void;
30
25
  }
31
26
  /**
32
27
  * Use a document in a React component.
33
- * - Use `useDocument(ref).data` to get the data of the document.
34
- * - Use `useDocument(ref).value` to get the data of the document or `null` if it doesn't exist.
35
- * - Use `useDocument(ref).exists` to check if the document is loaded before accessing `.data` or `.value`
28
+ * - Uses the default cache, so will error if not used inside `<Cache>`
36
29
  */
37
30
  export declare function useDocument<T extends Data>(ref: DocumentReference<T>): DocumentState<T>;
38
31
  export declare function useDocument<T extends Data>(ref?: DocumentReference<T>): DocumentState<T> | undefined;
@@ -1,16 +1,21 @@
1
- import { getDocumentData, isSameReference } from "../db/Reference.js";
1
+ import { reduceMapItem } from "../util/map.js";
2
+ import { getDocumentData } 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 { useReduce } from "./useReduce.js";
8
+ import { NOVALUE } from "../util/constants.js";
9
9
  import { useSubscribe } from "./useSubscribe.js";
10
+ import { useCache } from "./useCache.js";
10
11
  /** Hold the current state of a document. */
11
12
  export class DocumentState extends State {
12
13
  constructor(ref) {
13
- 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);
14
19
  this.busy = new BooleanState();
15
20
  /** Refresh this state from the source provider. */
16
21
  this.refresh = async () => {
@@ -30,23 +35,11 @@ export class DocumentState extends State {
30
35
  }
31
36
  }
32
37
  };
33
- this._table = findSourceProvider(ref.db.provider, CacheProvider).memory.getTable(ref);
38
+ this._time = time;
34
39
  this.ref = ref;
35
- // If the result is cached use it as the initial value.
36
- const isCached = typeof this._table.getDocumentTime(ref.id) === "number";
37
- if (isCached)
38
- this.next(this._table.getDocument(ref.id)); // Use the existing cached value.
39
- else
40
- void this.refresh(); // Queue a request to refresh the value.
41
- }
42
- /** Time this state was last updated with a new value. */
43
- get time() {
44
- return this._table.getDocumentTime(this.ref.id);
45
- }
46
- /** How old this state's value is (in milliseconds). */
47
- get age() {
48
- const time = this.time;
49
- 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();
50
43
  }
51
44
  /** Get the data of the document (throws `RequiredError` if document doesn't exist). */
52
45
  get data() {
@@ -65,11 +58,15 @@ export class DocumentState extends State {
65
58
  connectSource() {
66
59
  return this.connect(() => this.ref.subscribe({}));
67
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
+ }
68
67
  // Override to subscribe to the cache when an observer is added.
69
68
  _addFirstObserver() {
70
- // Connect this state to the source.
71
- // 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).
72
- this.connect(() => this._table.subscribeDocument(this.ref.id, new MatchObserver(() => this._table.getDocumentTime(this.ref.id) !== undefined, this)));
69
+ this.connectCache();
73
70
  }
74
71
  // Override to unsubscribe from the cache when an observer is removed.
75
72
  _removeLastObserver() {
@@ -78,9 +75,10 @@ export class DocumentState extends State {
78
75
  }
79
76
  }
80
77
  /** Reuse the previous `DocumentState` or create a new one. */
81
- const _getDocumentState = (previous, ref) => !ref ? undefined : previous && isSameReference(previous.ref, ref) ? previous : new DocumentState(ref);
78
+ const _reduceDocumentState = (existing, ref) => existing || new DocumentState(ref);
82
79
  export function useDocument(ref) {
83
- const state = useReduce(_getDocumentState, ref);
80
+ const cache = useCache();
81
+ const state = ref ? reduceMapItem(cache, ref.toString(), _reduceDocumentState, ref) : undefined;
84
82
  useSubscribe(state);
85
83
  return state;
86
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;
@@ -37,6 +30,8 @@ export declare class QueryState<T extends Data> extends State<Entities<T>> {
37
30
  refreshStale(maxAge: number): void;
38
31
  /** Subscribe this state to the source provider. */
39
32
  connectSource(): Unsubscribe;
33
+ /** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
34
+ connectCache(): Unsubscribe | void;
40
35
  protected _addFirstObserver(): void;
41
36
  protected _removeLastObserver(): void;
42
37
  /**
@@ -47,9 +42,7 @@ export declare class QueryState<T extends Data> extends State<Entities<T>> {
47
42
  }
48
43
  /**
49
44
  * Use a query in a React component.
50
- * - Use `useQuery(ref).data` to get the data of the query.
51
- * - Use `useQuery(ref).value` to get the data of the query or `null` if it doesn't exist.
52
- * - Use `useQuery(ref).exists` to check if the query is loaded before accessing `.data` or `.value`
45
+ * - Uses the default cache, so will error if not used inside `<Cache>`
53
46
  */
54
47
  export declare function useQuery<T extends Data>(ref: QueryReference<T>): QueryState<T>;
55
48
  export declare function useQuery<T extends Data>(ref?: QueryReference<T>): QueryState<T> | undefined;
package/react/useQuery.js CHANGED
@@ -1,17 +1,21 @@
1
- import { getQueryFirstData, getQueryFirstValue, isSameReference } from "../db/Reference.js";
1
+ import { reduceMapItem } from "../util/map.js";
2
+ import { getQueryFirstData, getQueryFirstValue } 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 { useReduce } from "./useReduce.js";
8
+ import { NOVALUE } from "../util/constants.js";
9
9
  import { useSubscribe } from "./useSubscribe.js";
10
+ import { useCache } from "./useCache.js";
10
11
  /** Hold the current state of a query. */
11
12
  export class QueryState extends State {
12
13
  constructor(ref) {
13
- var _a;
14
- 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);
15
19
  this.busy = new BooleanState();
16
20
  this._hasMore = false;
17
21
  /** Refresh this state from the source provider. */
@@ -55,24 +59,12 @@ export class QueryState extends State {
55
59
  }
56
60
  }
57
61
  };
58
- this._table = findSourceProvider(ref.db.provider, CacheProvider).memory.getTable(ref);
62
+ this._time = time;
59
63
  this.ref = ref;
60
- this.limit = (_a = ref.limit) !== null && _a !== void 0 ? _a : Infinity;
61
- // If the result is cached use it as the initial value.
62
- const isCached = typeof this._table.getQueryTime(ref) === "number";
63
- if (isCached)
64
- this.next(this._table.getQuery(ref)); // Use the existing cached value.
65
- else
66
- void this.refresh(); // Queue a request to refresh the value.
67
- }
68
- /** Time this state was last updated with a new value. */
69
- get time() {
70
- return this._table.getQueryTime(this.ref);
71
- }
72
- /** How old this state's value is (in milliseconds). */
73
- get age() {
74
- const time = this.time;
75
- 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();
76
68
  }
77
69
  /** Can more items be loaded after the current result. */
78
70
  get hasMore() {
@@ -111,11 +103,15 @@ export class QueryState extends State {
111
103
  connectSource() {
112
104
  return this.connect(() => this.ref.subscribe({}));
113
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
+ }
114
112
  // Override to subscribe to the cache when an observer is added.
115
113
  _addFirstObserver() {
116
- // Connect this state to the source.
117
- // 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).
118
- this.connect(() => this._table.subscribeQuery(this.ref, new MatchObserver(() => this._table.getQueryTime(this.ref) !== undefined, this)));
114
+ this.connectCache();
119
115
  }
120
116
  // Override to unsubscribe from the cache when an observer is removed.
121
117
  _removeLastObserver() {
@@ -124,9 +120,10 @@ export class QueryState extends State {
124
120
  }
125
121
  }
126
122
  /** Reuse the previous `QueryState` or create a new one. */
127
- const _getQueryState = (previous, ref) => !ref ? undefined : previous && isSameReference(previous.ref, ref) ? previous : new QueryState(ref);
123
+ const _reduceQueryState = (existing, ref) => existing || new QueryState(ref);
128
124
  export function useQuery(ref) {
129
- const state = useReduce(_getQueryState, ref);
125
+ const cache = useCache();
126
+ const state = ref ? reduceMapItem(cache, ref.toString(), _reduceQueryState, ref) : undefined;
130
127
  useSubscribe(state);
131
128
  return state;
132
129
  }
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
+ }