批处理(Batching)
在典型的应用开发中,当与外部 API、数据库或其他数据源交互时,我们通常会定义执行请求并相应处理结果或失败的函数。
简单模型设置
以下是一个基本模型,概述了数据结构和可能的错误类型:
import { Data } from "effect"
// ------------------------------
// 模型(Model)
// ------------------------------
interface User {
readonly _tag: "User"
readonly id: number
readonly name: string
readonly email: string
}
class GetUserError extends Data.TaggedError("GetUserError")<{}> {}
interface Todo {
readonly _tag: "Todo"
readonly id: number
readonly message: string
readonly ownerId: number
}
class GetTodosError extends Data.TaggedError("GetTodosError")<{}> {}
class SendEmailError extends Data.TaggedError("SendEmailError")<{}> {}
定义 API 函数
我们来定义与外部 API 交互的函数,处理获取待办事项、查询用户详情和发送邮件等常见操作:
import { Effect, Data } from "effect"
// ------------------------------
// 模型(Model)
// ------------------------------
// (此处省略 19 行代码,与上文模型定义一致)
interface User {
readonly _tag: "User"
readonly id: number
readonly name: string
readonly email: string
}
class GetUserError extends Data.TaggedError("GetUserError")<{}> {}
interface Todo {
readonly _tag: "Todo"
readonly id: number
readonly message: string
readonly ownerId: number
}
class GetTodosError extends Data.TaggedError("GetTodosError")<{}> {}
class SendEmailError extends Data.TaggedError("SendEmailError")<{}> {}
// ------------------------------
// API
// ------------------------------
// 从外部 API 获取待办事项列表
const getTodos = Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/todos").then(
(res) => res.json() as Promise<Array<Todo>>
),
catch: () => new GetTodosError()
})
// 通过 ID 从外部 API 获取用户
const getUserById = (id: number) =>
Effect.tryPromise({
try: () =>
fetch(`https://api.example.demo/getUserById?id=${id}`).then(
(res) => res.json() as Promise<User>
),
catch: () => new GetUserError()
})
// 通过外部 API 发送邮件
const sendEmail = (address: string, text: string) =>
Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/sendEmail", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ address, text })
}).then((res) => res.json() as Promise<void>),
catch: () => new SendEmailError()
})
// 先获取用户详情,再向该用户发送邮件
const sendEmailToUser = (id: number, message: string) =>
getUserById(id).pipe(
Effect.andThen((user) => sendEmail(user.email, message))
)
// 向待办事项的所有者发送邮件通知
const notifyOwner = (todo: Todo) =>
getUserById(todo.ownerId).pipe(
Effect.andThen((user) =>
sendEmailToUser(user.id, `嘿 ${user.name},你有新的待办事项!`)
)
)
验证 API 响应
在实际场景中,你可能无法完全信任 API 始终返回符合预期的数据。为此,你可以使用 effect/Schema 或类似的替代方案(如 zod)。
这种方法虽然简单直观、易于阅读,但可能并非最高效。特别是当多个待办事项属于同一所有者时,重复的 API 调用会显著增加网络开销,降低应用性能。
使用 API 函数
这些函数虽然清晰易懂,但使用方式可能不够高效。例如,通知待办事项所有者的过程中会产生重复 API 调用,这一问题可以通过优化解决:
import { Effect, Data } from "effect"
// ------------------------------
// 模型(Model)
// ------------------------------
// (此处省略 19 行代码,与上文模型定义一致)
interface User {
readonly _tag: "User"
readonly id: number
readonly name: string
readonly email: string
}
class GetUserError extends Data.TaggedError("GetUserError")<{}> {}
interface Todo {
readonly _tag: "Todo"
readonly id: number
readonly message: string
readonly ownerId: number
}
class GetTodosError extends Data.TaggedError("GetTodosError")<{}> {}
class SendEmailError extends Data.TaggedError("SendEmailError")<{}> {}
// ------------------------------
// API
// ------------------------------
// (此处省略 46 行代码,与上文 API 函数定义一致)
// 从外部 API 获取待办事项列表
const getTodos = Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/todos").then(
(res) => res.json() as Promise<Array<Todo>>
),
catch: () => new GetTodosError()
})
// 通过 ID 从外部 API 获取用户
const getUserById = (id: number) =>
Effect.tryPromise({
try: () =>
fetch(`https://api.example.demo/getUserById?id=${id}`).then(
(res) => res.json() as Promise<User>
),
catch: () => new GetUserError()
})
// 通过外部 API 发送邮件
const sendEmail = (address: string, text: string) =>
Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/sendEmail", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ address, text })
}).then((res) => res.json() as Promise<void>),
catch: () => new SendEmailError()
})
// 先获取用户详情,再向该用户发送邮件
const sendEmailToUser = (id: number, message: string) =>
getUserById(id).pipe(
Effect.andThen((user) => sendEmail(user.email, message))
)
// 向待办事项的所有者发送邮件通知
const notifyOwner = (todo: Todo) =>
getUserById(todo.ownerId).pipe(
Effect.andThen((user) =>
sendEmailToUser(user.id, `嘿 ${user.name},你有新的待办事项!`)
)
)
// 协调待办事项相关操作,通知所有所有者
const program = Effect.gen(function* () {
const todos = yield* getTodos
yield* Effect.forEach(todos, (todo) => notifyOwner(todo), {
concurrency: "unbounded" // 无限制并发
})
})
该实现会为每个待办事项执行一次 API 调用,以获取所有者详情并发送邮件。如果多个待办事项属于同一所有者,就会产生冗余的 API 调用。
通过批处理调用提升效率
如果后端支持批处理 API,我们可以通过批处理优化来减少 HTTP 请求数量。将多个操作合并为一个请求,既能提升性能,又能降低服务器负载。
假设 getUserById 和 sendEmail 支持批处理,我们可以在单次 HTTP 调用中处理多个请求,从而减少 API 调用次数,提升性能。
批处理分步指南
- 声明请求(Declaring Requests):将请求转换为结构化数据模型,明确输入参数、预期输出和可能的错误类型。这种结构化设计不仅便于高效管理数据,还能通过对比请求参数,识别可合并的重复请求。
- 声明解析器(Declaring Resolvers):解析器用于同时处理多个请求。借助请求的可对比性(确保指向相同输入参数),解析器可以一次性执行多个请求,最大化批处理的效用。
- 定义查询(Defining Queries):将结构化请求与对应的解析器绑定,形成可直接在应用中使用的功能组件。
确保请求的可对比性
请求的模型设计必须支持可对比性。通过实现对比逻辑(如使用 Equals.equals 方法),可以有效识别并批处理相同的请求。
声明请求(Declaring Requests)
我们基于 Request 概念设计模型,用于表示数据源支持的请求类型:
Request<Value, Error>
Request 是一种数据结构,表示对类型为 Value 的数据的请求,该请求可能因 Error 类型的错误而失败。
下面为数据源支持的请求类型定义结构化模型:
import { Request, Data } from "effect"
// ------------------------------
// 模型(Model)
// ------------------------------
// (此处省略 19 行代码,与上文模型定义一致)
interface User {
readonly _tag: "User"
readonly id: number
readonly name: string
readonly email: string
}
class GetUserError extends Data.TaggedError("GetUserError")<{}> {}
interface Todo {
readonly _tag: "Todo"
readonly id: number
readonly message: string
readonly ownerId: number
}
class GetTodosError extends Data.TaggedError("GetTodosError")<{}> {}
class SendEmailError extends Data.TaggedError("SendEmailError")<{}> {}
// ------------------------------
// 请求(Requests)
// ------------------------------
// 定义获取多个待办事项的请求,可能因 GetTodosError 失败
interface GetTodos extends Request.Request<Array<Todo>, GetTodosError> {
readonly _tag: "GetTodos"
}
// 为 GetTodos 请求创建带标签的构造函数
const GetTodos = Request.tagged<GetTodos>("GetTodos")
// 定义通过 ID 获取用户的请求,可能因 GetUserError 失败
interface GetUserById extends Request.Request<User, GetUserError> {
readonly _tag: "GetUserById"
readonly id: number
}
// 为 GetUserById 请求创建带标签的构造函数
const GetUserById = Request.tagged<GetUserById>("GetUserById")
// 定义发送邮件的请求,可能因 SendEmailError 失败
interface SendEmail extends Request.Request<void, SendEmailError> {
readonly _tag: "SendEmail"
readonly address: string
readonly text: string
}
// 为 SendEmail 请求创建带标签的构造函数
const SendEmail = Request.tagged<SendEmail>("SendEmail")
每个请求都基于通用的 Request 类型扩展,形成特定的数据结构,既包含独特的数据需求,也指定了对应的错误类型。
通过 Request.tagged 等带标签的构造函数,可轻松创建在应用中易于识别和管理的请求对象。
声明解析器(Declaring Resolvers)
定义请求后,下一步是配置 Effect 如何使用 RequestResolver 解析这些请求:
RequestResolver<A, R>
RequestResolver 需要环境 R,并能执行类型为 A 的请求。
我们为每种请求类型创建独立的解析器。解析器的粒度可灵活调整,通常需根据对应 API 的批处理能力来划分:
import { Effect, Request, RequestResolver, Data } from "effect"
// ------------------------------
// 模型(Model)
// ------------------------------
// (此处省略 19 行代码,与上文模型定义一致)
interface User {
readonly _tag: "User"
readonly id: number
readonly name: string
readonly email: string
}
class GetUserError extends Data.TaggedError("GetUserError")<{}> {}
interface Todo {
readonly _tag: "Todo"
readonly id: number
readonly message: string
readonly ownerId: number
}
class GetTodosError extends Data.TaggedError("GetTodosError")<{}> {}
class SendEmailError extends Data.TaggedError("SendEmailError")<{}> {}
// ------------------------------
// 请求(Requests)
// ------------------------------
// (此处省略 29 行代码,与上文请求定义一致)
// 定义获取多个待办事项的请求,可能因 GetTodosError 失败
interface GetTodos extends Request.Request<Array<Todo>, GetTodosError> {
readonly _tag: "GetTodos"
}
// 为 GetTodos 请求创建带标签的构造函数
const GetTodos = Request.tagged<GetTodos>("GetTodos")
// 定义通过 ID 获取用户的请求,可能因 GetUserError 失败
interface GetUserById extends Request.Request<User, GetUserError> {
readonly _tag: "GetUserById"
readonly id: number
}
// 为 GetUserById 请求创建带标签的构造函数
const GetUserById = Request.tagged<GetUserById>("GetUserById")
// 定义发送邮件的请求,可能因 SendEmailError 失败
interface SendEmail extends Request.Request<void, SendEmailError> {
readonly _tag: "SendEmail"
readonly address: string
readonly text: string
}
// 为 SendEmail 请求创建带标签的构造函数
const SendEmail = Request.tagged<SendEmail>("SendEmail")
// ------------------------------
// 解析器(Resolvers)
// ------------------------------
// 假设 GetTodos 不支持批处理,创建标准解析器
const GetTodosResolver = RequestResolver.fromEffect(
(_: GetTodos): Effect.Effect<Todo[], GetTodosError> =>
Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/todos").then(
(res) => res.json() as Promise<Array<Todo>>
),
catch: () => new GetTodosError()
})
)
// 假设 GetUserById 支持批处理,创建批处理解析器
const GetUserByIdResolver = RequestResolver.makeBatched(
(requests: ReadonlyArray<GetUserById>) =>
Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/getUserByIdBatch", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
users: requests.map(({ id }) => ({ id }))
})
}).then((res) => res.json()) as Promise<Array<User>>,
catch: () => new GetUserError()
}).pipe(
Effect.andThen((users) =>
Effect.forEach(requests, (request, index) =>
Request.completeEffect(request, Effect.succeed(users[index]!))
)
),
Effect.catchAll((error) =>
Effect.forEach(requests, (request) =>
Request.completeEffect(request, Effect.fail(error))
)
)
)
)
// 假设 SendEmail 支持批处理,创建批处理解析器
const SendEmailResolver = RequestResolver.makeBatched(
(requests: ReadonlyArray<SendEmail>) =>
Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/sendEmailBatch", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
emails: requests.map(({ address, text }) => ({
address,
text
}))
})
}).then((res) => res.json() as Promise<void>),
catch: () => new SendEmailError()
}).pipe(
Effect.andThen(
Effect.forEach(requests, (request) =>
Request.completeEffect(request, Effect.void)
)
),
Effect.catchAll((error) =>
Effect.forEach(requests, (request) =>
Request.completeEffect(request, Effect.fail(error))
)
)
)
)
解析器中的上下文访问
解析器可以像其他 Effect 一样访问上下文,且有多种创建解析器的方式。如需了解更多细节,可参考 RequestResolver 模块的参考文档。
本配置中:
GetTodosResolver用于获取多个Todo项,因不支持批处理,被设置为标准解析器。GetUserByIdResolver和SendEmailResolver被配置为批处理解析器,基于它们支持批处理的假设,以提升性能并减少 API 调用次数。
定义查询(Defining Queries)
完成解析器配置后,我们将所有组件整合,定义可在应用中有效执行数据操作的查询:
import { Effect, Request, RequestResolver, Data } from "effect"
// ------------------------------
// 模型(Model)
// ------------------------------
// (此处省略 19 行代码,与上文模型定义一致)
interface User {
readonly _tag: "User"
readonly id: number
readonly name: string
readonly email: string
}
class GetUserError extends Data.TaggedError("GetUserError")<{}> {}
interface Todo {
readonly _tag: "Todo"
readonly id: number
readonly message: string
readonly ownerId: number
}
class GetTodosError extends Data.TaggedError("GetTodosError")<{}> {}
class SendEmailError extends Data.TaggedError("SendEmailError")<{}> {}
// ------------------------------
// 请求(Requests)
// ------------------------------
// (此处省略 29 行代码,与上文请求定义一致)
// 定义获取多个待办事项的请求,可能因 GetTodosError 失败
interface GetTodos extends Request.Request<Array<Todo>, GetTodosError> {
readonly _tag: "GetTodos"
}
// 为 GetTodos 请求创建带标签的构造函数
const GetTodos = Request.tagged<GetTodos>("GetTodos")
// 定义通过 ID 获取用户的请求,可能因 GetUserError 失败
interface GetUserById extends Request.Request<User, GetUserError> {
readonly _tag: "GetUserById"
readonly id: number
}
// 为 GetUserById 请求创建带标签的构造函数
const GetUserById = Request.tagged<GetUserById>("GetUserById")
// 定义发送邮件的请求,可能因 SendEmailError 失败
interface SendEmail extends Request.Request<void, SendEmailError> {
readonly _tag: "SendEmail"
readonly address: string
readonly text: string
}
// 为 SendEmail 请求创建带标签的构造函数
const SendEmail = Request.tagged<SendEmail>("SendEmail")
// ------------------------------
// 解析器(Resolvers)
// ------------------------------
// (此处省略 72 行代码,与上文解析器定义一致)
// 假设 GetTodos 不支持批处理,创建标准解析器
const GetTodosResolver = RequestResolver.fromEffect(
(_: GetTodos): Effect.Effect<Todo[], GetTodosError> =>
Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/todos").then(
(res) => res.json() as Promise<Array<Todo>>
),
catch: () => new GetTodosError()
})
)
// 假设 GetUserById 支持批处理,创建批处理解析器
const GetUserByIdResolver = RequestResolver.makeBatched(
(requests: ReadonlyArray<GetUserById>) =>
Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/getUserByIdBatch", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
users: requests.map(({ id }) => ({ id }))
})
}).then((res) => res.json()) as Promise<Array<User>>,
catch: () => new GetUserError()
}).pipe(
Effect.andThen((users) =>
Effect.forEach(requests, (request, index) =>
Request.completeEffect(request, Effect.succeed(users[index]!))
)
),
Effect.catchAll((error) =>
Effect.forEach(requests, (request) =>
Request.completeEffect(request, Effect.fail(error))
)
)
)
)
// 假设 SendEmail 支持批处理,创建批处理解析器
const SendEmailResolver = RequestResolver.makeBatched(
(requests: ReadonlyArray<SendEmail>) =>
Effect.tryPromise({
try: () =>
fetch("https://api.example.demo/sendEmailBatch", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
emails: requests.map(({ address, text }) => ({
address,
text
}))
})
}).then((res) => res.json() as Promise<void>),
catch: () => new SendEmailError()
}).pipe(
Effect.andThen(
Effect.forEach(requests, (request) =>
Request.completeEffect(request, Effect.void)
)
),
Effect.catchAll((error) =>
Effect.forEach(requests, (request) =>
Request.completeEffect(request, Effect.fail(error))
)
)
)
)
// ------------------------------
// 查询(Queries)
// ------------------------------
// 定义获取所有待办事项的查询
const getTodos: Effect.Effect<
Array<Todo>,
GetTodosError
> = Effect.request(GetTodos({}), GetTodosResolver)
// 定义通过 ID 获取用户的查询
const getUserById = (id: number) =>
Effect.request(GetUserById({ id }), GetUserByIdResolver)
// 定义向指定地址发送邮件的查询
const sendEmail = (address: string, text: string) =>
Effect.request(SendEmail({ address, text }), SendEmailResolver)
// 组合 getUserById 和 sendEmail,向指定用户发送邮件
const sendEmailToUser = (id: number, message: string) =>
getUserById(id).pipe(
Effect.andThen((user) => sendEmail(user.email, message))
)
// 通过 getUserById 获取待办事项所有者,然后发送邮件通知
const notifyOwner = (todo: Todo) =>
getUserById(todo.ownerId).pipe(
Effect.andThen((user) =>
sendEmailToUser(user.id, `嘿 ${user.name},你有新的待办事项!`)
)
)
通过 Effect.request 函数,我们将解析器与请求模型有效整合,确保每个查询都能通过合适的解析器得到最优解析。
尽管代码结构与之前的示例相似,但解析器的使用大幅提升了效率,优化了请求处理方式,减少了不必要的 API 调用。
const program = Effect.gen(function* () {
const todos = yield* getTodos
yield* Effect.forEach(todos, (todo) => notifyOwner(todo), {
batching: true // 启用批处理
})
})
在最终配置中,无论待办事项数量多少,该程序都只会执行 3 次 API 查询。相比传统方式(可能执行 1 + 2n 次查询,其中 n 为待办事项数量),效率得到了显著提升,尤其适用于数据交互频繁的应用。
禁用批处理
可通过 Effect.withRequestBatching 工具在局部禁用批处理,示例如下:
const program = Effect.gen(function* () {
const todos = yield* getTodos
yield* Effect.forEach(todos, (todo) => notifyOwner(todo), {
concurrency: "unbounded" // 无限制并发
})
}).pipe(Effect.withRequestBatching(false)) // 禁用批处理
带上下文的解析器(Resolvers with Context)
在复杂应用中,解析器通常需要访问共享服务或配置才能有效处理请求。但如何在保持批处理能力的同时提供必要的上下文,是一个不小的挑战。下面我们将探讨如何在解析器中管理上下文,确保批处理功能不受影响。
创建请求解析器时,必须谨慎管理上下文。提供过多上下文或为解析器提供不同的服务,都可能导致它们无法被批处理。为避免此类问题,Effect.request 中使用的解析器上下文被显式设置为 never,这会强制开发者明确定义解析器中上下文的访问和使用方式。
以下示例展示了如何设置 HTTP 服务,供解析器执行 API 调用:
import { Effect, Context, RequestResolver, Request, Data } from "effect"
// ------------------------------
// Model
// ------------------------------
interface User {
readonly _tag: "User"
readonly id: number
readonly name: string
readonly email: string
}
class GetUserError extends Data.TaggedError("GetUserError")<{}> {}
interface Todo {
readonly _tag: "Todo"
readonly id: number
readonly message: string
readonly ownerId: number
}
class GetTodosError extends Data.TaggedError("GetTodosError")<{}> {}
class SendEmailError extends Data.TaggedError("SendEmailError")<{}> {}
// ------------------------------
// Requests
// ------------------------------
// Define a request to get multiple Todo items which might
// fail with a GetTodosError
interface GetTodos extends Request.Request<Array<Todo>, GetTodosError> {
readonly _tag: "GetTodos"
}
// Create a tagged constructor for GetTodos requests
const GetTodos = Request.tagged<GetTodos>("GetTodos")
// Define a request to fetch a User by ID which might
// fail with a GetUserError
interface GetUserById extends Request.Request<User, GetUserError> {
readonly _tag: "GetUserById"
readonly id: number
}
// Create a tagged constructor for GetUserById requests
const GetUserById = Request.tagged<GetUserById>("GetUserById")
// Define a request to send an email which might
// fail with a SendEmailError
interface SendEmail extends Request.Request<void, SendEmailError> {
readonly _tag: "SendEmail"
readonly address: string
readonly text: string
}
// Create a tagged constructor for SendEmail requests
const SendEmail = Request.tagged<SendEmail>("SendEmail")
// ------------------------------
// Resolvers With Context
// ------------------------------
class HttpService extends Context.Tag("HttpService")<
HttpService,
{ fetch: typeof fetch }
>() {}
const GetTodosResolver =
// we create a normal resolver like we did before
RequestResolver.fromEffect((_: GetTodos) =>
Effect.andThen(HttpService, (http) =>
Effect.tryPromise({
try: () =>
http
.fetch("https://api.example.demo/todos")
.then((res) => res.json() as Promise<Array<Todo>>),
catch: () => new GetTodosError()
})
)
).pipe(
// we list the tags that the resolver can access
RequestResolver.contextFromServices(HttpService)
)
带上下文的解析器与缓存
现在我们可以看到,GetTodosResolver 的类型不再是 RequestResolver,而是:
const GetTodosResolver: Effect<
RequestResolver<GetTodos, never>,
never,
HttpService
>
它是一个访问 HttpService 的 Effect,返回一个组合解析器,该解析器已具备可直接使用的最小上下文。
有了这样的 Effect 后,我们可以直接在查询定义中使用它:
const getTodos: Effect.Effect<Todo[], GetTodosError, HttpService> =
Effect.request(GetTodos({}), GetTodosResolver)
可以看到,该 Effect 正确要求提供 HttpService。
另外,你也可以将 RequestResolver 作为 Layer 的一部分创建,在构造时直接访问或封装上下文。
示例(Example)
import {
Effect,
Context,
RequestResolver,
Request,
Layer,
Data
} from "effect"
// ------------------------------
// Model
// ------------------------------
interface User {
readonly _tag: "User"
readonly id: number
readonly name: string
readonly email: string
}
class GetUserError extends Data.TaggedError("GetUserError")<{}> {}
interface Todo {
readonly _tag: "Todo"
readonly id: number
readonly message: string
readonly ownerId: number
}
class GetTodosError extends Data.TaggedError("GetTodosError")<{}> {}
class SendEmailError extends Data.TaggedError("SendEmailError")<{}> {}
// ------------------------------
// Requests
// ------------------------------
// Define a request to get multiple Todo items which might
// fail with a GetTodosError
interface GetTodos extends Request.Request<Array<Todo>, GetTodosError> {
readonly _tag: "GetTodos"
}
// Create a tagged constructor for GetTodos requests
const GetTodos = Request.tagged<GetTodos>("GetTodos")
// Define a request to fetch a User by ID which might
// fail with a GetUserError
interface GetUserById extends Request.Request<User, GetUserError> {
readonly _tag: "GetUserById"
readonly id: number
}
// Create a tagged constructor for GetUserById requests
const GetUserById = Request.tagged<GetUserById>("GetUserById")
// Define a request to send an email which might
// fail with a SendEmailError
interface SendEmail extends Request.Request<void, SendEmailError> {
readonly _tag: "SendEmail"
readonly address: string
readonly text: string
}
// Create a tagged constructor for SendEmail requests
const SendEmail = Request.tagged<SendEmail>("SendEmail")
// ------------------------------
// Resolvers With Context
// ------------------------------
class HttpService extends Context.Tag("HttpService")<
HttpService,
{ fetch: typeof fetch }
>() {}
const GetTodosResolver =
// we create a normal resolver like we did before
RequestResolver.fromEffect((_: GetTodos) =>
Effect.andThen(HttpService, (http) =>
Effect.tryPromise({
try: () =>
http
.fetch("https://api.example.demo/todos")
.then((res) => res.json() as Promise<Array<Todo>>),
catch: () => new GetTodosError()
})
)
).pipe(
// we list the tags that the resolver can access
RequestResolver.contextFromServices(HttpService)
)
// ------------------------------
// Layers
// ------------------------------
class TodosService extends Context.Tag("TodosService")<
TodosService,
{
getTodos: Effect.Effect<Array<Todo>, GetTodosError>
}
>() {}
const TodosServiceLive = Layer.effect(
TodosService,
Effect.gen(function* () {
const http = yield* HttpService
const resolver = RequestResolver.fromEffect((_: GetTodos) =>
Effect.tryPromise({
try: () =>
http
.fetch("https://api.example.demo/todos")
.then<any, Todo[]>((res) => res.json()),
catch: () => new GetTodosError()
})
)
return {
getTodos: Effect.request(GetTodos({}), resolver)
}
})
)
const getTodos: Effect.Effect<
Array<Todo>,
GetTodosError,
TodosService
> = Effect.andThen(TodosService, (service) => service.getTodos)
考虑到 Layer 是连接服务的天然基础组件,这种方式可能适用于大多数场景。
缓存(Caching)
虽然我们已经通过批处理大幅优化了请求,但还有一个可以提升应用效率的方向:缓存。如果没有缓存,即使经过批处理优化,相同的请求仍可能被多次执行,导致不必要的数据获取。
在 Effect 库中,缓存通过内置工具实现,允许请求结果被临时存储,避免重复获取未变更的数据。这一特性对于减轻服务器和网络负载至关重要,尤其适用于频繁发起相似请求的应用。
以下是为 getUserById 查询实现缓存的示例:
const getUserById = (id: number) =>
Effect.request(GetUserById({ id }), GetUserByIdResolver).pipe(
Effect.withRequestCaching(true) // 启用请求缓存
)
最终程序(Final Program)
假设所有组件都已正确配置:
const program = Effect.gen(function* () {
const todos = yield* getTodos
yield* Effect.forEach(todos, (todo) => notifyOwner(todo), {
concurrency: "unbounded" // 无限制并发
})
}).pipe(Effect.repeat(Schedule.fixed("10 seconds"))) // 每 10 秒重复一次
该程序中,getTodos 操作会获取所有待办事项。随后,Effect.forEach 会并发通知每个待办事项的所有者,无需等待前一个通知完成。
repeat 函数应用于整个操作链,通过固定调度确保程序每 10 秒重复一次。这意味着获取待办事项、发送通知的完整流程会以 10 秒为间隔重复执行。
程序集成了缓存机制,默认情况下可防止相同的 GetUserById 操作在 1 分钟内重复执行。这一特性有助于优化程序执行效率,减少不必要的用户数据获取请求。
此外,程序支持邮件批处理发送,能够高效利用资源,提升处理性能。
自定义请求缓存(Customizing Request Caching)
在实际应用中,合理的缓存策略能通过减少冗余数据获取显著提升性能。Effect 库提供了灵活的缓存机制,可针对应用的特定部分定制,或全局应用。
不同应用场景可能有独特的缓存需求:部分模块可能适合局部缓存,而其他模块可能需要全局缓存。以下将介绍如何配置自定义缓存以满足这些多样化需求。
创建自定义缓存(Creating a Custom Cache)
以下示例展示了如何创建自定义缓存并应用于应用的部分逻辑。该缓存会每 10 秒重复一次任务,并为请求设置容量和存活时间(TTL)等特定参数:
const program = Effect.gen(function* () {
const todos = yield* getTodos
yield* Effect.forEach(todos, (todo) => notifyOwner(todo), {
concurrency: "unbounded"
})
}).pipe(
Effect.repeat(Schedule.fixed("10 seconds")), // 每 10 秒重复
Effect.provide(
Layer.setRequestCache(
Request.makeCache({ capacity: 256, timeToLive: "60 minutes" }) // 缓存容量 256,存活时间 60 分钟
)
)
)
直接应用缓存(Direct Cache Application)
你也可以使用 Request.makeCache 构造缓存,并通过 Effect.withRequestCache 直接应用于特定程序。只要启用了缓存,该程序发起的所有请求都会由这个自定义缓存管理:
注:文档中未提供直接应用缓存的完整代码示例,以上为基于前文逻辑的合理补充说明。
1223

被折叠的 条评论
为什么被折叠?



