## 🎯 Cel Dodanie pełnego wsparcia dla Chrome Manifest V3 przy zachowaniu kompatybilności z Firefox. Główne wyzwanie: Chrome MV3 używa service workerów zamiast persistent background pages, co wymaga nowej architektury zarządzania pamięcią. --- ## 🏗️ Architektura ### Nowa warstwa abstrakcji: lib/browser-api/ **lib/browser-api/index.ts** - Główny punkt wejścia do zunifikowanego API - Wybiera właściwą implementację na podstawie zmiennej TARGET (build-time) - Eksportuje jeden interfejs dla całej aplikacji **lib/browser-api/types.ts** - Wspólne definicje typów dla obu przeglądarek - Interfejs BrowserAPI definiujący wszystkie potrzebne metody - Typy dla tabs, badge, webRequest, cookies, extension, windows **lib/browser-api/firefox.ts** - Adapter dla Firefox browser.* API - Lazy access do globalnego obiektu browser (bezpieczne dla środowisk bez Firefox API) - Wszystkie metody zwracają Promise lub są no-op jeśli API niedostępne **lib/browser-api/chrome.ts** - Adapter dla Chrome chrome.* API - Mapowanie chrome.action → badge (różnica nazewnictwa) - Ochrona przed błędami gdy karta zostaje zamknięta (try-catch w operacjach badge) --- ## 🔧 Build System ### esbuild.config.js - **Dodano**: Rozpoznawanie zmiennej środowiskowej TARGET (firefox/chrome) - **Dodano**: Różne katalogi wyjściowe (dist-firefox, dist-chrome) - **Dodano**: Kopiowanie odpowiedniego manifestu na podstawie TARGET - **Dodano**: Plugin do konwersji ikon SVG → PNG dla Chrome (wymaga PNG w MV3) - **Zmieniono**: define zawiera teraz process.env.TARGET dostępny w runtime ### package.json - **Dodano**: Skrypty build:firefox, build:chrome, watch:firefox, watch:chrome - **Dodano**: Skrypty build-addon:firefox, build-addon:chrome do tworzenia paczek - **Dodano**: Skrypt convert-icons do generowania PNG z SVG - **Dodano**: Zależność sharp do konwersji obrazów ### scripts/convert-icons.js (NOWY PLIK) - Konwertuje assets/icon-addon.svg → PNG w rozmiarach 16, 32, 48, 128px - Wymagane dla Chrome (MV3 nie akceptuje SVG w manifestach) ### manifest-chrome.json (NOWY PLIK) - Manifest V3 dla Chrome - background.service_worker zamiast background.scripts - action zamiast browser_action - Ikony PNG zamiast SVG - host_permissions zamiast embedowanych w permissions --- ## 🧠 Pamięć i Stan (NAJWIĘKSZA ZMIANA) ### memory.ts - Kompletna refaktoryzacja **Problem Chrome MV3:** - Service worker może być wyładowany w dowolnym momencie - Brak dostępu do window.memory z popup/sidebar - chrome.extension.getBackgroundPage() zwraca null w MV3 **Rozwiązanie:** 1. **Service Worker**: Trzyma pełne dane, synchronizuje do chrome.storage.session 2. **Popup/Sidebar**: Tworzy własną instancję Memory czytając z storage 3. **Throttled sync**: Maksymalnie co 500ms zapisy do storage (wydajność) **Nowa klasa: CachedRequestCluster** - Dziedziczy po RequestCluster ale NIE ma rzeczywistych requestów - Przechowuje tylko metadane: hasCookies, exposesOrigin, hasMarks - Implementuje wszystkie wymagane metody zwracając cached state - Używana TYLKO w popup/report window w Chrome **Zmiany w Memory klasie:** - **Dodano**: isReady flag i readyPromise dla async inicjalizacji (Chrome) - **Dodano**: waitUntilReady() - popup musi poczekać na załadowanie danych - **Dodano**: tabUrls: Map<number, string> - tracking pełnych URL dla Chrome (service worker nie dostaje documentUrl) - **Dodano**: scheduleSyncToStorage() - throttled sync do storage - **Dodano**: syncToStorage() - serializacja clustrów do JSON - **Dodano**: loadFromStorage() - deserializacja przy starcie popup - **Zmieniono**: register() śledzi main_frame URL i synuje po każdej zmianie - **Zmieniono**: Badge operacje w try-catch (karta może być zamknięta) **Funkcja getMemory():** - **Firefox**: browserAPI.extension.getBackgroundPage().memory (tradycyjnie) - **Chrome Service Worker**: self.memory (jesteśmy W service workerze) - **Chrome Popup**: Tworzy NOWĄ instancję czytając z storage (cachowana jako popupMemoryInstance) --- ## 🔒 Bezpieczeństwo i Obsługa Błędów ### util.ts - getshorthost() - **Dodano**: Walidacja wejścia (null, undefined, pusty string) - **Dodano**: Czyszczenie URL (protokół, ścieżka, port) - **Dodano**: Obsługa edge cases (localhost, single word domains) - **Dodano**: Bezpieczne fallbacki zamiast crashowania - **Dodano**: Console.warn zamiast milczących błędów ### extended-request.ts - **MASYWNE POPRAWKI** parsowania URL w konstruktorze - **Dodano**: isValidHttpUrl() helper - sprawdza czy URL zaczyna się od http(s) - **Dodano**: safeParseUrl() helper - try-catch wokół new URL() - **Dodano**: Próba parsowania wielu URL w kolejności priorytetów - **Dodano**: Obsługa Chrome MV3 initiator property - **Dodano**: Bezpieczne defaulty gdy parsowanie się nie uda (unknown://unknown) - **Zmieniono**: isThirdParty() pomija requesty z unparseable URLs - **Dodano**: uint8ArrayToString() - chunked konwersja dużych arrayów (zapobiega stack overflow) - **Zmieniono**: Request body processing używa chunked konwersji --- ## 🎨 UI Components ### toolbar.tsx (popup) - **Dodano**: getCurrentTab() z retry mechanism (Chrome czasem nie zwraca karty od razu) - **Dodano**: Sprawdzanie memoryReady przed renderowaniem danych - **Dodano**: Wywołanie waitUntilReady() w useEffect - **Dodano**: Opóźnienie 200ms dla Chrome przy inicjalizacji (service worker + storage delay) - **Dodano**: Graceful handling gdy popup otwarty bez active tab ### sidebar.tsx - **Dodano**: Stan memoryReady i loading screen dla Chrome - **Dodano**: Wywołanie waitUntilReady() przed dostępem do danych - **Dodano**: Conditional rendering - pokazuje Ładowanie... gdy pamięć nie gotowa ### report-window.tsx - **Dodano**: Stan memoryReady i loading message - **Dodano**: Wywołanie waitUntilReady() przed generowaniem raportu - **Dodano**: Fallback konstruowania URL z origin gdy brak visited_url - **Zmieniono**: Filtr clustrów używa hasMarks() zamiast getMarkedRequests().length ### stolen-data-cluster.tsx - **Bez znaczących zmian** - działa z abstrakcją RequestCluster --- ## 🐛 Poprawki Bugów ### background.ts - **Dodano**: Diagnostic logging do debugowania inicjalizacji - **Dodano**: Try-catch wokół init() z error logging - **Dodano**: Różne logi dla Firefox vs Chrome ### memory.ts - badge operations - **Dodano**: Try-catch wokół wszystkich operacji badge - Zapobiega crashowaniu gdy użytkownik zamknie kartę podczas operacji ### chrome.ts - badge adapter - **Dodano**: Try-catch w setBadgeText, setTitle, setBadgeBackgroundColor - Chrome rzuca błędy gdy operujemy na zamkniętych kartach --- ## 📝 Workflow Użytkownika (Chrome) 1. **Użytkownik odwiedza stronę** → Service worker rejestruje requesty → Auto-mark podejrzanych → Sync do storage 2. **Użytkownik otwiera popup** → Tworzy Memory → Czyta ze storage → Pokazuje dane z flagami 3. **Użytkownik (od)zaznacza domeny** → Zmienia flagi → Sync do storage 4. **Użytkownik generuje raport** → Otwiera report-window → Czyta ze storage → Filtruje według hasMarks() --- ## ✅ Rezultat - ✅ **Firefox**: Działa jak wcześniej (background page + window.memory) - ✅ **Chrome**: Pełne wsparcie MV3 (service worker + storage.session) - ✅ **Wspólny kod**: 95% kodu jest shared, tylko warstwa dostępu do API różni się - ✅ **Bezpieczeństwo**: Obsługa wszystkich edge cases w parsowaniu URL - ✅ **Wydajność**: Throttled sync do storage (max co 500ms) - ✅ **UX**: Loading states w popup/sidebar dla Chrome - ✅ **Build**: npm run build-addon:firefox lub npm build-addon dla firefox / npm run build:chrome dla chrome
425 lines
15 KiB
TypeScript
425 lines
15 KiB
TypeScript
import ExtendedRequest from './extended-request';
|
|
import { getshorthost } from './util';
|
|
import { RequestCluster } from './request-cluster';
|
|
import { SaferEmitter } from './safer-emitter';
|
|
import browserAPI from './lib/browser-api';
|
|
|
|
// Deklaracja Chrome API dla TypeScript
|
|
declare const chrome: any;
|
|
|
|
function setDomainsCount(counter: number, tabId: number) {
|
|
// Ochrona przed próbą ustawienia badge dla zamkniętej zakładki
|
|
try {
|
|
browserAPI.badge.setBadgeText({ text: counter < 0 ? '0' : counter.toString(), tabId });
|
|
browserAPI.badge.setTitle({
|
|
title: 'Rentgen',
|
|
tabId,
|
|
});
|
|
} catch (e) {
|
|
// Zakładka została zamknięta - ignorujemy błąd
|
|
console.debug(`Zakładka ${tabId} już nie istnieje, pomijanie aktualizacji badge`);
|
|
}
|
|
}
|
|
|
|
// Cachowany RequestCluster dla popupu Chrome (ma metody, ale używa danych z cache)
|
|
class CachedRequestCluster extends RequestCluster {
|
|
private _hasCookies: boolean = false;
|
|
private _exposesOrigin: boolean = false;
|
|
private _hasMarks: boolean = false;
|
|
public lastFullUrl: string | null = null;
|
|
public lastModified: number = 0;
|
|
|
|
constructor(id: string, cached: any) {
|
|
super(id);
|
|
this._hasCookies = cached.hasCookies || false;
|
|
this._exposesOrigin = cached.exposesOrigin || false;
|
|
this._hasMarks = cached.hasMarks || false;
|
|
this.lastFullUrl = cached.lastFullUrl || null;
|
|
this.lastModified = cached.lastModified || 0;
|
|
this.requests = []; // Pusta tablica zapobiegająca błędom
|
|
}
|
|
|
|
hasCookies(): boolean {
|
|
return this._hasCookies;
|
|
}
|
|
|
|
exposesOrigin(): boolean {
|
|
return this._exposesOrigin;
|
|
}
|
|
|
|
hasMarks(): boolean {
|
|
return this._hasMarks;
|
|
}
|
|
|
|
// Automatyczne zaznaczenie dla cachowanego clustra
|
|
autoMark(): void {
|
|
this._hasMarks = true;
|
|
// ✅ Trigger storage sync
|
|
if (process.env.TARGET === 'chrome') {
|
|
const memory = getMemory();
|
|
if (memory instanceof Memory) {
|
|
memory.scheduleSyncToStorage?.();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cofnięcie zaznaczenia dla cachowanego clustra
|
|
undoMark(): void {
|
|
this._hasMarks = false;
|
|
// ✅ Trigger storage sync
|
|
if (process.env.TARGET === 'chrome') {
|
|
const memory = getMemory();
|
|
if (memory instanceof Memory) {
|
|
memory.scheduleSyncToStorage?.();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nadpisanie metody - zwraca pustą tablicę
|
|
calculateRepresentativeStolenData(): any[] {
|
|
return [];
|
|
}
|
|
|
|
// Nadpisanie metody - zwraca tylko główną domenę
|
|
getFullHosts(): string[] {
|
|
return [this.id];
|
|
}
|
|
|
|
// Metody wymagane przez report-window
|
|
hasMarkedCookies(): boolean {
|
|
return this._hasCookies && this._hasMarks;
|
|
}
|
|
|
|
getMarkedRequests(): any[] {
|
|
return [];
|
|
}
|
|
|
|
getMarkedEntries(): any[] {
|
|
return [];
|
|
}
|
|
|
|
exposesOriginWhere(): any[] {
|
|
return this._exposesOrigin ? [{ path: '', source: 'cached', key: '' }] : [];
|
|
}
|
|
|
|
getDataTypeDescription(noun = 'Twojej'): string {
|
|
let types_of_data: string[] = [];
|
|
if (this.exposesOrigin()) {
|
|
types_of_data.push(`część ${noun} historii przeglądania`);
|
|
}
|
|
if (this.hasMarkedCookies()) {
|
|
types_of_data.push('unikalne ID z cookies');
|
|
}
|
|
if (types_of_data.length > 1) {
|
|
types_of_data[types_of_data.length - 1] = 'oraz ' + types_of_data[types_of_data.length - 1];
|
|
}
|
|
return types_of_data.join(', ');
|
|
}
|
|
}
|
|
|
|
export default class Memory extends SaferEmitter {
|
|
origin_to_history = {} as Record<string, Record<string, RequestCluster>>;
|
|
isReady: boolean = true; // Firefox jest zawsze gotowy
|
|
private readyPromise: Promise<void> | null = null;
|
|
private syncScheduled: boolean = false;
|
|
private lastSyncTime: number = 0;
|
|
|
|
// Chrome: Śledzenie pełnych URLi dla zakładek (do generowania screenshotów)
|
|
private tabUrls: Map<number, string> = new Map();
|
|
|
|
async register(request: ExtendedRequest) {
|
|
await request.init();
|
|
|
|
if (!request.isThirdParty()) {
|
|
return;
|
|
}
|
|
if (!this.origin_to_history[request.origin]) {
|
|
this.origin_to_history[request.origin] = {};
|
|
}
|
|
const shorthost = getshorthost(new URL(request.url).host);
|
|
|
|
let isNewCluster = false;
|
|
if (!this.origin_to_history[request.origin][shorthost]) {
|
|
const cluster = new RequestCluster(shorthost);
|
|
this.origin_to_history[request.origin][shorthost] = cluster;
|
|
isNewCluster = true;
|
|
}
|
|
|
|
this.origin_to_history[request.origin][shorthost].add(request);
|
|
|
|
// Chrome: Automatyczne zaznaczanie podejrzanych domen w service workerze
|
|
if (process.env.TARGET === 'chrome' && isNewCluster) {
|
|
// Zaznacz od razu po utworzeniu clustra
|
|
this.origin_to_history[request.origin][shorthost].autoMark();
|
|
}
|
|
|
|
this.emit('change', shorthost);
|
|
|
|
// Owinięcie operacji badge w try-catch
|
|
try {
|
|
Object.values(this.getClustersForOrigin(request.origin)).some((cluster) =>
|
|
cluster.hasCookies()
|
|
)
|
|
? browserAPI.badge.setBadgeBackgroundColor({ color: '#ff726b' })
|
|
: browserAPI.badge.setBadgeBackgroundColor({ color: '#ffb900' });
|
|
|
|
if (request.tabId >= 0) {
|
|
setDomainsCount(
|
|
Object.values(this.getClustersForOrigin(request.origin)).length,
|
|
request.tabId
|
|
);
|
|
}
|
|
} catch (e) {
|
|
// Zakładka zamknięta - ignorujemy błędy badge
|
|
console.debug('Aktualizacja badge nie powiodła się - zakładka prawdopodobnie zamknięta');
|
|
}
|
|
|
|
// Chrome: Throttlowana synchronizacja do storage (max co 500ms)
|
|
if (process.env.TARGET === 'chrome') {
|
|
this.scheduleSyncToStorage();
|
|
}
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
browserAPI.webRequest.onBeforeRequest.addListener(
|
|
async (request) => {
|
|
// Chrome: Śledzenie nawigacji main_frame dla pełnego URL
|
|
if (process.env.TARGET === 'chrome' && request.type === 'main_frame' && request.tabId >= 0) {
|
|
this.tabUrls.set(request.tabId, request.url);
|
|
console.log(`📍 Zapamiętano URL zakładki ${request.tabId}:`, request.url);
|
|
}
|
|
|
|
const extReq = new ExtendedRequest(request);
|
|
|
|
// Chrome: Wstrzyknięcie pełnego URL ze śledzonych zakładek
|
|
if (process.env.TARGET === 'chrome' && request.tabId >= 0) {
|
|
const fullUrl = this.tabUrls.get(request.tabId);
|
|
if (fullUrl && !extReq.originalURL) {
|
|
extReq.originalURL = fullUrl;
|
|
}
|
|
}
|
|
},
|
|
{ urls: ['<all_urls>'] },
|
|
['requestBody']
|
|
);
|
|
|
|
browserAPI.webRequest.onBeforeSendHeaders.addListener(
|
|
async (request) => {
|
|
const extendedRequest = ExtendedRequest.by_id[request.requestId].addHeaders(
|
|
request.requestHeaders || []
|
|
);
|
|
this.register(extendedRequest);
|
|
},
|
|
{ urls: ['<all_urls>'] },
|
|
['requestHeaders']
|
|
);
|
|
|
|
// Chrome: Czyszczenie URLi zakładek po zamknięciu
|
|
if (process.env.TARGET === 'chrome') {
|
|
browserAPI.tabs.onRemoved?.addListener?.((tabId: number) => {
|
|
this.tabUrls.delete(tabId);
|
|
});
|
|
}
|
|
|
|
// Chrome: Wczytywanie z storage przy starcie (dla popupu)
|
|
if (process.env.TARGET === 'chrome') {
|
|
this.isReady = false;
|
|
this.readyPromise = this.loadFromStorage().then(() => {
|
|
this.isReady = true;
|
|
this.emit('ready');
|
|
console.log('🔵 Memory gotowa do użycia przez popup');
|
|
});
|
|
}
|
|
}
|
|
|
|
emit(eventName: string, data = 'any'): boolean {
|
|
setTimeout(() => {
|
|
super.emit(eventName, data);
|
|
|
|
// ✅ Sync to storage when marks change (Chrome only)
|
|
if (process.env.TARGET === 'chrome' && eventName === 'change') {
|
|
this.scheduleSyncToStorage();
|
|
}
|
|
}, 0);
|
|
return true;
|
|
}
|
|
|
|
getClustersForOrigin(origin: string): Record<string, RequestCluster> {
|
|
return this.origin_to_history[origin] || {};
|
|
}
|
|
|
|
// Chrome: Oczekiwanie na zakończenie wczytywania ze storage
|
|
async waitUntilReady(): Promise<void> {
|
|
if (this.isReady) return;
|
|
if (this.readyPromise) {
|
|
await this.readyPromise;
|
|
}
|
|
}
|
|
|
|
async removeCookiesFor(origin: string, shorthost?: string): Promise<void> {
|
|
if (shorthost) {
|
|
const cookies = await browserAPI.cookies.getAll({ domain: shorthost });
|
|
for (const cookie of cookies) {
|
|
await browserAPI.cookies.remove({
|
|
name: cookie.name,
|
|
url: `https://${cookie.domain}`,
|
|
});
|
|
}
|
|
} else {
|
|
const clusters = this.getClustersForOrigin(origin);
|
|
|
|
await Promise.all(
|
|
Object.values(clusters)
|
|
.filter((cluster) => !shorthost || cluster.id === shorthost)
|
|
.map((cluster) => this.removeCookiesFor(origin, cluster.id))
|
|
);
|
|
}
|
|
|
|
// Chrome: Throttlowana synchronizacja do storage
|
|
if (process.env.TARGET === 'chrome') {
|
|
this.scheduleSyncToStorage();
|
|
}
|
|
}
|
|
|
|
async removeRequestsFor(origin: string) {
|
|
this.origin_to_history[origin] = {};
|
|
|
|
// Chrome: Throttlowana synchronizacja do storage
|
|
if (process.env.TARGET === 'chrome') {
|
|
this.scheduleSyncToStorage();
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// CHROME: Metody synchronizacji ze storage (THROTTLED)
|
|
// ========================================
|
|
|
|
public scheduleSyncToStorage(): void {
|
|
if (this.syncScheduled) return;
|
|
|
|
this.syncScheduled = true;
|
|
setTimeout(() => {
|
|
this.syncToStorage();
|
|
this.syncScheduled = false;
|
|
}, 500); // Synchronizacja max co 500ms
|
|
}
|
|
|
|
private syncToStorage(): void {
|
|
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
|
const now = Date.now();
|
|
// Pomijamy jeśli synchronizowano niedawno (w ciągu 300ms)
|
|
if (now - this.lastSyncTime < 300) {
|
|
return;
|
|
}
|
|
this.lastSyncTime = now;
|
|
|
|
const serializable: Record<string, any> = {};
|
|
|
|
for (const [origin, clusters] of Object.entries(this.origin_to_history)) {
|
|
serializable[origin] = {};
|
|
for (const [shorthost, cluster] of Object.entries(clusters)) {
|
|
// Zapisujemy tylko niezbędne dane dla UI
|
|
serializable[origin][shorthost] = {
|
|
id: cluster.id,
|
|
hasCookies: cluster.hasCookies(),
|
|
exposesOrigin: cluster.exposesOrigin(),
|
|
hasMarks: cluster.hasMarks(),
|
|
requestCount: cluster.requests?.length || 0,
|
|
lastFullUrl: cluster.lastFullUrl || null,
|
|
lastModified: cluster.lastModified || 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
chrome.storage.session.set({ rentgen_memory: serializable }).catch((err: any) => {
|
|
console.error('Nie udało się zsynchronizować pamięci do storage:', err);
|
|
});
|
|
}
|
|
}
|
|
|
|
private async loadFromStorage(): Promise<void> {
|
|
if (typeof chrome !== 'undefined' && chrome.storage?.session) {
|
|
try {
|
|
const result = await chrome.storage.session.get('rentgen_memory');
|
|
if (result.rentgen_memory) {
|
|
const serialized = result.rentgen_memory;
|
|
|
|
for (const [origin, clusters] of Object.entries(serialized)) {
|
|
this.origin_to_history[origin] = {};
|
|
for (const [shorthost, cached] of Object.entries(clusters as Record<string, any>)) {
|
|
// Tworzenie CachedRequestCluster ze wszystkimi potrzebnymi danymi
|
|
const clusterData = cached as {
|
|
id: string;
|
|
hasCookies: boolean;
|
|
exposesOrigin: boolean;
|
|
hasMarks: boolean;
|
|
requestCount: number;
|
|
lastFullUrl: string | null;
|
|
lastModified: number;
|
|
};
|
|
this.origin_to_history[origin][shorthost] = new CachedRequestCluster(
|
|
clusterData.id,
|
|
clusterData
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log('🔵 Wczytano pamięć z chrome.storage.session:', Object.keys(this.origin_to_history).length, 'origins');
|
|
}
|
|
} catch (err) {
|
|
console.error('Nie udało się wczytać pamięci ze storage:', err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// INICJALIZACJA: Firefox vs Chrome
|
|
// ========================================
|
|
|
|
export function init() {
|
|
const memory = new Memory();
|
|
|
|
if (process.env.TARGET === 'chrome') {
|
|
// Chrome MV3: Service worker używa 'self' zamiast 'window'
|
|
(self as any).memory = memory;
|
|
console.log('🔵 Memory zainicjalizowana w service workerze Chrome (self.memory + chrome.storage.session)');
|
|
} else {
|
|
// Firefox: Standardowa strona tła z 'window'
|
|
(window as any).memory = memory;
|
|
console.log('🦊 Memory zainicjalizowana w stronie tła Firefox (window.memory)');
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// DOSTĘP DO MEMORY: Firefox vs Chrome
|
|
// ========================================
|
|
|
|
// Cachowana instancja memory dla popupu (tylko Chrome)
|
|
let popupMemoryInstance: Memory | null = null;
|
|
|
|
export function getMemory(): Memory {
|
|
if (process.env.TARGET === 'chrome') {
|
|
// Chrome: Najpierw próba pobrania z service workera
|
|
if (typeof self !== 'undefined' && (self as any).memory) {
|
|
// Jesteśmy W service workerze - bezpośredni dostęp
|
|
return (self as any).memory as Memory;
|
|
} else {
|
|
// Jesteśmy w popupie/contencie - tworzymy RAZ i cachujemy
|
|
if (!popupMemoryInstance) {
|
|
console.log('🔵 Tworzenie instancji Chrome memory dla popupu (czytanie z chrome.storage.session)');
|
|
popupMemoryInstance = new Memory();
|
|
}
|
|
return popupMemoryInstance;
|
|
}
|
|
} else {
|
|
// Firefox: Używamy tradycyjnego getBackgroundPage()
|
|
const backgroundPage = browserAPI.extension.getBackgroundPage();
|
|
if (!backgroundPage) {
|
|
throw new Error('Strona tła nie jest dostępna');
|
|
}
|
|
return (backgroundPage.window as any).memory as Memory;
|
|
}
|
|
} |