shelving 1.189.1 → 1.190.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.189.1",
3
+ "version": "1.190.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,11 +1,10 @@
1
1
  import type { ImmutableArray, PossibleArray } from "../util/array.js";
2
- import type { NONE } from "../util/constants.js";
3
2
  import type { AnyCaller } from "../util/function.js";
4
- import { Store } from "./Store.js";
3
+ import { BusyStore } from "./BusyStore.js";
5
4
  /** Store an array. */
6
- export declare class ArrayStore<T> extends Store<PossibleArray<T>, ImmutableArray<T>> implements Iterable<T> {
7
- constructor(value?: ImmutableArray<T> | typeof NONE);
8
- convert(value: PossibleArray<T>, caller?: AnyCaller): ImmutableArray<T>;
5
+ export declare class ArrayStore<T> extends BusyStore<ImmutableArray<T>, PossibleArray<T>> implements Iterable<T> {
6
+ constructor(value?: PossibleArray<T>);
7
+ protected _convert(input: PossibleArray<T>, caller: AnyCaller): ImmutableArray<T>;
9
8
  /** Get the first item in this store or `null` if this query has no items. */
10
9
  get optionalFirst(): T | undefined;
11
10
  /** Get the last item in this store or `null` if this query has no items. */
@@ -1,13 +1,14 @@
1
1
  import { getFirst, getLast, omitArrayItems, requireArray, requireFirst, requireLast, toggleArrayItems, withArrayItems, } from "../util/array.js";
2
- import { Store } from "./Store.js";
2
+ import { BusyStore } from "./BusyStore.js";
3
3
  /** Store an array. */
4
- export class ArrayStore extends Store {
4
+ export class ArrayStore extends BusyStore {
5
+ // Override to set default value to `[]` and convert possible arrays.
5
6
  constructor(value = []) {
6
- super(value);
7
+ super(requireArray(value, undefined, undefined, ArrayStore));
7
8
  }
8
- // Implement to automatically convert `PossibleArray`
9
- convert(value, caller = this.convert) {
10
- return requireArray(value, undefined, undefined, caller);
9
+ // Implement to convert possible arrays.
10
+ _convert(input, caller) {
11
+ return requireArray(input, undefined, undefined, caller);
11
12
  }
12
13
  /** Get the first item in this store or `null` if this query has no items. */
13
14
  get optionalFirst() {
@@ -1,9 +1,9 @@
1
1
  import { Store } from "./Store.js";
2
2
  /** Store a boolean. */
3
- export declare class BooleanStore extends Store<unknown, boolean> {
3
+ export declare class BooleanStore extends Store<boolean, unknown> {
4
4
  constructor(value?: boolean);
5
- convert(value: unknown): boolean;
5
+ protected _convert(input: unknown): boolean;
6
+ protected _equal(a: boolean, b: boolean): boolean;
6
7
  /** Toggle the current boolean value. */
7
8
  toggle(): void;
8
- isEqual(a: boolean, b: boolean): boolean;
9
9
  }
@@ -1,20 +1,20 @@
1
1
  import { Store } from "./Store.js";
2
2
  /** Store a boolean. */
3
3
  export class BooleanStore extends Store {
4
- // Override to set default value to false.
4
+ // Override to set default value to `false`
5
5
  constructor(value = false) {
6
6
  super(value);
7
7
  }
8
8
  // Override to automatically convert to boolean.
9
- convert(value) {
10
- return !!value;
9
+ _convert(input) {
10
+ return !!input;
11
+ }
12
+ // Override for fast equality.
13
+ _equal(a, b) {
14
+ return a === b;
11
15
  }
12
16
  /** Toggle the current boolean value. */
13
17
  toggle() {
14
18
  this.value = !this.value;
15
19
  }
16
- // Override for fast equality.
17
- isEqual(a, b) {
18
- return a === b;
19
- }
20
20
  }
@@ -0,0 +1,12 @@
1
+ import { BooleanStore } from "./BooleanStore.js";
2
+ import { Store } from "./Store.js";
3
+ /**
4
+ * Store that tracks its busy status via a separate `this.busy` store.
5
+ * - "busy" means the store is awaiting a new value.
6
+ */
7
+ export declare class BusyStore<T, TT = T> extends Store<T, TT> {
8
+ readonly busy: BooleanStore;
9
+ await(pending: PromiseLike<TT>): Promise<boolean>;
10
+ abort(): void;
11
+ [Symbol.asyncDispose](): Promise<void>;
12
+ }
@@ -0,0 +1,27 @@
1
+ import { awaitDispose } from "../util/dispose.js";
2
+ import { BooleanStore } from "./BooleanStore.js";
3
+ import { Store } from "./Store.js";
4
+ /**
5
+ * Store that tracks its busy status via a separate `this.busy` store.
6
+ * - "busy" means the store is awaiting a new value.
7
+ */
8
+ export class BusyStore extends Store {
9
+ busy = new BooleanStore(false);
10
+ // Overload to set `this.busy` to `true` when we start awaiting a value.
11
+ // Gets set back to `false` when `abort()` is called (this also happens whenever a value or reason is set).
12
+ await(pending) {
13
+ this.busy.value = true;
14
+ return super.await(pending);
15
+ }
16
+ // Override to set busy to false on abort.
17
+ // This also happens whenever a value or reaosn is set.
18
+ abort() {
19
+ this.busy.value = false;
20
+ super.abort();
21
+ }
22
+ // Implement `AsyncDisposable`.
23
+ async [Symbol.asyncDispose]() {
24
+ await awaitDispose(() => this.abort(), this.busy, // Send `done: true` to any iterators of the busy store.
25
+ super[Symbol.asyncDispose]());
26
+ }
27
+ }
@@ -1,9 +1,9 @@
1
1
  import type { Data, DataKey } from "../util/data.js";
2
2
  import type { AnyCaller } from "../util/function.js";
3
3
  import type { Updates } from "../util/update.js";
4
- import { Store } from "./Store.js";
4
+ import { BusyStore } from "./BusyStore.js";
5
5
  /** Store a data object. */
6
- export declare class DataStore<T extends Data> extends Store<T, T> {
6
+ export declare class DataStore<T extends Data> extends BusyStore<T> {
7
7
  /** Get the data of this store. */
8
8
  get data(): T;
9
9
  /** Set the data of this store. */
@@ -14,10 +14,9 @@ export declare class DataStore<T extends Data> extends Store<T, 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;
18
17
  }
19
18
  /** Store an optional data object. */
20
- export declare class OptionalDataStore<T extends Data> extends Store<T | undefined, T | undefined> {
19
+ export declare class OptionalDataStore<T extends Data> extends BusyStore<T | undefined> {
21
20
  /** Get current data value of this store (or throw `Promise` that resolves to the next required value). */
22
21
  get data(): T;
23
22
  /** Set the data of this store. */
@@ -34,5 +33,4 @@ export declare class OptionalDataStore<T extends Data> extends Store<T | undefin
34
33
  set<K extends DataKey<T>>(name: K, value: T[K]): void;
35
34
  /** Set the data to `undefined`. */
36
35
  delete(): void;
37
- convert(value: T): T;
38
36
  }
@@ -2,9 +2,9 @@ import { RequiredError } from "../error/RequiredError.js";
2
2
  import { getGetter } from "../util/class.js";
3
3
  import { withProp } from "../util/object.js";
4
4
  import { updateData } from "../util/update.js";
5
- import { Store } from "./Store.js";
5
+ import { BusyStore } from "./BusyStore.js";
6
6
  /** Store a data object. */
7
- export class DataStore extends Store {
7
+ export class DataStore extends BusyStore {
8
8
  /** Get the data of this store. */
9
9
  get data() {
10
10
  return this.value;
@@ -25,13 +25,9 @@ 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
- }
32
28
  }
33
29
  /** Store an optional data object. */
34
- export class OptionalDataStore extends Store {
30
+ export class OptionalDataStore extends BusyStore {
35
31
  /** Get current data value of this store (or throw `Promise` that resolves to the next required value). */
36
32
  get data() {
37
33
  return this.require(getGetter(this, "data"));
@@ -67,8 +63,4 @@ export class OptionalDataStore extends Store {
67
63
  delete() {
68
64
  this.value = undefined;
69
65
  }
70
- // Implement to passthrough.
71
- convert(value) {
72
- return value;
73
- }
74
66
  }
@@ -1,10 +1,10 @@
1
1
  import type { DictionaryItem, ImmutableDictionary, PossibleDictionary } from "../util/dictionary.js";
2
2
  import type { Updates } from "../util/update.js";
3
- import { Store } from "./Store.js";
3
+ import { BusyStore } from "./BusyStore.js";
4
4
  /** Store a dictionary object. */
5
- export declare class DictionaryStore<T> extends Store<PossibleDictionary<T>, ImmutableDictionary<T>> implements Iterable<DictionaryItem<T>> {
5
+ export declare class DictionaryStore<T> extends BusyStore<ImmutableDictionary<T>, PossibleDictionary<T>> implements Iterable<DictionaryItem<T>> {
6
6
  constructor(value?: PossibleDictionary<T>);
7
- convert(possible: PossibleDictionary<T>): ImmutableDictionary<T>;
7
+ protected _convert(possible: PossibleDictionary<T>): ImmutableDictionary<T>;
8
8
  /** Get the length of the current value of this store. */
9
9
  get count(): number;
10
10
  /** Set a named entry in this object with a different value. */
@@ -1,15 +1,15 @@
1
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
- import { Store } from "./Store.js";
4
+ import { BusyStore } from "./BusyStore.js";
5
5
  /** Store a dictionary object. */
6
- export class DictionaryStore extends Store {
6
+ export class DictionaryStore extends BusyStore {
7
7
  // Override to set default value to empty dictionary.
8
8
  constructor(value = EMPTY_DICTIONARY) {
9
9
  super(requireDictionary(value));
10
10
  }
11
11
  // Override to convert a possible dictionary to a dictionary on set.
12
- convert(possible) {
12
+ _convert(possible) {
13
13
  return requireDictionary(possible);
14
14
  }
15
15
  /** Get the length of the current value of this store. */
@@ -1,6 +1,6 @@
1
- import { NONE } from "../util/constants.js";
2
- import { BooleanStore } from "./BooleanStore.js";
3
- import { Store } from "./Store.js";
1
+ import { type NONE } from "../util/constants.js";
2
+ import { BusyStore } from "./BusyStore.js";
3
+ import type { StoreInput } from "./Store.js";
4
4
  /** Callback for a callback fetch store. */
5
5
  export type FetchCallback<T> = (signal: AbortSignal) => T | PromiseLike<T>;
6
6
  /**
@@ -8,57 +8,41 @@ export type FetchCallback<T> = (signal: AbortSignal) => T | PromiseLike<T>;
8
8
  *
9
9
  * @param value The initial value for the store, or `NONE` if it does not have one yet.
10
10
  * @param callback An optional callback that, if set, will be called when the `refresh()` method is invoked to fetch the next value.
11
+ * - Override `this._fetch()` in subclasses to define custom fetching behaviour for a subclass.
11
12
  */
12
- export declare class FetchStore<T> extends Store<T | typeof NONE, T> {
13
- /**
14
- * Store that indicates the busy state of this store.
15
- * - `true` while a refresh is in-flight, `false` otherwise.
16
- * - Can be subscribed to for e.g. loading spinners.
17
- */
18
- readonly busy: BooleanStore;
13
+ export declare class FetchStore<T, TT = T> extends BusyStore<T, TT> {
19
14
  get loading(): boolean;
20
- get value(): T;
21
- set value(value: T | typeof NONE | PromiseLike<T | typeof NONE>);
22
- constructor(value: T | typeof NONE, callback?: FetchCallback<T>);
23
- convert(value: T | typeof NONE): T | typeof NONE;
15
+ write(input: StoreInput<TT>): void;
16
+ read(): import("./Store.js").StoreInternal<T>;
17
+ constructor(value: T | typeof NONE, callback?: FetchCallback<TT>);
24
18
  /**
25
19
  * Fetch the result for this store now.
26
20
  * - Triggered automatically when someone reads `value` or `loading`.
27
- * - Concurrent calls while a fetch is in-flight return the same promise (deduplication).
21
+ * - Refreshes are de-duplicated. Concurrent calls while a fetch is in-flight return the same promise.
28
22
  * - Never throws — errors are stored as `reason`.
29
- *
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.
32
23
  */
33
24
  refresh(): Promise<boolean> | boolean;
34
- private _inflight;
35
- private _refresh;
25
+ private _pendingRefresh;
36
26
  /**
37
27
  * Current `AbortSignal` for this store's in-flight fetch.
38
28
  * - Created lazily; a new signal is issued each time `refresh()` starts a new fetch or `abort()` is called.
29
+ * - Triggers `abort()` so any current awaits are cancelled.
39
30
  */
40
31
  get signal(): AbortSignal;
41
- private _controller;
32
+ private _aborter;
42
33
  /**
43
34
  * Call the fetch callback to get the next value.
44
35
  * @param signal `AbortSignal` for the current fetch — passed through to the callback so it can cancel HTTP requests etc.
45
36
  */
46
- protected _fetch(signal: AbortSignal): T | PromiseLike<T>;
37
+ protected _fetch(signal: AbortSignal): TT | PromiseLike<TT>;
47
38
  private _callback;
48
39
  /**
49
40
  * 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.
41
+ * - Triggers `abort()` so any current awaits are cancelled.
51
42
  */
52
43
  invalidate(): void;
53
44
  private _invalidation;
54
45
  /** Re-fetch now if the current value is older than `maxAge` milliseconds or has been invalidated. */
55
- refreshStale(maxAge: number): Promise<void>;
56
- /**
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.
61
- */
46
+ refreshStale(maxAge: number): Promise<boolean> | boolean;
62
47
  abort(): void;
63
- [Symbol.asyncDispose](): Promise<void>;
64
48
  }
@@ -1,70 +1,56 @@
1
1
  import { RequiredError } from "../error/RequiredError.js";
2
2
  import { isAsync } from "../util/async.js";
3
- import { ABORT, NONE } from "../util/constants.js";
4
- import { awaitDispose } from "../util/dispose.js";
5
- import { BooleanStore } from "./BooleanStore.js";
6
- import { Store } from "./Store.js";
3
+ import { ABORT, SKIP } from "../util/constants.js";
4
+ import { BusyStore } from "./BusyStore.js";
7
5
  /**
8
6
  * Store that fetches its values from a remote source.
9
7
  *
10
8
  * @param value The initial value for the store, or `NONE` if it does not have one yet.
11
9
  * @param callback An optional callback that, if set, will be called when the `refresh()` method is invoked to fetch the next value.
10
+ * - Override `this._fetch()` in subclasses to define custom fetching behaviour for a subclass.
12
11
  */
13
- export class FetchStore extends Store {
14
- /**
15
- * Store that indicates the busy state of this store.
16
- * - `true` while a refresh is in-flight, `false` otherwise.
17
- * - Can be subscribed to for e.g. loading spinners.
18
- */
19
- busy;
20
- // Override to possibly trigger a fetch when `this.loading` is read.
21
- // Reading `loading` signals intent to use the value, so we start a fetch if needed.
12
+ export class FetchStore extends BusyStore {
13
+ // Override to trigger a refresh when `this.loading` is read.
14
+ // Calling `store.loading` signals intent to use the value.
15
+ // We optimistically refresh so the value is available the next time the user wants it.
22
16
  get loading() {
23
17
  const loading = super.loading;
24
- if (loading || this._invalidation)
18
+ if (this._invalidation || loading)
25
19
  void this.refresh();
26
20
  return loading;
27
21
  }
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.
30
- get value() {
31
- if (super.loading)
32
- void this.refresh();
33
- return super.value;
22
+ // Override to reset the invalidation state.
23
+ // Setting a value means this store is no longer invalid.
24
+ write(input) {
25
+ if (input !== SKIP)
26
+ this._invalidation = 0;
27
+ super.write(input);
34
28
  }
35
- set value(value) {
36
- super.value = value; // calls Store.set value() which calls this.abort() then _applyValue()
37
- // Setting a value resets the invalid state.
38
- this._invalidation = 0;
29
+ // Override to trigger refresh on `NONE`
30
+ // Calling `store.value` signals intent to use the value.
31
+ // We optimistically refresh so the value is available the next time the user wants it.
32
+ read() {
33
+ this.loading; // Ping loading to possibly trigger the intiial fetch.
34
+ return super.read();
39
35
  }
36
+ // Override to create to save `callback`
40
37
  constructor(value, callback) {
41
38
  super(value);
42
- this.busy = new BooleanStore(value === NONE);
43
39
  this._callback = callback;
44
40
  }
45
- // Implement to allow `NONE`
46
- convert(value) {
47
- return value;
48
- }
49
41
  /**
50
42
  * Fetch the result for this store now.
51
43
  * - Triggered automatically when someone reads `value` or `loading`.
52
- * - Concurrent calls while a fetch is in-flight return the same promise (deduplication).
44
+ * - Refreshes are de-duplicated. Concurrent calls while a fetch is in-flight return the same promise.
53
45
  * - Never throws — errors are stored as `reason`.
54
- *
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.
57
46
  */
58
47
  refresh() {
59
- if (this._inflight)
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();
48
+ if (this._pendingRefresh)
49
+ return this._pendingRefresh;
64
50
  try {
65
- const value = this._fetch(this._controller.signal);
51
+ const value = this._fetch(this.signal); // Retrieving a new signal calls `abort()` which cancels the previous one.
66
52
  if (isAsync(value))
67
- return (this._inflight = this._refresh(value));
53
+ return (this._pendingRefresh = this.await(value));
68
54
  this.value = value;
69
55
  return true;
70
56
  }
@@ -73,28 +59,18 @@ export class FetchStore extends Store {
73
59
  return false;
74
60
  }
75
61
  }
76
- _inflight = undefined;
77
- async _refresh(asyncValue) {
78
- this.busy.value = true;
79
- try {
80
- const refreshed = await this.await(asyncValue);
81
- if (refreshed)
82
- this._invalidation = 0;
83
- return refreshed;
84
- }
85
- finally {
86
- this.busy.value = false;
87
- this._inflight = undefined;
88
- }
89
- }
62
+ _pendingRefresh = undefined;
90
63
  /**
91
64
  * Current `AbortSignal` for this store's in-flight fetch.
92
65
  * - Created lazily; a new signal is issued each time `refresh()` starts a new fetch or `abort()` is called.
66
+ * - Triggers `abort()` so any current awaits are cancelled.
93
67
  */
94
68
  get signal() {
95
- return (this._controller ||= new AbortController()).signal;
69
+ this.abort();
70
+ this._aborter = new AbortController();
71
+ return this._aborter.signal;
96
72
  }
97
- _controller;
73
+ _aborter;
98
74
  /**
99
75
  * Call the fetch callback to get the next value.
100
76
  * @param signal `AbortSignal` for the current fetch — passed through to the callback so it can cancel HTTP requests etc.
@@ -107,7 +83,7 @@ export class FetchStore extends Store {
107
83
  _callback;
108
84
  /**
109
85
  * 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.
86
+ * - Triggers `abort()` so any current awaits are cancelled.
111
87
  */
112
88
  invalidate() {
113
89
  this.abort();
@@ -115,26 +91,18 @@ export class FetchStore extends Store {
115
91
  }
116
92
  _invalidation = 0;
117
93
  /** Re-fetch now if the current value is older than `maxAge` milliseconds or has been invalidated. */
118
- async refreshStale(maxAge) {
94
+ refreshStale(maxAge) {
119
95
  if (this._invalidation || this.age > maxAge)
120
- await this.refresh();
96
+ return this.refresh();
97
+ return true;
121
98
  }
122
- /**
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.
127
- */
99
+ // Override to abort any current in-flight fetch and pending async operation.
100
+ // - Sends `ABORT` to the current `AbortSignal` and clears the controller (a new signal will be created on the next read or fetch).
101
+ // - Any pending `await()` result will be silently discarded.
128
102
  abort() {
129
- this._controller?.abort(ABORT);
130
- this._controller = undefined;
131
- this._inflight = undefined;
132
- this.busy.value = false;
103
+ this._aborter?.abort(ABORT);
104
+ this._aborter = undefined;
105
+ this._pendingRefresh = undefined;
133
106
  super.abort(); // clears _pendingValue
134
107
  }
135
- // Implement `AsyncDisposable`.
136
- async [Symbol.asyncDispose]() {
137
- await awaitDispose(() => this.abort(), this.busy, // Send `done: true` to any iterators of the busy store.
138
- super[Symbol.asyncDispose]());
139
- }
140
108
  }
@@ -1,11 +1,17 @@
1
1
  import type { AnyCaller } from "../util/function.js";
2
2
  import type { AbsolutePath, PossiblePath } from "../util/path.js";
3
- import { Store } from "./Store.js";
4
- /** Store an absolute path, e.g. `/a/b/c` */
5
- export declare class PathStore extends Store<PossiblePath, AbsolutePath> {
3
+ import { BusyStore } from "./BusyStore.js";
4
+ /**
5
+ * Store an absolute path, e.g. `/a/b/c`
6
+ *
7
+ * @param path: The initial value for the store.
8
+ * @param base: The base path to resolve relative paths against.
9
+ */
10
+ export declare class PathStore extends BusyStore<AbsolutePath, PossiblePath> {
6
11
  readonly base: AbsolutePath;
7
12
  constructor(path?: string, base?: AbsolutePath);
8
- convert(possible: PossiblePath, caller: AnyCaller): AbsolutePath;
13
+ protected _convert(possible: PossiblePath, caller: AnyCaller): AbsolutePath;
14
+ protected _equal(a: AbsolutePath, b: AbsolutePath): boolean;
9
15
  /** Based on the current store path, is a path active? */
10
16
  isActive(path: AbsolutePath): boolean;
11
17
  /** Based on the current store path, is a path proud (i.e. a child of the current store path)? */
@@ -1,17 +1,26 @@
1
1
  import { isPathActive, isPathProud, requirePath } from "../util/path.js";
2
- import { Store } from "./Store.js";
3
- /** Store an absolute path, e.g. `/a/b/c` */
4
- export class PathStore extends Store {
2
+ import { BusyStore } from "./BusyStore.js";
3
+ /**
4
+ * Store an absolute path, e.g. `/a/b/c`
5
+ *
6
+ * @param path: The initial value for the store.
7
+ * @param base: The base path to resolve relative paths against.
8
+ */
9
+ export class PathStore extends BusyStore {
5
10
  base;
6
11
  // Override to set default path to `.` and base to `/`
7
12
  constructor(path = ".", base = "/") {
8
13
  super(requirePath(path, base, PathStore));
9
14
  this.base = base;
10
15
  }
11
- // Implement to convert a possible path to an absolute path (relative to `this.base`).
12
- convert(possible, caller) {
16
+ // Override to convert a possible path to an absolute path (relative to `this.base`).
17
+ _convert(possible, caller) {
13
18
  return requirePath(possible, this.base, caller);
14
19
  }
20
+ // Override for fast equality.
21
+ _equal(a, b) {
22
+ return a === b;
23
+ }
15
24
  /** Based on the current store path, is a path active? */
16
25
  isActive(path) {
17
26
  return isPathActive(this.value, path);
@@ -1,7 +1,7 @@
1
1
  import type { NONE } from "../util/constants.js";
2
2
  import { FetchStore } from "./FetchStore.js";
3
- import { ValueStore } from "./ValueStore.js";
4
- export type PayloadFetchCallback<P, R> = (payload: P) => R | PromiseLike<R>;
3
+ import { Store } from "./Store.js";
4
+ export type PayloadFetchCallback<P, R> = (payload: P, signal: AbortSignal) => R | PromiseLike<R>;
5
5
  /**
6
6
  * Store that fetches its values from a remote source by sending a payload to them.
7
7
  *
@@ -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: ValueStore<P>;
17
+ readonly payload: Store<P>;
18
18
  constructor(payload: P, value: R | typeof NONE, callback?: PayloadFetchCallback<P, R>);
19
19
  [Symbol.asyncDispose](): Promise<void>;
20
20
  }
@@ -1,6 +1,6 @@
1
1
  import { awaitDispose } from "../util/dispose.js";
2
2
  import { FetchStore } from "./FetchStore.js";
3
- import { ValueStore } from "./ValueStore.js";
3
+ import { Store } from "./Store.js";
4
4
  /**
5
5
  * Store that fetches its values from a remote source by sending a payload to them.
6
6
  *
@@ -16,8 +16,8 @@ export class PayloadFetchStore extends FetchStore {
16
16
  payload;
17
17
  // Override to save initial payload and callback.
18
18
  constructor(payload, value, callback) {
19
- const payloadStore = new ValueStore(payload);
20
- super(value, callback && (() => callback(payloadStore.value)));
19
+ const payloadStore = new Store(payload);
20
+ super(value, callback && (signal => callback(payloadStore.value, signal)));
21
21
  this.payload = payloadStore;
22
22
  void _iterate(this);
23
23
  }
package/store/Store.d.ts CHANGED
@@ -4,10 +4,14 @@ import type { AnyCaller, Arguments } from "../util/function.js";
4
4
  import { type PossibleStarter } from "../util/start.js";
5
5
  /** Any `Store` instance. */
6
6
  export type AnyStore = Store<any, any>;
7
+ /** Values that a store natively knows how to process as inputs. */
8
+ export type StoreInput<I> = I | typeof SKIP | typeof NONE;
9
+ /** Internal storage value for a store. */
10
+ export type StoreInternal<O> = O | typeof NONE;
7
11
  /** Callback that sets a store's value (possibly asynchronously). */
8
- export type StoreCallback<I, A extends Arguments = []> = (...args: A) => I | PromiseLike<I>;
12
+ export type StoreCallback<I, A extends Arguments = []> = (...args: A) => StoreInput<I> | PromiseLike<StoreInput<I>>;
9
13
  /** Reducer that receives a store's current value and sets the stores next value (possibly asynchronously). */
10
- export type StoreReducer<I, O, A extends Arguments = []> = (value: O, ...args: A) => I | PromiseLike<I>;
14
+ export type StoreReducer<I, O, A extends Arguments = []> = (value: O, ...args: A) => StoreInput<I> | PromiseLike<StoreInput<I>>;
11
15
  /**
12
16
  * Store that retains its most recent value and is async-iterable to allow values to be observed.
13
17
  * - Current value can be read at `store.value` and `store.data`
@@ -18,43 +22,59 @@ export type StoreReducer<I, O, A extends Arguments = []> = (value: O, ...args: A
18
22
  * - To set this store to be loading, use the `NONE` constant or a `Promise` value.
19
23
  * - To set this store to an explicit value, use that value or another `Store` instance with a value.
20
24
  *
21
- * @param I The "input type" for this store.
22
- * - Indicates what values `this.value` can be set to.
25
+ * @param T The "main type" for this store.
26
+ * - Indicates what values `this.value` will return.
23
27
  * - Methods that set values like `this.call()` and `this.await()` can also accept these values.
24
- * @param O The "output type" for this store.
25
- * - Indicates what value `this.value` will return.
26
- * - Must extend the input type `I`.
27
- * - Defaults to I.
28
+ * @param TT The "input type" for this store.
29
+ * - Indicates what additional input types `this.value` can convert to `T`
30
+ * - Defaults to `T` (no conversion).
31
+ * - Override conversion by overriding `this._convert(v: TT): T`
32
+ * - Warning: With no override, default behaviour is to just assert TT is T (unsafe).
28
33
  */
29
- export declare abstract class Store<I, O> implements AsyncIterable<O, void, void>, AsyncDisposable {
34
+ export declare class Store<T, TT = T> implements AsyncIterable<T, void, void>, AsyncDisposable {
30
35
  /** Deferred sequence this store uses to issue values as they change. */
31
- readonly next: DeferredSequence<O, void, void>;
36
+ readonly next: DeferredSequence<T, void, void>;
32
37
  /**
33
38
  * Store is considered to be "loading" if it has no value or error.
34
39
  * - Calling `this.value` will throw `this.reason` if there's an error reason set, or a `Promise` if there's no value set.
35
40
  * - Calling `this.loading` is a way to check if this store has a value without triggering those throws.
36
41
  */
37
42
  get loading(): boolean;
38
- /**
39
- * Get the current value of this store.
40
- *
41
- * @throws {Promise} if this store currently has no value (resolves when this store receives its next value or error).
42
- * @throws {unknown} if this store currently has an error.
43
- */
44
- get value(): O;
45
43
  /**
46
44
  * Set the value of this store.
47
45
  * - Sets any sync values.
48
46
  * - Awaits any async values.
49
47
  * - Setting value the `NONE` symbol indicates the store has no value so should be in a "loading" state.
50
- * - Setting value to `SKIP` indicates the value should be silently ignored (sometimes it's helpful to have a way to skip setting entirely).
48
+ * - Setting value to `SKIP` indicates the value should be silently ignored (sometimes it's helpful to have a way to skip a write entirely).
51
49
  * - Setting value to the same as the existing value
52
50
  * - If this store has any pending `await()` calls they are aborted and their results are silently discarded.
53
51
  */
54
- set value(value: I | PromiseLike<I>);
52
+ set value(input: StoreInput<TT> | PromiseLike<StoreInput<TT>>);
53
+ /** Write a synchronous value to this store. */
54
+ write(input: StoreInput<TT>): void;
55
+ /**
56
+ * Convert input type to internal storage type.
57
+ * - Override in subclasses to change conversion behaviour.
58
+ * - Warning: With no override, default behaviour is to just assert TT is T (unsafe).
59
+ */
60
+ protected _convert(input: TT, _caller?: AnyCaller): T;
61
+ /** Compare two values for this store and return whether they are equal. */
62
+ protected _equal(a: T, b: T): boolean;
63
+ /** Internal storage for current value. */
55
64
  private _value;
56
- /** Convert a possible value for this store into an actual value of this store. */
57
- abstract convert(possible: I, _caller?: AnyCaller): O | typeof NONE | typeof SKIP;
65
+ /**
66
+ * Get the current value of this store.
67
+ *
68
+ * @throws {Promise} if this store currently is in a "loading" state (resolves when a value is set).
69
+ * @throws {unknown} if this store currently has an error.
70
+ */
71
+ get value(): T;
72
+ /**
73
+ * Called to read values. Can be used to override get behaviour.
74
+ * - Override in subclasses to change getting behaviour.
75
+ * - Note: doesn't throw `reason` if there is one!
76
+ */
77
+ read(): StoreInternal<T>;
58
78
  /**
59
79
  * Time (in milliseconds) this store was last updated with a new value.
60
80
  * - Will be `undefined` if the value is still loading.
@@ -76,33 +96,30 @@ export declare abstract class Store<I, O> implements AsyncIterable<O, void, void
76
96
  * Set a starter for this store to allow a function to execute when this store has subscribers or not.
77
97
  *
78
98
  * @todo DH: Change this significantly. Not happy with how it's settable like this. It should be set in `constructor()`?
99
+ * - Also would love some internal hooks
79
100
  */
80
101
  protected set starter(start: PossibleStarter<[this]>);
81
102
  private _starter;
82
- /** Store is initiated with an initial store. */
83
- constructor(value: O | typeof NONE);
103
+ /** Store is initiated with a value, or `NONE` to put it in a "loading" state. */
104
+ constructor(value: StoreInternal<T>);
84
105
  /** Set the value of this store as values are pulled from a sequence. */
85
- through(sequence: AsyncIterable<I>): AsyncIterable<I>;
106
+ through(sequence: AsyncIterable<TT>): AsyncIterable<TT>;
86
107
  /**
87
- * Call a callback that returns a new value (possibly async) for this store.
88
- * - Errors are stored as `reason`; never throws.
89
- *
90
- * @returns `true` if the value was applied, `false` if an error occurred or the result was superseded.
108
+ * Call a callback and save the returned value to this store.
91
109
  */
92
- call<A extends Arguments>(callback: StoreCallback<I, A>, ...args: A): Promise<boolean> | boolean;
110
+ call<A extends Arguments>(callback: StoreCallback<TT, A>, ...args: A): Promise<boolean> | boolean;
93
111
  /**
94
- * Reduce the current value using a reducer callback that receives the current value.
95
- *
96
- * @param reducer The callback function to call that should return a value to set on this store.
97
- * @param value The current value of this store.
98
- * @param args Any additional input values for the reducer.
99
- * @returns New value for this store (possibly async).
100
- * @oaram args Any arguments to pass to the callback.
101
- *
102
- * @throws {Promise} if this store currently has no value (resolves when this store receives its next value or error).
103
- * @throws {unknown} if this store currently has an error reason set.
112
+ * Send the current value to a callback and save the returned value to this store.
113
+ */
114
+ reduce<A extends Arguments>(reducer: StoreReducer<TT, T, A>, ...args: A): Promise<boolean> | boolean;
115
+ /**
116
+ * Run a callback and ignore any returned value.
117
+ */
118
+ run<A extends Arguments>(callback: (...args: A) => void | PromiseLike<void>, ...args: A): Promise<boolean> | boolean;
119
+ /**
120
+ * Send the current value to a callback and ignore any returned value.
104
121
  */
105
- reduce<A extends Arguments>(reducer: StoreReducer<I, O, A>, ...args: A): Promise<boolean> | boolean;
122
+ send<A extends Arguments>(callback: (value: T, ...args: A) => void | PromiseLike<void>, ...args: A): Promise<boolean> | boolean;
106
123
  /**
107
124
  * Await an async value and save it to this store.
108
125
  * - Saves the resolved value.
@@ -111,19 +128,23 @@ export declare abstract class Store<I, O> implements AsyncIterable<O, void, void
111
128
  * - Silently discarded if `await()` is called again.
112
129
  * - Silently discarded if `abort()` is called.
113
130
  *
114
- * @returns `true` if the value was applied, `false` if superseded, aborted, or errored.
131
+ * @param pending The pending value to await.
132
+ *
133
+ * @returns {true} If the callback returned a value and it was set.
134
+ * @returns {false} If the callback threw.
135
+ * @returns {Promise<true>} If the callback returned a promise and it resolved.
136
+ * @returns {Promise<false>} If the callback returned a promise and it rejected, or `abort()` was called before it resolved.
137
+ *
115
138
  * @throws {never} Never throws — safe to call without handling the return value.
116
139
  */
117
- await(pending: PromiseLike<I>): Promise<boolean>;
118
- private _pending;
140
+ await(pending: PromiseLike<StoreInput<TT>>): Promise<boolean>;
141
+ private _pendingValue;
119
142
  /**
120
143
  * Abort any current pending `await()` call.
121
144
  * - The pending call's result will be silently discarded and its error will not be stored.
122
145
  */
123
146
  abort(): void;
124
- [Symbol.asyncIterator](): AsyncIterator<O, void, void>;
147
+ [Symbol.asyncIterator](): AsyncIterator<T, void, void>;
125
148
  private _iterating;
126
- /** Compare two values for this store and return whether they are equal. */
127
- isEqual(a: O, b: O): boolean;
128
149
  [Symbol.asyncDispose](): Promise<void>;
129
150
  }
package/store/Store.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { DeferredSequence } from "../sequence/DeferredSequence.js";
2
2
  import { isAsync } from "../util/async.js";
3
- import { getSetter } from "../util/class.js";
4
3
  import { NONE, SKIP } from "../util/constants.js";
5
4
  import { awaitDispose } from "../util/dispose.js";
6
5
  import { isDeepEqual } from "../util/equal.js";
@@ -15,13 +14,14 @@ import { getStarter } from "../util/start.js";
15
14
  * - To set this store to be loading, use the `NONE` constant or a `Promise` value.
16
15
  * - To set this store to an explicit value, use that value or another `Store` instance with a value.
17
16
  *
18
- * @param I The "input type" for this store.
19
- * - Indicates what values `this.value` can be set to.
17
+ * @param T The "main type" for this store.
18
+ * - Indicates what values `this.value` will return.
20
19
  * - Methods that set values like `this.call()` and `this.await()` can also accept these values.
21
- * @param O The "output type" for this store.
22
- * - Indicates what value `this.value` will return.
23
- * - Must extend the input type `I`.
24
- * - Defaults to I.
20
+ * @param TT The "input type" for this store.
21
+ * - Indicates what additional input types `this.value` can convert to `T`
22
+ * - Defaults to `T` (no conversion).
23
+ * - Override conversion by overriding `this._convert(v: TT): T`
24
+ * - Warning: With no override, default behaviour is to just assert TT is T (unsafe).
25
25
  */
26
26
  export class Store {
27
27
  /** Deferred sequence this store uses to issue values as they change. */
@@ -32,58 +32,82 @@ export class Store {
32
32
  * - Calling `this.loading` is a way to check if this store has a value without triggering those throws.
33
33
  */
34
34
  get loading() {
35
- return this._value === NONE && this._reason === undefined;
35
+ return this._value === NONE && this.reason === undefined;
36
36
  }
37
+ /**
38
+ * Set the value of this store.
39
+ * - Sets any sync values.
40
+ * - Awaits any async values.
41
+ * - Setting value the `NONE` symbol indicates the store has no value so should be in a "loading" state.
42
+ * - Setting value to `SKIP` indicates the value should be silently ignored (sometimes it's helpful to have a way to skip a write entirely).
43
+ * - Setting value to the same as the existing value
44
+ * - If this store has any pending `await()` calls they are aborted and their results are silently discarded.
45
+ */
46
+ set value(input) {
47
+ if (isAsync(input))
48
+ void this.await(input);
49
+ else
50
+ this.write(input);
51
+ }
52
+ /** Write a synchronous value to this store. */
53
+ write(input) {
54
+ this.abort();
55
+ this._reason = undefined;
56
+ if (input === SKIP) {
57
+ // Skip this value entirely.
58
+ return;
59
+ }
60
+ else if (input === NONE) {
61
+ // Put the store into a loading state.
62
+ this._time = undefined;
63
+ this._value = NONE;
64
+ this.next.cancel();
65
+ return;
66
+ }
67
+ const storage = this._convert(input);
68
+ if (this._value === NONE || !this._equal(storage, this._value)) {
69
+ // Set this changed value.
70
+ this._time = Date.now();
71
+ this._value = storage;
72
+ this.next.resolve(storage);
73
+ }
74
+ }
75
+ /**
76
+ * Convert input type to internal storage type.
77
+ * - Override in subclasses to change conversion behaviour.
78
+ * - Warning: With no override, default behaviour is to just assert TT is T (unsafe).
79
+ */
80
+ _convert(input, _caller) {
81
+ return input;
82
+ }
83
+ /** Compare two values for this store and return whether they are equal. */
84
+ _equal(a, b) {
85
+ return isDeepEqual(a, b);
86
+ }
87
+ /** Internal storage for current value. */
88
+ _value;
37
89
  /**
38
90
  * Get the current value of this store.
39
91
  *
40
- * @throws {Promise} if this store currently has no value (resolves when this store receives its next value or error).
92
+ * @throws {Promise} if this store currently is in a "loading" state (resolves when a value is set).
41
93
  * @throws {unknown} if this store currently has an error.
42
94
  */
43
95
  get value() {
44
96
  if (this._reason !== undefined)
45
97
  throw this._reason;
46
- if (this._value === NONE)
98
+ const value = this.read();
99
+ if (value === NONE)
47
100
  throw this.next;
48
- return this._value;
101
+ return value;
49
102
  }
50
103
  /**
51
- * Set the value of this store.
52
- * - Sets any sync values.
53
- * - Awaits any async values.
54
- * - Setting value the `NONE` symbol indicates the store has no value so should be in a "loading" state.
55
- * - Setting value to `SKIP` indicates the value should be silently ignored (sometimes it's helpful to have a way to skip setting entirely).
56
- * - Setting value to the same as the existing value
57
- * - If this store has any pending `await()` calls they are aborted and their results are silently discarded.
104
+ * Called to read values. Can be used to override get behaviour.
105
+ * - Override in subclasses to change getting behaviour.
106
+ * - Note: doesn't throw `reason` if there is one!
58
107
  */
59
- set value(value) {
60
- if (isAsync(value)) {
61
- void this.await(value);
62
- }
63
- else {
64
- this.abort();
65
- this._reason = undefined;
66
- const v = value === NONE || value === SKIP
67
- ? value || typeof SKIP
68
- : this.convert(value, getSetter(this, "value"));
69
- if (v === NONE) {
70
- // Put the store back into a loading state.
71
- this._time = undefined;
72
- this._value = v;
73
- this.next.cancel();
74
- }
75
- else if (v === SKIP) {
76
- // Silently skip this set value call.
77
- }
78
- else if (this._value === NONE || !this.isEqual(v, this._value)) {
79
- // Set this value.
80
- this._time = Date.now();
81
- this._value = v;
82
- this.next.resolve(v);
83
- }
84
- }
108
+ read() {
109
+ return this._value;
85
110
  }
86
- _value;
87
111
  /**
88
112
  * Time (in milliseconds) this store was last updated with a new value.
89
113
  * - Will be `undefined` if the value is still loading.
@@ -117,6 +141,7 @@ export class Store {
117
141
  * Set a starter for this store to allow a function to execute when this store has subscribers or not.
118
142
  *
119
143
  * @todo DH: Change this significantly. Not happy with how it's settable like this. It should be set in `constructor()`?
144
+ * - Also would love some internal hooks
120
145
  */
121
146
  set starter(start) {
122
147
  if (this._starter)
@@ -126,7 +151,7 @@ export class Store {
126
151
  this._starter.start(this); // Start the new starter if we're already iterating.
127
152
  }
128
153
  _starter;
129
- /** Store is initiated with an initial store. */
154
+ /** Store is initiated with a value, or `NONE` to put it in a "loading" state. */
130
155
  constructor(value) {
131
156
  this._value = value;
132
157
  this._time = value === NONE ? -Infinity : Date.now();
@@ -134,22 +159,19 @@ export class Store {
134
159
  /** Set the value of this store as values are pulled from a sequence. */
135
160
  async *through(sequence) {
136
161
  for await (const value of sequence) {
137
- this.value = value;
162
+ this.write(value);
138
163
  yield value;
139
164
  }
140
165
  }
141
166
  /**
142
- * Call a callback that returns a new value (possibly async) for this store.
143
- * - Errors are stored as `reason`; never throws.
144
- *
145
- * @returns `true` if the value was applied, `false` if an error occurred or the result was superseded.
167
+ * Call a callback and save the returned value to this store.
146
168
  */
147
169
  call(callback, ...args) {
148
170
  try {
149
171
  const value = callback(...args);
150
172
  if (isAsync(value))
151
173
  return this.await(value);
152
- this.value = value;
174
+ this.write(value);
153
175
  return true;
154
176
  }
155
177
  catch (thrown) {
@@ -158,20 +180,23 @@ export class Store {
158
180
  }
159
181
  }
160
182
  /**
161
- * Reduce the current value using a reducer callback that receives the current value.
162
- *
163
- * @param reducer The callback function to call that should return a value to set on this store.
164
- * @param value The current value of this store.
165
- * @param args Any additional input values for the reducer.
166
- * @returns New value for this store (possibly async).
167
- * @oaram args Any arguments to pass to the callback.
168
- *
169
- * @throws {Promise} if this store currently has no value (resolves when this store receives its next value or error).
170
- * @throws {unknown} if this store currently has an error reason set.
183
+ * Send the current value to a callback and save the returned value to this store.
171
184
  */
172
185
  reduce(reducer, ...args) {
173
186
  return this.call(reducer, this.value, ...args);
174
187
  }
188
+ /**
189
+ * Run a callback and ignore any returned value.
190
+ */
191
+ run(callback, ...args) {
192
+ return this.call(_callSkipped, callback, ...args);
193
+ }
194
+ /**
195
+ * Send the current value to a callback and ignore any returned value.
196
+ */
197
+ send(callback, ...args) {
198
+ return this.call(_callSkipped, callback, this.value, ...args);
199
+ }
175
200
  /**
176
201
  * Await an async value and save it to this store.
177
202
  * - Saves the resolved value.
@@ -180,36 +205,42 @@ export class Store {
180
205
  * - Silently discarded if `await()` is called again.
181
206
  * - Silently discarded if `abort()` is called.
182
207
  *
183
- * @returns `true` if the value was applied, `false` if superseded, aborted, or errored.
208
+ * @param pending The pending value to await.
209
+ *
210
+ * @returns {true} If the callback returned a value and it was set.
211
+ * @returns {false} If the callback threw.
212
+ * @returns {Promise<true>} If the callback returned a promise and it resolved.
213
+ * @returns {Promise<false>} If the callback returned a promise and it rejected, or `abort()` was called before it resolved.
214
+ *
184
215
  * @throws {never} Never throws — safe to call without handling the return value.
185
216
  */
186
217
  async await(pending) {
187
218
  // Keep track of the value that is being awaited.
188
219
  // If `_pendingValue` changes while waiting for `asyncValue` to resolve, another call to `await()` has `set value`, `set reason`, or `abort()` has invalidated this one.
189
220
  // If that happens we silently discard the resolved value/reason of this await call.
190
- this._pending = pending;
221
+ this._pendingValue = pending;
191
222
  try {
192
223
  const value = await pending;
193
- if (this._pending === pending) {
194
- this.value = value;
224
+ if (this._pendingValue === pending) {
225
+ this.write(value);
195
226
  return true;
196
227
  }
197
228
  return false;
198
229
  }
199
230
  catch (reason) {
200
- if (this._pending === pending) {
231
+ if (this._pendingValue === pending) {
201
232
  this.reason = reason;
202
233
  }
203
234
  return false;
204
235
  }
205
236
  }
206
- _pending = undefined;
237
+ _pendingValue = undefined;
207
238
  /**
208
239
  * Abort any current pending `await()` call.
209
240
  * - The pending call's result will be silently discarded and its error will not be stored.
210
241
  */
211
242
  abort() {
212
- this._pending = undefined;
243
+ this._pendingValue = undefined;
213
244
  }
214
245
  // Implement `AsyncIterator`
215
246
  // Issues the current value of this store first, then any subsequent values that are issued.
@@ -218,10 +249,11 @@ export class Store {
218
249
  this._starter?.start(this);
219
250
  this._iterating++;
220
251
  try {
221
- if (this._reason !== undefined)
222
- throw this._reason;
223
- if (this._value !== NONE)
224
- yield this._value;
252
+ const reason = this.reason;
253
+ if (reason !== undefined)
254
+ throw reason;
255
+ if (!this.loading)
256
+ yield this.value;
225
257
  yield* this.next;
226
258
  }
227
259
  finally {
@@ -231,13 +263,21 @@ export class Store {
231
263
  }
232
264
  }
233
265
  _iterating = 0;
234
- /** Compare two values for this store and return whether they are equal. */
235
- isEqual(a, b) {
236
- return isDeepEqual(a, b);
237
- }
238
266
  // Implement `AsyncDisposable`
239
267
  async [Symbol.asyncDispose]() {
240
268
  await awaitDispose(this._starter, // Stop the starter.
241
269
  this.next);
242
270
  }
243
271
  }
272
+ /** Call a callback but always return or resolve to `SKIP` */
273
+ function _callSkipped(callback, ...args) {
274
+ const value = callback(...args);
275
+ if (isAsync(value))
276
+ return _awaitSkipped(value);
277
+ return SKIP;
278
+ }
279
+ /** Await a promise but resolve to `SKIP` */
280
+ async function _awaitSkipped(pending) {
281
+ await pending;
282
+ return SKIP;
283
+ }
@@ -1,12 +1,13 @@
1
1
  import type { AnyCaller } from "../util/function.js";
2
2
  import { type PossibleURIParams, type URIParams, type URIScheme } from "../util/uri.js";
3
3
  import { type PossibleURL, type URL, type URLString } from "../util/url.js";
4
- import { Store } from "./Store.js";
4
+ import { BusyStore } from "./BusyStore.js";
5
5
  /** Store a URL, e.g. `https://top.com/a/b/c` */
6
- export declare class URLStore extends Store<PossibleURL, URL> {
6
+ export declare class URLStore extends BusyStore<URL, PossibleURL> {
7
7
  readonly base: URL | undefined;
8
8
  constructor(url: PossibleURL, base?: PossibleURL);
9
- convert(value: PossibleURL, caller?: AnyCaller): URL;
9
+ protected _convert(value: PossibleURL, caller: AnyCaller): URL;
10
+ protected _equal(a: URL, b: URL): boolean;
10
11
  get href(): URLString;
11
12
  set href(href: URLString);
12
13
  get origin(): URLString;
@@ -45,6 +46,5 @@ export declare class URLStore extends Store<PossibleURL, URL> {
45
46
  omitParams(...keys: string[]): URL;
46
47
  /** Return the current URL with an additional param. */
47
48
  omitParam(key: string): URL;
48
- isEqual(a: URL, b: URL): boolean;
49
49
  toString(): string;
50
50
  }
package/store/URLStore.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { getGetter, getSetter } from "../util/class.js";
2
2
  import { clearURIParams, getURIParam, getURIParams, omitURIParams, requireURIParam, withURIParam, withURIParams, } from "../util/uri.js";
3
3
  import { getURL, requireURL } from "../util/url.js";
4
- import { Store } from "./Store.js";
4
+ import { BusyStore } from "./BusyStore.js";
5
5
  /** Store a URL, e.g. `https://top.com/a/b/c` */
6
- export class URLStore extends Store {
6
+ export class URLStore extends BusyStore {
7
7
  base;
8
8
  // Override to convert possible URL to URL.
9
9
  constructor(url, base) {
@@ -12,9 +12,13 @@ export class URLStore extends Store {
12
12
  this.base = baseURL;
13
13
  }
14
14
  // Override to convert possible URL to URL (relative to `this.base`).
15
- convert(value, caller = this.convert) {
15
+ _convert(value, caller) {
16
16
  return requireURL(value, this.base, caller);
17
17
  }
18
+ // Override for fast equality.
19
+ _equal(a, b) {
20
+ return a.href === b.href;
21
+ }
18
22
  get href() {
19
23
  return this.value.href;
20
24
  }
@@ -101,10 +105,6 @@ export class URLStore extends Store {
101
105
  omitParam(key) {
102
106
  return omitURIParams(this.value, key);
103
107
  }
104
- // Override `equal()` to just compare the hrefs.
105
- isEqual(a, b) {
106
- return a.href === b.href;
107
- }
108
108
  toString() {
109
109
  return this.href;
110
110
  }
package/store/index.d.ts CHANGED
@@ -3,9 +3,7 @@ export * from "./BooleanStore.js";
3
3
  export * from "./DataStore.js";
4
4
  export * from "./DictionaryStore.js";
5
5
  export * from "./FetchStore.js";
6
- export * from "./LoadingStore.js";
7
6
  export * from "./PathStore.js";
8
7
  export * from "./PayloadFetchStore.js";
9
8
  export * from "./Store.js";
10
9
  export * from "./URLStore.js";
11
- export * from "./ValueStore.js";
package/store/index.js CHANGED
@@ -3,9 +3,7 @@ export * from "./BooleanStore.js";
3
3
  export * from "./DataStore.js";
4
4
  export * from "./DictionaryStore.js";
5
5
  export * from "./FetchStore.js";
6
- export * from "./LoadingStore.js";
7
6
  export * from "./PathStore.js";
8
7
  export * from "./PayloadFetchStore.js";
9
8
  export * from "./Store.js";
10
9
  export * from "./URLStore.js";
11
- export * from "./ValueStore.js";
@@ -1,10 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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,5 +0,0 @@
1
- import { Store } from "./Store.js";
2
- /** Store that just stores a simple value. */
3
- export declare class ValueStore<T> extends Store<T, T> {
4
- convert(value: T): T;
5
- }
@@ -1,8 +0,0 @@
1
- import { Store } from "./Store.js";
2
- /** Store that just stores a simple value. */
3
- export class ValueStore extends Store {
4
- // Implement to passthrough.
5
- convert(value) {
6
- return value;
7
- }
8
- }