TempStorage: Generic refactoring of TokenStorage

The Tempstorage module holds an ``initialized`` promise that is resolved
once the data has been retrieved once from the worker. When that's done,
all data is directly accessible through getters/setters.

Data that is set in one tab/window through the modules getters/setters will
be accessible in other tabs/windows as well.

Change-Id: I7011995954fe362f6e45d2999fb998e5029cc769
This commit is contained in:
Florian Fuchs 2015-10-27 10:39:11 +01:00
parent c241346d16
commit 02d168f9db
11 changed files with 180 additions and 126 deletions

View File

@ -7,9 +7,9 @@ var shell = require('gulp-shell');
// var uglify = require('gulp-uglify');
var webpack = require('gulp-webpack');
var webpackConfig = require('./webpack.config.js');
var tokenWorkerConfig = require('./tokenworker.webpack.config.js');
var tempStorageWorkerConfig = require('./tempstorageworker.webpack.config.js');
gulp.task('webpack-app', ['webpack-token-worker'], function() {
gulp.task('webpack-app', ['webpack-tempstorage-worker'], function() {
return gulp.src('./src/js/index.js')
.pipe(webpack(webpackConfig))
.pipe(gulp.dest('dist/js'))
@ -43,9 +43,9 @@ gulp.task('fonts', function() {
.pipe(gulp.dest('./dist/fonts'));
});
gulp.task('webpack-token-worker', function() {
return gulp.src('./src/js/workers/TokenWorker.js')
.pipe(webpack(tokenWorkerConfig))
gulp.task('webpack-tempstorage-worker', function() {
return gulp.src('./src/js/workers/TempStorageWorker.js')
.pipe(webpack(tempStorageWorkerConfig))
.pipe(gulp.dest('./dist/js'))
.pipe(browserSync.stream());
});

View File

@ -2,7 +2,7 @@ const AppDispatcher = require('../../js/dispatchers/AppDispatcher');
const KeystoneApiService = require('../../js/services/KeystoneApiService');
const LoginActions = require('../../js/actions/LoginActions');
const LoginConstants = require('../../js/constants/LoginConstants');
const AuthTokenStorage = require('../../js/services/AuthTokenStorage.js');
const TempStorage = require('../../js/services/TempStorage.js');
let mockKeystoneAccess = {
token: {
@ -21,10 +21,13 @@ describe('LoginActions', () => {
});
it('creates action to login user with keystoneAccess response', () => {
spyOn(AuthTokenStorage, 'storeTokenId');
spyOn(TempStorage, 'setItem');
spyOn(AppDispatcher, 'dispatch').and.callThrough();
LoginActions.loginUser(mockKeystoneAccess);
expect(AuthTokenStorage.storeTokenId).toHaveBeenCalledWith(mockKeystoneAccess.token.id);
expect(TempStorage.setItem).toHaveBeenCalledWith(
'keystoneAuthTokenId',
mockKeystoneAccess.token.id
);
expect(AppDispatcher.dispatch).toHaveBeenCalledWith({
actionType: LoginConstants.LOGIN_USER,
keystoneAccess: mockKeystoneAccess
@ -32,10 +35,10 @@ describe('LoginActions', () => {
});
it('creates action to logout user', () => {
spyOn(AuthTokenStorage, 'removeTokenId');
spyOn(TempStorage, 'removeItem');
spyOn(AppDispatcher, 'dispatch').and.callThrough();
LoginActions.logoutUser();
expect(AuthTokenStorage.removeTokenId).toHaveBeenCalled();
expect(TempStorage.removeItem).toHaveBeenCalled();
expect(AppDispatcher.dispatch).toHaveBeenCalledWith({
actionType: LoginConstants.LOGOUT_USER
});

View File

@ -1,41 +0,0 @@
const AuthTokenStorage = require('../../js/services/AuthTokenStorage.js');
const LoginActions = require('../../js/actions/LoginActions');
describe('AuthTokenStorage.', () => {
describe('```AuthTokenStorage.getTokenId```', () => {
let cbObj = { cb: (token) => {} };
beforeEach(() => {
spyOn(cbObj, 'cb');
});
it('calls the given callback with the token if one has been set.', (done) => {
LoginActions.loginUser({
token: 'token-from-login',
user: null,
serviceCatalog: null,
metadata: null
});
// unset the sessionStorage value to make sure
// the value is retreived from the worker.
spyOn(sessionStorage, 'getItem').and.returnValue(null);
AuthTokenStorage.getTokenId(cbObj.cb);
done();
expect(cbObj.cb).toHaveBeenCalledWith('token-from-login');
});
it('gives precedence to the token in sessionStorage if one is set', () => {
spyOn(sessionStorage, 'getItem').and.returnValue('token-from-session-storage');
AuthTokenStorage.getTokenId(cbObj.cb);
expect(cbObj.cb).toHaveBeenCalledWith('token-from-session-storage');
});
});
describe('```AuthTokenStorage.onLogoutUser```', () => {
it('removes the token from sessionStorage', () => {
spyOn(sessionStorage, 'removeItem');
AuthTokenStorage.removeTokenId();
expect(sessionStorage.removeItem).toHaveBeenCalledWith('keystoneAuthTokenId');
});
});
});

View File

@ -0,0 +1,56 @@
const TempStorage = require('../../js/services/TempStorage.js');
const WORKER_URL = '/js/rdo_director_ui_tempstorage_worker.js';
describe('TempStorage', () => {
describe('.getItem', () => {
beforeEach(() => {
sessionStorage.removeItem('someKey');
});
it('returns ``null`` if no value has been set yet.', () => {
sessionStorage.removeItem('someKey');
expect(TempStorage.getItem('someKey')).toBeNull();
});
it('returns the value from sessionStorage if it is set', () => {
sessionStorage.setItem('someKey', 'someValue');
expect(TempStorage.getItem('someKey')).toEqual('someValue');
});
});
describe('worker updates', () => {
let worker;
if(window && window.SharedWorker) {
worker = new window.SharedWorker(WORKER_URL);
worker.port.start();
}
beforeEach(() => {
sessionStorage.removeItem('someKey');
});
it('update the sessionStorage items as well', () => {
expect(TempStorage.getItem('someKey')).toBeNull();
worker.port.postMessage({someKey: 'updated'});
setTimeout(() => {
expect(TempStorage.getItem('someKey')).toEqual('updated');
expect(sessionStorage.getItem('someKey')).toEqual('updated');
}, 10);
});
});
describe('```.setItem```', () => {
beforeEach(() => {
sessionStorage.removeItem('someKey');
});
it('updates the worker as well as sessionStorage', () => {
expect(TempStorage.getItem('someKey')).toBeNull();
TempStorage.setItem('someKey', 'newVal');
setTimeout(() => {
expect(sessionStorage.getItem('someKey')).toBe('newVal');
}, 50);
});
});
});

View File

@ -1,5 +1,5 @@
import AppDispatcher from '../dispatchers/AppDispatcher.js';
import AuthTokenStorage from '../services/AuthTokenStorage.js';
import TempStorage from '../services/TempStorage.js';
import KeystoneApiService from '../services/KeystoneApiService';
import LoginConstants from '../constants/LoginConstants';
@ -9,7 +9,7 @@ export default {
},
loginUser(keystoneAccess) {
AuthTokenStorage.storeTokenId(keystoneAccess.token.id);
TempStorage.setItem('keystoneAuthTokenId', keystoneAccess.token.id);
AppDispatcher.dispatch({
actionType: LoginConstants.LOGIN_USER,
keystoneAccess: keystoneAccess
@ -17,7 +17,7 @@ export default {
},
logoutUser() {
AuthTokenStorage.removeTokenId();
TempStorage.removeItem('keystoneAuthTokenId');
AppDispatcher.dispatch({
actionType: LoginConstants.LOGOUT_USER
});

View File

@ -4,7 +4,7 @@ import React from 'react';
import * as Router from 'react-router';
import App from './components/App';
import AuthTokenStorage from './services/AuthTokenStorage.js';
import TempStorage from './services/TempStorage.js';
import Login from './components/Login';
import LoginActions from './actions/LoginActions';
import Overview from './components/overview/Overview';
@ -21,8 +21,8 @@ let routes = (
</Route>
);
AuthTokenStorage.getTokenId((keystoneAuthTokenId) => {
TempStorage.initialized.then(() => {
let keystoneAuthTokenId = TempStorage.getItem('keystoneAuthTokenId');
if (keystoneAuthTokenId) {
LoginActions.authenticateUserViaToken(keystoneAuthTokenId);
}

View File

@ -1,51 +0,0 @@
class AuthTokenStorage {
constructor() {
this._createWorkerInstance();
}
_createWorkerInstance() {
if(window && window.SharedWorker) {
this.worker = new window.SharedWorker('/js/rdo_director_ui_token_worker.js');
this.worker.port.start();
}
}
/**
* Get the currently stored token ID.
* @returns {String|Boolean} The token Id or false.
*/
getTokenId(cb) {
let tokenId = sessionStorage.getItem('keystoneAuthTokenId');
// If there was a tokenId stored in sessionStorage,
// just call the cb and leave.
if(tokenId) {
return cb(tokenId);
}
// Create callback for temporary worker message listener.
let fn = (e) => {
cb(e.data);
this.worker.port.removeEventListener('message', fn);
};
this.worker.port.addEventListener('message', fn);
this.worker.port.postMessage(null);
}
storeTokenId(tokenId) {
sessionStorage.setItem('keystoneAuthTokenId', tokenId);
this.worker.port.postMessage(tokenId);
}
removeTokenId() {
sessionStorage.removeItem('keystoneAuthTokenId');
if(this.worker) {
// Post a value of false to erase the token.
this.worker.port.postMessage(false);
}
}
}
export default new AuthTokenStorage();

View File

@ -0,0 +1,81 @@
import when from 'when';
const WORKER_URL = '/js/rdo_director_ui_tempstorage_worker.js';
class TempStorage {
constructor() {
this._createWorkerInstance();
// This promise is resolved when the store has been loaded from
// the worker for the first time.
this._def = when.defer();
this.initialized = this._def.promise;
this._initStore();
}
_createWorkerInstance() {
if(window && window.SharedWorker) {
this.worker = new window.SharedWorker(WORKER_URL);
this.worker.port.start();
}
}
_initStore() {
if(this.worker) {
this.worker.port.onmessage = e => {
if(e.data !== null && typeof e.data === 'object') {
for(let key in e.data) {
let val = e.data[key];
if(val === undefined) {
sessionStorage.removeItem(key);
}
else {
// sessionStorage can only store text, so serialize if necessary.
val = (typeof(val) === 'object') ? JSON.stringify(val) : val;
sessionStorage.setItem(key, val);
}
}
this._def.resolve(e.data);
}
};
this.worker.port.postMessage(true);
}
}
/**
* Get an item from the store.
*/
getItem(key) {
let item = sessionStorage.getItem(key);
// Try to deserialize, if the original value was an object/array.
try {
return JSON.parse(item);
} catch(err) {
return item;
}
}
/**
* Add/modifiy an item in the store
*/
setItem(key, val) {
let storeObj = {};
// sessionStorage can only store text, so serialize if necessary.
val = (typeof(val) === 'object') ? JSON.stringify(val) : val;
storeObj[key] = val;
this.worker.port.postMessage(storeObj);
}
/**
* Remove an item from the store
*/
removeItem(key) {
let storeObj = {};
storeObj[key] = undefined;
this.worker.port.postMessage(storeObj);
}
}
export default new TempStorage();

View File

@ -0,0 +1,22 @@
'use strict';
let store = {};
let ports = [];
self.onconnect = connEvent => {
ports.push(connEvent.ports[0]);
connEvent.ports[0].onmessage = e => {
if(e.data !== null) {
if(typeof(e.data) === 'object') {
for(let key in e.data) {
store[key] = e.data[key];
}
}
}
ports.forEach(port => {
port.postMessage(store);
});
};
};

View File

@ -1,16 +0,0 @@
let message = undefined;
self.addEventListener('connect', e => {
let port = e.ports[0];
port.addEventListener('message', e => {
if(e.data !== null) {
message = e.data;
}
port.postMessage(message);
}, false);
port.start();
}, false);

View File

@ -1,8 +1,8 @@
module.exports = {
devtool: 'source-map',
output: {
filename: 'rdo_director_ui_token_worker.js',
sourceMapFilename: 'rdo_director_ui_token_worker.js.map'
filename: 'rdo_director_ui_tempstorage_worker.js',
sourceMapFilename: 'rdo_director_ui_tempstorage_worker.js.map'
},
module: {
loaders: [