@trpc/tanstack-react-query
Version:
TanStack React Query Integration for tRPC
348 lines (347 loc) • 14.2 kB
JavaScript
import { c as _objectSpread2, l as _defineProperty, o as readQueryKey } from "../utils-DghvpG0v.mjs";
import { isTRPCClientError } from "@trpc/client";
import { hashKey, isCancelledError } from "@tanstack/react-query";
import { getTRPCErrorFromUnknown } from "@trpc/server/unstable-core-do-not-import";
//#region src/unstable-handles/entryError.ts
/**
* What `loadHandles()` rejects with when a fetch or ensure entry fails. It
* names the entry, and keeps the request's error as `cause`.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#errors-ensure
*/
var TRPCEntryError = class extends Error {
constructor(opts) {
super(`Entry "${opts.key}" (${opts.path}) failed: ${opts.cause.message}`);
_defineProperty(this, "key", void 0);
_defineProperty(this, "path", void 0);
_defineProperty(this, "code", void 0);
_defineProperty(this, "cause", void 0);
this.name = "TRPCEntryError";
this.cause = opts.cause;
this.key = opts.key;
this.path = opts.path;
this.code = opts.code;
}
};
function codeOfShape(data) {
const code = data === null || data === void 0 ? void 0 : data.code;
return typeof code === "string" ? code : void 0;
}
function entryCause(error) {
if (isTRPCClientError(error)) return {
cause: error,
code: codeOfShape(error.data)
};
const cause = getTRPCErrorFromUnknown(error);
return {
cause,
code: cause.code
};
}
//#endregion
//#region src/unstable-handles/handles.ts
const notAHandle = "Expected a tRPC handle from `loadHandles()`";
function readHandle(handle, prefix) {
var _data$args, _data$prefix$, _data$prefix, _data$args2;
const queryKey = handle === null || handle === void 0 ? void 0 : handle.queryKey;
if (!Array.isArray(queryKey)) throw new Error(`${notAHandle}, got ${JSON.stringify(handle)}`);
const data = readQueryKey(queryKey);
const type = (_data$args = data.args) === null || _data$args === void 0 ? void 0 : _data$args.type;
if (type !== "query" && type !== "infinite") throw new Error(`${notAHandle}, got a key of type ${String(type)} for "${data.path.join(".")}"`);
if (((_data$prefix$ = (_data$prefix = data.prefix) === null || _data$prefix === void 0 ? void 0 : _data$prefix[0]) !== null && _data$prefix$ !== void 0 ? _data$prefix$ : void 0) !== prefix) {
var _data$prefix2;
throw new Error(`The handle for "${data.path.join(".")}" was created with keyPrefix ${JSON.stringify((_data$prefix2 = data.prefix) === null || _data$prefix2 === void 0 ? void 0 : _data$prefix2[0])}, but this proxy uses ${JSON.stringify(prefix)}`);
}
return {
type,
path: data.path,
input: (_data$args2 = data.args) === null || _data$args2 === void 0 ? void 0 : _data$args2.input,
queryKey
};
}
function toHandle(options, type) {
var _readQueryKey$args;
const input = (_readQueryKey$args = readQueryKey(options.queryKey).args) === null || _readQueryKey$args === void 0 ? void 0 : _readQueryKey$args.input;
const handle = _objectSpread2({ queryKey: options.queryKey }, input !== void 0 && { input });
if (type === "infinite" && options.initialPageParam != null) handle["initialCursor"] = options.initialPageParam;
return handle;
}
const entryState = Symbol("trpc.handleEntry");
function createEntry(state) {
return { [entryState]: state };
}
function readEntry(key, entry) {
const state = entry === null || entry === void 0 ? void 0 : entry[entryState];
if (!state) throw new Error(`Expected an entry from \`prefetchEntry()\`, \`fetchEntry()\` or \`ensureEntry()\` for "${key}", got ${JSON.stringify(entry)}`);
return state;
}
function startEntry(queryClient, state) {
const { options } = state;
if (state.type === "infinite") switch (state.kind) {
case "prefetch": return queryClient.prefetchInfiniteQuery(options);
case "ensure": return queryClient.ensureInfiniteQueryData(_objectSpread2(_objectSpread2({}, options), {}, { revalidateIfStale: true }));
case "fetch": return queryClient.fetchInfiniteQuery(options);
}
switch (state.kind) {
case "prefetch": return queryClient.prefetchQuery(options);
case "ensure": return queryClient.ensureQueryData(_objectSpread2(_objectSpread2({}, options), {}, { revalidateIfStale: true }));
case "fetch": return queryClient.fetchQuery(options);
}
}
function toEntryError(key, state, error) {
if (isCancelledError(error)) return error;
return new TRPCEntryError(_objectSpread2({
key,
path: readQueryKey(state.options.queryKey).path.join(".")
}, entryCause(error)));
}
function settleLoad(promise) {
return promise.then((data) => ({
status: "success",
data
}), (error) => {
if (error instanceof TRPCEntryError) return {
status: "error",
error
};
throw error;
});
}
async function startEntries(opts) {
const { queryClient, entries } = opts;
const loads = Object.entries(entries).map(([key, entry]) => {
const state = readEntry(key, entry);
if (state.queryClient !== queryClient) throw new Error(`The entry for "${key}" was built with a proxy for another QueryClient than the root proxy that loads it`);
return [key, state];
}).map(([key, state]) => ({
key,
state,
promise: startEntry(queryClient, state).catch((error) => {
throw toEntryError(key, state, error);
})
}));
const waited = loads.filter((load) => load.state.kind !== "prefetch");
const results = await Promise.all(waited.map((load) => opts.settle ? settleLoad(load.promise) : load.promise));
return [Object.fromEntries(loads.map((load) => [load.key, toHandle(load.state.options, load.state.type)])), Object.fromEntries(waited.map((load, index) => [load.key, results[index]]))];
}
function isServerEnvironment() {
return typeof window === "undefined" || "Deno" in globalThis;
}
const warnedNotHydrated = /* @__PURE__ */ new Set();
function warnIfNotHydrated(path, queryFn) {
return (context) => {
if (isServerEnvironment()) {
const queryHash = hashKey(context.queryKey);
if (!warnedNotHydrated.has(queryHash)) {
warnedNotHydrated.add(queryHash);
console.warn(`[@trpc/tanstack-react-query] The handle for "${path.join(".")}" is being fetched again during server rendering. Unless its data is stale, the query it refers to isn't in this QueryClient. Render the reading component inside <HydrateClient> or <HydrationBoundary>, and don't return handles from server functions or server actions. See https://trpc.io/docs/client/tanstack-react-query/handles#where-handles-dont-work`);
}
}
return queryFn(context);
};
}
const watchedHydrations = /* @__PURE__ */ new WeakSet();
/**
* A query dehydrated as pending that later rejects reaches the browser as a
* redacted error, before any `retry` from `fromHandle()` applies. Refetch such
* a query once so error boundaries see a `TRPCClientError` from this client.
*/
function refetchRejectedHydration(queryClient, type, options) {
const enabled = typeof options.enabled === "function" ? options.enabled : () => options.enabled;
if (isServerEnvironment()) return;
const query = queryClient.getQueryCache().find({
queryKey: options.queryKey,
exact: true
});
const promise = query === null || query === void 0 ? void 0 : query.promise;
if (!query || !promise || query.state.data !== void 0 || query.state.fetchStatus === "idle" || enabled(query) === false || watchedHydrations.has(promise)) return;
watchedHydrations.add(promise);
promise.catch((error) => {
var _defaulted$retry;
if (isTRPCClientError(error) || isCancelledError(error) || query.state.fetchStatus !== "idle" || enabled(query) === false) return;
const defaulted = queryClient.defaultQueryOptions(options);
(type === "infinite" ? queryClient.fetchInfiniteQuery(_objectSpread2(_objectSpread2({}, options), {}, { retry: (_defaulted$retry = defaulted.retry) !== null && _defaulted$retry !== void 0 ? _defaulted$retry : 3 })) : query.fetch(defaulted)).catch(() => {});
});
}
function prepareHandleRead(opts) {
var _globalThis$process;
const { options } = opts;
if (((_globalThis$process = globalThis.process) === null || _globalThis$process === void 0 || (_globalThis$process = _globalThis$process.env) === null || _globalThis$process === void 0 ? void 0 : _globalThis$process["NODE_ENV"]) !== "production" && typeof options.queryFn === "function") options.queryFn = warnIfNotHydrated(opts.handle.path, options.queryFn);
refetchRejectedHydration(opts.queryClient, opts.handle.type, options);
return options;
}
function readProxy(proxy, fn) {
var _key$prefix;
const queryKey = proxy === null || proxy === void 0 ? void 0 : proxy.queryKey;
if (Array.isArray(queryKey)) throw new Error(`\`${fn}()\` expects a tRPC options proxy or one of its procedures, got a handle for \`${readQueryKey(queryKey).path.join(".")}\``);
if (typeof queryKey !== "function") throw new Error(`\`${fn}()\` expects a tRPC options proxy or one of its procedures, got ${String(proxy)}`);
const key = readQueryKey(queryKey());
return {
proxy,
path: key.path,
prefix: (_key$prefix = key.prefix) === null || _key$prefix === void 0 ? void 0 : _key$prefix[0]
};
}
function queryClientOf(ctx) {
return ctx.proxy["~queryClient"]();
}
function optionsOf(procedure, type) {
return type === "infinite" ? procedure.infiniteQueryOptions : procedure.queryOptions;
}
function readRoot(trpc, fn) {
const ctx = readProxy(trpc, fn);
if (ctx.path.length > 0) throw new Error(`\`${fn}()\` takes the root proxy, \`trpc\`, got \`trpc.${ctx.path.join(".")}\``);
return ctx;
}
function entry(opts) {
const ctx = readProxy(opts.procedure, opts.fn);
return createEntry({
type: opts.type,
kind: opts.kind,
options: optionsOf(ctx.proxy, opts.type)(opts.input, opts.opts),
queryClient: queryClientOf(ctx)
});
}
function isTRPCEntryError(error, procedure) {
if (!(error instanceof TRPCEntryError)) return false;
return procedure === void 0 || error.path === readProxy(procedure, "isTRPCEntryError").path.join(".");
}
/**
* Starts every entry before it waits for any, so the order of the entries doesn't
* matter. Resolves to `[handles, data]` once every fetch and ensure entry has data:
* a handle for every entry, and the data of the fetch and ensure entries. Rejects as
* soon as a fetch or ensure entry fails. Prefetch entries never make it reject.
*
* Takes the root of an options proxy, from `createTRPCOptionsProxy` or `useTRPC()`.
* Call it in a route loader, a server component or an event handler, not during
* render: every render would start the entries again.
* @example
* ```ts
* const [handles, data] = await loadHandles(trpc, {
* comments: prefetchInfiniteEntry(trpc.post.comments, { postId: id }),
* post: ensureEntry(trpc.post.byId, { id }),
* });
* if (!data.post.published) throw notFound();
* return handles;
* ```
* @see https://trpc.io/docs/client/tanstack-react-query/handles
*/
function loadHandles(trpc, entries) {
return startEntries({
queryClient: queryClientOf(readRoot(trpc, "loadHandles")),
entries,
settle: false
});
}
/**
* Like `loadHandles()`, but it waits for every fetch and ensure entry to
* settle and never rejects because one failed. Resolves to `[handles, results]`:
* `results` has `{ status: 'success', data }` or `{ status: 'error', error }`,
* with a `TRPCEntryError`, for every fetch and ensure entry.
*
* A cancelled query still rejects, as it does with `loadHandles()`.
* @example
* ```ts
* const [handles, results] = await loadHandlesSettled(trpc, {
* post: ensureEntry(trpc.post.byId, { id }),
* comments: prefetchInfiniteEntry(trpc.post.comments, { postId: id }),
* });
* if (results.post.status === 'error') {
* if (results.post.error.code === 'NOT_FOUND') throw notFound();
* throw results.post.error;
* }
* ```
* @see https://trpc.io/docs/client/tanstack-react-query/handles#errors-as-values
*/
function loadHandlesSettled(trpc, entries) {
return startEntries({
queryClient: queryClientOf(readRoot(trpc, "loadHandlesSettled")),
entries,
settle: true
});
}
function prefetchEntry(procedure, input, opts) {
return entry({
fn: "prefetchEntry",
type: "query",
kind: "prefetch",
procedure,
input,
opts
});
}
function fetchEntry(procedure, input, opts) {
return entry({
fn: "fetchEntry",
type: "query",
kind: "fetch",
procedure,
input,
opts
});
}
function ensureEntry(procedure, input, opts) {
return entry({
fn: "ensureEntry",
type: "query",
kind: "ensure",
procedure,
input,
opts
});
}
/**
* Like `prefetchEntry`, for an infinite query. Loads the first page unless `pages` is set.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#infinite-queries
*/
function prefetchInfiniteEntry(procedure, input, opts) {
return entry({
fn: "prefetchInfiniteEntry",
type: "infinite",
kind: "prefetch",
procedure,
input,
opts
});
}
/**
* Like `fetchEntry`, for an infinite query.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#infinite-queries
*/
function fetchInfiniteEntry(procedure, input, opts) {
return entry({
fn: "fetchInfiniteEntry",
type: "infinite",
kind: "fetch",
procedure,
input,
opts
});
}
/**
* Like `ensureEntry`, for an infinite query.
* @see https://trpc.io/docs/client/tanstack-react-query/handles#infinite-queries
*/
function ensureInfiniteEntry(procedure, input, opts) {
return entry({
fn: "ensureInfiniteEntry",
type: "infinite",
kind: "ensure",
procedure,
input,
opts
});
}
function fromHandle(trpc, rawHandle, opts) {
const ctx = readProxy(trpc, "fromHandle");
const handle = readHandle(rawHandle, ctx.prefix);
if (!ctx.path.every((segment, index) => handle.path[index] === segment)) throw new Error(`\`fromHandle(trpc.${ctx.path.join(".")}, handle)\` got a handle for \`${handle.path.join(".")}\`, which isn't in \`${ctx.path.join(".")}\`. Pass the root proxy, \`trpc\``);
const options = optionsOf(handle.path.slice(ctx.path.length).reduce((level, segment) => level[segment], ctx.proxy), handle.type)(handle.input, handle.type === "infinite" ? _objectSpread2({ initialCursor: rawHandle.initialCursor }, opts) : opts);
return prepareHandleRead({
queryClient: queryClientOf(ctx),
handle,
options
});
}
//#endregion
export { TRPCEntryError, ensureEntry, ensureInfiniteEntry, fetchEntry, fetchInfiniteEntry, fromHandle, isTRPCEntryError, loadHandles, loadHandlesSettled, prefetchEntry, prefetchInfiniteEntry };
//# sourceMappingURL=index.mjs.map