├── .travis.yml ├── .gitignore ├── plugins ├── json2.js ├── dump.js ├── json2_test.js ├── defaults.js ├── all.js ├── update.js ├── observe.js ├── all_tests.js ├── dump_test.js ├── defaults_test.js ├── observe_test.js ├── expire.js ├── update_test.js ├── expire_test.js ├── operations.js ├── v1-backcompat.js ├── v1-backcompat_test.js ├── events_test.js ├── operations_test.js ├── events.js └── lib │ └── json2.js ├── dist ├── store.tests.js ├── store.everything.js ├── store.legacy.js ├── store.v1-backcompat.js ├── store.modern.js ├── store.modern.min.js ├── store.legacy.min.js ├── store.v1-backcompat.min.js ├── store.everything.min.js └── store.tests.min.js ├── scripts ├── create-tunnel.js ├── run-browser-tests-live-reload.js ├── run-node-tests.js ├── saucelabs │ ├── list-supported-browsers.js │ ├── saucelabs-api.js │ ├── tunnel.js │ ├── saucelabs-platformSets.js │ └── saucelabs.js ├── run-saucelabs-tests.js └── compile-builds.js ├── sublime-storejs.sublime-project ├── storages ├── all.js ├── localStorage.js ├── sessionStorage.js ├── memoryStorage.js ├── oldFF-globalStorage.js ├── cookieStorage.js └── oldIE-userDataStorage.js ├── Makefile ├── tests ├── util.js └── tests.js ├── LICENSE ├── package.json ├── src ├── util.js └── store-engine.js ├── Changelog ├── README-More.md └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *.sublime-workspace 4 | -------------------------------------------------------------------------------- /plugins/json2.js: -------------------------------------------------------------------------------- 1 | module.exports = json2Plugin 2 | 3 | function json2Plugin() { 4 | require('./lib/json2') 5 | return {} 6 | } 7 | -------------------------------------------------------------------------------- /dist/store.tests.js: -------------------------------------------------------------------------------- 1 | // store-test-suite will run all the store.js tests 2 | // and report the results. 3 | 4 | var tests = require('../tests/tests') 5 | 6 | tests.runTests() 7 | -------------------------------------------------------------------------------- /scripts/create-tunnel.js: -------------------------------------------------------------------------------- 1 | var port = 9575 2 | var tunnel = require('./saucelabs/tunnel') 3 | 4 | tunnel.setup(port, function(err, url) { 5 | console.log("Tunnel up and running at", url) 6 | }) 7 | -------------------------------------------------------------------------------- /dist/store.everything.js: -------------------------------------------------------------------------------- 1 | var engine = require('../src/store-engine') 2 | 3 | var storages = require('../storages/all') 4 | var plugins = require('../plugins/all') 5 | 6 | module.exports = engine.createStore(storages, plugins) 7 | -------------------------------------------------------------------------------- /dist/store.legacy.js: -------------------------------------------------------------------------------- 1 | var engine = require('../src/store-engine') 2 | 3 | var storages = require('../storages/all') 4 | var plugins = [require('../plugins/json2')] 5 | 6 | module.exports = engine.createStore(storages, plugins) 7 | -------------------------------------------------------------------------------- /dist/store.v1-backcompat.js: -------------------------------------------------------------------------------- 1 | var engine = require('../src/store-engine') 2 | 3 | var storages = require('../storages/all') 4 | var plugins = [require('../plugins/v1-backcompat')] 5 | 6 | module.exports = engine.createStore(storages, plugins) 7 | -------------------------------------------------------------------------------- /sublime-storejs.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "tab_size": 4 4 | }, 5 | "folders": [{ 6 | "path": "./", 7 | "folder_exclude_patterns": ["node_modules"], 8 | "file_exclude_patterns": ["*.min.js"], 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /plugins/dump.js: -------------------------------------------------------------------------------- 1 | module.exports = dumpPlugin 2 | 3 | function dumpPlugin() { 4 | return { 5 | dump: dump 6 | } 7 | 8 | function dump(_) { 9 | var res = {} 10 | this.each(function(val, key) { 11 | res[key] = val 12 | }) 13 | return res 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plugins/json2_test.js: -------------------------------------------------------------------------------- 1 | require('./json2') 2 | 3 | module.exports = { 4 | setup: setup, 5 | } 6 | 7 | function setup(store) { 8 | 9 | test('serialization with json2', function() { 10 | store.set('foo', { bar:'cat' }) 11 | assert(store.get('foo').bar === 'cat') 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /scripts/run-browser-tests-live-reload.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var budo = require('budo') 4 | 5 | budo(__dirname+'/../dist/store.tests.js', { 6 | live: true, 7 | stream: process.stdout, 8 | port: 9966, 9 | debug: true, 10 | open: true, 11 | title: 'store.js browser tests', 12 | }) 13 | -------------------------------------------------------------------------------- /dist/store.modern.js: -------------------------------------------------------------------------------- 1 | var engine = require('../src/store-engine') 2 | 3 | var storages = [ 4 | require('../storages/localStorage'), 5 | require('../storages/sessionStorage'), 6 | require('../storages/cookieStorage'), 7 | require('../storages/memoryStorage'), 8 | ] 9 | var plugins = [] 10 | 11 | module.exports = engine.createStore(storages, plugins) 12 | -------------------------------------------------------------------------------- /scripts/run-node-tests.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | 3 | // simulate localStorage - must be done before importing tests 4 | var { Global } = require('../src/util') 5 | var LocalStorage = require('node-localstorage').LocalStorage 6 | Global.localStorage = new LocalStorage('/tmp/store.js-test') 7 | 8 | // Import and run tests 9 | var tests = require('../tests/tests') 10 | 11 | tests.runTests() 12 | -------------------------------------------------------------------------------- /plugins/defaults.js: -------------------------------------------------------------------------------- 1 | module.exports = defaultsPlugin 2 | 3 | function defaultsPlugin() { 4 | var defaultValues = {} 5 | 6 | return { 7 | defaults: defaults, 8 | get: get 9 | } 10 | 11 | function defaults(_, values) { 12 | defaultValues = values 13 | } 14 | 15 | function get(super_fn, key) { 16 | var val = super_fn() 17 | return (val !== undefined ? val : defaultValues[key]) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /storages/all.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Listed in order of usage preference 3 | 'localStorage': require('./localStorage'), 4 | 'oldFF-globalStorage': require('./oldFF-globalStorage'), 5 | 'oldIE-userDataStorage': require('./oldIE-userDataStorage'), 6 | 'cookieStorage': require('./cookieStorage'), 7 | 'sessionStorage': require('./sessionStorage'), 8 | 'memoryStorage': require('./memoryStorage'), 9 | } 10 | -------------------------------------------------------------------------------- /plugins/all.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'defaults': require('./defaults'), 3 | 'dump': require('./dump'), 4 | 'events': require('./events'), 5 | 'observe': require('./observe'), 6 | 'expire': require('./expire'), 7 | 'json2': require('./json2'), 8 | 'operations': require('./operations'), 9 | 'update': require('./update'), 10 | 'v1-backcompat': require('./v1-backcompat'), 11 | } 12 | -------------------------------------------------------------------------------- /plugins/update.js: -------------------------------------------------------------------------------- 1 | module.exports = updatePlugin 2 | 3 | function updatePlugin() { 4 | return { 5 | update: update 6 | } 7 | 8 | function update(_, key, optDefaultVal, updateFn) { 9 | if (arguments.length == 3) { 10 | updateFn = optDefaultVal 11 | optDefaultVal = undefined 12 | } 13 | var val = this.get(key, optDefaultVal) 14 | var retVal = updateFn(val) 15 | this.set(key, retVal != undefined ? retVal : val) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugins/observe.js: -------------------------------------------------------------------------------- 1 | var eventsPlugin = require('./events') 2 | 3 | module.exports = [eventsPlugin, observePlugin] 4 | 5 | function observePlugin() { 6 | return { 7 | observe: observe, 8 | unobserve: unobserve 9 | } 10 | 11 | function observe(_, key, callback) { 12 | var subId = this.watch(key, callback) 13 | callback(this.get(key)) 14 | return subId 15 | } 16 | function unobserve(_, subId) { 17 | this.unwatch(subId) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: test-node 2 | 3 | test-node: 4 | node scripts/run-node-tests.js 5 | 6 | test-browser: 7 | node scripts/run-browser-tests-live-reload.js 8 | 9 | test-saucelabs: build 10 | node scripts/run-saucelabs-tests.js 11 | 12 | tunnel: 13 | node scripts/create-tunnel.js 14 | 15 | build: test lint 16 | node scripts/compile-builds.js 17 | 18 | lint: 19 | ./node_modules/.bin/eslint tests/* src/* \ 20 | --ignore-pattern src/addon/lib/json2.js \ 21 | -------------------------------------------------------------------------------- /tests/util.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | deepEqual: deepEqual, 3 | } 4 | 5 | function deepEqual(a,b) { 6 | if (typeof a != typeof b) { 7 | return false 8 | } 9 | if (typeof a != 'object') { 10 | return a === b 11 | } 12 | var key 13 | for (key in a) { 14 | if (!deepEqual(a[key], b[key])) { 15 | return false 16 | } 17 | } 18 | for (key in b) { 19 | if (!deepEqual(b[key], a[key])) { 20 | return false 21 | } 22 | } 23 | return true 24 | } 25 | -------------------------------------------------------------------------------- /scripts/saucelabs/list-supported-browsers.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | 3 | var username = 'storejs' 4 | var password = new Buffer('ZjhjMzUyNjgtNzc2ZC00ZjlkLWEwNWUtN2FkM2Q0ZDgyNzk5', 'base64').toString('utf8') 5 | var saucelabs = require('./saucelabs') 6 | 7 | saucelabs.setAuth(username, password) 8 | saucelabs.listAllSupportedPlatforms(function(err, res) { 9 | if (err) { throw err } 10 | for (var i=0; i= 0; i--) { 27 | var key = localStorage().key(i) 28 | fn(read(key), key) 29 | } 30 | } 31 | 32 | function remove(key) { 33 | return localStorage().removeItem(key) 34 | } 35 | 36 | function clearAll() { 37 | return localStorage().clear() 38 | } 39 | -------------------------------------------------------------------------------- /plugins/observe_test.js: -------------------------------------------------------------------------------- 1 | require('./observe') 2 | 3 | module.exports = { 4 | setup: setup, 5 | } 6 | 7 | function setup(store) { 8 | 9 | test('observe', function() { 10 | store.clearAll() 11 | var count = -1 12 | var expect = [undefined] 13 | var obsId = store.observe('foo', function(val, oldVal) { 14 | count += 1 15 | assert(expect[count] == val) 16 | assert(expect[count - 1] == oldVal) 17 | }) // count == 1 18 | store.unobserve(obsId) 19 | 20 | expect.push('bar') 21 | store.set('foo', 'bar') 22 | store.observe('foo', function(val, oldVal) { 23 | count += 1 24 | assert(expect[count] == val) 25 | assert(expect[count - 1] == oldVal) 26 | }) // count == 2 27 | 28 | expect.push('bar2') 29 | store.set('foo', 'bar2') // count == 3 30 | assert(count + 1 == expect.length) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /storages/sessionStorage.js: -------------------------------------------------------------------------------- 1 | var util = require('../src/util') 2 | var Global = util.Global 3 | 4 | module.exports = { 5 | name: 'sessionStorage', 6 | read: read, 7 | write: write, 8 | each: each, 9 | remove: remove, 10 | clearAll: clearAll, 11 | } 12 | 13 | function sessionStorage() { 14 | return Global.sessionStorage 15 | } 16 | 17 | function read(key) { 18 | return sessionStorage().getItem(key) 19 | } 20 | 21 | function write(key, data) { 22 | return sessionStorage().setItem(key, data) 23 | } 24 | 25 | function each(fn) { 26 | for (var i = sessionStorage().length - 1; i >= 0; i--) { 27 | var key = sessionStorage().key(i) 28 | fn(read(key), key) 29 | } 30 | } 31 | 32 | function remove(key) { 33 | return sessionStorage().removeItem(key) 34 | } 35 | 36 | function clearAll() { 37 | return sessionStorage().clear() 38 | } 39 | -------------------------------------------------------------------------------- /plugins/expire.js: -------------------------------------------------------------------------------- 1 | var namespace = 'expire_mixin' 2 | 3 | module.exports = expirePlugin 4 | 5 | function expirePlugin() { 6 | var expirations = this.namespace(namespace) 7 | 8 | return { 9 | set: expire_set, 10 | get: expire_get, 11 | remove: expire_remove 12 | } 13 | 14 | function expire_set(super_fn, key, val, expiration) { 15 | if (!this.hasNamespace(namespace)) { 16 | expirations.set(key, expiration) 17 | } 18 | return super_fn() 19 | } 20 | 21 | function expire_get(super_fn, key) { 22 | if (!this.hasNamespace(namespace)) { 23 | var expiration = expirations.get(key, Number.MAX_VALUE) 24 | if (expiration <= new Date().getTime()) { 25 | this.remove(key) 26 | } 27 | } 28 | return super_fn() 29 | } 30 | 31 | function expire_remove(super_fn, key) { 32 | if (!this.hasNamespace(namespace)) { 33 | expirations.remove(key) 34 | } 35 | return super_fn() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /storages/memoryStorage.js: -------------------------------------------------------------------------------- 1 | // memoryStorage is a useful last fallback to ensure that the store 2 | // is functions (meaning store.get(), store.set(), etc will all function). 3 | // However, stored values will not persist when the browser navigates to 4 | // a new page or reloads the current page. 5 | 6 | module.exports = { 7 | name: 'memoryStorage', 8 | read: read, 9 | write: write, 10 | each: each, 11 | remove: remove, 12 | clearAll: clearAll, 13 | } 14 | 15 | var memoryStorage = {} 16 | 17 | function read(key) { 18 | return memoryStorage[key] 19 | } 20 | 21 | function write(key, data) { 22 | memoryStorage[key] = data 23 | } 24 | 25 | function each(callback) { 26 | for (var key in memoryStorage) { 27 | if (memoryStorage.hasOwnProperty(key)) { 28 | callback(memoryStorage[key], key) 29 | } 30 | } 31 | } 32 | 33 | function remove(key) { 34 | delete memoryStorage[key] 35 | } 36 | 37 | function clearAll(key) { 38 | memoryStorage = {} 39 | } 40 | -------------------------------------------------------------------------------- /storages/oldFF-globalStorage.js: -------------------------------------------------------------------------------- 1 | // oldFF-globalStorage provides storage for Firefox 2 | // versions 6 and 7, where no localStorage, etc 3 | // is available. 4 | 5 | var util = require('../src/util') 6 | var Global = util.Global 7 | 8 | module.exports = { 9 | name: 'oldFF-globalStorage', 10 | read: read, 11 | write: write, 12 | each: each, 13 | remove: remove, 14 | clearAll: clearAll, 15 | } 16 | 17 | var globalStorage = Global.globalStorage 18 | 19 | function read(key) { 20 | return globalStorage[key] 21 | } 22 | 23 | function write(key, data) { 24 | globalStorage[key] = data 25 | } 26 | 27 | function each(fn) { 28 | for (var i = globalStorage.length - 1; i >= 0; i--) { 29 | var key = globalStorage.key(i) 30 | fn(globalStorage[key], key) 31 | } 32 | } 33 | 34 | function remove(key) { 35 | return globalStorage.removeItem(key) 36 | } 37 | 38 | function clearAll() { 39 | each(function(key, _) { 40 | delete globalStorage[key] 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /plugins/update_test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setup: setup, 3 | } 4 | 5 | function setup(store) { 6 | 7 | test('update', function() { 8 | store.set('foo', { cat:'mat' }) 9 | assert(store.get('foo').cat == 'mat') 10 | store.update('foo', function(foo) { 11 | foo.cat = 'mat2' 12 | }) 13 | assert(store.get('foo').cat == 'mat2') 14 | }) 15 | 16 | test('update return value', function() { 17 | store.clearAll() 18 | store.update('foo', function(foo) { 19 | assert(foo == undefined) 20 | return { cat:'mat4' } 21 | }) 22 | assert(store.get('foo').cat == 'mat4') 23 | }) 24 | 25 | test('update default value', function() { 26 | store.clearAll() 27 | store.update('foo2', {}, function(foo2) { 28 | foo2.bar = 'cat' 29 | }) 30 | assert(store.get('foo2').bar == 'cat') 31 | }) 32 | 33 | test('update default value + return', function() { 34 | store.clearAll() 35 | store.update('foo2', [], function(foor2) { 36 | return { bar2:'cat2' } 37 | }) 38 | assert(typeof store.get('foo2') == 'object') 39 | assert(store.get('foo2').bar == undefined) 40 | assert(store.get('foo2').bar2 == 'cat2') 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2010-2017 Marcus Westin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/run-saucelabs-tests.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | 3 | var port = 9574 4 | var username = 'storejs' 5 | var password = new Buffer('ZjhjMzUyNjgtNzc2ZC00ZjlkLWEwNWUtN2FkM2Q0ZDgyNzk5', 'base64').toString('utf8') 6 | 7 | // TODO: Contribute to npm-saucelabs? Create new module? 8 | var saucelabs = require('./saucelabs/saucelabs') 9 | var tunnel = require('./saucelabs/tunnel') 10 | 11 | main(function(err) { 12 | if (err) { throw err } 13 | log('All done!') 14 | }) 15 | 16 | function main() { 17 | tunnel.setup(port, function(err, url) { 18 | saucelabs.setAuth(username, password) 19 | var s = saucelabs.platformSets 20 | var platformSets = [ 21 | // All supported platforms: 22 | /////////////////////////// 23 | s.ie, 24 | s.safari, 25 | s.firefox, 26 | s.chrome, 27 | s.android, 28 | s.ios, 29 | s.opera, 30 | 31 | // Specific test targets for development: 32 | ///////////////////////////////////////// 33 | // s.fast, 34 | // s.ie6, s.ie7, s.ie8, 35 | // s.ie9, s.ie10, s.ie11, 36 | // s.firefox4, s.firefox5, 37 | // s.ie10, 38 | ] 39 | saucelabs.runTest(url, platformSets, onDone) 40 | function onDone(err) { 41 | if (err) { 42 | console.log('Error', err) 43 | process.exit(1) 44 | } else { 45 | log('All tests passed!') 46 | process.exit(0) 47 | } 48 | } 49 | }) 50 | } 51 | 52 | function log() { 53 | console.log.apply(console, arguments) 54 | } 55 | -------------------------------------------------------------------------------- /plugins/expire_test.js: -------------------------------------------------------------------------------- 1 | require('./expire') 2 | 3 | module.exports = { 4 | setup: setup, 5 | } 6 | 7 | function setup(store) { 8 | 9 | test('expire', function(done) { 10 | // Ive observed multiple times when legacy browsers in various 11 | // environments (saucelabs, VMs, etc) have executed a scheduled 12 | // timeout function too soon. The solution is to run a longer, 13 | // timeout, but this substantially slows down the test suite. 14 | // Instead, we allow multiple attempts with increasing durations. 15 | attempt(5, 10) 16 | 17 | function attempt(remaining, duration) { 18 | runTests(duration, function check(ok) { 19 | if (ok) { 20 | return true 21 | } 22 | 23 | if (remaining > 0) { 24 | setTimeout(function() { 25 | attempt(remaining - 1, duration * 2) 26 | }, 0) 27 | return false 28 | } 29 | 30 | return assert(false) 31 | }) 32 | } 33 | 34 | function runTests(duration, check) { 35 | var expiration = new Date().getTime() + duration 36 | store.set('foo', 'bar', expiration) 37 | if (!check(store.get('foo') == 'bar')) { return } 38 | 39 | setTimeout(function() { 40 | if (!check(new Date().getTime() > expiration)) { return } 41 | if (!check(store.get('foo') == undefined)) { return } 42 | 43 | store.set('foo', 'bar') 44 | setTimeout(function() { 45 | if (!check(store.get('foo') == 'bar')) { return } 46 | 47 | done() 48 | }, 5) 49 | }, duration) 50 | } 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /scripts/saucelabs/saucelabs-api.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | 3 | module.exports = { 4 | setAuth: setAuth, 5 | get: get, 6 | post: post 7 | } 8 | 9 | var auth = { 10 | user: null, 11 | password: null, 12 | } 13 | 14 | function setAuth(saucelabsUsername, saucelabsToken) { 15 | auth.user = saucelabsUsername 16 | auth.password = saucelabsToken 17 | } 18 | 19 | 20 | function get(path, callback) { 21 | var params = { 22 | url: 'https://saucelabs.com/rest/v1/'+path, 23 | auth: auth 24 | } 25 | // console.log("REQ", params) 26 | request.get(params, function(err, res, body) { 27 | if (err) { 28 | throw err 29 | } 30 | if (res.statusCode != 200) { 31 | console.log(params) 32 | throw new Error('Non-200 status code: '+body) 33 | } 34 | // console.log("RES", params.url, body) 35 | callback(JSON.parse(body)) 36 | }) 37 | } 38 | 39 | 40 | function post(path, data, callback) { 41 | var params = { 42 | url: 'https://saucelabs.com/rest/v1/'+auth.user+'/'+path, 43 | auth: { user:auth.user, password:auth.password }, 44 | json: data 45 | } 46 | // console.log("REQ", params) 47 | request.post(params, function(err, res, body) { 48 | if (err) { 49 | throw err 50 | } 51 | if (res.statusCode != 200) { 52 | throw new Error('Non-200 status code: '+body) 53 | } 54 | // console.log("RES", params.url, body) 55 | callback(body) 56 | }) 57 | } 58 | 59 | // https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods#JavaScriptUnitTestingMethods-StartJSUnitTests 60 | -------------------------------------------------------------------------------- /plugins/operations.js: -------------------------------------------------------------------------------- 1 | var util = require('../src/util') 2 | var slice = util.slice 3 | var assign = util.assign 4 | 5 | var updatePlugin = require('./update') 6 | 7 | module.exports = [updatePlugin, operationsPlugin] 8 | 9 | function operationsPlugin() { 10 | return { 11 | // array 12 | push: push, 13 | pop: pop, 14 | shift: shift, 15 | unshift: unshift, 16 | 17 | // obj 18 | assign: assign_, 19 | } 20 | 21 | // array 22 | function push(_, key, val1, val2, val3, etc) { 23 | return _arrayOp.call(this, 'push', arguments) 24 | } 25 | function pop(_, key) { 26 | return _arrayOp.call(this, 'pop', arguments) 27 | } 28 | function shift(_, key) { 29 | return _arrayOp.call(this, 'shift', arguments) 30 | } 31 | function unshift(_, key, val1, val2, val3, etc) { 32 | return _arrayOp.call(this, 'unshift', arguments) 33 | } 34 | 35 | // obj 36 | function assign_(_, key, props1, props2, props3, etc) { 37 | var varArgs = slice(arguments, 2) 38 | return this.update(key, {}, function(val) { 39 | if (typeof val != 'object') { 40 | throw new Error('store.assign called for non-object value with key "'+key+'"') 41 | } 42 | varArgs.unshift(val) 43 | return assign.apply(Object, varArgs) 44 | }) 45 | } 46 | 47 | // internal 48 | /////////// 49 | function _arrayOp(arrayFn, opArgs) { 50 | var res 51 | var key = opArgs[1] 52 | var rest = slice(opArgs, 2) 53 | this.update(key, [], function(arrVal) { 54 | res = Array.prototype[arrayFn].apply(arrVal, rest) 55 | }) 56 | return res 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /plugins/v1-backcompat.js: -------------------------------------------------------------------------------- 1 | var dumpPlugin = require('./dump') 2 | var json2Plugin = require('./json2') 3 | 4 | module.exports = [dumpPlugin, json2Plugin, v1BackcompatPlugin] 5 | 6 | function v1BackcompatPlugin() { 7 | this.disabled = !this.enabled 8 | return { 9 | has: backcompat_has, 10 | transact: backcompat_transact, 11 | clear: backcompat_clear, 12 | forEach: backcompat_forEach, 13 | getAll: backcompat_getAll, 14 | serialize: backcompat_serialize, 15 | deserialize: backcompat_deserialize, 16 | } 17 | } 18 | 19 | function backcompat_has(_, key) { 20 | return this.get(key) !== undefined 21 | } 22 | function backcompat_transact(_, key, defaultVal, transactionFn) { 23 | if (transactionFn == null) { 24 | transactionFn = defaultVal 25 | defaultVal = null 26 | } 27 | if (defaultVal == null) { 28 | defaultVal = {} 29 | } 30 | var val = this.get(key, defaultVal) 31 | var ret = transactionFn(val) 32 | this.set(key, ret === undefined ? val : ret) 33 | } 34 | function backcompat_clear(_) { 35 | return this.clearAll.call(this) 36 | } 37 | function backcompat_forEach(_, fn) { 38 | return this.each.call(this, function(val, key) { 39 | fn(key, val) 40 | }) 41 | } 42 | function backcompat_getAll(_) { 43 | return this.dump.call(this) 44 | } 45 | function backcompat_serialize(_, value) { 46 | return JSON.stringify(value) 47 | } 48 | function backcompat_deserialize(_, value) { 49 | if (typeof value != 'string') { return undefined } 50 | try { return JSON.parse(value) } 51 | catch(e) { return value || undefined } 52 | } 53 | -------------------------------------------------------------------------------- /plugins/v1-backcompat_test.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setup: setup, 3 | } 4 | 5 | function setup(store) { 6 | 7 | test('backwards compatability with v1', function() { 8 | store.clear() 9 | 10 | assert(typeof store.disabled == 'boolean') 11 | assert(typeof store.enabled == 'boolean') 12 | assert(typeof store.version == 'string') 13 | assert(typeof store.set == 'function') 14 | assert(typeof store.get == 'function') 15 | assert(typeof store.has == 'function') 16 | assert(typeof store.remove == 'function') 17 | assert(typeof store.clear == 'function') 18 | assert(typeof store.transact == 'function') 19 | assert(typeof store.getAll == 'function') 20 | assert(typeof store.forEach == 'function') 21 | assert(typeof store.serialize == 'function') 22 | assert(typeof store.deserialize == 'function') 23 | 24 | store.transact('foosact', function(val) { 25 | assert(typeof val == 'object', "new key is not an object at beginning of transaction") 26 | val.foo = 'foo' 27 | }) 28 | store.transact('foosact', function(val) { 29 | assert(val.foo == 'foo', "first transaction did not register") 30 | val.bar = 'bar' 31 | }) 32 | assert(store.getAll().foosact.foo == 'foo') 33 | var wasCalled = false 34 | store.forEach(function(key, val) { 35 | wasCalled = true 36 | assert(key == 'foosact') 37 | assert(val.foo == 'foo') 38 | }) 39 | assert(wasCalled) 40 | assert(store.serialize({}) == '{}') 41 | assert(store.get('foosact').bar == 'bar', "second transaction did not register") 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /storages/cookieStorage.js: -------------------------------------------------------------------------------- 1 | // cookieStorage is useful Safari private browser mode, where localStorage 2 | // doesn't work but cookies do. This implementation is adopted from 3 | // https://developer.mozilla.org/en-US/docs/Web/API/Storage/LocalStorage 4 | 5 | var util = require('../src/util') 6 | var Global = util.Global 7 | var trim = util.trim 8 | 9 | module.exports = { 10 | name: 'cookieStorage', 11 | read: read, 12 | write: write, 13 | each: each, 14 | remove: remove, 15 | clearAll: clearAll, 16 | } 17 | 18 | var doc = Global.document 19 | 20 | function read(key) { 21 | if (!key || !_has(key)) { return null } 22 | var regexpStr = "(?:^|.*;\\s*)" + 23 | escape(key).replace(/[\-\.\+\*]/g, "\\$&") + 24 | "\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*" 25 | return unescape(doc.cookie.replace(new RegExp(regexpStr), "$1")) 26 | } 27 | 28 | function each(callback) { 29 | var cookies = doc.cookie.split(/; ?/g) 30 | for (var i = cookies.length - 1; i >= 0; i--) { 31 | if (!trim(cookies[i])) { 32 | continue 33 | } 34 | var kvp = cookies[i].split('=') 35 | var key = unescape(kvp[0]) 36 | var val = unescape(kvp[1]) 37 | callback(val, key) 38 | } 39 | } 40 | 41 | function write(key, data) { 42 | if(!key) { return } 43 | doc.cookie = escape(key) + "=" + escape(data) + "; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/" 44 | } 45 | 46 | function remove(key) { 47 | if (!key || !_has(key)) { 48 | return 49 | } 50 | doc.cookie = escape(key) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/" 51 | } 52 | 53 | function clearAll() { 54 | each(function(_, key) { 55 | remove(key) 56 | }) 57 | } 58 | 59 | function _has(key) { 60 | return (new RegExp("(?:^|;\\s*)" + escape(key).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(doc.cookie) 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "store", 3 | "version": "2.0.3", 4 | "description": "A localStorage wrapper for all browsers without using cookies or flash. Uses localStorage, globalStorage, and userData behavior under the hood", 5 | "main": "dist/store.legacy.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/marcuswestin/store.js.git" 12 | }, 13 | "author": "Marcus Westin ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "http://github.com/marcuswestin/store.js/issues" 17 | }, 18 | "homepage": "https://github.com/marcuswestin/store.js#readme", 19 | "devDependencies": { 20 | "babel-preset-es2015": "^6.22.0", 21 | "babelify": "^7.3.0", 22 | "browserify": "^14.1.0", 23 | "budo": "^7.1.0", 24 | "eslint": "^3.12.2", 25 | "lodash": "^3.10.1", 26 | "ngrok": "^2.2.6", 27 | "node-localstorage": "^0.6.0", 28 | "request": "^2.67.0", 29 | "tinytest": "^1.1.3", 30 | "uglify-js": "^2.7.5" 31 | }, 32 | "eslintConfig": { 33 | "env": { 34 | "browser": true, 35 | "commonjs": true, 36 | "es6": true, 37 | "node": true 38 | }, 39 | "extends": "eslint:recommended", 40 | "parserOptions": { 41 | "sourceType": "module" 42 | }, 43 | "globals": { 44 | "test": false, 45 | "assert": false, 46 | "print": false 47 | }, 48 | "rules": { 49 | "no-unused-vars": [ 50 | "error", 51 | { 52 | "vars": "all", 53 | "args": "none", 54 | "varsIgnorePattern": "^_$" 55 | } 56 | ], 57 | "indent": [ 58 | "error", 59 | "tab" 60 | ], 61 | "linebreak-style": [ 62 | "error", 63 | "unix" 64 | ], 65 | "semi": [ 66 | "error", 67 | "never" 68 | ] 69 | } 70 | }, 71 | "engines": { 72 | "node": "*" 73 | }, 74 | "directories": { 75 | "lib": "." 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /plugins/events_test.js: -------------------------------------------------------------------------------- 1 | require('./events') 2 | 3 | module.exports = { 4 | setup: setup, 5 | } 6 | 7 | function setup(store) { 8 | 9 | test('events', function() { 10 | store.set('foo', 'bar') 11 | 12 | var expectationNone = _createExpectation('expectNone', undefined) 13 | store.watch('foo', function(){}) 14 | var expectation1 = _createExpectation('foo', 'bar') 15 | var expectationOnce = _createExpectation('foo', 'bar', true) 16 | store.watch('foo', function(){}) 17 | 18 | expectation1.add('bar2') 19 | expectationOnce.add('bar2') 20 | store.set('foo', 'bar2') 21 | 22 | expectation1.add(undefined) 23 | store.remove('foo') 24 | 25 | expectation1.add('bar3') 26 | store.set('foo', 'bar3') 27 | 28 | var expectation2 = _createExpectation('foo', 'bar3') 29 | expectation1.add(undefined) 30 | expectation2.add(undefined) 31 | store.clearAll() // Should fire for foo 32 | store.clearAll() // Should not fire anything 33 | 34 | expectation1.unwatch() 35 | expectation2.add('bar4') 36 | store.set('foo', 'bar4') // Should only fire for expectation2 37 | 38 | expectation1.check() 39 | expectationOnce.check() 40 | expectation2.check() 41 | expectationNone.check() 42 | expectation2.unwatch() 43 | }) 44 | 45 | function _createExpectation(key, firstOldVal, useOnce) { 46 | var expectation = { 47 | values: [firstOldVal], 48 | count: 0, 49 | add: function(value) { 50 | this.values.push(value) 51 | }, 52 | check: function() { 53 | assert(expectation.count + 1 == expectation.values.length) 54 | }, 55 | unwatch: function() { 56 | store.unwatch(watchId) 57 | } 58 | } 59 | 60 | var watchId = (useOnce 61 | ? store.once(key, callback) 62 | : store.watch(key, callback) 63 | ) 64 | function callback(val, oldVal) { 65 | expectation.count += 1 66 | assert(expectation.values[expectation.count] == val) 67 | assert(expectation.values[expectation.count - 1] == oldVal) 68 | } 69 | 70 | return expectation 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /plugins/operations_test.js: -------------------------------------------------------------------------------- 1 | var { each, map } = require('../src/util') 2 | var { deepEqual } = require('../tests/util') 3 | 4 | require('./operations') 5 | 6 | module.exports = { 7 | setup: setup, 8 | } 9 | 10 | function setup(store) { 11 | 12 | test('push', function() { 13 | _testArrayOp('push', [], [ 14 | [], 15 | ['a'], 16 | ['b','c'], 17 | [null], 18 | [[], {}] 19 | ]) 20 | }) 21 | 22 | test('unshift', function() { 23 | _testArrayOp('unshift', undefined, [ 24 | [], 25 | ['a'], 26 | ['b','c'], 27 | [null], 28 | [[], {}] 29 | ]) 30 | }) 31 | 32 | test('pop', function() { 33 | var arr = ['a', 'b', 'c', null, [[], {}]] 34 | // Call pop arr.length + 1 times. No args each time 35 | var argsList = map(arr, function() { return [] }).concat([]) 36 | _testArrayOp('pop', arr, argsList) 37 | }) 38 | 39 | test('shift', function() { 40 | var arr = ['a', 'b', 'c', null, [[], {}]] 41 | // Call shift arr.length + 1 times. No args each time 42 | var argsList = map(arr, function() { return [] }).concat([]) 43 | _testArrayOp('shift', arr, argsList) 44 | }) 45 | 46 | test('assign', function() { 47 | store.clearAll() 48 | var expect = { bar:'cat', mat:{ hat:'bat', arr:[1,2,3] }} 49 | store.assign('foo', expect) 50 | assert(deepEqual(store.get('foo'), expect)) 51 | var add = { bar:'cat2', mat:{ hat:'bat2' }, newProp:'newProp'} 52 | store.assign('foo', add) 53 | each(add, function(val, key) { 54 | expect[key] = val 55 | }) 56 | assert(deepEqual(store.get('foo'), expect)) 57 | }) 58 | 59 | function _testArrayOp(fnName, arr, argLists) { 60 | var key = 'test-'+fnName 61 | store.set(key, arr) 62 | arr = (arr || []) 63 | var arrFn = arr[fnName] 64 | var storeFn = store[fnName] 65 | each(argLists, function(args) { 66 | var expectedFnResult = arrFn.apply(arr, args) 67 | var actualFnResult = storeFn.apply(store, [key].concat(args)) 68 | assert(deepEqual(expectedFnResult, actualFnResult)) 69 | var actual = store.get(key) 70 | assert(deepEqual(arr, actual)) 71 | }) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /scripts/saucelabs/tunnel.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var fs = require('fs') 3 | var ngrok = require('ngrok') 4 | 5 | module.exports = { 6 | setup: setup, 7 | } 8 | 9 | function setup(port, callback) { 10 | startServer(port, function(err) { 11 | if (err) { return callback(err) } 12 | console.log("Creating tunnel - this might take a few seconds") 13 | startTunnel(port, function(err, url) { 14 | if (err) { return callback(err) } 15 | console.log("tunnel up at", url) 16 | callback(null, url) 17 | }) 18 | }) 19 | } 20 | 21 | function startTunnel(port, callback) { 22 | // return callback(null, 'https://07f51ed4.ngrok.io') 23 | var authtoken = new Buffer('NTJuelB1dUpVSDNycDNjZ3pldHVEXzVnWlNObkpuMlFaR013WjZ0eUZUQw==', 'base64').toString('utf8') 24 | ngrok.connect({ addr:port, subdomain:'storejs-test', authtoken:authtoken }, function(err, url) { 25 | if (err) { return callback(err) } 26 | url = url.replace('https:', 'http:') 27 | callback(null, url) 28 | }) 29 | } 30 | 31 | function startServer(port, callback) { 32 | var server = http.createServer(handleReq) 33 | server.listen(port) 34 | server.on('listening', function(err) { 35 | if (err) { return callback(err) } 36 | console.log('local server listening on http://localhost:'+port+'/') 37 | callback() 38 | }) 39 | 40 | function handleReq(req, res) { 41 | console.log(req.url) 42 | if (req.url == '/') { 43 | res.writeHead(200, { 'Content-Type':'text/html' }) 44 | res.end(testRunnerHTML) 45 | 46 | } else if (req.url == '/store.tests.min.js') { 47 | var headers = { 48 | 'Content-Type':'application/javascript', 49 | 'Cache-Control': 'no-cache, no-store, must-revalidate', 50 | 'Pragma': 'no-cache', 51 | 'Expires': '0' 52 | } 53 | res.writeHead(200, headers) 54 | fs.createReadStream(__dirname+'/../../dist/store.tests.min.js').pipe(res) 55 | 56 | } else { 57 | res.writeHead(404) 58 | res.end('Not found') 59 | } 60 | } 61 | 62 | var testRunnerHTML = ` 63 | 64 | 65 | store.js test runner 66 | 67 | 68 |

