Directly use
in localStorage with no conversion.
ย
This is a synchronous JavaScript interface for the HTML5 localStorage API that--
- transparently sets/gets key values using data "types" such as Array, BigInt, Boolean, Date, Float, Integer, Object and String;
- provides lightweight data obfuscation;
- intelligently compresses strings (to save storage space);
- facilitates robust lookup including query by key (name), query by (key) value and query by existence (boolean check);
- enforces segmented shared storage within the same domain by prefixing keys;
- lets you respond to localStorage change events on the same page/tab that fired them;
- broadcasts change events across the origin for the benefit of other windows/tabs;
- lets you easily work with arrays using dedicated Array Keys; and
- offers Memory Keys (that can be backed up to disk) for the fastest read times possible.
ย
Version 3.0.0
Author: W. โMacโ McMeans
Date: 12 AUG 2022
ย
<script
src="https://cdn.jsdelivr.net/gh/macmcmeans/localDataStorage@master/localDataStorage-3.0.0.min.js"
integrity="sha512-dEhk3bL90qpWkcHCJDErHbZEY7hGc4ozmKss33HSjwMeSBKBtiw/XVIE7tb5u+iOEp6dTIR9sCWW7J3txeTQIw=="
crossorigin="anonymous"
></script>
Since the source file is delivered from a repository over which we have no control, best practices demand we do not trust it. Thus we use Subresource Integrity (SRI) to prevent tampering.
Unlike the BrowseAloud story, my library is a static, self-contained resource which can be fully protected by SRI.
ย
Primary usage is the ability to seamlessly set/get keys for typical data types without having to perform conversion in your own logic. Toss out an integer and have it returned. Throw an array into storage and get it back, including the ability to add and remove elements. Are you also working with dates, booleans or objects? No problem. While itโs not rocket science to convert, track and restore your own data, letting the interface handle that chore for you is exceptionally convenient. It also saves you the hassle having to incorporate conversion logic in your application. To track a data type, storage management requires one additional byte of memory overhead per key value, so an eight-byte array, for example, will be stored using nine bytes.
Data Protection
Since localStorage data resides in a client environment where the information is not protected from access or tampering, this data can be rendered unintelligible at the expense of a small amount of memory overhead. Depending on your application, this may be worth the tradeoff. To hide your data, key values may be obfuscated using safeset/safeget. A master scramble key may be set globally, or individual scramble keys may be used per each safeset/safeget call. Scramble keys can be any value and of any type (array, boolean, date, float, integer, etc.) Key values that have been safeset with an individual scramble key can always be retrieved, but cannot be reconstructed apart from the exact scramble key with which they were obfuscated. For convenience, the global scramble key is stored in the interface. For security, individual scramble keys are not. The global scramble key may be accessed using setscramblekey/getscramblekey methods. Individual scramble keys must be remembered.
Scrambling is not encryption. For example, no attempt is made to conceal data lengths by artificially padding to a minimum length. This would be counterproductive to minimizing memory usage.
Compression
Strings are intelligently compressed onโtheโfly when storing. This means they are first analyzed to determine whether compression would lower the actual byte count (not string length) needed for storage, and if so, are silently compressed for you. This works well for common English texts (shortโlength, 7โbit ASCII), and not much else. If desired, you may manually crunch/uncrunch your own key values.
Robust Access
One may query by key (the standard way) or query by existence (haskey checks if the key is in the store while hasval checks if the store contains the value). You can query for duplicate values with listdupes and showdupes. Convenience methods prevent writing over an existing key (softset) and permit deleting a key immediately upon retrieval (chopget). You can easily query and update an array key using push/pull and contains without the need for external array logic. Key values can be checked for their data type, for example, using isfloat, and keys can be renamed (rename). Lastly, bypass methods (forceset/forceget) permit accessing localStorage directly.
Storage Management
Since HTML5 localStorage is accessible to all processes running in the browser for the domain visited, it is advisable to have an interface that segments access. To that end, the use of prefixed keys is strongly encouraged, and localDataStorage will only read/write/delete its own values. Unlike the HTML5 API, there is no method in the interface to clear all storage keys, only all prefixed keys. At any time, memory usage can be tracked against key values and key names, for example, using valbytes or keybytes.
The domain of operation for HTML5 localStorage is specific to the protocol, host & port; and multiple instances of localDataStorage can be run against the same domain at the same time. It is emojiโfriendly ๐คช๐คทโโ๏ธ๐๐, which is to say that key names and their values, as well as scramble keys and storage prefixes, are multibyte Unicodeโsafe.
The native localStorage change event is... lacking. Per the misguided whims of the interweb gods, a web page in your browser isnโt permitted to listen to change events that it alone triggers. However, in the event you'd like to keep an ear out for changes, localDataStorage will let you. The interface fires a custom event on key value changes, such as those made by the chopget, clear, forceset, remove, rename, safeset, set and softset methods. The event returns an activity timestamp and message, as well as expected details about the affected key with its old and new values (and old and new data types, etc.) Insert your own function here to catch the changes. This code snippet shows what's exposed so you can respond accordingly:
const nowICanSeeLocalStorageChangeEvents = function( e ) {
console.log(
"subscriber: " + e.currentTarget.nodeName + "\n" +
"date: " + e.detail.date + "\n" +
"timestamp: " + e.detail.timestamp + "\n" +
"prefix: " + e.detail.prefix + "\n" +
"message: " + e.detail.message + "\n" +
"method: " + e.detail.method + "\n" +
"old key: " + e.detail.oldkey + "\n" +
"new key: " + e.detail.newkey + "\n" +
"old value: " + e.detail.oldval + "\n" +
"new value: " + e.detail.newval + "\n" +
"old type: " + e.detail.oldtype + "\n" +
"new type: " + e.detail.newtype + "\n" +
"old base: " + e.detail.oldbase + "\n" +
"new base: " + e.detail.newbase
);
// respond to key change by passing key name
myCustomChangeFunction( e.detail.oldkey );
}
document.addEventListener(
"localDataStorage"
, nowICanSeeLocalStorageChangeEvents
, false
);
ย
There are no external dependencies.
Internally, obfuscation is supported by fisherYatesDurstenfeldKnuthShuffle and aleaPRNG (my own libraries).
ย
Recognizing there is no way I can predict how a change in my software will affect users, I still use semantic versioning (semver) to signal those changes.
Per Hyrumโs Law, I gently suggest you may enjoy, but I do not promise you will actually have a bug-free upgrade experience.
ย
There is a universe of difference between a variable considered null and one that is undefined. In JavaScript, both of these are falsy and both are primitive types (despite what typeof null
tells you). Yet they are also quite distinct.
A value of null means the variable has been set to nothing whatsoever, but it has been set nevertheless.
In contrast, an undefined variable has no value because the variable itself doesn't exist (it is undeclared).
With null, the variable is set to no value but if undefined, it isn't set. The distinction is important. The native localStorage API returns null when getting a key that doesn't exist. (Pedantically, this is false.)
However, localDataStorage returns undefined for an undeclared key simply because it isn't present in the store. Further, you could actually store a null value key using localDataStorage if your use case required, but at no time will non-existant keys be returned as nulls. The distinction is just. Too. Important.
ย
Here's the documentation for the interface (95% complete).
ย
Create an instance of localDataStorage using the specified key name prefix
localData = localDataStorage( 'passphrase.life' )
--> Instantiated. Prefix adds 16.00 bytes to every key name (stored using 32.00 bytes).
ย
typical set/get calls (data types are respected and returned transparently)
localData.set( 'key1', 19944.25 )
localData.get( 'key1' )
--> 19944.25
localData.set( 'key2', 2519944 )
localData.get( 'key2' )
--> 2519944
localData.set( 'key3', true )
localData.get( 'key3' )
--> true
localData.set( 'key4', 'data' )
localData.get( 'key4' )
--> "data"
localData.set( 'key5', [1,2,3,4,9] )
localData.get( 'key5' )
--> [1, 2, 3, 4, 9]
localData.set( 'key6', new Date() )
localData.get( 'key6' )
--> Mon May 01 2017 14:39:11 GMT-0400 (Eastern Daylight Time)
localData.set( 'key7', {'a': [1,2,3] } )
localData.get( 'key7' )
--> Object {a: Array(3)}
ย
get the "size" of a key's value (codepoints)
localData.size( 'key4' )
--> 4
total codepoints in value (not length, not graphemes)
ย
results when querying a non-existing key
localData.forceget( 'non-existing key' )
--> null
same as localStorage.getItem( 'non-existing key' )
localData.get( 'non-existing key' )
--> undefined
the key is undefined because it does not exist, it is NOT null
localData.chopget( 'non-existing key' )
--> undefined
localData.safeget( 'non-existing key' )
--> undefined
ย
read then delete a key
x = localData.chopget( 'key7' )
--> Object {a: Array(3)}
localData.get( 'key7' )
--> undefined
ย
don't overwrite an existing key
localData.softset( 'key4', 'new data' )
localData.get( 'key4' )
--> "data"
ย
set/get key, bypassing any data type embedding, but still observing key prefixes
localData.forceset( 'api', 13579 )
all values are stored as strings, in this case under the key passphrase.life.api
localData.forceget( 'api' )
--> "13579"
localData.forceget( 'key6' )
--> ""2017-05-01T18:39:11.443Z""
ย
find duplicate key values
localData.set( 'key8', 'data' )
now key4 and key8 have the same values
localData.countdupes()
--> 1
localData.showdupes()
--> ["data"]
this key value occurs twice minimum
ย
// handling duplicates; localData vs localStorage API
localData.forceset( 'dupekey1', 1234 )
will be stored as a string
localData.forceset( 'dupekey2', '1234' )
will be stored as a string
ย
// look for duplicates (among localStorage keys)
localData.showdupes()
--> [1234, "data"]
ย
// remove a key
localData.remove( 'dupekey1' )
prep
localData.remove( 'dupekey2' )
prep
localData.remove( 'key8' )
prep
ย
localData.set( 'dupekey3', 1234 )
stored as string, but recognized as integer
localData.set( 'dupekey4', '1234' )
stored and recognized as string
ย
// look for duplicates (among localData types)
localData.showdupes()
--> []
since data types are respected, no dupes were found
ย
localData.set( 'dupekey1', 1234 )
prep
localData.set( 'dupekey2', '1234' )
prep
localData.set( 'key8', 'data' )
prep
ย
localData.countdupes()
--> 3
localData.listdupes()
--> Object {dupecount: 3, dupes: Object}
localData.listdupes().dupecount
--> 3
localData.listdupes().dupes
--> Object {0: Object, 1: Object, 2: Object}
localData.listdupes().dupes[0]
--> Object {value: 1234, keys: Array(2)}
localData.listdupes().dupes[0].value
--> 1234
localData.listdupes().dupes[0].keys
--> ["dupekey1", "dupekey3"]
ย
check if key exists
localData.haskey( 'dupekey3' )
--> true
ย
check if value exists
localData.hasval( 1234 )
--> true
checks value AND data type
ย
localData.set( 'testkey', 89.221 )
prep
localData.hasval( '89.221' )
--> false
the float (number) type does not match the string type
ย
localData.forceset( 'LSkey1', 98765 )
set key value using localStorage API (handled as string)
localData.forcehasval( 98765 )
--> true
localData.forcehasval( '98765' )
--> true
localStorage API does not discern between data types
ย
localData.hasval( 98765 )
--> true
localData attempts to coerce any value not explicity set by it
localData.hasval( '98765' )
--> false
localData will first coerce a value to a number, if possible
ย
show key's value type
localData.showtype( 'dupekey3' )
--> "integer"
localData.showtype( 'dupekey4' )
--> "string"
localData.showtype( 'key1' )
--> "float"
localData.showtype( 'key3' )
--> "boolean"
localData.showtype( 'key5' )
--> "array"
localData.showtype( 'key6' )
--> "date"
localData.set( 'key7', {'local' : ['d', 'a', 't', 'a']} )
prep
localData.showtype( 'key7' )
--> "object"
ย
boolean check the data type of a key's value
localData.isarray( 'key5' )
--> true
localData.isfloat( 'testkey' )
--> true
localData.isnumber( 'testkey' )
--> true
ย
query by key value, not key name (returns first found)
localData.showkey( 1234 )
--> "dupekey1"
localData.showkey( '1234' )
--> "dupekey2"
ย
// returns all found
localData.showkeys( 1234 )
--> ["dupekey1", "dupekey3"]
ย
using the global scramble key for obfuscation
localData.getscramblekey()
--> 123456789
default global scramble key (integer)
localData.safeset( 'ss1', '007' )
--> (stored scrambled)
localData.safeget( 'ss1' )
--> "007"
ย
localData.setscramblekey( new Date() )
// set global scramble key to the current date, as date object
localData.getscramblekey()
--> Mon May 01 2017 22:28:11 GMT-0400 (Eastern Daylight Time)
ย
localData.safeget( 'ss1' )
--> (garbled data)
different global scramble key used for retrieval
ย
// using an individual scramble key for obfuscation
localData.safeset( 'ss2', 'test', {'scramble': ['key']} )
--> (stored scrambled)
scramble keys can be any value and of any data type
localData.safeget( 'ss2', {'scramble': ['key']} )
--> "test"
ย
localData.safeget( 'ss1', 123456789 )
-> "007"
ย
safeget will not retrieve an unscrambled key
localData.safeget( 'key4' )
--> (garbled data)
ย
renaming keys
// non-scambled keys can safely be renamed
localData.rename( 'key4', 'key4-renamed' )
key4 no longer exists
localData.get( 'key4' )
--> undefined
localData.get( 'key4-renamed' )
--> "data"
ย
// scrambled keys cannot be renamed: the key name and the value together produce the obfuscation
localData.rename( 'ss1', 'ss1-renamed' )
key ss1 no longer exists
localData.safeget( 'ss1' )
--> undefined
localData.safeget( 'ss1-renamed', 123456789 )
--> (garbled data)
this was the correct scramble key for the 'ss1' key, but not for the 'ss1-renamed' key
ย
localData.rename( 'ss1-renamed', 'ss1' )
key ss1-renamed no longer exists
localData.safeget( 'ss1-renamed' )
--> undefined
localData.safeget( 'ss1', 123456789 )
--> "007"
ย
how localDataStorage reacts to values set via the localStorage API
localData.forceset( 'lsAPIkey', 77.042 )
always stored as a string by the native API
localData.forceget( 'lsAPIkey' )
--> "77.042"
localData.get( 'lsAPIkey' )
--> 77.042
localData will coerce value to number when possible
localData.showtype( 'lsAPIkey' )
--> "presumed number"
('presumed' because value was coerced, not set)
ย
there are several ways to track memory usage
// show memory required to store key value
localData.showtype( 'dupekey4' )
--> "string";
localData.get( 'dupekey4' )
--> "1234"
localData.size( 'dupekey4' )
--> 4
localData.valbytes( 'dupekey4' )
--> "8.00 bytes"
localStorage uses 16 bits to store 1 byte (only the data is counted)
localData.valbytesall( 'dupekey4' )
--> "12.00 bytes"
now we include the 2-byte embedded marker (total data)
ย
// show memory required to store key name
localData.keybytes( 'dupekey4' )
--> "48.00 bytes"
the prefix ('passphrase.life' + '.') is 32 bytes, plus key name is 16 bytes more ('dupekey4' ), yielding 48 bytes
ย
// show memory used by the key-value pair
// key name + raw value
localData.bytes( 'dupekey4' )
--> "56.00 bytes"
8 bytes for raw value and 48 bytes for name, i.e. valbytes + keybytes
ย
// key name + total value (include value marker byte)
localData.bytesall( 'dupekey4' )
--> "60.00 bytes"
now includes the embedded data type marker (it's 2 bytes, stored as 4)
ย
view memory usage of compressed key values
localData.set( 'crunchedkey', 'this is some test data' )
only strings can be compressed; other data types will ignore compression
localData.size( 'crunchedkey' )
--> 22
localData.valbytes( 'crunchedkey' )
--> "44.00 bytes"
memory used to store raw string of 22 graphemes (each is 7-bit ASCII)
localData.valbytesall( 'crunchedkey' )
--> "34.00 bytes"
total memory required to store compressed string + embedded data type marker
ย
unicode-safe data storage
localData.set( 'unicodeKey1', '๐' )
storing an emoji; 1 grapheme (1 codepoint in 4 bytes)
localData.get( 'unicodeKey1' )
--> "๐"
localData.size( 'unicodeKey1' )
--> 1; one codepoint
localData.valbytes( 'unicodeKey1' )
--> "8.00 bytes"
localData.valbytesall( 'unicodeKey1' )
--> "12.00 bytes"
ย
localData.set( 'unicodeKey2', '๐๐๐๐โ ' )
storing 5 graphemes (5 codepoints in 19 bytes)
localData.get( 'unicodeKey2' )
--> "๐๐๐๐โ "
localData.size( 'unicodeKey2' )
--> 5
localData.valbytes( 'unicodeKey2' )
--> "38.00 bytes"
localData.valbytesall( 'unicodeKey2' )
--> "42.00 bytes"
ย
// using emojis for key name, key value and individual scramble key
localData.safeset( '๐๐๐ท', '๐๐ป', '๐' )
localData.safeget( '๐๐๐ท', '๐' )
--> "๐๐ป"
ย
// using emojis in the global scramble key
localData.setscramblekey( '๐ต๐ถ๐ถ๐ป' )
localData.safeset( 'Ron Wyden', '.@NSAGov ๐ป๐ฑ๐ก๐๐๐๐๐ #EndThisDragnet' )
localData.safeget( 'Ron Wyden' )
--> ".@NSAGov ๐ป๐ฑ๐ก๐๐๐๐๐ #EndThisDragnet"
ย
get tally of keys
localData.keys()
--> 24
ย
delete all prefixed keys in the domain (unprefixed localStorage keys are not affected)
localStorage.setItem( 'API-key', 'test data' )
create a key in the same domain completely outside our instance of localDataStorage
localData.clear()
--> "24 keys removed"
localStorage.getItem( 'API-key' )
--> "test data"
any unprefixed localStorage keys are untouched
localData.safeget( 'Ron Wyden' )
--> undefined
all localData keys have been removed
https://github.com/macmcmeans/aleaPRNG
https://github.com/macmcmeans/fisherYatesDurstenfeldKnuthShuffle
ย
Chrome(ium) browsers (blink engine) and FireFox (gecko) on Win 10 (x64).
ย
-
3.0.0 - 12 AUG 2022
release
Major release. Added major features, fixed numerous bugs and refactored code.
patch
Consolidated private references to the localStorage.removeItem() method.
feature
Added length property.
patch
Fixed bug where clear() method did not remove all prefixed keys.
feature
Added import/export methods.
feature
Adjusted logic that computes memory storage requirements for keys.
patch
Improved the logic for string compression checks.
patch
Streamlined error checking for localStorage availability prior to instantiation.
feature
Added key copy method.
patch
Improved the efficiency of storage flags by 50%.
patch
Strengthened the obfuscation of data protected by the safeset method.
major feature
Added Memory Keys (and associated methods).
major feature
Added Array Keys (and associated methods).
major feature
Added ability to broadcast change events across the origin.
feature
On startup, the library computes the browser's quota.
feature
Added channel property.
feature
Added backup/restore methods.
feature
Dates, floats and integers are now automatically stored compressed.
update
Revised the wiki.
ย -
2.0.1 - 12 MAY 2022
patch
Fixed minor bugs, light refactoring.
patch
Updated the internal custom event to show both the date and the timestamp.
update
Revised the wiki.
ย -
2.0.0 - 1 MAY 2022
release
Major release. Added support for BigInt. Fixed numerous bugs and refactored code.
ย -
1.3.1 - 21 FEB 2022
patch
Fixed a bug where the Clear() method did not fire an event upon deletion of keys.
patch
Corrected logic in the Rename() method.
patch
Updated the internal custom event to display old key name and new key name.
update
Revised the wiki.
ย -
1.3.0 - 4 MAY 2020
feature
Implement optional flag useful on instantiation to mute console messages warning of the existence of previous keys and the space required for prefix storage.
ย -
1.2.0 - 19 JUN 2017
update
Checks whether localStorage is available and if not, gracefully fails when called. This means that all methods will simply return false instead of nasty type errors.
ย -
1.1.0 - 17 MAY 2017
feature
Add ability to listen to key value change events (in same window/tab).
ย -
1.0.0 - 15 MAY 2017
release
Initial release.
ย
Copyright (c) 2017-2022, W. "Mac" McMeans
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
-
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
-
Neither the name of copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.