@@ -5,24 +5,26 @@ import {
55 pruneSessionsOlderThan ,
66 upsertTabSnapshot
77} from "../shared/db.js" ;
8- import { toDurationSeconds } from "../shared/time .js" ;
9- import { extractDomain , isExcludedDomain , isTrackableUrl , readableTitle } from "../shared/url.js" ;
8+ import { createSessionEngine } from "./session-engine .js" ;
9+ import { isExcludedDomain , isTrackableUrl , readableTitle } from "../shared/url.js" ;
1010
1111const CLEANUP_ALARM = "retention-cleanup" ;
1212const CLEANUP_INTERVAL_MINUTES = 360 ;
13- const SWITCH_DEBOUNCE_MS = 250 ;
13+ const RUNTIME_STORAGE_KEY = "runtime_state_v1" ;
1414
1515const state = {
16- activeSession : null ,
1716 focusedWindowId : chrome . windows . WINDOW_ID_NONE ,
1817 idleState : "active" ,
1918 paused : false ,
2019 excludedDomains : [ ] ,
21- retentionDays : 30 ,
22- lastContextFingerprint : "" ,
23- lastContextAt : 0
20+ retentionDays : 30
2421} ;
2522
23+ const sessionEngine = createSessionEngine ( {
24+ debounceMs : 250 ,
25+ isTrackableTab
26+ } ) ;
27+
2628function getDashboardUrl ( ) {
2729 return chrome . runtime . getURL ( "ui/dashboard.html" ) ;
2830}
@@ -43,87 +45,59 @@ function isTrackableTab(tab) {
4345 return true ;
4446}
4547
46- function createSession ( tab ) {
47- const now = Date . now ( ) ;
48- return {
49- id : crypto . randomUUID ( ) ,
50- tabId : tab . id ,
51- windowId : tab . windowId ,
52- url : tab . url ,
53- title : readableTitle ( tab . title , tab . url ) ,
54- domain : extractDomain ( tab . url ) ,
55- startAt : now ,
56- endAt : now ,
57- durationSec : 0 ,
58- endReason : "unknown"
59- } ;
60- }
61-
6248async function refreshSettingsCache ( ) {
6349 const settings = await getSettings ( ) ;
6450 state . paused = Boolean ( settings . paused ) ;
6551 state . retentionDays = Number ( settings . retentionDays ) || 30 ;
6652 state . excludedDomains = Array . isArray ( settings . excludedDomains ) ? settings . excludedDomains : [ ] ;
6753}
6854
69- async function endActiveSession ( reason ) {
70- if ( ! state . activeSession ) {
71- return ;
72- }
73-
74- const now = Date . now ( ) ;
75- const completed = {
76- ...state . activeSession ,
77- endAt : now ,
78- durationSec : toDurationSeconds ( state . activeSession . startAt , now ) ,
79- endReason : reason
80- } ;
81-
82- state . activeSession = null ;
83- state . lastContextFingerprint = "" ;
84- state . lastContextAt = 0 ;
55+ function getActiveSession ( ) {
56+ return sessionEngine . readState ( ) . activeSession ;
57+ }
8558
86- if ( ! completed . url || completed . durationSec < 0 ) {
59+ async function persistRuntimeState ( ) {
60+ const runtimeState = sessionEngine . exportRuntimeState ( ) ;
61+ if ( runtimeState . activeSession ) {
62+ await chrome . storage . local . set ( {
63+ [ RUNTIME_STORAGE_KEY ] : runtimeState
64+ } ) ;
8765 return ;
8866 }
8967
90- await addSession ( completed ) ;
68+ await chrome . storage . local . remove ( RUNTIME_STORAGE_KEY ) ;
9169}
9270
93- function isDebouncedContext ( tab ) {
94- const fingerprint = `${ tab . windowId } :${ tab . id } :${ tab . url } ` ;
95- const now = Date . now ( ) ;
96- const isDebounced =
97- fingerprint === state . lastContextFingerprint && now - state . lastContextAt < SWITCH_DEBOUNCE_MS ;
71+ async function loadRuntimeState ( ) {
72+ const stored = await chrome . storage . local . get ( RUNTIME_STORAGE_KEY ) ;
73+ const runtimeState = stored ?. [ RUNTIME_STORAGE_KEY ] ;
74+ if ( ! runtimeState ) {
75+ return false ;
76+ }
9877
99- state . lastContextFingerprint = fingerprint ;
100- state . lastContextAt = now ;
101- return isDebounced ;
78+ return sessionEngine . hydrateRuntimeState ( runtimeState ) ;
10279}
10380
104- async function startOrSwitchSession ( tab , reason ) {
105- if ( ! isTrackableTab ( tab ) ) {
106- await endActiveSession ( reason ) ;
81+ async function storeEndedSession ( endedSession ) {
82+ if ( ! endedSession || ! endedSession . url || endedSession . durationSec < 0 ) {
10783 return ;
10884 }
10985
110- if ( isDebouncedContext ( tab ) ) {
111- return ;
112- }
86+ await addSession ( endedSession ) ;
87+ }
11388
114- if (
115- state . activeSession &&
116- state . activeSession . tabId === tab . id &&
117- state . activeSession . windowId === tab . windowId
118- ) {
119- state . activeSession . url = tab . url || state . activeSession . url ;
120- state . activeSession . title = readableTitle ( tab . title , state . activeSession . url ) ;
121- state . activeSession . domain = extractDomain ( state . activeSession . url ) ;
122- return ;
123- }
89+ async function endActiveSession ( reason ) {
90+ const endedSession = sessionEngine . endActiveSession ( reason ) ;
91+ await persistRuntimeState ( ) ;
92+ await storeEndedSession ( endedSession ) ;
93+ return endedSession ;
94+ }
12495
125- await endActiveSession ( reason ) ;
126- state . activeSession = createSession ( tab ) ;
96+ async function startOrSwitchSession ( tab , reason ) {
97+ const result = sessionEngine . startOrSwitchSession ( tab , reason ) ;
98+ await persistRuntimeState ( ) ;
99+ await storeEndedSession ( result . endedSession ) ;
100+ return result ;
127101}
128102
129103async function cacheTabSnapshot ( tab ) {
@@ -189,6 +163,7 @@ async function runRetentionCleanup() {
189163async function initializeExtension ( reason ) {
190164 await initDatabase ( ) ;
191165 await refreshSettingsCache ( ) ;
166+ await loadRuntimeState ( ) ;
192167
193168 await chrome . alarms . clear ( CLEANUP_ALARM ) ;
194169 await chrome . alarms . create ( CLEANUP_ALARM , { periodInMinutes : CLEANUP_INTERVAL_MINUTES } ) ;
@@ -226,29 +201,23 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
226201 console . error ( "Failed to cache tab snapshot" , error ) ;
227202 } ) ;
228203
229- if (
230- state . activeSession &&
231- tabId === state . activeSession . tabId &&
232- typeof changeInfo . url === "string" &&
233- isTrackableUrl ( changeInfo . url )
234- ) {
235- state . activeSession . url = changeInfo . url ;
236- state . activeSession . domain = extractDomain ( changeInfo . url ) ;
237- }
238-
239- if ( state . activeSession && tabId === state . activeSession . tabId && typeof changeInfo . title === "string" ) {
240- state . activeSession . title = readableTitle ( changeInfo . title , state . activeSession . url ) ;
204+ const activeSessionChanged = sessionEngine . updateActiveSessionMetadata ( tabId , changeInfo ) ;
205+ if ( activeSessionChanged ) {
206+ persistRuntimeState ( ) . catch ( ( error ) => {
207+ console . error ( "Failed to persist runtime state on metadata update" , error ) ;
208+ } ) ;
241209 }
242210
243- if ( tab . active && tab . windowId === state . focusedWindowId && ( changeInfo . url || changeInfo . status === "complete" ) ) {
211+ if ( tab && tab . active && tab . windowId === state . focusedWindowId && ( changeInfo . url || changeInfo . status === "complete" ) ) {
244212 syncCurrentActiveContext ( "navigation" ) . catch ( ( error ) => {
245213 console . error ( "Failed to sync on navigation" , error ) ;
246214 } ) ;
247215 }
248216} ) ;
249217
250218chrome . tabs . onRemoved . addListener ( ( tabId ) => {
251- if ( state . activeSession && tabId === state . activeSession . tabId ) {
219+ const activeSession = getActiveSession ( ) ;
220+ if ( activeSession && tabId === activeSession . tabId ) {
252221 endActiveSession ( "tab_closed" )
253222 . then ( ( ) => syncCurrentActiveContext ( "tab_closed" ) )
254223 . catch ( ( error ) => {
@@ -316,9 +285,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
316285 }
317286
318287 if ( message ?. type === "get-runtime-status" ) {
288+ const runtimeState = sessionEngine . readState ( ) ;
319289 sendResponse ( {
320290 ok : true ,
321- activeSession : state . activeSession ,
291+ activeSession : runtimeState . activeSession ,
322292 paused : state . paused ,
323293 retentionDays : state . retentionDays ,
324294 idleState : state . idleState
0 commit comments