From 332f11ab7eeb9e5b92959921e534f5d2ee4efa71 Mon Sep 17 00:00:00 2001 From: murashit Date: Tue, 30 Jun 2026 23:13:12 +0900 Subject: [PATCH] Simplify settings dynamic section lifecycle --- src/settings/dynamic-sections-controller.ts | 163 ++++---------------- src/settings/lifecycle.ts | 39 ++--- tests/settings/settings-lifecycle.test.ts | 37 ++--- 3 files changed, 55 insertions(+), 184 deletions(-) diff --git a/src/settings/dynamic-sections-controller.ts b/src/settings/dynamic-sections-controller.ts index 5aae85f5..b6fffc4b 100644 --- a/src/settings/dynamic-sections-controller.ts +++ b/src/settings/dynamic-sections-controller.ts @@ -9,7 +9,9 @@ import type { SettingsDynamicSectionsHost } from "./host"; import { createSettingsDynamicSectionLifecycle, type SettingsDynamicSectionLifecycleState, - transitionSettingsDynamicSectionLifecycle, + settingsDynamicSectionFailed, + settingsDynamicSectionLoaded, + settingsDynamicSectionLoading, } from "./lifecycle"; interface SettingsDynamicSectionsControllerCallbacks { @@ -32,7 +34,6 @@ export interface SettingsDynamicSectionsSnapshot { export class SettingsDynamicSectionsController { private dynamicSectionsAutoLoadStarted = false; - private dynamicSectionsRefreshOperationToken = 0; private modelsOperationToken = 0; private hooksOperationToken = 0; private archivedThreadsOperationToken = 0; @@ -62,11 +63,7 @@ export class SettingsDynamicSectionsController { if (archivedThreads) { this.archivedThreads = [...archivedThreads]; this.archivedThreadsLoaded = true; - this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, { - type: "loaded", - status: archivedThreadsStatus(archivedThreads.length), - operationToken: this.archivedThreadsOperationToken, - }); + this.archivedThreadsLifecycle = settingsDynamicSectionLoaded(archivedThreadsStatus(archivedThreads.length)); } this.unsubscribeModels = this.host.dynamicData.observeModelsResult( (result) => { @@ -90,7 +87,6 @@ export class SettingsDynamicSectionsController { resetDynamicSectionContext(): void { this.dynamicSectionsAutoLoadStarted = false; - this.dynamicSectionsRefreshOperationToken += 1; this.modelsOperationToken += 1; this.hooksOperationToken += 1; this.archivedThreadsOperationToken += 1; @@ -126,36 +122,19 @@ export class SettingsDynamicSectionsController { this.archivedThreads = [...observedThreads]; this.archivedThreadsLoaded = true; if (this.archivedThreadsLifecycle.kind !== "loading") { - this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, { - type: "loaded", - status: archivedThreadsStatus(observedThreads.length), - operationToken: this.archivedThreadsOperationToken, - }); + this.archivedThreadsLifecycle = settingsDynamicSectionLoaded(archivedThreadsStatus(observedThreads.length)); } this.callbacks.display(); } async refreshDynamicSections(options: { forceModels?: boolean } = {}): Promise { this.dynamicSectionsAutoLoadStarted = true; - const operationToken = this.nextDynamicSectionsRefreshOperationToken(); const modelsOperationToken = this.nextModelsOperationToken(); const hooksOperationToken = this.nextHooksOperationToken(); const archivedThreadsOperationToken = this.nextArchivedThreadsOperationToken(); - this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, { - type: "started", - status: "Loading models...", - operationToken: modelsOperationToken, - }); - this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, { - type: "started", - status: "Loading archived threads...", - operationToken: archivedThreadsOperationToken, - }); - this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, { - type: "started", - status: "Loading hooks...", - operationToken: hooksOperationToken, - }); + this.modelsLifecycle = settingsDynamicSectionLoading("Loading models..."); + this.archivedThreadsLifecycle = settingsDynamicSectionLoading("Loading archived threads..."); + this.hooksLifecycle = settingsDynamicSectionLoading("Loading hooks..."); this.callbacks.display(); let failedCount = 0; @@ -165,26 +144,19 @@ export class SettingsDynamicSectionsController { this.host.dynamicData.loadHooks(), this.host.dynamicData.refreshArchivedThreads(), ] as const); - if (this.isStaleDynamicSectionsRefreshOperation(operationToken)) return; if (this.isStaleModelsOperation(modelsOperationToken)) { // A newer models operation owns this section. } else if (modelsResult.status === "fulfilled") { this.models = [...modelsResult.value]; - this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, { - type: "loaded", - status: `Loaded ${String(modelsResult.value.length)} model${modelsResult.value.length === 1 ? "" : "s"}.`, - operationToken: modelsOperationToken, - }); + this.modelsLifecycle = settingsDynamicSectionLoaded( + `Loaded ${String(modelsResult.value.length)} model${modelsResult.value.length === 1 ? "" : "s"}.`, + ); } else if (isStaleSettingsDynamicDataContextError(modelsResult.reason)) { return; } else { failedCount += 1; - this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, { - type: "failed", - status: `Could not load models: ${errorMessage(modelsResult.reason)}`, - operationToken: modelsOperationToken, - }); + this.modelsLifecycle = settingsDynamicSectionFailed(`Could not load models: ${errorMessage(modelsResult.reason)}`); } if (this.isStaleHooksOperation(hooksOperationToken)) { @@ -194,18 +166,10 @@ export class SettingsDynamicSectionsController { this.hookWarnings = [...hooksResult.value.warnings]; this.hookErrors = [...hooksResult.value.errors]; this.hooksLoaded = true; - this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, { - type: "loaded", - status: hooksResult.value.status, - operationToken: hooksOperationToken, - }); + this.hooksLifecycle = settingsDynamicSectionLoaded(hooksResult.value.status); } else { failedCount += 1; - this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, { - type: "failed", - status: `Could not load hooks: ${errorMessage(hooksResult.reason)}`, - operationToken: hooksOperationToken, - }); + this.hooksLifecycle = settingsDynamicSectionFailed(`Could not load hooks: ${errorMessage(hooksResult.reason)}`); } if (this.isStaleArchivedThreadsOperation(archivedThreadsOperationToken)) { @@ -213,54 +177,32 @@ export class SettingsDynamicSectionsController { } else if (archivedThreadsResult.status === "fulfilled") { this.archivedThreads = [...archivedThreadsResult.value]; this.archivedThreadsLoaded = true; - this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, { - type: "loaded", - status: archivedThreadsStatus(archivedThreadsResult.value.length), - operationToken: archivedThreadsOperationToken, - }); + this.archivedThreadsLifecycle = settingsDynamicSectionLoaded(archivedThreadsStatus(archivedThreadsResult.value.length)); } else if (isStaleSettingsDynamicDataContextError(archivedThreadsResult.reason)) { return; } else { failedCount += 1; - this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, { - type: "failed", - status: `Could not load archived threads: ${errorMessage(archivedThreadsResult.reason)}`, - operationToken: archivedThreadsOperationToken, - }); + this.archivedThreadsLifecycle = settingsDynamicSectionFailed( + `Could not load archived threads: ${errorMessage(archivedThreadsResult.reason)}`, + ); } } catch (error) { - if (this.isStaleDynamicSectionsRefreshOperation(operationToken)) return; failedCount = 3; const message = errorMessage(error); if (!this.isStaleModelsOperation(modelsOperationToken)) { - this.modelsLifecycle = transitionSettingsDynamicSectionLifecycle(this.modelsLifecycle, { - type: "failed", - status: `Could not load models: ${message}`, - operationToken: modelsOperationToken, - }); + this.modelsLifecycle = settingsDynamicSectionFailed(`Could not load models: ${message}`); } if (!this.isStaleHooksOperation(hooksOperationToken)) { - this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, { - type: "failed", - status: `Could not load hooks: ${message}`, - operationToken: hooksOperationToken, - }); + this.hooksLifecycle = settingsDynamicSectionFailed(`Could not load hooks: ${message}`); } if (!this.isStaleArchivedThreadsOperation(archivedThreadsOperationToken)) { - this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, { - type: "failed", - status: `Could not load archived threads: ${message}`, - operationToken: archivedThreadsOperationToken, - }); + this.archivedThreadsLifecycle = settingsDynamicSectionFailed(`Could not load archived threads: ${message}`); } } finally { - const staleRefresh = this.isStaleDynamicSectionsRefreshOperation(operationToken); - if (!staleRefresh) { - if (failedCount > 0) { - this.callbacks.notify("Could not refresh all Codex details."); - } - this.callbacks.display(); + if (failedCount > 0) { + this.callbacks.notify("Could not refresh all Codex details."); } + this.callbacks.display(); } } @@ -294,11 +236,7 @@ export class SettingsDynamicSectionsController { operation: async (operationToken) => { await this.host.dynamicData.trustHook(hook); if (this.isStaleHooksOperation(operationToken)) return; - this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, { - type: "loaded", - status: "Trusted hook definition.", - operationToken, - }); + this.hooksLifecycle = settingsDynamicSectionLoaded("Trusted hook definition."); await this.loadHooks(); }, }); @@ -313,11 +251,7 @@ export class SettingsDynamicSectionsController { operation: async (operationToken) => { await this.host.dynamicData.setHookEnabled(hook, enabled); if (this.isStaleHooksOperation(operationToken)) return; - this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, { - type: "loaded", - status: enabled ? "Enabled hook." : "Disabled hook.", - operationToken, - }); + this.hooksLifecycle = settingsDynamicSectionLoaded(enabled ? "Enabled hook." : "Disabled hook."); await this.loadHooks(); }, }); @@ -335,11 +269,7 @@ export class SettingsDynamicSectionsController { }); if (this.isStaleArchivedThreadsOperation(operationToken)) return; this.archivedThreads = this.archivedThreads.filter((thread) => thread.id !== threadId); - this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, { - type: "loaded", - status: `Restored "${threadArchiveDisplayTitle(restoredThread)}".`, - operationToken, - }); + this.archivedThreadsLifecycle = settingsDynamicSectionLoaded(`Restored "${threadArchiveDisplayTitle(restoredThread)}".`); }, }); } @@ -358,11 +288,7 @@ export class SettingsDynamicSectionsController { }); if (this.isStaleArchivedThreadsOperation(operationToken)) return; this.archivedThreads = this.archivedThreads.filter((thread) => thread.id !== threadId); - this.archivedThreadsLifecycle = transitionSettingsDynamicSectionLifecycle(this.archivedThreadsLifecycle, { - type: "loaded", - status: `Deleted "${title}".`, - operationToken, - }); + this.archivedThreadsLifecycle = settingsDynamicSectionLoaded(`Deleted "${title}".`); }, }); } @@ -397,11 +323,7 @@ export class SettingsDynamicSectionsController { this.hookWarnings = [...hooks.warnings]; this.hookErrors = [...hooks.errors]; this.hooksLoaded = true; - this.hooksLifecycle = transitionSettingsDynamicSectionLifecycle(this.hooksLifecycle, { - type: "loaded", - status: hooks.status, - operationToken, - }); + this.hooksLifecycle = settingsDynamicSectionLoaded(hooks.status); }, }); } @@ -416,8 +338,6 @@ export class SettingsDynamicSectionsController { const operationToken = options.section === "hooks" ? this.nextHooksOperationToken() : this.nextArchivedThreadsOperationToken(); const stale = (): boolean => options.section === "hooks" ? this.isStaleHooksOperation(operationToken) : this.isStaleArchivedThreadsOperation(operationToken); - const lifecycle = (): SettingsDynamicSectionLifecycleState => - options.section === "hooks" ? this.hooksLifecycle : this.archivedThreadsLifecycle; const setLifecycle = (state: SettingsDynamicSectionLifecycleState): void => { if (options.section === "hooks") { this.hooksLifecycle = state; @@ -426,36 +346,19 @@ export class SettingsDynamicSectionsController { } }; - setLifecycle( - transitionSettingsDynamicSectionLifecycle(lifecycle(), { - type: "started", - status: options.loadingStatus, - operationToken, - }), - ); + setLifecycle(settingsDynamicSectionLoading(options.loadingStatus)); this.callbacks.display(); try { await options.operation(operationToken); } catch (error) { if (stale()) return; - setLifecycle( - transitionSettingsDynamicSectionLifecycle(lifecycle(), { - type: "failed", - status: options.failureStatus(error), - operationToken, - }), - ); + setLifecycle(settingsDynamicSectionFailed(options.failureStatus(error))); this.callbacks.notify(options.failureNotice); } finally { if (!stale()) this.callbacks.display(); } } - private nextDynamicSectionsRefreshOperationToken(): number { - this.dynamicSectionsRefreshOperationToken += 1; - return this.dynamicSectionsRefreshOperationToken; - } - private nextModelsOperationToken(): number { this.modelsOperationToken += 1; return this.modelsOperationToken; @@ -471,10 +374,6 @@ export class SettingsDynamicSectionsController { return this.archivedThreadsOperationToken; } - private isStaleDynamicSectionsRefreshOperation(operationToken: number): boolean { - return operationToken !== this.dynamicSectionsRefreshOperationToken; - } - private isStaleModelsOperation(operationToken: number): boolean { return operationToken !== this.modelsOperationToken; } diff --git a/src/settings/lifecycle.ts b/src/settings/lifecycle.ts index 1cb8141d..8bcfb4a7 100644 --- a/src/settings/lifecycle.ts +++ b/src/settings/lifecycle.ts @@ -1,38 +1,21 @@ export type SettingsDynamicSectionLifecycleState = | { kind: "idle"; status: "" } - | { kind: "loading"; status: string; operationToken: number } - | { kind: "loaded"; status: string; operationToken: number } - | { kind: "failed"; status: string; operationToken: number }; - -export type SettingsDynamicSectionLifecycleEvent = - | { type: "started"; status: string; operationToken: number } - | { type: "loaded"; status: string; operationToken: number } - | { type: "failed"; status: string; operationToken: number } - | { type: "reset" }; + | { kind: "loading"; status: string } + | { kind: "loaded"; status: string } + | { kind: "failed"; status: string }; export function createSettingsDynamicSectionLifecycle(): SettingsDynamicSectionLifecycleState { return { kind: "idle", status: "" }; } -export function transitionSettingsDynamicSectionLifecycle( - state: SettingsDynamicSectionLifecycleState, - event: SettingsDynamicSectionLifecycleEvent, -): SettingsDynamicSectionLifecycleState { - switch (event.type) { - case "started": - if (isStaleSettingsDynamicSectionEvent(state, event.operationToken)) return state; - return { kind: "loading", status: event.status, operationToken: event.operationToken }; - case "loaded": - if (isStaleSettingsDynamicSectionEvent(state, event.operationToken)) return state; - return { kind: "loaded", status: event.status, operationToken: event.operationToken }; - case "failed": - if (isStaleSettingsDynamicSectionEvent(state, event.operationToken)) return state; - return { kind: "failed", status: event.status, operationToken: event.operationToken }; - case "reset": - return createSettingsDynamicSectionLifecycle(); - } +export function settingsDynamicSectionLoading(status: string): SettingsDynamicSectionLifecycleState { + return { kind: "loading", status }; } -function isStaleSettingsDynamicSectionEvent(state: SettingsDynamicSectionLifecycleState, operationToken: number): boolean { - return "operationToken" in state && state.operationToken > operationToken; +export function settingsDynamicSectionLoaded(status: string): SettingsDynamicSectionLifecycleState { + return { kind: "loaded", status }; +} + +export function settingsDynamicSectionFailed(status: string): SettingsDynamicSectionLifecycleState { + return { kind: "failed", status }; } diff --git a/tests/settings/settings-lifecycle.test.ts b/tests/settings/settings-lifecycle.test.ts index 9930d6e9..66399edf 100644 --- a/tests/settings/settings-lifecycle.test.ts +++ b/tests/settings/settings-lifecycle.test.ts @@ -1,36 +1,25 @@ import { describe, expect, it } from "vitest"; -import { createSettingsDynamicSectionLifecycle, transitionSettingsDynamicSectionLifecycle } from "../../src/settings/lifecycle"; +import { + createSettingsDynamicSectionLifecycle, + settingsDynamicSectionFailed, + settingsDynamicSectionLoaded, + settingsDynamicSectionLoading, +} from "../../src/settings/lifecycle"; describe("settings lifecycle", () => { it("tracks dynamic section lifecycle", () => { const idle = createSettingsDynamicSectionLifecycle(); expect(idle).toEqual({ kind: "idle", status: "" }); - const loading = transitionSettingsDynamicSectionLifecycle(idle, { type: "started", status: "Loading hooks...", operationToken: 1 }); - expect(loading).toEqual({ kind: "loading", status: "Loading hooks...", operationToken: 1 }); + const loading = settingsDynamicSectionLoading("Loading hooks..."); + expect(loading).toEqual({ kind: "loading", status: "Loading hooks..." }); - expect(transitionSettingsDynamicSectionLifecycle(loading, { type: "loaded", status: "Stale result.", operationToken: 0 })).toBe( - loading, - ); + const loaded = settingsDynamicSectionLoaded("Loaded 1 hook."); + expect(loaded).toEqual({ kind: "loaded", status: "Loaded 1 hook." }); - const loaded = transitionSettingsDynamicSectionLifecycle(loading, { type: "loaded", status: "Loaded 1 hook.", operationToken: 1 }); - expect(loaded).toEqual({ kind: "loaded", status: "Loaded 1 hook.", operationToken: 1 }); + const failed = settingsDynamicSectionFailed("Could not load hooks."); + expect(failed).toEqual({ kind: "failed", status: "Could not load hooks." }); - const failed = transitionSettingsDynamicSectionLifecycle(loaded, { - type: "failed", - status: "Could not load hooks.", - operationToken: 1, - }); - expect(failed).toEqual({ kind: "failed", status: "Could not load hooks.", operationToken: 1 }); - - const laterLoaded = transitionSettingsDynamicSectionLifecycle(failed, { type: "loaded", status: "Loaded 2 hooks.", operationToken: 2 }); - expect( - transitionSettingsDynamicSectionLifecycle(laterLoaded, { type: "started", status: "Loading old hooks...", operationToken: 1 }), - ).toBe(laterLoaded); - expect(transitionSettingsDynamicSectionLifecycle(laterLoaded, { type: "failed", status: "Late old failure.", operationToken: 1 })).toBe( - laterLoaded, - ); - - expect(transitionSettingsDynamicSectionLifecycle(failed, { type: "reset" })).toEqual(idle); + expect(createSettingsDynamicSectionLifecycle()).toEqual(idle); }); });