@@ -3,12 +3,14 @@ import type {
33 AssessmentRun ,
44 ResultTone ,
55 ResultsCounts ,
6+ SpreadsheetStateEnvelope ,
67 StatusFilter ,
78} from "@/app/lib/types/assessment" ;
89import {
910 ACTIVE_ASSESSMENT_STATUSES ,
1011 COMPLETED_ASSESSMENT_STATUSES ,
1112 FAILED_ASSESSMENT_STATUSES ,
13+ SPREADSHEET_STATE_SCHEMA_VERSION ,
1214 SPREADSHEET_STATE_STORAGE_PREFIX ,
1315} from "@/app/lib/assessment/constants" ;
1416
@@ -77,14 +79,66 @@ export function spreadsheetStorageKey(runId: number): string {
7779export function loadSpreadsheetState ( runId : number ) : object | null {
7880 try {
7981 const raw = localStorage . getItem ( spreadsheetStorageKey ( runId ) ) ;
80- return raw ? ( JSON . parse ( raw ) as object ) : null ;
82+ if ( ! raw ) return null ;
83+ const parsed = JSON . parse ( raw ) as Partial < SpreadsheetStateEnvelope > ;
84+ if ( parsed ?. v !== SPREADSHEET_STATE_SCHEMA_VERSION || ! parsed . data ) {
85+ localStorage . removeItem ( spreadsheetStorageKey ( runId ) ) ;
86+ return null ;
87+ }
88+ return parsed . data ;
8189 } catch {
8290 return null ;
8391 }
8492}
8593
94+ // Evict oldest spreadsheet-state entries until below `keep` count.
95+ function evictOldestSpreadsheetStates ( keep : number ) : void {
96+ const entries : Array < { key : string ; ts : number } > = [ ] ;
97+ for ( let i = 0 ; i < localStorage . length ; i ++ ) {
98+ const key = localStorage . key ( i ) ;
99+ if ( ! key ?. startsWith ( SPREADSHEET_STATE_STORAGE_PREFIX ) ) continue ;
100+ try {
101+ const raw = localStorage . getItem ( key ) ;
102+ if ( ! raw ) continue ;
103+ const parsed = JSON . parse ( raw ) as Partial < SpreadsheetStateEnvelope > ;
104+ entries . push ( { key, ts : parsed ?. ts ?? 0 } ) ;
105+ } catch {
106+ // malformed — treat as oldest so it gets dropped first
107+ entries . push ( { key, ts : 0 } ) ;
108+ }
109+ }
110+ entries . sort ( ( a , b ) => a . ts - b . ts ) ;
111+ const toDrop = Math . max ( 0 , entries . length - keep ) ;
112+ for ( let i = 0 ; i < toDrop ; i ++ ) {
113+ localStorage . removeItem ( entries [ i ] . key ) ;
114+ }
115+ }
116+
86117export function persistSpreadsheetState ( runId : number , data : object ) : void {
87- localStorage . setItem ( spreadsheetStorageKey ( runId ) , JSON . stringify ( data ) ) ;
118+ const envelope : SpreadsheetStateEnvelope = {
119+ v : SPREADSHEET_STATE_SCHEMA_VERSION ,
120+ ts : Date . now ( ) ,
121+ data,
122+ } ;
123+ const key = spreadsheetStorageKey ( runId ) ;
124+ const payload = JSON . stringify ( envelope ) ;
125+ try {
126+ localStorage . setItem ( key , payload ) ;
127+ } catch ( err ) {
128+ // Quota exceeded — drop oldest sheets (keep current) and retry once
129+ const isQuota =
130+ err instanceof DOMException &&
131+ ( err . name === "QuotaExceededError" ||
132+ err . name === "NS_ERROR_DOM_QUOTA_REACHED" ) ;
133+ if ( ! isQuota ) return ;
134+ try {
135+ localStorage . removeItem ( key ) ;
136+ evictOldestSpreadsheetStates ( 5 ) ;
137+ localStorage . setItem ( key , payload ) ;
138+ } catch {
139+ // still failing — give up silently; in-memory state remains intact
140+ }
141+ }
88142}
89143
90144type SpreadsheetCellEntry = { v : string | number ; t : number ; s ?: object } ;
0 commit comments