fp-tsを使ったプロミスタスクのグルーピング化
こんにちわ。nap5です。
fp-tsを使ったプロミスタスクのグルーピング化の紹介です。
前回の記事と関連です。デモはほぼ同じです。
前回では並行処理を逐次的にchainしていたのに対し、今回はユーザー単位にプロミスタスクをまとめておき、このプロセスをタスク化して並行処理するといったデモになります。
こちらのほうがユーザー単位にグルーピングされていることが分かりやすくていい感じかと思います。
import { pipe } from "fp-ts/lib/function";
import seedrandom from "seedrandom";
import * as TE from "fp-ts/lib/TaskEither";
import * as T from "fp-ts/lib/Task";
import * as A from 'fp-ts/lib/Array'
const rng = seedrandom("fixed-seed");
interface ErrorFormat {
fn: string,
detail: Record<string, unknown>
}
interface CustomErrorOptions extends ErrorOptions {
cause?: ErrorFormat;
}
class CustomError extends Error {
cause?: ErrorFormat
constructor(message: string, options?: CustomErrorOptions) {
super(message, options);
this.cause = options?.cause
}
}
export class CreateUserError extends CustomError {
name = "CreateUserError" as const;
constructor(message: string, options?: CustomErrorOptions) {
super(message, options);
}
}
export class DeleteUserError extends CustomError {
name = "DeleteUserError" as const;
constructor(message: string, options?: CustomErrorOptions) {
super(message, options);
}
}
export class NotifyToUserError extends CustomError {
name = "NotifyToUserError" as const;
constructor(message: string, options?: CustomErrorOptions) {
super(message, options);
}
}
export type User = {
id: number;
name: string;
isDeleted?: boolean;
};
const createUser = (
formData: User
): TE.TaskEither<CreateUserError, User> =>
rng() < 0.1
? TE.left(
new CreateUserError("Failed createUser.", { cause: { fn: "createUser", detail: formData } })
)
: TE.right({ ...formData, isDeleted: rng() > 0.8 });
const deleteUser = (formData: User): TE.TaskEither<DeleteUserError, User> =>
!formData.isDeleted
? TE.right({ ...formData, isDeleted: !formData.isDeleted })
: TE.left(new DeleteUserError("Already deleted.", { cause: { fn: "deleteUser", detail: formData } }));
const notifyToUser = (formData: User): TE.TaskEither<NotifyToUserError, User> =>
rng() < 0.8
? TE.right({ ...formData })
: TE.left(new NotifyToUserError("Failed notification.", { cause: { fn: "notifyToUser", detail: formData } }));
type RepositoryError = CreateUserError | NotifyToUserError | DeleteUserError
type ProcessResult = {
user: User;
error?: RepositoryError;
};
const processUser = (user: User): T.Task<ProcessResult> => {
return pipe(
createUser(user),
TE.chainW(deleteUser),
TE.chainW(notifyToUser),
TE.fold(
(error) => T.of({ user, error }),
(successfulUser) => T.of({ user: successfulUser })
)
);
};
export const demo = (data: User[]): T.Task<ProcessResult[]> => {
return A.sequence(T.ApplicativePar)(data.map(processUser));
};
使用側で以下のようにハンドリングします。
import { test, expect, describe } from "vitest";
import { User, demo } from ".";
describe("demo", () => {
test("Nice test", async () => {
const data: User[] = [
{ id: 1, name: "User1" },
{ id: 2, name: "User2" },
{ id: 3, name: "User3" },
{ id: 4, name: "User4" },
{ id: 5, name: "User5" },
];
const results = await demo(data)();
const errors = results.filter((d) => !!d.error);
const values = results.filter((d) => !d.error);
expect({
summary: {
processCount: results.length,
okCount: values.length,
errCount: errors.length,
},
detail: {
input: data,
values,
errors: errors
.map((d) => d.error)
.map((d) => ({
name: d?.name,
data: d?.cause?.detail,
fn: d?.cause?.fn,
message: d?.message,
})),
},
}).toStrictEqual({
summary: {
processCount: 5,
okCount: 2,
errCount: 3,
},
detail: {
input: [
{
id: 1,
name: "User1",
},
{
id: 2,
name: "User2",
},
{
id: 3,
name: "User3",
},
{
id: 4,
name: "User4",
},
{
id: 5,
name: "User5",
},
],
values: [
{
user: {
id: 4,
name: "User4",
isDeleted: true,
},
},
{
user: {
id: 5,
name: "User5",
isDeleted: true,
},
},
],
errors: [
{
name: "DeleteUserError",
data: {
id: 1,
name: "User1",
isDeleted: true,
},
fn: "deleteUser",
message: "Already deleted.",
},
{
name: "CreateUserError",
data: {
id: 2,
name: "User2",
},
fn: "createUser",
message: "Failed createUser.",
},
{
name: "NotifyToUserError",
data: {
id: 3,
name: "User3",
isDeleted: true,
},
fn: "notifyToUser",
message: "Failed notification.",
},
],
},
});
});
});
最後にデモコードになります。
簡単ですが、以上です。