Skip to content

Instantly share code, notes, and snippets.

@slorber
Last active August 29, 2015 14:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save slorber/096953f247ba2c744057 to your computer and use it in GitHub Desktop.
Save slorber/096953f247ba2c744057 to your computer and use it in GitHub Desktop.
'use strict';
var React = require("react/addons");
var Q = require("q");
var Main = require("components/layout/main");
var CurrentUserService = require("services/currentUserService");
var ApiRequestSessionHolder = require("repositories/utils/apiRequestSessionHolder");
var AppState = require("model/appState");
var Atom = require("state/atom/atom");
var SelectedSpaceModel = require("model/selectedSpaceModel");
var ViewedSpaceModel = require("model/viewedSpaceModel");
var TimelineStoreManager = require("state/stores/timelineStoreManager")
var TopLevelScreenStoreManager = require("state/stores/topLevelScreenStoreManager")
var CategoryNavigationStoreManager = require("state/stores/categoryNavigationStoreManager")
var GlobalEventBus = require("common/globalEventBus");
var AppEvents = require("appEvents");
var AppRouter = require("appRouter");
var app = {
initialize: function() {
Q.longStackSupport = true;
// TODO in next versions of React it was said to be on by default, so probably deprecated
React.initializeTouchEvents(true);
document.addEventListener('deviceready', this.onDeviceReady.bind(this) , false);
},
onDeviceReady: function() {
var self = this;
var initialState = new AppState();
initialState.routing = {};
initialState.stores = {};
this.appStateAtom = new Atom({
initialState: initialState,
onChange: this.onAtomChange.bind(this),
reactToChange: this.reactToAtomChange.bind(this)
});
this.appRouter = new AppRouter(this.appStateAtom);
// TODO the stores should probably register to the atom themselves...
this.timelineStoreManager = new TimelineStoreManager(this.appStateAtom,["stores","timelineStore"]);
this.topLevelScreenStoreManager = new TopLevelScreenStoreManager(this.appStateAtom,["stores","topLevelScreenStore"]);
this.categoryNavigationStoreManager = new CategoryNavigationStoreManager(this.appStateAtom,["stores","categoryNavigationStore"]);
this.appStateAtom.cursor().follow("authenticatedUser").setAsyncValue( CurrentUserService.autoLogin() );
GlobalEventBus.subscribe(function(event) {
self.handleEvent(event)
});
},
// TODO bad should be part of router code
handleEvent: function(event) {
console.debug("Event:",event);
switch (event.name) {
case AppEvents.Names.OPEN_TOP_LEVEL_SCREEN:
this.appStateAtom.cursor().follow("topLevelScreen").set(event.data.screen);
break;
case AppEvents.Names.CLOSE_TOP_LEVEL_SCREEN:
this.appStateAtom.cursor().follow("topLevelScreen").unset();
break;
}
// TODO bad but temporary
this.timelineStoreManager.handleEvent(event);
this.topLevelScreenStoreManager.handleEvent(event);
this.categoryNavigationStoreManager.handleEvent(event);
},
onAtomChange: function() {
this.render();
},
// TODO this probably can be done in a better and easier way
reactToAtomChange: function(previousState) {
if ( this.userHasLoggedIn(previousState) ) {
this.appStateAtom.setPathValue(["routing","selectedPane"],"Stamples");
this.appStateAtom.setPathValue(["routing","selectedSpace"],SelectedSpaceModel.forAll());
this.appStateAtom.setPathValue(["routing","viewedSpace"],ViewedSpaceModel.forRoot());
console.debug("User has just logged in, atom updated",this.appStateAtom.get());
}
// TODO store managers should subscribe on events and route change
this.timelineStoreManager.reactToChange(previousState);
this.topLevelScreenStoreManager.reactToChange(previousState);
this.categoryNavigationStoreManager.reactToChange(previousState);
},
userHasLoggedIn: function(previousState) {
var state = this.appStateAtom.get();
var authenticatedUserIsModified = (state.authenticatedUser && previousState.authenticatedUser && state.authenticatedUser !== previousState.authenticatedUser);
if ( authenticatedUserIsModified ) {
var wasLoading = previousState.authenticatedUser.isLoading();
var isNowLoaded = state.authenticatedUser.isSuccess();
return wasLoading && isNowLoaded;
}
return false;
},
render: function() {
var self = this;
if ( this.appStateAtom.get().currentUser ) {
// This is required to automatically perform authenticated requests
// without having to configure the request headers every time...
ApiRequestSessionHolder.setApiRequestSession(this.appStateAtom.get().currentUser.sessionToken);
}
var props = {
appStateCursor: this.appStateAtom.cursor(),
appRouter: this.appRouter
};
if ( !this.appComponent ) {
var mountNode = document.getElementById('reactAppContainer');
var mountComponent = Main(props);
console.debug("Rendering main component",this.appStateAtom.get());
this.appComponent = React.renderComponent(mountComponent,mountNode);
}
else {
console.debug("Re-rendering main component",this.appStateAtom.get());
this.appComponent.setProps(props);
}
}
};
app.initialize();
'use strict';
var React = require("react/addons");
var DeepFreeze = require("common/deepFreeze");
var AtomUtils = require("state/atom/atomUtils");
var AtomCursor = require("state/atom/atomCursor");
function noop() { } // Convenient but probably not performant: TODO ?
/**
* Creates an Atom
* It contains an immutable state that is never modified directly, but can be swapped to a new immutable state
* @param options
* @constructor
*/
var Atom = function Atom(options) {
this.state = options.initialState || {};
this.onChange = options.onChange || noop;
this.reactToChange = options.reactToChange || noop;
DeepFreeze(this.state);
};
/**
* Change the state reference hold in this Atom
* @param newState
*/
Atom.prototype.set = function(newState) {
if ( newState === this.state ) {
console.debug("Atom state did not change")
return; // Nothing to do here, the atom doesn't change its state
}
var previousState = this.state;
this.state = newState;
DeepFreeze(this.state);
if ( !this.currentlyReactingToChanges ) {
this.currentlyReactingToChanges = true;
try {
this.reactToChange(previousState);
} catch ( error ) {
throw error; // TODO do something more clever?
} finally {
this.currentlyReactingToChanges = false;
}
}
scheduleOnChange(this)
};
/**
* Get the current state of the Atom
* @return the Atom state
*/
Atom.prototype.get = function() {
return this.state;
};
/**
* Get a cursor,, that permits to focus on a given path of the Atom
* @param path (defaults to atom root cursor)
* @return {AtomCursor}
*/
Atom.prototype.cursor = function(path) {
path = path || [];
return new AtomCursor(this,path);
};
/**
* Change the value at a given path of the atom
* @param path
* @param value
*/
Atom.prototype.setPathValue = function(path,value) {
var newState = AtomUtils.updatePathValue(this.state,path,value);
this.set(newState);
};
Atom.prototype.unsetPathValue = function(path) {
// TODO find a better thing than "undefined" to set
var newState = AtomUtils.updatePathValue(this.state,path,undefined);
this.set(newState);
};
/**
* Get the value at a given path of the atom
* @param path
* @return value
*/
Atom.prototype.getPathValue = function(path) {
return AtomUtils.getPathValue(this.state,path);
};
/**
* Compare and swap a valua at a given path of the atom
* @param path
* @param expectedValue
* @param newValue
* @return true if the CAS operation was successful
*/
Atom.prototype.compareAndSwapPathValue = function(path,expectedValue,newValue) {
var actualValue = this.getPathValue(path);
if ( actualValue === expectedValue ) {
this.setPathValue(path,newValue);
return true;
}
return false;
};
// We fire the onChange callback only once
// even if has been modified multiple time during the same execution context
function scheduleOnChange(atom) {
if ( !atom.onChangeTimer ) {
atom.onChangeTimer = setTimeout(
(function() {
delete atom.onChangeTimer;
atom.onChange();
}).bind(this)
,0
);
}
};
module.exports = Atom;
'use strict';
var _ = require("lodash");
var AtomAsyncValueStates = {
LOADIND: {value:"LOADIND"},
SUCCESS: {value:"SUCCESS"},
ERROR: {value:"ERROR"}
}
exports.AtomAsyncValueStates = AtomAsyncValueStates;
var AtomAsyncValue = function(state) {
this.state = state || AtomAsyncValueStates.LOADIND;
};
AtomAsyncValue.prototype.isLoading = function() {
return this.state === AtomAsyncValueStates.LOADIND;
};
AtomAsyncValue.prototype.isSuccess = function() {
return this.state === AtomAsyncValueStates.SUCCESS;
};
AtomAsyncValue.prototype.isError = function() {
return this.state === AtomAsyncValueStates.ERROR;
};
AtomAsyncValue.prototype.toSuccess = function(value) {
var async = new AtomAsyncValue(AtomAsyncValueStates.SUCCESS);
async.value = value;
return async;
};
AtomAsyncValue.prototype.asSuccess = function() {
if ( !this.isSuccess() ) throw new Error("Can't convert async value as success becauuse its state is " + this.state);
return this.value;
};
AtomAsyncValue.prototype.toError = function(error) {
var async = new AtomAsyncValue(AtomAsyncValueStates.ERROR);
async.error = error;
return async;
};
AtomAsyncValue.prototype.asError = function() {
if ( !this.isError() ) throw new Error("Can't convert async value as error becauuse its state is " + this.state);
return this.error;
};
exports.AtomAsyncValue = AtomAsyncValue;
function setupAsyncValueSwapping(atom,path,asyncValue,promise) {
promise
.then(function asyncCompletionSuccess(data) {
var swapped = atom.compareAndSwapPathValue(path,asyncValue,asyncValue.toSuccess(data));
console.debug("Async value completion",path,"Swap success=",swapped);
})
.fail(function asyncCompletionError(error) {
var swapped = atom.compareAndSwapPathValue(path,asyncValue,asyncValue.toError(error));
console.error("Async value completion error",path,"Swap success=",swapped);
console.error(error.stack);
})
.done();
};
function setPathAsyncValue(atom,path,promise) {
var asyncValue = new AtomAsyncValue();
atom.setPathValue(path,asyncValue);
setupAsyncValueSwapping(atom,path,asyncValue,promise);
};
exports.setPathAsyncValue = setPathAsyncValue;
function setPathResolvedAsyncValue(atom,path,resolvedValue) {
var asyncValue = new AtomAsyncValue().toSuccess(resolvedValue);
atom.setPathValue(path,asyncValue);
};
exports.setPathResolvedAsyncValue = setPathResolvedAsyncValue;
function pushPathAsyncValue(atom,listPath,promise) {
var list = atom.getPathValue(listPath) || [];
if ( list instanceof Array ) {
var asyncValueIndex = list.length;
var asyncValuePath = listPath.concat([asyncValueIndex]);
var asyncValue = new AtomAsyncValue();
list.push(asyncValue);
atom.setPathValue(listPath,list);
setupAsyncValueSwapping(atom,asyncValuePath,asyncValue,promise);
} else {
throw new Error("Can't push async value in list because list is " + JSON.stringify(list));
}
};
exports.pushPathAsyncValue = pushPathAsyncValue;
function getPathAsyncValueListCursors(atom,listPath) {
var asyncValueList = atom.getPathValue(listPath);
if ( asyncValueList instanceof Array ) {
var cursorsArray = asyncValueList.map(function(asyncValue,asyncValueIndex) {
if ( asyncValue.isSuccess() ) {
if ( asyncValue.value instanceof Array ) {
return asyncValue.value.map(function(asyncValueItem,asyncValueItemIndex) {
var cursorPath = listPath.concat([asyncValueIndex,"value",asyncValueItemIndex]);
return atom.cursor(cursorPath);
})
} else {
return [asyncValue.value]
}
} else {
return [];
}
});
return _.flatten(cursorsArray); // TODO maybe remove dependency to underscore
} else {
throw new Error("can only be called on an array, not a " + asyncValueList);
}
}
exports.getPathAsyncValueListCursors = getPathAsyncValueListCursors;
'use strict';
var _ = require("lodash");
var AtomAsyncUtils = require("state/atom/atomAsyncUtils");
var AtomCursor = function AtomCursor(atom,atomPath) {
this.atom = atom;
this.atomPath = atomPath;
this.atomValue = atom.getPathValue(atomPath); // TODO this probably can be optimized in some cases when navigating from a previous cursor
};
function ensureIsArray(maybeArray,message) {
if ( !(maybeArray instanceof Array) ) {
throw new Error("Not an array: " + maybeArray + " -> " + message);
}
}
AtomCursor.prototype.get = function() {
return this.atomValue;
};
AtomCursor.prototype.set = function(value) {
this.atom.setPathValue(this.atomPath,value);
};
AtomCursor.prototype.unset = function() {
this.atom.unsetPathValue(this.atomPath);
};
AtomCursor.prototype.push = function(value) {
var list = this.atomValue || [];
ensureIsArray(list,"can only call push on an array");
var newList = list.concat([value]);
this.atom.setPathValue(this.atomPath,newList);
};
AtomCursor.prototype.without = function(value) {
var list = this.atomValue;
ensureIsArray(list,"can only call without on an array");
var newList = _.without(list,value);
this.atom.setPathValue(this.atomPath,newList);
};
AtomCursor.prototype.update = function(updateFunction) {
if ( !this.atomValue ) throw new Error("you can't update an unexisting value")
var valueToSet = updateFunction(this.atomValue);
this.atom.setPathValue(this.atomPath,valueToSet);
};
AtomCursor.prototype.plus = function(number) {
this.update(function(value) { return value+number });
};
AtomCursor.prototype.minus = function(number) {
this.update(function(value) { return value-number });
};
AtomCursor.prototype.followPath = function(path) {
var newPath = this.atomPath.concat(path);
return new AtomCursor(this.atom,newPath);
};
AtomCursor.prototype.follow = function(pathElement) {
return this.followPath( [pathElement] );
};
AtomCursor.prototype.list = function() {
var list = this.atomValue;
ensureIsArray(list,"can only call list on an array");
return list.map(function(item,index) {
return this.follow(index);
}.bind(this));
};
AtomCursor.prototype.asyncSuccess = function() {
if ( this.atomValue && this.atomValue.isSuccess() ) {
return this.follow("value");
} else {
throw new Error("You can't follow an async value that is not successfully loaded: "+this.atomValue);
}
};
AtomCursor.prototype.asyncList = function() {
return AtomAsyncUtils.getPathAsyncValueListCursors(this.atom,this.atomPath);
};
AtomCursor.prototype.setAsyncValue = function(promise) {
AtomAsyncUtils.setPathAsyncValue(this.atom,this.atomPath,promise);
};
AtomCursor.prototype.pushAsyncValue = function(promise) {
AtomAsyncUtils.pushPathAsyncValue(this.atom,this.atomPath,promise);
};
module.exports = AtomCursor;
'use strict';
var React = require("react/addons");
function pathToReactUpdatePath(path,objectAtPath) {
if ( path.length == 0 ) {
return objectAtPath;
} else {
var head = path[0];
var tail = path.slice(1);
var result = {};
result[head] = pathToReactUpdatePath(tail,objectAtPath);
return result;
}
};
function getPathValue(object,path) {
if ( path.length == 0 ) {
return object;
} else {
var head = path[0];
var tail = path.slice(1);
var headValue = object[head];
if ( headValue ) {
return getPathValue(headValue,tail);
} else {
return undefined;
}
}
};
exports.getPathValue = getPathValue;
function updatePathValue(object,path,value) {
try {
var objectPath = pathToReactUpdatePath(path,{$set: value});
return React.addons.update(object, objectPath);
} catch (error) {
// TODO we should probably create the missing path instead of raison the exception
throw new Error(
"Can't set value " + JSON.stringify(value) +
" at path " + JSON.stringify(path) +
" because of error: " + error.message
);
}
};
exports.updatePathValue = updatePathValue;
function modifiedKeys(objectBefore,objectAfter) {
if ( objectBefore === objectAfter ) {
return [];
}
else {
// Merge and deduplicate keys
var keysToCompare = _.union(
_.keys(objectBefore),
_.keys(objectAfter)
);
// Return only modified keys by reference equality check between the 2 objects
return keysToCompare.filter(function(key) {
return objectBefore[key] === objectAfter[key];
})
}
};
exports.modifiedKeys = modifiedKeys;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment