shelving 1.67.0 → 1.68.2
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/index.js +3 -0
- package/package.json +3 -2
- package/provider/MemoryProvider.d.ts +8 -4
- package/provider/MemoryProvider.js +38 -3
- package/provider/ThroughProvider.d.ts +1 -1
- package/provider/ThroughProvider.js +0 -2
- package/react/index.d.ts +1 -0
- package/react/index.js +1 -0
- package/react/useCache.d.ts +46 -0
- package/react/useCache.js +56 -0
- package/react/useDocument.d.ts +6 -12
- package/react/useDocument.js +40 -40
- package/react/useQuery.d.ts +8 -13
- package/react/useQuery.js +59 -58
- package/state/State.d.ts +8 -1
- package/state/State.js +11 -1
- package/util/map.d.ts +3 -0
- package/util/map.js +8 -0
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.
|
|
14
|
+
"version": "1.68.2",
|
|
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
|
-
"./
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 ? `
|
|
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/react/index.d.ts
CHANGED
package/react/index.js
CHANGED
|
@@ -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;
|
package/react/useDocument.d.ts
CHANGED
|
@@ -1,38 +1,32 @@
|
|
|
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`)? */
|
|
20
13
|
get exists(): boolean;
|
|
21
14
|
constructor(ref: DocumentReference<T>);
|
|
22
15
|
/** Refresh this state from the source provider. */
|
|
23
|
-
readonly refresh: () =>
|
|
16
|
+
readonly refresh: () => void;
|
|
17
|
+
_refresh(): Promise<void>;
|
|
24
18
|
/** Refresh this state if data in the cache is older than `maxAge` (in milliseconds). */
|
|
25
19
|
refreshStale(maxAge: number): void;
|
|
26
20
|
/** Subscribe this state to the source provider. */
|
|
27
21
|
connectSource(): Unsubscribe;
|
|
22
|
+
/** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
|
|
23
|
+
connectCache(): Unsubscribe | void;
|
|
28
24
|
protected _addFirstObserver(): void;
|
|
29
25
|
protected _removeLastObserver(): void;
|
|
30
26
|
}
|
|
31
27
|
/**
|
|
32
28
|
* Use a document in a React component.
|
|
33
|
-
* -
|
|
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`
|
|
29
|
+
* - Uses the default cache, so will error if not used inside `<Cache>`
|
|
36
30
|
*/
|
|
37
31
|
export declare function useDocument<T extends Data>(ref: DocumentReference<T>): DocumentState<T>;
|
|
38
32
|
export declare function useDocument<T extends Data>(ref?: DocumentReference<T>): DocumentState<T> | undefined;
|
package/react/useDocument.js
CHANGED
|
@@ -1,52 +1,34 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
this.refresh =
|
|
21
|
+
this.refresh = () => {
|
|
17
22
|
if (this.closed)
|
|
18
23
|
throw new ConditionError("State is closed");
|
|
19
|
-
if (!this.busy.value)
|
|
20
|
-
this.
|
|
21
|
-
try {
|
|
22
|
-
const result = await this.ref.value;
|
|
23
|
-
this.next(result);
|
|
24
|
-
}
|
|
25
|
-
catch (thrown) {
|
|
26
|
-
this.error(thrown);
|
|
27
|
-
}
|
|
28
|
-
finally {
|
|
29
|
-
this.busy.next(false);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
24
|
+
if (!this.busy.value)
|
|
25
|
+
void this._refresh();
|
|
32
26
|
};
|
|
33
|
-
this.
|
|
27
|
+
this._time = time;
|
|
34
28
|
this.ref = ref;
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
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;
|
|
29
|
+
// Queue a request to refresh the value if it doesn't exist.
|
|
30
|
+
if (this.loading)
|
|
31
|
+
this.refresh();
|
|
50
32
|
}
|
|
51
33
|
/** Get the data of the document (throws `RequiredError` if document doesn't exist). */
|
|
52
34
|
get data() {
|
|
@@ -56,20 +38,37 @@ export class DocumentState extends State {
|
|
|
56
38
|
get exists() {
|
|
57
39
|
return !!this.value;
|
|
58
40
|
}
|
|
41
|
+
async _refresh() {
|
|
42
|
+
this.busy.next(true);
|
|
43
|
+
try {
|
|
44
|
+
const result = await this.ref.value;
|
|
45
|
+
this.next(result);
|
|
46
|
+
}
|
|
47
|
+
catch (thrown) {
|
|
48
|
+
this.error(thrown);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
this.busy.next(false);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
59
54
|
/** Refresh this state if data in the cache is older than `maxAge` (in milliseconds). */
|
|
60
55
|
refreshStale(maxAge) {
|
|
61
56
|
if (this.age > maxAge)
|
|
62
|
-
|
|
57
|
+
this.refresh();
|
|
63
58
|
}
|
|
64
59
|
/** Subscribe this state to the source provider. */
|
|
65
60
|
connectSource() {
|
|
66
61
|
return this.connect(() => this.ref.subscribe({}));
|
|
67
62
|
}
|
|
63
|
+
/** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
|
|
64
|
+
connectCache() {
|
|
65
|
+
var _a;
|
|
66
|
+
const table = (_a = findSourceProvider(this.ref.db.provider, CacheProvider)) === null || _a === void 0 ? void 0 : _a.memory.getTable(this.ref);
|
|
67
|
+
table && this.connect(() => table.subscribeCachedDocument(this.ref.id, this));
|
|
68
|
+
}
|
|
68
69
|
// Override to subscribe to the cache when an observer is added.
|
|
69
70
|
_addFirstObserver() {
|
|
70
|
-
|
|
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)));
|
|
71
|
+
this.connectCache();
|
|
73
72
|
}
|
|
74
73
|
// Override to unsubscribe from the cache when an observer is removed.
|
|
75
74
|
_removeLastObserver() {
|
|
@@ -78,9 +77,10 @@ export class DocumentState extends State {
|
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
79
|
/** Reuse the previous `DocumentState` or create a new one. */
|
|
81
|
-
const
|
|
80
|
+
const _reduceDocumentState = (existing, ref) => existing || new DocumentState(ref);
|
|
82
81
|
export function useDocument(ref) {
|
|
83
|
-
const
|
|
82
|
+
const cache = useCache();
|
|
83
|
+
const state = ref ? reduceMapItem(cache, ref.toString(), _reduceDocumentState, ref) : undefined;
|
|
84
84
|
useSubscribe(state);
|
|
85
85
|
return state;
|
|
86
86
|
}
|
package/react/useQuery.d.ts
CHANGED
|
@@ -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;
|
|
@@ -32,24 +25,26 @@ export declare class QueryState<T extends Data> extends State<Entities<T>> {
|
|
|
32
25
|
get count(): number;
|
|
33
26
|
constructor(ref: QueryReference<T>);
|
|
34
27
|
/** Refresh this state from the source provider. */
|
|
35
|
-
readonly refresh: () =>
|
|
28
|
+
readonly refresh: () => void;
|
|
29
|
+
_refresh(): Promise<void>;
|
|
36
30
|
/** Refresh this state if data in the cache is older than `maxAge` (in milliseconds). */
|
|
37
31
|
refreshStale(maxAge: number): void;
|
|
38
32
|
/** Subscribe this state to the source provider. */
|
|
39
33
|
connectSource(): Unsubscribe;
|
|
34
|
+
/** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
|
|
35
|
+
connectCache(): Unsubscribe | void;
|
|
40
36
|
protected _addFirstObserver(): void;
|
|
41
37
|
protected _removeLastObserver(): void;
|
|
42
38
|
/**
|
|
43
39
|
* Load more items after the last once.
|
|
44
40
|
* - Promise that needs to be handled.
|
|
45
41
|
*/
|
|
46
|
-
readonly loadMore: () =>
|
|
42
|
+
readonly loadMore: () => void;
|
|
43
|
+
_loadMore(): Promise<void>;
|
|
47
44
|
}
|
|
48
45
|
/**
|
|
49
46
|
* Use a query in a React component.
|
|
50
|
-
* -
|
|
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`
|
|
47
|
+
* - Uses the default cache, so will error if not used inside `<Cache>`
|
|
53
48
|
*/
|
|
54
49
|
export declare function useQuery<T extends Data>(ref: QueryReference<T>): QueryState<T>;
|
|
55
50
|
export declare function useQuery<T extends Data>(ref?: QueryReference<T>): QueryState<T> | undefined;
|
package/react/useQuery.js
CHANGED
|
@@ -1,78 +1,46 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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. */
|
|
18
|
-
this.refresh =
|
|
22
|
+
this.refresh = () => {
|
|
19
23
|
if (this.closed)
|
|
20
24
|
throw new ConditionError("State is closed");
|
|
21
|
-
if (!this.busy.value)
|
|
22
|
-
this.
|
|
23
|
-
try {
|
|
24
|
-
const result = await this.ref.value;
|
|
25
|
-
this._hasMore = result.length < this.limit;
|
|
26
|
-
this.next(result);
|
|
27
|
-
}
|
|
28
|
-
catch (thrown) {
|
|
29
|
-
this.error(thrown);
|
|
30
|
-
}
|
|
31
|
-
finally {
|
|
32
|
-
this.busy.next(false);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
25
|
+
if (!this.busy.value)
|
|
26
|
+
void this._refresh();
|
|
35
27
|
};
|
|
36
28
|
/**
|
|
37
29
|
* Load more items after the last once.
|
|
38
30
|
* - Promise that needs to be handled.
|
|
39
31
|
*/
|
|
40
|
-
this.loadMore =
|
|
32
|
+
this.loadMore = () => {
|
|
41
33
|
if (this.closed)
|
|
42
34
|
throw new ConditionError("State is closed");
|
|
43
|
-
if (!this.busy.value)
|
|
44
|
-
this.
|
|
45
|
-
try {
|
|
46
|
-
const items = await this.ref.after(this.lastData).value;
|
|
47
|
-
this.next([...this.value, ...items]);
|
|
48
|
-
this._hasMore = items.length < this.limit;
|
|
49
|
-
}
|
|
50
|
-
catch (thrown) {
|
|
51
|
-
this.error(thrown);
|
|
52
|
-
}
|
|
53
|
-
finally {
|
|
54
|
-
this.busy.next(false);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
35
|
+
if (!this.busy.value)
|
|
36
|
+
void this._loadMore();
|
|
57
37
|
};
|
|
58
|
-
this.
|
|
38
|
+
this._time = time;
|
|
59
39
|
this.ref = ref;
|
|
60
|
-
this.limit = (
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
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;
|
|
40
|
+
this.limit = (_b = ref.limit) !== null && _b !== void 0 ? _b : Infinity;
|
|
41
|
+
// Queue a request to refresh the value if it doesn't exist.
|
|
42
|
+
if (this.loading)
|
|
43
|
+
this.refresh();
|
|
76
44
|
}
|
|
77
45
|
/** Can more items be loaded after the current result. */
|
|
78
46
|
get hasMore() {
|
|
@@ -102,31 +70,64 @@ export class QueryState extends State {
|
|
|
102
70
|
get count() {
|
|
103
71
|
return this.value.length;
|
|
104
72
|
}
|
|
73
|
+
async _refresh() {
|
|
74
|
+
this.busy.next(true);
|
|
75
|
+
try {
|
|
76
|
+
const result = await this.ref.value;
|
|
77
|
+
this._hasMore = result.length < this.limit;
|
|
78
|
+
this.next(result);
|
|
79
|
+
}
|
|
80
|
+
catch (thrown) {
|
|
81
|
+
this.error(thrown);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.busy.next(false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
105
87
|
/** Refresh this state if data in the cache is older than `maxAge` (in milliseconds). */
|
|
106
88
|
refreshStale(maxAge) {
|
|
107
89
|
if (this.age > maxAge)
|
|
108
|
-
|
|
90
|
+
this.refresh();
|
|
109
91
|
}
|
|
110
92
|
/** Subscribe this state to the source provider. */
|
|
111
93
|
connectSource() {
|
|
112
94
|
return this.connect(() => this.ref.subscribe({}));
|
|
113
95
|
}
|
|
96
|
+
/** Subscribe this state to any `CacheProvider` that exists in the provider chain. */
|
|
97
|
+
connectCache() {
|
|
98
|
+
var _a;
|
|
99
|
+
const table = (_a = findSourceProvider(this.ref.db.provider, CacheProvider)) === null || _a === void 0 ? void 0 : _a.memory.getTable(this.ref);
|
|
100
|
+
return table && this.connect(() => table.subscribeCachedQuery(this.ref, this));
|
|
101
|
+
}
|
|
114
102
|
// Override to subscribe to the cache when an observer is added.
|
|
115
103
|
_addFirstObserver() {
|
|
116
|
-
|
|
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)));
|
|
104
|
+
this.connectCache();
|
|
119
105
|
}
|
|
120
106
|
// Override to unsubscribe from the cache when an observer is removed.
|
|
121
107
|
_removeLastObserver() {
|
|
122
108
|
// Disconnect all sources.
|
|
123
109
|
this.disconnect();
|
|
124
110
|
}
|
|
111
|
+
async _loadMore() {
|
|
112
|
+
this.busy.next(true);
|
|
113
|
+
try {
|
|
114
|
+
const items = await this.ref.after(this.lastData).value;
|
|
115
|
+
this.next([...this.value, ...items]);
|
|
116
|
+
this._hasMore = items.length < this.limit;
|
|
117
|
+
}
|
|
118
|
+
catch (thrown) {
|
|
119
|
+
this.error(thrown);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
this.busy.next(false);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
125
|
}
|
|
126
126
|
/** Reuse the previous `QueryState` or create a new one. */
|
|
127
|
-
const
|
|
127
|
+
const _reduceQueryState = (existing, ref) => existing || new QueryState(ref);
|
|
128
128
|
export function useQuery(ref) {
|
|
129
|
-
const
|
|
129
|
+
const cache = useCache();
|
|
130
|
+
const state = ref ? reduceMapItem(cache, ref.toString(), _reduceQueryState, ref) : undefined;
|
|
130
131
|
useSubscribe(state);
|
|
131
132
|
return state;
|
|
132
133
|
}
|
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
|
+
}
|