feat(FN-1336): add write-through cache to GlobalSettingsStore

- Add in-memory cache to GlobalSettingsStore for fast settings reads
- Add getSettingsFast() to TaskStore that uses the cached settings
- Update GET /settings route to use getSettingsFast() for faster responses
- Add invalidateCache() method for testing and external process scenarios
- Document write-through cache pattern in .fusion/memory.md
This commit is contained in:
gsxdsm
2026-04-10 03:03:51 -07:00
parent f246fe4896
commit 3cb59159e5
7 changed files with 168 additions and 10 deletions

View File

@@ -70,6 +70,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): TaskStore {
archiveTask: vi.fn(),
unarchiveTask: vi.fn(),
getSettings: vi.fn().mockResolvedValue({}),
getSettingsFast: vi.fn().mockResolvedValue({}),
updateSettings: vi.fn(),
updateGlobalSettings: vi.fn(),
getSettingsByScope: vi.fn().mockResolvedValue({ global: {}, project: {} }),
@@ -253,7 +254,7 @@ describe("Standardized error responses", () => {
it("returns 500 errors as { error } and logs to console.error", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
(store.getSettings as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("Config read failed"));
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("Config read failed"));
const res = await GET(buildApp(), "/api/settings");
@@ -8593,7 +8594,7 @@ describe("GET /settings", () => {
it("returns persisted settings merged with defaults", async () => {
const persistedSettings = { maxConcurrent: 5, autoMerge: false };
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue({ ...DEFAULT_SETTINGS, ...persistedSettings });
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockResolvedValue({ ...DEFAULT_SETTINGS, ...persistedSettings });
const res = await GET(buildApp(), "/api/settings");
@@ -8604,7 +8605,7 @@ describe("GET /settings", () => {
});
it("injects githubTokenConfigured as true when token is configured", async () => {
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue(DEFAULT_SETTINGS);
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockResolvedValue(DEFAULT_SETTINGS);
const res = await GET(buildApp(), "/api/settings");
@@ -8617,7 +8618,7 @@ describe("GET /settings", () => {
app.use(express.json());
app.use("/api", createApiRoutes(store)); // no githubToken option
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue(DEFAULT_SETTINGS);
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockResolvedValue(DEFAULT_SETTINGS);
const res = await GET(app, "/api/settings");
@@ -8626,7 +8627,7 @@ describe("GET /settings", () => {
});
it("returns 500 on store error", async () => {
(store.getSettings as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Config read failed"));
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Config read failed"));
const res = await GET(buildApp(), "/api/settings");
@@ -8912,7 +8913,7 @@ describe("PUT /settings", () => {
expect(updateRes.status).toBe(200);
// Then, verify GET /settings returns the persisted values
(store.getSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
(store.getSettingsFast as ReturnType<typeof vi.fn>).mockResolvedValue(updatedSettings);
const getRes = await GET(buildApp(), "/api/settings");
expect(getRes.status).toBe(200);

View File

@@ -1536,7 +1536,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
router.get("/settings", async (req, res) => {
try {
const scopedStore = await getScopedStore(req);
const settings = await scopedStore.getSettings();
const settings = await scopedStore.getSettingsFast();
// Inject server-side configuration flags
res.json({
...settings,