@socketsupply/socket
Version:
A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.
420 lines (349 loc) • 9.01 kB
JavaScript
/* global XMLHttpRequest */
import ipc from '../ipc.js'
/**
* A container for a database lookup query.
*/
export class DatabaseQueryResult {
/**
* @type {string}
*/
name = ''
/**
* @type {string}
*/
mime = ''
/**
* @type {Database?}
*/
database = null
/**
* `DatabaseQueryResult` class constructor.
* @ignore
* @param {Database|null} database
* @param {string} name
* @param {string} mime
*/
constructor (database, name, mime) {
this.database = database
this.name = name
this.mime = mime
}
}
/**
* A container for MIME types by class (audio, video, text, etc)
* @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml}
*/
export class Database {
/**
* The name of the MIME database.
* @type {string}
*/
name = ''
/**
* The URL of the MIME database.
* @type {URL}
*/
url = null
/**
* The mapping of MIME name to the MIME "content type"
* @type {Map}
*/
map = new Map()
/**
* An index of MIME "content type" to the MIME name.
* @type {Map}
*/
index = new Map()
/**
* `Database` class constructor.
* @param {string} name
*/
constructor (name) {
// @ts-ignore
this.url = new URL(`${name}.json`, import.meta.url)
this.name = name
}
/**
* An enumeration of all database entries.
* @return {Array<Array<string>>}
*/
entries () {
return this.map.entries()
}
/**
* Loads database MIME entries into internal map.
* @return {Promise}
*/
async load () {
if (this.map.size === 0) {
const response = await fetch(this.url)
const json = await response.json()
for (const [key, value] of Object.entries(json)) {
this.map.set(key, value)
// @ts-ignore
this.index.set(value.toLowerCase(), key.toLowerCase())
}
}
}
/**
* Loads database MIME entries synchronously into internal map.
*/
loadSync () {
if (this.map.size === 0) {
const request = new XMLHttpRequest()
request.open('GET', this.url, false)
request.send(null)
let responseText = null
try {
// @ts-ignore
responseText = request.responseText // can throw `InvalidStateError` error
} catch {
responseText = request.response
}
const json = JSON.parse(responseText)
for (const [key, value] of Object.entries(json)) {
this.map.set(key, value)
// @ts-ignore
this.index.set(value.toLowerCase(), key.toLowerCase())
}
}
}
/**
* Lookup MIME type by name or content type
* @param {string} query
* @return {Promise<DatabaseQueryResult[]>}
*/
async lookup (query) {
await this.load()
return this.query(query)
}
/**
* Lookup MIME type by name or content type synchronously.
* @param {string} query
* @return {Promise<DatabaseQueryResult[]>}
*/
lookupSync (query) {
this.loadSync()
return this.query(query)
}
/**
* Queries database map and returns an array of results
* @param {string} query
* @return {DatabaseQueryResult[]}
*/
query (query) {
query = query.toLowerCase()
const queryParts = query.split('+')
const results = []
for (const [key, value] of this.map.entries()) {
const name = key.toLowerCase()
const mime = value.toLowerCase()
if (query === name) {
results.push(new DatabaseQueryResult(this, name, mime))
} else if (query === mime) { // reverse lookup
results.push(new DatabaseQueryResult(this, name, mime))
} else if (name.startsWith(query)) {
const nameParts = key.toLowerCase().split('+')
if (queryParts.length <= nameParts.length) {
let match = false
for (let i = 0; i < queryParts.length; ++i) {
if (queryParts[i] === nameParts[i]) {
match = true
} else {
match = false
break
}
}
if (match) {
results.push(new DatabaseQueryResult(this, name, mime))
}
}
}
}
return results
}
}
/**
* A database of MIME types for 'application/' content types
* @type {Database}
* @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#application}
*/
export const application = new Database('application')
/**
* A database of MIME types for 'audio/' content types
* @type {Database}
* @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#audio}
*/
export const audio = new Database('audio')
/**
* A database of MIME types for 'font/' content types
* @type {Database}
* @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#font}
*/
export const font = new Database('font')
/**
* A database of MIME types for 'image/' content types
* @type {Database}
* @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#image}
*/
export const image = new Database('image')
/**
* A database of MIME types for 'model/' content types
* @type {Database}
* @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#model}
*/
export const model = new Database('model')
/**
* A database of MIME types for 'multipart/' content types
* @type {Database}
* @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#multipart}
*/
export const multipart = new Database('multipart')
/**
* A database of MIME types for 'text/' content types
* @type {Database}
* @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#text}
*/
export const text = new Database('text')
/**
* A database of MIME types for 'video/' content types
* @type {Database}
* @see {@link https://www.iana.org/assignments/media-types/media-types.xhtml#video}
*/
export const video = new Database('video')
/**
* An array of known MIME databases. Custom databases can be added to this
* array in userspace for lookup with `mime.lookup()`
* @type {Database[]}
*/
export const databases = [
application,
audio,
font,
image,
model,
multipart,
text,
video
]
/**
* Look up a MIME type in various MIME databases.
* @param {string} query
* @return {Promise<DatabaseQueryResult[]>}
*/
export async function lookup (query) {
const results = []
// preload all databases
await Promise.all(databases.map((db) => db.load()))
for (const database of databases) {
const result = await database.lookup(query)
results.push(...result)
}
if (query && results.length === 0) {
const result = await ipc.request('mime.lookup', query)
if (result.err) {
throw result.err
}
results.push(new DatabaseQueryResult(null, '', result.data.type))
}
return results
}
/**
* Look up a MIME type in various MIME databases synchronously.
* @param {string} query
* @return {DatabaseQueryResult[]}
*/
export async function lookupSync (query) {
const results = []
for (const database of databases) {
const result = database.lookupSync(query)
results.push(...result)
}
if (query && results.length === 0) {
const result = await ipc.sendSync('mime.lookup', query)
if (result.err) {
throw result.err
}
results.push(new DatabaseQueryResult(null, '', result.data.type))
}
return results
}
export class MIMEParams extends Map {
toString () {
return Array
.from(this.entries())
.map((entry) => entry.join('='))
.join(';')
}
}
export class MIMEType {
#type = null
#params = null
#subtype = null
constructor (input) {
input = String(input)
const args = input.split(';')
const mime = args.shift()
const types = mime.split('/')
if (types.length !== 2 || !types[0] || !types[1]) {
throw new TypeError(`Invalid MIMEType input given: ${mime}`)
}
const [type, subtype] = types
this.#type = type.toLowerCase()
// @ts-ignore
this.#params = new MIMEParams(args.map((a) => a.trim().split('=').map((v) => v.trim())))
this.#subtype = subtype
}
get type () {
return this.#type
}
set type (value) {
if (!value || typeof value !== 'string') {
throw new TypeError('MIMEType type must be a string')
}
this.#type = value
}
get subtype () {
return this.#subtype
}
set subtype (value) {
if (!value || typeof value !== 'string') {
throw new TypeError('MIMEType subtype must be a string')
}
this.#subtype = value
}
get essence () {
return `${this.type}/${this.subtype}`
}
get params () {
return this.#params
}
toString () {
const params = this.params.toString()
if (params) {
return `${this.essence};${params}`
}
return this.essence
}
toJSON () {
return this.toString()
}
}
export default {
// API
Database,
databases,
lookup,
lookupSync,
MIMEParams,
MIMEType,
// databases
application,
audio,
font,
image,
model,
multipart,
text,
video
}