Skip to content

Commit 2f5ab21

Browse files
committed
fix(chrome): improve Chrome MV3 compatibility and config timing
- Add localStorage guard in storage.ts for service worker context - Fix isChrome detection using browser.runtime.getBrowserInfo - Add ErrorResponse type to RuntimeResponse union - Remove syncToLocalStorage calls from background context - Add config gating in page-script to ensure correct settings on first fetch - Dispatch CustomEvent from page-inject for faster config delivery - Move ensureConfigReady after URL filtering to avoid delaying non-API requests - Bump version to 1.6.2
1 parent 378c8fa commit 2f5ab21

File tree

9 files changed

+122
-38
lines changed

9 files changed

+122
-38
lines changed

extension/manifest.chrome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "LightSession Pro for ChatGPT",
4-
"version": "1.6.1",
4+
"version": "1.6.2",
55
"description": "Keep ChatGPT fast by keeping only the last N messages in the DOM. Local-only.",
66
"icons": {
77
"16": "icons/icon-16.png",

extension/manifest.firefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "LightSession Pro for ChatGPT",
4-
"version": "1.6.1",
4+
"version": "1.6.2",
55
"description": "Keep ChatGPT fast by keeping only the last N messages in the DOM. Local-only.",
66
"icons": {
77
"16": "icons/icon-16.png",

extension/src/content/content.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import browser from '../shared/browser-polyfill';
1313
import type { LsSettings, TrimStatus } from '../shared/types';
14-
import { loadSettings, validateSettings } from '../shared/storage';
14+
import { loadSettings, validateSettings, syncToLocalStorage } from '../shared/storage';
1515
import { TIMING } from '../shared/constants';
1616
import { setDebugMode, logDebug, logInfo, logWarn, logError } from '../shared/logger';
1717
import {
@@ -199,6 +199,10 @@ function handleStorageChange(
199199
// Validate settings to ensure proper types and ranges
200200
const newSettings = validateSettings(changes.ls_settings.newValue as Partial<LsSettings>);
201201
logInfo('Settings changed via storage:', newSettings);
202+
203+
// Sync to localStorage for page-script access (Chrome MV3 workaround)
204+
syncToLocalStorage(newSettings);
205+
202206
applySettings(newSettings);
203207
}
204208

@@ -296,6 +300,9 @@ async function initialize(): Promise<void> {
296300
const settings = await loadSettings();
297301
logInfo('Loaded settings:', settings);
298302

303+
// Sync to localStorage for page-script access (Chrome MV3 workaround)
304+
syncToLocalStorage(settings);
305+
299306
// Apply settings
300307
applySettings(settings);
301308

extension/src/content/page-inject.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,28 @@ const STORAGE_KEY = 'ls_settings';
1515
const LOCAL_STORAGE_KEY = 'ls_config';
1616

1717
/**
18-
* Sync settings from browser.storage to localStorage.
19-
* This runs BEFORE page-script injection to ensure localStorage has correct data.
18+
* Sync settings from browser.storage to localStorage AND dispatch CustomEvent.
19+
* Runs in parallel with page-script injection. The CustomEvent signals config ready
20+
* to page-script, allowing it to gate first fetch until config is available.
2021
*/
2122
async function syncSettingsToLocalStorage(): Promise<void> {
2223
try {
2324
const result = await browser.storage.local.get(STORAGE_KEY);
2425
const stored = result[STORAGE_KEY] as { enabled?: boolean; keep?: number; debug?: boolean } | undefined;
25-
26+
2627
if (stored) {
2728
const config = {
2829
enabled: stored.enabled ?? true,
2930
limit: stored.keep ?? 10,
3031
debug: stored.debug ?? false,
3132
};
33+
// Write to localStorage for page-script access
3234
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(config));
35+
// Dispatch event immediately - faster than waiting for content.ts (document_idle)
36+
window.dispatchEvent(new CustomEvent('lightsession-config', { detail: JSON.stringify(config) }));
3337
}
3438
} catch {
35-
// Storage access failed - page-script will use defaults or existing localStorage
39+
// Storage access failed - page-script will use defaults after timeout
3640
}
3741
}
3842

@@ -57,8 +61,9 @@ function injectPageScript(): void {
5761
}
5862

5963
// Main execution:
60-
// 1. Start syncing settings (async, but fast)
61-
// 2. Inject page script immediately (can't wait - need to patch fetch early)
62-
// The sync will complete and update localStorage, which page-script checks on each fetch.
63-
void syncSettingsToLocalStorage();
64+
// 1. Inject page script IMMEDIATELY to patch fetch before ChatGPT's code runs
65+
// 2. Sync localStorage in parallel (best effort for first fetch)
66+
// 3. content.ts will dispatch config via CustomEvent as fallback
67+
// Priority is early patching - page-script uses defaults if localStorage not ready.
6468
injectPageScript();
69+
void syncSettingsToLocalStorage();

extension/src/page/page-script.ts

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,48 @@ const DEFAULT_CONFIG: LsConfig = {
4848
debug: false,
4949
};
5050

51+
// ============================================================================
52+
// Config Ready Gating
53+
// ============================================================================
54+
55+
/**
56+
* Promise that resolves when config is ready (from localStorage or CustomEvent).
57+
* First fetch waits on this to ensure correct config is used.
58+
*/
59+
let resolveConfigReady: (() => void) | null = null;
60+
const configReady = new Promise<void>((resolve) => {
61+
resolveConfigReady = resolve;
62+
});
63+
64+
/**
65+
* Resolve the configReady promise (idempotent - only resolves once).
66+
*/
67+
function tryResolveConfigReady(): void {
68+
if (resolveConfigReady) {
69+
resolveConfigReady();
70+
resolveConfigReady = null;
71+
}
72+
}
73+
74+
/**
75+
* Wait for config to be ready with timeout.
76+
* Returns immediately if config already loaded.
77+
* After timeout, marks config as ready to avoid repeated delays on subsequent fetches.
78+
* @param timeoutMs Max time to wait (default 50ms)
79+
*/
80+
async function ensureConfigReady(timeoutMs = 50): Promise<void> {
81+
if (!resolveConfigReady) {
82+
// Already resolved
83+
return;
84+
}
85+
await Promise.race([
86+
configReady,
87+
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
88+
]);
89+
// After timeout (or config arrived), mark as ready so subsequent fetches don't wait
90+
tryResolveConfigReady();
91+
}
92+
5193
/**
5294
* localStorage key - must match storage.ts LOCAL_STORAGE_KEY
5395
*/
@@ -197,13 +239,6 @@ async function interceptedFetch(
197239
nativeFetch: typeof fetch,
198240
...args: Parameters<typeof fetch>
199241
): Promise<Response> {
200-
const cfg = getConfig();
201-
202-
// Skip if disabled
203-
if (!cfg.enabled) {
204-
return nativeFetch(...args);
205-
}
206-
207242
// Extract URL/method BEFORE fetching (handles string, URL, Request)
208243
// This avoids "Body has already been consumed" error when args[0] is a Request
209244
const [input, init] = args;
@@ -223,11 +258,21 @@ async function interceptedFetch(
223258

224259
const url = new URL(urlString, location.href);
225260

226-
// Early return for non-matching requests (before fetching)
261+
// Early return for non-matching requests - no config wait needed
227262
if (!isConversationRequest(method, url)) {
228263
return nativeFetch(...args);
229264
}
230265

266+
// Wait for config only for ChatGPT API requests (max 50ms on first request)
267+
await ensureConfigReady();
268+
269+
const cfg = getConfig();
270+
271+
// Skip if disabled
272+
if (!cfg.enabled) {
273+
return nativeFetch(...args);
274+
}
275+
231276
// Fetch and process matching requests
232277
const res = await nativeFetch(...args);
233278

@@ -355,6 +400,9 @@ function setupConfigListener(): void {
355400
debug: config.debug ?? DEFAULT_CONFIG.debug,
356401
};
357402
log('Config updated:', window.__LS_CONFIG__);
403+
404+
// Signal that config is ready (unblocks first fetch)
405+
tryResolveConfigReady();
358406
}
359407
}) as EventListener);
360408
}
@@ -369,6 +417,14 @@ function setupConfigListener(): void {
369417
window.__LS_DEBUG__ = false;
370418
}
371419

