shelving 1.167.1 → 1.168.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { type Schema } from "../schema/Schema.js";
2
2
  import type { AnyCaller } from "../util/function.js";
3
- import type { AbsoluteLink } from "../util/link.js";
3
+ import type { URLString } from "../util/url.js";
4
4
  import type { EndpointCallback, EndpointHandler } from "./util.js";
5
5
  /** HTTP request methods. */
6
6
  export type EndpointMethod = EndpointBodyMethod | EndpointHeadMethod;
@@ -22,12 +22,12 @@ export declare class Endpoint<P, R> {
22
22
  /** Endpoint method. */
23
23
  readonly method: EndpointMethod;
24
24
  /** Endpoint URL, possibly including placeholders e.g. `https://api.mysite.com/users/{id}` */
25
- readonly url: AbsoluteLink;
25
+ readonly url: URLString;
26
26
  /** Payload schema. */
27
27
  readonly payload: Schema<P>;
28
28
  /** Result schema. */
29
29
  readonly result: Schema<R>;
30
- constructor(method: EndpointMethod, url: AbsoluteLink, payload: Schema<P>, result: Schema<R>);
30
+ constructor(method: EndpointMethod, url: URLString, payload: Schema<P>, result: Schema<R>);
31
31
  /**
32
32
  * Return an `EndpointHandler` for this endpoint.
33
33
  *
@@ -82,34 +82,34 @@ export type EndpointType<X extends Endpoint<unknown, unknown>> = X extends Endpo
82
82
  * Represent a GET request to a specified URL, with validated payload and return types.
83
83
  * "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data and should not contain a request content."
84
84
  */
85
- export declare function GET<P, R>(url: AbsoluteLink, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
86
- export declare function GET<P>(url: AbsoluteLink, payload: Schema<P>): Endpoint<P, undefined>;
87
- export declare function GET<R>(url: AbsoluteLink, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
85
+ export declare function GET<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
86
+ export declare function GET<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
87
+ export declare function GET<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
88
88
  /**
89
89
  * Represent a POST request to a specified URL, with validated payload and return types.
90
90
  * "The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.
91
91
  */
92
- export declare function POST<P, R>(url: AbsoluteLink, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
93
- export declare function POST<P>(url: AbsoluteLink, payload: Schema<P>): Endpoint<P, undefined>;
94
- export declare function POST<R>(url: AbsoluteLink, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
92
+ export declare function POST<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
93
+ export declare function POST<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
94
+ export declare function POST<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
95
95
  /**
96
96
  * Represent a PUT request to a specified URL, with validated payload and return types.
97
97
  * "The PUT method replaces all current representations of the target resource with the request content."
98
98
  */
99
- export declare function PUT<P, R>(url: AbsoluteLink, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
100
- export declare function PUT<P>(url: AbsoluteLink, payload: Schema<P>): Endpoint<P, undefined>;
101
- export declare function PUT<R>(url: AbsoluteLink, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
99
+ export declare function PUT<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
100
+ export declare function PUT<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
101
+ export declare function PUT<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
102
102
  /**
103
103
  * Represent a PATCH request to a specified URL, with validated payload and return types.
104
104
  * "The PATCH method applies partial modifications to a resource."
105
105
  */
106
- export declare function PATCH<P, R>(url: AbsoluteLink, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
107
- export declare function PATCH<P>(url: AbsoluteLink, payload: Schema<P>): Endpoint<P, undefined>;
108
- export declare function PATCH<R>(url: AbsoluteLink, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
106
+ export declare function PATCH<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
107
+ export declare function PATCH<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
108
+ export declare function PATCH<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
109
109
  /**
110
110
  * Represent a DELETE request to a specified URL, with validated payload and return types.
111
111
  * "The DELETE method deletes the specified resource."
112
112
  */
113
- export declare function DELETE<P, R>(url: AbsoluteLink, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
114
- export declare function DELETE<P>(url: AbsoluteLink, payload: Schema<P>): Endpoint<P, undefined>;
115
- export declare function DELETE<R>(url: AbsoluteLink, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
113
+ export declare function DELETE<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
114
+ export declare function DELETE<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
115
+ export declare function DELETE<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
@@ -4,7 +4,7 @@ import { assertDictionary } from "../util/dictionary.js";
4
4
  import { getMessage } from "../util/error.js";
5
5
  import { getResponse, getResponseContent } from "../util/http.js";
6
6
  import { getPlaceholders, renderTemplate } from "../util/template.js";
7
- import { omitURLParams, withURLParams } from "../util/url.js";
7
+ import { omitURIParams, withURIParams } from "../util/uri.js";
8
8
  import { getValid } from "../util/validate.js";
9
9
  /**
10
10
  * An abstract API resource definition, used to specify types for e.g. serverless functions.
@@ -90,7 +90,7 @@ export class Endpoint {
90
90
  const content = await getResponseContent(response, caller);
91
91
  // Throw `ResponseError` if the API returns status outside the 200-299 range.
92
92
  if (!ok)
93
- throw new ResponseError(getMessage(content) ?? `Error ${status}`);
93
+ throw new ResponseError(getMessage(content) ?? `Error ${status}`, { cause: Response, caller });
94
94
  // Validate the success response.
95
95
  return getValid(content, this.result, ResponseError, caller);
96
96
  }
@@ -154,11 +154,11 @@ function createHeadRequest(method, url, params, options = {}, caller = createHea
154
154
  const placeholders = getPlaceholders(url);
155
155
  // URL has `{placeholders}` to render, so rendere those to the URL and add all other params as `?query` params.
156
156
  if (placeholders.length) {
157
- const rendered = omitURLParams(withURLParams(renderTemplate(url, params, caller), params, caller), ...placeholders);
157
+ const rendered = omitURIParams(withURIParams(renderTemplate(url, params, caller), params, caller), ...placeholders);
158
158
  return new Request(rendered, { ...options, method });
159
159
  }
160
160
  // URL has no `{placeholders}`, so add all payload params to the URL.
161
- return new Request(withURLParams(url, params, caller), { ...options, method });
161
+ return new Request(withURIParams(url, params, caller), { ...options, method });
162
162
  }
163
163
  /**
164
164
  * Create a body request to a URL.
@@ -15,10 +15,9 @@ import { getURL } from "../util/url.js";
15
15
  */
16
16
  export function handleEndpoints(request, endpoints, caller = handleEndpoints) {
17
17
  // Parse the URL of the request.
18
- const requestUrl = request.url;
19
- const url = getURL(requestUrl);
18
+ const url = getURL(request.url);
20
19
  if (!url)
21
- throw new RequestError("Invalid request URL", { received: requestUrl, caller });
20
+ throw new RequestError("Invalid request URL", { received: request.url, caller });
22
21
  const { origin, pathname, searchParams } = url;
23
22
  // Iterate over the handlers and return the first one that matches the request.
24
23
  for (const { endpoint, callback } of endpoints) {
@@ -33,15 +32,15 @@ export function handleEndpoints(request, endpoints, caller = handleEndpoints) {
33
32
  // Make a simple dictionary object from the `{placeholder}` path params and the `?a=123` query params from the URL.
34
33
  const combinedParams = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
35
34
  // Get the response by calling the callback.
36
- return handleEndpoint(endpoint, callback, combinedParams, request);
35
+ return handleEndpoint(endpoint, callback, combinedParams, request, caller);
37
36
  }
38
37
  // No handler matched the request.
39
- throw new NotFoundError("No matching endpoint", { received: requestUrl, caller });
38
+ throw new NotFoundError("No matching endpoint", { received: request.url, caller });
40
39
  }
41
40
  /** Handle an individual call to an endpoint callback. */
42
- async function handleEndpoint(endpoint, callback, params, request) {
41
+ async function handleEndpoint(endpoint, callback, params, request, caller = handleEndpoint) {
43
42
  // Extract a data object from the request body and validate it against the endpoint's payload type.
44
- const content = await getRequestContent(request, handleEndpoints);
43
+ const content = await getRequestContent(request, caller);
45
44
  // If content is undefined, it means the request has no body, so params are the only payload.
46
45
  // - If the content is a plain object, merge if with the params.
47
46
  // - If the content is anything else (e.g. string, number, array), return it directly (but you'll have no way to access the other params).
package/index.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  * - Modules that use peer dependencies, like `shelving/react` and `shelving/firebase/client`
4
4
  * - Modules that are for internal use, like `shelving/test`
5
5
  */
6
- export * from "./api/index.js";
7
6
  export * from "./db/index.js";
7
+ export * from "./endpoint/index.js";
8
8
  export * from "./error/index.js";
9
9
  export * from "./feedback/index.js";
10
10
  export * from "./markup/index.js";
package/index.js CHANGED
@@ -3,8 +3,8 @@
3
3
  * - Modules that use peer dependencies, like `shelving/react` and `shelving/firebase/client`
4
4
  * - Modules that are for internal use, like `shelving/test`
5
5
  */
6
- export * from "./api/index.js";
7
6
  export * from "./db/index.js";
7
+ export * from "./endpoint/index.js";
8
8
  export * from "./error/index.js";
9
9
  export * from "./feedback/index.js";
10
10
  // export * from "./firestore/client/index.js"; // Not exported.
@@ -1,16 +1,16 @@
1
- import { formatURL } from "../../util/format.js";
2
- import { getLink } from "../../util/link.js";
1
+ import { formatURI } from "../../util/format.js";
3
2
  import { getRegExp } from "../../util/regexp.js";
3
+ import { getURI, HTTP_SCHEMES } from "../../util/uri.js";
4
4
  import { getURL } from "../../util/url.js";
5
5
  import { renderMarkup } from "../render.js";
6
6
  import { REACT_ELEMENT_TYPE } from "../util/internal.js";
7
7
  import { getMarkupRule } from "../util/rule.js";
8
8
  /** Render `<a href="">` if the link is a valid one, or `<a>` (with no `href`) if it isn't. */
9
9
  function renderLinkMarkupRule({ groups: { title, href: unsafeHref } }, options, key) {
10
- const { base, schemes, hosts, rel } = options;
11
- const url = getURL(unsafeHref, base);
12
- const href = getLink(url, base, schemes, hosts);
13
- const children = title ? renderMarkup(title, options, "link") : url ? formatURL(url) : "";
10
+ const { base, schemes = HTTP_SCHEMES, rel } = options;
11
+ const uri = getURL(unsafeHref, base) ?? getURI(unsafeHref);
12
+ const href = uri && schemes.includes(uri.protocol) ? uri.href : undefined;
13
+ const children = title ? renderMarkup(title, options, "link") : uri ? formatURI(uri) : "";
14
14
  return {
15
15
  key,
16
16
  $$typeof: REACT_ELEMENT_TYPE,
@@ -1,4 +1,5 @@
1
- import type { AbsoluteLink, LinkHosts, LinkSchemes } from "../../util/link.js";
1
+ import type { URISchemes } from "../../util/uri.js";
2
+ import type { URLString } from "../../util/url.js";
2
3
  import type { MarkupRules } from "./rule.js";
3
4
  /** The current parsing options (represents the current state of the parsing). */
4
5
  export type MarkupOptions = {
@@ -10,19 +11,14 @@ export type MarkupOptions = {
10
11
  */
11
12
  readonly rel?: string | undefined;
12
13
  /**
13
- * Set the base URL that any relative links will be relative to
14
+ * Set the base URL that any relative URLs will be relative to
14
15
  * @default `window.location.href` in browser environments, and `undefined` in server environments.
15
16
  */
16
- readonly base?: AbsoluteLink | undefined;
17
+ readonly base?: URLString | undefined;
17
18
  /**
18
- * Valid URL schemes/protocols for links.
19
+ * Valid URI schemes/protocols for URLs and URIs.
19
20
  * @example ["http:", "https:"]
20
21
  * @default ["http:", "https:"]
21
22
  */
22
- readonly schemes?: LinkSchemes | undefined;
23
- /**
24
- * Valid URL hosts for links.
25
- * @example ["google.com", "www.google.com"]
26
- */
27
- readonly hosts?: LinkHosts | undefined;
23
+ readonly schemes?: URISchemes | undefined;
28
24
  };
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.167.1",
14
+ "version": "1.168.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
@@ -57,11 +57,11 @@
57
57
  "build:test:unit": "bun test ./dist/**/*.test.js --bail"
58
58
  },
59
59
  "devDependencies": {
60
- "@biomejs/biome": "^2.2.6",
60
+ "@biomejs/biome": "^2.3.8",
61
61
  "@google-cloud/firestore": "^7.11.6",
62
- "@types/bun": "^1.3.0",
63
- "@types/react": "^19.2.2",
64
- "@types/react-dom": "^19.2.2",
62
+ "@types/bun": "^1.3.3",
63
+ "@types/react": "^19.2.7",
64
+ "@types/react-dom": "^19.2.3",
65
65
  "firebase": "^11.10.0",
66
66
  "react": "^19.2.0",
67
67
  "react-dom": "^19.2.0",
@@ -0,0 +1,21 @@
1
+ import { type URISchemes, type URIString } from "../util/uri.js";
2
+ import type { StringSchemaOptions } from "./StringSchema.js";
3
+ import { StringSchema } from "./StringSchema.js";
4
+ /** Allowed options for `URISchema` */
5
+ export interface URISchemaOptions extends Omit<StringSchemaOptions, "input" | "min" | "max" | "multiline"> {
6
+ readonly schemes?: URISchemes | undefined;
7
+ }
8
+ /**
9
+ * Type of `StringSchema` that defines a valid URI string.
10
+ * - Checks URI scheme against a whitelist (always).
11
+ * - URIs are limited to 512 characters, but generally these won't be data: URIs so this is a reasonable limit.
12
+ */
13
+ export declare class URISchema extends StringSchema {
14
+ readonly schemes: URISchemes;
15
+ constructor({ one, title, schemes, ...options }: URISchemaOptions);
16
+ validate(unsafeValue: unknown): URIString;
17
+ }
18
+ /** Valid URI string, e.g. `https://www.google.com` */
19
+ export declare const URI_SCHEMA: URISchema;
20
+ /** Valid URI string, e.g. `https://www.google.com`, or `null` */
21
+ export declare const NULLABLE_URI_SCHEMA: import("./NullableSchema.js").NullableSchema<`${string}:${string}`>;
@@ -0,0 +1,38 @@
1
+ import { ValueFeedback } from "../feedback/Feedback.js";
2
+ import { getURI, HTTP_SCHEMES } from "../util/uri.js";
3
+ import { NULLABLE } from "./NullableSchema.js";
4
+ import { StringSchema } from "./StringSchema.js";
5
+ /**
6
+ * Type of `StringSchema` that defines a valid URI string.
7
+ * - Checks URI scheme against a whitelist (always).
8
+ * - URIs are limited to 512 characters, but generally these won't be data: URIs so this is a reasonable limit.
9
+ */
10
+ export class URISchema extends StringSchema {
11
+ schemes;
12
+ constructor({ one = "URI", title = "URI", schemes = HTTP_SCHEMES, ...options }) {
13
+ super({
14
+ one,
15
+ title,
16
+ ...options,
17
+ input: "url",
18
+ min: 1,
19
+ max: 512,
20
+ multiline: false,
21
+ });
22
+ this.schemes = schemes;
23
+ }
24
+ // Override to validate the URI and check the schemes and hosts against the whitelists.
25
+ validate(unsafeValue) {
26
+ const str = super.validate(unsafeValue);
27
+ const uri = getURI(str);
28
+ if (!uri)
29
+ throw new ValueFeedback(str ? "Invalid format" : "Required", str);
30
+ if (this.schemes && !this.schemes.includes(uri.protocol))
31
+ throw new ValueFeedback("Invalid URI scheme", str);
32
+ return uri.href;
33
+ }
34
+ }
35
+ /** Valid URI string, e.g. `https://www.google.com` */
36
+ export const URI_SCHEMA = new URISchema({});
37
+ /** Valid URI string, e.g. `https://www.google.com`, or `null` */
38
+ export const NULLABLE_URI_SCHEMA = NULLABLE(URI_SCHEMA);
@@ -0,0 +1,24 @@
1
+ import { type URISchemes } from "../util/uri.js";
2
+ import { type URLString } from "../util/url.js";
3
+ import type { StringSchemaOptions } from "./StringSchema.js";
4
+ import { StringSchema } from "./StringSchema.js";
5
+ /** Allowed options for `URLSchema` */
6
+ export interface URLSchemaOptions extends Omit<StringSchemaOptions, "input" | "min" | "max" | "multiline"> {
7
+ readonly base?: URL | URLString | undefined;
8
+ readonly schemes?: URISchemes | undefined;
9
+ }
10
+ /**
11
+ * Type of `StringSchema` that defines a valid URL string.
12
+ * - Checks URL scheme against a whitelist (always), and checks URL domain against a whitelist (optional).
13
+ * - URLs are limited to 512 characters, but generally these won't be data: URIs so this is a reasonable limit.
14
+ */
15
+ export declare class URLSchema extends StringSchema {
16
+ readonly base: URLString | undefined;
17
+ readonly schemes: URISchemes;
18
+ constructor({ one, title, base, schemes, ...options }: URLSchemaOptions);
19
+ validate(unsafeValue: unknown): URLString;
20
+ }
21
+ /** Valid URL string, e.g. `https://www.google.com` */
22
+ export declare const URL_SCHEMA: URLSchema;
23
+ /** Valid URL string, e.g. `https://www.google.com`, or `null` */
24
+ export declare const NULLABLE_URL_SCHEMA: import("./NullableSchema.js").NullableSchema<`${string}://${string}`>;
@@ -1,18 +1,17 @@
1
1
  import { ValueFeedback } from "../feedback/Feedback.js";
2
- import { getLinkURL } from "../util/link.js";
2
+ import { HTTP_SCHEMES } from "../util/uri.js";
3
+ import { getURL } from "../util/url.js";
3
4
  import { NULLABLE } from "./NullableSchema.js";
4
5
  import { StringSchema } from "./StringSchema.js";
5
6
  /**
6
- * Type of `StringSchema` that defines a valid URL link.
7
+ * Type of `StringSchema` that defines a valid URL string.
7
8
  * - Checks URL scheme against a whitelist (always), and checks URL domain against a whitelist (optional).
8
9
  * - URLs are limited to 512 characters, but generally these won't be data: URIs so this is a reasonable limit.
9
- * - Falsy values are converted to `""` empty string.
10
10
  */
11
- export class LinkSchema extends StringSchema {
11
+ export class URLSchema extends StringSchema {
12
12
  base;
13
13
  schemes;
14
- hosts;
15
- constructor({ one = "link", title = "Link", base, schemes, hosts, ...options }) {
14
+ constructor({ one = "URL", title = "URL", base, schemes = HTTP_SCHEMES, ...options }) {
16
15
  super({
17
16
  one,
18
17
  title,
@@ -22,20 +21,21 @@ export class LinkSchema extends StringSchema {
22
21
  max: 512,
23
22
  multiline: false,
24
23
  });
25
- this.base = base;
24
+ this.base = getURL(base)?.href;
26
25
  this.schemes = schemes;
27
- this.hosts = hosts;
28
26
  }
29
- // Override to clean the URL using builtin helper functions and check the schemes and hosts against the whitelists.
27
+ // Override to validate the URL and check the schemes and hosts against the whitelists.
30
28
  validate(unsafeValue) {
31
29
  const str = super.validate(unsafeValue);
32
- const url = getLinkURL(str, this.base, this.schemes, this.hosts);
30
+ const url = getURL(str, this.base);
33
31
  if (!url)
34
32
  throw new ValueFeedback(str ? "Invalid format" : "Required", str);
33
+ if (this.schemes && !this.schemes.includes(url.protocol))
34
+ throw new ValueFeedback("Invalid URL scheme", str);
35
35
  return url.href;
36
36
  }
37
37
  }
38
- /** Valid link, e.g. `https://www.google.com` */
39
- export const LINK = new LinkSchema({});
40
- /** Valid link, e.g. `https://www.google.com`, or `null` */
41
- export const NULLABLE_LINK = NULLABLE(LINK);
38
+ /** Valid URL string, e.g. `https://www.google.com` */
39
+ export const URL_SCHEMA = new URLSchema({});
40
+ /** Valid URL string, e.g. `https://www.google.com`, or `null` */
41
+ export const NULLABLE_URL_SCHEMA = NULLABLE(URL_SCHEMA);
package/schema/index.d.ts CHANGED
@@ -10,7 +10,6 @@ export * from "./EmailSchema.js";
10
10
  export * from "./EntitySchema.js";
11
11
  export * from "./FileSchema.js";
12
12
  export * from "./KeySchema.js";
13
- export * from "./LinkSchema.js";
14
13
  export * from "./NullableSchema.js";
15
14
  export * from "./NumberSchema.js";
16
15
  export * from "./OptionalSchema.js";
@@ -21,4 +20,6 @@ export * from "./SlugSchema.js";
21
20
  export * from "./StringSchema.js";
22
21
  export * from "./ThroughSchema.js";
23
22
  export * from "./TimeSchema.js";
23
+ export * from "./URISchema.js";
24
+ export * from "./URLSchema.js";
24
25
  export * from "./UUIDSchema.js";
package/schema/index.js CHANGED
@@ -1,4 +1,3 @@
1
- // Field schemas.
2
1
  export * from "./ArraySchema.js";
3
2
  export * from "./BooleanSchema.js";
4
3
  export * from "./ChoiceSchema.js";
@@ -11,7 +10,6 @@ export * from "./EmailSchema.js";
11
10
  export * from "./EntitySchema.js";
12
11
  export * from "./FileSchema.js";
13
12
  export * from "./KeySchema.js";
14
- export * from "./LinkSchema.js";
15
13
  export * from "./NullableSchema.js";
16
14
  export * from "./NumberSchema.js";
17
15
  export * from "./OptionalSchema.js";
@@ -20,7 +18,8 @@ export * from "./RequiredSchema.js";
20
18
  export * from "./Schema.js";
21
19
  export * from "./SlugSchema.js";
22
20
  export * from "./StringSchema.js";
23
- // Utility schemas.
24
21
  export * from "./ThroughSchema.js";
25
22
  export * from "./TimeSchema.js";
23
+ export * from "./URISchema.js";
24
+ export * from "./URLSchema.js";
26
25
  export * from "./UUIDSchema.js";
@@ -0,0 +1,24 @@
1
+ import type { Endpoint } from "../endpoint/Endpoint.js";
2
+ import { NONE } from "../util/constants.js";
3
+ import { Store } from "./Store.js";
4
+ /**
5
+ * Store object that loads a result from an API endpoint and manages its state.
6
+ *
7
+ * @todo Needs support for `EndpointOptions` to set headers etc.
8
+ */
9
+ export declare class EndpointStore<P, R> extends Store<R> {
10
+ static CANCELLED: symbol;
11
+ readonly endpoint: Endpoint<P, R>;
12
+ private _payload;
13
+ private _abort;
14
+ get payload(): P;
15
+ set payload(next: P);
16
+ /** Maximum age data can be before a fetch is triggered (defaults to 5 minutes). */
17
+ maxAge: number;
18
+ get value(): R;
19
+ set value(value: R | typeof NONE);
20
+ constructor(endpoint: Endpoint<P, R>, payload: P);
21
+ refetch(): Promise<void>;
22
+ /** Call the API now to fetch the data. */
23
+ private _call;
24
+ }
@@ -0,0 +1,74 @@
1
+ import { MINUTE, NONE } from "../util/constants.js";
2
+ import { isDeepEqual } from "../util/equal.js";
3
+ import { Store } from "./Store.js";
4
+ /**
5
+ * Store object that loads a result from an API endpoint and manages its state.
6
+ *
7
+ * @todo Needs support for `EndpointOptions` to set headers etc.
8
+ */
9
+ export class EndpointStore extends Store {
10
+ static CANCELLED = Symbol("CANCELLED");
11
+ endpoint;
12
+ _payload;
13
+ _abort = undefined;
14
+ get payload() {
15
+ return this._payload;
16
+ }
17
+ set payload(next) {
18
+ const current = this._payload;
19
+ // Did the payload actually change?
20
+ if (!isDeepEqual(current, next)) {
21
+ this._payload = next;
22
+ // If there's already a fetch in progress, cancel it.
23
+ if (this._abort) {
24
+ this._abort.abort(EndpointStore.CANCELLED);
25
+ this._abort = undefined;
26
+ }
27
+ // Trigger a fetch.
28
+ this._call();
29
+ }
30
+ }
31
+ /** Maximum age data can be before a fetch is triggered (defaults to 5 minutes). */
32
+ maxAge = 5 * MINUTE;
33
+ // Override to possibly trigger a fetch when value is read.
34
+ get value() {
35
+ // Queue `this.call()` if...
36
+ // 1. Value is still loading.
37
+ // 2. Value is stale (older than maxAge).
38
+ if (this.loading || this.age > this.maxAge)
39
+ if (!this.reason)
40
+ this._call();
41
+ return super.value;
42
+ }
43
+ set value(value) {
44
+ super.value = value;
45
+ }
46
+ constructor(endpoint, payload) {
47
+ super(NONE);
48
+ this.endpoint = endpoint;
49
+ this.payload = payload;
50
+ }
51
+ refetch() {
52
+ return this._call();
53
+ }
54
+ /** Call the API now to fetch the data. */
55
+ async _call() {
56
+ if (this._abort)
57
+ return; // Already fetching.
58
+ this.reason = undefined; // Optimistically clear any error.
59
+ try {
60
+ this._abort = new AbortController();
61
+ const value = await this.endpoint.fetch(this._payload, { signal: this._abort.signal });
62
+ this.value = value;
63
+ }
64
+ catch (thrown) {
65
+ console.error(thrown);
66
+ if (thrown === EndpointStore.CANCELLED)
67
+ return; // Cancelled on purpose.
68
+ this.reason = thrown;
69
+ }
70
+ finally {
71
+ this._abort = undefined;
72
+ }
73
+ }
74
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Data } from "../util/data.js";
2
- import { type PossibleURL, type PossibleURLParams, type URLParams } from "../util/url.js";
2
+ import { type PossibleURIParams, type URIParams } from "../util/uri.js";
3
+ import { type PossibleURL, type URL } from "../util/url.js";
3
4
  import { Store } from "./Store.js";
4
5
  /** Store a URL, e.g. `https://top.com/a/b/c` */
5
6
  export declare class URLStore extends Store<URL> {
@@ -20,7 +21,7 @@ export declare class URLStore extends Store<URL> {
20
21
  /** Get the URL params as a string. */
21
22
  get search(): string;
22
23
  /** Get the URL params as a dictionary. */
23
- get params(): URLParams;
24
+ get params(): URIParams;
24
25
  /** Return a single param in this URL, or `undefined` if it could not be found. */
25
26
  getParam(key: string): string | undefined;
26
27
  /** Require a single param in this URL, or throw `RequiredError` if it could not be found. */
@@ -36,7 +37,7 @@ export declare class URLStore extends Store<URL> {
36
37
  /** Return the current URL with an additional param. */
37
38
  withParam(key: string, value: unknown): URL;
38
39
  /** Return the current URL with an additional param. */
39
- withParams(params: PossibleURLParams): URL;
40
+ withParams(params: PossibleURIParams): URL;
40
41
  /** Return the current URL with an additional param. */
41
42
  omitParams(...keys: string[]): URL;
42
43
  /** Return the current URL with an additional param. */
package/store/URLStore.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getGetter, getSetter } from "../util/class.js";
2
- import { getURL, getURLParam, getURLParams, omitURLParams, requireURL, requireURLParam, withURLParam, withURLParams, } from "../util/url.js";
2
+ import { getURIParam, getURIParams, omitURIParams, requireURIParam, withURIParam, withURIParams, } from "../util/uri.js";
3
+ import { getURL, requireURL } from "../util/url.js";
3
4
  import { Store } from "./Store.js";
4
5
  /** Store a URL, e.g. `https://top.com/a/b/c` */
5
6
  export class URLStore extends Store {
@@ -51,47 +52,47 @@ export class URLStore extends Store {
51
52
  }
52
53
  /** Get the URL params as a dictionary. */
53
54
  get params() {
54
- return getURLParams(this.value.searchParams, getGetter(this, "params"));
55
+ return getURIParams(this.value.searchParams, getGetter(this, "params"));
55
56
  }
56
57
  /** Return a single param in this URL, or `undefined` if it could not be found. */
57
58
  getParam(key) {
58
- return getURLParam(this.value.searchParams, key);
59
+ return getURIParam(this.value.searchParams, key);
59
60
  }
60
61
  /** Require a single param in this URL, or throw `RequiredError` if it could not be found. */
61
62
  requireParam(key) {
62
- return requireURLParam(this.value.searchParams, key, getSetter(this, "requireParam"));
63
+ return requireURIParam(this.value.searchParams, key, getSetter(this, "requireParam"));
63
64
  }
64
65
  /** Update a single param in this URL. */
65
66
  setParam(key, value) {
66
- this.value = withURLParam(this.value, key, value, this.setParams);
67
+ this.value = withURIParam(this.value, key, value, this.setParams);
67
68
  }
68
69
  /** Update several params in this URL. */
69
70
  setParams(params) {
70
- this.value = withURLParams(this.value, params, this.setParams);
71
+ this.value = withURIParams(this.value, params, this.setParams);
71
72
  }
72
73
  /** Delete one or more params in this URL. */
73
74
  deleteParam(key, ...keys) {
74
- this.value = omitURLParams(this.value, key, ...keys);
75
+ this.value = omitURIParams(this.value, key, ...keys);
75
76
  }
76
77
  /** Delete one or more params in this URL. */
77
78
  deleteParams(key, ...keys) {
78
- this.value = omitURLParams(this.value, key, ...keys);
79
+ this.value = omitURIParams(this.value, key, ...keys);
79
80
  }
80
81
  /** Return the current URL with an additional param. */
81
82
  withParam(key, value) {
82
- return withURLParam(this.value, key, value, this.withParam);
83
+ return withURIParam(this.value, key, value, this.withParam);
83
84
  }
84
85
  /** Return the current URL with an additional param. */
85
86
  withParams(params) {
86
- return withURLParams(this.value, params, this.withParams);
87
+ return withURIParams(this.value, params, this.withParams);
87
88
  }
88
89
  /** Return the current URL with an additional param. */
89
90
  omitParams(...keys) {
90
- return omitURLParams(this.value, ...keys);
91
+ return omitURIParams(this.value, ...keys);
91
92
  }
92
93
  /** Return the current URL with an additional param. */
93
94
  omitParam(key) {
94
- return omitURLParams(this.value, key);
95
+ return omitURIParams(this.value, key);
95
96
  }
96
97
  toString() {
97
98
  return this.href;
package/store/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from "./ArrayStore.js";
2
2
  export * from "./BooleanStore.js";
3
3
  export * from "./DataStore.js";
4
4
  export * from "./DictionaryStore.js";
5
+ export * from "./EndpointStore.js";
5
6
  export * from "./PathStore.js";
6
7
  export * from "./Store.js";
7
8
  export * from "./URLStore.js";
package/store/index.js CHANGED
@@ -2,6 +2,7 @@ export * from "./ArrayStore.js";
2
2
  export * from "./BooleanStore.js";
3
3
  export * from "./DataStore.js";
4
4
  export * from "./DictionaryStore.js";
5
+ export * from "./EndpointStore.js";
5
6
  export * from "./PathStore.js";
6
7
  export * from "./Store.js";
7
8
  export * from "./URLStore.js";