@@ -65,24 +65,45 @@ import {
65
65
symmetricDifference ,
66
66
type ,
67
67
} from 'ramda' ;
68
+ import { createAction } from 'redux-actions' ;
69
+ import uniqid from 'uniqid' ;
68
70
69
71
import Registry from './registry' ;
70
72
71
73
const storePrefix = '_dash_persistence.' ;
72
74
const UNDEFINED = 'U' ;
73
75
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
+
74
92
/*
75
93
* 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)
77
96
*/
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 ;
81
101
}
82
102
83
103
class WebStore {
84
- constructor ( storage ) {
85
- this . _storage = storage ;
104
+ constructor ( backEnd ) {
105
+ this . _name = backEnd ;
106
+ this . _storage = window [ backEnd ] ;
86
107
}
87
108
88
109
hasItem ( key ) {
@@ -96,18 +117,37 @@ class WebStore {
96
117
return gotVal === UNDEFINED ? void 0 : JSON . parse ( gotVal ) ;
97
118
}
98
119
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 ) {
100
125
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
+ }
102
138
}
103
139
104
140
removeItem ( key ) {
105
141
this . _storage . removeItem ( storePrefix + key ) ;
106
142
}
107
143
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
+ */
108
148
clear ( keyPrefix ) {
109
- const fullPrefix = storePrefix + keyPrefix ;
110
- const keyMatch = keyPrefixMatch ( fullPrefix ) ;
149
+ const fullPrefix = storePrefix + ( keyPrefix || '' ) ;
150
+ const keyMatch = keyPrefixMatch ( fullPrefix , keyPrefix ? '.' : '' ) ;
111
151
const keysToRemove = [ ] ;
112
152
// 2-step process, so we don't depend on any particular behavior of
113
153
// key order while removing some
@@ -117,7 +157,7 @@ class WebStore {
117
157
keysToRemove . push ( fullKey ) ;
118
158
}
119
159
}
120
- forEach ( this . _storage . removeItem , keysToRemove ) ;
160
+ forEach ( k => this . _storage . removeItem ( k ) , keysToRemove ) ;
121
161
}
122
162
}
123
163
@@ -145,19 +185,82 @@ class MemStore {
145
185
}
146
186
147
187
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 ;
152
207
}
208
+ return s ;
153
209
}
154
210
155
211
const stores = {
156
- local : new WebStore ( window . localStorage ) ,
157
- session : new WebStore ( window . sessionStorage ) ,
158
212
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.
159
215
} ;
160
216
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
+
161
264
const noopTransform = {
162
265
extract : propValue => propValue ,
163
266
apply : ( storedValue , _propValue ) => storedValue ,
@@ -195,7 +298,7 @@ const getProps = layout => {
195
298
return { id, props, element, persistence, persisted_props, persistence_type} ;
196
299
} ;
197
300
198
- export function recordUiEdit ( layout , newProps ) {
301
+ export function recordUiEdit ( layout , newProps , dispatch ) {
199
302
const {
200
303
id,
201
304
props,
@@ -211,7 +314,7 @@ export function recordUiEdit(layout, newProps) {
211
314
forEach ( persistedProp => {
212
315
const [ propName , propPart ] = persistedProp . split ( '.' ) ;
213
316
if ( newProps [ propName ] ) {
214
- const storage = stores [ persistence_type ] ;
317
+ const storage = getStore ( persistence_type , dispatch ) ;
215
318
const { extract} = getTransform ( element , propName , propPart ) ;
216
319
217
320
const newValKey = getNewValKey ( id , persistedProp ) ;
@@ -227,17 +330,21 @@ export function recordUiEdit(layout, newProps) {
227
330
! storage . hasItem ( newValKey ) ||
228
331
storage . getItem ( persistIdKey ) !== persistence
229
332
) {
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 ) ;
232
339
}
233
- storage . setItem ( newValKey , newVal ) ;
340
+ storage . setItem ( newValKey , newVal , dispatch ) ;
234
341
}
235
342
}
236
343
} , persisted_props ) ;
237
344
}
238
345
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 ) ;
241
348
const newValKey = getNewValKey ( id , persistedProp ) ;
242
349
243
350
if ( storage . hasItem ( newValKey ) ) {
@@ -251,15 +358,15 @@ function clearUIEdit(id, persistence_type, persistedProp) {
251
358
* Used for entire layouts (on load) or partial layouts (from children
252
359
* callbacks) to apply previously-stored UI edits to components
253
360
*/
254
- export function applyPersistence ( layout ) {
361
+ export function applyPersistence ( layout , dispatch ) {
255
362
if ( type ( layout ) !== 'Object' || ! layout . props ) {
256
363
return layout ;
257
364
}
258
365
259
- return persistenceMods ( layout , layout , [ ] ) ;
366
+ return persistenceMods ( layout , layout , [ ] , dispatch ) ;
260
367
}
261
368
262
- function persistenceMods ( layout , component , path ) {
369
+ function persistenceMods ( layout , component , path , dispatch ) {
263
370
const {
264
371
id,
265
372
props,
@@ -271,7 +378,7 @@ function persistenceMods(layout, component, path) {
271
378
272
379
let layoutOut = layout ;
273
380
if ( persistence ) {
274
- const storage = stores [ persistence_type ] ;
381
+ const storage = getStore ( persistence_type , dispatch ) ;
275
382
const update = { } ;
276
383
forEach ( persistedProp => {
277
384
const [ propName , propPart ] = persistedProp . split ( '.' ) ;
@@ -294,7 +401,7 @@ function persistenceMods(layout, component, path) {
294
401
propName in update ? update [ propName ] : props [ propName ]
295
402
) ;
296
403
} else {
297
- clearUIEdit ( id , persistence_type , persistedProp ) ;
404
+ clearUIEdit ( id , persistence_type , persistedProp , dispatch ) ;
298
405
}
299
406
}
300
407
} , persisted_props ) ;
@@ -316,16 +423,17 @@ function persistenceMods(layout, component, path) {
316
423
layoutOut = persistenceMods (
317
424
layoutOut ,
318
425
child ,
319
- path . concat ( 'props' , 'children' , i )
426
+ path . concat ( 'props' , 'children' , i ) ,
427
+ dispatch
320
428
) ;
321
429
}
322
430
} ) ;
323
- forEach ( applyPersistence , children ) ;
324
431
} else if ( type ( children ) === 'Object' && children . props ) {
325
432
layoutOut = persistenceMods (
326
433
layoutOut ,
327
434
children ,
328
- path . concat ( 'props' , 'children' )
435
+ path . concat ( 'props' , 'children' ) ,
436
+ dispatch
329
437
) ;
330
438
}
331
439
return layoutOut ;
@@ -336,7 +444,7 @@ function persistenceMods(layout, component, path) {
336
444
* these override UI-driven edits of those exact props
337
445
* but not for props nested inside children
338
446
*/
339
- export function prunePersistence ( layout , newProps ) {
447
+ export function prunePersistence ( layout , newProps , dispatch ) {
340
448
const {
341
449
id,
342
450
persistence,
@@ -354,15 +462,16 @@ export function prunePersistence(layout, newProps) {
354
462
( 'persistence_type' in newProps &&
355
463
newProps . persistence_type !== persistence_type )
356
464
) {
357
- stores [ persistence_type ] . clear ( id ) ;
465
+ getStore ( persistence_type , dispatch ) . clear ( id ) ;
358
466
return ;
359
467
}
360
468
361
469
// if the persisted props list itself changed, clear any props not
362
470
// present in both the new and old
363
471
if ( 'persisted_props' in newProps ) {
364
472
forEach (
365
- persistedProp => clearUIEdit ( id , persistence_type , persistedProp ) ,
473
+ persistedProp =>
474
+ clearUIEdit ( id , persistence_type , persistedProp , dispatch ) ,
366
475
symmetricDifference ( persisted_props , newProps . persisted_props )
367
476
) ;
368
477
}
@@ -374,10 +483,15 @@ export function prunePersistence(layout, newProps) {
374
483
const propTransforms = transforms [ propName ] ;
375
484
if ( propTransforms ) {
376
485
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
+ ) ;
378
492
}
379
493
} else {
380
- clearUIEdit ( id , persistence_type , propName ) ;
494
+ clearUIEdit ( id , persistence_type , propName , dispatch ) ;
381
495
}
382
496
}
383
497
}
0 commit comments