next-safe-action
Version:
Type safe and validated Server Actions in your Next.js project.
443 lines (431 loc) • 12.5 kB
JavaScript
"use client";
// src/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/hooks.ts
var useAction = (safeActionFn, cb) => {
const [isTransitioning, startTransition] = React2.useTransition();
const [result, setResult] = React2.useState({});
const [clientInput, setClientInput] = React2.useState();
const [isExecuting, setIsExecuting] = React2.useState(false);
const [navigationError, setNavigationError] = React2.useState(null);
const [thrownError, setThrownError] = React2.useState(null);
const [isIdle, setIsIdle] = React2.useState(true);
const status = getActionStatus({
isExecuting,
isTransitioning,
result,
isIdle,
hasNavigated: navigationError !== null,
hasThrownError: thrownError !== null,
});
const execute = React2.useCallback(
(input) => {
setTimeout(() => {
setIsIdle(false);
setNavigationError(null);
setThrownError(null);
setClientInput(input);
setIsExecuting(true);
}, 0);
startTransition(() => {
safeActionFn(input)
.then((res) => setResult(res ?? {}))
.catch((e) => {
setResult({});
if (FrameworkErrorHandler.isNavigationError(e)) {
setNavigationError(e);
return;
}
setThrownError(e);
throw e;
})
.finally(() => {
setIsExecuting(false);
});
});
},
[safeActionFn]
);
const executeAsync = React2.useCallback(
(input) => {
const fn = new Promise((resolve, reject) => {
setTimeout(() => {
setIsIdle(false);
setNavigationError(null);
setThrownError(null);
setClientInput(input);
setIsExecuting(true);
}, 0);
startTransition(() => {
safeActionFn(input)
.then((res) => {
setResult(res ?? {});
resolve(res);
})
.catch((e) => {
setResult({});
if (FrameworkErrorHandler.isNavigationError(e)) {
setNavigationError(e);
return;
}
setThrownError(e);
reject(e);
})
.finally(() => {
setIsExecuting(false);
});
});
});
return fn;
},
[safeActionFn]
);
const reset = React2.useCallback(() => {
setIsIdle(true);
setNavigationError(null);
setThrownError(null);
setClientInput(void 0);
setResult({});
}, []);
useActionCallbacks({
result: result ?? {},
input: clientInput,
status,
navigationError,
thrownError,
cb,
});
return {
execute,
executeAsync,
input: clientInput,
result,
reset,
status,
...getActionShorthandStatusObject(status),
};
};
var useOptimisticAction = (safeActionFn, utils) => {
const [isTransitioning, startTransition] = React2.useTransition();
const [result, setResult] = React2.useState({});
const [clientInput, setClientInput] = React2.useState();
const [isExecuting, setIsExecuting] = React2.useState(false);
const [navigationError, setNavigationError] = React2.useState(null);
const [thrownError, setThrownError] = React2.useState(null);
const [isIdle, setIsIdle] = React2.useState(true);
const [optimisticState, setOptimisticValue] = React2.useOptimistic(utils.currentState, utils.updateFn);
const status = getActionStatus({
isExecuting,
isTransitioning,
result,
isIdle,
hasNavigated: navigationError !== null,
hasThrownError: thrownError !== null,
});
const execute = React2.useCallback(
(input) => {
setTimeout(() => {
setIsIdle(false);
setClientInput(input);
setNavigationError(null);
setThrownError(null);
setIsExecuting(true);
}, 0);
startTransition(() => {
setOptimisticValue(input);
safeActionFn(input)
.then((res) => setResult(res ?? {}))
.catch((e) => {
setResult({});
if (FrameworkErrorHandler.isNavigationError(e)) {
setNavigationError(e);
return;
}
setThrownError(e);
throw e;
})
.finally(() => {
setIsExecuting(false);
});
});
},
[safeActionFn, setOptimisticValue]
);
const executeAsync = React2.useCallback(
(input) => {
const fn = new Promise((resolve, reject) => {
setTimeout(() => {
setIsIdle(false);
setClientInput(input);
setNavigationError(null);
setThrownError(null);
setIsExecuting(true);
}, 0);
startTransition(() => {
setOptimisticValue(input);
safeActionFn(input)
.then((res) => {
setResult(res ?? {});
resolve(res);
})
.catch((e) => {
setResult({});
if (FrameworkErrorHandler.isNavigationError(e)) {
setNavigationError(e);
return;
}
setThrownError(e);
reject(e);
})
.finally(() => {
setIsExecuting(false);
});
});
});
return fn;
},
[safeActionFn, setOptimisticValue]
);
const reset = React2.useCallback(() => {
setIsIdle(true);
setClientInput(void 0);
setNavigationError(null);
setThrownError(null);
setResult({});
}, []);
useActionCallbacks({
result: result ?? {},
input: clientInput,
status,
navigationError,
thrownError,
cb: {
onExecute: utils.onExecute,
onSuccess: utils.onSuccess,
onError: utils.onError,
onSettled: utils.onSettled,
onNavigation: utils.onNavigation,
},
});
return {
execute,
executeAsync,
input: clientInput,
result,
optimisticState,
reset,
status,
...getActionShorthandStatusObject(status),
};
};
export { useAction, useOptimisticAction };
//# sourceMappingURL=hooks.mjs.map