Last active
July 16, 2025 22:43
-
-
Save 16-9/ec26d6cdab02503f2b915564e0e89113 to your computer and use it in GitHub Desktop.
BTK Storage Helpers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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