shelving 1.188.5 → 1.189.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { MethodNotAllowedError, NotFoundError } from "../../error/RequestError.js";
2
2
  import { ValueError } from "../../error/ValueError.js";
3
- import { getDictionary } from "../../util/dictionary.js";
3
+ import { requireDictionary } from "../../util/dictionary.js";
4
4
  import { getResponse, isRequestMethod, parseRequestBody } from "../../util/http.js";
5
5
  import { isPlainObject } from "../../util/object.js";
6
6
  import { requireURL } from "../../util/url.js";
@@ -19,7 +19,7 @@ export function handleEndpoints(base, handlers, request, context, caller = handl
19
19
  const pathParams = handler.endpoint.match(method, targetPath, caller);
20
20
  if (!pathParams)
21
21
  continue;
22
- const params = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
22
+ const params = searchParams.size ? { ...requireDictionary(searchParams), ...pathParams } : pathParams;
23
23
  return _handleEndpoint(handler, params, request, context, handleEndpoints);
24
24
  }
25
25
  throw new NotFoundError("No matching endpoint", { received: targetPath, caller });
@@ -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: this.signal });
13
+ _fetch(signal) {
14
+ return this.provider.call(this.endpoint, this.payload.value, { signal });
15
15
  }
16
16
  }
@@ -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
  }
@@ -38,7 +38,7 @@ export class ItemStore extends FetchStore {
38
38
  this.id = id;
39
39
  }
40
40
  // Override to get the item from the provider.
41
- _fetch() {
41
+ _fetch(_signal) {
42
42
  return this.provider.getItem(this.collection, this.id);
43
43
  }
44
44
  }
@@ -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.
@@ -62,7 +62,7 @@ export class QueryStore extends FetchStore {
62
62
  this.query = query;
63
63
  }
64
64
  // Override to fetch the result from the database provider.
