Effect -- 批处理(Batching)

批处理(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 调用次数,提升性能。

批处理分步指南

  1. 声明请求(Declaring Requests):将请求转换为结构化数据模型,明确输入参数、预期输出和可能的错误类型。这种结构化设计不仅便于高效管理数据,还能通过对比请求参数,识别可合并的重复请求。
  2. 声明解析器(Declaring Resolvers):解析器用于同时处理多个请求。借助请求的可对比性(确保指向相同输入参数),解析器可以一次性执行多个请求,最大化批处理的效用。
  3. 定义查询(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 直接应用于特定程序。只要启用了缓存,该程序发起的所有请求都会由这个自定义缓存管理:

注:文档中未提供直接应用缓存的完整代码示例,以上为基于前文逻辑的合理补充说明。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值