This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/429255/955951/TM%20dat.js
// ==UserScript==
// @name TM dat
// @namespace https://icelava.root
// @version 0.7.0
// @description Nested, type secure and auto saving data proxy on Tampermonkey.
// @author ForkKILLET
// @match http://localhost:1633/*
// @noframes
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant unsafeWindow
// @grant GM_listValues
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// ==/UserScript==
"use strict"
Object.clone = o => JSON.parse(JSON.stringify(o))
const err = (t, m) => { throw window[t](`[TM dat] ${m}`) }
/* eslint-disable */
const type_dat = v =>
v?.__scm__?.ty ? v.__scm__.ty :
v === null ? "null" :
v instanceof Array ? "array" :
v instanceof RegExp ? "regexp" :
typeof v
/* eslint-enable */
type_dat.convert = {
string_number: v => + v,
string_regexp: v => v,
number_string: v => "" + v,
number_boolean: v => !! v
}
let raw_dat
const proto_scm = {
object: { rec: 1, ctn: () => ({}) },
tuple: { rec: 1, ctn: () => [] },
array: { rec: 2, ctn: () => [], api: (A, P, s = P.__scm__, tar = P.__tar__) => ({
$new(i, n = 1) {
for (const j = i + n; i < j; i ++) {
const scm = A.scm.lvs[j]
if (scm) err("ReferenceError", `Leaf @ ${scm.path} already exists, but was attempted to re-new.`)
init_scm(A, j, tar, true)
}
},
get $length() {
return s.lvs.length
},
$push(...a) {
a.forEach(v => P[ s.lvs.length ] = v)
return s.lvs.length
},
$fill(v, i = 0, j = s.lvs.length) {
for (; i < j; i ++) P[i] = v
return P
},
$pop() {
const l = s.lvs.length
const v = P[ l - 1 ]
delete P[ l - 1 ]
s.lvs.length --
return v
},
$splice(i, n) {
const l = s.lvs.length
n ??= l
n = Math.min(l - i, n)
for(; i < l; i ++)
P[i] = i + n < l ? P[ i + n ] : undefined
s.lvs.length -= n
},
$swap(i, j) {
P.__tmp__ = P[i]
P[i] = P[j]
P[j] = P.__tmp__
delete P.__tmp__
},
$reverse() {
const l = s.lvs.length
const m = ~~ (l / 2)
for (let i = 0; i < m; i ++) if (i in s.lvs) {
P.$swap(i, l - i - 1)
}
return P
},
$includes(v) {
const l = s.lvs.length
for (let i = 0; i < l; i ++) if (i in s.lvs)
if (v === P[i]) return true
return false
},
$indexOf(v) {
const l = s.lvs.length
for (let i = 0; i < l; i ++) if (i in s.lvs)
if (v === P[i]) return + i
return -1
},
$lastIndexOf(v) {
for (let i = s.lvs.length - 1; i >= 0; i --) if (i in s.lvs)
if (v === P[i]) return + i
return -1
},
$find(f) {
const l = s.lvs.length
for (let i = 0; i < l; i ++) if (i in s.lvs) {
const v = P[i]
if (f(v)) return v
}
},
$findIndex(f) {
const l = s.lvs.length
for (let i = 0; i < l; i ++) if (i in s.lvs) {
const v = P[i]
if (f(v)) return + i
}
},
$forEach(f) {
const l = s.lvs.length
for (let i = 0; i < l; i ++) if (i in s.lvs)
f(P[i], + i, P)
},
$at(i) {
return i < 0 ? P[ s.lvs.length + i ] : P[i]
},
*[Symbol.iterator] () {
for (const k in s.lvs) yield P[k]
}
}) },
}
const init_scm = (A, k, tar, isNew) => {
const { dat, map, scm, oldRoot, old } = A
if (isNew) scm.lvs[k] = Object.clone(scm.itm)
const s = scm.lvs[k]
s.path = (scm.path ?? "") + "." + k
const proto = proto_scm[s.ty]
s.rec = proto?.rec ?? 0
if (s.rec) {
dat[k] = proto.ctn()
if (s.rec > 1) s.lvs = proto.ctn()
}
if (s.ty === "tuple") s.lvs = s.lvs.flatMap(
i => Array.from({ length: i.repeat ?? 1 }, () => Object.clone(i))
)
map(s)
s.pathRoot = s.root ? "#" + s.path : scm.pathRoot ?? k
s.raw = (s.root ? null : scm.raw) ?? (() => dat[k])
const Ak = {
dat: dat[k],
map,
scm: s,
oldRoot,
old: old?.[k]
}
if (s.rec) tar[k] = proxy_dat(Ak)
else tar[k] = dat[k] = (s.root ? oldRoot[s.pathRoot] : old?.[k]) ?? s.dft ?? null
if (proto?.api) s.api = proto.api(Ak, tar[k])
}
const proxy_dat = A => {
const { dat, scm, oldRoot, old } = A
const tar = {}
switch (scm.rec) {
case 1:
for (let k in scm.lvs) init_scm(A, k, tar)
break
case 2:
const keys = scm.itmRoot
? Object.keys(oldRoot).map(k => k.match(String.raw`^#${scm.path}\.([^.]+)`)?.[1]).filter(k => k)
: Object.keys(old ?? {})
keys.forEach(k => init_scm(A, k, tar, true))
break
}
const eP = `Parent ${scm.ty} @ ${scm.path}`
const cAR = k => {
if (typeof k === "symbol") return
if (scm.ty === "array") {
const eR = eP + ` requires the index to be in [ ${scm.minIdx ??= 0}, ${scm.maxIdx ??= +Infinity} ], but got ${k}. `
if (k < scm.minIdx || k > scm.maxIdx) err("RangeError", eR)
}
}
const P = new Proxy(tar, {
get: (_, k) => {
if (k === "__scm__") return scm
if (k === "__tar__") return tar
if (scm.api && k in scm.api) return scm.api[k]
cAR(k)
return tar[k]
},
set: (_, k, v) => {
cAR(k)
if (! scm.lvs[k]) switch (scm.rec) {
case 1:
err("TypeError", eP + ` doesn't have leaf ${k}.`)
break
case 2:
init_scm(A, k, tar, true)
break
}
if (scm.api && k in scm.api) {
err("TypeError", eP + ` has API ${k}. Failed.`)
}
const s = scm.lvs[k]
const eF = `Leaf @ ${s.path}`, eS = "Failed strictly.", eT = eF + ` is ${ [ "simple", "fixed complex", "flexible complex" ][s.rec] } type, `
if (s.locked) err("TypeError", eF + ` is locked, but was attempted to modify.`)
if (s.ty === "enum" && ! s.vals.includes(v)) {
err("TypeError", eF + ` requires to be in the enum { ${ s.vals.join(", ") } }, but got ${v}.`)
}
const ty = type_dat(v)
if (ty === "undefined") {
if (scm.rec === 1 && ! scm.del) err("TypeError", eT + `but its ` + eF + " was attempted to delete.")
if (s.rec) {
s.del = true
for (let j in s.lvs) delete tar[k][j]
}
delete scm.lvs[k]
}
else if (
ty === "array" && s.lvs instanceof Array ||
ty === "object" && s.lvs && ! (s.lvs instanceof Array)
) {
for (let j in s.lvs) tar[k][j] = v[j]
return true
}
else if (! [ "any", "enum" ].includes(s.ty) && ty !== s.ty) {
const eM = eF + ` requires type ${s.ty}, but got ${ty}: ${v}. `
if (s.strict) err("TypeError", eM + eS)
const f = type_dat.convert[`${ty}_${s.ty}`]
if (f) v = f(v)
else err("TypeError", eM + "Failed to convert.")
}
if (s.ty === "number") {
const eR = eF + ` requires to be in [ ${s.min ??= -Infinity}, ${s.max ??= +Infinity} ], but got ${v}. `
if (v < s.min || v > s.max) err("RangeError", eR)
if (s.int) {
if (s.strict) err("RangeError", eF + ` requires to be an integer. ` + eS)
v = ~~ v
}
}
tar[k] = dat[k] = v
if (s.quick || s.root) {
const vRoot = s.raw()
if (vRoot === undefined) GM_deleteValue(s.pathRoot)
else GM_setValue(s.pathRoot, JSON.stringify(vRoot))
}
return true
},
deleteProperty: (_, k) => {
P[k] = undefined
return true
},
has: (_, k) => k in scm.lvs
})
return P
}
const load_dat = (lvs, { autoSave, old, map }) => {
if (raw_dat) err("Error", `Dat cannot be loaded multiple times.`)
raw_dat = {}
old ??= GM_listValues().reduce((o, k) => (
o[k] = JSON.parse(GM_getValue(k) ?? "null"), o
), {})
if (autoSave) window.addEventListener("beforeunload", () => save_dat())
return proxy_dat({
dat: raw_dat,
scm: { lvs, rec: 1 },
map: map ?? (s => s),
old, oldRoot: old
})
}
const save_dat = (dat = raw_dat) => {
Object.keys(dat).forEach(k => GM_setValue(k, JSON.stringify(dat[k])))
}
const clear_dat = () => {
raw_dat = null
GM_listValues().forEach(GM_deleteValue)
}
// Debug
if (location.host === "localhost:1633") Object.assign(unsafeWindow, {
TM_dat: { type_dat, proxy_dat, load_dat, save_dat, clear_dat, raw_dat: () => raw_dat }
})