65
- _fetch() {
65
+ _fetch(_signal) {
66
66
  return this.provider.getQuery(this.collection, this.query);
67
67
  }
68
68
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.188.5",
3
+ "version": "1.189.0",
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",
12
+ "@biomejs/biome": "^2.4.13",
13
13
  "@google-cloud/firestore": "^8.5.0",
14
- "@types/bun": "^1.3.12",
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.20260418.1",
18
- "firebase": "^12.12.0",
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"
@@ -1,9 +1,11 @@
1
- import type { ImmutableArray } from "../util/array.js";
1
+ import type { ImmutableArray, PossibleArray } from "../util/array.js";
2
2
  import type { NONE } from "../util/constants.js";
3
+ import type { AnyCaller } from "../util/function.js";
3
4
  import { Store } from "./Store.js";
4
5
  /** Store an array. */
5
- export declare class ArrayStore<T> extends Store<ImmutableArray<T>> implements Iterable<T> {
6
+ export declare class ArrayStore<T> extends Store<PossibleArray<T>, ImmutableArray<T>> implements Iterable<T> {
6
7
  constructor(value?: ImmutableArray<T> | typeof NONE);
8
+ convert(value: PossibleArray<T>, caller?: AnyCaller): ImmutableArray<T>;
7
9
  /** Get the first item in this store or `null` if this query has no items. */
8
10
  get optionalFirst(): T | undefined;
9
11
  /** Get the last item in this store or `null` if this query has no items. */
@@ -1,10 +1,14 @@
1
- import { getFirst, getLast, omitArrayItems, requireFirst, requireLast, toggleArrayItems, withArrayItems } from "../util/array.js";
1
+ import { getFirst, getLast, omitArrayItems, requireArray, requireFirst, requireLast, toggleArrayItems, withArrayItems, } from "../util/array.js";
2
2
  import { Store } from "./Store.js";
3
3
  /** Store an array. */
4
4
  export class ArrayStore extends Store {
5
5
  constructor(value = []) {
6
6
  super(value);
7
7
  }
8
+ // Implement to automatically convert `PossibleArray`
9
+ convert(value, caller = this.convert) {
10
+ return requireArray(value, undefined, undefined, caller);
11
+ }
8
12
  /** Get the first item in this store or `null` if this query has no items. */
9
13
  get optionalFirst() {
10
14
  return getFirst(this.value);
@@ -1,8 +1,7 @@
1
- import type { NONE } from "../util/constants.js";
2
1
  import { Store } from "./Store.js";
3
2
  /** Store a boolean. */
4
- export declare class BooleanStore extends Store<boolean> {
5
- constructor(value?: boolean | typeof NONE);
3
+ export declare class BooleanStore extends Store<unknown, boolean> {
4
+ convert(value: unknown): boolean;
6
5
  /** Toggle the current boolean value. */
7
6
  toggle(): void;
8
7
  isEqual(a: boolean, b: boolean): boolean;
@@ -1,8 +1,9 @@
1
1
  import { Store } from "./Store.js";
2
2
  /** Store a boolean. */
3
3
  export class BooleanStore extends Store {
4
- constructor(value = false) {
5
- super(value);
4
+ // Override to automatically convert to boolean.
5
+ convert(value) {
6
+ return !!value;
6
7
  }
7
8
  /** Toggle the current boolean value. */
8
9
  toggle() {
@@ -3,7 +3,7 @@ import type { AnyCaller } from "../util/function.js";
3
3
  import type { Updates } from "../util/update.js";
4
4
  import { Store } from "./Store.js";
5
5
  /** Store a data object. */
6
- export declare class DataStore<T extends Data> extends Store<T> {
6
+ export declare class DataStore<T extends Data> extends Store<T, T> {
7
7
  /** Get the data of this store. */
8
8
  get data(): T;
9
9
  /** Set the data of this store. */
@@ -14,9 +14,10 @@ export declare class DataStore<T extends Data> extends Store<T> {
14
14
  get<K extends DataKey<T>>(name: K): T[K];
15
15
  /** Update a single named prop in this data. */
16
16
  set<K extends DataKey<T>>(name: K, value: T[K]): void;
17
+ convert(value: T): T;
17
18
  }
18
19
  /** Store an optional data object. */
19
- export declare class OptionalDataStore<T extends Data> extends Store<T | undefined> {
20
+ export declare class OptionalDataStore<T extends Data> extends Store<T | undefined, T | undefined> {
20
21
  /** Get current data value of this store (or throw `Promise` that resolves to the next required value). */
21
22
  get data(): T;
22
23
  /** Set the data of this store. */
@@ -33,4 +34,5 @@ export declare class OptionalDataStore<T extends Data> extends Store<T | undefin
33
34
  set<K extends DataKey<T>>(name: K, value: T[K]): void;
34
35
  /** Set the data to `undefined`. */
35
36
  delete(): void;
37
+ convert(value: T): T;
36
38
  }
@@ -25,6 +25,10 @@ export class DataStore extends Store {
25
25
  set(name, value) {
26
26
  this.value = withProp(this.data, name, value);
27
27
  }
28
+ // Implement to passthrough.
29
+ convert(value) {
30
+ return value;
31
+ }
28
32
  }
29
33
  /** Store an optional data object. */
30
34
  export class OptionalDataStore extends Store {
@@ -63,4 +67,8 @@ export class OptionalDataStore extends Store {
63
67
  delete() {
64
68
  this.value = undefined;
65
69
  }
70
+ // Implement to passthrough.
71
+ convert(value) {
72
+ return value;
73
+ }
66
74
  }
@@ -1,9 +1,10 @@
1
- import type { DictionaryItem, ImmutableDictionary } from "../util/dictionary.js";
1
+ import type { DictionaryItem, ImmutableDictionary, PossibleDictionary } from "../util/dictionary.js";
2
2
  import type { Updates } from "../util/update.js";
3
3
  import { Store } from "./Store.js";
4
4
  /** Store a dictionary object. */
5
- export declare class DictionaryStore<T> extends Store<ImmutableDictionary<T>> implements Iterable<DictionaryItem<T>> {
6
- constructor(value?: ImmutableDictionary<T>);
5
+ export declare class DictionaryStore<T> extends Store<PossibleDictionary<T>, ImmutableDictionary<T>> implements Iterable<DictionaryItem<T>> {
6
+ constructor(value?: PossibleDictionary<T>);
7
+ convert(possible: PossibleDictionary<T>): ImmutableDictionary<T>;
7
8
  /** Get the length of the current value of this store. */
8
9
  get count(): number;
9
10
  /** Set a named entry in this object with a different value. */
@@ -1,11 +1,16 @@
1
- import { getDictionaryItems, omitDictionaryItems } from "../util/dictionary.js";
1
+ import { EMPTY_DICTIONARY, getDictionaryItems, omitDictionaryItems, requireDictionary } from "../util/dictionary.js";
2
2
  import { omitProps, withProp } from "../util/object.js";
3
3
  import { updateData } from "../util/update.js";
4
4
  import { Store } from "./Store.js";
5
5
  /** Store a dictionary object. */
6
6
  export class DictionaryStore extends Store {
7
- constructor(value = {}) {
8
- super(value);
7
+ // Override to set default value to empty dictionary.
8
+ constructor(value = EMPTY_DICTIONARY) {
9
+ super(requireDictionary(value));
10
+ }
11
+ // Override to convert a possible dictionary to a dictionary on set.
12
+ convert(possible) {
13
+ return requireDictionary(possible);
9
14
  }
10
15
  /** Get the length of the current value of this store. */
11
16
  get count() {
@@ -2,53 +2,63 @@ 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 `fetch()` method is invoked to fetch the next value.
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
- export declare class FetchStore<T> extends Store<T> {
12
+ export declare class FetchStore<T> extends Store<T | typeof NONE, T> {
13
13
  /**
14
14
  * Store that indicates the busy state of this store.
15
- * - Can be listened to for e.g. loading spinners etc.
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>);
23
+ convert(value: T | typeof NONE): T | typeof NONE;
22
24
  /**
23
- * Fetch the result for this endpoint now.
24
- * - Triggered automatically when someone reads `value` or `loading`
25
- * - Multiple requests to `fetch()` while one is inflight will return the same promise.
25
+ * Fetch the result for this store now.
26
+ * - Triggered automatically when someone reads `value` or `loading`.
27
+ * - Concurrent calls while a fetch is in-flight return the same promise (deduplication).
28
+ * - Never throws — errors are stored as `reason`.
26
29
  *
27
- * @returns {Promise} that resolves when the fetch is done.
28
- * @returns {void} if this store returned a synchronous value.
29
- * @throws {never} Never throws so safe to call unhandled.
30
+ * @returns `true` if the fetch completed and the value was applied, `false` if aborted or superseded.
31
+ * @returns Synchronous `boolean` if the callback returned a synchronous value.
30
32
  */
31
- refresh(): Promise<void> | void;
32
- /** An in-flight refresh, so we don't de-duplicate these. */
33
+ refresh(): Promise<boolean> | boolean;
33
34
  private _inflight;
34
- await(value: PromiseLike<T>): Promise<void>;
35
- private _awaits;
36
- /** Call the callback with the current payload. */
37
- protected _fetch(): T | PromiseLike<T>;
35
+ private _refresh;
36
+ /**
37
+ * Current `AbortSignal` for this store's in-flight fetch.
38
+ * - Created lazily; a new signal is issued each time `refresh()` starts a new fetch or `abort()` is called.
39
+ */
40
+ get signal(): AbortSignal;
41
+ private _controller;
42
+ /**
43
+ * Call the fetch callback to get the next value.
44
+ * @param signal `AbortSignal` for the current fetch — passed through to the callback so it can cancel HTTP requests etc.
45
+ */
46
+ protected _fetch(signal: AbortSignal): T | PromiseLike<T>;
38
47
  private _callback;
39
- /** Invalidate this endpoint, so a new fetch is triggered next time `this.value` is called. */
48
+ /**
49
+ * Invalidate this store so a new fetch is triggered on the next read of `loading` or `value`.
50
+ * - Also aborts any current in-flight fetch.
51
+ */
40
52
  invalidate(): void;
41
53
  private _invalidation;
42
- /** Re-fetch the result now if the current value is older than `maxAge` millisecond or has been invalidated. */
54
+ /** Re-fetch now if the current value is older than `maxAge` milliseconds or has been invalidated. */
43
55
  refreshStale(maxAge: number): Promise<void>;
44
56
  /**
45
- * Create or get an `AbortSignal` that can be used to cancel an in-flight fetch for this store.
46
- * - implementation code or subclasses can use this signal when fetching to cancel the most recent request
47
- * - The signal will be reset whenever a fetch completes or a new fetch starts.
57
+ * Abort any current in-flight fetch and pending async operation.
58
+ * - Aborts the current `AbortSignal` and clears the controller (a new signal will be created on the next read or fetch).
59
+ * - Clears the in-flight promise and resets busy state.
60
+ * - Any pending `await()` result will be silently discarded.
48
61
  */
49
- get signal(): AbortSignal;
50
- private _controller;
51
- /** Abort the current signal now. */
52
62
  abort(): void;
53
63
  [Symbol.asyncDispose](): Promise<void>;
54
64
  }
@@ -1,6 +1,6 @@
1
1
  import { RequiredError } from "../error/RequiredError.js";
2
2
  import { isAsync } from "../util/async.js";
3
- import { ABORTED, NONE } from "../util/constants.js";
3
+ import { ABORT, NONE } from "../util/constants.js";
4
4
  import { awaitDispose } from "../util/dispose.js";
5
5
  import { BooleanStore } from "./BooleanStore.js";
6
6
  import { Store } from "./Store.js";
@@ -8,126 +8,129 @@ 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 `fetch()` method is invoked to fetch the next value.
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
- * - Can be listened to for e.g. loading spinners etc.
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
- // This is because when we check `store.loading` in a component we are signalling intent that we wish to use that value.
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 in a loading state or is invalid.
28
- // This is because when we check `store.loading` in a component we are signalling intent that we wish to use that value.
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 in the invalid state.
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
42
  this.busy = new BooleanStore(value === NONE);
43
43
  this._callback = callback;
44
44
  }
45
+ // Implement to allow `NONE`
46
+ convert(value) {
47
+ return value;
48
+ }
45
49
  /**
46
- * Fetch the result for this endpoint now.
47
- * - Triggered automatically when someone reads `value` or `loading`
48
- * - Multiple requests to `fetch()` while one is inflight will return the same promise.
50
+ * Fetch the result for this store now.
51
+ * - Triggered automatically when someone reads `value` or `loading`.
52
+ * - Concurrent calls while a fetch is in-flight return the same promise (deduplication).
53
+ * - Never throws — errors are stored as `reason`.
49
54
  *
50
- * @returns {Promise} that resolves when the fetch is done.
51
- * @returns {void} if this store returned a synchronous value.
52
- * @throws {never} Never throws so safe to call unhandled.
55
+ * @returns `true` if the fetch completed and the value was applied, `false` if aborted or superseded.
56
+ * @returns Synchronous `boolean` if the callback returned a synchronous value.
53
57
  */
54
58
  refresh() {
55
59
  if (this._inflight)
56
60
  return this._inflight;
61
+ // Cancel any existing controller and create a fresh one for this fetch.
62
+ this._controller?.abort(ABORT);
63
+ this._controller = new AbortController();
57
64
  try {
58
- const value = this._fetch();
65
+ const value = this._fetch(this._controller.signal);
59
66
  if (isAsync(value))
60
- return (this._inflight = this.await(value));
67
+ return (this._inflight = this._refresh(value));
61
68
  this.value = value;
69
+ return true;
62
70
  }
63
71
  catch (thrown) {
64
72
  this.reason = thrown;
73
+ return false;
65
74
  }
66
75
  }
67
- /** An in-flight refresh, so we don't de-duplicate these. */
68
76
  _inflight = undefined;
69
- // Override to start/stop `this.busy` when awaiting values, and handle aborts correctly.
70
- async await(value) {
77
+ async _refresh(asyncValue) {
71
78
  this.busy.value = true;
72
- this._awaits.add(value);
73
79
  try {
74
- // Capture the invalidation number before the change.
75
- const invalidation = this._invalidation;
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)
80
+ const refreshed = await this.await(asyncValue);
81
+ if (refreshed)
80
82
  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;
83
+ return refreshed;
86
84
  }
87
85
  finally {
88
- this._awaits.delete(value);
89
- if (!this._awaits.size) {
90
- this.busy.value = false;
91
- this._inflight = undefined; // Clear any inflight refresh if this was the last await.
92
- }
93
- this._controller = undefined;
86
+ this.busy.value = false;
87
+ this._inflight = undefined;
94
88
  }
95
89
  }
96
- _awaits = new Set(); // Used to only set `busy` when we have no awaited values left.
97
- /** Call the callback with the current payload. */
98
- _fetch() {
90
+ /**
91
+ * Current `AbortSignal` for this store's in-flight fetch.
92
+ * - Created lazily; a new signal is issued each time `refresh()` starts a new fetch or `abort()` is called.
93
+ */
94
+ get signal() {
95
+ return (this._controller ||= new AbortController()).signal;
96
+ }
97
+ _controller;
98
+ /**
99
+ * Call the fetch callback to get the next value.
100
+ * @param signal `AbortSignal` for the current fetch — passed through to the callback so it can cancel HTTP requests etc.
101
+ */
102
+ _fetch(signal) {
99
103
  if (!this._callback)
100
104
  throw new RequiredError("FetchStore has no callback() function", { store: this, caller: this.refresh });
101
- return this._callback();
105
+ return this._callback(signal);
102
106
  }
103
107
  _callback;
104
- /** Invalidate this endpoint, so a new fetch is triggered next time `this.value` is called. */
108
+ /**
109
+ * Invalidate this store so a new fetch is triggered on the next read of `loading` or `value`.
110
+ * - Also aborts any current in-flight fetch.
111
+ */
105
112
  invalidate() {
106
113
  this.abort();
107
114
  this._invalidation++;
108
115
  }
109
- _invalidation = 0; // Used to track the "invalidation number" which increments on each invalidation and
110
- /** Re-fetch the result now if the current value is older than `maxAge` millisecond or has been invalidated. */
116
+ _invalidation = 0;
117
+ /** Re-fetch now if the current value is older than `maxAge` milliseconds or has been invalidated. */
111
118
  async refreshStale(maxAge) {
112
119
  if (this._invalidation || this.age > maxAge)
113
120
  await this.refresh();
114
121
  }
115
122
  /**
116
- * Create or get an `AbortSignal` that can be used to cancel an in-flight fetch for this store.
117
- * - implementation code or subclasses can use this signal when fetching to cancel the most recent request
118
- * - The signal will be reset whenever a fetch completes or a new fetch starts.
123
+ * Abort any current in-flight fetch and pending async operation.
124
+ * - Aborts the current `AbortSignal` and clears the controller (a new signal will be created on the next read or fetch).
125
+ * - Clears the in-flight promise and resets busy state.
126
+ * - Any pending `await()` result will be silently discarded.
119
127
  */
120
- get signal() {
121
- return (this._controller ||= new AbortController()).signal;
122
- }
123
- _controller;
124
- /** Abort the current signal now. */
125
128
  abort() {
126
- const controller = this._controller;
127
- if (controller) {
128
- controller.abort(ABORTED);
129
- this._controller = undefined;
130
- }
129
+ this._controller?.abort(ABORT);
130
+ this._controller = undefined;
131
+ this._inflight = undefined;
132
+ this.busy.value = false;
133
+ super.abort(); // clears _pendingValue
131
134
  }
132
135
  // Implement `AsyncDisposable`.
133
136
  async [Symbol.asyncDispose]() {
@@ -0,0 +1,10 @@
1
+ import { NONE } from "../util/constants.js";
2
+ import { Store } from "./Store.js";
3
+ /**
4
+ * Store that is explicitly allows a "loading" state when it has no value (which is also the default value).
5
+ * - Note: All stores have know how to interpret `NONE` — this subclass simply _allows_ that through its types.
6
+ */
7
+ export declare class LoadingStore<T> extends Store<T, T | typeof NONE> {
8
+ constructor(value?: T | typeof NONE);
9
+ convert(value: T | typeof NONE): T | typeof NONE;
10
+ }
@@ -0,0 +1,16 @@
1
+ import { NONE } from "../util/constants.js";
2
+ import { Store } from "./Store.js";
3
+ /**
4
+ * Store that is explicitly allows a "loading" state when it has no value (which is also the default value).
5
+ * - Note: All stores have know how to interpret `NONE` — this subclass simply _allows_ that through its types.
6
+ */
7
+ export class LoadingStore extends Store {
8
+ // Override to default to `NONE`
9
+ constructor(value = NONE) {
10
+ super(value);
11
+ }
12
+ // Implement to allow `NONE`
13
+ convert(value) {
14
+ return value;
15
+ }
16
+ }
@@ -1,10 +1,11 @@
1
- import type { AbsolutePath } from "../util/path.js";
1
+ import type { AnyCaller } from "../util/function.js";
2
+ import type { AbsolutePath, PossiblePath } from "../util/path.js";
2
3
  import { Store } from "./Store.js";
3
4
  /** Store an absolute path, e.g. `/a/b/c` */
4
- export declare class PathStore extends Store<AbsolutePath> {
5
- constructor(path?: string);
6
- get value(): AbsolutePath;
7
- set value(path: string);
5
+ export declare class PathStore extends Store<PossiblePath, AbsolutePath> {
6
+ readonly base: AbsolutePath;
7
+ constructor(path?: string, base?: AbsolutePath);
8
+ convert(possible: PossiblePath, caller: AnyCaller): AbsolutePath;
8
9
  /** Based on the current store path, is a path active? */
9
10
  isActive(path: AbsolutePath): boolean;
10
11
  /** Based on the current store path, is a path proud (i.e. a child of the current store path)? */
@@ -2,15 +2,15 @@ import { isPathActive, isPathProud, requirePath } from "../util/path.js";
2
2
  import { Store } from "./Store.js";
3
3
  /** Store an absolute path, e.g. `/a/b/c` */
4
4
  export class PathStore extends Store {
5
- constructor(path = ".") {
6
- super(requirePath(path));
5
+ base;
6
+ // Override to set default path to `.` and base to `/`
7
+ constructor(path = ".", base = "/") {
8
+ super(requirePath(path, base, PathStore));
9
+ this.base = base;
7
10
  }
8
- // Override to clean the path on set.
9
- get value() {
10
- return super.value;
11
- }
12
- set value(path) {
13
- super.value = requirePath(path, super.value);
11
+ // Implement to convert a possible path to an absolute path (relative to `this.base`).
12
+ convert(possible, caller) {
13
+ return requirePath(possible, this.base, caller);
14
14
  }
15
15
  /** Based on the current store path, is a path active? */
16
16
  isActive(path) {
@@ -1,6 +1,6 @@
1
1
  import type { NONE } from "../util/constants.js";
2
2
  import { FetchStore } from "./FetchStore.js";
3
- import { Store } from "./Store.js";
3
+ import { ValueStore } from "./ValueStore.js";
4
4
  export type PayloadFetchCallback<P, R> = (payload: P) => R | PromiseLike<R>;
5
5
  /**
6
6
  * Store that fetches its values from a remote source by sending a payload to them.
@@ -14,7 +14,7 @@ export declare class PayloadFetchStore<P, R> extends FetchStore<R> {
14
14
  * Store keeping the current payload to send to the fetch on send.
15
15
  * - New payloads can be set using `this.payload.value`
16
16
  */
17
- readonly payload: Store<P>;
17
+ readonly payload: ValueStore<P>;
18
18
  constructor(payload: P, value: R | typeof NONE, callback?: PayloadFetchCallback<P, R>);
19
19
  [Symbol.asyncDispose](): Promise<void>;
20
20
  }