UNPKG

next-safe-action

Version:

Type safe and validated Server Actions in your Next.js project.

278 lines (266 loc) 8.48 kB
"use client"; // src/stateful-hooks.ts import * as React2 from "react"; // src/hooks-utils.ts import * as React from "react"; // src/next/errors/bailout-to-csr.ts var BAILOUT_TO_CSR = "BAILOUT_TO_CLIENT_SIDE_RENDERING"; function isBailoutToCSRError(err) { if (typeof err !== "object" || err === null || !("digest" in err)) { return false; } return err.digest === BAILOUT_TO_CSR; } // src/next/errors/http-access-fallback.ts var HTTPAccessErrorStatus = { NOT_FOUND: 404, FORBIDDEN: 403, UNAUTHORIZED: 401, }; var ALLOWED_CODES = new Set(Object.values(HTTPAccessErrorStatus)); var HTTP_ERROR_FALLBACK_ERROR_CODE = "NEXT_HTTP_ERROR_FALLBACK"; function isHTTPAccessFallbackError(error) { if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") { return false; } const [prefix, httpStatus] = error.digest.split(";"); return prefix === HTTP_ERROR_FALLBACK_ERROR_CODE && ALLOWED_CODES.has(Number(httpStatus)); } function getAccessFallbackHTTPStatus(error) { const httpStatus = error.digest.split(";")[1]; return Number(httpStatus); } // src/next/errors/redirect.ts var RedirectStatusCode = /* @__PURE__ */ ((RedirectStatusCode2) => { RedirectStatusCode2[(RedirectStatusCode2["SeeOther"] = 303)] = "SeeOther"; RedirectStatusCode2[(RedirectStatusCode2["TemporaryRedirect"] = 307)] = "TemporaryRedirect"; RedirectStatusCode2[(RedirectStatusCode2["PermanentRedirect"] = 308)] = "PermanentRedirect"; return RedirectStatusCode2; })(RedirectStatusCode || {}); var REDIRECT_ERROR_CODE = "NEXT_REDIRECT"; function isRedirectError(error) { if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") { return false; } const digest = error.digest.split(";"); const [errorCode, type] = digest; const destination = digest.slice(2, -2).join(";"); const status = digest.at(-2); const statusCode = Number(status); return ( errorCode === REDIRECT_ERROR_CODE && (type === "replace" || type === "push") && typeof destination === "string" && !isNaN(statusCode) && statusCode in RedirectStatusCode ); } // src/next/errors/router.ts function isNextRouterError(error) { return isRedirectError(error) || isHTTPAccessFallbackError(error); } // src/next/errors/dynamic-usage.ts var DYNAMIC_ERROR_CODE = "DYNAMIC_SERVER_USAGE"; function isDynamicServerError(err) { if (typeof err !== "object" || err === null || !("digest" in err) || typeof err.digest !== "string") { return false; } return err.digest === DYNAMIC_ERROR_CODE; } function isDynamicPostponeReason(reason) { return ( reason.includes("needs to bail out of prerendering at this point because it used") && reason.includes("Learn more: https://nextjs.org/docs/messages/ppr-caught-error") ); } function isDynamicPostpone(err) { if ( typeof err === "object" && err !== null && // eslint-disable-next-line typeof err.message === "string" ) { return isDynamicPostponeReason(err.message); } return false; } var isDynamicUsageError = (err) => isDynamicServerError(err) || isBailoutToCSRError(err) || isNextRouterError(err) || isDynamicPostpone(err); // src/next/errors/postpone.ts var REACT_POSTPONE_TYPE = Symbol.for("react.postpone"); function isPostpone(error) { return ( typeof error === "object" && error !== null && // eslint-disable-next-line error.$$typeof === REACT_POSTPONE_TYPE ); } // src/next/errors/index.ts var FrameworkErrorHandler = class _FrameworkErrorHandler { #frameworkError; static isNavigationError(error) { return isNextRouterError(error) || isBailoutToCSRError(error) || isDynamicUsageError(error) || isPostpone(error); } static getNavigationKind(error) { if (isRedirectError(error)) { return "redirect"; } else if (isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 404) { return "notFound"; } else if (isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 403) { return "forbidden"; } else if (isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 401) { return "unauthorized"; } else { return "other"; } } // Used in action builder. handleError(e) { if (_FrameworkErrorHandler.isNavigationError(e)) { this.#frameworkError = e; return; } throw e; } get error() { return this.#frameworkError; } }; // src/hooks-utils.ts var getActionStatus = ({ isIdle, isExecuting, isTransitioning, result, hasNavigated, hasThrownError }) => { if (isIdle) { return "idle"; } else if (isExecuting) { return "executing"; } else if (isTransitioning) { return "transitioning"; } else if ( hasThrownError || typeof result.validationErrors !== "undefined" || typeof result.serverError !== "undefined" ) { return "hasErrored"; } else if (hasNavigated) { return "hasNavigated"; } else { return "hasSucceeded"; } }; var getActionShorthandStatusObject = (status) => { return { isIdle: status === "idle", isExecuting: status === "executing", isTransitioning: status === "transitioning", isPending: status === "executing" || status === "transitioning", hasSucceeded: status === "hasSucceeded", hasErrored: status === "hasErrored", hasNavigated: status === "hasNavigated", }; }; var useActionCallbacks = ({ result, input, status, cb, navigationError, thrownError }) => { const onExecuteRef = React.useRef(cb?.onExecute); const onSuccessRef = React.useRef(cb?.onSuccess); const onErrorRef = React.useRef(cb?.onError); const onSettledRef = React.useRef(cb?.onSettled); const onNavigationRef = React.useRef(cb?.onNavigation); React.useEffect(() => { const onExecute = onExecuteRef.current; const onSuccess = onSuccessRef.current; const onError = onErrorRef.current; const onSettled = onSettledRef.current; const onNavigation = onNavigationRef.current; const executeCallbacks = async () => { switch (status) { case "executing": await Promise.resolve(onExecute?.({ input })).then(() => {}); break; case "transitioning": break; case "hasSucceeded": if (navigationError || thrownError) { break; } await Promise.all([ Promise.resolve(onSuccess?.({ data: result?.data, input })), Promise.resolve(onSettled?.({ result, input })), ]); break; case "hasErrored": await Promise.all([ Promise.resolve(onError?.({ error: { ...result, ...(thrownError ? { thrownError } : {}) }, input })), Promise.resolve(onSettled?.({ result, input })), ]); break; } if (!navigationError) return; const navigationKind = FrameworkErrorHandler.getNavigationKind(navigationError); if (navigationKind === "redirect" || status === "hasNavigated") { const navigationKind2 = FrameworkErrorHandler.getNavigationKind(navigationError); await Promise.all([ Promise.resolve( onNavigation?.({ input, navigationKind: navigationKind2, }) ), Promise.resolve(onSettled?.({ result, input, navigationKind: navigationKind2 })), ]); } throw navigationError; }; executeCallbacks().catch(console.error); }, [input, status, result, navigationError, thrownError]); }; // src/stateful-hooks.ts var useStateAction = (safeActionFn, utils) => { const [result, dispatcher, isExecuting] = React2.useActionState( safeActionFn, utils?.initResult ?? {}, utils?.permalink ); const [isIdle, setIsIdle] = React2.useState(true); const [isTransitioning, startTransition] = React2.useTransition(); const [clientInput, setClientInput] = React2.useState(); const status = getActionStatus({ isExecuting, isTransitioning, result: result ?? {}, isIdle, // HACK: This is a workaround to avoid the status being "hasNavigated" when the action is executed. hasNavigated: false, hasThrownError: false, }); const execute = React2.useCallback( (input) => { setTimeout(() => { setIsIdle(false); setClientInput(input); }, 0); startTransition(() => { dispatcher(input); }); }, [dispatcher] ); useActionCallbacks({ result: result ?? {}, input: clientInput, status, cb: { onExecute: utils?.onExecute, onSuccess: utils?.onSuccess, onError: utils?.onError, onSettled: utils?.onSettled, }, navigationError: null, thrownError: null, }); return { execute, input: clientInput, result, status, ...getActionShorthandStatusObject(status), }; }; export { useStateAction }; //# sourceMappingURL=stateful-hooks.mjs.map