/********************************************************************** * * * **********************************************************************/ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var actions = require('lib/actions') var features = require('lib/features') var core = require('features/core') /*********************************************************************/ // XXX TODO: // - key syntax (path) (DONE) // : // - path variables // $VAR or ${VAR} // - ability to store/load only a specific key from a specific store (DONE) // Q: path patterns?? // localstorage:* - save/load everything on localstorage // *:config - save load config from all stores... // XXX should we unify this with the save/load API var StoreActions = actions.Actions({ config: { // Storage mode... // // This can be: // 'read-only' // 'read-write' // null - ignore store // // NOTE: this only affects start/stop/timer event handling, manual // call to .loadData(..) / .saveData(..) are not affected... 'store-mode': 'read-write', }, // Store handler dict... // // Format: // { // : , // ... // } // // XXX this is almost the same as .collection_handlers... // XXX might be a good idea to shift this from meaning "store handlers" // to "store APIs", i.e. mark all specific store actions with // store tag and collect/group them for reference... // new format: // { // : [ // , // ... // ], // ... // } // ...might also be a good idea to split this to: // .stores - list of store tags (i.e. Object.keys(this.stores_actions)) // .stores_actions - dict of store-specific actions get stores(){ return this.cache('stores', function(d){ var res = {} this.actions.forEach(function(action){ var store = this.getActionAttr(action, 'handle_data_store') res[store] && console.warn('Multiple handlers for store:', store) if(store){ res[store] = action } }.bind(this)) return res }) }, // events... // XXX update signature -- see doc for: .loadStore(..) storeDataLoaded: ['- Store/', core.doc`Store data loaded event... This is tirggered as soon per store as soon as data is loaded, this is sync for sync stores. NOTE: only one store data set is included per call.`, core.Event(function(store, data){ // Store data loaded event... // // Not intended for direct use, use .declareReady() to initiate. return data })], // base API... // XXX .stores: see .stores notes above for new semantics... parseStoreQuery: ['- Store/', core.doc` Get default query... .parseStoreQuery() -> defaults Get unexpanded default query... .parseStoreQuery(true) -> defaults Parse query... .parseStoreQuery(query) -> query Parse query without expanding... .parseStoreQuery(query, true) -> query Query syntax: :: : Format: { query: , date: , event: 'manual' | , store: '*' | | [, ...] key: '*' | | [, ...] } `, function(query, no_expand){ var defaults = { date: Date.timeStamp(), event: 'manual', store: '*', key: '*', } if(query === true || query === false){ no_expand = query query = null } // parse string... if(typeof(query) == typeof('str')){ var res = {} res.query = query query = query.split(/:/g) res.event = query.length > 2 ? query.shift() : defaults.event res.store = (this.stores[query[0]] || query.length > 1) ? query.shift().split(/\|/g) : defaults.store //: Object.keys(this.stores) res.key = query.length > 0 ? query.pop().split(/\|/g) : defaults.key res.date = defaults.date query = res // get the defaults... } else if(query == null){ query = defaults // pass on the input... } else { } if(!no_expand){ // XXX var handlers = this.stores // expand store '*'... query.store = (query.store.length == 1 && query.store[0] == '*') ? Object.keys(handlers) : query.store // expand key '*'... query.key = query.key.length == 1 && query.key[0] == '*' ? '*' : query.key } return query }], // HL API... // // XXX sync or async or both??? // ...should we add an .onStore(query, handler) event/promise??? // one way to do this is to place promises into res where async... store: ['- Store/', core.doc` Get stored key(s)... .store(query) -> value{s} Write value to key(s)... .store(query, value) Remove key(s)... .store(query, null) Returns: { : { : , ... }, ... } NOTE: for query syntax see .parseStoreQuery(..) Store API: - High-level API... .store(..) - Application-level API... .prepareStoreToLoad(..) .prepareStoreToSave(..) - Storage-level general API... .loadStore(..) .saveStore(..) .clearStore(..) NOTE: .prepareStoreToSave(..) is triggered only when no data is passed to .saveStore(..)... NOTE: .prepareStoreToLoad(..) is called every time .loadStore(..) is called... `, function(query, value){ query = this.parseStoreQuery(query) return ( // get... arguments.length == 1 ? this.loadStore(query) // delete... : (value === undefined && arguments.length == 2) ? this.clearStore(query) // set... : (this.saveStore(query, value) && undefined) ) }], // Application API... prepareStoreToSave: ['- Store/', core.doc` Format: { // metadata... mode: , data: , // the actual data... data: { : { : , ... }, ... }, } `, function(query, data){ var defaults = this.parseStoreQuery() query = this.parseStoreQuery(query) var stores = query.store || defaults.store data = data || {} query.store.forEach(function(s){ data[s] = {} }) return { date: query.date || Date.timeStamp(), event: query.event || defaults.event, key: query.key || defaults.key, data: data || {}, } }], prepareStoreToLoad: ['- Store/', core.doc` NOTE: this can be called multiple times, once per each store. NOTE: only one store data set is included per call.`, function(query, data){ return data || {} }], // Store API... saveStore: ['- Store/', core.doc` Build and store all/query data... .saveStore() .saveStore(query) -> data Store query data... .saveStore(query, data) NOTE: this only calls .prepareStoreToSave(..) if no data is given... NOTE: the returned data is the same format as returned by .store(..) `, core.notUserCallable(function(query, data){ // Extending action must: // - save query/data to it's store... return data == null ? this.prepareStoreToSave(this.parseStoreQuery(query), {}).data : undefined })], loadStore: ['- Store/', core.doc` .loadStore(query) -> data NOTE: async stores will write promises to .data `, core.notUserCallable(function(query){ // Extending action must: // - populate query from it's store... // - trigger .storeDataLoaded(store) event when done... // // NOTE: if query.store is a single value then the root object // must be populated, otherwise the appropriate section // should be used... // e.g. // if(query.store.length == 1){ // res = data // } else { // res['store-key'] = data // } var res = {} var q = this.parseStoreQuery(query) query && q.store.length >= 1 && q.store.forEach(function(s){ res[s] = {} }) return this.prepareStoreToLoad(q, res) })], clearStore: ['- Store/', core.doc` .clearStore(query) `, core.notUserCallable(function(query){ // Extending action: // - must clear query from it's store... })], // XXX LEGACY STUFF... (???) // ...this is currently used to store config -- migrate to new API... // XXX this is different from .prepareIndexForWrite(..) in that there // is no default data set... // XXX async??? saveData: ['- Store/', // XXX signature not final... function(query, data){ var defaults = this.parseStoreQuery() query = this.parseStoreQuery(query) var handlers = this.stores // save the given data... // NOTE: we are not calling .prepareStoreToSave(..) here, thus // we need not care about .key, .date, and other stuff... if(data){ if(query.store == defaults.store || query.key == defaults.key){ console.error('saveData: at least "store" and "key" ' +'must be explicitly set in query...') return } var d = { data: {} } var stores = query.store == '*' ? handlers : query.store stores = stores instanceof Array ? stores : [stores] stores.forEach(function(s){ d.data[s] = { [query.key]: data, } }) data = d // build the data... } else { data = this.prepareStoreToSave(query) } // iterate and handle stores... Object.keys(data.data).forEach(function(store){ var handler = handlers[store] handler && this[handler].call(this, data.data[store]) }.bind(this)) }], // XXX add query support... (???) // ...we can't support keys other than '*' until we register // store keys... loadData: ['- Store/', function(query){ var handlers = this.stores var defaults = this.parseStoreQuery() query = this.parseStoreQuery(query) // XXX need to filter loading by query.key... var data = {} return Promise .all(query.store .map(function(s){ var res = this[handlers[s]]() return res instanceof Promise ? // async store... res.then(function(d){ d && (data[s] = d) && this.storeDataLoaded(s, this.prepareStoreToLoad(query, {[s]: d})) }.bind(this)) // sync store... : (res && (data[s] = res) && this.storeDataLoaded(s, this.prepareStoreToLoad(query, {[s]: res}))) }.bind(this))) .then(function(){ return data })}], // XXX do we need to do a partial clear??? clearData: ['- Store/', function(target){ var handlers = this.stores Object.keys(handlers).forEach(function(store){ var handler = handlers[store] handler && this[handler].call(this, null) }.bind(this)) }], }) var Store = module.Store = core.ImageGridFeatures.Feature({ title: '', doc: '', tag: 'store', depends: [ 'cache', ], suggested: [ 'store-localstorage', 'store-fs', ], isApplicable: function(){ return typeof(localStorage) != 'undefined' }, actions: StoreActions, handlers: [ ['start.pre', function(){ if(this.config['store-mode'] != null){ this.requestReadyAnnounce() this .loadData() .then(function(){ this.declareReady() }.bind(this)) } }], ['stop', function(){ this.config['store-mode'] == 'read-write' && this.saveData() }], // XXX timer??? // XXX ], }) //--------------------------------------------------------------------- // NOTE: the doc is reused for both localStorage and sessionStorage with // appropriate automated changes... var __storageHandler_doc = core.doc`Handle localStorage store data... Write data to key... .localStorageDataHandler(key, data) Get data from key... .localStorageDataHandler(key) -> value Remove key... .localStorageDataHandler(key, null) .localStorageDataHandler([key, ..], null) Get stored keys... .localStorageDataHandler('?') -> keys .localStorageDataHandler() .localStorageDataHandler('*') .localStorageDataHandler('??') -> data .localStorageDataHandler([key, ..], '??') -> data Expand data to store... .localStorageDataHandler(data) Remove all data... .localStorageDataHandler(null) NOTE: when handling key expansion the unexpanded key should be used to access data, e.g. if writing '\${INSTANCE}/xyz' the same string is returned via .localStorageDataHandler('??') and should be used to get the value, i.e. .localStorageDataHandler('\${INSTANCE}/xyz') ` function makeStorageHandler(storage){ var func = function(a, b){ // normalize a... a = a == '??' ? '*' : a storage = typeof(storage) == typeof('str') ? window[storage] : storage var instance = this.config['store-instance-key'] var resolvePath = function(p){ return p .replace('${INSTANCE}', instance) } var keys = Object.keys(storage) var dict_key = '${INSTANCE}/__dict__' var k = resolvePath(dict_key) var dict = JSON.parse(storage[k] || '{}') dict[k] = dict_key // get list of keys... if(a == '?'){ return keys .map(function(key){ return dict[key] || key }) // get store contents... } else if(a == '*' || arguments.length == 0 || (a instanceof Array && (b == '??' || arguments.length == 1))){ var res = {} keys = a instanceof Array ? a : keys keys // add parsed candidates... .map(function(k){ return resolvePath(k) }) // clear keys not in store... .filter(function(k){ return dict[k] in storage || k in storage }) .forEach(function(k){ res[dict[k] || k] = JSON.parse(storage[k]) }) return res // remove all keys... } else if(a === null || (a instanceof Array && b === null)){ ;(a || keys) .forEach(function(key){ delete storage[key] }) dict = {} // expand data to store... } else if(typeof(a) != typeof('str')){ Object.keys(a).forEach(function(key){ var k = resolvePath(key) k != key && (dict[k] = key) storage[k] = JSON.stringify(a[key]) }) // remove key... // XXX revise, should this be null or undefined??? } else if(b === null){ var k = resolvePath(a) delete dict[k] delete storage[k] // get keys... // XXX revise, should this be null or undefined??? } else if(b === undefined){ var k = resolvePath(a) return k in storage ? JSON.parse(storage[k]) : actions.UNDEFINED // write key... } else { var k = resolvePath(a) k != a && (dict[k] = a) storage[k] = JSON.stringify(b) } storage[resolvePath('${INSTANCE}/__dict__')] = JSON.stringify(dict) } if(typeof(storage) == typeof('str')){ func.long_doc = __storageHandler_doc.replace(/localStorage/g, storage) } return func } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // XXX we should have a separate store config with settings of how to // load the store... (???) var StoreLocalStorageActions = actions.Actions({ config: { 'store-instance-key': 'ImageGrid.Viewer.main', }, // NOTE: for docs see __storageHandler_doc... localStorageDataHandler: ['- Store/', {handle_data_store: 'storage',}, makeStorageHandler('localStorage')], sessionStorageDataHandler: ['- Store/', {handle_data_store: 'session',}, makeStorageHandler('sessionStorage')], }) var StoreLocalStorage = module.StoreLocalStorage = core.ImageGridFeatures.Feature({ title: '', doc: '', tag: 'store-localstorage', depends: [ 'store', ], isApplicable: function(){ return typeof(localStorage) != 'undefined' && typeof(sessionStorage) != 'undefined' }, actions: StoreLocalStorageActions, handlers: [ // localStorage... // XXX should we use .localStorageDataHandler(..) here??? // XXX the following two sections are almost identical... ['saveStore', function(res, query, value){ var q = this.parseStoreQuery(query) query && q.key instanceof Array && q.store.indexOf('storage') >= 0 && q.key.forEach(function(k){ this.localStorageDataHandler( k, res !== this ? res.data : value) }.bind(this)) }], ['loadStore', function(res, query){ query = this.parseStoreQuery(query) res.storage && Object.assign(res.storage, this.localStorageDataHandler(query.key)) }], ['clearStore', function(_, query){ var q = this.parseStoreQuery(query) query && q.store.indexOf('storage') >= 0 && this.localStorageDataHandler(q.key, null) }], // sessionStorage... ['saveStore', function(res, query, value){ var q = this.parseStoreQuery(query) query && q.key instanceof Array && q.store.indexOf('session') >= 0 && q.key.forEach(function(k){ this.sessionStorageDataHandler( k, res !== this ? res.data : value) }.bind(this)) }], ['loadStore', function(res, query){ query = this.parseStoreQuery(query) res.session && Object.assign(res.session, this.sessionStorageDataHandler(query.key)) }], ['clearStore', function(_, query){ var q = this.parseStoreQuery(query) query && q.store.indexOf('session') >= 0 && this.sessionStorageDataHandler(q.key, null) }], ], }) //--------------------------------------------------------------------- // XXX StoreFSJSONSync // Lookup order: // - app dir // - $HOME var StoreFSJSONActions = actions.Actions({ config: { }, // XXX localStorageFSJSONSyncHandler: ['- Store/', {handle_data_store: 'fileSync',}, function(a, b){ // XXX }], // XXX localStorageFSJSONHandler: ['- Store/', {handle_data_store: 'file',}, function(a, b){ // XXX return new Promise(function(resolve, reject){ // XXX resolve() }) }], }) var StoreFSJSON = module.StoreFSJSONSync = core.ImageGridFeatures.Feature({ title: '', doc: '', tag: 'store-fs', depends: [ 'fs', 'store', ], actions: StoreFSJSONActions, }) /********************************************************************** * vim:set ts=4 sw=4 : */ return module })