Skip to content

Commit 83b8cd3

Browse files
committed
fallback if web store isn't available, or is full from the start
1 parent 78df979 commit 83b8cd3

File tree

4 files changed

+159
-41
lines changed

4 files changed

+159
-41
lines changed

dash-renderer/src/APIController.react.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ class UnconnectedContainer extends Component {
4949
dispatch(apiThunk('_dash-layout', 'GET', 'layoutRequest'));
5050
} else if (layoutRequest.status === STATUS.OK) {
5151
if (isEmpty(layout)) {
52-
dispatch(setLayout(applyPersistence(layoutRequest.content)));
52+
const finalLayout = applyPersistence(
53+
layoutRequest.content,
54+
dispatch
55+
);
56+
dispatch(setLayout(finalLayout));
5357
} else if (isNil(paths)) {
5458
dispatch(computePaths({subTree: layout, startingPath: []}));
5559
}

dash-renderer/src/TreeContainer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ class TreeContainer extends Component {
189189

190190
// setProps here is triggered by the UI - record these changes
191191
// for persistence
192-
recordUiEdit(_dashprivate_layout, newProps);
192+
recordUiEdit(_dashprivate_layout, newProps, _dashprivate_dispatch);
193193

194194
// Always update this component's props
195195
_dashprivate_dispatch(

dash-renderer/src/actions/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,11 +541,11 @@ function updateOutput(
541541

542542
// This is a callback-generated update.
543543
// Check if this invalidates existing persisted prop values,
544-
prunePersistence(path(itempath, layout), updatedProps);
544+
prunePersistence(path(itempath, layout), updatedProps, dispatch);
545545

546546
// In case the update contains whole components, see if any of
547547
// those components have props to update to persist user edits.
548-
const finalProps = applyPersistence(updatedProps);
548+
const finalProps = applyPersistence(updatedProps, dispatch);
549549

550550
dispatch(
551551
updateProps({

dash-renderer/src/persistence.js

Lines changed: 151 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -65,24 +65,45 @@ import {
6565
symmetricDifference,
6666
type,
6767
} from 'ramda';
68+
import {createAction} from 'redux-actions';
69+
import uniqid from 'uniqid';
6870

6971
import Registry from './registry';
7072

7173
const storePrefix = '_dash_persistence.';
7274
const 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

83103
class 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

155211
const 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+
161264
const 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

Comments
 (0)