Skip to content

Commit

Permalink
Merge pull request #71 from janglad/performance
Browse files Browse the repository at this point in the history
Performance
  • Loading branch information
janglad authored Nov 22, 2024
2 parents a53a6e6 + cdb6966 commit fbe14b1
Show file tree
Hide file tree
Showing 19 changed files with 644 additions and 400 deletions.
6 changes: 6 additions & 0 deletions .changeset/good-lemons-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"safe-fn": minor
---

- About a ~10x performance increase for single `safe-fn` instances
- About a ~17x performance increase when testing a `safe-fn` with 10 parents
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const { execute, result, isPending } = useServerAction(createTodoAction);
```

However, the magic of the integration of NeverThrow really comes out if your other functions are also returning a `Result` or `ResultAsync`. Instead of passing a regular function via `handler()` you can also use `safeHandler()`.
This builds on Neverthrow's `safeTry()` and takes in a generator function. This generator function receives the same args as `handler()`, but it allows you to `yield *` other results (note that in `Neverthrow` versions before 8.0.0 you need to use `.safeUnwrap()`).
This builds on Neverthrow's `safeTry()` and takes in a generator function. This generator function receives the same args as `handler()`, but it allows you to `yield *` other results (note that in `Neverthrow` versions before 8.1.0 you need to use `.safeUnwrap()`).
This is meant to emulate Rust's `?` operator and allows a very ergonomic way to write "return early if this fails, otherwise continue" logic.

This function has the same return type as the `handler()` example above.
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/content/docs/create/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Tab, Tabs } from "fumadocs-ui/components/tabs";
## Installation

<Callout type="info">
Please note that due to an incompatibility in types, NeverThrow version 8 or
higher is required.
Please note that due to an incompatibility in types, NeverThrow version 8.1.0
or higher is required.
</Callout>

<Tabs items={["npm", "pnpm", "yarn", "bun"]}>
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const { execute, result, isPending } = useServerAction(createTodoAction);
```

However, the magic of the integration of NeverThrow really comes out if your other functions are also returning a `Result` or `ResultAsync`. Instead of passing a regular function via `handler()` you can also use `safeHandler()`.
This builds on Neverthrow's `safeTry()` and takes in a generator function. This generator function receives the same args as `handler()`, but it allows you to `yield *` other results (note that in `Neverthrow` versions before 8.0.0 you need to use `.safeUnwrap()`).
This builds on Neverthrow's `safeTry()` and takes in a generator function. This generator function receives the same args as `handler()`, but it allows you to `yield *` other results (note that in `Neverthrow` versions before 8.1.0 you need to use `.safeUnwrap()`).
This is meant to emulate Rust's `?` operator and allows a very ergonomic way to write "return early if this fails, otherwise continue" logic.

This function has the same return type as the `handler()` example above.
Expand Down
21 changes: 21 additions & 0 deletions packages/benchmark/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { safeFnAsyncBench } from "./safe-fn";
import { safeFnAsyncWithCallbacksBench } from "./safe-fn-callbacks";
import { safeFnAsyncGenBench } from "./safe-fn-gen";
import { zsaBench } from "./zsa";

const runBench = async () => {
await safeFnAsyncBench.run();
console.log(safeFnAsyncBench.name);
console.table(safeFnAsyncBench.table());
await safeFnAsyncGenBench.run();
console.log(safeFnAsyncGenBench.name);
console.table(safeFnAsyncGenBench.table());
await zsaBench.run();
console.log(zsaBench.name);
console.table(zsaBench.table());
await safeFnAsyncWithCallbacksBench.run();
console.log(safeFnAsyncWithCallbacksBench.name);
console.table(safeFnAsyncWithCallbacksBench.table());
};

runBench();
19 changes: 19 additions & 0 deletions packages/benchmark/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"private": true,
"version": "0.0.0",
"name": "@repo/benchmark",
"devDependencies": {
"@repo/typescript-config": "workspace:^",
"typescript": "^5.5.4"
},
"scripts": {
"bench": "bun run index.ts"
},
"dependencies": {
"neverthrow": "^8.0.0",
"safe-fn": "workspace:^",
"tinybench": "^3.0.6",
"zod": "^3.23.8",
"zsa": "^0.6.0"
}
}
60 changes: 60 additions & 0 deletions packages/benchmark/safe-fn-callbacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createSafeFn } from "safe-fn";
import { Bench } from "tinybench";

import { ok } from "neverthrow";
import { z } from "zod";

const schema = z.unknown();

const handlerFn = async (args: any) => ok(args);

const callbackFn = async (args: any) => {};

const rootSafeFn = createSafeFn()
.input(schema)
.output(schema)
.handler(handlerFn)
.onComplete(callbackFn)
.onError(callbackFn)
.onSuccess(callbackFn)
.onStart(callbackFn);

const addSafeFn = (root: any) => {
return createSafeFn()
.use(root)
.input(schema)
.output(schema)
.handler(handlerFn)
.onComplete(callbackFn)
.onError(callbackFn)
.onSuccess(callbackFn)
.onStart(callbackFn);
};

const getSafeFnWithNMiddlewares = (n: number) => {
let safeFn: any = rootSafeFn;
for (let i = 0; i < n; i++) {
safeFn = addSafeFn(safeFn);
}
return safeFn;
};

const with10 = getSafeFnWithNMiddlewares(10);
const with100 = getSafeFnWithNMiddlewares(100);
const with1000 = getSafeFnWithNMiddlewares(1000);

