@@ -16,6 +16,83 @@ type PersistTarget = {
1616
1717const LEGACY_STORAGE = "default.dat"
1818const GLOBAL_STORAGE = "opencode.global.dat"
19+ const LOCAL_PREFIX = "opencode."
20+ const fallback = { disabled : false }
21+ const cache = new Map < string , string > ( )
22+
23+ function quota ( error : unknown ) {
24+ if ( error instanceof DOMException ) {
25+ if ( error . name === "QuotaExceededError" ) return true
26+ if ( error . name === "NS_ERROR_DOM_QUOTA_REACHED" ) return true
27+ if ( error . name === "QUOTA_EXCEEDED_ERR" ) return true
28+ if ( error . code === 22 || error . code === 1014 ) return true
29+ return false
30+ }
31+
32+ if ( ! error || typeof error !== "object" ) return false
33+ const name = ( error as { name ?: string } ) . name
34+ if ( name === "QuotaExceededError" || name === "NS_ERROR_DOM_QUOTA_REACHED" ) return true
35+ if ( name && / q u o t a / i. test ( name ) ) return true
36+
37+ const code = ( error as { code ?: number } ) . code
38+ if ( code === 22 || code === 1014 ) return true
39+
40+ const message = ( error as { message ?: string } ) . message
41+ if ( typeof message !== "string" ) return false
42+ if ( / q u o t a / i. test ( message ) ) return true
43+ return false
44+ }
45+
46+ type Evict = { key : string ; size : number }
47+
48+ function evict ( storage : Storage , keep : string , value : string ) {
49+ const total = storage . length
50+ const indexes = Array . from ( { length : total } , ( _ , index ) => index )
51+ const items : Evict [ ] = [ ]
52+
53+ for ( const index of indexes ) {
54+ const name = storage . key ( index )
55+ if ( ! name ) continue
56+ if ( ! name . startsWith ( LOCAL_PREFIX ) ) continue
57+ if ( name === keep ) continue
58+ const stored = storage . getItem ( name )
59+ items . push ( { key : name , size : stored ?. length ?? 0 } )
60+ }
61+
62+ items . sort ( ( a , b ) => b . size - a . size )
63+
64+ for ( const item of items ) {
65+ storage . removeItem ( item . key )
66+
67+ try {
68+ storage . setItem ( keep , value )
69+ return true
70+ } catch ( error ) {
71+ if ( ! quota ( error ) ) throw error
72+ }
73+ }
74+
75+ return false
76+ }
77+
78+ function write ( storage : Storage , key : string , value : string ) {
79+ try {
80+ storage . setItem ( key , value )
81+ return true
82+ } catch ( error ) {
83+ if ( ! quota ( error ) ) throw error
84+ }
85+
86+ try {
87+ storage . removeItem ( key )
88+ storage . setItem ( key , value )
89+ return true
90+ } catch ( error ) {
91+ if ( ! quota ( error ) ) throw error
92+ }
93+
94+ return evict ( storage , key , value )
95+ }
1996
2097function snapshot ( value : unknown ) {
2198 return JSON . parse ( JSON . stringify ( value ) ) as unknown
@@ -67,10 +144,66 @@ function workspaceStorage(dir: string) {
67144
68145function localStorageWithPrefix ( prefix : string ) : SyncStorage {
69146 const base = `${ prefix } :`
147+ const item = ( key : string ) => base + key
148+ return {
149+ getItem : ( key ) => {
150+ const name = item ( key )
151+ const cached = cache . get ( name )
152+ if ( fallback . disabled && cached !== undefined ) return cached
153+
154+ const stored = localStorage . getItem ( name )
155+ if ( stored === null ) return cached ?? null
156+ cache . set ( name , stored )
157+ return stored
158+ } ,
159+ setItem : ( key , value ) => {
160+ const name = item ( key )
161+ cache . set ( name , value )
162+ if ( fallback . disabled ) return
163+ try {
164+ if ( write ( localStorage , name , value ) ) return
165+ } catch {
166+ fallback . disabled = true
167+ return
168+ }
169+ fallback . disabled = true
170+ } ,
171+ removeItem : ( key ) => {
172+ const name = item ( key )
173+ cache . delete ( name )
174+ if ( fallback . disabled ) return
175+ localStorage . removeItem ( name )
176+ } ,
177+ }
178+ }
179+
180+ function localStorageDirect ( ) : SyncStorage {
70181 return {
71- getItem : ( key ) => localStorage . getItem ( base + key ) ,
72- setItem : ( key , value ) => localStorage . setItem ( base + key , value ) ,
73- removeItem : ( key ) => localStorage . removeItem ( base + key ) ,
182+ getItem : ( key ) => {
183+ const cached = cache . get ( key )
184+ if ( fallback . disabled && cached !== undefined ) return cached
185+
186+ const stored = localStorage . getItem ( key )
187+ if ( stored === null ) return cached ?? null
188+ cache . set ( key , stored )
189+ return stored
190+ } ,
191+ setItem : ( key , value ) => {
192+ cache . set ( key , value )
193+ if ( fallback . disabled ) return
194+ try {
195+ if ( write ( localStorage , key , value ) ) return
196+ } catch {
197+ fallback . disabled = true
198+ return
199+ }
200+ fallback . disabled = true
201+ } ,
202+ removeItem : ( key ) => {
203+ cache . delete ( key )
204+ if ( fallback . disabled ) return
205+ localStorage . removeItem ( key )
206+ } ,
74207 }
75208}
76209
@@ -99,7 +232,7 @@ export function removePersisted(target: { storage?: string; key: string }) {
99232 }
100233
101234 if ( ! target . storage ) {
102- localStorage . removeItem ( target . key )
235+ localStorageDirect ( ) . removeItem ( target . key )
103236 return
104237 }
105238
@@ -120,12 +253,12 @@ export function persisted<T>(
120253
121254 const currentStorage = ( ( ) => {
122255 if ( isDesktop ) return platform . storage ?.( config . storage )
123- if ( ! config . storage ) return localStorage
256+ if ( ! config . storage ) return localStorageDirect ( )
124257 return localStorageWithPrefix ( config . storage )
125258 } ) ( )
126259
127260 const legacyStorage = ( ( ) => {
128- if ( ! isDesktop ) return localStorage
261+ if ( ! isDesktop ) return localStorageDirect ( )
129262 if ( ! config . storage ) return platform . storage ?.( )
130263 return platform . storage ?.( LEGACY_STORAGE )
131264 } ) ( )
0 commit comments