mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
perf: settingstate caching improvements
This commit is contained in:
@@ -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) => {
|
||||||
|
const oldValue = target.data[prop];
|
||||||
|
|
||||||
|
// Only save if the value actually changed
|
||||||
|
if (oldValue !== value) {
|
||||||
Reflect.set(target.data, prop, value);
|
Reflect.set(target.data, prop, value);
|
||||||
target.saveToStorage();
|
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();
|
||||||
|
if (!instance.initialized) {
|
||||||
await instance.loadFromStorage();
|
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 {
|
||||||
|
const oldValue = this.data[key];
|
||||||
|
if (oldValue !== value) {
|
||||||
this.data[key] = value;
|
this.data[key] = value;
|
||||||
this.saveToStorage();
|
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
|
// @ts-expect-error
|
||||||
await browser.storage.local.set(this.data);
|
await browser.storage.local.set(this.data);
|
||||||
this.notifySubscribers();
|
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> {
|
private async removeFromStorage(key: string): Promise<void> {
|
||||||
@@ -91,22 +147,38 @@ 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)) {
|
||||||
|
// Only process if value actually changed
|
||||||
|
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
||||||
if (newValue !== undefined) {
|
if (newValue !== undefined) {
|
||||||
(this.data as any)[key] = newValue;
|
(this.data as any)[key] = newValue;
|
||||||
} else {
|
} else {
|
||||||
delete (this.data as any)[key];
|
delete (this.data as any)[key];
|
||||||
}
|
}
|
||||||
if (this.listeners[key]) {
|
actualChanges.push(key);
|
||||||
for (const listener of this.listeners[key]) {
|
|
||||||
|
// Notify specific listeners
|
||||||
|
const listeners = this.listeners.get(key);
|
||||||
|
if (listeners) {
|
||||||
|
for (const listener of listeners) {
|
||||||
listener(newValue, oldValue);
|
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) {
|
||||||
|
for (const key of actualChanges) {
|
||||||
|
const { oldValue, newValue } = changes[key];
|
||||||
listener(newValue, oldValue, 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,16 +46,16 @@ export class StorageChangeHandler {
|
|||||||
newValue: CustomShortcut[],
|
newValue: CustomShortcut[],
|
||||||
oldValue: CustomShortcut[],
|
oldValue: CustomShortcut[],
|
||||||
) {
|
) {
|
||||||
if (newValue) {
|
if (!newValue || !oldValue) return;
|
||||||
|
|
||||||
if (newValue.length > oldValue.length) {
|
if (newValue.length > oldValue.length) {
|
||||||
|
// New shortcut added - add the last one
|
||||||
CreateCustomShortcutDiv(newValue[oldValue.length]);
|
CreateCustomShortcutDiv(newValue[oldValue.length]);
|
||||||
} else if (newValue.length < 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(
|
const removedElement = oldValue.find(
|
||||||
(oldItem: any) =>
|
(oldItem) => !newSet.has(JSON.stringify(oldItem))
|
||||||
!newValue.some(
|
|
||||||
(newItem: any) =>
|
|
||||||
JSON.stringify(oldItem) === JSON.stringify(newItem),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (removedElement) {
|
if (removedElement) {
|
||||||
@@ -63,36 +63,41 @@ export class StorageChangeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private handleShortcutsChange(
|
private handleShortcutsChange(
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Newly disabled shortcuts
|
||||||
|
if (!newItem.enabled && oldEnabled === true) {
|
||||||
|
removedShortcuts.push(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedShortcuts.length > 0) {
|
||||||
addShortcuts(addedShortcuts);
|
addShortcuts(addedShortcuts);
|
||||||
|
}
|
||||||
|
if (removedShortcuts.length > 0) {
|
||||||
RemoveShortcutDiv(removedShortcuts);
|
RemoveShortcutDiv(removedShortcuts);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private handleTransparencyEffectsChange(newValue: boolean) {
|
private handleTransparencyEffectsChange(newValue: boolean) {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
|
|||||||
Reference in New Issue
Block a user