420+
// Check localStorage first - if already synced by page-inject, resolve immediately
421+
const stored = loadFromLocalStorage();
422+
if (stored) {
423+
window.__LS_CONFIG__ = stored;
424+
window.__LS_DEBUG__ = stored.debug;
425+
tryResolveConfigReady();
426+
}
427+
372428
setupConfigListener();
373429
patchFetch();
374430

extension/src/shared/messages.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import browser from './browser-polyfill';
7-
import type { RuntimeMessage, RuntimeResponse } from './types';
7+
import type { RuntimeMessage, RuntimeResponse, ErrorResponse } from './types';
88
import { TIMING } from './constants';
99
import { logError } from './logger';
1010

@@ -18,7 +18,11 @@ export async function sendMessageWithTimeout<T extends RuntimeResponse>(
1818
message: RuntimeMessage,
1919
timeoutMs: number = TIMING.MESSAGE_TIMEOUT_MS
2020
): Promise<T> {
21-
const isChrome = typeof chrome !== 'undefined' && typeof browser === 'undefined';
21+
// Detect Chrome vs Firefox using feature detection
22+
// Firefox has browser.runtime.getBrowserInfo() which Chrome doesn't have
23+
// This is more reliable than checking global objects which can be polyfilled
24+
const isFirefox = typeof browser.runtime.getBrowserInfo === 'function';
25+
const isChrome = !isFirefox && typeof chrome !== 'undefined' && !!chrome.runtime;
2226
const retryDelays = TIMING.MESSAGE_RETRY_DELAYS_MS;
2327
let lastError: Error | undefined;
2428

@@ -47,12 +51,17 @@ export async function sendMessageWithTimeout<T extends RuntimeResponse>(
4751
throw new Error('Service worker not responding - received undefined after retries');
4852
}
4953

54+
// Check for error response from handler (don't retry real errors)
55+
if ('error' in response && typeof response.error === 'string') {
56+
throw new Error(`Handler error: ${response.error}`);
57+
}
58+
5059
return response;
5160
} catch (error) {
5261
lastError = error instanceof Error ? error : new Error(String(error));
5362

54-
// Don't retry on timeout - it's already waited long enough
55-
if (lastError.message === 'Message timeout') {
63+
// Don't retry on timeout or handler errors - these are real failures
64+
if (lastError.message === 'Message timeout' || lastError.message.startsWith('Handler error:')) {
5665
throw lastError;
5766
}
5867

@@ -112,8 +121,10 @@ export function createMessageHandler(
112121
sendResponse(response);
113122
} catch (error) {
114123
logError('Message handler error:', error);
115-
// Send error response so caller doesn't hang
116-
sendResponse({ error: String(error) } as unknown as RuntimeResponse);
124+
// Send error response so caller doesn't hang waiting for response
125+
// Caller must check for error field in response
126+
const errorResponse: ErrorResponse = { error: String(error) };
127+
sendResponse(errorResponse);
117128
}
118129
})();
119130

