Nested, type secure and auto saving data proxy on Tampermonkey.
Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta
// @require https://update.greasyfork.org/scripts/429255/1046760/TM%20dat.js
// ==UserScript==
// @name TM dat
// @namespace https://icelava.root
// @version 0.8.3
// @description Nested, type secure and auto saving data proxy on Tampermonkey.
// @author ForkKILLET
// @include 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"
const clone = o => JSON.parse(JSON.stringify(o))
const err = (t, m) => { throw window[t](`[TM dat] ${m}`) }
const op = "GM" in Window ? {
get: GM_getValue,
set: GM_setValue,
del: GM_deleteValue,
list: GM_listValues
} : {
get: k => localStorage.getItem(k),
set: (k, v) => localStorage.setItem(k, v),
del: k => localStorage.removeItem(k),
list: () => Object.keys(localStorage)
}
/* 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, P, 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 = Math.floor(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, P, tar, isNew) => {
const { dat, map, scm, oldRoot, old } = A
if (isNew) scm.lvs[k] = 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()
}
const eS = s => JSON.stringify(s, null, 2) + ": "
if (s.ty === "enum") {
s.get ??= "val"
s.set ??= "val"
s.fromOld = v => {
let set = s.set
s.set = "id"
P[k] = v
s.set = set
}
if (s.get !== "both" && s.set === "both") err("SyntaxError", eS(s) + `{ ty: "enum" → ¬(get: "both" ∧ set: ¬ "both") }`)
}
if (s.ty === "tuple") s.lvs = s.lvs.flatMap(
i => {
let r = 1
if ("repeat" in i) {
r = i.repeat
if (typeof r !== "number" || r < 0 || r % 1)
err("SyntaxError", eS(s) + `{ ty: "tuple" → itm: [ ∀ i: { repeat?: integer } } ]`)
delete i.repeat
}
return Array.from({ length: r }, () => 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 {
let old_v = s.root ? oldRoot[s.pathRoot] : old?.[k]
if (old_v !== undefined) {
if (s.ty === "enum") s.fromOld(old_v)
else P[k] = old_v
}
else if ("dft" in s) P[k] = s.dft
}
if (proto?.api) s.api = proto.api(Ak, tar[k])
}
const proxy_dat = A => {
const { dat, scm, oldRoot, old } = A
const tar = {}
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]
const s = scm.lvs[k]
if (s.ty === "enum") switch (s.get) {
case "id":
return tar[k]
case "val":
return s.vals[tar[k]]
case "both":
return {
get id() { return tar[k] },
set id(v) {
const o_set = s.set
s.set = "id"
P[k] = v
s.set = o_set
},
get val() { return s.vals[tar[k]] },
set val(v) {
const o_set = s.set
s.set = "val"
P[k] = v
s.set = o_set
},
}
}
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, P, 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.`)
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 && v % 1) {
if (s.strict) err("RangeError", eF + ` requires to be an integer. ` + eS)
v = Math.floor(v)
}
}
else if (s.ty === "enum") switch (s.set) {
case "id":
if (typeof v !== "number" || ! v in s.vals)
err("RangeError", eF + ` requires to be an enum index in [ ${0}, ${s.vals.length} ], but got ${v}.`)
break
case "val":
v = s.vals.findIndex(val => val === v)
if (v < 0)
err("RangeError", eF + ` requires to be in the enum { ${ s.vals.join(", ") } }, but got ${v}.`)
break
case "both":
err("TypeError", eF + ` is an enum accepting both id and value ways of modification, but was attempted to modify without using any setter.`)
}
tar[k] = dat[k] = v
if (s.quick || s.root) {
const vRoot = s.raw()
if (vRoot === undefined) op.del(s.pathRoot)
else op.set(s.pathRoot, JSON.stringify(vRoot))
}
return true
},
deleteProperty: (_, k) => {
P[k] = undefined
return true
},
has: (_, k) => k in scm.lvs
})
switch (scm.rec) {
case 1:
for (let k in scm.lvs) init_scm(A, k, P, 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, P, tar, true))
break
}
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] = op.get(k), 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 => op.set(k, JSON.stringify(dat[k])))
}
const clear_dat = () => {
raw_dat = null
op.list().forEach(op.del)
}
// 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 }
})