@@ -65,24 +65,45 @@ import {
6565 symmetricDifference ,
6666 type ,
6767} from 'ramda' ;
68+ import { createAction } from 'redux-actions' ;
69+ import uniqid from 'uniqid' ;
6870
6971import Registry from './registry' ;
7072
7173const storePrefix = '_dash_persistence.' ;
7274const UNDEFINED = 'U' ;
7375
76+ function err ( e ) {
77+ const error = typeof e === 'string' ? new Error ( e ) : e ;
78+
79+ /* eslint-disable no-console */
80+ // Send this to the console too, so it's still available with debug off
81+ console . error ( e ) ;
82+ /* eslint-disable no-console */
83+
84+ return createAction ( 'ON_ERROR' ) ( {
85+ myUID : uniqid ( ) ,
86+ myID : storePrefix ,
87+ type : 'frontEnd' ,
88+ error,
89+ } ) ;
90+ }
91+
7492/*
7593 * Does a key fit this prefix? Must either be an exact match
76- * or a scoped match - exact prefix followed by a dot (then anything else)
94+ * or, if a separator is provided, a scoped match - exact prefix
95+ * followed by the separator (then anything else)
7796 */
78- function keyPrefixMatch ( prefix ) {
79- return key =>
80- key === prefix || key . substr ( 0 , prefix . length + 1 ) === prefix + '.' ;
97+ function keyPrefixMatch ( prefix , separator ) {
98+ const fullStr = prefix + separator ;
99+ const fullLen = fullStr . length ;
100+ return key => key === prefix || key . substr ( 0 , fullLen ) === fullStr ;
81101}
82102
83103class WebStore {
84- constructor ( storage ) {
85- this . _storage = storage ;
104+ constructor ( backEnd ) {
105+ this . _name = backEnd ;
106+ this . _storage = window [ backEnd ] ;
86107 }
87108
88109 hasItem ( key ) {
@@ -96,18 +117,37 @@ class WebStore {
96117 return gotVal === UNDEFINED ? void 0 : JSON . parse ( gotVal ) ;
97118 }
98119
99- setItem ( key , value ) {
120+ /*
121+ * In addition to the regular key->value to set, setItem takes
122+ * dispatch as a parameter, so it can report OOM to devtools
123+ */
124+ setItem ( key , value , dispatch ) {
100125 const setVal = value === void 0 ? UNDEFINED : JSON . stringify ( value ) ;
101- this . _storage . setItem ( storePrefix + key , setVal ) ;
126+ try {
127+ this . _storage . setItem ( storePrefix + key , setVal ) ;
128+ } catch ( e ) {
129+ if ( dispatch ) {
130+ dispatch ( err ( e ) ) ;
131+ } else {
132+ throw e ;
133+ }
134+ // TODO: Should we clear storage here? Or fall back to memory?
135+ // Probably not, unless we want to handle this at a higher level
136+ // so we can keep all 3 items in sync
137+ }
102138 }
103139
104140 removeItem ( key ) {
105141 this . _storage . removeItem ( storePrefix + key ) ;
106142 }
107143
144+ /*
145+ * clear matching keys matching (optionally followed by a dot and more
146+ * characters) - or all keys associated with this store if no prefix.
147+ */
108148 clear ( keyPrefix ) {
109- const fullPrefix = storePrefix + keyPrefix ;
110- const keyMatch = keyPrefixMatch ( fullPrefix ) ;
149+ const fullPrefix = storePrefix + ( keyPrefix || '' ) ;
150+ const keyMatch = keyPrefixMatch ( fullPrefix , keyPrefix ? '.' : '' ) ;
111151 const keysToRemove = [ ] ;
112152 // 2-step process, so we don't depend on any particular behavior of
113153 // key order while removing some
@@ -117,7 +157,7 @@ class WebStore {
117157 keysToRemove . push ( fullKey ) ;
118158 }
119159 }
120- forEach ( this . _storage . removeItem , keysToRemove ) ;
160+ forEach ( k => this . _storage . removeItem ( k ) , keysToRemove ) ;
121161 }
122162}
123163
@@ -145,19 +185,82 @@ class MemStore {
145185 }
146186
147187 clear ( keyPrefix ) {
148- forEach (
149- key => delete this . _data [ key ] ,
150- filter ( keyPrefixMatch ( keyPrefix ) , keys ( this . _data ) )
151- ) ;
188+ if ( keyPrefix ) {
189+ forEach (
190+ key => delete this . _data [ key ] ,
191+ filter ( keyPrefixMatch ( keyPrefix , '.' ) , keys ( this . _data ) )
192+ ) ;
193+ } else {
194+ this . _data = { } ;
195+ }
196+ }
197+ }
198+
199+ // Make a string 2^16 characters long (*2 bytes/char = 130kB), to test storage.
200+ // That should be plenty for common persistence use cases,
201+ // without getting anywhere near typical browser limits
202+ const pow = 16 ;
203+ function longString ( ) {
204+ let s = 'Spam' ;
205+ for ( let i = 2 ; i < pow ; i ++ ) {
206+ s += s ;
152207 }
208+ return s ;
153209}
154210
155211const stores = {
156- local : new WebStore ( window . localStorage ) ,
157- session : new WebStore ( window . sessionStorage ) ,
158212 memory : new MemStore ( ) ,
213+ // Defer testing & making local/session stores until requested.
214+ // That way if we have errors here they can show up in devtools.
159215} ;
160216
217+ const backEnds = {
218+ local : 'localStorage' ,
219+ session : 'sessionStorage' ,
220+ } ;
221+
222+ function tryGetWebStore ( backEnd , dispatch ) {
223+ const store = new WebStore ( backEnd ) ;
224+ const fallbackStore = stores . memory ;
225+ const storeTest = longString ( ) ;
226+ const testKey = 'x.x' ;
227+ try {
228+ store . setItem ( testKey , storeTest ) ;
229+ if ( store . getItem ( testKey ) !== storeTest ) {
230+ dispatch (
231+ err ( `${ backEnd } init failed set/get, falling back to memory` )
232+ ) ;
233+ return fallbackStore ;
234+ }
235+ store . removeItem ( testKey ) ;
236+ return store ;
237+ } catch ( e ) {
238+ dispatch (
239+ err ( `${ backEnd } init first try failed; clearing and retrying` )
240+ ) ;
241+ }
242+ try {
243+ store . clear ( ) ;
244+ store . setItem ( testKey , storeTest ) ;
245+ if ( store . getItem ( testKey ) !== storeTest ) {
246+ throw new Error ( 'nope' ) ;
247+ }
248+ store . removeItem ( testKey ) ;
249+ dispatch ( err ( `${ backEnd } init set/get succeeded after clearing!` ) ) ;
250+ return store ;
251+ } catch ( e ) {
252+ dispatch ( err ( `${ backEnd } init still failed, falling back to memory` ) ) ;
253+ return fallbackStore ;
254+ }
255+ }
256+
257+ function getStore ( type , dispatch ) {
258+ if ( ! stores [ type ] ) {
259+ stores [ type ] = tryGetWebStore ( backEnds [ type ] , dispatch ) ;
260+ }
261+ return stores [ type ] ;
262+ }
263+
161264const noopTransform = {
162265 extract : propValue => propValue ,
163266 apply : ( storedValue , _propValue ) => storedValue ,
@@ -195,7 +298,7 @@ const getProps = layout => {
195298 return { id, props, element, persistence, persisted_props, persistence_type} ;
196299} ;
197300
198- export function recordUiEdit ( layout , newProps ) {
301+ export function recordUiEdit ( layout , newProps , dispatch ) {
199302 const {
200303 id,
201304 props,
@@ -211,7 +314,7 @@ export function recordUiEdit(layout, newProps) {
211314 forEach ( persistedProp => {
212315 const [ propName , propPart ] = persistedProp . split ( '.' ) ;
213316 if ( newProps [ propName ] ) {
214- const storage = stores [ persistence_type ] ;
317+ const storage = getStore ( persistence_type , dispatch ) ;
215318 const { extract} = getTransform ( element , propName , propPart ) ;
216319
217320 const newValKey = getNewValKey ( id , persistedProp ) ;
@@ -227,17 +330,21 @@ export function recordUiEdit(layout, newProps) {
227330 ! storage . hasItem ( newValKey ) ||
228331 storage . getItem ( persistIdKey ) !== persistence
229332 ) {
230- storage . setItem ( getOriginalValKey ( newValKey ) , previousVal ) ;
231- storage . setItem ( persistIdKey , persistence ) ;
333+ storage . setItem (
334+ getOriginalValKey ( newValKey ) ,
335+ previousVal ,
336+ dispatch
337+ ) ;
338+ storage . setItem ( persistIdKey , persistence , dispatch ) ;
232339 }
233- storage . setItem ( newValKey , newVal ) ;
340+ storage . setItem ( newValKey , newVal , dispatch ) ;
234341 }
235342 }
236343 } , persisted_props ) ;
237344}
238345
239- function clearUIEdit ( id , persistence_type , persistedProp ) {
240- const storage = stores [ persistence_type ] ;
346+ function clearUIEdit ( id , persistence_type , persistedProp , dispatch ) {
347+ const storage = getStore ( persistence_type , dispatch ) ;
241348 const newValKey = getNewValKey ( id , persistedProp ) ;
242349
243350 if ( storage . hasItem ( newValKey ) ) {
@@ -251,15 +358,15 @@ function clearUIEdit(id, persistence_type, persistedProp) {
251358 * Used for entire layouts (on load) or partial layouts (from children
252359 * callbacks) to apply previously-stored UI edits to components
253360 */
254- export function applyPersistence ( layout ) {
361+ export function applyPersistence ( layout , dispatch ) {
255362 if ( type ( layout ) !== 'Object' || ! layout . props ) {
256363 return layout ;
257364 }
258365
259- return persistenceMods ( layout , layout , [ ] ) ;
366+ return persistenceMods ( layout , layout , [ ] , dispatch ) ;
260367}
261368
262- function persistenceMods ( layout , component , path ) {
369+ function persistenceMods ( layout , component , path , dispatch ) {
263370 const {
264371 id,
265372 props,
@@ -271,7 +378,7 @@ function persistenceMods(layout, component, path) {
271378
272379 let layoutOut = layout ;
273380 if ( persistence ) {
274- const storage = stores [ persistence_type ] ;
381+ const storage = getStore ( persistence_type , dispatch ) ;
275382 const update = { } ;
276383 forEach ( persistedProp => {
277384 const [ propName , propPart ] = persistedProp . split ( '.' ) ;
@@ -294,7 +401,7 @@ function persistenceMods(layout, component, path) {
294401 propName in update ? update [ propName ] : props [ propName ]
295402 ) ;
296403 } else {
297- clearUIEdit ( id , persistence_type , persistedProp ) ;
404+ clearUIEdit ( id , persistence_type , persistedProp , dispatch ) ;
298405 }
299406 }
300407 } , persisted_props ) ;
@@ -316,16 +423,17 @@ function persistenceMods(layout, component, path) {
316423 layoutOut = persistenceMods (
317424 layoutOut ,
318425 child ,
319- path . concat ( 'props' , 'children' , i )
426+ path . concat ( 'props' , 'children' , i ) ,
427+ dispatch
320428 ) ;
321429 }
322430 } ) ;
323- forEach ( applyPersistence , children ) ;
324431 } else if ( type ( children ) === 'Object' && children . props ) {
325432 layoutOut = persistenceMods (
326433 layoutOut ,
327434 children ,
328- path . concat ( 'props' , 'children' )
435+ path . concat ( 'props' , 'children' ) ,
436+ dispatch
329437 ) ;
330438 }
331439 return layoutOut ;
@@ -336,7 +444,7 @@ function persistenceMods(layout, component, path) {
336444 * these override UI-driven edits of those exact props
337445 * but not for props nested inside children
338446 */
339- export function prunePersistence ( layout , newProps ) {
447+ export function prunePersistence ( layout , newProps , dispatch ) {
340448 const {
341449 id,
342450 persistence,
@@ -354,15 +462,16 @@ export function prunePersistence(layout, newProps) {
354462 ( 'persistence_type' in newProps &&
355463 newProps . persistence_type !== persistence_type )
356464 ) {
357- stores [ persistence_type ] . clear ( id ) ;
465+ getStore ( persistence_type , dispatch ) . clear ( id ) ;
358466 return ;
359467 }
360468
361469 // if the persisted props list itself changed, clear any props not
362470 // present in both the new and old
363471 if ( 'persisted_props' in newProps ) {
364472 forEach (
365- persistedProp => clearUIEdit ( id , persistence_type , persistedProp ) ,
473+ persistedProp =>
474+ clearUIEdit ( id , persistence_type , persistedProp , dispatch ) ,
366475 symmetricDifference ( persisted_props , newProps . persisted_props )
367476 ) ;
368477 }
@@ -374,10 +483,15 @@ export function prunePersistence(layout, newProps) {
374483 const propTransforms = transforms [ propName ] ;
375484 if ( propTransforms ) {
376485 for ( const propPart in propTransforms ) {
377- clearUIEdit ( id , persistence_type , `${ propName } .${ propPart } ` ) ;
486+ clearUIEdit (
487+ id ,
488+ persistence_type ,
489+ `${ propName } .${ propPart } ` ,
490+ dispatch
491+ ) ;
378492 }
379493 } else {
380- clearUIEdit ( id , persistence_type , propName ) ;
494+ clearUIEdit ( id , persistence_type , propName , dispatch ) ;
381495 }
382496 }
383497}
0 commit comments