Skip to content

Instantly share code, notes, and snippets.

@polotek
Last active October 5, 2015 13:57
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save polotek/2816129 to your computer and use it in GitHub Desktop.
Save polotek/2816129 to your computer and use it in GitHub Desktop.
A little wrapper around localStorage that supports compound keys and non-string values
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Commenting this out is preferred by some people, see
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
# Users Environment Variables
.lock-wscript

min-local-store

The MIT License (MIT)
Copyright (c) 2014 Marco Rogers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Minimal lib

I whipped up the little lib below because I needed to interface with localStorage for a small personal project. I started looking at different localStorage libs first of course. I found several that would've been fine. So besides a general tendency to shave yaks on Sunday, why did I write this? A few thoughts that went into it. I ended up feeling like all the libs I saw did "too much". The requirements I was using to evaluate were much more narrow than most of the other libs were using. Keep in mind that this is a personal project, so half of these requirements were made up by me.

Requirements

  • I want all the features I plan to use, and non that I don't. If it's not clear up front, go with less.
  • I want only as much code as necessary. Where "necessary" is defined by the rest of the requirements and my personal preferences.
  • I wanted "just javascript". No other dependencies added just for convenience.
  • Respect the environment. Minimum globals. (No globals would be ideal. Still thinking about what this means exactly).
  • Know the environment. This is going to be used in a Chrome extension. So any assumptions about environment come from there and only there. If it's primarily for node or for modern mobile browsers or whatever, then act accordingly.
  • Sensible and complete api. By sensible I mean no frills, and by complete I mean the api should do what it can to support the intended usages.
  • Extensible and composable structure. If I want more than what's here, it should be easy enough to build on top without changing this at all.
  • No build step required.
  • Tests (not here).
  • Liberally licensed in case I want to mess with it.

Implementation rules

Forget older js environments

Assume the newer features of js and the browser (Array.isArray, localStorage, etc). There is a strong urge to over-engineer these things, especially because we're used to thinking about all the different environments in which the code might be used. We could fall back to an object if localStorage isn't available. We could add a "persistent store" abstraction layer so we could plug-in different backends. But we're not gonna do that. YAGNI.

This also means the intended environment(s) should be published prominently. And the tests should pass in the actual environment or as close to it as possible. So for instance, I need a test environment that runs in a Chrome extension.

Don't hide everything

This goes to extensibility and composability (E&C). Don't hide all the potentially interesting stuff. In a previous version I had prefix in a closure so it couldn't be changed. But then I remembered that js conventions are usually enough. It has an underscore on the front. Don't mess with it. Unless you want to. Then do whatever you want. But keep in mind that it might break later.

The getKey method is also important. Since we support compound keys, it's a good idea to make this public so people can have maximum flexibility. So you can call getKey yourself and the lib will make sure it doesn't happen more than once.

Minimal feature set

Often I'm thinking I want to pull in a library, but It's only to use a small subset of the features. Often underscore.js is a good example of this. I really only use 10-15% of underscore ever. This can easily be built on top of. But if you only want this. You're good to go.

Allow reflection on the api

This also goes toward E&C. You should be able to inspect the full public api and use the full flexibility of js to build on top. Here that means exposing the constructor with it's prototype. That isn't the only way to go about this though.

Driven by code

The lib only does stuff when you tell it to. Of course declarative features or even DSLs can be build on top, but they shouldn't be included. And even if they are included, they shouldn't activate by default. The reason I think this is a good idea is because it's more likely you'll get a usable lib that can be used programmatically without the declarative stuff. In my experience, when a lib is built with a mind towards non code-driven usages, it ends up being coupled to those usages. This often ruins E&C. Node even has a bit of this with libs built to be run at the command line.

Minimal node/browser support.

If it's for the browser, don't worry too much about node. Just do a quick check and export some stuff. A lot of people want to take node libs and use them in the browser, or take browser libs and use them in node. That's fine and I don't feel strongly about that either way. But I want a lib that does the minimum amount that's needed for this. If you want to polyfill localStorage in node, go for it. But you can require this and mess with it without any of that. You can also open up the file and change it.

The thought behind this requirement needs some refinement. Notice the storage lib has no wrapper function. I may add one, but it's not really necessary right now. The story for retrofitting libs built for node into the browser or vice versa, is not so great. You end up pulling in additional libs that wrap things and dynamically load things and inject things. Or you add a build step. That's all great and I'm sure people will mention stuff like browserify or require.js in the comments. But none of that has anything to do with this lib. What I really want is a minimal setup for my lib that extends the E&C idea to include "easily plugged into environments outside of the intended ones".

No golfing

I want it small, but not at the expense of readability. It's also not necessary to try to write for performance optimizations, because v8 is smarter than me. And if it's not, it will be soon.

Thoughts?

I'm interested to hear how people feel about this sort of thing. I'm sure these opinions will morph over time. But I've been writing js for a long time. And my experience makes me want alternative libs like this for several common things.

  • object manipulation - Really just mixin/extend and proper object iteration.
  • function composition - setting timeouts, only allow 1 call, etc.
  • DOM - Nice api like jQuery/zepto, but way less frills.
  • Ajax - Nice api like jQuery/zepto, but without all the other crap.
  • Events/PubSub - Without DOM, but can be composed onto a DOM lib.
  • Testing - really just sandboxing and asserts

