diff --git a/src/hooks/config-storage-listener.mjs b/src/hooks/config-storage-listener.mjs new file mode 100644 index 000000000..16c4e82f8 --- /dev/null +++ b/src/hooks/config-storage-listener.mjs @@ -0,0 +1,11 @@ +export function createConfigStorageListener(setConfig, ignoreSession = true) { + return (changes) => { + if (ignoreSession && Object.keys(changes).length === 1 && 'sessions' in changes) return + + const configUpdate = {} + for (const key of Object.keys(changes)) { + configUpdate[key] = changes[key].newValue + } + setConfig((currentConfig) => ({ ...currentConfig, ...configUpdate })) + } +} diff --git a/src/hooks/use-config.mjs b/src/hooks/use-config.mjs index 151c6c99d..935735b18 100644 --- a/src/hooks/use-config.mjs +++ b/src/hooks/use-config.mjs @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react' import { defaultConfig, getUserConfig } from '../config/index.mjs' import Browser from 'webextension-polyfill' +import { createConfigStorageListener } from './config-storage-listener.mjs' export function useConfig(initFn, ignoreSession = true) { const [config, setConfig] = useState(defaultConfig) @@ -11,20 +12,11 @@ export function useConfig(initFn, ignoreSession = true) { }) }, []) useEffect(() => { - const listener = (changes) => { - if (ignoreSession) if (Object.keys(changes).length === 1 && 'sessions' in changes) return - - const changedItems = Object.keys(changes) - let newConfig = {} - for (const key of changedItems) { - newConfig[key] = changes[key].newValue - } - setConfig({ ...config, ...newConfig }) - } + const listener = createConfigStorageListener(setConfig, ignoreSession) Browser.storage.local.onChanged.addListener(listener) return () => { Browser.storage.local.onChanged.removeListener(listener) } - }, [config]) + }, [ignoreSession]) return config } diff --git a/tests/unit/hooks/config-storage-listener.test.mjs b/tests/unit/hooks/config-storage-listener.test.mjs new file mode 100644 index 000000000..a2ceffecf --- /dev/null +++ b/tests/unit/hooks/config-storage-listener.test.mjs @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { createConfigStorageListener } from '../../../src/hooks/config-storage-listener.mjs' + +const collectUpdates = () => { + const updates = [] + return { + updates, + setConfig(update) { + updates.push(update) + }, + } +} + +test('storage changes merge against the latest queued config state', () => { + const { updates, setConfig } = collectUpdates() + const listener = createConfigStorageListener(setConfig) + + listener({ themeMode: { oldValue: 'light', newValue: 'dark' } }) + listener({ preferredLanguage: { oldValue: 'en', newValue: 'zh_TW' } }) + + assert.equal(updates.length, 2) + assert.equal(typeof updates[0], 'function') + assert.equal(typeof updates[1], 'function') + + const config = updates.reduce( + (currentConfig, update) => update(currentConfig), + { themeMode: 'light', preferredLanguage: 'en', modelName: 'chatgptFree35' }, + ) + + assert.deepEqual(config, { + themeMode: 'dark', + preferredLanguage: 'zh_TW', + modelName: 'chatgptFree35', + }) +}) + +test('session-only changes remain ignored by default', () => { + const { updates, setConfig } = collectUpdates() + const listener = createConfigStorageListener(setConfig) + + listener({ sessions: { oldValue: [], newValue: [{ id: 'session-1' }] } }) + + assert.deepEqual(updates, []) +}) + +test('mixed changes still update config when sessions are ignored', () => { + const { updates, setConfig } = collectUpdates() + const listener = createConfigStorageListener(setConfig) + + listener({ + sessions: { oldValue: [], newValue: [{ id: 'session-1' }] }, + themeMode: { oldValue: 'light', newValue: 'dark' }, + }) + + assert.equal(updates.length, 1) + assert.deepEqual(updates[0]({ themeMode: 'light' }), { + sessions: [{ id: 'session-1' }], + themeMode: 'dark', + }) +}) + +test('session-only changes update config when filtering is disabled', () => { + const { updates, setConfig } = collectUpdates() + const listener = createConfigStorageListener(setConfig, false) + const sessions = [{ id: 'session-1' }] + + listener({ sessions: { oldValue: [], newValue: sessions } }) + + assert.equal(updates.length, 1) + assert.deepEqual(updates[0]({ themeMode: 'light' }), { + themeMode: 'light', + sessions, + }) +})