-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BREAKING CHANGE: storage migration #11
Changes from 6 commits
497f81a
f4dc619
f928462
704f29e
9feb53e
a49f9e3
19f48a6
cf784e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -290,12 +290,12 @@ function* handleFetchLandAmountRequest(action: FetchLandAmountRequestAction) { | |
|
||
## Storage | ||
|
||
The storage module allows you to save parts of the redux store in localStorage to make them persistent. | ||
This module is required to use other modules like `Transaction` and `Translation`. | ||
The storage module allows you to save parts of the redux store in localStorage to make them persistent and migrate it from different versions without loosing it. | ||
This module is required to use other modules like `Transaction`, `Translation`, `Wallet` and `Storage`. | ||
|
||
### Installation | ||
|
||
You need to add a middleware and a two reducers to your dApp. | ||
You need to add a middleware and two reducers to your dApp. | ||
|
||
**Middleware**: | ||
|
||
|
@@ -305,15 +305,17 @@ You will need to create a `storageMiddleware` and add apply it along with your o | |
// store.ts | ||
import { applyMiddleware, compose, createStore } from 'redux' | ||
import { createStorageMiddleware } from 'decentraland-dapps/dist/modules/storage/middleware' | ||
import { migrations } from './migrations' | ||
|
||
const composeEnhancers = | ||
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose | ||
|
||
const { storageMiddleware, loadStorageMiddleware } = createStorageMiddleware( | ||
'storage-key', // this is the key used to save the state in localStorage (required) | ||
[], // array of paths from state to be persisted (optional) | ||
[] // array of actions types that will trigger a SAVE (optional) | ||
) | ||
const { storageMiddleware, loadStorageMiddleware } = createStorageMiddleware({ | ||
storageKey: 'storage-key' // this is the key used to save the state in localStorage (required) | ||
paths: [] // array of paths from state to be persisted (optional) | ||
actions: [] // array of actions types that will trigger a SAVE (optional) | ||
migrations: migrations // migration object that will migrate your localstorage | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be marked as |
||
}) | ||
|
||
const middleware = applyMiddleware( | ||
// your other middlewares | ||
|
@@ -325,6 +327,30 @@ const store = createStore(rootReducer, enhancer) | |
loadStorageMiddleware(store) | ||
``` | ||
|
||
**Migrations**: | ||
|
||
`migrations` looks like | ||
|
||
`migrations.ts`: | ||
|
||
```ts | ||
export const migrations = { | ||
1: migrateToVersion1(data), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, good catch! |
||
2: migrateToVersion2(data), | ||
3: migrateToVersion3(data) | ||
} | ||
``` | ||
|
||
Where every `key` represent a migration and every `method` should return the new localstorage data: | ||
|
||
```ts | ||
function migrateToVersion1(data) { | ||
return omit(data, 'translations') | ||
} | ||
``` | ||
|
||
You don't need to care about updating the version of the migration because it will be set automatically. | ||
|
||
**Reducer**: | ||
|
||
You will need to add `storageReducer` as `storage` to your `rootReducer` and then wrap the whole reducer with `storageReducerWrapper` | ||
|
@@ -347,7 +373,7 @@ export const rootReducer = storageReducerWrapper( | |
|
||
### Advanced Usage | ||
|
||
This module is necessary to use other modules like `Transaction` or `Translation`, but you can also use it to make other parts of your dApp's state persistent | ||
This module is necessary to use other modules like `Transaction`, `Translation`, `Wallet` and `Storage`, but you can also use it to make other parts of your dApp's state persistent | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as the comment above #11 (comment) |
||
|
||
<details><summary>Learn More</summary> | ||
<p> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { expect } from 'chai' | ||
import { Migrations } from './types' | ||
import { | ||
hasLocalStorage, | ||
migrateStorage, | ||
getLocalStorage | ||
} from './localStorage' | ||
declare var global: any | ||
let fakeStore = {} | ||
global.window = {} | ||
|
||
describe('localStorage', function() { | ||
const migrations: Migrations<any> = { | ||
'1': (data: any) => data, | ||
'2': (data: any) => data | ||
} | ||
|
||
beforeEach(function() { | ||
fakeStore = {} | ||
global.window['localStorage'] = { | ||
getItem: (key: string) => fakeStore[key], | ||
setItem: (key: string, value: string) => (fakeStore[key] = value), | ||
removeItem: (key: string) => delete fakeStore[key] | ||
} | ||
}) | ||
|
||
describe('hasLocalStorage', function() { | ||
it('should return false if localStorage is not available', function() { | ||
delete global.window['localStorage'] | ||
expect(hasLocalStorage()).to.equal(false) | ||
}) | ||
it('should return true if localStorage is available', function() { | ||
expect(hasLocalStorage()).to.equal(true) | ||
}) | ||
}) | ||
|
||
describe('migrateStorage', function() { | ||
it('should migrate', function() { | ||
const key = 'key' | ||
const localStorage = getLocalStorage() | ||
localStorage.setItem(key, JSON.stringify('{}')) | ||
let data = JSON.parse(localStorage.getItem(key) as string) | ||
expect(data.storage).to.equal(undefined) | ||
migrateStorage(key, migrations) | ||
data = JSON.parse(localStorage.getItem(key) as string) | ||
expect(data.storage.version).to.equal(2) | ||
}) | ||
|
||
it('should not migrate if there is no migrations left', function() { | ||
const key = 'key' | ||
const localStorage = getLocalStorage() | ||
localStorage.setItem(key, JSON.stringify('{}')) | ||
let data = JSON.parse(localStorage.getItem(key) as string) | ||
expect(data.storage).to.equal(undefined) | ||
migrateStorage(key, migrations) | ||
data = JSON.parse(localStorage.getItem(key) as string) | ||
expect(data.storage.version).to.equal(2) | ||
migrateStorage(key, migrations) | ||
expect(data.storage.version).to.equal(2) | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
import { Migrations, LocalStorage } from './types' | ||
|
||
export function hasLocalStorage(): boolean { | ||
try { | ||
// https://gist.github.com/paulirish/5558557 | ||
|
@@ -11,6 +13,34 @@ export function hasLocalStorage(): boolean { | |
} | ||
} | ||
|
||
export const localStorage = hasLocalStorage() | ||
? window.localStorage | ||
: { getItem: () => null, setItem: () => null, removeItem: () => null } | ||
export function getLocalStorage(): LocalStorage { | ||
return hasLocalStorage() | ||
? window.localStorage | ||
: { | ||
getItem: () => null, | ||
setItem: () => null, | ||
removeItem: () => null | ||
} | ||
} | ||
|
||
export function migrateStorage<T>(key: string, migrations: Migrations<T>) { | ||
let version = 1 | ||
const localStorage = getLocalStorage() | ||
const dataString = localStorage.getItem(key) | ||
if (dataString) { | ||
const data = JSON.parse(dataString as string) | ||
|
||
if (data.storage) { | ||
version = parseInt(data.storage.version || 0, 10) + 1 | ||
} | ||
|
||
while (migrations[version]) { | ||
const newData = migrations[version](data) | ||
localStorage.setItem( | ||
key, | ||
JSON.stringify({ ...(newData as Object), storage: { version } }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it looks like this line is getting rid of the JSON.stringify({ ...(newData as Object), storage: { ...newData.storage, version } }) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is not a big deal because the Also, can we get rid of the as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nicosantangelo @cazala I guess we are not picking the |
||
) | ||
version++ | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { Migrations } from '../../lib/types' | ||
|
||
export interface StorageMiddleware<T> { | ||
storageKey: string | ||
paths: string[] | string[][] | ||
actions: string[] | ||
migrations: Migrations<T> | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
Storage
module is irrelevant in this enumeration (this is already theStorage
module)