export const safeFnAsyncWithCallbacksBench = new Bench({
name: "Safe FN Middleware With Callbacks",
})
.add("with1", async () => {
const res = await rootSafeFn.run({});
})
.add("with10", async () => {
const res = await with10.run({});
})
.add("with100", async () => {
const res = await with100.run({});
})
.add("with1000", async () => {
const res = await with1000.run({});
});
51 changes: 51 additions & 0 deletions packages/benchmark/safe-fn-gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ok } from "neverthrow";
import { createSafeFn } from "safe-fn";
import { Bench } from "tinybench";
import { z } from "zod";

const schema = z.unknown();

const handlerFn = async function* (args: any) {
return ok(args);
};

const rootSafeFn = createSafeFn()
.input(schema)
.output(schema)
.safeHandler(handlerFn);

const addSafeFn = (root: any) => {
return createSafeFn()
.use(root)
.input(schema)
.output(schema)
.safeHandler(handlerFn);
};

const getSafeFnWithNMiddlewares = (n: number) => {
let safeFn: any = rootSafeFn;
for (let i = 0; i < n; i++) {
safeFn = addSafeFn(safeFn);
}
return safeFn;
};

const with10 = getSafeFnWithNMiddlewares(10);
const with100 = getSafeFnWithNMiddlewares(100);
const with1000 = getSafeFnWithNMiddlewares(1000);

export const safeFnAsyncGenBench = new Bench({
name: "Safe FN Async Gen Middleware",
})
.add("with1", async () => {
const res = await rootSafeFn.run({});
})
.add("with10", async () => {
const res = await with10.run({});
})
.add("with100", async () => {
const res = await with100.run({});
})
.add("with1000", async () => {
const res = await with1000.run({});
});
50 changes: 50 additions & 0 deletions packages/benchmark/safe-fn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createSafeFn } from "safe-fn";
import { Bench } from "tinybench";

import { ok } from "neverthrow";
import { z } from "zod";

const schema = z.unknown();

const handlerFn = async (args: any) => ok(args);

const rootSafeFn = createSafeFn()
.input(schema)
.output(schema)
.handler(handlerFn);

const addSafeFn = (root: any) => {
return createSafeFn()
.use(root)
.input(schema)
.output(schema)
.handler(handlerFn);
};

const getSafeFnWithNMiddlewares = (n: number) => {
let safeFn: any = rootSafeFn;
for (let i = 0; i < n; i++) {
safeFn = addSafeFn(safeFn);
}
return safeFn;
};

const with10 = getSafeFnWithNMiddlewares(10);
const with100 = getSafeFnWithNMiddlewares(100);
const with1000 = getSafeFnWithNMiddlewares(1000);
console.log("Done with this");
export const safeFnAsyncBench = new Bench({
name: "Safe FN Middleware",
})
.add("with1", async () => {
const res = await rootSafeFn.run({});
})
.add("with10", async () => {
const res = await with10.run({});
})
.add("with100", async () => {
const res = await with100.run({});
})
.add("with1000", async () => {
const res = await with1000.run({});
});
3 changes: 3 additions & 0 deletions packages/benchmark/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "@repo/typescript-config/base.json"
}
51 changes: 51 additions & 0 deletions packages/benchmark/zsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Bench } from "tinybench";
import { z } from "zod";

import { createServerAction, createServerActionProcedure } from "zsa";

const schema = z.unknown();

const rootProcedure = createServerActionProcedure()
.input(schema)
.output(schema)
.handler((e) => e);

const bareAction = createServerAction()
.input(schema)
.output(schema)
.handler((e) => e);

const getProcedureWithNMiddlewares = (n: number) => {
let procedure: any = rootProcedure;
for (let i = 1; i < n; i++) {
procedure = createServerActionProcedure(procedure)
.input(schema)
.output(schema)
.handler((e) => e);
}
return procedure
.createServerAction()
.input(schema)
.output(schema)
.handler((e) => e);
};

const with10 = getProcedureWithNMiddlewares(10);
const with100 = getProcedureWithNMiddlewares(100);
const with1000 = getProcedureWithNMiddlewares(1000);

export const zsaBench = new Bench({
name: "ZSA",
})
.add("with1", async () => {
const res = await bareAction({});
})
.add("with10", async () => {
const res = await with10({});
})
.add("with100", async () => {
const res = await with100({});
})
.add("with1000", async () => {
const res = await with1000({});
});
2 changes: 1 addition & 1 deletion packages/safe-fn-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const { execute, result, isPending } = useServerAction(createTodoAction);
```

However, the magic of the integration of NeverThrow really comes out if your other functions are also returning a `Result` or `ResultAsync`. Instead of passing a regular function via `handler()` you can also use `safeHandler()`.
This builds on Neverthrow's `safeTry()` and takes in a generator function. This generator function receives the same args as `handler()`, but it allows you to `yield *` other results (note that in `Neverthrow` versions before 8.0.0 you need to use `.safeUnwrap()`).
This builds on Neverthrow's `safeTry()` and takes in a generator function. This generator function receives the same args as `handler()`, but it allows you to `yield *` other results (note that in `Neverthrow` versions before 8.1.0 you need to use `.safeUnwrap()`).
This is meant to emulate Rust's `?` operator and allows a very ergonomic way to write "return early if this fails, otherwise continue" logic.

This function has the same return type as the `handler()` example above.
Expand Down
2 changes: 1 addition & 1 deletion packages/safe-fn-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@types/react": "^18.3.3",
"@typescript-eslint/parser": "^8.4.0",
"eslint": "^8.57.0",
"neverthrow": "^8.0.0",
"neverthrow": "^8.1.1",
"react": "^18.3.1",
"typescript": "^5.5.4"
},
Expand Down
Loading

0 comments on commit fbe14b1

Please sign in to comment.