mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-10 12:46:15 -08:00
589 lines
13 KiB
Plaintext
589 lines
13 KiB
Plaintext
// Const
|
|
const TRANSFORMED_TYPE_KEY = '@t'
|
|
const CIRCULAR_REF_KEY = '@r'
|
|
const KEY_REQUIRE_ESCAPING_RE = /^#*@(t|r)$/
|
|
|
|
const GLOBAL = (function getGlobal() {
|
|
// NOTE: see http://www.ecma-international.org/ecma-262/6.0/index.html#sec-performeval step 10
|
|
const savedEval = eval
|
|
|
|
return savedEval('this')
|
|
})()
|
|
|
|
const ARRAY_BUFFER_SUPPORTED = typeof ArrayBuffer === 'function'
|
|
const MAP_SUPPORTED = typeof Map === 'function'
|
|
const SET_SUPPORTED = typeof Set === 'function'
|
|
|
|
const TYPED_ARRAY_CTORS = [
|
|
'Int8Array',
|
|
'Uint8Array',
|
|
'Uint8ClampedArray',
|
|
'Int16Array',
|
|
'Uint16Array',
|
|
'Int32Array',
|
|
'Uint32Array',
|
|
'Float32Array',
|
|
'Float64Array',
|
|
]
|
|
|
|
// Saved proto functions
|
|
const arrSlice = Array.prototype.slice
|
|
|
|
// Default serializer
|
|
const JSONSerializer = {
|
|
serialize(val: any) {
|
|
return JSON.stringify(val)
|
|
},
|
|
|
|
deserialize(val: any) {
|
|
return JSON.parse(val)
|
|
},
|
|
}
|
|
|
|
// EncodingTransformer
|
|
class EncodingTransformer {
|
|
references: any
|
|
transforms: any
|
|
circularCandidates: any
|
|
circularCandidatesDescrs: any
|
|
circularRefCount: any
|
|
|
|
constructor(val: any, transforms: any) {
|
|
this.references = val
|
|
this.transforms = transforms
|
|
this.circularCandidates = []
|
|
this.circularCandidatesDescrs = []
|
|
this.circularRefCount = 0
|
|
}
|
|
|
|
static _createRefMark(idx: any) {
|
|
const obj = Object.create(null)
|
|
|
|
obj[CIRCULAR_REF_KEY] = idx
|
|
|
|
return obj
|
|
}
|
|
|
|
_createCircularCandidate(val: any, parent: any, key: any) {
|
|
this.circularCandidates.push(val)
|
|
this.circularCandidatesDescrs.push({ parent, key, refIdx: -1 })
|
|
}
|
|
|
|
_applyTransform(val: any, parent: any, key: any, transform: any) {
|
|
const result = Object.create(null)
|
|
const serializableVal = transform.toSerializable(val)
|
|
|
|
if (typeof serializableVal === 'object')
|
|
this._createCircularCandidate(val, parent, key)
|
|
|
|
result[TRANSFORMED_TYPE_KEY] = transform.type
|
|
result.data = this._handleValue(() => serializableVal, parent, key)
|
|
|
|
return result
|
|
}
|
|
|
|
_handleArray(arr: any): any {
|
|
const result = [] as any
|
|
|
|
for (let i = 0; i < arr.length; i++)
|
|
result[i] = this._handleValue(() => arr[i], result, i)
|
|
|
|
return result
|
|
}
|
|
|
|
_handlePlainObject(obj: any) {
|
|
const result = Object.create(null)
|
|
|
|
for (const key in obj) {
|
|
if (Reflect.has(obj, key)) {
|
|
const resultKey = KEY_REQUIRE_ESCAPING_RE.test(key) ? `#${key}` : key
|
|
|
|
result[resultKey] = this._handleValue(() => obj[key], result, resultKey)
|
|
}
|
|
}
|
|
|
|
const name = obj?.__proto__?.constructor?.name
|
|
if (name && name !== 'Object') {
|
|
result.constructor = { name }
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
_handleObject(obj: any, parent: any, key: any) {
|
|
this._createCircularCandidate(obj, parent, key)
|
|
|
|
return Array.isArray(obj)
|
|
? this._handleArray(obj)
|
|
: this._handlePlainObject(obj)
|
|
}
|
|
|
|
_ensureCircularReference(obj: any) {
|
|
const circularCandidateIdx = this.circularCandidates.indexOf(obj)
|
|
|
|
if (circularCandidateIdx > -1) {
|
|
const descr = this.circularCandidatesDescrs[circularCandidateIdx]
|
|
|
|
if (descr.refIdx === -1)
|
|
descr.refIdx = descr.parent ? ++this.circularRefCount : 0
|
|
|
|
return EncodingTransformer._createRefMark(descr.refIdx)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
_handleValue(getVal: () => any, parent: any, key: any) {
|
|
try {
|
|
const val = getVal()
|
|
const type = typeof val
|
|
const isObject = type === 'object' && val !== null
|
|
|
|
if (isObject) {
|
|
const refMark = this._ensureCircularReference(val)
|
|
|
|
if (refMark) return refMark
|
|
}
|
|
|
|
for (const transform of this.transforms) {
|
|
if (transform.shouldTransform(type, val))
|
|
return this._applyTransform(val, parent, key, transform)
|
|
}
|
|
|
|
if (isObject) return this._handleObject(val, parent, key)
|
|
|
|
return val
|
|
} catch (e) {
|
|
try {
|
|
return this._handleValue(
|
|
() => (e instanceof Error ? e : new Error(e)),
|
|
parent,
|
|
key
|
|
)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
}
|
|
|
|
transform() {
|
|
const references = [this._handleValue(() => this.references, null, null)]
|
|
|
|
for (const descr of this.circularCandidatesDescrs) {
|
|
if (descr.refIdx > 0) {
|
|
references[descr.refIdx] = descr.parent[descr.key]
|
|
descr.parent[descr.key] = EncodingTransformer._createRefMark(
|
|
descr.refIdx
|
|
)
|
|
}
|
|
}
|
|
|
|
return references
|
|
}
|
|
}
|
|
|
|
// DecodingTransform
|
|
class DecodingTransformer {
|
|
references: any
|
|
transformMap: any
|
|
activeTransformsStack = [] as any
|
|
visitedRefs = Object.create(null)
|
|
|
|
constructor(references: any, transformsMap: any) {
|
|
this.references = references
|
|
this.transformMap = transformsMap
|
|
}
|
|
|
|
_handlePlainObject(obj: any) {
|
|
const unescaped = Object.create(null)
|
|
|
|
if ('constructor' in obj) {
|
|
if (!obj.constructor || typeof obj.constructor.name !== 'string') {
|
|
obj.constructor = {
|
|
name: 'Object',
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
this._handleValue(obj[key], obj, key)
|
|
|
|
if (KEY_REQUIRE_ESCAPING_RE.test(key)) {
|
|
// NOTE: use intermediate object to avoid unescaped and escaped keys interference
|
|
// E.g. unescaped "##@t" will be "#@t" which can overwrite escaped "#@t".
|
|
unescaped[key.substring(1)] = obj[key]
|
|
delete obj[key]
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const unsecapedKey in unescaped)
|
|
obj[unsecapedKey] = unescaped[unsecapedKey]
|
|
}
|
|
|
|
_handleTransformedObject(obj: any, parent: any, key: any) {
|
|
const transformType = obj[TRANSFORMED_TYPE_KEY]
|
|
const transform = this.transformMap[transformType]
|
|
|
|
if (!transform)
|
|
throw new Error(`Can't find transform for "${transformType}" type.`)
|
|
|
|
this.activeTransformsStack.push(obj)
|
|
this._handleValue(obj.data, obj, 'data')
|
|
this.activeTransformsStack.pop()
|
|
|
|
parent[key] = transform.fromSerializable(obj.data)
|
|
}
|
|
|
|
_handleCircularSelfRefDuringTransform(refIdx: any, parent: any, key: any) {
|
|
// NOTE: we've hit a hard case: object reference itself during transformation.
|
|
// We can't dereference it since we don't have resulting object yet. And we'll
|
|
// not be able to restore reference lately because we will need to traverse
|
|
// transformed object again and reference might be unreachable or new object contain
|
|
// new circular references. As a workaround we create getter, so once transformation
|
|
// complete, dereferenced property will point to correct transformed object.
|
|
const references = this.references
|
|
|
|
Object.defineProperty(parent, key, {
|
|
// @ts-ignore
|
|
val: void 0,
|
|
configurable: true,
|
|
enumerable: true,
|
|
|
|
get() {
|
|
if (this.val === void 0) this.val = references[refIdx]
|
|
|
|
return (<any>this).val
|
|
},
|
|
|
|
set(value) {
|
|
this.val = value
|
|
},
|
|
})
|
|
}
|
|
|
|
_handleCircularRef(refIdx: any, parent: any, key: any) {
|
|
if (this.activeTransformsStack.includes(this.references[refIdx]))
|
|
this._handleCircularSelfRefDuringTransform(refIdx, parent, key)
|
|
else {
|
|
if (!this.visitedRefs[refIdx]) {
|
|
this.visitedRefs[refIdx] = true
|
|
this._handleValue(this.references[refIdx], this.references, refIdx)
|
|
}
|
|
|
|
parent[key] = this.references[refIdx]
|
|
}
|
|
}
|
|
|
|
_handleValue(val: any, parent: any, key: any) {
|
|
if (typeof val !== 'object' || val === null) return
|
|
|
|
const refIdx = val[CIRCULAR_REF_KEY]
|
|
|
|
if (refIdx !== void 0) this._handleCircularRef(refIdx, parent, key)
|
|
else if (val[TRANSFORMED_TYPE_KEY])
|
|
this._handleTransformedObject(val, parent, key)
|
|
else if (Array.isArray(val)) {
|
|
for (let i = 0; i < val.length; i++) this._handleValue(val[i], val, i)
|
|
} else this._handlePlainObject(val)
|
|
}
|
|
|
|
transform() {
|
|
this.visitedRefs[0] = true
|
|
this._handleValue(this.references[0], this.references, 0)
|
|
|
|
return this.references[0]
|
|
}
|
|
}
|
|
|
|
// Transforms
|
|
const builtInTransforms = [
|
|
{
|
|
type: '[[NaN]]',
|
|
|
|
shouldTransform(type: any, val: any) {
|
|
return type === 'number' && isNaN(val)
|
|
},
|
|
|
|
toSerializable() {
|
|
return ''
|
|
},
|
|
|
|
fromSerializable() {
|
|
return NaN
|
|
},
|
|
},
|
|
|
|
{
|
|
type: '[[undefined]]',
|
|
|
|
shouldTransform(type: any) {
|
|
return type === 'undefined'
|
|
},
|
|
|
|
toSerializable() {
|
|
return ''
|
|
},
|
|
|
|
fromSerializable() {
|
|
return void 0
|
|
},
|
|
},
|
|
{
|
|
type: '[[Date]]',
|
|
|
|
shouldTransform(type: any, val: any) {
|
|
return val instanceof Date
|
|
},
|
|
|
|
toSerializable(date: any) {
|
|
return date.getTime()
|
|
},
|
|
|
|
fromSerializable(val: any) {
|
|
const date = new Date()
|
|
|
|
date.setTime(val)
|
|
return date
|
|
},
|
|
},
|
|
{
|
|
type: '[[RegExp]]',
|
|
|
|
shouldTransform(type: any, val: any) {
|
|
return val instanceof RegExp
|
|
},
|
|
|
|
toSerializable(re: any) {
|
|
const result = {
|
|
src: re.source,
|
|
flags: '',
|
|
}
|
|
|
|
if (re.global) result.flags += 'g'
|
|
|
|
if (re.ignoreCase) result.flags += 'i'
|
|
|
|
if (re.multiline) result.flags += 'm'
|
|
|
|
return result
|
|
},
|
|
|
|
fromSerializable(val: any) {
|
|
return new RegExp(val.src, val.flags)
|
|
},
|
|
},
|
|
|
|
{
|
|
type: '[[Error]]',
|
|
|
|
shouldTransform(type: any, val: any) {
|
|
return val instanceof Error
|
|
},
|
|
|
|
toSerializable(err: any) {
|
|
if (!err.stack) {
|
|
;(Error as any).captureStackTrace?.(err)
|
|
}
|
|
|
|
return {
|
|
name: err.name,
|
|
message: err.message,
|
|
stack: err.stack,
|
|
}
|
|
},
|
|
|
|
fromSerializable(val: any) {
|
|
const Ctor = GLOBAL[val.name] || Error
|
|
const err = new Ctor(val.message)
|
|
|
|
err.stack = val.stack
|
|
return err
|
|
},
|
|
},
|
|
|
|
{
|
|
type: '[[ArrayBuffer]]',
|
|
|
|
shouldTransform(type: any, val: any) {
|
|
return ARRAY_BUFFER_SUPPORTED && val instanceof ArrayBuffer
|
|
},
|
|
|
|
toSerializable(buffer: any) {
|
|
const view = new Int8Array(buffer)
|
|
|
|
return arrSlice.call(view)
|
|
},
|
|
|
|
fromSerializable(val: any) {
|
|
if (ARRAY_BUFFER_SUPPORTED) {
|
|
const buffer = new ArrayBuffer(val.length)
|
|
const view = new Int8Array(buffer)
|
|
|
|
view.set(val)
|
|
|
|
return buffer
|
|
}
|
|
|
|
return val
|
|
},
|
|
},
|
|
|
|
{
|
|
type: '[[TypedArray]]',
|
|
|
|
shouldTransform(type: any, val: any) {
|
|
for (const ctorName of TYPED_ARRAY_CTORS) {
|
|
if (
|
|
typeof GLOBAL[ctorName] === 'function' &&
|
|
val instanceof GLOBAL[ctorName]
|
|
)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
},
|
|
|
|
toSerializable(arr: any) {
|
|
return {
|
|
ctorName: arr.constructor.name,
|
|
arr: arrSlice.call(arr),
|
|
}
|
|
},
|
|
|
|
fromSerializable(val: any) {
|
|
return typeof GLOBAL[val.ctorName] === 'function'
|
|
? new GLOBAL[val.ctorName](val.arr)
|
|
: val.arr
|
|
},
|
|
},
|
|
|
|
{
|
|
type: '[[Map]]',
|
|
|
|
shouldTransform(type: any, val: any) {
|
|
return MAP_SUPPORTED && val instanceof Map
|
|
},
|
|
|
|
toSerializable(map: any) {
|
|
const flattenedKVArr: any = []
|
|
|
|
map.forEach((val: any, key: any) => {
|
|
flattenedKVArr.push(key)
|
|
flattenedKVArr.push(val)
|
|
})
|
|
|
|
return flattenedKVArr
|
|
},
|
|
|
|
fromSerializable(val: any) {
|
|
if (MAP_SUPPORTED) {
|
|
// NOTE: new Map(iterable) is not supported by all browsers
|
|
const map = new Map()
|
|
|
|
for (var i = 0; i < val.length; i += 2) map.set(val[i], val[i + 1])
|
|
|
|
return map
|
|
}
|
|
|
|
const kvArr = []
|
|
|
|
// @ts-ignore
|
|
for (let j = 0; j < val.length; j += 2) kvArr.push([val[i], val[i + 1]])
|
|
|
|
return kvArr
|
|
},
|
|
},
|
|
|
|
{
|
|
type: '[[Set]]',
|
|
|
|
shouldTransform(type: any, val: any) {
|
|
return SET_SUPPORTED && val instanceof Set
|
|
},
|
|
|
|
toSerializable(set: any) {
|
|
const arr: any = []
|
|
|
|
set.forEach((val: any) => {
|
|
arr.push(val)
|
|
})
|
|
|
|
return arr
|
|
},
|
|
|
|
fromSerializable(val: any) {
|
|
if (SET_SUPPORTED) {
|
|
// NOTE: new Set(iterable) is not supported by all browsers
|
|
const set = new Set()
|
|
|
|
for (let i = 0; i < val.length; i++) set.add(val[i])
|
|
|
|
return set
|
|
}
|
|
|
|
return val
|
|
},
|
|
},
|
|
]
|
|
|
|
// Replicator
|
|
class Replicator {
|
|
transforms = [] as any
|
|
transformsMap = Object.create(null)
|
|
serializer: any
|
|
|
|
constructor(serializer?: any) {
|
|
this.serializer = serializer || JSONSerializer
|
|
|
|
this.addTransforms(builtInTransforms)
|
|
}
|
|
|
|
addTransforms(transforms: any) {
|
|
transforms = Array.isArray(transforms) ? transforms : [transforms]
|
|
|
|
for (const transform of transforms) {
|
|
if (this.transformsMap[transform.type])
|
|
throw new Error(
|
|
`Transform with type "${transform.type}" was already added.`
|
|
)
|
|
|
|
this.transforms.push(transform)
|
|
this.transformsMap[transform.type] = transform
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
removeTransforms(transforms: any) {
|
|
transforms = Array.isArray(transforms) ? transforms : [transforms]
|
|
|
|
for (const transform of transforms) {
|
|
const idx = this.transforms.indexOf(transform)
|
|
|
|
if (idx > -1) this.transforms.splice(idx, 1)
|
|
|
|
delete this.transformsMap[transform.type]
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
encode(val: any) {
|
|
const transformer = new EncodingTransformer(val, this.transforms)
|
|
const references = transformer.transform()
|
|
|
|
return this.serializer.serialize(references)
|
|
}
|
|
|
|
decode(val: any) {
|
|
const references = this.serializer.deserialize(val)
|
|
const transformer = new DecodingTransformer(references, this.transformsMap)
|
|
|
|
return transformer.transform()
|
|
}
|
|
}
|
|
|
|
export default Replicator
|