1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
|
path = require('path')
fs = require('fs')
events = require('events')
writeSync = require('write-file-atomic').sync
KEY_FOR_EMPTY_STRING = '---.EMPTY_STRING.---' # Chose something that no one is likely to ever use
_emptyDirectory = (target) ->
_rm(path.join(target, p)) for p in fs.readdirSync(target)
_rm = (target) ->
if fs.statSync(target).isDirectory()
_emptyDirectory(target)
fs.rmdirSync(target)
else
fs.unlinkSync(target)
_escapeKey = (key) ->
if key is ''
newKey = KEY_FOR_EMPTY_STRING
else
newKey = key.toString()
return newKey
class QUOTA_EXCEEDED_ERR extends Error
constructor: (@message = 'Unknown error.') ->
if Error.captureStackTrace?
Error.captureStackTrace(this, @constructor)
@name = @constructor.name
toString: () ->
return "#{@name}: #{@message}"
class StorageEvent
constructor: (@key, @oldValue, @newValue, @url, @storageArea = 'localStorage') ->
class MetaKey # MetaKey contains key and size
constructor: (@key, @index) ->
unless this instanceof MetaKey
return new MetaKey(@key, @index)
createMap = -> # createMap contains Metakeys as properties
Map = ->
return
Map.prototype = Object.create(null);
return new Map()
class LocalStorage extends events.EventEmitter
instanceMap = {}
constructor: (@_location, @quota = 5 * 1024 * 1024) ->
unless this instanceof LocalStorage
return new LocalStorage(@_location, @quota)
@_location = path.resolve(@_location)
if instanceMap[@_location]?
return instanceMap[@_location]
@length = 0 # !TODO: Maybe change this to a property with __defineProperty__
@_bytesInUse = 0
@_keys = []
@_metaKeyMap = createMap()
@_eventUrl = "pid:" + process.pid
@_init()
@_QUOTA_EXCEEDED_ERR = QUOTA_EXCEEDED_ERR
# if Proxy?
# handler =
# set: (receiver, key, value) =>
# if @[key]?
# return @[key] = value
# else
# @setItem(key, value)
#
# get: (receiver, key) =>
# if @[key]?
# return @[key]
# else
# return @getItem(key)
#
# instanceMap[@_location] = Proxy.create(handler, this)
# return instanceMap[@_location]
instanceMap[@_location] = this
return instanceMap[@_location]
# else it'll return this
_init: () ->
try
stat = fs.statSync(@_location)
if stat? and not stat.isDirectory()
throw new Error("A file exists at the location '#{@_location}' when trying to create/open localStorage")
# At this point, it exists and is definitely a directory. So read it.
@_bytesInUse = 0
@length = 0
_keys = fs.readdirSync(@_location)
for k, index in _keys
_decodedKey = decodeURIComponent(k)
@_keys.push(_decodedKey)
_MetaKey = new MetaKey(k, index)
@_metaKeyMap[_decodedKey] = _MetaKey
stat = @_getStat(k)
if stat?.size?
_MetaKey.size = stat.size
@_bytesInUse += stat.size
@length = _keys.length
return
catch
# If it errors, that means it didn't exist, so create it
fs.mkdirSync(@_location)
return
setItem: (key, value) ->
hasListeners = events.EventEmitter.listenerCount(this, 'storage')
oldValue = null
if hasListeners
oldValue = this.getItem(key)
key = _escapeKey(key)
encodedKey = encodeURIComponent(key)
filename = path.join(@_location, encodedKey)
valueString = value.toString()
valueStringLength = valueString.length
metaKey = @_metaKeyMap[key]
existsBeforeSet = !!metaKey
if existsBeforeSet
oldLength = metaKey.size
else
oldLength = 0
if @_bytesInUse - oldLength + valueStringLength > @quota
throw new QUOTA_EXCEEDED_ERR()
writeSync(filename, valueString, 'utf8')
unless existsBeforeSet
metaKey = new MetaKey(encodedKey, (@_keys.push(key)) - 1)
metaKey.size = valueStringLength
@_metaKeyMap[key] = metaKey
@length += 1
@_bytesInUse += valueStringLength
if hasListeners
evnt = new StorageEvent(key, oldValue, value, @_eventUrl)
this.emit('storage', evnt)
getItem: (key) ->
key = _escapeKey(key)
metaKey = @_metaKeyMap[key]
if !!metaKey
filename = path.join(@_location, metaKey.key)
return fs.readFileSync(filename, 'utf8')
else
return null
_getStat: (key) ->
key = _escapeKey(key)
filename = path.join(@_location, encodeURIComponent(key))
try
return fs.statSync(filename)
catch
return null
removeItem: (key) ->
key = _escapeKey(key)
metaKey = @_metaKeyMap[key]
if (!!metaKey)
hasListeners = events.EventEmitter.listenerCount(this, 'storage')
oldValue = null
if hasListeners
oldValue = this.getItem(key)
delete @_metaKeyMap[key]
@length -= 1
@_bytesInUse -= metaKey.size
filename = path.join(@_location, metaKey.key)
@_keys.splice(metaKey.index,1)
for k,v of @_metaKeyMap
meta = @_metaKeyMap[k]
if meta.index > metaKey.index
meta.index -= 1
_rm(filename)
if hasListeners
evnt = new StorageEvent(key, oldValue, null, @_eventUrl)
this.emit('storage', evnt)
key: (n) ->
return @_keys[n]
clear: () ->
_emptyDirectory(@_location)
@_metaKeyMap = createMap()
@_keys = []
@length = 0
@_bytesInUse = 0
if events.EventEmitter.listenerCount(this, 'storage')
evnt = new StorageEvent(null, null, null, @_eventUrl)
this.emit('storage', evnt)
_getBytesInUse: () ->
return @_bytesInUse
_deleteLocation: () ->
delete instanceMap[@_location]
_rm(@_location)
@_metaKeyMap = {}
@_keys = []
@length = 0
@_bytesInUse = 0
class JSONStorage extends LocalStorage
setItem: (key, value) ->
newValue = JSON.stringify(value)
super(key, newValue)
getItem: (key) ->
return JSON.parse(super(key))
exports.LocalStorage = LocalStorage
exports.JSONStorage = JSONStorage
exports.QUOTA_EXCEEDED_ERR = QUOTA_EXCEEDED_ERR
|