Skip to content

Instantly share code, notes, and snippets.

@16-9
Last active July 16, 2025 22:43
Show Gist options
  • Save 16-9/ec26d6cdab02503f2b915564e0e89113 to your computer and use it in GitHub Desktop.
Save 16-9/ec26d6cdab02503f2b915564e0e89113 to your computer and use it in GitHub Desktop.
BTK Storage Helpers
// ==UserScript==
// @name BTK Storage
// @namespace http://btk/storage
// @version 0.9977
// @description BTK unified storage interface for localStorage and GM storage
// ==/UserScript==
/*
Requires: btk-core.js
Registers: BTK.STORAGE
*/
/* global BTK */
var MODULE = 'STORAGE', VERSION = '0.9977', DEBUG = false;
if (typeof BTK === "undefined" || typeof BTK.define === "undefined") {
console.warn('[BTK] Module',MODULE,'skipped: CORE not yet loaded.');
return;
}
BTK.define(MODULE, () => {
const version = VERSION;
const debug = DEBUG;
let _scope = 'site'; // default storage scope
let DB;
let _gm_get = () => { throw new Error("GM_getValue not bound"); };
let _gm_set = () => { throw new Error("GM_setValue not bound"); };
let _gm_del = () => { throw new Error("GM_deleteValue not bound"); };
const _storageHandlers = {
site: {
set: function (key, value) {localStorage.setItem(key, JSON.stringify(value))},
remove: function (key) {localStorage.removeItem(key)},
get: function (key) {return JSON.parse(localStorage.getItem(key))}
},
script: {
set: (key, val) => _gm_set(key, val),
get: (key) => _gm_get(key),
remove: (key) => _gm_del(key)
}
};
const bindGMStorage = function (handlers = {}) {
if (typeof handlers.set === 'function') _gm_set = handlers.set;
if (typeof handlers.get === 'function') _gm_get = handlers.get;
if (typeof handlers.remove === 'function') _gm_del = handlers.remove;
if (debug) console.info('[BTK.STORAGE] Bound GM storage handlers:', Object.keys(handlers).join(', '));
};
// Public API
const set = (key, value, scope = _scope) => {
const func = 'set';
if (debug) console.info(`[BTK.STORAGE] ${scope} ${func}: ${key}`);
_storageHandlers[scope][func](key, value);
};
const get = (key, scope = _scope) => {
const func = 'get';
if (debug) console.info(`[BTK.STORAGE] ${scope} ${func}: ${key}`);
return _storageHandlers[scope][func](key);
};
const remove = (key, scope = _scope) => {
const func = 'remove';
if (debug) console.info(`[BTK.STORAGE] ${scope} ${func}: ${key}`);
_storageHandlers[scope][func](key);
};
const scope = (newScope) => {
if (typeof newScope === 'undefined') {
return _scope;
}
if (newScope === 'site' || newScope === 'script') {
_scope = newScope;
if (debug) console.info(`[BTK.STORAGE] default scope set to: ${_scope}`);
} else {
console.warn(`[BTK.STORAGE] invalid scope ignored: ${newScope}`);
}
};
// DB functions
const createDB = function(name, options={}, scope=_scope) {
scope = (scope === 'site' || scope === 'script') ? scope : _scope;
let existingDB = get(name,scope);
if (existingDB === undefined) {
set(name, Object.assign({
name: name, scope: scope, autocommit: true,
tables: {default: {key: ['id'], rows: {}}}},
options),
scope);
existingDB = get(name,scope);
}
DB = existingDB;
_scope = existingDB.scope;
return DB;
}
const getDB = function (dbOrName=DB, scope=_scope) {
if (typeof dbOrName === 'object' && dbOrName !== null && 'tables' in dbOrName) {
return dbOrName; // Already a DB object
}
if (typeof dbOrName === 'string') {
const db = get(dbOrName, scope);
if (db) return db;
throw new Error(`Database "${dbOrName}" not found in ${scope} scope`);
}
throw new Error(`Invalid database argument: ${dbOrName}`);
};
const sameDB = function (dbOrName1, dbOrName2 = DB, scope = _scope) {
let db1, db2;
try { db1 = getDB(dbOrName1, scope); }
catch { db1 = { name: 'none1', scope: 'no scope' }; }
try { db2 = getDB(dbOrName2, scope); }
catch { db2 = { name: 'none2', scope: 'no scope' }; }
return db1.name === db2.name && db1.scope === db2.scope;
};
const loadDB = function (dbOrName=DB, scope=_scope) {
const existingDB = getDB(dbOrName, scope);
DB = existingDB;
_scope=existingDB.scope;
return DB;
}
const dropDB = function(dbOrName=DB, scope=_scope) {
const existingDB = getDB(dbOrName, scope);
if (sameDB(DB, existingDB)) {
DB = undefined;
_scope='site'
}
scope = existingDB.scope;
const name = existingDB.name;
remove(name,scope);
console.info(`Dropped DB: ${name} in ${scope} scope`);
}
const commit = function (dbOrName=DB, scope=_scope) {
const existingDB = getDB(dbOrName, scope);
set(existingDB.name, existingDB, existingDB.scope);
}
const commitIfNeeded = function (dbOrName=DB,scope=_scope) {
const existingDB = getDB(dbOrName, scope);
if (existingDB.autocommit) {
commit(existingDB)
}
}
const createTable = function (tableName,key,dbOrName=DB,scope=_scope) {
const currentDB = getDB(dbOrName, scope);
try {
const existingTable = getTable(tableName, currentDB);
throw new Error(`Table ${tableName} already defined in ${currentDB.name} DB with ${currentDB.scope} scope`);
} catch (e) {
if (!e.message.includes(`Table "${tableName}" not found`)) throw e;
// Table doesn't exist—safe to create.
const composite = Array.isArray(key)
? key
: (typeof key === 'string' && key.includes('|'))
? key.split('|')
: [key];
currentDB.tables[tableName]={key: composite, rows: {}};
commitIfNeeded(currentDB);
}
}
const getTable = function (tableName='default', dbOrName=DB, scope=_scope) {
const currentDB = getDB(dbOrName, scope);
const table = currentDB.tables[tableName];
if (!table) {
throw new Error(`Table "${tableName}" not found in ${currentDB.name} DB with ${currentDB.scope} scope`);
}
return table;
};
const dropTable = function (tableName = 'default',dbOrName=DB,scope=_scope) {
const currentDB = getDB(dbOrName, scope);
const table = getTable(tableName, currentDB);
if (tableName === 'default') {
console.info(`Reinitialized default table in ${currentDB.name} DB with ${currentDB.scope} scope`);
currentDB.tables.default.rows = {}
} else {
delete currentDB.tables[tableName];
console.info(`Dropped table: ${tableName} in ${currentDB.name} DB with ${currentDB.scope} scope`);
}
commitIfNeeded(currentDB);
}
const calculateKey = function (keylist, record) {
if (typeof record === 'string') {
const parts = record.split('|');
if (parts.length !== keylist.length) {
throw new Error(`Invalid key: expected ${keylist.length} parts, got ${parts.length}`);
}
return record; // Already a valid key string.
}
const missingKeys = keylist.filter(k => !(k in record)).join(', ');
if (missingKeys) {
throw new Error(`Missing key(s): ${missingKeys}`);
}
return keylist.map(k => record[k]).join('|');
}
const insert = function (record,tableName='default',dbOrName=DB,scope=_scope) {
delete record.created; delete record.modified;
const currentDB = getDB(dbOrName, scope);
const table = getTable(tableName, currentDB);
const recordKey = calculateKey(table.key,record)
if (recordKey in table.rows) {
throw new Error(`Duplicate key on insert: ${recordKey} in ${tableName} table of ${currentDB.name} DB with ${currentDB.scope} scope`);
}
const m = Date.now();
const c = m;
table.rows[recordKey] = Object.assign(record, {created: c, modified: m});
commitIfNeeded(currentDB);
}
const update = function (record, tableName = 'default',dbOrName=DB,scope=_scope) {
delete record.created; delete record.modified;
const currentDB = getDB(dbOrName, scope);
const table = getTable(tableName, currentDB);
const recordKey = calculateKey(table.key, record);
const existing = table.rows[recordKey];
if (! existing) {
throw new Error(`Missing record on update: ${recordKey} in ${tableName} table`);
}
const m = Date.now();
const c = existing?.created || m;
table.rows[recordKey] = Object.assign(existing, record, {created: c, modified: m});
commitIfNeeded(currentDB);
};
const upsert = function (record, tableName = 'default',dbOrName=DB,scope=_scope) {
delete record.created; delete record.modified;
const currentDB = getDB(dbOrName, scope);
const table = getTable(tableName, currentDB);
const recordKey = calculateKey(table.key, record);
const existing = table.rows[recordKey] || {};
const m = Date.now();
const c = existing?.created || m;
table.rows[recordKey] = Object.assign(existing, record, {created: c, modified: m});
commitIfNeeded(currentDB);
};
const replace = function (record, tableName = 'default',dbOrName=DB,scope=_scope) {
delete record.created; delete record.modified;
const currentDB = getDB(dbOrName, scope);
const table = getTable(tableName, currentDB);
const recordKey = calculateKey(table.key, record);
const existing = table.rows[recordKey];
const m = Date.now();
const c = existing?.created || m;
table.rows[recordKey] = Object.assign(record, {created: c, modified: m});
commitIfNeeded(currentDB);
};
const destroy = function (record, tableName = 'default',dbOrName=DB,scope=_scope) {
const currentDB = getDB(dbOrName, scope);
const table = getTable(tableName, currentDB);
const recordKey = calculateKey(table.key, record);
delete table.rows[recordKey]; // Idempotent: deleting non-existing key does nothing.
commitIfNeeded(currentDB);
};
const select = function (record, tableName = 'default',dbOrName=DB,scope=_scope) {
const currentDB = getDB(dbOrName, scope);
const table = getTable(tableName, currentDB);
const subtable = { key: table.key, rows: {} };
const missingKeys = table.key.filter(k => !(k in record));
if (missingKeys.length === 0) {
const recordKey = calculateKey(table.key, record);
const row = table.rows[recordKey];
if (row !== undefined) {
subtable.rows[recordKey] = row;
}
} else {
for (const [key, row] of Object.entries(table.rows)) {
if (Object.entries(record).every(([k, v]) => row[k] === v)) {
subtable.rows[key] = row;
}
}
}
return subtable;
};
return {
version,
debug,
set,
get,
remove,
bindGMStorage,
scope,
createDB, getDB, sameDB, loadDB, dropDB, commit, commitIfNeeded, createTable, getTable, dropTable, calculateKey, insert, update, replace, upsert, destroy, select
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment