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
+104 -14
View File
@@ -8,15 +8,18 @@ type GlobalChangeListener = (newValue: any, oldValue: any, key: string) => void;
class StorageManager {
private static instance: StorageManager;
private data: SettingsState;
private listeners: { [key: string]: ChangeListener[] };
private globalListeners: GlobalChangeListener[];
private listeners: Map<string, Set<ChangeListener>>;
private globalListeners: Set<GlobalChangeListener>;
private subscribers: Set<Subscriber<SettingsState>> = new Set();
private saveTimeout: NodeJS.Timeout | null = null;
private pendingSave = false;
private initialized = false;
private constructor() {
this.data = {} as SettingsState;
this.listeners = {};
this.globalListeners = [];
this.loadFromStorage();
this.listeners = new Map();
this.globalListeners = new Set();
// Don't call async loadFromStorage in constructor
const handler: ProxyHandler<StorageManager> = {
get: (target, prop: keyof SettingsState | "register" | "initialize") => {
@@ -26,8 +29,21 @@ class StorageManager {
return Reflect.get(target.data, prop);
},
set: (target, prop: keyof SettingsState, value) => {
const oldValue = target.data[prop];
// 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;
},
deleteProperty: (target, prop: keyof SettingsState) => {
@@ -35,8 +51,9 @@ class StorageManager {
if (oldValue !== undefined) {
delete target.data[prop];
target.removeFromStorage(prop);
if (target.listeners[prop]) {
for (const listener of target.listeners[prop]) {
const listeners = target.listeners.get(prop as string);
if (listeners) {
for (const listener of listeners) {
listener(undefined, oldValue);
}
}
@@ -59,7 +76,10 @@ class StorageManager {
public static async initialize(): Promise<StorageManager & SettingsState> {
const instance = StorageManager.getInstance();
if (!instance.initialized) {
await instance.loadFromStorage();
instance.initialized = true;
}
return instance;
}
@@ -67,8 +87,19 @@ class StorageManager {
key: K,
value: SettingsState[K],
): void {
const oldValue = this.data[key];
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> {
@@ -79,9 +110,34 @@ class StorageManager {
}
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
await browser.storage.local.set(this.data);
this.notifySubscribers();
this.pendingSave = false;
}
private async removeFromStorage(key: string): Promise<void> {
@@ -91,22 +147,38 @@ class StorageManager {
private initStorageListener(): void {
browser.storage.onChanged.addListener((changes, areaName) => {
if (areaName === "local") {
const actualChanges: string[] = [];
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
// Only process if value actually changed
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
if (newValue !== undefined) {
(this.data as any)[key] = newValue;
} else {
delete (this.data as any)[key];
}
if (this.listeners[key]) {
for (const listener of this.listeners[key]) {
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 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)
*/
public register(prop: keyof SettingsState, listener: ChangeListener): void {
if (!this.listeners[prop]) {
this.listeners[prop] = [];
const key = prop as string;
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.
* @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 {
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);
}
/**
+28 -23
View File
@@ -46,16 +46,16 @@ export class StorageChangeHandler {
newValue: CustomShortcut[],
oldValue: CustomShortcut[],
) {
if (newValue) {
if (!newValue || !oldValue) return;
if (newValue.length > oldValue.length) {
// 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: any) =>
!newValue.some(
(newItem: any) =>
JSON.stringify(oldItem) === JSON.stringify(newItem),
),
(oldItem) => !newSet.has(JSON.stringify(oldItem))
);
if (removedElement) {
@@ -63,36 +63,41 @@ export class StorageChangeHandler {
}
}
}
}
private handleShortcutsChange(
newValue: { enabled: boolean; name: string }[],
oldValue: { enabled: boolean; name: string }[],
) {
const addedShortcuts = newValue.filter((newItem: any) => {
const wasDisabledAndNowEnabled = oldValue.some((oldItem: any) => {
return oldItem.name === newItem.name && !oldItem.enabled && newItem.enabled;
});
if (!newValue || !oldValue) return;
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) => {
const isRemoved = oldValue.some((oldItem: any) => {
const match = oldItem.name === newItem.name;
const wasEnabled = oldItem.enabled;
const isDisabled = !newItem.enabled;
return match && wasEnabled && isDisabled;
});
// Check for changes in shortcuts
for (const newItem of newValue) {
const oldEnabled = oldMap.get(newItem.name);
return isRemoved;
});
// Newly enabled shortcuts
if (newItem.enabled && (oldEnabled === undefined || !oldEnabled)) {
addedShortcuts.push(newItem);
}
// Newly disabled shortcuts
if (!newItem.enabled && oldEnabled === true) {
removedShortcuts.push(newItem);
}
}
if (addedShortcuts.length > 0) {
addShortcuts(addedShortcuts);
}
if (removedShortcuts.length > 0) {
RemoveShortcutDiv(removedShortcuts);
}
}
private handleTransparencyEffectsChange(newValue: boolean) {
if (newValue) {