Skip to content

Commit f7b5d59

Browse files
committed
ext: resolve merge conflict in use-tab-tracking-data by standardizing on callWithTimeout and removing Promise.allSettled path
2 parents 8b215f7 + af97dfb commit f7b5d59

7 files changed

Lines changed: 153 additions & 129 deletions

File tree

apps/ext/.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
SUPABASE_URL=
2-
SUPABASE_ANON_KEY=
1+
VITE_SUPABASE_URL=
2+
VITE_SUPABASE_ANON_KEY=

apps/ext/src/entrypoints/background.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class TabTracker {
8383
await this.performDataCleanupIfNeeded();
8484
logger.initializationStep('performDataCleanupIfNeeded', true, { component: 'TabTracker' });
8585

86-
// Initialize sync service
86+
// Initialize sync service (safe if not configured)
8787
await syncService.initialize();
8888
logger.initializationStep('syncService.initialize', true, { component: 'TabTracker' });
8989

@@ -121,7 +121,7 @@ class TabTracker {
121121
} catch (error) {
122122
logger.timeEnd('TabTracker Initialization');
123123
logger.fatal('Failed to initialize TabTracker', error as Error, { component: 'TabTracker' });
124-
throw error;
124+
// Do not rethrow; keep background alive for message handling
125125
}
126126
}
127127

