From 4000dda49c351b5c59290852a9e3f0488f0d540d Mon Sep 17 00:00:00 2001 From: Saba Date: Wed, 11 Sep 2024 21:26:29 +0300 Subject: [PATCH 1/6] Feat: Chrome-backed svelte stores --- src/background/index.ts | 5 ++- src/components/Options.svelte | 39 +++++------------- src/components/Overlay.svelte | 9 +---- src/content/index.ts | 5 ++- src/options/index.ts | 10 ++--- src/popup/index.ts | 10 ++--- src/sidepanel/index.ts | 10 ++--- src/storage.ts | 75 +++++++++++++++++++++++++++++------ 8 files changed, 90 insertions(+), 73 deletions(-) diff --git a/src/background/index.ts b/src/background/index.ts index f6e8416..41a5349 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,10 +1,11 @@ -import { storage } from "../storage"; +import { get } from "svelte/store"; +import { count } from "../storage"; // Background service workers // https://developer.chrome.com/docs/extensions/mv3/service_workers/ chrome.runtime.onInstalled.addListener(() => { - storage.get().then(console.log); + console.log(get(count)); }); // NOTE: If you want to toggle the side panel from the extension's action button, diff --git a/src/components/Options.svelte b/src/components/Options.svelte index de2b5d6..47e2c17 100644 --- a/src/components/Options.svelte +++ b/src/components/Options.svelte @@ -1,35 +1,19 @@
-

Current count: {count}

+

Current count: {$count}