extension/src/shared/storage.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export function validateSettings(input: Partial<LsSettings>): LsSettings {
4141
* This eliminates race conditions on page load.
4242
*/
4343
export function syncToLocalStorage(settings: LsSettings): void {
44+
// Guard: localStorage unavailable in service worker (Chrome MV3)
45+
if (typeof localStorage === 'undefined') {
46+
logDebug('localStorage not available (service worker context)');
47+
return;
48+
}
49+
4450
try {
4551
const config = {
4652
enabled: settings.enabled,
@@ -67,21 +73,15 @@ export async function loadSettings(): Promise<LsSettings> {
6773

6874
if (stored) {
6975
logDebug('Loaded settings from storage:', stored);
70-
const validated = validateSettings(stored);
71-
syncToLocalStorage(validated);
72-
return validated;
76+
return validateSettings(stored);
7377
}
7478

7579
// No stored settings, return defaults
7680
logDebug('No stored settings found, using defaults');
77-
const defaults = validateSettings({});
78-
syncToLocalStorage(defaults);
79-
return defaults;
81+
return validateSettings({});
8082
} catch (error) {
8183
logError('Failed to load settings:', error);
82-
const defaults = validateSettings({});
83-
syncToLocalStorage(defaults);
84-
return defaults;
84+
return validateSettings({});
8585
}
8686
}
8787

@@ -99,7 +99,6 @@ export async function updateSettings(updates: Partial<Omit<LsSettings, 'version'
9999

100100
// Save to storage
101101
await browser.storage.local.set({ [STORAGE_KEY]: merged });
102-
syncToLocalStorage(merged);
103102

104103
logDebug('Updated settings:', merged);
105104
} catch (error) {
@@ -118,7 +117,6 @@ export async function initializeSettings(): Promise<void> {
118117

119118
if (!result[STORAGE_KEY]) {
120119
await browser.storage.local.set({ [STORAGE_KEY]: DEFAULT_SETTINGS });
121-
syncToLocalStorage(DEFAULT_SETTINGS);
122120
logDebug('Initialized default settings');
123121
}
124122
} catch (error) {

extension/src/shared/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ export interface PongMessage {
7979
timestamp: number;
8080
}
8181

82+
/**
83+
* Error response from message handler
84+
*/
85+
export interface ErrorResponse {
86+
error: string;
87+
}
88+
8289
/**
8390
* Union of all runtime messages
8491
*/
@@ -87,4 +94,4 @@ export type RuntimeMessage = GetSettingsMessage | SetSettingsMessage | PingMessa
8794
/**
8895
* Union of all runtime responses
8996
*/
90-
export type RuntimeResponse = GetSettingsResponse | SetSettingsResponse | PongMessage;
97+
export type RuntimeResponse = GetSettingsResponse | SetSettingsResponse | PongMessage | ErrorResponse;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "light-session",
3-
"version": "1.6.1",
3+
"version": "1.6.2",
44
"type": "module",
55
"description": "LightSession Pro - Browser extension to optimize ChatGPT performance",
66
"engines": {

0 commit comments

Comments
 (0)