@@ -764,7 +764,9 @@ class TabTracker {
764764
export default defineBackground(() => {
765765
const tracker = new TabTracker();
766766

767-
// Initialize tracker
767+
logger.info('Background service worker boot', { component: 'Background' });
768+
769+
// Initialize tracker in a fire-and-forget fashion
768770
tracker.initialize();
769771

770772
// Tab event listeners
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import React from 'react';
21
import ReactDOM from 'react-dom/client';
32
import App from './app.tsx';
43
import './style.css';
54

65
ReactDOM.createRoot(document.getElementById('root')!).render(
7-
<React.StrictMode>
8-
<App />
9-
</React.StrictMode>
6+
// StrictMode triggers double effects in dev, causing flicker and duplicate timers
7+
// Remove it in extension popup to stabilize UX.
8+
<App />
109
);

apps/ext/src/hooks/use-tab-tracking-data.ts

Lines changed: 81 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useState } from 'react';
1+
import { useCallback, useEffect, useRef, useState } from 'react';
22
import { logger } from '../lib/logger';
33

44
type TabSession = {
@@ -42,107 +42,88 @@ export function useTabTrackingData(): UseTabTrackingDataReturn {
4242
const [isLoading, setIsLoading] = useState(true);
4343
const [error, setError] = useState<string | null>(null);
4444

45+
const isLoadingRef = useRef(false);
46+
const hasLoadedOnceRef = useRef(false);
47+
const intervalRef = useRef<number | null>(null);
48+
49+
// Wrapper that times out a message call and logs a per-call warning without throwing
50+
const callWithTimeout = useCallback(
51+
async <T>(action: string, timeoutMs = 3000): Promise<T | null> => {
52+
try {
53+
const responsePromise = browser.runtime.sendMessage({ action }) as Promise<T>;
54+
const timeoutPromise = new Promise<null>((resolve) => {
55+
setTimeout(() => resolve(null), timeoutMs);
56+
});
57+
const result = (await Promise.race([responsePromise, timeoutPromise])) as T | null;
58+
if (result === null) {
59+
logger.warn(
60+
`Popup data: failed to load ${action.toLowerCase().replace('get_', '').replace(/_/g, ' ')}`,
61+
{
62+
component: 'useTabTrackingData',
63+
reason: `Timed out: ${action}`,
64+
}
65+
);
66+
return null;
67+
}
68+
return result;
69+
} catch (err) {
70+
logger.warn(
71+
`Popup data: failed to load ${action.toLowerCase().replace('get_', '').replace(/_/g, ' ')}`,
72+
{
73+
component: 'useTabTrackingData',
74+
reason: err instanceof Error ? err.message : 'Unknown error',
75+
}
76+
);
77+
return null;
78+
}
79+
},
80+
[]
81+
);
82+
4583
const loadData = useCallback(async () => {
84+
if (isLoadingRef.current) {
85+
// Prevent overlapping loads which caused duplicate timers and flicker
86+
return;
87+
}
88+
isLoadingRef.current = true;
89+
90+
// Only time when not already timing this logical load
4691
logger.debug('Loading popup data', { component: 'useTabTrackingData' });
4792
logger.time('TabTrackingData Load');
48-
49-
const withTimeout = <T>(promise: Promise<T>, ms: number, label: string): Promise<T> => {
50-
return new Promise<T>((resolve, reject) => {
51-
const timer = setTimeout(() => {
52-
reject(new Error(`Timed out: ${label}`));
53-
}, ms);
54-
promise.then(
55-
(value) => {
56-
clearTimeout(timer);
57-
resolve(value);
58-
},
59-
(reason) => {
60-
clearTimeout(timer);
61-
reject(reason);
62-
}
63-
);
64-
});
65-
};
93+
6694

6795
try {
68-
setIsLoading(true);
96+
setIsLoading(!hasLoadedOnceRef.current);
6997
setError(null);
7098

71-
const results = await Promise.allSettled([
72-
withTimeout(
73-
browser.runtime.sendMessage({ action: 'GET_DAILY_STATS' }) as Promise<DailyStats | null>,
74-
3000,
75-
'GET_DAILY_STATS'
76-
),
77-
withTimeout(
78-
browser.runtime.sendMessage({
79-
action: 'GET_CURRENT_SESSION',
80-
}) as Promise<TabSession | null>,
81-
3000,
82-
'GET_CURRENT_SESSION'
83-
),
84-
withTimeout(
85-
browser.runtime.sendMessage({ action: 'GET_TRACKING_STATUS' }) as Promise<boolean>,
86-
3000,
87-
'GET_TRACKING_STATUS'
88-
),
89-
withTimeout(
90-
browser.runtime.sendMessage({ action: 'GET_SYNC_STATUS' }) as Promise<SyncStatus>,
91-
3000,
92-
'GET_SYNC_STATUS'
93-
),
99+
const [statsResponse, sessionResponse, trackingResponse, syncResponse] = await Promise.all([
100+
callWithTimeout<DailyStats | null>('GET_DAILY_STATS'),
101+
callWithTimeout<TabSession | null>('GET_CURRENT_SESSION'),
102+
callWithTimeout<boolean>('GET_TRACKING_STATUS'),
103+
callWithTimeout<SyncStatus>('GET_SYNC_STATUS'),
94104
]);
95105

96-
const [statsRes, sessionRes, trackingRes, syncRes] = results;
97-
98-
if (statsRes.status === 'fulfilled') {
99-
setDailyStats(statsRes.value);
100-
} else {
101-
logger.warn('Popup data: failed to load daily stats', {
102-
component: 'useTabTrackingData',
103-
reason: (statsRes.reason as Error)?.message,
104-
});
106+
if (statsResponse !== null) {
107+
setDailyStats(statsResponse);
105108
}
106-
107-
if (sessionRes.status === 'fulfilled') {
108-
setCurrentSession(sessionRes.value);
109-
} else {
110-
logger.warn('Popup data: failed to load current session', {
111-
component: 'useTabTrackingData',
112-
reason: (sessionRes.reason as Error)?.message,
113-
});
109+
if (sessionResponse !== null) {
110+
setCurrentSession(sessionResponse);
114111
}
115-
116-
if (trackingRes.status === 'fulfilled') {
117-
setIsTrackingEnabled(trackingRes.value);
118-
} else {
119-
logger.warn('Popup data: failed to load tracking status', {
120-
component: 'useTabTrackingData',
121-
reason: (trackingRes.reason as Error)?.message,
122-
});
112+
if (typeof trackingResponse === 'boolean') {
113+
setIsTrackingEnabled(trackingResponse);
123114
}
124-
125-
if (syncRes.status === 'fulfilled') {
126-
setSyncStatus(syncRes.value);
127-
} else {
128-
logger.warn('Popup data: failed to load sync status', {
129-
component: 'useTabTrackingData',
130-
reason: (syncRes.reason as Error)?.message,
131-
});
115+
if (syncResponse !== null) {
116+
setSyncStatus(syncResponse);
132117
}
133118

134119
logger.timeEnd('TabTrackingData Load');
135120
logger.info('Tab tracking data loaded (partial ok)', {
136121
component: 'useTabTrackingData',
137-
hasStats: statsRes.status === 'fulfilled' && !!statsRes.value,
138-
hasSession: sessionRes.status === 'fulfilled' && !!sessionRes.value,
139-
trackingEnabled:
140-
trackingRes.status === 'fulfilled' ? Boolean(trackingRes.value) : undefined,
122+
hasStats: !!statsResponse,
123+
hasSession: !!sessionResponse,
124+
trackingEnabled: (trackingResponse as unknown as boolean) ?? undefined,
141125
});
142126

143-
if (results.some((r) => r.status === 'rejected')) {
144-
setError('Some data failed to load. Try again.');
145-
}
146127
} catch (err) {
147128
logger.timeEnd('TabTrackingData Load');
148129
logger.error('Failed to load tab tracking data', err as Error, {
@@ -151,18 +132,31 @@ export function useTabTrackingData(): UseTabTrackingDataReturn {
151132
setError('Failed to load tracking data');
152133
} finally {
153134
setIsLoading(false);
135+
isLoadingRef.current = false;
136+
hasLoadedOnceRef.current = true;
154137
}
155-
}, []);
138+
}, [callWithTimeout]);
156139

157140
useEffect(() => {
158141
loadData();
159142

160-
const interval = setInterval(() => {
143+
// Clear existing interval if any (hot-reloads/StrictMode)
144+
if (intervalRef.current) {
145+
clearInterval(intervalRef.current);
146+
intervalRef.current = null;
147+
}
148+
149+
intervalRef.current = setInterval(() => {
161150
logger.trace('Auto-refreshing tab tracking data', { component: 'useTabTrackingData' });
162151
loadData();
163-
}, 5000);
152+
}, 5000) as unknown as number;
164153

165-
return () => clearInterval(interval);
154+
return () => {
155+
if (intervalRef.current) {
156+
clearInterval(intervalRef.current);
157+
intervalRef.current = null;
158+
}
159+
};
166160
}, [loadData]);
167161

168162
return {

apps/ext/src/lib/supabase.ts

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,41 @@
1-
import { createClient } from '@supabase/supabase-js';
1+
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
2+
import { logger } from './logger';
23
import type { Database } from './types';
34

4-
// These will need to be set in the extension's environment
5-
const supabaseUrl = process.env.SUPABASE_URL;
6-
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY;
5+
// Read from Vite/WXT env (exposed at build-time). Prefix must be VITE_.
6+
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL as string | undefined;
7+
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY as string | undefined;
78

8-
if (!supabaseUrl) {
9-
throw new Error('SUPABASE_URL is not set');
10-
}
11-
12-
if (!supabaseAnonKey) {
13-
throw new Error('SUPABASE_ANON_KEY is not set');
14-
}
9+
let client: SupabaseClient<Database> | null = null;
1510

16-
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
17-
auth: {
18-
persistSession: true,
19-
autoRefreshToken: true,
20-
detectSessionInUrl: false,
21-
storage: {
22-
getItem: async (key: string) => {
23-
const result = await browser.storage.local.get(key);
24-
return result[key] || null;
25-
},
26-
setItem: async (key: string, value: string) => {
27-
await browser.storage.local.set({ [key]: value });
28-
},
29-
removeItem: async (key: string) => {
30-
await browser.storage.local.remove(key);
11+
if (supabaseUrl && supabaseAnonKey) {
12+
client = createClient<Database>(supabaseUrl, supabaseAnonKey, {
13+
auth: {
14+
persistSession: true,
15+
autoRefreshToken: true,
16+
detectSessionInUrl: false,
17+
storage: {
18+
getItem: async (key: string) => {
19+
const result = await browser.storage.local.get(key);
20+
return result[key] || null;
21+
},
22+
setItem: async (key: string, value: string) => {
23+
await browser.storage.local.set({ [key]: value });
24+
},
25+
removeItem: async (key: string) => {
26+
await browser.storage.local.remove(key);
27+
},
3128
},
3229
},
33-
},
34-
});
30+
});
31+
} else if (import.meta.env.DEV) {
32+
logger.info(
33+
'Supabase not configured (VITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEY missing). Cloud sync disabled in extension dev.',
34+
{
35+
component: 'supabase',
36+
}
37+
);
38+
}
3539

40+
export const supabase = client;
3641
export type { Database } from './types';

0 commit comments

Comments
 (0)