iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0

這篇來要講個我個人認為挺有趣的東西,資源管理,一般如果是在前端,需要資源管理的情況可能比較沒有那麼多,因為網頁重整了就什麼都沒了,這可能是後端會比較需要的,不過之前也有碰過一個案例是在前端的,那時前端要使用 pyodide 的這個在瀏覽器中的 python runtime ,用完後總是要好好的把它關掉,不然它會一直佔著記憶體不放,如果這樣的情況多了,用不了多久你的網頁就會耗盡資源而 crash 的

總之,這篇就是要來分享這些你在使用完後需要明確的釋放掉的資源,小的像 URL.createObjectURL 需要使用 URL.revokeObjectURL 來釋放,或是像資料庫連線, mutex lock,或是上面提到 pyodide ,釋放資源的需求其實越來越多,多到 JavaScript 都出現了一個新的語法 using 來處理,不過這次要講的是 Effect 中的做法

清理資源

如果你有什麼動作需要在 Effect 的流程結束時做的話,你可以使用 Effect.ensuring

pipe(
  Effect.gen(function * () {
    yield* Console.log('do some work')
    
    if (Math.random() > 0.5) {
      yield * Effect.fail(new Error('oh no'))
    } else {
      return 'success'
    }
  }),
  // ensuring 會在前一個 Effect 結束時執行指定的 Effect ,不論前一個 Effect 成功或失敗
  Effect.ensuring(Console.log('work done')),
  Effect.tap(res => {
    // 可以注意這個 console ,如果之前的 Effect 失敗就不會印出來了
    console.log(res)
  }),
  Effect.runPromise
)

(playground link)

如果你需要前一個 Effect 的執行結果的話,你可以用 Effect.onExit

pipe(
  Effect.gen(function * () {
    yield* Console.log('do some work')
    if (Math.random() > 0.5) {
      yield * Effect.fail(new Error('oh no'))
    } else {
      return 'success'
    }
  }),
  // 可以注意到基本用法跟 ensuring 一樣,不同的是我們可以取得執行結果
  Effect.onExit((exit) => Console.log('work done', exit)),
  Effect.tap(res => {
    console.log(res)
  }),
  Effect.runPromise
)

(playground link)

題外話,在這個系列 「15. Effect 實戰分享 3: 資料遷移」中的實際案例,其實那時我們就有用到 Effect.onExit 來收集資料遷移的成功與失敗的數量

如果你要動態的加入清理的 function ,你可以用 Effect.addFinalizer

import { Effect, pipe, Console } from "effect"

pipe(
  Effect.gen(function * () {
    // 增加一個 finalizer ,這讓你可以動態的註冊結束時的 function
    yield * Effect.addFinalizer((exit) => Console.log('work done', exit))
    yield* Console.log('do some work')
    if (Math.random() > 0.5) {
      yield * Effect.fail(new Error('oh no'))
    } else {
      return 'success'
    }
  }),
  // 這跟之後要介紹到的 scope 有關,現在先知道需要這個才有辦法執行
  Effect.scoped,
  Effect.tap(res => {
    console.log(res)
  }),
  Effect.runPromise
)

(playground link)

取得與釋放資源

很多的資源都需要做取得與釋放的動作,比如像資料庫在使用前就需要建立連線,使用完後則需要關掉連線,這個過程我們可以像這樣去完成

interface Database {
  query: (sql: string) => Effect.Effect<unknown>
  close: () => Effect.Effect<void>
}

function connectDatabase(): Effect.Effect<Database> {
  /* 連線的實作 */
}

pipe(
  Effect.gen(function*() {
    const db = yield* connectDatabase()
    yield* Effect.addFinalizer(() => db.close())
    yield* db.query("query")
  }),
  Effect.scoped,
  Effect.runPromise
)

(playground link)

不過這樣還要自己加入清理的 code ,除了自己把建立連線跟 Effect.addFinalizer 包裝起來,還有什麼方法嗎?還真的有,因為這個 pattern 太常見了,所以就有了 Effect.acquireRelease

// 等下會解釋這個 Scope 是什麼
function getDatabase(): Effect.Effect<Database, never, Scope.Scope> {
  // acquireRelease 可以讓你把取得資源跟釋放資源封裝起來
  return Effect.acquireRelease(connectDatabase(), (db) => db.close())
}

pipe(
  Effect.gen(function*() {
    // 注意這邊沒有了自己加的 addFinalizer
    const db = yield* getDatabase()
    yield* db.query("query")
  }),
  Effect.scoped,
  Effect.runPromise
)

(playground link)

Effect 中的 scope

這邊終於要來解釋到上面的 Scope 到底是什麼了,簡單來說就是資源有效可以使用的範圍的概念,像上面的 db ,在切斷連線後正常就無法被使用了,同時當離開這個範圍時,這些資源因為不再使用了,應該要被釋放掉,當我們使用 Effect.acquireRelease 或 Effect.addFinalizer 這些具有資源生命週期管理概念的 function 時,Effect 會將 Scope 做為一個必要的 service 放到 Effect 的 requirement 中,這表示我們需要透過 Effect.scoped 來建立一個 Scope ,它會負責在 Effect 結束時自動關閉這個範圍並釋放所有註冊的資源

pipe(
  Effect.gen(function*() {
    // 當你需要 finalizer 時,你就需要指定一個「範圍」,範圍結束時,裡面的東西應該要被執行
    yield * Effect.addFinalizer(() => Effect.void)
  }),
  // 透過 scoped 來指定範圍
  Effect.scoped,
  Effect.runPromise
)

這篇我們介紹了怎麼在 Effect 中取得與釋放資源,這在需要跟外部系統互動時會很常見,下一篇我們要來看的是 Effect 的 runtime ,如何自訂 Effect 該怎麼執行

Reference


上一篇
19. Effect 實戰分享 4: 取得看版資料
下一篇
21. Effect runtime :自訂如何執行 effect
系列文
Effect 魔法:打造堅不可摧的應用程式22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言