Skip to content

Commit

Permalink
feat: restore fallback for broken sessionStorage (#74)
Browse files Browse the repository at this point in the history
The subset of the history of browser history stack entries within the current session is mirrored in sessionStorage so that it can be restored into Redux on page reload or on navigation from external history stack entries (using the borwser back/forward buttons, reload button, unfreezing, etc). There used to be a fallback for browsers where sessionStorage is not supported which instead stored the entries inside the current history state. This fallback was removed as part of #61. However, although all modern browsers officially support sessionStorage, there are various circumstances in which it fails to work:
- if the user fills the entire quota (very unlikely that Rudy itself would do this, but other user code might)
- in private browsing mode on iOS Safari
- if all cookies are disabled in Chrome (and possibly also in some modified versions of chrome for android)

This change restores that fallback. It comes with the same caveats that were there before:
- since only the current history index is accessible, only the current and previous stack entries can be seen. This means that when returning to a stack entry in the middle of r the Rudy stack, the redux mirror of the stack entries will only include current and past entries, not future ones.
- as a consequence, route callbacks that occur before a transition to a route will not work when navigating forward beyond the entries in the redux state.
  • Loading branch information
hedgepigdaniel committed Mar 27, 2020
1 parent 30e698c commit 8df1154
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 14 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
**/.gitignore
**/.flowconfig
**/.npmignore
**/.eslintcache

# Ignored packages outside sub packages
LICENSE
Expand Down
51 changes: 37 additions & 14 deletions packages/rudy/src/history/utils/sessionStorage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow
/* eslint-env browser */
import { toEntries } from '../../utils'
import { supportsSessionStorage } from '@respond-framework/utils'

// API:

Expand All @@ -9,16 +10,16 @@ import { toEntries } from '../../utils'
// - `saveHistory` is called every time the history entries or index changes
// - `restoreHistory` is called on startup obviously

// Essentially the idea is that if there is no `sessionStorage`, we maintain the entire
// storage object on EACH AND EVERY history entry's `state`. I.e. `history.state` on
// every page will have the `index` and `entries` array. That way when browsers disable
// cookies/sessionStorage, we can still grab the data we need off off of history state :)
//
// It's a bit crazy, but it works very well, and there's plenty of space allowed for storing
// things there to get a lot of mileage out of it. We store the minimum amount of data necessary.
//
// Firefox has the lowest limit of 640kb PER ENTRY. IE has 1mb and chrome has at least 10mb:
// https://stackoverflow.com/questions/6460377/html5-history-api-what-is-the-max-size-the-state-object-can-be
/**
* If there is no `sessionStorage` (which happens e.g. in incognito mode in
* iOS safari), we have a fallback which is to store the history of stack
* entries inside the current browser history stack entry. Since we can only
* access the current history stack entry, this means that if the user
* returns to the middle of a set of entries within the app, then Rudy will
* not be aware of the future entries. Navigation will still work, but the
* entries in the redux state will not include future states, and callbacks
* related to future states will therefore not work.
*/

export const saveHistory = ({ entries }) => {
entries = entries.map((e) => [e.location.url, e.state, e.location.key]) // one entry has the url, a state object, and a 6 digit key
Expand All @@ -36,17 +37,39 @@ export const restoreHistory = (api) => {
}

export const clear = () => {
window.sessionStorage.setItem(key(), '')
if (supportsSessionStorage()) {
window.sessionStorage.setItem(key(), '')
} else {
const state = window.history.state
if (state) {
delete state.stack
window.history.replaceState(state, null)
}
}
historySet({ index: 0, id: key() })
}

const set = (val) => window.sessionStorage.setItem(key(), JSON.stringify(val))
const set = (val) => {
const json = JSON.stringify(val)
if (supportsSessionStorage()) {
window.sessionStorage.setItem(key(), json)
} else {
const state = window.history.state || {}
state.stack = json
window.history.replaceState(state, null)
}
}

export const get = () => {
let json
if (supportsSessionStorage()) {
json = window.sessionStorage.getItem(key())
} else {
json = (window.history.state || {}).stack
}
try {
const json = window.sessionStorage.getItem(key())
return JSON.parse(json)
} catch (error) {
} catch {
return null
}
}
Expand Down
7 changes: 7 additions & 0 deletions packages/scroll-restorer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ScrollRestorer,
ScrollRestorerCreator,
} from '@respond-framework/types'
import { supportsSessionStorage } from '@respond-framework/utils'

export { ScrollPosition } from 'scroll-behavior'

Expand Down Expand Up @@ -57,6 +58,9 @@ export class RudyScrollRestorer<Action extends FluxStandardRoutingAction>
key: string | null,
value: ScrollPosition,
): void => {
if (!supportsSessionStorage()) {
return
}
window.sessionStorage.setItem(
this.makeStorageKey(entry, key),
JSON.stringify(value),
Expand All @@ -67,6 +71,9 @@ export class RudyScrollRestorer<Action extends FluxStandardRoutingAction>
entry: LocationEntry<Action>,
key: string | null,
): ScrollPosition | null => {
if (!supportsSessionStorage()) {
return null
}
const savedItem = window.sessionStorage.getItem(
this.makeStorageKey(entry, key),
)
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as isServer } from './isServer'
export { default as createSelector } from './createSelector'
export { default as supportsSessionStorage } from './supportsSessionStorage'
27 changes: 27 additions & 0 deletions packages/utils/src/supportsSessionStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/* eslint-env browser */

let _supportsSessionStorage: boolean

export default (): boolean => {
if (_supportsSessionStorage !== undefined) {
return _supportsSessionStorage
}
try {
window.sessionStorage.setItem('rudytestitem', 'testvalue')
if (window.sessionStorage.getItem('rudytestitem') === 'testvalue') {
window.sessionStorage.removeItem('rudytestitem')
_supportsSessionStorage = true
} else {
_supportsSessionStorage = false
}
} catch {
_supportsSessionStorage = false
}
if (!_supportsSessionStorage) {
// eslint-disable-next-line no-console
console.warn(
'[rudy]: WARNING: This browser does not support sessionStorage!',
)
}
return _supportsSessionStorage
}

0 comments on commit 8df1154

Please sign in to comment.