shelving 1.141.0 → 1.143.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/util.d.ts CHANGED
@@ -6,8 +6,12 @@ import type { Endpoint } from "./Endpoint.js";
6
6
  * - Payload is validated by the payload validator for the `Endpoint`.
7
7
  * - If the body of the `Request` is a data object (i.e. a plain object), then body data is merged with the path and query parameters to form a single flat object.
8
8
  * - If payload is _not_ a data object (i.e. it's another JSON type like `string` or `number`) then the payload include the path and query parameters, and a key called `content` that contains the body of the request.
9
+ *
10
+ * @param request The raw `Request` object in case it needs any additional processing.
11
+ *
12
+ * @returns The correct `Result` type for the `Endpoint`, or a raw `Response` object if you wish to return a custom response.
9
13
  */
10
- export type EndpointCallback<P, R> = (payload: P, request: Request) => R | Promise<R>;
14
+ export type EndpointCallback<P, R> = (payload: P, request: Request) => R | Response | Promise<R | Response>;
11
15
  /**
12
16
  * Object combining an abstract `Endpoint` and an `EndpointCallback` implementation.
13
17
  */
@@ -24,7 +28,11 @@ export type AnyEndpointHandler = EndpointHandler<any, any>;
24
28
  */
25
29
  export type EndpointHandlers = ReadonlyArray<AnyEndpointHandler>;
26
30
  /**
27
- * Handler a `Request` with the first matching `OptionalHandler` in a `Handlers` array.
31
+ * Handler a `Request` with the first matching `EndpointHandlers`.
32
+ *
33
+ * 1. Define your `Endpoint` objects with a method, path, payload and result validators, e.g. `GET("/test/{id}", PAYLOAD, STRING)`
34
+ * 2. Make an array of `EndpointHandler` objects combining an `Endpoint` with a `callback` function
35
+ * -
28
36
  *
29
37
  * @returns The resulting `Response` from the first handler that matches the `Request`.
30
38
  * @throws `NotFoundError` if no handler matches the `Request`.
package/api/util.js CHANGED
@@ -7,7 +7,11 @@ import { matchTemplate } from "../util/template.js";
7
7
  import { getURL } from "../util/url.js";
8
8
  import { getValid } from "../util/validate.js";
9
9
  /**
10
- * Handler a `Request` with the first matching `OptionalHandler` in a `Handlers` array.
10
+ * Handler a `Request` with the first matching `EndpointHandlers`.
11
+ *
12
+ * 1. Define your `Endpoint` objects with a method, path, payload and result validators, e.g. `GET("/test/{id}", PAYLOAD, STRING)`
13
+ * 2. Make an array of `EndpointHandler` objects combining an `Endpoint` with a `callback` function
14
+ * -
11
15
  *
12
16
  * @returns The resulting `Response` from the first handler that matches the `Request`.
13
17
  * @throws `NotFoundError` if no handler matches the `Request`.
@@ -28,14 +32,15 @@ export function handleEndpoints(request, endpoints) {
28
32
  const pathParams = matchTemplate(endpoint.path, pathname, handleEndpoints);
29
33
  if (!pathParams)
30
34
  continue;
31
- // Merge the search params and path params.
35
+ // Make a simple dictionary object from the `{placeholder}` path params and the `?a=123` query params from the URL.
32
36
  const params = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
33
37
  // Get the response by calling the callback.
34
- return _getResponse(endpoint, callback, params, request);
38
+ return getEndpointResponse(endpoint, callback, params, request);
35
39
  }
40
+ // No handler matched the request.
36
41
  throw new NotFoundError("Not found", { request, caller: handleEndpoints });
37
42
  }
38
- async function _getResponse(endpoint, callback, params, request) {
43
+ async function getEndpointResponse(endpoint, callback, params, request) {
39
44
  // Extract a data object from the request body and validate it against the endpoint's payload type.
40
45
  const content = await getRequestContent(request, handleEndpoints);
41
46
  // If content is undefined, it means the request has no body, so params are the only payload.
@@ -43,11 +48,14 @@ async function _getResponse(endpoint, callback, params, request) {
43
48
  // If the content is not a data object (e.g. string, number, array), set a single `content` property and merge it with the params.
44
49
  const unsafePayload = content === undefined ? params : isData(content) ? { ...content, ...params } : { content, ...params };
45
50
  const payload = endpoint.prepare(unsafePayload);
46
- // Call the handler with the validated payload to get the result.
47
- const unsafeResult = await callback(payload, request);
48
- // Validate the result against the endpoint's result type.
51
+ // Call the callback with the validated payload to get the result.
52
+ const returned = await callback(payload, request);
53
+ // If the callback returned a `Response`, return it directly.
54
+ if (returned instanceof Response)
55
+ return returned;
56
+ // Otherwise validate the result against the endpoint's result type.
49
57
  // Throw a `ValueError` if the result is not valid, which indicates an internal error in the callback implementation.
50
- const result = getValid(unsafeResult, endpoint, ValueError, handleEndpoints);
58
+ const result = getValid(returned, endpoint, ValueError, handleEndpoints);
51
59
  // Return a new `Response` with a 200 status and the validated result data.
52
60
  return Response.json(result);
53
61
  }
@@ -1,2 +1,2 @@
1
1
  /** React security symbol — see https://github.com/facebook/react/pull/4832 */
2
- export const REACT_ELEMENT_TYPE = Symbol.for("react.element");
2
+ export const REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.141.0",
14
+ "version": "1.143.0",
15
15
  "repository": "https://github.com/dhoulb/shelving",
16
16
  "author": "Dave Houlbrooke <dave@shax.com>",
17
17
  "license": "0BSD",
@@ -57,19 +57,19 @@
57
57
  "build:test:unit": "bun test ./dist/**/*.test.js --bail"
58
58
  },
