Skip to content

Instantly share code, notes, and snippets.

@jeroenransijn
Created November 22, 2012 19:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeroenransijn/4132586 to your computer and use it in GitHub Desktop.
Save jeroenransijn/4132586 to your computer and use it in GitHub Desktop.
CoffeeScript Cache Class on localStorage
# 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'
# 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