store.js test runner

69 | 70 | 71 | 72 | `.replace(/\n\t\t/g, '\n').replace(/^\n/, '') 73 | } 74 | -------------------------------------------------------------------------------- /plugins/events.js: -------------------------------------------------------------------------------- 1 | var util = require('../src/util') 2 | var bind = util.bind 3 | var each = util.each 4 | var create = util.create 5 | var slice = util.slice 6 | 7 | module.exports = eventsPlugin 8 | 9 | function eventsPlugin() { 10 | var pubsub = _newPubSub() 11 | 12 | return { 13 | watch: watch, 14 | unwatch: unwatch, 15 | once: once, 16 | 17 | set: set, 18 | remove: remove, 19 | clearAll: clearAll 20 | } 21 | 22 | // new pubsub functions 23 | function watch(_, key, listener) { 24 | return pubsub.on(key, bind(this, listener)) 25 | } 26 | function unwatch(_, subId) { 27 | pubsub.off(subId) 28 | } 29 | function once(_, key, listener) { 30 | pubsub.once(key, bind(this, listener)) 31 | } 32 | 33 | // overwrite function to fire when appropriate 34 | function set(super_fn, key, val) { 35 | var oldVal = this.get(key) 36 | super_fn() 37 | pubsub.fire(key, val, oldVal) 38 | } 39 | function remove(super_fn, key) { 40 | var oldVal = this.get(key) 41 | super_fn() 42 | pubsub.fire(key, undefined, oldVal) 43 | } 44 | function clearAll(super_fn) { 45 | var oldVals = {} 46 | this.each(function(val, key) { 47 | oldVals[key] = val 48 | }) 49 | super_fn() 50 | each(oldVals, function(oldVal, key) { 51 | pubsub.fire(key, undefined, oldVal) 52 | }) 53 | } 54 | } 55 | 56 | 57 | function _newPubSub() { 58 | return create(_pubSubBase, { 59 | _id: 0, 60 | _subSignals: {}, 61 | _subCallbacks: {} 62 | }) 63 | } 64 | 65 | var _pubSubBase = { 66 | _id: null, 67 | _subCallbacks: null, 68 | _subSignals: null, 69 | on: function(signal, callback) { 70 | if (!this._subCallbacks[signal]) { 71 | this._subCallbacks[signal] = {} 72 | } 73 | this._id += 1 74 | this._subCallbacks[signal][this._id] = callback 75 | this._subSignals[this._id] = signal 76 | return this._id 77 | }, 78 | off: function(subId) { 79 | var signal = this._subSignals[subId] 80 | delete this._subCallbacks[signal][subId] 81 | delete this._subSignals[subId] 82 | }, 83 | once: function(signal, callback) { 84 | var subId = this.on(signal, bind(this, function() { 85 | callback.apply(this, arguments) 86 | this.off(subId) 87 | })) 88 | }, 89 | fire: function(signal) { 90 | var args = slice(arguments, 1) 91 | each(this._subCallbacks[signal], function(callback) { 92 | callback.apply(this, args) 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /scripts/compile-builds.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | var browserify = require('browserify') 6 | var UglifyJS = require('uglify-js') 7 | var base = __dirname + '/..' 8 | 9 | module.exports = { 10 | run: run, 11 | } 12 | 13 | if (require.main === module) { 14 | main() 15 | } 16 | 17 | function main() { 18 | run(function(err) { 19 | if (err) { throw err } 20 | }) 21 | } 22 | 23 | function run(callback) { 24 | var dir = base+'/dist' 25 | fs.readdir(dir, function(err, items) { 26 | next() 27 | function next() { 28 | var item = items.shift() 29 | if (!item) { 30 | return callback() 31 | } 32 | if (item[0] == '.') { 33 | return next() 34 | } 35 | if (item.match(/\.min\.js$/)) { 36 | return next() 37 | } 38 | var input = path.resolve(dir+'/'+item) 39 | var output = input.replace(/\.js$/, '.min.js') 40 | console.log('compile', input, '->', output) 41 | compileFile(input, output, function(err) { 42 | if (err) { 43 | return callback(err) 44 | } 45 | next() 46 | }) 47 | } 48 | }) 49 | } 50 | 51 | function compileFile(input, output, callback) { 52 | var copyright = '/* store.js - Copyright (c) 2010-2017 Marcus Westin */' 53 | // TODO: sourcemaps - depends on https://github.com/mishoo/UglifyJS2/issues/520 54 | browserify([input], { standalone:'store', expose:'store' }) // TODO: sourcemaps - use `debug:true` 55 | .transform('babelify', { presets:['es2015'] }) // TODO: sourcemaps - use `sourceMaps:true` 56 | .bundle(processResult) 57 | 58 | function processResult(err, buf) { 59 | if (err) { return callback(err) } 60 | var code = buf.toString() 61 | code = minify(code) 62 | var result = copyright+'\n'+code 63 | fs.writeFile(output, result, function(err) { 64 | if (err) { return callback(err) } 65 | var b = Buffer.byteLength(result, 'utf8') 66 | var k = Math.round(b/1000) 67 | console.log(k+'k \t('+b+')') 68 | callback() 69 | }) 70 | } 71 | } 72 | 73 | function minify(code) { 74 | var minified = UglifyJS.minify(code, { 75 | fromString: true, 76 | compress: { screw_ie8:false }, 77 | mangle: { screw_ie8:false }, 78 | output: { screw_ie8:false }, 79 | // warnings: true, 80 | // mangleProperties: { reserved:[] }, 81 | }) 82 | return minified.code // TODO: sourcemaps - use `result.map`. 83 | } 84 | -------------------------------------------------------------------------------- /scripts/saucelabs/saucelabs-platformSets.js: -------------------------------------------------------------------------------- 1 | // See https://wiki.saucelabs.com/display/DOCS/Platform+Configurator?_ga=1.24059122.934400320.1451142104#/ 2 | // See ./list-saucelabs-platforms.js 3 | var CURRENT_VERSION = '' 4 | var BETA_VERSION = 'beta' 5 | var CHROME_VERSIONS = ['31', CURRENT_VERSION] 6 | var FIREFOX_VERSIONS = ['4', '5', '6', '7', CURRENT_VERSION] 7 | var OPERA_VERSIONS = ['11', '12'] 8 | 9 | var platforms = module.exports = { 10 | // Fast trial runs 11 | ////////////////// 12 | fast: { 13 | 'Linux': { 'chrome': [CURRENT_VERSION] }, 14 | }, 15 | 16 | // Common browser sets 17 | ////////////////////// 18 | ie: { 19 | 'Windows XP': { 'internet explorer': ['6', '7', '8'] }, 20 | 'Windows 7': { 'internet explorer': ['9'] }, 21 | 'Windows 8': { 'internet explorer': ['10'] }, 22 | 'Windows 10': { 'internet explorer': ['11'], 'microsoftedge': [CURRENT_VERSION] }, 23 | }, 24 | safari: { 25 | 'Windows 7': { 'safari': ['5'] }, 26 | 'OS X 10.8': { 'safari': ['6'] }, 27 | 'OS X 10.9': { 'safari': ['7'] }, 28 | 'OS X 10.10': { 'safari': ['8'] }, 29 | 'OS X 10.11': { 'safari': ['9'] }, 30 | 'OS X 10.12': { 'safari': ['10'] }, 31 | }, 32 | firefox: { 33 | 'Linux': { 'firefox': ['40'] }, 34 | 'Windows XP': { 'firefox': ['4', '5'] }, 35 | 'Windows 10': { 'firefox': [CURRENT_VERSION] }, 36 | 'Mac 10.12': { 'firefox': [CURRENT_VERSION] }, 37 | }, 38 | android: { 39 | 'Linux': { 'android': ['4.4','5.0','5.1'] }, 40 | }, 41 | ios: { 42 | 'Mac 10.10': { 43 | 'ipad': ['8.4'], 44 | 'iphone':['8.4'], 45 | }, 46 | 'Mac 10.11': { 47 | 'ipad': ['9.3', '10.0'], 48 | 'iphone':['9.3', '10.0'], 49 | } 50 | }, 51 | chrome: { 52 | 'Mac 10.12': { 'chrome':['27', CURRENT_VERSION] }, 53 | 'Windows 10': { 'chrome':['26', CURRENT_VERSION] }, 54 | }, 55 | opera: { 56 | 'Windows XP': { 'opera':'11' }, 57 | 'Linux': { 'opera':'12' }, 58 | }, 59 | 60 | // Individual browser versions 61 | ////////////////////////////// 62 | ie6: { 'Windows XP': { 'internet explorer': ['6'] } }, 63 | ie7: { 'Windows XP': { 'internet explorer': ['7'] } }, 64 | ie8: { 'Windows XP': { 'internet explorer': ['8'] } }, 65 | ie9: { 'Windows 7': { 'internet explorer': ['9'] } }, 66 | ie10:{ 'Windows 8': { 'internet explorer': ['10'] } }, 67 | ie11:{ 'Windows 10': { 'internet explorer': ['11'] } }, 68 | 69 | firefox4: { 'Windows XP': { 'firefox': ['4'] } }, 70 | firefox5: { 'Windows XP': { 'firefox': ['5'] } }, 71 | } 72 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | var assign = make_assign() 2 | var create = make_create() 3 | var trim = make_trim() 4 | var Global = (typeof window !== 'undefined' ? window : global) 5 | 6 | module.exports = { 7 | assign: assign, 8 | create: create, 9 | trim: trim, 10 | bind: bind, 11 | slice: slice, 12 | each: each, 13 | map: map, 14 | pluck: pluck, 15 | isList: isList, 16 | isFunction: isFunction, 17 | isObject: isObject, 18 | Global: Global, 19 | } 20 | 21 | function make_assign() { 22 | if (Object.assign) { 23 | return Object.assign 24 | } else { 25 | return function shimAssign(obj, props1, props2, etc) { 26 | for (var i = 1; i < arguments.length; i++) { 27 | each(Object(arguments[i]), function(val, key) { 28 | obj[key] = val 29 | }) 30 | } 31 | return obj 32 | } 33 | } 34 | } 35 | 36 | function make_create() { 37 | if (Object.create) { 38 | return function create(obj, assignProps1, assignProps2, etc) { 39 | var assignArgsList = slice(arguments, 1) 40 | return assign.apply(this, [Object.create(obj)].concat(assignArgsList)) 41 | } 42 | } else { 43 | function F() {} // eslint-disable-line no-inner-declarations 44 | return function create(obj, assignProps1, assignProps2, etc) { 45 | var assignArgsList = slice(arguments, 1) 46 | F.prototype = obj 47 | return assign.apply(this, [new F()].concat(assignArgsList)) 48 | } 49 | } 50 | } 51 | 52 | function make_trim() { 53 | if (String.prototype.trim) { 54 | return function trim(str) { 55 | return String.prototype.trim.call(str) 56 | } 57 | } else { 58 | return function trim(str) { 59 | return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '') 60 | } 61 | } 62 | } 63 | 64 | function bind(obj, fn) { 65 | return function() { 66 | return fn.apply(obj, Array.prototype.slice.call(arguments, 0)) 67 | } 68 | } 69 | 70 | function slice(arr, index) { 71 | return Array.prototype.slice.call(arr, index || 0) 72 | } 73 | 74 | function each(obj, fn) { 75 | pluck(obj, function(key, val) { 76 | fn(key, val) 77 | return false 78 | }) 79 | } 80 | 81 | function map(obj, fn) { 82 | var res = (isList(obj) ? [] : {}) 83 | pluck(obj, function(v, k) { 84 | res[k] = fn(v, k) 85 | return false 86 | }) 87 | return res 88 | } 89 | 90 | function pluck(obj, fn) { 91 | if (isList(obj)) { 92 | for (var i=0; i tag for IE7 (GH issue #68) 51 | 52 | v1.3.7 53 | + Fix store.getAll for IE6 54 | 55 | v1.3.6 56 | + Remove globalStorage and drop FF 2.0/3.0 support (See https://github.com/marcuswestin/store.js/issues/44) 57 | 58 | v1.3.5 59 | + Now store.set returns the set value: `store.set(key, value) == value` 60 | + Values previously set with localStorage directly are now parsed handler by store.js: `localStorage['foo'] = 1; assert(store.get('foo') == 1)` 61 | 62 | v1.3.4 63 | + Add store.enabled 64 | + Deprecate store.disabled 65 | + Add link to Jack Franklin's screencast 66 | 67 | v1.3.3 68 | + Fix IE keys beginning with numeric characters (nice find @pauldwaite) 69 | 70 | v1.3.2 71 | + Implement store.getAll() (patch by @blq) 72 | 73 | v1.3.0 74 | + Use uglify.js for minifying store.min.js and store+json.min.js 75 | + Add build script 76 | 77 | v1.2.0 78 | + Remove same-path restrictions in IE6/7! (Thanks @mjpizz!) 79 | + Support CommonJS and AMD module systems (Thanks @pereckerdal!) 80 | + Fix: store.set('foo', undefined); store.get('foo') no longer throws (Thanks @buger!) 81 | 82 | v1.1.1 83 | + Publish in npm as "store" rather than "store.js" 84 | + Add commonjs export for require support 85 | + Add supported browsers Chrome 6-11, Firefox 4.0 86 | 87 | v1.1.0 88 | + First versioned version. 89 | + API: store.set, store.get, store.remove, store.clear, store.transact 90 | + Minified versions are included: store.min.js for store.js only, and store+json2.min.js for store.js and json2.js 91 | 92 | TODO 93 | - Get around IE6/7 per-directory restrition. @lrbabe/@louis_remi had the idea of putting the store.js API in an anonymous iframe a la https://github.com/meebo/embed-code and see what directory restriction that would fall under 94 | -------------------------------------------------------------------------------- /storages/oldIE-userDataStorage.js: -------------------------------------------------------------------------------- 1 | // oldIE-userDataStorage provides storage for Internet Explorer 2 | // versions 6 and 7, where no localStorage, sessionStorage, etc 3 | // is available. 4 | 5 | var util = require('../src/util') 6 | var Global = util.Global 7 | 8 | module.exports = { 9 | name: 'oldIE-userDataStorage', 10 | write: write, 11 | read: read, 12 | each: each, 13 | remove: remove, 14 | clearAll: clearAll, 15 | } 16 | 17 | var storageName = 'storejs' 18 | var doc = Global.document 19 | var _withStorageEl = _makeIEStorageElFunction() 20 | var disable = (Global.navigator ? Global.navigator.userAgent : '').match(/ (MSIE 8|MSIE 9|MSIE 10)\./) // MSIE 9.x, MSIE 10.x 21 | 22 | function write(unfixedKey, data) { 23 | if (disable) { return } 24 | var fixedKey = fixKey(unfixedKey) 25 | _withStorageEl(function(storageEl) { 26 | storageEl.setAttribute(fixedKey, data) 27 | storageEl.save(storageName) 28 | }) 29 | } 30 | 31 | function read(unfixedKey) { 32 | if (disable) { return } 33 | var fixedKey = fixKey(unfixedKey) 34 | var res = null 35 | _withStorageEl(function(storageEl) { 36 | res = storageEl.getAttribute(fixedKey) 37 | }) 38 | return res 39 | } 40 | 41 | function each(callback) { 42 | _withStorageEl(function(storageEl) { 43 | var attributes = storageEl.XMLDocument.documentElement.attributes 44 | for (var i=attributes.length-1; i>=0; i--) { 45 | var attr = attributes[i] 46 | callback(storageEl.getAttribute(attr.name), attr.name) 47 | } 48 | }) 49 | } 50 | 51 | function remove(unfixedKey) { 52 | var fixedKey = fixKey(unfixedKey) 53 | _withStorageEl(function(storageEl) { 54 | storageEl.removeAttribute(fixedKey) 55 | storageEl.save(storageName) 56 | }) 57 | } 58 | 59 | function clearAll() { 60 | _withStorageEl(function(storageEl) { 61 | var attributes = storageEl.XMLDocument.documentElement.attributes 62 | storageEl.load(storageName) 63 | for (var i=attributes.length-1; i>=0; i--) { 64 | storageEl.removeAttribute(attributes[i].name) 65 | } 66 | storageEl.save(storageName) 67 | }) 68 | } 69 | 70 | // Helpers 71 | ////////// 72 | 73 | // In IE7, keys cannot start with a digit or contain certain chars. 74 | // See https://github.com/marcuswestin/store.js/issues/40 75 | // See https://github.com/marcuswestin/store.js/issues/83 76 | var forbiddenCharsRegex = new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]", "g") 77 | function fixKey(key) { 78 | return key.replace(/^\d/, '___$&').replace(forbiddenCharsRegex, '___') 79 | } 80 | 81 | function _makeIEStorageElFunction() { 82 | if (!doc || !doc.documentElement || !doc.documentElement.addBehavior) { 83 | return null 84 | } 85 | var scriptTag = 'script', 86 | storageOwner, 87 | storageContainer, 88 | storageEl 89 | 90 | // Since #userData storage applies only to specific paths, we need to 91 | // somehow link our data to a specific path. We choose /favicon.ico 92 | // as a pretty safe option, since all browsers already make a request to 93 | // this URL anyway and being a 404 will not hurt us here. We wrap an 94 | // iframe pointing to the favicon in an ActiveXObject(htmlfile) object 95 | // (see: http://msdn.microsoft.com/en-us/library/aa752574(v=VS.85).aspx) 96 | // since the iframe access rules appear to allow direct access and 97 | // manipulation of the document element, even for a 404 page. This 98 | // document can be used instead of the current document (which would 99 | // have been limited to the current path) to perform #userData storage. 100 | try { 101 | /* global ActiveXObject */ 102 | storageContainer = new ActiveXObject('htmlfile') 103 | storageContainer.open() 104 | storageContainer.write('<'+scriptTag+'>document.w=window') 105 | storageContainer.close() 106 | storageOwner = storageContainer.w.frames[0].document 107 | storageEl = storageOwner.createElement('div') 108 | } catch(e) { 109 | // somehow ActiveXObject instantiation failed (perhaps some special 110 | // security settings or otherwse), fall back to per-path storage 111 | storageEl = doc.createElement('div') 112 | storageOwner = doc.body 113 | } 114 | 115 | return function(storeFunction) { 116 | var args = [].slice.call(arguments, 0) 117 | args.unshift(storageEl) 118 | // See http://msdn.microsoft.com/en-us/library/ms531081(v=VS.85).aspx 119 | // and http://msdn.microsoft.com/en-us/library/ms531424(v=VS.85).aspx 120 | storageOwner.appendChild(storageEl) 121 | storageEl.addBehavior('#default#userData') 122 | storageEl.load(storageName) 123 | storeFunction.apply(this, args) 124 | storageOwner.removeChild(storageEl) 125 | return 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /scripts/saucelabs/saucelabs.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var api = require('./saucelabs-api') 3 | 4 | module.exports = { 5 | setAuth: api.setAuth, 6 | listAllSupportedPlatforms: listAllSupportedPlatforms, 7 | runTest: runTest, 8 | platformSets: require('./saucelabs-platformSets'), 9 | } 10 | 11 | function listAllSupportedPlatforms(callback) { 12 | api.get('info/platforms/webdriver', function(platformsInfo) { 13 | var platforms = _.map(platformsInfo, function(info) { 14 | return [info['os'], info['api_name'], info['short_version']] 15 | }) 16 | platforms.sort(function(a, b) { 17 | a = a.join('-') 18 | b = b.join('-') 19 | return a < b ? -1 : b < a ? 1 : 0 20 | }) 21 | callback(null, filterUniquePlatforms(platforms)) 22 | }) 23 | } 24 | 25 | function runTests(url, platforms, callback) { 26 | var params = { maxDuration:1800, url:url, platforms:platforms, framework:'custom', recordVideo:false, recordScreenshots:false, recordLogs:true } 27 | api.post('js-tests', params, callback) 28 | } 29 | 30 | function getPlatformId(platform) { 31 | return platform.join('-') 32 | .replace('OS X', 'Mac') 33 | .replace('Windows XP', 'Windows 2003') 34 | .replace('Windows 7', 'Windows 2008') 35 | .replace('Windows 8', 'Windows 2012') 36 | } 37 | 38 | function filterUniquePlatforms(platforms) { 39 | var seen = {} 40 | return _.filter(platforms, function(platform) { 41 | var platformId = getPlatformId(platform) 42 | if (seen[platformId]) { return false } 43 | seen[platformId] = true 44 | return true 45 | }) 46 | } 47 | 48 | function runTest(url, platformSets, callback) { 49 | getPlatformsArg(platformSets, function(platforms) { 50 | var runTestsRes 51 | runTests(url, platforms, function(res) { 52 | runTestsRes = res 53 | loopCheckStatus() 54 | }) 55 | function loopCheckStatus() { 56 | getTestsStatus(runTestsRes, function(res) { 57 | var pending = [] 58 | var passed = [] 59 | var failed = [] 60 | _.each(res['js tests'], function(test) { 61 | var status = getTestStatus(test) 62 | if (status == PENDING) { pending.push(test) } 63 | else if (status == PASSED) { passed.push(test) } 64 | else if (status == FAILED) { failed.push(test) } 65 | else { throw new Error('Bad status') } 66 | }) 67 | _.each(_.flatten([passed, pending, failed]), function(test) { 68 | console.log(getTestStatus(test), test.id, test.status, test.platform) 69 | }) 70 | if (pending.length == 0) { 71 | console.log("Test suite completed") 72 | callback(checkTestResults(res)) 73 | } else if (res.completed) { 74 | throw new Error('No pending tests, but res.completed == true') 75 | } else { 76 | var delay = 5 77 | console.log("Check again in", delay, "seconds") 78 | setTimeout(loopCheckStatus, delay * 1000) 79 | } 80 | }) 81 | } 82 | }) 83 | } 84 | 85 | function getPlatformsArg(platformSets, callback) { 86 | listAllSupportedPlatforms(function(err, supportedPlatforms) { 87 | if (err) { return callback(err) } 88 | var allSupportedPlatforms = {} 89 | _.each(supportedPlatforms, function(platform) { 90 | allSupportedPlatforms[getPlatformId(platform)] = true 91 | }) 92 | 93 | var platforms = _.flatten(_.flatten(_.flatten( 94 | _.map(platformSets, function(platformSet) { 95 | return _.map(platformSet, function(browserSpecs, osName) { 96 | return _.map(browserSpecs, function(browserVersions, browserName) { 97 | if (typeof browserVersions == 'string') { 98 | browserVersions = [browserVersions] 99 | } 100 | return _.map(browserVersions, function(browserVersion) { 101 | return [osName, browserName, browserVersion] 102 | }) 103 | }) 104 | }) 105 | }) 106 | ))) 107 | 108 | _.each(platforms, function(platform) { 109 | if (!platform[2]) { return } // Don't sanity-check CURRENT_VERSION 110 | var platformId = getPlatformId(platform) 111 | if (!allSupportedPlatforms[platformId]) { 112 | throw new Error('Unsupported platform: '+platform.join(', ')+' ('+platformId+')') 113 | } 114 | }) 115 | 116 | callback(filterUniquePlatforms(platforms)) 117 | }) 118 | } 119 | 120 | function getTestsStatus(runTestsRes, callback) { 121 | api.post('js-tests/status', { 'js tests':runTestsRes['js tests'] }, function(res) { 122 | callback(res) 123 | }) 124 | } 125 | 126 | var PENDING = 'PENDING' 127 | var FAILED = 'FAILED ' 128 | var PASSED = 'PASSED ' 129 | function getTestStatus(test) { 130 | if (test.status == 'test error') { 131 | return FAILED 132 | } else if (test.result) { 133 | return (test.result.failed ? FAILED : PASSED) 134 | } else { 135 | return PENDING 136 | } 137 | } 138 | 139 | function checkTestResults(res) { 140 | var failed = 0 141 | _.each(res['js tests'], function(test) { 142 | console.log(getTestStatus(test), test.id, test.status, test.platform, test.url) 143 | if (getTestStatus(test) == FAILED) { 144 | failed += 1 145 | console.log('Result:', test.result) 146 | } 147 | }) 148 | return (failed ? failed+' tests failed' : null) 149 | } 150 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | var tinytest = require('tinytest') 2 | 3 | tinytest.hijackConsoleLog() 4 | 5 | var { createStore } = require('../src/store-engine') 6 | var { each } = require('../src/util') 7 | var storages = require('../storages/all') 8 | var allPlugins = require('../plugins/all') 9 | var allPluginTests = require('../plugins/all_tests') 10 | 11 | module.exports = { 12 | output:null, 13 | outputError:null, 14 | runTests: runTests, 15 | failed:false, 16 | } 17 | 18 | function runTests() { 19 | setupEngineTests() 20 | each(storages, function(storage) { 21 | test.group(storage.name, function() { 22 | if (!_checkEnabled(storage)) { 23 | test.skip('disabled') 24 | } 25 | test('Storage tests', function() { 26 | var store = createStore() 27 | store.addStorage(storage) 28 | runStorageTests(store) 29 | }) 30 | each(allPluginTests, function(pluginTest, pluginName) { 31 | var plugin = allPlugins[pluginName] 32 | test.group('plugin: '+pluginName, function() { 33 | var store = createStore() 34 | store.addStorage(storage) 35 | store.addPlugin(plugin) 36 | pluginTest.setup(store) 37 | }) 38 | }) 39 | }) 40 | }) 41 | 42 | tinytest.runTests({ 43 | failFast: false 44 | }) 45 | } 46 | 47 | function _checkEnabled(storage) { 48 | if (!storage) { 49 | print('Skip unsupported storage:', storage.name) 50 | return false 51 | } 52 | var store = createStore([storage], []) 53 | if (!store.enabled) { 54 | print('Skip disabled storage:', storage.name) 55 | return false 56 | } 57 | return true 58 | } 59 | 60 | function setupEngineTests(store) { 61 | test('Addon super_fn args', function() { 62 | var store = createStore(storages.memoryStorage) 63 | var calls = 0 64 | store.addPlugin(function underlying() { 65 | return { 66 | set: function(super_fn, key, val, customArg1, customArg2) { 67 | assert(key == 'key'+'appended') 68 | assert(val == 'val') 69 | assert(customArg1 == 'overridden-customArg1') 70 | assert(customArg2 == 'customArg2') 71 | calls++ 72 | } 73 | } 74 | }) 75 | store.addPlugin(function overlying() { 76 | return { 77 | set: function(super_fn, key, val) { 78 | super_fn(key+'appended', val, 'overridden-customArg1') 79 | calls++ 80 | } 81 | } 82 | }) 83 | store.set('key', 'val', 'customArg1', 'customArg2') 84 | assert(calls == 2) 85 | }) 86 | } 87 | 88 | function runStorageTests(store) { 89 | assert(store.enabled && store.enabled, "store should be enabled") 90 | store.clearAll() 91 | 92 | store.get('unsetValue') // see https://github.com/marcuswestin/store.js/issues/63 93 | 94 | store.set('foo', 'bar') 95 | assert(store.get('foo') == 'bar', "stored key 'foo' not equal to stored value 'bar'") 96 | 97 | store.remove('foo') 98 | assert(store.get('foo') === undefined, "removed key 'foo' not undefined") 99 | 100 | assert(store.get('foo') === undefined, "key 'foo' exists when it shouldn't") 101 | assert(store.set('foo','value') == 'value', "store#set returns the stored value") 102 | assert(store.get('foo') !== undefined, "key 'foo' doesn't exist when it should") 103 | 104 | store.set('foo', 'bar1') 105 | store.set('foo', 'bar2') 106 | assert(store.get('foo') == 'bar2', "key 'foo' is not equal to second value set 'bar2'") 107 | 108 | store.set('foo', 'bar') 109 | store.set('bar', 'foo') 110 | store.remove('foo') 111 | assert(store.get('foo') === undefined, "key 'foo' exists when it shouldn't") 112 | assert(store.get('bar') == 'foo', "removing key 'foo' also removed key 'bar'") 113 | 114 | store.set('foo', 'bar') 115 | store.set('bar', 'foo') 116 | store.clearAll() 117 | assert(store.get('foo') === undefined && store.get('bar') === undefined, "keys foo and bar not cleared after store cleared") 118 | 119 | assert(store.get('defaultVal', 123) == 123, "store.get should return default value") 120 | 121 | store.set('foo', { name: 'marcus', arr: [1,2,3] }) 122 | assert(typeof store.get('foo') == 'object', "type of stored object 'foo' is not 'object'") 123 | assert(store.get('foo') instanceof Object, "stored object 'foo' is not an instance of Object") 124 | assert(store.get('foo').name == 'marcus', "property 'name' of stored object 'foo' is not 'marcus'") 125 | assert(store.get('foo').arr instanceof Array, "Array property 'arr' of stored object 'foo' is not an instance of Array") 126 | assert(store.get('foo').arr.length == 3, "The length of Array property 'arr' stored on object 'foo' is not 3") 127 | 128 | store.remove('circularReference') 129 | var circularOne = {} 130 | var circularTwo = { one:circularOne } 131 | circularOne.two = circularTwo 132 | var threw = false 133 | try { store.set('circularReference', circularOne) } 134 | catch(e) { threw = true } 135 | assert(threw, "storing object with circular reference did not throw") 136 | assert(!store.get('circularReference'), "attempting to store object with circular reference which should have faile affected store state") 137 | 138 | // If plain local storage was used before store.js, we should attempt to JSON.parse them into javascript values. 139 | // Store values using vanilla localStorage, then read them out using store.js 140 | var promoteValues = { 141 | 'int' : 42, 142 | 'bool' : true, 143 | 'float' : 3.141592653, 144 | 'string' : "Don't Panic", 145 | 'odd_string' : "{ZYX'} abc:;::)))" 146 | } 147 | for (var key in promoteValues) { 148 | store._storage.resolved.write(key, promoteValues[key]) 149 | assert(store.get(key) == promoteValues[key], key+" was not correctly promoted to valid JSON") 150 | store.remove(key) 151 | } 152 | store.clearAll() 153 | var count = 0 154 | store.each(function() { 155 | count += 1 156 | }) 157 | assert(count === 0) 158 | } 159 | -------------------------------------------------------------------------------- /README-More.md: -------------------------------------------------------------------------------- 1 | How does it work? 2 | ------------------ 3 | store.js uses localStorage when available, and falls back on the userData behavior in IE6 and IE7. No flash to slow down your page load. No cookies to fatten your network requests. 4 | 5 | store.js depends on JSON for serialization to disk. 6 | 7 | 8 | Installation 9 | ------------ 10 | Just grab [store.min.js] or [store+json2.min.js] and include them with a script tag. 11 | 12 | 13 | `store.enabled` flag 14 | -------------------- 15 | If your product depends on store.js, you must check the `store.enabled` flag first: 16 | 17 | ```html 18 | 19 | 30 | ``` 31 | 32 | LocalStorage may sometimes appear to be available but throw an error when used. An example is Safari's private browsing mode. Other browsers allow the user to temporarily disable localStorage. Store.js detects these conditions and sets the `store.enabled` flag appropriately. 33 | 34 | 35 | Screencast 36 | ----------- 37 | [Introductory Screencast to Store.js](http://javascriptplayground.com/blog/2012/06/javascript-local-storage-store-js-tutorial) by Jack Franklin. 38 | 39 | 40 | Contributors & Forks 41 | -------------------- 42 | Contributors: https://github.com/marcuswestin/store.js/graphs/contributors 43 | 44 | Forks: https://github.com/marcuswestin/store.js/network/members 45 | 46 | 47 | In node.js 48 | ---------- 49 | store.js works as expected in node.js, assuming that global.localStorage has been set: 50 | 51 | ``` 52 | global.localStorage = require('localStorage') 53 | var store = require('./store') 54 | store.set('foo', 1) 55 | console.log(store.get('foo')) 56 | ``` 57 | 58 | 59 | Supported browsers 60 | ------------------ 61 | - Tested in iOS 4+ 62 | - Tested in Firefox 3.5 63 | - Tested in Firefox 3.6 64 | - Tested in Firefox 4.0+ 65 | - Support dropped for Firefox < 3.5 (see notes below) 66 | - Tested in Chrome 5 67 | - Tested in Chrome 6 68 | - Tested in Chrome 7 69 | - Tested in Chrome 8 70 | - Tested in Chrome 10 71 | - Tested in Chrome 11+ 72 | - Tested in Safari 4 73 | - Tested in Safari 5 74 | - Tested in IE6 75 | - Tested in IE7 76 | - Tested in IE8 77 | - Tested in IE9 78 | - Tested in IE10 79 | - Tested in Opera 10 80 | - Tested in Opera 11 81 | - Tested in Opera 12 82 | - Tested in Node.js v0.10.4 (with https://github.com/coolaj86/node-localStorage 1.0.2) 83 | 84 | *Private mode* Store.js may not work while browsing in private mode. This is as it should be. Check the `store.enabled` flag before relying on store.js. 85 | 86 | *Saucelabs.com rocks* Extensive browser testing of store.js is possible thanks to Saucelabs.com. Check them out, they're awesome. 87 | 88 | *Firefox 3.0 & 2.0:* Support for FF 2 & 3 was dropped in v1.3.6. If you require support for ancient versions of FF, use v1.3.5 of store.js. 89 | 90 | *Important note:* In IE6 and IE7, many special characters are not allowed in the keys used to store any key/value pair. With [@mferretti](https://github.com/mferretti)'s help, there's a suitable workaround which replaces most forbidden characters with "___". 91 | 92 | 93 | Storage limits 94 | -------------- 95 | - IE6 & IE7: 1MB total, but 128kb per "path" or "document" (see http://msdn.microsoft.com/en-us/library/ms531424(v=vs.85).aspx) 96 | - See http://dev-test.nemikor.com/web-storage/support-test/ for a list of limits per browser 97 | 98 | Unsupported browsers 99 | ------------------- 100 | - Firefox 1.0: no means (beside cookies and flash) 101 | - Safari 2: no means (beside cookies and flash) 102 | - Safari 3: no synchronous api (has asynch sqlite api, but store.js is synch) 103 | - Opera 9: don't know if there is synchronous api for storing data locally 104 | - Firefox 1.5: don't know if there is synchronous api for storing data locally 105 | - Microsoft IIS & IE7: With meta tag & "charset=iso-8859-1", things stop working. See issue #47. 106 | 107 | 108 | Some notes on serialization 109 | --------------------------- 110 | localStorage, when used without store.js, calls toString on all stored values. This means that you can't conveniently store and retrieve numbers, objects or arrays: 111 | 112 | ```js 113 | localStorage.myage = 24 114 | localStorage.myage !== 24 115 | localStorage.myage === '24' 116 | 117 | localStorage.user = { name: 'marcus', likes: 'javascript' } 118 | localStorage.user === "[object Object]" 119 | 120 | localStorage.tags = ['javascript', 'localStorage', 'store.js'] 121 | localStorage.tags.length === 32 122 | localStorage.tags === "javascript,localStorage,store.js" 123 | ``` 124 | 125 | What we want (and get with store.js) is 126 | 127 | ```js 128 | store.set('myage', 24) 129 | store.get('myage') === 24 130 | 131 | store.set('user', { name: 'marcus', likes: 'javascript' }) 132 | alert("Hi my name is " + store.get('user').name + "!") 133 | 134 | store.set('tags', ['javascript', 'localStorage', 'store.js']) 135 | alert("We've got " + store.get('tags').length + " tags here") 136 | ``` 137 | 138 | The native serialization engine of javascript is JSON. Rather than leaving it up to you to serialize and deserialize your values, store.js uses JSON.stringify() and JSON.parse() on each call to store.set() and store.get(), respectively. 139 | 140 | Some browsers do not have native support for JSON. For those browsers you should include [JSON2.js] \(non-minified copy is included in this repo). 141 | 142 | 143 | No sessionStorage/auto-expiration? 144 | ---------------------------------- 145 | No. I believe there is no way to provide sessionStorage semantics cross browser. However, it is trivial to expire values on read on top of store.js: 146 | 147 | ```js 148 | var storeWithExpiration = { 149 | set: function(key, val, exp) { 150 | store.set(key, { val:val, exp:exp, time:new Date().getTime() }) 151 | }, 152 | get: function(key) { 153 | var info = store.get(key) 154 | if (!info) { return null } 155 | if (new Date().getTime() - info.time > info.exp) { return null } 156 | return info.val 157 | } 158 | } 159 | storeWithExpiration.set('foo', 'bar', 1000) 160 | setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 500) // -> "bar" 161 | setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 1500) // -> null 162 | ``` 163 | 164 | 165 | 166 | [JSON2.js]: https://raw.githubusercontent.com/douglascrockford/JSON-js/master/json2.js 167 | [store.min.js]: https://raw.github.com/marcuswestin/store.js/master/store.min.js 168 | [store+json2.min.js]: https://raw.github.com/marcuswestin/store.js/master/store+json2.min.js 169 | -------------------------------------------------------------------------------- /dist/store.modern.min.js: -------------------------------------------------------------------------------- 1 | /* store.js - Copyright (c) 2010-2017 Marcus Westin */ 2 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.store=e()}}(function(){return function e(t,r,n){function o(a,s){if(!r[a]){if(!t[a]){var u="function"==typeof require&&require;if(!s&&u)return u(a,!0);if(i)return i(a,!0);var c=new Error("Cannot find module '"+a+"'");throw c.code="MODULE_NOT_FOUND",c}var f=r[a]={exports:{}};t[a][0].call(f.exports,function(e){var r=t[a][1][e];return o(r?r:e)},f,f.exports,e,t,r,n)}return r[a].exports}for(var i="function"==typeof require&&require,a=0;a=0;r--)if(l(t[r])){var n=t[r].split("="),o=unescape(n[0]),i=unescape(n[1]);e(i,o)}}function i(e,t){e&&(p.cookie=escape(e)+"="+escape(t)+"; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/")}function a(e){e&&u(e)&&(p.cookie=escape(e)+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/")}function s(){o(function(e,t){a(t)})}function u(e){return new RegExp("(?:^|;\\s*)"+escape(e).replace(/[\-\.\+\*]/g,"\\$&")+"\\s*\\=").test(p.cookie)}var c=e("../src/util"),f=c.Global,l=c.trim;t.exports={name:"cookieStorage",read:n,write:i,each:o,remove:a,clearAll:s};var p=f.document},{"../src/util":3}],5:[function(e,t,r){"use strict";function n(){return f.localStorage}function o(e){return n().getItem(e)}function i(e,t){return n().setItem(e,t)}function a(e){for(var t=n().length-1;t>=0;t--){var r=n().key(t);e(o(r),r)}}function s(e){return n().removeItem(e)}function u(){return n().clear()}var c=e("../src/util"),f=c.Global;t.exports={name:"localStorage",read:o,write:i,each:a,remove:s,clearAll:u}},{"../src/util":3}],6:[function(e,t,r){"use strict";function n(e){return u[e]}function o(e,t){u[e]=t}function i(e){for(var t in u)u.hasOwnProperty(t)&&e(u[t],t)}function a(e){delete u[e]}function s(e){u={}}t.exports={name:"memoryStorage",read:n,write:o,each:i,remove:a,clearAll:s};var u={}},{}],7:[function(e,t,r){"use strict";function n(){return f.sessionStorage}function o(e){return n().getItem(e)}function i(e,t){return n().setItem(e,t)}function a(e){for(var t=n().length-1;t>=0;t--){var r=n().key(t);e(o(r),r)}}function s(e){return n().removeItem(e)}function u(){return n().clear()}var c=e("../src/util"),f=c.Global;t.exports={name:"sessionStorage",read:o,write:i,each:a,remove:s,clearAll:u}},{"../src/util":3}]},{},[1])(1)}); -------------------------------------------------------------------------------- /src/store-engine.js: -------------------------------------------------------------------------------- 1 | var util = require('./util') 2 | var slice = util.slice 3 | var pluck = util.pluck 4 | var each = util.each 5 | var create = util.create 6 | var isList = util.isList 7 | var isFunction = util.isFunction 8 | var isObject = util.isObject 9 | 10 | module.exports = { 11 | createStore: createStore, 12 | } 13 | 14 | var storeAPI = { 15 | version: '2.0.3', 16 | enabled: false, 17 | 18 | // addStorage adds another storage to this store. The store 19 | // will use the first storage it receives that is enabled, so 20 | // call addStorage in the order of preferred storage. 21 | addStorage: function(storage) { 22 | if (this.enabled) { return } 23 | if (this._testStorage(storage)) { 24 | this._storage.resolved = storage 25 | this.enabled = true 26 | } 27 | }, 28 | 29 | // addPlugin will add a plugin to this store. 30 | addPlugin: function(plugin) { 31 | var self = this 32 | 33 | // If the plugin is an array, then add all plugins in the array. 34 | // This allows for a plugin to depend on other plugins. 35 | if (isList(plugin)) { 36 | each(plugin, function(plugin) { 37 | self.addPlugin(plugin) 38 | }) 39 | return 40 | } 41 | 42 | // Keep track of all plugins we've seen so far, so that we 43 | // don't add any of them twice. 44 | var seenPlugin = pluck(this._seenPlugins, function(seenPlugin) { return (plugin === seenPlugin) }) 45 | if (seenPlugin) { 46 | return 47 | } 48 | this._seenPlugins.push(plugin) 49 | 50 | // Check that the plugin is properly formed 51 | if (!isFunction(plugin)) { 52 | throw new Error('Plugins must be function values that return objects') 53 | } 54 | 55 | var pluginProperties = plugin.call(this) 56 | if (!isObject(pluginProperties)) { 57 | throw new Error('Plugins must return an object of function properties') 58 | } 59 | 60 | // Add the plugin function properties to this store instance. 61 | each(pluginProperties, function(pluginFnProp, propName) { 62 | if (!isFunction(pluginFnProp)) { 63 | throw new Error('Bad plugin property: '+propName+' from plugin '+plugin.name+'. Plugins should only return functions.') 64 | } 65 | self._assignPluginFnProp(pluginFnProp, propName) 66 | }) 67 | }, 68 | 69 | // get returns the value of the given key. If that value 70 | // is undefined, it returns optionalDefaultValue instead. 71 | get: function(key, optionalDefaultValue) { 72 | var data = this._storage().read(this._namespacePrefix + key) 73 | return this._deserialize(data, optionalDefaultValue) 74 | }, 75 | 76 | // set will store the given value at key and returns value. 77 | // Calling set with value === undefined is equivalent to calling remove. 78 | set: function(key, value) { 79 | if (value === undefined) { 80 | return this.remove(key) 81 | } 82 | this._storage().write(this._namespacePrefix + key, this._serialize(value)) 83 | return value 84 | }, 85 | 86 | // remove deletes the key and value stored at the given key. 87 | remove: function(key) { 88 | this._storage().remove(this._namespacePrefix + key) 89 | }, 90 | 91 | // each will call the given callback once for each key-value pair 92 | // in this store. 93 | each: function(callback) { 94 | var self = this 95 | this._storage().each(function(val, namespacedKey) { 96 | callback(self._deserialize(val), namespacedKey.replace(self._namespaceRegexp, '')) 97 | }) 98 | }, 99 | 100 | // clearAll will remove all the stored key-value pairs in this store. 101 | clearAll: function() { 102 | this._storage().clearAll() 103 | }, 104 | 105 | // additional functionality that can't live in plugins 106 | // --------------------------------------------------- 107 | 108 | // hasNamespace returns true if this store instance has the given namespace. 109 | hasNamespace: function(namespace) { 110 | return (this._namespacePrefix == '__storejs_'+namespace+'_') 111 | }, 112 | 113 | // namespace clones the current store and assigns it the given namespace 114 | namespace: function(namespace) { 115 | if (!this._legalNamespace.test(namespace)) { 116 | throw new Error('store.js namespaces can only have alhpanumerics + underscores and dashes') 117 | } 118 | // create a prefix that is very unlikely to collide with un-namespaced keys 119 | var namespacePrefix = '__storejs_'+namespace+'_' 120 | return create(this, { 121 | _namespacePrefix: namespacePrefix, 122 | _namespaceRegexp: namespacePrefix ? new RegExp('^'+namespacePrefix) : null 123 | }) 124 | }, 125 | 126 | // createStore creates a store.js instance with the first 127 | // functioning storage in the list of storage candidates, 128 | // and applies the the given mixins to the instance. 129 | createStore: function(storages, plugins) { 130 | return createStore(storages, plugins) 131 | }, 132 | } 133 | 134 | function createStore(storages, plugins) { 135 | var _privateStoreProps = { 136 | _seenPlugins: [], 137 | _namespacePrefix: '', 138 | _namespaceRegexp: null, 139 | _legalNamespace: /^[a-zA-Z0-9_\-]+$/, // alpha-numeric + underscore and dash 140 | 141 | _storage: function() { 142 | if (!this.enabled) { 143 | throw new Error("store.js: No supported storage has been added! "+ 144 | "Add one (e.g store.addStorage(require('store/storages/cookieStorage')) "+ 145 | "or use a build with more built-in storages (e.g "+ 146 | "https://github.com/marcuswestin/store.js/tree/master/dist/store.legacy.min.js)") 147 | } 148 | return this._storage.resolved 149 | }, 150 | 151 | _testStorage: function(storage) { 152 | try { 153 | var testStr = '__storejs__test__' 154 | storage.write(testStr, testStr) 155 | var ok = (storage.read(testStr) === testStr) 156 | storage.remove(testStr) 157 | return ok 158 | } catch(e) { 159 | return false 160 | } 161 | }, 162 | 163 | _assignPluginFnProp: function(pluginFnProp, propName) { 164 | var oldFn = this[propName] 165 | this[propName] = function pluginFn() { 166 | var args = slice(arguments, 0) 167 | var self = this 168 | 169 | // super_fn calls the old function which was overwritten by 170 | // this mixin. 171 | function super_fn() { 172 | if (!oldFn) { return } 173 | each(arguments, function(arg, i) { 174 | args[i] = arg 175 | }) 176 | return oldFn.apply(self, args) 177 | } 178 | 179 | // Give mixing function access to super_fn by prefixing all mixin function 180 | // arguments with super_fn. 181 | var newFnArgs = [super_fn].concat(args) 182 | 183 | return pluginFnProp.apply(self, newFnArgs) 184 | } 185 | }, 186 | 187 | _serialize: function(obj) { 188 | return JSON.stringify(obj) 189 | }, 190 | 191 | _deserialize: function(strVal, defaultVal) { 192 | if (!strVal) { return defaultVal } 193 | // It is possible that a raw string value has been previously stored 194 | // in a storage without using store.js, meaning it will be a raw 195 | // string value instead of a JSON serialized string. By defaulting 196 | // to the raw string value in case of a JSON parse error, we allow 197 | // for past stored values to be forwards-compatible with store.js 198 | var val = '' 199 | try { val = JSON.parse(strVal) } 200 | catch(e) { val = strVal } 201 | 202 | return (val !== undefined ? val : defaultVal) 203 | }, 204 | } 205 | 206 | var store = create(_privateStoreProps, storeAPI) 207 | each(storages, function(storage) { 208 | store.addStorage(storage) 209 | }) 210 | each(plugins, function(plugin) { 211 | store.addPlugin(plugin) 212 | }) 213 | return store 214 | } 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Store.js 2 | ======== 3 | 4 | [![Build Status](https://travis-ci.org/marcuswestin/store.js.svg?branch=master)](https://travis-ci.org/marcuswestin/store.js) 5 | [![npm version](https://badge.fury.io/js/store.svg)](https://badge.fury.io/js/store) 6 | [![npm](https://img.shields.io/npm/dm/store.svg?maxAge=2592000)]() 7 | 8 | 1. [Version 2.0](#user-content-version-20) 9 | - What's new? 10 | 2. [Basic Usage](#user-content-basic-usage) 11 | - All you need to get started 12 | - [API](#user-content-api) 13 | - [Installation](#user-content-installation) 14 | 3. [Supported Browsers](#user-content-supported-browsers) 15 | - All of them, pretty much :) 16 | - [List of supported browsers](#user-content-list-of-supported-browsers) 17 | 4. [Plugins](#user-content-plugins) 18 | - Additional common functionality 19 | - [List of all Plugins](#user-content-list-of-all-plugins) 20 | - [Using Plugins](#user-content-using-plugins) 21 | - [Write your own Plugin](#user-content-write-your-own-plugin) 22 | 5. [Builds](#user-content-builds) 23 | - Choose which build is right for you 24 | - [List of default Builds](#user-content-list-of-default-builds) 25 | - [Make your own Build](#user-content-make-your-own-build) 26 | 6. [Storages](#user-content-storages) 27 | - Storages provide underlying persistence 28 | - [List of all Storages](#user-content-list-of-all-storages) 29 | - [Storages limits](#user-content-storages-limits) 30 | - [Write your own Storage](#user-content-write-your-own-storage) 31 | 32 | 33 | Version 2.0 34 | ----------- 35 | 36 | Store.js has been around since 2010 ([first commit](https://github.com/marcuswestin/store.js/commit/cb0198c2c02ff5f17c084276eeb4f28c79849d5e)! [HN discussion](https://news.ycombinator.com/item?id=1468802)!), and is live on tens of thousands of websites - like cnn.com! 37 | 38 | For many years v1.x provided basic cross-browser persistent storage, and over time more and more people [started asking](https://github.com/marcuswestin/store.js/issues?q=is%3Aissue+is%3Aclosed) for additional functionality. 39 | 40 | Store.js version 2 is a full revamp with pluggable storage (it will automatically fall back to one that works in every scenario by default), pluggable extra functionality (like [expirations](plugins/expire.js), [default values](plugins/defaults.js), common [array/object operations](plugins/operations.js), etc), and fully cross-browser automatic testing using saucelabs.com. 41 | 42 | 43 | 44 | Basic Usage 45 | ----------- 46 | 47 | All you need to know to get started: 48 | 49 | ### API 50 | 51 | store.js exposes a simple API for cross-browser local storage: 52 | 53 | ```js 54 | // Store current user 55 | store.set('user', { name:'Marcus' }) 56 | 57 | // Get current user 58 | store.get('user') 59 | 60 | // Remove current user 61 | store.remove('user') 62 | 63 | // Clear all keys 64 | store.clearAll() 65 | 66 | // Loop over all stored values 67 | store.each(function(value, key) { 68 | console.log(key, '==', value) 69 | }) 70 | ``` 71 | 72 | ### Installation 73 | 74 | Using npm: 75 | 76 | ```js 77 | // Example store.js usage with npm 78 | var store = require('store') 79 | store.set('user', { name:'Marcus' }) 80 | store.get('user').name == 'Marcus' 81 | ``` 82 | 83 | Using script tag (first download one of the [builds](dist/)): 84 | 85 | ```html 86 | 87 | 88 | 93 | ``` 94 | 95 | 96 | 97 | Supported Browsers 98 | ------------------ 99 | 100 | All of them, pretty much :) 101 | 102 | To support all browsers (including IE 6, IE 7, Firefox 4, etc.), use `require('store')` (alias for `require('store/dist/store.legacy')`) or [store.legacy.min.js](dist/store.legacy.min.js). 103 | 104 | To save some kilobytes but still support all modern browsers, use `require('store/dist/store.modern')` or [store.modern.min.js](dist/store.modern.min.js) instead. 105 | 106 | ### List of supported browsers 107 | 108 | - Tested on IE6+ 109 | - Tested on iOS 8+ 110 | - Tested on Android 4+ 111 | - Tested on Firefox 4+ 112 | - Tested on Chrome 27+ 113 | - Tested on Safari 5+ 114 | - Tested on Opera 11+ 115 | - Tested on Node (with https://github.com/coolaj86/node-localStorage) 116 | 117 | 118 | 119 | 120 | Plugins 121 | ------- 122 | 123 | Plugins provide additional common functionality that some users might need: 124 | 125 | ### List of all Plugins 126 | 127 | - [all.js](plugins/all.js): All the plugins in one handy place. 128 | - [defaults.js](plugins/defaults.js): Declare default values. [Example usage](plugins/defaults_test.js) 129 | - [dump.js](plugins/dump.js): Dump all stored values. [Example usage](plugins/dump_test.js) 130 | - [events.js](plugins/events.js): Get notified when stored values change. [Example usage](plugins/events_test.js) 131 | - [expire.js](plugins/expire.js): Expire stored values after a given time. [Example usage](plugins/expire_test.js) 132 | - [observe.js](plugins/observe.js): Observe stored values and their changes. [Example usage](plugins/observe_test.js) 133 | - [operations.js](plugins/operations.js): Useful operations like push, shift & assign. [Example usage](plugins/operations_test.js) 134 | - [update.js](plugins/update.js): Update a stored object, or create it if null. [Example usage](plugins/update_test.js) 135 | - [v1-backcompat.js](plugins/v1-backcompat.js): Full backwards compatibility with store.js v1. [Example usage](plugins/v1-backcompat_test.js) 136 | 137 | ### Using Plugins 138 | 139 | With npm: 140 | 141 | ```js 142 | // Example plugin usage: 143 | var expirePlugin = require('store/plugins/expire') 144 | store.addPlugin(expirePlugin) 145 | ``` 146 | 147 | If you're using script tags, you can either use [store.everything.min.js](dist/store.everything.min.js) (which 148 | has all plugins built-in), or clone this repo to add or modify a build and run `make build`. 149 | 150 | ### Write your own plugin 151 | 152 | A store.js plugin is a function that returns an object that gets added to the store. 153 | If any of the plugin functions overrides existing functions, the plugin function can still call 154 | the original function using the first argument (super_fn). 155 | 156 | ```js 157 | // Example plugin that stores a version history of every value 158 | var versionHistoryPlugin = function() { 159 | var historyStore = this.namespace('history') 160 | return { 161 | set: function(super_fn, key, value) { 162 | var history = historyStore.get(key) || [] 163 | history.push(value) 164 | historyStore.set(key, history) 165 | return super_fn() 166 | }, 167 | getHistory: function(key) { 168 | return historyStore.get(key) 169 | } 170 | } 171 | } 172 | store.addPlugin(versionHistoryPlugin) 173 | store.set('foo', 'bar 1') 174 | store.set('foo', 'bar 2') 175 | store.getHistory('foo') == ['bar 1', 'bar 2'] 176 | ``` 177 | 178 | Let me know if you need more info on writing plugins. For the moment I recommend 179 | taking a look at the [current plugins](plugins/). Good example plugins are 180 | [plugins/defaults](plugins/defaults.js), [plugins/expire](plugins/expire.js) and 181 | [plugins/events](plugins/events.js). 182 | 183 | 184 | 185 | Builds 186 | ------ 187 | 188 | Choose which build is right for you! 189 | 190 | ### List of default builds 191 | 192 | - [store.everything.min.js](dist/store.everything.min.js): All the plugins, all the storages. [Source](dist/store.everything.js) 193 | - [store.legacy.min.js](dist/store.legacy.min.js): Full support for all tested browsers. Add plugins separately. [Source](dist/store.legacy.js) 194 | - [store.modern.min.js](dist/store.modern.min.js): Full support for all modern browsers. Add plugins separately. [Source](dist/store.modern.js) 195 | - [store.v1-backcompat.min.js](dist/store.v1-backcompat.min.js): Full backwards compatibility with [store.js v1](https://github.com/marcuswestin/store.js/releases/tag/v1.3.20). [Source](dist/store.v1-backcompat.js) 196 | 197 | ### Make your own Build 198 | 199 | If you're using npm you can create your own build: 200 | 201 | ```js 202 | // Example custom build usage: 203 | var engine = require('store/src/store-engine') 204 | var storages = [ 205 | require('store/storages/localStorage'), 206 | require('store/storages/cookieStorage') 207 | ] 208 | var plugins = [ 209 | require('store/plugins/defaults'), 210 | require('store/plugins/expire') 211 | ] 212 | var store = engine.createStore(storages, plugins) 213 | store.set('foo', 'bar', new Date().getTime() + 3000) // Using expire plugin to expire in 3 seconds 214 | ``` 215 | 216 | 217 | 218 | 219 | Storages 220 | -------- 221 | Store.js will pick the best available storage, and automatically falls back to the first available storage that works: 222 | 223 | ### List of all Storages 224 | 225 | - [all.js](storages/all.js) All the storages in one handy place. 226 | - [localStorage.js](storages/localStorage.js) Store values in localStorage. Great for all modern browsers. 227 | - [sessionStorage.js](storages/sessionStorage.js) Store values in sessionStorage. 228 | - [cookieStorage.js](storages/cookieStorage.js) Store values in cookies. Useful for Safari Private mode. 229 | - [memoryStorage.js](storages/memoryStorage.js) Store values in memory. Great fallback to ensure store functionality at all times. 230 | - [oldFF-globalStorage.js](storages/oldFF-globalStorage.js) Store values in globalStorage. Only useful for legacy Firefox 3+. 231 | - [oldIE-userDataStorage.js](storages/oldIE-userDataStorage.js) Store values in userData. Only useful for legacy IE 6+. 232 | 233 | 234 | ### Storages limits 235 | 236 | Each storage has different limits, restrictions and overflow behavior on different browser. For example, Android has has a 4.57M localStorage limit in 4.0, a 2.49M limit in 4.1, and a 4.98M limit in 4.2... Yeah. 237 | 238 | To simplify things we provide these recommendations to ensure cross browser behavior: 239 | 240 | | Storage | Targets | Recommendations | More info | 241 | |:----------------|:-----------------------|:--------------------------------|:-------------------------------------------------| 242 | | all | All browsers | Store < 1 million characters | (Except Safari Private mode) | 243 | | all | All & Private mode | Store < 32 thousand characters | (Including Safari Private mode) | 244 | | localStorage | Modern browsers | Max 2mb (~1M chars) | [limits][local-limits], [android][local-android] | 245 | | sessionStorage | Modern browsers | Max 5mb (~2M chars) | [limits][session-limits] | 246 | | cookieStorage | Safari Private mode | Max 4kb (~2K chars) | [limits][cookie-limits] | 247 | | userDataStorage | IE5, IE6 & IE7 | Max 64kb (~32K chars) | [limits][userdata-limits] | 248 | | globalStorage | Firefox 2-5 | Max 5mb (~2M chars) | [limits][global-limits] | 249 | | memoryStorage | All browsers, fallback | Does not persist across pages! | | 250 | 251 | [local-limits]: https://arty.name/localstorage.html 252 | [local-android]: http://dev-test.nemikor.com/web-storage/support-test/ 253 | [session-limits]: http://stackoverflow.com/questions/15840976/how-large-is-html5-session-storage 254 | [cookie-limits]: http://browsercookielimits.squawky.net/ 255 | [userdata-limits]: https://msdn.microsoft.com/en-us/library/ms533015(v=vs.85).aspx 256 | [global-limits]: https://github.com/jeremydurham/persist-js/blob/master/README.md#4-size-limits 257 | [more]: https://www.html5rocks.com/en/tutorials/offline/quota-research/ 258 | 259 | 260 | ### Write your own Storage 261 | 262 | Chances are you won't ever need another storage. But if you do... 263 | 264 | See [storages/](storages/) for examples. Two good examples are [memoryStorage](storages/memoryStorage.js) and [localStorage](storages/localStorage.js). 265 | 266 | Basically, you just need an object that looks like this: 267 | 268 | ```js 269 | // Example custom storage 270 | var storage = { 271 | name: 'myStorage', 272 | read: function(key) { ... }, 273 | write: function(key, value) { ... }, 274 | each: function(fn) { ... }, 275 | remove: function(key) { ... }, 276 | clearAll: function() { ... } 277 | } 278 | store.addStorage(storage) 279 | ``` 280 | -------------------------------------------------------------------------------- /dist/store.legacy.min.js: -------------------------------------------------------------------------------- 1 | /* store.js - Copyright (c) 2010-2017 Marcus Westin */ 2 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.store=e()}}(function(){var define,module,exports;return function e(t,r,n){function o(u,a){if(!r[u]){if(!t[u]){var s="function"==typeof require&&require;if(!a&&s)return s(u,!0);if(i)return i(u,!0);var c=new Error("Cannot find module '"+u+"'");throw c.code="MODULE_NOT_FOUND",c}var f=r[u]={exports:{}};t[u][0].call(f.exports,function(e){var r=t[u][1][e];return o(r?r:e)},f,f.exports,e,t,r,n)}return r[u].exports}for(var i="function"==typeof require&&require,u=0;u=0;r--)if(l(t[r])){var n=t[r].split("="),o=unescape(n[0]),i=unescape(n[1]);e(i,o)}}function i(e,t){e&&(p.cookie=escape(e)+"="+escape(t)+"; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/")}function u(e){e&&s(e)&&(p.cookie=escape(e)+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/")}function a(){o(function(e,t){u(t)})}function s(e){return new RegExp("(?:^|;\\s*)"+escape(e).replace(/[\-\.\+\*]/g,"\\$&")+"\\s*\\=").test(p.cookie)}var c=e("../src/util"),f=c.Global,l=c.trim;t.exports={name:"cookieStorage",read:n,write:i,each:o,remove:u,clearAll:a};var p=f.document},{"../src/util":5}],8:[function(e,t,r){"use strict";function n(){return f.localStorage}function o(e){return n().getItem(e)}function i(e,t){return n().setItem(e,t)}function u(e){for(var t=n().length-1;t>=0;t--){var r=n().key(t);e(o(r),r)}}function a(e){return n().removeItem(e)}function s(){return n().clear()}var c=e("../src/util"),f=c.Global;t.exports={name:"localStorage",read:o,write:i,each:u,remove:a,clearAll:s}},{"../src/util":5}],9:[function(e,t,r){"use strict";function n(e){return s[e]}function o(e,t){s[e]=t}function i(e){for(var t in s)s.hasOwnProperty(t)&&e(s[t],t)}function u(e){delete s[e]}function a(e){s={}}t.exports={name:"memoryStorage",read:n,write:o,each:i,remove:u,clearAll:a};var s={}},{}],10:[function(e,t,r){"use strict";function n(e){return f[e]}function o(e,t){f[e]=t}function i(e){for(var t=f.length-1;t>=0;t--){var r=f.key(t);e(f[r],r)}}function u(e){return f.removeItem(e)}function a(){i(function(e,t){delete f[e]})}var s=e("../src/util"),c=s.Global;t.exports={name:"oldFF-globalStorage",read:n,write:o,each:i,remove:u,clearAll:a};var f=c.globalStorage},{"../src/util":5}],11:[function(e,t,r){"use strict";function n(e,t){if(!v){var r=s(e);d(function(e){e.setAttribute(r,t),e.save(p)})}}function o(e){if(!v){var t=s(e),r=null;return d(function(e){r=e.getAttribute(t)}),r}}function i(e){d(function(t){for(var r=t.XMLDocument.documentElement.attributes,n=r.length-1;n>=0;n--){var o=r[n];e(t.getAttribute(o.name),o.name)}})}function u(e){var t=s(e);d(function(e){e.removeAttribute(t),e.save(p)})}function a(){d(function(e){var t=e.XMLDocument.documentElement.attributes;e.load(p);for(var r=t.length-1;r>=0;r--)e.removeAttribute(t[r].name);e.save(p)})}function s(e){return e.replace(/^d/,"___$&").replace(h,"___")}function c(){if(!g||!g.documentElement||!g.documentElement.addBehavior)return null;var e,t,r,n="script";try{t=new ActiveXObject("htmlfile"),t.open(),t.write("<"+n+">document.w=window'),t.close(),e=t.w.frames[0].document,r=e.createElement("div")}catch(o){r=g.createElement("div"),e=g.body}return function(t){var n=[].slice.call(arguments,0);n.unshift(r),e.appendChild(r),r.addBehavior("#default#userData"),r.load(p),t.apply(this,n),e.removeChild(r)}}var f=e("../src/util"),l=f.Global;t.exports={name:"oldIE-userDataStorage",write:n,read:o,each:i,remove:u,clearAll:a};var p="storejs",g=l.document,d=c(),v=(l.navigator?l.navigator.userAgent:"").match(/ (MSIE 8|MSIE 9|MSIE 10)\./),h=new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]","g")},{"../src/util":5}],12:[function(e,t,r){"use strict";function n(){return f.sessionStorage}function o(e){return n().getItem(e)}function i(e,t){return n().setItem(e,t)}function u(e){for(var t=n().length-1;t>=0;t--){var r=n().key(t);e(o(r),r)}}function a(e){return n().removeItem(e)}function s(){return n().clear()}var c=e("../src/util"),f=c.Global;t.exports={name:"sessionStorage",read:o,write:i,each:u,remove:a,clearAll:s}},{"../src/util":5}]},{},[1])(1)}); -------------------------------------------------------------------------------- /dist/store.v1-backcompat.min.js: -------------------------------------------------------------------------------- 1 | /* store.js - Copyright (c) 2010-2017 Marcus Westin */ 2 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.store=e()}}(function(){var define,module,exports;return function e(t,r,n){function o(u,a){if(!r[u]){if(!t[u]){var s="function"==typeof require&&require;if(!a&&s)return s(u,!0);if(i)return i(u,!0);var c=new Error("Cannot find module '"+u+"'");throw c.code="MODULE_NOT_FOUND",c}var f=r[u]={exports:{}};t[u][0].call(f.exports,function(e){var r=t[u][1][e];return o(r?r:e)},f,f.exports,e,t,r,n)}return r[u].exports}for(var i="function"==typeof require&&require,u=0;u=0;r--)if(l(t[r])){var n=t[r].split("="),o=unescape(n[0]),i=unescape(n[1]);e(i,o)}}function i(e,t){e&&(p.cookie=escape(e)+"="+escape(t)+"; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/")}function u(e){e&&s(e)&&(p.cookie=escape(e)+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/")}function a(){o(function(e,t){u(t)})}function s(e){return new RegExp("(?:^|;\\s*)"+escape(e).replace(/[\-\.\+\*]/g,"\\$&")+"\\s*\\=").test(p.cookie)}var c=e("../src/util"),f=c.Global,l=c.trim;t.exports={name:"cookieStorage",read:n,write:i,each:o,remove:u,clearAll:a};var p=f.document},{"../src/util":7}],10:[function(e,t,r){"use strict";function n(){return f.localStorage}function o(e){return n().getItem(e)}function i(e,t){return n().setItem(e,t)}function u(e){for(var t=n().length-1;t>=0;t--){var r=n().key(t);e(o(r),r)}}function a(e){return n().removeItem(e)}function s(){return n().clear()}var c=e("../src/util"),f=c.Global;t.exports={name:"localStorage",read:o,write:i,each:u,remove:a,clearAll:s}},{"../src/util":7}],11:[function(e,t,r){"use strict";function n(e){return s[e]}function o(e,t){s[e]=t}function i(e){for(var t in s)s.hasOwnProperty(t)&&e(s[t],t)}function u(e){delete s[e]}function a(e){s={}}t.exports={name:"memoryStorage",read:n,write:o,each:i,remove:u,clearAll:a};var s={}},{}],12:[function(e,t,r){"use strict";function n(e){return f[e]}function o(e,t){f[e]=t}function i(e){for(var t=f.length-1;t>=0;t--){var r=f.key(t);e(f[r],r)}}function u(e){return f.removeItem(e)}function a(){i(function(e,t){delete f[e]})}var s=e("../src/util"),c=s.Global;t.exports={name:"oldFF-globalStorage",read:n,write:o,each:i,remove:u,clearAll:a};var f=c.globalStorage},{"../src/util":7}],13:[function(e,t,r){"use strict";function n(e,t){if(!h){var r=s(e);d(function(e){e.setAttribute(r,t),e.save(p)})}}function o(e){if(!h){var t=s(e),r=null;return d(function(e){r=e.getAttribute(t)}),r}}function i(e){d(function(t){for(var r=t.XMLDocument.documentElement.attributes,n=r.length-1;n>=0;n--){var o=r[n];e(t.getAttribute(o.name),o.name)}})}function u(e){var t=s(e);d(function(e){e.removeAttribute(t),e.save(p)})}function a(){d(function(e){var t=e.XMLDocument.documentElement.attributes;e.load(p);for(var r=t.length-1;r>=0;r--)e.removeAttribute(t[r].name);e.save(p)})}function s(e){return e.replace(/^d/,"___$&").replace(v,"___")}function c(){if(!g||!g.documentElement||!g.documentElement.addBehavior)return null;var e,t,r,n="script";try{t=new ActiveXObject("htmlfile"),t.open(),t.write("<"+n+">document.w=window'),t.close(),e=t.w.frames[0].document,r=e.createElement("div")}catch(o){r=g.createElement("div"),e=g.body}return function(t){var n=[].slice.call(arguments,0);n.unshift(r),e.appendChild(r),r.addBehavior("#default#userData"),r.load(p),t.apply(this,n),e.removeChild(r)}}var f=e("../src/util"),l=f.Global;t.exports={name:"oldIE-userDataStorage",write:n,read:o,each:i,remove:u,clearAll:a};var p="storejs",g=l.document,d=c(),h=(l.navigator?l.navigator.userAgent:"").match(/ (MSIE 8|MSIE 9|MSIE 10)\./),v=new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]","g")},{"../src/util":7}],14:[function(e,t,r){"use strict";function n(){return f.sessionStorage}function o(e){return n().getItem(e)}function i(e,t){return n().setItem(e,t)}function u(e){for(var t=n().length-1;t>=0;t--){var r=n().key(t);e(o(r),r)}}function a(e){return n().removeItem(e)}function s(){return n().clear()}var c=e("../src/util"),f=c.Global;t.exports={name:"sessionStorage",read:o,write:i,each:u,remove:a,clearAll:s}},{"../src/util":7}]},{},[1])(1)}); -------------------------------------------------------------------------------- /dist/store.everything.min.js: -------------------------------------------------------------------------------- 1 | /* store.js - Copyright (c) 2010-2017 Marcus Westin */ 2 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.store=e()}}(function(){var define,module,exports;return function e(t,n,r){function o(u,a){if(!n[u]){if(!t[u]){var s="function"==typeof require&&require;if(!a&&s)return s(u,!0);if(i)return i(u,!0);var c=new Error("Cannot find module '"+u+"'");throw c.code="MODULE_NOT_FOUND",c}var f=n[u]={exports:{}};t[u][0].call(f.exports,function(e){var n=t[u][1][e];return o(n?n:e)},f,f.exports,e,t,n,r)}return n[u].exports}for(var i="function"==typeof require&&require,u=0;u=0;n--)if(l(t[n])){var r=t[n].split("="),o=unescape(r[0]),i=unescape(r[1]);e(i,o)}}function i(e,t){e&&(p.cookie=escape(e)+"="+escape(t)+"; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/")}function u(e){e&&s(e)&&(p.cookie=escape(e)+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/")}function a(){o(function(e,t){u(t)})}function s(e){return new RegExp("(?:^|;\\s*)"+escape(e).replace(/[\-\.\+\*]/g,"\\$&")+"\\s*\\=").test(p.cookie)}var c=e("../src/util"),f=c.Global,l=c.trim;t.exports={name:"cookieStorage",read:r,write:i,each:o,remove:u,clearAll:a};var p=f.document},{"../src/util":14}],17:[function(e,t,n){"use strict";function r(){return f.localStorage}function o(e){return r().getItem(e)}function i(e,t){return r().setItem(e,t)}function u(e){for(var t=r().length-1;t>=0;t--){var n=r().key(t);e(o(n),n)}}function a(e){return r().removeItem(e)}function s(){return r().clear()}var c=e("../src/util"),f=c.Global;t.exports={name:"localStorage",read:o,write:i,each:u,remove:a,clearAll:s}},{"../src/util":14}],18:[function(e,t,n){"use strict";function r(e){return s[e]}function o(e,t){s[e]=t}function i(e){for(var t in s)s.hasOwnProperty(t)&&e(s[t],t)}function u(e){delete s[e]}function a(e){s={}}t.exports={name:"memoryStorage",read:r,write:o,each:i,remove:u,clearAll:a};var s={}},{}],19:[function(e,t,n){"use strict";function r(e){return f[e]}function o(e,t){f[e]=t}function i(e){for(var t=f.length-1;t>=0;t--){var n=f.key(t);e(f[n],n)}}function u(e){return f.removeItem(e)}function a(){i(function(e,t){delete f[e]})}var s=e("../src/util"),c=s.Global;t.exports={name:"oldFF-globalStorage",read:r,write:o,each:i,remove:u,clearAll:a};var f=c.globalStorage},{"../src/util":14}],20:[function(e,t,n){"use strict";function r(e,t){if(!h){var n=s(e);g(function(e){e.setAttribute(n,t),e.save(p)})}}function o(e){if(!h){var t=s(e),n=null;return g(function(e){n=e.getAttribute(t)}),n}}function i(e){g(function(t){for(var n=t.XMLDocument.documentElement.attributes,r=n.length-1;r>=0;r--){var o=n[r];e(t.getAttribute(o.name),o.name)}})}function u(e){var t=s(e);g(function(e){e.removeAttribute(t),e.save(p)})}function a(){g(function(e){var t=e.XMLDocument.documentElement.attributes;e.load(p);for(var n=t.length-1;n>=0;n--)e.removeAttribute(t[n].name);e.save(p)})}function s(e){return e.replace(/^d/,"___$&").replace(v,"___")}function c(){if(!d||!d.documentElement||!d.documentElement.addBehavior)return null;var e,t,n,r="script";try{t=new ActiveXObject("htmlfile"),t.open(),t.write("<"+r+">document.w=window'),t.close(),e=t.w.frames[0].document,n=e.createElement("div")}catch(o){n=d.createElement("div"),e=d.body}return function(t){var r=[].slice.call(arguments,0);r.unshift(n),e.appendChild(n),n.addBehavior("#default#userData"),n.load(p),t.apply(this,r),e.removeChild(n)}}var f=e("../src/util"),l=f.Global;t.exports={name:"oldIE-userDataStorage",write:r,read:o,each:i,remove:u,clearAll:a};var p="storejs",d=l.document,g=c(),h=(l.navigator?l.navigator.userAgent:"").match(/ (MSIE 8|MSIE 9|MSIE 10)\./),v=new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]","g")},{"../src/util":14}],21:[function(e,t,n){"use strict";function r(){return f.sessionStorage}function o(e){return r().getItem(e)}function i(e,t){return r().setItem(e,t)}function u(e){for(var t=r().length-1;t>=0;t--){var n=r().key(t);e(o(n),n)}}function a(e){return r().removeItem(e)}function s(){return r().clear()}var c=e("../src/util"),f=c.Global;t.exports={name:"sessionStorage",read:o,write:i,each:u,remove:a,clearAll:s}},{"../src/util":14}]},{},[1])(1)}); -------------------------------------------------------------------------------- /plugins/lib/json2.js: -------------------------------------------------------------------------------- 1 | // json2.js 2 | // 2016-10-28 3 | // Public Domain. 4 | // NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 5 | // See http://www.JSON.org/js.html 6 | // This code should be minified before deployment. 7 | // See http://javascript.crockford.com/jsmin.html 8 | 9 | // USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 10 | // NOT CONTROL. 11 | 12 | // This file creates a global JSON object containing two methods: stringify 13 | // and parse. This file provides the ES5 JSON capability to ES3 systems. 14 | // If a project might run on IE8 or earlier, then this file should be included. 15 | // This file does nothing on ES5 systems. 16 | 17 | // JSON.stringify(value, replacer, space) 18 | // value any JavaScript value, usually an object or array. 19 | // replacer an optional parameter that determines how object 20 | // values are stringified for objects. It can be a 21 | // function or an array of strings. 22 | // space an optional parameter that specifies the indentation 23 | // of nested structures. If it is omitted, the text will 24 | // be packed without extra whitespace. If it is a number, 25 | // it will specify the number of spaces to indent at each 26 | // level. If it is a string (such as "\t" or " "), 27 | // it contains the characters used to indent at each level. 28 | // This method produces a JSON text from a JavaScript value. 29 | // When an object value is found, if the object contains a toJSON 30 | // method, its toJSON method will be called and the result will be 31 | // stringified. A toJSON method does not serialize: it returns the 32 | // value represented by the name/value pair that should be serialized, 33 | // or undefined if nothing should be serialized. The toJSON method 34 | // will be passed the key associated with the value, and this will be 35 | // bound to the value. 36 | 37 | // For example, this would serialize Dates as ISO strings. 38 | 39 | // Date.prototype.toJSON = function (key) { 40 | // function f(n) { 41 | // // Format integers to have at least two digits. 42 | // return (n < 10) 43 | // ? "0" + n 44 | // : n; 45 | // } 46 | // return this.getUTCFullYear() + "-" + 47 | // f(this.getUTCMonth() + 1) + "-" + 48 | // f(this.getUTCDate()) + "T" + 49 | // f(this.getUTCHours()) + ":" + 50 | // f(this.getUTCMinutes()) + ":" + 51 | // f(this.getUTCSeconds()) + "Z"; 52 | // }; 53 | 54 | // You can provide an optional replacer method. It will be passed the 55 | // key and value of each member, with this bound to the containing 56 | // object. The value that is returned from your method will be 57 | // serialized. If your method returns undefined, then the member will 58 | // be excluded from the serialization. 59 | 60 | // If the replacer parameter is an array of strings, then it will be 61 | // used to select the members to be serialized. It filters the results 62 | // such that only members with keys listed in the replacer array are 63 | // stringified. 64 | 65 | // Values that do not have JSON representations, such as undefined or 66 | // functions, will not be serialized. Such values in objects will be 67 | // dropped; in arrays they will be replaced with null. You can use 68 | // a replacer function to replace those with JSON values. 69 | 70 | // JSON.stringify(undefined) returns undefined. 71 | 72 | // The optional space parameter produces a stringification of the 73 | // value that is filled with line breaks and indentation to make it 74 | // easier to read. 75 | 76 | // If the space parameter is a non-empty string, then that string will 77 | // be used for indentation. If the space parameter is a number, then 78 | // the indentation will be that many spaces. 79 | 80 | // Example: 81 | 82 | // text = JSON.stringify(["e", {pluribus: "unum"}]); 83 | // // text is '["e",{"pluribus":"unum"}]' 84 | 85 | // text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t"); 86 | // // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 87 | 88 | // text = JSON.stringify([new Date()], function (key, value) { 89 | // return this[key] instanceof Date 90 | // ? "Date(" + this[key] + ")" 91 | // : value; 92 | // }); 93 | // // text is '["Date(---current time---)"]' 94 | 95 | // JSON.parse(text, reviver) 96 | // This method parses a JSON text to produce an object or array. 97 | // It can throw a SyntaxError exception. 98 | 99 | // The optional reviver parameter is a function that can filter and 100 | // transform the results. It receives each of the keys and values, 101 | // and its return value is used instead of the original value. 102 | // If it returns what it received, then the structure is not modified. 103 | // If it returns undefined then the member is deleted. 104 | 105 | // Example: 106 | 107 | // // Parse the text. Values that look like ISO date strings will 108 | // // be converted to Date objects. 109 | 110 | // myData = JSON.parse(text, function (key, value) { 111 | // var a; 112 | // if (typeof value === "string") { 113 | // a = 114 | // /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 115 | // if (a) { 116 | // return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 117 | // +a[5], +a[6])); 118 | // } 119 | // } 120 | // return value; 121 | // }); 122 | 123 | // myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 124 | // var d; 125 | // if (typeof value === "string" && 126 | // value.slice(0, 5) === "Date(" && 127 | // value.slice(-1) === ")") { 128 | // d = new Date(value.slice(5, -1)); 129 | // if (d) { 130 | // return d; 131 | // } 132 | // } 133 | // return value; 134 | // }); 135 | 136 | // This is a reference implementation. You are free to copy, modify, or 137 | // redistribute. 138 | 139 | /*jslint 140 | eval, for, this 141 | */ 142 | 143 | /*property 144 | JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 145 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 146 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 147 | test, toJSON, toString, valueOf 148 | */ 149 | 150 | 151 | // Create a JSON object only if one does not already exist. We create the 152 | // methods in a closure to avoid creating global variables. 153 | 154 | if (typeof JSON !== "object") { 155 | JSON = {}; 156 | } 157 | 158 | (function () { 159 | "use strict"; 160 | 161 | var rx_one = /^[\],:{}\s]*$/; 162 | var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; 163 | var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; 164 | var rx_four = /(?:^|:|,)(?:\s*\[)+/g; 165 | var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 166 | var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 167 | 168 | function f(n) { 169 | // Format integers to have at least two digits. 170 | return n < 10 171 | ? "0" + n 172 | : n; 173 | } 174 | 175 | function this_value() { 176 | return this.valueOf(); 177 | } 178 | 179 | if (typeof Date.prototype.toJSON !== "function") { 180 | 181 | Date.prototype.toJSON = function () { 182 | 183 | return isFinite(this.valueOf()) 184 | ? this.getUTCFullYear() + "-" + 185 | f(this.getUTCMonth() + 1) + "-" + 186 | f(this.getUTCDate()) + "T" + 187 | f(this.getUTCHours()) + ":" + 188 | f(this.getUTCMinutes()) + ":" + 189 | f(this.getUTCSeconds()) + "Z" 190 | : null; 191 | }; 192 | 193 | Boolean.prototype.toJSON = this_value; 194 | Number.prototype.toJSON = this_value; 195 | String.prototype.toJSON = this_value; 196 | } 197 | 198 | var gap; 199 | var indent; 200 | var meta; 201 | var rep; 202 | 203 | 204 | function quote(string) { 205 | 206 | // If the string contains no control characters, no quote characters, and no 207 | // backslash characters, then we can safely slap some quotes around it. 208 | // Otherwise we must also replace the offending characters with safe escape 209 | // sequences. 210 | 211 | rx_escapable.lastIndex = 0; 212 | return rx_escapable.test(string) 213 | ? "\"" + string.replace(rx_escapable, function (a) { 214 | var c = meta[a]; 215 | return typeof c === "string" 216 | ? c 217 | : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); 218 | }) + "\"" 219 | : "\"" + string + "\""; 220 | } 221 | 222 | 223 | function str(key, holder) { 224 | 225 | // Produce a string from holder[key]. 226 | 227 | var i; // The loop counter. 228 | var k; // The member key. 229 | var v; // The member value. 230 | var length; 231 | var mind = gap; 232 | var partial; 233 | var value = holder[key]; 234 | 235 | // If the value has a toJSON method, call it to obtain a replacement value. 236 | 237 | if (value && typeof value === "object" && 238 | typeof value.toJSON === "function") { 239 | value = value.toJSON(key); 240 | } 241 | 242 | // If we were called with a replacer function, then call the replacer to 243 | // obtain a replacement value. 244 | 245 | if (typeof rep === "function") { 246 | value = rep.call(holder, key, value); 247 | } 248 | 249 | // What happens next depends on the value's type. 250 | 251 | switch (typeof value) { 252 | case "string": 253 | return quote(value); 254 | 255 | case "number": 256 | 257 | // JSON numbers must be finite. Encode non-finite numbers as null. 258 | 259 | return isFinite(value) 260 | ? String(value) 261 | : "null"; 262 | 263 | case "boolean": 264 | case "null": 265 | 266 | // If the value is a boolean or null, convert it to a string. Note: 267 | // typeof null does not produce "null". The case is included here in 268 | // the remote chance that this gets fixed someday. 269 | 270 | return String(value); 271 | 272 | // If the type is "object", we might be dealing with an object or an array or 273 | // null. 274 | 275 | case "object": 276 | 277 | // Due to a specification blunder in ECMAScript, typeof null is "object", 278 | // so watch out for that case. 279 | 280 | if (!value) { 281 | return "null"; 282 | } 283 | 284 | // Make an array to hold the partial results of stringifying this object value. 285 | 286 | gap += indent; 287 | partial = []; 288 | 289 | // Is the value an array? 290 | 291 | if (Object.prototype.toString.apply(value) === "[object Array]") { 292 | 293 | // The value is an array. Stringify every element. Use null as a placeholder 294 | // for non-JSON values. 295 | 296 | length = value.length; 297 | for (i = 0; i < length; i += 1) { 298 | partial[i] = str(i, value) || "null"; 299 | } 300 | 301 | // Join all of the elements together, separated with commas, and wrap them in 302 | // brackets. 303 | 304 | v = partial.length === 0 305 | ? "[]" 306 | : gap 307 | ? "[\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "]" 308 | : "[" + partial.join(",") + "]"; 309 | gap = mind; 310 | return v; 311 | } 312 | 313 | // If the replacer is an array, use it to select the members to be stringified. 314 | 315 | if (rep && typeof rep === "object") { 316 | length = rep.length; 317 | for (i = 0; i < length; i += 1) { 318 | if (typeof rep[i] === "string") { 319 | k = rep[i]; 320 | v = str(k, value); 321 | if (v) { 322 | partial.push(quote(k) + ( 323 | gap 324 | ? ": " 325 | : ":" 326 | ) + v); 327 | } 328 | } 329 | } 330 | } else { 331 | 332 | // Otherwise, iterate through all of the keys in the object. 333 | 334 | for (k in value) { 335 | if (Object.prototype.hasOwnProperty.call(value, k)) { 336 | v = str(k, value); 337 | if (v) { 338 | partial.push(quote(k) + ( 339 | gap 340 | ? ": " 341 | : ":" 342 | ) + v); 343 | } 344 | } 345 | } 346 | } 347 | 348 | // Join all of the member texts together, separated with commas, 349 | // and wrap them in braces. 350 | 351 | v = partial.length === 0 352 | ? "{}" 353 | : gap 354 | ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}" 355 | : "{" + partial.join(",") + "}"; 356 | gap = mind; 357 | return v; 358 | } 359 | } 360 | 361 | // If the JSON object does not yet have a stringify method, give it one. 362 | 363 | if (typeof JSON.stringify !== "function") { 364 | meta = { // table of character substitutions 365 | "\b": "\\b", 366 | "\t": "\\t", 367 | "\n": "\\n", 368 | "\f": "\\f", 369 | "\r": "\\r", 370 | "\"": "\\\"", 371 | "\\": "\\\\" 372 | }; 373 | JSON.stringify = function (value, replacer, space) { 374 | 375 | // The stringify method takes a value and an optional replacer, and an optional 376 | // space parameter, and returns a JSON text. The replacer can be a function 377 | // that can replace values, or an array of strings that will select the keys. 378 | // A default replacer method can be provided. Use of the space parameter can 379 | // produce text that is more easily readable. 380 | 381 | var i; 382 | gap = ""; 383 | indent = ""; 384 | 385 | // If the space parameter is a number, make an indent string containing that 386 | // many spaces. 387 | 388 | if (typeof space === "number") { 389 | for (i = 0; i < space; i += 1) { 390 | indent += " "; 391 | } 392 | 393 | // If the space parameter is a string, it will be used as the indent string. 394 | 395 | } else if (typeof space === "string") { 396 | indent = space; 397 | } 398 | 399 | // If there is a replacer, it must be a function or an array. 400 | // Otherwise, throw an error. 401 | 402 | rep = replacer; 403 | if (replacer && typeof replacer !== "function" && 404 | (typeof replacer !== "object" || 405 | typeof replacer.length !== "number")) { 406 | throw new Error("JSON.stringify"); 407 | } 408 | 409 | // Make a fake root object containing our value under the key of "". 410 | // Return the result of stringifying the value. 411 | 412 | return str("", {"": value}); 413 | }; 414 | } 415 | 416 | 417 | // If the JSON object does not yet have a parse method, give it one. 418 | 419 | if (typeof JSON.parse !== "function") { 420 | JSON.parse = function (text, reviver) { 421 | 422 | // The parse method takes a text and an optional reviver function, and returns 423 | // a JavaScript value if the text is a valid JSON text. 424 | 425 | var j; 426 | 427 | function walk(holder, key) { 428 | 429 | // The walk method is used to recursively walk the resulting structure so 430 | // that modifications can be made. 431 | 432 | var k; 433 | var v; 434 | var value = holder[key]; 435 | if (value && typeof value === "object") { 436 | for (k in value) { 437 | if (Object.prototype.hasOwnProperty.call(value, k)) { 438 | v = walk(value, k); 439 | if (v !== undefined) { 440 | value[k] = v; 441 | } else { 442 | delete value[k]; 443 | } 444 | } 445 | } 446 | } 447 | return reviver.call(holder, key, value); 448 | } 449 | 450 | 451 | // Parsing happens in four stages. In the first stage, we replace certain 452 | // Unicode characters with escape sequences. JavaScript handles many characters 453 | // incorrectly, either silently deleting them, or treating them as line endings. 454 | 455 | text = String(text); 456 | rx_dangerous.lastIndex = 0; 457 | if (rx_dangerous.test(text)) { 458 | text = text.replace(rx_dangerous, function (a) { 459 | return "\\u" + 460 | ("0000" + a.charCodeAt(0).toString(16)).slice(-4); 461 | }); 462 | } 463 | 464 | // In the second stage, we run the text against regular expressions that look 465 | // for non-JSON patterns. We are especially concerned with "()" and "new" 466 | // because they can cause invocation, and "=" because it can cause mutation. 467 | // But just to be safe, we want to reject all unexpected forms. 468 | 469 | // We split the second stage into 4 regexp operations in order to work around 470 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 471 | // replace the JSON backslash pairs with "@" (a non-JSON character). Second, we 472 | // replace all simple value tokens with "]" characters. Third, we delete all 473 | // open brackets that follow a colon or comma or that begin the text. Finally, 474 | // we look to see that the remaining characters are only whitespace or "]" or 475 | // "," or ":" or "{" or "}". If that is so, then the text is safe for eval. 476 | 477 | if ( 478 | rx_one.test( 479 | text 480 | .replace(rx_two, "@") 481 | .replace(rx_three, "]") 482 | .replace(rx_four, "") 483 | ) 484 | ) { 485 | 486 | // In the third stage we use the eval function to compile the text into a 487 | // JavaScript structure. The "{" operator is subject to a syntactic ambiguity 488 | // in JavaScript: it can begin a block or an object literal. We wrap the text 489 | // in parens to eliminate the ambiguity. 490 | 491 | j = eval("(" + text + ")"); 492 | 493 | // In the optional fourth stage, we recursively walk the new structure, passing 494 | // each name/value pair to a reviver function for possible transformation. 495 | 496 | return (typeof reviver === "function") 497 | ? walk({"": j}, "") 498 | : j; 499 | } 500 | 501 | // If the text is not JSON parseable, then a SyntaxError is thrown. 502 | 503 | throw new SyntaxError("JSON.parse"); 504 | }; 505 | } 506 | }()); -------------------------------------------------------------------------------- /dist/store.tests.min.js: -------------------------------------------------------------------------------- 1 | /* store.js - Copyright (c) 2010-2017 Marcus Westin */ 2 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.store=t()}}(function(){var define,module,exports;return function t(e,n,r){function o(i,a){if(!n[i]){if(!e[i]){var u="function"==typeof require&&require;if(!a&&u)return u(i,!0);if(s)return s(i,!0);var c=new Error("Cannot find module '"+i+"'");throw c.code="MODULE_NOT_FOUND",c}var f=n[i]={exports:{}};e[i][0].call(f.exports,function(t){var n=e[i][1][t];return o(n?n:t)},f,f.exports,t,e,n,r)}return n[i].exports}for(var s="function"==typeof require&&require,i=0;i1)for(var n=1;n/g,">");return''+n+""}}function r(t){var e=o[t];return function(t){return e[0]+t+e[1]}}var o=e.exports={grey:["",""],green:["",""],yellow:["",""],red:["",""]},s="undefined"!=typeof t.window;for(var i in o)o.hasOwnProperty(i)&&(o[i]=s?n(i):r(i))}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],4:[function(t,e,n){(function(n){function r(t){o(m,t),S.t0=new Date,S._runNextTest()}function o(t,e,n,r){t||(t={});for(var o=1;o");m.outputEl.appendChild(document.createElement("div")).innerHTML=t.join(" "),S.hasFailedTest||(document.documentElement.scrollTop=document.body.scrollTop=99999999)}else f.apply(this,arguments)}function c(){console.log=function(){var t=Array.prototype.slice.call(arguments);t.unshift(g.grey("console.log():")),u.apply(this,t)}}function f(){f.consoleLog.apply(console,arguments)}function l(){for(var t in v)"_old"!=t&&v.hasOwnProperty(t)&&(this[t]=v._old[t])}function p(t){d(),m.onDone?m.onDone(t):h.process&&h.process.exit&&n.exit(t)}function d(){for(var t=S.tests.length,e=S.failedTests.length,n=t-e,r=[],o=0;o0?(setTimeout(function(){n(t-1,2*e)},0),!1):assert(!1))})}function r(n,r){var o=(new Date).getTime()+n;t.set("foo","bar",o),r("bar"==t.get("foo"))&&setTimeout(function(){r((new Date).getTime()>o)&&r(void 0==t.get("foo"))&&(t.set("foo","bar"),setTimeout(function(){r("bar"==t.get("foo"))&&e()},5))},n)}n(5,10)})}t("./expire"),e.exports={setup:r}},{"./expire":13}],15:[function(t,e,n){"use strict";function r(){return t("./lib/json2"),{}}e.exports=r},{"./lib/json2":17}],16:[function(t,e,n){"use strict";function r(t){test("serialization with json2",function(){t.set("foo",{bar:"cat"}),assert("cat"===t.get("foo").bar)})}t("./json2"),e.exports={setup:r}},{"./json2":15}],17:[function(require,module,exports){"use strict";var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};"object"!==("undefined"==typeof JSON?"undefined":_typeof(JSON))&&(JSON={}),function(){function f(t){return t<10?"0"+t:t}function this_value(){return this.valueOf()}function quote(t){return rx_escapable.lastIndex=0,rx_escapable.test(t)?'"'+t.replace(rx_escapable,function(t){var e=meta[t];return"string"==typeof e?e:"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+t+'"'}function str(t,e){var n,r,o,s,i,a=gap,u=e[t];switch(u&&"object"===("undefined"==typeof u?"undefined":_typeof(u))&&"function"==typeof u.toJSON&&(u=u.toJSON(t)),"function"==typeof rep&&(u=rep.call(e,t,u)),"undefined"==typeof u?"undefined":_typeof(u)){case"string":return quote(u);case"number":return isFinite(u)?String(u):"null";case"boolean":case"null":return String(u);case"object":if(!u)return"null";if(gap+=indent,i=[],"[object Array]"===Object.prototype.toString.apply(u)){for(s=u.length,n=0;n=0;n--)if(l(e[n])){var r=e[n].split("="),o=unescape(r[0]),s=unescape(r[1]);t(s,o)}}function s(t,e){t&&(p.cookie=escape(t)+"="+escape(e)+"; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/")}function i(t){t&&u(t)&&(p.cookie=escape(t)+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/")}function a(){o(function(t,e){i(e)})}function u(t){return new RegExp("(?:^|;\\s*)"+escape(t).replace(/[\-\.\+\*]/g,"\\$&")+"\\s*\\=").test(p.cookie)}var c=t("../src/util"),f=c.Global,l=c.trim;e.exports={name:"cookieStorage",read:r,write:s,each:o,remove:i,clearAll:a};var p=f.document},{"../src/util":27}],30:[function(t,e,n){"use strict";function r(){return f.localStorage}function o(t){return r().getItem(t)}function s(t,e){return r().setItem(t,e)}function i(t){for(var e=r().length-1;e>=0;e--){var n=r().key(e);t(o(n),n)}}function a(t){return r().removeItem(t)}function u(){return r().clear()}var c=t("../src/util"),f=c.Global;e.exports={name:"localStorage",read:o,write:s,each:i,remove:a,clearAll:u}},{"../src/util":27}],31:[function(t,e,n){"use strict";function r(t){return u[t]}function o(t,e){u[t]=e}function s(t){for(var e in u)u.hasOwnProperty(e)&&t(u[e],e)}function i(t){delete u[t]}function a(t){u={}}e.exports={name:"memoryStorage",read:r,write:o,each:s,remove:i,clearAll:a};var u={}},{}],32:[function(t,e,n){"use strict";function r(t){return f[t]}function o(t,e){f[t]=e}function s(t){for(var e=f.length-1;e>=0;e--){var n=f.key(e);t(f[n],n)}}function i(t){return f.removeItem(t)}function a(){s(function(t,e){delete f[t]})}var u=t("../src/util"),c=u.Global;e.exports={name:"oldFF-globalStorage",read:r,write:o,each:s,remove:i,clearAll:a};var f=c.globalStorage},{"../src/util":27}],33:[function(t,e,n){"use strict";function r(t,e){if(!h){var n=u(t);g(function(t){t.setAttribute(n,e),t.save(p)})}}function o(t){if(!h){var e=u(t),n=null;return g(function(t){n=t.getAttribute(e)}),n}}function s(t){g(function(e){for(var n=e.XMLDocument.documentElement.attributes,r=n.length-1;r>=0;r--){var o=n[r];t(e.getAttribute(o.name),o.name)}})}function i(t){var e=u(t);g(function(t){t.removeAttribute(e),t.save(p)})}function a(){g(function(t){var e=t.XMLDocument.documentElement.attributes;t.load(p);for(var n=e.length-1;n>=0;n--)t.removeAttribute(e[n].name);t.save(p)})}function u(t){return t.replace(/^d/,"___$&").replace(v,"___")}function c(){if(!d||!d.documentElement||!d.documentElement.addBehavior)return null;var t,e,n,r="script";try{e=new ActiveXObject("htmlfile"),e.open(),e.write("<"+r+">document.w=window'),e.close(),t=e.w.frames[0].document,n=t.createElement("div")}catch(o){n=d.createElement("div"),t=d.body}return function(e){var r=[].slice.call(arguments,0);r.unshift(n),t.appendChild(n),n.addBehavior("#default#userData"),n.load(p),e.apply(this,r),t.removeChild(n)}}var f=t("../src/util"),l=f.Global;e.exports={name:"oldIE-userDataStorage",write:r,read:o,each:s,remove:i,clearAll:a};var p="storejs",d=l.document,g=c(),h=(l.navigator?l.navigator.userAgent:"").match(/ (MSIE 8|MSIE 9|MSIE 10)\./),v=new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]","g")},{"../src/util":27}],34:[function(t,e,n){"use strict";function r(){return f.sessionStorage}function o(t){return r().getItem(t)}function s(t,e){return r().setItem(t,e)}function i(t){for(var e=r().length-1;e>=0;e--){var n=r().key(e);t(o(n),n)}}function a(t){return r().removeItem(t)}function u(){return r().clear()}var c=t("../src/util"),f=c.Global;e.exports={name:"sessionStorage",read:o,write:s,each:i,remove:a,clearAll:u}},{"../src/util":27}],35:[function(t,e,n){"use strict";function r(){l(p,function(t){test.group(t.name,function(){o(t)||test.skip("disabled"),test("Storage tests",function(){var e=c();e.addStorage(t),s(e)}),l(g,function(e,n){var r=d[n];test.group("plugin: "+n,function(){var n=c();n.addStorage(t),n.addPlugin(r),e.setup(n)})})})}),a.runTests({failFast:!1})}function o(t){if(!t)return print("Skip unsupported storage:",t.name),!1;var e=c([t],[]);return!!e.enabled||(print("Skip disabled storage:",t.name),!1)}function s(t){assert(t.enabled&&t.enabled,"store should be enabled"),t.clearAll(),t.get("unsetValue"),t.set("foo","bar"),assert("bar"==t.get("foo"),"stored key 'foo' not equal to stored value 'bar'"),t.remove("foo"),assert(void 0===t.get("foo"),"removed key 'foo' not undefined"),assert(void 0===t.get("foo"),"key 'foo' exists when it shouldn't"),assert("value"==t.set("foo","value"),"store#set returns the stored value"),assert(void 0!==t.get("foo"),"key 'foo' doesn't exist when it should"),t.set("foo","bar1"),t.set("foo","bar2"),assert("bar2"==t.get("foo"),"key 'foo' is not equal to second value set 'bar2'"),t.set("foo","bar"), 3 | t.set("bar","foo"),t.remove("foo"),assert(void 0===t.get("foo"),"key 'foo' exists when it shouldn't"),assert("foo"==t.get("bar"),"removing key 'foo' also removed key 'bar'"),t.set("foo","bar"),t.set("bar","foo"),t.clearAll(),assert(void 0===t.get("foo")&&void 0===t.get("bar"),"keys foo and bar not cleared after store cleared"),assert(123==t.get("defaultVal",123),"store.get should return default value"),t.set("foo",{name:"marcus",arr:[1,2,3]}),assert("object"==i(t.get("foo")),"type of stored object 'foo' is not 'object'"),assert(t.get("foo")instanceof Object,"stored object 'foo' is not an instance of Object"),assert("marcus"==t.get("foo").name,"property 'name' of stored object 'foo' is not 'marcus'"),assert(t.get("foo").arr instanceof Array,"Array property 'arr' of stored object 'foo' is not an instance of Array"),assert(3==t.get("foo").arr.length,"The length of Array property 'arr' stored on object 'foo' is not 3"),t.remove("circularReference");var e={},n={one:e};e.two=n;var r=!1;try{t.set("circularReference",e)}catch(o){r=!0}assert(r,"storing object with circular reference did not throw"),assert(!t.get("circularReference"),"attempting to store object with circular reference which should have faile affected store state");var s={"int":42,bool:!0,"float":3.141592653,string:"Don't Panic",odd_string:"{ZYX'} abc:;::)))"};for(var a in s)t._storage.resolved.write(a,s[a]),assert(t.get(a)==s[a],a+" was not correctly promoted to valid JSON"),t.remove(a);t.clearAll();var u=0;t.each(function(){u+=1}),assert(0===u)}var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},a=t("tinytest");a.hijackConsoleLog();var u=t("../src/store-engine"),c=u.createStore,f=t("../src/util"),l=f.each,p=t("../storages/all"),d=t("../plugins/all"),g=t("../plugins/all_tests");e.exports={output:null,outputError:null,runTests:r,failed:!1}},{"../plugins/all":5,"../plugins/all_tests":6,"../src/store-engine":26,"../src/util":27,"../storages/all":28,tinytest:4}],36:[function(t,e,n){"use strict";function r(t,e){if(("undefined"==typeof t?"undefined":o(t))!=("undefined"==typeof e?"undefined":o(e)))return!1;if("object"!=("undefined"==typeof t?"undefined":o(t)))return t===e;var n;for(n in t)if(!r(t[n],e[n]))return!1;for(n in e)if(!r(e[n],t[n]))return!1;return!0}var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};e.exports={deepEqual:r}},{}]},{},[1])(1)}); --------------------------------------------------------------------------------