Skip to content

Commit 2fbfb56

Browse files
SYM01claude
andcommitted
fix: propagate profile updates across extension contexts
`onProfileUpdate` was an in-memory callback list, so saves made from the popup/config page never reached the background. Alarm reconciliation therefore missed interval changes until the next service-worker wake. Route `onProfileUpdate` through `chrome.storage.local.onChanged` instead — the browser dispatches storage events in every extension context, so subscribers fire regardless of which context wrote. `overwriteProfiles` no longer fans out in-memory; the storage event does it. Added `onLocalStorageChanged` to the adapter layer (Chrome/Firefox wrap the native event; the WebBrowser dev adapter simulates it so local dev reactivity still works). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f6b3cb7 commit 2fbfb56

File tree

5 files changed

+46
-1
lines changed

5 files changed

+46
-1
lines changed

src/adapters/base.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ export abstract class BaseAdapter {
8080
return ret;
8181
}
8282

83+
// Fires when local storage changes in any extension context, including
84+
// this one — the browser dispatches the event everywhere.
85+
abstract onLocalStorageChanged(
86+
callback: (changes: Record<string, { newValue?: unknown }>) => void
87+
): void;
88+
8389
// proxy
8490
abstract setProxy(cfg: ProxyConfig): Promise<void>;
8591
abstract clearProxy(): Promise<void>;

src/adapters/chrome.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export class Chrome extends BaseAdapter {
3131
return ret[key] as T | undefined;
3232
}
3333

34+
onLocalStorageChanged(
35+
callback: (changes: Record<string, { newValue?: unknown }>) => void
36+
): void {
37+
chrome.storage.local.onChanged.addListener(callback);
38+
}
39+
3440
async setProxy(cfg: ProxyConfig): Promise<void> {
3541
await chrome.proxy.settings.set({
3642
value: cfg,

src/adapters/firefox.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ export class Firefox extends BaseAdapter {
2626
return ret[key];
2727
}
2828

29+
onLocalStorageChanged(
30+
callback: (changes: Record<string, { newValue?: unknown }>) => void
31+
): void {
32+
browser.storage.local.onChanged.addListener(callback);
33+
}
34+
2935
async setProxy(cfg: ProxyConfig): Promise<void> {
3036
const proxyCfg: browser.proxy.ProxyConfig = {};
3137

src/adapters/web.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,27 @@ export class WebBrowser extends BaseAdapter {
2828
return BrowserFlavor.Web;
2929
}
3030

31+
private storageListeners: ((
32+
changes: Record<string, { newValue?: unknown }>
33+
) => void)[] = [];
34+
3135
async set<T>(key: string, val: T): Promise<void> {
3236
localStorage.setItem(key, JSON.stringify(val));
37+
const changes = { [key]: { newValue: val } };
38+
this.storageListeners.forEach((cb) => cb(changes));
3339
}
3440
async get<T>(key: string): Promise<T | undefined> {
3541
let s: any;
3642
s = localStorage.getItem(key);
3743
return s && JSON.parse(s);
3844
}
3945

46+
onLocalStorageChanged(
47+
callback: (changes: Record<string, { newValue?: unknown }>) => void
48+
): void {
49+
this.storageListeners.push(callback);
50+
}
51+
4052
async setProxy(_: ProxyConfig): Promise<void> {
4153
window.localStorage.setItem("proxy", JSON.stringify(_));
4254
}

src/services/profile.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,20 +122,35 @@ export const SystemProfile: Record<string, ProxyProfile> = {
122122
const keyProfileStorage = "profiles";
123123
export type ProfilesStorage = Record<string, ProxyProfile>;
124124
const onProfileUpdateListeners: ((p: ProfilesStorage) => void)[] = [];
125+
let storageListenerRegistered = false;
125126

126127
// list all user defined profiles. System profiles are not included
127128
export async function listProfiles(): Promise<ProfilesStorage> {
128129
const s = await Host.get<ProfilesStorage>(keyProfileStorage);
129130
return s || {};
130131
}
131132

133+
/**
134+
* Subscribe to profile updates. Fires in every extension context (popup,
135+
* config page, background) whenever the profiles storage key is written,
136+
* regardless of which context made the write — backed by the browser's
137+
* storage change events.
138+
*/
132139
export function onProfileUpdate(callback: (p: ProfilesStorage) => void) {
140+
if (!storageListenerRegistered) {
141+
storageListenerRegistered = true;
142+
Host.onLocalStorageChanged((changes) => {
143+
if (!(keyProfileStorage in changes)) return;
144+
const profiles = (changes[keyProfileStorage].newValue ??
145+
{}) as ProfilesStorage;
146+
onProfileUpdateListeners.forEach((cb) => cb(profiles));
147+
});
148+
}
133149
onProfileUpdateListeners.push(callback);
134150
}
135151

136152
async function overwriteProfiles(profiles: ProfilesStorage) {
137153
await Host.set(keyProfileStorage, deepClone(profiles));
138-
onProfileUpdateListeners.forEach((cb) => cb(profiles));
139154
}
140155

141156
/**

0 commit comments

Comments
 (0)