Naming?

The only hard part is naming. What shall I call this storage lib? I want something that captures a lot of the spirit of what I outlined above. "Microlibs" is already taken and I'm not sure I like that anyway.

{
"name": "min-local-store",
"version": "0.9.0",
"author": "Marco Rogers",
"description": "A little wrapper around localStorage that supports compound keys and non-string values",
"main": "storage.js",
"devDependencies": {
"tape": "*"
},
"scripts": {
"test": "tape test.js"
},
"repository": {
"type": "git",
"url": "https://github.com/polotek/min-local-store.git"
},
"keywords": [
"localstorage"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/polotek/min-local-store/issues"
},
"homepage": "https://github.com/polotek/min-local-store"
}
/**
* A little wrapper around localStorage that supports compound keys and
* non-string values
*/
var Storage = function(prefix) {
this._prefix = prefix ? prefix + '::' : '';
};
Storage.prototype = {
getKey: function(key) {
if(Array.isArray(key)) {
// compound key
key = key.join('::');
} else if(typeof key === 'string' && key.indexOf(this._prefix) === 0) {
// already this._prefixed
return key;
}
return this._prefix + key;
}
, has: function(key) {
if(arguments.length > 1) {
// compound key arguments
key = Array.prototype.slice.call(arguments);
}
key = this.getKey(key);
return localStorage.hasOwnProperty(key);
}
, get: function(key) {
if(arguments.length > 1) {
// compound key arguments
key = Array.prototype.slice.call(arguments);
}
key = this.getKey(key);
var val = localStorage[key];
if(val === undefined) { return undefined; }
try {
val = JSON.parse(val);
} catch(e) {
val = null;
}
return val;
}
, set: function(key, val) {
if(!val && (val === undefined || isNaN(val))) { return false; }
key = this.getKey(key);
val = JSON.stringify(val);
localStorage[key] = val;
return true;
}
, del: function(key) {
if(arguments.length > 1) {
// compound key arguments
key = Array.prototype.slice.call(arguments);
}
key = this.getKey(key);
var val = this.get(key);
delete localStorage[key];
return val;
}
};
if(typeof require === 'function' && typeof module !== 'undefined') {
module.exports = Storage;
}
var Storage = require('./')
, test = require('tape');
test('with no prefix', function(t) {
t.plan(1);
var s = new Storage()
, key = s.getKey('test_key');
t.equal(key, 'test_key', 'key is returned unchanged');
});
test('with prefix', function (t) {
t.plan(2);
var s = new Storage('__test_prefix__')
, key = s.getKey('test_key')
, index = key.indexOf('__test_prefix__');
t.equal(index, 0, 'prefix is prepended to key value');
index = key.indexOf('test_key');
t.ok(index > 0, 'original key is present in generated key');
});
test('with compound key', function(t) {
t.plan(3);
var s = new Storage()
, keys = ['test_key1', 'test_key2'];
var key = s.getKey(keys)
, index = key.indexOf(keys[0])
, index2 = key.indexOf(keys[1]);
t.ok(index > -1, 'all keys present in generated key');
t.ok(index2 > -1, 'all keys present in generated key');
t.ok(index < index2, 'keys are appended in order to generated key');
});

The MIT License (MIT) Copyright (c) 2012 Marco Rogers http://marcorogers.com/

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@chrisjpowers
Copy link

Looks good! Just FYI JSON doesn't do well with Date and Error objects, so you might want to add edge cases to support those with custom encoding/decoding (I just had to do this last week with some local storage code).

@polotek
Copy link
Author

polotek commented May 28, 2012

@chrisjpowers JSON doesn't do well with a lot of stuff :) I may expand on this a bit. I was really looking for a minimal library. Not too many frills.

@chilts
Copy link

chilts commented May 28, 2012

For the Ajax API, have you seen this?

Cheers,
Andy

@dshaw
Copy link

dshaw commented May 28, 2012

Storage.js is apparently already a thing. How about "locket"? Too frou-frou?

@polotek
Copy link
Author

polotek commented May 28, 2012

@chilts Never used superagent, but I like it. If it didn't tack on an event emitter, it would be a pretty good example I think.

I does raise the question of "completeness" though. Should superagent be "atomic" and only deal with ajax? And it's up to the user to combine that with an atomic event emitter lib? The event emitter lib in superagent could definitely stand on it's own. But dropping it into the same file and extending super agent is also done cleanly. There's an argument that superagent needs some asynchronous way to surface errors. So maybe it's part of the minimum package.

This is all just thinking out loud of course. Doesn't matter that much. If you're looking to drop in an ajax lib, you could definitely do worse than this.

@dominictarr
Copy link

I agree with pretty much everything you say, but I've decided that it is easier to just use browserify, despite extra build step. then you have node event emitters, etc.
may be request for the browser?
https://github.com/iriscouch/browser-request

I always go for the most obvious sounding names possible. if it does a thing, name it after that thing.
no cutesy names, this wraps local storage. i'd probably just call it LocalStorage.

@jed
Copy link

jed commented Aug 27, 2012

did you mean

typeof module !== "undefined"

on line 72?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment