這篇來要講個我個人認為挺有趣的東西,資源管理,一般如果是在前端,需要資源管理的情況可能比較沒有那麼多,因為網頁重整了就什麼都沒了,這可能是後端會比較需要的,不過之前也有碰過一個案例是在前端的,那時前端要使用 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
)
如果你需要前一個 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
)
題外話,在這個系列 「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
)
很多的資源都需要做取得與釋放的動作,比如像資料庫在使用前就需要建立連線,使用完後則需要關掉連線,這個過程我們可以像這樣去完成
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
)
不過這樣還要自己加入清理的 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
)
這邊終於要來解釋到上面的 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 該怎麼執行