159 lines
4.5 KiB
JavaScript
159 lines
4.5 KiB
JavaScript
|
'use strict'
|
||
|
|
||
|
const ModuleError = require('module-error')
|
||
|
const encodings = require('./lib/encodings')
|
||
|
const { Encoding } = require('./lib/encoding')
|
||
|
const { BufferFormat, ViewFormat, UTF8Format } = require('./lib/formats')
|
||
|
|
||
|
const kFormats = Symbol('formats')
|
||
|
const kEncodings = Symbol('encodings')
|
||
|
const validFormats = new Set(['buffer', 'view', 'utf8'])
|
||
|
|
||
|
/** @template T */
|
||
|
class Transcoder {
|
||
|
/**
|
||
|
* @param {Array<'buffer'|'view'|'utf8'>} formats
|
||
|
*/
|
||
|
constructor (formats) {
|
||
|
if (!Array.isArray(formats)) {
|
||
|
throw new TypeError("The first argument 'formats' must be an array")
|
||
|
} else if (!formats.every(f => validFormats.has(f))) {
|
||
|
// Note: we only only support aliases in key- and valueEncoding options (where we already did)
|
||
|
throw new TypeError("Format must be one of 'buffer', 'view', 'utf8'")
|
||
|
}
|
||
|
|
||
|
/** @type {Map<string|MixedEncoding<any, any, any>, Encoding<any, any, any>>} */
|
||
|
this[kEncodings] = new Map()
|
||
|
this[kFormats] = new Set(formats)
|
||
|
|
||
|
// Register encodings (done early in order to populate encodings())
|
||
|
for (const k in encodings) {
|
||
|
try {
|
||
|
this.encoding(k)
|
||
|
} catch (err) {
|
||
|
/* istanbul ignore if: assertion */
|
||
|
if (err.code !== 'LEVEL_ENCODING_NOT_SUPPORTED') throw err
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @returns {Array<Encoding<any,T,any>>}
|
||
|
*/
|
||
|
encodings () {
|
||
|
return Array.from(new Set(this[kEncodings].values()))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string|MixedEncoding<any, any, any>} encoding
|
||
|
* @returns {Encoding<any, T, any>}
|
||
|
*/
|
||
|
encoding (encoding) {
|
||
|
let resolved = this[kEncodings].get(encoding)
|
||
|
|
||
|
if (resolved === undefined) {
|
||
|
if (typeof encoding === 'string' && encoding !== '') {
|
||
|
resolved = lookup[encoding]
|
||
|
|
||
|
if (!resolved) {
|
||
|
throw new ModuleError(`Encoding '${encoding}' is not found`, {
|
||
|
code: 'LEVEL_ENCODING_NOT_FOUND'
|
||
|
})
|
||
|
}
|
||
|
} else if (typeof encoding !== 'object' || encoding === null) {
|
||
|
throw new TypeError("First argument 'encoding' must be a string or object")
|
||
|
} else {
|
||
|
resolved = from(encoding)
|
||
|
}
|
||
|
|
||
|
const { name, format } = resolved
|
||
|
|
||
|
if (!this[kFormats].has(format)) {
|
||
|
if (this[kFormats].has('view')) {
|
||
|
resolved = resolved.createViewTranscoder()
|
||
|
} else if (this[kFormats].has('buffer')) {
|
||
|
resolved = resolved.createBufferTranscoder()
|
||
|
} else if (this[kFormats].has('utf8')) {
|
||
|
resolved = resolved.createUTF8Transcoder()
|
||
|
} else {
|
||
|
throw new ModuleError(`Encoding '${name}' cannot be transcoded`, {
|
||
|
code: 'LEVEL_ENCODING_NOT_SUPPORTED'
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (const k of [encoding, name, resolved.name, resolved.commonName]) {
|
||
|
this[kEncodings].set(k, resolved)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return resolved
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exports.Transcoder = Transcoder
|
||
|
|
||
|
/**
|
||
|
* @param {MixedEncoding<any, any, any>} options
|
||
|
* @returns {Encoding<any, any, any>}
|
||
|
*/
|
||
|
function from (options) {
|
||
|
if (options instanceof Encoding) {
|
||
|
return options
|
||
|
}
|
||
|
|
||
|
// Loosely typed for ecosystem compatibility
|
||
|
const maybeType = 'type' in options && typeof options.type === 'string' ? options.type : undefined
|
||
|
const name = options.name || maybeType || `anonymous-${anonymousCount++}`
|
||
|
|
||
|
switch (detectFormat(options)) {
|
||
|
case 'view': return new ViewFormat({ ...options, name })
|
||
|
case 'utf8': return new UTF8Format({ ...options, name })
|
||
|
case 'buffer': return new BufferFormat({ ...options, name })
|
||
|
default: {
|
||
|
throw new TypeError("Format must be one of 'buffer', 'view', 'utf8'")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* If format is not provided, fallback to detecting `level-codec`
|
||
|
* or `multiformats` encodings, else assume a format of buffer.
|
||
|
* @param {MixedEncoding<any, any, any>} options
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
function detectFormat (options) {
|
||
|
if ('format' in options && options.format !== undefined) {
|
||
|
return options.format
|
||
|
} else if ('buffer' in options && typeof options.buffer === 'boolean') {
|
||
|
return options.buffer ? 'buffer' : 'utf8' // level-codec
|
||
|
} else if ('code' in options && Number.isInteger(options.code)) {
|
||
|
return 'view' // multiformats
|
||
|
} else {
|
||
|
return 'buffer'
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @typedef {import('./lib/encoding').MixedEncoding<TIn,TFormat,TOut>} MixedEncoding
|
||
|
* @template TIn, TFormat, TOut
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @type {Object.<string, Encoding<any, any, any>>}
|
||
|
*/
|
||
|
const aliases = {
|
||
|
binary: encodings.buffer,
|
||
|
'utf-8': encodings.utf8
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @type {Object.<string, Encoding<any, any, any>>}
|
||
|
*/
|
||
|
const lookup = {
|
||
|
...encodings,
|
||
|
...aliases
|
||
|
}
|
||
|
|
||
|
let anonymousCount = 0
|