Created
November 22, 2012 19:13
-
-
Save jeroenransijn/4132586 to your computer and use it in GitHub Desktop.
CoffeeScript Cache Class on localStorage
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Cache.coffee | |
# ------------------------------------------------------------ | |
# Hard dependency: underscore.js | |
class Cache | |
# @description if a string is given it will be the namespace | |
# @param {string|object} options | |
constructor: (options) -> | |
settings = { | |
# will be used for the localStorage | |
namespace: 'global' | |
# needs to be a function, is called on every set | |
backend: false | |
} | |
# if options is a string use it as the namespace | |
options.namespace = options if _.isString options | |
# extend the settings | |
_.extend settings, options if options | |
# set the namespace | |
@setNamespace settings.namespace | |
# set the backend | |
@setBackend settings.backend if settings.backend | |
# set storage | |
@storage = window.localStorage | |
@latestSet = 0 | |
@latestSetAll = 0 | |
@latestSaveAll = 0 | |
@latestSyncAll = 0 | |
# bind all | |
_.bindAll @ | |
# @param {function} backend | |
setBackend: (backend) -> | |
if _.isFunction backend | |
@backend = backend | |
else | |
throw new TypeError "(Cache)(setBackend): backend is not a function" | |
# @param {string} namespace | |
setNamespace: (namespace) -> | |
if _.isString namespace | |
@namespace = @_dasherize namespace | |
else | |
throw new TypeError "(Cache)(setNamespace): namespace is not a string" | |
# @param {string} key | |
# @return {string} dasherized key prepended with namespace | |
_ns: (key) -> | |
"#{@namespace}.#{@_makeValidKey(key)}" | |
# @param {string} key | |
# @return {string} dasherized key | |
_makeValidKey: (key) -> | |
if _.isString key | |
return @_dasherize key | |
else | |
throw new TypeError "(Cache)(_makeValidKey): key '#{key}' is not a string" | |
# @param {string} string | |
# @return {string} dasherized | |
_dasherize: (string) -> | |
string.replace(/_/g, '-') | |
# @param {string} key | |
# @return {all} value of the key in the cache | |
get: (key) -> | |
key = @_makeValidKey key | |
# return early if possible | |
if not @isObjectExpired() | |
return @cachedObject[key] if @cachedObject[key] | |
# get value | |
JSON.parse @storage.getItem @_ns key | |
# @param {string} key, key to be set | |
# @param {all} value, stringified and stored | |
# @return {all} value | |
set: (key, value) -> | |
@latestSet = new Date().getTime() | |
@storage.setItem @_ns(key), JSON.stringify value | |
value # return value | |
# @param {object} items | |
setAll: (items) -> | |
if _.isObject items | |
_.each items, (key, item) => | |
# just to be sure we make a valid key | |
# this might be hurtful depending on | |
# the relation with the backend | |
key = @_makeValidKey key | |
# each key is a key in localStorage | |
@set key, item | |
else | |
throw new TypeError "(Cache)(setAll): items is not an object" | |
# @description push an item to an array and store | |
# @param {string} key, on localStorage | |
# @param {all} item, pushed on the array | |
# @return {array|false} if not an array return false and throw TypeError | |
append: (key, item) -> | |
arr = @get key | |
# type check | |
if _.isArray arr | |
arr.push item | |
@set key, arr | |
else if arr == null | |
# create it on the fly | |
arr = [item] | |
@set key, arr | |
else | |
throw new TypeError '(Cache)(append): array expected, instead: ' + typeof arr | |
# @description unshift an item to an array and store | |
# @param {string} key, on localStorage | |
# @param {all} item, unshifted on the array | |
# @return {array|false} if not an array return false and throw TypeError | |
prepend: (key, item) -> | |
arr = @get key | |
# type check | |
if _.isArray arr | |
arr.unshift item | |
@set key, arr | |
else if arr == null | |
arr = [item] | |
@set key, arr | |
else | |
throw new TypeError '(Cache)(prepend): array expected' | |
# @param {string} key | |
# @param {object} object, to extend current item with | |
extend: (key, object) -> | |
if not _.isObject object | |
# return early on error | |
throw new TypeError "(Cache)(extend): object argument '#{object}' is not an object" | |
current = @get key | |
# type check | |
if _.isObject current | |
# extend current with the given object | |
_.extend current, object | |
@set key, current | |
else | |
throw new TypeError "(Cache)(extend): current value for key '#{key}' is not an object" | |
# @description get all items on a namespace | |
# @param {bool} isObjectReturned, return an object instead of an array | |
# @return {array|object|null} all items on a namespace, or null if ns is empty | |
all: (isObjectReturned) -> | |
# set default to false | |
isObjectReturned = !!isObjectReturned || false | |
# return early if the object/array is cached | |
return @cachedObject if isObjectReturned and not @isObjectExpired() | |
return @cachedArray if not @isArrayExpired() | |
# get the keys of all the localStorage objects | |
objKeys = Object.keys @storage | |
# only return the keys with the namespace | |
# cache the nsLength | |
nsLength = @namespace.length | |
# iterate the keys and reject the ones with a different namespace | |
keys = _.filter objKeys, (key) => | |
# substring from index to namespace length | |
(key.substring 0, nsLength) == @namespace | |
# return null early if there are no keys | |
return null if _.isEmpty keys | |
if isObjectReturned | |
obj = {} | |
# return only the values of this keys | |
_.each keys, (key) => | |
# key is actually with namespace and dot | |
# need to strip it of | |
key = key.substring nsLength + 1, key.length | |
obj[key] = @get key | |
@_cacheObject obj # return object | |
else | |
# return an array | |
arr = [] | |
_.each keys, (key) => | |
# key is actually with namespace and dot | |
# need to strip it of | |
key = key.substring nsLength + 1, key.length | |
arr.push @get key | |
@_cacheArray arr # return array | |
# @param {array} array | |
# @return {array} array | |
_cacheArray: (array) -> | |
@cachedArray = array | |
@cachedArrayTime = new Date().getTime() | |
array | |
# @param {object} object | |
# @return {object} object | |
_cacheObject: (object) -> | |
@cachedObject = object | |
@cachedObjectTime = new Date().getTime() | |
object | |
# @return {bool} | |
isArrayExpired: -> | |
return @cachedArrayTime >= @latestSet if @cachedArrayTime | |
true # default is true if there is no cachedArrayTime | |
# @return {bool} | |
isObjectExpired: -> | |
return @cachedObjectTime >= @latestSet if @cachedObjectTime | |
true # default is true if there is no cachedObjectTime | |
# @return {bool} | |
isEmpty: -> | |
# return false if @cachedArray | |
# return false if @cachedObject | |
Object.keys(@storage) == null | |
# @return {object} data object | |
data: -> | |
{ | |
namespace: @namespace | |
latestSet: @latestSet | |
latestSetAll: @latestSetAll | |
latestSaveAll: @latestSaveAll | |
latestSyncAll: @latestSyncAll | |
} | |
# TODO: save method | |
# TODO: sync method | |
# @description sync with backend | |
syncAll: -> | |
if @backend | |
allItemsFromBackend = @backend { | |
method: 'syncAll' | |
items: @all(true) | |
data: @data | |
} | |
@setAll allItemsFromBackend if allItemsFromBackend | |
@latestSyncAll = new Date().getTime() | |
else | |
throw new Error "(Cache)(syncAll): no backend is set" | |
# @description one way saveAll call to backend | |
saveAll: -> | |
if @backend | |
@backend { | |
method: 'saveAll' | |
items: @all(true) | |
data: @data | |
} | |
# set the latest saveAll | |
@latestSaveAll = new Date().getTime() | |
else | |
throw new Error '(Cache)(saveAll): no backend is set' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# NOTE: underscore.js is a hard dependency! | |
# ------------------------------------------------------------ | |
# OVERVIEW: | |
# ------------------------------------------------------------ | |
# primary methods: | |
# - get key, value | |
# - set key, value | |
# higher level methods: | |
# - isEmpty => returns bool | |
# - setAll object | |
# - all | |
# - all true => returns object, used to cache | |
# - isArrayExpired => returns bool | |
# - isObjectExpired => return bool | |
# - data => returns object all times are expressed in time (new Date().getTime() is called) | |
# { | |
# namespace: @namespace | |
# latestSet: @latestSet | |
# latestSetAll: @latestSetAll | |
# latestSaveAll: @latestSaveAll | |
# latestSyncAll: @latestSyncAll | |
# } | |
# setters: | |
# - setNamespace string | |
# - setBackend function | |
# backend: | |
# - syncAll => backend to localStorage | |
# - saveAll => localStorage to backend | |
# for arrays: | |
# - append key, value | |
# - prepend key, value | |
# for objects: | |
# - extend key, object | |
# ------------------------------------------------------------ | |
# EXAMPLES: | |
# ------------------------------------------------------------ | |
# create a new cache | |
# which is basically a sort of model | |
# the argument will be the namespace in the localStorage keys | |
shop = new Cache 'shop' | |
# same as | |
shop = new Cache { namespace: 'shop' } | |
# by default it will use the namespace 'global' | |
global = new Cache | |
# set method the same as on localStorage, notice there is no need to call | |
# JSON.stringify, this is where the Cache remove the headache | |
shop.set 'fruits', ['apple','orange'] | |
# shows up in localStorage as "shop.fruits" | |
# get is almost the same as on localStorage, again no need to call JSON.parse | |
fruits = shop.get 'fruits' | |
console.log fruits | |
# => ['apple','orange'] | |
# since the value for 'fruits' is an array an append method helps to get things done | |
shop.append 'fruits', 'pineapple' | |
# => 'shop.fruits' = ['apple','orange','pineapple'] | |
# calling the all method caches our array in a property, | |
# so no need to call JSON.parse, in a small array this is not a big deal | |
# but with big arrays/objects this can save some expensive calls | |
# I will set some more properties to make sense of this method | |
shop.set 'beers', ['Heineken','Amstel'] | |
arrayOfAllItemsOnThisNamespace = shop.all() | |
# => [['apple','orange','pineapple'],['Heineken','Amstel']] | |
shop.cachedArray | |
# => [['apple','orange','pineapple'],['Heineken','Amstel']] | |
# it's nice to have all the items on this namespace in an array | |
# but it can't be used as for caching | |
# instead we need to call: | |
objectOfAllItemsOnThisNamespace = shop.all(true) | |
# => { 'fruits': ['apple','orange','pineapple'], 'beers' : ['Heineken','Amstel'] } | |
# all methods involving getting values will now use this object | |
shop.cachedObject | |
# => { 'fruits': ['apple','orange','pineapple'], 'beers' : ['Heineken','Amstel'] } | |
# will get it from the cachedObject | |
# saving on the JSON.parse calls | |
shop.get 'fruits' | |
shop.get 'beers' | |
shop.all() | |
# all keys are dasherized | |
shop.set 'o m g', 'hello world' | |
# => 'shop.o-m-g' : 'hello world' | |
# ------------------------------------------------------------ | |
# USING A BACKEND | |
# ------------------------------------------------------------ | |
# the main use of this cache is to sync it with some backend system | |
# this might be to write json to a file, or send some json to the server | |
# create a methods object or you can have whatever structure | |
methods = | |
"syncAll": (items, data) -> | |
#...... | |
_.extend items, someDataFromTheBackend | |
"saveAll": (items, data) -> | |
#...... | |
saveToMyBackend items | |
# @param {string} method | |
# @param {object} items | |
# @param {object} data | |
backend = (method, items, data) -> | |
if methods[method] | |
methods[method].call items, data | |
projects = new Cache { namespace: 'project', backend: backend } | |
# now you can call this methods | |
projects.syncAll() | |
projects.set 'node-webkit', 'is awesome' | |
projects.saveAll() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment