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