- - - - {#if successMessage}{successMessage}{/if} + +
@@ -52,9 +36,4 @@ button:focus { background-color: #27ae60; } - - .success { - color: #2ecc71; - font-weight: bold; - } diff --git a/src/components/Overlay.svelte b/src/components/Overlay.svelte index 3b0f287..b2dbbb8 100644 --- a/src/components/Overlay.svelte +++ b/src/components/Overlay.svelte @@ -1,13 +1,6 @@
diff --git a/src/content/index.ts b/src/content/index.ts index b3e194d..5dfc43d 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -1,5 +1,6 @@ +import { get } from "svelte/store"; import Overlay from "../components/Overlay.svelte"; -import { storage } from "../storage"; +import { count } from "../storage"; // Content scripts // https://developer.chrome.com/docs/extensions/mv3/content_scripts/ @@ -8,7 +9,7 @@ import { storage } from "../storage"; import "./styles.css"; // Some JS on the page -storage.get().then(console.log); +console.log(`CONTENT: ${get(count)}`); // Some svelte component on the page new Overlay({ target: document.body }); diff --git a/src/options/index.ts b/src/options/index.ts index 2d2a7c1..de5fb02 100644 --- a/src/options/index.ts +++ b/src/options/index.ts @@ -1,5 +1,5 @@ import Options from "../components/Options.svelte"; -import { storage } from "../storage"; +import { count } from "../storage"; // Options // https://developer.chrome.com/docs/extensions/mv3/options/ @@ -8,11 +8,9 @@ function render() { const target = document.getElementById("app"); if (target) { - storage.get().then(({ count }) => { - new Options({ - target, - props: { count }, - }); + new Options({ + target, + props: { count }, }); } } diff --git a/src/popup/index.ts b/src/popup/index.ts index 4a00ba0..d9d125a 100644 --- a/src/popup/index.ts +++ b/src/popup/index.ts @@ -1,5 +1,5 @@ import Options from "../components/Options.svelte"; -import { storage } from "../storage"; +import { count } from "../storage"; // Action popup // https://developer.chrome.com/docs/extensions/reference/action/ @@ -8,11 +8,9 @@ function render() { const target = document.getElementById("app"); if (target) { - storage.get().then(({ count }) => { - new Options({ - target, - props: { count }, - }); + new Options({ + target, + props: { count }, }); } } diff --git a/src/sidepanel/index.ts b/src/sidepanel/index.ts index 1fffe4e..f8df213 100644 --- a/src/sidepanel/index.ts +++ b/src/sidepanel/index.ts @@ -1,5 +1,5 @@ import Options from "../components/Options.svelte"; -import { storage } from "../storage"; +import { count } from "../storage"; // Side panel // https://developer.chrome.com/docs/extensions/reference/sidePanel/ @@ -8,11 +8,9 @@ function render() { const target = document.getElementById("app"); if (target) { - storage.get().then(({ count }) => { - new Options({ - target, - props: { count }, - }); + new Options({ + target, + props: { count }, }); } } diff --git a/src/storage.ts b/src/storage.ts index 015c59d..818ec50 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,13 +1,62 @@ -type IStorage = { - count: number; -}; - -const defaultStorage: IStorage = { - count: 0, -}; - -export const storage = { - get: (): Promise => - chrome.storage.sync.get(defaultStorage) as Promise, - set: (value: IStorage): Promise => chrome.storage.sync.set(value), -}; +import { writable, type Writable } from 'svelte/store'; + +/** + * Creates a persistent Svelte store backed by Chrome's sync storage. + * @template T The type of the store's value + * @param key The key to use in Chrome's storage + * @param initialValue The initial value of the store + * @returns A writable Svelte store + */ +export function persistentStore(key: string, initialValue: T): Writable { + const store = writable(initialValue); + let isUpdatingFromChrome = false; + let isUpdatingFromStore = false; + const debug = true; + + function watchStore() { + if (debug) console.log("STORE subscribe"); + store.subscribe((value) => { + if (debug) console.log(`[${String(!isUpdatingFromChrome).toUpperCase()}] svelte => chrome ${value}`); + // Prevent circular updates + if (isUpdatingFromChrome) return; + + isUpdatingFromStore = true; + chrome.storage.sync.set({ [key]: value }).then(() => { + isUpdatingFromStore = false; + }); + if (debug) console.log(`[END] svelte => chrome ${value}`); + }); + } + + function watchChrome() { + if (debug) console.log("CHROME subscribe"); + chrome.storage.onChanged.addListener((changes, namespace) => { + if (debug) console.log(`[${String(!isUpdatingFromStore).toUpperCase()}] chrome => svelte ${changes[key].newValue}`); + + // Prevent circular updates + if (isUpdatingFromStore || namespace !== 'sync' || !(Object.hasOwn(changes, key))) return; + isUpdatingFromChrome = true; + store.set(changes[key].newValue); + isUpdatingFromChrome = false; + + if (debug) console.log(`[END] chrome => svelte ${changes[key].newValue}`); + }); + } + + // Initialize the store with the value from Chrome storage + chrome.storage.sync.get(key).then((result) => { + let value = Object.hasOwn(result, key) ? result[key] : initialValue; + if (!(Object.hasOwn(result, key))) { + console.log(`Persistent store: couldn't find key [${key}] in chrome storage. Default to initial value [${initialValue}]`) + } + if (debug) console.log(`[START] storage.sync.get => ${Object.hasOwn(result, key)}: ${value}`); + store.set(value); + watchStore(); + watchChrome(); + if (debug) console.log(`[END] storage.sync.get => ${Object.hasOwn(result, key)}: ${value}`); + }); + + return store; +} + +export const count = persistentStore("count", 10); From 299f691fbc421845835a2765e29a6dcf69b532b1 Mon Sep 17 00:00:00 2001 From: Saba Date: Wed, 11 Sep 2024 21:34:48 +0300 Subject: [PATCH 2/6] Remove debug messages - cleaner code --- src/storage.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/storage.ts b/src/storage.ts index 818ec50..c06fa24 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -11,35 +11,25 @@ export function persistentStore(key: string, initialValue: T): Writable { const store = writable(initialValue); let isUpdatingFromChrome = false; let isUpdatingFromStore = false; - const debug = true; function watchStore() { - if (debug) console.log("STORE subscribe"); store.subscribe((value) => { - if (debug) console.log(`[${String(!isUpdatingFromChrome).toUpperCase()}] svelte => chrome ${value}`); // Prevent circular updates if (isUpdatingFromChrome) return; - isUpdatingFromStore = true; chrome.storage.sync.set({ [key]: value }).then(() => { isUpdatingFromStore = false; }); - if (debug) console.log(`[END] svelte => chrome ${value}`); }); } function watchChrome() { - if (debug) console.log("CHROME subscribe"); chrome.storage.onChanged.addListener((changes, namespace) => { - if (debug) console.log(`[${String(!isUpdatingFromStore).toUpperCase()}] chrome => svelte ${changes[key].newValue}`); - // Prevent circular updates if (isUpdatingFromStore || namespace !== 'sync' || !(Object.hasOwn(changes, key))) return; isUpdatingFromChrome = true; store.set(changes[key].newValue); isUpdatingFromChrome = false; - - if (debug) console.log(`[END] chrome => svelte ${changes[key].newValue}`); }); } @@ -49,11 +39,9 @@ export function persistentStore(key: string, initialValue: T): Writable { if (!(Object.hasOwn(result, key))) { console.log(`Persistent store: couldn't find key [${key}] in chrome storage. Default to initial value [${initialValue}]`) } - if (debug) console.log(`[START] storage.sync.get => ${Object.hasOwn(result, key)}: ${value}`); store.set(value); watchStore(); watchChrome(); - if (debug) console.log(`[END] storage.sync.get => ${Object.hasOwn(result, key)}: ${value}`); }); return store; From 3ae25ff53e1ac19232dea8834409c20db01d6d1a Mon Sep 17 00:00:00 2001 From: Saba Date: Tue, 17 Sep 2024 01:41:13 +0300 Subject: [PATCH 3/6] Manage update strategy with queues --- src/storage.ts | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/storage.ts b/src/storage.ts index c06fa24..5c387e7 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -9,36 +9,45 @@ import { writable, type Writable } from 'svelte/store'; */ export function persistentStore(key: string, initialValue: T): Writable { const store = writable(initialValue); - let isUpdatingFromChrome = false; - let isUpdatingFromStore = false; + let storeValues: T[] = []; + let chromeValues: T[] = []; function watchStore() { store.subscribe((value) => { // Prevent circular updates - if (isUpdatingFromChrome) return; - isUpdatingFromStore = true; - chrome.storage.sync.set({ [key]: value }).then(() => { - isUpdatingFromStore = false; - }); + if (chromeValues && value === chromeValues[0]) { + chromeValues.shift(); + return; + } + + storeValues.push(value); + chrome.storage.sync.set({ [key]: value }); }); } function watchChrome() { - chrome.storage.onChanged.addListener((changes, namespace) => { - // Prevent circular updates - if (isUpdatingFromStore || namespace !== 'sync' || !(Object.hasOwn(changes, key))) return; - isUpdatingFromChrome = true; - store.set(changes[key].newValue); - isUpdatingFromChrome = false; + chrome.storage.sync.onChanged.addListener((changes) => { + if (!(Object.hasOwn(changes, key))) return; + + const value = changes[key].newValue as T; + console.log(`watchChrome got ${value}`); + if (storeValues && value === storeValues[0]) { + storeValues.shift(); + return; + } + + chromeValues.push(value); + store.set(value); }); } // Initialize the store with the value from Chrome storage chrome.storage.sync.get(key).then((result) => { let value = Object.hasOwn(result, key) ? result[key] : initialValue; - if (!(Object.hasOwn(result, key))) { + if (!Object.hasOwn(result, key)) { console.log(`Persistent store: couldn't find key [${key}] in chrome storage. Default to initial value [${initialValue}]`) } + chromeValues.push(value); store.set(value); watchStore(); watchChrome(); From 95dcf676b494ca97191485bf888546c6199bdc23 Mon Sep 17 00:00:00 2001 From: The Brogrammer Date: Tue, 17 Sep 2024 11:31:30 +0300 Subject: [PATCH 4/6] Apply suggestions from code review Remove debug prints Co-authored-by: Nikita --- src/storage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/storage.ts b/src/storage.ts index 5c387e7..d510507 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -30,7 +30,6 @@ export function persistentStore(key: string, initialValue: T): Writable { if (!(Object.hasOwn(changes, key))) return; const value = changes[key].newValue as T; - console.log(`watchChrome got ${value}`); if (storeValues && value === storeValues[0]) { storeValues.shift(); return; From 38ebdd663b48ba8032e8718014c8c84a6a3ac621 Mon Sep 17 00:00:00 2001 From: Saba Date: Tue, 17 Sep 2024 11:38:14 +0300 Subject: [PATCH 5/6] Check for arrays not empty --- src/storage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storage.ts b/src/storage.ts index d510507..871645d 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -15,7 +15,7 @@ export function persistentStore(key: string, initialValue: T): Writable { function watchStore() { store.subscribe((value) => { // Prevent circular updates - if (chromeValues && value === chromeValues[0]) { + if (chromeValues.length > 0 && value === chromeValues[0]) { chromeValues.shift(); return; } @@ -30,7 +30,7 @@ export function persistentStore(key: string, initialValue: T): Writable { if (!(Object.hasOwn(changes, key))) return; const value = changes[key].newValue as T; - if (storeValues && value === storeValues[0]) { + if (storeValues.length > 0 && value === storeValues[0]) { storeValues.shift(); return; } From 52dbb668f9127386bffc6a2735bf8f86841e577e Mon Sep 17 00:00:00 2001 From: Saba Date: Tue, 17 Sep 2024 13:19:31 +0300 Subject: [PATCH 6/6] Rename value queue --- src/storage.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/storage.ts b/src/storage.ts index 871645d..5265f4c 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -9,18 +9,18 @@ import { writable, type Writable } from 'svelte/store'; */ export function persistentStore(key: string, initialValue: T): Writable { const store = writable(initialValue); - let storeValues: T[] = []; - let chromeValues: T[] = []; + // Ensure each value is updated exactly once in store and in chrome storage + let storeValueQueue: T[] = []; + let chromeValueQueue: T[] = []; function watchStore() { store.subscribe((value) => { - // Prevent circular updates - if (chromeValues.length > 0 && value === chromeValues[0]) { - chromeValues.shift(); + if (chromeValueQueue.length > 0 && value === chromeValueQueue[0]) { + chromeValueQueue.shift(); return; } - storeValues.push(value); + storeValueQueue.push(value); chrome.storage.sync.set({ [key]: value }); }); } @@ -30,12 +30,12 @@ export function persistentStore(key: string, initialValue: T): Writable { if (!(Object.hasOwn(changes, key))) return; const value = changes[key].newValue as T; - if (storeValues.length > 0 && value === storeValues[0]) { - storeValues.shift(); + if (storeValueQueue.length > 0 && value === storeValueQueue[0]) { + storeValueQueue.shift(); return; } - chromeValues.push(value); + chromeValueQueue.push(value); store.set(value); }); } @@ -46,7 +46,7 @@ export function persistentStore(key: string, initialValue: T): Writable { if (!Object.hasOwn(result, key)) { console.log(`Persistent store: couldn't find key [${key}] in chrome storage. Default to initial value [${initialValue}]`) } - chromeValues.push(value); + chromeValueQueue.push(value); store.set(value); watchStore(); watchChrome(); @@ -55,4 +55,4 @@ export function persistentStore(key: string, initialValue: T): Writable { return store; } -export const count = persistentStore("count", 10); +export const count = persistentStore("count", 10); \ No newline at end of file