Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/popup/site-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,15 @@

async function persist(nextLevels) {
const normalized = normalizeLevels(nextLevels);
await storage.setSiteLevels(normalized);
cache = normalized;
return getCache();
const previousCache = cache;
try {
await storage.setSiteLevels(normalized);
cache = normalized;
return getCache();
} catch (error) {
cache = previousCache;
throw error;
Comment on lines 37 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Deferring cache update drops concurrent site changes

The new persist postpones cache assignment until setSiteLevels resolves and restores the previous object on failure. Because upsert/remove copy from cache to build the next payload, any second call invoked while the first write is still pending will read the stale pre‑update cache. If both promises resolve successfully, whichever finishes last will overwrite the stored levels and the in‑memory cache with only its own changes, silently discarding the earlier update (e.g. quickly editing two different hosts in the manager). The previous implementation updated cache optimistically so subsequent calls merged pending changes; this change reintroduces lost updates unless writes are serialized or merged explicitly.

Useful? React with 👍 / 👎.

}
}

async function upsert(host, level) {
Expand Down
51 changes: 50 additions & 1 deletion tests/storage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ let storageGet;
let nextResult;
let nextError;
let windowStub;
let siteStorage;
const noopSetSiteLevels = async () => {};

function clone(obj) {
if (typeof obj === 'undefined') {
Expand All @@ -23,7 +25,8 @@ before(async () => {
SITE_KEY: globalThis.SITE_KEY,
SCHEDULE_KEY: globalThis.SCHEDULE_KEY,
chrome: globalThis.chrome,
ScreenDimmerStorage: globalThis.ScreenDimmerStorage
ScreenDimmerStorage: globalThis.ScreenDimmerStorage,
ScreenDimmerSiteStorage: globalThis.ScreenDimmerSiteStorage
};

windowStub = {
Expand Down Expand Up @@ -78,11 +81,21 @@ before(async () => {
await import('../src/shared/storage.js');
storageGet = globalThis.window.ScreenDimmerStorage.storageGet;
globalThis.ScreenDimmerStorage = globalThis.window.ScreenDimmerStorage;

await import('../src/popup/site-storage.js');
siteStorage = globalThis.window.ScreenDimmerSiteStorage;
globalThis.ScreenDimmerSiteStorage = siteStorage;
});

beforeEach(() => {
nextResult = { sync: undefined, local: undefined };
nextError = { sync: null, local: null };
if (siteStorage) {
siteStorage.setCache({});
}
if (globalThis.ScreenDimmerStorage) {
globalThis.ScreenDimmerStorage.setSiteLevels = noopSetSiteLevels;
}
});

after(() => {
Expand Down Expand Up @@ -149,3 +162,39 @@ test('getSchedule falls back to local copy when sync is missing', async () => {
assert.equal('enabled' in result.rules[0], false);
assert.equal('location' in result, false);
});

test('site storage cache updates only after persistence succeeds', async () => {
siteStorage.setCache({ 'example.com': 0.2 });

let resolvePersist;
const pending = new Promise((resolve) => {
resolvePersist = resolve;
});
const capturedLevels = [];

globalThis.ScreenDimmerStorage.setSiteLevels = async (levels) => {
capturedLevels.push(levels);
return pending;
};

const persistPromise = siteStorage.upsert('new.example.com', 0.6);

assert.deepEqual(siteStorage.getCache(), { 'example.com': 0.2 });
resolvePersist();
await persistPromise;

assert.deepEqual(capturedLevels, [{ 'example.com': 0.2, 'new.example.com': 0.6 }]);
assert.deepEqual(siteStorage.getCache(), { 'example.com': 0.2, 'new.example.com': 0.6 });
});

test('site storage restores previous cache when persistence fails', async () => {
siteStorage.setCache({ 'existing.com': 0.4 });
const error = new Error('persist failed');

globalThis.ScreenDimmerStorage.setSiteLevels = async () => {
throw error;
};

await assert.rejects(siteStorage.remove('existing.com'), /persist failed/);
assert.deepEqual(siteStorage.getCache(), { 'existing.com': 0.4 });
});