iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0

這篇我們要來看 Effect 中如何做到 batch request ,但為什麼我們會需要 batch request 呢?我之前正好有寫過類似的文章,有興趣的話可以去看看,這邊簡單的說一下,在平常寫程式中,我們可能會寫出像這樣的程式碼

function fetchData(id: number): Effect.Effect<unknown> {
  // 取得 data 的邏輯
}

function processData(data: unknown) {
  // 處理資料
}

const listOfDataID = [1, 3, 4, 7];

pipe(
  listOfDataID.map((id) =>
    pipe(
      fetchData(id),
      Effect.map((data) => processData(data))
    )
  ),
  Effect.allWith({ concurrency: "unbounded" })
);

像這樣,取得某個列表中的每個元素的詳細資料,但有時候我們都忘了, fetchData 需要呼叫後端的 API 耶,如果資料一多是不是會因此送出太多的 request 而造成後端的負擔呢?這時我們可能會開始善用之前學到的限制一下 concurrency 數,但若後端可以配合的話,我們是不是可以只用一個 request 就查到我們需要的資料並請後端回傳呢?

function batchFetchDatum(id: number[]) {
  // 這邊使用某種特別的 API ,可以傳入一組 id ,並且一次回傳所有資料
}

pipe(
  batchFetchDatum(listOfDataID),
  // 但這邊我們也要跟著配合調整我們是如何處理資料的
  Effect.map((datum) => datum.map((data) => processData(data)))
);

但有時候,我們的資料處理可能不是那邊容易就可以改寫的,例如裡面可能還牽扯到了其它的非同步的操作等等,如果有什麼神奇的方法讓我們可以像是一個一個各別取得,但又同時可以在背後自動轉換成 batch request 的 API 該有多好,這時候就是 Effect 的 RequestRequestResolver 登場的時候了

什麼是 RequestRequestResolver

在 Effect 中的 Request 是代表你需要跟某個外部的資源互動,並且這個互動會交由 RequestResolver 來完成

https://ithelp.ithome.com.tw/upload/images/20251003/201118027d9DrUzJn4.png

這跟自己取得資料不同的地方在於,你將這個請求交給的 Effect 的 runtime 執行,這使得 Effect 的 runtime 有機會在平行處理你的請求時,幫你調度具有 batch request 能力的 resolver 來處理 request ,這樣說可能有點抽像,我們實際看個例子吧

定義 Request

在開始前,我們要先來定義我們的 request

// 假設我們的資料長成這樣
interface Data {
  id: number
  content: string
}

// 這是我們的 request ,三個 type 參數分別代表:成功回傳的資料,失敗時的 error ,輸入的參數
class GetDataRequest extends Request.TaggedClass('data')<Data, never, {id: number}> {}

像這樣,我們就定義好了我們的請求,接下來就是要來定義 resolver

定義一般的 resolver

這邊我們先來定義一個一般的 resolver ,並且等下我們可以來實驗看看一般的 resolver 跟 batch resolver 的差別

const GetDataResolver = RequestResolver.fromEffect((request: GetDataRequest) => {
  console.log("GetDataResolver", request)
  
  // 這邊可以回傳 Effect ,假裝這就是我們取的資料的邏輯
  return Effect.succeed({
    id: request.id,
    content: `Data ${request.id}`
  })
})

像這樣我們就定義好了一個 resolver ,再來我們來定義一個 batch 的 resolver

定義 batch resolver

定義一個 batch resolver 會比較複雜,因為它接收的是 Effect runtime 收集到的多個請求(以 Array.NonEmptyArray 形式,保證至少有一個元素,這是 makeBatched 的特性)。你需要自己處理這些批次請求,並將結果正確地對應回每個原始的 request

// 這邊的參數是 Effect 幫你收集來的 request
// Array.NonEmptyArray 是 Effect 中定義的,保證至少有一個元素的 array
const BatchGetDataResolver = RequestResolver.makeBatched((requests: Array.NonEmptyArray<GetDataRequest>) => {
  console.log('BatchGetDataResolver', requests)
  return Effect.forEach(requests, (request) =>
    // 透過這個 function 可以完成對應的 request
    Request.completeEffect(
      request,
      Effect.succeed({
        id: request.id,
        content: `Data ${request.id}`
    })
  ))
})

組合 Request 與 RequestResolver

看完上面的定義,你可能會想,奇怪了, RequestResolver 除了 type 以外,又沒有傳入 Request 的任何東西, Effect 是要怎麼知道哪個 request 對應的是哪個 resolver ,畢竟我們都知道 TypeScript 的 type 到了執行時就不存在了。答案是:不知道,因為你在使用時需要兩個一起給 Effect

function getData(id: number) {
  // 在將 request 交給 Effect 的同時,你需要將 resolver 也一起傳過去
  return Effect.request(new GetDataRequest({ id }), GetDataResolver)
}

function getDataBatch(id: number) {
  return Effect.request(new GetDataRequest({ id }), BatchGetDataResolver)
}

通常會定義像這樣的 helper function,將 Request 和 RequestResolver 封裝起來,以簡化 Effect.request 的呼叫並提高可讀性

實際使用

接下來我們就來實際的使用看看吧

const listOfDataID = [1, 1, 2, 3, 5, 8, 11, 13]

pipe(
  listOfDataID,
  // 這邊可以試看看換成 getData
  Array.map((id) => getDataBatch(id)),
  // 這邊可以試看看關掉 batching
  Effect.allWith({ batching: true }),
  Effect.tap((result) => {
    console.log(result)
  }),
  Effect.runPromise
)

(playground link)

上面的 code 可以自行實驗看看,不論是換成 getData 或是關掉 batching 都會讓 batch 的效果消失

在這篇裡面我們認識是怎麼使用 Effect 來 batch request ,下一篇會再來分享一個實際的經驗,怎麼取得與處理像看板這種大量且複雜的資料

Reference


上一篇
17. Effect 的 concurrency 調度器: Fiber 簡介
下一篇
19. Effect 實戰分享 4: 取得看版資料
系列文
Effect 魔法:打造堅不可摧的應用程式22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言