perf: settingstate caching improvements

This commit is contained in:
SethBurkart123
2025-08-15 10:44:14 +10:00
parent f3048d0cae
commit a396aa8a9d
2 changed files with 157 additions and 62 deletions
+116 -26
View File
@@ -8,15 +8,18 @@ type GlobalChangeListener = (newValue: any, oldValue: any, key: string) => void;
class StorageManager { class StorageManager {
private static instance: StorageManager; private static instance: StorageManager;
private data: SettingsState; private data: SettingsState;
private listeners: { [key: string]: ChangeListener[] }; private listeners: Map<string, Set<ChangeListener>>;
private globalListeners: GlobalChangeListener[]; private globalListeners: Set<GlobalChangeListener>;
private subscribers: Set<Subscriber<SettingsState>> = new Set(); private subscribers: Set<Subscriber<SettingsState>> = new Set();
private saveTimeout: NodeJS.Timeout | null = null;
private pendingSave = false;
private initialized = false;
private constructor() { private constructor() {
this.data = {} as SettingsState; this.data = {} as SettingsState;
this.listeners = {}; this.listeners = new Map();
this.globalListeners = []; this.globalListeners = new Set();
this.loadFromStorage(); // Don't call async loadFromStorage in constructor
const handler: ProxyHandler<StorageManager> = { const handler: ProxyHandler<StorageManager> = {
get: (target, prop: keyof SettingsState | "register" | "initialize") => { get: (target, prop: keyof SettingsState | "register" | "initialize") => {
@@ -26,8 +29,21 @@ class StorageManager {
return Reflect.get(target.data, prop); return Reflect.get(target.data, prop);
}, },
set: (target, prop: keyof SettingsState, value) => { set: (target, prop: keyof SettingsState, value) => {
Reflect.set(target.data, prop, value); const oldValue = target.data[prop];
target.saveToStorage();
// Only save if the value actually changed
if (oldValue !== value) {
Reflect.set(target.data, prop, value);
target.saveToStorage();
// Notify listeners immediately for responsiveness
const listeners = target.listeners.get(prop as string);
if (listeners) {
for (const listener of listeners) {
listener(value, oldValue);
}
}
}
return true; return true;
}, },
deleteProperty: (target, prop: keyof SettingsState) => { deleteProperty: (target, prop: keyof SettingsState) => {
@@ -35,8 +51,9 @@ class StorageManager {
if (oldValue !== undefined) { if (oldValue !== undefined) {
delete target.data[prop]; delete target.data[prop];
target.removeFromStorage(prop); target.removeFromStorage(prop);
if (target.listeners[prop]) { const listeners = target.listeners.get(prop as string);
for (const listener of target.listeners[prop]) { if (listeners) {
for (const listener of listeners) {
listener(undefined, oldValue); listener(undefined, oldValue);
} }
} }
@@ -59,7 +76,10 @@ class StorageManager {
public static async initialize(): Promise<StorageManager & SettingsState> { public static async initialize(): Promise<StorageManager & SettingsState> {
const instance = StorageManager.getInstance(); const instance = StorageManager.getInstance();
await instance.loadFromStorage(); if (!instance.initialized) {
await instance.loadFromStorage();
instance.initialized = true;
}
return instance; return instance;
} }
@@ -67,8 +87,19 @@ class StorageManager {
key: K, key: K,
value: SettingsState[K], value: SettingsState[K],
): void { ): void {
this.data[key] = value; const oldValue = this.data[key];
this.saveToStorage(); if (oldValue !== value) {
this.data[key] = value;
this.saveToStorage();
// Notify listeners
const listeners = this.listeners.get(key as string);
if (listeners) {
for (const listener of listeners) {
listener(value, oldValue);
}
}
}
} }
private async loadFromStorage(): Promise<void> { private async loadFromStorage(): Promise<void> {
@@ -79,9 +110,34 @@ class StorageManager {
} }
private async saveToStorage(): Promise<void> { private async saveToStorage(): Promise<void> {
// Clear any existing timeout
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
}
// Set a new timeout to batch changes
this.saveTimeout = setTimeout(async () => {
if (this.pendingSave) {
// @ts-expect-error
await browser.storage.local.set(this.data);
this.notifySubscribers();
this.pendingSave = false;
}
}, 100); // Adjust delay as needed
this.pendingSave = true;
}
// Add immediate save method for critical updates
public async saveImmediately(): Promise<void> {
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
this.saveTimeout = null;
}
// @ts-expect-error // @ts-expect-error
await browser.storage.local.set(this.data); await browser.storage.local.set(this.data);
this.notifySubscribers(); this.notifySubscribers();
this.pendingSave = false;
} }
private async removeFromStorage(key: string): Promise<void> { private async removeFromStorage(key: string): Promise<void> {
@@ -91,19 +147,35 @@ class StorageManager {
private initStorageListener(): void { private initStorageListener(): void {
browser.storage.onChanged.addListener((changes, areaName) => { browser.storage.onChanged.addListener((changes, areaName) => {
if (areaName === "local") { if (areaName === "local") {
const actualChanges: string[] = [];
for (const [key, { oldValue, newValue }] of Object.entries(changes)) { for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
if (newValue !== undefined) { // Only process if value actually changed
(this.data as any)[key] = newValue; if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
} else { if (newValue !== undefined) {
delete (this.data as any)[key]; (this.data as any)[key] = newValue;
} } else {
if (this.listeners[key]) { delete (this.data as any)[key];
for (const listener of this.listeners[key]) { }
listener(newValue, oldValue); actualChanges.push(key);
// Notify specific listeners
const listeners = this.listeners.get(key);
if (listeners) {
for (const listener of listeners) {
listener(newValue, oldValue);
}
} }
} }
}
// Only notify global listeners if there were actual changes
if (actualChanges.length > 0 && this.globalListeners.size > 0) {
for (const listener of this.globalListeners) { for (const listener of this.globalListeners) {
listener(newValue, oldValue, key); for (const key of actualChanges) {
const { oldValue, newValue } = changes[key];
listener(newValue, oldValue, key);
}
} }
} }
} }
@@ -116,18 +188,36 @@ class StorageManager {
* @param listener The listener to call when the setting changes -> takes two arguments, (newValue, oldValue) * @param listener The listener to call when the setting changes -> takes two arguments, (newValue, oldValue)
*/ */
public register(prop: keyof SettingsState, listener: ChangeListener): void { public register(prop: keyof SettingsState, listener: ChangeListener): void {
if (!this.listeners[prop]) { const key = prop as string;
this.listeners[prop] = []; if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
} }
this.listeners[prop].push(listener); this.listeners.get(key)!.add(listener);
}
/**
* Unregister a listener for a setting.
* @param prop The setting to stop listening to.
* @param listener The listener to remove.
*/
public unregister(prop: keyof SettingsState, listener: ChangeListener): void {
this.listeners.get(prop as string)?.delete(listener);
} }
/** /**
* Register a listener for any setting. * Register a listener for any setting.
* @param listener The listener to call when any setting changes -> takes two arguments, (newValue, oldValue) * @param listener The listener to call when any setting changes -> takes three arguments, (newValue, oldValue, key)
*/ */
public registerGlobal(listener: GlobalChangeListener): void { public registerGlobal(listener: GlobalChangeListener): void {
this.globalListeners.push(listener); this.globalListeners.add(listener);
}
/**
* Unregister a global listener.
* @param listener The listener to remove.
*/
public unregisterGlobal(listener: GlobalChangeListener): void {
this.globalListeners.delete(listener);
} }
/** /**
+37 -32
View File
@@ -46,21 +46,20 @@ export class StorageChangeHandler {
newValue: CustomShortcut[], newValue: CustomShortcut[],
oldValue: CustomShortcut[], oldValue: CustomShortcut[],
) { ) {
if (newValue) { if (!newValue || !oldValue) return;
if (newValue.length > oldValue.length) {
CreateCustomShortcutDiv(newValue[oldValue.length]);
} else if (newValue.length < oldValue.length) {
const removedElement = oldValue.find(
(oldItem: any) =>
!newValue.some(
(newItem: any) =>
JSON.stringify(oldItem) === JSON.stringify(newItem),
),
);
if (removedElement) { if (newValue.length > oldValue.length) {
RemoveShortcutDiv([removedElement]); // New shortcut added - add the last one
} CreateCustomShortcutDiv(newValue[oldValue.length]);
} else if (newValue.length < oldValue.length) {
// Shortcut removed - find which one was removed
const newSet = new Set(newValue.map(item => JSON.stringify(item)));
const removedElement = oldValue.find(
(oldItem) => !newSet.has(JSON.stringify(oldItem))
);
if (removedElement) {
RemoveShortcutDiv([removedElement]);
} }
} }
} }
@@ -69,29 +68,35 @@ export class StorageChangeHandler {
newValue: { enabled: boolean; name: string }[], newValue: { enabled: boolean; name: string }[],
oldValue: { enabled: boolean; name: string }[], oldValue: { enabled: boolean; name: string }[],
) { ) {
const addedShortcuts = newValue.filter((newItem: any) => { if (!newValue || !oldValue) return;
const wasDisabledAndNowEnabled = oldValue.some((oldItem: any) => {
return oldItem.name === newItem.name && !oldItem.enabled && newItem.enabled;
});
const isNewShortcut = !oldValue.some((oldItem: any) => oldItem.name === newItem.name); // Create map for faster lookup
const oldMap = new Map(oldValue.map(item => [item.name, item.enabled]));
return (wasDisabledAndNowEnabled || isNewShortcut) && newItem.enabled; const addedShortcuts: { enabled: boolean; name: string }[] = [];
}); const removedShortcuts: { enabled: boolean; name: string }[] = [];
const removedShortcuts = newValue.filter((newItem: any) => { // Check for changes in shortcuts
const isRemoved = oldValue.some((oldItem: any) => { for (const newItem of newValue) {
const match = oldItem.name === newItem.name; const oldEnabled = oldMap.get(newItem.name);
const wasEnabled = oldItem.enabled;
const isDisabled = !newItem.enabled;
return match && wasEnabled && isDisabled;
});
return isRemoved; // Newly enabled shortcuts
}); if (newItem.enabled && (oldEnabled === undefined || !oldEnabled)) {
addedShortcuts.push(newItem);
}
addShortcuts(addedShortcuts); // Newly disabled shortcuts
RemoveShortcutDiv(removedShortcuts); if (!newItem.enabled && oldEnabled === true) {
removedShortcuts.push(newItem);
}
}
if (addedShortcuts.length > 0) {
addShortcuts(addedShortcuts);
}
if (removedShortcuts.length > 0) {
RemoveShortcutDiv(removedShortcuts);
}
} }
private handleTransparencyEffectsChange(newValue: boolean) { private handleTransparencyEffectsChange(newValue: boolean) {