59
59
  "devDependencies": {
60
- "@biomejs/biome": "^1.8.3",
61
- "@google-cloud/firestore": "^7.9.0",
62
- "@types/bun": "^1.1.6",
63
- "@types/react": "^18.3.3",
64
- "@types/react-dom": "^18.3.0",
65
- "firebase": "^10.12.5",
66
- "react": "^18.3.1",
67
- "react-dom": "^18.3.1",
68
- "typescript": "^5.8.2"
60
+ "@biomejs/biome": "^1.9.4",
61
+ "@google-cloud/firestore": "^7.11.3",
62
+ "@types/bun": "^1.2.18",
63
+ "@types/react": "^19.1.8",
64
+ "@types/react-dom": "^19.1.6",
65
+ "firebase": "^11.10.0",
66
+ "react": "^19.1.0",
67
+ "react-dom": "^19.1.0",
68
+ "typescript": "^5.8.3"
69
69
  },
70
70
  "peerDependencies": {
71
- "@google-cloud/firestore": ">=4.0.0",
72
- "firebase": ">=9.0.0",
73
- "react": ">=17.0.0"
71
+ "@google-cloud/firestore": ">=7.0.0",
72
+ "firebase": ">=11.0.0",
73
+ "react": ">=19.0.0"
74
74
  }
75
75
  }
@@ -1,21 +1,22 @@
1
- import { createContext, createElement, useContext, useRef } from "react";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
2
3
  import { UnexpectedError } from "../error/UnexpectedError.js";
4
+ import { useMap } from "./useMap.js";
3
5
  /**
4
6
  * Create a cache context that can be provided to React elements and allows them to call `useCache()`
5
7
  * - Cache is a `Map` indexed by strings that can be used to store any value.
6
8
  */
7
9
  export function createCacheContext() {
8
- const context = createContext(undefined);
10
+ const Context = createContext(undefined);
9
11
  const useCache = () => {
10
- const cache = useContext(context);
12
+ const cache = useContext(Context);
11
13
  if (!cache)
12
- throw new UnexpectedError("useCache() must be used inside <Cache>", { caller: useCache });
14
+ throw new UnexpectedError("useCache() must be used inside <CacheContext>", { caller: useCache });
13
15
  return cache;
14
16
  };
15
17
  const CacheContext = ({ children }) => {
16
- // biome-ignore lint/suspicious/noAssignInExpressions: This is the most efficient way to do this.
17
- const cache = (useRef().current ||= new Map());
18
- return createElement(context.Provider, { children, value: cache });
18
+ const cache = useMap();
19
+ return _jsx(Context, { value: cache, children: children });
19
20
  };
20
21
  return { useCache, CacheContext };
21
22
  }
