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.
@@ -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>): void;
30
+ refreshAll<PP extends P, RR extends R>(endpoint: Endpoint<PP, RR>, maxAge?: number): void;
23
31
  [Symbol.asyncDispose](): Promise<void>;
24
32
  }
@@ -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: BaseURL;
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 BaseURL, type PossibleURL, type URL } from "../../util/url.js";
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
- /** The common base URL for all rendered endpoint requests. */
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: BaseURL;
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(): BaseURL;
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>>): Promise<void>;
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);
@@ -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>): Promise<void>;
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>>): Promise<void>;
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>): Promise<void>;
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>): Promise<void>;
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
  }
@@ -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.193.2",
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.13",
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.20260421.2",
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"
@@ -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
  }
@@ -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._invalidation || loading)
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;
@@ -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 PossibleURL, type URL, type URLString } from "../util/url.js";
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<URL, PossibleURL> {
8
- readonly base: URL | undefined;
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): URL;
11
- protected _equal(a: URL, b: URL): boolean;
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): URL;
43
+ withParam(key: string, value: unknown): ImmutableURL;
44
44
  /** Return the current URL with an additional param. */
45
- withParams(params: PossibleURIParams): URL;
45
+ withParams(params: PossibleURIParams): ImmutableURL;
46
46
  /** Return the current URL with an additional param. */
47
- omitParams(...keys: string[]): URL;
47
+ omitParams(...keys: string[]): ImmutableURL;
48
48
  /** Return the current URL with an additional param. */
49
- omitParam(key: string): URL;
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 { URL, URLString } from "./url.js";
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 URIConstructor {
45
- new (input: URIString | URI): URI;
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 URI: URIConstructor;
48
- /** Values that can be converted to a URI instance. */
49
- export type PossibleURI = string | globalThis.URL;
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 URI;
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 URI;
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>): URI | undefined;
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): URI;
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(input: PossibleURIParams, caller?: AnyCaller): URIParams;
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(input: PossibleURIParams, key: string): string | undefined;
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(input: PossibleURIParams, key: string, caller?: AnyCaller): string;
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(url: URL | URLString, key: string, value: unknown, caller?: AnyCaller): URL;
82
- export declare function withURIParam(url: PossibleURI, key: string, value: unknown, caller?: AnyCaller): URI;
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(url: URL | URLString, params: Nullish<PossibleURIParams>, caller?: AnyCaller): URL;
93
- export declare function withURIParams(url: PossibleURI, params: Nullish<PossibleURIParams>, caller?: AnyCaller): URI;
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(url: URL | URLString, ...keys: string[]): URL;
98
- export declare function omitURIParams(url: PossibleURI, ...keys: string[]): URI;
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: (url: PossibleURI, key: string) => URI;
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(url: URL | URLString, caller?: AnyCaller): URL;
103
- export declare function clearURIParams(url: PossibleURI, caller?: AnyCaller): URI;
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 URI = globalThis.URL;
6
+ export const ImmutableURI = URL;
7
7
  /** Is an unknown value a URI object? */
8
8
  export function isURI(value) {
9
- return value instanceof URI;
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 globalThis.URL(possible, _BASE);
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(input, caller = getURIParams) {
54
- if (input instanceof URLSearchParams) {
55
- yield* input;
53
+ function* getURIEntries(params, caller = getURIParams) {
54
+ if (params instanceof URLSearchParams) {
55
+ yield* params;
56
56
  }
57
- else if (isString(input) || input instanceof globalThis.URL) {
58
- yield* requireURI(input, caller).searchParams;
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(input)) {
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(input, caller = getURIParams) {
79
+ export function getURIParams(params, caller = getURIParams) {
80
80
  const output = {};
81
- for (const [key, str] of getURIEntries(input, caller))
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(input, key) {
87
- if (input instanceof URLSearchParams)
88
- return input.get(key) || undefined;
89
- if (isDictionary(input))
90
- return getString(input[key]);
91
- return getURIParams(input)[key];
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(input, key, caller = requireURIParam) {
95
- const value = getURIParam(input, key);
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: input, caller });
97
+ throw new RequiredError(`URI param "${key}" is required`, { received: params, caller });
98
98
  return value;
99
99
  }
100
- export function withURIParam(url, key, value, caller = withURIParam) {
101
- const input = requireURI(url, caller);
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 URI(input);
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(url, params, caller = withURIParams) {
112
- const input = requireURI(url, caller);
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 URI(input);
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(url, ...keys) {
121
- const input = requireURI(url, omitURIParams);
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 URI(input);
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(url, caller = clearURIParams) {
132
- const input = requireURI(url, caller);
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 URI(input);
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 { URI } from "./uri.js";
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 URL extends URI {
39
+ export interface ImmutableURL extends ImmutableURI {
37
40
  readonly href: URLString;
38
41
  readonly origin: URLString;
39
- readonly pathname: `/` | `/${string}`;
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 URL: URLConstructor;
44
+ export declare const ImmutableURL: ImmutableURLConstructor;
53
45
  /** Values that can be converted to a URL instance. */
54
- export type PossibleURL = string | BuiltinURL;
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 URL;
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 URL;
53
+ export declare function assertURL(value: unknown, caller?: AnyCaller): asserts value is ImmutableURL;
62
54
  /**
63
- * Convert a possible URL to a URL, or return `undefined` if conversion fails.
64
- * - Slightly subverts the builtin relative URL parsing by appending a '/' trailing slash to `base`
65
- * - This means if the current URL has a path segment like `/a/b/c` then a relative path like `d/e/f` will be parsed relative to `c` (default behaviour is to strip segments without a trailing slash, which is usually unexpected).
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(possible: Nullish<PossibleURL>, base?: PossibleURL): URL | undefined;
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(possible: PossibleURL, base?: PossibleURL, caller?: AnyCaller): URL;
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 URL {
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 BuiltinURL = globalThis.URL;
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 BuiltinURL && _isURL(value);
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
- * Convert a possible URL to a URL, or return `undefined` if conversion fails.
21
- * - Slightly subverts the builtin relative URL parsing by appending a '/' trailing slash to `base`
22
- * - This means if the current URL has a path segment like `/a/b/c` then a relative path like `d/e/f` will be parsed relative to `c` (default behaviour is to strip segments without a trailing slash, which is usually unexpected).
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(possible, base) {
25
- if (!possible)
27
+ export function getURL(target, base) {
28
+ if (!target)
26
29
  return;
27
- const uri = _getBuiltinURL(possible, base);
30
+ const uri = _getURL(target, base);
28
31
  if (uri && _isURL(uri))
29
32
  return uri;
30
33
  }
31
- function _getBuiltinURL(possible, base) {
32
- if (possible instanceof BuiltinURL)
33
- return possible;
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 BuiltinURL(possible, baseURL);
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(possible, base, caller = requireURL) {
46
- const url = getURL(possible, base);
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 = _getBuiltinURL(input, undefined);
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 BuiltinURL(uri);
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
  }