mirror of
https://github.com/murashit/codex-panel.git
synced 2026-07-22 06:57:10 +00:00
Simplify settings dynamic section lifecycle
This commit is contained in:
parent
478e5ef563
commit
332f11ab7e
3 changed files with 55 additions and 184 deletions
|
|
@ -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<void> {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue