shelving 1.193.2 → 1.195.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/api/cache/APICache.d.ts +10 -2
- package/api/cache/APICache.js +13 -4
- package/api/cache/EndpointCache.d.ts +9 -2
- package/api/cache/EndpointCache.js +15 -4
- package/api/provider/APIProvider.d.ts +1 -2
- package/api/provider/ClientAPIProvider.d.ts +10 -3
- package/api/provider/ClientAPIProvider.js +4 -0
- package/api/provider/ThroughAPIProvider.d.ts +1 -2
- package/db/cache/CollectionCache.d.ts +5 -5
- package/db/cache/CollectionCache.js +10 -10
- package/db/cache/DBCache.d.ts +5 -5
- package/db/cache/DBCache.js +10 -10
- package/package.json +3 -3
- package/store/FetchStore.d.ts +4 -3
- package/store/FetchStore.js +12 -8
- package/store/Store.d.ts +9 -0
- package/store/Store.js +11 -0
- package/store/URLStore.d.ts +9 -9
- package/util/uri.d.ts +44 -38
- package/util/uri.js +32 -32
- package/util/url.d.ts +29 -34
- package/util/url.js +20 -17
package/api/cache/APICache.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AnyCaller } from "../../util/function.js";
|
|
1
2
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
2
3
|
import type { APIProvider } from "../provider/APIProvider.js";
|
|
3
4
|
import { EndpointCache } from "./EndpointCache.js";
|
|
@@ -12,13 +13,20 @@ export declare class APICache<P, R> implements AsyncDisposable {
|
|
|
12
13
|
private _get;
|
|
13
14
|
/** Get (or create) the `EndpointCache` for the given endpoint. */
|
|
14
15
|
get<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>): EndpointCache<PP, RR>;
|
|
16
|
+
/**
|
|
17
|
+
* Fetch (or return a cached result) for the given endpoint and payload.
|
|
18
|
+
* - Returns the cached value immediately if one exists.
|
|
19
|
+
* - Waits for the in-flight fetch if the store is loading.
|
|
20
|
+
* - Throws if the fetch fails, matching `APIProvider.call` behaviour.
|
|
21
|
+
*/
|
|
22
|
+
call<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, maxAge?: number, caller?: AnyCaller): Promise<RR>;
|
|
15
23
|
/** Invalidate a specific store for an endpoint. */
|
|
16
24
|
invalidate<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP): void;
|
|
17
25
|
/** Invalidate all stores for an endpoint. */
|
|
18
26
|
invalidateAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>): void;
|
|
19
27
|
/** Trigger a refetch on a specific store for an endpoint. */
|
|
20
|
-
refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP): void;
|
|
28
|
+
refresh<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, maxAge?: number): void;
|
|
21
29
|
/** Trigger a refetch on all stores for an endpoint. */
|
|
22
|
-
refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR
|
|
30
|
+
refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, maxAge?: number): void;
|
|
23
31
|
[Symbol.asyncDispose](): Promise<void>;
|
|
24
32
|
}
|
package/api/cache/APICache.js
CHANGED
|
@@ -17,6 +17,15 @@ export class APICache {
|
|
|
17
17
|
get(endpoint) {
|
|
18
18
|
return this._get(endpoint) || setMapItem(this._endpoints, endpoint, new EndpointCache(endpoint, this.provider));
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Fetch (or return a cached result) for the given endpoint and payload.
|
|
22
|
+
* - Returns the cached value immediately if one exists.
|
|
23
|
+
* - Waits for the in-flight fetch if the store is loading.
|
|
24
|
+
* - Throws if the fetch fails, matching `APIProvider.call` behaviour.
|
|
25
|
+
*/
|
|
26
|
+
async call(endpoint, payload, maxAge, caller = this.call) {
|
|
27
|
+
return this.get(endpoint).call(payload, maxAge, caller);
|
|
28
|
+
}
|
|
20
29
|
/** Invalidate a specific store for an endpoint. */
|
|
21
30
|
invalidate(endpoint, payload) {
|
|
22
31
|
this._get(endpoint)?.invalidate(payload);
|
|
@@ -26,12 +35,12 @@ export class APICache {
|
|
|
26
35
|
this._get(endpoint)?.invalidateAll();
|
|
27
36
|
}
|
|
28
37
|
/** Trigger a refetch on a specific store for an endpoint. */
|
|
29
|
-
refresh(endpoint, payload) {
|
|
30
|
-
this._get(endpoint)?.refresh(payload);
|
|
38
|
+
refresh(endpoint, payload, maxAge) {
|
|
39
|
+
this._get(endpoint)?.refresh(payload, maxAge);
|
|
31
40
|
}
|
|
32
41
|
/** Trigger a refetch on all stores for an endpoint. */
|
|
33
|
-
refreshAll(endpoint) {
|
|
34
|
-
this._get(endpoint)?.refreshAll();
|
|
42
|
+
refreshAll(endpoint, maxAge) {
|
|
43
|
+
this._get(endpoint)?.refreshAll(maxAge);
|
|
35
44
|
}
|
|
36
45
|
// Implement `AsyncDisposable`
|
|
37
46
|
[Symbol.asyncDispose]() {
|
|
@@ -13,13 +13,20 @@ export declare class EndpointCache<P = unknown, R = unknown> implements AsyncDis
|
|
|
13
13
|
constructor(endpoint: Endpoint<P, R>, provider: APIProvider<P, R>);
|
|
14
14
|
/** Get (or create) the `EndpointStore` for the given payload. */
|
|
15
15
|
get(payload: P, caller?: AnyCaller): EndpointStore<P, R>;
|
|
16
|
+
/**
|
|
17
|
+
* Fetch (or return a cached result) for the given payload.
|
|
18
|
+
* - Returns the cached value immediately if one exists.
|
|
19
|
+
* - Waits for the in-flight fetch if the store is loading.
|
|
20
|
+
* - Throws if the fetch fails, matching `APIProvider.call` behaviour.
|
|
21
|
+
*/
|
|
22
|
+
call(payload: P, maxAge?: number, caller?: AnyCaller): Promise<R>;
|
|
16
23
|
/** Invalidate a specific store. */
|
|
17
24
|
invalidate(payload: P, caller?: AnyCaller): void;
|
|
18
25
|
/** Invalidate all stores. */
|
|
19
26
|
invalidateAll(): void;
|
|
20
27
|
/** Trigger a refetch on a specific store. */
|
|
21
|
-
refresh(payload: P, caller?: AnyCaller): Promise<void>;
|
|
28
|
+
refresh(payload: P, maxAge?: number, caller?: AnyCaller): Promise<void>;
|
|
22
29
|
/** Trigger a refetch on all stores. */
|
|
23
|
-
refreshAll(): Promise<void>;
|
|
30
|
+
refreshAll(maxAge?: number): Promise<void>;
|
|
24
31
|
[Symbol.asyncDispose](): Promise<void>;
|
|
25
32
|
}
|
|
@@ -19,6 +19,17 @@ export class EndpointCache {
|
|
|
19
19
|
const url = this.provider.renderURL(this.endpoint, payload, caller).href;
|
|
20
20
|
return this._endpoints.get(url) || setMapItem(this._endpoints, url, new EndpointStore(this.endpoint, payload, this.provider));
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Fetch (or return a cached result) for the given payload.
|
|
24
|
+
* - Returns the cached value immediately if one exists.
|
|
25
|
+
* - Waits for the in-flight fetch if the store is loading.
|
|
26
|
+
* - Throws if the fetch fails, matching `APIProvider.call` behaviour.
|
|
27
|
+
*/
|
|
28
|
+
async call(payload, maxAge, caller = this.call) {
|
|
29
|
+
const store = this.get(payload, caller);
|
|
30
|
+
await store.refresh(maxAge);
|
|
31
|
+
return store.value;
|
|
32
|
+
}
|
|
22
33
|
/** Invalidate a specific store. */
|
|
23
34
|
invalidate(payload, caller = this.invalidate) {
|
|
24
35
|
this.get(payload, caller)?.invalidate();
|
|
@@ -29,12 +40,12 @@ export class EndpointCache {
|
|
|
29
40
|
store.invalidate();
|
|
30
41
|
}
|
|
31
42
|
/** Trigger a refetch on a specific store. */
|
|
32
|
-
async refresh(payload, caller = this.invalidate) {
|
|
33
|
-
await this.get(payload, caller)?.refresh();
|
|
43
|
+
async refresh(payload, maxAge, caller = this.invalidate) {
|
|
44
|
+
await this.get(payload, caller)?.refresh(maxAge);
|
|
34
45
|
}
|
|
35
46
|
/** Trigger a refetch on all stores. */
|
|
36
|
-
async refreshAll() {
|
|
37
|
-
await awaitValues(...this._endpoints.values().map(store => store.refresh()));
|
|
47
|
+
async refreshAll(maxAge) {
|
|
48
|
+
await awaitValues(...this._endpoints.values().map(store => store.refresh(maxAge)));
|
|
38
49
|
}
|
|
39
50
|
// Implement `AsyncDisposable`
|
|
40
51
|
[Symbol.asyncDispose]() {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { AnyCaller } from "../../util/function.js";
|
|
2
2
|
import type { RequestOptions } from "../../util/http.js";
|
|
3
|
-
import type { BaseURL, URL } from "../../util/url.js";
|
|
4
3
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
5
4
|
/** Provider for API endpoints rooted at a common base URL. */
|
|
6
5
|
export declare abstract class APIProvider<P = unknown, R = unknown> {
|
|
7
6
|
/** The base URL for this API. */
|
|
8
|
-
abstract readonly url:
|
|
7
|
+
abstract readonly url: URL;
|
|
9
8
|
/**
|
|
10
9
|
* Render the full final URL for an API request to a given endpoint with a given payload.
|
|
11
10
|
* - Includes `?query` params if this is a `HEAD` or `GET` request.
|
|
@@ -2,12 +2,19 @@ import type { AnyCaller } from "../../util/function.js";
|
|
|
2
2
|
import { type RequestBodyMethod, type RequestHeadMethod, type RequestOptions } from "../../util/http.js";
|
|
3
3
|
import type { Nullish } from "../../util/null.js";
|
|
4
4
|
import { type PossibleURIParams } from "../../util/uri.js";
|
|
5
|
-
import { type
|
|
5
|
+
import { type PossibleURL } from "../../util/url.js";
|
|
6
6
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
7
7
|
import { APIProvider } from "./APIProvider.js";
|
|
8
8
|
/** Options for a `ClientAPIProvider`. */
|
|
9
9
|
export interface ClientAPIProviderOptions {
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The common base URL for all rendered endpoint requests.
|
|
12
|
+
*
|
|
13
|
+
* Note: When resolving URLs for endpoints this is treated as if it ends in a slash.
|
|
14
|
+
* - e.g. in `http://p.com/a/b/c` the path will be relative to `c` as if a `/` trailing slash was present.
|
|
15
|
+
* - This is different to the default behaviour of `new URL()`, but is the more natural expected result
|
|
16
|
+
* - This is consistent with our e.g. `getURL()` utilities.
|
|
17
|
+
*/
|
|
11
18
|
readonly url: PossibleURL;
|
|
12
19
|
/**
|
|
13
20
|
* Options used for HTTP requests created with `this.getRequest()` and `this.fetch()`
|
|
@@ -27,7 +34,7 @@ export interface ClientAPIProviderOptions {
|
|
|
27
34
|
*/
|
|
28
35
|
export declare class ClientAPIProvider<P = unknown, R = unknown> extends APIProvider<P, R> {
|
|
29
36
|
/** The common base URL for all rendered endpoint requests. */
|
|
30
|
-
readonly url:
|
|
37
|
+
readonly url: URL;
|
|
31
38
|
/** Default options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
|
|
32
39
|
readonly options: RequestOptions;
|
|
33
40
|
/** Timeout in milliseconds, or `undefined` for no timeout. */
|
|
@@ -28,6 +28,10 @@ export class ClientAPIProvider extends APIProvider {
|
|
|
28
28
|
this.timeout = timeout;
|
|
29
29
|
}
|
|
30
30
|
renderURL(endpoint, payload, caller = this.renderURL) {
|
|
31
|
+
// Construct the full URL from `this.url` and the rendered path.
|
|
32
|
+
// Adding the `.` turns the absolute path from `renderPath()` into a relative URL.
|
|
33
|
+
// `requireURL()` resolves that path relative to `this.url`
|
|
34
|
+
// Note that `requireURL()` rendering treats paths as folders, e.g. in `/a/b/c` the path will be relative to `c` not `b`
|
|
31
35
|
const url = requireURL(`.${endpoint.renderPath(payload, caller)}`, this.url, caller);
|
|
32
36
|
// HEAD or GET have no body (but payload can only be data object).
|
|
33
37
|
if (isRequestHeadMethod(endpoint.method)) {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AnyCaller } from "../../util/function.js";
|
|
2
2
|
import type { RequestOptions } from "../../util/http.js";
|
|
3
3
|
import type { Sourceable } from "../../util/source.js";
|
|
4
|
-
import type { BaseURL, URL } from "../../util/url.js";
|
|
5
4
|
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
6
5
|
import { APIProvider } from "./APIProvider.js";
|
|
7
6
|
/**
|
|
@@ -9,7 +8,7 @@ import { APIProvider } from "./APIProvider.js";
|
|
|
9
8
|
* - Extend this when you want to intercept only selected API operations, such as injecting auth headers or logging.
|
|
10
9
|
*/
|
|
11
10
|
export declare class ThroughAPIProvider<P, R> extends APIProvider<P, R> implements Sourceable<APIProvider<P, R>> {
|
|
12
|
-
get url():
|
|
11
|
+
get url(): URL;
|
|
13
12
|
readonly source: APIProvider<P, R>;
|
|
14
13
|
constructor(source: APIProvider<P, R>);
|
|
15
14
|
renderURL<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, payload: PP, caller?: AnyCaller): URL;
|
|
@@ -23,15 +23,15 @@ export declare class CollectionCache<I extends Identifier, T extends Data> imple
|
|
|
23
23
|
/** Get (or create) the `QueryStore` for the given query. */
|
|
24
24
|
getQuery(query: Query<Item<I, T>>): QueryStore<I, T>;
|
|
25
25
|
/** Refresh a specific item store. */
|
|
26
|
-
refreshItem(id: I): Promise<void>;
|
|
26
|
+
refreshItem(id: I, maxAge?: number): Promise<void>;
|
|
27
27
|
/** Refresh every cached item store. */
|
|
28
|
-
refreshItems(): Promise<void>;
|
|
28
|
+
refreshItems(maxAge?: number): Promise<void>;
|
|
29
29
|
/** Refresh a specific query store. */
|
|
30
|
-
refreshQuery(query: Query<Item<I, T
|
|
30
|
+
refreshQuery(query: Query<Item<I, T>>, maxAge?: number): Promise<void>;
|
|
31
31
|
/** Refresh every cached query store. */
|
|
32
|
-
refreshQueries(): Promise<void>;
|
|
32
|
+
refreshQueries(maxAge?: number): Promise<void>;
|
|
33
33
|
/** Refresh every cached store (items and queries). */
|
|
34
|
-
refreshAll(): Promise<void>;
|
|
34
|
+
refreshAll(maxAge?: number): Promise<void>;
|
|
35
35
|
private _queryKey;
|
|
36
36
|
[Symbol.asyncDispose](): Promise<void>;
|
|
37
37
|
}
|
|
@@ -29,24 +29,24 @@ export class CollectionCache {
|
|
|
29
29
|
return this._queries.get(key) || setMapItem(this._queries, key, new QueryStore(this.collection, query, this.provider, this.memory));
|
|
30
30
|
}
|
|
31
31
|
/** Refresh a specific item store. */
|
|
32
|
-
async refreshItem(id) {
|
|
33
|
-
await this._items.get(id)?.refresh();
|
|
32
|
+
async refreshItem(id, maxAge) {
|
|
33
|
+
await this._items.get(id)?.refresh(maxAge);
|
|
34
34
|
}
|
|
35
35
|
/** Refresh every cached item store. */
|
|
36
|
-
async refreshItems() {
|
|
37
|
-
await awaitValues(...this._items.values().map(store => store.refresh()));
|
|
36
|
+
async refreshItems(maxAge) {
|
|
37
|
+
await awaitValues(...this._items.values().map(store => store.refresh(maxAge)));
|
|
38
38
|
}
|
|
39
39
|
/** Refresh a specific query store. */
|
|
40
|
-
async refreshQuery(query) {
|
|
41
|
-
await this._queries.get(this._queryKey(query))?.refresh();
|
|
40
|
+
async refreshQuery(query, maxAge) {
|
|
41
|
+
await this._queries.get(this._queryKey(query))?.refresh(maxAge);
|
|
42
42
|
}
|
|
43
43
|
/** Refresh every cached query store. */
|
|
44
|
-
async refreshQueries() {
|
|
45
|
-
await awaitValues(...this._queries.values().map(store => store.refresh()));
|
|
44
|
+
async refreshQueries(maxAge) {
|
|
45
|
+
await awaitValues(...this._queries.values().map(store => store.refresh(maxAge)));
|
|
46
46
|
}
|
|
47
47
|
/** Refresh every cached store (items and queries). */
|
|
48
|
-
async refreshAll() {
|
|
49
|
-
await awaitValues(this.refreshItems(), this.refreshQueries());
|
|
48
|
+
async refreshAll(maxAge) {
|
|
49
|
+
await awaitValues(this.refreshItems(maxAge), this.refreshQueries(maxAge));
|
|
50
50
|
}
|
|
51
51
|
_queryKey(query) {
|
|
52
52
|
return JSON.stringify(query);
|
package/db/cache/DBCache.d.ts
CHANGED
|
@@ -25,14 +25,14 @@ export declare class DBCache<I extends Identifier = Identifier, T extends Data =
|
|
|
25
25
|
/** Get (or create) a `QueryStore` for a collection/query in one hop. */
|
|
26
26
|
getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>): QueryStore<II, TT>;
|
|
27
27
|
/** Refresh a specific item store for a collection. */
|
|
28
|
-
refreshItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
|
|
28
|
+
refreshItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II, maxAge?: number): Promise<void>;
|
|
29
29
|
/** Refresh every cached item store for a collection. */
|
|
30
|
-
refreshItems<II extends I, TT extends T>(collection: Collection<string, II, TT
|
|
30
|
+
refreshItems<II extends I, TT extends T>(collection: Collection<string, II, TT>, maxAge?: number): Promise<void>;
|
|
31
31
|
/** Refresh a specific query store for a collection. */
|
|
32
|
-
refreshQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT
|
|
32
|
+
refreshQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, maxAge?: number): Promise<void>;
|
|
33
33
|
/** Refresh every cached query store for a collection. */
|
|
34
|
-
refreshQueries<II extends I, TT extends T>(collection: Collection<string, II, TT
|
|
34
|
+
refreshQueries<II extends I, TT extends T>(collection: Collection<string, II, TT>, maxAge?: number): Promise<void>;
|
|
35
35
|
/** Refresh every cached store (items and queries) for a collection. */
|
|
36
|
-
refreshAll<II extends I, TT extends T>(collection: Collection<string, II, TT
|
|
36
|
+
refreshAll<II extends I, TT extends T>(collection: Collection<string, II, TT>, maxAge?: number): Promise<void>;
|
|
37
37
|
[Symbol.asyncDispose](): Promise<void>;
|
|
38
38
|
}
|
package/db/cache/DBCache.js
CHANGED
|
@@ -32,24 +32,24 @@ export class DBCache {
|
|
|
32
32
|
return this.get(collection).getQuery(query);
|
|
33
33
|
}
|
|
34
34
|
/** Refresh a specific item store for a collection. */
|
|
35
|
-
async refreshItem(collection, id) {
|
|
36
|
-
await this._get(collection)?.refreshItem(id);
|
|
35
|
+
async refreshItem(collection, id, maxAge) {
|
|
36
|
+
await this._get(collection)?.refreshItem(id, maxAge);
|
|
37
37
|
}
|
|
38
38
|
/** Refresh every cached item store for a collection. */
|
|
39
|
-
async refreshItems(collection) {
|
|
40
|
-
await this._get(collection)?.refreshItems();
|
|
39
|
+
async refreshItems(collection, maxAge) {
|
|
40
|
+
await this._get(collection)?.refreshItems(maxAge);
|
|
41
41
|
}
|
|
42
42
|
/** Refresh a specific query store for a collection. */
|
|
43
|
-
async refreshQuery(collection, query) {
|
|
44
|
-
await this._get(collection)?.refreshQuery(query);
|
|
43
|
+
async refreshQuery(collection, query, maxAge) {
|
|
44
|
+
await this._get(collection)?.refreshQuery(query, maxAge);
|
|
45
45
|
}
|
|
46
46
|
/** Refresh every cached query store for a collection. */
|
|
47
|
-
async refreshQueries(collection) {
|
|
48
|
-
await this._get(collection)?.refreshQueries();
|
|
47
|
+
async refreshQueries(collection, maxAge) {
|
|
48
|
+
await this._get(collection)?.refreshQueries(maxAge);
|
|
49
49
|
}
|
|
50
50
|
/** Refresh every cached store (items and queries) for a collection. */
|
|
51
|
-
async refreshAll(collection) {
|
|
52
|
-
await this._get(collection)?.refreshAll();
|
|
51
|
+
async refreshAll(collection, maxAge) {
|
|
52
|
+
await this._get(collection)?.refreshAll(maxAge);
|
|
53
53
|
}
|
|
54
54
|
// Implement `AsyncDisposable`
|
|
55
55
|
async [Symbol.asyncDispose]() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shelving",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.195.0",
|
|
4
4
|
"author": "Dave Houlbrooke <dave@shax.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"main": "./index.js",
|
|
10
10
|
"module": "./index.js",
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"@biomejs/biome": "^2.4.
|
|
12
|
+
"@biomejs/biome": "^2.4.14",
|
|
13
13
|
"@google-cloud/firestore": "^8.5.0",
|
|
14
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.
|
|
17
|
+
"@typescript/native-preview": "^7.0.0-dev.20260502.1",
|
|
18
18
|
"firebase": "^12.12.1",
|
|
19
19
|
"react": "^19.2.5",
|
|
20
20
|
"react-dom": "^19.2.5"
|
package/store/FetchStore.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class FetchStore<T, TT = T> extends BusyStore<T, TT> {
|
|
|
14
14
|
get loading(): boolean;
|
|
15
15
|
write(input: StoreInput<TT>): void;
|
|
16
16
|
read(): import("./Store.js").StoreInternal<T>;
|
|
17
|
+
get age(): number;
|
|
17
18
|
constructor(value: T | typeof NONE, callback?: FetchCallback<TT>);
|
|
18
19
|
/**
|
|
19
20
|
* Fetch the result for this store now.
|
|
@@ -21,7 +22,7 @@ export declare class FetchStore<T, TT = T> extends BusyStore<T, TT> {
|
|
|
21
22
|
* - Refreshes are de-duplicated. Concurrent calls while a fetch is in-flight return the same promise.
|
|
22
23
|
* - Never throws — errors are stored as `reason`.
|
|
23
24
|
*/
|
|
24
|
-
refresh(): Promise<boolean> | boolean;
|
|
25
|
+
refresh(maxAge?: number): Promise<boolean> | boolean;
|
|
25
26
|
private _pendingRefresh;
|
|
26
27
|
/**
|
|
27
28
|
* Current `AbortSignal` for this store's in-flight fetch.
|
|
@@ -36,13 +37,13 @@ export declare class FetchStore<T, TT = T> extends BusyStore<T, TT> {
|
|
|
36
37
|
*/
|
|
37
38
|
protected _fetch(signal: AbortSignal): TT | PromiseLike<TT>;
|
|
38
39
|
private _callback;
|
|
40
|
+
/** Whether this store is has currently been invalidated and needs a refresh. */
|
|
41
|
+
get invalidated(): boolean;
|
|
39
42
|
/**
|
|
40
43
|
* Invalidate this store so a new fetch is triggered on the next read of `loading` or `value`.
|
|
41
44
|
* - Triggers `abort()` so any current awaits are cancelled.
|
|
42
45
|
*/
|
|
43
46
|
invalidate(): void;
|
|
44
47
|
private _invalidation;
|
|
45
|
-
/** Re-fetch now if the current value is older than `maxAge` milliseconds or has been invalidated. */
|
|
46
|
-
refreshStale(maxAge: number): Promise<boolean> | boolean;
|
|
47
48
|
abort(): void;
|
|
48
49
|
}
|
package/store/FetchStore.js
CHANGED
|
@@ -15,7 +15,7 @@ export class FetchStore extends BusyStore {
|
|
|
15
15
|
// We optimistically refresh so the value is available the next time the user wants it.
|
|
16
16
|
get loading() {
|
|
17
17
|
const loading = super.loading;
|
|
18
|
-
if (this.
|
|
18
|
+
if (this.invalidated || loading)
|
|
19
19
|
void this.refresh();
|
|
20
20
|
return loading;
|
|
21
21
|
}
|
|
@@ -33,6 +33,10 @@ export class FetchStore extends BusyStore {
|
|
|
33
33
|
this.loading; // Ping loading to possibly trigger the intiial fetch.
|
|
34
34
|
return super.read();
|
|
35
35
|
}
|
|
36
|
+
// Override to consider invalid to be really old.
|
|
37
|
+
get age() {
|
|
38
|
+
return this.invalidated ? Infinity : super.age;
|
|
39
|
+
}
|
|
36
40
|
// Override to create to save `callback`
|
|
37
41
|
constructor(value, callback) {
|
|
38
42
|
super(value);
|
|
@@ -44,9 +48,11 @@ export class FetchStore extends BusyStore {
|
|
|
44
48
|
* - Refreshes are de-duplicated. Concurrent calls while a fetch is in-flight return the same promise.
|
|
45
49
|
* - Never throws — errors are stored as `reason`.
|
|
46
50
|
*/
|
|
47
|
-
refresh() {
|
|
51
|
+
refresh(maxAge) {
|
|
48
52
|
if (this._pendingRefresh)
|
|
49
53
|
return this._pendingRefresh;
|
|
54
|
+
if (!this.stale(maxAge))
|
|
55
|
+
return false;
|
|
50
56
|
try {
|
|
51
57
|
const value = this._fetch(this.signal); // Retrieving a new signal calls `abort()` which cancels the previous one.
|
|
52
58
|
if (isAsync(value))
|
|
@@ -81,6 +87,10 @@ export class FetchStore extends BusyStore {
|
|
|
81
87
|
return this._callback(signal);
|
|
82
88
|
}
|
|
83
89
|
_callback;
|
|
90
|
+
/** Whether this store is has currently been invalidated and needs a refresh. */
|
|
91
|
+
get invalidated() {
|
|
92
|
+
return !!this._invalidation;
|
|
93
|
+
}
|
|
84
94
|
/**
|
|
85
95
|
* Invalidate this store so a new fetch is triggered on the next read of `loading` or `value`.
|
|
86
96
|
* - Triggers `abort()` so any current awaits are cancelled.
|
|
@@ -90,12 +100,6 @@ export class FetchStore extends BusyStore {
|
|
|
90
100
|
this._invalidation++;
|
|
91
101
|
}
|
|
92
102
|
_invalidation = 0;
|
|
93
|
-
/** Re-fetch now if the current value is older than `maxAge` milliseconds or has been invalidated. */
|
|
94
|
-
refreshStale(maxAge) {
|
|
95
|
-
if (this._invalidation || this.age > maxAge)
|
|
96
|
-
return this.refresh();
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
103
|
// Override to abort any current in-flight fetch and pending async operation.
|
|
100
104
|
// - Sends `ABORT` to the current `AbortSignal` and clears the controller (a new signal will be created on the next read or fetch).
|
|
101
105
|
// - Any pending `await()` result will be silently discarded.
|
package/store/Store.d.ts
CHANGED
|
@@ -92,6 +92,15 @@ export declare class Store<T, TT = T> implements AsyncIterable<T, void, void>, A
|
|
|
92
92
|
* @example if (store.age > MINUTE) refreshStore(store);
|
|
93
93
|
*/
|
|
94
94
|
get age(): number;
|
|
95
|
+
/**
|
|
96
|
+
* Whether this store is stale based on a `maxAge` value in milliseconds.
|
|
97
|
+
*
|
|
98
|
+
* @param maxAge The maximum age for the stale check.
|
|
99
|
+
* - `0` zero means "always refresh" (this is the default).
|
|
100
|
+
* - `Infinity` means "refresh only if store is still in a loading state.
|
|
101
|
+
* - Any other value may or may not be stale based on `this.age`
|
|
102
|
+
*/
|
|
103
|
+
stale(maxAge?: number): boolean;
|
|
95
104
|
/** Current error of this store, or `undefined` if there is no error. */
|
|
96
105
|
get reason(): unknown;
|
|
97
106
|
set reason(reason: unknown);
|
package/store/Store.js
CHANGED
|
@@ -132,6 +132,17 @@ export class Store {
|
|
|
132
132
|
const time = this.time;
|
|
133
133
|
return typeof time === "number" ? Date.now() - time : Infinity;
|
|
134
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether this store is stale based on a `maxAge` value in milliseconds.
|
|
137
|
+
*
|
|
138
|
+
* @param maxAge The maximum age for the stale check.
|
|
139
|
+
* - `0` zero means "always refresh" (this is the default).
|
|
140
|
+
* - `Infinity` means "refresh only if store is still in a loading state.
|
|
141
|
+
* - Any other value may or may not be stale based on `this.age`
|
|
142
|
+
*/
|
|
143
|
+
stale(maxAge = 0) {
|
|
144
|
+
return this.age >= maxAge;
|
|
145
|
+
}
|
|
135
146
|
/** Current error of this store, or `undefined` if there is no error. */
|
|
136
147
|
get reason() {
|
|
137
148
|
return this._reason;
|
package/store/URLStore.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { AnyCaller } from "../util/function.js";
|
|
2
2
|
import type { AbsolutePath } from "../util/index.js";
|
|
3
3
|
import { type PossibleURIParams, type URIParams, type URIScheme } from "../util/uri.js";
|
|
4
|
-
import { type
|
|
4
|
+
import { type ImmutableURL, type PossibleURL, type URLString } from "../util/url.js";
|
|
5
5
|
import { BusyStore } from "./BusyStore.js";
|
|
6
6
|
/** Store a URL, e.g. `https://top.com/a/b/c` */
|
|
7
|
-
export declare class URLStore extends BusyStore<
|
|
8
|
-
readonly base:
|
|
7
|
+
export declare class URLStore extends BusyStore<ImmutableURL, PossibleURL> {
|
|
8
|
+
readonly base: ImmutableURL | undefined;
|
|
9
9
|
constructor(url: PossibleURL, base?: PossibleURL);
|
|
10
|
-
protected _convert(value: PossibleURL, caller: AnyCaller):
|
|
11
|
-
protected _equal(a:
|
|
10
|
+
protected _convert(value: PossibleURL, caller: AnyCaller): ImmutableURL;
|
|
11
|
+
protected _equal(a: ImmutableURL, b: ImmutableURL): boolean;
|
|
12
12
|
get href(): URLString;
|
|
13
13
|
set href(href: URLString);
|
|
14
14
|
get origin(): URLString;
|
|
@@ -40,12 +40,12 @@ export declare class URLStore extends BusyStore<URL, PossibleURL> {
|
|
|
40
40
|
/** Clear all params from this URL. */
|
|
41
41
|
clearParams(): void;
|
|
42
42
|
/** Return the current URL with an additional param. */
|
|
43
|
-
withParam(key: string, value: unknown):
|
|
43
|
+
withParam(key: string, value: unknown): ImmutableURL;
|
|
44
44
|
/** Return the current URL with an additional param. */
|
|
45
|
-
withParams(params: PossibleURIParams):
|
|
45
|
+
withParams(params: PossibleURIParams): ImmutableURL;
|
|
46
46
|
/** Return the current URL with an additional param. */
|
|
47
|
-
omitParams(...keys: string[]):
|
|
47
|
+
omitParams(...keys: string[]): ImmutableURL;
|
|
48
48
|
/** Return the current URL with an additional param. */
|
|
49
|
-
omitParam(key: string):
|
|
49
|
+
omitParam(key: string): ImmutableURL;
|
|
50
50
|
toString(): string;
|
|
51
51
|
}
|
package/util/uri.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ImmutableArray } from "./array.js";
|
|
|
2
2
|
import { type ImmutableDictionary } from "./dictionary.js";
|
|
3
3
|
import type { AnyCaller } from "./function.js";
|
|
4
4
|
import { type Nullish } from "./null.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ImmutableURL, URLString } from "./url.js";
|
|
6
6
|
/**
|
|
7
7
|
* Valid URI string is anything following `protocol:resource` format, e.g. `urn:isbn:0451450523` or `http://example.com/path/to/resource`
|
|
8
8
|
*
|
|
@@ -14,6 +14,18 @@ import type { URL, URLString } from "./url.js";
|
|
|
14
14
|
* - All URLs are also URIs, but not all URIs are URLs.
|
|
15
15
|
*/
|
|
16
16
|
export type URIString = `${string}:${string}`;
|
|
17
|
+
export type URISearch = `?${string}`;
|
|
18
|
+
export type URIHash = `#${string}`;
|
|
19
|
+
/**
|
|
20
|
+
* Construct a correctly-typed `URI` object.
|
|
21
|
+
* - This is a more correctly typed version of the builtin Javascript `URI` constructor.
|
|
22
|
+
* - Requires a URI string, URI object, or path as input, and optionally a base URI.
|
|
23
|
+
* - If a path is provided as input, a base URI _must_ also be provided.
|
|
24
|
+
* - The returned type is
|
|
25
|
+
*/
|
|
26
|
+
export interface ImmutableURIConstructor {
|
|
27
|
+
new (input: URIString | ImmutableURI): ImmutableURI;
|
|
28
|
+
}
|
|
17
29
|
/**
|
|
18
30
|
* Object that describes a valid URI, e.g. `urn:isbn:0451450523` or `http://example.com/path/to/resource`
|
|
19
31
|
* - Improves the builtin Javascript `URL` class to more accurately type its properties.
|
|
@@ -24,37 +36,31 @@ export type URIString = `${string}:${string}`;
|
|
|
24
36
|
* - The absence of `//` indicates a non-hierarchical URI.
|
|
25
37
|
* - URLs can be considered as "hierarchical URIs".
|
|
26
38
|
* - All URLs are also URIs, but not all URIs are URLs.
|
|
27
|
-
*
|
|
28
|
-
* Javascript URL problems:
|
|
29
|
-
* - Javascript `URL` instance can actually represent any kind of URI (not just URLs).
|
|
30
|
-
* - It's more "correct" terminology to use `URI` to refer to what the Javascript `URL` class represents.
|
|
31
|
-
* - You can tell the difference because a URL will have a non-empty `host` property, whereas URIs will never have a `host` (it will be `""` empty string).
|
|
32
|
-
*/
|
|
33
|
-
export interface URI extends globalThis.URL {
|
|
34
|
-
protocol: URIScheme;
|
|
35
|
-
href: URIString;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Construct a correctly-typed `URI` object.
|
|
39
|
-
* - This is a more correctly typed version of the builtin Javascript `URI` constructor.
|
|
40
|
-
* - Requires a URI string, URI object, or path as input, and optionally a base URI.
|
|
41
|
-
* - If a path is provided as input, a base URI _must_ also be provided.
|
|
42
|
-
* - The returned type is
|
|
43
39
|
*/
|
|
44
|
-
export interface
|
|
45
|
-
|
|
40
|
+
export interface ImmutableURI extends URL {
|
|
41
|
+
readonly hash: URIHash | ``;
|
|
42
|
+
readonly host: string;
|
|
43
|
+
readonly hostname: string;
|
|
44
|
+
readonly href: URIString;
|
|
45
|
+
readonly origin: URIString | `null`;
|
|
46
|
+
readonly password: string;
|
|
47
|
+
readonly pathname: string;
|
|
48
|
+
readonly port: string;
|
|
49
|
+
readonly protocol: URIScheme;
|
|
50
|
+
readonly search: URISearch | ``;
|
|
51
|
+
readonly username: string;
|
|
46
52
|
}
|
|
47
|
-
export declare const
|
|
48
|
-
/** Values that can be converted to
|
|
49
|
-
export type PossibleURI = string |
|
|
53
|
+
export declare const ImmutableURI: ImmutableURIConstructor;
|
|
54
|
+
/** Values that can be converted to an ImmutableURI instance. */
|
|
55
|
+
export type PossibleURI = string | URL;
|
|
50
56
|
/** Is an unknown value a URI object? */
|
|
51
|
-
export declare function isURI(value: unknown): value is
|
|
57
|
+
export declare function isURI(value: unknown): value is ImmutableURI;
|
|
52
58
|
/** Assert that an unknown value is a URI object. */
|
|
53
|
-
export declare function assertURI(value: unknown, caller?: AnyCaller): asserts value is
|
|
59
|
+
export declare function assertURI(value: unknown, caller?: AnyCaller): asserts value is ImmutableURI;
|
|
54
60
|
/** Convert a possible URI to a URI, or return `undefined` if conversion fails. */
|
|
55
|
-
export declare function getURI(possible: Nullish<PossibleURI>):
|
|
61
|
+
export declare function getURI(possible: Nullish<PossibleURI>): ImmutableURI | undefined;
|
|
56
62
|
/** Convert a possible URI to a URI, or throw `RequiredError` if conversion fails. */
|
|
57
|
-
export declare function requireURI(possible: PossibleURI, caller?: AnyCaller):
|
|
63
|
+
export declare function requireURI(possible: PossibleURI, caller?: AnyCaller): ImmutableURI;
|
|
58
64
|
/** Convert a possible URI to a URI string, or return `undefined` if conversion fails. */
|
|
59
65
|
export declare function getURIString(possible: Nullish<PossibleURI>): URIString | undefined;
|
|
60
66
|
/** Convert a possible URI to a URI string, or throw `RequiredError` if conversion fails. */
|
|
@@ -67,19 +73,19 @@ export type PossibleURIParams = PossibleURI | URLSearchParams | ImmutableDiction
|
|
|
67
73
|
* Get a set of params for a URI as a dictionary.
|
|
68
74
|
* - Any params with `undefined` value will be ignored.
|
|
69
75
|
*/
|
|
70
|
-
export declare function getURIParams(
|
|
76
|
+
export declare function getURIParams(params: PossibleURIParams, caller?: AnyCaller): URIParams;
|
|
71
77
|
/** Get a single named param from a URI. */
|
|
72
|
-
export declare function getURIParam(
|
|
78
|
+
export declare function getURIParam(params: PossibleURIParams, key: string): string | undefined;
|
|
73
79
|
/** Get a single named param from a URI. */
|
|
74
|
-
export declare function requireURIParam(
|
|
80
|
+
export declare function requireURIParam(params: PossibleURIParams, key: string, caller?: AnyCaller): string;
|
|
75
81
|
/**
|
|
76
82
|
* Return a URI with a new param set (or same URI if no changes were made).
|
|
77
83
|
* - Any params with `undefined` value will be ignored.
|
|
78
84
|
*
|
|
79
85
|
* @throws `ValueError` if the value could not be converted to a string.
|
|
80
86
|
*/
|
|
81
|
-
export declare function withURIParam(
|
|
82
|
-
export declare function withURIParam(
|
|
87
|
+
export declare function withURIParam(uri: ImmutableURL | URLString, key: string, value: unknown, caller?: AnyCaller): ImmutableURL;
|
|
88
|
+
export declare function withURIParam(uri: PossibleURI, key: string, value: unknown, caller?: AnyCaller): ImmutableURI;
|
|
83
89
|
/**
|
|
84
90
|
* Return a URI with several new params set (or same URI if no changes were made).
|
|
85
91
|
* - Any params with `undefined` value will be ignored.
|
|
@@ -89,18 +95,18 @@ export declare function withURIParam(url: PossibleURI, key: string, value: unkno
|
|
|
89
95
|
*
|
|
90
96
|
* @throws `ValueError` if any of the values could not be converted to strings.
|
|
91
97
|
*/
|
|
92
|
-
export declare function withURIParams(
|
|
93
|
-
export declare function withURIParams(
|
|
98
|
+
export declare function withURIParams(uri: ImmutableURL | URLString, params: Nullish<PossibleURIParams>, caller?: AnyCaller): ImmutableURL;
|
|
99
|
+
export declare function withURIParams(uri: PossibleURI, params: Nullish<PossibleURIParams>, caller?: AnyCaller): ImmutableURI;
|
|
94
100
|
/**
|
|
95
101
|
* Return a URI without one or more params (or same URI if no changes were made).
|
|
96
102
|
*/
|
|
97
|
-
export declare function omitURIParams(
|
|
98
|
-
export declare function omitURIParams(
|
|
103
|
+
export declare function omitURIParams(uri: ImmutableURL | URLString, ...keys: string[]): ImmutableURL;
|
|
104
|
+
export declare function omitURIParams(uri: PossibleURI, ...keys: string[]): ImmutableURI;
|
|
99
105
|
/** Return a URI without a param (or same URI if no changes were made). */
|
|
100
|
-
export declare const omitURIParam: (
|
|
106
|
+
export declare const omitURIParam: (uri: PossibleURI, key: string) => ImmutableURI;
|
|
101
107
|
/** Return a URI with no search params (or same URI if no changes were made). */
|
|
102
|
-
export declare function clearURIParams(
|
|
103
|
-
export declare function clearURIParams(
|
|
108
|
+
export declare function clearURIParams(uri: ImmutableURL | URLString, caller?: AnyCaller): ImmutableURL;
|
|
109
|
+
export declare function clearURIParams(uri: PossibleURI, caller?: AnyCaller): ImmutableURI;
|
|
104
110
|
/** A single schema for a URL. */
|
|
105
111
|
export type URIScheme = `${string}:`;
|
|
106
112
|
/** List of allowed URI schemes. */
|
package/util/uri.js
CHANGED
|
@@ -3,10 +3,10 @@ import { ValueError } from "../error/ValueError.js";
|
|
|
3
3
|
import { getDictionaryItems, isDictionary } from "./dictionary.js";
|
|
4
4
|
import { notNullish } from "./null.js";
|
|
5
5
|
import { getString, isString } from "./string.js";
|
|
6
|
-
export const
|
|
6
|
+
export const ImmutableURI = URL;
|
|
7
7
|
/** Is an unknown value a URI object? */
|
|
8
8
|
export function isURI(value) {
|
|
9
|
-
return value instanceof
|
|
9
|
+
return value instanceof ImmutableURI;
|
|
10
10
|
}
|
|
11
11
|
/** Assert that an unknown value is a URI object. */
|
|
12
12
|
export function assertURI(value, caller = assertURI) {
|
|
@@ -19,7 +19,7 @@ export function getURI(possible) {
|
|
|
19
19
|
if (isURI(possible))
|
|
20
20
|
return possible;
|
|
21
21
|
try {
|
|
22
|
-
return new
|
|
22
|
+
return new URL(possible, _BASE);
|
|
23
23
|
}
|
|
24
24
|
catch {
|
|
25
25
|
return undefined;
|
|
@@ -50,16 +50,16 @@ export function requireURIString(possible, caller = requireURIString) {
|
|
|
50
50
|
* 2. So when converting this to a simple data object, only one value per key can be represented, but it needs to be the _first_ one.
|
|
51
51
|
* 3. Since we're looping through anyway, we also take the time to convert values to strings, so we can accept a wider range of input types.
|
|
52
52
|
*/
|
|
53
|
-
function* getURIEntries(
|
|
54
|
-
if (
|
|
55
|
-
yield*
|
|
53
|
+
function* getURIEntries(params, caller = getURIParams) {
|
|
54
|
+
if (params instanceof URLSearchParams) {
|
|
55
|
+
yield* params;
|
|
56
56
|
}
|
|
57
|
-
else if (isString(
|
|
58
|
-
yield* requireURI(
|
|
57
|
+
else if (isString(params) || params instanceof URL) {
|
|
58
|
+
yield* requireURI(params, caller).searchParams;
|
|
59
59
|
}
|
|
60
60
|
else {
|
|
61
61
|
const done = [];
|
|
62
|
-
for (const [key, value] of getDictionaryItems(
|
|
62
|
+
for (const [key, value] of getDictionaryItems(params)) {
|
|
63
63
|
if (value === undefined)
|
|
64
64
|
continue; // Skip undefined.
|
|
65
65
|
if (done.includes(key))
|
|
@@ -76,63 +76,63 @@ function* getURIEntries(input, caller = getURIParams) {
|
|
|
76
76
|
* Get a set of params for a URI as a dictionary.
|
|
77
77
|
* - Any params with `undefined` value will be ignored.
|
|
78
78
|
*/
|
|
79
|
-
export function getURIParams(
|
|
79
|
+
export function getURIParams(params, caller = getURIParams) {
|
|
80
80
|
const output = {};
|
|
81
|
-
for (const [key, str] of getURIEntries(
|
|
81
|
+
for (const [key, str] of getURIEntries(params, caller))
|
|
82
82
|
output[key] = str;
|
|
83
83
|
return output;
|
|
84
84
|
}
|
|
85
85
|
/** Get a single named param from a URI. */
|
|
86
|
-
export function getURIParam(
|
|
87
|
-
if (
|
|
88
|
-
return
|
|
89
|
-
if (isDictionary(
|
|
90
|
-
return getString(
|
|
91
|
-
return getURIParams(
|
|
86
|
+
export function getURIParam(params, key) {
|
|
87
|
+
if (params instanceof URLSearchParams)
|
|
88
|
+
return params.get(key) || undefined;
|
|
89
|
+
if (isDictionary(params))
|
|
90
|
+
return getString(params[key]);
|
|
91
|
+
return getURIParams(params)[key];
|
|
92
92
|
}
|
|
93
93
|
/** Get a single named param from a URI. */
|
|
94
|
-
export function requireURIParam(
|
|
95
|
-
const value = getURIParam(
|
|
94
|
+
export function requireURIParam(params, key, caller = requireURIParam) {
|
|
95
|
+
const value = getURIParam(params, key);
|
|
96
96
|
if (value === undefined)
|
|
97
|
-
throw new RequiredError(`URI param "${key}" is required`, { received:
|
|
97
|
+
throw new RequiredError(`URI param "${key}" is required`, { received: params, caller });
|
|
98
98
|
return value;
|
|
99
99
|
}
|
|
100
|
-
export function withURIParam(
|
|
101
|
-
const input = requireURI(
|
|
100
|
+
export function withURIParam(uri, key, value, caller = withURIParam) {
|
|
101
|
+
const input = requireURI(uri, caller);
|
|
102
102
|
if (value === undefined)
|
|
103
103
|
return input; // Ignore undefined.
|
|
104
|
-
const output = new
|
|
104
|
+
const output = new ImmutableURI(input);
|
|
105
105
|
const str = getString(value);
|
|
106
106
|
if (str === undefined)
|
|
107
107
|
throw new ValueError(`URI param "${key}" must be string`, { received: value, caller });
|
|
108
108
|
output.searchParams.set(key, str);
|
|
109
109
|
return input.href === output.href ? input : output;
|
|
110
110
|
}
|
|
111
|
-
export function withURIParams(
|
|
112
|
-
const input = requireURI(
|
|
111
|
+
export function withURIParams(uri, params, caller = withURIParams) {
|
|
112
|
+
const input = requireURI(uri, caller);
|
|
113
113
|
if (!params)
|
|
114
114
|
return input;
|
|
115
|
-
const output = new
|
|
115
|
+
const output = new ImmutableURI(input);
|
|
116
116
|
for (const [key, str] of getURIEntries(params, caller))
|
|
117
117
|
output.searchParams.set(key, str);
|
|
118
118
|
return input.href === output.href ? input : output;
|
|
119
119
|
}
|
|
120
|
-
export function omitURIParams(
|
|
121
|
-
const input = requireURI(
|
|
120
|
+
export function omitURIParams(uri, ...keys) {
|
|
121
|
+
const input = requireURI(uri, omitURIParams);
|
|
122
122
|
if (!keys.length)
|
|
123
123
|
return input;
|
|
124
|
-
const output = new
|
|
124
|
+
const output = new ImmutableURI(input);
|
|
125
125
|
for (const key of keys)
|
|
126
126
|
output.searchParams.delete(key);
|
|
127
127
|
return input.href === output.href ? input : output;
|
|
128
128
|
}
|
|
129
129
|
/** Return a URI without a param (or same URI if no changes were made). */
|
|
130
130
|
export const omitURIParam = omitURIParams;
|
|
131
|
-
export function clearURIParams(
|
|
132
|
-
const input = requireURI(
|
|
131
|
+
export function clearURIParams(uri, caller = clearURIParams) {
|
|
132
|
+
const input = requireURI(uri, caller);
|
|
133
133
|
if (!input.search.length)
|
|
134
134
|
return input;
|
|
135
|
-
const output = new
|
|
135
|
+
const output = new URL(input);
|
|
136
136
|
output.search = "";
|
|
137
137
|
return output;
|
|
138
138
|
}
|
package/util/url.d.ts
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import type { AnyCaller } from "./function.js";
|
|
2
2
|
import type { Nullish } from "./null.js";
|
|
3
3
|
import type { AbsolutePath } from "./path.js";
|
|
4
|
-
import type {
|
|
5
|
-
type BuiltinURL = globalThis.URL;
|
|
6
|
-
declare const BuiltinURL: {
|
|
7
|
-
new (url: string | globalThis.URL, base?: string | globalThis.URL): globalThis.URL;
|
|
8
|
-
prototype: globalThis.URL;
|
|
9
|
-
canParse(url: string | globalThis.URL, base?: string | globalThis.URL): boolean;
|
|
10
|
-
createObjectURL(obj: Blob | MediaSource): string;
|
|
11
|
-
parse(url: string | globalThis.URL, base?: string | globalThis.URL): globalThis.URL | null;
|
|
12
|
-
revokeObjectURL(url: string): void;
|
|
13
|
-
};
|
|
4
|
+
import type { ImmutableURI } from "./uri.js";
|
|
14
5
|
/**
|
|
15
6
|
* A URL string has a protocol and a `//`.
|
|
16
7
|
* - The `//` at the start of a URL indicates that it has a hierarchical path component, so this makes it a URL.
|
|
17
8
|
* - URLs have a concept of "absolute" or "relative" URLs, since they have a path.
|
|
18
9
|
*/
|
|
19
10
|
export type URLString = `${string}://${string}`;
|
|
11
|
+
/**
|
|
12
|
+
* Construct a correctly-typed `URL` object.
|
|
13
|
+
* - This is a more correctly typed version of the builtin Javascript `URL` constructor.
|
|
14
|
+
* - Requires a URL string, URL object, or path as input, and optionally a base URL.
|
|
15
|
+
* - If a path is provided as input, a base URL _must_ also be provided.
|
|
16
|
+
* - The returned type is
|
|
17
|
+
*/
|
|
18
|
+
export interface ImmutableURLConstructor {
|
|
19
|
+
new (input: URLString | ImmutableURL, base?: URLString | ImmutableURL): ImmutableURL;
|
|
20
|
+
new (input: URLString | ImmutableURL | string, base: URLString | ImmutableURL): ImmutableURL;
|
|
21
|
+
}
|
|
20
22
|
/**
|
|
21
23
|
* Object that describes a valid URL, e.g. `http://example.com/path/to/resource`
|
|
22
24
|
* - Improves the builtin Javascript `URL` class to more accurately type its properties.
|
|
@@ -32,41 +34,35 @@ export type URLString = `${string}://${string}`;
|
|
|
32
34
|
* - Javascript `URL` instance can actually represent any kind of URI (not just URLs).
|
|
33
35
|
* - It's more "correct" terminology to use `URI` to refer to what the Javascript `URL` class represents.
|
|
34
36
|
* - You can tell the difference because a URL will have a non-empty `host` property, whereas URIs will never have a `host` (it will be `""` empty string).
|
|
37
|
+
* - Javascript URLs are mutable which can lead to subtle bugs.
|
|
35
38
|
*/
|
|
36
|
-
export interface
|
|
39
|
+
export interface ImmutableURL extends ImmutableURI {
|
|
37
40
|
readonly href: URLString;
|
|
38
41
|
readonly origin: URLString;
|
|
39
|
-
readonly pathname:
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Construct a correctly-typed `URL` object.
|
|
43
|
-
* - This is a more correctly typed version of the builtin Javascript `URL` constructor.
|
|
44
|
-
* - Requires a URL string, URL object, or path as input, and optionally a base URL.
|
|
45
|
-
* - If a path is provided as input, a base URL _must_ also be provided.
|
|
46
|
-
* - The returned type is
|
|
47
|
-
*/
|
|
48
|
-
export interface URLConstructor {
|
|
49
|
-
new (input: URLString | URL, base?: URLString | URL): URL;
|
|
50
|
-
new (input: URLString | URL | string, base: URLString | URL): URL;
|
|
42
|
+
readonly pathname: AbsolutePath;
|
|
51
43
|
}
|
|
52
|
-
export declare const
|
|
44
|
+
export declare const ImmutableURL: ImmutableURLConstructor;
|
|
53
45
|
/** Values that can be converted to a URL instance. */
|
|
54
|
-
export type PossibleURL = string |
|
|
46
|
+
export type PossibleURL = string | URL;
|
|
55
47
|
/**
|
|
56
48
|
* Is an unknown value a URL object?
|
|
57
49
|
* - Must be a `URL` instance and its origin must start with `scheme://`
|
|
58
50
|
*/
|
|
59
|
-
export declare function isURL(value: unknown): value is
|
|
51
|
+
export declare function isURL(value: unknown): value is ImmutableURL;
|
|
60
52
|
/** Assert that an unknown value is a URL object. */
|
|
61
|
-
export declare function assertURL(value: unknown, caller?: AnyCaller): asserts value is
|
|
53
|
+
export declare function assertURL(value: unknown, caller?: AnyCaller): asserts value is ImmutableURL;
|
|
62
54
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
55
|
+
* Resolve a possible URL relative to a base URL, or return `undefined` if conversion fails.
|
|
56
|
+
*
|
|
57
|
+
* Note: When resolving relative URLs this treats `base` as if it ends in a slash.
|
|
58
|
+
* - e.g. if `base` is `http://p.com/a/b/c` the path will be relative to `c` as if a `/` trailing slash was present.
|
|
59
|
+
* - This is different to the default behaviour of `new URL()`, but is the more natural expected result
|
|
60
|
+
* - This is consistent with our e.g. `getURL()` utilities.
|
|
61
|
+
*
|
|
66
62
|
*/
|
|
67
|
-
export declare function getURL(
|
|
63
|
+
export declare function getURL(target: Nullish<PossibleURL>, base?: PossibleURL): ImmutableURL | undefined;
|
|
68
64
|
/** Convert a possible URL to a URL, or throw `RequiredError` if conversion fails. */
|
|
69
|
-
export declare function requireURL(
|
|
65
|
+
export declare function requireURL(target: PossibleURL, base?: PossibleURL, caller?: AnyCaller): ImmutableURL;
|
|
70
66
|
/**
|
|
71
67
|
* Resolve and match a target URL/path against a base URL and return the remaining path.
|
|
72
68
|
*
|
|
@@ -80,7 +76,7 @@ export declare function requireURL(possible: PossibleURL, base?: PossibleURL, ca
|
|
|
80
76
|
*/
|
|
81
77
|
export declare function matchURLPrefix(target: PossibleURL, base: PossibleURL, caller?: AnyCaller): AbsolutePath | undefined;
|
|
82
78
|
/** BaseURL is a URL with a guaranteed trailing slash on pathname. */
|
|
83
|
-
export interface BaseURL extends
|
|
79
|
+
export interface BaseURL extends ImmutableURL {
|
|
84
80
|
readonly pathname: `/` | `/${string}/`;
|
|
85
81
|
}
|
|
86
82
|
/** Is an unknown value a valid Base URL. */
|
|
@@ -89,4 +85,3 @@ export declare function isBaseURL(value: PossibleURL): value is BaseURL;
|
|
|
89
85
|
export declare function getBaseURL(input: Nullish<PossibleURL>): BaseURL | undefined;
|
|
90
86
|
/** Require a Base URL. */
|
|
91
87
|
export declare function requireBaseURL(value: PossibleURL, caller: AnyCaller): BaseURL;
|
|
92
|
-
export {};
|
package/util/url.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { RequiredError } from "../error/RequiredError.js";
|
|
2
|
-
const
|
|
3
|
-
export const URL = BuiltinURL;
|
|
2
|
+
export const ImmutableURL = URL;
|
|
4
3
|
/**
|
|
5
4
|
* Is an unknown value a URL object?
|
|
6
5
|
* - Must be a `URL` instance and its origin must start with `scheme://`
|
|
7
6
|
*/
|
|
8
7
|
export function isURL(value) {
|
|
9
|
-
return value instanceof
|
|
8
|
+
return value instanceof URL && _isURL(value);
|
|
10
9
|
}
|
|
11
10
|
function _isURL(uri) {
|
|
12
11
|
return uri.href.startsWith(`${uri.protocol}//`);
|
|
@@ -17,33 +16,37 @@ export function assertURL(value, caller = assertURL) {
|
|
|
17
16
|
throw new RequiredError("Invalid URL", { received: value, caller });
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* Resolve a possible URL relative to a base URL, or return `undefined` if conversion fails.
|
|
20
|
+
*
|
|
21
|
+
* Note: When resolving relative URLs this treats `base` as if it ends in a slash.
|
|
22
|
+
* - e.g. if `base` is `http://p.com/a/b/c` the path will be relative to `c` as if a `/` trailing slash was present.
|
|
23
|
+
* - This is different to the default behaviour of `new URL()`, but is the more natural expected result
|
|
24
|
+
* - This is consistent with our e.g. `getURL()` utilities.
|
|
25
|
+
*
|
|
23
26
|
*/
|
|
24
|
-
export function getURL(
|
|
25
|
-
if (!
|
|
27
|
+
export function getURL(target, base) {
|
|
28
|
+
if (!target)
|
|
26
29
|
return;
|
|
27
|
-
const uri =
|
|
30
|
+
const uri = _getURL(target, base);
|
|
28
31
|
if (uri && _isURL(uri))
|
|
29
32
|
return uri;
|
|
30
33
|
}
|
|
31
|
-
function
|
|
32
|
-
if (
|
|
33
|
-
return
|
|
34
|
+
function _getURL(target, base) {
|
|
35
|
+
if (target instanceof URL)
|
|
36
|
+
return target;
|
|
34
37
|
try {
|
|
35
38
|
// We need a base URL to potentially parse this URL against.
|
|
36
39
|
// Use the document base (if set) as the default URL.
|
|
37
40
|
const baseURL = getBaseURL(base ?? (typeof document === "object" ? document.baseURI : undefined));
|
|
38
|
-
return new
|
|
41
|
+
return new URL(target, baseURL);
|
|
39
42
|
}
|
|
40
43
|
catch {
|
|
41
44
|
//
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
/** Convert a possible URL to a URL, or throw `RequiredError` if conversion fails. */
|
|
45
|
-
export function requireURL(
|
|
46
|
-
const url = getURL(
|
|
48
|
+
export function requireURL(target, base, caller = requireURL) {
|
|
49
|
+
const url = getURL(target, base);
|
|
47
50
|
assertURL(url, caller);
|
|
48
51
|
return url;
|
|
49
52
|
}
|
|
@@ -83,12 +86,12 @@ function _isBaseURL(uri) {
|
|
|
83
86
|
export function getBaseURL(input) {
|
|
84
87
|
if (!input)
|
|
85
88
|
return;
|
|
86
|
-
const uri =
|
|
89
|
+
const uri = _getURL(input, undefined);
|
|
87
90
|
if (!uri || !_isURL(uri))
|
|
88
91
|
return;
|
|
89
92
|
if (_isBaseURL(uri))
|
|
90
93
|
return uri;
|
|
91
|
-
const base = typeof input === "string" ? uri : new
|
|
94
|
+
const base = typeof input === "string" ? uri : new URL(uri);
|
|
92
95
|
base.pathname = `${uri.pathname}/`; // Add a trailing slash.
|
|
93
96
|
return base;
|
|
94
97
|
}
|