package/react/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./createCacheContext.js";
2
2
  export * from "./createDataContext.js";
3
- export * from "./useInstance.js";
3
+ export * from "./useProps.js";
4
+ export * from "./useMap.js";
4
5
  export * from "./useLazy.js";
5
6
  export * from "./useReduce.js";
6
7
  export * from "./useSequence.js";
package/react/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./createCacheContext.js";
2
2
  export * from "./createDataContext.js";
3
- export * from "./useInstance.js";
3
+ export * from "./useProps.js";
4
+ export * from "./useMap.js";
4
5
  export * from "./useLazy.js";
5
6
  export * from "./useReduce.js";
6
7
  export * from "./useSequence.js";
@@ -1,17 +1,16 @@
1
1
  import { isArrayEqual } from "../util/equal.js";
2
- import { useInternals } from "./useInternals.js";
2
+ import { useProps } from "./useProps.js";
3
3
  /**
4
4
  * Use a memoised class instance.
5
5
  * - Creates a new instance of `Constructor` using `args`
6
6
  * - Returns same instance for as long as `args` is equal to previous `args`.
7
7
  */
8
8
  export function useInstance(Constructor, ...args) {
9
- const internals = useInternals();
9
+ const internals = useProps();
10
10
  // Update `internals` if `args` changes or `instance` is not set.
11
- if (!internals.args || !isArrayEqual(args, internals.args)) {
11
+ if (!internals.args || !internals.instance || !isArrayEqual(args, internals.args)) {
12
12
  internals.instance = new Constructor(...args);
13
13
  internals.args = args;
14
14
  }
15
- // biome-ignore lint/style/noNonNullAssertion: We know this is set.
16
15
  return internals.instance;
17
16
  }
package/react/useLazy.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import { isArrayEqual } from "../util/equal.js";
2
2
  import { getLazy } from "../util/lazy.js";
3
- import { useInternals } from "./useInternals.js";
3
+ import { useProps } from "./useProps.js";
4
4
  export function useLazy(value, ...args) {
5
- const internals = useInternals();
5
+ const internals = useProps();
6
6
  // Update `internals` if `args` changes.
7
- if (internals.args === undefined || !isArrayEqual(args, internals.args)) {
7
+ if (!internals.args || !isArrayEqual(args, internals.args)) {
8
8
  internals.value = getLazy(value, ...args);
9
9
  internals.args = args;
10
10
  }
11
- // biome-ignore lint/style/noNonNullAssertion: We know this is set.
12
11
  return internals.value;
13
12
  }
@@ -0,0 +1,2 @@
1
+ /** Create a mutable Map that persist for the lifetime of the component. */
2
+ export declare function useMap<K, V>(): Map<K, V>;
@@ -0,0 +1,8 @@
1
+ import { useRef } from "react";
2
+ /** Create a mutable Map that persist for the lifetime of the component. */
3
+ export function useMap() {
4
+ const ref = useRef(undefined);
5
+ if (!ref.current)
6
+ ref.current = new Map();
7
+ return ref.current;
8
+ }
@@ -0,0 +1,6 @@
1
+ type Mutable<T> = {
2
+ -readonly [K in keyof T]: T[K];
3
+ };
4
+ /** Create an object that persist for the lifetime of the component. */
5
+ export declare function useProps<T>(): Partial<Mutable<T>>;
6
+ export {};
@@ -0,0 +1,5 @@
1
+ import { useRef } from "react";
2
+ /** Create an object that persist for the lifetime of the component. */
3
+ export function useProps() {
4
+ return useRef(undefined);
5
+ }
package/react/useStore.js CHANGED
@@ -2,17 +2,16 @@ import { useSyncExternalStore } from "react";
2
2
  import { NONE } from "../util/constants.js";
3
3
  import { BLACKHOLE } from "../util/function.js";
4
4
  import { runSequence } from "../util/sequence.js";
5
- import { useInternals } from "./useInternals.js";
5
+ import { useProps } from "./useProps.js";
6
6
  export function useStore(store) {
7
7
  // Store memoized versions of `subscribe()` and `getSnapshot()` so `useSyncExternalStore()` doesn't re-subscribe on every render.
8
- const internals = useInternals();
8
+ const internals = useProps();
9
9
  // Update `internals` if `store` changes.
10
- if (store !== internals.store) {
10
+ if (store !== internals.store || !internals.subscribe || !internals.getSnapshot) {
11
11
  internals.subscribe = onStoreChange => (store ? runSequence(store, onStoreChange, onStoreChange) : BLACKHOLE);
12
12
  internals.getSnapshot = () => (!store ? undefined : store.loading ? NONE : store.value);
13
13
  internals.store = store;
14
14
  }
15
- // biome-ignore lint/style/noNonNullAssertion: We know these are set.
16
15
  useSyncExternalStore(internals.subscribe, internals.getSnapshot);
17
16
  return store;
18
17
  }
package/util/http.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { AnyCaller } from "../error/BaseError.js";
2
2
  import { RequestError } from "../error/RequestError.js";
3
3
  import { ResponseError } from "../error/ResponseError.js";
4
4
  /** A handler function takes a `Request` and returns a `Response` (possibly asynchronously). */
5
- export type Handler = (request: Request) => Response | Promise<Response>;
5
+ export type RequestHandler = (request: Request) => Response | Promise<Response>;
6
6
  export declare function _getMessageJSON(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
7
7
  export declare function _getMessageFormData(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
8
8
  export declare function _getMessageContent(message: Request | Response, MessageError: typeof RequestError | typeof ResponseError, caller: AnyCaller): Promise<unknown>;
package/util/source.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { AnyCaller } from "../error/BaseError.js";
1
2
  import type { Class } from "./class.js";
2
3
  /** Something that has a source of a specified type. */
3
4
  export interface Sourceable<T> {
@@ -6,4 +7,4 @@ export interface Sourceable<T> {
6
7
  /** Recurse through `Sourceable` objects and return the first one that is an instance of `type`, or `undefined` if no source object matches. */
7
8
  export declare function getSource<T>(type: Class<T>, value: unknown): T | undefined;
8
9
  /** Recurse through `Sourceable` objects and return the first one that is an instance of `type`, or throw `RequiredError` if no source object matches. */
9
- export declare function requireSource<T>(type: Class<T>, data: unknown): T;
10
+ export declare function requireSource<T>(type: Class<T>, data: unknown, caller?: AnyCaller): T;
package/util/source.js CHANGED
@@ -10,9 +10,9 @@ export function getSource(type, value) {
10
10
  }
11
11
  }
12
12
  /** Recurse through `Sourceable` objects and return the first one that is an instance of `type`, or throw `RequiredError` if no source object matches. */
13
- export function requireSource(type, data) {
13
+ export function requireSource(type, data, caller = requireSource) {
14
14
  const source = getSource(type, data);
15
15
  if (!source)
16
- throw new RequiredError(`Source "${type.name}" not found`, { received: data, expected: type, caller: requireSource });
16
+ throw new RequiredError(`Source "${type.name}" not found`, { received: data, expected: type, caller });
17
17
  return source;
18
18
  }
@@ -1,5 +0,0 @@
1
- import type { Data } from "../util/data.js";
2
- /** Store internal implementation details for a hook that persist for the lifetime of the component. */
3
- export declare const useInternals: <T extends Data>() => T | {
4
- [K in keyof T]: undefined;
5
- };
@@ -1,3 +0,0 @@
1
- import { useRef } from "react";
2
- /** Store internal implementation details for a hook that persist for the lifetime of the component. */
3
- export const useInternals = useRef;