Loader API
載入器會發布一個 Loader
服務。外掛注入它,就能讀取並編輯設定檔中的項目:
import { Deferred, Effect, Layer } from "effect"
import { Graft, Plugin } from "@graftland/graft"import { Entry, Loader, ModuleResolver } from "@graftland/loader"
const admin = Plugin.make({ name: "app/admin", inject: [Loader.Loader], apply: () => Effect.gen(function* () { const loader = yield* Loader.Loader const id = yield* loader.create({ name: "./plugins/cron.mjs" }) yield* loader.update(id, { config: { every: "5 minutes" } }) yield* loader.update("api", { disabled: true }) }),})
const cron = Plugin.make({ name: "app/cron", apply: () => Effect.void })const api = Plugin.make({ name: "app/api", apply: () => Effect.void })const resolver = ModuleResolver.fromRecord({ "./plugins/cron.mjs": cron, api })
const entries = await Effect.runPromise( Effect.gen(function* () { const loaderFork = yield* Graft.plugin(Loader.plugin, [ { id: "api", name: "api" }, ]) yield* loaderFork.await const fork = yield* Graft.plugin(admin) yield* fork.await const read = yield* Deferred.make<ReadonlyArray<Entry.Entry>>() const probe = Plugin.make({ name: "app/probe", inject: [Loader.Loader], apply: () => Loader.Loader.pipe( Effect.flatMap((loader) => Deferred.into(loader.entries, read)), Effect.asVoid, ), }) yield* Graft.plugin(probe) return yield* Deferred.await(read) }).pipe( Effect.scoped, Effect.provide(Layer.provideMerge(Graft.layer, resolver)), ),)
entries[0]?.disabled // => trueentries[1]?.config // => { every: "5 minutes" }樹中存在受管理 fork 時,Loader 在呼叫儲存修改之前拒絕編輯、重新整理和重試。繞過保護見執行轉換。
透過程式碼編輯設定項目
Section titled “透過程式碼編輯設定項目”| 成員 | 作用 |
|---|---|
entries |
最新已持久化的目標項目。 |
changes |
目前已持久化的目標,之後是已接受的目標,皆在執行期協調之前。 |
create(entry, options?) |
新增項目,位置在根清單的末尾,或群組 parent 的 position。回傳其 id。 |
update(id, patch) |
修改項目的部分欄位。 |
remove(id) |
移除項目。 |
ready |
等待目前的目標協調完成,且沒有任何 fork 處於 Loading 狀態。 |
retry |
重試所有失敗的項目,並重新整理整棵樹中的 !env 值。 |
編輯會寫入檔案。Graft 會保留檔案中的註解、格式和 !env 標籤。
編輯提供的保證
Section titled “編輯提供的保證”| 階段 | 保證 |
|---|---|
| 已接納 | 編輯進入了載入器有上限的工作佇列。在此之前呼叫端若被取消,編輯不會產生任何效果。 |
| 已儲存 | 檔案已寫入,目標已發布。編輯的效果在此完成。 |
| 已協調 | 外掛已完成更新。編輯完成並不代表這一點。請使用 ready。 |
編輯被接納後,取消呼叫端只會停止等待。載入器由 scope 管理的工作者會繼續儲存檔案並發布目標。
- 每個呼叫端透過
Deferred取得自己操作的結果。如果呼叫端已經離開,失敗也會連同完整的 cause 一併記錄。 - 關閉載入器會停止它的工作者、結算尚未完成的呼叫端,並以
LoaderClosed拒絕新的編輯。
執行期協調在另一個由 scope 管理的 fiber 中執行,可能會略過中間的目標。
| 你想要… | 做法 |
|---|---|
| 等待目前的目標和載入中的 fork | 在生命週期回呼之外使用 ready。 |
| 從生命週期回呼中提交編輯 | 可以提交,但絕不要在那裡等待 ready:它會等待回呼自己。 |
| 重新整理目前已儲存的目標 | 先 retry,再 ready。retry 在發布其請求之後回傳。 |
重新整理整棵樹的 !env |
loader.retry,或 loader.root.update(entries)(項目未變更時也會重新整理)。 |
| 持久化設定變更 | 使用一般的編輯方法。 |
ready
會等待目前的目標版本、已排隊和執行中的環境重新整理,以及所有載入中的後代。它把
Pending 和 Failed 的 fork 視為已完成,而外掛在提供者啟動後,仍可能短暫處於
Pending。因此 ready 不保證每個外掛都已啟用。核心的
fork.stable
屏障只描述目前的 fork 狀態,並不能取代這些載入器檢查。
關於 retry:
- 當
retry再次發布未變更的目標時,changes也會發出。 - 它會觸及生命週期、注入和群組包裝層之下的失敗外掛,並重新啟動失敗的外掛 fork。
- 除非重新整理的值需要更新或重新載入,否則它會保留健康的實例和尚未完成的解析。
- 一般編輯會重試已協調群組中失敗的項目,但不會重新整理未變更的健康子樹。
載入器會為它接受的每個目標保留一份凍結快照。
- 在 API 編輯進入佇列之前,載入器會複製陣列和一般物件(包括中繼資料)。接受初始目標或重新整理後的目標時,也會複製它們。
- 之後修改你傳入的物件,不會改變執行中的目標或已儲存的目標;要套用新值,請再提交一次編輯。
loader.entries和loader.changes提供的是凍結的資料。
| 值 | 處理方式 |
|---|---|
| 陣列和一般物件 | 複製。共用的參照和環會保留在快照中,但環仍會驗證失敗。 |
類別實例(Env、Data 類別、Date 等) |
保留原本的參照。請把它們當作不可變的設定來使用。 |
| 環境變數解析中共用的陣列和記錄 | 保持原樣,不展開成樹。身分快取只屬於單次執行。 |
快照複製、環偵測和設定比較也都能處理共用圖。比較時,每對一般容器只會走訪一次,不透明值沿用 Effect 的相等性,包括自訂的相等性實作。含環或不透明值的設定在比對 ID 時改用保守的後備方式,同名項目較多時仍可能需要平方級比較。
檔案的寫入方式
Section titled “檔案的寫入方式”Graft 先寫入暫存檔,因此設定檔不會停留在寫到一半的狀態。
- 在同一個目錄中建立暫存檔:獨佔建立,權限模式為
0600。 - 保留目標的權限位元,以及 Graft 能讀到的 uid/gid。新檔案預設為
0600。 - 對檔案執行
fsync並關閉。 - 以 rename 換入原位。
| 情況 | 行為 |
|---|---|
| 寫入失敗或被中斷 | Graft 會移除暫存檔。如果清理也失敗,兩個失敗會一併保留。 |
| 擴充 ACL 和屬性 | 不複製。 |
| 斷電 | 不會對目錄執行 fsync,因此 Graft 不保證 rename 能保留下來。 |
| 解析或序列化失敗 | 以 StoreError 失敗並保留原始 cause。檔案不變。 |
Graft 在寫入之前,會解碼最終文件並與目標比對。
YAML 錨點與別名
Section titled “YAML 錨點與別名”| 情況 | 行為 |
|---|---|
| 編輯變更或移除了錨點 | 受該編輯影響的別名會變成其先前值的獨立副本,其他別名保持原樣。 |
| 編輯透過別名群組變更子項目 | Graft 會先建立本機副本。來源集合不會改變。 |
| 項目/群組結構上的別名 | 在收集並分配 ID 之前先具體化,因此匿名子項目會取得各自的 ID。 |
| 自動分配 ID | Graft 計算一份獨立的邏輯目標,只具體化受插入影響的別名。一般外掛設定不會繼承產生的 ID。 |
Graft 會解析序列化後的 YAML 並與規劃的目標比對。如果兩者不同,它會回傳
StoreError 並讓檔案保持不變。
與外部編輯的衝突
Section titled “與外部編輯的衝突”如果自 Graft 上次讀取之後有人編輯過檔案,Graft 會在套用編輯之前重新讀取。如果編輯所修改的欄位在檔案中也被修改過,以檔案為準:Graft 會丟棄該編輯並記錄一則警告。
Graft 也會在 rename 已備妥的暫存檔之前,再檢查一次目的地:
- 如果檔案已變更,Graft 會丟棄暫存檔、讀取新版本並重新套用編輯。
- 同欄位規則仍以原始快照為準,因此也能發現準備期間發生的變更。
- Graft 最多嘗試三次。如果檔案持續變更,它會回傳
StoreError,並且不提交該編輯。 - 清理失敗會停止重試,並保留其完整的 cause。
建立第一個檔案和自動分配 ID 也使用相同的檢查。
定位受管理的項目
Section titled “定位受管理的項目”將項目 id 作為 Graft.transition 的 target;也支援數字 fork
id。Loader 在項目註冊驅動前綁定名稱。嵌入式註冊必須明確提供
instance;名稱衝突或重複註冊會在啟動執行單元前失敗。
update、remove、create、更新和重試仍屬於繞過協調器的操作:只要存在受管理的後代,就會在呼叫 store 前拒絕。檔案邊界見受管理的設定。已接受的 transition 不改變
loader.entries;它代表持久化設定,而非目前執行的世代。