Last active
November 12, 2016 19:54
-
-
Save HyphnKnight/ec8cdc50996c0e68fcb41ea2ce382cf0 to your computer and use it in GitHub Desktop.
A very small state management tool for javascript.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* Store.js */ | |
| function identity( x ) { | |
| return x; | |
| } | |
| function curry2( func, x, y ) { | |
| return function curryInside( z ) { | |
| return func( x, y, z ); | |
| } | |
| } | |
| function uniqueId() { | |
| return Math.floor( Math.random() * 100000 ) + '-' + Date.now(); | |
| } | |
| function isDate( date ) { | |
| return Object.prototype.toString.call( date ) === '[object Date]'; | |
| } | |
| function isComparable( value ) { | |
| return typeof value === 'number' || | |
| typeof value === 'boolean' || | |
| typeof value === 'string' || | |
| typeof value === 'symbol' || | |
| typeof value === 'function'; | |
| } | |
| function isEqual( valueA, valueB ) { | |
| if ( typeof valueA !== typeof valueB ) { | |
| return false; | |
| } else if ( typeof valueA === 'undefined' ) { | |
| return true; | |
| } else if ( valueA === null || valueB === null || isComparable( valueA ) ) { | |
| return valueA === valueB; | |
| } else if ( isDate( valueA ) && isDate( valueB ) ) { | |
| return valueA.getTime() === valueB.getTime(); | |
| } else if ( Array.isArray( valueA ) && Array.isArray( valueB ) ) { | |
| return valueA.length === valueB.length && !valueA.find( ( val, index ) => !isEqual( valueB[ index ], val ) ); | |
| } else if ( valueA instanceof Map && valueB instanceof Map ) { | |
| return valueA.size === valueB.size && isEqual( Array.from( valueA.entries() ), Array.from( valueB.entries() ) ); | |
| } else if ( valueA instanceof Set && valueB instanceof Set ) { | |
| return valueA.size === valueB.size && isEqual( Array.from( valueA ), Array.from( valueB ) ); | |
| } else if ( !!valueA.__store__ && !!valueB.__store__ ) { | |
| return isEqual( valueA.__store__, valueB.__store__ ); | |
| } else if ( Object.keys( valueA ).length === Object.keys( valueA ).length ) { | |
| return !Object.keys( valueA ).find( val => !isEqual( valueA[ val ], valueB[ val ] ) ); | |
| } else { | |
| return false; | |
| } | |
| } | |
| function setStore( store, subscriptions, obj ) { | |
| const objKeys = Object.keys( obj ); | |
| let oldStore; | |
| if ( objKeys.map( key => { | |
| if ( !isEqual( obj[key], store.__store__[ key ] ) ) { | |
| if ( !oldStore ) { | |
| oldStore = Object.assign( {}, store.__store__ ); | |
| } | |
| store.__store__[ key ] = obj[key]; | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| } ).some( identity ) ) { | |
| Object.keys( subscriptions ).forEach( key => subscriptions[ key ]( store, oldStore ) ); | |
| } | |
| } | |
| function Store( protoObj ) { | |
| protoObj = typeof protoObj === 'string' ? JSON.parse( protoObj ) : protoObj; | |
| if ( Object.keys( protoObj ).some( key => key === 'subscribe' || key === '__store__' || key === 'set' ) ) { | |
| throw `Store can not be created from an object with reserved keys: subscribe, __store__ and set.`; | |
| } | |
| const | |
| subscriptions = {}, | |
| newStore = { | |
| subscribe: func => { | |
| const key = uniqueId(); | |
| subscriptions[ key ] = func; | |
| return () => { | |
| delete subscriptions[ key ]; | |
| }; | |
| }, | |
| __store__: {} | |
| }; | |
| Object.keys( protoObj ).forEach( key => { | |
| newStore.__store__[ key ] = protoObj[ key ]; | |
| Object.defineProperty( newStore, key, { | |
| set: value => { | |
| if ( !isEqual( value, newStore.__store__[ key ] ) ) { | |
| const oldStore = Object.assign( {}, newStore.__store__ ); | |
| newStore.__store__[ key ] = value; | |
| Object.keys( subscriptions ).forEach( key => subscriptions[ key ]( newStore, oldStore ) ); | |
| } | |
| }, | |
| get: () => newStore.__store__[ key ] | |
| } ); | |
| } ); | |
| Object.defineProperty( newStore, 'JSON', { | |
| set: string => newStore.set( JSON.parse( string ) ), | |
| get: () => JSON.stringify( newStore.__store__ ) | |
| } ); | |
| newStore[ Symbol.iterator ] = () => ( function iteratorMaker( protoObj ) { | |
| 'use strict'; | |
| const objKeys = Object.keys( protoObj ); | |
| let index = 0; | |
| return { | |
| next: function next() { | |
| const value = protoObj[ objKeys[ index ] ]; | |
| let result; | |
| if ( index > objKeys.length - 1 ) { | |
| result = { done: true }; | |
| } else { | |
| result = { value, done: false }; | |
| ++index; | |
| } | |
| return result; | |
| } | |
| } | |
| } )( protoObj ); | |
| newStore.set = curry2( setStore, newStore, subscriptions ); | |
| return Object.preventExtensions( newStore ); | |
| } | |
| export default Store; | |
| /* Example: | |
| const | |
| // Create a Store. | |
| myStore = Store( { myName: 'Rafe', yourName: 'Tara' } ), | |
| // Create a subscription. | |
| cancelSubscribe = myStore.subscribe( state => console.log( state ) ); | |
| // This triggers your subscribe callbacks and sets myName to Dave; | |
| myStore.myName = 'Dave'; | |
| // This will throw an error because you tried to set a property you did not initialize with. | |
| myStore.theName = 'Jeff'; | |
| // The subscribe callback will no longer fire; | |
| cancelSubscribe(); | |
| */ | |
| /* Fun Example: | |
| function createEquipment( weapon, armor ) { | |
| return Store( { weapon, armor } ); | |
| } | |
| function createBarbarian( name, gender ) { | |
| const | |
| barbInfo = Store({ | |
| name, | |
| class: 'Barbarian', | |
| gender: !gender ? 'male' : 'female', | |
| health: 100, | |
| equipment: createEquipment( 'dagger', 'helmet' ), | |
| potions: ['health','mana'] | |
| } ); | |
| let | |
| possessive = !gender ? 'his' : 'her', | |
| pronoun = !gender ? 'he' : 'she'; | |
| barbInfo.equipment.subscribe( state => { | |
| console.log( `${barbInfo.name} has decided to use the ${state.weapon} & ${state.armor} combo!` ); | |
| barbInfo.equipment = state; | |
| } ); | |
| barbInfo.subscribe( ( state, oldState ) => { | |
| if ( state.gender !== oldState.gender ) { | |
| possessive = !state.gender ? 'his' : 'her'; | |
| pronoun = !state.gender ? 'he' : 'she'; | |
| } | |
| if ( barbInfo.health <= 0 ) { | |
| console.log( `${barbInfo.name} is dead, quick take ${possessive} ${barbInfo.equipment.armor}!` ); | |
| } else if ( barbInfo.health > 0 && barbInfo.health < 90 ) { | |
| console.log( `${barbInfo.name} has just a flesh wound, surely ${pronoun} will maim you with ${possessive} ${barbInfo.equipment.weapon}!` ); | |
| } else { | |
| console.log( `${barbInfo.name} is feeling hearty, ${pronoun} is feeling stabby!` ); | |
| } | |
| } ); | |
| return barbInfo; | |
| } | |
| const | |
| bobTheBarb = createBarbarian( 'Bob' ), | |
| birthaTheBarb = createBarbarian( 'Birtha', true ); | |
| function switchEquipment( barb, weapon, armor ) { | |
| barb.equipment.set( { weapon, armor } ); | |
| } | |
| function createBite( animal, damage ) { | |
| return function animalBite( barb ) { | |
| console.log( `A ${animal} has bitten ${barb.name}.` ); | |
| barb.health -= damage; | |
| } | |
| } | |
| const | |
| spiderBite = createBite( 'spider', 10 ), | |
| wolfBite = createBite( 'wolf', 20 ), | |
| bearBite = createBite( 'bear', 40 ), | |
| dragonBite = createBite( 'dragon', 80 ); | |
| setTimeout( () => spiderBite(bobTheBarb), 1000 ); | |
| setTimeout( () => wolfBite(bobTheBarb), 2500 ); | |
| setTimeout( () => bearBite(birthaTheBarb), 4000 ); | |
| setTimeout( () => switchEquipment( bobTheBarb, 'sword','shield'), 5500 ); | |
| setTimeout( () => dragonBite(bobTheBarb), 7000 ); | |
| setTimeout( () => switchEquipment( birthaTheBarb, 'sword','shield'), 9000 ); | |
| setTimeout( () => bearBite(birthaTheBarb), 10500 ); | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment