shelving 1.188.4 → 1.188.6
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/api/store/EndpointStore.d.ts +1 -1
- package/api/store/EndpointStore.js +2 -2
- package/db/store/ItemStore.d.ts +1 -1
- package/db/store/ItemStore.js +1 -1
- package/db/store/QueryStore.d.ts +1 -1
- package/db/store/QueryStore.js +1 -1
- package/package.json +5 -5
- package/store/FetchStore.d.ts +33 -24
- package/store/FetchStore.js +57 -58
- package/store/PayloadFetchStore.js +2 -3
- package/store/Store.d.ts +44 -15
- package/store/Store.js +83 -31
|
@@ -6,5 +6,5 @@ export declare class EndpointStore<P, R> extends PayloadFetchStore<P, R> {
|
|
|
6
6
|
readonly provider: APIProvider<P, R>;
|
|
7
7
|
readonly endpoint: Endpoint<P, R>;
|
|
8
8
|
constructor(endpoint: Endpoint<P, R>, payload: P, provider: APIProvider<P, R>);
|
|
9
|
-
protected _fetch(): Promise<R>;
|
|
9
|
+
protected _fetch(signal: AbortSignal): Promise<R>;
|
|
10
10
|
}
|
|
@@ -10,7 +10,7 @@ export class EndpointStore extends PayloadFetchStore {
|
|
|
10
10
|
this.provider = provider;
|
|
11
11
|
}
|
|
12
12
|
// Override to fetch the value using the provider and endpoint.
|
|
13
|
-
_fetch() {
|
|
14
|
-
return this.provider.call(this.endpoint, this.payload.value, { signal
|
|
13
|
+
_fetch(signal) {
|
|
14
|
+
return this.provider.call(this.endpoint, this.payload.value, { signal });
|
|
15
15
|
}
|
|
16
16
|
}
|
package/db/store/ItemStore.d.ts
CHANGED
|
@@ -16,5 +16,5 @@ export declare class ItemStore<I extends Identifier, T extends Data> extends Fet
|
|
|
16
16
|
get item(): Item<I, T>;
|
|
17
17
|
set item(data: T | Item<I, T>);
|
|
18
18
|
constructor(collection: Collection<string, I, T>, id: I, provider: DBProvider<I>, memory?: MemoryDBProvider<I>);
|
|
19
|
-
_fetch(): Promise<OptionalItem<I, T>>;
|
|
19
|
+
_fetch(_signal: AbortSignal): Promise<OptionalItem<I, T>>;
|
|
20
20
|
}
|
package/db/store/ItemStore.js
CHANGED
package/db/store/QueryStore.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare class QueryStore<I extends Identifier, T extends Data> extends Fe
|
|
|
23
23
|
/** Get the last item in this store. */
|
|
24
24
|
get last(): Item<I, T>;
|
|
25
25
|
constructor(collection: Collection<string, I, T>, query: Query<Item<I, T>>, provider: DBProvider<I>, memory?: MemoryDBProvider<I>);
|
|
26
|
-
_fetch(): Promise<Items<I, T>>;
|
|
26
|
+
_fetch(_signal: AbortSignal): Promise<Items<I, T>>;
|
|
27
27
|
/**
|
|
28
28
|
* Load more items after the last once.
|
|
29
29
|
* - Promise that needs to be handled.
|
package/db/store/QueryStore.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shelving",
|
|
3
|
-
"version": "1.188.
|
|
3
|
+
"version": "1.188.6",
|
|
4
4
|
"author": "Dave Houlbrooke <dave@shax.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
"main": "./index.js",
|
|
10
10
|
"module": "./index.js",
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@biomejs/biome": "^2.4.
|
|
12
|
+
"@biomejs/biome": "^2.4.13",
|
|
13
13
|
"@google-cloud/firestore": "^8.5.0",
|
|
14
|
-
"@types/bun": "^1.3.
|
|
14
|
+
"@types/bun": "^1.3.13",
|
|
15
15
|
"@types/react": "^19.2.14",
|
|
16
16
|
"@types/react-dom": "^19.2.3",
|
|
17
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
18
|
-
"firebase": "^12.12.
|
|
17
|
+
"@typescript/native-preview": "^7.0.0-dev.20260425.1",
|
|
18
|
+
"firebase": "^12.12.1",
|
|
19
19
|
"react": "^19.2.5",
|
|
20
20
|
"react-dom": "^19.2.5",
|
|
21
21
|
"typescript": "^5.9.3"
|
package/store/FetchStore.d.ts
CHANGED
|
@@ -2,53 +2,62 @@ import { NONE } from "../util/constants.js";
|
|
|
2
2
|
import { BooleanStore } from "./BooleanStore.js";
|
|
3
3
|
import { Store } from "./Store.js";
|
|
4
4
|
/** Callback for a callback fetch store. */
|
|
5
|
-
export type FetchCallback<T> = () => T | PromiseLike<T>;
|
|
5
|
+
export type FetchCallback<T> = (signal: AbortSignal) => T | PromiseLike<T>;
|
|
6
6
|
/**
|
|
7
7
|
* Store that fetches its values from a remote source.
|
|
8
8
|
*
|
|
9
9
|
* @param value The initial value for the store, or `NONE` if it does not have one yet.
|
|
10
|
-
* @param callback An optional callback that, if set, will be called when the `
|
|
10
|
+
* @param callback An optional callback that, if set, will be called when the `refresh()` method is invoked to fetch the next value.
|
|
11
11
|
*/
|
|
12
12
|
export declare class FetchStore<T> extends Store<T> {
|
|
13
13
|
/**
|
|
14
14
|
* Store that indicates the busy state of this store.
|
|
15
|
-
* -
|
|
15
|
+
* - `true` while a refresh is in-flight, `false` otherwise.
|
|
16
|
+
* - Can be subscribed to for e.g. loading spinners.
|
|
16
17
|
*/
|
|
17
18
|
readonly busy: BooleanStore;
|
|
18
19
|
get loading(): boolean;
|
|
19
20
|
get value(): T;
|
|
20
|
-
set value(value: T | typeof NONE);
|
|
21
|
+
set value(value: T | typeof NONE | PromiseLike<T | typeof NONE>);
|
|
21
22
|
constructor(value: T | typeof NONE, callback?: FetchCallback<T>);
|
|
22
23
|
/**
|
|
23
|
-
* Fetch the result for this
|
|
24
|
-
* - Triggered automatically when someone reads `value` or `loading
|
|
25
|
-
* -
|
|
24
|
+
* Fetch the result for this store now.
|
|
25
|
+
* - Triggered automatically when someone reads `value` or `loading`.
|
|
26
|
+
* - Concurrent calls while a fetch is in-flight return the same promise (deduplication).
|
|
27
|
+
* - Never throws — errors are stored as `reason`.
|
|
26
28
|
*
|
|
27
|
-
* @returns
|
|
28
|
-
* @returns
|
|
29
|
-
* @throws {never} Never throws so safe to call unhandled.
|
|
29
|
+
* @returns `true` if the fetch completed and the value was applied, `false` if aborted or superseded.
|
|
30
|
+
* @returns Synchronous `boolean` if the callback returned a synchronous value.
|
|
30
31
|
*/
|
|
31
|
-
refresh(): Promise<
|
|
32
|
-
/** An in-flight refresh, so we don't de-duplicate these. */
|
|
32
|
+
refresh(): Promise<boolean> | boolean;
|
|
33
33
|
private _inflight;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
private _refresh;
|
|
35
|
+
/**
|
|
36
|
+
* Current `AbortSignal` for this store's in-flight fetch.
|
|
37
|
+
* - Created lazily; a new signal is issued each time `refresh()` starts a new fetch or `abort()` is called.
|
|
38
|
+
*/
|
|
39
|
+
get signal(): AbortSignal;
|
|
40
|
+
private _controller;
|
|
41
|
+
/**
|
|
42
|
+
* Call the fetch callback to get the next value.
|
|
43
|
+
* @param signal `AbortSignal` for the current fetch — passed through to the callback so it can cancel HTTP requests etc.
|
|
44
|
+
*/
|
|
45
|
+
protected _fetch(signal: AbortSignal): T | PromiseLike<T>;
|
|
38
46
|
private _callback;
|
|
39
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Invalidate this store so a new fetch is triggered on the next read of `loading` or `value`.
|
|
49
|
+
* - Also aborts any current in-flight fetch.
|
|
50
|
+
*/
|
|
40
51
|
invalidate(): void;
|
|
41
52
|
private _invalidation;
|
|
42
|
-
/** Re-fetch
|
|
53
|
+
/** Re-fetch now if the current value is older than `maxAge` milliseconds or has been invalidated. */
|
|
43
54
|
refreshStale(maxAge: number): Promise<void>;
|
|
44
55
|
/**
|
|
45
|
-
*
|
|
46
|
-
* -
|
|
47
|
-
* -
|
|
56
|
+
* Abort any current in-flight fetch and pending async operation.
|
|
57
|
+
* - Aborts the current `AbortSignal` and clears the controller (a new signal will be created on the next read or fetch).
|
|
58
|
+
* - Clears the in-flight promise and resets busy state.
|
|
59
|
+
* - Any pending `await()` result will be silently discarded.
|
|
48
60
|
*/
|
|
49
|
-
get signal(): AbortSignal;
|
|
50
|
-
private _controller;
|
|
51
|
-
/** Abort the current signal now. */
|
|
52
61
|
abort(): void;
|
|
53
62
|
[Symbol.asyncDispose](): Promise<void>;
|
|
54
63
|
}
|
package/store/FetchStore.js
CHANGED
|
@@ -8,126 +8,125 @@ import { Store } from "./Store.js";
|
|
|
8
8
|
* Store that fetches its values from a remote source.
|
|
9
9
|
*
|
|
10
10
|
* @param value The initial value for the store, or `NONE` if it does not have one yet.
|
|
11
|
-
* @param callback An optional callback that, if set, will be called when the `
|
|
11
|
+
* @param callback An optional callback that, if set, will be called when the `refresh()` method is invoked to fetch the next value.
|
|
12
12
|
*/
|
|
13
13
|
export class FetchStore extends Store {
|
|
14
14
|
/**
|
|
15
15
|
* Store that indicates the busy state of this store.
|
|
16
|
-
* -
|
|
16
|
+
* - `true` while a refresh is in-flight, `false` otherwise.
|
|
17
|
+
* - Can be subscribed to for e.g. loading spinners.
|
|
17
18
|
*/
|
|
18
19
|
busy;
|
|
19
20
|
// Override to possibly trigger a fetch when `this.loading` is read.
|
|
20
|
-
//
|
|
21
|
+
// Reading `loading` signals intent to use the value, so we start a fetch if needed.
|
|
21
22
|
get loading() {
|
|
22
23
|
const loading = super.loading;
|
|
23
24
|
if (loading || this._invalidation)
|
|
24
25
|
void this.refresh();
|
|
25
26
|
return loading;
|
|
26
27
|
}
|
|
27
|
-
// Override to possibly trigger a fetch if `this.value` is still
|
|
28
|
-
//
|
|
28
|
+
// Override to possibly trigger a fetch if `this.value` is still loading.
|
|
29
|
+
// Reading `value` signals intent to use the value, so we start a fetch if needed.
|
|
29
30
|
get value() {
|
|
30
31
|
if (super.loading)
|
|
31
32
|
void this.refresh();
|
|
32
33
|
return super.value;
|
|
33
34
|
}
|
|
34
35
|
set value(value) {
|
|
35
|
-
super.value = value;
|
|
36
|
-
// Setting a value resets
|
|
36
|
+
super.value = value; // calls Store.set value() which calls this.abort() then _applyValue()
|
|
37
|
+
// Setting a value resets the invalid state.
|
|
37
38
|
this._invalidation = 0;
|
|
38
39
|
}
|
|
39
|
-
// Override to save callback.
|
|
40
40
|
constructor(value, callback) {
|
|
41
41
|
super(value);
|
|
42
|
-
this.busy = new BooleanStore(value
|
|
42
|
+
this.busy = new BooleanStore(value === NONE);
|
|
43
43
|
this._callback = callback;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* Fetch the result for this
|
|
47
|
-
* - Triggered automatically when someone reads `value` or `loading
|
|
48
|
-
* -
|
|
46
|
+
* Fetch the result for this store now.
|
|
47
|
+
* - Triggered automatically when someone reads `value` or `loading`.
|
|
48
|
+
* - Concurrent calls while a fetch is in-flight return the same promise (deduplication).
|
|
49
|
+
* - Never throws — errors are stored as `reason`.
|
|
49
50
|
*
|
|
50
|
-
* @returns
|
|
51
|
-
* @returns
|
|
52
|
-
* @throws {never} Never throws so safe to call unhandled.
|
|
51
|
+
* @returns `true` if the fetch completed and the value was applied, `false` if aborted or superseded.
|
|
52
|
+
* @returns Synchronous `boolean` if the callback returned a synchronous value.
|
|
53
53
|
*/
|
|
54
54
|
refresh() {
|
|
55
55
|
if (this._inflight)
|
|
56
56
|
return this._inflight;
|
|
57
|
+
// Cancel any existing controller and create a fresh one for this fetch.
|
|
58
|
+
this._controller?.abort(ABORTED);
|
|
59
|
+
this._controller = new AbortController();
|
|
57
60
|
try {
|
|
58
|
-
const value = this._fetch();
|
|
61
|
+
const value = this._fetch(this._controller.signal);
|
|
59
62
|
if (isAsync(value))
|
|
60
|
-
return (this._inflight = this.
|
|
63
|
+
return (this._inflight = this._refresh(value));
|
|
61
64
|
this.value = value;
|
|
65
|
+
return true;
|
|
62
66
|
}
|
|
63
67
|
catch (thrown) {
|
|
64
68
|
this.reason = thrown;
|
|
69
|
+
return false;
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
|
-
/** An in-flight refresh, so we don't de-duplicate these. */
|
|
68
72
|
_inflight = undefined;
|
|
69
|
-
|
|
70
|
-
async await(value) {
|
|
73
|
+
async _refresh(asyncValue) {
|
|
71
74
|
this.busy.value = true;
|
|
72
|
-
this._awaits.add(value);
|
|
73
75
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Use super.value to set the value directly without resetting the invalidation number.
|
|
77
|
-
super.value = await value;
|
|
78
|
-
// If this store was not invalidated while awaiting the value (i.e. invalidation number did not change) then reset the invalidation number.
|
|
79
|
-
if (invalidation === this._invalidation)
|
|
76
|
+
const refreshed = await this.await(asyncValue);
|
|
77
|
+
if (refreshed)
|
|
80
78
|
this._invalidation = 0;
|
|
81
|
-
|
|
82
|
-
catch (thrown) {
|
|
83
|
-
// If the throw was not an on-purpose abort, save it as the reason.
|
|
84
|
-
if (thrown !== ABORTED)
|
|
85
|
-
this.reason = thrown;
|
|
79
|
+
return refreshed;
|
|
86
80
|
}
|
|
87
81
|
finally {
|
|
88
|
-
this.
|
|
89
|
-
|
|
90
|
-
this.busy.value = false;
|
|
91
|
-
this._inflight = undefined; // Clear any inflight refresh if this was the last await.
|
|
92
|
-
}
|
|
93
|
-
this._controller = undefined;
|
|
82
|
+
this.busy.value = false;
|
|
83
|
+
this._inflight = undefined;
|
|
94
84
|
}
|
|
95
85
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Current `AbortSignal` for this store's in-flight fetch.
|
|
88
|
+
* - Created lazily; a new signal is issued each time `refresh()` starts a new fetch or `abort()` is called.
|
|
89
|
+
*/
|
|
90
|
+
get signal() {
|
|
91
|
+
return (this._controller ||= new AbortController()).signal;
|
|
92
|
+
}
|
|
93
|
+
_controller;
|
|
94
|
+
/**
|
|
95
|
+
* Call the fetch callback to get the next value.
|
|
96
|
+
* @param signal `AbortSignal` for the current fetch — passed through to the callback so it can cancel HTTP requests etc.
|
|
97
|
+
*/
|
|
98
|
+
_fetch(signal) {
|
|
99
99
|
if (!this._callback)
|
|
100
100
|
throw new RequiredError("FetchStore has no callback() function", { store: this, caller: this.refresh });
|
|
101
|
-
return this._callback();
|
|
101
|
+
return this._callback(signal);
|
|
102
102
|
}
|
|
103
103
|
_callback;
|
|
104
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Invalidate this store so a new fetch is triggered on the next read of `loading` or `value`.
|
|
106
|
+
* - Also aborts any current in-flight fetch.
|
|
107
|
+
*/
|
|
105
108
|
invalidate() {
|
|
106
109
|
this.abort();
|
|
107
110
|
this._invalidation++;
|
|
108
111
|
}
|
|
109
|
-
_invalidation = 0;
|
|
110
|
-
/** Re-fetch
|
|
112
|
+
_invalidation = 0;
|
|
113
|
+
/** Re-fetch now if the current value is older than `maxAge` milliseconds or has been invalidated. */
|
|
111
114
|
async refreshStale(maxAge) {
|
|
112
115
|
if (this._invalidation || this.age > maxAge)
|
|
113
116
|
await this.refresh();
|
|
114
117
|
}
|
|
115
118
|
/**
|
|
116
|
-
*
|
|
117
|
-
* -
|
|
118
|
-
* -
|
|
119
|
+
* Abort any current in-flight fetch and pending async operation.
|
|
120
|
+
* - Aborts the current `AbortSignal` and clears the controller (a new signal will be created on the next read or fetch).
|
|
121
|
+
* - Clears the in-flight promise and resets busy state.
|
|
122
|
+
* - Any pending `await()` result will be silently discarded.
|
|
119
123
|
*/
|
|
120
|
-
get signal() {
|
|
121
|
-
return (this._controller ||= new AbortController()).signal;
|
|
122
|
-
}
|
|
123
|
-
_controller;
|
|
124
|
-
/** Abort the current signal now. */
|
|
125
124
|
abort() {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
this._controller?.abort(ABORTED);
|
|
126
|
+
this._controller = undefined;
|
|
127
|
+
this._inflight = undefined;
|
|
128
|
+
this.busy.value = false;
|
|
129
|
+
super.abort(); // clears _pendingValue
|
|
131
130
|
}
|
|
132
131
|
// Implement `AsyncDisposable`.
|
|
133
132
|
async [Symbol.asyncDispose]() {
|
|
@@ -33,8 +33,7 @@ export class PayloadFetchStore extends FetchStore {
|
|
|
33
33
|
*/
|
|
34
34
|
async function _iterate(store) {
|
|
35
35
|
for await (const _payload of store.payload.next) {
|
|
36
|
-
store.
|
|
37
|
-
store.
|
|
38
|
-
void store.refresh(); // Eagerly start a fresh fetch if no other fetch is already in-flight.
|
|
36
|
+
store.invalidate(); // Abort any in-flight fetch and mark stale.
|
|
37
|
+
void store.refresh(); // Eagerly start a fresh fetch with the new payload.
|
|
39
38
|
}
|
|
40
39
|
}
|
package/store/Store.d.ts
CHANGED
|
@@ -10,9 +10,9 @@ export type AnyStore = Store<any>;
|
|
|
10
10
|
* - Stores also send their most-recent value to any new subscribers immediately when a new subscriber is added.
|
|
11
11
|
* - Stores can also be in a loading store where they do not have a current value.
|
|
12
12
|
*
|
|
13
|
-
* @param initial The initial value for
|
|
14
|
-
* - To set
|
|
15
|
-
* - To set
|
|
13
|
+
* @param initial The initial value for this store, a `Promise` that resolves to the initial value, a source `Subscribable` to subscribe to, or another `Store` instance to take the initial value from and subscribe to.
|
|
14
|
+
* - To set this store to be loading, use the `NONE` constant or a `Promise` value.
|
|
15
|
+
* - To set this store to an explicit value, use that value or another `Store` instance with a value.
|
|
16
16
|
*/
|
|
17
17
|
export declare class Store<T> implements AsyncIterable<T, void, void>, AsyncDisposable {
|
|
18
18
|
/** Deferred sequence this store uses to issue values as they change. */
|
|
@@ -24,13 +24,18 @@ export declare class Store<T> implements AsyncIterable<T, void, void>, AsyncDisp
|
|
|
24
24
|
*/
|
|
25
25
|
get loading(): boolean;
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* Get the current value of this store.
|
|
28
28
|
*
|
|
29
|
-
* @throws {Promise}
|
|
30
|
-
* @throws {unknown} if
|
|
29
|
+
* @throws {Promise} if this store currently has no value (resolves when this store receives its next value or error).
|
|
30
|
+
* @throws {unknown} if this store currently has an error.
|
|
31
31
|
*/
|
|
32
32
|
get value(): T;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Set the value of this store .
|
|
35
|
+
* - Silently discards any pending `await()` calls.
|
|
36
|
+
* - Awaits any async values.
|
|
37
|
+
*/
|
|
38
|
+
set value(value: T | typeof NONE | PromiseLike<T | typeof NONE>);
|
|
34
39
|
private _value;
|
|
35
40
|
/**
|
|
36
41
|
* Time (in milliseconds) this store was last updated with a new value.
|
|
@@ -58,22 +63,46 @@ export declare class Store<T> implements AsyncIterable<T, void, void>, AsyncDisp
|
|
|
58
63
|
private _starter;
|
|
59
64
|
/** Store is initiated with an initial store. */
|
|
60
65
|
constructor(value: T | typeof NONE);
|
|
61
|
-
/** Set the value of
|
|
66
|
+
/** Set the value of this store as values are pulled from a sequence. */
|
|
62
67
|
through(sequence: AsyncIterable<T>): AsyncIterable<T>;
|
|
63
68
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
69
|
+
* Call a callback that returns a new value (possibly async) for this store.
|
|
70
|
+
* - Errors are stored as `reason`; never throws.
|
|
71
|
+
*
|
|
72
|
+
* @returns `true` if the value was applied, `false` if an error occurred or the result was superseded.
|
|
73
|
+
*/
|
|
74
|
+
call<A extends Arguments = []>(callback: (...args: A) => T | PromiseLike<T>, ...args: A): Promise<boolean> | boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Reduce the current value using a reducer callback that receives the current value.
|
|
77
|
+
*
|
|
78
|
+
* @param reducer The callback function to call that should return a value to set on this store.
|
|
79
|
+
* @param value The current value of this store.
|
|
80
|
+
* @param args Any additional input values for the reducer.
|
|
81
|
+
* @returns New value for this store (possibly async).
|
|
66
82
|
* @oaram args Any arguments to pass to the callback.
|
|
83
|
+
*
|
|
84
|
+
* @throws {Promise} if this store currently has no value (resolves when this store receives its next value or error).
|
|
85
|
+
* @throws {unknown} if this store currently has an error reason set.
|
|
67
86
|
*/
|
|
68
|
-
|
|
87
|
+
reduce<A extends Arguments = []>(reducer: (value: T, ...args: A) => T | PromiseLike<T>, ...args: A): Promise<boolean> | boolean;
|
|
69
88
|
/**
|
|
70
89
|
* Await an async value and save it to this store.
|
|
71
|
-
* -
|
|
90
|
+
* - Saves the resolved value.
|
|
91
|
+
* - If it rejects saves the rejection as `reason`.
|
|
92
|
+
* - Silently discarded if a newer value is set.
|
|
93
|
+
* - Silently discarded if `await()` is called again.
|
|
94
|
+
* - Silently discarded if `abort()` is called.
|
|
72
95
|
*
|
|
73
|
-
* @returns
|
|
74
|
-
* @throws {never} Never throws
|
|
96
|
+
* @returns `true` if the value was applied, `false` if superseded, aborted, or errored.
|
|
97
|
+
* @throws {never} Never throws — safe to call without handling the return value.
|
|
98
|
+
*/
|
|
99
|
+
await(asyncValue: PromiseLike<T | typeof NONE>): Promise<boolean>;
|
|
100
|
+
private _pendingValue;
|
|
101
|
+
/**
|
|
102
|
+
* Abort any current pending `await()` call.
|
|
103
|
+
* - The pending call's result will be silently discarded and its error will not be stored.
|
|
75
104
|
*/
|
|
76
|
-
|
|
105
|
+
abort(): void;
|
|
77
106
|
[Symbol.asyncIterator](): AsyncIterator<T, void, void>;
|
|
78
107
|
private _iterating;
|
|
79
108
|
/** Compare two values for this store and return whether they are equal. */
|
package/store/Store.js
CHANGED
|
@@ -10,9 +10,9 @@ import { getStarter } from "../util/start.js";
|
|
|
10
10
|
* - Stores also send their most-recent value to any new subscribers immediately when a new subscriber is added.
|
|
11
11
|
* - Stores can also be in a loading store where they do not have a current value.
|
|
12
12
|
*
|
|
13
|
-
* @param initial The initial value for
|
|
14
|
-
* - To set
|
|
15
|
-
* - To set
|
|
13
|
+
* @param initial The initial value for this store, a `Promise` that resolves to the initial value, a source `Subscribable` to subscribe to, or another `Store` instance to take the initial value from and subscribe to.
|
|
14
|
+
* - To set this store to be loading, use the `NONE` constant or a `Promise` value.
|
|
15
|
+
* - To set this store to an explicit value, use that value or another `Store` instance with a value.
|
|
16
16
|
*/
|
|
17
17
|
export class Store {
|
|
18
18
|
/** Deferred sequence this store uses to issue values as they change. */
|
|
@@ -26,10 +26,10 @@ export class Store {
|
|
|
26
26
|
return this._value === NONE && this._reason === undefined;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* Get the current value of this store.
|
|
30
30
|
*
|
|
31
|
-
* @throws {Promise}
|
|
32
|
-
* @throws {unknown} if
|
|
31
|
+
* @throws {Promise} if this store currently has no value (resolves when this store receives its next value or error).
|
|
32
|
+
* @throws {unknown} if this store currently has an error.
|
|
33
33
|
*/
|
|
34
34
|
get value() {
|
|
35
35
|
if (this._reason !== undefined)
|
|
@@ -38,17 +38,28 @@ export class Store {
|
|
|
38
38
|
throw this.next;
|
|
39
39
|
return this._value;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Set the value of this store .
|
|
43
|
+
* - Silently discards any pending `await()` calls.
|
|
44
|
+
* - Awaits any async values.
|
|
45
|
+
*/
|
|
41
46
|
set value(value) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this._time = undefined;
|
|
45
|
-
this._value = value;
|
|
46
|
-
this.next.cancel();
|
|
47
|
+
if (isAsync(value)) {
|
|
48
|
+
void this.await(value);
|
|
47
49
|
}
|
|
48
|
-
else
|
|
49
|
-
this.
|
|
50
|
-
this.
|
|
51
|
-
|
|
50
|
+
else {
|
|
51
|
+
this.abort();
|
|
52
|
+
this._reason = undefined;
|
|
53
|
+
if (value === NONE) {
|
|
54
|
+
this._time = undefined;
|
|
55
|
+
this._value = value;
|
|
56
|
+
this.next.cancel();
|
|
57
|
+
}
|
|
58
|
+
else if (this._value === NONE || !this.isEqual(value, this._value)) {
|
|
59
|
+
this._time = Date.now();
|
|
60
|
+
this._value = value;
|
|
61
|
+
this.next.resolve(value);
|
|
62
|
+
}
|
|
52
63
|
}
|
|
53
64
|
}
|
|
54
65
|
_value;
|
|
@@ -75,10 +86,10 @@ export class Store {
|
|
|
75
86
|
return this._reason;
|
|
76
87
|
}
|
|
77
88
|
set reason(reason) {
|
|
89
|
+
this.abort();
|
|
78
90
|
this._reason = reason;
|
|
79
|
-
if (reason !== undefined)
|
|
91
|
+
if (reason !== undefined)
|
|
80
92
|
this.next.reject(reason);
|
|
81
|
-
}
|
|
82
93
|
}
|
|
83
94
|
_reason = undefined;
|
|
84
95
|
/**
|
|
@@ -99,7 +110,7 @@ export class Store {
|
|
|
99
110
|
this._value = value;
|
|
100
111
|
this._time = value === NONE ? -Infinity : Date.now();
|
|
101
112
|
}
|
|
102
|
-
/** Set the value of
|
|
113
|
+
/** Set the value of this store as values are pulled from a sequence. */
|
|
103
114
|
async *through(sequence) {
|
|
104
115
|
for await (const value of sequence) {
|
|
105
116
|
this.value = value;
|
|
@@ -107,39 +118,80 @@ export class Store {
|
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
120
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
121
|
+
* Call a callback that returns a new value (possibly async) for this store.
|
|
122
|
+
* - Errors are stored as `reason`; never throws.
|
|
123
|
+
*
|
|
124
|
+
* @returns `true` if the value was applied, `false` if an error occurred or the result was superseded.
|
|
113
125
|
*/
|
|
114
126
|
call(callback, ...args) {
|
|
115
127
|
try {
|
|
116
128
|
const value = callback(...args);
|
|
117
129
|
if (isAsync(value))
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
return this.await(value);
|
|
131
|
+
this.value = value;
|
|
132
|
+
return true;
|
|
121
133
|
}
|
|
122
134
|
catch (thrown) {
|
|
123
135
|
this.reason = thrown;
|
|
136
|
+
return false;
|
|
124
137
|
}
|
|
125
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Reduce the current value using a reducer callback that receives the current value.
|
|
141
|
+
*
|
|
142
|
+
* @param reducer The callback function to call that should return a value to set on this store.
|
|
143
|
+
* @param value The current value of this store.
|
|
144
|
+
* @param args Any additional input values for the reducer.
|
|
145
|
+
* @returns New value for this store (possibly async).
|
|
146
|
+
* @oaram args Any arguments to pass to the callback.
|
|
147
|
+
*
|
|
148
|
+
* @throws {Promise} if this store currently has no value (resolves when this store receives its next value or error).
|
|
149
|
+
* @throws {unknown} if this store currently has an error reason set.
|
|
150
|
+
*/
|
|
151
|
+
reduce(reducer, ...args) {
|
|
152
|
+
return this.call(reducer, this.value, ...args);
|
|
153
|
+
}
|
|
126
154
|
/**
|
|
127
155
|
* Await an async value and save it to this store.
|
|
128
|
-
* -
|
|
156
|
+
* - Saves the resolved value.
|
|
157
|
+
* - If it rejects saves the rejection as `reason`.
|
|
158
|
+
* - Silently discarded if a newer value is set.
|
|
159
|
+
* - Silently discarded if `await()` is called again.
|
|
160
|
+
* - Silently discarded if `abort()` is called.
|
|
129
161
|
*
|
|
130
|
-
* @returns
|
|
131
|
-
* @throws {never} Never throws
|
|
162
|
+
* @returns `true` if the value was applied, `false` if superseded, aborted, or errored.
|
|
163
|
+
* @throws {never} Never throws — safe to call without handling the return value.
|
|
132
164
|
*/
|
|
133
|
-
async await(
|
|
165
|
+
async await(asyncValue) {
|
|
166
|
+
// Keep track of the value that is being awaited.
|
|
167
|
+
// If `_pendingValue` changes while waiting for `asyncValue` to resolve, another call to `await()` has `set value`, `set reason`, or `abort()` has invalidated this one.
|
|
168
|
+
// If that happens we silently discard the resolved value/reason of this await call.
|
|
169
|
+
this._pendingValue = asyncValue;
|
|
134
170
|
try {
|
|
135
|
-
|
|
171
|
+
const value = await asyncValue;
|
|
172
|
+
if (this._pendingValue === asyncValue) {
|
|
173
|
+
this.value = value;
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
136
177
|
}
|
|
137
178
|
catch (reason) {
|
|
138
|
-
this.
|
|
179
|
+
if (this._pendingValue === asyncValue) {
|
|
180
|
+
this.reason = reason;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
139
183
|
}
|
|
140
184
|
}
|
|
185
|
+
_pendingValue = undefined;
|
|
186
|
+
/**
|
|
187
|
+
* Abort any current pending `await()` call.
|
|
188
|
+
* - The pending call's result will be silently discarded and its error will not be stored.
|
|
189
|
+
*/
|
|
190
|
+
abort() {
|
|
191
|
+
this._pendingValue = undefined;
|
|
192
|
+
}
|
|
141
193
|
// Implement `AsyncIterator`
|
|
142
|
-
// Issues the current value of
|
|
194
|
+
// Issues the current value of this store first, then any subsequent values that are issued.
|
|
143
195
|
async *[Symbol.asyncIterator]() {
|
|
144
196
|
await Promise.resolve(); // Introduce a slight delay, i.e. don't immediately yield `this.value` in case it is changed synchronously.
|
|
145
197
|
this._starter?.start(this);
|