diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 025d148..05c579e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,6 +20,7 @@ jobs: node-version: 24 - run: bun install --frozen-lockfile + - run: bun fmt - run: bun test - run: bun run build - - run: npm publish + - run: npm publish --tag beta diff --git a/.gitignore b/.gitignore index f06235c..82ee25a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +.idea diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..8d55b5b --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,14 @@ +{ + "printWidth": 60, + "experimentalSortImports": { + "groups": [ + ["side-effect"], + ["builtin"], + ["external", "external-type"], + ["internal", "internal-type"], + ["parent", "parent-type"], + ["sibling", "sibling-type"], + ["index", "index-type"] + ] + } +} diff --git a/README.md b/README.md index 8097193..7af9ab1 100644 --- a/README.md +++ b/README.md @@ -19,20 +19,23 @@ Create a safe function from an unsafe one: ```ts const getUser = n.safeFn( - async (id: string) => { - const res = await fetch(`https://example.com/users/${id}`); - if (!res.ok) return { success: false, error: "FAILED_TO_FETCH" }; - - return { success: true, data: await res.json() }; - }, - (err) => "FAILED_TO_GET_USER", + async (id: string) => { + const res = await fetch( + `https://example.com/users/${id}`, + ); + if (!res.ok) + return { success: false, error: "FAILED_TO_FETCH" }; + + return { success: true, data: await res.json() }; + }, + (err) => "FAILED_TO_GET_USER", ); const getUserResult = await getUser("some-user-id"); if (!getUserResult.success) { - console.error(getUserResult.error); + console.error(getUserResult.error); } else { - console.log(getUserResult.data); + console.log(getUserResult.data); } ``` @@ -43,12 +46,12 @@ Runs the provided callback function, catching any thrown errors and returning a ```ts const user = await n.fromUnsafe( - () => db.findUser("some-user-id"), - (err) => "FAILED_T0_FIND_USER", + () => db.findUser("some-user-id"), + (err) => "FAILED_T0_FIND_USER", ); if (!user.success) { - console.error(user.error); + console.error(user.error); } else { - console.log(user.data); + console.log(user.data); } ``` diff --git a/bun.lock b/bun.lock index 847fd65..e060fc4 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "oh", "devDependencies": { "@types/bun": "latest", + "oxfmt": "0.32.0", }, "peerDependencies": { "typescript": "5", @@ -12,6 +14,44 @@ }, }, "packages": { + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.32.0", "", { "os": "android", "cpu": "arm" }, "sha512-DpVyuVzgLH6/MvuB/YD3vXO9CN/o9EdRpA0zXwe/tagP6yfVSFkFWkPqTROdqp0mlzLH5Yl+/m+hOrcM601EbA=="], + + "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-w1cmNXf9zs0vKLuNgyUF3hZ9VUAS1hBmQGndYJv1OmcVqStBtRTRNxSWkWM0TMkrA9UbvIvM9gfN+ib4Wy6lkQ=="], + + "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-m6wQojz/hn94XdZugFPtdFbOvXbOSYEqPsR2gyLyID3BvcrC2QsJyT1o3gb4BZEGtZrG1NiKVGwDRLM0dHd2mg=="], + + "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hN966Uh6r3Erkg2MvRcrJWaB6QpBzP15rxWK/QtkUyD47eItJLsAQ2Hrm88zMIpFZ3COXZLuN3hqgSlUtvB0Xw=="], + + "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g5UZPGt8tJj263OfSiDGdS54HPa0KgFfspLVAUivVSdoOgsk6DkwVS9nO16xQTDztzBPGxTvrby8WuufF0g86Q=="], + + "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-F4ZY83/PVQo9ZJhtzoMqbmjqEyTVEZjbaw4x1RhzdfUhddB41ZB2Vrt4eZi7b4a4TP85gjPRHgQBeO0c1jbtaw=="], + + "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-olR37eG16Lzdj9OBSvuoT5RxzgM5xfQEHm1OEjB3M7Wm4KWa5TDWIT13Aiy74GvAN77Hq1+kUKcGVJ/0ynf75g=="], + + "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-eZhk6AIjRCDeLoXYBhMW7qq/R1YyVi+tGnGfc3kp7AZQrMsFaWtP/bgdCJCTNXMpbMwymtVz0qhSQvR5w2sKcg=="], + + "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UYiqO9MlipntFbdbUKOIo84vuyzrK4TVIs7Etat91WNMFSW54F6OnHq08xa5ZM+K9+cyYMgQPXvYCopuP+LyKw=="], + + "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.32.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-IDH/fxMv+HmKsMtsjEbXqhScCKDIYp38sgGEcn0QKeXMxrda67PPZA7HMfoUwEtFUG+jsO1XJxTrQsL+kQ90xQ=="], + + "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.32.0", "", { "os": "linux", "cpu": "none" }, "sha512-bQFGPDa0buYWJFeK2I7ah8wRZjrAgamaG2OAGv+Ua5UMYEnHxmHcv+r8lWUUrwP2oqQGvp1SB8JIVtBbYuAueQ=="], + + "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.32.0", "", { "os": "linux", "cpu": "none" }, "sha512-3vFp9DW1ItEKWltADzCFqG5N7rYFToT4ztlhg8wALoo2E2VhveLD88uAF4FF9AxD9NhgHDGmPCV+WZl/Qlj8cQ=="], + + "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.32.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Fub2y8S9ImuPzAzpbgkoz/EVTWFFBolxFZYCMRhRZc8cJZI2gl/NlZswqhvJd/U0Jopnwgm/OJ2x128vVzFFWA=="], + + "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XufwsnV3BF81zO2ofZvhT4FFaMmLTzZEZnC9HpFz/quPeg9C948+kbLlZnsfjmp+1dUxKMCpfmRMqOfF4AOLsA=="], + + "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-u2f9tC2qYfikKmA2uGpnEJgManwmk0ZXWs5BB4ga4KDu2JNLdA3i634DGHeMLK9wY9+iRf3t7IYpgN3OVFrvDw=="], + + "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.32.0", "", { "os": "none", "cpu": "arm64" }, "sha512-5ZXb1wrdbZ1YFXuNXNUCePLlmLDy4sUt4evvzD4Cgumbup5wJgS9PIe5BOaLywUg9f1wTH6lwltj3oT7dFpIGA=="], + + "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-IGSMm/Agq+IA0++aeAV/AGPfjcBdjrsajB5YpM3j7cMcwoYgUTi/k2YwAmsHH3ueZUE98pSM/Ise2J7HtyRjOA=="], + + "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.32.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-H/9gsuqXmceWMsVoCPZhtJG2jLbnBeKr7xAXm2zuKpxLVF7/2n0eh7ocOLB6t+L1ARE76iORuUsRMnuGjj8FjQ=="], + + "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-fF8VIOeligq+mA6KfKvWtFRXbf0EFy73TdR6ZnNejdJRM8VWN1e3QFhYgIwD7O8jBrQsd7EJbUpkAr/YlUOokg=="], + "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], "@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="], @@ -22,6 +62,10 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "oxfmt": ["oxfmt@0.32.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.32.0", "@oxfmt/binding-android-arm64": "0.32.0", "@oxfmt/binding-darwin-arm64": "0.32.0", "@oxfmt/binding-darwin-x64": "0.32.0", "@oxfmt/binding-freebsd-x64": "0.32.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.32.0", "@oxfmt/binding-linux-arm-musleabihf": "0.32.0", "@oxfmt/binding-linux-arm64-gnu": "0.32.0", "@oxfmt/binding-linux-arm64-musl": "0.32.0", "@oxfmt/binding-linux-ppc64-gnu": "0.32.0", "@oxfmt/binding-linux-riscv64-gnu": "0.32.0", "@oxfmt/binding-linux-riscv64-musl": "0.32.0", "@oxfmt/binding-linux-s390x-gnu": "0.32.0", "@oxfmt/binding-linux-x64-gnu": "0.32.0", "@oxfmt/binding-linux-x64-musl": "0.32.0", "@oxfmt/binding-openharmony-arm64": "0.32.0", "@oxfmt/binding-win32-arm64-msvc": "0.32.0", "@oxfmt/binding-win32-ia32-msvc": "0.32.0", "@oxfmt/binding-win32-x64-msvc": "0.32.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-KArQhGzt/Y8M1eSAX98Y8DLtGYYDQhkR55THUPY5VNcpFQ+9nRZkL3ULXhagHMD2hIvjy8JSeEQEP5/yYJSrLA=="], + + "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], diff --git a/index.test.ts b/index.test.ts index cf92fdd..5f8646a 100644 --- a/index.test.ts +++ b/index.test.ts @@ -1,11 +1,20 @@ -import { describe, expect, it } from "bun:test"; -import { n } from "."; +import { + describe, + expect, + expectTypeOf, + it, +} from "bun:test"; + +import { n, Result } from "."; describe("safeFn", () => { it("should catch any thrown errors and return success false", async () => { - const safeFunction = n.safeFn(async () => { - throw new Error("Unexpected error."); - }); + const safeFunction = n.safeFn( + async () => { + throw new Error("Unexpected error."); + }, + (originalError) => n.err(originalError), + ); const result = await safeFunction(); @@ -13,18 +22,20 @@ describe("safeFn", () => { }); it("should call and return the value from the error handler if an error is thrown", async () => { - const expectedErrorMessage = "an-unknown-error-occured" as const; + const expectedErrorMessage = + "an-unknown-error-occured" as const; const safeFunction = n.safeFn( async () => { throw new Error("Unexpected error."); }, - () => expectedErrorMessage + () => n.err(expectedErrorMessage), ); const result = await safeFunction(); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); expect(result.error).toBe(expectedErrorMessage); @@ -33,13 +44,17 @@ describe("safeFn", () => { it("should return the success result of the callback if it doesn't throw", async () => { const expectedData = "some data" as const; - const safeFunction = n.safeFn(async () => { - return { success: true, data: expectedData }; - }); + const safeFunction = n.safeFn( + async () => { + return { success: true, data: expectedData }; + }, + (originalError) => n.err(originalError), + ); const result = await safeFunction(); - if (!result.success) throw new Error("Result should be success."); + if (!result.success) + throw new Error("Result should be success."); expect(result.success).toBe(true); expect(result.data).toBe(expectedData); @@ -48,13 +63,17 @@ describe("safeFn", () => { it("should return the error result of the callback if it doesn't throw", async () => { const expectedError = "some-error" as const; - const safeFunction = n.safeFn(async () => { - return { success: false, error: expectedError }; - }); + const safeFunction = n.safeFn( + async () => { + return { success: false, error: expectedError }; + }, + (originalError) => n.err(originalError), + ); const result = await safeFunction(); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); expect(result.error).toBe(expectedError); @@ -63,13 +82,17 @@ describe("safeFn", () => { it("should pass arguments to the callback", async () => { const expectedName = "Bob"; - const safeFunction = n.safeFn(async (name: string) => { - return { success: true, data: name }; - }); + const safeFunction = n.safeFn( + async (name: string) => { + return { success: true, data: name }; + }, + (originalError) => n.err(originalError), + ); const result = await safeFunction(expectedName); - if (!result.success) throw new Error("Result should be success."); + if (!result.success) + throw new Error("Result should be success."); expect(result.success).toBe(true); expect(result.data).toBe(expectedName); @@ -77,32 +100,45 @@ describe("safeFn", () => { }); describe("fromUnsafe", () => { - it("should return the value from the callback", async () => { + it("should return the value from the callback", () => { const expectedReturn = "some result"; - const result = await n.fromUnsafe(() => expectedReturn); + const result = n.fromUnsafe( + () => expectedReturn, + (originalError) => n.err(originalError), + ); - if (!result.success) throw new Error("Result should be success."); + if (!result.success) + throw new Error("Result should be success."); expect(result.success).toBe(true); expect(result.data).toBe(expectedReturn); }); it("should handle synchronous errors", async () => { - const result = n.fromUnsafe(() => { - if (true as boolean) throw new Error("Some synchronous error"); - }); + const result = n.fromUnsafe( + () => { + if (true as boolean) + throw new Error("Some synchronous error"); + }, + (originalError) => n.err(originalError), + ); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); }); it("should handle asynchronous errors", async () => { - const result = await n.fromUnsafe(async () => { - throw new Error("Some synchronous error"); - }); + const result = await n.fromUnsafe( + async () => { + throw new Error("Some synchronous error"); + }, + (originalError) => n.err(originalError), + ); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); }); @@ -115,7 +151,10 @@ describe("fromUnsafe", () => { async () => { throw thrownError; }, - (e) => (originalError = e) + (e) => { + originalError = e; + return n.err(originalError); + }, ); expect(originalError).toBe(thrownError); @@ -128,12 +167,76 @@ describe("fromUnsafe", () => { async () => { throw new Error("Some synchronous error"); }, - () => expectedError + (originalError) => n.err(expectedError), ); - if (result.success) throw new Error("Result should not be success."); + if (result.success) + throw new Error("Result should not be success."); expect(result.success).toBe(false); expect(result.error).toBe(expectedError); }); }); + +describe("resultsToResult", () => { + it("should return success false if a single result is success false", () => { + const results = [ + { success: true, data: "some data" as const }, + { success: false, error: "SOME_ERROR" as const }, + { + success: false, + error: "SOME_OTHER_ERROR" as const, + }, + ] satisfies Result[]; + + const result = n.resultsToResult(results); + + if (result.success) { + expectTypeOf(result).toMatchObjectType<{ + success: true; + data: "some data"[]; + }>(); + } + + if (!result.success) { + expectTypeOf(result).toMatchObjectType<{ + success: false; + error: ("SOME_ERROR" | "SOME_OTHER_ERROR")[]; + }>(); + + expect(result.success).toBe(false); + expect(result.error).toMatchObject([ + "SOME_ERROR" as const, + "SOME_OTHER_ERROR" as const, + ]); + } + }); + + it("should return success true if there are no success false results", () => { + const results = [ + { success: true, data: "some data" as const }, + { success: true, data: "other data" as const }, + ] satisfies Result[]; + + const result = n.resultsToResult(results); + + if (!result.success) { + expectTypeOf(result).toMatchObjectType<{ + success: false; + error: never[]; + }>(); + throw new Error("Should be success result."); + } + + expectTypeOf(result).toMatchObjectType<{ + success: true; + data: ("some data" | "other data")[]; + }>(); + + expect(result.success).toBe(true); + expect(result.data).toMatchObject([ + "some data" as const, + "other data" as const, + ]); + }); +}); diff --git a/index.ts b/index.ts index 00278f1..67e5756 100644 --- a/index.ts +++ b/index.ts @@ -1,21 +1,27 @@ - - export type Result = - | { - success: true; - data: T; - } - | { success: false; error: E }; - -type DataOf = R extends { success: true; data: infer D } ? D : never -type ErrorOf = R extends { success: false; error: infer E } ? E : never + | { + success: true; + data: T; + } + | { success: false; error: E }; + +type DataOf = R extends { + success: true; + data: infer D; +} + ? D + : never; +type ErrorOf = R extends { + success: false; + error: infer E; +} + ? E + : never; /** - * Create a safe function from an unsafe one. + * Create a typesafe instance of neverpanic. * - * @param cb - The async function to wrap. - * @param [eh] - Optional fallback error handler. - * @returns A new function that returns a typesafe Result. + * @returns An instance of neverpanic that conforms to the types specified in the generic arguments. * * @example * const getUser = n.safeFn( @@ -35,95 +41,154 @@ type ErrorOf = R extends { success: false; error: infer E } ? * console.log(getUserResult.data); * } */ -function safeFn< - T extends Result | Promise, - A extends unknown[], - E = null, ->( - cb: (...args: A) => T, - eh?: (e: unknown) => E, -): (...args: A) => T | Result { - const createErrorResult = (e: unknown) => - ({ - success: false, - error: eh?.(e) ?? null, - }) as const; - - return (...args) => { - try { - const result = cb(...args); - - if (result instanceof Promise) - return result.catch(createErrorResult) as T; - - return result; - } catch (e) { - return createErrorResult(e) as T; - } - }; -} - -/** - * Run an unsafe function, handle any errors and return a Result. - * - * @param cb - The async function to call. - * @param [eh] - Optional fallback error handler. - * @returns The awaited return value of cb. - * - * @example - * const user = await n.fromUnsafe(() => db.findUser('some-user-id'), () => 'FAILED_T0_FIND_USER') - * if (!user.success) { - * console.error(user.error) - * } else { - * console.log(user.data) - * } - */ -function fromUnsafe< - T, - E = null, - R = T extends Promise - ? Promise, E>> - : Result, ->(cb: () => T, eh?: (err: unknown) => E): R { - const createErrorResult = (e: unknown) => ({ - success: false, - error: eh?.(e) ?? null, - }); - - const createSuccessResult = (data: T) => - ({ - success: true, - data, - }) as const; - - try { - const result = cb(); - - if (result instanceof Promise) - return result.then(createSuccessResult).catch(createErrorResult) as R; - - return createSuccessResult(result) as R; - } catch (e) { - return createErrorResult(e) as R; - } -} - -function resultsToResult(results: R): Result[], ErrorOf[]> { - let success: boolean = true - - const error: any[] = [] - const data: any[] = [] - - for (const result of results) { - if (!result.success) { - success = false - error.push(result.error) - } else { - data.push(result.data) - } - } - - return success ? { success: true, data } : { success: false, error } -} - -export const n = { safeFn, fromUnsafe, resultsToResult }; +export const createNeverpanic = < + D = unknown, + E = unknown, +>() => { + const ok = ( + data: T, + ): { success: true; data: T } => ({ + success: true as const, + data, + }); + + const err = ( + error: T, + ): { success: false; error: T } => ({ + success: false as const, + error, + }); + + /** + * Create a safe function from an unsafe one. + * + * @param cb - The async function to wrap. + * @param [eh] - Optional fallback error handler. + * @returns A new function that returns a typesafe Result. + * + * @example + * const getUser = n.safeFn( + * async (id: string) => { + * const res = await fetch(`https://example.com/users/${id}`); + * if (!res.ok) return { success: false, error: "FAILED_TO_FETCH" }; + * + * return { success: true, data: await res.json() }; + * }, + * () => "FAILED_TO_GET_USER" + * ); + * + * const getUserResult = await getUser("some-user-id"); + * if (!getUserResult.success) { + * console.error(getUserResult.error); + * } else { + * console.log(getUserResult.data); + * } + */ + const safeFn = + < + T extends Result | Promise>, + A extends unknown[], + EH extends Result, + >( + cb: (...args: A) => T, + eh: (e: unknown) => EH, + ): ((...args: A) => T | EH) => + (...args) => { + try { + const result = cb(...args); + + if (result instanceof Promise) + return result.catch(eh) as T; + + return result; + } catch (e) { + return eh(e); + } + }; + + /** + * Run an unsafe function, handle any errors and return a Result. + * + * @param cb - The async function to call. + * @param [eh] - Optional fallback error handler. + * @returns The awaited return value of cb. + * + * @example + * const user = await n.fromUnsafe(() => db.findUser('some-user-id'), () => 'FAILED_T0_FIND_USER') + * if (!user.success) { + * console.error(user.error) + * } else { + * console.log(user.data) + * } + */ + const fromUnsafe = < + T extends D | Promise, + EH extends Result, + R = T extends Promise + ? Promise<{ success: true; data: U }> + : { success: true; data: T }, + >( + cb: () => T, + eh: (err: unknown) => EH, + ): R | EH => { + try { + const result = cb(); + + if (result instanceof Promise) + return result.then(ok).catch(eh) as R; + + return ok(result as D) as R; + } catch (e) { + return eh(e); + } + }; + + /** + * Convert a list of results into a single result. + * + * @param results - A list of Results. + * @returns A single result containing the data / errors of the input results. + * + * @example + * const findUserResults = userIds.map((userId) => + * n.fromUnsafe( + * () => db.findUser(userId), + * () => "FAILED_TO_FIND_USER" as const, + * ), + * ); + * + * const result = n.resultsToResult(findUserResults) + */ + const resultsToResult = ( + results: R, + ): Result[], ErrorOf[]> => { + const errors = results + .filter((result) => !result.success) + .map((result) => result.error as ErrorOf); + + if (errors.length) + return { + success: false, + error: errors, + }; + + const successes = results + .filter((result) => !!result.success) + .map((result) => result.data as DataOf); + + return { + success: true, + data: successes, + }; + }; + + return { + ok, + err, + safeFn, + fromUnsafe, + resultsToResult, + }; +}; + +export const n = createNeverpanic(); diff --git a/package.json b/package.json index 3a04b29..a95c18f 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,26 @@ { "name": "neverpanic", - "module": "index.ts", - "version": "0.0.5", - "type": "module", + "version": "1.0.0-beta.5", "repository": { "url": "https://github.com/bgrcs/neverpanic" }, - "scripts": { - "build": "tsc", - "test": "bun test" - }, - "files": ["dist"], + "files": [ + "dist" + ], + "type": "module", + "module": "index.ts", "exports": { ".": "./dist/index.js" }, + "scripts": { + "build": "tsc", + "test": "bun test", + "fmt": "oxfmt --check", + "fmt:fix": "oxfmt --write" + }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "oxfmt": "0.32.0" }, "peerDependencies": { "typescript": "5"