feat: add application-owned query cache contract
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { createFailure } from "../../contracts/errors.js";
|
||||
|
||||
export const QUERY_CACHE_DEFAULTS = Object.freeze({
|
||||
staleTime: 30_000,
|
||||
gcTime: 300_000,
|
||||
refetchOnWindowFocus: true,
|
||||
retry: false,
|
||||
mutationRetry: false,
|
||||
persistence: false,
|
||||
});
|
||||
|
||||
export function createQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: QUERY_CACHE_DEFAULTS.staleTime,
|
||||
gcTime: QUERY_CACHE_DEFAULTS.gcTime,
|
||||
refetchOnWindowFocus: QUERY_CACHE_DEFAULTS.refetchOnWindowFocus,
|
||||
retry: QUERY_CACHE_DEFAULTS.retry,
|
||||
},
|
||||
mutations: {
|
||||
retry: QUERY_CACHE_DEFAULTS.mutationRetry,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryClient} queryClient
|
||||
* @returns {import("../../application/ports/query-cache-port.js").QueryCachePort}
|
||||
*/
|
||||
export function createQueryCacheAdapter(queryClient) {
|
||||
return Object.freeze({
|
||||
read(key) {
|
||||
try {
|
||||
return { ok: true, value: queryClient.getQueryData(key) };
|
||||
} catch {
|
||||
return cacheFailure("read", key);
|
||||
}
|
||||
},
|
||||
write(key, value) {
|
||||
try {
|
||||
queryClient.setQueryData(key, structuredClone(value));
|
||||
return { ok: true };
|
||||
} catch {
|
||||
return cacheFailure("write", key);
|
||||
}
|
||||
},
|
||||
async invalidate(namespace) {
|
||||
try {
|
||||
await queryClient.invalidateQueries({ queryKey: namespace, exact: false });
|
||||
return { ok: true };
|
||||
} catch {
|
||||
return cacheFailure("invalidate", namespace);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {string} phase @param {readonly unknown[]} key */
|
||||
function cacheFailure(phase, key) {
|
||||
const namespace = typeof key[0] === "string" ? key[0] : "unknown";
|
||||
return {
|
||||
ok: /** @type {false} */ (false),
|
||||
error: createFailure("QUERY_CACHE_FAILURE", "QUERY_CACHE", 0, {
|
||||
code: `QUERY_CACHE_${phase.toUpperCase()}_FAILED`,
|
||||
causeClass: `namespace:${namespace}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* @typedef {{
|
||||
* read(key: readonly unknown[]): unknown,
|
||||
* write(key: readonly unknown[], value: unknown): void,
|
||||
* invalidate(namespace: readonly unknown[]): Promise<void>
|
||||
* read(key: readonly unknown[]): { ok: true, value: unknown } |
|
||||
* { ok: false, error: import("../../contracts/errors.js").ApiFailure },
|
||||
* write(key: readonly unknown[], value: unknown): { ok: true } |
|
||||
* { ok: false, error: import("../../contracts/errors.js").ApiFailure },
|
||||
* invalidate(namespace: readonly unknown[]): Promise<{ ok: true } |
|
||||
* { ok: false, error: import("../../contracts/errors.js").ApiFailure }>
|
||||
* }} QueryCachePort
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const RESOURCE_NAMESPACE = Object.freeze(["resource", 1]);
|
||||
|
||||
export const queryKeys = Object.freeze({
|
||||
resource: Object.freeze({
|
||||
all: () => RESOURCE_NAMESPACE,
|
||||
list: (filters = {}) =>
|
||||
Object.freeze([...RESOURCE_NAMESPACE, "list", canonicalize(filters)]),
|
||||
/** @param {string} resourceId */
|
||||
detail: (resourceId) =>
|
||||
Object.freeze([...RESOURCE_NAMESPACE, "detail", String(resourceId)]),
|
||||
}),
|
||||
});
|
||||
|
||||
export const QUERY_REGISTRY = Object.freeze({
|
||||
RESOURCE: Object.freeze({
|
||||
namespace: RESOURCE_NAMESPACE,
|
||||
serialization: "canonical-object-order",
|
||||
identity: "no-pii-token-or-raw-url",
|
||||
invalidation: "resource namespace after successful mutation",
|
||||
version: 1,
|
||||
persistence: "disabled",
|
||||
}),
|
||||
});
|
||||
|
||||
/** @param {unknown} value @returns {unknown} */
|
||||
export function canonicalize(value) {
|
||||
if (Array.isArray(value)) return value.map(canonicalize);
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, item]) => [key, canonicalize(item)]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user