summaryrefslogtreecommitdiff
path: root/src/fossil.storage.js
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/fossil.storage.js
downloadrelic-master.tar.gz
initial commitHEADmaster
Diffstat (limited to 'src/fossil.storage.js')
-rw-r--r--src/fossil.storage.js220
1 files changed, 220 insertions, 0 deletions
diff --git a/src/fossil.storage.js b/src/fossil.storage.js
new file mode 100644
index 0000000..3a11940
--- /dev/null
+++ b/src/fossil.storage.js
@@ -0,0 +1,220 @@
+(function(F){
+ /**
+ fossil.storage is a basic wrapper around localStorage
+ or sessionStorage or a dummy proxy object if neither
+ of those are available.
+ */
+ const tryStorage = function f(obj){
+ if(!f.key) f.key = 'fossil.access.check';
+ try{
+ obj.setItem(f.key, 'f');
+ const x = obj.getItem(f.key);
+ obj.removeItem(f.key);
+ if(x!=='f') throw new Error(f.key+" failed")
+ return obj;
+ }catch(e){
+ return undefined;
+ }
+ };
+
+ /** Internal storage impl for fossil.storage. */
+ const $storage =
+ tryStorage(window.localStorage)
+ || tryStorage(window.sessionStorage)
+ || tryStorage({
+ // A basic dummy xyzStorage stand-in
+ $$$:{},
+ setItem: function(k,v){this.$$$[k]=v},
+ getItem: function(k){
+ return this.$$$.hasOwnProperty(k) ? this.$$$[k] : undefined;
+ },
+ removeItem: function(k){delete this.$$$[k]},
+ clear: function(){this.$$$={}}
+ });
+
+ /**
+ For the dummy storage we need to differentiate between
+ $storage and its real property storage for hasOwnProperty()
+ to work properly...
+ */
+ const $storageHolder = $storage.hasOwnProperty('$$$') ? $storage.$$$ : $storage;
+
+ /**
+ A prefix which gets internally applied to all fossil.storage
+ property keys so that localStorage and sessionStorage across the
+ same browser profile instance do not "leak" across multiple repos
+ being hosted by the same origin server. Such polination is still
+ there but, with this key prefix applied, it won't be immediately
+ visible via the storage API.
+
+ With this in place we can justify using localStorage instead of
+ sessionStorage again.
+
+ One implication, it was discovered after the release of 2.12, of
+ using localStorage and sessionStorage, is that their scope (the
+ same "origin" and client application/profile) allows multiple
+ repos on the same origin to use the same storage. Thus a user
+ editing a wiki in /repoA/wikiedit could then see those edits in
+ /repoB/wikiedit. The data do not cross user- or browser
+ boundaries, though, so it "might" arguably be called a bug. Even
+ so, it was never intended for that to happen. Rather than lose
+ localStorage access altogether, storageKeyPrefix was added so
+ that we can sandbox that state for the various repos.
+
+ See: https://fossil-scm.org/forum/forumpost/4afc4d34de
+
+ Sidebar: it might seem odd to provide a key prefix and stick all
+ properties in the topmost level of the storage object. We do that
+ because adding a layer of object to sandbox each repo would mean
+ (de)serializing that whole tree on every storage property change
+ (and we update storage often during editing sessions).
+ e.g. instead of storageObject.projectName.foo we have
+ storageObject[storageKeyPrefix+'foo']. That's soley for
+ efficiency's sake (in terms of battery life and
+ environment-internal storage-level effort). Even so, it might (or
+ might not) be useful to do that someday.
+ */
+ const storageKeyPrefix = (
+ $storageHolder===$storage/*localStorage or sessionStorage*/
+ ? (
+ F.config.projectCode || F.config.projectName
+ || F.config.shortProjectName || window.location.pathname
+ )+'::' : (
+ '' /* transient storage */
+ )
+ );
+
+ /**
+ Proxy for custom events. Created on demand.
+ */
+ let events;
+ /**
+ A proxy for localStorage or sessionStorage or a
+ page-instance-local proxy, if neither one is availble.
+
+ Which exact storage implementation is uses is unspecified, and
+ apps must not rely on it.
+ */
+ F.storage = {
+ storageKeyPrefix: storageKeyPrefix,
+ addEventListener(...args){
+ events ??= new EventTarget()
+ return events.addEventListener(...args);
+ },
+ removeEventListener(...args){
+ events ??= new EventTarget()
+ return events.removeEventListener(...args);
+ },
+ /** Sets the storage key k to value v, implicitly converting
+ it to a string.
+
+ Fires a 'set' CustomEvent with a detail value in the form
+ {key, value} with the new value.
+ */
+ set: (k,v)=>{
+ $storage.setItem(storageKeyPrefix+k,v);
+ if( events ){
+ events.dispatchEvent(
+ new CustomEvent('set',{
+ detail: F.nu({
+ key: k, value: v
+ })
+ })
+ );
+ }
+ },
+ /** Sets storage key k to JSON.stringify(v). */
+ setJSON: function(k,v){
+ return this.set(k,JSON.stringify(v));
+ },
+ /** Returns the value for the given storage key, or
+ dflt if the key is not found in the storage. */
+ get: (k,dflt)=>$storageHolder.hasOwnProperty(
+ storageKeyPrefix+k
+ ) ? $storage.getItem(storageKeyPrefix+k) : dflt,
+ /** Returns true if the given key has a value of "true". If the
+ key is not found, it returns true if the boolean value of dflt
+ is "true". (Remember that JS persistent storage values are all
+ strings.) */
+ getBool: function(k,dflt){
+ return 'true'===this.get(k,''+(!!dflt));
+ },
+ /** Returns the JSON.parse()'d value of the given
+ storage key's value, or dflt is the key is not
+ found or JSON.parse() fails. */
+ getJSON: function f(k,dflt){
+ try {
+ const x = this.get(k,f);
+ return x===f ? dflt : JSON.parse(x);
+ }
+ catch(e){return dflt}
+ },
+ /** Returns true if the storage contains the given key,
+ else false. */
+ contains: (k)=>$storageHolder.hasOwnProperty(storageKeyPrefix+k),
+ /**
+ Removes the given key from the storage. Returns this.
+
+ Fires a 'remove' CustomEvent with a detail value in the form
+ {key}.
+ */
+ remove: function(k){
+ const kk = storageKeyPrefix+k;
+ if( events ){
+ const had = $storageHolder.hasOwnProperty(kk)
+ $storage.removeItem(kk);
+ if( had ){
+ events.dispatchEvent(
+ new CustomEvent('remove',{
+ detail: F.nu({key: k})
+ })
+ );
+ }
+ }else{
+ $storage.removeItem(kk);
+ }
+ return this;
+ },
+ /** Clears ALL keys from the storage. Returns this. */
+ clear: function(){
+ this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
+ return this;
+ },
+ /** Returns an array of all keys currently in the storage. If full
+ is true then the keys include the storage key prefix, else
+ they don't. It should default to false but does not for
+ historical compatibility. */
+ keys: function(full=true){
+ const li = Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix));
+ if( full ) return li;
+ const n = this.storageKeyPrefix.length;
+ return li.map(v=>v.substring(n));
+ },
+ /** Returns true if this storage is transient (only available
+ until the page is reloaded), indicating that fileStorage
+ and sessionStorage are unavailable. */
+ isTransient: ()=>$storageHolder!==$storage,
+ /** Returns a symbolic name for the current storage mechanism. */
+ storageImplName: function(){
+ if($storage===window.localStorage) return 'localStorage';
+ else if($storage===window.sessionStorage) return 'sessionStorage';
+ else return 'transient';
+ },
+
+ /**
+ Returns a brief help text string for the currently-selected
+ storage type.
+ */
+ storageHelpDescription: function(){
+ return {
+ localStorage: "Browser-local persistent storage with an "+
+ "unspecified long-term lifetime (survives closing the browser, "+
+ "but maybe not a browser upgrade).",
+ sessionStorage: "Storage local to this browser tab, "+
+ "lost if this tab is closed.",
+ "transient": "Transient storage local to this invocation of this page."
+ }[this.storageImplName()];
+ }
+ };
+
+})(window.fossil);